@keyfunc/keybricks-ds 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/INTEGRATION.md +42 -1
  2. package/_scripts/build-tokens.mjs +3 -3
  3. package/components/_shared/frame-theme.js +5 -0
  4. package/components/_shared/src-buttons.js +1 -1
  5. package/components/_shared/src-data.js +1 -1
  6. package/components/_shared/src-display.js +1 -1
  7. package/components/_shared/src-feedback.js +1 -1
  8. package/components/_shared/src-forms.js +1 -1
  9. package/components/_shared/src-navigation.js +1 -1
  10. package/components/_shared/src-pipeline.js +1 -1
  11. package/components/_shared/src-tabs.js +1 -1
  12. package/components/_vue/index.mjs +21 -0
  13. package/components/buttons/Button.vue +0 -20
  14. package/components/buttons/ButtonGroup.vue +16 -0
  15. package/components/data/KeyValue.vue +0 -16
  16. package/components/data/KeyValueList.vue +12 -0
  17. package/components/data/ListRow.vue +1 -1
  18. package/components/display/AreaChart.vue +14 -0
  19. package/components/display/Avatar.vue +0 -17
  20. package/components/display/AvatarGroup.vue +13 -0
  21. package/components/display/Badge.vue +0 -34
  22. package/components/display/Card.vue +0 -15
  23. package/components/display/Donut.vue +17 -0
  24. package/components/display/IconTile.vue +13 -0
  25. package/components/display/MiniBars.vue +11 -0
  26. package/components/display/ProgressBar.vue +0 -54
  27. package/components/display/StatCard.vue +11 -0
  28. package/components/display/StatusBadge.vue +11 -0
  29. package/components/feedback/ActivityItem.vue +1 -32
  30. package/components/feedback/Alert.vue +0 -36
  31. package/components/feedback/DotActivity.vue +15 -0
  32. package/components/feedback/EmptyState.vue +12 -0
  33. package/components/feedback/LogViewer.vue +8 -0
  34. package/components/feedback/Modal.vue +0 -45
  35. package/components/feedback/Skeleton.vue +6 -0
  36. package/components/feedback/Spinner.vue +6 -0
  37. package/components/feedback/Toast.vue +19 -0
  38. package/components/forms/Combobox.vue +1 -1
  39. package/components/forms/DatePicker.vue +1 -1
  40. package/components/forms/Input.jsx +2 -1
  41. package/components/forms/Input.vue +2 -24
  42. package/components/forms/Textarea.vue +20 -0
  43. package/components/forms/input-base.js +3 -0
  44. package/components/navigation/Breadcrumb.vue +0 -22
  45. package/components/navigation/PageHeading.vue +9 -0
  46. package/components/navigation/PageToolbar.vue +15 -0
  47. package/components/navigation/ThemeToggle.vue +18 -0
  48. package/components/navigation/TopBar.vue +0 -32
  49. package/components/pipeline/NodeCard.vue +2 -1
  50. package/components/pipeline/PaletteItem.vue +1 -1
  51. package/components/pipeline/PalettePanel.jsx +2 -8
  52. package/components/pipeline/PalettePanel.vue +1 -8
  53. package/components/pipeline/pipeline-catalog.js +8 -0
  54. package/components/tabs/PanelTabs.vue +10 -0
  55. package/components/tabs/PillTabs.vue +10 -0
  56. package/components/tabs/Tabs.vue +0 -28
  57. package/package.json +1 -1
  58. package/readme.md +1 -0
  59. package/tokens/alpha.css +0 -4
  60. package/tokens/charts.css +0 -6
  61. package/tokens/colors-extended.css +0 -1
  62. package/tokens/dark.css +4 -46
  63. package/tokens/interaction.css +0 -1
  64. package/tokens/theme.js +11 -0
  65. package/tokens/tokens.d.ts +1 -1
  66. package/tokens/tokens.js +1 -1
  67. package/tokens/tokens.json +2 -2
