{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "code-navigator", "title": "Code Navigator", "description": "A breadcrumb navigator toolbar for jumping to sections in JSON/YAML code editors.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "https://ds.formance.com/r/code-themes.json", "https://ds.formance.com/r/badge.json", "https://ds.formance.com/r/select.json" ], "files": [ { "path": "registry/default/ui/code/code-navigator.tsx", "content": "'use client';\n\nimport { List } from 'lucide-react';\nimport { Fragment, useMemo, useState } from 'react';\n\nimport { Badge } from '@/registry/default/ui/badge';\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@/registry/default/ui/select';\nimport { type TMonacoEditorInstance } from '@/registry/default/ui/code/code-editor';\nimport { type TCodeLanguage } from '@/registry/default/ui/code/code-themes';\nimport {\n type TNavigationNode,\n parseNavigationTree,\n} from '@/registry/default/ui/code/parse-navigation-tree';\n\nexport type TCodeEditorLang = TCodeLanguage;\n\ntype TCodeNavigatorProps = {\n value: string;\n language: TCodeLanguage;\n editorRef: TMonacoEditorInstance | null;\n maxDepth?: number;\n};\n\nconst DEFAULT_MAX_DEPTH = 4;\n\nfunction CodeNavigator({\n value,\n language,\n editorRef,\n maxDepth = DEFAULT_MAX_DEPTH,\n}: TCodeNavigatorProps) {\n const [path, setPath] = useState([]);\n\n const outline = useMemo(\n () => parseNavigationTree(value, language),\n [value, language]\n );\n\n // Validate path against current outline (content may have changed)\n const validPath = useMemo(() => {\n const result: string[] = [];\n let nodes = outline;\n for (const key of path) {\n const found = nodes.find((n) => n.key === key);\n if (!found) break;\n result.push(key);\n nodes = found.children;\n }\n\n return result;\n }, [outline, path]);\n\n // Build breadcrumb levels: one per path segment + one for the next drilldown\n const levels: { id: string; nodes: TNavigationNode[]; selected?: string }[] =\n [];\n let currentNodes = outline;\n\n levels.push({ id: 'root', nodes: currentNodes, selected: validPath[0] });\n\n for (let i = 0; i < validPath.length && levels.length < maxDepth; i++) {\n const found = currentNodes.find((n) => n.key === validPath[i]);\n if (!found || found.children.length === 0) break;\n currentNodes = found.children;\n levels.push({\n id: validPath.slice(0, i + 1).join('/'),\n nodes: currentNodes,\n selected: validPath[i + 1],\n });\n }\n\n const jumpToLine = (line: number) => {\n if (!editorRef) return;\n editorRef.revealLineInCenter(line);\n editorRef.setPosition({ lineNumber: line, column: 1 });\n editorRef.focus();\n };\n\n const handleSelect = (level: number, key: string) => {\n const newPath = [...validPath.slice(0, level), key];\n setPath(newPath);\n\n // Walk tree to find the selected node and jump\n let nodes = outline;\n for (const k of newPath) {\n const found = nodes.find((n) => n.key === k);\n if (!found) return;\n if (k === key) {\n jumpToLine(found.line);\n\n return;\n }\n nodes = found.children;\n }\n };\n\n if (outline.length === 0) return null;\n\n return (\n
\n \n\n {levels.map((level, i) => (\n \n {i > 0 && /}\n handleSelect(i, key)}\n >\n span:first-child]:truncate\"\n >\n \n \n \n {level.nodes.map((node) => (\n \n \n {node.key}\n {node.children.length > 0 && (\n \n {node.children.length}\n \n )}\n \n \n ))}\n \n \n \n ))}\n
\n );\n}\n\nexport { CodeNavigator };\nexport type { TCodeNavigatorProps };\n", "type": "registry:ui", "target": "components/code/code-navigator.tsx" }, { "path": "registry/default/ui/code/parse-navigation-tree.ts", "content": "import { type TCodeLanguage } from '@/registry/default/ui/code/code-themes';\n\n/**\n * A node in the navigation tree.\n * Each node represents a navigable section in the code (a key that contains\n * nested structure). Leaf scalars (e.g. `name: \"foo\"`) are excluded — only\n * keys that open objects, arrays, or blocks appear in the tree.\n */\nexport type TNavigationNode = {\n /** Display name — the key name, or the value of name/id/slug for array items */\n key: string;\n /** 1-based line number in the source content */\n line: number;\n /** Child nodes (deeper nesting levels) */\n children: TNavigationNode[];\n};\n\n/**\n * Build a navigation tree from formatted JSON or YAML content.\n *\n * The tree is used by `CodeNavigator` to render a breadcrumb of selects\n * that let users jump to any section in the editor.\n *\n * How it works:\n * - Line-by-line scan using indentation to determine parent→child relationships\n * - A stack tracks the current nesting path; when a line has shallower indent\n * than the stack top, the stack pops back to the correct parent\n * - Leaf values (scalars) are skipped — they aren't useful navigation targets\n * - Array items (`- name: foo` in YAML, standalone `{` in JSON) are identified\n * by looking for `name`, `id`, or `slug` fields\n *\n * Assumes machine-formatted input:\n * - JSON from `JSON.stringify(x, null, 2)` → 2-space indent\n * - YAML from `yamlDump()` → 2-space indent\n */\nexport function parseNavigationTree(\n content: string,\n language: TCodeLanguage\n): TNavigationNode[] {\n if (language === 'json') return parseJson(content);\n if (language === 'yaml') return parseYaml(content);\n\n return [];\n}\n\n// Keys used to identify array items by their value\n// e.g. `- name: gift-card-ledger` → node named \"gift-card-ledger\"\nconst IDENTIFIER_KEYS = new Set(['name', 'id', 'slug']);\n\n// ─── YAML ──────────────────────────────────────────────────────────────────────\n//\n// Indent determines nesting:\n// `ledgers:` (indent 0) → root node\n// ` - name: foo` (indent 2, array item) → child of ledgers, named \"foo\"\n// ` schema:` (indent 4) → child of the array item\n// ` 1.0.1:` (indent 6) → child of schema\n//\n// Skipped lines:\n// - Empty / comments\n// - Leaf scalars: `key: value` where value is non-empty\n// - Multiline blocks: `key: |` or `key: >` and their indented body\n\nfunction parseYaml(content: string): TNavigationNode[] {\n const lines = content.split('\\n');\n const root: TNavigationNode[] = [];\n const stack: { indent: number; children: TNavigationNode[] }[] = [\n { indent: -1, children: root },\n ];\n let skipUntilIndent = -1;\n\n for (let i = 0; i < lines.length; i++) {\n const raw = lines[i]!;\n const trimmed = raw.trimStart();\n\n if (trimmed === '' || trimmed.startsWith('#')) continue;\n\n const indent = raw.length - trimmed.length;\n\n // Inside a multiline block (| or >) — skip until indent returns\n if (skipUntilIndent >= 0) {\n if (indent > skipUntilIndent) continue;\n skipUntilIndent = -1;\n }\n\n // Strip array item prefix: `- key: value` → `key: value`\n const isArrayItem = trimmed.startsWith('- ');\n const keyContent = isArrayItem ? trimmed.slice(2) : trimmed;\n\n const match = keyContent.match(/^([^:]+?):\\s*(.*)/);\n if (!match) continue;\n\n const key = match[1]!.trim();\n const value = match[2]!.trim();\n if (!key) continue;\n\n // Multiline scalar block — not navigable\n if (value === '|' || value === '>') {\n skipUntilIndent = indent;\n continue;\n }\n\n // Leaf scalar (has inline value) — not navigable\n const isLeaf = value !== '';\n\n // Pop stack back to the parent at a shallower indent\n while (stack.length > 1 && stack[stack.length - 1]!.indent >= indent) {\n stack.pop();\n }\n const parent = stack[stack.length - 1]!;\n\n // Array items get named by their identifier field value\n if (isArrayItem) {\n const displayKey = IDENTIFIER_KEYS.has(key) ? value || key : key;\n const node: TNavigationNode = {\n key: displayKey,\n line: i + 1,\n children: [],\n };\n parent.children.push(node);\n stack.push({ indent, children: node.children });\n continue;\n }\n\n if (isLeaf) continue;\n\n // Non-leaf key — push as a navigable node\n const node: TNavigationNode = { key, line: i + 1, children: [] };\n parent.children.push(node);\n stack.push({ indent, children: node.children });\n }\n\n return root;\n}\n\n// ─── JSON ──────────────────────────────────────────────────────────────────────\n//\n// Three line patterns matter:\n// `\"key\": {` or `\"key\": [` → non-leaf key, opens nested structure\n// `{` alone (indent > 0) → array item object, named by lookahead\n// everything else → leaf value or bracket — skipped\n//\n// The root `{` at indent 0 is ignored (it's the outer object wrapper).\n\nfunction parseJson(content: string): TNavigationNode[] {\n const lines = content.split('\\n');\n const root: TNavigationNode[] = [];\n const stack: { indent: number; children: TNavigationNode[] }[] = [\n { indent: -1, children: root },\n ];\n\n for (let i = 0; i < lines.length; i++) {\n const raw = lines[i]!;\n const trimmed = raw.trimStart();\n const indent = raw.length - trimmed.length;\n\n // Non-leaf key opening an object or array\n const nonLeafMatch = trimmed.match(/^\"([^\"]+)\":\\s*(?:\\{|\\[)/);\n if (nonLeafMatch) {\n while (stack.length > 1 && stack[stack.length - 1]!.indent >= indent) {\n stack.pop();\n }\n const parent = stack[stack.length - 1]!;\n const node: TNavigationNode = {\n key: nonLeafMatch[1]!,\n line: i + 1,\n children: [],\n };\n parent.children.push(node);\n stack.push({ indent, children: node.children });\n continue;\n }\n\n // Standalone { inside an array (not the root object at indent 0)\n // Look one line ahead for a name/id/slug field to use as display name\n if (trimmed === '{' && indent > 0) {\n while (stack.length > 1 && stack[stack.length - 1]!.indent >= indent) {\n stack.pop();\n }\n\n let displayKey = '[item]';\n const nextLine = lines[i + 1];\n if (nextLine) {\n const identMatch = nextLine\n .trimStart()\n .match(/^\"([^\"]+)\":\\s*\"([^\"]+)\"/);\n if (identMatch && IDENTIFIER_KEYS.has(identMatch[1]!)) {\n displayKey = identMatch[2]!;\n }\n }\n\n const parent = stack[stack.length - 1]!;\n const node: TNavigationNode = {\n key: displayKey,\n line: i + 1,\n children: [],\n };\n parent.children.push(node);\n stack.push({ indent, children: node.children });\n continue;\n }\n }\n\n return root;\n}\n", "type": "registry:ui", "target": "components/code/parse-navigation-tree.ts" } ], "type": "registry:ui" }