From 6207986b3c49991ad5cd9aa7b67f71b7c2711651 Mon Sep 17 00:00:00 2001 From: Aditya Yadav Date: Sun, 5 Apr 2026 07:43:53 +0530 Subject: [PATCH 1/4] fix: prevent keyboard auto-opening on node selection in mobile (#266) * fix: prevent keyboard auto-opening on node selection in mobile * added changeset --- .changeset/brown-olives-fix.md | 5 +++++ src/components/MonacoEditor.tsx | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .changeset/brown-olives-fix.md diff --git a/.changeset/brown-olives-fix.md b/.changeset/brown-olives-fix.md new file mode 100644 index 0000000..046c994 --- /dev/null +++ b/.changeset/brown-olives-fix.md @@ -0,0 +1,5 @@ +--- +"json-schema-studio": patch +--- + +fix: prevent keyboard auto-opening on node selection in mobile diff --git a/src/components/MonacoEditor.tsx b/src/components/MonacoEditor.tsx index 87478bf..9e15fda 100644 --- a/src/components/MonacoEditor.tsx +++ b/src/components/MonacoEditor.tsx @@ -177,7 +177,6 @@ const MonacoEditor = () => { editorRef.current.revealPositionInCenter(startPos); editorRef.current.setPosition(startPos); - editorRef.current.focus(); const decoration = { range: new (window as any).monaco.Range( From 4f289d52f3fe7cacd1d88c456f6995a2650d3bea Mon Sep 17 00:00:00 2001 From: Agnivesh Chaubey Date: Sun, 5 Apr 2026 13:30:09 +0530 Subject: [PATCH 2/4] chore: version bump (#239) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/brown-olives-fix.md | 5 ----- .changeset/humble-bananas-rush.md | 5 ----- .changeset/late-sloths-film.md | 5 ----- .changeset/proud-seals-watch.md | 5 ----- .changeset/silver-teeth-notice.md | 5 ----- .changeset/wacky-cities-share.md | 5 ----- CHANGELOG.md | 15 +++++++++++++++ package.json | 2 +- 8 files changed, 16 insertions(+), 31 deletions(-) delete mode 100644 .changeset/brown-olives-fix.md delete mode 100644 .changeset/humble-bananas-rush.md delete mode 100644 .changeset/late-sloths-film.md delete mode 100644 .changeset/proud-seals-watch.md delete mode 100644 .changeset/silver-teeth-notice.md delete mode 100644 .changeset/wacky-cities-share.md create mode 100644 CHANGELOG.md diff --git a/.changeset/brown-olives-fix.md b/.changeset/brown-olives-fix.md deleted file mode 100644 index 046c994..0000000 --- a/.changeset/brown-olives-fix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"json-schema-studio": patch ---- - -fix: prevent keyboard auto-opening on node selection in mobile diff --git a/.changeset/humble-bananas-rush.md b/.changeset/humble-bananas-rush.md deleted file mode 100644 index b8d338c..0000000 --- a/.changeset/humble-bananas-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"json-schema-studio": patch ---- - -Fix incorrect Dark Mode Tooltip Behavior diff --git a/.changeset/late-sloths-film.md b/.changeset/late-sloths-film.md deleted file mode 100644 index 2374d17..0000000 --- a/.changeset/late-sloths-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"json-schema-studio": patch ---- - -Fix incorrect header label on $ref target nodes in the graph view diff --git a/.changeset/proud-seals-watch.md b/.changeset/proud-seals-watch.md deleted file mode 100644 index f401a07..0000000 --- a/.changeset/proud-seals-watch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"json-schema-studio": patch ---- - -Update CI workflows for better version handling diff --git a/.changeset/silver-teeth-notice.md b/.changeset/silver-teeth-notice.md deleted file mode 100644 index 810416e..0000000 --- a/.changeset/silver-teeth-notice.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"json-schema-studio": patch ---- - -Fix graph zoom reset when editor panel is resized diff --git a/.changeset/wacky-cities-share.md b/.changeset/wacky-cities-share.md deleted file mode 100644 index 7f64e1c..0000000 --- a/.changeset/wacky-cities-share.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"json-schema-studio": minor ---- - -Added changeset bot diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ace27be --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# json-schema-studio + +## 0.7.0 + +### Minor Changes + +- 1f46606: Added changeset bot + +### Patch Changes + +- 6207986: fix: prevent keyboard auto-opening on node selection in mobile +- 0e8a67a: Fix incorrect Dark Mode Tooltip Behavior +- 900e962: Fix incorrect header label on $ref target nodes in the graph view +- 2253d53: Update CI workflows for better version handling +- 24e6b85: Fix graph zoom reset when editor panel is resized diff --git a/package.json b/package.json index 033f58b..cf76d9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "json-schema-studio", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "homepage": "studio.ioflux.org", "repository": "https://github.com/ioflux-org/studio-json-schema", From a173afb1836a3006744b2322181b6580c6160816 Mon Sep 17 00:00:00 2001 From: Chanrattnak Mong <82812258+rattnak@users.noreply.github.com> Date: Sun, 5 Apr 2026 07:24:44 -0400 Subject: [PATCH 3/4] feat: unified search bar with synchronized editor and Graph View navigation (#245) * feat: lift search state to context and remove search input from graph panel (#205) * feat: add unified search bar above editor panel (#205) * feat: highlight matching node block in editor on search using monaco-highlight-line (#205) * refactor: move format toggle from NavigationBar into editor toolbar Relocates the JSON/YAML format select from the top NavigationBar into the editor panel toolbar row, alongside the unified search input. * Revert "refactor: move format toggle from NavigationBar into editor toolbar" This reverts commit 167b8edf4278d36d1429e9ee29e235e88978d1c9. * fix: remove redundant no-match error notification from graph view * feat: sync editor search Enter to Graph View focus with node/property match type handling * feat: expand graph search to match node properties and values, exclude root * feat: add navigateGraphMatch and registerNavigateGraphMatch to context * feat: register navigateMatch to context and wire activateEditorMatch into nav buttons * feat: extract activateMatch and call navigateGraphMatch on Enter * fix: move graph search nav pill to bottom-center * fix: walk up JSON pointer to focus nearest graph node on editor search Enter * feat: add matchedNodeIds to context for cross-panel search sync * feat: publish matchedNodeIds from graph search to context * refactor: drive editor highlights and navigation from graph matchedNodeIds * fix: use substring matching for graph node search * refactor: add format toggle to editor toolbar row for 204 compatibility * chore: bump version to 0.5.15-beta * fix: restrict graph search to node label with substring matching * refactor: remove format toggle from editor toolbar on 205 * feat: move search bar to navbar with expandable icon and Enter navigation * fix: remove arrow key navigation, keep Enter only * fix: restore error notifier for no-match search results * style: match native search cancel button color to navigation text color * feat: show search bar inline with search icon and styled cancel button * feat: add format toggle above editor from upstream/main * chore: bump version to 0.6.1 * chore: add changeset for unified search bar feature * chore: final add changeset for unified search bar feature * test: trigger preview deploy * feat: refine search box design * fix: reset match index to 0 on new search to avoid stale navigation * fix: support Shift+Enter for prev match and fix stale nodes in focus effect * fix: remove unused compiledSchemaRef based on coderabbitai's rec * style: improve search box accessibility w/ theme-aware background & border * fix: prevent graphFocusRequest effect from re-running on stale seq * fix: skip JSON range parsing and decorations when editor is in YAML mode * fix: support Shift+Enter for reverse navigation in search input * fix: wrap app in flex column to anchor layout to viewport height * fix: replace fixed 92vh with flex-1 so editor fills remaining space * fix: hide search bar on small screens to prevent nav overflow * feat: add expandable mobile search with responsive icon state * fix: require minimum 3 characters before triggering graph search * perf: cache parsed AST by version ID to avoid redundant parseTree calls * fix: prevent Ctrl+K from stealing focus when typing in Monaco or inputs * fix: reset graph to default view when search finds no matches * chore: undo version bump for changeset to manage * refactor: remove unnecessary complexity Signed-off-by: Agnivesh Chaubey * fix: TS errors Signed-off-by: Agnivesh Chaubey --------- Signed-off-by: Agnivesh Chaubey Co-authored-by: Agnivesh Chaubey --- .changeset/unified-search-bar.md | 5 + src/App.css | 11 +- src/components/GraphView.tsx | 130 +++++++++-------------- src/components/MonacoEditor.tsx | 20 ++-- src/components/NavigationBar.tsx | 171 +++++++++++++++++++++++++++---- src/contexts/AppContext.tsx | 11 +- src/contexts/AppProvider.tsx | 24 ++++- 7 files changed, 259 insertions(+), 113 deletions(-) create mode 100644 .changeset/unified-search-bar.md diff --git a/.changeset/unified-search-bar.md b/.changeset/unified-search-bar.md new file mode 100644 index 0000000..4f0b43c --- /dev/null +++ b/.changeset/unified-search-bar.md @@ -0,0 +1,5 @@ +--- +"json-schema-studio": minor +--- + +Add unified search bar that highlights matching graph nodes and corresponding editor code diff --git a/src/App.css b/src/App.css index fecd830..5a38dbf 100644 --- a/src/App.css +++ b/src/App.css @@ -1,11 +1,14 @@ -body{ - background-color: var(--bg-color) +body { + background-color: var(--bg-color); } .visualize { background-color: var(--visualize-bg-color); - background-image: - linear-gradient(to right, var(--visualize-bg-image) 0.1px, transparent 1px), + background-image: linear-gradient( + to right, + var(--visualize-bg-image) 0.1px, + transparent 1px + ), linear-gradient(to bottom, var(--visualize-bg-image) 0.1px, transparent 1px); background-size: 30px 30px; } diff --git a/src/components/GraphView.tsx b/src/components/GraphView.tsx index 7e970a3..2cba710 100644 --- a/src/components/GraphView.tsx +++ b/src/components/GraphView.tsx @@ -49,7 +49,8 @@ const GraphView = ({ compiledSchema: CompiledSchema | null; }) => { const { setCenter, getZoom, fitView } = useReactFlow(); - const { selectedNode, setSelectedNode } = useContext(AppContext); + const { selectedNode, setSelectedNode, searchString, registerNavigateMatch } = + useContext(AppContext); const containerRef = useRef(null); const [nodes, setNodes, onNodeChange] = useNodesState([]); @@ -58,7 +59,6 @@ const GraphView = ({ const [hoveredEdgeId, setHoveredEdgeId] = useState(null); const [matchedNodes, setMatchedNodes] = useState([]); const [currentMatchIndex, setCurrentMatchIndex] = useState(0); - const [searchString, setSearchString] = useState(""); const [errorMessage, setErrorMessage] = useState(""); const [showErrorPopup, setShowErrorPopup] = useState(true); const matchCount = matchedNodes.length; @@ -78,6 +78,9 @@ const GraphView = ({ const x = foundNode.position.x + NODE_WIDTH / 2; const y = foundNode.position.y + NODE_HEIGHT / 2; + setSelectedNode({ + id: foundNode.id, + }); setCenter(x, y, { zoom: Math.max(getZoom(), 1), duration: 500 }); setNodes((nds) => @@ -90,12 +93,12 @@ const GraphView = ({ return newIndex; }); }, - [matchedNodes] + [matchedNodes, matchCount, setCenter, getZoom, setNodes] ); - const handleChange = useCallback((e: React.ChangeEvent) => { - setSearchString(e.target.value); - }, []); + useEffect(() => { + registerNavigateMatch(navigateMatch); + }, [navigateMatch, registerNavigateMatch]); const onNodeClick: NodeMouseHandler = useCallback((_event, node) => { setSelectedNode({ @@ -275,8 +278,13 @@ const GraphView = ({ const observer = new ResizeObserver(() => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { - const currentZoom = getZoom(); - fitView({ duration: 800, minZoom: currentZoom, maxZoom: currentZoom, padding: 0.05 }); + const currentZoom = getZoom(); + fitView({ + duration: 800, + minZoom: currentZoom, + maxZoom: currentZoom, + padding: 0.05, + }); }, 100); }); @@ -292,7 +300,7 @@ const GraphView = ({ const trimmed = searchString.trim(); const timeout = setTimeout(() => { - if (!trimmed) { + if (!trimmed || trimmed.length < 3) { setMatchedNodes([]); setCurrentMatchIndex(0); setErrorMessage(""); @@ -307,17 +315,22 @@ const GraphView = ({ searchWords.length === 0 ? [] : nodes.filter((node) => { - const labelWords = extractKeywords(node.data.nodeLabel); - return searchWords.every((word) => labelWords.includes(word)); + const titleKeyWords = extractKeywords(node.data.nodeLabel); + return searchWords.every((word) => titleKeyWords.includes(word)); }); setMatchedNodes(foundNodes); if (foundNodes.length > 0) { - const firstNode = foundNodes[currentMatchIndex % foundNodes.length]; + setCurrentMatchIndex(0); + const firstNode = foundNodes[0]; const x = firstNode.position.x + NODE_WIDTH / 2; const y = firstNode.position.y + NODE_HEIGHT / 2; + setSelectedNode({ + id: firstNode.id, + }); + setCenter(x, y, { zoom: Math.max(getZoom(), 1), duration: 500 }); setNodes((nds) => { let changed = false; @@ -331,33 +344,19 @@ const GraphView = ({ setErrorMessage(""); } else { + setSelectedNode(null); + fitView({ duration: 800, padding: 0.05 }); setErrorMessage(`${trimmed} is not in schema`); } }, 300); return () => clearTimeout(timeout); }, [searchString]); - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (matchCount <= 1) return; - - if (e.key === "ArrowRight" || e.key === "Enter") { - e.preventDefault(); - navigateMatch("next"); - } else if (e.key === "ArrowLeft") { - e.preventDefault(); - navigateMatch("prev"); - } - }, - [matchCount, navigateMatch] - ); - return (
- {selectedNode && ( + {selectedNode?.data && (
)} -
-
- - - {searchString && ( - - )} + {matchCount > 1 && ( +
+ + + {currentMatchIndex + 1}/{matchCount} + +
- {matchCount > 1 && ( -
- - - - {currentMatchIndex + 1}/{matchCount} - - - -
- )} -
+ )}
); }; diff --git a/src/components/MonacoEditor.tsx b/src/components/MonacoEditor.tsx index 9e15fda..9891127 100644 --- a/src/components/MonacoEditor.tsx +++ b/src/components/MonacoEditor.tsx @@ -89,8 +89,14 @@ const saveSchemaJSON = (key: string, schema: JSONSchema) => { }; const MonacoEditor = () => { - const { theme, isFullScreen, containerRef, schemaFormat, changeSchemaFormat, selectedNode } = - useContext(AppContext); + const { + theme, + isFullScreen, + containerRef, + schemaFormat, + changeSchemaFormat, + selectedNode, + } = useContext(AppContext); const editorRef = useRef(null); const editorPanelRef = useRef(null); @@ -284,9 +290,7 @@ const MonacoEditor = () => { isAnimating ? "panel-animating" : "" }`} > - {isFullScreen && ( - - )} + {isFullScreen && } {
setSearchString(e.target.value)} + onKeyDown={handleKeyDown} + /> +
+ {searchString ? ( + + ) : ( + + ⌘K + + )} +
+
+ + +
  • + +
  • +
  • - {theme === "light" && ( - - )} - - + {theme === "light" && ( + + )}
  • {
  • + + {mobileSearchOpen && ( +
    + + + { + setSearchString(e.target.value); + }} + onKeyDown={handleKeyDown} + /> + +
    + )} ); }; diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx index 9ecd0e7..410d286 100644 --- a/src/contexts/AppContext.tsx +++ b/src/contexts/AppContext.tsx @@ -4,9 +4,11 @@ export type SchemaFormat = "json" | "yaml"; export type SelectedNode = { id: string; - data: Record; + data?: Record; }; +export type NavigationDirection = "next" | "prev"; + type AppContextType = { containerRef: Ref; isFullScreen: boolean; @@ -19,8 +21,13 @@ type AppContextType = { changeSchemaFormat: (format: SchemaFormat) => void; selectedNode: SelectedNode | null; - setSelectedNode: (selectedNode: SelectedNode | null) => void; + + searchString: string; + setSearchString: (search: string) => void; + + registerNavigateMatch: (fn: (dir: NavigationDirection) => void) => void; + triggerNavigateMatch: (dir: NavigationDirection) => void; }; export const AppContext = createContext({} as AppContextType); diff --git a/src/contexts/AppProvider.tsx b/src/contexts/AppProvider.tsx index be7df06..f1969ca 100644 --- a/src/contexts/AppProvider.tsx +++ b/src/contexts/AppProvider.tsx @@ -5,7 +5,12 @@ import { useRef, type ReactNode, } from "react"; -import { AppContext, type SchemaFormat, type SelectedNode } from "./AppContext"; +import { + AppContext, + type NavigationDirection, + type SchemaFormat, + type SelectedNode, +} from "./AppContext"; export const AppProvider = ({ children }: { children: ReactNode }) => { const containerRef = useRef(null); @@ -38,6 +43,19 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { }; const [selectedNode, setSelectedNode] = useState(null); + const [searchString, setSearchString] = useState(""); + + const navigateMatchRef = useRef<((dir: NavigationDirection) => void) | null>( + null + ); + + const registerNavigateMatch = (fn: (dir: NavigationDirection) => void) => { + navigateMatchRef.current = fn; + }; + + const triggerNavigateMatch = (dir: NavigationDirection) => { + navigateMatchRef.current?.(dir); + }; const toggleFullScreen = useCallback(() => { const el = containerRef.current; @@ -81,6 +99,10 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { changeSchemaFormat, selectedNode, setSelectedNode, + searchString, + setSearchString, + registerNavigateMatch, + triggerNavigateMatch, }; return {children}; From af42fd2b67048256caebfce3b2a3b16259de1e97 Mon Sep 17 00:00:00 2001 From: Aditya Yadav Date: Sat, 18 Apr 2026 13:07:31 +0530 Subject: [PATCH 4/4] Added worklow --- .github/workflows/build-preview.yml | 4 ++ .github/workflows/deploy-preview.yml | 80 +++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-preview.yml b/.github/workflows/build-preview.yml index cb3b969..380083c 100644 --- a/.github/workflows/build-preview.yml +++ b/.github/workflows/build-preview.yml @@ -35,6 +35,9 @@ jobs: - name: Save PR number run: echo "${{ github.event.pull_request.number }}" > pr_number.txt + - name: Save PR head SHA + run: echo "${{ github.event.pull_request.head.sha }}" > pr_head_sha.txt + - name: Upload build artifact uses: actions/upload-artifact@v4 with: @@ -42,3 +45,4 @@ jobs: path: | dist pr_number.txt + pr_head_sha.txt diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index 840ef89..075c9ab 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -32,6 +32,36 @@ jobs: PR_NUMBER=$(cat artifact/pr_number.txt) echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + - name: Read PR head SHA + id: sha + run: | + PR_HEAD_SHA=$(cat artifact/pr_head_sha.txt) + echo "pr_head_sha=$PR_HEAD_SHA" >> $GITHUB_OUTPUT + + - name: Create GitHub Deployment + id: deployment + uses: actions/github-script@v7 + with: + script: | + const deployment = await github.rest.repos.createDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: '${{ steps.sha.outputs.pr_head_sha }}', + environment: 'pr-preview-${{ steps.pr.outputs.pr_number }}', + auto_merge: false, + required_contexts: [], + transient_environment: true, + description: 'PR Preview Deployment' + }); + core.setOutput('deployment_id', deployment.data.id); + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.data.id, + state: 'in_progress', + description: 'Deploying preview to Cloudflare Pages...' + }); + - name: Publish to Cloudflare Pages id: cloudflare_deploy uses: cloudflare/wrangler-action@v3 @@ -43,12 +73,41 @@ jobs: --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }} --branch=pr-${{ steps.pr.outputs.pr_number }} + - name: Set deployment status to success + if: success() + uses: actions/github-script@v7 + with: + script: | + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: ${{ steps.deployment.outputs.deployment_id }}, + state: 'success', + environment_url: '${{ steps.cloudflare_deploy.outputs.pages-deployment-alias-url }}', + description: 'Preview deployed successfully' + }); + + - name: Set deployment status to failure + if: failure() + uses: actions/github-script@v7 + with: + script: | + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: ${{ steps.deployment.outputs.deployment_id }}, + state: 'failure', + log_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}', + description: 'Preview deployment failed' + }); + - name: Comment PR with Preview Link + if: success() uses: thollander/actions-comment-pull-request@v3 with: pr-number: ${{ steps.pr.outputs.pr_number }} message: | - ### Preview Deployed! + ### ✅ Preview Deployed! | Item | Status | | :--- | :--- | @@ -57,4 +116,21 @@ jobs: | **Action** | [View Logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | _Last updated at ${{ github.event.workflow_run.updated_at }}_ - comment-tag: preview_deploy_status # This ensures it updates the same comment instead of spamming + comment-tag: preview_deploy_status + + - name: Comment PR with failure notice + if: failure() + uses: thollander/actions-comment-pull-request@v3 + with: + pr-number: ${{ steps.pr.outputs.pr_number }} + message: | + ### ⚠️ Preview Deploy Failed! + + | Item | Status | + | :--- | :--- | + | **Latest Deploy** | ❌ Failed | + | **Environment** | `Preview (PR-${{ steps.pr.outputs.pr_number }})` | + | **Action** | [View Logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | + + _Last updated at ${{ github.event.workflow_run.updated_at }}_ + comment-tag: preview_deploy_status