@@ -1 +1 @@
1
- window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"pipeline/ConfigPanel.jsx":"import React from \"react\";\nimport { Button } from \"../buttons/Button.jsx\";\nimport { PanelTabs } from \"../tabs/Tabs.jsx\";\nimport { NumberStepper } from \"../forms/NumberStepper.jsx\";\n\n/** Right node-config panel — 400px: title/subtitle + Save Settings, preview-rows row, PanelTabs, body slot. */\nexport function ConfigPanel({ title, subtitle, tabs = [\"Node Info\", \"Calc Rules\", \"Output Fields\", \"SQL Preview\"], activeTab, onTab, previewRows = 100, onPreviewRows, onSave, onPreview, children, height = \"100%\", style }) {\n const [tab, setTab] = React.useState(tabs[0]);\n const cur = activeTab != null ? activeTab : tab;\n return (\n <div style={{ width: \"var(--panel-w)\", height, background: \"var(--bg-surface)\", borderLeft: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ padding: \"12px 16px 0\", borderBottom: \"1px solid var(--border-card)\", flexShrink: 0 }}>\n <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"flex-start\" }}>\n <div style={{ display: \"flex\", flexDirection: \"column\" }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-md)\", lineHeight: \"17.5px\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-muted)\", paddingTop: 5 }}>{subtitle}</span>\n </div>\n <Button size=\"sm\" onClick={onSave}>Save Settings</Button>\n </div>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8, padding: \"8px 0\" }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-secondary)\" }}>Preview rows</span>\n <NumberStepper value={previewRows} onChange={onPreviewRows} />\n <Button variant=\"primary-soft\" size=\"sm\" icon=\"eye\" style={{ height: 28, borderRadius: \"var(--radius-sm)\", padding: \"4px 10px\" }} onClick={onPreview}>Data Preview</Button>\n </div>\n <PanelTabs tabs={tabs} active={cur} onChange={(t) => { setTab(t); onTab && onTab(t); }} style={{ margin: \"0 -16px\", padding: \"0 16px\", borderBottom: \"none\" }} />\n </div>\n <div style={{ flex: 1, overflowY: \"auto\", padding: 16, display: \"flex\", flexDirection: \"column\", gap: 16 }}>{children}</div>\n </div>\n );\n}\n","pipeline/ConfigPanel.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Button from '../buttons/Button.vue'\nimport PanelTabs from '../tabs/Tabs.vue'\nimport NumberStepper from '../forms/NumberStepper.vue'\n// Right node-config panel — 400px: title/subtitle + Save Settings, preview-rows row, PanelTabs, body slot.\nconst props = defineProps({ title: String, subtitle: String, tabs: { type: Array, default: () => ['Node Info', 'Calc Rules', 'Output Fields', 'SQL Preview'] }, activeTab: String, previewRows: { type: Number, default: 100 }, height: { type: [String, Number], default: '100%' }, style: Object })\nconst emit = defineEmits(['tab', 'update:previewRows', 'save', 'preview'])\nconst tabState = ref(props.tabs[0])\nconst cur = computed(() => props.activeTab != null ? props.activeTab : tabState.value)\nfunction setTab(t) { tabState.value = t; emit('tab', t) }\n</script>\n<template>\n <div :style=\"{ width: 'var(--panel-w)', height, background: 'var(--bg-surface)', borderLeft: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"padding:12px 16px 0;border-bottom:1px solid var(--border-card);flex-shrink:0\">\n <div style=\"display:flex;justify-content:space-between;align-items:flex-start\">\n <div style=\"display:flex;flex-direction:column\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:17.5px;letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted);padding-top:5px\">{{ subtitle }}</span>\n </div>\n <Button size=\"sm\" @click=\"emit('save')\">Save Settings</Button>\n </div>\n <div style=\"display:flex;align-items:center;gap:8px;padding:8px 0\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)\">Preview rows</span>\n <NumberStepper :model-value=\"previewRows\" @update:modelValue=\"v => emit('update:previewRows', v)\" />\n <Button variant=\"primary-soft\" size=\"sm\" icon=\"eye\" style=\"height:28px;border-radius:var(--radius-sm);padding:4px 10px\" @click=\"emit('preview')\">Data Preview</Button>\n </div>\n <PanelTabs :tabs=\"tabs\" :active=\"cur\" @change=\"setTab\" style=\"margin:0 -16px;padding:0 16px;border-bottom:none\" />\n </div>\n <div style=\"flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:16px\"><slot /></div>\n </div>\n</template>\n","pipeline/DataPreview.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { Checkbox } from \"../forms/Checkbox.jsx\";\nimport { DataTable } from \"../data/DataTable.jsx\";\n\n/** Bottom data-preview drawer — 250px: 40px header (title, Debug Mode, collapse) + dense table. */\nexport function DataPreview({ title = \"Data Preview\", columns = [], rows = [], debug = false, onDebug, collapsed, onCollapse, style }) {\n return (\n <div style={{ background: \"var(--bg-surface)\", borderTop: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", height: collapsed ? 40 : \"var(--drawer-h)\", overflow: \"hidden\", boxSizing: \"border-box\", flexShrink: 0, transition: \"height .15s ease\", ...style }}>\n <div style={{ height: 40, display: \"flex\", alignItems: \"center\", gap: 12, padding: \"0 16px\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\" }}>{title}</span>\n <span style={{ flex: 1 }} />\n <Checkbox label=\"Debug Mode\" checked={debug} onChange={onDebug} />\n <IconButton icon={collapsed ? \"chevron-up\" : \"chevron-down\"} size=\"xs\" muted onClick={onCollapse} />\n </div>\n {!collapsed ? (\n <div style={{ overflowY: \"auto\", borderTop: \"1px solid var(--border-subtle)\" }}>\n <DataTable dense columns={columns} rows={rows} headerBorder={false} />\n </div>\n ) : null}\n </div>\n );\n}\n","pipeline/DataPreview.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\nimport Checkbox from '../forms/Checkbox.vue'\nimport DataTable from '../data/DataTable.vue'\n// Bottom data-preview drawer — 250px: 40px header (title, Debug Mode, collapse) + dense table.\ndefineProps({ title: { type: String, default: 'Data Preview' }, columns: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, debug: Boolean, collapsed: Boolean, style: Object })\nconst emit = defineEmits(['update:debug', 'toggleCollapse'])\n</script>\n<template>\n <div :style=\"{ background: 'var(--bg-surface)', borderTop: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', height: collapsed ? 40 : 'var(--drawer-h)', overflow: 'hidden', boxSizing: 'border-box', flexShrink: 0, transition: 'height .15s ease', ...style }\">\n <div style=\"height:40px;display:flex;align-items:center;gap:12px;padding:0 16px;box-sizing:border-box;flex-shrink:0\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)\">{{ title }}</span>\n <span style=\"flex:1\" />\n <Checkbox label=\"Debug Mode\" :checked=\"debug\" @change=\"v => emit('update:debug', v)\" />\n <IconButton :icon=\"collapsed ? 'chevron-up' : 'chevron-down'\" size=\"xs\" muted @click=\"emit('toggleCollapse')\" />\n </div>\n <div v-if=\"!collapsed\" style=\"overflow-y:auto;border-top:1px solid var(--border-subtle)\">\n <DataTable dense :columns=\"columns\" :rows=\"rows\" :header-border=\"false\" />\n </div>\n </div>\n</template>\n","pipeline/NodeCard.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconTile, StatusBadge } from \"../display/Badge.jsx\";\n\n/* Ports are the connection handles: they light up on hover, stay lit while a wire is\n being dragged out of them, and show a crosshair when they're a valid drop target. */\nfunction portSkin(tone, hover, interactive) {\n const hot = tone === \"active\" || tone === \"target\" || (interactive && hover);\n return {\n background: tone === \"target\" ? \"var(--soft-bg)\" : \"var(--bg-surface)\",\n border: `2px solid ${hot ? \"var(--action-primary)\" : \"var(--port-border)\"}`,\n boxShadow: tone === \"active\" || tone === \"target\" ? \"0 0 0 3px var(--focus-ring)\" : \"none\",\n color: hot ? \"var(--action-primary)\" : \"var(--text-muted)\",\n cursor: interactive ? (tone === \"active\" ? \"grabbing\" : \"grab\") : \"default\",\n transition: \"var(--t-hover)\",\n };\n}\n\n/** Round output/input port — 22px. tone: \"default\" | \"active\" (dragging from it) | \"target\" (drop here). */\nexport function Port({ tone = \"default\", interactive = false, style, onMouseEnter, onMouseLeave, ...rest }) {\n const [hover, setHover] = React.useState(false);\n return (\n <span {...rest}\n onMouseEnter={(e) => { setHover(true); onMouseEnter && onMouseEnter(e); }}\n onMouseLeave={(e) => { setHover(false); onMouseLeave && onMouseLeave(e); }}\n style={{ width: 22, height: 22, borderRadius: \"var(--radius-full)\", boxSizing: \"border-box\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", ...portSkin(tone, hover, interactive), ...style }}>\n <Icon name={tone === \"target\" ? \"plus\" : \"arrow-right\"} size={12} strokeWidth={2.2} />\n </span>\n );\n}\n\n/** Labeled input port pill (\"A >\", \"B >\") — 28×22, same three tones as Port. */\nexport function PortLabel({ label = \"A\", tone = \"default\", interactive = false, style, onMouseEnter, onMouseLeave, ...rest }) {\n const [hover, setHover] = React.useState(false);\n return (\n <span {...rest}\n onMouseEnter={(e) => { setHover(true); onMouseEnter && onMouseEnter(e); }}\n onMouseLeave={(e) => { setHover(false); onMouseLeave && onMouseLeave(e); }}\n style={{ width: 28, height: 22, borderRadius: \"var(--radius-full)\", boxSizing: \"border-box\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", gap: 2, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", letterSpacing: \"var(--tracking-xs)\", lineHeight: 1, ...portSkin(tone, hover, interactive), ...style }}>\n {tone === \"target\" ? <Icon name=\"plus\" size={12} strokeWidth={2.2} /> : <>{label}<Icon name=\"chevron-right\" size={9} strokeWidth={2.5} /></>}\n </span>\n );\n}\n\n/**\n * Canvas node card — 220×90, r16, soft shadow. Selected = blue border + blue-100 ring.\n * badge: \"error\" | \"warning\" | \"clock\" overlay on the icon tile.\n * Port interaction is driven by the canvas: onOutputDown starts a wire, onInputUp lands it,\n * and outputTone / inputTones light the handles.\n */\nexport function NodeCard({ title, subtitle, icon = \"table\", subtitleIcon, hue = \"source\", selected, badge, inputs = [], output = true, showMenu = true, onClick, onMenu, onOutputDown, onInputUp, onInputEnter, onInputLeave, outputTone = \"default\", inputTones = [], style }) {\n const live = !!(onOutputDown || onInputUp);\n return (\n <div onClick={onClick} style={{ position: \"relative\", width: 220, height: 90, borderRadius: \"var(--radius-node)\", background: \"var(--bg-surface)\", border: `1px solid ${selected ? \"var(--selected-border)\" : \"var(--border-control)\"}`, boxShadow: selected ? \"var(--shadow-selected)\" : \"var(--shadow-node)\", boxSizing: \"border-box\", cursor: \"pointer\", ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 12, padding: 16, height: \"100%\", boxSizing: \"border-box\" }}>\n <span style={{ position: \"relative\", flexShrink: 0 }}>\n <IconTile icon={icon} hue={hue} kind=\"node\" />\n {badge === \"error\" ? <StatusBadge tone=\"danger\" icon=\"bug\" size={20} style={{ position: \"absolute\", top: -6, right: -6 }} /> : null}\n {badge === \"warning\" ? <StatusBadge tone=\"warning\" icon=\"triangle-alert\" size={20} style={{ position: \"absolute\", top: -6, right: -6 }} /> : null}\n {badge === \"clock\" ? <StatusBadge tone=\"pending\" icon=\"clock\" size={20} style={{ position: \"absolute\", top: -6, right: -6 }} /> : null}\n </span>\n <div style={{ display: \"flex\", flexDirection: \"column\", minWidth: 0, flex: 1, paddingRight: 18 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-base)\", lineHeight: \"17.875px\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{title}</span>\n <span style={{ display: \"flex\", alignItems: \"flex-start\", gap: 4, paddingTop: 4, minWidth: 0 }}>\n {subtitleIcon !== false ? <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", flexShrink: 0, paddingTop: 1 }}><Icon name={subtitleIcon || \"info\"} size={13} /></span> : null}\n {/* Node types run long (\"Field Value Calculation\"), so the subtitle wraps to a\n second line and only then clamps — the card size never changes. */}\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"13.75px\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-muted)\", minWidth: 0, overflow: \"hidden\", display: \"-webkit-box\", WebkitBoxOrient: \"vertical\", WebkitLineClamp: 2, overflowWrap: \"break-word\" }}>{subtitle}</span>\n </span>\n </div>\n </div>\n {showMenu ? (\n <button type=\"button\" onClick={(e) => { e.stopPropagation(); onMenu && onMenu(e); }} style={{ position: \"absolute\", top: 10, right: 12, width: 23, height: 23, borderRadius: \"var(--radius-md)\", border: \"none\", background: \"transparent\", cursor: \"pointer\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", color: \"var(--text-muted)\", padding: 0 }}\n onMouseEnter={(e) => e.currentTarget.style.background = \"var(--bg-hover)\"} onMouseLeave={(e) => e.currentTarget.style.background = \"transparent\"}>\n <Icon name=\"ellipsis\" size={15} />\n </button>\n ) : null}\n {inputs.length === 0 ? null : inputs.length === 1 && !inputs[0] ? (\n <Port tone={inputTones[0] || \"default\"} interactive={live}\n onMouseEnter={onInputEnter ? () => onInputEnter(0) : undefined} onMouseLeave={onInputLeave ? () => onInputLeave(0) : undefined}\n onPointerUp={onInputUp ? (e) => onInputUp(0, e) : undefined}\n style={{ position: \"absolute\", left: -11, top: 33 }} />\n ) : inputs.map((l, i) => (\n <PortLabel key={i} label={l} tone={inputTones[i] || \"default\"} interactive={live}\n onMouseEnter={onInputEnter ? () => onInputEnter(i) : undefined} onMouseLeave={onInputLeave ? () => onInputLeave(i) : undefined}\n onPointerUp={onInputUp ? (e) => onInputUp(i, e) : undefined}\n style={{ position: \"absolute\", left: -14, top: inputs.length === 1 ? 33 : 12 + i * 38 }} />\n ))}\n {output ? (\n <Port tone={outputTone} interactive={live}\n onPointerDown={onOutputDown ? (e) => { e.stopPropagation(); onOutputDown(e); } : undefined}\n onClick={onOutputDown ? (e) => e.stopPropagation() : undefined}\n style={{ position: \"absolute\", right: -11, top: 33 }} />\n ) : null}\n </div>\n );\n}\n","pipeline/NodeCard.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport { IconTile, StatusBadge } from '../display/Badge.vue'\n/**\n * Canvas node card — 220×90, r16, soft shadow. Selected = blue border + blue-100 ring.\n * badge: \"error\" | \"warning\" | \"clock\" overlay on the icon tile.\n * Ports light up on hover, stay lit while a wire is dragged out (outputTone=\"active\"),\n * and show a crosshair when they are a valid drop target (inputTones[i]=\"target\").\n */\nconst props = defineProps({ title: String, subtitle: String, icon: { type: String, default: 'table' }, subtitleIcon: { type: [String, Boolean], default: 'info' }, hue: { type: String, default: 'source' }, selected: Boolean, badge: String, inputs: { type: Array, default: () => [] }, output: { type: Boolean, default: true }, showMenu: { type: Boolean, default: true }, live: Boolean, outputTone: { type: String, default: 'default' }, inputTones: { type: Array, default: () => [] }, style: Object })\nconst emit = defineEmits(['click', 'menu', 'outputDown', 'inputUp', 'inputEnter', 'inputLeave'])\nconst hoverOut = ref(false)\nconst hoverIn = ref(-1)\nfunction skin(tone, hover) {\n const hot = tone === 'active' || tone === 'target' || (props.live && hover)\n return {\n background: tone === 'target' ? 'var(--soft-bg)' : 'var(--bg-surface)',\n border: `2px solid ${hot ? 'var(--action-primary)' : 'var(--port-border)'}`,\n boxShadow: tone === 'active' || tone === 'target' ? '0 0 0 3px var(--focus-ring)' : 'none',\n color: hot ? 'var(--action-primary)' : 'var(--text-muted)',\n cursor: props.live ? (tone === 'active' ? 'grabbing' : 'grab') : 'default',\n transition: 'var(--t-hover)',\n }\n}\nconst portBase = { width: '22px', height: '22px', borderRadius: 'var(--radius-full)', boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }\nconst pillBase = { width: '28px', height: '22px', borderRadius: 'var(--radius-full)', boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '2px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', letterSpacing: 'var(--tracking-xs)', lineHeight: 1 }\nconst tone = i => props.inputTones[i] || 'default'\n</script>\n<template>\n <div @click=\"emit('click')\" :style=\"{ position: 'relative', width: '220px', height: '90px', borderRadius: 'var(--radius-node)', background: 'var(--bg-surface)', border: `1px solid ${selected ? 'var(--selected-border)' : 'var(--border-control)'}`, boxShadow: selected ? 'var(--shadow-selected)' : 'var(--shadow-node)', boxSizing: 'border-box', cursor: 'pointer', ...style }\">\n <div style=\"display:flex;align-items:center;gap:12px;padding:16px;height:100%;box-sizing:border-box\">\n <span style=\"position:relative;flex-shrink:0\">\n <IconTile :icon=\"icon\" :hue=\"hue\" kind=\"node\" />\n <StatusBadge v-if=\"badge === 'error'\" tone=\"danger\" icon=\"bug\" :size=\"20\" style=\"position:absolute;top:-6px;right:-6px\" />\n <StatusBadge v-if=\"badge === 'warning'\" tone=\"warning\" icon=\"triangle-alert\" :size=\"20\" style=\"position:absolute;top:-6px;right:-6px\" />\n <StatusBadge v-if=\"badge === 'clock'\" tone=\"pending\" icon=\"clock\" :size=\"20\" style=\"position:absolute;top:-6px;right:-6px\" />\n </span>\n <div style=\"display:flex;flex-direction:column;min-width:0;flex:1;padding-right:18px\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-base);line-height:17.875px;letter-spacing:var(--tracking-base);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ title }}</span>\n <span style=\"display:flex;align-items:flex-start;gap:4px;padding-top:4px;min-width:0\">\n <span v-if=\"subtitleIcon !== false\" style=\"color:var(--text-muted);display:inline-flex;flex-shrink:0;padding-top:1px\"><Icon :name=\"subtitleIcon || 'info'\" :size=\"13\" /></span>\n <!-- Node types run long (\"Field Value Calculation\"): wrap to a second line, then clamp. -->\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:13.75px;letter-spacing:var(--tracking-xs);color:var(--text-muted);min-width:0;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow-wrap:break-word\">{{ subtitle }}</span>\n </span>\n </div>\n </div>\n <button v-if=\"showMenu\" type=\"button\" @click.stop=\"emit('menu', $event)\" class=\"kb-node-menu\" style=\"position:absolute;top:10px;right:12px;width:23px;height:23px;border-radius:var(--radius-md);border:none;background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text-muted);padding:0\">\n <Icon name=\"ellipsis\" :size=\"15\" />\n </button>\n <template v-if=\"inputs.length\">\n <span v-if=\"inputs.length === 1 && !inputs[0]\"\n @mouseenter=\"hoverIn = 0; emit('inputEnter', 0)\" @mouseleave=\"hoverIn = -1; emit('inputLeave', 0)\"\n @pointerup=\"emit('inputUp', 0, $event)\"\n :style=\"{ ...portBase, ...skin(tone(0), hoverIn === 0), position: 'absolute', left: '-11px', top: '33px' }\">\n <Icon :name=\"tone(0) === 'target' ? 'plus' : 'arrow-right'\" :size=\"12\" :stroke-width=\"2.2\" />\n </span>\n <span v-else v-for=\"(l, i) in inputs\" :key=\"i\"\n @mouseenter=\"hoverIn = i; emit('inputEnter', i)\" @mouseleave=\"hoverIn = -1; emit('inputLeave', i)\"\n @pointerup=\"emit('inputUp', i, $event)\"\n :style=\"{ ...pillBase, ...skin(tone(i), hoverIn === i), position: 'absolute', left: '-14px', top: (inputs.length === 1 ? 33 : 12 + i * 38) + 'px' }\">\n <Icon v-if=\"tone(i) === 'target'\" name=\"plus\" :size=\"12\" :stroke-width=\"2.2\" />\n <template v-else>{{ l }}<Icon name=\"chevron-right\" :size=\"9\" :stroke-width=\"2.5\" /></template>\n </span>\n </template>\n <span v-if=\"output\" @mouseenter=\"hoverOut = true\" @mouseleave=\"hoverOut = false\"\n @pointerdown.stop=\"emit('outputDown', $event)\" @click.stop\n :style=\"{ ...portBase, ...skin(outputTone, hoverOut), position: 'absolute', right: '-11px', top: '33px' }\">\n <Icon name=\"arrow-right\" :size=\"12\" :stroke-width=\"2.2\" />\n </span>\n </div>\n</template>\n<style scoped>\n.kb-node-menu:hover{background:var(--bg-hover)}\n</style>\n","pipeline/PaletteItem.vue":"<script setup>\nimport { ref } from 'vue'\nimport { IconTile } from '../display/Badge.vue'\n// Palette grid tile — 40px tinted icon + 10px two-line label.\ndefineProps({ icon: String, hue: String, label: String, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false)\n</script>\n<template>\n <div role=\"button\" tabindex=\"0\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ width: 67.664, borderRadius: 'var(--radius-lg)', padding: 8, display: 'flex', flexDirection: 'column', gap: 6, alignItems: 'center', cursor: 'grab', boxSizing: 'border-box', background: hover ? 'var(--bg-hover)' : 'transparent', transition: 'background .12s ease', ...style }\">\n <IconTile :icon=\"icon\" :hue=\"hue\" kind=\"palette\" />\n <span style=\"font-family:var(--font-sans);font-size:var(--text-2xs);line-height:var(--leading-2xs);letter-spacing:var(--tracking-2xs);color:var(--text-body);text-align:center\">{{ label }}</span>\n </div>\n</template>\n","pipeline/PaletteListItem.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Palette list row — 14px hued glyph + 13px label, indented under its category.\ndefineProps({ icon: String, hue: String, label: String, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false)\n</script>\n<template>\n <div role=\"button\" tabindex=\"0\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ height: 'var(--h-subnav-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: 8, padding: '6px 8px', boxSizing: 'border-box', cursor: 'grab', background: hover ? 'var(--bg-hover)' : 'transparent', ...style }\">\n <span :style=\"{ color: `var(--cat-${hue})`, display: 'inline-flex' }\"><Icon :name=\"icon\" :size=\"14\" /></span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-body);white-space:nowrap\">{{ label }}</span>\n </div>\n</template>\n","pipeline/PalettePanel.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconTile } from \"../display/Badge.jsx\";\nimport { ViewToggle } from \"../tabs/ViewToggle.jsx\";\n\n/** Palette grid tile — 40px tinted icon + 10px two-line label. */\nexport function PaletteItem({ icon, hue, label, onClick, style }) {\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={{ width: 67.664, borderRadius: \"var(--radius-lg)\", padding: 8, display: \"flex\", flexDirection: \"column\", gap: 6, alignItems: \"center\", cursor: \"grab\", boxSizing: \"border-box\", background: hover ? \"var(--bg-hover)\" : \"transparent\", transition: \"background .12s ease\", ...style }}>\n <IconTile icon={icon} hue={hue} kind=\"palette\" />\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-2xs)\", lineHeight: \"var(--leading-2xs)\", letterSpacing: \"var(--tracking-2xs)\", color: \"var(--text-body)\", textAlign: \"center\" }}>{label}</span>\n </div>\n );\n}\n\n/** Palette list row — 14px hued glyph + 13px label, indented under its category. */\nexport function PaletteListItem({ icon, hue, label, onClick, style }) {\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: \"var(--h-subnav-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 8, padding: \"6px 8px\", boxSizing: \"border-box\", cursor: \"grab\", background: hover ? \"var(--bg-hover)\" : \"transparent\", ...style }}>\n <span style={{ color: `var(--cat-${hue})`, display: \"inline-flex\" }}><Icon name={icon} size={14} /></span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-body)\", whiteSpace: \"nowrap\" }}>{label}</span>\n </div>\n );\n}\n\n/** The full component set of the pipeline editor, verbatim from the file. */\nexport const PIPELINE_CATALOG = [\n { label: \"Data Source\", icon: \"table\", hue: \"source\", items: [{ label: \"Detail Sheet\", icon: \"table\" }, { label: \"Model\", icon: \"layers\" }, { label: \"Dimension\", icon: \"grid-3x3\" }] },\n { label: \"Data Join\", icon: \"text-search\", hue: \"join\", items: [{ label: \"Lookup Data\", icon: \"text-search\" }, { label: \"Association Match\", icon: \"shuffle\" }] },\n { label: \"Data Transformation\", icon: \"sliders-horizontal\", hue: \"transform\", items: [{ label: \"Field Value Calculation\", icon: \"sliders-horizontal\" }, { label: \"Field Split\", icon: \"split\" }, { label: \"Row to Column\", icon: \"arrow-right-left\" }, { label: \"Column to Row\", icon: \"menu\" }, { label: \"Multi-Condition Judge\", icon: \"funnel\" }, { label: \"Regional Calculation\", icon: \"chart-no-axes-column\" }, { label: \"Advanced Transformation\", icon: \"trending-up\" }, { label: \"JS Enhanced Processing\", icon: \"code\" }] },\n { label: \"Data Merge\", icon: \"database\", hue: \"merge\", items: [{ label: \"Group Calculation\", icon: \"database\" }, { label: \"Concat Merge\", icon: \"merge\" }] },\n { label: \"Data Output\", icon: \"table\", hue: \"output\", items: [{ label: \"Detail Sheet\", icon: \"table\" }, { label: \"Model\", icon: \"layers\" }, { label: \"Dimension\", icon: \"grid-3x3\" }] },\n];\n\n/** 240px \"Components\" palette panel with list/grid toggle and collapsible hued categories. */\nexport function PalettePanel({ catalog = PIPELINE_CATALOG, height = \"100%\", onPick, style }) {\n const [view, setView] = React.useState(\"grid\");\n const [closed, setClosed] = React.useState([]);\n const toggle = (l) => setClosed(closed.includes(l) ? closed.filter((x) => x !== l) : [...closed, l]);\n return (\n <div style={{ width: \"var(--sidebar-w)\", height, background: \"var(--bg-surface)\", borderRight: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ height: 43, borderBottom: \"1px solid var(--border-subtle)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"8px 12px\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-body)\" }}>Components</span>\n <ViewToggle value={view} onChange={setView} />\n </div>\n <div style={{ flex: 1, overflowY: \"auto\", padding: 8 }}>\n {catalog.map((cat) => (\n <div key={cat.label}>\n <div onClick={() => toggle(cat.label)} style={{ height: \"var(--h-subnav-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 8, padding: \"6px 8px\", boxSizing: \"border-box\", cursor: \"pointer\" }}>\n <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", transform: closed.includes(cat.label) ? \"rotate(-90deg)\" : \"none\", transition: \"transform .15s ease\" }}><Icon name=\"chevron-down\" size={14} /></span>\n <span style={{ color: `var(--cat-${cat.hue})`, display: \"inline-flex\" }}><Icon name={cat.icon} size={14} /></span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-strong)\" }}>{cat.label}</span>\n </div>\n {!closed.includes(cat.label) ? (\n view === \"grid\" ? (\n <div style={{ display: \"flex\", flexWrap: \"wrap\", padding: \"8px 0 8px 4px\" }}>\n {cat.items.map((it) => <PaletteItem key={it.label} icon={it.icon} hue={cat.hue} label={it.label} onClick={() => onPick && onPick(cat.label, it.label)} />)}\n </div>\n ) : (\n <div style={{ padding: \"2px 0 6px 20px\", display: \"flex\", flexDirection: \"column\" }}>\n {cat.items.map((it) => <PaletteListItem key={it.label} icon={it.icon} hue={cat.hue} label={it.label} onClick={() => onPick && onPick(cat.label, it.label)} />)}\n </div>\n )\n ) : null}\n </div>\n ))}\n </div>\n </div>\n );\n}\n","pipeline/PalettePanel.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport { IconTile } from '../display/Badge.vue'\nimport ViewToggle from '../tabs/ViewToggle.vue'\nimport PaletteItem from './PaletteItem.vue'\nimport PaletteListItem from './PaletteListItem.vue'\n\nexport const PIPELINE_CATALOG = [\n { label: 'Data Source', icon: 'table', hue: 'source', items: [{ label: 'Detail Sheet', icon: 'table' }, { label: 'Model', icon: 'layers' }, { label: 'Dimension', icon: 'grid-3x3' }] },\n { label: 'Data Join', icon: 'text-search', hue: 'join', items: [{ label: 'Lookup Data', icon: 'text-search' }, { label: 'Association Match', icon: 'shuffle' }] },\n { label: 'Data Transformation', icon: 'sliders-horizontal', hue: 'transform', items: [{ label: 'Field Value Calculation', icon: 'sliders-horizontal' }, { label: 'Field Split', icon: 'split' }, { label: 'Row to Column', icon: 'arrow-right-left' }, { label: 'Column to Row', icon: 'menu' }, { label: 'Multi-Condition Judge', icon: 'funnel' }, { label: 'Regional Calculation', icon: 'chart-no-axes-column' }, { label: 'Advanced Transformation', icon: 'trending-up' }, { label: 'JS Enhanced Processing', icon: 'code' }] },\n { label: 'Data Merge', icon: 'database', hue: 'merge', items: [{ label: 'Group Calculation', icon: 'database' }, { label: 'Concat Merge', icon: 'merge' }] },\n { label: 'Data Output', icon: 'table', hue: 'output', items: [{ label: 'Detail Sheet', icon: 'table' }, { label: 'Model', icon: 'layers' }, { label: 'Dimension', icon: 'grid-3x3' }] },\n]\n// 240px \"Components\" palette panel with list/grid toggle and collapsible hued categories.\nconst props = defineProps({ catalog: { type: Array, default: () => PIPELINE_CATALOG }, height: { type: [String, Number], default: '100%' }, style: Object })\nconst emit = defineEmits(['pick'])\nconst view = ref('grid')\nconst closed = ref([])\nfunction toggle(l) { closed.value = closed.value.includes(l) ? closed.value.filter(x => x !== l) : [...closed.value, l] }\n</script>\n<template>\n <div :style=\"{ width: 'var(--sidebar-w)', height, background: 'var(--bg-surface)', borderRight: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"height:43px;border-bottom:1px solid var(--border-subtle);display:flex;align-items:center;justify-content:space-between;padding:8px 12px;box-sizing:border-box;flex-shrink:0\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-body)\">Components</span>\n <ViewToggle v-model=\"view\" />\n </div>\n <div style=\"flex:1;overflow-y:auto;padding:8px\">\n <div v-for=\"cat in catalog\" :key=\"cat.label\">\n <div @click=\"toggle(cat.label)\" style=\"height:var(--h-subnav-item);border-radius:var(--radius-md);display:flex;align-items:center;gap:8px;padding:6px 8px;box-sizing:border-box;cursor:pointer\">\n <span :style=\"{ color: 'var(--text-muted)', display: 'inline-flex', transform: closed.includes(cat.label) ? 'rotate(-90deg)' : 'none', transition: 'transform .15s ease' }\"><Icon name=\"chevron-down\" :size=\"14\" /></span>\n <span :style=\"{ color: `var(--cat-${cat.hue})`, display: 'inline-flex' }\"><Icon :name=\"cat.icon\" :size=\"14\" /></span>\n <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);color:var(--text-strong)\">{{ cat.label }}</span>\n </div>\n <div v-if=\"!closed.includes(cat.label) && view === 'grid'\" style=\"display:flex;flex-wrap:wrap;padding:8px 0 8px 4px\">\n <PaletteItem v-for=\"it in cat.items\" :key=\"it.label\" :icon=\"it.icon\" :hue=\"cat.hue\" :label=\"it.label\" @click=\"emit('pick', cat.label, it.label)\" />\n </div>\n <div v-else-if=\"!closed.includes(cat.label)\" style=\"padding:2px 0 6px 20px;display:flex;flex-direction:column\">\n <PaletteListItem v-for=\"it in cat.items\" :key=\"it.label\" :icon=\"it.icon\" :hue=\"cat.hue\" :label=\"it.label\" @click=\"emit('pick', cat.label, it.label)\" />\n </div>\n </div>\n </div>\n </div>\n</template>\n","pipeline/PipelineCanvas.jsx":"import React from \"react\";\nimport { NodeCard } from \"./NodeCard.jsx\";\n\nconst NODE_W = 220;\nconst outPt = (n) => ({ x: n.x + NODE_W, y: n.y + 44 });\nconst inPt = (n, portIdx = 0) => {\n const multi = (n.inputs || []).filter(Boolean).length > 1;\n return { x: n.x, y: multi ? n.y + 23 + portIdx * 38 : n.y + 44 };\n};\n/** Horizontal cubic bezier; the control offset keeps a backward link bowed out of the cards. */\nconst curve = (p1, p2) => {\n const dx = Math.max(40, (p2.x - p1.x) / 2);\n return `M ${p1.x} ${p1.y} C ${p1.x + dx} ${p1.y}, ${p2.x - dx} ${p2.y}, ${p2.x} ${p2.y}`;\n};\n\n/**\n * Pipeline canvas: #fafafa surface, slate bezier edges, absolutely-placed NodeCards.\n * nodes: [{id,x,y,...NodeCardProps}]; edges: [{from,to,toPort}] (toPort index for A/B inputs).\n * With onConnect, holding an output port drags a dashed wire; input ports light up as drop\n * targets and the drop reports {from,to,toPort}. scale tells the canvas its zoom factor so\n * pointer coordinates stay in canvas space.\n */\nexport function PipelineCanvas({ nodes = [], edges = [], selectedId, onSelect, onMenu, onConnect, snapRadius = 30, scale = 1, width = \"100%\", height = \"100%\", style }) {\n const byId = Object.fromEntries(nodes.map((n) => [n.id, n]));\n const ref = React.useRef(null);\n const [drag, setDrag] = React.useState(null); // {from, x, y, over:{id,port}}\n\n /* The drop target is hit-tested from the pointer position rather than port hover:\n during a drag the cursor is often over the wire layer, not the 22px handle. */\n const hitTest = React.useCallback((x, y, fromId) => {\n let best = null;\n nodes.forEach((n) => {\n if (n.id === fromId) return;\n const ports = n.inputs || [];\n if (!ports.length) return;\n ports.forEach((_, i) => {\n const p = inPt(n, i);\n const d = Math.hypot(p.x - x, p.y - y);\n if (d <= snapRadius && (!best || d < best.d)) best = { id: n.id, port: i, d };\n });\n });\n return best ? { id: best.id, port: best.port } : null;\n }, [nodes, snapRadius]);\n\n React.useEffect(() => {\n if (!drag) return;\n const move = (e) => {\n const r = ref.current.getBoundingClientRect();\n const x = (e.clientX - r.left) / scale, y = (e.clientY - r.top) / scale;\n setDrag((d) => d && { ...d, x, y, over: hitTest(x, y, d.from) });\n };\n const up = () => setDrag((d) => {\n if (d && d.over && onConnect) onConnect({ from: d.from, to: d.over.id, toPort: d.over.port });\n return null;\n });\n window.addEventListener(\"pointermove\", move);\n window.addEventListener(\"pointerup\", up);\n return () => { window.removeEventListener(\"pointermove\", move); window.removeEventListener(\"pointerup\", up); };\n }, [drag, scale, onConnect, hitTest]);\n\n const start = (id, e) => {\n const r = ref.current.getBoundingClientRect();\n setDrag({ from: id, x: (e.clientX - r.left) / scale, y: (e.clientY - r.top) / scale, over: null });\n };\n\n const path = (e) => {\n const a = byId[e.from], b = byId[e.to];\n return a && b ? curve(outPt(a), inPt(b, e.toPort || 0)) : \"\";\n };\n const dragPath = () => {\n const a = byId[drag.from];\n if (!a) return \"\";\n const end = drag.over && byId[drag.over.id] ? inPt(byId[drag.over.id], drag.over.port) : { x: drag.x, y: drag.y };\n return curve(outPt(a), end);\n };\n\n return (\n <div ref={ref} style={{ position: \"relative\", width, height, background: \"var(--bg-app)\", overflow: \"hidden\", cursor: drag ? \"grabbing\" : \"default\", ...style }}>\n <svg style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\", pointerEvents: \"none\", overflow: \"visible\" }}>\n {edges.map((e, i) => <path key={i} d={path(e)} fill=\"none\" stroke=\"var(--edge)\" strokeWidth=\"1.5\" />)}\n {drag ? <path d={dragPath()} fill=\"none\" stroke=\"var(--action-primary)\" strokeWidth=\"1.5\" strokeDasharray=\"5 4\" /> : null}\n </svg>\n {nodes.map(({ id, x, y, ...props }) => (\n <NodeCard key={id} {...props} selected={selectedId === id}\n onClick={() => !drag && onSelect && onSelect(id)} onMenu={(e) => onMenu && onMenu(id, e)}\n onOutputDown={onConnect ? (e) => start(id, e) : undefined}\n onInputUp={onConnect ? () => {} : undefined}\n outputTone={drag && drag.from === id ? \"active\" : \"default\"}\n inputTones={drag && drag.over && drag.over.id === id ? Object.assign([], { [drag.over.port]: \"target\" }) : []}\n style={{ position: \"absolute\", left: x, top: y }} />\n ))}\n </div>\n );\n}\n","pipeline/PipelineCanvas.vue":"<script setup>\nimport { computed, ref, onUnmounted } from 'vue'\nimport NodeCard from './NodeCard.vue'\n/**\n * Pipeline canvas: #fafafa surface, slate bezier edges, absolutely-placed NodeCards.\n * nodes: [{id,x,y,...NodeCardProps}]; edges: [{from,to,toPort}] (toPort index for A/B inputs).\n * With connectable, holding an output port drags a dashed wire; input ports light up as drop\n * targets and the drop emits connect {from,to,toPort}. scale tells the canvas its zoom factor.\n */\nconst props = defineProps({ nodes: { type: Array, default: () => [] }, edges: { type: Array, default: () => [] }, selectedId: [String, Number], connectable: Boolean, snapRadius: { type: Number, default: 30 }, scale: { type: Number, default: 1 }, width: { type: [String, Number], default: '100%' }, height: { type: [String, Number], default: '100%' }, style: Object })\nconst emit = defineEmits(['select', 'menu', 'connect'])\nconst NODE_W = 220\nconst root = ref(null)\nconst drag = ref(null)\nconst byId = computed(() => Object.fromEntries(props.nodes.map(n => [n.id, n])))\nfunction outPt(n) { return { x: n.x + NODE_W, y: n.y + 44 } }\nfunction inPt(n, portIdx = 0) { const multi = (n.inputs || []).filter(Boolean).length > 1; return { x: n.x, y: multi ? n.y + 23 + portIdx * 38 : n.y + 44 } }\nfunction curve(p1, p2) { const dx = Math.max(40, (p2.x - p1.x) / 2); return `M ${p1.x} ${p1.y} C ${p1.x + dx} ${p1.y}, ${p2.x - dx} ${p2.y}, ${p2.x} ${p2.y}` }\nfunction path(e) { const a = byId.value[e.from], b = byId.value[e.to]; return a && b ? curve(outPt(a), inPt(b, e.toPort || 0)) : '' }\nconst dragPath = computed(() => {\n const d = drag.value; if (!d) return ''\n const a = byId.value[d.from]; if (!a) return ''\n const end = d.over && byId.value[d.over.id] ? inPt(byId.value[d.over.id], d.over.port) : { x: d.x, y: d.y }\n return curve(outPt(a), end)\n})\nfunction at(e) { const r = root.value.getBoundingClientRect(); return { x: (e.clientX - r.left) / props.scale, y: (e.clientY - r.top) / props.scale } }\n/* The drop target is hit-tested from the pointer position rather than port hover:\n during a drag the cursor is often over the wire layer, not the 22px handle. */\nfunction hitTest(x, y, fromId) {\n let best = null\n for (const n of props.nodes) {\n if (n.id === fromId) continue\n const ports = n.inputs || []\n for (let i = 0; i < ports.length; i++) {\n const p = inPt(n, i)\n const d = Math.hypot(p.x - x, p.y - y)\n if (d <= props.snapRadius && (!best || d < best.d)) best = { id: n.id, port: i, d }\n }\n }\n return best ? { id: best.id, port: best.port } : null\n}\nfunction move(e) {\n if (!drag.value) return\n const p = at(e)\n drag.value = { ...drag.value, ...p, over: hitTest(p.x, p.y, drag.value.from) }\n}\nfunction up() {\n const d = drag.value\n if (d && d.over) emit('connect', { from: d.from, to: d.over.id, toPort: d.over.port })\n drag.value = null\n window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up)\n}\nfunction start(id, e) {\n if (!props.connectable) return\n drag.value = { from: id, ...at(e), over: null }\n window.addEventListener('pointermove', move); window.addEventListener('pointerup', up)\n}\nconst inputTones = id => (drag.value && drag.value.over && drag.value.over.id === id ? Object.assign([], { [drag.value.over.port]: 'target' }) : [])\nonUnmounted(() => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up) })\n</script>\n<template>\n <div ref=\"root\" :style=\"{ position: 'relative', width, height, background: 'var(--bg-app)', overflow: 'hidden', cursor: drag ? 'grabbing' : 'default', ...style }\">\n <svg style=\"position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible\">\n <path v-for=\"(e, i) in edges\" :key=\"i\" :d=\"path(e)\" fill=\"none\" stroke=\"var(--edge)\" stroke-width=\"1.5\" />\n <path v-if=\"drag\" :d=\"dragPath\" fill=\"none\" stroke=\"var(--action-primary)\" stroke-width=\"1.5\" stroke-dasharray=\"5 4\" />\n </svg>\n <NodeCard v-for=\"n in nodes\" :key=\"n.id\" v-bind=\"n\" :selected=\"selectedId === n.id\" :live=\"connectable\"\n :output-tone=\"drag && drag.from === n.id ? 'active' : 'default'\" :input-tones=\"inputTones(n.id)\"\n @click=\"!drag && emit('select', n.id)\" @menu=\"emit('menu', n.id)\"\n @output-down=\"start(n.id, $event)\"\n :style=\"{ position: 'absolute', left: n.x + 'px', top: n.y + 'px' }\" />\n </div>\n</template>\n","pipeline/ZoomControls.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Floating canvas zoom control (− / percentage / + / fit-to-view). */\nexport function ZoomControls({ value = 100, onChange, onFit, style }) {\n return (\n <div style={{ display: \"inline-flex\", alignItems: \"center\", gap: 2, background: \"var(--bg-surface)\", border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-md)\", boxShadow: \"var(--shadow-menu)\", padding: 3, boxSizing: \"border-box\", ...style }}>\n <IconButton icon=\"minus\" size=\"sm\" onClick={() => onChange && onChange(Math.max(25, value - 10))} label=\"Zoom out\" />\n <span style={{ minWidth: 40, textAlign: \"center\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\" }}>{value}%</span>\n <IconButton icon=\"plus\" size=\"sm\" onClick={() => onChange && onChange(Math.min(200, value + 10))} label=\"Zoom in\" />\n <span style={{ width: 1, alignSelf: \"stretch\", background: \"var(--border-subtle)\", margin: \"0 2px\" }} />\n <IconButton icon=\"target\" size=\"sm\" onClick={onFit} label=\"Fit to view\" />\n </div>\n );\n}\n","pipeline/ZoomControls.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\n// Floating canvas zoom control (− / percentage / + / fit-to-view).\nconst props = defineProps({ modelValue: { type: Number, default: 100 }, style: Object })\nconst emit = defineEmits(['update:modelValue', 'fit'])\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', alignItems: 'center', gap: 2, background: 'var(--bg-surface)', border: '1px solid var(--border-control)', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-menu)', padding: 3, boxSizing: 'border-box', ...style }\">\n <IconButton icon=\"minus\" size=\"sm\" label=\"Zoom out\" @click=\"emit('update:modelValue', Math.max(25, modelValue - 10))\" />\n <span style=\"min-width:40px;text-align:center;font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ modelValue }}%</span>\n <IconButton icon=\"plus\" size=\"sm\" label=\"Zoom in\" @click=\"emit('update:modelValue', Math.min(200, modelValue + 10))\" />\n <span style=\"width:1px;align-self:stretch;background:var(--border-subtle);margin:0 2px\" />\n <IconButton icon=\"target\" size=\"sm\" label=\"Fit to view\" @click=\"emit('fit')\" />\n </div>\n</template>\n"});
1
+ window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"pipeline/ConfigPanel.jsx":"import React from \"react\";\nimport { Button } from \"../buttons/Button.jsx\";\nimport { PanelTabs } from \"../tabs/Tabs.jsx\";\nimport { NumberStepper } from \"../forms/NumberStepper.jsx\";\n\n/** Right node-config panel — 400px: title/subtitle + Save Settings, preview-rows row, PanelTabs, body slot. */\nexport function ConfigPanel({ title, subtitle, tabs = [\"Node Info\", \"Calc Rules\", \"Output Fields\", \"SQL Preview\"], activeTab, onTab, previewRows = 100, onPreviewRows, onSave, onPreview, children, height = \"100%\", style }) {\n const [tab, setTab] = React.useState(tabs[0]);\n const cur = activeTab != null ? activeTab : tab;\n return (\n <div style={{ width: \"var(--panel-w)\", height, background: \"var(--bg-surface)\", borderLeft: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ padding: \"12px 16px 0\", borderBottom: \"1px solid var(--border-card)\", flexShrink: 0 }}>\n <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"flex-start\" }}>\n <div style={{ display: \"flex\", flexDirection: \"column\" }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-md)\", lineHeight: \"17.5px\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-muted)\", paddingTop: 5 }}>{subtitle}</span>\n </div>\n <Button size=\"sm\" onClick={onSave}>Save Settings</Button>\n </div>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8, padding: \"8px 0\" }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-secondary)\" }}>Preview rows</span>\n <NumberStepper value={previewRows} onChange={onPreviewRows} />\n <Button variant=\"primary-soft\" size=\"sm\" icon=\"eye\" style={{ height: 28, borderRadius: \"var(--radius-sm)\", padding: \"4px 10px\" }} onClick={onPreview}>Data Preview</Button>\n </div>\n <PanelTabs tabs={tabs} active={cur} onChange={(t) => { setTab(t); onTab && onTab(t); }} style={{ margin: \"0 -16px\", padding: \"0 16px\", borderBottom: \"none\" }} />\n </div>\n <div style={{ flex: 1, overflowY: \"auto\", padding: 16, display: \"flex\", flexDirection: \"column\", gap: 16 }}>{children}</div>\n </div>\n );\n}\n","pipeline/ConfigPanel.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Button from '../buttons/Button.vue'\nimport PanelTabs from '../tabs/Tabs.vue'\nimport NumberStepper from '../forms/NumberStepper.vue'\n// Right node-config panel — 400px: title/subtitle + Save Settings, preview-rows row, PanelTabs, body slot.\nconst props = defineProps({ title: String, subtitle: String, tabs: { type: Array, default: () => ['Node Info', 'Calc Rules', 'Output Fields', 'SQL Preview'] }, activeTab: String, previewRows: { type: Number, default: 100 }, height: { type: [String, Number], default: '100%' }, style: Object })\nconst emit = defineEmits(['tab', 'update:previewRows', 'save', 'preview'])\nconst tabState = ref(props.tabs[0])\nconst cur = computed(() => props.activeTab != null ? props.activeTab : tabState.value)\nfunction setTab(t) { tabState.value = t; emit('tab', t) }\n</script>\n<template>\n <div :style=\"{ width: 'var(--panel-w)', height, background: 'var(--bg-surface)', borderLeft: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"padding:12px 16px 0;border-bottom:1px solid var(--border-card);flex-shrink:0\">\n <div style=\"display:flex;justify-content:space-between;align-items:flex-start\">\n <div style=\"display:flex;flex-direction:column\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:17.5px;letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted);padding-top:5px\">{{ subtitle }}</span>\n </div>\n <Button size=\"sm\" @click=\"emit('save')\">Save Settings</Button>\n </div>\n <div style=\"display:flex;align-items:center;gap:8px;padding:8px 0\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)\">Preview rows</span>\n <NumberStepper :model-value=\"previewRows\" @update:modelValue=\"v => emit('update:previewRows', v)\" />\n <Button variant=\"primary-soft\" size=\"sm\" icon=\"eye\" style=\"height:28px;border-radius:var(--radius-sm);padding:4px 10px\" @click=\"emit('preview')\">Data Preview</Button>\n </div>\n <PanelTabs :tabs=\"tabs\" :active=\"cur\" @change=\"setTab\" style=\"margin:0 -16px;padding:0 16px;border-bottom:none\" />\n </div>\n <div style=\"flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:16px\"><slot /></div>\n </div>\n</template>\n","pipeline/DataPreview.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { Checkbox } from \"../forms/Checkbox.jsx\";\nimport { DataTable } from \"../data/DataTable.jsx\";\n\n/** Bottom data-preview drawer — 250px: 40px header (title, Debug Mode, collapse) + dense table. */\nexport function DataPreview({ title = \"Data Preview\", columns = [], rows = [], debug = false, onDebug, collapsed, onCollapse, style }) {\n return (\n <div style={{ background: \"var(--bg-surface)\", borderTop: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", height: collapsed ? 40 : \"var(--drawer-h)\", overflow: \"hidden\", boxSizing: \"border-box\", flexShrink: 0, transition: \"height .15s ease\", ...style }}>\n <div style={{ height: 40, display: \"flex\", alignItems: \"center\", gap: 12, padding: \"0 16px\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\" }}>{title}</span>\n <span style={{ flex: 1 }} />\n <Checkbox label=\"Debug Mode\" checked={debug} onChange={onDebug} />\n <IconButton icon={collapsed ? \"chevron-up\" : \"chevron-down\"} size=\"xs\" muted onClick={onCollapse} />\n </div>\n {!collapsed ? (\n <div style={{ overflowY: \"auto\", borderTop: \"1px solid var(--border-subtle)\" }}>\n <DataTable dense columns={columns} rows={rows} headerBorder={false} />\n </div>\n ) : null}\n </div>\n );\n}\n","pipeline/DataPreview.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\nimport Checkbox from '../forms/Checkbox.vue'\nimport DataTable from '../data/DataTable.vue'\n// Bottom data-preview drawer — 250px: 40px header (title, Debug Mode, collapse) + dense table.\ndefineProps({ title: { type: String, default: 'Data Preview' }, columns: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, debug: Boolean, collapsed: Boolean, style: Object })\nconst emit = defineEmits(['update:debug', 'toggleCollapse'])\n</script>\n<template>\n <div :style=\"{ background: 'var(--bg-surface)', borderTop: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', height: collapsed ? 40 : 'var(--drawer-h)', overflow: 'hidden', boxSizing: 'border-box', flexShrink: 0, transition: 'height .15s ease', ...style }\">\n <div style=\"height:40px;display:flex;align-items:center;gap:12px;padding:0 16px;box-sizing:border-box;flex-shrink:0\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)\">{{ title }}</span>\n <span style=\"flex:1\" />\n <Checkbox label=\"Debug Mode\" :checked=\"debug\" @change=\"v => emit('update:debug', v)\" />\n <IconButton :icon=\"collapsed ? 'chevron-up' : 'chevron-down'\" size=\"xs\" muted @click=\"emit('toggleCollapse')\" />\n </div>\n <div v-if=\"!collapsed\" style=\"overflow-y:auto;border-top:1px solid var(--border-subtle)\">\n <DataTable dense :columns=\"columns\" :rows=\"rows\" :header-border=\"false\" />\n </div>\n </div>\n</template>\n","pipeline/NodeCard.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconTile, StatusBadge } from \"../display/Badge.jsx\";\n\n/* Ports are the connection handles: they light up on hover, stay lit while a wire is\n being dragged out of them, and show a crosshair when they're a valid drop target. */\nfunction portSkin(tone, hover, interactive) {\n const hot = tone === \"active\" || tone === \"target\" || (interactive && hover);\n return {\n background: tone === \"target\" ? \"var(--soft-bg)\" : \"var(--bg-surface)\",\n border: `2px solid ${hot ? \"var(--action-primary)\" : \"var(--port-border)\"}`,\n boxShadow: tone === \"active\" || tone === \"target\" ? \"0 0 0 3px var(--focus-ring)\" : \"none\",\n color: hot ? \"var(--action-primary)\" : \"var(--text-muted)\",\n cursor: interactive ? (tone === \"active\" ? \"grabbing\" : \"grab\") : \"default\",\n transition: \"var(--t-hover)\",\n };\n}\n\n/** Round output/input port — 22px. tone: \"default\" | \"active\" (dragging from it) | \"target\" (drop here). */\nexport function Port({ tone = \"default\", interactive = false, style, onMouseEnter, onMouseLeave, ...rest }) {\n const [hover, setHover] = React.useState(false);\n return (\n <span {...rest}\n onMouseEnter={(e) => { setHover(true); onMouseEnter && onMouseEnter(e); }}\n onMouseLeave={(e) => { setHover(false); onMouseLeave && onMouseLeave(e); }}\n style={{ width: 22, height: 22, borderRadius: \"var(--radius-full)\", boxSizing: \"border-box\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", ...portSkin(tone, hover, interactive), ...style }}>\n <Icon name={tone === \"target\" ? \"plus\" : \"arrow-right\"} size={12} strokeWidth={2.2} />\n </span>\n );\n}\n\n/** Labeled input port pill (\"A >\", \"B >\") — 28×22, same three tones as Port. */\nexport function PortLabel({ label = \"A\", tone = \"default\", interactive = false, style, onMouseEnter, onMouseLeave, ...rest }) {\n const [hover, setHover] = React.useState(false);\n return (\n <span {...rest}\n onMouseEnter={(e) => { setHover(true); onMouseEnter && onMouseEnter(e); }}\n onMouseLeave={(e) => { setHover(false); onMouseLeave && onMouseLeave(e); }}\n style={{ width: 28, height: 22, borderRadius: \"var(--radius-full)\", boxSizing: \"border-box\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", gap: 2, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", letterSpacing: \"var(--tracking-xs)\", lineHeight: 1, ...portSkin(tone, hover, interactive), ...style }}>\n {tone === \"target\" ? <Icon name=\"plus\" size={12} strokeWidth={2.2} /> : <>{label}<Icon name=\"chevron-right\" size={9} strokeWidth={2.5} /></>}\n </span>\n );\n}\n\n/**\n * Canvas node card — 220×90, r16, soft shadow. Selected = blue border + blue-100 ring.\n * badge: \"error\" | \"warning\" | \"clock\" overlay on the icon tile.\n * Port interaction is driven by the canvas: onOutputDown starts a wire, onInputUp lands it,\n * and outputTone / inputTones light the handles.\n */\nexport function NodeCard({ title, subtitle, icon = \"table\", subtitleIcon, hue = \"source\", selected, badge, inputs = [], output = true, showMenu = true, onClick, onMenu, onOutputDown, onInputUp, onInputEnter, onInputLeave, outputTone = \"default\", inputTones = [], style }) {\n const live = !!(onOutputDown || onInputUp);\n return (\n <div onClick={onClick} style={{ position: \"relative\", width: 220, height: 90, borderRadius: \"var(--radius-node)\", background: \"var(--bg-surface)\", border: `1px solid ${selected ? \"var(--selected-border)\" : \"var(--border-control)\"}`, boxShadow: selected ? \"var(--shadow-selected)\" : \"var(--shadow-node)\", boxSizing: \"border-box\", cursor: \"pointer\", ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 12, padding: 16, height: \"100%\", boxSizing: \"border-box\" }}>\n <span style={{ position: \"relative\", flexShrink: 0 }}>\n <IconTile icon={icon} hue={hue} kind=\"node\" />\n {badge === \"error\" ? <StatusBadge tone=\"danger\" icon=\"bug\" size={20} style={{ position: \"absolute\", top: -6, right: -6 }} /> : null}\n {badge === \"warning\" ? <StatusBadge tone=\"warning\" icon=\"triangle-alert\" size={20} style={{ position: \"absolute\", top: -6, right: -6 }} /> : null}\n {badge === \"clock\" ? <StatusBadge tone=\"pending\" icon=\"clock\" size={20} style={{ position: \"absolute\", top: -6, right: -6 }} /> : null}\n </span>\n <div style={{ display: \"flex\", flexDirection: \"column\", minWidth: 0, flex: 1, paddingRight: 18 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-base)\", lineHeight: \"17.875px\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{title}</span>\n <span style={{ display: \"flex\", alignItems: \"flex-start\", gap: 4, paddingTop: 4, minWidth: 0 }}>\n {subtitleIcon !== false ? <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", flexShrink: 0, paddingTop: 1 }}><Icon name={subtitleIcon || \"info\"} size={13} /></span> : null}\n {/* Node types run long (\"Field Value Calculation\"), so the subtitle wraps to a\n second line and only then clamps — the card size never changes. */}\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"13.75px\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-muted)\", minWidth: 0, overflow: \"hidden\", display: \"-webkit-box\", WebkitBoxOrient: \"vertical\", WebkitLineClamp: 2, overflowWrap: \"break-word\" }}>{subtitle}</span>\n </span>\n </div>\n </div>\n {showMenu ? (\n <button type=\"button\" onClick={(e) => { e.stopPropagation(); onMenu && onMenu(e); }} style={{ position: \"absolute\", top: 10, right: 12, width: 23, height: 23, borderRadius: \"var(--radius-md)\", border: \"none\", background: \"transparent\", cursor: \"pointer\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", color: \"var(--text-muted)\", padding: 0 }}\n onMouseEnter={(e) => e.currentTarget.style.background = \"var(--bg-hover)\"} onMouseLeave={(e) => e.currentTarget.style.background = \"transparent\"}>\n <Icon name=\"ellipsis\" size={15} />\n </button>\n ) : null}\n {inputs.length === 0 ? null : inputs.length === 1 && !inputs[0] ? (\n <Port tone={inputTones[0] || \"default\"} interactive={live}\n onMouseEnter={onInputEnter ? () => onInputEnter(0) : undefined} onMouseLeave={onInputLeave ? () => onInputLeave(0) : undefined}\n onPointerUp={onInputUp ? (e) => onInputUp(0, e) : undefined}\n style={{ position: \"absolute\", left: -11, top: 33 }} />\n ) : inputs.map((l, i) => (\n <PortLabel key={i} label={l} tone={inputTones[i] || \"default\"} interactive={live}\n onMouseEnter={onInputEnter ? () => onInputEnter(i) : undefined} onMouseLeave={onInputLeave ? () => onInputLeave(i) : undefined}\n onPointerUp={onInputUp ? (e) => onInputUp(i, e) : undefined}\n style={{ position: \"absolute\", left: -14, top: inputs.length === 1 ? 33 : 12 + i * 38 }} />\n ))}\n {output ? (\n <Port tone={outputTone} interactive={live}\n onPointerDown={onOutputDown ? (e) => { e.stopPropagation(); onOutputDown(e); } : undefined}\n onClick={onOutputDown ? (e) => e.stopPropagation() : undefined}\n style={{ position: \"absolute\", right: -11, top: 33 }} />\n ) : null}\n </div>\n );\n}\n","pipeline/NodeCard.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconTile from '../display/IconTile.vue'\nimport StatusBadge from '../display/StatusBadge.vue'\n/**\n * Canvas node card — 220×90, r16, soft shadow. Selected = blue border + blue-100 ring.\n * badge: \"error\" | \"warning\" | \"clock\" overlay on the icon tile.\n * Ports light up on hover, stay lit while a wire is dragged out (outputTone=\"active\"),\n * and show a crosshair when they are a valid drop target (inputTones[i]=\"target\").\n */\nconst props = defineProps({ title: String, subtitle: String, icon: { type: String, default: 'table' }, subtitleIcon: { type: [String, Boolean], default: 'info' }, hue: { type: String, default: 'source' }, selected: Boolean, badge: String, inputs: { type: Array, default: () => [] }, output: { type: Boolean, default: true }, showMenu: { type: Boolean, default: true }, live: Boolean, outputTone: { type: String, default: 'default' }, inputTones: { type: Array, default: () => [] }, style: Object })\nconst emit = defineEmits(['click', 'menu', 'outputDown', 'inputUp', 'inputEnter', 'inputLeave'])\nconst hoverOut = ref(false)\nconst hoverIn = ref(-1)\nfunction skin(tone, hover) {\n const hot = tone === 'active' || tone === 'target' || (props.live && hover)\n return {\n background: tone === 'target' ? 'var(--soft-bg)' : 'var(--bg-surface)',\n border: `2px solid ${hot ? 'var(--action-primary)' : 'var(--port-border)'}`,\n boxShadow: tone === 'active' || tone === 'target' ? '0 0 0 3px var(--focus-ring)' : 'none',\n color: hot ? 'var(--action-primary)' : 'var(--text-muted)',\n cursor: props.live ? (tone === 'active' ? 'grabbing' : 'grab') : 'default',\n transition: 'var(--t-hover)',\n }\n}\nconst portBase = { width: '22px', height: '22px', borderRadius: 'var(--radius-full)', boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }\nconst pillBase = { width: '28px', height: '22px', borderRadius: 'var(--radius-full)', boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '2px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', letterSpacing: 'var(--tracking-xs)', lineHeight: 1 }\nconst tone = i => props.inputTones[i] || 'default'\n</script>\n<template>\n <div @click=\"emit('click')\" :style=\"{ position: 'relative', width: '220px', height: '90px', borderRadius: 'var(--radius-node)', background: 'var(--bg-surface)', border: `1px solid ${selected ? 'var(--selected-border)' : 'var(--border-control)'}`, boxShadow: selected ? 'var(--shadow-selected)' : 'var(--shadow-node)', boxSizing: 'border-box', cursor: 'pointer', ...style }\">\n <div style=\"display:flex;align-items:center;gap:12px;padding:16px;height:100%;box-sizing:border-box\">\n <span style=\"position:relative;flex-shrink:0\">\n <IconTile :icon=\"icon\" :hue=\"hue\" kind=\"node\" />\n <StatusBadge v-if=\"badge === 'error'\" tone=\"danger\" icon=\"bug\" :size=\"20\" style=\"position:absolute;top:-6px;right:-6px\" />\n <StatusBadge v-if=\"badge === 'warning'\" tone=\"warning\" icon=\"triangle-alert\" :size=\"20\" style=\"position:absolute;top:-6px;right:-6px\" />\n <StatusBadge v-if=\"badge === 'clock'\" tone=\"pending\" icon=\"clock\" :size=\"20\" style=\"position:absolute;top:-6px;right:-6px\" />\n </span>\n <div style=\"display:flex;flex-direction:column;min-width:0;flex:1;padding-right:18px\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-base);line-height:17.875px;letter-spacing:var(--tracking-base);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ title }}</span>\n <span style=\"display:flex;align-items:flex-start;gap:4px;padding-top:4px;min-width:0\">\n <span v-if=\"subtitleIcon !== false\" style=\"color:var(--text-muted);display:inline-flex;flex-shrink:0;padding-top:1px\"><Icon :name=\"subtitleIcon || 'info'\" :size=\"13\" /></span>\n <!-- Node types run long (\"Field Value Calculation\"): wrap to a second line, then clamp. -->\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:13.75px;letter-spacing:var(--tracking-xs);color:var(--text-muted);min-width:0;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow-wrap:break-word\">{{ subtitle }}</span>\n </span>\n </div>\n </div>\n <button v-if=\"showMenu\" type=\"button\" @click.stop=\"emit('menu', $event)\" class=\"kb-node-menu\" style=\"position:absolute;top:10px;right:12px;width:23px;height:23px;border-radius:var(--radius-md);border:none;background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text-muted);padding:0\">\n <Icon name=\"ellipsis\" :size=\"15\" />\n </button>\n <template v-if=\"inputs.length\">\n <span v-if=\"inputs.length === 1 && !inputs[0]\"\n @mouseenter=\"hoverIn = 0; emit('inputEnter', 0)\" @mouseleave=\"hoverIn = -1; emit('inputLeave', 0)\"\n @pointerup=\"emit('inputUp', 0, $event)\"\n :style=\"{ ...portBase, ...skin(tone(0), hoverIn === 0), position: 'absolute', left: '-11px', top: '33px' }\">\n <Icon :name=\"tone(0) === 'target' ? 'plus' : 'arrow-right'\" :size=\"12\" :stroke-width=\"2.2\" />\n </span>\n <span v-else v-for=\"(l, i) in inputs\" :key=\"i\"\n @mouseenter=\"hoverIn = i; emit('inputEnter', i)\" @mouseleave=\"hoverIn = -1; emit('inputLeave', i)\"\n @pointerup=\"emit('inputUp', i, $event)\"\n :style=\"{ ...pillBase, ...skin(tone(i), hoverIn === i), position: 'absolute', left: '-14px', top: (inputs.length === 1 ? 33 : 12 + i * 38) + 'px' }\">\n <Icon v-if=\"tone(i) === 'target'\" name=\"plus\" :size=\"12\" :stroke-width=\"2.2\" />\n <template v-else>{{ l }}<Icon name=\"chevron-right\" :size=\"9\" :stroke-width=\"2.5\" /></template>\n </span>\n </template>\n <span v-if=\"output\" @mouseenter=\"hoverOut = true\" @mouseleave=\"hoverOut = false\"\n @pointerdown.stop=\"emit('outputDown', $event)\" @click.stop\n :style=\"{ ...portBase, ...skin(outputTone, hoverOut), position: 'absolute', right: '-11px', top: '33px' }\">\n <Icon name=\"arrow-right\" :size=\"12\" :stroke-width=\"2.2\" />\n </span>\n </div>\n</template>\n<style scoped>\n.kb-node-menu:hover{background:var(--bg-hover)}\n</style>\n","pipeline/PaletteItem.vue":"<script setup>\nimport { ref } from 'vue'\nimport IconTile from '../display/IconTile.vue'\n// Palette grid tile — 40px tinted icon + 10px two-line label.\ndefineProps({ icon: String, hue: String, label: String, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false)\n</script>\n<template>\n <div role=\"button\" tabindex=\"0\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ width: 67.664, borderRadius: 'var(--radius-lg)', padding: 8, display: 'flex', flexDirection: 'column', gap: 6, alignItems: 'center', cursor: 'grab', boxSizing: 'border-box', background: hover ? 'var(--bg-hover)' : 'transparent', transition: 'background .12s ease', ...style }\">\n <IconTile :icon=\"icon\" :hue=\"hue\" kind=\"palette\" />\n <span style=\"font-family:var(--font-sans);font-size:var(--text-2xs);line-height:var(--leading-2xs);letter-spacing:var(--tracking-2xs);color:var(--text-body);text-align:center\">{{ label }}</span>\n </div>\n</template>\n","pipeline/PaletteListItem.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Palette list row — 14px hued glyph + 13px label, indented under its category.\ndefineProps({ icon: String, hue: String, label: String, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false)\n</script>\n<template>\n <div role=\"button\" tabindex=\"0\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ height: 'var(--h-subnav-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: 8, padding: '6px 8px', boxSizing: 'border-box', cursor: 'grab', background: hover ? 'var(--bg-hover)' : 'transparent', ...style }\">\n <span :style=\"{ color: `var(--cat-${hue})`, display: 'inline-flex' }\"><Icon :name=\"icon\" :size=\"14\" /></span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-body);white-space:nowrap\">{{ label }}</span>\n </div>\n</template>\n","pipeline/PalettePanel.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconTile } from \"../display/Badge.jsx\";\nimport { ViewToggle } from \"../tabs/ViewToggle.jsx\";\nimport { PIPELINE_CATALOG } from \"./pipeline-catalog.js\";\n\n/** Palette grid tile — 40px tinted icon + 10px two-line label. */\nexport function PaletteItem({ icon, hue, label, onClick, style }) {\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={{ width: 67.664, borderRadius: \"var(--radius-lg)\", padding: 8, display: \"flex\", flexDirection: \"column\", gap: 6, alignItems: \"center\", cursor: \"grab\", boxSizing: \"border-box\", background: hover ? \"var(--bg-hover)\" : \"transparent\", transition: \"background .12s ease\", ...style }}>\n <IconTile icon={icon} hue={hue} kind=\"palette\" />\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-2xs)\", lineHeight: \"var(--leading-2xs)\", letterSpacing: \"var(--tracking-2xs)\", color: \"var(--text-body)\", textAlign: \"center\" }}>{label}</span>\n </div>\n );\n}\n\n/** Palette list row — 14px hued glyph + 13px label, indented under its category. */\nexport function PaletteListItem({ icon, hue, label, onClick, style }) {\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: \"var(--h-subnav-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 8, padding: \"6px 8px\", boxSizing: \"border-box\", cursor: \"grab\", background: hover ? \"var(--bg-hover)\" : \"transparent\", ...style }}>\n <span style={{ color: `var(--cat-${hue})`, display: \"inline-flex\" }}><Icon name={icon} size={14} /></span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-body)\", whiteSpace: \"nowrap\" }}>{label}</span>\n </div>\n );\n}\n\nexport { PIPELINE_CATALOG };\n\n/** 240px \"Components\" palette panel with list/grid toggle and collapsible hued categories. */\nexport function PalettePanel({ catalog = PIPELINE_CATALOG, height = \"100%\", onPick, style }) {\n const [view, setView] = React.useState(\"grid\");\n const [closed, setClosed] = React.useState([]);\n const toggle = (l) => setClosed(closed.includes(l) ? closed.filter((x) => x !== l) : [...closed, l]);\n return (\n <div style={{ width: \"var(--sidebar-w)\", height, background: \"var(--bg-surface)\", borderRight: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ height: 43, borderBottom: \"1px solid var(--border-subtle)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"8px 12px\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-body)\" }}>Components</span>\n <ViewToggle value={view} onChange={setView} />\n </div>\n <div style={{ flex: 1, overflowY: \"auto\", padding: 8 }}>\n {catalog.map((cat) => (\n <div key={cat.label}>\n <div onClick={() => toggle(cat.label)} style={{ height: \"var(--h-subnav-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 8, padding: \"6px 8px\", boxSizing: \"border-box\", cursor: \"pointer\" }}>\n <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", transform: closed.includes(cat.label) ? \"rotate(-90deg)\" : \"none\", transition: \"transform .15s ease\" }}><Icon name=\"chevron-down\" size={14} /></span>\n <span style={{ color: `var(--cat-${cat.hue})`, display: \"inline-flex\" }}><Icon name={cat.icon} size={14} /></span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-strong)\" }}>{cat.label}</span>\n </div>\n {!closed.includes(cat.label) ? (\n view === \"grid\" ? (\n <div style={{ display: \"flex\", flexWrap: \"wrap\", padding: \"8px 0 8px 4px\" }}>\n {cat.items.map((it) => <PaletteItem key={it.label} icon={it.icon} hue={cat.hue} label={it.label} onClick={() => onPick && onPick(cat.label, it.label)} />)}\n </div>\n ) : (\n <div style={{ padding: \"2px 0 6px 20px\", display: \"flex\", flexDirection: \"column\" }}>\n {cat.items.map((it) => <PaletteListItem key={it.label} icon={it.icon} hue={cat.hue} label={it.label} onClick={() => onPick && onPick(cat.label, it.label)} />)}\n </div>\n )\n ) : null}\n </div>\n ))}\n </div>\n </div>\n );\n}\n","pipeline/PalettePanel.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport { PIPELINE_CATALOG } from './pipeline-catalog.js'\nimport ViewToggle from '../tabs/ViewToggle.vue'\nimport PaletteItem from './PaletteItem.vue'\nimport PaletteListItem from './PaletteListItem.vue'\n\n// 240px \"Components\" palette panel with list/grid toggle and collapsible hued categories.\nconst props = defineProps({ catalog: { type: Array, default: () => PIPELINE_CATALOG }, height: { type: [String, Number], default: '100%' }, style: Object })\nconst emit = defineEmits(['pick'])\nconst view = ref('grid')\nconst closed = ref([])\nfunction toggle(l) { closed.value = closed.value.includes(l) ? closed.value.filter(x => x !== l) : [...closed.value, l] }\n</script>\n<template>\n <div :style=\"{ width: 'var(--sidebar-w)', height, background: 'var(--bg-surface)', borderRight: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"height:43px;border-bottom:1px solid var(--border-subtle);display:flex;align-items:center;justify-content:space-between;padding:8px 12px;box-sizing:border-box;flex-shrink:0\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-body)\">Components</span>\n <ViewToggle v-model=\"view\" />\n </div>\n <div style=\"flex:1;overflow-y:auto;padding:8px\">\n <div v-for=\"cat in catalog\" :key=\"cat.label\">\n <div @click=\"toggle(cat.label)\" style=\"height:var(--h-subnav-item);border-radius:var(--radius-md);display:flex;align-items:center;gap:8px;padding:6px 8px;box-sizing:border-box;cursor:pointer\">\n <span :style=\"{ color: 'var(--text-muted)', display: 'inline-flex', transform: closed.includes(cat.label) ? 'rotate(-90deg)' : 'none', transition: 'transform .15s ease' }\"><Icon name=\"chevron-down\" :size=\"14\" /></span>\n <span :style=\"{ color: `var(--cat-${cat.hue})`, display: 'inline-flex' }\"><Icon :name=\"cat.icon\" :size=\"14\" /></span>\n <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);color:var(--text-strong)\">{{ cat.label }}</span>\n </div>\n <div v-if=\"!closed.includes(cat.label) && view === 'grid'\" style=\"display:flex;flex-wrap:wrap;padding:8px 0 8px 4px\">\n <PaletteItem v-for=\"it in cat.items\" :key=\"it.label\" :icon=\"it.icon\" :hue=\"cat.hue\" :label=\"it.label\" @click=\"emit('pick', cat.label, it.label)\" />\n </div>\n <div v-else-if=\"!closed.includes(cat.label)\" style=\"padding:2px 0 6px 20px;display:flex;flex-direction:column\">\n <PaletteListItem v-for=\"it in cat.items\" :key=\"it.label\" :icon=\"it.icon\" :hue=\"cat.hue\" :label=\"it.label\" @click=\"emit('pick', cat.label, it.label)\" />\n </div>\n </div>\n </div>\n </div>\n</template>\n","pipeline/PipelineCanvas.jsx":"import React from \"react\";\nimport { NodeCard } from \"./NodeCard.jsx\";\n\nconst NODE_W = 220;\nconst outPt = (n) => ({ x: n.x + NODE_W, y: n.y + 44 });\nconst inPt = (n, portIdx = 0) => {\n const multi = (n.inputs || []).filter(Boolean).length > 1;\n return { x: n.x, y: multi ? n.y + 23 + portIdx * 38 : n.y + 44 };\n};\n/** Horizontal cubic bezier; the control offset keeps a backward link bowed out of the cards. */\nconst curve = (p1, p2) => {\n const dx = Math.max(40, (p2.x - p1.x) / 2);\n return `M ${p1.x} ${p1.y} C ${p1.x + dx} ${p1.y}, ${p2.x - dx} ${p2.y}, ${p2.x} ${p2.y}`;\n};\n\n/**\n * Pipeline canvas: #fafafa surface, slate bezier edges, absolutely-placed NodeCards.\n * nodes: [{id,x,y,...NodeCardProps}]; edges: [{from,to,toPort}] (toPort index for A/B inputs).\n * With onConnect, holding an output port drags a dashed wire; input ports light up as drop\n * targets and the drop reports {from,to,toPort}. scale tells the canvas its zoom factor so\n * pointer coordinates stay in canvas space.\n */\nexport function PipelineCanvas({ nodes = [], edges = [], selectedId, onSelect, onMenu, onConnect, snapRadius = 30, scale = 1, width = \"100%\", height = \"100%\", style }) {\n const byId = Object.fromEntries(nodes.map((n) => [n.id, n]));\n const ref = React.useRef(null);\n const [drag, setDrag] = React.useState(null); // {from, x, y, over:{id,port}}\n\n /* The drop target is hit-tested from the pointer position rather than port hover:\n during a drag the cursor is often over the wire layer, not the 22px handle. */\n const hitTest = React.useCallback((x, y, fromId) => {\n let best = null;\n nodes.forEach((n) => {\n if (n.id === fromId) return;\n const ports = n.inputs || [];\n if (!ports.length) return;\n ports.forEach((_, i) => {\n const p = inPt(n, i);\n const d = Math.hypot(p.x - x, p.y - y);\n if (d <= snapRadius && (!best || d < best.d)) best = { id: n.id, port: i, d };\n });\n });\n return best ? { id: best.id, port: best.port } : null;\n }, [nodes, snapRadius]);\n\n React.useEffect(() => {\n if (!drag) return;\n const move = (e) => {\n const r = ref.current.getBoundingClientRect();\n const x = (e.clientX - r.left) / scale, y = (e.clientY - r.top) / scale;\n setDrag((d) => d && { ...d, x, y, over: hitTest(x, y, d.from) });\n };\n const up = () => setDrag((d) => {\n if (d && d.over && onConnect) onConnect({ from: d.from, to: d.over.id, toPort: d.over.port });\n return null;\n });\n window.addEventListener(\"pointermove\", move);\n window.addEventListener(\"pointerup\", up);\n return () => { window.removeEventListener(\"pointermove\", move); window.removeEventListener(\"pointerup\", up); };\n }, [drag, scale, onConnect, hitTest]);\n\n const start = (id, e) => {\n const r = ref.current.getBoundingClientRect();\n setDrag({ from: id, x: (e.clientX - r.left) / scale, y: (e.clientY - r.top) / scale, over: null });\n };\n\n const path = (e) => {\n const a = byId[e.from], b = byId[e.to];\n return a && b ? curve(outPt(a), inPt(b, e.toPort || 0)) : \"\";\n };\n const dragPath = () => {\n const a = byId[drag.from];\n if (!a) return \"\";\n const end = drag.over && byId[drag.over.id] ? inPt(byId[drag.over.id], drag.over.port) : { x: drag.x, y: drag.y };\n return curve(outPt(a), end);\n };\n\n return (\n <div ref={ref} style={{ position: \"relative\", width, height, background: \"var(--bg-app)\", overflow: \"hidden\", cursor: drag ? \"grabbing\" : \"default\", ...style }}>\n <svg style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\", pointerEvents: \"none\", overflow: \"visible\" }}>\n {edges.map((e, i) => <path key={i} d={path(e)} fill=\"none\" stroke=\"var(--edge)\" strokeWidth=\"1.5\" />)}\n {drag ? <path d={dragPath()} fill=\"none\" stroke=\"var(--action-primary)\" strokeWidth=\"1.5\" strokeDasharray=\"5 4\" /> : null}\n </svg>\n {nodes.map(({ id, x, y, ...props }) => (\n <NodeCard key={id} {...props} selected={selectedId === id}\n onClick={() => !drag && onSelect && onSelect(id)} onMenu={(e) => onMenu && onMenu(id, e)}\n onOutputDown={onConnect ? (e) => start(id, e) : undefined}\n onInputUp={onConnect ? () => {} : undefined}\n outputTone={drag && drag.from === id ? \"active\" : \"default\"}\n inputTones={drag && drag.over && drag.over.id === id ? Object.assign([], { [drag.over.port]: \"target\" }) : []}\n style={{ position: \"absolute\", left: x, top: y }} />\n ))}\n </div>\n );\n}\n","pipeline/PipelineCanvas.vue":"<script setup>\nimport { computed, ref, onUnmounted } from 'vue'\nimport NodeCard from './NodeCard.vue'\n/**\n * Pipeline canvas: #fafafa surface, slate bezier edges, absolutely-placed NodeCards.\n * nodes: [{id,x,y,...NodeCardProps}]; edges: [{from,to,toPort}] (toPort index for A/B inputs).\n * With connectable, holding an output port drags a dashed wire; input ports light up as drop\n * targets and the drop emits connect {from,to,toPort}. scale tells the canvas its zoom factor.\n */\nconst props = defineProps({ nodes: { type: Array, default: () => [] }, edges: { type: Array, default: () => [] }, selectedId: [String, Number], connectable: Boolean, snapRadius: { type: Number, default: 30 }, scale: { type: Number, default: 1 }, width: { type: [String, Number], default: '100%' }, height: { type: [String, Number], default: '100%' }, style: Object })\nconst emit = defineEmits(['select', 'menu', 'connect'])\nconst NODE_W = 220\nconst root = ref(null)\nconst drag = ref(null)\nconst byId = computed(() => Object.fromEntries(props.nodes.map(n => [n.id, n])))\nfunction outPt(n) { return { x: n.x + NODE_W, y: n.y + 44 } }\nfunction inPt(n, portIdx = 0) { const multi = (n.inputs || []).filter(Boolean).length > 1; return { x: n.x, y: multi ? n.y + 23 + portIdx * 38 : n.y + 44 } }\nfunction curve(p1, p2) { const dx = Math.max(40, (p2.x - p1.x) / 2); return `M ${p1.x} ${p1.y} C ${p1.x + dx} ${p1.y}, ${p2.x - dx} ${p2.y}, ${p2.x} ${p2.y}` }\nfunction path(e) { const a = byId.value[e.from], b = byId.value[e.to]; return a && b ? curve(outPt(a), inPt(b, e.toPort || 0)) : '' }\nconst dragPath = computed(() => {\n const d = drag.value; if (!d) return ''\n const a = byId.value[d.from]; if (!a) return ''\n const end = d.over && byId.value[d.over.id] ? inPt(byId.value[d.over.id], d.over.port) : { x: d.x, y: d.y }\n return curve(outPt(a), end)\n})\nfunction at(e) { const r = root.value.getBoundingClientRect(); return { x: (e.clientX - r.left) / props.scale, y: (e.clientY - r.top) / props.scale } }\n/* The drop target is hit-tested from the pointer position rather than port hover:\n during a drag the cursor is often over the wire layer, not the 22px handle. */\nfunction hitTest(x, y, fromId) {\n let best = null\n for (const n of props.nodes) {\n if (n.id === fromId) continue\n const ports = n.inputs || []\n for (let i = 0; i < ports.length; i++) {\n const p = inPt(n, i)\n const d = Math.hypot(p.x - x, p.y - y)\n if (d <= props.snapRadius && (!best || d < best.d)) best = { id: n.id, port: i, d }\n }\n }\n return best ? { id: best.id, port: best.port } : null\n}\nfunction move(e) {\n if (!drag.value) return\n const p = at(e)\n drag.value = { ...drag.value, ...p, over: hitTest(p.x, p.y, drag.value.from) }\n}\nfunction up() {\n const d = drag.value\n if (d && d.over) emit('connect', { from: d.from, to: d.over.id, toPort: d.over.port })\n drag.value = null\n window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up)\n}\nfunction start(id, e) {\n if (!props.connectable) return\n drag.value = { from: id, ...at(e), over: null }\n window.addEventListener('pointermove', move); window.addEventListener('pointerup', up)\n}\nconst inputTones = id => (drag.value && drag.value.over && drag.value.over.id === id ? Object.assign([], { [drag.value.over.port]: 'target' }) : [])\nonUnmounted(() => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up) })\n</script>\n<template>\n <div ref=\"root\" :style=\"{ position: 'relative', width, height, background: 'var(--bg-app)', overflow: 'hidden', cursor: drag ? 'grabbing' : 'default', ...style }\">\n <svg style=\"position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible\">\n <path v-for=\"(e, i) in edges\" :key=\"i\" :d=\"path(e)\" fill=\"none\" stroke=\"var(--edge)\" stroke-width=\"1.5\" />\n <path v-if=\"drag\" :d=\"dragPath\" fill=\"none\" stroke=\"var(--action-primary)\" stroke-width=\"1.5\" stroke-dasharray=\"5 4\" />\n </svg>\n <NodeCard v-for=\"n in nodes\" :key=\"n.id\" v-bind=\"n\" :selected=\"selectedId === n.id\" :live=\"connectable\"\n :output-tone=\"drag && drag.from === n.id ? 'active' : 'default'\" :input-tones=\"inputTones(n.id)\"\n @click=\"!drag && emit('select', n.id)\" @menu=\"emit('menu', n.id)\"\n @output-down=\"start(n.id, $event)\"\n :style=\"{ position: 'absolute', left: n.x + 'px', top: n.y + 'px' }\" />\n </div>\n</template>\n","pipeline/ZoomControls.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Floating canvas zoom control (− / percentage / + / fit-to-view). */\nexport function ZoomControls({ value = 100, onChange, onFit, style }) {\n return (\n <div style={{ display: \"inline-flex\", alignItems: \"center\", gap: 2, background: \"var(--bg-surface)\", border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-md)\", boxShadow: \"var(--shadow-menu)\", padding: 3, boxSizing: \"border-box\", ...style }}>\n <IconButton icon=\"minus\" size=\"sm\" onClick={() => onChange && onChange(Math.max(25, value - 10))} label=\"Zoom out\" />\n <span style={{ minWidth: 40, textAlign: \"center\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\" }}>{value}%</span>\n <IconButton icon=\"plus\" size=\"sm\" onClick={() => onChange && onChange(Math.min(200, value + 10))} label=\"Zoom in\" />\n <span style={{ width: 1, alignSelf: \"stretch\", background: \"var(--border-subtle)\", margin: \"0 2px\" }} />\n <IconButton icon=\"target\" size=\"sm\" onClick={onFit} label=\"Fit to view\" />\n </div>\n );\n}\n","pipeline/ZoomControls.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\n// Floating canvas zoom control (− / percentage / + / fit-to-view).\nconst props = defineProps({ modelValue: { type: Number, default: 100 }, style: Object })\nconst emit = defineEmits(['update:modelValue', 'fit'])\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', alignItems: 'center', gap: 2, background: 'var(--bg-surface)', border: '1px solid var(--border-control)', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-menu)', padding: 3, boxSizing: 'border-box', ...style }\">\n <IconButton icon=\"minus\" size=\"sm\" label=\"Zoom out\" @click=\"emit('update:modelValue', Math.max(25, modelValue - 10))\" />\n <span style=\"min-width:40px;text-align:center;font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ modelValue }}%</span>\n <IconButton icon=\"plus\" size=\"sm\" label=\"Zoom in\" @click=\"emit('update:modelValue', Math.min(200, modelValue + 10))\" />\n <span style=\"width:1px;align-self:stretch;background:var(--border-subtle);margin:0 2px\" />\n <IconButton icon=\"target\" size=\"sm\" label=\"Fit to view\" @click=\"emit('fit')\" />\n </div>\n</template>\n","pipeline/pipeline-catalog.js":"// The full component set of the pipeline editor, verbatim from the file. Shared by PalettePanel.vue; also re-exported by PalettePanel.jsx.\nexport const PIPELINE_CATALOG = [\n { label: 'Data Source', icon: 'table', hue: 'source', items: [{ label: 'Detail Sheet', icon: 'table' }, { label: 'Model', icon: 'layers' }, { label: 'Dimension', icon: 'grid-3x3' }] },\n { label: 'Data Join', icon: 'text-search', hue: 'join', items: [{ label: 'Lookup Data', icon: 'text-search' }, { label: 'Association Match', icon: 'shuffle' }] },\n { label: 'Data Transformation', icon: 'sliders-horizontal', hue: 'transform', items: [{ label: 'Field Value Calculation', icon: 'sliders-horizontal' }, { label: 'Field Split', icon: 'split' }, { label: 'Row to Column', icon: 'arrow-right-left' }, { label: 'Column to Row', icon: 'menu' }, { label: 'Multi-Condition Judge', icon: 'funnel' }, { label: 'Regional Calculation', icon: 'chart-no-axes-column' }, { label: 'Advanced Transformation', icon: 'trending-up' }, { label: 'JS Enhanced Processing', icon: 'code' }] },\n { label: 'Data Merge', icon: 'database', hue: 'merge', items: [{ label: 'Group Calculation', icon: 'database' }, { label: 'Concat Merge', icon: 'merge' }] },\n { label: 'Data Output', icon: 'table', hue: 'output', items: [{ label: 'Detail Sheet', icon: 'table' }, { label: 'Model', icon: 'layers' }, { label: 'Dimension', icon: 'grid-3x3' }] },\n]\n"});
@@ -1 +1 @@
1
- window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"tabs/SegmentedControl.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Segmented control — inset track, raised active segment. options: string[] or [{label,icon}]. */\nexport function SegmentedControl({ options = [], value, onChange, size = \"md\", style }) {\n const h = size === \"sm\" ? 26 : 32;\n return (\n <div style={{ display: \"inline-flex\", padding: 2, gap: 2, background: \"var(--bg-inset)\", border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-md)\", boxSizing: \"border-box\", ...style }}>\n {options.map((o) => {\n const label = typeof o === \"string\" ? o : o.label;\n const is = value === label;\n return (\n <button key={label} type=\"button\" onClick={() => onChange && onChange(label)}\n style={{ height: h - 6, padding: \"0 10px\", display: \"inline-flex\", alignItems: \"center\", gap: 6, borderRadius: \"var(--radius-sm)\", border: \"none\", cursor: \"pointer\", background: is ? \"var(--bg-surface)\" : \"transparent\", boxShadow: is ? \"0 1px 2px rgba(0,0,0,.08)\" : \"none\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: size === \"sm\" ? \"var(--text-xs)\" : \"var(--text-sm)\", color: is ? \"var(--text-heading)\" : \"var(--text-secondary)\", whiteSpace: \"nowrap\", transition: \"var(--t-hover)\" }}>\n {typeof o === \"object\" && o.icon ? <Icon name={o.icon} size={13} /> : null}{label}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/SegmentedControl.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\n// Segmented control — inset track, raised active segment. options: string[] or [{label,icon}].\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, size: { type: String, default: 'md' }, style: Object })\nconst emit = defineEmits(['update:modelValue'])\nfunction label(o) { return typeof o === 'string' ? o : o.label }\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', padding: 2, gap: 2, background: 'var(--bg-inset)', border: '1px solid var(--border-control)', borderRadius: 'var(--radius-md)', boxSizing: 'border-box', ...style }\">\n <button v-for=\"o in options\" :key=\"label(o)\" type=\"button\" @click=\"emit('update:modelValue', label(o))\"\n :style=\"{ height: (size === 'sm' ? 26 : 32) - 6, padding: '0 10px', display: 'inline-flex', alignItems: 'center', gap: 6, borderRadius: 'var(--radius-sm)', border: 'none', cursor: 'pointer', background: modelValue === label(o) ? 'var(--bg-surface)' : 'transparent', boxShadow: modelValue === label(o) ? '0 1px 2px rgba(0,0,0,.08)' : 'none', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: size === 'sm' ? 'var(--text-xs)' : 'var(--text-sm)', color: modelValue === label(o) ? 'var(--text-heading)' : 'var(--text-secondary)', whiteSpace: 'nowrap', transition: 'var(--t-hover)' }\">\n <Icon v-if=\"typeof o === 'object' && o.icon\" :name=\"o.icon\" :size=\"13\" />{{ label(o) }}\n </button>\n </div>\n</template>\n","tabs/Tabs.jsx":"import React from \"react\";\n\n/** Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline. */\nexport function Tabs({ tabs = [], active, onChange, style }) {\n return (\n <div style={{ display: \"flex\", gap: 4, borderBottom: \"1px solid var(--border-control)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" onClick={() => onChange && onChange(t)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", padding: \"9px 16px\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: is ? \"var(--action-primary)\" : \"var(--text-body)\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n\n/** Compact panel tabs (Node Info · Calc Rules · Output Fields · SQL Preview), 12px. */\nexport function PanelTabs({ tabs = [], active, onChange, style }) {\n return (\n <div style={{ display: \"flex\", borderBottom: \"1px solid var(--border-subtle)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" onClick={() => onChange && onChange(t)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", padding: \"8px 12px\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: is ? \"var(--action-primary)\" : \"var(--text-secondary)\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n\n/** Text pill filter tabs (For Review · To be Submitted · For Approval). Active = #f5f5f5 pill. */\nexport function PillTabs({ tabs = [], active, onChange, style }) {\n return (\n <div style={{ display: \"flex\", gap: 8, ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" onClick={() => onChange && onChange(t)}\n style={{ background: is ? \"var(--bg-active)\" : \"transparent\", border: \"none\", cursor: \"pointer\", height: 28, padding: \"0 11px\", borderRadius: \"var(--radius-md)\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: is ? \"var(--text-heading)\" : \"var(--text-secondary)\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/Tabs.vue":"<script setup>\n// Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline.\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', gap: 4, borderBottom: '1px solid var(--border-control)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" @click=\"emit('change', t)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', padding: '9px 16px', marginBottom: -1, borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', color: t === active ? 'var(--action-primary)' : 'var(--text-body)' }\">{{ t }}</button>\n </div>\n</template>\n\n<!-- PanelTabs — compact panel tabs (Node Info · Calc Rules · Output Fields · SQL Preview), 12px. -->\n<!--\n<script setup name=\"PanelTabs\">\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', borderBottom: '1px solid var(--border-subtle)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" @click=\"emit('change', t)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', padding: '8px 12px', marginBottom: -1, borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: t === active ? 'var(--action-primary)' : 'var(--text-secondary)' }\">{{ t }}</button>\n </div>\n</template>\n-->\n\n<!-- PillTabs — text pill filter tabs (For Review · To be Submitted · For Approval). Active = filled pill. -->\n<!--\n<script setup name=\"PillTabs\">\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', gap: 8, ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" @click=\"emit('change', t)\"\n :style=\"{ background: t === active ? 'var(--bg-active)' : 'transparent', border: 'none', cursor: 'pointer', height: 28, padding: '0 11px', borderRadius: 'var(--radius-md)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: t === active ? 'var(--text-heading)' : 'var(--text-secondary)' }\">{{ t }}</button>\n </div>\n</template>\n-->\n","tabs/ViewToggle.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Two-state list/grid icon toggle (palette header). */\nexport function ViewToggle({ value = \"grid\", onChange, style }) {\n return (\n <div style={{ display: \"inline-flex\", gap: 2, ...style }}>\n <IconButton icon=\"list\" size=\"sm\" active={value === \"list\"} onClick={() => onChange && onChange(\"list\")} label=\"List view\" />\n <IconButton icon=\"layout-grid\" size=\"sm\" active={value === \"grid\"} onClick={() => onChange && onChange(\"grid\")} label=\"Grid view\" />\n </div>\n );\n}\n","tabs/ViewToggle.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\n// Two-state list/grid icon toggle (palette header).\ndefineProps({ modelValue: { type: String, default: 'grid' }, style: Object })\nconst emit = defineEmits(['update:modelValue'])\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', gap: 2, ...style }\">\n <IconButton icon=\"list\" size=\"sm\" :active=\"modelValue === 'list'\" label=\"List view\" @click=\"emit('update:modelValue', 'list')\" />\n <IconButton icon=\"layout-grid\" size=\"sm\" :active=\"modelValue === 'grid'\" label=\"Grid view\" @click=\"emit('update:modelValue', 'grid')\" />\n </div>\n</template>\n"});
1
+ window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"tabs/SegmentedControl.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Segmented control — inset track, raised active segment. options: string[] or [{label,icon}]. */\nexport function SegmentedControl({ options = [], value, onChange, size = \"md\", style }) {\n const h = size === \"sm\" ? 26 : 32;\n return (\n <div style={{ display: \"inline-flex\", padding: 2, gap: 2, background: \"var(--bg-inset)\", border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-md)\", boxSizing: \"border-box\", ...style }}>\n {options.map((o) => {\n const label = typeof o === \"string\" ? o : o.label;\n const is = value === label;\n return (\n <button key={label} type=\"button\" onClick={() => onChange && onChange(label)}\n style={{ height: h - 6, padding: \"0 10px\", display: \"inline-flex\", alignItems: \"center\", gap: 6, borderRadius: \"var(--radius-sm)\", border: \"none\", cursor: \"pointer\", background: is ? \"var(--bg-surface)\" : \"transparent\", boxShadow: is ? \"0 1px 2px rgba(0,0,0,.08)\" : \"none\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: size === \"sm\" ? \"var(--text-xs)\" : \"var(--text-sm)\", color: is ? \"var(--text-heading)\" : \"var(--text-secondary)\", whiteSpace: \"nowrap\", transition: \"var(--t-hover)\" }}>\n {typeof o === \"object\" && o.icon ? <Icon name={o.icon} size={13} /> : null}{label}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/SegmentedControl.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\n// Segmented control — inset track, raised active segment. options: string[] or [{label,icon}].\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, size: { type: String, default: 'md' }, style: Object })\nconst emit = defineEmits(['update:modelValue'])\nfunction label(o) { return typeof o === 'string' ? o : o.label }\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', padding: 2, gap: 2, background: 'var(--bg-inset)', border: '1px solid var(--border-control)', borderRadius: 'var(--radius-md)', boxSizing: 'border-box', ...style }\">\n <button v-for=\"o in options\" :key=\"label(o)\" type=\"button\" @click=\"emit('update:modelValue', label(o))\"\n :style=\"{ height: (size === 'sm' ? 26 : 32) - 6, padding: '0 10px', display: 'inline-flex', alignItems: 'center', gap: 6, borderRadius: 'var(--radius-sm)', border: 'none', cursor: 'pointer', background: modelValue === label(o) ? 'var(--bg-surface)' : 'transparent', boxShadow: modelValue === label(o) ? '0 1px 2px rgba(0,0,0,.08)' : 'none', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: size === 'sm' ? 'var(--text-xs)' : 'var(--text-sm)', color: modelValue === label(o) ? 'var(--text-heading)' : 'var(--text-secondary)', whiteSpace: 'nowrap', transition: 'var(--t-hover)' }\">\n <Icon v-if=\"typeof o === 'object' && o.icon\" :name=\"o.icon\" :size=\"13\" />{{ label(o) }}\n </button>\n </div>\n</template>\n","tabs/Tabs.jsx":"import React from \"react\";\n\n/** Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline. */\nexport function Tabs({ tabs = [], active, onChange, style }) {\n return (\n <div style={{ display: \"flex\", gap: 4, borderBottom: \"1px solid var(--border-control)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" onClick={() => onChange && onChange(t)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", padding: \"9px 16px\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: is ? \"var(--action-primary)\" : \"var(--text-body)\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n\n/** Compact panel tabs (Node Info · Calc Rules · Output Fields · SQL Preview), 12px. */\nexport function PanelTabs({ tabs = [], active, onChange, style }) {\n return (\n <div style={{ display: \"flex\", borderBottom: \"1px solid var(--border-subtle)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" onClick={() => onChange && onChange(t)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", padding: \"8px 12px\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: is ? \"var(--action-primary)\" : \"var(--text-secondary)\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n\n/** Text pill filter tabs (For Review · To be Submitted · For Approval). Active = #f5f5f5 pill. */\nexport function PillTabs({ tabs = [], active, onChange, style }) {\n return (\n <div style={{ display: \"flex\", gap: 8, ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" onClick={() => onChange && onChange(t)}\n style={{ background: is ? \"var(--bg-active)\" : \"transparent\", border: \"none\", cursor: \"pointer\", height: 28, padding: \"0 11px\", borderRadius: \"var(--radius-md)\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: is ? \"var(--text-heading)\" : \"var(--text-secondary)\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/Tabs.vue":"<script setup>\n// Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline.\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', gap: 4, borderBottom: '1px solid var(--border-control)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" @click=\"emit('change', t)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', padding: '9px 16px', marginBottom: -1, borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', color: t === active ? 'var(--action-primary)' : 'var(--text-body)' }\">{{ t }}</button>\n </div>\n</template>\n","tabs/ViewToggle.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Two-state list/grid icon toggle (palette header). */\nexport function ViewToggle({ value = \"grid\", onChange, style }) {\n return (\n <div style={{ display: \"inline-flex\", gap: 2, ...style }}>\n <IconButton icon=\"list\" size=\"sm\" active={value === \"list\"} onClick={() => onChange && onChange(\"list\")} label=\"List view\" />\n <IconButton icon=\"layout-grid\" size=\"sm\" active={value === \"grid\"} onClick={() => onChange && onChange(\"grid\")} label=\"Grid view\" />\n </div>\n );\n}\n","tabs/ViewToggle.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\n// Two-state list/grid icon toggle (palette header).\ndefineProps({ modelValue: { type: String, default: 'grid' }, style: Object })\nconst emit = defineEmits(['update:modelValue'])\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', gap: 2, ...style }\">\n <IconButton icon=\"list\" size=\"sm\" :active=\"modelValue === 'list'\" label=\"List view\" @click=\"emit('update:modelValue', 'list')\" />\n <IconButton icon=\"layout-grid\" size=\"sm\" :active=\"modelValue === 'grid'\" label=\"Grid view\" @click=\"emit('update:modelValue', 'grid')\" />\n </div>\n</template>\n","tabs/PanelTabs.vue":"<script setup>\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', borderBottom: '1px solid var(--border-subtle)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" @click=\"emit('change', t)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', padding: '8px 12px', marginBottom: -1, borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: t === active ? 'var(--action-primary)' : 'var(--text-secondary)' }\">{{ t }}</button>\n </div>\n</template>\n","tabs/PillTabs.vue":"<script setup>\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', gap: 8, ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" @click=\"emit('change', t)\"\n :style=\"{ background: t === active ? 'var(--bg-active)' : 'transparent', border: 'none', cursor: 'pointer', height: 28, padding: '0 11px', borderRadius: 'var(--radius-md)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: t === active ? 'var(--text-heading)' : 'var(--text-secondary)' }\">{{ t }}</button>\n </div>\n</template>\n"});
@@ -7,6 +7,7 @@ export { default as BrandVisual } from "../brand/BrandVisual.vue";
7
7
  /* buttons/ */
8
8
  export { default as ActionChip } from "../buttons/ActionChip.vue";
9
9
  export { default as Button } from "../buttons/Button.vue";
10
+ export { default as ButtonGroup } from "../buttons/ButtonGroup.vue";
10
11
  export { default as IconButton } from "../buttons/IconButton.vue";
11
12
 
12
13
  /* charts/ */
@@ -45,18 +46,26 @@ export { default as DataTable } from "../data/DataTable.vue";
45
46
  export { default as DiffValue } from "../data/DiffValue.vue";
46
47
  export { default as FilterBar } from "../data/FilterBar.vue";
47
48
  export { default as KeyValue } from "../data/KeyValue.vue";
49
+ export { default as KeyValueList } from "../data/KeyValueList.vue";
48
50
  export { default as ListRow } from "../data/ListRow.vue";
49
51
  export { default as Pagination } from "../data/Pagination.vue";
50
52
  export { default as SmartTable } from "../data/SmartTable.vue";
51
53
  export { default as Spreadsheet } from "../data/Spreadsheet.vue";
52
54
 
53
55
  /* display/ */
56
+ export { default as AreaChart } from "../display/AreaChart.vue";
54
57
  export { default as Avatar } from "../display/Avatar.vue";
58
+ export { default as AvatarGroup } from "../display/AvatarGroup.vue";
55
59
  export { default as Badge } from "../display/Badge.vue";
56
60
  export { default as Card } from "../display/Card.vue";
57
61
  export { default as Divider } from "../display/Divider.vue";
62
+ export { default as Donut } from "../display/Donut.vue";
63
+ export { default as IconTile } from "../display/IconTile.vue";
58
64
  export { default as Kbd } from "../display/Kbd.vue";
65
+ export { default as MiniBars } from "../display/MiniBars.vue";
59
66
  export { default as ProgressBar } from "../display/ProgressBar.vue";
67
+ export { default as StatCard } from "../display/StatCard.vue";
68
+ export { default as StatusBadge } from "../display/StatusBadge.vue";
60
69
  export { default as Tag } from "../display/Tag.vue";
61
70
 
62
71
  /* feedback/ */
@@ -65,11 +74,17 @@ export { default as ActivityItem } from "../feedback/ActivityItem.vue";
65
74
  export { default as Alert } from "../feedback/Alert.vue";
66
75
  export { default as Banner } from "../feedback/Banner.vue";
67
76
  export { default as ContextMenu } from "../feedback/ContextMenu.vue";
77
+ export { default as DotActivity } from "../feedback/DotActivity.vue";
68
78
  export { default as Drawer } from "../feedback/Drawer.vue";
79
+ export { default as EmptyState } from "../feedback/EmptyState.vue";
80
+ export { default as LogViewer } from "../feedback/LogViewer.vue";
69
81
  export { default as Menu } from "../feedback/Menu.vue";
70
82
  export { default as Modal } from "../feedback/Modal.vue";
83
+ export { default as Skeleton } from "../feedback/Skeleton.vue";
84
+ export { default as Spinner } from "../feedback/Spinner.vue";
71
85
  export { default as Stepper } from "../feedback/Stepper.vue";
72
86
  export { default as Timeline } from "../feedback/Timeline.vue";
87
+ export { default as Toast } from "../feedback/Toast.vue";
73
88
 
74
89
  /* forms/ */
75
90
  export { default as Checkbox } from "../forms/Checkbox.vue";
@@ -86,6 +101,7 @@ export { default as SearchInput } from "../forms/SearchInput.vue";
86
101
  export { default as Select } from "../forms/Select.vue";
87
102
  export { default as Slider } from "../forms/Slider.vue";
88
103
  export { default as Switch } from "../forms/Switch.vue";
104
+ export { default as Textarea } from "../forms/Textarea.vue";
89
105
 
90
106
  /* icons/ */
91
107
  export { default as Icon } from "../icons/Icon.vue";
@@ -94,7 +110,10 @@ export { default as Unicon } from "../icons/Unicon.vue";
94
110
  /* navigation/ */
95
111
  export { default as Breadcrumb } from "../navigation/Breadcrumb.vue";
96
112
  export { default as DocTree } from "../navigation/DocTree.vue";
113
+ export { default as PageHeading } from "../navigation/PageHeading.vue";
114
+ export { default as PageToolbar } from "../navigation/PageToolbar.vue";
97
115
  export { default as Sidebar } from "../navigation/Sidebar.vue";
116
+ export { default as ThemeToggle } from "../navigation/ThemeToggle.vue";
98
117
  export { default as TopBar } from "../navigation/TopBar.vue";
99
118
  export { default as Tree } from "../navigation/Tree.vue";
100
119
 
@@ -109,6 +128,8 @@ export { default as PipelineCanvas } from "../pipeline/PipelineCanvas.vue";
109
128
  export { default as ZoomControls } from "../pipeline/ZoomControls.vue";
110
129
 
111
130
  /* tabs/ */
131
+ export { default as PanelTabs } from "../tabs/PanelTabs.vue";
132
+ export { default as PillTabs } from "../tabs/PillTabs.vue";
112
133
  export { default as SegmentedControl } from "../tabs/SegmentedControl.vue";
113
134
  export { default as Tabs } from "../tabs/Tabs.vue";
114
135
  export { default as ViewToggle } from "../tabs/ViewToggle.vue";
@@ -65,23 +65,3 @@ const iSize = computed(() => props.iconSize || (props.size === 'sm' ? 12 : 14))
65
65
  <Icon v-if="!loading && icon && iconPosition === 'right'" :name="icon" :size="iSize" />
66
66
  </button>
67
67
  </template>
68
-
69
- <!-- ButtonGroup — attached row of Buttons sharing one outline (segmented action group). -->
70
- <!--
71
- <script setup name="ButtonGroup">
72
- import { useSlots, computed } from 'vue'
73
- const props = defineProps({ style: Object })
74
- const slots = useSlots()
75
- const items = computed(() => slots.default ? slots.default() : [])
76
- </script>
77
- <template>
78
- <div :style="{ display: 'inline-flex', ...style }">
79
- <template v-for="(child, i) in items" :key="i">
80
- <component :is="child" :style="{ borderRadius: 0,
81
- ...(i === 0 ? { borderTopLeftRadius: 'var(--radius-md)', borderBottomLeftRadius: 'var(--radius-md)' } : null),
82
- ...(i === items.length - 1 ? { borderTopRightRadius: 'var(--radius-md)', borderBottomRightRadius: 'var(--radius-md)' } : null),
83
- marginLeft: i === 0 ? 0 : -1 }" />
84
- </template>
85
- </div>
86
- </template>
87
- -->
@@ -0,0 +1,16 @@
1
+ <script setup>
2
+ import { useSlots, computed } from 'vue'
3
+ const props = defineProps({ style: Object })
4
+ const slots = useSlots()
5
+ const items = computed(() => slots.default ? slots.default() : [])
6
+ </script>
7
+ <template>
8
+ <div :style="{ display: 'inline-flex', ...style }">
9
+ <template v-for="(child, i) in items" :key="i">
10
+ <component :is="child" :style="{ borderRadius: 0,
11
+ ...(i === 0 ? { borderTopLeftRadius: 'var(--radius-md)', borderBottomLeftRadius: 'var(--radius-md)' } : null),
12
+ ...(i === items.length - 1 ? { borderTopRightRadius: 'var(--radius-md)', borderBottomRightRadius: 'var(--radius-md)' } : null),
13
+ marginLeft: i === 0 ? 0 : -1 }" />
14
+ </template>
15
+ </div>
16
+ </template>
@@ -8,19 +8,3 @@ defineProps({ label: String, value: [String, Number], mono: Boolean, style: Obje
8
8
  <span :style="{ fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: mono ? 'var(--text-xs)' : 'var(--text-sm)', letterSpacing: mono ? 'var(--tracking-xs)' : 0, color: 'var(--text-heading)', textAlign: 'right' }">{{ value }}</span>
9
9
  </div>
10
10
  </template>
11
-
12
- <!-- KeyValueList — stack of KeyValue rows with hairline separators. rows: [{label,value,mono}]. -->
13
- <!--
14
- <script setup name="KeyValueList">
15
- import KeyValue from './KeyValue.vue'
16
- defineProps({ rows: { type: Array, default: () => [] }, style: Object })
17
- </script>
18
- <template>
19
- <div :style="{ display: 'flex', flexDirection: 'column', ...style }">
20
- <template v-for="(r, i) in rows" :key="r.label">
21
- <KeyValue v-bind="r" />
22
- <div v-if="i < rows.length - 1" style="height:1px;background:var(--border-subtle)" />
23
- </template>
24
- </div>
25
- </template>
26
- -->
@@ -0,0 +1,12 @@
1
+ <script setup>
2
+ import KeyValue from './KeyValue.vue'
3
+ defineProps({ rows: { type: Array, default: () => [] }, style: Object })
4
+ </script>
5
+ <template>
6
+ <div :style="{ display: 'flex', flexDirection: 'column', ...style }">
7
+ <template v-for="(r, i) in rows" :key="r.label">
8
+ <KeyValue v-bind="r" />
9
+ <div v-if="i < rows.length - 1" style="height:1px;background:var(--border-subtle)" />
10
+ </template>
11
+ </div>
12
+ </template>
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
2
  import { ref } from 'vue'
3
3
  import Icon from '../icons/Icon.vue'
4
- import { IconTile } from '../display/Badge.vue'
4
+ import IconTile from '../display/IconTile.vue'
5
5
  // Expandable workflow list row: chevron + bordered 32px icon tile + title/meta + right count.
6
6
  defineProps({ icon: { type: String, default: 'file-text' }, title: String, meta: String, trailing: [String, Number], expanded: Boolean, style: Object })
7
7
  defineEmits(['click'])
@@ -0,0 +1,14 @@
1
+ <script setup>
2
+ import { computed } from 'vue'
3
+ const props = defineProps({ points: { type: Array, default: () => [65, 75, 60, 85, 70, 90, 85, 95, 100] }, width: { type: Number, default: 350 }, height: { type: Number, default: 128 }, color: { type: String, default: 'var(--chart-blue)' }, style: Object })
4
+ const gid = 'g' + Math.round(Math.random() * 1e6)
5
+ const step = computed(() => props.width / (props.points.length - 1))
6
+ const line = computed(() => props.points.map((p, i) => `${i ? 'L' : 'M'} ${i * step.value} ${props.height - (p / 100) * props.height * .48 - props.height * .5}`).join(' '))
7
+ </script>
8
+ <template>
9
+ <svg :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" :style="{ display: 'block', ...style }">
10
+ <defs><linearGradient :id="gid" x1="0" y1="0" x2="0" y2="1"><stop offset="0" :stop-color="color" stop-opacity=".3" /><stop offset="1" :stop-color="color" stop-opacity="0" /></linearGradient></defs>
11
+ <path :d="`${line} L ${width} ${height} L 0 ${height} Z`" :fill="`url(#${gid})`" />
12
+ <path :d="line" fill="none" :stroke="color" stroke-width="1.22" />
13
+ </svg>
14
+ </template>
@@ -15,20 +15,3 @@ const hue = computed(() => palette[[...props.name].reduce((a, c) => a + c.charCo
15
15
  <img v-if="src" :src="src" :alt="name" :style="{ width: size, height: size, borderRadius: 'var(--radius-full)', objectFit: 'cover', flexShrink: 0, ...style }" />
16
16
  <span v-else :title="name" :style="{ width: size, height: size, borderRadius: 'var(--radius-full)', background: 'var(--bg-hover)', color: hue, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: Math.max(9, Math.round(size * .4)), letterSpacing: 0, flexShrink: 0, boxSizing: 'border-box', border: '1px solid var(--border-card)', ...style }">{{ initials(name) }}</span>
17
17
  </template>
18
-
19
- <!-- AvatarGroup — overlapping avatar row (assignees on a task). -->
20
- <!--
21
- <script setup name="AvatarGroup">
22
- import { computed } from 'vue'
23
- import Avatar from './Avatar.vue'
24
- const props = defineProps({ names: { type: Array, default: () => [] }, size: { type: Number, default: 24 }, max: { type: Number, default: 4 }, style: Object })
25
- const shown = computed(() => props.names.slice(0, props.max))
26
- const rest = computed(() => props.names.length - shown.value.length)
27
- </script>
28
- <template>
29
- <div :style="{ display: 'flex', alignItems: 'center', ...style }">
30
- <Avatar v-for="(n, i) in shown" :key="n + i" :name="n" :size="size" :style="{ marginLeft: i ? -8 : 0, boxShadow: 'var(--ring-surface)' }" />
31
- <span v-if="rest > 0" :style="{ marginLeft: -8, width: size, height: size, borderRadius: 'var(--radius-full)', background: 'var(--bg-active)', color: 'var(--text-secondary)', boxShadow: 'var(--ring-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontSize: Math.max(9, Math.round(size * .38)) }">+{{ rest }}</span>
32
- </div>
33
- </template>
34
- -->
@@ -0,0 +1,13 @@
1
+ <script setup>
2
+ import { computed } from 'vue'
3
+ import Avatar from './Avatar.vue'
4
+ const props = defineProps({ names: { type: Array, default: () => [] }, size: { type: Number, default: 24 }, max: { type: Number, default: 4 }, style: Object })
5
+ const shown = computed(() => props.names.slice(0, props.max))
6
+ const rest = computed(() => props.names.length - shown.value.length)
7
+ </script>
8
+ <template>
9
+ <div :style="{ display: 'flex', alignItems: 'center', ...style }">
10
+ <Avatar v-for="(n, i) in shown" :key="n + i" :name="n" :size="size" :style="{ marginLeft: i ? -8 : 0, boxShadow: 'var(--ring-surface)' }" />
11
+ <span v-if="rest > 0" :style="{ marginLeft: -8, width: size, height: size, borderRadius: 'var(--radius-full)', background: 'var(--bg-active)', color: 'var(--text-secondary)', boxShadow: 'var(--ring-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontSize: Math.max(9, Math.round(size * .38)) }">+{{ rest }}</span>
12
+ </div>
13
+ </template>
@@ -1,20 +1,4 @@
1
1
  <script setup>
2
- import { computed } from 'vue'
3
- import Icon from '../icons/Icon.vue'
4
- const badgeTones = { success: 'var(--success)', danger: 'var(--danger)', warning: 'var(--warning)', pending: 'var(--dot-neutral)', info: 'var(--action-primary)' }
5
- // StatusBadge — circular status badge with white ring (node error/warning dots, step icons).
6
- const props = defineProps({ tone: { type: String, default: 'danger' }, icon: String, size: { type: Number, default: 20 }, ring: { type: Boolean, default: true }, style: Object })
7
- const iconSize = computed(() => Math.round(props.size * .6))
8
- </script>
9
- <template>
10
- <span :style="{ width: size, height: size, borderRadius: 'var(--radius-full)', background: badgeTones[tone], boxShadow: ring ? 'var(--ring-surface)' : 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--on-accent)', flexShrink: 0, ...style }">
11
- <Icon v-if="icon" :name="icon" :size="iconSize" :stroke-width="2.5" />
12
- </span>
13
- </template>
14
-
15
- <!-- Badge — inline tinted text badge (table statuses: Completed / Failed / Partial). -->
16
- <!--
17
- <script setup name="Badge">
18
2
  const tones = {
19
3
  success: { background: 'var(--success-bg)', color: 'var(--success-text)' },
20
4
  danger: { background: 'var(--danger-bg)', color: 'var(--danger-text)' },
@@ -27,21 +11,3 @@ defineProps({ tone: { type: String, default: 'neutral' }, style: Object })
27
11
  <template>
28
12
  <span :style="{ display: 'inline-flex', alignItems: 'center', height: 20, padding: '0 8px', borderRadius: 'var(--radius-full)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-xs)', lineHeight: 1, letterSpacing: 'var(--tracking-xs)', ...tones[tone], ...style }"><slot /></span>
29
13
  </template>
30
- -->
31
-
32
- <!-- IconTile — category icon tile — the signature tinted square. kind: palette 40/r14 · node 48/r16 · row 32/r8 (hairline). -->
33
- <!--
34
- <script setup name="IconTile">
35
- import { computed } from 'vue'
36
- import Icon from '../icons/Icon.vue'
37
- const dims = { palette: { box: 40, r: 'var(--radius-tile)', icon: 18 }, node: { box: 48, r: 'var(--radius-node)', icon: 22 }, row: { box: 32, r: 'var(--radius-md)', icon: 16 } }
38
- const props = defineProps({ icon: String, hue: { type: String, default: 'source' }, kind: { type: String, default: 'palette' }, style: Object })
39
- const t = computed(() => dims[props.kind])
40
- const tinted = computed(() => props.kind !== 'row')
41
- </script>
42
- <template>
43
- <span :style="{ width: t.box, height: t.box, borderRadius: t.r, background: tinted ? `var(--cat-${hue}-bg)` : 'var(--bg-surface)', border: tinted ? 'none' : '1px solid var(--border-control)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: tinted ? `var(--cat-${hue})` : 'var(--text-body)', flexShrink: 0, boxSizing: 'border-box', ...style }">
44
- <Icon :name="icon" :size="t.icon" :stroke-width="2" />
45
- </span>
46
- </template>
47
- -->