@keyfunc/keybricks-ds 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/INTEGRATION.md +18 -0
  2. package/components/_shared/src-buttons.js +1 -1
  3. package/components/_shared/src-data.js +1 -1
  4. package/components/_shared/src-display.js +1 -1
  5. package/components/_shared/src-feedback.js +1 -1
  6. package/components/_shared/src-forms.js +1 -1
  7. package/components/_shared/src-navigation.js +1 -1
  8. package/components/_shared/src-pipeline.js +1 -1
  9. package/components/_shared/src-tabs.js +1 -1
  10. package/components/_vue/index.mjs +21 -0
  11. package/components/buttons/Button.vue +0 -20
  12. package/components/buttons/ButtonGroup.vue +16 -0
  13. package/components/data/KeyValue.vue +0 -16
  14. package/components/data/KeyValueList.vue +12 -0
  15. package/components/data/ListRow.vue +1 -1
  16. package/components/display/AreaChart.vue +14 -0
  17. package/components/display/Avatar.vue +0 -17
  18. package/components/display/AvatarGroup.vue +13 -0
  19. package/components/display/Badge.vue +0 -34
  20. package/components/display/Card.vue +0 -15
  21. package/components/display/Donut.vue +17 -0
  22. package/components/display/IconTile.vue +13 -0
  23. package/components/display/MiniBars.vue +11 -0
  24. package/components/display/ProgressBar.vue +0 -54
  25. package/components/display/StatCard.vue +11 -0
  26. package/components/display/StatusBadge.vue +11 -0
  27. package/components/feedback/ActivityItem.vue +1 -32
  28. package/components/feedback/Alert.vue +0 -36
  29. package/components/feedback/DotActivity.vue +15 -0
  30. package/components/feedback/EmptyState.vue +12 -0
  31. package/components/feedback/LogViewer.vue +8 -0
  32. package/components/feedback/Modal.vue +0 -45
  33. package/components/feedback/Skeleton.vue +6 -0
  34. package/components/feedback/Spinner.vue +6 -0
  35. package/components/feedback/Toast.vue +19 -0
  36. package/components/forms/Combobox.vue +1 -1
  37. package/components/forms/DatePicker.vue +1 -1
  38. package/components/forms/Input.jsx +2 -1
  39. package/components/forms/Input.vue +2 -24
  40. package/components/forms/Textarea.vue +20 -0
  41. package/components/forms/input-base.js +3 -0
  42. package/components/navigation/Breadcrumb.vue +0 -22
  43. package/components/navigation/PageHeading.vue +9 -0
  44. package/components/navigation/PageToolbar.vue +15 -0
  45. package/components/navigation/ThemeToggle.vue +18 -0
  46. package/components/navigation/TopBar.vue +0 -32
  47. package/components/pipeline/NodeCard.vue +2 -1
  48. package/components/pipeline/PaletteItem.vue +1 -1
  49. package/components/pipeline/PalettePanel.jsx +2 -8
  50. package/components/pipeline/PalettePanel.vue +1 -8
  51. package/components/pipeline/pipeline-catalog.js +8 -0
  52. package/components/tabs/PanelTabs.vue +10 -0
  53. package/components/tabs/PillTabs.vue +10 -0
  54. package/components/tabs/Tabs.vue +0 -28
  55. package/package.json +1 -1
  56. package/readme.md +1 -0
  57. package/tokens/tokens.json +1 -1
