@keyfunc/keybricks-ds 1.0.4 → 2.3.1
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 +14 -5
- package/DESIGN.md +245 -59
- package/INTEGRATION.md +243 -49
- package/SKILL.md +13 -11
- package/_adherence.oxlintrc.json +183 -70
- package/_scripts/build-index.mjs +24 -4
- package/_scripts/build-tokens.mjs +5 -2
- package/assets/icons/icon-index.js +1 -1
- package/assets/icons/icon-index.json +1 -1
- package/components/_cards/accordion.card.html +2 -2
- package/components/_cards/action-chip.card.html +11 -9
- package/components/_cards/activity.card.html +3 -3
- package/components/_cards/alert.card.html +3 -3
- package/components/_cards/app-chrome.card.html +4 -4
- package/components/_cards/banner.card.html +1 -1
- package/components/_cards/breadcrumb.card.html +5 -5
- package/components/_cards/button-1-base.card.html +23 -26
- package/components/_cards/button-2-states.card.html +27 -25
- package/components/_cards/button-3-sizes-icons.card.html +20 -19
- package/components/_cards/button-4-group.card.html +14 -31
- package/components/_cards/canvas-zoom-controls.card.html +1 -1
- package/components/_cards/cards-indicators.card.html +2 -2
- package/components/_cards/charts-1-sparkline.card.html +5 -5
- package/components/_cards/charts-10-gauge.card.html +5 -5
- package/components/_cards/charts-11-funnel.card.html +3 -3
- package/components/_cards/charts-12-treemap.card.html +3 -3
- package/components/_cards/charts-14-radar.card.html +3 -3
- package/components/_cards/charts-15-distribution.card.html +2 -2
- package/components/_cards/charts-16-pareto.card.html +3 -3
- package/components/_cards/charts-17-candlestick.card.html +3 -3
- package/components/_cards/charts-18-sankey.card.html +3 -3
- package/components/_cards/charts-19-chord.card.html +3 -3
- package/components/_cards/charts-2-line.card.html +4 -4
- package/components/_cards/charts-20-sunburst.card.html +3 -3
- package/components/_cards/charts-21-worldmap.card.html +4 -4
- package/components/_cards/charts-22-gantt.card.html +3 -3
- package/components/_cards/charts-23-bar3d.card.html +3 -3
- package/components/_cards/charts-24-pie3d.card.html +3 -3
- package/components/_cards/charts-25-surface3d.card.html +3 -3
- package/components/_cards/charts-26-scatter3d.card.html +3 -3
- package/components/_cards/charts-27-ribbon3d.card.html +3 -3
- package/components/_cards/charts-3-bar.card.html +7 -7
- package/components/_cards/charts-4-barlist.card.html +3 -3
- package/components/_cards/charts-5-donut.card.html +4 -4
- package/components/_cards/charts-6-scatter.card.html +3 -3
- package/components/_cards/charts-7-heatmap.card.html +3 -3
- package/components/_cards/charts-8-waterfall.card.html +3 -3
- package/components/_cards/charts-9-bullet.card.html +3 -3
- package/components/_cards/charts-99-fullpage.card.html +5 -5
- package/components/_cards/checkbox.card.html +9 -6
- package/components/_cards/codeblock.card.html +2 -2
- package/components/_cards/combobox.card.html +6 -3
- package/components/_cards/context-menu.card.html +6 -3
- package/components/_cards/date-picker.card.html +10 -4
- package/components/_cards/diffvalue.card.html +3 -3
- package/components/_cards/divider.card.html +4 -4
- package/components/_cards/drawer.card.html +4 -4
- package/components/_cards/dropdown-menu.card.html +7 -7
- package/components/_cards/dropzone.card.html +5 -3
- package/components/_cards/empty-state.card.html +6 -6
- package/components/_cards/filter-bar.card.html +5 -5
- package/components/_cards/icon-button.card.html +14 -14
- package/components/_cards/icons-1-core.card.html +4 -4
- package/components/_cards/icons-2-feather.card.html +4 -4
- package/components/_cards/icons-3-unicons.card.html +4 -4
- package/components/_cards/input.card.html +5 -2
- package/components/_cards/kbd.card.html +2 -2
- package/components/_cards/keyvalue.card.html +2 -2
- package/components/_cards/listrow.card.html +2 -2
- package/components/_cards/loading.card.html +3 -3
- package/components/_cards/log-viewer.card.html +3 -3
- package/components/_cards/modal.card.html +9 -9
- package/components/_cards/multi-select.card.html +6 -3
- package/components/_cards/number-stepper.card.html +9 -5
- package/components/_cards/pagination.card.html +6 -3
- package/components/_cards/people.card.html +4 -4
- package/components/_cards/pipeline-node.card.html +8 -8
- package/components/_cards/pipeline-palette.card.html +5 -5
- package/components/_cards/radio.card.html +12 -7
- package/components/_cards/search.card.html +6 -3
- package/components/_cards/segmented-control.card.html +11 -10
- package/components/_cards/select.card.html +8 -4
- package/components/_cards/slider.card.html +2 -2
- package/components/_cards/smart-table.card.html +3 -3
- package/components/_cards/spreadsheet.card.html +3 -3
- package/components/_cards/stepper.card.html +3 -3
- package/components/_cards/table.card.html +165 -0
- package/components/_cards/tabs.card.html +6 -3
- package/components/_cards/tag.card.html +6 -3
- package/components/_cards/textarea.card.html +2 -2
- package/components/_cards/timeline.card.html +3 -3
- package/components/_cards/toast.card.html +10 -8
- package/components/_cards/toggles.card.html +15 -10
- package/components/_cards/tooltip.card.html +7 -7
- package/components/_cards/tree.card.html +3 -3
- package/components/_shared/card-kit.css +25 -0
- package/components/_shared/card-kit.js +50 -0
- package/components/_shared/src-buttons.js +1 -1
- package/components/_shared/src-charts.js +1 -1
- package/components/_shared/src-data.js +1 -1
- package/components/_shared/src-display.js +1 -1
- package/components/_shared/src-feedback.js +1 -1
- package/components/_shared/src-forms.js +1 -1
- package/components/_shared/src-icons.js +1 -1
- package/components/_shared/src-navigation.js +1 -1
- package/components/_shared/src-pipeline.js +1 -1
- package/components/_shared/src-tabs.js +1 -1
- package/components/_shared/view-source.js +3 -3
- package/components/_vue/index.mjs +16 -5
- package/components/buttons/ActionChip.d.ts +3 -1
- package/components/buttons/ActionChip.jsx +4 -4
- package/components/buttons/ActionChip.vue +4 -4
- package/components/buttons/Button.d.ts +5 -2
- package/components/buttons/Button.jsx +15 -18
- package/components/buttons/Button.vue +17 -18
- package/components/buttons/ButtonGroup.vue +1 -1
- package/components/buttons/IconButton.d.ts +2 -1
- package/components/buttons/IconButton.jsx +13 -9
- package/components/buttons/IconButton.vue +12 -9
- package/components/buttons/buttons.prompt.md +6 -5
- package/components/charts/Bar3D.jsx +2 -2
- package/components/charts/Bar3D.vue +3 -3
- package/components/charts/BarChart.jsx +4 -4
- package/components/charts/BarChart.vue +6 -6
- package/components/charts/BarList.jsx +4 -4
- package/components/charts/BarList.vue +4 -4
- package/components/charts/BoxPlot.jsx +1 -1
- package/components/charts/BoxPlot.vue +3 -3
- package/components/charts/BulletChart.jsx +6 -6
- package/components/charts/BulletChart.vue +6 -6
- package/components/charts/CandlestickChart.jsx +2 -2
- package/components/charts/CandlestickChart.vue +5 -5
- package/components/charts/ChartKit.jsx +8 -7
- package/components/charts/ChartLegend.vue +16 -0
- package/components/charts/ChartTooltip.vue +16 -0
- package/components/charts/ChordChart.jsx +2 -2
- package/components/charts/ChordChart.vue +3 -3
- package/components/charts/DonutChart.jsx +1 -1
- package/components/charts/DonutChart.vue +2 -2
- package/components/charts/FunnelChart.jsx +3 -3
- package/components/charts/FunnelChart.vue +4 -4
- package/components/charts/GanttChart.jsx +2 -2
- package/components/charts/GanttChart.vue +3 -3
- package/components/charts/GaugeChart.jsx +2 -2
- package/components/charts/GaugeChart.vue +2 -2
- package/components/charts/HeatmapChart.jsx +2 -2
- package/components/charts/HeatmapChart.vue +2 -2
- package/components/charts/Histogram.jsx +2 -2
- package/components/charts/Histogram.vue +4 -4
- package/components/charts/LineChart.jsx +1 -1
- package/components/charts/LineChart.vue +4 -4
- package/components/charts/ParetoChart.jsx +3 -3
- package/components/charts/ParetoChart.vue +5 -5
- package/components/charts/Pie3D.jsx +1 -1
- package/components/charts/Pie3D.vue +2 -2
- package/components/charts/PlotGrid.vue +18 -0
- package/components/charts/RadarChart.jsx +1 -1
- package/components/charts/RadarChart.vue +2 -2
- package/components/charts/Ribbon3D.jsx +2 -2
- package/components/charts/Ribbon3D.vue +3 -3
- package/components/charts/SankeyChart.jsx +2 -2
- package/components/charts/SankeyChart.vue +3 -3
- package/components/charts/Scatter3D.jsx +1 -1
- package/components/charts/Scatter3D.vue +2 -2
- package/components/charts/ScatterChart.jsx +3 -3
- package/components/charts/ScatterChart.vue +5 -5
- package/components/charts/Sparkline.jsx +1 -1
- package/components/charts/Sparkline.vue +1 -1
- package/components/charts/SunburstChart.jsx +1 -1
- package/components/charts/SunburstChart.vue +2 -2
- package/components/charts/Surface3D.jsx +2 -2
- package/components/charts/Surface3D.vue +3 -3
- package/components/charts/TreemapChart.jsx +2 -2
- package/components/charts/TreemapChart.vue +3 -3
- package/components/charts/WaterfallChart.jsx +2 -2
- package/components/charts/WaterfallChart.vue +6 -5
- package/components/charts/WorldMap.jsx +4 -4
- package/components/charts/WorldMap.vue +5 -5
- package/components/charts/XLabels.vue +11 -0
- package/components/data/CodeBlock.jsx +1 -1
- package/components/data/CodeBlock.vue +1 -1
- package/components/data/DiffValue.d.ts +1 -0
- package/components/data/DiffValue.jsx +1 -1
- package/components/data/DiffValue.vue +1 -1
- package/components/data/FilterBar.d.ts +2 -0
- package/components/data/FilterBar.jsx +10 -9
- package/components/data/FilterBar.vue +9 -8
- package/components/data/KeyValue.d.ts +1 -1
- package/components/data/KeyValue.jsx +4 -4
- package/components/data/KeyValue.vue +4 -4
- package/components/data/ListRow.jsx +2 -2
- package/components/data/ListRow.vue +2 -2
- package/components/data/Pagination.d.ts +5 -1
- package/components/data/Pagination.jsx +16 -12
- package/components/data/Pagination.vue +14 -9
- package/components/data/SmartTable.d.ts +4 -1
- package/components/data/SmartTable.jsx +16 -14
- package/components/data/SmartTable.vue +26 -23
- package/components/data/Spreadsheet.d.ts +2 -0
- package/components/data/Spreadsheet.jsx +16 -15
- package/components/data/Spreadsheet.vue +19 -18
- package/components/data/Table.d.ts +101 -0
- package/components/data/Table.jsx +223 -0
- package/components/data/Table.vue +153 -0
- package/components/data/data.prompt.md +3 -3
- package/components/display/Avatar.jsx +2 -2
- package/components/display/Avatar.vue +3 -2
- package/components/display/AvatarGroup.vue +3 -2
- package/components/display/Badge.d.ts +4 -1
- package/components/display/Badge.jsx +10 -5
- package/components/display/Badge.vue +1 -1
- package/components/display/Divider.vue +3 -3
- package/components/display/Donut.vue +3 -2
- package/components/display/IconTile.vue +10 -4
- package/components/display/Kbd.d.ts +1 -1
- package/components/display/Kbd.jsx +1 -1
- package/components/display/Kbd.vue +2 -2
- package/components/display/ProgressBar.jsx +1 -1
- package/components/display/ProgressBar.vue +1 -1
- package/components/display/StatCard.vue +1 -1
- package/components/display/Tag.d.ts +3 -1
- package/components/display/Tag.jsx +4 -4
- package/components/display/Tag.vue +4 -4
- package/components/feedback/Accordion.jsx +2 -2
- package/components/feedback/Accordion.vue +2 -2
- package/components/feedback/ActivityItem.d.ts +3 -1
- package/components/feedback/ActivityItem.jsx +6 -6
- package/components/feedback/ActivityItem.vue +5 -4
- package/components/feedback/Alert.jsx +4 -4
- package/components/feedback/Alert.vue +3 -3
- package/components/feedback/Banner.jsx +5 -5
- package/components/feedback/Banner.vue +5 -5
- package/components/feedback/ContextMenu.d.ts +3 -1
- package/components/feedback/ContextMenu.jsx +5 -5
- package/components/feedback/ContextMenu.vue +5 -5
- package/components/feedback/DotActivity.vue +3 -3
- package/components/feedback/Drawer.d.ts +2 -0
- package/components/feedback/Drawer.jsx +6 -6
- package/components/feedback/Drawer.vue +22 -15
- package/components/feedback/EmptyState.vue +2 -2
- package/components/feedback/LogViewer.vue +1 -1
- package/components/feedback/Modal.d.ts +2 -0
- package/components/feedback/Modal.jsx +12 -11
- package/components/feedback/Modal.vue +10 -8
- package/components/feedback/Skeleton.vue +2 -1
- package/components/feedback/Spinner.vue +2 -1
- package/components/feedback/Stepper.vue +1 -1
- package/components/feedback/Timeline.jsx +1 -1
- package/components/feedback/Timeline.vue +3 -3
- package/components/feedback/Toast.vue +4 -4
- package/components/feedback/ToastStack.vue +11 -0
- package/components/feedback/Tooltip.vue +19 -0
- package/components/feedback/feedback.prompt.md +4 -4
- package/components/forms/Checkbox.d.ts +3 -2
- package/components/forms/Checkbox.jsx +7 -7
- package/components/forms/Checkbox.vue +8 -8
- package/components/forms/Combobox.d.ts +2 -0
- package/components/forms/Combobox.jsx +12 -10
- package/components/forms/Combobox.vue +11 -10
- package/components/forms/DatePicker.d.ts +4 -0
- package/components/forms/DatePicker.jsx +16 -12
- package/components/forms/DatePicker.vue +15 -11
- package/components/forms/Dropzone.jsx +2 -2
- package/components/forms/Dropzone.vue +5 -5
- package/components/forms/Field.d.ts +5 -1
- package/components/forms/Field.jsx +4 -4
- package/components/forms/Field.vue +4 -4
- package/components/forms/Input.d.ts +5 -1
- package/components/forms/Input.jsx +8 -8
- package/components/forms/Input.vue +3 -3
- package/components/forms/MultiSelect.d.ts +2 -0
- package/components/forms/MultiSelect.jsx +13 -11
- package/components/forms/MultiSelect.vue +12 -11
- package/components/forms/NumberStepper.d.ts +3 -1
- package/components/forms/NumberStepper.jsx +7 -7
- package/components/forms/NumberStepper.vue +8 -7
- package/components/forms/Radio.d.ts +5 -3
- package/components/forms/Radio.jsx +13 -15
- package/components/forms/Radio.vue +9 -10
- package/components/forms/RadioGroup.vue +5 -5
- package/components/forms/SearchInput.d.ts +3 -1
- package/components/forms/SearchInput.jsx +6 -6
- package/components/forms/SearchInput.vue +7 -6
- package/components/forms/Select.d.ts +3 -2
- package/components/forms/Select.jsx +6 -7
- package/components/forms/Select.vue +6 -6
- package/components/forms/Slider.jsx +3 -3
- package/components/forms/Slider.vue +3 -3
- package/components/forms/Switch.d.ts +2 -2
- package/components/forms/Switch.jsx +20 -22
- package/components/forms/Switch.vue +22 -18
- package/components/forms/Textarea.vue +5 -5
- package/components/forms/forms.prompt.md +1 -1
- package/components/forms/input-base.js +2 -1
- package/components/icons/Icon.jsx +17 -1
- package/components/icons/Icon.prompt.md +2 -0
- package/components/icons/Icon.vue +1 -1
- package/components/icons/icon-paths.js +15 -0
- package/components/index.d.ts +1 -2
- package/components/index.js +4 -5
- package/components/navigation/Breadcrumb.jsx +6 -10
- package/components/navigation/Breadcrumb.vue +6 -12
- package/components/navigation/DocTree.jsx +3 -3
- package/components/navigation/DocTree.vue +3 -3
- package/components/navigation/PageHeading.vue +1 -1
- package/components/navigation/PageToolbar.vue +7 -6
- package/components/navigation/Sidebar.jsx +4 -23
- package/components/navigation/Sidebar.vue +5 -10
- package/components/navigation/ThemeToggle.vue +2 -1
- package/components/navigation/TopBar.d.ts +3 -1
- package/components/navigation/TopBar.jsx +6 -6
- package/components/navigation/Tree.jsx +5 -5
- package/components/navigation/Tree.vue +13 -8
- package/components/navigation/nav-model.js +23 -0
- package/components/navigation/theme-store.js +9 -0
- package/components/pipeline/ConfigPanel.d.ts +2 -0
- package/components/pipeline/ConfigPanel.jsx +9 -9
- package/components/pipeline/ConfigPanel.vue +9 -9
- package/components/pipeline/DataPreview.d.ts +3 -1
- package/components/pipeline/DataPreview.jsx +7 -7
- package/components/pipeline/DataPreview.vue +7 -7
- package/components/pipeline/NodeCard.jsx +3 -3
- package/components/pipeline/NodeCard.vue +3 -3
- package/components/pipeline/PaletteItem.vue +2 -2
- package/components/pipeline/PaletteListItem.vue +1 -1
- package/components/pipeline/PalettePanel.jsx +2 -2
- package/components/pipeline/PalettePanel.vue +1 -1
- package/components/pipeline/Port.vue +15 -0
- package/components/pipeline/PortLabel.vue +15 -0
- package/components/pipeline/ZoomControls.vue +1 -1
- package/components/pipeline/port-skin.js +13 -0
- package/components/px.js +4 -0
- package/components/tabs/PanelTabs.vue +5 -4
- package/components/tabs/PillTabs.vue +5 -4
- package/components/tabs/SegmentedControl.d.ts +2 -2
- package/components/tabs/SegmentedControl.jsx +7 -7
- package/components/tabs/SegmentedControl.vue +7 -7
- package/components/tabs/Tabs.d.ts +5 -3
- package/components/tabs/Tabs.jsx +15 -15
- package/components/tabs/Tabs.vue +5 -5
- package/components/tabs/ViewToggle.d.ts +3 -1
- package/components/tabs/ViewToggle.jsx +5 -5
- package/components/tabs/ViewToggle.vue +5 -5
- package/components/tabs/tabs.prompt.md +1 -1
- package/keybricks.css +4 -0
- package/package.json +6 -3
- package/readme.md +91 -122
- package/readme.zh-CN.md +121 -0
- package/tokens/alpha.css +3 -1
- package/tokens/charts.css +20 -17
- package/tokens/colors.css +3 -3
- package/tokens/dark.css +3 -3
- package/tokens/hover.css +32 -0
- package/tokens/interaction.css +3 -1
- package/tokens/layout.css +31 -7
- package/tokens/tokens.d.ts +58 -0
- package/tokens/tokens.js +179 -92
- package/tokens/tokens.json +327 -114
- package/tokens/typography.css +45 -15
- package/components/_cards/brand-visual.card.html +0 -43
- package/components/_cards/datatable.card.html +0 -48
- package/components/_shared/src-brand.js +0 -1
- package/components/brand/BrandVisual.d.ts +0 -17
- package/components/brand/BrandVisual.jsx +0 -29
- package/components/brand/BrandVisual.vue +0 -34
- package/components/brand/_dir.card.html +0 -13
- package/components/brand/brandMotifs.js +0 -210
- package/components/buttons/_dir.card.html +0 -13
- package/components/charts/ChartKit.vue +0 -47
- package/components/charts/_dir.card.html +0 -7
- package/components/data/DataTable.d.ts +0 -28
- package/components/data/DataTable.jsx +0 -32
- package/components/data/DataTable.vue +0 -25
- package/components/data/_dir.card.html +0 -13
- package/components/display/_dir.card.html +0 -13
- package/components/feedback/_dir.card.html +0 -13
- package/components/forms/_dir.card.html +0 -13
- package/components/icons/_dir.card.html +0 -13
- package/components/navigation/_dir.card.html +0 -13
- package/components/pipeline/_dir.card.html +0 -13
- package/components/tabs/_dir.card.html +0 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"icons/Icon.jsx":"import React from \"react\";\n// Keybricks icon set — the source file uses Lucide stroke glyphs (24-grid, stroke 2,\n// round caps/joins, currentColor) rendered at 14/16px. Path data below is from the\n// public Lucide set, matching the glyphs used across Keyfunc UI v8.\nexport const ICON_PATHS = {\n search: '<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>',\n bell: '<path d=\"M10.268 21a2 2 0 0 0 3.464 0\"/><path d=\"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326\"/>',\n sparkles: '<path d=\"M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z\"/><path d=\"M20 3v4\"/><path d=\"M22 5h-4\"/><path d=\"M4 17v2\"/><path d=\"M5 18H3\"/>',\n \"panel-left\": '<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M9 3v18\"/>',\n house: '<path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\"/><path d=\"M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/>',\n file: '<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/>',\n \"file-text\": '<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/><path d=\"M10 9H8\"/><path d=\"M16 13H8\"/><path d=\"M16 17H8\"/>',\n \"file-check\": '<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/><path d=\"m9 15 2 2 4-4\"/>',\n \"layout-grid\": '<rect width=\"7\" height=\"7\" x=\"3\" y=\"3\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"14\" y=\"3\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"14\" y=\"14\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"3\" y=\"14\" rx=\"1\"/>',\n database: '<ellipse cx=\"12\" cy=\"5\" rx=\"9\" ry=\"3\"/><path d=\"M3 5V19A9 3 0 0 0 21 19V5\"/><path d=\"M3 12A9 3 0 0 0 21 12\"/>',\n scale: '<path d=\"m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z\"/><path d=\"m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z\"/><path d=\"M7 21h10\"/><path d=\"M12 3v18\"/><path d=\"M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2\"/>',\n network: '<rect x=\"16\" y=\"16\" width=\"6\" height=\"6\" rx=\"1\"/><rect x=\"2\" y=\"16\" width=\"6\" height=\"6\" rx=\"1\"/><rect x=\"9\" y=\"2\" width=\"6\" height=\"6\" rx=\"1\"/><path d=\"M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3\"/><path d=\"M12 12V8\"/>',\n folder: '<path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/>',\n \"folder-open\": '<path d=\"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2\"/>',\n \"folder-cog\": '<path d=\"M10.5 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v3\"/><circle cx=\"18\" cy=\"18\" r=\"3\"/><path d=\"M18 14v1\"/><path d=\"M18 21v1\"/><path d=\"M22 18h-1\"/><path d=\"M15 18h-1\"/><path d=\"m21.2 16.2-.9.5\"/><path d=\"m15.7 19.3-.9.5\"/><path d=\"m21.2 19.8-.9-.5\"/><path d=\"m15.7 16.7-.9-.5\"/>',\n settings: '<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>',\n \"settings-2\": '<path d=\"M20 7h-9\"/><path d=\"M14 17H5\"/><circle cx=\"17\" cy=\"17\" r=\"3\"/><circle cx=\"7\" cy=\"7\" r=\"3\"/>',\n moon: '<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"/>',\n \"circle-help\": '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"/><path d=\"M12 17h.01\"/>',\n box: '<path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/>',\n workflow: '<rect width=\"8\" height=\"8\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M7 11v4a2 2 0 0 0 2 2h4\"/><rect width=\"8\" height=\"8\" x=\"13\" y=\"13\" rx=\"2\"/>',\n shield: '<path d=\"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z\"/>',\n \"refresh-cw\": '<path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/>',\n \"chart-line\": '<path d=\"M3 3v16a2 2 0 0 0 2 2h16\"/><path d=\"m19 9-5 5-4-4-3 3\"/>',\n \"chart-column\": '<path d=\"M3 3v16a2 2 0 0 0 2 2h16\"/><path d=\"M18 17V9\"/><path d=\"M13 17V5\"/><path d=\"M8 17v-3\"/>',\n \"chart-no-axes-column\": '<line x1=\"18\" x2=\"18\" y1=\"20\" y2=\"10\"/><line x1=\"12\" x2=\"12\" y1=\"20\" y2=\"4\"/><line x1=\"6\" x2=\"6\" y1=\"20\" y2=\"14\"/>',\n \"list-todo\": '<rect x=\"3\" y=\"5\" width=\"6\" height=\"6\" rx=\"1\"/><path d=\"m3 17 2 2 4-4\"/><path d=\"M13 6h8\"/><path d=\"M13 12h8\"/><path d=\"M13 18h8\"/>',\n \"circle-check\": '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/>',\n target: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/>',\n \"book-open\": '<path d=\"M12 7v14\"/><path d=\"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\"/>',\n \"chevron-down\": '<path d=\"m6 9 6 6 6-6\"/>',\n \"chevron-right\": '<path d=\"m9 18 6-6-6-6\"/>',\n \"chevron-left\": '<path d=\"m15 18-6-6 6-6\"/>',\n \"chevron-up\": '<path d=\"m18 15-6-6-6 6\"/>',\n \"arrow-left\": '<path d=\"m12 19-7-7 7-7\"/><path d=\"M19 12H5\"/>',\n \"arrow-right\": '<path d=\"M5 12h14\"/><path d=\"m12 5 7 7-7 7\"/>',\n \"arrow-right-left\": '<path d=\"m16 3 4 4-4 4\"/><path d=\"M20 7H4\"/><path d=\"m8 21-4-4 4-4\"/><path d=\"M4 17h16\"/>',\n table: '<path d=\"M12 3v18\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M3 9h18\"/><path d=\"M3 15h18\"/>',\n layers: '<path d=\"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z\"/><path d=\"m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65\"/><path d=\"m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65\"/>',\n \"grid-3x3\": '<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M3 9h18\"/><path d=\"M3 15h18\"/><path d=\"M9 3v18\"/><path d=\"M15 3v18\"/>',\n \"text-search\": '<path d=\"M21 6H3\"/><path d=\"M10 12H3\"/><path d=\"M10 18H3\"/><circle cx=\"17\" cy=\"15\" r=\"3\"/><path d=\"m21 19-1.9-1.9\"/>',\n shuffle: '<path d=\"M2 18h1.4c1.3 0 2.5-.6 3.3-1.7l6.1-8.6c.7-1.1 2-1.7 3.3-1.7H22\"/><path d=\"m18 2 4 4-4 4\"/><path d=\"M2 6h1.9c1.5 0 2.9.9 3.6 2.2\"/><path d=\"M22 18h-5.9c-1.3 0-2.6-.7-3.3-1.8l-.5-.8\"/><path d=\"m18 14 4 4-4 4\"/>',\n split: '<path d=\"M16 3h5v5\"/><path d=\"M8 3H3v5\"/><path d=\"M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3\"/><path d=\"m15 9 6-6\"/>',\n menu: '<line x1=\"4\" x2=\"20\" y1=\"12\" y2=\"12\"/><line x1=\"4\" x2=\"20\" y1=\"6\" y2=\"6\"/><line x1=\"4\" x2=\"20\" y1=\"18\" y2=\"18\"/>',\n list: '<line x1=\"8\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"8\" x2=\"21\" y1=\"12\" y2=\"12\"/><line x1=\"8\" x2=\"21\" y1=\"18\" y2=\"18\"/><line x1=\"3\" x2=\"3.01\" y1=\"6\" y2=\"6\"/><line x1=\"3\" x2=\"3.01\" y1=\"12\" y2=\"12\"/><line x1=\"3\" x2=\"3.01\" y1=\"18\" y2=\"18\"/>',\n funnel: '<polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"/>',\n \"sliders-horizontal\": '<line x1=\"21\" x2=\"14\" y1=\"4\" y2=\"4\"/><line x1=\"10\" x2=\"3\" y1=\"4\" y2=\"4\"/><line x1=\"21\" x2=\"12\" y1=\"12\" y2=\"12\"/><line x1=\"8\" x2=\"3\" y1=\"12\" y2=\"12\"/><line x1=\"21\" x2=\"16\" y1=\"20\" y2=\"20\"/><line x1=\"12\" x2=\"3\" y1=\"20\" y2=\"20\"/><line x1=\"14\" x2=\"14\" y1=\"2\" y2=\"6\"/><line x1=\"8\" x2=\"8\" y1=\"10\" y2=\"14\"/><line x1=\"16\" x2=\"16\" y1=\"18\" y2=\"22\"/>',\n \"trending-up\": '<polyline points=\"22 7 13.5 15.5 8.5 10.5 2 17\"/><polyline points=\"16 7 22 7 22 13\"/>',\n code: '<path d=\"m16 18 6-6-6-6\"/><path d=\"m8 6-6 6 6 6\"/>',\n merge: '<path d=\"m8 6 4-4 4 4\"/><path d=\"M12 2v10.3a4 4 0 0 1-1.172 2.872L4 22\"/><path d=\"m20 22-5-5\"/>',\n upload: '<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><polyline points=\"17 8 12 3 7 8\"/><line x1=\"12\" x2=\"12\" y1=\"3\" y2=\"15\"/>',\n download: '<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><polyline points=\"7 10 12 15 17 10\"/><line x1=\"12\" x2=\"12\" y1=\"15\" y2=\"3\"/>',\n check: '<path d=\"M20 6 9 17l-5-5\"/>',\n x: '<path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/>',\n plus: '<path d=\"M5 12h14\"/><path d=\"M12 5v14\"/>',\n minus: '<path d=\"M5 12h14\"/>',\n ellipsis: '<circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/>',\n play: '<polygon points=\"6 3 20 12 6 21 6 3\"/>',\n send: '<path d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\"/><path d=\"m21.854 2.147-10.94 10.939\"/>',\n eye: '<path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>',\n copy: '<rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/>',\n \"trash-2\": '<path d=\"M3 6h18\"/><path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"/><path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"/><line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\"/><line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\"/>',\n clock: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/>',\n calendar: '<path d=\"M8 2v4\"/><path d=\"M16 2v4\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\"/><path d=\"M3 10h18\"/>',\n \"undo-2\": '<path d=\"M9 14 4 9l5-5\"/><path d=\"M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11\"/>',\n \"redo-2\": '<path d=\"m15 14 5-5-5-5\"/><path d=\"M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13\"/>',\n history: '<path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"/><path d=\"M3 3v5h5\"/><path d=\"M12 7v5l4 2\"/>',\n bug: '<path d=\"m8 2 1.88 1.88\"/><path d=\"M14.12 3.88 16 2\"/><path d=\"M9 7.13v-1a3.003 3.003 0 1 1 6 0v1\"/><path d=\"M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6\"/><path d=\"M12 20v-9\"/><path d=\"M6.53 9C4.6 8.8 3 7.1 3 5\"/><path d=\"M6 13H2\"/><path d=\"M3 21c0-2.1 1.7-3.9 3.8-4\"/><path d=\"M20.97 5c0 2.1-1.6 3.8-3.5 4\"/><path d=\"M22 13h-4\"/><path d=\"M17.2 17c2.1.1 3.8 1.9 3.8 4\"/>',\n info: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/>',\n \"triangle-alert\": '<path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 20h16a2 2 0 0 0 1.73-2Z\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/>',\n calendar: '<path d=\"M8 2v4\"/><path d=\"M16 2v4\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\"/><path d=\"M3 10h18\"/>',\n};\n\nimport { FEATHER_PATHS } from \"./feather-paths.js\";\n\n/** Lucide-style stroke icon. name: key of ICON_PATHS (core set) or any Feather icon name (feather-paths.js fallback); size in px; strokeWidth defaults to 2 (24-grid). Browse both sets with keywords in the Icon Gallery card. */\nexport function Icon({ name, size = 16, strokeWidth = 2, color, style, className }) {\n const body = ICON_PATHS[name] || FEATHER_PATHS[name] || \"\";\n return (\n <svg\n width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n strokeWidth={strokeWidth} strokeLinecap=\"round\" strokeLinejoin=\"round\"\n className={className}\n style={{ display: \"block\", flexShrink: 0, color, ...style }}\n dangerouslySetInnerHTML={{ __html: body }}\n />\n );\n}\n","icons/Icon.vue":"<script setup>\n// Keybricks icon set — Lucide stroke glyphs (24-grid, stroke 2, round caps/joins, currentColor), rendered at 14/16px.\n// Full path dictionary mirrors icons/Icon.jsx's ICON_PATHS (80+ entries) — see that file for the complete list.\nimport { VUE_ICON_PATHS } from './icon-paths.js'\nimport { FEATHER_PATHS } from './feather-paths.js'\nconst props = defineProps({ name: String, size: { type: Number, default: 16 }, strokeWidth: { type: Number, default: 2 }, color: String, style: Object })\nconst body = (n) => VUE_ICON_PATHS[n] || FEATHER_PATHS[n] || ''\n</script>\n<template>\n <svg :width=\"size\" :height=\"size\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" :stroke-width=\"strokeWidth\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n :style=\"{ display: 'block', flexShrink: 0, color, ...style }\" v-html=\"body(name)\" />\n</template>\n","icons/Unicon.jsx":"import React from \"react\";\n\nlet uniconsCss = false;\nfunction ensureUniconsCss() {\n if (uniconsCss || document.getElementById(\"kb-unicons-css\")) { uniconsCss = true; return; }\n const l = document.createElement(\"link\");\n l.id = \"kb-unicons-css\"; l.rel = \"stylesheet\";\n l.href = \"https://unicons.iconscout.com/release/v4.0.8/css/line.css\";\n document.head.appendChild(l); uniconsCss = true;\n}\n\n/** Unicons line icon (iconscout.com/unicons, Apache 2.0) — 1,206 glyphs loaded from the official Unicons CDN webfont. name is the Unicons id without the \"uil-\" prefix (e.g. \"chart-pie-alt\"). Browse names + keywords in the Unicons gallery card; keyword index lives in assets/icons/unicons-index.json. */\nexport function Unicon({ name, size = 16, color, style, className }) {\n React.useEffect(ensureUniconsCss, []);\n return <i aria-hidden=\"true\" className={\"uil uil-\" + name + (className ? \" \" + className : \"\")} style={{ fontSize: size, lineHeight: 1, display: \"inline-block\", flexShrink: 0, fontStyle: \"normal\", color, ...style }}></i>;\n}\n","icons/Unicon.vue":"<script setup>\nimport { onMounted } from 'vue'\n// Unicons line icon — 1,206 glyphs from the official Unicons CDN webfont. name = Unicons id without \"uil-\".\nconst props = defineProps({ name: String, size: { type: Number, default: 16 }, color: String, style: Object })\nonMounted(() => {\n if (document.getElementById('kb-unicons-css')) return\n const l = document.createElement('link')\n l.id = 'kb-unicons-css'; l.rel = 'stylesheet'\n l.href = 'https://unicons.iconscout.com/release/v4.0.8/css/line.css'\n document.head.appendChild(l)\n})\n</script>\n<template>\n <i aria-hidden=\"true\" :class=\"'uil uil-' + name\" :style=\"{ fontSize: size + 'px', lineHeight: 1, display: 'inline-block', flexShrink: 0, fontStyle: 'normal', color, ...style }\" />\n</template>\n"});
|
|
1
|
+
window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"icons/Icon.jsx":"import React from \"react\";\n// Keybricks icon set — the source file uses Lucide stroke glyphs (24-grid, stroke 2,\n// round caps/joins, currentColor) rendered at 14/16px. Path data below is from the\n// public Lucide set, matching the glyphs used across Keyfunc UI v8, plus thirteen Keybricks\n// glyphs (event … time-restatement, ai, ai-sparkles) drawn to the same rules.\nexport const ICON_PATHS = {\n search: '<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>',\n bell: '<path d=\"M10.268 21a2 2 0 0 0 3.464 0\"/><path d=\"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326\"/>',\n sparkles: '<path d=\"M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z\"/><path d=\"M20 3v4\"/><path d=\"M22 5h-4\"/><path d=\"M4 17v2\"/><path d=\"M5 18H3\"/>',\n \"panel-left\": '<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M9 3v18\"/>',\n house: '<path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\"/><path d=\"M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/>',\n file: '<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/>',\n \"file-text\": '<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/><path d=\"M10 9H8\"/><path d=\"M16 13H8\"/><path d=\"M16 17H8\"/>',\n \"file-check\": '<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/><path d=\"m9 15 2 2 4-4\"/>',\n \"layout-grid\": '<rect width=\"7\" height=\"7\" x=\"3\" y=\"3\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"14\" y=\"3\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"14\" y=\"14\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"3\" y=\"14\" rx=\"1\"/>',\n database: '<ellipse cx=\"12\" cy=\"5\" rx=\"9\" ry=\"3\"/><path d=\"M3 5V19A9 3 0 0 0 21 19V5\"/><path d=\"M3 12A9 3 0 0 0 21 12\"/>',\n scale: '<path d=\"m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z\"/><path d=\"m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z\"/><path d=\"M7 21h10\"/><path d=\"M12 3v18\"/><path d=\"M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2\"/>',\n network: '<rect x=\"16\" y=\"16\" width=\"6\" height=\"6\" rx=\"1\"/><rect x=\"2\" y=\"16\" width=\"6\" height=\"6\" rx=\"1\"/><rect x=\"9\" y=\"2\" width=\"6\" height=\"6\" rx=\"1\"/><path d=\"M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3\"/><path d=\"M12 12V8\"/>',\n folder: '<path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/>',\n \"folder-open\": '<path d=\"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2\"/>',\n \"folder-cog\": '<path d=\"M10.5 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v3\"/><circle cx=\"18\" cy=\"18\" r=\"3\"/><path d=\"M18 14v1\"/><path d=\"M18 21v1\"/><path d=\"M22 18h-1\"/><path d=\"M15 18h-1\"/><path d=\"m21.2 16.2-.9.5\"/><path d=\"m15.7 19.3-.9.5\"/><path d=\"m21.2 19.8-.9-.5\"/><path d=\"m15.7 16.7-.9-.5\"/>',\n settings: '<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>',\n \"settings-2\": '<path d=\"M20 7h-9\"/><path d=\"M14 17H5\"/><circle cx=\"17\" cy=\"17\" r=\"3\"/><circle cx=\"7\" cy=\"7\" r=\"3\"/>',\n moon: '<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"/>',\n \"circle-help\": '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"/><path d=\"M12 17h.01\"/>',\n box: '<path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/>',\n workflow: '<rect width=\"8\" height=\"8\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M7 11v4a2 2 0 0 0 2 2h4\"/><rect width=\"8\" height=\"8\" x=\"13\" y=\"13\" rx=\"2\"/>',\n shield: '<path d=\"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z\"/>',\n \"refresh-cw\": '<path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/>',\n \"chart-line\": '<path d=\"M3 3v16a2 2 0 0 0 2 2h16\"/><path d=\"m19 9-5 5-4-4-3 3\"/>',\n \"chart-column\": '<path d=\"M3 3v16a2 2 0 0 0 2 2h16\"/><path d=\"M18 17V9\"/><path d=\"M13 17V5\"/><path d=\"M8 17v-3\"/>',\n \"chart-no-axes-column\": '<line x1=\"18\" x2=\"18\" y1=\"20\" y2=\"10\"/><line x1=\"12\" x2=\"12\" y1=\"20\" y2=\"4\"/><line x1=\"6\" x2=\"6\" y1=\"20\" y2=\"14\"/>',\n \"list-todo\": '<rect x=\"3\" y=\"5\" width=\"6\" height=\"6\" rx=\"1\"/><path d=\"m3 17 2 2 4-4\"/><path d=\"M13 6h8\"/><path d=\"M13 12h8\"/><path d=\"M13 18h8\"/>',\n \"circle-check\": '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/>',\n target: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/>',\n \"book-open\": '<path d=\"M12 7v14\"/><path d=\"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\"/>',\n \"chevron-down\": '<path d=\"m6 9 6 6 6-6\"/>',\n \"chevron-right\": '<path d=\"m9 18 6-6-6-6\"/>',\n \"chevron-left\": '<path d=\"m15 18-6-6 6-6\"/>',\n \"chevron-up\": '<path d=\"m18 15-6-6-6 6\"/>',\n \"arrow-left\": '<path d=\"m12 19-7-7 7-7\"/><path d=\"M19 12H5\"/>',\n \"arrow-right\": '<path d=\"M5 12h14\"/><path d=\"m12 5 7 7-7 7\"/>',\n \"arrow-right-left\": '<path d=\"m16 3 4 4-4 4\"/><path d=\"M20 7H4\"/><path d=\"m8 21-4-4 4-4\"/><path d=\"M4 17h16\"/>',\n table: '<path d=\"M12 3v18\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M3 9h18\"/><path d=\"M3 15h18\"/>',\n layers: '<path d=\"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z\"/><path d=\"m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65\"/><path d=\"m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65\"/>',\n \"grid-3x3\": '<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M3 9h18\"/><path d=\"M3 15h18\"/><path d=\"M9 3v18\"/><path d=\"M15 3v18\"/>',\n \"text-search\": '<path d=\"M21 6H3\"/><path d=\"M10 12H3\"/><path d=\"M10 18H3\"/><circle cx=\"17\" cy=\"15\" r=\"3\"/><path d=\"m21 19-1.9-1.9\"/>',\n shuffle: '<path d=\"M2 18h1.4c1.3 0 2.5-.6 3.3-1.7l6.1-8.6c.7-1.1 2-1.7 3.3-1.7H22\"/><path d=\"m18 2 4 4-4 4\"/><path d=\"M2 6h1.9c1.5 0 2.9.9 3.6 2.2\"/><path d=\"M22 18h-5.9c-1.3 0-2.6-.7-3.3-1.8l-.5-.8\"/><path d=\"m18 14 4 4-4 4\"/>',\n split: '<path d=\"M16 3h5v5\"/><path d=\"M8 3H3v5\"/><path d=\"M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3\"/><path d=\"m15 9 6-6\"/>',\n menu: '<line x1=\"4\" x2=\"20\" y1=\"12\" y2=\"12\"/><line x1=\"4\" x2=\"20\" y1=\"6\" y2=\"6\"/><line x1=\"4\" x2=\"20\" y1=\"18\" y2=\"18\"/>',\n list: '<line x1=\"8\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"8\" x2=\"21\" y1=\"12\" y2=\"12\"/><line x1=\"8\" x2=\"21\" y1=\"18\" y2=\"18\"/><line x1=\"3\" x2=\"3.01\" y1=\"6\" y2=\"6\"/><line x1=\"3\" x2=\"3.01\" y1=\"12\" y2=\"12\"/><line x1=\"3\" x2=\"3.01\" y1=\"18\" y2=\"18\"/>',\n funnel: '<polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"/>',\n \"sliders-horizontal\": '<line x1=\"21\" x2=\"14\" y1=\"4\" y2=\"4\"/><line x1=\"10\" x2=\"3\" y1=\"4\" y2=\"4\"/><line x1=\"21\" x2=\"12\" y1=\"12\" y2=\"12\"/><line x1=\"8\" x2=\"3\" y1=\"12\" y2=\"12\"/><line x1=\"21\" x2=\"16\" y1=\"20\" y2=\"20\"/><line x1=\"12\" x2=\"3\" y1=\"20\" y2=\"20\"/><line x1=\"14\" x2=\"14\" y1=\"2\" y2=\"6\"/><line x1=\"8\" x2=\"8\" y1=\"10\" y2=\"14\"/><line x1=\"16\" x2=\"16\" y1=\"18\" y2=\"22\"/>',\n \"trending-up\": '<polyline points=\"22 7 13.5 15.5 8.5 10.5 2 17\"/><polyline points=\"16 7 22 7 22 13\"/>',\n code: '<path d=\"m16 18 6-6-6-6\"/><path d=\"m8 6-6 6 6 6\"/>',\n merge: '<path d=\"m8 6 4-4 4 4\"/><path d=\"M12 2v10.3a4 4 0 0 1-1.172 2.872L4 22\"/><path d=\"m20 22-5-5\"/>',\n upload: '<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><polyline points=\"17 8 12 3 7 8\"/><line x1=\"12\" x2=\"12\" y1=\"3\" y2=\"15\"/>',\n download: '<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><polyline points=\"7 10 12 15 17 10\"/><line x1=\"12\" x2=\"12\" y1=\"15\" y2=\"3\"/>',\n check: '<path d=\"M20 6 9 17l-5-5\"/>',\n x: '<path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/>',\n plus: '<path d=\"M5 12h14\"/><path d=\"M12 5v14\"/>',\n minus: '<path d=\"M5 12h14\"/>',\n ellipsis: '<circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/>',\n play: '<polygon points=\"6 3 20 12 6 21 6 3\"/>',\n send: '<path d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\"/><path d=\"m21.854 2.147-10.94 10.939\"/>',\n eye: '<path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>',\n copy: '<rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/>',\n \"trash-2\": '<path d=\"M3 6h18\"/><path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"/><path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"/><line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\"/><line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\"/>',\n clock: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/>',\n calendar: '<path d=\"M8 2v4\"/><path d=\"M16 2v4\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\"/><path d=\"M3 10h18\"/>',\n \"undo-2\": '<path d=\"M9 14 4 9l5-5\"/><path d=\"M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11\"/>',\n \"redo-2\": '<path d=\"m15 14 5-5-5-5\"/><path d=\"M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13\"/>',\n history: '<path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"/><path d=\"M3 3v5h5\"/><path d=\"M12 7v5l4 2\"/>',\n bug: '<path d=\"m8 2 1.88 1.88\"/><path d=\"M14.12 3.88 16 2\"/><path d=\"M9 7.13v-1a3.003 3.003 0 1 1 6 0v1\"/><path d=\"M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6\"/><path d=\"M12 20v-9\"/><path d=\"M6.53 9C4.6 8.8 3 7.1 3 5\"/><path d=\"M6 13H2\"/><path d=\"M3 21c0-2.1 1.7-3.9 3.8-4\"/><path d=\"M20.97 5c0 2.1-1.6 3.8-3.5 4\"/><path d=\"M22 13h-4\"/><path d=\"M17.2 17c2.1.1 3.8 1.9 3.8 4\"/>',\n info: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/>',\n \"triangle-alert\": '<path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 20h16a2 2 0 0 0 1.73-2Z\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/>',\n calendar: '<path d=\"M8 2v4\"/><path d=\"M16 2v4\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\"/><path d=\"M3 10h18\"/>',\n // Keybricks glyphs — drawn to the core-set rules (see readme.md › Iconography)\n event: '<path d=\"M17 2 22 12 17 22 12 12Z\"/><path d=\"M7 2 2 12l5 10\"/><path d=\"M12 2 7 12l5 10\"/>',\n consolidation: '<rect width=\"4\" height=\"4\" x=\"2\" y=\"17\" rx=\"1\"/><rect width=\"4\" height=\"4\" x=\"10\" y=\"17\" rx=\"1\"/><rect width=\"4\" height=\"4\" x=\"18\" y=\"17\" rx=\"1\"/><path d=\"M4 17v-5a2 2 0 0 1 2-2h2\"/><path d=\"M20 17v-5a2 2 0 0 0-2-2h-2\"/><path d=\"M12 17v-4\"/><path d=\"M12 3v6\"/><path d=\"M9 6h6\"/>',\n journal: '<rect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"/><path d=\"M2 8h20\"/><path d=\"M12 8v12\"/><path d=\"M6 12h2\"/><path d=\"M16 16h2\"/>',\n reconciliation: '<path d=\"M3 6h8\"/><path d=\"m15 4 2 2 4-4\"/><path d=\"M3 10h18\"/><path d=\"M3 18h8\"/><path d=\"m15 16 2 2 4-4\"/><path d=\"M3 22h18\"/>',\n \"view-tree\": '<rect width=\"4\" height=\"4\" x=\"4\" y=\"2\" rx=\"1\"/><rect width=\"4\" height=\"4\" x=\"16\" y=\"10\" rx=\"1\"/><rect width=\"4\" height=\"4\" x=\"16\" y=\"18\" rx=\"1\"/><path d=\"M6 6v4a2 2 0 0 0 2 2h8\"/><path d=\"M6 10v8a2 2 0 0 0 2 2h8\"/>',\n \"drilldown\": '<path d=\"M12 2v11\"/><path d=\"m8 9 4 4 4-4\"/><path d=\"M19 17H5\"/><path d=\"M16 21H8\"/>',\n \"add-below\": '<path d=\"M11 2v10\"/><path d=\"M6 7h10\"/><path d=\"M22 9v5a4 4 0 0 1-4 4H2\"/><path d=\"m6 14-4 4 4 4\"/>',\n \"copy-data\": '<path d=\"M14 22h5a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-5\"/><path d=\"m11 16 4-4-4-4\"/><path d=\"M3 22v-6a4 4 0 0 1 4-4h8\"/>',\n \"task-execution\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><path d=\"M8 14h8\"/><path d=\"m13 11 3 3-3 3\"/>',\n \"time-future\": '<path d=\"M20 6A10 10 0 1 0 22 12\"/><path d=\"M12 7v5h5\"/><path d=\"M21 2v5h-5\"/>',\n \"time-restatement\": '<path d=\"M2 12A10 10 0 1 0 4 6\"/><path d=\"M12 7v5h5\"/><path d=\"M3 2v5h5\"/>',\n // AI glyphs (2.3.0) — traced from the two reference glyphs, redrawn to core rules (stroke 2, no fill, integer endpoints, >=4 between separate elements)\n ai: '<path d=\"M9 4H6a4 4 0 0 0-4 4v10a4 4 0 0 0 4 4h10a4 4 0 0 0 4-4v-3\"/><path d=\"M16 2c1.2 3 3 4.8 6 6c-3 1.2-4.8 3-6 6c-1.2-3-3-4.8-6-6c3-1.2 4.8-3 6-6Z\"/><path d=\"M9 12c.6 1.5 1.5 2.4 3 3c-1.5.6-2.4 1.5-3 3c-.6-1.5-1.5-2.4-3-3c1.5-.6 2.4-1.5 3-3Z\"/>',\n \"ai-sparkles\": '<path d=\"M9 2c1.4 3.5 3.5 5.6 7 7c-3.5 1.4-5.6 3.5-7 7c-1.4-3.5-3.5-5.6-7-7c3.5-1.4 5.6-3.5 7-7Z\"/><path d=\"M17 13c.8 2 2 3.2 4 4c-2 .8-3.2 2-4 4c-.8-2-2-3.2-4-4c2-.8 3.2-2 4-4Z\"/>',\n};\n\nimport { FEATHER_PATHS } from \"./feather-paths.js\";\n\n/** Lucide-style stroke icon. name: key of ICON_PATHS (core set) or any Feather icon name (feather-paths.js fallback); size in px; strokeWidth defaults to 2 (24-grid). Browse both sets with keywords in the Icon Gallery card. */\nexport function Icon({ name, size = 16, strokeWidth = 2, color, style, className }) {\n const body = ICON_PATHS[name] || FEATHER_PATHS[name] || \"\";\n return (\n <svg\n width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n strokeWidth={strokeWidth} strokeLinecap=\"round\" strokeLinejoin=\"round\"\n className={className}\n style={{ display: \"block\", flexShrink: 0, color, ...style }}\n dangerouslySetInnerHTML={{ __html: body }}\n />\n );\n}\n","icons/Icon.vue":"<script setup>\n// Keybricks icon set — Lucide stroke glyphs (24-grid, stroke 2, round caps/joins, currentColor), rendered at 14/16px.\n// Full path dictionary mirrors icons/Icon.jsx's ICON_PATHS (81 entries) — see that file for the complete list.\nimport { VUE_ICON_PATHS } from './icon-paths.js'\nimport { FEATHER_PATHS } from './feather-paths.js'\nconst props = defineProps({ name: String, size: { type: Number, default: 16 }, strokeWidth: { type: Number, default: 2 }, color: String, style: Object })\nconst body = (n) => VUE_ICON_PATHS[n] || FEATHER_PATHS[n] || ''\n</script>\n<template>\n <svg :width=\"size\" :height=\"size\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" :stroke-width=\"strokeWidth\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n :style=\"{ display: 'block', flexShrink: 0, color, ...style }\" v-html=\"body(name)\" />\n</template>\n","icons/Unicon.jsx":"import React from \"react\";\n\nlet uniconsCss = false;\nfunction ensureUniconsCss() {\n if (uniconsCss || document.getElementById(\"kb-unicons-css\")) { uniconsCss = true; return; }\n const l = document.createElement(\"link\");\n l.id = \"kb-unicons-css\"; l.rel = \"stylesheet\";\n l.href = \"https://unicons.iconscout.com/release/v4.0.8/css/line.css\";\n document.head.appendChild(l); uniconsCss = true;\n}\n\n/** Unicons line icon (iconscout.com/unicons, Apache 2.0) — 1,206 glyphs loaded from the official Unicons CDN webfont. name is the Unicons id without the \"uil-\" prefix (e.g. \"chart-pie-alt\"). Browse names + keywords in the Unicons gallery card; keyword index lives in assets/icons/unicons-index.json. */\nexport function Unicon({ name, size = 16, color, style, className }) {\n React.useEffect(ensureUniconsCss, []);\n return <i aria-hidden=\"true\" className={\"uil uil-\" + name + (className ? \" \" + className : \"\")} style={{ fontSize: size, lineHeight: 1, display: \"inline-block\", flexShrink: 0, fontStyle: \"normal\", color, ...style }}></i>;\n}\n","icons/Unicon.vue":"<script setup>\nimport { onMounted } from 'vue'\n// Unicons line icon — 1,206 glyphs from the official Unicons CDN webfont. name = Unicons id without \"uil-\".\nconst props = defineProps({ name: String, size: { type: Number, default: 16 }, color: String, style: Object })\nonMounted(() => {\n if (document.getElementById('kb-unicons-css')) return\n const l = document.createElement('link')\n l.id = 'kb-unicons-css'; l.rel = 'stylesheet'\n l.href = 'https://unicons.iconscout.com/release/v4.0.8/css/line.css'\n document.head.appendChild(l)\n})\n</script>\n<template>\n <i aria-hidden=\"true\" :class=\"'uil uil-' + name\" :style=\"{ fontSize: size + 'px', lineHeight: 1, display: 'inline-block', flexShrink: 0, fontStyle: 'normal', color, ...style }\" />\n</template>\n"});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"navigation/Breadcrumb.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nfunction Crumb({ label, last, hovered, onSelect }) {\n const [hover, setHover] = React.useState(false);\n const on = (hover || hovered) && !last;\n return (\n <span role={last ? undefined : \"button\"} tabIndex={last ? undefined : 0}\n onClick={() => !last && onSelect && onSelect(label)}\n onKeyDown={(e) => { if (!last && (e.key === \"Enter\" || e.key === \" \")) { e.preventDefault(); onSelect && onSelect(label); } }}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: last || on ? \"var(--text-heading)\" : \"var(--text-secondary)\", fontWeight: last ? \"var(--weight-medium)\" : \"var(--weight-regular)\", cursor: last ? \"default\" : \"pointer\", borderRadius: \"var(--radius-sm)\", transition: \"var(--t-hover)\" }}>{label}</span>\n );\n}\n\nfunction Sep() {\n return <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\" }}><Icon name=\"chevron-right\" size={13} /></span>;\n}\n\n/** Overflow crumb: an ellipsis button that drops down the levels hidden by collapsing. */\nfunction Overflow({ items, onSelect }) {\n const [open, setOpen] = React.useState(false);\n const [hover, setHover] = React.useState(false);\n const root = React.useRef(null);\n React.useEffect(() => {\n if (!open) return;\n const h = (e) => { if (root.current && !root.current.contains(e.target)) setOpen(false); };\n document.addEventListener(\"mousedown\", h);\n return () => document.removeEventListener(\"mousedown\", h);\n }, [open]);\n return (\n <span ref={root} style={{ position: \"relative\", display: \"inline-flex\" }}>\n <span role=\"button\" tabIndex={0} aria-label={`Show ${items.length} hidden levels`} aria-expanded={open}\n onClick={() => setOpen(!open)}\n onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); setOpen(!open); } if (e.key === \"Escape\") setOpen(false); }}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", width: 20, height: 18, borderRadius: \"var(--radius-sm)\", cursor: \"pointer\", color: open || hover ? \"var(--text-heading)\" : \"var(--text-muted)\", background: open ? \"var(--bg-active)\" : hover ? \"var(--bg-hover)\" : \"transparent\", transition: \"var(--t-hover)\" }}>\n <Icon name=\"ellipsis\" size={14} />\n </span>\n {open ? (\n <span style={{ position: \"absolute\", top: \"calc(100% + 5px)\", left: 0, zIndex: \"var(--z-menu)\", minWidth: 168, maxWidth: 260, background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-md)\", boxShadow: \"var(--shadow-menu)\", padding: 4, display: \"flex\", flexDirection: \"column\" }}>\n {items.map((it, i) => <OverflowItem key={it} label={it} depth={i} onSelect={(l) => { setOpen(false); onSelect && onSelect(l); }} />)}\n </span>\n ) : null}\n </span>\n );\n}\n\nfunction OverflowItem({ label, depth, onSelect }) {\n const [hover, setHover] = React.useState(false);\n return (\n <span role=\"menuitem\" tabIndex={0} onClick={() => onSelect(label)}\n onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); onSelect(label); } }}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ display: \"flex\", alignItems: \"center\", gap: 6, padding: \"6px 9px\", paddingLeft: 9 + depth * 12, borderRadius: \"var(--radius-sm)\", cursor: \"pointer\", background: hover ? \"var(--bg-hover)\" : \"transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\", transition: \"var(--t-hover)\" }}>\n {depth > 0 ? <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", flexShrink: 0 }}><Icon name=\"chevron-right\" size={11} /></span> : null}\n {label}\n </span>\n );\n}\n\n/** Breadcrumb trail — 12px, chevron separators, last crumb in heading color. Clickable crumbs darken on hover and carry press + keyboard focus from the global interaction layer. Trails longer than `maxItems` collapse to first · … · last two, with the hidden levels in a dropdown on the ellipsis. */\nexport function Breadcrumb({ items = [], onSelect, maxItems = 4, hoveredIndex, style }) {\n const collapse = items.length > maxItems && maxItems >= 3;\n const tailCount = maxItems - 2;\n const head = collapse ? items.slice(0, 1) : items;\n const hidden = collapse ? items.slice(1, items.length - tailCount) : [];\n const tail = collapse ? items.slice(items.length - tailCount) : [];\n const lastLabel = items[items.length - 1];\n return (\n <nav style={{ display: \"flex\", alignItems: \"center\", gap: 4, ...style }}>\n {head.map((it, i) => (\n <React.Fragment key={it}>\n {i > 0 ? <Sep /> : null}\n <Crumb label={it} last={it === lastLabel} hovered={hoveredIndex === i} onSelect={onSelect} />\n </React.Fragment>\n ))}\n {collapse ? (\n <React.Fragment>\n <Sep />\n <Overflow items={hidden} onSelect={onSelect} />\n {tail.map((it) => (\n <React.Fragment key={it}>\n <Sep />\n <Crumb label={it} last={it === lastLabel} onSelect={onSelect} />\n </React.Fragment>\n ))}\n </React.Fragment>\n ) : null}\n </nav>\n );\n}\n\n/** Theme toggle — flips <html data-theme> and persists to localStorage(\"kb-theme\"). Pass `dark`+`onToggle` to control it externally. */\nexport function ThemeToggle({ size = 34, dark: darkProp, onToggle, style }) {\n const controlled = darkProp != null;\n const [darkState, setDark] = React.useState(() => (typeof document !== \"undefined\" && document.documentElement.dataset.theme === \"dark\"));\n const dark = controlled ? darkProp : darkState;\n React.useEffect(() => {\n if (controlled || typeof document === \"undefined\") return;\n document.documentElement.dataset.theme = dark ? \"dark\" : \"light\";\n try { localStorage.setItem(\"kb-theme\", dark ? \"dark\" : \"light\"); } catch (e) {}\n }, [dark, controlled]);\n const [hover, setHover] = React.useState(false);\n return (\n <button type=\"button\" aria-label={dark ? \"Switch to light mode\" : \"Switch to dark mode\"} onClick={() => (controlled ? onToggle && onToggle(!dark) : setDark(!dark))}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ width: size, height: size, borderRadius: \"var(--radius-md)\", border: \"none\", cursor: \"pointer\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", background: hover ? \"var(--bg-hover)\" : \"transparent\", color: \"var(--text-body)\", padding: 0, transition: \"var(--t-hover)\", ...style }}>\n <Icon name={dark ? \"sparkles\" : \"moon\"} size={18} />\n </button>\n );\n}\n\n/** Reads the persisted theme and applies it before first paint. Call once at app start. */\nexport function applyStoredTheme() {\n if (typeof document === \"undefined\") return;\n let t = \"light\";\n try { t = localStorage.getItem(\"kb-theme\") || \"light\"; } catch (e) {}\n document.documentElement.dataset.theme = t;\n}\n","navigation/Breadcrumb.vue":"<script setup>\nimport { ref, computed, watch, onUnmounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Breadcrumb trail — 12px, chevron separators, last crumb in heading color.\n// Clickable crumbs darken on hover (no underline) and carry press + keyboard focus from the global interaction layer.\n// Trails longer than maxItems collapse to first · … · last two, with the hidden levels in a dropdown on the ellipsis.\nconst props = defineProps({ items: { type: Array, default: () => [] }, maxItems: { type: Number, default: 4 }, hoveredIndex: { type: Number, default: null }, style: Object })\nconst emit = defineEmits(['select'])\nconst hover = ref(-1)\nconst ovHover = ref(false)\nconst menuHover = ref(-1)\nconst open = ref(false)\nconst root = ref(null)\nconst collapse = computed(() => props.items.length > props.maxItems && props.maxItems >= 3)\nconst tailCount = computed(() => props.maxItems - 2)\nconst head = computed(() => collapse.value ? props.items.slice(0, 1) : props.items)\nconst hidden = computed(() => collapse.value ? props.items.slice(1, props.items.length - tailCount.value) : [])\nconst tail = computed(() => collapse.value ? props.items.slice(props.items.length - tailCount.value) : [])\nconst lastLabel = computed(() => props.items[props.items.length - 1])\nconst isLast = (it) => it === lastLabel.value\nconst on = (i, it) => (hover.value === i || props.hoveredIndex === i) && !isLast(it)\nfunction pick(it) { if (!isLast(it)) emit('select', it) }\nfunction pickHidden(it) { open.value = false; emit('select', it) }\nfunction onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }\nwatch(open, (v) => { if (v) document.addEventListener('mousedown', onDoc); else document.removeEventListener('mousedown', onDoc) })\nonUnmounted(() => document.removeEventListener('mousedown', onDoc))\nconst crumbStyle = (i, it) => ({ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: isLast(it) || on(i, it) ? 'var(--text-heading)' : 'var(--text-secondary)', fontWeight: isLast(it) ? 'var(--weight-medium)' : 'var(--weight-regular)', cursor: isLast(it) ? 'default' : 'pointer', borderRadius: 'var(--radius-sm)', transition: 'var(--t-hover)' })\n</script>\n<template>\n <nav :style=\"{ display: 'flex', alignItems: 'center', gap: 4, ...style }\">\n <template v-for=\"(it, i) in head\" :key=\"it\">\n <span v-if=\"i > 0\" style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"chevron-right\" :size=\"13\" /></span>\n <span :role=\"isLast(it) ? undefined : 'button'\" :tabindex=\"isLast(it) ? undefined : 0\"\n @click=\"pick(it)\" @keydown.enter.prevent=\"pick(it)\" @keydown.space.prevent=\"pick(it)\"\n @mouseenter=\"hover = i\" @mouseleave=\"hover = -1\" :style=\"crumbStyle(i, it)\">{{ it }}</span>\n </template>\n <template v-if=\"collapse\">\n <span style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"chevron-right\" :size=\"13\" /></span>\n <span ref=\"root\" style=\"position:relative;display:inline-flex\">\n <span role=\"button\" tabindex=\"0\" :aria-label=\"`Show ${hidden.length} hidden levels`\" :aria-expanded=\"open\"\n @click=\"open = !open\" @keydown.enter.prevent=\"open = !open\" @keydown.space.prevent=\"open = !open\" @keydown.esc=\"open = false\"\n @mouseenter=\"ovHover = true\" @mouseleave=\"ovHover = false\"\n :style=\"{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '20px', height: '18px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', color: open || ovHover ? 'var(--text-heading)' : 'var(--text-muted)', background: open ? 'var(--bg-active)' : ovHover ? 'var(--bg-hover)' : 'transparent', transition: 'var(--t-hover)' }\">\n <Icon name=\"ellipsis\" :size=\"14\" />\n </span>\n <span v-if=\"open\" style=\"position:absolute;top:calc(100% + 5px);left:0;z-index:var(--z-menu);min-width:168px;max-width:260px;background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;display:flex;flex-direction:column\">\n <span v-for=\"(h, hi) in hidden\" :key=\"h\" role=\"menuitem\" tabindex=\"0\"\n @click=\"pickHidden(h)\" @keydown.enter.prevent=\"pickHidden(h)\" @keydown.space.prevent=\"pickHidden(h)\"\n @mouseenter=\"menuHover = hi\" @mouseleave=\"menuHover = -1\"\n :style=\"{ display: 'flex', alignItems: 'center', gap: '6px', padding: '6px 9px', paddingLeft: (9 + hi * 12) + 'px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: menuHover === hi ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', transition: 'var(--t-hover)' }\">\n <span v-if=\"hi > 0\" style=\"color:var(--text-muted);display:inline-flex;flex-shrink:0\"><Icon name=\"chevron-right\" :size=\"11\" /></span>{{ h }}\n </span>\n </span>\n </span>\n <template v-for=\"it in tail\" :key=\"it\">\n <span style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"chevron-right\" :size=\"13\" /></span>\n <span :role=\"isLast(it) ? undefined : 'button'\" :tabindex=\"isLast(it) ? undefined : 0\"\n @click=\"pick(it)\" @keydown.enter.prevent=\"pick(it)\" @keydown.space.prevent=\"pick(it)\"\n :style=\"crumbStyle(-1, it)\">{{ it }}</span>\n </template>\n </template>\n </nav>\n</template>\n\n<!-- applyStoredTheme() — reads the persisted theme and applies it before first paint. Call once at app start (main.js): -->\n<!--\nexport function applyStoredTheme() {\n let t = 'light'\n try { t = localStorage.getItem('kb-theme') || 'light' } catch (e) {}\n document.documentElement.dataset.theme = t\n}\n-->\n","navigation/DocTree.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { SearchInput } from \"../forms/SearchInput.jsx\";\n\nconst docHues = { orange: \"var(--cat-transform)\", green: \"var(--cat-source)\", purple: \"var(--cat-merge)\", blue: \"var(--info-fg)\", teal: \"var(--cat-output)\", gray: \"var(--text-muted)\" };\n\n/** Secondary documents panel — 240px: title header, search row, 30px tree items. items: [{label, kind:\"folder\"|\"doc\", hue, icon}]. */\nexport function DocTree({ title = \"Documents\", items = [], active, onSelect, style }) {\n return (\n <div style={{ width: \"var(--sidebar-w)\", background: \"var(--bg-surface)\", borderRight: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ height: \"var(--topbar-h)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"0 12px 0 16px\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n <IconButton icon=\"chevron-right\" size=\"md\" muted />\n </div>\n <div style={{ padding: \"12px 16px 8px\", borderBottom: \"1px solid var(--border-subtle)\" }}>\n <SearchInput bare placeholder=\"Search...\" style={{ gap: 8 }} />\n </div>\n <div style={{ padding: 8, display: \"flex\", flexDirection: \"column\", gap: 2, overflowY: \"auto\" }}>\n {items.map((it) => {\n const is = active === it.label;\n return (\n <div key={it.label} role=\"treeitem\" tabIndex={0} onClick={() => onSelect && onSelect(it.label)}\n style={{ height: \"var(--h-tree-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 8, padding: \"6px 8px\", boxSizing: \"border-box\", cursor: \"pointer\", background: is ? \"var(--bg-active)\" : \"transparent\" }}\n onMouseEnter={(e) => { if (!is) e.currentTarget.style.background = \"var(--bg-hover)\"; }}\n onMouseLeave={(e) => { if (!is) e.currentTarget.style.background = \"transparent\"; }}>\n <span style={{ color: it.kind === \"folder\" ? \"var(--text-muted)\" : docHues[it.hue || \"gray\"], display: \"inline-flex\" }}>\n <Icon name={it.icon || (it.kind === \"folder\" ? \"folder\" : \"file-text\")} size={16} />\n </span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{it.label}</span>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n","navigation/DocTree.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport SearchInput from '../forms/SearchInput.vue'\nconst docHues = { orange: 'var(--cat-transform)', green: 'var(--cat-source)', purple: 'var(--cat-merge)', blue: 'var(--info-fg)', teal: 'var(--cat-output)', gray: 'var(--text-muted)' }\n// Secondary documents panel — 240px: title header, search row, 30px tree items. items: [{label, kind:\"folder\"|\"doc\", hue, icon}].\ndefineProps({ title: { type: String, default: 'Documents' }, items: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['select'])\n</script>\n<template>\n <div :style=\"{ width: 'var(--sidebar-w)', background: 'var(--bg-surface)', borderRight: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"height:var(--topbar-h);border-bottom:1px solid var(--border-card);display:flex;align-items:center;justify-content:space-between;padding:0 12px 0 16px;box-sizing:border-box;flex-shrink:0\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <IconButton icon=\"chevron-right\" size=\"md\" muted />\n </div>\n <div style=\"padding:12px 16px 8px;border-bottom:1px solid var(--border-subtle)\"><SearchInput bare placeholder=\"Search...\" style=\"gap:8px\" /></div>\n <div style=\"padding:8px;display:flex;flex-direction:column;gap:2px;overflow-y:auto\">\n <div v-for=\"it in items\" :key=\"it.label\" @click=\"emit('select', it.label)\"\n :style=\"{ height: 'var(--h-tree-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: 8, padding: '6px 8px', boxSizing: 'border-box', cursor: 'pointer', background: active === it.label ? 'var(--bg-active)' : 'transparent' }\">\n <span :style=\"{ color: it.kind === 'folder' ? 'var(--text-muted)' : docHues[it.hue || 'gray'], display: 'inline-flex' }\"><Icon :name=\"it.icon || (it.kind === 'folder' ? 'folder' : 'file-text')\" :size=\"16\" /></span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ it.label }}</span>\n </div>\n </div>\n </div>\n</template>\n","navigation/Sidebar.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nfunction NavItem({ icon, label, active, indent, collapsed, badge, onClick }) {\n const [hover, setHover] = React.useState(false);\n return (\n <div role=\"button\" tabIndex={0} onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ height: indent ? \"var(--h-subnav-item)\" : \"var(--h-nav-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", justifyContent: collapsed ? \"center\" : \"flex-start\", gap: 10, padding: collapsed ? \"8px 10px\" : indent ? \"6px 10px\" : \"8px 10px\", boxSizing: \"border-box\", cursor: \"pointer\", background: active ? \"var(--bg-active)\" : hover ? \"var(--bg-hover)\" : \"transparent\", color: active ? \"var(--text-heading)\" : \"var(--text-body)\", transition: \"var(--t-hover)\" }}>\n <Icon name={icon} size={indent ? 14 : 16} />\n {!collapsed ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", whiteSpace: \"nowrap\", flex: 1 }}>{label}</span> : null}\n {!collapsed && badge ? badge : null}\n </div>\n );\n}\n\n/**\n * Keybricks app sidebar — 240px (collapsed: 64px icon rail). White, hairline right border.\n * items: [{icon,label,children:[{icon,label}]}]; active by label; footer items pinned.\n */\nexport function Sidebar({ items = [], footerItems = [{ icon: \"circle-help\", label: \"Help\" }, { icon: \"settings\", label: \"Settings\" }, { icon: \"moon\", label: \"Dark Mode\" }], active, onSelect, collapsed = false, onToggleCollapse, logo = \"assets/keybricks-logo.png\", brand = \"Keybricks\", height = \"100%\", style }) {\n const [openSections, setOpen] = React.useState(() => items.filter((i) => i.children && (i.open || (i.children.some((c) => c.label === active)))).map((i) => i.label));\n const toggleSection = (l) => setOpen(openSections.includes(l) ? openSections.filter((x) => x !== l) : [...openSections, l]);\n return (\n <aside style={{ width: collapsed ? \"var(--rail-w)\" : \"var(--sidebar-w)\", height, background: \"var(--bg-surface)\", borderRight: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, transition: \"width .15s ease\", ...style }}>\n <div style={{ height: \"var(--topbar-h)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", gap: 10, padding: \"0 16px\", boxSizing: \"border-box\", justifyContent: collapsed ? \"center\" : \"flex-start\", flexShrink: 0 }}>\n <img src={logo} alt=\"\" style={{ width: 24, height: 24, objectFit: \"contain\" }} />\n {!collapsed ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-lg)\", lineHeight: \"var(--leading-lg)\", letterSpacing: \"-.609px\", color: \"var(--text-primary)\" }}>{brand}</span> : null}\n </div>\n <nav style={{ flex: 1, overflowY: \"auto\", padding: 8, display: \"flex\", flexDirection: \"column\", gap: 0 }}>\n {onToggleCollapse ? <NavItem icon=\"panel-left\" label=\"Collapse\" collapsed={collapsed} onClick={onToggleCollapse} /> : null}\n {items.map((it) => it.children ? (\n <div key={it.label}>\n <div onClick={() => toggleSection(it.label)} style={{ height: \"var(--h-subnav-item)\", display: \"flex\", alignItems: \"center\", gap: 10, padding: collapsed ? \"6px 10px\" : \"6px 10px\", justifyContent: collapsed ? \"center\" : \"flex-start\", cursor: \"pointer\", color: \"var(--text-body)\", borderRadius: \"var(--radius-md)\", boxSizing: \"border-box\" }}>\n <Icon name={it.icon} size={16} />\n {!collapsed ? <><span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", letterSpacing: \"var(--tracking-base)\", flex: 1 }}>{it.label}</span><Icon name={openSections.includes(it.label) ? \"chevron-up\" : \"chevron-down\"} size={14} /></> : null}\n </div>\n {!collapsed && openSections.includes(it.label) ? (\n <div style={{ marginLeft: 24, display: \"flex\", flexDirection: \"column\", gap: 2, paddingTop: 2 }}>\n {it.children.map((c) => <NavItem key={c.label} icon={c.icon} label={c.label} indent active={active === c.label} onClick={() => onSelect && onSelect(c.label)} />)}\n </div>\n ) : null}\n </div>\n ) : (\n <NavItem key={it.label} icon={it.icon} label={it.label} collapsed={collapsed} active={active === it.label} onClick={() => onSelect && onSelect(it.label)} />\n ))}\n </nav>\n <div style={{ borderTop: \"1px solid var(--border-card)\", padding: 8, display: \"flex\", flexDirection: \"column\", flexShrink: 0 }}>\n {footerItems.map((f) => <NavItem key={f.label} icon={f.icon} label={f.label} collapsed={collapsed} onClick={() => onSelect && onSelect(f.label)} />)}\n </div>\n </aside>\n );\n}\n\n/** Default Keybricks nav model (Home dashboard). */\nexport const KEYBRICKS_NAV = [\n { icon: \"house\", label: \"Home\" },\n { icon: \"file-text\", label: \"Documents\" },\n { icon: \"layout-grid\", label: \"Business Intelligence\" },\n { icon: \"database\", label: \"Data Integration\" },\n { icon: \"scale\", label: \"Reconciliation\" },\n { icon: \"network\", label: \"Consolidation\" },\n {\n icon: \"folder-cog\", label: \"Management\", children: [\n { icon: \"box\", label: \"Modeling\" },\n { icon: \"workflow\", label: \"Workflow\" },\n { icon: \"shield\", label: \"Security Management\" },\n { icon: \"refresh-cw\", label: \"System Migration\" },\n { icon: \"chart-column\", label: \"Business Logic\" },\n { icon: \"list-todo\", label: \"Tasks Management\" },\n { icon: \"circle-check\", label: \"Validation Rules\" },\n { icon: \"settings-2\", label: \"Control Center\" },\n { icon: \"network\", label: \"Reconciliation\" },\n ],\n },\n];\n","navigation/Sidebar.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\n\n/**\n * Keybricks app sidebar — 240px (collapsed: 64px icon rail). White, hairline right border.\n * items: [{icon,label,children:[{icon,label}]}]; active by label; footer items pinned.\n */\nconst props = defineProps({\n items: { type: Array, default: () => [] },\n footerItems: { type: Array, default: () => [{ icon: 'circle-help', label: 'Help' }, { icon: 'settings', label: 'Settings' }, { icon: 'moon', label: 'Dark Mode' }] },\n active: String, collapsed: { type: Boolean, default: false },\n logo: { type: String, default: 'assets/keybricks-logo.png' }, brand: { type: String, default: 'Keybricks' },\n height: { type: [String, Number], default: '100%' }, style: Object, canCollapse: Boolean,\n})\nconst emit = defineEmits(['select', 'toggleCollapse'])\nconst openSections = ref(props.items.filter(i => i.children && (i.open || i.children.some(c => c.label === props.active))).map(i => i.label))\nfunction toggleSection(l) { openSections.value = openSections.value.includes(l) ? openSections.value.filter(x => x !== l) : [...openSections.value, l] }\nfunction itemStyle(active, indent) { return { height: indent ? 'var(--h-subnav-item)' : 'var(--h-nav-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: 10, padding: indent ? '6px 10px' : '8px 10px', boxSizing: 'border-box', cursor: 'pointer', background: active ? 'var(--bg-active)' : 'transparent', color: active ? 'var(--text-heading)' : 'var(--text-body)', transition: 'var(--t-hover)' } }\n</script>\n<template>\n <aside :style=\"{ width: collapsed ? 'var(--rail-w)' : 'var(--sidebar-w)', height, background: 'var(--bg-surface)', borderRight: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, transition: 'width .15s ease', ...style }\">\n <div :style=\"{ height: 'var(--topbar-h)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', gap: 10, padding: '0 16px', boxSizing: 'border-box', justifyContent: collapsed ? 'center' : 'flex-start', flexShrink: 0 }\">\n <img :src=\"logo\" alt=\"\" style=\"width:24px;height:24px;object-fit:contain\" />\n <span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-lg);line-height:var(--leading-lg);letter-spacing:-.609px;color:var(--text-primary)\">{{ brand }}</span>\n </div>\n <nav style=\"flex:1;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:0\">\n <div v-if=\"canCollapse\" role=\"button\" tabindex=\"0\" @click=\"emit('toggleCollapse')\" :style=\"itemStyle(false, false)\"><Icon name=\"panel-left\" :size=\"16\" /><span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);white-space:nowrap;flex:1\">Collapse</span></div>\n <template v-for=\"it in items\" :key=\"it.label\">\n <div v-if=\"it.children\">\n <div role=\"button\" tabindex=\"0\" @click=\"toggleSection(it.label)\" :style=\"{ height: 'var(--h-subnav-item)', display: 'flex', alignItems: 'center', gap: 10, padding: '6px 10px', justifyContent: collapsed ? 'center' : 'flex-start', cursor: 'pointer', color: 'var(--text-body)', borderRadius: 'var(--radius-md)', boxSizing: 'border-box' }\">\n <Icon :name=\"it.icon\" :size=\"16\" />\n <template v-if=\"!collapsed\"><span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);letter-spacing:var(--tracking-base);flex:1\">{{ it.label }}</span><Icon :name=\"openSections.includes(it.label) ? 'chevron-up' : 'chevron-down'\" :size=\"14\" /></template>\n </div>\n <div v-if=\"!collapsed && openSections.includes(it.label)\" style=\"margin-left:24px;display:flex;flex-direction:column;gap:2px;padding-top:2px\">\n <div v-for=\"c in it.children\" :key=\"c.label\" role=\"button\" tabindex=\"0\" @click=\"emit('select', c.label)\" :style=\"itemStyle(active === c.label, true)\"><Icon :name=\"c.icon\" :size=\"14\" /><span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);white-space:nowrap;flex:1\">{{ c.label }}</span></div>\n </div>\n </div>\n <div v-else role=\"button\" tabindex=\"0\" @click=\"emit('select', it.label)\" :style=\"itemStyle(active === it.label, false)\"><Icon :name=\"it.icon\" :size=\"16\" /><span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);white-space:nowrap;flex:1\">{{ it.label }}</span></div>\n </template>\n </nav>\n <div style=\"border-top:1px solid var(--border-card);padding:8px;display:flex;flex-direction:column;flex-shrink:0\">\n <div v-for=\"f in footerItems\" :key=\"f.label\" role=\"button\" tabindex=\"0\" @click=\"emit('select', f.label)\" :style=\"itemStyle(false, false)\"><Icon :name=\"f.icon\" :size=\"16\" /><span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);white-space:nowrap;flex:1\">{{ f.label }}</span></div>\n </div>\n </aside>\n</template>\n\n<!-- KEYBRICKS_NAV — default Keybricks nav model (Home dashboard), same shape as Sidebar.jsx's export, for reference:\nexport const KEYBRICKS_NAV = [\n { icon: 'house', label: 'Home' }, { icon: 'file-text', label: 'Documents' },\n { icon: 'layout-grid', label: 'Business Intelligence' }, { icon: 'database', label: 'Data Integration' },\n { icon: 'scale', label: 'Reconciliation' }, { icon: 'network', label: 'Consolidation' },\n { icon: 'folder-cog', label: 'Management', children: [ ... ] },\n] -->\n","navigation/TopBar.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { Button } from \"../buttons/Button.jsx\";\nimport { SearchInput } from \"../forms/SearchInput.jsx\";\n\n/** 56px top bar: bare search left, bell + dark AI Assistant button right. */\nexport function TopBar({ placeholder = \"Search documents, workflows, data...\", actions, onSearch, style }) {\n return (\n <header style={{ height: \"var(--topbar-h)\", background: \"var(--bg-surface)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"0 24px\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <SearchInput bare placeholder={placeholder} onChange={onSearch} style={{ width: 576, maxWidth: \"48%\" }} />\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n {actions || (<>\n <IconButton icon=\"bell\" size=\"lg\" />\n <Button variant=\"dark\" icon=\"sparkles\">AI Assistant</Button>\n </>)}\n </div>\n </header>\n );\n}\n\n/** Editor toolbar: back chevron, 14px/600 title, \"• Editing\" status, right action slot. */\nexport function PageToolbar({ title, status = \"• Editing\", actions, onBack, style }) {\n return (\n <div style={{ height: \"var(--topbar-h)\", background: \"var(--bg-surface)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"0 16px\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n {onBack ? <IconButton icon=\"chevron-left\" size=\"md\" onClick={onBack} /> : null}\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n {status ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-muted)\", marginLeft: 6 }}>{status}</span> : null}\n </div>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>{actions}</div>\n </div>\n );\n}\n\n/** Page heading block: 15px/500 title + 13px muted subtitle (System Migration pages). */\nexport function PageHeading({ title, subtitle, style }) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 4, ...style }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-lg)\", lineHeight: \"var(--leading-lg)\", letterSpacing: \"var(--tracking-lg)\", color: \"var(--text-primary)\" }}>{title}</span>\n {subtitle ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-secondary)\" }}>{subtitle}</span> : null}\n </div>\n );\n}\n","navigation/TopBar.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\nimport Button from '../buttons/Button.vue'\nimport SearchInput from '../forms/SearchInput.vue'\n// 56px top bar: bare search left, bell + dark AI Assistant button right.\ndefineProps({ placeholder: { type: String, default: 'Search documents, workflows, data...' }, style: Object })\nconst emit = defineEmits(['search'])\n</script>\n<template>\n <header :style=\"{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 24px', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <SearchInput bare :placeholder=\"placeholder\" @update:modelValue=\"v => emit('search', v)\" style=\"width:576px;max-width:48%\" />\n <div style=\"display:flex;align-items:center;gap:8px\">\n <slot name=\"actions\">\n <IconButton icon=\"bell\" size=\"lg\" />\n <Button variant=\"dark\" icon=\"sparkles\">AI Assistant</Button>\n </slot>\n </div>\n </header>\n</template>\n","navigation/Tree.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { Checkbox } from \"../forms/Checkbox.jsx\";\n\nconst docHues = { orange: \"var(--cat-transform)\", green: \"var(--cat-source)\", purple: \"var(--cat-merge)\", blue: \"var(--info-fg)\", teal: \"var(--cat-output)\", gray: \"var(--text-muted)\" };\nconst INDENT = 18;\n\nconst keyOf = (n) => (n.key != null ? n.key : n.label);\nconst titleOf = (n) => (n.title != null ? n.title : n.label);\nconst kidsOf = (n) => n.children || [];\nconst isBranch = (n) => kidsOf(n).length > 0 || n.isLeaf === false;\n\nfunction walk(nodes, fn, parentKey = null, depth = 0) {\n nodes.forEach((n) => { fn(n, parentKey, depth); walk(kidsOf(n), fn, keyOf(n), depth + 1); });\n}\nfunction indexNodes(items) {\n const byKey = {}, parent = {}, all = [];\n walk(items, (n, p, d) => { const k = keyOf(n); byKey[k] = n; parent[k] = p; all.push({ node: n, key: k, depth: d }); });\n return { byKey, parent, all };\n}\nfunction descendants(node, out = []) {\n kidsOf(node).forEach((c) => { out.push(c); descendants(c, out); });\n return out;\n}\n/** Is `key` the dragged node itself, or anywhere inside its subtree? */\nfunction inSubtree(items, rootKey, key) {\n const { byKey } = indexNodes(items);\n const root = byKey[rootKey];\n if (!root) return false;\n return rootKey === key || descendants(root).some((d) => keyOf(d) === key);\n}\nfunction keysMatching(items, test) {\n const hit = [];\n const visit = (n, trail) => {\n const t = [...trail, keyOf(n)];\n if (test(n)) hit.push(...trail);\n kidsOf(n).forEach((c) => visit(c, t));\n };\n items.forEach((n) => visit(n, []));\n return [...new Set(hit)];\n}\n\n/* Cascading check: a parent is checked when every enabled descendant is checked,\n indeterminate when only some are. Disabled nodes never change state. */\nfunction cascade(items, key, next) {\n const { byKey, parent } = indexNodes(items);\n const set = new Set(next);\n const node = byKey[key];\n if (!node) return next;\n const checked = set.has(key);\n descendants(node).forEach((d) => {\n if (d.disabled || d.disableCheckbox) return;\n checked ? set.add(keyOf(d)) : set.delete(keyOf(d));\n });\n let p = parent[key];\n while (p != null) {\n const pn = byKey[p];\n const kids = kidsOf(pn).filter((c) => !c.disabled && !c.disableCheckbox);\n const on = kids.filter((c) => set.has(keyOf(c)));\n on.length && on.length === kids.length ? set.add(p) : set.delete(p);\n p = parent[p];\n }\n return [...set];\n}\n/* Display state for every branch: full when all enabled descendants are checked,\n half when only some. Keeps parents honest whatever the caller passed in. */\nfunction rollUp(items, checkedKeys) {\n const full = new Set(checkedKeys), half = new Set();\n const visit = (n) => {\n kidsOf(n).forEach(visit);\n if (!kidsOf(n).length) return;\n const ds = descendants(n).filter((d) => !d.disabled && !d.disableCheckbox);\n if (!ds.length) return;\n const on = ds.filter((d) => full.has(keyOf(d))).length;\n if (on === ds.length) full.add(keyOf(n));\n else { full.delete(keyOf(n)); if (on > 0) half.add(keyOf(n)); }\n };\n items.forEach(visit);\n return { full, half };\n}\nfunction halfKeys(items, checkedKeys) {\n return [...rollUp(items, checkedKeys).half];\n}\n\nfunction Highlight({ text, query }) {\n if (!query) return text;\n const i = String(text).toLowerCase().indexOf(query.toLowerCase());\n if (i < 0) return text;\n return <>{String(text).slice(0, i)}<mark style={{ background: \"var(--soft-bg)\", color: \"var(--info-fg)\", fontWeight: \"var(--weight-medium)\", padding: 0, borderRadius: 2 }}>{String(text).slice(i, i + query.length)}</mark>{String(text).slice(i + query.length)}</>;\n}\n\n/** Guide lines for showLine: one 18px column per ancestor level, then the elbow. */\nfunction Guides({ trail, isLast, hasKids }) {\n return (\n <>\n {trail.map((ancestorHasNext, i) => (\n <span key={i} style={{ width: INDENT, alignSelf: \"stretch\", flexShrink: 0, borderLeft: ancestorHasNext ? \"1px solid var(--border-subtle)\" : \"1px solid transparent\", marginLeft: i === 0 ? 6 : 0 }} />\n ))}\n <span style={{ width: INDENT, alignSelf: \"stretch\", flexShrink: 0, position: \"relative\", marginLeft: trail.length === 0 ? 6 : 0 }}>\n <span style={{ position: \"absolute\", left: 0, top: 0, height: isLast ? \"50%\" : \"100%\", borderLeft: \"1px solid var(--border-subtle)\" }} />\n <span style={{ position: \"absolute\", left: 0, top: \"50%\", width: hasKids ? 6 : INDENT - 4, borderTop: \"1px solid var(--border-subtle)\" }} />\n </span>\n </>\n );\n}\n\nfunction Row({ item, state, cfg, api }) {\n const { node, key, depth, trail, isLast } = item;\n const kids = kidsOf(node);\n const branch = isBranch(node);\n const open = state.expanded.includes(key);\n const selected = state.selected.includes(key);\n const checked = state.checked.has(key);\n const half = !checked && state.half.has(key);\n const loading = state.loading.includes(key);\n const focused = state.focus === key;\n const ringed = focused && state.focusMoved;\n const [hover, setHover] = React.useState(false);\n const disabled = !!node.disabled;\n const dropAt = state.drop && state.drop.key === key ? state.drop.pos : null;\n const selectable = cfg.selectable && node.selectable !== false;\n\n const chevron = branch ? (\n <span onClick={(e) => { e.stopPropagation(); api.toggle(key); }} style={{ width: 14, height: 14, display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", color: \"var(--text-muted)\", flexShrink: 0, cursor: \"pointer\", transform: open ? \"rotate(90deg)\" : \"none\", transition: \"transform var(--dur-fast) var(--ease-out)\" }}>\n {loading ? <Icon name=\"loader\" size={12} style={{ animation: \"kb-tree-spin 900ms linear infinite\" }} /> : <Icon name=\"chevron-right\" size={13} />}\n </span>\n ) : <span style={{ width: cfg.showLine ? 0 : 14, flexShrink: 0 }} />;\n\n return (\n <>\n <div role=\"treeitem\" tabIndex={focused ? 0 : -1} aria-expanded={branch ? open : undefined} aria-selected={selected} aria-disabled={disabled || undefined}\n ref={(el) => { if (el && focused && state.focusMoved) el.focus({ preventScroll: true }); }}\n draggable={cfg.draggable && !disabled}\n onDragStart={(e) => { e.dataTransfer.effectAllowed = \"move\"; api.dragStart(key); }}\n onDragOver={(e) => { if (!cfg.draggable) return; e.preventDefault(); const r = e.currentTarget.getBoundingClientRect(); api.dragOver(key, (e.clientY - r.top) / r.height); }}\n onDragLeave={() => api.dragOver(null)}\n onDrop={(e) => { e.preventDefault(); api.drop(key); }}\n onDragEnd={() => api.dragEnd()}\n onClick={() => { if (disabled) return; api.focus(key); if (selectable) api.select(key); if (branch) api.toggle(key); }}\n onContextMenu={(e) => { if (cfg.onContextMenu) { e.preventDefault(); cfg.onContextMenu(key, e); } }}\n onKeyDown={(e) => api.keyDown(e, item)}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{\n minHeight: \"var(--h-tree-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 6,\n padding: cfg.showLine ? \"0 8px 0 2px\" : \"5px 8px\", paddingLeft: cfg.showLine ? 2 : 8 + depth * INDENT, boxSizing: \"border-box\",\n cursor: disabled ? \"not-allowed\" : \"pointer\", opacity: disabled ? 0.45 : 1,\n background: dropAt === \"inside\" ? \"var(--soft-bg)\" : selected ? \"var(--bg-active)\" : hover && !disabled ? \"var(--bg-hover)\" : \"transparent\",\n outline: dropAt === \"inside\" ? \"2px solid var(--action-primary)\" : ringed ? \"2px solid var(--focus-border)\" : \"none\", outlineOffset: -2,\n boxShadow: dropAt === \"before\" ? \"inset 0 2px 0 var(--action-primary)\" : dropAt === \"after\" ? \"inset 0 -2px 0 var(--action-primary)\" : \"none\",\n transition: \"var(--t-hover)\",\n }}>\n {cfg.showLine ? <Guides trail={trail} isLast={isLast} hasKids={branch} /> : null}\n {chevron}\n {cfg.checkable && node.checkable !== false ? (\n <span onClick={(e) => e.stopPropagation()} style={{ display: \"inline-flex\", flexShrink: 0 }}>\n <Checkbox size=\"sm\" checked={checked} indeterminate={half} disabled={disabled || node.disableCheckbox} onChange={() => api.check(key)} />\n </span>\n ) : null}\n {cfg.showIcon ? (\n <span style={{ color: branch ? \"var(--text-muted)\" : docHues[node.hue || \"gray\"], display: \"inline-flex\", flexShrink: 0 }}>\n <Icon name={node.icon || (branch ? (open ? \"folder-open\" : \"folder\") : \"file-text\")} size={15} />\n </span>\n ) : null}\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", fontWeight: selected ? \"var(--weight-medium)\" : \"var(--weight-regular)\", color: disabled ? \"var(--text-muted)\" : \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>\n <Highlight text={titleOf(node)} query={cfg.searchValue} />\n </span>\n {node.tag ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-2xs)\", lineHeight: 1, padding: \"2px 5px\", borderRadius: \"var(--radius-sm)\", background: \"var(--bg-inset)\", color: \"var(--text-secondary)\", flexShrink: 0 }}>{node.tag}</span> : null}\n {node.count != null ? <span style={{ marginLeft: \"auto\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", color: \"var(--text-muted)\", paddingLeft: 8 }}>{node.count}</span> : null}\n {cfg.rowActions && !disabled ? <span style={{ marginLeft: node.count != null ? 6 : \"auto\", display: \"flex\", gap: 2, opacity: hover ? 1 : 0, transition: \"var(--t-hover)\" }} onClick={(e) => e.stopPropagation()}>{cfg.rowActions(node)}</span> : null}\n </div>\n {open && loading && !kids.length ? (\n <div style={{ paddingLeft: 8 + (depth + 1) * INDENT + 14, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-muted)\", height: \"var(--h-tree-item)\", display: \"flex\", alignItems: \"center\" }}>Loading…</div>\n ) : null}\n </>\n );\n}\n\n/**\n * Nested tree view. Expand/collapse, single or multiple selection, cascading checkboxes\n * (with indeterminate parents and `checkStrictly` opt-out), disabled nodes and disabled\n * checkboxes, connector lines, async child loading, search highlighting, drag to move,\n * and full keyboard navigation. Node shape: { key, title, icon, hue, count, tag, children,\n * disabled, disableCheckbox, selectable, checkable, isLeaf }.\n */\nexport function Tree({\n items = [], treeData,\n defaultExpanded = [], expandedKeys, onExpand,\n defaultActive, defaultSelected = [], selectedKeys, onSelect,\n checkable = false, checkStrictly = false, defaultChecked = [], checkedKeys, onCheck,\n multiple = false, selectable = true, showIcon = true, showLine = false, draggable = false,\n searchValue = \"\", autoExpandOnSearch = true, loadData, onDrop, onContextMenu, rowActions, style, reorder = false,\n}) {\n const raw = treeData || items;\n /* Callers usually pass an inline array literal, so compare by value —\n re-seeding on identity alone would wipe async-loaded children. */\n const rawKey = JSON.stringify(raw);\n const [data, setData] = React.useState(raw);\n const seenKey = React.useRef(rawKey);\n React.useEffect(() => { if (seenKey.current !== rawKey) { seenKey.current = rawKey; setData(raw); } }, [rawKey]);\n\n const seedExpanded = defaultExpanded.length ? defaultExpanded : defaultActive ? [] : [];\n const [expandedS, setExpandedS] = React.useState(seedExpanded);\n const [selectedS, setSelectedS] = React.useState(defaultActive ? [defaultActive] : defaultSelected);\n const [checkedS, setCheckedS] = React.useState(defaultChecked);\n const [loading, setLoading] = React.useState([]);\n const [loaded, setLoaded] = React.useState([]);\n const [focus, setFocus] = React.useState(null);\n const [focusMoved, setFocusMoved] = React.useState(false);\n const [dragKey, setDragKey] = React.useState(null);\n const [drop, setDrop] = React.useState(null);\n\n const expanded = expandedKeys || expandedS;\n const selected = selectedKeys || selectedS;\n const checked = checkedKeys || checkedS;\n\n const searchExpanded = React.useMemo(\n () => (searchValue && autoExpandOnSearch ? keysMatching(data, (n) => String(titleOf(n)).toLowerCase().includes(searchValue.toLowerCase())) : []),\n [data, searchValue, autoExpandOnSearch]\n );\n const openKeys = React.useMemo(() => [...new Set([...expanded, ...searchExpanded])], [expanded, searchExpanded]);\n const view = React.useMemo(\n () => (checkable && !checkStrictly ? rollUp(data, checked) : { full: new Set(checked), half: new Set() }),\n [checkable, checkStrictly, data, checked]\n );\n\n /* Visible rows, with the sibling map each row needs to draw its guide lines. */\n const rows = React.useMemo(() => {\n const out = [];\n const push = (nodes, depth, trail) => {\n nodes.forEach((n, i) => {\n const isLast = i === nodes.length - 1;\n const key = keyOf(n);\n out.push({ node: n, key, depth, trail, isLast });\n if (kidsOf(n).length && openKeys.includes(key)) push(kidsOf(n), depth + 1, [...trail, !isLast]);\n });\n };\n push(data, 0, []);\n return out;\n }, [data, openKeys]);\n\n const setExpanded = (next) => { if (!expandedKeys) setExpandedS(next); onExpand && onExpand(next); };\n\n const api = {\n focus: (k) => { setFocus(k); setFocusMoved(false); },\n toggle: (k) => {\n const open = openKeys.includes(k);\n if (!open && loadData && !loaded.includes(k) && !kidsOf(indexNodes(data).byKey[k]).length) {\n setLoading((l) => [...l, k]);\n setExpanded([...expanded, k]);\n Promise.resolve(loadData(indexNodes(data).byKey[k])).then((kids) => {\n setData((d) => {\n const clone = JSON.parse(JSON.stringify(d));\n const { byKey } = indexNodes(clone);\n if (byKey[k]) byKey[k].children = kids || [];\n return clone;\n });\n setLoaded((l) => [...l, k]);\n setLoading((l) => l.filter((x) => x !== k));\n });\n return;\n }\n setExpanded(open ? expanded.filter((x) => x !== k) : [...expanded, k]);\n },\n select: (k) => {\n const next = multiple ? (selected.includes(k) ? selected.filter((x) => x !== k) : [...selected, k]) : selected.includes(k) && multiple ? [] : [k];\n if (!selectedKeys) setSelectedS(next);\n onSelect && onSelect(multiple ? next : k, next);\n },\n check: (k) => {\n const on = view.full.has(k);\n let next = on ? [...view.full].filter((x) => x !== k) : [...view.full, k];\n if (!checkStrictly) next = cascade(data, k, next);\n if (!checkedKeys) setCheckedS(next);\n onCheck && onCheck(next, { key: k, checked: !on, half: checkStrictly ? [] : halfKeys(data, next) });\n },\n dragStart: (k) => setDragKey(k),\n /* Validity lives here, not in the row: only branches can be dropped *into*, and a node\n can never land inside its own subtree (that would detach the whole branch). */\n dragOver: (k, ratio) => {\n if (k == null) return setDrop(null);\n const node = indexNodes(data).byKey[k];\n /* Only folders are drop targets: a file can't contain anything, so hovering one shows\n nothing at all. With reorder, a folder's outer 20% becomes an insert rule instead. */\n if (!node || node.disabled || !dragKey || !isBranch(node) || inSubtree(data, dragKey, k)) return setDrop(null);\n const pos = reorder ? (ratio < 0.2 ? \"before\" : ratio > 0.8 ? \"after\" : \"inside\") : \"inside\";\n setDrop({ key: k, pos });\n },\n dragEnd: () => { setDragKey(null); setDrop(null); },\n drop: (k) => {\n const info = drop && drop.key === k ? drop : null;\n setDrop(null);\n if (!dragKey || !info || inSubtree(data, dragKey, k)) return setDragKey(null);\n /* Computed outside setData: React re-invokes updaters (StrictMode, concurrent\n rebasing), which would fire the consumer's onDrop twice for one gesture. */\n const clone = JSON.parse(JSON.stringify(data));\n let moved = null;\n const prune = (nodes) => nodes.filter((n) => {\n if (keyOf(n) === dragKey) { moved = n; return false; }\n if (n.children) n.children = prune(n.children);\n return true;\n });\n const rootA = prune(clone);\n if (!moved) return setDragKey(null);\n let inserted = false;\n const insert = (nodes) => {\n const out = [];\n nodes.forEach((n) => {\n if (keyOf(n) === k && info.pos === \"before\") { out.push(moved); inserted = true; }\n if (keyOf(n) === k && info.pos === \"inside\") { n.children = [...(n.children || []), moved]; inserted = true; }\n else if (n.children) n.children = insert(n.children);\n out.push(n);\n if (keyOf(n) === k && info.pos === \"after\") { out.push(moved); inserted = true; }\n });\n return out;\n };\n const next = insert(rootA);\n if (!inserted) return setDragKey(null);\n setData(next);\n /* Dropping into a collapsed folder would otherwise look like the node vanished. */\n if (info.pos === \"inside\" && !expanded.includes(k)) setExpanded([...expanded, k]);\n onDrop && onDrop({ dragKey, dropKey: k, position: info.pos, treeData: next });\n setDragKey(null);\n },\n keyDown: (e, item) => {\n const i = rows.findIndex((r) => r.key === item.key);\n const go = (n) => { const t = rows[n]; if (!t) return; setFocus(t.key); setFocusMoved(true); };\n if (e.key === \"ArrowDown\") { e.preventDefault(); go(i + 1); }\n else if (e.key === \"ArrowUp\") { e.preventDefault(); go(i - 1); }\n else if (e.key === \"ArrowRight\") { e.preventDefault(); if (isBranch(item.node) && !openKeys.includes(item.key)) api.toggle(item.key); else go(i + 1); }\n else if (e.key === \"ArrowLeft\") { e.preventDefault(); if (isBranch(item.node) && openKeys.includes(item.key)) api.toggle(item.key); else { const p = rows.slice(0, i).reverse().find((r) => r.depth === item.depth - 1); if (p) { setFocus(p.key); setFocusMoved(true); } } }\n else if (e.key === \"Enter\") { e.preventDefault(); if (!item.node.disabled) { if (selectable && item.node.selectable !== false) api.select(item.key); if (isBranch(item.node)) api.toggle(item.key); } }\n else if (e.key === \" \") { e.preventDefault(); if (checkable && !item.node.disabled && item.node.checkable !== false && !item.node.disableCheckbox) api.check(item.key); }\n },\n };\n\n const state = { expanded: openKeys, selected, checked: view.full, half: view.half, loading, focus: focus || (rows[0] && rows[0].key), focusMoved, drop };\n const cfg = { checkable, checkStrictly, multiple, selectable, showIcon, showLine, draggable, searchValue, onContextMenu, rowActions };\n\n return (\n <div role=\"tree\" aria-multiselectable={multiple || undefined} style={{ display: \"flex\", flexDirection: \"column\", gap: showLine ? 0 : 1, ...style }}>\n <style>{\"@keyframes kb-tree-spin{to{transform:rotate(360deg)}}\"}</style>\n {rows.length === 0 ? (\n <div style={{ padding: \"10px 8px\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", color: \"var(--text-muted)\" }}>No matching items</div>\n ) : rows.map((r) => <Row key={r.key} item={r} state={state} cfg={cfg} api={api} />)}\n </div>\n );\n}\n","navigation/Tree.vue":"<script setup>\nimport { ref, computed, watch } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport Checkbox from '../forms/Checkbox.vue'\n// Nested tree view: expand/collapse, single or multiple selection, cascading checkboxes with\n// indeterminate parents (checkStrictly opts out), disabled nodes, connector lines, async\n// loadData, search highlight, drag to move, keyboard navigation.\nconst docHues = { orange: 'var(--cat-transform)', green: 'var(--cat-source)', purple: 'var(--cat-merge)', blue: 'var(--info-fg)', teal: 'var(--cat-output)', gray: 'var(--text-muted)' }\nconst INDENT = 18\nconst props = defineProps({\n items: { type: Array, default: () => [] },\n defaultExpanded: { type: Array, default: () => [] },\n defaultActive: String,\n defaultSelected: { type: Array, default: () => [] },\n defaultChecked: { type: Array, default: () => [] },\n checkable: Boolean, checkStrictly: Boolean, multiple: Boolean,\n selectable: { type: Boolean, default: true },\n showIcon: { type: Boolean, default: true },\n showLine: Boolean, draggable: Boolean,\n searchValue: { type: String, default: '' },\n autoExpandOnSearch: { type: Boolean, default: true },\n reorder: Boolean,\n loadData: Function, rowActions: Function, style: Object,\n})\nconst emit = defineEmits(['select', 'check', 'expand', 'drop'])\n\nconst data = ref(JSON.parse(JSON.stringify(props.items)))\n// Compare by value: re-seeding on identity alone would wipe async-loaded children.\nwatch(() => JSON.stringify(props.items), (next, prev) => { if (next !== prev) data.value = JSON.parse(next) })\nconst expanded = ref([...props.defaultExpanded])\nconst selected = ref(props.defaultActive ? [props.defaultActive] : [...props.defaultSelected])\nconst checked = ref([...props.defaultChecked])\nconst loading = ref([]); const loaded = ref([]); const focus = ref(null)\nconst dragKey = ref(null); const drop = ref(null)\n\nconst keyOf = n => (n.key != null ? n.key : n.label)\nconst titleOf = n => (n.title != null ? n.title : n.label)\nconst kidsOf = n => n.children || []\nconst isBranch = n => kidsOf(n).length > 0 || n.isLeaf === false\nfunction descendants(n, out = []) { for (const c of kidsOf(n)) { out.push(c); descendants(c, out) } return out }\n/** Is `key` the dragged node itself, or anywhere inside its subtree? */\nfunction inSubtree(rootKey, key) {\n const root = index(data.value).byKey[rootKey]\n if (!root) return false\n return rootKey === key || descendants(root).some(d => keyOf(d) === key)\n}\nfunction index(nodes, parent = null, acc = { byKey: {}, parent: {} }) {\n for (const n of nodes) { acc.byKey[keyOf(n)] = n; acc.parent[keyOf(n)] = parent; index(kidsOf(n), keyOf(n), acc) }\n return acc\n}\nfunction ancestorsOfMatches(nodes, test, trail = [], hit = []) {\n for (const n of nodes) {\n if (test(n)) hit.push(...trail)\n ancestorsOfMatches(kidsOf(n), test, [...trail, keyOf(n)], hit)\n }\n return [...new Set(hit)]\n}\nconst searchExpanded = computed(() => props.searchValue && props.autoExpandOnSearch\n ? ancestorsOfMatches(data.value, n => String(titleOf(n)).toLowerCase().includes(props.searchValue.toLowerCase()))\n : [])\nconst openKeys = computed(() => [...new Set([...expanded.value, ...searchExpanded.value])])\n\nconst view = computed(() => {\n const full = new Set(checked.value); const half = new Set()\n if (!props.checkable || props.checkStrictly) return { full, half }\n const visit = n => {\n kidsOf(n).forEach(visit)\n if (!kidsOf(n).length) return\n const ds = descendants(n).filter(d => !d.disabled && !d.disableCheckbox)\n if (!ds.length) return\n const on = ds.filter(d => full.has(keyOf(d))).length\n if (on === ds.length) full.add(keyOf(n))\n else { full.delete(keyOf(n)); if (on > 0) half.add(keyOf(n)) }\n }\n data.value.forEach(visit)\n return { full, half }\n})\n\nconst rows = computed(() => {\n const out = []\n const push = (nodes, depth, trail) => {\n nodes.forEach((n, i) => {\n const isLast = i === nodes.length - 1\n out.push({ n, key: keyOf(n), depth, trail, isLast })\n if (kidsOf(n).length && openKeys.value.includes(keyOf(n))) push(kidsOf(n), depth + 1, [...trail, !isLast])\n })\n }\n push(data.value, 0, [])\n return out\n})\n\nfunction toggle(k) {\n const open = openKeys.value.includes(k)\n const { byKey } = index(data.value)\n if (!open && props.loadData && !loaded.value.includes(k) && !kidsOf(byKey[k]).length) {\n loading.value = [...loading.value, k]\n expanded.value = [...expanded.value, k]\n Promise.resolve(props.loadData(byKey[k])).then(kids => {\n const clone = JSON.parse(JSON.stringify(data.value))\n const t = index(clone).byKey[k]\n if (t) t.children = kids || []\n data.value = clone\n loaded.value = [...loaded.value, k]\n loading.value = loading.value.filter(x => x !== k)\n })\n return\n }\n expanded.value = open ? expanded.value.filter(x => x !== k) : [...expanded.value, k]\n emit('expand', expanded.value)\n}\nfunction select(k) {\n selected.value = props.multiple\n ? (selected.value.includes(k) ? selected.value.filter(x => x !== k) : [...selected.value, k])\n : [k]\n emit('select', props.multiple ? selected.value : k, selected.value)\n}\nfunction check(k) {\n const set = new Set(view.value.full)\n const on = set.has(k)\n on ? set.delete(k) : set.add(k)\n if (!props.checkStrictly) {\n const { byKey, parent } = index(data.value)\n descendants(byKey[k]).forEach(d => {\n if (d.disabled || d.disableCheckbox) return\n on ? set.delete(keyOf(d)) : set.add(keyOf(d))\n })\n let p = parent[k]\n while (p != null) {\n const kids = kidsOf(byKey[p]).filter(c => !c.disabled && !c.disableCheckbox)\n const hit = kids.filter(c => set.has(keyOf(c)))\n hit.length && hit.length === kids.length ? set.add(p) : set.delete(p)\n p = parent[p]\n }\n }\n checked.value = [...set]\n emit('check', checked.value, { key: k, checked: !on, half: [...view.value.half] })\n}\nfunction rowClick(n) {\n if (n.disabled) return\n focus.value = keyOf(n)\n if (props.selectable && n.selectable !== false) select(keyOf(n))\n if (isBranch(n)) toggle(keyOf(n))\n}\nfunction onDragOver(e, k) {\n if (!props.draggable) return\n e.preventDefault()\n if (!dragKey.value) return\n const n = index(data.value).byKey[k]\n // Only folders are drop targets: hovering a file shows nothing, since it can't contain anything.\n if (!n || n.disabled || !isBranch(n) || inSubtree(dragKey.value, k)) { drop.value = null; return }\n const r = e.currentTarget.getBoundingClientRect()\n const y = (e.clientY - r.top) / r.height\n drop.value = { key: k, pos: props.reorder ? (y < 0.2 ? 'before' : y > 0.8 ? 'after' : 'inside') : 'inside' }\n}\nfunction onDrop(k) {\n const info = drop.value && drop.value.key === k ? drop.value : null\n drop.value = null\n if (!dragKey.value || !info || inSubtree(dragKey.value, k)) { dragKey.value = null; return }\n const clone = JSON.parse(JSON.stringify(data.value))\n let moved = null\n const prune = nodes => nodes.filter(n => {\n if (keyOf(n) === dragKey.value) { moved = n; return false }\n if (n.children) n.children = prune(n.children)\n return true\n })\n const pruned = prune(clone)\n if (!moved) { dragKey.value = null; return }\n const insert = nodes => {\n const out = []\n for (const n of nodes) {\n if (keyOf(n) === k && info.pos === 'before') out.push(moved)\n if (keyOf(n) === k && info.pos === 'inside') n.children = [...(n.children || []), moved]\n else if (n.children) n.children = insert(n.children)\n out.push(n)\n if (keyOf(n) === k && info.pos === 'after') out.push(moved)\n }\n return out\n }\n data.value = insert(pruned)\n if (info.pos === 'inside' && !expanded.value.includes(k)) expanded.value = [...expanded.value, k]\n emit('drop', { dragKey: dragKey.value, dropKey: k, position: info.pos, treeData: data.value })\n dragKey.value = null\n}\nfunction keyDown(e, row) {\n const i = rows.value.findIndex(r => r.key === row.key)\n const go = n => { const t = rows.value[n]; if (t) focus.value = t.key }\n if (e.key === 'ArrowDown') { e.preventDefault(); go(i + 1) }\n else if (e.key === 'ArrowUp') { e.preventDefault(); go(i - 1) }\n else if (e.key === 'ArrowRight') { e.preventDefault(); isBranch(row.n) && !openKeys.value.includes(row.key) ? toggle(row.key) : go(i + 1) }\n else if (e.key === 'ArrowLeft') { e.preventDefault(); if (isBranch(row.n) && openKeys.value.includes(row.key)) toggle(row.key) }\n else if (e.key === 'Enter') { e.preventDefault(); rowClick(row.n) }\n else if (e.key === ' ') { e.preventDefault(); if (props.checkable && !row.n.disabled && row.n.checkable !== false && !row.n.disableCheckbox) check(row.key) }\n}\nfunction parts(text) {\n const q = props.searchValue\n const s = String(text)\n if (!q) return [{ t: s, hit: false }]\n const i = s.toLowerCase().indexOf(q.toLowerCase())\n if (i < 0) return [{ t: s, hit: false }]\n return [{ t: s.slice(0, i), hit: false }, { t: s.slice(i, i + q.length), hit: true }, { t: s.slice(i + q.length), hit: false }]\n}\nconst dropAt = k => (drop.value && drop.value.key === k ? drop.value.pos : null)\n</script>\n<template>\n <div role=\"tree\" :style=\"{ display: 'flex', flexDirection: 'column', gap: showLine ? 0 : '1px', ...style }\">\n <div v-for=\"row in rows\" :key=\"row.key\" role=\"treeitem\" class=\"kb-tree-row\"\n :tabindex=\"focus === row.key || (!focus && row === rows[0]) ? 0 : -1\"\n :aria-expanded=\"isBranch(row.n) ? openKeys.includes(row.key) : undefined\"\n :aria-selected=\"selected.includes(row.key)\" :aria-disabled=\"row.n.disabled || undefined\"\n :draggable=\"draggable && !row.n.disabled\"\n @dragstart=\"dragKey = row.key\" @dragover=\"onDragOver($event, row.key)\" @dragleave=\"drop = null\"\n @drop.prevent=\"onDrop(row.key)\" @dragend=\"dragKey = null; drop = null\"\n @click=\"rowClick(row.n)\" @keydown=\"keyDown($event, row)\"\n :class=\"{ 'is-disabled': row.n.disabled }\"\n :style=\"{\n minHeight: 'var(--h-tree-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: '6px',\n padding: showLine ? '0 8px 0 2px' : '5px 8px', paddingLeft: showLine ? '2px' : (8 + row.depth * INDENT) + 'px', boxSizing: 'border-box',\n cursor: row.n.disabled ? 'not-allowed' : 'pointer', opacity: row.n.disabled ? 0.45 : 1,\n background: dropAt(row.key) === 'inside' ? 'var(--soft-bg)' : selected.includes(row.key) ? 'var(--bg-active)' : 'transparent',\n outline: dropAt(row.key) === 'inside' ? '2px solid var(--action-primary)' : 'none',\n outlineOffset: '-2px',\n boxShadow: dropAt(row.key) === 'before' ? 'inset 0 2px 0 var(--action-primary)' : dropAt(row.key) === 'after' ? 'inset 0 -2px 0 var(--action-primary)' : 'none',\n transition: 'var(--t-hover)' }\">\n <template v-if=\"showLine\">\n <span v-for=\"(hasNext, i) in row.trail\" :key=\"i\"\n :style=\"{ width: INDENT + 'px', alignSelf: 'stretch', flexShrink: 0, marginLeft: i === 0 ? '6px' : 0, borderLeft: hasNext ? '1px solid var(--border-subtle)' : '1px solid transparent' }\" />\n <span :style=\"{ width: INDENT + 'px', alignSelf: 'stretch', flexShrink: 0, position: 'relative', marginLeft: row.trail.length ? 0 : '6px' }\">\n <span :style=\"{ position: 'absolute', left: 0, top: 0, height: row.isLast ? '50%' : '100%', borderLeft: '1px solid var(--border-subtle)' }\" />\n <span :style=\"{ position: 'absolute', left: 0, top: '50%', width: (isBranch(row.n) ? 6 : INDENT - 4) + 'px', borderTop: '1px solid var(--border-subtle)' }\" />\n </span>\n </template>\n <span v-if=\"isBranch(row.n)\" @click.stop=\"toggle(row.key)\"\n :style=\"{ width: '14px', height: '14px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-muted)', flexShrink: 0, cursor: 'pointer', transform: openKeys.includes(row.key) ? 'rotate(90deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }\">\n <Icon :name=\"loading.includes(row.key) ? 'loader' : 'chevron-right'\" :size=\"13\" :class=\"loading.includes(row.key) ? 'kb-tree-spin' : ''\" />\n </span>\n <span v-else :style=\"{ width: showLine ? 0 : '14px', flexShrink: 0 }\" />\n <span v-if=\"checkable && row.n.checkable !== false\" @click.stop style=\"display:inline-flex;flex-shrink:0\">\n <Checkbox size=\"sm\" :checked=\"view.full.has(row.key)\" :indeterminate=\"view.half.has(row.key)\"\n :disabled=\"row.n.disabled || row.n.disableCheckbox\" @change=\"check(row.key)\" />\n </span>\n <span v-if=\"showIcon\" :style=\"{ color: isBranch(row.n) ? 'var(--text-muted)' : docHues[row.n.hue || 'gray'], display: 'inline-flex', flexShrink: 0 }\">\n <Icon :name=\"row.n.icon || (isBranch(row.n) ? (openKeys.includes(row.key) ? 'folder-open' : 'folder') : 'file-text')\" :size=\"15\" />\n </span>\n <span :style=\"{ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', fontWeight: selected.includes(row.key) ? 'var(--weight-medium)' : 'var(--weight-regular)', color: row.n.disabled ? 'var(--text-muted)' : 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }\">\n <template v-for=\"(p, i) in parts(titleOf(row.n))\" :key=\"i\"><mark v-if=\"p.hit\" style=\"background:var(--soft-bg);color:var(--info-fg);font-weight:var(--weight-medium);padding:0;border-radius:2px\">{{ p.t }}</mark><template v-else>{{ p.t }}</template></template>\n </span>\n <span v-if=\"row.n.tag\" style=\"font-family:var(--font-sans);font-size:var(--text-2xs);line-height:1;padding:2px 5px;border-radius:var(--radius-sm);background:var(--bg-inset);color:var(--text-secondary);flex-shrink:0\">{{ row.n.tag }}</span>\n <span v-if=\"row.n.count != null\" style=\"margin-left:auto;font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--text-muted);padding-left:8px\">{{ row.n.count }}</span>\n </div>\n <div v-if=\"!rows.length\" style=\"padding:10px 8px;font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-muted)\">No matching items</div>\n </div>\n</template>\n<style scoped>\n.kb-tree-row:not(.is-disabled):hover{background:var(--bg-hover)}\n.kb-tree-row:focus-visible{outline:2px solid var(--focus-ring);outline-offset:-2px}\n.kb-tree-spin{animation:kb-tree-spin 900ms linear infinite}\n@keyframes kb-tree-spin{to{transform:rotate(360deg)}}\n</style>\n","navigation/PageHeading.vue":"<script setup>\ndefineProps({ title: String, subtitle: String, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', flexDirection: 'column', gap: 4, ...style }\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-lg);line-height:var(--leading-lg);letter-spacing:var(--tracking-lg);color:var(--text-primary)\">{{ title }}</span>\n <span v-if=\"subtitle\" style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-secondary)\">{{ subtitle }}</span>\n </div>\n</template>\n","navigation/PageToolbar.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\ndefineProps({ title: String, status: { type: String, default: '• Editing' }, showBack: Boolean, style: Object })\nconst emit = defineEmits(['back'])\n</script>\n<template>\n <div :style=\"{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 16px', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"display:flex;align-items:center;gap:8px\">\n <IconButton v-if=\"showBack\" icon=\"chevron-left\" size=\"md\" @click=\"emit('back')\" />\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"status\" style=\"font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-muted);margin-left:6px\">{{ status }}</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:8px\"><slot name=\"actions\" /></div>\n </div>\n</template>\n","navigation/ThemeToggle.vue":"<script setup>\nimport { ref, computed, watch, onMounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\nconst props = defineProps({ size: { type: Number, default: 34 }, dark: { type: Boolean, default: null }, style: Object })\nconst emit = defineEmits(['update:dark'])\nconst darkState = ref(false)\nconst dark = computed(() => props.dark != null ? props.dark : darkState.value)\nconst hover = ref(false)\nonMounted(() => { darkState.value = document.documentElement.dataset.theme === 'dark' })\nwatch(dark, (v) => { if (props.dark != null) return; document.documentElement.dataset.theme = v ? 'dark' : 'light'; try { localStorage.setItem('kb-theme', v ? 'dark' : 'light') } catch (e) {} })\nfunction toggle() { if (props.dark != null) emit('update:dark', !dark.value); else darkState.value = !darkState.value }\n</script>\n<template>\n <button type=\"button\" :aria-label=\"dark ? 'Switch to light mode' : 'Switch to dark mode'\" @click=\"toggle\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ width: size, height: size, borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: hover ? 'var(--bg-hover)' : 'transparent', color: 'var(--text-body)', padding: 0, transition: 'var(--t-hover)', ...style }\">\n <Icon :name=\"dark ? 'sparkles' : 'moon'\" :size=\"18\" />\n </button>\n</template>\n"});
|
|
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"});
|