@keyfunc/keybricks-ds 2.3.1 → 2.4.0
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.
- package/DEPLOY.md +10 -4
- package/DESIGN.md +44 -1
- package/INTEGRATION.md +24 -6
- package/_adherence.oxlintrc.json +28 -13
- package/assets/logos/datasource/dat.png +0 -0
- package/assets/logos/datasource/excel.png +0 -0
- package/assets/logos/datasource/ftp.png +0 -0
- package/assets/logos/datasource/hierarchy.png +0 -0
- package/assets/logos/datasource/hive.png +0 -0
- package/assets/logos/datasource/json.png +0 -0
- package/assets/logos/datasource/mariadb.png +0 -0
- package/assets/logos/datasource/mongodb.png +0 -0
- package/assets/logos/datasource/mysql.png +0 -0
- package/assets/logos/datasource/odbc.png +0 -0
- package/assets/logos/datasource/oracle.png +0 -0
- package/assets/logos/datasource/postgresql.png +0 -0
- package/assets/logos/datasource/rest-api.png +0 -0
- package/assets/logos/datasource/sap-alt.png +0 -0
- package/assets/logos/datasource/sap.png +0 -0
- package/assets/logos/datasource/soap.png +0 -0
- package/assets/logos/datasource/sql-server.png +0 -0
- package/components/_cards/app-chrome.card.html +33 -21
- package/components/_cards/context-menu.card.html +7 -2
- package/components/_cards/icons-0-usage.card.html +88 -0
- package/components/_cards/icons-4-datasource.card.html +5 -0
- package/components/_cards/segmented-control.card.html +9 -3
- package/components/_cards/tabs.card.html +5 -2
- package/components/_shared/src-feedback.js +1 -1
- package/components/_shared/src-navigation.js +1 -1
- package/components/_shared/src-tabs.js +1 -1
- package/components/_shared/view-source.js +12 -12
- package/components/_vue/index.mjs +4 -1
- package/components/feedback/ContextMenu.d.ts +3 -2
- package/components/feedback/ContextMenu.jsx +8 -4
- package/components/feedback/ContextMenu.vue +9 -6
- package/components/feedback/Menu.d.ts +1 -1
- package/components/feedback/feedback.prompt.md +1 -1
- package/components/index.d.ts +1 -0
- package/components/index.js +4 -3
- package/components/navigation/SidePanel.d.ts +22 -0
- package/components/navigation/SidePanel.jsx +23 -0
- package/components/navigation/SidePanel.vue +21 -0
- package/components/navigation/Sidebar.d.ts +23 -7
- package/components/navigation/Sidebar.jsx +38 -37
- package/components/navigation/Sidebar.vue +40 -39
- package/components/navigation/TopBar.d.ts +28 -3
- package/components/navigation/TopBar.jsx +54 -7
- package/components/navigation/TopBar.vue +26 -8
- package/components/navigation/UserMenu.vue +27 -0
- package/components/navigation/chrome-tones.js +22 -0
- package/components/navigation/nav-model.js +21 -18
- package/components/navigation/navigation.prompt.md +2 -0
- package/components/tabs/CountBadge.vue +12 -0
- package/components/tabs/PanelTabs.vue +7 -3
- package/components/tabs/PillTabs.vue +7 -3
- package/components/tabs/SegmentedControl.d.ts +4 -2
- package/components/tabs/SegmentedControl.jsx +5 -3
- package/components/tabs/SegmentedControl.vue +4 -3
- package/components/tabs/Tabs.d.ts +14 -4
- package/components/tabs/Tabs.jsx +35 -20
- package/components/tabs/Tabs.vue +7 -3
- package/components/tabs/tabs.prompt.md +2 -2
- package/package.json +2 -2
- package/readme.md +4 -4
- package/readme.zh-CN.md +2 -2
- package/tokens/layout.css +4 -3
- package/tokens/tokens.d.ts +2 -0
- package/tokens/tokens.js +4 -1
- package/tokens/tokens.json +8 -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\";\nimport { applyStoredTheme } from \"./theme-store.js\";\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-base)\", lineHeight: \"var(--leading-base)\", 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: 24, height: 24, 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-base)\", lineHeight: \"var(--leading-base)\", 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. Defined in theme-store.js, shared with the Vue entry. */\nexport { applyStoredTheme };\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-base)', lineHeight: 'var(--leading-base)', 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: '4px', ...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: '24px', height: '24px', 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-base)', lineHeight: 'var(--leading-base)', 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() lives in ./theme-store.js (shared with React) and is exported from the Vue entry.\n Call it once in main.js before mounting. -->\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} data-wash={is ? undefined : \"\"} 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)\" : \"var(--wash, transparent)\", transition: \"var(--t-hover)\" }}\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-base)\", lineHeight: \"var(--leading-base)\", 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\" role=\"treeitem\" tabindex=\"0\" @click=\"emit('select', it.label)\" @keydown.enter=\"emit('select', it.label)\"\n :style=\"{ height: 'var(--h-tree-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 8px', boxSizing: 'border-box', cursor: 'pointer', background: active === it.label ? 'var(--bg-active)' : 'var(--wash, transparent)', transition: 'var(--t-hover)' }\" :data-wash=\"active === it.label ? undefined : ''\">\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-base);line-height:var(--leading-base);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\";\nimport { KEYBRICKS_NAV } from \"./nav-model.js\";\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 role=\"button\" tabIndex={0} data-wash=\"\" onClick={() => toggleSection(it.label)} onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); toggleSection(it.label); } }} style={{ background: \"var(--wash, transparent)\", transition: \"var(--t-hover)\", 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) — defined in nav-model.js, shared with the Vue entry. */\nexport { KEYBRICKS_NAV };\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: '10px', 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: '10px', 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\" data-wash=\"\" @click=\"toggleSection(it.label)\" @keydown.enter.prevent=\"toggleSection(it.label)\" @keydown.space.prevent=\"toggleSection(it.label)\" :style=\"{ background: 'var(--wash, transparent)', transition: 'var(--t-hover)', height: 'var(--h-subnav-item)', display: 'flex', alignItems: 'center', gap: '10px', 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<!-- The product's nav model is KEYBRICKS_NAV in ./nav-model.js — also exported from the Vue entry:\n import { Sidebar, KEYBRICKS_NAV } from '@keyfunc/keybricks-ds/vue' -->\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, size, style }) {\n return (\n <div data-size={size} style={{ height: \"var(--topbar-h)\", background: \"var(--bg-surface)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"0 var(--edge-pad)\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"var(--control-gap)\" }}>\n {onBack ? <IconButton icon=\"chevron-left\" onClick={onBack} label=\"Back\" /> : 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(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-muted)\", marginLeft: 6 }}>{status}</span> : null}\n </div>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"var(--control-gap)\" }}>{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 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-base)\", lineHeight: \"var(--leading-base)\", 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-xs)\", 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-sans)\", fontVariantNumeric: \"tabular-nums\", fontSize: \"var(--text-xs)\", 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-base)\", 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 treeData: Array, // alias of items (same as Tree.jsx)\n expandedKeys: Array, selectedKeys: Array, checkedKeys: Array, // controlled: the parent's value wins whenever it changes\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.treeData || props.items)))\n// Compare by value: re-seeding on identity alone would wipe async-loaded children.\nwatch(() => JSON.stringify(props.treeData || 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])\nwatch(() => props.expandedKeys, (v) => { if (v) expanded.value = [...v] }, { immediate: true })\nwatch(() => props.selectedKeys, (v) => { if (v) selected.value = [...v] }, { immediate: true })\nwatch(() => props.checkedKeys, (v) => { if (v) checked.value = [...v] }, { immediate: true })\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 :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-base)', lineHeight: 'var(--leading-base)', 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-xs);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-sans);font-variant-numeric:tabular-nums;font-size:var(--text-xs);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-base);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-border);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: '4px', ...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'\n// Page toolbar under the top bar: back · title · status, actions on the right. A size region (default inherit → md).\ndefineProps({ title: String, status: { type: String, default: '• Editing' }, showBack: Boolean, size: String, style: Object })\nconst emit = defineEmits(['back'])\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 var(--edge-pad)', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"display:flex;align-items:center;gap:var(--control-gap)\">\n <IconButton v-if=\"showBack\" icon=\"chevron-left\" label=\"Back\" @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(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-muted);margin-left:6px\">{{ status }}</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:var(--control-gap)\"><slot name=\"actions\" /></div>\n </div>\n</template>\n","navigation/ThemeToggle.vue":"<script setup>\nimport { px } from '../px.js'\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: px(size), height: px(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"});
|
|
1
|
+
window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"navigation/Breadcrumb.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { applyStoredTheme } from \"./theme-store.js\";\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-base)\", lineHeight: \"var(--leading-base)\", 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: 24, height: 24, 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-base)\", lineHeight: \"var(--leading-base)\", 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. Defined in theme-store.js, shared with the Vue entry. */\nexport { applyStoredTheme };\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-base)', lineHeight: 'var(--leading-base)', 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: '4px', ...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: '24px', height: '24px', 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-base)', lineHeight: 'var(--leading-base)', 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() lives in ./theme-store.js (shared with React) and is exported from the Vue entry.\n Call it once in main.js before mounting. -->\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} data-wash={is ? undefined : \"\"} 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)\" : \"var(--wash, transparent)\", transition: \"var(--t-hover)\" }}\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-base)\", lineHeight: \"var(--leading-base)\", 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\" role=\"treeitem\" tabindex=\"0\" @click=\"emit('select', it.label)\" @keydown.enter=\"emit('select', it.label)\"\n :style=\"{ height: 'var(--h-tree-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 8px', boxSizing: 'border-box', cursor: 'pointer', background: active === it.label ? 'var(--bg-active)' : 'var(--wash, transparent)', transition: 'var(--t-hover)' }\" :data-wash=\"active === it.label ? undefined : ''\">\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-base);line-height:var(--leading-base);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\";\nimport { KEYBRICKS_NAV, KEYBRICKS_NAV_FOOTER, KEYBRICKS_MGMT } from \"./nav-model.js\";\nimport { chromeTone, SIDEBAR_COLORS } from \"./chrome-tones.js\";\n\nconst LABEL = { flex: 1, minWidth: 0, overflow: \"hidden\", textOverflow: \"ellipsis\", whiteSpace: \"nowrap\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"18px\", letterSpacing: \"var(--tracking-base)\", fontWeight: \"var(--weight-medium)\" };\n\n/* One rail row: 36px min, radius-md, 16px icon, medium label, optional trailing chevron (the item opens a side panel). */\nfunction NavItem({ icon, label, active, indent, collapsed, trailing, onClick }) {\n const [hover, setHover] = React.useState(false);\n return (\n <div role=\"button\" tabIndex={0} title={collapsed ? label : undefined} aria-current={active ? \"page\" : undefined} onClick={onClick} onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); onClick && onClick(); } }} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ minHeight: indent ? 32 : 36, borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", justifyContent: collapsed ? \"center\" : \"flex-start\", gap: 10, padding: indent ? \"6px 10px\" : \"8px 10px\", boxSizing: \"border-box\", cursor: \"pointer\", userSelect: \"none\", background: active ? \"var(--sb-active)\" : hover ? \"var(--sb-hover)\" : \"transparent\", color: active ? \"var(--sb-active-text)\" : \"var(--sb-text)\", boxShadow: active ? \"var(--sb-shadow)\" : \"none\", transition: \"var(--t-hover)\" }}>\n {icon ? <span style={{ display: \"inline-flex\", flexShrink: 0 }}><Icon name={icon} size={indent ? 14 : 16} /></span> : null}\n {!collapsed ? <span style={LABEL}>{label}</span> : null}\n {!collapsed && trailing ? <span style={{ display: \"inline-flex\", flexShrink: 0, opacity: 0.6 }}><Icon name=\"chevron-right\" size={14} /></span> : null}\n </div>\n );\n}\n\n/**\n * Keybricks app sidebar — 240px, 64px icon rail when collapsed. A 56px brand head (no rule), a 2px-gap list, pinned footer items and a version line.\n * tone: \"recessed\" (grey, default) | \"flush\" (white) | \"contrast\" (dark) | \"custom\" (+ color: a SIDEBAR_COLORS key or hex).\n * items / footerItems: [{ icon, label, trailing?, children? }] — trailing draws a chevron (the item opens a SidePanel); children make a collapsible group.\n */\nexport function Sidebar({ items = KEYBRICKS_NAV, footerItems = KEYBRICKS_NAV_FOOTER, active, onSelect, collapsed = false, onToggleCollapse, logo = \"assets/keybricks-logo.png\", brand = \"Keybricks\", version, tone = \"recessed\", color, labels, height = \"100%\", style }) {\n const L = { collapse: \"Collapse\", expand: \"Expand\", ...labels };\n const t = chromeTone(tone, color);\n const [open, setOpen] = React.useState(() => items.filter((i) => i.children && i.children.some((c) => (c.label || c) === active)).map((i) => i.label));\n const toggle = (l) => setOpen(open.includes(l) ? open.filter((x) => x !== l) : [...open, l]);\n const pick = (l) => onSelect && onSelect(l);\n const list = (arr) => arr.map((it) => it.children ? (\n <div key={it.label} style={{ display: \"flex\", flexDirection: \"column\", gap: 2 }}>\n <NavItem icon={it.icon} label={it.label} collapsed={collapsed} trailing={false} active={collapsed && it.children.some((c) => (c.label || c) === active)} onClick={() => (collapsed ? pick(it.label) : toggle(it.label))} />\n {!collapsed && open.includes(it.label) ? <div style={{ marginLeft: 24, display: \"flex\", flexDirection: \"column\", gap: 2 }}>{it.children.map((c) => { const lb = c.label || c; return <NavItem key={lb} icon={c.icon} label={lb} indent active={active === lb} onClick={() => pick(lb)} />; })}</div> : null}\n </div>\n ) : <NavItem key={it.label} icon={it.icon} label={it.label} collapsed={collapsed} trailing={it.trailing} active={active === it.label} onClick={() => pick(it.label)} />);\n return (\n <aside data-theme={t.theme} style={{ ...t.vars, width: collapsed ? \"var(--rail-w)\" : \"var(--sidebar-w)\", height, background: \"var(--sb-bg)\", borderRight: \"1px solid var(--sb-border)\", color: \"var(--sb-text)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, transition: \"width .15s ease\", ...style }}>\n <div style={{ height: \"var(--topbar-h)\", 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\", flexShrink: 0 }} />\n {!collapsed ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", fontWeight: \"var(--weight-semibold)\", color: \"var(--sb-brand)\", whiteSpace: \"nowrap\" }}>{brand}</span> : null}\n </div>\n <nav style={{ flex: 1, overflowY: \"auto\", overflowX: \"hidden\", padding: 8, display: \"flex\", flexDirection: \"column\", gap: 2 }}>\n {onToggleCollapse ? <NavItem icon={collapsed ? \"panel-left\" : \"chevron-left\"} label={collapsed ? L.expand : L.collapse} collapsed={collapsed} onClick={onToggleCollapse} /> : null}\n {list(items)}\n </nav>\n <div style={{ padding: 8, display: \"flex\", flexDirection: \"column\", gap: 2, flexShrink: 0 }}>\n {list(footerItems)}\n {version ? <span title={version} style={{ padding: collapsed ? \"8px 0 4px\" : \"8px 10px 4px\", textAlign: collapsed ? \"center\" : \"left\", fontFamily: \"var(--font-sans)\", fontSize: collapsed ? \"var(--text-glyph-min)\" : \"var(--text-xs)\", lineHeight: collapsed ? \"var(--leading-2xs)\" : \"var(--leading-xs)\", color: \"var(--sb-muted)\", whiteSpace: \"nowrap\", overflow: \"hidden\" }}>{collapsed ? \"v\" + String(version).split(\" \").pop().replace(/^v/i, \"\") : version}</span> : null}\n </div>\n </aside>\n );\n}\n\n/** Nav models and tones — defined in framework-neutral modules, shared with the Vue entry. */\nexport { KEYBRICKS_NAV, KEYBRICKS_NAV_FOOTER, KEYBRICKS_MGMT, SIDEBAR_COLORS };\n","navigation/Sidebar.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport { KEYBRICKS_NAV, KEYBRICKS_NAV_FOOTER } from './nav-model.js'\nimport { chromeTone } from './chrome-tones.js'\n// Keybricks app sidebar — 240px, 64px icon rail when collapsed. 56px brand head (no rule), 2px-gap list, pinned footer, version line.\n// tone: \"recessed\" (grey, default) | \"flush\" (white) | \"contrast\" (dark) | \"custom\" (+ color: a SIDEBAR_COLORS key or hex).\n// items / footerItems: [{ icon, label, trailing?, children? }] — trailing draws a chevron (the item opens a SidePanel); children make a collapsible group.\nconst props = defineProps({ items: { type: Array, default: () => KEYBRICKS_NAV }, footerItems: { type: Array, default: () => KEYBRICKS_NAV_FOOTER }, active: String, collapsed: Boolean, logo: { type: String, default: 'assets/keybricks-logo.png' }, brand: { type: String, default: 'Keybricks' }, version: String, tone: { type: String, default: 'recessed' }, color: String, labels: Object, height: { type: String, default: '100%' }, showToggle: { type: Boolean, default: true }, style: Object })\nconst emit = defineEmits(['select', 'toggleCollapse'])\nconst L = computed(() => ({ collapse: 'Collapse', expand: 'Expand', ...(props.labels || {}) }))\nconst t = computed(() => chromeTone(props.tone, props.color))\nconst hover = ref(null)\nconst open = ref(props.items.filter(i => i.children && i.children.some(c => (c.label || c) === props.active)).map(i => i.label))\nconst toggle = (l) => { open.value = open.value.includes(l) ? open.value.filter(x => x !== l) : [...open.value, l] }\nconst lb = (c) => c.label || c\nconst LABEL = { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-base)', lineHeight: '18px', letterSpacing: 'var(--tracking-base)', fontWeight: 'var(--weight-medium)' }\nfunction row(key, active, indent) { return { minHeight: indent ? '32px' : '36px', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', justifyContent: props.collapsed ? 'center' : 'flex-start', gap: '10px', padding: indent ? '6px 10px' : '8px 10px', boxSizing: 'border-box', cursor: 'pointer', userSelect: 'none', background: active ? 'var(--sb-active)' : hover.value === key ? 'var(--sb-hover)' : 'transparent', color: active ? 'var(--sb-active-text)' : 'var(--sb-text)', boxShadow: active ? 'var(--sb-shadow)' : 'none', transition: 'var(--t-hover)' } }\nfunction key(e, fn) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); fn() } }\nconst groups = computed(() => [{ id: 'main', list: props.items }, { id: 'foot', list: props.footerItems }])\nconst versionText = computed(() => props.collapsed && props.version ? 'v' + String(props.version).split(' ').pop().replace(/^v/i, '') : props.version)\n</script>\n<template>\n <aside :data-theme=\"t.theme\" :style=\"{ ...t.vars, width: collapsed ? 'var(--rail-w)' : 'var(--sidebar-w)', height, background: 'var(--sb-bg)', borderRight: '1px solid var(--sb-border)', color: 'var(--sb-text)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, transition: 'width .15s ease', ...style }\">\n <div :style=\"{ height: 'var(--topbar-h)', display: 'flex', alignItems: 'center', gap: '10px', 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;flex-shrink:0\" />\n <span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);font-weight:var(--weight-semibold);color:var(--sb-brand);white-space:nowrap\">{{ brand }}</span>\n </div>\n <template v-for=\"g in groups\" :key=\"g.id\">\n <component :is=\"g.id === 'main' ? 'nav' : 'div'\" :style=\"g.id === 'main' ? { flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: '8px', display: 'flex', flexDirection: 'column', gap: '2px' } : { padding: '8px', display: 'flex', flexDirection: 'column', gap: '2px', flexShrink: 0 }\">\n <div v-if=\"g.id === 'main' && showToggle\" role=\"button\" tabindex=\"0\" :title=\"collapsed ? L.expand : undefined\" :style=\"row('__toggle', false)\" @click=\"emit('toggleCollapse')\" @keydown=\"e => key(e, () => emit('toggleCollapse'))\" @mouseenter=\"hover = '__toggle'\" @mouseleave=\"hover = null\">\n <span style=\"display:inline-flex;flex-shrink:0\"><Icon :name=\"collapsed ? 'panel-left' : 'chevron-left'\" :size=\"16\" /></span><span v-if=\"!collapsed\" :style=\"LABEL\">{{ collapsed ? L.expand : L.collapse }}</span>\n </div>\n <template v-for=\"it in g.list\" :key=\"it.label\">\n <div role=\"button\" tabindex=\"0\" :title=\"collapsed ? it.label : undefined\" :aria-current=\"active === it.label ? 'page' : undefined\" :style=\"row(it.label, active === it.label || (collapsed && it.children && it.children.some(c => lb(c) === active)))\" @click=\"it.children && !collapsed ? toggle(it.label) : emit('select', it.label)\" @keydown=\"e => key(e, () => it.children && !collapsed ? toggle(it.label) : emit('select', it.label))\" @mouseenter=\"hover = it.label\" @mouseleave=\"hover = null\">\n <span v-if=\"it.icon\" style=\"display:inline-flex;flex-shrink:0\"><Icon :name=\"it.icon\" :size=\"16\" /></span>\n <span v-if=\"!collapsed\" :style=\"LABEL\">{{ it.label }}</span>\n <span v-if=\"!collapsed && it.trailing\" style=\"display:inline-flex;flex-shrink:0;opacity:.6\"><Icon name=\"chevron-right\" :size=\"14\" /></span>\n </div>\n <div v-if=\"it.children && !collapsed && open.includes(it.label)\" style=\"margin-left:24px;display:flex;flex-direction:column;gap:2px\">\n <div v-for=\"c in it.children\" :key=\"lb(c)\" role=\"button\" tabindex=\"0\" :aria-current=\"active === lb(c) ? 'page' : undefined\" :style=\"row('c:' + lb(c), active === lb(c), true)\" @click=\"emit('select', lb(c))\" @keydown=\"e => key(e, () => emit('select', lb(c)))\" @mouseenter=\"hover = 'c:' + lb(c)\" @mouseleave=\"hover = null\">\n <span v-if=\"c.icon\" style=\"display:inline-flex;flex-shrink:0\"><Icon :name=\"c.icon\" :size=\"14\" /></span><span :style=\"LABEL\">{{ lb(c) }}</span>\n </div>\n </div>\n </template>\n <span v-if=\"g.id === 'foot' && version\" :title=\"version\" :style=\"{ padding: collapsed ? '8px 0 4px' : '8px 10px 4px', textAlign: collapsed ? 'center' : 'left', fontFamily: 'var(--font-sans)', fontSize: collapsed ? 'var(--text-glyph-min)' : 'var(--text-xs)', lineHeight: collapsed ? 'var(--leading-2xs)' : 'var(--leading-xs)', color: 'var(--sb-muted)', whiteSpace: 'nowrap', overflow: 'hidden' }\">{{ versionText }}</span>\n </component>\n </template>\n </aside>\n</template>\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\";\nimport { Avatar } from \"../display/Avatar.jsx\";\nimport { SegmentedControl } from \"../tabs/SegmentedControl.jsx\";\nimport { ContextMenu } from \"../feedback/ContextMenu.jsx\";\n\nconst TXT = { fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\" };\nconst ONE = { whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" };\nexport const THEME_OPTIONS = [{ label: \"Light\", icon: \"sun\" }, { label: \"Dark\", icon: \"moon\" }, { label: \"System\", icon: \"monitor\" }];\n\n/** Account menu (opened from the TopBar's user trigger): avatar + name + email, an Appearance row with a light / dark / system switch, then rows.\n * theme: \"Light\" | \"Dark\" | \"System\" (labels of themeOptions). items: [{ icon, label, danger?, divider? }] → onSelect(label). */\nexport function UserMenu({ name, email, theme, onThemeChange, themeOptions = THEME_OPTIONS, appearanceLabel = \"Appearance\", items = [], onSelect, style }) {\n return (\n <div role=\"menu\" style={{ width: 288, background: \"var(--bg-raised)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-lg)\", boxShadow: \"var(--shadow-menu)\", padding: 4, boxSizing: \"border-box\", ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 10, padding: \"8px 12px\" }}>\n <Avatar name={name} size={32} />\n <div style={{ display: \"flex\", flexDirection: \"column\", minWidth: 0 }}>\n <span style={{ ...TXT, ...ONE, fontWeight: \"var(--weight-medium)\", color: \"var(--text-heading)\" }}>{name}</span>\n {email ? <span style={{ ...ONE, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-secondary)\" }}>{email}</span> : null}\n </div>\n </div>\n <div style={{ height: 1, background: \"var(--border-subtle)\", margin: \"4px 0\" }} />\n {onThemeChange || theme ? <div style={{ height: 40, display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", gap: 12, padding: \"0 12px\", boxSizing: \"border-box\" }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 10, minWidth: 0 }}><span style={{ display: \"inline-flex\", color: \"var(--text-body)\" }}><Icon name=\"sliders\" size={16} /></span><span style={{ ...TXT, ...ONE, color: \"var(--text-heading)\" }}>{appearanceLabel}</span></div>\n <SegmentedControl iconOnly options={themeOptions} value={theme} onChange={onThemeChange} style={{ flexShrink: 0 }} />\n </div> : null}\n {items.length ? <ContextMenu items={items} onSelect={onSelect} style={{ display: \"flex\", border: \"none\", boxShadow: \"none\", padding: 0, minWidth: 0, background: \"transparent\" }} /> : null}\n </div>\n );\n}\n\n/** 56px top bar: bare search on the left; AI Assistant (dark), notifications, help and the user trigger on the right.\n * The user trigger opens a UserMenu below it (closes on outside click). actions replaces the right-hand group entirely. */\nexport function TopBar({ placeholder = \"Search documents, workflows, data...\", onSearch, actions, aiLabel = \"AI Assistant\", onAi, onNotifications, onHelp, notificationsLabel = \"Notifications\", helpLabel = \"Help\", user, userMenu, style }) {\n const [open, setOpen] = React.useState(false);\n const ref = React.useRef(null);\n React.useEffect(() => {\n if (!open) return;\n const off = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };\n const esc = (e) => { if (e.key === \"Escape\") setOpen(false); };\n document.addEventListener(\"mousedown\", off); document.addEventListener(\"keydown\", esc);\n return () => { document.removeEventListener(\"mousedown\", off); document.removeEventListener(\"keydown\", esc); };\n }, [open]);\n const [hover, setHover] = React.useState(false);\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\", gap: 16, padding: \"0 24px\", boxSizing: \"border-box\", flexShrink: 0, minWidth: 0, position: \"relative\", ...style }}>\n <SearchInput bare placeholder={placeholder} onChange={onSearch} style={{ flex: \"0 1 576px\", minWidth: 0 }} />\n <div ref={ref} style={{ display: \"flex\", alignItems: \"center\", gap: 8, flexShrink: 0 }}>\n {actions || (<>\n <Button variant=\"dark\" icon=\"ai\" onClick={onAi}>{aiLabel}</Button>\n <IconButton icon=\"bell\" label={notificationsLabel} title={notificationsLabel} onClick={onNotifications} />\n <IconButton icon=\"circle-help\" label={helpLabel} title={helpLabel} onClick={onHelp} />\n {user ? <div role=\"button\" tabIndex={0} aria-haspopup=\"menu\" aria-expanded={open} onClick={() => setOpen(!open)} onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); setOpen(!open); } }} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ height: 32, display: \"flex\", alignItems: \"center\", gap: 8, padding: \"0 8px 0 10px\", borderRadius: \"var(--radius-md)\", cursor: \"pointer\", userSelect: \"none\", background: open ? \"var(--bg-active)\" : hover ? \"var(--bg-hover)\" : \"transparent\", transition: \"var(--t-hover)\" }}>\n <span style={{ ...TXT, fontWeight: \"var(--weight-medium)\", color: \"var(--text-heading)\", whiteSpace: \"nowrap\" }}>{user.name}</span>\n <span style={{ display: \"inline-flex\", color: \"var(--text-muted)\" }}><Icon name=\"chevron-down\" size={16} /></span>\n </div> : null}\n </>)}\n {open && user ? <div style={{ position: \"absolute\", right: 24, top: 52, zIndex: \"var(--z-menu)\" }}>{userMenu || <UserMenu name={user.name} email={user.email} />}</div> : null}\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, size, style }) {\n return (\n <div data-size={size} style={{ height: \"var(--topbar-h)\", background: \"var(--bg-surface)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"0 var(--edge-pad)\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"var(--control-gap)\" }}>\n {onBack ? <IconButton icon=\"chevron-left\" onClick={onBack} label=\"Back\" /> : 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(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-muted)\", marginLeft: 6 }}>{status}</span> : null}\n </div>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"var(--control-gap)\" }}>{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 { ref, onMounted, onUnmounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport Button from '../buttons/Button.vue'\nimport SearchInput from '../forms/SearchInput.vue'\nimport UserMenu from './UserMenu.vue'\n// 56px top bar: bare search on the left; AI Assistant (dark), notifications, help and the user trigger on the right.\n// The user trigger opens a UserMenu below it (closes on outside click / Escape); #user-menu slot replaces its content.\n// #actions slot replaces the whole right-hand group.\nconst props = defineProps({ placeholder: { type: String, default: 'Search documents, workflows, data...' }, aiLabel: { type: String, default: 'AI Assistant' }, notificationsLabel: { type: String, default: 'Notifications' }, helpLabel: { type: String, default: 'Help' }, user: Object, style: Object })\nconst emit = defineEmits(['search', 'ai', 'notifications', 'help'])\nconst open = ref(false), hover = ref(false), root = ref(null)\nconst off = (e) => { if (open.value && root.value && !root.value.contains(e.target)) open.value = false }\nconst esc = (e) => { if (e.key === 'Escape') open.value = false }\nonMounted(() => { document.addEventListener('mousedown', off); document.addEventListener('keydown', esc) })\nonUnmounted(() => { document.removeEventListener('mousedown', off); document.removeEventListener('keydown', esc) })\nfunction key(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open.value = !open.value } }\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', gap: '16px', padding: '0 24px', boxSizing: 'border-box', flexShrink: 0, minWidth: 0, position: 'relative', ...style }\">\n <SearchInput bare :placeholder=\"placeholder\" @update:modelValue=\"v => emit('search', v)\" style=\"flex:0 1 576px;min-width:0\" />\n <div ref=\"root\" style=\"display:flex;align-items:center;gap:8px;flex-shrink:0\">\n <slot name=\"actions\">\n <Button variant=\"dark\" icon=\"ai\" @click=\"emit('ai')\">{{ aiLabel }}</Button>\n <IconButton icon=\"bell\" :label=\"notificationsLabel\" :title=\"notificationsLabel\" @click=\"emit('notifications')\" />\n <IconButton icon=\"circle-help\" :label=\"helpLabel\" :title=\"helpLabel\" @click=\"emit('help')\" />\n <div v-if=\"user\" role=\"button\" tabindex=\"0\" aria-haspopup=\"menu\" :aria-expanded=\"open\" @click=\"open = !open\" @keydown=\"key\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ height: '32px', display: 'flex', alignItems: 'center', gap: '8px', padding: '0 8px 0 10px', borderRadius: 'var(--radius-md)', cursor: 'pointer', userSelect: 'none', background: open ? 'var(--bg-active)' : hover ? 'var(--bg-hover)' : 'transparent', transition: 'var(--t-hover)' }\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);font-weight:var(--weight-medium);color:var(--text-heading);white-space:nowrap\">{{ user.name }}</span>\n <span style=\"display:inline-flex;color:var(--text-muted)\"><Icon name=\"chevron-down\" :size=\"16\" /></span>\n </div>\n </slot>\n <div v-if=\"open && user\" style=\"position:absolute;right:24px;top:52px;z-index:var(--z-menu)\"><slot name=\"user-menu\"><UserMenu :name=\"user.name\" :email=\"user.email\" :show-appearance=\"false\" /></slot></div>\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 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-base)\", lineHeight: \"var(--leading-base)\", 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-xs)\", 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-sans)\", fontVariantNumeric: \"tabular-nums\", fontSize: \"var(--text-xs)\", 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-base)\", 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 treeData: Array, // alias of items (same as Tree.jsx)\n expandedKeys: Array, selectedKeys: Array, checkedKeys: Array, // controlled: the parent's value wins whenever it changes\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.treeData || props.items)))\n// Compare by value: re-seeding on identity alone would wipe async-loaded children.\nwatch(() => JSON.stringify(props.treeData || 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])\nwatch(() => props.expandedKeys, (v) => { if (v) expanded.value = [...v] }, { immediate: true })\nwatch(() => props.selectedKeys, (v) => { if (v) selected.value = [...v] }, { immediate: true })\nwatch(() => props.checkedKeys, (v) => { if (v) checked.value = [...v] }, { immediate: true })\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 :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-base)', lineHeight: 'var(--leading-base)', 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-xs);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-sans);font-variant-numeric:tabular-nums;font-size:var(--text-xs);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-base);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-border);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: '4px', ...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'\n// Page toolbar under the top bar: back · title · status, actions on the right. A size region (default inherit → md).\ndefineProps({ title: String, status: { type: String, default: '• Editing' }, showBack: Boolean, size: String, style: Object })\nconst emit = defineEmits(['back'])\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 var(--edge-pad)', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"display:flex;align-items:center;gap:var(--control-gap)\">\n <IconButton v-if=\"showBack\" icon=\"chevron-left\" label=\"Back\" @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(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-muted);margin-left:6px\">{{ status }}</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:var(--control-gap)\"><slot name=\"actions\" /></div>\n </div>\n</template>\n","navigation/ThemeToggle.vue":"<script setup>\nimport { px } from '../px.js'\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: px(size), height: px(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","navigation/SidePanel.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { chromeTone } from \"./chrome-tones.js\";\n\n/**\n * Side panel docked beside the Sidebar (Documents, Management). 240px, same tone as the rail, opens by growing from 0\n * (--dur-slow, --ease-out) while its content fades in from 12px left. 56px header: title + actions + close.\n * Body scrolls; put a SegmentedControl, SearchInput, Tree or rows inside. size=\"sm\" makes its controls compact.\n */\nexport function SidePanel({ open = true, title, actions, onClose, closeLabel = \"Close\", closeIcon = \"x\", tone = \"recessed\", color, width = 240, size, children, style }) {\n const t = chromeTone(tone, color);\n return (\n <div aria-hidden={open ? undefined : \"true\"} style={{ width: open ? width : 0, flexShrink: 0, overflow: \"hidden\", height: \"100%\", transition: \"width var(--dur-slow) var(--ease-out)\" }}>\n <div data-theme={t.theme} data-size={size} style={{ ...t.vars, width, height: \"100%\", background: \"var(--sb-bg)\", borderRight: \"1px solid var(--sb-border)\", color: \"var(--sb-text)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", opacity: open ? 1 : 0, transform: open ? \"none\" : \"translateX(-12px)\", transition: \"opacity var(--dur) var(--ease-out), transform var(--dur-slow) var(--ease-out)\", pointerEvents: open ? \"auto\" : \"none\", ...style }}>\n <div style={{ height: \"var(--topbar-h)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", gap: 8, padding: \"0 8px 0 16px\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <span style={{ minWidth: 0, overflow: \"hidden\", textOverflow: \"ellipsis\", whiteSpace: \"nowrap\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", fontWeight: \"var(--weight-medium)\", color: \"var(--text-heading)\" }}>{title}</span>\n <div data-size=\"md\" style={{ display: \"flex\", alignItems: \"center\", gap: 2, flexShrink: 0 }}>{actions}{onClose ? <IconButton icon={closeIcon} muted label={closeLabel} title={closeLabel} onClick={onClose} /> : null}</div>\n </div>\n <div style={{ flex: 1, minHeight: 0, overflow: \"auto\", display: \"flex\", flexDirection: \"column\" }}>{children}</div>\n </div>\n </div>\n );\n}\n","navigation/SidePanel.vue":"<script setup>\nimport { computed } from 'vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport { chromeTone } from './chrome-tones.js'\n// Side panel docked beside the Sidebar (Documents, Management). 240px, same tone as the rail; opens by growing from 0 while\n// its content fades in from 12px left. 56px header: title + #actions slot + close (@close). Default slot is the scrolling body.\nconst props = defineProps({ open: { type: Boolean, default: true }, title: String, closable: { type: Boolean, default: true }, closeLabel: { type: String, default: 'Close' }, closeIcon: { type: String, default: 'x' }, tone: { type: String, default: 'recessed' }, color: String, width: { type: Number, default: 240 }, size: String, style: Object })\nconst emit = defineEmits(['close'])\nconst t = computed(() => chromeTone(props.tone, props.color))\n</script>\n<template>\n <div :aria-hidden=\"open ? undefined : 'true'\" :style=\"{ width: (open ? width : 0) + 'px', flexShrink: 0, overflow: 'hidden', height: '100%', transition: 'width var(--dur-slow) var(--ease-out)' }\">\n <div :data-theme=\"t.theme\" :data-size=\"size\" :style=\"{ ...t.vars, width: width + 'px', height: '100%', background: 'var(--sb-bg)', borderRight: '1px solid var(--sb-border)', color: 'var(--sb-text)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', opacity: open ? 1 : 0, transform: open ? 'none' : 'translateX(-12px)', transition: 'opacity var(--dur) var(--ease-out), transform var(--dur-slow) var(--ease-out)', pointerEvents: open ? 'auto' : 'none', ...style }\">\n <div style=\"height:var(--topbar-h);display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 8px 0 16px;box-sizing:border-box;flex-shrink:0\">\n <span style=\"min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);font-weight:var(--weight-medium);color:var(--text-heading)\">{{ title }}</span>\n <div data-size=\"md\" style=\"display:flex;align-items:center;gap:2px;flex-shrink:0\"><slot name=\"actions\" /><IconButton v-if=\"closable\" :icon=\"closeIcon\" muted :label=\"closeLabel\" :title=\"closeLabel\" @click=\"emit('close')\" /></div>\n </div>\n <div style=\"flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column\"><slot /></div>\n </div>\n </div>\n</template>\n","navigation/UserMenu.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\nimport Avatar from '../display/Avatar.vue'\nimport SegmentedControl from '../tabs/SegmentedControl.vue'\nimport ContextMenu from '../feedback/ContextMenu.vue'\n// Account menu (opened from the TopBar's user trigger): avatar + name + email, an Appearance row with a light / dark / system switch\n// (v-model:theme, labels of themeOptions), then rows (items → @select(label)).\ndefineProps({ name: String, email: String, theme: String, themeOptions: { type: Array, default: () => [{ label: 'Light', icon: 'sun' }, { label: 'Dark', icon: 'moon' }, { label: 'System', icon: 'monitor' }] }, appearanceLabel: { type: String, default: 'Appearance' }, showAppearance: { type: Boolean, default: true }, items: { type: Array, default: () => [] }, style: Object })\nconst emit = defineEmits(['update:theme', 'select'])\n</script>\n<template>\n <div role=\"menu\" :style=\"{ width: '288px', background: 'var(--bg-raised)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-menu)', padding: '4px', boxSizing: 'border-box', ...style }\">\n <div style=\"display:flex;align-items:center;gap:10px;padding:8px 12px\">\n <Avatar :name=\"name\" :size=\"32\" />\n <div style=\"display:flex;flex-direction:column;min-width:0\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);font-weight:var(--weight-medium);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ name }}</span>\n <span v-if=\"email\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ email }}</span>\n </div>\n </div>\n <div style=\"height:1px;background:var(--border-subtle);margin:4px 0\"></div>\n <div v-if=\"showAppearance\" style=\"height:40px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 12px;box-sizing:border-box\">\n <div style=\"display:flex;align-items:center;gap:10px;min-width:0\"><span style=\"display:inline-flex;color:var(--text-body)\"><Icon name=\"sliders\" :size=\"16\" /></span><span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ appearanceLabel }}</span></div>\n <SegmentedControl icon-only :options=\"themeOptions\" :model-value=\"theme\" @update:model-value=\"v => emit('update:theme', v)\" style=\"flex-shrink:0\" />\n </div>\n <ContextMenu v-if=\"items.length\" :items=\"items\" @select=\"l => emit('select', l)\" :style=\"{ display: 'flex', border: 'none', boxShadow: 'none', padding: 0, minWidth: 0, background: 'transparent' }\" />\n </div>\n</template>\n","navigation/chrome-tones.js":"// App chrome tones — framework-neutral, shared by Sidebar and SidePanel in React and Vue.\n// recessed (default): grey rail, the active item is a white tile with a hairline ring.\n// flush: white rail, neutral selected fill. contrast: the rail is scoped to the dark theme.\n// custom: dark scope on a solid colour (SIDEBAR_COLORS or any hex); washes are white at 10 / 16 %.\n// chromeTone() returns the data-theme for the rail and the CSS custom properties it sets on itself:\n// --sb-* drive the rail's own surfaces; --bg-hover / --bg-active / … are remapped so components inside\n// (SearchInput, SegmentedControl, Tree) sit correctly on the rail colour.\nexport const SIDEBAR_COLORS = { slate: '#1e293b', navy: '#14305c', indigo: '#312e81', violet: '#4c1d95', plum: '#701a75', wine: '#7f1d1d', forest: '#14532d', teal: '#134e4a', umber: '#78350f', charcoal: '#262626' }\nconst wash = (p) => 'color-mix(in srgb, var(--on-accent) ' + p + '%, transparent)'\nconst BASE = { '--sb-border': 'var(--border-card)', '--sb-text': 'var(--text-body)', '--sb-brand': 'var(--text-primary)', '--sb-active-text': 'var(--text-heading)', '--sb-muted': 'var(--text-muted)' }\nconst GREY = { ...BASE, '--sb-bg': 'var(--bg-table-header)', '--sb-active': 'var(--bg-surface)', '--sb-hover': 'var(--bg-press)', '--sb-shadow': '0 0 0 1px var(--border-card)', '--bg-hover': 'var(--bg-press)', '--bg-active': 'var(--bg-surface)', '--bg-inset': 'var(--overlay-hover)' }\nconst WHITE = { ...BASE, '--sb-bg': 'var(--bg-surface)', '--sb-active': 'var(--bg-active)', '--sb-hover': 'var(--bg-hover)', '--sb-shadow': 'none' }\nexport function chromeTone(tone, color) {\n const t = tone || 'recessed'\n if (t === 'flush') return { theme: undefined, vars: WHITE }\n if (t === 'contrast') return { theme: 'dark', vars: GREY }\n if (t === 'custom') {\n const bg = SIDEBAR_COLORS[color] || color || SIDEBAR_COLORS.slate\n return { theme: 'dark', vars: { ...GREY, '--sb-bg': bg, '--sb-border': wash(12), '--sb-active': wash(16), '--sb-hover': wash(10), '--sb-shadow': 'none', '--bg-hover': wash(10), '--bg-press': wash(16), '--bg-active': wash(16), '--bg-inset': wash(10), '--border-control': wash(16) } }\n }\n return { theme: undefined, vars: GREY }\n}\n","navigation/nav-model.js":"// The product's nav model. Framework-neutral: re-exported by the React entry and exported from the Vue entry.\n// KEYBRICKS_NAV — the main rail; trailing: true marks an item that opens a side panel (chevron on the right).\n// KEYBRICKS_NAV_FOOTER — pinned at the bottom of the rail. KEYBRICKS_MGMT — the Management side panel's groups.\nexport const KEYBRICKS_NAV = [\n { icon: 'house', label: 'Home' },\n { icon: 'file-text', label: 'Documents', trailing: true },\n { icon: 'journal', label: 'Voucher' },\n { icon: 'credit-card', label: 'Fund Payment Plan' },\n { icon: 'database', label: 'Data Integration' },\n { icon: 'git-branch', label: 'Data Flow' },\n { icon: 'reconciliation', label: 'Reconciliation' },\n { icon: 'consolidation', label: 'Consolidation' },\n { icon: 'task-execution', label: 'Task Execution' },\n]\nexport const KEYBRICKS_NAV_FOOTER = [\n { icon: 'settings', label: 'Management', trailing: true },\n { icon: 'book-open', label: 'Knowledge Base' },\n]\nexport const KEYBRICKS_MGMT = [\n { icon: 'box', label: 'Modeling', children: ['Dimension', 'Model', 'Environment', 'Journal Template', 'Detail Template', 'Relationship'] },\n { icon: 'repeat', label: 'Workflow', children: ['Workflow', 'Requests', 'Task Tracker'] },\n { icon: 'shield', label: 'Security Management', children: ['Users', 'Roles', 'Permissions'] },\n { icon: 'refresh-cw', label: 'System Migration' },\n { icon: 'circle-check', label: 'Validation Rules' },\n { icon: 'settings-2', label: 'Control Center' },\n]\n"});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"tabs/SegmentedControl.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/**\n * Segmented control — inset track, raised active segment. Switches views of the same data, applies instantly.\n * options: string[] | { label, icon?, disabled? }[] value/onChange by label\n * size sm 28 | md 32 | lg 40 — omit it to inherit the region's tier · iconOnly hides labels (label becomes aria-label / title)\n * disabled greys the whole control · fullWidth stretches segments evenly\n * Keyboard: ← → move between enabled segments.\n */\nconst tierIcon = { width: \"var(--control-icon)\", height: \"var(--control-icon)\" };\nexport function SegmentedControl({ options = [], value, onChange, size, iconOnly, disabled, fullWidth, style }) {\n const opts = options.map((o) => (typeof o === \"string\" ? { label: o } : o));\n const enabled = opts.filter((o) => !o.disabled);\n const onKey = (e) => {\n if (e.key !== \"ArrowLeft\" && e.key !== \"ArrowRight\" || disabled || !enabled.length) return;\n e.preventDefault();\n const i = enabled.findIndex((o) => o.label === value);\n const n = e.key === \"ArrowRight\" ? enabled[(i + 1) % enabled.length] : enabled[(i - 1 + enabled.length) % enabled.length];\n onChange && onChange(n.label);\n };\n return (\n <div role=\"tablist\" data-size={size} aria-disabled={disabled || undefined} onKeyDown={onKey} style={{ display: fullWidth ? \"flex\" : \"inline-flex\", height: \"var(--control-h)\", padding: 2, gap: 2, background: \"var(--bg-inset)\", border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-md)\", boxSizing: \"border-box\", opacity: disabled ? .5 : 1, ...style }}>\n {opts.map((o) => {\n const is = value === o.label, off = disabled || o.disabled;\n return (\n <button key={o.label} type=\"button\" role=\"tab\" aria-selected={is} aria-label={iconOnly ? o.label : undefined} title={iconOnly ? o.label : undefined} disabled={off} tabIndex={is ? 0 : -1} data-ink={is || off ? undefined : \"\"}\n onClick={() => !off && onChange && onChange(o.label)}\n style={{ height: \"calc(var(--control-h) - 6px)\", padding: iconOnly ? \"0 calc(var(--control-px) - 4px)\" : \"0 calc(var(--control-px) - 2px)\", flex: fullWidth ? 1 : \"none\", justifyContent: \"center\", display: \"inline-flex\", alignItems: \"center\", gap: 6, borderRadius: \"var(--radius-sm)\", border: \"none\", cursor: off ? \"not-allowed\" : \"pointer\", background: is ? \"var(--bg-surface)\" : \"transparent\", boxShadow: is ? \"0 1px 2px rgba(0,0,0,.08)\" : \"none\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: is ? \"var(--text-heading)\" : \"var(--ink, var(--text-secondary))\", opacity: o.disabled && !disabled ? .45 : 1, whiteSpace: \"nowrap\", transition: \"var(--t-hover)\" }}>\n {o.icon ? <Icon name={o.icon} size={16} style={tierIcon} /> : null}{iconOnly ? null : o.label}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/SegmentedControl.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Segmented control — inset track, raised active segment. Switches views of the same data, applies instantly.\n// options: string[] | { label, icon?, disabled? }[] · v-model by label · size sm 28 | md 32 | lg 40 — omit it to inherit the region's tier\n// iconOnly hides labels (label → aria-label/title) · disabled greys the control · fullWidth stretches segments · ← → keyboard.\nconst tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, size: String, iconOnly: Boolean, disabled: Boolean, fullWidth: Boolean, style: Object })\nconst emit = defineEmits(['update:modelValue'])\nconst opts = computed(() => props.options.map(o => typeof o === 'string' ? { label: o } : o))\nconst enabled = computed(() => opts.value.filter(o => !o.disabled))\nconst pick = (o) => { if (!(props.disabled || o.disabled)) emit('update:modelValue', o.label) }\nfunction onKey(e) {\n if ((e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') || props.disabled || !enabled.value.length) return\n e.preventDefault()\n const i = enabled.value.findIndex(o => o.label === props.modelValue), n = enabled.value.length\n emit('update:modelValue', (e.key === 'ArrowRight' ? enabled.value[(i + 1) % n] : enabled.value[(i - 1 + n) % n]).label)\n}\n</script>\n<template>\n <div role=\"tablist\" :data-size=\"size\" :aria-disabled=\"disabled || undefined\" @keydown=\"onKey\" :style=\"{ display: fullWidth ? 'flex' : 'inline-flex', height: 'var(--control-h)', padding: '2px', gap: '2px', background: 'var(--bg-inset)', border: '1px solid var(--border-control)', borderRadius: 'var(--radius-md)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...style }\">\n <button v-for=\"o in opts\" :key=\"o.label\" type=\"button\" role=\"tab\" :aria-selected=\"modelValue === o.label\" :aria-label=\"iconOnly ? o.label : undefined\" :title=\"iconOnly ? o.label : undefined\" :disabled=\"disabled || o.disabled\" :tabindex=\"modelValue === o.label ? 0 : -1\" :data-ink=\"modelValue === o.label || disabled || o.disabled ? undefined : ''\" @click=\"pick(o)\"\n :style=\"{ height: 'calc(var(--control-h) - 6px)', padding: iconOnly ? '0 calc(var(--control-px) - 4px)' : '0 calc(var(--control-px) - 2px)', flex: fullWidth ? 1 : 'none', justifyContent: 'center', display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: (disabled || o.disabled) ? 'not-allowed' : 'pointer', background: modelValue === o.label ? 'var(--bg-surface)' : 'transparent', boxShadow: modelValue === o.label ? '0 1px 2px rgba(0,0,0,.08)' : 'none', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: modelValue === o.label ? 'var(--text-heading)' : 'var(--ink, var(--text-secondary))', opacity: o.disabled && !disabled ? .45 : 1, whiteSpace: 'nowrap', transition: 'var(--t-hover)' }\">\n <Icon v-if=\"o.icon\" :name=\"o.icon\" :size=\"16\" :style=\"tierIcon\" /><template v-if=\"!iconOnly\">{{ o.label }}</template>\n </button>\n </div>\n</template>\n","tabs/Tabs.jsx":"import React from \"react\";\n\n/** Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline. The bar is the region's bar height (sm 32 · md 40 · lg 44) with the tier's control text; omit `size` to inherit. */\nexport function Tabs({ tabs = [], active, onChange, size, style }) {\n return (\n <div role=\"tablist\" data-size={size} style={{ display: \"flex\", gap: 4, borderBottom: \"1px solid var(--border-control)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" role=\"tab\" aria-selected={is} data-ink={is ? undefined : \"\"} onClick={() => onChange && onChange(t)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", height: \"var(--bar-h)\", boxSizing: \"border-box\", padding: \"0 var(--control-px)\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: is ? \"var(--action-primary)\" : \"var(--ink, var(--text-body))\", whiteSpace: \"nowrap\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n\n/** Panel tabs (Node Info · Calc Rules · Output Fields · SQL Preview): a lighter underline tab bar at the region's bar height with regular-weight control text. Put the panel in an sm region for the compact 32px/12px look. */\nexport function PanelTabs({ tabs = [], active, onChange, size, style }) {\n return (\n <div role=\"tablist\" data-size={size} style={{ display: \"flex\", borderBottom: \"1px solid var(--border-subtle)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" role=\"tab\" aria-selected={is} data-ink={is ? undefined : \"\"} onClick={() => onChange && onChange(t)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", height: \"var(--bar-h)\", boxSizing: \"border-box\", padding: \"0 var(--control-px)\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: is ? \"var(--action-primary)\" : \"var(--ink, var(--text-secondary))\", whiteSpace: \"nowrap\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n\n/** Text pill filter tabs (For Review · To be Submitted · For Approval). Active = tinted pill. Pills are control-height (sm 28 · md 32 · lg 40); omit `size` to inherit. */\nexport function PillTabs({ tabs = [], active, onChange, size, style }) {\n return (\n <div role=\"tablist\" data-size={size} style={{ display: \"flex\", gap: \"var(--control-gap)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" role=\"tab\" aria-selected={is} data-ink={is ? undefined : \"\"} data-wash={is ? undefined : \"\"} onClick={() => onChange && onChange(t)}\n style={{ background: is ? \"var(--bg-active)\" : \"var(--wash, transparent)\", border: \"none\", cursor: \"pointer\", height: \"var(--control-h)\", padding: \"0 var(--control-px)\", borderRadius: \"var(--radius-md)\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: is ? \"var(--text-heading)\" : \"var(--ink, var(--text-secondary))\", whiteSpace: \"nowrap\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/Tabs.vue":"<script setup>\n// Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline. The bar is the region's bar height (sm 32 · md 40 · lg 44) with the tier's control text; omit `size` to inherit.\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, size: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div role=\"tablist\" :data-size=\"size\" :style=\"{ display: 'flex', gap: '4px', borderBottom: '1px solid var(--border-control)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" role=\"tab\" :aria-selected=\"t === active\" :data-ink=\"t === active ? undefined : ''\" @click=\"emit('change', t)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', height: 'var(--bar-h)', boxSizing: 'border-box', padding: '0 var(--control-px)', marginBottom: '-1px', borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t === active ? 'var(--action-primary)' : 'var(--ink, var(--text-body))', whiteSpace: 'nowrap' }\">{{ t }}</button>\n </div>\n</template>\n","tabs/ViewToggle.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Two-state list/grid icon toggle (palette header). Two IconButtons at the region's tier; omit `size` to inherit. */\nexport function ViewToggle({ value = \"grid\", onChange, size, style }) {\n return (\n <div data-size={size} style={{ display: \"inline-flex\", gap: 2, ...style }}>\n <IconButton icon=\"list\" active={value === \"list\"} onClick={() => onChange && onChange(\"list\")} label=\"List view\" />\n <IconButton icon=\"layout-grid\" active={value === \"grid\"} onClick={() => onChange && onChange(\"grid\")} label=\"Grid view\" />\n </div>\n );\n}\n","tabs/ViewToggle.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\n// Two-state list/grid icon toggle (palette header). Two IconButtons at the region's tier; omit `size` to inherit.\ndefineProps({ modelValue: { type: String, default: 'grid' }, size: String, style: Object })\nconst emit = defineEmits(['update:modelValue'])\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ display: 'inline-flex', gap: '2px', ...style }\">\n <IconButton icon=\"list\" :active=\"modelValue === 'list'\" label=\"List view\" @click=\"emit('update:modelValue', 'list')\" />\n <IconButton icon=\"layout-grid\" :active=\"modelValue === 'grid'\" label=\"Grid view\" @click=\"emit('update:modelValue', 'grid')\" />\n </div>\n</template>\n","tabs/PanelTabs.vue":"<script setup>\n// Panel tabs (Node Info · Calc Rules · …): a lighter underline tab bar at the region's bar height with regular-weight control text. Put the panel in an sm region for the compact 32px/12px look.\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, size: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div role=\"tablist\" :data-size=\"size\" :style=\"{ display: 'flex', borderBottom: '1px solid var(--border-subtle)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" role=\"tab\" :aria-selected=\"t === active\" :data-ink=\"t === active ? undefined : ''\" @click=\"emit('change', t)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', height: 'var(--bar-h)', boxSizing: 'border-box', padding: '0 var(--control-px)', marginBottom: '-1px', borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t === active ? 'var(--action-primary)' : 'var(--ink, var(--text-secondary))', whiteSpace: 'nowrap' }\">{{ t }}</button>\n </div>\n</template>\n","tabs/PillTabs.vue":"<script setup>\n// Text pill filter tabs. Active = tinted pill. Pills are control-height (sm 28 · md 32 · lg 40); omit `size` to inherit.\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, size: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div role=\"tablist\" :data-size=\"size\" :style=\"{ display: 'flex', gap: 'var(--control-gap)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" role=\"tab\" :aria-selected=\"t === active\" :data-ink=\"t === active ? undefined : ''\" :data-wash=\"t === active ? undefined : ''\" @click=\"emit('change', t)\"\n :style=\"{ background: t === active ? 'var(--bg-active)' : 'var(--wash, transparent)', border: 'none', cursor: 'pointer', height: 'var(--control-h)', padding: '0 var(--control-px)', borderRadius: 'var(--radius-md)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t === active ? 'var(--text-heading)' : 'var(--ink, var(--text-secondary))', whiteSpace: 'nowrap' }\">{{ t }}</button>\n </div>\n</template>\n"});
|
|
1
|
+
window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"tabs/SegmentedControl.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { CountBadge } from \"./Tabs.jsx\";\n\n/**\n * Segmented control — inset track, raised active segment. Switches views of the same data, applies instantly.\n * options: string[] | { label, icon?, disabled?, count?, showZero? }[] value/onChange by label\n * count draws a CountBadge 6px right of the label (hidden for 0 unless showZero — per option or on the control); the match stays by label.\n * size sm 28 | md 32 | lg 40 — omit it to inherit the region's tier · iconOnly hides labels (label becomes aria-label / title)\n * disabled greys the whole control · fullWidth stretches segments evenly\n * Keyboard: ← → move between enabled segments.\n */\nconst tierIcon = { width: \"var(--control-icon)\", height: \"var(--control-icon)\" };\nexport function SegmentedControl({ options = [], value, onChange, size, iconOnly, disabled, fullWidth, showZero, style }) {\n const opts = options.map((o) => (typeof o === \"string\" ? { label: o } : o));\n const enabled = opts.filter((o) => !o.disabled);\n const onKey = (e) => {\n if (e.key !== \"ArrowLeft\" && e.key !== \"ArrowRight\" || disabled || !enabled.length) return;\n e.preventDefault();\n const i = enabled.findIndex((o) => o.label === value);\n const n = e.key === \"ArrowRight\" ? enabled[(i + 1) % enabled.length] : enabled[(i - 1 + enabled.length) % enabled.length];\n onChange && onChange(n.label);\n };\n return (\n <div role=\"tablist\" data-size={size} aria-disabled={disabled || undefined} onKeyDown={onKey} style={{ display: fullWidth ? \"flex\" : \"inline-flex\", height: \"var(--control-h)\", padding: 2, gap: 2, background: \"var(--bg-inset)\", border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-md)\", boxSizing: \"border-box\", opacity: disabled ? .5 : 1, ...style }}>\n {opts.map((o) => {\n const is = value === o.label, off = disabled || o.disabled;\n return (\n <button key={o.label} type=\"button\" role=\"tab\" aria-selected={is} aria-label={iconOnly ? o.label : undefined} title={iconOnly ? o.label : undefined} disabled={off} tabIndex={is ? 0 : -1} data-ink={is || off ? undefined : \"\"}\n onClick={() => !off && onChange && onChange(o.label)}\n style={{ height: \"calc(var(--control-h) - 6px)\", padding: iconOnly ? \"0 calc(var(--control-px) - 4px)\" : \"0 calc(var(--control-px) - 2px)\", flex: fullWidth ? 1 : \"none\", justifyContent: \"center\", display: \"inline-flex\", alignItems: \"center\", gap: 6, borderRadius: \"var(--radius-sm)\", border: \"none\", cursor: off ? \"not-allowed\" : \"pointer\", background: is ? \"var(--bg-surface)\" : \"transparent\", boxShadow: is ? \"0 1px 2px rgba(0,0,0,.08)\" : \"none\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: is ? \"var(--text-heading)\" : \"var(--ink, var(--text-secondary))\", opacity: o.disabled && !disabled ? .45 : 1, whiteSpace: \"nowrap\", transition: \"var(--t-hover)\" }}>\n {o.icon ? <Icon name={o.icon} size={16} style={tierIcon} /> : null}{iconOnly ? null : o.label}{iconOnly ? null : <CountBadge count={o.count} selected={is} showZero={o.showZero ?? showZero} />}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/SegmentedControl.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport CountBadge from './CountBadge.vue'\n// Segmented control — inset track, raised active segment. Switches views of the same data, applies instantly.\n// options: string[] | { label, icon?, disabled?, count?, showZero? }[] · v-model by label · count draws a CountBadge 6px right of the label (hidden for 0 unless showZero) · size sm 28 | md 32 | lg 40 — omit it to inherit the region's tier\n// iconOnly hides labels (label → aria-label/title) · disabled greys the control · fullWidth stretches segments · ← → keyboard.\nconst tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, size: String, iconOnly: Boolean, disabled: Boolean, fullWidth: Boolean, showZero: Boolean, style: Object })\nconst emit = defineEmits(['update:modelValue'])\nconst opts = computed(() => props.options.map(o => typeof o === 'string' ? { label: o } : o))\nconst enabled = computed(() => opts.value.filter(o => !o.disabled))\nconst pick = (o) => { if (!(props.disabled || o.disabled)) emit('update:modelValue', o.label) }\nfunction onKey(e) {\n if ((e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') || props.disabled || !enabled.value.length) return\n e.preventDefault()\n const i = enabled.value.findIndex(o => o.label === props.modelValue), n = enabled.value.length\n emit('update:modelValue', (e.key === 'ArrowRight' ? enabled.value[(i + 1) % n] : enabled.value[(i - 1 + n) % n]).label)\n}\n</script>\n<template>\n <div role=\"tablist\" :data-size=\"size\" :aria-disabled=\"disabled || undefined\" @keydown=\"onKey\" :style=\"{ display: fullWidth ? 'flex' : 'inline-flex', height: 'var(--control-h)', padding: '2px', gap: '2px', background: 'var(--bg-inset)', border: '1px solid var(--border-control)', borderRadius: 'var(--radius-md)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...style }\">\n <button v-for=\"o in opts\" :key=\"o.label\" type=\"button\" role=\"tab\" :aria-selected=\"modelValue === o.label\" :aria-label=\"iconOnly ? o.label : undefined\" :title=\"iconOnly ? o.label : undefined\" :disabled=\"disabled || o.disabled\" :tabindex=\"modelValue === o.label ? 0 : -1\" :data-ink=\"modelValue === o.label || disabled || o.disabled ? undefined : ''\" @click=\"pick(o)\"\n :style=\"{ height: 'calc(var(--control-h) - 6px)', padding: iconOnly ? '0 calc(var(--control-px) - 4px)' : '0 calc(var(--control-px) - 2px)', flex: fullWidth ? 1 : 'none', justifyContent: 'center', display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: (disabled || o.disabled) ? 'not-allowed' : 'pointer', background: modelValue === o.label ? 'var(--bg-surface)' : 'transparent', boxShadow: modelValue === o.label ? '0 1px 2px rgba(0,0,0,.08)' : 'none', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: modelValue === o.label ? 'var(--text-heading)' : 'var(--ink, var(--text-secondary))', opacity: o.disabled && !disabled ? .45 : 1, whiteSpace: 'nowrap', transition: 'var(--t-hover)' }\">\n <Icon v-if=\"o.icon\" :name=\"o.icon\" :size=\"16\" :style=\"tierIcon\" /><template v-if=\"!iconOnly\">{{ o.label }}<CountBadge :count=\"o.count\" :selected=\"modelValue === o.label\" :show-zero=\"o.showZero ?? showZero\" /></template>\n </button>\n </div>\n</template>\n","tabs/Tabs.jsx":"import React from \"react\";\n\n/* tabs: string[] | { label, count?, showZero? }[] — shared by Tabs, PanelTabs and PillTabs; active is matched by label. */\nconst norm = (tabs) => tabs.map((t) => (typeof t === \"string\" ? { label: t } : t));\n\n/** Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline. The bar is the region's bar height (sm 32 · md 40 · lg 44) with the tier's control text; omit `size` to inherit.\n * A tab's count renders as a CountBadge 6px right of the label (the badge stays neutral under the blue active tab). */\nexport function Tabs({ tabs = [], active, onChange, size, showZero, style }) {\n return (\n <div role=\"tablist\" data-size={size} style={{ display: \"flex\", gap: 4, borderBottom: \"1px solid var(--border-control)\", ...style }}>\n {norm(tabs).map((t) => {\n const is = t.label === active;\n return (\n <button key={t.label} type=\"button\" role=\"tab\" aria-selected={is} data-ink={is ? undefined : \"\"} onClick={() => onChange && onChange(t.label)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", height: \"var(--bar-h)\", boxSizing: \"border-box\", padding: \"0 var(--control-px)\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", display: \"inline-flex\", alignItems: \"center\", gap: 6, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: is ? \"var(--action-primary)\" : \"var(--ink, var(--text-body))\", whiteSpace: \"nowrap\" }}>\n {t.label}<CountBadge count={t.count} selected={is} showZero={t.showZero ?? showZero} />\n </button>\n );\n })}\n </div>\n );\n}\n\n/** Panel tabs (Node Info · Calc Rules · Output Fields · SQL Preview): a lighter underline tab bar at the region's bar height with regular-weight control text. Put the panel in an sm region for the compact 32px/12px look. */\nexport function PanelTabs({ tabs = [], active, onChange, size, showZero, style }) {\n return (\n <div role=\"tablist\" data-size={size} style={{ display: \"flex\", borderBottom: \"1px solid var(--border-subtle)\", ...style }}>\n {norm(tabs).map((t) => {\n const is = t.label === active;\n return (\n <button key={t.label} type=\"button\" role=\"tab\" aria-selected={is} data-ink={is ? undefined : \"\"} onClick={() => onChange && onChange(t.label)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", height: \"var(--bar-h)\", boxSizing: \"border-box\", padding: \"0 var(--control-px)\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", display: \"inline-flex\", alignItems: \"center\", gap: 6, fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: is ? \"var(--action-primary)\" : \"var(--ink, var(--text-secondary))\", whiteSpace: \"nowrap\" }}>\n {t.label}<CountBadge count={t.count} selected={is} showZero={t.showZero ?? showZero} />\n </button>\n );\n })}\n </div>\n );\n}\n\n\n/* Count badge beside a segment / pill label: --count-h tall (sm 16 · md 18 · lg 20), 6px sides, full radius, 12px tabular figures\n (digits are Latin, so the size is not bumped in CJK). The fill is the alpha wash --neutral-alpha-8, so it darkens whatever it sits on by the\n same step (white tab, #f5f5f5 active pill, dark theme); the text tells selected from unselected: --text-heading vs --text-body\n (a lighter grey fails 4.5:1 on a grey fill at 12px). Hidden for 0 or a missing count unless showZero, which draws a muted 0. */\nexport function CountBadge({ count, selected, showZero, style }) {\n const zero = count === 0 || count == null;\n if (zero && !showZero) return null;\n return <span aria-hidden=\"true\" style={{ display: \"inline-flex\", alignItems: \"center\", height: \"var(--count-h)\", padding: \"0 6px\", borderRadius: \"var(--radius-full)\", background: \"var(--neutral-alpha-8)\", color: zero ? \"var(--text-muted)\" : selected ? \"var(--text-heading)\" : \"var(--text-body)\", fontFamily: \"var(--font-sans)\", fontSize: 12, lineHeight: \"var(--count-h)\", fontWeight: \"var(--weight-medium)\", letterSpacing: 0, fontVariantNumeric: \"tabular-nums\", flexShrink: 0, ...style }}>{zero ? 0 : count}</span>;\n}\n/** Text pill filter tabs (For Review · To be Submitted · For Approval). Active = tinted pill. Pills are control-height (sm 28 · md 32 · lg 40); omit `size` to inherit.\n * tabs: string[] | { label, count?, showZero? }[] — a count draws a CountBadge 6px right of the label; active is matched by label. */\nexport function PillTabs({ tabs = [], active, onChange, size, showZero, style }) {\n return (\n <div role=\"tablist\" data-size={size} style={{ display: \"flex\", gap: \"var(--control-gap)\", ...style }}>\n {norm(tabs).map((t) => {\n const is = t.label === active;\n return (\n <button key={t.label} type=\"button\" role=\"tab\" aria-selected={is} data-ink={is ? undefined : \"\"} data-wash={is ? undefined : \"\"} onClick={() => onChange && onChange(t.label)}\n style={{ background: is ? \"var(--bg-active)\" : \"var(--wash, transparent)\", border: \"none\", cursor: \"pointer\", height: \"var(--control-h)\", padding: \"0 var(--control-px)\", borderRadius: \"var(--radius-md)\", display: \"inline-flex\", alignItems: \"center\", gap: 6, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: is ? \"var(--text-heading)\" : \"var(--ink, var(--text-secondary))\", whiteSpace: \"nowrap\" }}>\n {t.label}<CountBadge count={t.count} selected={is} showZero={t.showZero ?? showZero} />\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/Tabs.vue":"<script setup>\nimport { computed } from 'vue'\nimport CountBadge from './CountBadge.vue'\n// Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline. The bar is the region's bar height (sm 32 · md 40 · lg 44) with the tier's control text; omit `size` to inherit.\n// tabs: string[] | { label, count?, showZero? }[] — a count renders as a CountBadge 6px right of the label (neutral under the blue active tab); active is matched by label.\nconst props = defineProps({ tabs: { type: Array, default: () => [] }, active: String, size: String, showZero: Boolean, style: Object })\nconst emit = defineEmits(['change'])\nconst items = computed(() => props.tabs.map(t => typeof t === 'string' ? { label: t } : t))\n</script>\n<template>\n <div role=\"tablist\" :data-size=\"size\" :style=\"{ display: 'flex', gap: '4px', borderBottom: '1px solid var(--border-control)', ...style }\">\n <button v-for=\"t in items\" :key=\"t.label\" type=\"button\" role=\"tab\" :aria-selected=\"t.label === active\" :data-ink=\"t.label === active ? undefined : ''\" @click=\"emit('change', t.label)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', height: 'var(--bar-h)', boxSizing: 'border-box', padding: '0 var(--control-px)', marginBottom: '-1px', borderBottom: t.label === active ? '2px solid var(--action-primary)' : '2px solid transparent', display: 'inline-flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t.label === active ? 'var(--action-primary)' : 'var(--ink, var(--text-body))', whiteSpace: 'nowrap' }\">{{ t.label }}<CountBadge :count=\"t.count\" :selected=\"t.label === active\" :show-zero=\"t.showZero ?? showZero\" /></button>\n </div>\n</template>\n","tabs/ViewToggle.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Two-state list/grid icon toggle (palette header). Two IconButtons at the region's tier; omit `size` to inherit. */\nexport function ViewToggle({ value = \"grid\", onChange, size, style }) {\n return (\n <div data-size={size} style={{ display: \"inline-flex\", gap: 2, ...style }}>\n <IconButton icon=\"list\" active={value === \"list\"} onClick={() => onChange && onChange(\"list\")} label=\"List view\" />\n <IconButton icon=\"layout-grid\" active={value === \"grid\"} onClick={() => onChange && onChange(\"grid\")} label=\"Grid view\" />\n </div>\n );\n}\n","tabs/ViewToggle.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\n// Two-state list/grid icon toggle (palette header). Two IconButtons at the region's tier; omit `size` to inherit.\ndefineProps({ modelValue: { type: String, default: 'grid' }, size: String, style: Object })\nconst emit = defineEmits(['update:modelValue'])\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ display: 'inline-flex', gap: '2px', ...style }\">\n <IconButton icon=\"list\" :active=\"modelValue === 'list'\" label=\"List view\" @click=\"emit('update:modelValue', 'list')\" />\n <IconButton icon=\"layout-grid\" :active=\"modelValue === 'grid'\" label=\"Grid view\" @click=\"emit('update:modelValue', 'grid')\" />\n </div>\n</template>\n","tabs/PanelTabs.vue":"<script setup>\nimport { computed } from 'vue'\nimport CountBadge from './CountBadge.vue'\n// Panel tabs (Node Info · Calc Rules · …): a lighter underline tab bar at the region's bar height with regular-weight control text. Put the panel in an sm region for the compact 32px/12px look.\n// tabs: string[] | { label, count?, showZero? }[] — a count renders as a CountBadge 6px right of the label; active is matched by label.\nconst props = defineProps({ tabs: { type: Array, default: () => [] }, active: String, size: String, showZero: Boolean, style: Object })\nconst emit = defineEmits(['change'])\nconst items = computed(() => props.tabs.map(t => typeof t === 'string' ? { label: t } : t))\n</script>\n<template>\n <div role=\"tablist\" :data-size=\"size\" :style=\"{ display: 'flex', borderBottom: '1px solid var(--border-subtle)', ...style }\">\n <button v-for=\"t in items\" :key=\"t.label\" type=\"button\" role=\"tab\" :aria-selected=\"t.label === active\" :data-ink=\"t.label === active ? undefined : ''\" @click=\"emit('change', t.label)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', height: 'var(--bar-h)', boxSizing: 'border-box', padding: '0 var(--control-px)', marginBottom: '-1px', borderBottom: t.label === active ? '2px solid var(--action-primary)' : '2px solid transparent', display: 'inline-flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t.label === active ? 'var(--action-primary)' : 'var(--ink, var(--text-secondary))', whiteSpace: 'nowrap' }\">{{ t.label }}<CountBadge :count=\"t.count\" :selected=\"t.label === active\" :show-zero=\"t.showZero ?? showZero\" /></button>\n </div>\n</template>\n","tabs/PillTabs.vue":"<script setup>\nimport { computed } from 'vue'\nimport CountBadge from './CountBadge.vue'\n// Text pill filter tabs. Active = tinted pill. Pills are control-height (sm 28 · md 32 · lg 40); omit `size` to inherit.\n// tabs: string[] | { label, count?, showZero? }[] — a count draws a CountBadge 6px right of the label; active is matched by label.\nconst props = defineProps({ tabs: { type: Array, default: () => [] }, active: String, size: String, showZero: Boolean, style: Object })\nconst emit = defineEmits(['change'])\nconst items = computed(() => props.tabs.map(t => typeof t === 'string' ? { label: t } : t))\n</script>\n<template>\n <div role=\"tablist\" :data-size=\"size\" :style=\"{ display: 'flex', gap: 'var(--control-gap)', ...style }\">\n <button v-for=\"t in items\" :key=\"t.label\" type=\"button\" role=\"tab\" :aria-selected=\"t.label === active\" :data-ink=\"t.label === active ? undefined : ''\" :data-wash=\"t.label === active ? undefined : ''\" @click=\"emit('change', t.label)\"\n :style=\"{ background: t.label === active ? 'var(--bg-active)' : 'var(--wash, transparent)', border: 'none', cursor: 'pointer', height: 'var(--control-h)', padding: '0 var(--control-px)', borderRadius: 'var(--radius-md)', display: 'inline-flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t.label === active ? 'var(--text-heading)' : 'var(--ink, var(--text-secondary))', whiteSpace: 'nowrap' }\">{{ t.label }}<CountBadge :count=\"t.count\" :selected=\"t.label === active\" :show-zero=\"t.showZero ?? showZero\" /></button>\n </div>\n</template>\n","tabs/CountBadge.vue":"<script setup>\nimport { computed } from 'vue'\n// Count badge beside a segment / pill label: --count-h tall (sm 16 · md 18 · lg 20), 6px sides, full radius, 12px tabular figures\n// (digits are Latin, so not bumped in CJK). Fill: the alpha wash --neutral-alpha-8, so it darkens any surface by the same step (white tab,\n// #f5f5f5 active pill, dark theme); text tells selected from unselected: --text-heading vs --text-body (a lighter grey fails 4.5:1 at 12px).\n// Hidden for 0 or a missing count unless showZero, which draws a muted 0.\nconst props = defineProps({ count: Number, selected: Boolean, showZero: Boolean, style: Object })\nconst zero = computed(() => props.count === 0 || props.count == null)\n</script>\n<template>\n <span v-if=\"!zero || showZero\" aria-hidden=\"true\" :style=\"{ display: 'inline-flex', alignItems: 'center', height: 'var(--count-h)', padding: '0 6px', borderRadius: 'var(--radius-full)', background: 'var(--neutral-alpha-8)', color: zero ? 'var(--text-muted)' : selected ? 'var(--text-heading)' : 'var(--text-body)', fontFamily: 'var(--font-sans)', fontSize: '12px', lineHeight: 'var(--count-h)', fontWeight: 'var(--weight-medium)', letterSpacing: 0, fontVariantNumeric: 'tabular-nums', flexShrink: 0, ...style }\">{{ zero ? 0 : count }}</span>\n</template>\n"});
|