@@ -1 +1 @@
1
- window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"navigation/Breadcrumb.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nfunction Crumb({ label, last, hovered, onSelect }) {\n const [hover, setHover] = React.useState(false);\n const on = (hover || hovered) && !last;\n return (\n <span role={last ? undefined : \"button\"} tabIndex={last ? undefined : 0}\n onClick={() => !last && onSelect && onSelect(label)}\n onKeyDown={(e) => { if (!last && (e.key === \"Enter\" || e.key === \" \")) { e.preventDefault(); onSelect && onSelect(label); } }}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: last || on ? \"var(--text-heading)\" : \"var(--text-secondary)\", fontWeight: last ? \"var(--weight-medium)\" : \"var(--weight-regular)\", cursor: last ? \"default\" : \"pointer\", borderRadius: \"var(--radius-sm)\", transition: \"var(--t-hover)\" }}>{label}</span>\n );\n}\n\nfunction Sep() {\n return <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\" }}><Icon name=\"chevron-right\" size={13} /></span>;\n}\n\n/** Overflow crumb: an ellipsis button that drops down the levels hidden by collapsing. */\nfunction Overflow({ items, onSelect }) {\n const [open, setOpen] = React.useState(false);\n const [hover, setHover] = React.useState(false);\n const root = React.useRef(null);\n React.useEffect(() => {\n if (!open) return;\n const h = (e) => { if (root.current && !root.current.contains(e.target)) setOpen(false); };\n document.addEventListener(\"mousedown\", h);\n return () => document.removeEventListener(\"mousedown\", h);\n }, [open]);\n return (\n <span ref={root} style={{ position: \"relative\", display: \"inline-flex\" }}>\n <span role=\"button\" tabIndex={0} aria-label={`Show ${items.length} hidden levels`} aria-expanded={open}\n onClick={() => setOpen(!open)}\n onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); setOpen(!open); } if (e.key === \"Escape\") setOpen(false); }}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", width: 20, height: 18, borderRadius: \"var(--radius-sm)\", cursor: \"pointer\", color: open || hover ? \"var(--text-heading)\" : \"var(--text-muted)\", background: open ? \"var(--bg-active)\" : hover ? \"var(--bg-hover)\" : \"transparent\", transition: \"var(--t-hover)\" }}>\n <Icon name=\"ellipsis\" size={14} />\n </span>\n {open ? (\n <span style={{ position: \"absolute\", top: \"calc(100% + 5px)\", left: 0, zIndex: \"var(--z-menu)\", minWidth: 168, maxWidth: 260, background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-md)\", boxShadow: \"var(--shadow-menu)\", padding: 4, display: \"flex\", flexDirection: \"column\" }}>\n {items.map((it, i) => <OverflowItem key={it} label={it} depth={i} onSelect={(l) => { setOpen(false); onSelect && onSelect(l); }} />)}\n </span>\n ) : null}\n </span>\n );\n}\n\nfunction OverflowItem({ label, depth, onSelect }) {\n const [hover, setHover] = React.useState(false);\n return (\n <span role=\"menuitem\" tabIndex={0} onClick={() => onSelect(label)}\n onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); onSelect(label); } }}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ display: \"flex\", alignItems: \"center\", gap: 6, padding: \"6px 9px\", paddingLeft: 9 + depth * 12, borderRadius: \"var(--radius-sm)\", cursor: \"pointer\", background: hover ? \"var(--bg-hover)\" : \"transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\", transition: \"var(--t-hover)\" }}>\n {depth > 0 ? <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", flexShrink: 0 }}><Icon name=\"chevron-right\" size={11} /></span> : null}\n {label}\n </span>\n );\n}\n\n/** Breadcrumb trail — 12px, chevron separators, last crumb in heading color. Clickable crumbs darken on hover and carry press + keyboard focus from the global interaction layer. Trails longer than `maxItems` collapse to first · … · last two, with the hidden levels in a dropdown on the ellipsis. */\nexport function Breadcrumb({ items = [], onSelect, maxItems = 4, hoveredIndex, style }) {\n const collapse = items.length > maxItems && maxItems >= 3;\n const tailCount = maxItems - 2;\n const head = collapse ? items.slice(0, 1) : items;\n const hidden = collapse ? items.slice(1, items.length - tailCount) : [];\n const tail = collapse ? items.slice(items.length - tailCount) : [];\n const lastLabel = items[items.length - 1];\n return (\n <nav style={{ display: \"flex\", alignItems: \"center\", gap: 4, ...style }}>\n {head.map((it, i) => (\n <React.Fragment key={it}>\n {i > 0 ? <Sep /> : null}\n <Crumb label={it} last={it === lastLabel} hovered={hoveredIndex === i} onSelect={onSelect} />\n </React.Fragment>\n ))}\n {collapse ? (\n <React.Fragment>\n <Sep />\n <Overflow items={hidden} onSelect={onSelect} />\n {tail.map((it) => (\n <React.Fragment key={it}>\n <Sep />\n <Crumb label={it} last={it === lastLabel} onSelect={onSelect} />\n </React.Fragment>\n ))}\n </React.Fragment>\n ) : null}\n </nav>\n );\n}\n\n/** Theme toggle — flips <html data-theme> and persists to localStorage(\"kb-theme\"). Pass `dark`+`onToggle` to control it externally. */\nexport function ThemeToggle({ size = 34, dark: darkProp, onToggle, style }) {\n const controlled = darkProp != null;\n const [darkState, setDark] = React.useState(() => (typeof document !== \"undefined\" && document.documentElement.dataset.theme === \"dark\"));\n const dark = controlled ? darkProp : darkState;\n React.useEffect(() => {\n if (controlled || typeof document === \"undefined\") return;\n document.documentElement.dataset.theme = dark ? \"dark\" : \"light\";\n try { localStorage.setItem(\"kb-theme\", dark ? \"dark\" : \"light\"); } catch (e) {}\n }, [dark, controlled]);\n const [hover, setHover] = React.useState(false);\n return (\n <button type=\"button\" aria-label={dark ? \"Switch to light mode\" : \"Switch to dark mode\"} onClick={() => (controlled ? onToggle && onToggle(!dark) : setDark(!dark))}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ width: size, height: size, borderRadius: \"var(--radius-md)\", border: \"none\", cursor: \"pointer\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", background: hover ? \"var(--bg-hover)\" : \"transparent\", color: \"var(--text-body)\", padding: 0, transition: \"var(--t-hover)\", ...style }}>\n <Icon name={dark ? \"sparkles\" : \"moon\"} size={18} />\n </button>\n );\n}\n\n/** Reads the persisted theme and applies it before first paint. Call once at app start. */\nexport function applyStoredTheme() {\n if (typeof document === \"undefined\") return;\n let t = \"light\";\n try { t = localStorage.getItem(\"kb-theme\") || \"light\"; } catch (e) {}\n document.documentElement.dataset.theme = t;\n}\n","navigation/Breadcrumb.vue":"<script setup>\nimport { ref, computed, watch, onUnmounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Breadcrumb trail — 12px, chevron separators, last crumb in heading color.\n// Clickable crumbs darken on hover (no underline) and carry press + keyboard focus from the global interaction layer.\n// Trails longer than maxItems collapse to first · … · last two, with the hidden levels in a dropdown on the ellipsis.\nconst props = defineProps({ items: { type: Array, default: () => [] }, maxItems: { type: Number, default: 4 }, hoveredIndex: { type: Number, default: null }, style: Object })\nconst emit = defineEmits(['select'])\nconst hover = ref(-1)\nconst ovHover = ref(false)\nconst menuHover = ref(-1)\nconst open = ref(false)\nconst root = ref(null)\nconst collapse = computed(() => props.items.length > props.maxItems && props.maxItems >= 3)\nconst tailCount = computed(() => props.maxItems - 2)\nconst head = computed(() => collapse.value ? props.items.slice(0, 1) : props.items)\nconst hidden = computed(() => collapse.value ? props.items.slice(1, props.items.length - tailCount.value) : [])\nconst tail = computed(() => collapse.value ? props.items.slice(props.items.length - tailCount.value) : [])\nconst lastLabel = computed(() => props.items[props.items.length - 1])\nconst isLast = (it) => it === lastLabel.value\nconst on = (i, it) => (hover.value === i || props.hoveredIndex === i) && !isLast(it)\nfunction pick(it) { if (!isLast(it)) emit('select', it) }\nfunction pickHidden(it) { open.value = false; emit('select', it) }\nfunction onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }\nwatch(open, (v) => { if (v) document.addEventListener('mousedown', onDoc); else document.removeEventListener('mousedown', onDoc) })\nonUnmounted(() => document.removeEventListener('mousedown', onDoc))\nconst crumbStyle = (i, it) => ({ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: isLast(it) || on(i, it) ? 'var(--text-heading)' : 'var(--text-secondary)', fontWeight: isLast(it) ? 'var(--weight-medium)' : 'var(--weight-regular)', cursor: isLast(it) ? 'default' : 'pointer', borderRadius: 'var(--radius-sm)', transition: 'var(--t-hover)' })\n</script>\n<template>\n <nav :style=\"{ display: 'flex', alignItems: 'center', gap: 4, ...style }\">\n <template v-for=\"(it, i) in head\" :key=\"it\">\n <span v-if=\"i > 0\" style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"chevron-right\" :size=\"13\" /></span>\n <span :role=\"isLast(it) ? undefined : 'button'\" :tabindex=\"isLast(it) ? undefined : 0\"\n @click=\"pick(it)\" @keydown.enter.prevent=\"pick(it)\" @keydown.space.prevent=\"pick(it)\"\n @mouseenter=\"hover = i\" @mouseleave=\"hover = -1\" :style=\"crumbStyle(i, it)\">{{ it }}</span>\n </template>\n <template v-if=\"collapse\">\n <span style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"chevron-right\" :size=\"13\" /></span>\n <span ref=\"root\" style=\"position:relative;display:inline-flex\">\n <span role=\"button\" tabindex=\"0\" :aria-label=\"`Show ${hidden.length} hidden levels`\" :aria-expanded=\"open\"\n @click=\"open = !open\" @keydown.enter.prevent=\"open = !open\" @keydown.space.prevent=\"open = !open\" @keydown.esc=\"open = false\"\n @mouseenter=\"ovHover = true\" @mouseleave=\"ovHover = false\"\n :style=\"{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '20px', height: '18px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', color: open || ovHover ? 'var(--text-heading)' : 'var(--text-muted)', background: open ? 'var(--bg-active)' : ovHover ? 'var(--bg-hover)' : 'transparent', transition: 'var(--t-hover)' }\">\n <Icon name=\"ellipsis\" :size=\"14\" />\n </span>\n <span v-if=\"open\" style=\"position:absolute;top:calc(100% + 5px);left:0;z-index:var(--z-menu);min-width:168px;max-width:260px;background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;display:flex;flex-direction:column\">\n <span v-for=\"(h, hi) in hidden\" :key=\"h\" role=\"menuitem\" tabindex=\"0\"\n @click=\"pickHidden(h)\" @keydown.enter.prevent=\"pickHidden(h)\" @keydown.space.prevent=\"pickHidden(h)\"\n @mouseenter=\"menuHover = hi\" @mouseleave=\"menuHover = -1\"\n :style=\"{ display: 'flex', alignItems: 'center', gap: '6px', padding: '6px 9px', paddingLeft: (9 + hi * 12) + 'px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: menuHover === hi ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', transition: 'var(--t-hover)' }\">\n <span v-if=\"hi > 0\" style=\"color:var(--text-muted);display:inline-flex;flex-shrink:0\"><Icon name=\"chevron-right\" :size=\"11\" /></span>{{ h }}\n </span>\n </span>\n </span>\n <template v-for=\"it in tail\" :key=\"it\">\n <span style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"chevron-right\" :size=\"13\" /></span>\n <span :role=\"isLast(it) ? undefined : 'button'\" :tabindex=\"isLast(it) ? undefined : 0\"\n @click=\"pick(it)\" @keydown.enter.prevent=\"pick(it)\" @keydown.space.prevent=\"pick(it)\"\n :style=\"crumbStyle(-1, it)\">{{ it }}</span>\n </template>\n </template>\n </nav>\n</template>\n\n<!-- ThemeToggle — flips <html data-theme> and persists to localStorage(\"kb-theme\"). Pass `dark` (v-model) to control it externally. -->\n<!--\n<script setup name=\"ThemeToggle\">\nimport { ref, computed, watch, onMounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\nconst props = defineProps({ size: { type: Number, default: 34 }, dark: { type: Boolean, default: null }, style: Object })\nconst emit = defineEmits(['update:dark'])\nconst darkState = ref(false)\nconst dark = computed(() => props.dark != null ? props.dark : darkState.value)\nconst hover = ref(false)\nonMounted(() => { darkState.value = document.documentElement.dataset.theme === 'dark' })\nwatch(dark, (v) => { if (props.dark != null) return; document.documentElement.dataset.theme = v ? 'dark' : 'light'; try { localStorage.setItem('kb-theme', v ? 'dark' : 'light') } catch (e) {} })\nfunction toggle() { if (props.dark != null) emit('update:dark', !dark.value); else darkState.value = !darkState.value }\n</script>\n<template>\n <button type=\"button\" :aria-label=\"dark ? 'Switch to light mode' : 'Switch to dark mode'\" @click=\"toggle\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ width: size, height: size, borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: hover ? 'var(--bg-hover)' : 'transparent', color: 'var(--text-body)', padding: 0, transition: 'var(--t-hover)', ...style }\">\n <Icon :name=\"dark ? 'sparkles' : 'moon'\" :size=\"18\" />\n </button>\n</template>\n-->\n\n<!-- applyStoredTheme() — reads the persisted theme and applies it before first paint. Call once at app start (main.js): -->\n<!--\nexport function applyStoredTheme() {\n let t = 'light'\n try { t = localStorage.getItem('kb-theme') || 'light' } catch (e) {}\n document.documentElement.dataset.theme = t\n}\n-->\n","navigation/DocTree.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { SearchInput } from \"../forms/SearchInput.jsx\";\n\nconst docHues = { orange: \"var(--cat-transform)\", green: \"var(--cat-source)\", purple: \"var(--cat-merge)\", blue: \"var(--info-fg)\", teal: \"var(--cat-output)\", gray: \"var(--text-muted)\" };\n\n/** Secondary documents panel — 240px: title header, search row, 30px tree items. items: [{label, kind:\"folder\"|\"doc\", hue, icon}]. */\nexport function DocTree({ title = \"Documents\", items = [], active, onSelect, style }) {\n return (\n <div style={{ width: \"var(--sidebar-w)\", background: \"var(--bg-surface)\", borderRight: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ height: \"var(--topbar-h)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"0 12px 0 16px\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n <IconButton icon=\"chevron-right\" size=\"md\" muted />\n </div>\n <div style={{ padding: \"12px 16px 8px\", borderBottom: \"1px solid var(--border-subtle)\" }}>\n <SearchInput bare placeholder=\"Search...\" style={{ gap: 8 }} />\n </div>\n <div style={{ padding: 8, display: \"flex\", flexDirection: \"column\", gap: 2, overflowY: \"auto\" }}>\n {items.map((it) => {\n const is = active === it.label;\n return (\n <div key={it.label} role=\"treeitem\" tabIndex={0} onClick={() => onSelect && onSelect(it.label)}\n style={{ height: \"var(--h-tree-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 8, padding: \"6px 8px\", boxSizing: \"border-box\", cursor: \"pointer\", background: is ? \"var(--bg-active)\" : \"transparent\" }}\n onMouseEnter={(e) => { if (!is) e.currentTarget.style.background = \"var(--bg-hover)\"; }}\n onMouseLeave={(e) => { if (!is) e.currentTarget.style.background = \"transparent\"; }}>\n <span style={{ color: it.kind === \"folder\" ? \"var(--text-muted)\" : docHues[it.hue || \"gray\"], display: \"inline-flex\" }}>\n <Icon name={it.icon || (it.kind === \"folder\" ? \"folder\" : \"file-text\")} size={16} />\n </span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{it.label}</span>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n","navigation/DocTree.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport SearchInput from '../forms/SearchInput.vue'\nconst docHues = { orange: 'var(--cat-transform)', green: 'var(--cat-source)', purple: 'var(--cat-merge)', blue: 'var(--info-fg)', teal: 'var(--cat-output)', gray: 'var(--text-muted)' }\n// Secondary documents panel — 240px: title header, search row, 30px tree items. items: [{label, kind:\"folder\"|\"doc\", hue, icon}].\ndefineProps({ title: { type: String, default: 'Documents' }, items: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['select'])\n</script>\n<template>\n <div :style=\"{ width: 'var(--sidebar-w)', background: 'var(--bg-surface)', borderRight: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"height:var(--topbar-h);border-bottom:1px solid var(--border-card);display:flex;align-items:center;justify-content:space-between;padding:0 12px 0 16px;box-sizing:border-box;flex-shrink:0\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <IconButton icon=\"chevron-right\" size=\"md\" muted />\n </div>\n <div style=\"padding:12px 16px 8px;border-bottom:1px solid var(--border-subtle)\"><SearchInput bare placeholder=\"Search...\" style=\"gap:8px\" /></div>\n <div style=\"padding:8px;display:flex;flex-direction:column;gap:2px;overflow-y:auto\">\n <div v-for=\"it in items\" :key=\"it.label\" @click=\"emit('select', it.label)\"\n :style=\"{ height: 'var(--h-tree-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: 8, padding: '6px 8px', boxSizing: 'border-box', cursor: 'pointer', background: active === it.label ? 'var(--bg-active)' : 'transparent' }\">\n <span :style=\"{ color: it.kind === 'folder' ? 'var(--text-muted)' : docHues[it.hue || 'gray'], display: 'inline-flex' }\"><Icon :name=\"it.icon || (it.kind === 'folder' ? 'folder' : 'file-text')\" :size=\"16\" /></span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ it.label }}</span>\n </div>\n </div>\n </div>\n</template>\n","navigation/Sidebar.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nfunction NavItem({ icon, label, active, indent, collapsed, badge, onClick }) {\n const [hover, setHover] = React.useState(false);\n return (\n <div role=\"button\" tabIndex={0} onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ height: indent ? \"var(--h-subnav-item)\" : \"var(--h-nav-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", justifyContent: collapsed ? \"center\" : \"flex-start\", gap: 10, padding: collapsed ? \"8px 10px\" : indent ? \"6px 10px\" : \"8px 10px\", boxSizing: \"border-box\", cursor: \"pointer\", background: active ? \"var(--bg-active)\" : hover ? \"var(--bg-hover)\" : \"transparent\", color: active ? \"var(--text-heading)\" : \"var(--text-body)\", transition: \"var(--t-hover)\" }}>\n <Icon name={icon} size={indent ? 14 : 16} />\n {!collapsed ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", whiteSpace: \"nowrap\", flex: 1 }}>{label}</span> : null}\n {!collapsed && badge ? badge : null}\n </div>\n );\n}\n\n/**\n * Keybricks app sidebar — 240px (collapsed: 64px icon rail). White, hairline right border.\n * items: [{icon,label,children:[{icon,label}]}]; active by label; footer items pinned.\n */\nexport function Sidebar({ items = [], footerItems = [{ icon: \"circle-help\", label: \"Help\" }, { icon: \"settings\", label: \"Settings\" }, { icon: \"moon\", label: \"Dark Mode\" }], active, onSelect, collapsed = false, onToggleCollapse, logo = \"assets/keybricks-logo.png\", brand = \"Keybricks\", height = \"100%\", style }) {\n const [openSections, setOpen] = React.useState(() => items.filter((i) => i.children && (i.open || (i.children.some((c) => c.label === active)))).map((i) => i.label));\n const toggleSection = (l) => setOpen(openSections.includes(l) ? openSections.filter((x) => x !== l) : [...openSections, l]);\n return (\n <aside style={{ width: collapsed ? \"var(--rail-w)\" : \"var(--sidebar-w)\", height, background: \"var(--bg-surface)\", borderRight: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, transition: \"width .15s ease\", ...style }}>\n <div style={{ height: \"var(--topbar-h)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", gap: 10, padding: \"0 16px\", boxSizing: \"border-box\", justifyContent: collapsed ? \"center\" : \"flex-start\", flexShrink: 0 }}>\n <img src={logo} alt=\"\" style={{ width: 24, height: 24, objectFit: \"contain\" }} />\n {!collapsed ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-lg)\", lineHeight: \"var(--leading-lg)\", letterSpacing: \"-.609px\", color: \"var(--text-primary)\" }}>{brand}</span> : null}\n </div>\n <nav style={{ flex: 1, overflowY: \"auto\", padding: 8, display: \"flex\", flexDirection: \"column\", gap: 0 }}>\n {onToggleCollapse ? <NavItem icon=\"panel-left\" label=\"Collapse\" collapsed={collapsed} onClick={onToggleCollapse} /> : null}\n {items.map((it) => it.children ? (\n <div key={it.label}>\n <div onClick={() => toggleSection(it.label)} style={{ height: \"var(--h-subnav-item)\", display: \"flex\", alignItems: \"center\", gap: 10, padding: collapsed ? \"6px 10px\" : \"6px 10px\", justifyContent: collapsed ? \"center\" : \"flex-start\", cursor: \"pointer\", color: \"var(--text-body)\", borderRadius: \"var(--radius-md)\", boxSizing: \"border-box\" }}>\n <Icon name={it.icon} size={16} />\n {!collapsed ? <><span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", letterSpacing: \"var(--tracking-base)\", flex: 1 }}>{it.label}</span><Icon name={openSections.includes(it.label) ? \"chevron-up\" : \"chevron-down\"} size={14} /></> : null}\n </div>\n {!collapsed && openSections.includes(it.label) ? (\n <div style={{ marginLeft: 24, display: \"flex\", flexDirection: \"column\", gap: 2, paddingTop: 2 }}>\n {it.children.map((c) => <NavItem key={c.label} icon={c.icon} label={c.label} indent active={active === c.label} onClick={() => onSelect && onSelect(c.label)} />)}\n </div>\n ) : null}\n </div>\n ) : (\n <NavItem key={it.label} icon={it.icon} label={it.label} collapsed={collapsed} active={active === it.label} onClick={() => onSelect && onSelect(it.label)} />\n ))}\n </nav>\n <div style={{ borderTop: \"1px solid var(--border-card)\", padding: 8, display: \"flex\", flexDirection: \"column\", flexShrink: 0 }}>\n {footerItems.map((f) => <NavItem key={f.label} icon={f.icon} label={f.label} collapsed={collapsed} onClick={() => onSelect && onSelect(f.label)} />)}\n </div>\n </aside>\n );\n}\n\n/** Default Keybricks nav model (Home dashboard). */\nexport const KEYBRICKS_NAV = [\n { icon: \"house\", label: \"Home\" },\n { icon: \"file-text\", label: \"Documents\" },\n { icon: \"layout-grid\", label: \"Business Intelligence\" },\n { icon: \"database\", label: \"Data Integration\" },\n { icon: \"scale\", label: \"Reconciliation\" },\n { icon: \"network\", label: \"Consolidation\" },\n {\n icon: \"folder-cog\", label: \"Management\", children: [\n { icon: \"box\", label: \"Modeling\" },\n { icon: \"workflow\", label: \"Workflow\" },\n { icon: \"shield\", label: \"Security Management\" },\n { icon: \"refresh-cw\", label: \"System Migration\" },\n { icon: \"chart-column\", label: \"Business Logic\" },\n { icon: \"list-todo\", label: \"Tasks Management\" },\n { icon: \"circle-check\", label: \"Validation Rules\" },\n { icon: \"settings-2\", label: \"Control Center\" },\n { icon: \"network\", label: \"Reconciliation\" },\n ],\n },\n];\n","navigation/Sidebar.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\n\n/**\n * Keybricks app sidebar — 240px (collapsed: 64px icon rail). White, hairline right border.\n * items: [{icon,label,children:[{icon,label}]}]; active by label; footer items pinned.\n */\nconst props = defineProps({\n items: { type: Array, default: () => [] },\n footerItems: { type: Array, default: () => [{ icon: 'circle-help', label: 'Help' }, { icon: 'settings', label: 'Settings' }, { icon: 'moon', label: 'Dark Mode' }] },\n active: String, collapsed: { type: Boolean, default: false },\n logo: { type: String, default: 'assets/keybricks-logo.png' }, brand: { type: String, default: 'Keybricks' },\n height: { type: [String, Number], default: '100%' }, style: Object, canCollapse: Boolean,\n})\nconst emit = defineEmits(['select', 'toggleCollapse'])\nconst openSections = ref(props.items.filter(i => i.children && (i.open || i.children.some(c => c.label === props.active))).map(i => i.label))\nfunction toggleSection(l) { openSections.value = openSections.value.includes(l) ? openSections.value.filter(x => x !== l) : [...openSections.value, l] }\nfunction itemStyle(active, indent) { return { height: indent ? 'var(--h-subnav-item)' : 'var(--h-nav-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: 10, padding: indent ? '6px 10px' : '8px 10px', boxSizing: 'border-box', cursor: 'pointer', background: active ? 'var(--bg-active)' : 'transparent', color: active ? 'var(--text-heading)' : 'var(--text-body)', transition: 'var(--t-hover)' } }\n</script>\n<template>\n <aside :style=\"{ width: collapsed ? 'var(--rail-w)' : 'var(--sidebar-w)', height, background: 'var(--bg-surface)', borderRight: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, transition: 'width .15s ease', ...style }\">\n <div :style=\"{ height: 'var(--topbar-h)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', gap: 10, padding: '0 16px', boxSizing: 'border-box', justifyContent: collapsed ? 'center' : 'flex-start', flexShrink: 0 }\">\n <img :src=\"logo\" alt=\"\" style=\"width:24px;height:24px;object-fit:contain\" />\n <span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-lg);line-height:var(--leading-lg);letter-spacing:-.609px;color:var(--text-primary)\">{{ brand }}</span>\n </div>\n <nav style=\"flex:1;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:0\">\n <div v-if=\"canCollapse\" role=\"button\" tabindex=\"0\" @click=\"emit('toggleCollapse')\" :style=\"itemStyle(false, false)\"><Icon name=\"panel-left\" :size=\"16\" /><span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);white-space:nowrap;flex:1\">Collapse</span></div>\n <template v-for=\"it in items\" :key=\"it.label\">\n <div v-if=\"it.children\">\n <div role=\"button\" tabindex=\"0\" @click=\"toggleSection(it.label)\" :style=\"{ height: 'var(--h-subnav-item)', display: 'flex', alignItems: 'center', gap: 10, padding: '6px 10px', justifyContent: collapsed ? 'center' : 'flex-start', cursor: 'pointer', color: 'var(--text-body)', borderRadius: 'var(--radius-md)', boxSizing: 'border-box' }\">\n <Icon :name=\"it.icon\" :size=\"16\" />\n <template v-if=\"!collapsed\"><span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);letter-spacing:var(--tracking-base);flex:1\">{{ it.label }}</span><Icon :name=\"openSections.includes(it.label) ? 'chevron-up' : 'chevron-down'\" :size=\"14\" /></template>\n </div>\n <div v-if=\"!collapsed && openSections.includes(it.label)\" style=\"margin-left:24px;display:flex;flex-direction:column;gap:2px;padding-top:2px\">\n <div v-for=\"c in it.children\" :key=\"c.label\" role=\"button\" tabindex=\"0\" @click=\"emit('select', c.label)\" :style=\"itemStyle(active === c.label, true)\"><Icon :name=\"c.icon\" :size=\"14\" /><span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);white-space:nowrap;flex:1\">{{ c.label }}</span></div>\n </div>\n </div>\n <div v-else role=\"button\" tabindex=\"0\" @click=\"emit('select', it.label)\" :style=\"itemStyle(active === it.label, false)\"><Icon :name=\"it.icon\" :size=\"16\" /><span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);white-space:nowrap;flex:1\">{{ it.label }}</span></div>\n </template>\n </nav>\n <div style=\"border-top:1px solid var(--border-card);padding:8px;display:flex;flex-direction:column;flex-shrink:0\">\n <div v-for=\"f in footerItems\" :key=\"f.label\" role=\"button\" tabindex=\"0\" @click=\"emit('select', f.label)\" :style=\"itemStyle(false, false)\"><Icon :name=\"f.icon\" :size=\"16\" /><span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);white-space:nowrap;flex:1\">{{ f.label }}</span></div>\n </div>\n </aside>\n</template>\n\n<!-- KEYBRICKS_NAV — default Keybricks nav model (Home dashboard), same shape as Sidebar.jsx's export, for reference:\nexport const KEYBRICKS_NAV = [\n { icon: 'house', label: 'Home' }, { icon: 'file-text', label: 'Documents' },\n { icon: 'layout-grid', label: 'Business Intelligence' }, { icon: 'database', label: 'Data Integration' },\n { icon: 'scale', label: 'Reconciliation' }, { icon: 'network', label: 'Consolidation' },\n { icon: 'folder-cog', label: 'Management', children: [ ... ] },\n] -->\n","navigation/TopBar.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { Button } from \"../buttons/Button.jsx\";\nimport { SearchInput } from \"../forms/SearchInput.jsx\";\n\n/** 56px top bar: bare search left, bell + dark AI Assistant button right. */\nexport function TopBar({ placeholder = \"Search documents, workflows, data...\", actions, onSearch, style }) {\n return (\n <header style={{ height: \"var(--topbar-h)\", background: \"var(--bg-surface)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"0 24px\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <SearchInput bare placeholder={placeholder} onChange={onSearch} style={{ width: 576, maxWidth: \"48%\" }} />\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n {actions || (<>\n <IconButton icon=\"bell\" size=\"lg\" />\n <Button variant=\"dark\" icon=\"sparkles\">AI Assistant</Button>\n </>)}\n </div>\n </header>\n );\n}\n\n/** Editor toolbar: back chevron, 14px/600 title, \"• Editing\" status, right action slot. */\nexport function PageToolbar({ title, status = \"• Editing\", actions, onBack, style }) {\n return (\n <div style={{ height: \"var(--topbar-h)\", background: \"var(--bg-surface)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"0 16px\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n {onBack ? <IconButton icon=\"chevron-left\" size=\"md\" onClick={onBack} /> : null}\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n {status ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-muted)\", marginLeft: 6 }}>{status}</span> : null}\n </div>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>{actions}</div>\n </div>\n );\n}\n\n/** Page heading block: 15px/500 title + 13px muted subtitle (System Migration pages). */\nexport function PageHeading({ title, subtitle, style }) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 4, ...style }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-lg)\", lineHeight: \"var(--leading-lg)\", letterSpacing: \"var(--tracking-lg)\", color: \"var(--text-primary)\" }}>{title}</span>\n {subtitle ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-secondary)\" }}>{subtitle}</span> : null}\n </div>\n );\n}\n","navigation/TopBar.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\nimport Button from '../buttons/Button.vue'\nimport SearchInput from '../forms/SearchInput.vue'\n// 56px top bar: bare search left, bell + dark AI Assistant button right.\ndefineProps({ placeholder: { type: String, default: 'Search documents, workflows, data...' }, style: Object })\nconst emit = defineEmits(['search'])\n</script>\n<template>\n <header :style=\"{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 24px', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <SearchInput bare :placeholder=\"placeholder\" @update:modelValue=\"v => emit('search', v)\" style=\"width:576px;max-width:48%\" />\n <div style=\"display:flex;align-items:center;gap:8px\">\n <slot name=\"actions\">\n <IconButton icon=\"bell\" size=\"lg\" />\n <Button variant=\"dark\" icon=\"sparkles\">AI Assistant</Button>\n </slot>\n </div>\n </header>\n</template>\n\n<!-- PageToolbar — editor toolbar: back chevron, 14px/600 title, \"• Editing\" status, right action slot. -->\n<!--\n<script setup name=\"PageToolbar\">\nimport IconButton from '../buttons/IconButton.vue'\ndefineProps({ title: String, status: { type: String, default: '• Editing' }, showBack: Boolean, style: Object })\nconst emit = defineEmits(['back'])\n</script>\n<template>\n <div :style=\"{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 16px', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"display:flex;align-items:center;gap:8px\">\n <IconButton v-if=\"showBack\" icon=\"chevron-left\" size=\"md\" @click=\"emit('back')\" />\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"status\" style=\"font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-muted);margin-left:6px\">{{ status }}</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:8px\"><slot name=\"actions\" /></div>\n </div>\n</template>\n-->\n\n<!-- PageHeading — page heading block: 15px/500 title + 13px muted subtitle. -->\n<!--\n<script setup name=\"PageHeading\">\ndefineProps({ title: String, subtitle: String, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', flexDirection: 'column', gap: 4, ...style }\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-lg);line-height:var(--leading-lg);letter-spacing:var(--tracking-lg);color:var(--text-primary)\">{{ title }}</span>\n <span v-if=\"subtitle\" style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-secondary)\">{{ subtitle }}</span>\n </div>\n</template>\n-->\n","navigation/Tree.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { Checkbox } from \"../forms/Checkbox.jsx\";\n\nconst docHues = { orange: \"var(--cat-transform)\", green: \"var(--cat-source)\", purple: \"var(--cat-merge)\", blue: \"var(--info-fg)\", teal: \"var(--cat-output)\", gray: \"var(--text-muted)\" };\nconst INDENT = 18;\n\nconst keyOf = (n) => (n.key != null ? n.key : n.label);\nconst titleOf = (n) => (n.title != null ? n.title : n.label);\nconst kidsOf = (n) => n.children || [];\nconst isBranch = (n) => kidsOf(n).length > 0 || n.isLeaf === false;\n\nfunction walk(nodes, fn, parentKey = null, depth = 0) {\n nodes.forEach((n) => { fn(n, parentKey, depth); walk(kidsOf(n), fn, keyOf(n), depth + 1); });\n}\nfunction indexNodes(items) {\n const byKey = {}, parent = {}, all = [];\n walk(items, (n, p, d) => { const k = keyOf(n); byKey[k] = n; parent[k] = p; all.push({ node: n, key: k, depth: d }); });\n return { byKey, parent, all };\n}\nfunction descendants(node, out = []) {\n kidsOf(node).forEach((c) => { out.push(c); descendants(c, out); });\n return out;\n}\n/** Is `key` the dragged node itself, or anywhere inside its subtree? */\nfunction inSubtree(items, rootKey, key) {\n const { byKey } = indexNodes(items);\n const root = byKey[rootKey];\n if (!root) return false;\n return rootKey === key || descendants(root).some((d) => keyOf(d) === key);\n}\nfunction keysMatching(items, test) {\n const hit = [];\n const visit = (n, trail) => {\n const t = [...trail, keyOf(n)];\n if (test(n)) hit.push(...trail);\n kidsOf(n).forEach((c) => visit(c, t));\n };\n items.forEach((n) => visit(n, []));\n return [...new Set(hit)];\n}\n\n/* Cascading check: a parent is checked when every enabled descendant is checked,\n indeterminate when only some are. Disabled nodes never change state. */\nfunction cascade(items, key, next) {\n const { byKey, parent } = indexNodes(items);\n const set = new Set(next);\n const node = byKey[key];\n if (!node) return next;\n const checked = set.has(key);\n descendants(node).forEach((d) => {\n if (d.disabled || d.disableCheckbox) return;\n checked ? set.add(keyOf(d)) : set.delete(keyOf(d));\n });\n let p = parent[key];\n while (p != null) {\n const pn = byKey[p];\n const kids = kidsOf(pn).filter((c) => !c.disabled && !c.disableCheckbox);\n const on = kids.filter((c) => set.has(keyOf(c)));\n on.length && on.length === kids.length ? set.add(p) : set.delete(p);\n p = parent[p];\n }\n return [...set];\n}\n/* Display state for every branch: full when all enabled descendants are checked,\n half when only some. Keeps parents honest whatever the caller passed in. */\nfunction rollUp(items, checkedKeys) {\n const full = new Set(checkedKeys), half = new Set();\n const visit = (n) => {\n kidsOf(n).forEach(visit);\n if (!kidsOf(n).length) return;\n const ds = descendants(n).filter((d) => !d.disabled && !d.disableCheckbox);\n if (!ds.length) return;\n const on = ds.filter((d) => full.has(keyOf(d))).length;\n if (on === ds.length) full.add(keyOf(n));\n else { full.delete(keyOf(n)); if (on > 0) half.add(keyOf(n)); }\n };\n items.forEach(visit);\n return { full, half };\n}\nfunction halfKeys(items, checkedKeys) {\n return [...rollUp(items, checkedKeys).half];\n}\n\nfunction Highlight({ text, query }) {\n if (!query) return text;\n const i = String(text).toLowerCase().indexOf(query.toLowerCase());\n if (i < 0) return text;\n return <>{String(text).slice(0, i)}<mark style={{ background: \"var(--soft-bg)\", color: \"var(--info-fg)\", fontWeight: \"var(--weight-medium)\", padding: 0, borderRadius: 2 }}>{String(text).slice(i, i + query.length)}</mark>{String(text).slice(i + query.length)}</>;\n}\n\n/** Guide lines for showLine: one 18px column per ancestor level, then the elbow. */\nfunction Guides({ trail, isLast, hasKids }) {\n return (\n <>\n {trail.map((ancestorHasNext, i) => (\n <span key={i} style={{ width: INDENT, alignSelf: \"stretch\", flexShrink: 0, borderLeft: ancestorHasNext ? \"1px solid var(--border-subtle)\" : \"1px solid transparent\", marginLeft: i === 0 ? 6 : 0 }} />\n ))}\n <span style={{ width: INDENT, alignSelf: \"stretch\", flexShrink: 0, position: \"relative\", marginLeft: trail.length === 0 ? 6 : 0 }}>\n <span style={{ position: \"absolute\", left: 0, top: 0, height: isLast ? \"50%\" : \"100%\", borderLeft: \"1px solid var(--border-subtle)\" }} />\n <span style={{ position: \"absolute\", left: 0, top: \"50%\", width: hasKids ? 6 : INDENT - 4, borderTop: \"1px solid var(--border-subtle)\" }} />\n </span>\n </>\n );\n}\n\nfunction Row({ item, state, cfg, api }) {\n const { node, key, depth, trail, isLast } = item;\n const kids = kidsOf(node);\n const branch = isBranch(node);\n const open = state.expanded.includes(key);\n const selected = state.selected.includes(key);\n const checked = state.checked.has(key);\n const half = !checked && state.half.has(key);\n const loading = state.loading.includes(key);\n const focused = state.focus === key;\n const ringed = focused && state.focusMoved;\n const [hover, setHover] = React.useState(false);\n const disabled = !!node.disabled;\n const dropAt = state.drop && state.drop.key === key ? state.drop.pos : null;\n const selectable = cfg.selectable && node.selectable !== false;\n\n const chevron = branch ? (\n <span onClick={(e) => { e.stopPropagation(); api.toggle(key); }} style={{ width: 14, height: 14, display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", color: \"var(--text-muted)\", flexShrink: 0, cursor: \"pointer\", transform: open ? \"rotate(90deg)\" : \"none\", transition: \"transform var(--dur-fast) var(--ease-out)\" }}>\n {loading ? <Icon name=\"loader\" size={12} style={{ animation: \"kb-tree-spin 900ms linear infinite\" }} /> : <Icon name=\"chevron-right\" size={13} />}\n </span>\n ) : <span style={{ width: cfg.showLine ? 0 : 14, flexShrink: 0 }} />;\n\n return (\n <>\n <div role=\"treeitem\" tabIndex={focused ? 0 : -1} aria-expanded={branch ? open : undefined} aria-selected={selected} aria-disabled={disabled || undefined}\n ref={(el) => { if (el && focused && state.focusMoved) el.focus({ preventScroll: true }); }}\n draggable={cfg.draggable && !disabled}\n onDragStart={(e) => { e.dataTransfer.effectAllowed = \"move\"; api.dragStart(key); }}\n onDragOver={(e) => { if (!cfg.draggable) return; e.preventDefault(); const r = e.currentTarget.getBoundingClientRect(); api.dragOver(key, (e.clientY - r.top) / r.height); }}\n onDragLeave={() => api.dragOver(null)}\n onDrop={(e) => { e.preventDefault(); api.drop(key); }}\n onDragEnd={() => api.dragEnd()}\n onClick={() => { if (disabled) return; api.focus(key); if (selectable) api.select(key); if (branch) api.toggle(key); }}\n onContextMenu={(e) => { if (cfg.onContextMenu) { e.preventDefault(); cfg.onContextMenu(key, e); } }}\n onKeyDown={(e) => api.keyDown(e, item)}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{\n minHeight: \"var(--h-tree-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 6,\n padding: cfg.showLine ? \"0 8px 0 2px\" : \"5px 8px\", paddingLeft: cfg.showLine ? 2 : 8 + depth * INDENT, boxSizing: \"border-box\",\n cursor: disabled ? \"not-allowed\" : \"pointer\", opacity: disabled ? 0.45 : 1,\n background: dropAt === \"inside\" ? \"var(--soft-bg)\" : selected ? \"var(--bg-active)\" : hover && !disabled ? \"var(--bg-hover)\" : \"transparent\",\n outline: dropAt === \"inside\" ? \"2px solid var(--action-primary)\" : ringed ? \"2px solid var(--focus-border)\" : \"none\", outlineOffset: -2,\n boxShadow: dropAt === \"before\" ? \"inset 0 2px 0 var(--action-primary)\" : dropAt === \"after\" ? \"inset 0 -2px 0 var(--action-primary)\" : \"none\",\n transition: \"var(--t-hover)\",\n }}>\n {cfg.showLine ? <Guides trail={trail} isLast={isLast} hasKids={branch} /> : null}\n {chevron}\n {cfg.checkable && node.checkable !== false ? (\n <span onClick={(e) => e.stopPropagation()} style={{ display: \"inline-flex\", flexShrink: 0 }}>\n <Checkbox size=\"sm\" checked={checked} indeterminate={half} disabled={disabled || node.disableCheckbox} onChange={() => api.check(key)} />\n </span>\n ) : null}\n {cfg.showIcon ? (\n <span style={{ color: branch ? \"var(--text-muted)\" : docHues[node.hue || \"gray\"], display: \"inline-flex\", flexShrink: 0 }}>\n <Icon name={node.icon || (branch ? (open ? \"folder-open\" : \"folder\") : \"file-text\")} size={15} />\n </span>\n ) : null}\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", fontWeight: selected ? \"var(--weight-medium)\" : \"var(--weight-regular)\", color: disabled ? \"var(--text-muted)\" : \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>\n <Highlight text={titleOf(node)} query={cfg.searchValue} />\n </span>\n {node.tag ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-2xs)\", lineHeight: 1, padding: \"2px 5px\", borderRadius: \"var(--radius-sm)\", background: \"var(--bg-inset)\", color: \"var(--text-secondary)\", flexShrink: 0 }}>{node.tag}</span> : null}\n {node.count != null ? <span style={{ marginLeft: \"auto\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", color: \"var(--text-muted)\", paddingLeft: 8 }}>{node.count}</span> : null}\n {cfg.rowActions && !disabled ? <span style={{ marginLeft: node.count != null ? 6 : \"auto\", display: \"flex\", gap: 2, opacity: hover ? 1 : 0, transition: \"var(--t-hover)\" }} onClick={(e) => e.stopPropagation()}>{cfg.rowActions(node)}</span> : null}\n </div>\n {open && loading && !kids.length ? (\n <div style={{ paddingLeft: 8 + (depth + 1) * INDENT + 14, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-muted)\", height: \"var(--h-tree-item)\", display: \"flex\", alignItems: \"center\" }}>Loading…</div>\n ) : null}\n </>\n );\n}\n\n/**\n * Nested tree view. Expand/collapse, single or multiple selection, cascading checkboxes\n * (with indeterminate parents and `checkStrictly` opt-out), disabled nodes and disabled\n * checkboxes, connector lines, async child loading, search highlighting, drag to move,\n * and full keyboard navigation. Node shape: { key, title, icon, hue, count, tag, children,\n * disabled, disableCheckbox, selectable, checkable, isLeaf }.\n */\nexport function Tree({\n items = [], treeData,\n defaultExpanded = [], expandedKeys, onExpand,\n defaultActive, defaultSelected = [], selectedKeys, onSelect,\n checkable = false, checkStrictly = false, defaultChecked = [], checkedKeys, onCheck,\n multiple = false, selectable = true, showIcon = true, showLine = false, draggable = false,\n searchValue = \"\", autoExpandOnSearch = true, loadData, onDrop, onContextMenu, rowActions, style, reorder = false,\n}) {\n const raw = treeData || items;\n /* Callers usually pass an inline array literal, so compare by value —\n re-seeding on identity alone would wipe async-loaded children. */\n const rawKey = JSON.stringify(raw);\n const [data, setData] = React.useState(raw);\n const seenKey = React.useRef(rawKey);\n React.useEffect(() => { if (seenKey.current !== rawKey) { seenKey.current = rawKey; setData(raw); } }, [rawKey]);\n\n const seedExpanded = defaultExpanded.length ? defaultExpanded : defaultActive ? [] : [];\n const [expandedS, setExpandedS] = React.useState(seedExpanded);\n const [selectedS, setSelectedS] = React.useState(defaultActive ? [defaultActive] : defaultSelected);\n const [checkedS, setCheckedS] = React.useState(defaultChecked);\n const [loading, setLoading] = React.useState([]);\n const [loaded, setLoaded] = React.useState([]);\n const [focus, setFocus] = React.useState(null);\n const [focusMoved, setFocusMoved] = React.useState(false);\n const [dragKey, setDragKey] = React.useState(null);\n const [drop, setDrop] = React.useState(null);\n\n const expanded = expandedKeys || expandedS;\n const selected = selectedKeys || selectedS;\n const checked = checkedKeys || checkedS;\n\n const searchExpanded = React.useMemo(\n () => (searchValue && autoExpandOnSearch ? keysMatching(data, (n) => String(titleOf(n)).toLowerCase().includes(searchValue.toLowerCase())) : []),\n [data, searchValue, autoExpandOnSearch]\n );\n const openKeys = React.useMemo(() => [...new Set([...expanded, ...searchExpanded])], [expanded, searchExpanded]);\n const view = React.useMemo(\n () => (checkable && !checkStrictly ? rollUp(data, checked) : { full: new Set(checked), half: new Set() }),\n [checkable, checkStrictly, data, checked]\n );\n\n /* Visible rows, with the sibling map each row needs to draw its guide lines. */\n const rows = React.useMemo(() => {\n const out = [];\n const push = (nodes, depth, trail) => {\n nodes.forEach((n, i) => {\n const isLast = i === nodes.length - 1;\n const key = keyOf(n);\n out.push({ node: n, key, depth, trail, isLast });\n if (kidsOf(n).length && openKeys.includes(key)) push(kidsOf(n), depth + 1, [...trail, !isLast]);\n });\n };\n push(data, 0, []);\n return out;\n }, [data, openKeys]);\n\n const setExpanded = (next) => { if (!expandedKeys) setExpandedS(next); onExpand && onExpand(next); };\n\n const api = {\n focus: (k) => { setFocus(k); setFocusMoved(false); },\n toggle: (k) => {\n const open = openKeys.includes(k);\n if (!open && loadData && !loaded.includes(k) && !kidsOf(indexNodes(data).byKey[k]).length) {\n setLoading((l) => [...l, k]);\n setExpanded([...expanded, k]);\n Promise.resolve(loadData(indexNodes(data).byKey[k])).then((kids) => {\n setData((d) => {\n const clone = JSON.parse(JSON.stringify(d));\n const { byKey } = indexNodes(clone);\n if (byKey[k]) byKey[k].children = kids || [];\n return clone;\n });\n setLoaded((l) => [...l, k]);\n setLoading((l) => l.filter((x) => x !== k));\n });\n return;\n }\n setExpanded(open ? expanded.filter((x) => x !== k) : [...expanded, k]);\n },\n select: (k) => {\n const next = multiple ? (selected.includes(k) ? selected.filter((x) => x !== k) : [...selected, k]) : selected.includes(k) && multiple ? [] : [k];\n if (!selectedKeys) setSelectedS(next);\n onSelect && onSelect(multiple ? next : k, next);\n },\n check: (k) => {\n const on = view.full.has(k);\n let next = on ? [...view.full].filter((x) => x !== k) : [...view.full, k];\n if (!checkStrictly) next = cascade(data, k, next);\n if (!checkedKeys) setCheckedS(next);\n onCheck && onCheck(next, { key: k, checked: !on, half: checkStrictly ? [] : halfKeys(data, next) });\n },\n dragStart: (k) => setDragKey(k),\n /* Validity lives here, not in the row: only branches can be dropped *into*, and a node\n can never land inside its own subtree (that would detach the whole branch). */\n dragOver: (k, ratio) => {\n if (k == null) return setDrop(null);\n const node = indexNodes(data).byKey[k];\n /* Only folders are drop targets: a file can't contain anything, so hovering one shows\n nothing at all. With reorder, a folder's outer 20% becomes an insert rule instead. */\n if (!node || node.disabled || !dragKey || !isBranch(node) || inSubtree(data, dragKey, k)) return setDrop(null);\n const pos = reorder ? (ratio < 0.2 ? \"before\" : ratio > 0.8 ? \"after\" : \"inside\") : \"inside\";\n setDrop({ key: k, pos });\n },\n dragEnd: () => { setDragKey(null); setDrop(null); },\n drop: (k) => {\n const info = drop && drop.key === k ? drop : null;\n setDrop(null);\n if (!dragKey || !info || inSubtree(data, dragKey, k)) return setDragKey(null);\n /* Computed outside setData: React re-invokes updaters (StrictMode, concurrent\n rebasing), which would fire the consumer's onDrop twice for one gesture. */\n const clone = JSON.parse(JSON.stringify(data));\n let moved = null;\n const prune = (nodes) => nodes.filter((n) => {\n if (keyOf(n) === dragKey) { moved = n; return false; }\n if (n.children) n.children = prune(n.children);\n return true;\n });\n const rootA = prune(clone);\n if (!moved) return setDragKey(null);\n let inserted = false;\n const insert = (nodes) => {\n const out = [];\n nodes.forEach((n) => {\n if (keyOf(n) === k && info.pos === \"before\") { out.push(moved); inserted = true; }\n if (keyOf(n) === k && info.pos === \"inside\") { n.children = [...(n.children || []), moved]; inserted = true; }\n else if (n.children) n.children = insert(n.children);\n out.push(n);\n if (keyOf(n) === k && info.pos === \"after\") { out.push(moved); inserted = true; }\n });\n return out;\n };\n const next = insert(rootA);\n if (!inserted) return setDragKey(null);\n setData(next);\n /* Dropping into a collapsed folder would otherwise look like the node vanished. */\n if (info.pos === \"inside\" && !expanded.includes(k)) setExpanded([...expanded, k]);\n onDrop && onDrop({ dragKey, dropKey: k, position: info.pos, treeData: next });\n setDragKey(null);\n },\n keyDown: (e, item) => {\n const i = rows.findIndex((r) => r.key === item.key);\n const go = (n) => { const t = rows[n]; if (!t) return; setFocus(t.key); setFocusMoved(true); };\n if (e.key === \"ArrowDown\") { e.preventDefault(); go(i + 1); }\n else if (e.key === \"ArrowUp\") { e.preventDefault(); go(i - 1); }\n else if (e.key === \"ArrowRight\") { e.preventDefault(); if (isBranch(item.node) && !openKeys.includes(item.key)) api.toggle(item.key); else go(i + 1); }\n else if (e.key === \"ArrowLeft\") { e.preventDefault(); if (isBranch(item.node) && openKeys.includes(item.key)) api.toggle(item.key); else { const p = rows.slice(0, i).reverse().find((r) => r.depth === item.depth - 1); if (p) { setFocus(p.key); setFocusMoved(true); } } }\n else if (e.key === \"Enter\") { e.preventDefault(); if (!item.node.disabled) { if (selectable && item.node.selectable !== false) api.select(item.key); if (isBranch(item.node)) api.toggle(item.key); } }\n else if (e.key === \" \") { e.preventDefault(); if (checkable && !item.node.disabled && item.node.checkable !== false && !item.node.disableCheckbox) api.check(item.key); }\n },\n };\n\n const state = { expanded: openKeys, selected, checked: view.full, half: view.half, loading, focus: focus || (rows[0] && rows[0].key), focusMoved, drop };\n const cfg = { checkable, checkStrictly, multiple, selectable, showIcon, showLine, draggable, searchValue, onContextMenu, rowActions };\n\n return (\n <div role=\"tree\" aria-multiselectable={multiple || undefined} style={{ display: \"flex\", flexDirection: \"column\", gap: showLine ? 0 : 1, ...style }}>\n <style>{\"@keyframes kb-tree-spin{to{transform:rotate(360deg)}}\"}</style>\n {rows.length === 0 ? (\n <div style={{ padding: \"10px 8px\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", color: \"var(--text-muted)\" }}>No matching items</div>\n ) : rows.map((r) => <Row key={r.key} item={r} state={state} cfg={cfg} api={api} />)}\n </div>\n );\n}\n","navigation/Tree.vue":"<script setup>\nimport { ref, computed, watch } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport Checkbox from '../forms/Checkbox.vue'\n// Nested tree view: expand/collapse, single or multiple selection, cascading checkboxes with\n// indeterminate parents (checkStrictly opts out), disabled nodes, connector lines, async\n// loadData, search highlight, drag to move, keyboard navigation.\nconst docHues = { orange: 'var(--cat-transform)', green: 'var(--cat-source)', purple: 'var(--cat-merge)', blue: 'var(--info-fg)', teal: 'var(--cat-output)', gray: 'var(--text-muted)' }\nconst INDENT = 18\nconst props = defineProps({\n items: { type: Array, default: () => [] },\n defaultExpanded: { type: Array, default: () => [] },\n defaultActive: String,\n defaultSelected: { type: Array, default: () => [] },\n defaultChecked: { type: Array, default: () => [] },\n checkable: Boolean, checkStrictly: Boolean, multiple: Boolean,\n selectable: { type: Boolean, default: true },\n showIcon: { type: Boolean, default: true },\n showLine: Boolean, draggable: Boolean,\n searchValue: { type: String, default: '' },\n autoExpandOnSearch: { type: Boolean, default: true },\n reorder: Boolean,\n loadData: Function, rowActions: Function, style: Object,\n})\nconst emit = defineEmits(['select', 'check', 'expand', 'drop'])\n\nconst data = ref(JSON.parse(JSON.stringify(props.items)))\n// Compare by value: re-seeding on identity alone would wipe async-loaded children.\nwatch(() => JSON.stringify(props.items), (next, prev) => { if (next !== prev) data.value = JSON.parse(next) })\nconst expanded = ref([...props.defaultExpanded])\nconst selected = ref(props.defaultActive ? [props.defaultActive] : [...props.defaultSelected])\nconst checked = ref([...props.defaultChecked])\nconst loading = ref([]); const loaded = ref([]); const focus = ref(null)\nconst dragKey = ref(null); const drop = ref(null)\n\nconst keyOf = n => (n.key != null ? n.key : n.label)\nconst titleOf = n => (n.title != null ? n.title : n.label)\nconst kidsOf = n => n.children || []\nconst isBranch = n => kidsOf(n).length > 0 || n.isLeaf === false\nfunction descendants(n, out = []) { for (const c of kidsOf(n)) { out.push(c); descendants(c, out) } return out }\n/** Is `key` the dragged node itself, or anywhere inside its subtree? */\nfunction inSubtree(rootKey, key) {\n const root = index(data.value).byKey[rootKey]\n if (!root) return false\n return rootKey === key || descendants(root).some(d => keyOf(d) === key)\n}\nfunction index(nodes, parent = null, acc = { byKey: {}, parent: {} }) {\n for (const n of nodes) { acc.byKey[keyOf(n)] = n; acc.parent[keyOf(n)] = parent; index(kidsOf(n), keyOf(n), acc) }\n return acc\n}\nfunction ancestorsOfMatches(nodes, test, trail = [], hit = []) {\n for (const n of nodes) {\n if (test(n)) hit.push(...trail)\n ancestorsOfMatches(kidsOf(n), test, [...trail, keyOf(n)], hit)\n }\n return [...new Set(hit)]\n}\nconst searchExpanded = computed(() => props.searchValue && props.autoExpandOnSearch\n ? ancestorsOfMatches(data.value, n => String(titleOf(n)).toLowerCase().includes(props.searchValue.toLowerCase()))\n : [])\nconst openKeys = computed(() => [...new Set([...expanded.value, ...searchExpanded.value])])\n\nconst view = computed(() => {\n const full = new Set(checked.value); const half = new Set()\n if (!props.checkable || props.checkStrictly) return { full, half }\n const visit = n => {\n kidsOf(n).forEach(visit)\n if (!kidsOf(n).length) return\n const ds = descendants(n).filter(d => !d.disabled && !d.disableCheckbox)\n if (!ds.length) return\n const on = ds.filter(d => full.has(keyOf(d))).length\n if (on === ds.length) full.add(keyOf(n))\n else { full.delete(keyOf(n)); if (on > 0) half.add(keyOf(n)) }\n }\n data.value.forEach(visit)\n return { full, half }\n})\n\nconst rows = computed(() => {\n const out = []\n const push = (nodes, depth, trail) => {\n nodes.forEach((n, i) => {\n const isLast = i === nodes.length - 1\n out.push({ n, key: keyOf(n), depth, trail, isLast })\n if (kidsOf(n).length && openKeys.value.includes(keyOf(n))) push(kidsOf(n), depth + 1, [...trail, !isLast])\n })\n }\n push(data.value, 0, [])\n return out\n})\n\nfunction toggle(k) {\n const open = openKeys.value.includes(k)\n const { byKey } = index(data.value)\n if (!open && props.loadData && !loaded.value.includes(k) && !kidsOf(byKey[k]).length) {\n loading.value = [...loading.value, k]\n expanded.value = [...expanded.value, k]\n Promise.resolve(props.loadData(byKey[k])).then(kids => {\n const clone = JSON.parse(JSON.stringify(data.value))\n const t = index(clone).byKey[k]\n if (t) t.children = kids || []\n data.value = clone\n loaded.value = [...loaded.value, k]\n loading.value = loading.value.filter(x => x !== k)\n })\n return\n }\n expanded.value = open ? expanded.value.filter(x => x !== k) : [...expanded.value, k]\n emit('expand', expanded.value)\n}\nfunction select(k) {\n selected.value = props.multiple\n ? (selected.value.includes(k) ? selected.value.filter(x => x !== k) : [...selected.value, k])\n : [k]\n emit('select', props.multiple ? selected.value : k, selected.value)\n}\nfunction check(k) {\n const set = new Set(view.value.full)\n const on = set.has(k)\n on ? set.delete(k) : set.add(k)\n if (!props.checkStrictly) {\n const { byKey, parent } = index(data.value)\n descendants(byKey[k]).forEach(d => {\n if (d.disabled || d.disableCheckbox) return\n on ? set.delete(keyOf(d)) : set.add(keyOf(d))\n })\n let p = parent[k]\n while (p != null) {\n const kids = kidsOf(byKey[p]).filter(c => !c.disabled && !c.disableCheckbox)\n const hit = kids.filter(c => set.has(keyOf(c)))\n hit.length && hit.length === kids.length ? set.add(p) : set.delete(p)\n p = parent[p]\n }\n }\n checked.value = [...set]\n emit('check', checked.value, { key: k, checked: !on, half: [...view.value.half] })\n}\nfunction rowClick(n) {\n if (n.disabled) return\n focus.value = keyOf(n)\n if (props.selectable && n.selectable !== false) select(keyOf(n))\n if (isBranch(n)) toggle(keyOf(n))\n}\nfunction onDragOver(e, k) {\n if (!props.draggable) return\n e.preventDefault()\n if (!dragKey.value) return\n const n = index(data.value).byKey[k]\n // Only folders are drop targets: hovering a file shows nothing, since it can't contain anything.\n if (!n || n.disabled || !isBranch(n) || inSubtree(dragKey.value, k)) { drop.value = null; return }\n const r = e.currentTarget.getBoundingClientRect()\n const y = (e.clientY - r.top) / r.height\n drop.value = { key: k, pos: props.reorder ? (y < 0.2 ? 'before' : y > 0.8 ? 'after' : 'inside') : 'inside' }\n}\nfunction onDrop(k) {\n const info = drop.value && drop.value.key === k ? drop.value : null\n drop.value = null\n if (!dragKey.value || !info || inSubtree(dragKey.value, k)) { dragKey.value = null; return }\n const clone = JSON.parse(JSON.stringify(data.value))\n let moved = null\n const prune = nodes => nodes.filter(n => {\n if (keyOf(n) === dragKey.value) { moved = n; return false }\n if (n.children) n.children = prune(n.children)\n return true\n })\n const pruned = prune(clone)\n if (!moved) { dragKey.value = null; return }\n const insert = nodes => {\n const out = []\n for (const n of nodes) {\n if (keyOf(n) === k && info.pos === 'before') out.push(moved)\n if (keyOf(n) === k && info.pos === 'inside') n.children = [...(n.children || []), moved]\n else if (n.children) n.children = insert(n.children)\n out.push(n)\n if (keyOf(n) === k && info.pos === 'after') out.push(moved)\n }\n return out\n }\n data.value = insert(pruned)\n if (info.pos === 'inside' && !expanded.value.includes(k)) expanded.value = [...expanded.value, k]\n emit('drop', { dragKey: dragKey.value, dropKey: k, position: info.pos, treeData: data.value })\n dragKey.value = null\n}\nfunction keyDown(e, row) {\n const i = rows.value.findIndex(r => r.key === row.key)\n const go = n => { const t = rows.value[n]; if (t) focus.value = t.key }\n if (e.key === 'ArrowDown') { e.preventDefault(); go(i + 1) }\n else if (e.key === 'ArrowUp') { e.preventDefault(); go(i - 1) }\n else if (e.key === 'ArrowRight') { e.preventDefault(); isBranch(row.n) && !openKeys.value.includes(row.key) ? toggle(row.key) : go(i + 1) }\n else if (e.key === 'ArrowLeft') { e.preventDefault(); if (isBranch(row.n) && openKeys.value.includes(row.key)) toggle(row.key) }\n else if (e.key === 'Enter') { e.preventDefault(); rowClick(row.n) }\n else if (e.key === ' ') { e.preventDefault(); if (props.checkable && !row.n.disabled && row.n.checkable !== false && !row.n.disableCheckbox) check(row.key) }\n}\nfunction parts(text) {\n const q = props.searchValue\n const s = String(text)\n if (!q) return [{ t: s, hit: false }]\n const i = s.toLowerCase().indexOf(q.toLowerCase())\n if (i < 0) return [{ t: s, hit: false }]\n return [{ t: s.slice(0, i), hit: false }, { t: s.slice(i, i + q.length), hit: true }, { t: s.slice(i + q.length), hit: false }]\n}\nconst dropAt = k => (drop.value && drop.value.key === k ? drop.value.pos : null)\n</script>\n<template>\n <div role=\"tree\" :style=\"{ display: 'flex', flexDirection: 'column', gap: showLine ? 0 : '1px', ...style }\">\n <div v-for=\"row in rows\" :key=\"row.key\" role=\"treeitem\" class=\"kb-tree-row\"\n :tabindex=\"focus === row.key || (!focus && row === rows[0]) ? 0 : -1\"\n :aria-expanded=\"isBranch(row.n) ? openKeys.includes(row.key) : undefined\"\n :aria-selected=\"selected.includes(row.key)\" :aria-disabled=\"row.n.disabled || undefined\"\n :draggable=\"draggable && !row.n.disabled\"\n @dragstart=\"dragKey = row.key\" @dragover=\"onDragOver($event, row.key)\" @dragleave=\"drop = null\"\n @drop.prevent=\"onDrop(row.key)\" @dragend=\"dragKey = null; drop = null\"\n @click=\"rowClick(row.n)\" @keydown=\"keyDown($event, row)\"\n :class=\"{ 'is-disabled': row.n.disabled }\"\n :style=\"{\n minHeight: 'var(--h-tree-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: '6px',\n padding: showLine ? '0 8px 0 2px' : '5px 8px', paddingLeft: showLine ? '2px' : (8 + row.depth * INDENT) + 'px', boxSizing: 'border-box',\n cursor: row.n.disabled ? 'not-allowed' : 'pointer', opacity: row.n.disabled ? 0.45 : 1,\n background: dropAt(row.key) === 'inside' ? 'var(--soft-bg)' : selected.includes(row.key) ? 'var(--bg-active)' : 'transparent',\n outline: dropAt(row.key) === 'inside' ? '2px solid var(--action-primary)' : 'none',\n outlineOffset: '-2px',\n boxShadow: dropAt(row.key) === 'before' ? 'inset 0 2px 0 var(--action-primary)' : dropAt(row.key) === 'after' ? 'inset 0 -2px 0 var(--action-primary)' : 'none',\n transition: 'var(--t-hover)' }\">\n <template v-if=\"showLine\">\n <span v-for=\"(hasNext, i) in row.trail\" :key=\"i\"\n :style=\"{ width: INDENT + 'px', alignSelf: 'stretch', flexShrink: 0, marginLeft: i === 0 ? '6px' : 0, borderLeft: hasNext ? '1px solid var(--border-subtle)' : '1px solid transparent' }\" />\n <span :style=\"{ width: INDENT + 'px', alignSelf: 'stretch', flexShrink: 0, position: 'relative', marginLeft: row.trail.length ? 0 : '6px' }\">\n <span :style=\"{ position: 'absolute', left: 0, top: 0, height: row.isLast ? '50%' : '100%', borderLeft: '1px solid var(--border-subtle)' }\" />\n <span :style=\"{ position: 'absolute', left: 0, top: '50%', width: (isBranch(row.n) ? 6 : INDENT - 4) + 'px', borderTop: '1px solid var(--border-subtle)' }\" />\n </span>\n </template>\n <span v-if=\"isBranch(row.n)\" @click.stop=\"toggle(row.key)\"\n :style=\"{ width: '14px', height: '14px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-muted)', flexShrink: 0, cursor: 'pointer', transform: openKeys.includes(row.key) ? 'rotate(90deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }\">\n <Icon :name=\"loading.includes(row.key) ? 'loader' : 'chevron-right'\" :size=\"13\" :class=\"loading.includes(row.key) ? 'kb-tree-spin' : ''\" />\n </span>\n <span v-else :style=\"{ width: showLine ? 0 : '14px', flexShrink: 0 }\" />\n <span v-if=\"checkable && row.n.checkable !== false\" @click.stop style=\"display:inline-flex;flex-shrink:0\">\n <Checkbox size=\"sm\" :checked=\"view.full.has(row.key)\" :indeterminate=\"view.half.has(row.key)\"\n :disabled=\"row.n.disabled || row.n.disableCheckbox\" @change=\"check(row.key)\" />\n </span>\n <span v-if=\"showIcon\" :style=\"{ color: isBranch(row.n) ? 'var(--text-muted)' : docHues[row.n.hue || 'gray'], display: 'inline-flex', flexShrink: 0 }\">\n <Icon :name=\"row.n.icon || (isBranch(row.n) ? (openKeys.includes(row.key) ? 'folder-open' : 'folder') : 'file-text')\" :size=\"15\" />\n </span>\n <span :style=\"{ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', fontWeight: selected.includes(row.key) ? 'var(--weight-medium)' : 'var(--weight-regular)', color: row.n.disabled ? 'var(--text-muted)' : 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }\">\n <template v-for=\"(p, i) in parts(titleOf(row.n))\" :key=\"i\"><mark v-if=\"p.hit\" style=\"background:var(--soft-bg);color:var(--info-fg);font-weight:var(--weight-medium);padding:0;border-radius:2px\">{{ p.t }}</mark><template v-else>{{ p.t }}</template></template>\n </span>\n <span v-if=\"row.n.tag\" style=\"font-family:var(--font-sans);font-size:var(--text-2xs);line-height:1;padding:2px 5px;border-radius:var(--radius-sm);background:var(--bg-inset);color:var(--text-secondary);flex-shrink:0\">{{ row.n.tag }}</span>\n <span v-if=\"row.n.count != null\" style=\"margin-left:auto;font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--text-muted);padding-left:8px\">{{ row.n.count }}</span>\n </div>\n <div v-if=\"!rows.length\" style=\"padding:10px 8px;font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-muted)\">No matching items</div>\n </div>\n</template>\n<style scoped>\n.kb-tree-row:not(.is-disabled):hover{background:var(--bg-hover)}\n.kb-tree-row:focus-visible{outline:2px solid var(--focus-ring);outline-offset:-2px}\n.kb-tree-spin{animation:kb-tree-spin 900ms linear infinite}\n@keyframes kb-tree-spin{to{transform:rotate(360deg)}}\n</style>\n"});
1
+ window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"navigation/Breadcrumb.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nfunction Crumb({ label, last, hovered, onSelect }) {\n const [hover, setHover] = React.useState(false);\n const on = (hover || hovered) && !last;\n return (\n <span role={last ? undefined : \"button\"} tabIndex={last ? undefined : 0}\n onClick={() => !last && onSelect && onSelect(label)}\n onKeyDown={(e) => { if (!last && (e.key === \"Enter\" || e.key === \" \")) { e.preventDefault(); onSelect && onSelect(label); } }}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: last || on ? \"var(--text-heading)\" : \"var(--text-secondary)\", fontWeight: last ? \"var(--weight-medium)\" : \"var(--weight-regular)\", cursor: last ? \"default\" : \"pointer\", borderRadius: \"var(--radius-sm)\", transition: \"var(--t-hover)\" }}>{label}</span>\n );\n}\n\nfunction Sep() {\n return <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\" }}><Icon name=\"chevron-right\" size={13} /></span>;\n}\n\n/** Overflow crumb: an ellipsis button that drops down the levels hidden by collapsing. */\nfunction Overflow({ items, onSelect }) {\n const [open, setOpen] = React.useState(false);\n const [hover, setHover] = React.useState(false);\n const root = React.useRef(null);\n React.useEffect(() => {\n if (!open) return;\n const h = (e) => { if (root.current && !root.current.contains(e.target)) setOpen(false); };\n document.addEventListener(\"mousedown\", h);\n return () => document.removeEventListener(\"mousedown\", h);\n }, [open]);\n return (\n <span ref={root} style={{ position: \"relative\", display: \"inline-flex\" }}>\n <span role=\"button\" tabIndex={0} aria-label={`Show ${items.length} hidden levels`} aria-expanded={open}\n onClick={() => setOpen(!open)}\n onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); setOpen(!open); } if (e.key === \"Escape\") setOpen(false); }}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", width: 20, height: 18, borderRadius: \"var(--radius-sm)\", cursor: \"pointer\", color: open || hover ? \"var(--text-heading)\" : \"var(--text-muted)\", background: open ? \"var(--bg-active)\" : hover ? \"var(--bg-hover)\" : \"transparent\", transition: \"var(--t-hover)\" }}>\n <Icon name=\"ellipsis\" size={14} />\n </span>\n {open ? (\n <span style={{ position: \"absolute\", top: \"calc(100% + 5px)\", left: 0, zIndex: \"var(--z-menu)\", minWidth: 168, maxWidth: 260, background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-md)\", boxShadow: \"var(--shadow-menu)\", padding: 4, display: \"flex\", flexDirection: \"column\" }}>\n {items.map((it, i) => <OverflowItem key={it} label={it} depth={i} onSelect={(l) => { setOpen(false); onSelect && onSelect(l); }} />)}\n </span>\n ) : null}\n </span>\n );\n}\n\nfunction OverflowItem({ label, depth, onSelect }) {\n const [hover, setHover] = React.useState(false);\n return (\n <span role=\"menuitem\" tabIndex={0} onClick={() => onSelect(label)}\n onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); onSelect(label); } }}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ display: \"flex\", alignItems: \"center\", gap: 6, padding: \"6px 9px\", paddingLeft: 9 + depth * 12, borderRadius: \"var(--radius-sm)\", cursor: \"pointer\", background: hover ? \"var(--bg-hover)\" : \"transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\", transition: \"var(--t-hover)\" }}>\n {depth > 0 ? <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", flexShrink: 0 }}><Icon name=\"chevron-right\" size={11} /></span> : null}\n {label}\n </span>\n );\n}\n\n/** Breadcrumb trail — 12px, chevron separators, last crumb in heading color. Clickable crumbs darken on hover and carry press + keyboard focus from the global interaction layer. Trails longer than `maxItems` collapse to first · … · last two, with the hidden levels in a dropdown on the ellipsis. */\nexport function Breadcrumb({ items = [], onSelect, maxItems = 4, hoveredIndex, style }) {\n const collapse = items.length > maxItems && maxItems >= 3;\n const tailCount = maxItems - 2;\n const head = collapse ? items.slice(0, 1) : items;\n const hidden = collapse ? items.slice(1, items.length - tailCount) : [];\n const tail = collapse ? items.slice(items.length - tailCount) : [];\n const lastLabel = items[items.length - 1];\n return (\n <nav style={{ display: \"flex\", alignItems: \"center\", gap: 4, ...style }}>\n {head.map((it, i) => (\n <React.Fragment key={it}>\n {i > 0 ? <Sep /> : null}\n <Crumb label={it} last={it === lastLabel} hovered={hoveredIndex === i} onSelect={onSelect} />\n </React.Fragment>\n ))}\n {collapse ? (\n <React.Fragment>\n <Sep />\n <Overflow items={hidden} onSelect={onSelect} />\n {tail.map((it) => (\n <React.Fragment key={it}>\n <Sep />\n <Crumb label={it} last={it === lastLabel} onSelect={onSelect} />\n </React.Fragment>\n ))}\n </React.Fragment>\n ) : null}\n </nav>\n );\n}\n\n/** Theme toggle — flips <html data-theme> and persists to localStorage(\"kb-theme\"). Pass `dark`+`onToggle` to control it externally. */\nexport function ThemeToggle({ size = 34, dark: darkProp, onToggle, style }) {\n const controlled = darkProp != null;\n const [darkState, setDark] = React.useState(() => (typeof document !== \"undefined\" && document.documentElement.dataset.theme === \"dark\"));\n const dark = controlled ? darkProp : darkState;\n React.useEffect(() => {\n if (controlled || typeof document === \"undefined\") return;\n document.documentElement.dataset.theme = dark ? \"dark\" : \"light\";\n try { localStorage.setItem(\"kb-theme\", dark ? \"dark\" : \"light\"); } catch (e) {}\n }, [dark, controlled]);\n const [hover, setHover] = React.useState(false);\n return (\n <button type=\"button\" aria-label={dark ? \"Switch to light mode\" : \"Switch to dark mode\"} onClick={() => (controlled ? onToggle && onToggle(!dark) : setDark(!dark))}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ width: size, height: size, borderRadius: \"var(--radius-md)\", border: \"none\", cursor: \"pointer\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", background: hover ? \"var(--bg-hover)\" : \"transparent\", color: \"var(--text-body)\", padding: 0, transition: \"var(--t-hover)\", ...style }}>\n <Icon name={dark ? \"sparkles\" : \"moon\"} size={18} />\n </button>\n );\n}\n\n/** Reads the persisted theme and applies it before first paint. Call once at app start. */\nexport function applyStoredTheme() {\n if (typeof document === \"undefined\") return;\n let t = \"light\";\n try { t = localStorage.getItem(\"kb-theme\") || \"light\"; } catch (e) {}\n document.documentElement.dataset.theme = t;\n}\n","navigation/Breadcrumb.vue":"<script setup>\nimport { ref, computed, watch, onUnmounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Breadcrumb trail — 12px, chevron separators, last crumb in heading color.\n// Clickable crumbs darken on hover (no underline) and carry press + keyboard focus from the global interaction layer.\n// Trails longer than maxItems collapse to first · … · last two, with the hidden levels in a dropdown on the ellipsis.\nconst props = defineProps({ items: { type: Array, default: () => [] }, maxItems: { type: Number, default: 4 }, hoveredIndex: { type: Number, default: null }, style: Object })\nconst emit = defineEmits(['select'])\nconst hover = ref(-1)\nconst ovHover = ref(false)\nconst menuHover = ref(-1)\nconst open = ref(false)\nconst root = ref(null)\nconst collapse = computed(() => props.items.length > props.maxItems && props.maxItems >= 3)\nconst tailCount = computed(() => props.maxItems - 2)\nconst head = computed(() => collapse.value ? props.items.slice(0, 1) : props.items)\nconst hidden = computed(() => collapse.value ? props.items.slice(1, props.items.length - tailCount.value) : [])\nconst tail = computed(() => collapse.value ? props.items.slice(props.items.length - tailCount.value) : [])\nconst lastLabel = computed(() => props.items[props.items.length - 1])\nconst isLast = (it) => it === lastLabel.value\nconst on = (i, it) => (hover.value === i || props.hoveredIndex === i) && !isLast(it)\nfunction pick(it) { if (!isLast(it)) emit('select', it) }\nfunction pickHidden(it) { open.value = false; emit('select', it) }\nfunction onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }\nwatch(open, (v) => { if (v) document.addEventListener('mousedown', onDoc); else document.removeEventListener('mousedown', onDoc) })\nonUnmounted(() => document.removeEventListener('mousedown', onDoc))\nconst crumbStyle = (i, it) => ({ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: isLast(it) || on(i, it) ? 'var(--text-heading)' : 'var(--text-secondary)', fontWeight: isLast(it) ? 'var(--weight-medium)' : 'var(--weight-regular)', cursor: isLast(it) ? 'default' : 'pointer', borderRadius: 'var(--radius-sm)', transition: 'var(--t-hover)' })\n</script>\n<template>\n <nav :style=\"{ display: 'flex', alignItems: 'center', gap: 4, ...style }\">\n <template v-for=\"(it, i) in head\" :key=\"it\">\n <span v-if=\"i > 0\" style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"chevron-right\" :size=\"13\" /></span>\n <span :role=\"isLast(it) ? undefined : 'button'\" :tabindex=\"isLast(it) ? undefined : 0\"\n @click=\"pick(it)\" @keydown.enter.prevent=\"pick(it)\" @keydown.space.prevent=\"pick(it)\"\n @mouseenter=\"hover = i\" @mouseleave=\"hover = -1\" :style=\"crumbStyle(i, it)\">{{ it }}</span>\n </template>\n <template v-if=\"collapse\">\n <span style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"chevron-right\" :size=\"13\" /></span>\n <span ref=\"root\" style=\"position:relative;display:inline-flex\">\n <span role=\"button\" tabindex=\"0\" :aria-label=\"`Show ${hidden.length} hidden levels`\" :aria-expanded=\"open\"\n @click=\"open = !open\" @keydown.enter.prevent=\"open = !open\" @keydown.space.prevent=\"open = !open\" @keydown.esc=\"open = false\"\n @mouseenter=\"ovHover = true\" @mouseleave=\"ovHover = false\"\n :style=\"{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '20px', height: '18px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', color: open || ovHover ? 'var(--text-heading)' : 'var(--text-muted)', background: open ? 'var(--bg-active)' : ovHover ? 'var(--bg-hover)' : 'transparent', transition: 'var(--t-hover)' }\">\n <Icon name=\"ellipsis\" :size=\"14\" />\n </span>\n <span v-if=\"open\" style=\"position:absolute;top:calc(100% + 5px);left:0;z-index:var(--z-menu);min-width:168px;max-width:260px;background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;display:flex;flex-direction:column\">\n <span v-for=\"(h, hi) in hidden\" :key=\"h\" role=\"menuitem\" tabindex=\"0\"\n @click=\"pickHidden(h)\" @keydown.enter.prevent=\"pickHidden(h)\" @keydown.space.prevent=\"pickHidden(h)\"\n @mouseenter=\"menuHover = hi\" @mouseleave=\"menuHover = -1\"\n :style=\"{ display: 'flex', alignItems: 'center', gap: '6px', padding: '6px 9px', paddingLeft: (9 + hi * 12) + 'px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: menuHover === hi ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', transition: 'var(--t-hover)' }\">\n <span v-if=\"hi > 0\" style=\"color:var(--text-muted);display:inline-flex;flex-shrink:0\"><Icon name=\"chevron-right\" :size=\"11\" /></span>{{ h }}\n </span>\n </span>\n </span>\n <template v-for=\"it in tail\" :key=\"it\">\n <span style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"chevron-right\" :size=\"13\" /></span>\n <span :role=\"isLast(it) ? undefined : 'button'\" :tabindex=\"isLast(it) ? undefined : 0\"\n @click=\"pick(it)\" @keydown.enter.prevent=\"pick(it)\" @keydown.space.prevent=\"pick(it)\"\n :style=\"crumbStyle(-1, it)\">{{ it }}</span>\n </template>\n </template>\n </nav>\n</template>\n\n<!-- applyStoredTheme() — reads the persisted theme and applies it before first paint. Call once at app start (main.js): -->\n<!--\nexport function applyStoredTheme() {\n let t = 'light'\n try { t = localStorage.getItem('kb-theme') || 'light' } catch (e) {}\n document.documentElement.dataset.theme = t\n}\n-->\n","navigation/DocTree.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { SearchInput } from \"../forms/SearchInput.jsx\";\n\nconst docHues = { orange: \"var(--cat-transform)\", green: \"var(--cat-source)\", purple: \"var(--cat-merge)\", blue: \"var(--info-fg)\", teal: \"var(--cat-output)\", gray: \"var(--text-muted)\" };\n\n/** Secondary documents panel — 240px: title header, search row, 30px tree items. items: [{label, kind:\"folder\"|\"doc\", hue, icon}]. */\nexport function DocTree({ title = \"Documents\", items = [], active, onSelect, style }) {\n return (\n <div style={{ width: \"var(--sidebar-w)\", background: \"var(--bg-surface)\", borderRight: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ height: \"var(--topbar-h)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"0 12px 0 16px\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n <IconButton icon=\"chevron-right\" size=\"md\" muted />\n </div>\n <div style={{ padding: \"12px 16px 8px\", borderBottom: \"1px solid var(--border-subtle)\" }}>\n <SearchInput bare placeholder=\"Search...\" style={{ gap: 8 }} />\n </div>\n <div style={{ padding: 8, display: \"flex\", flexDirection: \"column\", gap: 2, overflowY: \"auto\" }}>\n {items.map((it) => {\n const is = active === it.label;\n return (\n <div key={it.label} role=\"treeitem\" tabIndex={0} onClick={() => onSelect && onSelect(it.label)}\n style={{ height: \"var(--h-tree-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 8, padding: \"6px 8px\", boxSizing: \"border-box\", cursor: \"pointer\", background: is ? \"var(--bg-active)\" : \"transparent\" }}\n onMouseEnter={(e) => { if (!is) e.currentTarget.style.background = \"var(--bg-hover)\"; }}\n onMouseLeave={(e) => { if (!is) e.currentTarget.style.background = \"transparent\"; }}>\n <span style={{ color: it.kind === \"folder\" ? \"var(--text-muted)\" : docHues[it.hue || \"gray\"], display: \"inline-flex\" }}>\n <Icon name={it.icon || (it.kind === \"folder\" ? \"folder\" : \"file-text\")} size={16} />\n </span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{it.label}</span>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n","navigation/DocTree.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport SearchInput from '../forms/SearchInput.vue'\nconst docHues = { orange: 'var(--cat-transform)', green: 'var(--cat-source)', purple: 'var(--cat-merge)', blue: 'var(--info-fg)', teal: 'var(--cat-output)', gray: 'var(--text-muted)' }\n// Secondary documents panel — 240px: title header, search row, 30px tree items. items: [{label, kind:\"folder\"|\"doc\", hue, icon}].\ndefineProps({ title: { type: String, default: 'Documents' }, items: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['select'])\n</script>\n<template>\n <div :style=\"{ width: 'var(--sidebar-w)', background: 'var(--bg-surface)', borderRight: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"height:var(--topbar-h);border-bottom:1px solid var(--border-card);display:flex;align-items:center;justify-content:space-between;padding:0 12px 0 16px;box-sizing:border-box;flex-shrink:0\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <IconButton icon=\"chevron-right\" size=\"md\" muted />\n </div>\n <div style=\"padding:12px 16px 8px;border-bottom:1px solid var(--border-subtle)\"><SearchInput bare placeholder=\"Search...\" style=\"gap:8px\" /></div>\n <div style=\"padding:8px;display:flex;flex-direction:column;gap:2px;overflow-y:auto\">\n <div v-for=\"it in items\" :key=\"it.label\" @click=\"emit('select', it.label)\"\n :style=\"{ height: 'var(--h-tree-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: 8, padding: '6px 8px', boxSizing: 'border-box', cursor: 'pointer', background: active === it.label ? 'var(--bg-active)' : 'transparent' }\">\n <span :style=\"{ color: it.kind === 'folder' ? 'var(--text-muted)' : docHues[it.hue || 'gray'], display: 'inline-flex' }\"><Icon :name=\"it.icon || (it.kind === 'folder' ? 'folder' : 'file-text')\" :size=\"16\" /></span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ it.label }}</span>\n </div>\n </div>\n </div>\n</template>\n","navigation/Sidebar.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nfunction NavItem({ icon, label, active, indent, collapsed, badge, onClick }) {\n const [hover, setHover] = React.useState(false);\n return (\n <div role=\"button\" tabIndex={0} onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ height: indent ? \"var(--h-subnav-item)\" : \"var(--h-nav-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", justifyContent: collapsed ? \"center\" : \"flex-start\", gap: 10, padding: collapsed ? \"8px 10px\" : indent ? \"6px 10px\" : \"8px 10px\", boxSizing: \"border-box\", cursor: \"pointer\", background: active ? \"var(--bg-active)\" : hover ? \"var(--bg-hover)\" : \"transparent\", color: active ? \"var(--text-heading)\" : \"var(--text-body)\", transition: \"var(--t-hover)\" }}>\n <Icon name={icon} size={indent ? 14 : 16} />\n {!collapsed ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", whiteSpace: \"nowrap\", flex: 1 }}>{label}</span> : null}\n {!collapsed && badge ? badge : null}\n </div>\n );\n}\n\n/**\n * Keybricks app sidebar — 240px (collapsed: 64px icon rail). White, hairline right border.\n * items: [{icon,label,children:[{icon,label}]}]; active by label; footer items pinned.\n */\nexport function Sidebar({ items = [], footerItems = [{ icon: \"circle-help\", label: \"Help\" }, { icon: \"settings\", label: \"Settings\" }, { icon: \"moon\", label: \"Dark Mode\" }], active, onSelect, collapsed = false, onToggleCollapse, logo = \"assets/keybricks-logo.png\", brand = \"Keybricks\", height = \"100%\", style }) {\n const [openSections, setOpen] = React.useState(() => items.filter((i) => i.children && (i.open || (i.children.some((c) => c.label === active)))).map((i) => i.label));\n const toggleSection = (l) => setOpen(openSections.includes(l) ? openSections.filter((x) => x !== l) : [...openSections, l]);\n return (\n <aside style={{ width: collapsed ? \"var(--rail-w)\" : \"var(--sidebar-w)\", height, background: \"var(--bg-surface)\", borderRight: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, transition: \"width .15s ease\", ...style }}>\n <div style={{ height: \"var(--topbar-h)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", gap: 10, padding: \"0 16px\", boxSizing: \"border-box\", justifyContent: collapsed ? \"center\" : \"flex-start\", flexShrink: 0 }}>\n <img src={logo} alt=\"\" style={{ width: 24, height: 24, objectFit: \"contain\" }} />\n {!collapsed ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-lg)\", lineHeight: \"var(--leading-lg)\", letterSpacing: \"-.609px\", color: \"var(--text-primary)\" }}>{brand}</span> : null}\n </div>\n <nav style={{ flex: 1, overflowY: \"auto\", padding: 8, display: \"flex\", flexDirection: \"column\", gap: 0 }}>\n {onToggleCollapse ? <NavItem icon=\"panel-left\" label=\"Collapse\" collapsed={collapsed} onClick={onToggleCollapse} /> : null}\n {items.map((it) => it.children ? (\n <div key={it.label}>\n <div onClick={() => toggleSection(it.label)} style={{ height: \"var(--h-subnav-item)\", display: \"flex\", alignItems: \"center\", gap: 10, padding: collapsed ? \"6px 10px\" : \"6px 10px\", justifyContent: collapsed ? \"center\" : \"flex-start\", cursor: \"pointer\", color: \"var(--text-body)\", borderRadius: \"var(--radius-md)\", boxSizing: \"border-box\" }}>\n <Icon name={it.icon} size={16} />\n {!collapsed ? <><span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", letterSpacing: \"var(--tracking-base)\", flex: 1 }}>{it.label}</span><Icon name={openSections.includes(it.label) ? \"chevron-up\" : \"chevron-down\"} size={14} /></> : null}\n </div>\n {!collapsed && openSections.includes(it.label) ? (\n <div style={{ marginLeft: 24, display: \"flex\", flexDirection: \"column\", gap: 2, paddingTop: 2 }}>\n {it.children.map((c) => <NavItem key={c.label} icon={c.icon} label={c.label} indent active={active === c.label} onClick={() => onSelect && onSelect(c.label)} />)}\n </div>\n ) : null}\n </div>\n ) : (\n <NavItem key={it.label} icon={it.icon} label={it.label} collapsed={collapsed} active={active === it.label} onClick={() => onSelect && onSelect(it.label)} />\n ))}\n </nav>\n <div style={{ borderTop: \"1px solid var(--border-card)\", padding: 8, display: \"flex\", flexDirection: \"column\", flexShrink: 0 }}>\n {footerItems.map((f) => <NavItem key={f.label} icon={f.icon} label={f.label} collapsed={collapsed} onClick={() => onSelect && onSelect(f.label)} />)}\n </div>\n </aside>\n );\n}\n\n/** Default Keybricks nav model (Home dashboard). */\nexport const KEYBRICKS_NAV = [\n { icon: \"house\", label: \"Home\" },\n { icon: \"file-text\", label: \"Documents\" },\n { icon: \"layout-grid\", label: \"Business Intelligence\" },\n { icon: \"database\", label: \"Data Integration\" },\n { icon: \"scale\", label: \"Reconciliation\" },\n { icon: \"network\", label: \"Consolidation\" },\n {\n icon: \"folder-cog\", label: \"Management\", children: [\n { icon: \"box\", label: \"Modeling\" },\n { icon: \"workflow\", label: \"Workflow\" },\n { icon: \"shield\", label: \"Security Management\" },\n { icon: \"refresh-cw\", label: \"System Migration\" },\n { icon: \"chart-column\", label: \"Business Logic\" },\n { icon: \"list-todo\", label: \"Tasks Management\" },\n { icon: \"circle-check\", label: \"Validation Rules\" },\n { icon: \"settings-2\", label: \"Control Center\" },\n { icon: \"network\", label: \"Reconciliation\" },\n ],\n },\n];\n","navigation/Sidebar.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\n\n/**\n * Keybricks app sidebar — 240px (collapsed: 64px icon rail). White, hairline right border.\n * items: [{icon,label,children:[{icon,label}]}]; active by label; footer items pinned.\n */\nconst props = defineProps({\n items: { type: Array, default: () => [] },\n footerItems: { type: Array, default: () => [{ icon: 'circle-help', label: 'Help' }, { icon: 'settings', label: 'Settings' }, { icon: 'moon', label: 'Dark Mode' }] },\n active: String, collapsed: { type: Boolean, default: false },\n logo: { type: String, default: 'assets/keybricks-logo.png' }, brand: { type: String, default: 'Keybricks' },\n height: { type: [String, Number], default: '100%' }, style: Object, canCollapse: Boolean,\n})\nconst emit = defineEmits(['select', 'toggleCollapse'])\nconst openSections = ref(props.items.filter(i => i.children && (i.open || i.children.some(c => c.label === props.active))).map(i => i.label))\nfunction toggleSection(l) { openSections.value = openSections.value.includes(l) ? openSections.value.filter(x => x !== l) : [...openSections.value, l] }\nfunction itemStyle(active, indent) { return { height: indent ? 'var(--h-subnav-item)' : 'var(--h-nav-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: 10, padding: indent ? '6px 10px' : '8px 10px', boxSizing: 'border-box', cursor: 'pointer', background: active ? 'var(--bg-active)' : 'transparent', color: active ? 'var(--text-heading)' : 'var(--text-body)', transition: 'var(--t-hover)' } }\n</script>\n<template>\n <aside :style=\"{ width: collapsed ? 'var(--rail-w)' : 'var(--sidebar-w)', height, background: 'var(--bg-surface)', borderRight: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, transition: 'width .15s ease', ...style }\">\n <div :style=\"{ height: 'var(--topbar-h)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', gap: 10, padding: '0 16px', boxSizing: 'border-box', justifyContent: collapsed ? 'center' : 'flex-start', flexShrink: 0 }\">\n <img :src=\"logo\" alt=\"\" style=\"width:24px;height:24px;object-fit:contain\" />\n <span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-lg);line-height:var(--leading-lg);letter-spacing:-.609px;color:var(--text-primary)\">{{ brand }}</span>\n </div>\n <nav style=\"flex:1;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:0\">\n <div v-if=\"canCollapse\" role=\"button\" tabindex=\"0\" @click=\"emit('toggleCollapse')\" :style=\"itemStyle(false, false)\"><Icon name=\"panel-left\" :size=\"16\" /><span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);white-space:nowrap;flex:1\">Collapse</span></div>\n <template v-for=\"it in items\" :key=\"it.label\">\n <div v-if=\"it.children\">\n <div role=\"button\" tabindex=\"0\" @click=\"toggleSection(it.label)\" :style=\"{ height: 'var(--h-subnav-item)', display: 'flex', alignItems: 'center', gap: 10, padding: '6px 10px', justifyContent: collapsed ? 'center' : 'flex-start', cursor: 'pointer', color: 'var(--text-body)', borderRadius: 'var(--radius-md)', boxSizing: 'border-box' }\">\n <Icon :name=\"it.icon\" :size=\"16\" />\n <template v-if=\"!collapsed\"><span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);letter-spacing:var(--tracking-base);flex:1\">{{ it.label }}</span><Icon :name=\"openSections.includes(it.label) ? 'chevron-up' : 'chevron-down'\" :size=\"14\" /></template>\n </div>\n <div v-if=\"!collapsed && openSections.includes(it.label)\" style=\"margin-left:24px;display:flex;flex-direction:column;gap:2px;padding-top:2px\">\n <div v-for=\"c in it.children\" :key=\"c.label\" role=\"button\" tabindex=\"0\" @click=\"emit('select', c.label)\" :style=\"itemStyle(active === c.label, true)\"><Icon :name=\"c.icon\" :size=\"14\" /><span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);white-space:nowrap;flex:1\">{{ c.label }}</span></div>\n </div>\n </div>\n <div v-else role=\"button\" tabindex=\"0\" @click=\"emit('select', it.label)\" :style=\"itemStyle(active === it.label, false)\"><Icon :name=\"it.icon\" :size=\"16\" /><span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);white-space:nowrap;flex:1\">{{ it.label }}</span></div>\n </template>\n </nav>\n <div style=\"border-top:1px solid var(--border-card);padding:8px;display:flex;flex-direction:column;flex-shrink:0\">\n <div v-for=\"f in footerItems\" :key=\"f.label\" role=\"button\" tabindex=\"0\" @click=\"emit('select', f.label)\" :style=\"itemStyle(false, false)\"><Icon :name=\"f.icon\" :size=\"16\" /><span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);white-space:nowrap;flex:1\">{{ f.label }}</span></div>\n </div>\n </aside>\n</template>\n\n<!-- KEYBRICKS_NAV — default Keybricks nav model (Home dashboard), same shape as Sidebar.jsx's export, for reference:\nexport const KEYBRICKS_NAV = [\n { icon: 'house', label: 'Home' }, { icon: 'file-text', label: 'Documents' },\n { icon: 'layout-grid', label: 'Business Intelligence' }, { icon: 'database', label: 'Data Integration' },\n { icon: 'scale', label: 'Reconciliation' }, { icon: 'network', label: 'Consolidation' },\n { icon: 'folder-cog', label: 'Management', children: [ ... ] },\n] -->\n","navigation/TopBar.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { Button } from \"../buttons/Button.jsx\";\nimport { SearchInput } from \"../forms/SearchInput.jsx\";\n\n/** 56px top bar: bare search left, bell + dark AI Assistant button right. */\nexport function TopBar({ placeholder = \"Search documents, workflows, data...\", actions, onSearch, style }) {\n return (\n <header style={{ height: \"var(--topbar-h)\", background: \"var(--bg-surface)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"0 24px\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <SearchInput bare placeholder={placeholder} onChange={onSearch} style={{ width: 576, maxWidth: \"48%\" }} />\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n {actions || (<>\n <IconButton icon=\"bell\" size=\"lg\" />\n <Button variant=\"dark\" icon=\"sparkles\">AI Assistant</Button>\n </>)}\n </div>\n </header>\n );\n}\n\n/** Editor toolbar: back chevron, 14px/600 title, \"• Editing\" status, right action slot. */\nexport function PageToolbar({ title, status = \"• Editing\", actions, onBack, style }) {\n return (\n <div style={{ height: \"var(--topbar-h)\", background: \"var(--bg-surface)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"0 16px\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n {onBack ? <IconButton icon=\"chevron-left\" size=\"md\" onClick={onBack} /> : null}\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n {status ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-muted)\", marginLeft: 6 }}>{status}</span> : null}\n </div>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>{actions}</div>\n </div>\n );\n}\n\n/** Page heading block: 15px/500 title + 13px muted subtitle (System Migration pages). */\nexport function PageHeading({ title, subtitle, style }) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 4, ...style }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-lg)\", lineHeight: \"var(--leading-lg)\", letterSpacing: \"var(--tracking-lg)\", color: \"var(--text-primary)\" }}>{title}</span>\n {subtitle ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-secondary)\" }}>{subtitle}</span> : null}\n </div>\n );\n}\n","navigation/TopBar.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\nimport Button from '../buttons/Button.vue'\nimport SearchInput from '../forms/SearchInput.vue'\n// 56px top bar: bare search left, bell + dark AI Assistant button right.\ndefineProps({ placeholder: { type: String, default: 'Search documents, workflows, data...' }, style: Object })\nconst emit = defineEmits(['search'])\n</script>\n<template>\n <header :style=\"{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 24px', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <SearchInput bare :placeholder=\"placeholder\" @update:modelValue=\"v => emit('search', v)\" style=\"width:576px;max-width:48%\" />\n <div style=\"display:flex;align-items:center;gap:8px\">\n <slot name=\"actions\">\n <IconButton icon=\"bell\" size=\"lg\" />\n <Button variant=\"dark\" icon=\"sparkles\">AI Assistant</Button>\n </slot>\n </div>\n </header>\n</template>\n","navigation/Tree.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { Checkbox } from \"../forms/Checkbox.jsx\";\n\nconst docHues = { orange: \"var(--cat-transform)\", green: \"var(--cat-source)\", purple: \"var(--cat-merge)\", blue: \"var(--info-fg)\", teal: \"var(--cat-output)\", gray: \"var(--text-muted)\" };\nconst INDENT = 18;\n\nconst keyOf = (n) => (n.key != null ? n.key : n.label);\nconst titleOf = (n) => (n.title != null ? n.title : n.label);\nconst kidsOf = (n) => n.children || [];\nconst isBranch = (n) => kidsOf(n).length > 0 || n.isLeaf === false;\n\nfunction walk(nodes, fn, parentKey = null, depth = 0) {\n nodes.forEach((n) => { fn(n, parentKey, depth); walk(kidsOf(n), fn, keyOf(n), depth + 1); });\n}\nfunction indexNodes(items) {\n const byKey = {}, parent = {}, all = [];\n walk(items, (n, p, d) => { const k = keyOf(n); byKey[k] = n; parent[k] = p; all.push({ node: n, key: k, depth: d }); });\n return { byKey, parent, all };\n}\nfunction descendants(node, out = []) {\n kidsOf(node).forEach((c) => { out.push(c); descendants(c, out); });\n return out;\n}\n/** Is `key` the dragged node itself, or anywhere inside its subtree? */\nfunction inSubtree(items, rootKey, key) {\n const { byKey } = indexNodes(items);\n const root = byKey[rootKey];\n if (!root) return false;\n return rootKey === key || descendants(root).some((d) => keyOf(d) === key);\n}\nfunction keysMatching(items, test) {\n const hit = [];\n const visit = (n, trail) => {\n const t = [...trail, keyOf(n)];\n if (test(n)) hit.push(...trail);\n kidsOf(n).forEach((c) => visit(c, t));\n };\n items.forEach((n) => visit(n, []));\n return [...new Set(hit)];\n}\n\n/* Cascading check: a parent is checked when every enabled descendant is checked,\n indeterminate when only some are. Disabled nodes never change state. */\nfunction cascade(items, key, next) {\n const { byKey, parent } = indexNodes(items);\n const set = new Set(next);\n const node = byKey[key];\n if (!node) return next;\n const checked = set.has(key);\n descendants(node).forEach((d) => {\n if (d.disabled || d.disableCheckbox) return;\n checked ? set.add(keyOf(d)) : set.delete(keyOf(d));\n });\n let p = parent[key];\n while (p != null) {\n const pn = byKey[p];\n const kids = kidsOf(pn).filter((c) => !c.disabled && !c.disableCheckbox);\n const on = kids.filter((c) => set.has(keyOf(c)));\n on.length && on.length === kids.length ? set.add(p) : set.delete(p);\n p = parent[p];\n }\n return [...set];\n}\n/* Display state for every branch: full when all enabled descendants are checked,\n half when only some. Keeps parents honest whatever the caller passed in. */\nfunction rollUp(items, checkedKeys) {\n const full = new Set(checkedKeys), half = new Set();\n const visit = (n) => {\n kidsOf(n).forEach(visit);\n if (!kidsOf(n).length) return;\n const ds = descendants(n).filter((d) => !d.disabled && !d.disableCheckbox);\n if (!ds.length) return;\n const on = ds.filter((d) => full.has(keyOf(d))).length;\n if (on === ds.length) full.add(keyOf(n));\n else { full.delete(keyOf(n)); if (on > 0) half.add(keyOf(n)); }\n };\n items.forEach(visit);\n return { full, half };\n}\nfunction halfKeys(items, checkedKeys) {\n return [...rollUp(items, checkedKeys).half];\n}\n\nfunction Highlight({ text, query }) {\n if (!query) return text;\n const i = String(text).toLowerCase().indexOf(query.toLowerCase());\n if (i < 0) return text;\n return <>{String(text).slice(0, i)}<mark style={{ background: \"var(--soft-bg)\", color: \"var(--info-fg)\", fontWeight: \"var(--weight-medium)\", padding: 0, borderRadius: 2 }}>{String(text).slice(i, i + query.length)}</mark>{String(text).slice(i + query.length)}</>;\n}\n\n/** Guide lines for showLine: one 18px column per ancestor level, then the elbow. */\nfunction Guides({ trail, isLast, hasKids }) {\n return (\n <>\n {trail.map((ancestorHasNext, i) => (\n <span key={i} style={{ width: INDENT, alignSelf: \"stretch\", flexShrink: 0, borderLeft: ancestorHasNext ? \"1px solid var(--border-subtle)\" : \"1px solid transparent\", marginLeft: i === 0 ? 6 : 0 }} />\n ))}\n <span style={{ width: INDENT, alignSelf: \"stretch\", flexShrink: 0, position: \"relative\", marginLeft: trail.length === 0 ? 6 : 0 }}>\n <span style={{ position: \"absolute\", left: 0, top: 0, height: isLast ? \"50%\" : \"100%\", borderLeft: \"1px solid var(--border-subtle)\" }} />\n <span style={{ position: \"absolute\", left: 0, top: \"50%\", width: hasKids ? 6 : INDENT - 4, borderTop: \"1px solid var(--border-subtle)\" }} />\n </span>\n </>\n );\n}\n\nfunction Row({ item, state, cfg, api }) {\n const { node, key, depth, trail, isLast } = item;\n const kids = kidsOf(node);\n const branch = isBranch(node);\n const open = state.expanded.includes(key);\n const selected = state.selected.includes(key);\n const checked = state.checked.has(key);\n const half = !checked && state.half.has(key);\n const loading = state.loading.includes(key);\n const focused = state.focus === key;\n const ringed = focused && state.focusMoved;\n const [hover, setHover] = React.useState(false);\n const disabled = !!node.disabled;\n const dropAt = state.drop && state.drop.key === key ? state.drop.pos : null;\n const selectable = cfg.selectable && node.selectable !== false;\n\n const chevron = branch ? (\n <span onClick={(e) => { e.stopPropagation(); api.toggle(key); }} style={{ width: 14, height: 14, display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", color: \"var(--text-muted)\", flexShrink: 0, cursor: \"pointer\", transform: open ? \"rotate(90deg)\" : \"none\", transition: \"transform var(--dur-fast) var(--ease-out)\" }}>\n {loading ? <Icon name=\"loader\" size={12} style={{ animation: \"kb-tree-spin 900ms linear infinite\" }} /> : <Icon name=\"chevron-right\" size={13} />}\n </span>\n ) : <span style={{ width: cfg.showLine ? 0 : 14, flexShrink: 0 }} />;\n\n return (\n <>\n <div role=\"treeitem\" tabIndex={focused ? 0 : -1} aria-expanded={branch ? open : undefined} aria-selected={selected} aria-disabled={disabled || undefined}\n ref={(el) => { if (el && focused && state.focusMoved) el.focus({ preventScroll: true }); }}\n draggable={cfg.draggable && !disabled}\n onDragStart={(e) => { e.dataTransfer.effectAllowed = \"move\"; api.dragStart(key); }}\n onDragOver={(e) => { if (!cfg.draggable) return; e.preventDefault(); const r = e.currentTarget.getBoundingClientRect(); api.dragOver(key, (e.clientY - r.top) / r.height); }}\n onDragLeave={() => api.dragOver(null)}\n onDrop={(e) => { e.preventDefault(); api.drop(key); }}\n onDragEnd={() => api.dragEnd()}\n onClick={() => { if (disabled) return; api.focus(key); if (selectable) api.select(key); if (branch) api.toggle(key); }}\n onContextMenu={(e) => { if (cfg.onContextMenu) { e.preventDefault(); cfg.onContextMenu(key, e); } }}\n onKeyDown={(e) => api.keyDown(e, item)}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{\n minHeight: \"var(--h-tree-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 6,\n padding: cfg.showLine ? \"0 8px 0 2px\" : \"5px 8px\", paddingLeft: cfg.showLine ? 2 : 8 + depth * INDENT, boxSizing: \"border-box\",\n cursor: disabled ? \"not-allowed\" : \"pointer\", opacity: disabled ? 0.45 : 1,\n background: dropAt === \"inside\" ? \"var(--soft-bg)\" : selected ? \"var(--bg-active)\" : hover && !disabled ? \"var(--bg-hover)\" : \"transparent\",\n outline: dropAt === \"inside\" ? \"2px solid var(--action-primary)\" : ringed ? \"2px solid var(--focus-border)\" : \"none\", outlineOffset: -2,\n boxShadow: dropAt === \"before\" ? \"inset 0 2px 0 var(--action-primary)\" : dropAt === \"after\" ? \"inset 0 -2px 0 var(--action-primary)\" : \"none\",\n transition: \"var(--t-hover)\",\n }}>\n {cfg.showLine ? <Guides trail={trail} isLast={isLast} hasKids={branch} /> : null}\n {chevron}\n {cfg.checkable && node.checkable !== false ? (\n <span onClick={(e) => e.stopPropagation()} style={{ display: \"inline-flex\", flexShrink: 0 }}>\n <Checkbox size=\"sm\" checked={checked} indeterminate={half} disabled={disabled || node.disableCheckbox} onChange={() => api.check(key)} />\n </span>\n ) : null}\n {cfg.showIcon ? (\n <span style={{ color: branch ? \"var(--text-muted)\" : docHues[node.hue || \"gray\"], display: \"inline-flex\", flexShrink: 0 }}>\n <Icon name={node.icon || (branch ? (open ? \"folder-open\" : \"folder\") : \"file-text\")} size={15} />\n </span>\n ) : null}\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", fontWeight: selected ? \"var(--weight-medium)\" : \"var(--weight-regular)\", color: disabled ? \"var(--text-muted)\" : \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>\n <Highlight text={titleOf(node)} query={cfg.searchValue} />\n </span>\n {node.tag ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-2xs)\", lineHeight: 1, padding: \"2px 5px\", borderRadius: \"var(--radius-sm)\", background: \"var(--bg-inset)\", color: \"var(--text-secondary)\", flexShrink: 0 }}>{node.tag}</span> : null}\n {node.count != null ? <span style={{ marginLeft: \"auto\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", color: \"var(--text-muted)\", paddingLeft: 8 }}>{node.count}</span> : null}\n {cfg.rowActions && !disabled ? <span style={{ marginLeft: node.count != null ? 6 : \"auto\", display: \"flex\", gap: 2, opacity: hover ? 1 : 0, transition: \"var(--t-hover)\" }} onClick={(e) => e.stopPropagation()}>{cfg.rowActions(node)}</span> : null}\n </div>\n {open && loading && !kids.length ? (\n <div style={{ paddingLeft: 8 + (depth + 1) * INDENT + 14, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-muted)\", height: \"var(--h-tree-item)\", display: \"flex\", alignItems: \"center\" }}>Loading…</div>\n ) : null}\n </>\n );\n}\n\n/**\n * Nested tree view. Expand/collapse, single or multiple selection, cascading checkboxes\n * (with indeterminate parents and `checkStrictly` opt-out), disabled nodes and disabled\n * checkboxes, connector lines, async child loading, search highlighting, drag to move,\n * and full keyboard navigation. Node shape: { key, title, icon, hue, count, tag, children,\n * disabled, disableCheckbox, selectable, checkable, isLeaf }.\n */\nexport function Tree({\n items = [], treeData,\n defaultExpanded = [], expandedKeys, onExpand,\n defaultActive, defaultSelected = [], selectedKeys, onSelect,\n checkable = false, checkStrictly = false, defaultChecked = [], checkedKeys, onCheck,\n multiple = false, selectable = true, showIcon = true, showLine = false, draggable = false,\n searchValue = \"\", autoExpandOnSearch = true, loadData, onDrop, onContextMenu, rowActions, style, reorder = false,\n}) {\n const raw = treeData || items;\n /* Callers usually pass an inline array literal, so compare by value —\n re-seeding on identity alone would wipe async-loaded children. */\n const rawKey = JSON.stringify(raw);\n const [data, setData] = React.useState(raw);\n const seenKey = React.useRef(rawKey);\n React.useEffect(() => { if (seenKey.current !== rawKey) { seenKey.current = rawKey; setData(raw); } }, [rawKey]);\n\n const seedExpanded = defaultExpanded.length ? defaultExpanded : defaultActive ? [] : [];\n const [expandedS, setExpandedS] = React.useState(seedExpanded);\n const [selectedS, setSelectedS] = React.useState(defaultActive ? [defaultActive] : defaultSelected);\n const [checkedS, setCheckedS] = React.useState(defaultChecked);\n const [loading, setLoading] = React.useState([]);\n const [loaded, setLoaded] = React.useState([]);\n const [focus, setFocus] = React.useState(null);\n const [focusMoved, setFocusMoved] = React.useState(false);\n const [dragKey, setDragKey] = React.useState(null);\n const [drop, setDrop] = React.useState(null);\n\n const expanded = expandedKeys || expandedS;\n const selected = selectedKeys || selectedS;\n const checked = checkedKeys || checkedS;\n\n const searchExpanded = React.useMemo(\n () => (searchValue && autoExpandOnSearch ? keysMatching(data, (n) => String(titleOf(n)).toLowerCase().includes(searchValue.toLowerCase())) : []),\n [data, searchValue, autoExpandOnSearch]\n );\n const openKeys = React.useMemo(() => [...new Set([...expanded, ...searchExpanded])], [expanded, searchExpanded]);\n const view = React.useMemo(\n () => (checkable && !checkStrictly ? rollUp(data, checked) : { full: new Set(checked), half: new Set() }),\n [checkable, checkStrictly, data, checked]\n );\n\n /* Visible rows, with the sibling map each row needs to draw its guide lines. */\n const rows = React.useMemo(() => {\n const out = [];\n const push = (nodes, depth, trail) => {\n nodes.forEach((n, i) => {\n const isLast = i === nodes.length - 1;\n const key = keyOf(n);\n out.push({ node: n, key, depth, trail, isLast });\n if (kidsOf(n).length && openKeys.includes(key)) push(kidsOf(n), depth + 1, [...trail, !isLast]);\n });\n };\n push(data, 0, []);\n return out;\n }, [data, openKeys]);\n\n const setExpanded = (next) => { if (!expandedKeys) setExpandedS(next); onExpand && onExpand(next); };\n\n const api = {\n focus: (k) => { setFocus(k); setFocusMoved(false); },\n toggle: (k) => {\n const open = openKeys.includes(k);\n if (!open && loadData && !loaded.includes(k) && !kidsOf(indexNodes(data).byKey[k]).length) {\n setLoading((l) => [...l, k]);\n setExpanded([...expanded, k]);\n Promise.resolve(loadData(indexNodes(data).byKey[k])).then((kids) => {\n setData((d) => {\n const clone = JSON.parse(JSON.stringify(d));\n const { byKey } = indexNodes(clone);\n if (byKey[k]) byKey[k].children = kids || [];\n return clone;\n });\n setLoaded((l) => [...l, k]);\n setLoading((l) => l.filter((x) => x !== k));\n });\n return;\n }\n setExpanded(open ? expanded.filter((x) => x !== k) : [...expanded, k]);\n },\n select: (k) => {\n const next = multiple ? (selected.includes(k) ? selected.filter((x) => x !== k) : [...selected, k]) : selected.includes(k) && multiple ? [] : [k];\n if (!selectedKeys) setSelectedS(next);\n onSelect && onSelect(multiple ? next : k, next);\n },\n check: (k) => {\n const on = view.full.has(k);\n let next = on ? [...view.full].filter((x) => x !== k) : [...view.full, k];\n if (!checkStrictly) next = cascade(data, k, next);\n if (!checkedKeys) setCheckedS(next);\n onCheck && onCheck(next, { key: k, checked: !on, half: checkStrictly ? [] : halfKeys(data, next) });\n },\n dragStart: (k) => setDragKey(k),\n /* Validity lives here, not in the row: only branches can be dropped *into*, and a node\n can never land inside its own subtree (that would detach the whole branch). */\n dragOver: (k, ratio) => {\n if (k == null) return setDrop(null);\n const node = indexNodes(data).byKey[k];\n /* Only folders are drop targets: a file can't contain anything, so hovering one shows\n nothing at all. With reorder, a folder's outer 20% becomes an insert rule instead. */\n if (!node || node.disabled || !dragKey || !isBranch(node) || inSubtree(data, dragKey, k)) return setDrop(null);\n const pos = reorder ? (ratio < 0.2 ? \"before\" : ratio > 0.8 ? \"after\" : \"inside\") : \"inside\";\n setDrop({ key: k, pos });\n },\n dragEnd: () => { setDragKey(null); setDrop(null); },\n drop: (k) => {\n const info = drop && drop.key === k ? drop : null;\n setDrop(null);\n if (!dragKey || !info || inSubtree(data, dragKey, k)) return setDragKey(null);\n /* Computed outside setData: React re-invokes updaters (StrictMode, concurrent\n rebasing), which would fire the consumer's onDrop twice for one gesture. */\n const clone = JSON.parse(JSON.stringify(data));\n let moved = null;\n const prune = (nodes) => nodes.filter((n) => {\n if (keyOf(n) === dragKey) { moved = n; return false; }\n if (n.children) n.children = prune(n.children);\n return true;\n });\n const rootA = prune(clone);\n if (!moved) return setDragKey(null);\n let inserted = false;\n const insert = (nodes) => {\n const out = [];\n nodes.forEach((n) => {\n if (keyOf(n) === k && info.pos === \"before\") { out.push(moved); inserted = true; }\n if (keyOf(n) === k && info.pos === \"inside\") { n.children = [...(n.children || []), moved]; inserted = true; }\n else if (n.children) n.children = insert(n.children);\n out.push(n);\n if (keyOf(n) === k && info.pos === \"after\") { out.push(moved); inserted = true; }\n });\n return out;\n };\n const next = insert(rootA);\n if (!inserted) return setDragKey(null);\n setData(next);\n /* Dropping into a collapsed folder would otherwise look like the node vanished. */\n if (info.pos === \"inside\" && !expanded.includes(k)) setExpanded([...expanded, k]);\n onDrop && onDrop({ dragKey, dropKey: k, position: info.pos, treeData: next });\n setDragKey(null);\n },\n keyDown: (e, item) => {\n const i = rows.findIndex((r) => r.key === item.key);\n const go = (n) => { const t = rows[n]; if (!t) return; setFocus(t.key); setFocusMoved(true); };\n if (e.key === \"ArrowDown\") { e.preventDefault(); go(i + 1); }\n else if (e.key === \"ArrowUp\") { e.preventDefault(); go(i - 1); }\n else if (e.key === \"ArrowRight\") { e.preventDefault(); if (isBranch(item.node) && !openKeys.includes(item.key)) api.toggle(item.key); else go(i + 1); }\n else if (e.key === \"ArrowLeft\") { e.preventDefault(); if (isBranch(item.node) && openKeys.includes(item.key)) api.toggle(item.key); else { const p = rows.slice(0, i).reverse().find((r) => r.depth === item.depth - 1); if (p) { setFocus(p.key); setFocusMoved(true); } } }\n else if (e.key === \"Enter\") { e.preventDefault(); if (!item.node.disabled) { if (selectable && item.node.selectable !== false) api.select(item.key); if (isBranch(item.node)) api.toggle(item.key); } }\n else if (e.key === \" \") { e.preventDefault(); if (checkable && !item.node.disabled && item.node.checkable !== false && !item.node.disableCheckbox) api.check(item.key); }\n },\n };\n\n const state = { expanded: openKeys, selected, checked: view.full, half: view.half, loading, focus: focus || (rows[0] && rows[0].key), focusMoved, drop };\n const cfg = { checkable, checkStrictly, multiple, selectable, showIcon, showLine, draggable, searchValue, onContextMenu, rowActions };\n\n return (\n <div role=\"tree\" aria-multiselectable={multiple || undefined} style={{ display: \"flex\", flexDirection: \"column\", gap: showLine ? 0 : 1, ...style }}>\n <style>{\"@keyframes kb-tree-spin{to{transform:rotate(360deg)}}\"}</style>\n {rows.length === 0 ? (\n <div style={{ padding: \"10px 8px\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", color: \"var(--text-muted)\" }}>No matching items</div>\n ) : rows.map((r) => <Row key={r.key} item={r} state={state} cfg={cfg} api={api} />)}\n </div>\n );\n}\n","navigation/Tree.vue":"<script setup>\nimport { ref, computed, watch } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport Checkbox from '../forms/Checkbox.vue'\n// Nested tree view: expand/collapse, single or multiple selection, cascading checkboxes with\n// indeterminate parents (checkStrictly opts out), disabled nodes, connector lines, async\n// loadData, search highlight, drag to move, keyboard navigation.\nconst docHues = { orange: 'var(--cat-transform)', green: 'var(--cat-source)', purple: 'var(--cat-merge)', blue: 'var(--info-fg)', teal: 'var(--cat-output)', gray: 'var(--text-muted)' }\nconst INDENT = 18\nconst props = defineProps({\n items: { type: Array, default: () => [] },\n defaultExpanded: { type: Array, default: () => [] },\n defaultActive: String,\n defaultSelected: { type: Array, default: () => [] },\n defaultChecked: { type: Array, default: () => [] },\n checkable: Boolean, checkStrictly: Boolean, multiple: Boolean,\n selectable: { type: Boolean, default: true },\n showIcon: { type: Boolean, default: true },\n showLine: Boolean, draggable: Boolean,\n searchValue: { type: String, default: '' },\n autoExpandOnSearch: { type: Boolean, default: true },\n reorder: Boolean,\n loadData: Function, rowActions: Function, style: Object,\n})\nconst emit = defineEmits(['select', 'check', 'expand', 'drop'])\n\nconst data = ref(JSON.parse(JSON.stringify(props.items)))\n// Compare by value: re-seeding on identity alone would wipe async-loaded children.\nwatch(() => JSON.stringify(props.items), (next, prev) => { if (next !== prev) data.value = JSON.parse(next) })\nconst expanded = ref([...props.defaultExpanded])\nconst selected = ref(props.defaultActive ? [props.defaultActive] : [...props.defaultSelected])\nconst checked = ref([...props.defaultChecked])\nconst loading = ref([]); const loaded = ref([]); const focus = ref(null)\nconst dragKey = ref(null); const drop = ref(null)\n\nconst keyOf = n => (n.key != null ? n.key : n.label)\nconst titleOf = n => (n.title != null ? n.title : n.label)\nconst kidsOf = n => n.children || []\nconst isBranch = n => kidsOf(n).length > 0 || n.isLeaf === false\nfunction descendants(n, out = []) { for (const c of kidsOf(n)) { out.push(c); descendants(c, out) } return out }\n/** Is `key` the dragged node itself, or anywhere inside its subtree? */\nfunction inSubtree(rootKey, key) {\n const root = index(data.value).byKey[rootKey]\n if (!root) return false\n return rootKey === key || descendants(root).some(d => keyOf(d) === key)\n}\nfunction index(nodes, parent = null, acc = { byKey: {}, parent: {} }) {\n for (const n of nodes) { acc.byKey[keyOf(n)] = n; acc.parent[keyOf(n)] = parent; index(kidsOf(n), keyOf(n), acc) }\n return acc\n}\nfunction ancestorsOfMatches(nodes, test, trail = [], hit = []) {\n for (const n of nodes) {\n if (test(n)) hit.push(...trail)\n ancestorsOfMatches(kidsOf(n), test, [...trail, keyOf(n)], hit)\n }\n return [...new Set(hit)]\n}\nconst searchExpanded = computed(() => props.searchValue && props.autoExpandOnSearch\n ? ancestorsOfMatches(data.value, n => String(titleOf(n)).toLowerCase().includes(props.searchValue.toLowerCase()))\n : [])\nconst openKeys = computed(() => [...new Set([...expanded.value, ...searchExpanded.value])])\n\nconst view = computed(() => {\n const full = new Set(checked.value); const half = new Set()\n if (!props.checkable || props.checkStrictly) return { full, half }\n const visit = n => {\n kidsOf(n).forEach(visit)\n if (!kidsOf(n).length) return\n const ds = descendants(n).filter(d => !d.disabled && !d.disableCheckbox)\n if (!ds.length) return\n const on = ds.filter(d => full.has(keyOf(d))).length\n if (on === ds.length) full.add(keyOf(n))\n else { full.delete(keyOf(n)); if (on > 0) half.add(keyOf(n)) }\n }\n data.value.forEach(visit)\n return { full, half }\n})\n\nconst rows = computed(() => {\n const out = []\n const push = (nodes, depth, trail) => {\n nodes.forEach((n, i) => {\n const isLast = i === nodes.length - 1\n out.push({ n, key: keyOf(n), depth, trail, isLast })\n if (kidsOf(n).length && openKeys.value.includes(keyOf(n))) push(kidsOf(n), depth + 1, [...trail, !isLast])\n })\n }\n push(data.value, 0, [])\n return out\n})\n\nfunction toggle(k) {\n const open = openKeys.value.includes(k)\n const { byKey } = index(data.value)\n if (!open && props.loadData && !loaded.value.includes(k) && !kidsOf(byKey[k]).length) {\n loading.value = [...loading.value, k]\n expanded.value = [...expanded.value, k]\n Promise.resolve(props.loadData(byKey[k])).then(kids => {\n const clone = JSON.parse(JSON.stringify(data.value))\n const t = index(clone).byKey[k]\n if (t) t.children = kids || []\n data.value = clone\n loaded.value = [...loaded.value, k]\n loading.value = loading.value.filter(x => x !== k)\n })\n return\n }\n expanded.value = open ? expanded.value.filter(x => x !== k) : [...expanded.value, k]\n emit('expand', expanded.value)\n}\nfunction select(k) {\n selected.value = props.multiple\n ? (selected.value.includes(k) ? selected.value.filter(x => x !== k) : [...selected.value, k])\n : [k]\n emit('select', props.multiple ? selected.value : k, selected.value)\n}\nfunction check(k) {\n const set = new Set(view.value.full)\n const on = set.has(k)\n on ? set.delete(k) : set.add(k)\n if (!props.checkStrictly) {\n const { byKey, parent } = index(data.value)\n descendants(byKey[k]).forEach(d => {\n if (d.disabled || d.disableCheckbox) return\n on ? set.delete(keyOf(d)) : set.add(keyOf(d))\n })\n let p = parent[k]\n while (p != null) {\n const kids = kidsOf(byKey[p]).filter(c => !c.disabled && !c.disableCheckbox)\n const hit = kids.filter(c => set.has(keyOf(c)))\n hit.length && hit.length === kids.length ? set.add(p) : set.delete(p)\n p = parent[p]\n }\n }\n checked.value = [...set]\n emit('check', checked.value, { key: k, checked: !on, half: [...view.value.half] })\n}\nfunction rowClick(n) {\n if (n.disabled) return\n focus.value = keyOf(n)\n if (props.selectable && n.selectable !== false) select(keyOf(n))\n if (isBranch(n)) toggle(keyOf(n))\n}\nfunction onDragOver(e, k) {\n if (!props.draggable) return\n e.preventDefault()\n if (!dragKey.value) return\n const n = index(data.value).byKey[k]\n // Only folders are drop targets: hovering a file shows nothing, since it can't contain anything.\n if (!n || n.disabled || !isBranch(n) || inSubtree(dragKey.value, k)) { drop.value = null; return }\n const r = e.currentTarget.getBoundingClientRect()\n const y = (e.clientY - r.top) / r.height\n drop.value = { key: k, pos: props.reorder ? (y < 0.2 ? 'before' : y > 0.8 ? 'after' : 'inside') : 'inside' }\n}\nfunction onDrop(k) {\n const info = drop.value && drop.value.key === k ? drop.value : null\n drop.value = null\n if (!dragKey.value || !info || inSubtree(dragKey.value, k)) { dragKey.value = null; return }\n const clone = JSON.parse(JSON.stringify(data.value))\n let moved = null\n const prune = nodes => nodes.filter(n => {\n if (keyOf(n) === dragKey.value) { moved = n; return false }\n if (n.children) n.children = prune(n.children)\n return true\n })\n const pruned = prune(clone)\n if (!moved) { dragKey.value = null; return }\n const insert = nodes => {\n const out = []\n for (const n of nodes) {\n if (keyOf(n) === k && info.pos === 'before') out.push(moved)\n if (keyOf(n) === k && info.pos === 'inside') n.children = [...(n.children || []), moved]\n else if (n.children) n.children = insert(n.children)\n out.push(n)\n if (keyOf(n) === k && info.pos === 'after') out.push(moved)\n }\n return out\n }\n data.value = insert(pruned)\n if (info.pos === 'inside' && !expanded.value.includes(k)) expanded.value = [...expanded.value, k]\n emit('drop', { dragKey: dragKey.value, dropKey: k, position: info.pos, treeData: data.value })\n dragKey.value = null\n}\nfunction keyDown(e, row) {\n const i = rows.value.findIndex(r => r.key === row.key)\n const go = n => { const t = rows.value[n]; if (t) focus.value = t.key }\n if (e.key === 'ArrowDown') { e.preventDefault(); go(i + 1) }\n else if (e.key === 'ArrowUp') { e.preventDefault(); go(i - 1) }\n else if (e.key === 'ArrowRight') { e.preventDefault(); isBranch(row.n) && !openKeys.value.includes(row.key) ? toggle(row.key) : go(i + 1) }\n else if (e.key === 'ArrowLeft') { e.preventDefault(); if (isBranch(row.n) && openKeys.value.includes(row.key)) toggle(row.key) }\n else if (e.key === 'Enter') { e.preventDefault(); rowClick(row.n) }\n else if (e.key === ' ') { e.preventDefault(); if (props.checkable && !row.n.disabled && row.n.checkable !== false && !row.n.disableCheckbox) check(row.key) }\n}\nfunction parts(text) {\n const q = props.searchValue\n const s = String(text)\n if (!q) return [{ t: s, hit: false }]\n const i = s.toLowerCase().indexOf(q.toLowerCase())\n if (i < 0) return [{ t: s, hit: false }]\n return [{ t: s.slice(0, i), hit: false }, { t: s.slice(i, i + q.length), hit: true }, { t: s.slice(i + q.length), hit: false }]\n}\nconst dropAt = k => (drop.value && drop.value.key === k ? drop.value.pos : null)\n</script>\n<template>\n <div role=\"tree\" :style=\"{ display: 'flex', flexDirection: 'column', gap: showLine ? 0 : '1px', ...style }\">\n <div v-for=\"row in rows\" :key=\"row.key\" role=\"treeitem\" class=\"kb-tree-row\"\n :tabindex=\"focus === row.key || (!focus && row === rows[0]) ? 0 : -1\"\n :aria-expanded=\"isBranch(row.n) ? openKeys.includes(row.key) : undefined\"\n :aria-selected=\"selected.includes(row.key)\" :aria-disabled=\"row.n.disabled || undefined\"\n :draggable=\"draggable && !row.n.disabled\"\n @dragstart=\"dragKey = row.key\" @dragover=\"onDragOver($event, row.key)\" @dragleave=\"drop = null\"\n @drop.prevent=\"onDrop(row.key)\" @dragend=\"dragKey = null; drop = null\"\n @click=\"rowClick(row.n)\" @keydown=\"keyDown($event, row)\"\n :class=\"{ 'is-disabled': row.n.disabled }\"\n :style=\"{\n minHeight: 'var(--h-tree-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: '6px',\n padding: showLine ? '0 8px 0 2px' : '5px 8px', paddingLeft: showLine ? '2px' : (8 + row.depth * INDENT) + 'px', boxSizing: 'border-box',\n cursor: row.n.disabled ? 'not-allowed' : 'pointer', opacity: row.n.disabled ? 0.45 : 1,\n background: dropAt(row.key) === 'inside' ? 'var(--soft-bg)' : selected.includes(row.key) ? 'var(--bg-active)' : 'transparent',\n outline: dropAt(row.key) === 'inside' ? '2px solid var(--action-primary)' : 'none',\n outlineOffset: '-2px',\n boxShadow: dropAt(row.key) === 'before' ? 'inset 0 2px 0 var(--action-primary)' : dropAt(row.key) === 'after' ? 'inset 0 -2px 0 var(--action-primary)' : 'none',\n transition: 'var(--t-hover)' }\">\n <template v-if=\"showLine\">\n <span v-for=\"(hasNext, i) in row.trail\" :key=\"i\"\n :style=\"{ width: INDENT + 'px', alignSelf: 'stretch', flexShrink: 0, marginLeft: i === 0 ? '6px' : 0, borderLeft: hasNext ? '1px solid var(--border-subtle)' : '1px solid transparent' }\" />\n <span :style=\"{ width: INDENT + 'px', alignSelf: 'stretch', flexShrink: 0, position: 'relative', marginLeft: row.trail.length ? 0 : '6px' }\">\n <span :style=\"{ position: 'absolute', left: 0, top: 0, height: row.isLast ? '50%' : '100%', borderLeft: '1px solid var(--border-subtle)' }\" />\n <span :style=\"{ position: 'absolute', left: 0, top: '50%', width: (isBranch(row.n) ? 6 : INDENT - 4) + 'px', borderTop: '1px solid var(--border-subtle)' }\" />\n </span>\n </template>\n <span v-if=\"isBranch(row.n)\" @click.stop=\"toggle(row.key)\"\n :style=\"{ width: '14px', height: '14px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-muted)', flexShrink: 0, cursor: 'pointer', transform: openKeys.includes(row.key) ? 'rotate(90deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }\">\n <Icon :name=\"loading.includes(row.key) ? 'loader' : 'chevron-right'\" :size=\"13\" :class=\"loading.includes(row.key) ? 'kb-tree-spin' : ''\" />\n </span>\n <span v-else :style=\"{ width: showLine ? 0 : '14px', flexShrink: 0 }\" />\n <span v-if=\"checkable && row.n.checkable !== false\" @click.stop style=\"display:inline-flex;flex-shrink:0\">\n <Checkbox size=\"sm\" :checked=\"view.full.has(row.key)\" :indeterminate=\"view.half.has(row.key)\"\n :disabled=\"row.n.disabled || row.n.disableCheckbox\" @change=\"check(row.key)\" />\n </span>\n <span v-if=\"showIcon\" :style=\"{ color: isBranch(row.n) ? 'var(--text-muted)' : docHues[row.n.hue || 'gray'], display: 'inline-flex', flexShrink: 0 }\">\n <Icon :name=\"row.n.icon || (isBranch(row.n) ? (openKeys.includes(row.key) ? 'folder-open' : 'folder') : 'file-text')\" :size=\"15\" />\n </span>\n <span :style=\"{ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', fontWeight: selected.includes(row.key) ? 'var(--weight-medium)' : 'var(--weight-regular)', color: row.n.disabled ? 'var(--text-muted)' : 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }\">\n <template v-for=\"(p, i) in parts(titleOf(row.n))\" :key=\"i\"><mark v-if=\"p.hit\" style=\"background:var(--soft-bg);color:var(--info-fg);font-weight:var(--weight-medium);padding:0;border-radius:2px\">{{ p.t }}</mark><template v-else>{{ p.t }}</template></template>\n </span>\n <span v-if=\"row.n.tag\" style=\"font-family:var(--font-sans);font-size:var(--text-2xs);line-height:1;padding:2px 5px;border-radius:var(--radius-sm);background:var(--bg-inset);color:var(--text-secondary);flex-shrink:0\">{{ row.n.tag }}</span>\n <span v-if=\"row.n.count != null\" style=\"margin-left:auto;font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--text-muted);padding-left:8px\">{{ row.n.count }}</span>\n </div>\n <div v-if=\"!rows.length\" style=\"padding:10px 8px;font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-muted)\">No matching items</div>\n </div>\n</template>\n<style scoped>\n.kb-tree-row:not(.is-disabled):hover{background:var(--bg-hover)}\n.kb-tree-row:focus-visible{outline:2px solid var(--focus-ring);outline-offset:-2px}\n.kb-tree-spin{animation:kb-tree-spin 900ms linear infinite}\n@keyframes kb-tree-spin{to{transform:rotate(360deg)}}\n</style>\n","navigation/PageHeading.vue":"<script setup>\ndefineProps({ title: String, subtitle: String, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', flexDirection: 'column', gap: 4, ...style }\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-lg);line-height:var(--leading-lg);letter-spacing:var(--tracking-lg);color:var(--text-primary)\">{{ title }}</span>\n <span v-if=\"subtitle\" style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-secondary)\">{{ subtitle }}</span>\n </div>\n</template>\n","navigation/PageToolbar.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\ndefineProps({ title: String, status: { type: String, default: '• Editing' }, showBack: Boolean, style: Object })\nconst emit = defineEmits(['back'])\n</script>\n<template>\n <div :style=\"{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 16px', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"display:flex;align-items:center;gap:8px\">\n <IconButton v-if=\"showBack\" icon=\"chevron-left\" size=\"md\" @click=\"emit('back')\" />\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"status\" style=\"font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-muted);margin-left:6px\">{{ status }}</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:8px\"><slot name=\"actions\" /></div>\n </div>\n</template>\n","navigation/ThemeToggle.vue":"<script setup>\nimport { ref, computed, watch, onMounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\nconst props = defineProps({ size: { type: Number, default: 34 }, dark: { type: Boolean, default: null }, style: Object })\nconst emit = defineEmits(['update:dark'])\nconst darkState = ref(false)\nconst dark = computed(() => props.dark != null ? props.dark : darkState.value)\nconst hover = ref(false)\nonMounted(() => { darkState.value = document.documentElement.dataset.theme === 'dark' })\nwatch(dark, (v) => { if (props.dark != null) return; document.documentElement.dataset.theme = v ? 'dark' : 'light'; try { localStorage.setItem('kb-theme', v ? 'dark' : 'light') } catch (e) {} })\nfunction toggle() { if (props.dark != null) emit('update:dark', !dark.value); else darkState.value = !darkState.value }\n</script>\n<template>\n <button type=\"button\" :aria-label=\"dark ? 'Switch to light mode' : 'Switch to dark mode'\" @click=\"toggle\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ width: size, height: size, borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: hover ? 'var(--bg-hover)' : 'transparent', color: 'var(--text-body)', padding: 0, transition: 'var(--t-hover)', ...style }\">\n <Icon :name=\"dark ? 'sparkles' : 'moon'\" :size=\"18\" />\n </button>\n</template>\n"});