@keyfunc/keybricks-ds 1.0.4 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/DEPLOY.md +4 -4
  2. package/DESIGN.md +154 -53
  3. package/INTEGRATION.md +174 -35
  4. package/SKILL.md +6 -4
  5. package/_adherence.oxlintrc.json +164 -68
  6. package/_scripts/build-index.mjs +9 -0
  7. package/_scripts/build-tokens.mjs +5 -2
  8. package/assets/icons/icon-index.js +1 -1
  9. package/assets/icons/icon-index.json +1 -1
  10. package/components/_cards/accordion.card.html +2 -2
  11. package/components/_cards/action-chip.card.html +8 -3
  12. package/components/_cards/activity.card.html +2 -2
  13. package/components/_cards/alert.card.html +2 -2
  14. package/components/_cards/app-chrome.card.html +4 -4
  15. package/components/_cards/banner.card.html +1 -1
  16. package/components/_cards/breadcrumb.card.html +2 -2
  17. package/components/_cards/button-1-base.card.html +2 -2
  18. package/components/_cards/button-2-states.card.html +14 -14
  19. package/components/_cards/button-3-sizes-icons.card.html +1 -1
  20. package/components/_cards/button-4-group.card.html +5 -5
  21. package/components/_cards/canvas-zoom-controls.card.html +1 -1
  22. package/components/_cards/cards-indicators.card.html +2 -2
  23. package/components/_cards/charts-1-sparkline.card.html +2 -2
  24. package/components/_cards/charts-10-gauge.card.html +4 -4
  25. package/components/_cards/charts-11-funnel.card.html +2 -2
  26. package/components/_cards/charts-12-treemap.card.html +2 -2
  27. package/components/_cards/charts-14-radar.card.html +1 -1
  28. package/components/_cards/charts-15-distribution.card.html +1 -1
  29. package/components/_cards/charts-16-pareto.card.html +2 -2
  30. package/components/_cards/charts-17-candlestick.card.html +2 -2
  31. package/components/_cards/charts-18-sankey.card.html +2 -2
  32. package/components/_cards/charts-19-chord.card.html +2 -2
  33. package/components/_cards/charts-2-line.card.html +2 -2
  34. package/components/_cards/charts-20-sunburst.card.html +2 -2
  35. package/components/_cards/charts-21-worldmap.card.html +2 -2
  36. package/components/_cards/charts-22-gantt.card.html +2 -2
  37. package/components/_cards/charts-23-bar3d.card.html +2 -2
  38. package/components/_cards/charts-24-pie3d.card.html +2 -2
  39. package/components/_cards/charts-25-surface3d.card.html +2 -2
  40. package/components/_cards/charts-26-scatter3d.card.html +2 -2
  41. package/components/_cards/charts-27-ribbon3d.card.html +2 -2
  42. package/components/_cards/charts-3-bar.card.html +2 -2
  43. package/components/_cards/charts-4-barlist.card.html +2 -2
  44. package/components/_cards/charts-5-donut.card.html +3 -3
  45. package/components/_cards/charts-6-scatter.card.html +2 -2
  46. package/components/_cards/charts-7-heatmap.card.html +2 -2
  47. package/components/_cards/charts-8-waterfall.card.html +2 -2
  48. package/components/_cards/charts-9-bullet.card.html +2 -2
  49. package/components/_cards/charts-99-fullpage.card.html +5 -5
  50. package/components/_cards/checkbox.card.html +4 -4
  51. package/components/_cards/codeblock.card.html +1 -1
  52. package/components/_cards/combobox.card.html +3 -2
  53. package/components/_cards/context-menu.card.html +3 -2
  54. package/components/_cards/date-picker.card.html +5 -2
  55. package/components/_cards/diffvalue.card.html +2 -2
  56. package/components/_cards/divider.card.html +3 -3
  57. package/components/_cards/drawer.card.html +2 -2
  58. package/components/_cards/dropdown-menu.card.html +6 -6
  59. package/components/_cards/dropzone.card.html +1 -1
  60. package/components/_cards/empty-state.card.html +5 -5
  61. package/components/_cards/filter-bar.card.html +5 -5
  62. package/components/_cards/icon-button.card.html +2 -2
  63. package/components/_cards/icons-1-core.card.html +2 -2
  64. package/components/_cards/icons-2-feather.card.html +3 -3
  65. package/components/_cards/icons-3-unicons.card.html +3 -3
  66. package/components/_cards/input.card.html +5 -2
  67. package/components/_cards/kbd.card.html +2 -2
  68. package/components/_cards/keyvalue.card.html +2 -2
  69. package/components/_cards/listrow.card.html +2 -2
  70. package/components/_cards/loading.card.html +2 -2
  71. package/components/_cards/log-viewer.card.html +2 -2
  72. package/components/_cards/modal.card.html +9 -9
  73. package/components/_cards/multi-select.card.html +3 -2
  74. package/components/_cards/number-stepper.card.html +3 -2
  75. package/components/_cards/pagination.card.html +3 -2
  76. package/components/_cards/people.card.html +3 -3
  77. package/components/_cards/pipeline-node.card.html +2 -2
  78. package/components/_cards/pipeline-palette.card.html +4 -4
  79. package/components/_cards/radio.card.html +2 -2
  80. package/components/_cards/search.card.html +5 -2
  81. package/components/_cards/segmented-control.card.html +2 -2
  82. package/components/_cards/select.card.html +4 -4
  83. package/components/_cards/slider.card.html +1 -1
  84. package/components/_cards/smart-table.card.html +1 -1
  85. package/components/_cards/spreadsheet.card.html +2 -2
  86. package/components/_cards/stepper.card.html +2 -2
  87. package/components/_cards/table.card.html +103 -0
  88. package/components/_cards/tabs.card.html +3 -2
  89. package/components/_cards/tag.card.html +5 -2
  90. package/components/_cards/textarea.card.html +2 -2
  91. package/components/_cards/timeline.card.html +1 -1
  92. package/components/_cards/toast.card.html +4 -4
  93. package/components/_cards/toggles.card.html +3 -3
  94. package/components/_cards/tooltip.card.html +5 -5
  95. package/components/_cards/tree.card.html +2 -2
  96. package/components/_shared/src-buttons.js +1 -1
  97. package/components/_shared/src-charts.js +1 -1
  98. package/components/_shared/src-data.js +1 -1
  99. package/components/_shared/src-display.js +1 -1
  100. package/components/_shared/src-feedback.js +1 -1
  101. package/components/_shared/src-forms.js +1 -1
  102. package/components/_shared/src-navigation.js +1 -1
  103. package/components/_shared/src-pipeline.js +1 -1
  104. package/components/_shared/src-tabs.js +1 -1
  105. package/components/_vue/index.mjs +9 -5
  106. package/components/buttons/ActionChip.d.ts +3 -1
  107. package/components/buttons/ActionChip.jsx +4 -4
  108. package/components/buttons/ActionChip.vue +4 -4
  109. package/components/buttons/Button.d.ts +5 -2
  110. package/components/buttons/Button.jsx +15 -18
  111. package/components/buttons/Button.vue +17 -18
  112. package/components/buttons/ButtonGroup.vue +1 -1
  113. package/components/buttons/IconButton.d.ts +2 -1
  114. package/components/buttons/IconButton.jsx +9 -7
  115. package/components/buttons/IconButton.vue +10 -8
  116. package/components/buttons/buttons.prompt.md +6 -5
  117. package/components/charts/Bar3D.jsx +2 -2
  118. package/components/charts/Bar3D.vue +2 -2
  119. package/components/charts/BarChart.jsx +4 -4
  120. package/components/charts/BarChart.vue +4 -4
  121. package/components/charts/BarList.jsx +1 -1
  122. package/components/charts/BarList.vue +1 -1
  123. package/components/charts/BoxPlot.jsx +1 -1
  124. package/components/charts/BoxPlot.vue +2 -2
  125. package/components/charts/BulletChart.jsx +1 -1
  126. package/components/charts/BulletChart.vue +1 -1
  127. package/components/charts/CandlestickChart.jsx +2 -2
  128. package/components/charts/CandlestickChart.vue +3 -3
  129. package/components/charts/ChartKit.jsx +5 -5
  130. package/components/charts/ChartLegend.vue +16 -0
  131. package/components/charts/ChartTooltip.vue +16 -0
  132. package/components/charts/ChordChart.jsx +2 -2
  133. package/components/charts/ChordChart.vue +2 -2
  134. package/components/charts/GanttChart.jsx +2 -2
  135. package/components/charts/GanttChart.vue +2 -2
  136. package/components/charts/GaugeChart.jsx +2 -2
  137. package/components/charts/GaugeChart.vue +2 -2
  138. package/components/charts/HeatmapChart.jsx +2 -2
  139. package/components/charts/HeatmapChart.vue +2 -2
  140. package/components/charts/Histogram.jsx +2 -2
  141. package/components/charts/Histogram.vue +3 -3
  142. package/components/charts/LineChart.jsx +1 -1
  143. package/components/charts/LineChart.vue +3 -3
  144. package/components/charts/ParetoChart.jsx +3 -3
  145. package/components/charts/ParetoChart.vue +4 -4
  146. package/components/charts/PlotGrid.vue +18 -0
  147. package/components/charts/RadarChart.jsx +1 -1
  148. package/components/charts/RadarChart.vue +1 -1
  149. package/components/charts/Ribbon3D.jsx +2 -2
  150. package/components/charts/Ribbon3D.vue +2 -2
  151. package/components/charts/SankeyChart.jsx +2 -2
  152. package/components/charts/SankeyChart.vue +2 -2
  153. package/components/charts/Scatter3D.jsx +1 -1
  154. package/components/charts/Scatter3D.vue +1 -1
  155. package/components/charts/ScatterChart.jsx +3 -3
  156. package/components/charts/ScatterChart.vue +4 -4
  157. package/components/charts/Sparkline.jsx +1 -1
  158. package/components/charts/Sparkline.vue +1 -1
  159. package/components/charts/SunburstChart.jsx +1 -1
  160. package/components/charts/SunburstChart.vue +1 -1
  161. package/components/charts/Surface3D.jsx +2 -2
  162. package/components/charts/Surface3D.vue +2 -2
  163. package/components/charts/TreemapChart.jsx +2 -2
  164. package/components/charts/TreemapChart.vue +2 -2
  165. package/components/charts/WaterfallChart.jsx +2 -2
  166. package/components/charts/WaterfallChart.vue +3 -3
  167. package/components/charts/WorldMap.jsx +2 -2
  168. package/components/charts/WorldMap.vue +2 -2
  169. package/components/charts/XLabels.vue +11 -0
  170. package/components/data/CodeBlock.jsx +1 -1
  171. package/components/data/CodeBlock.vue +1 -1
  172. package/components/data/DiffValue.jsx +1 -1
  173. package/components/data/DiffValue.vue +1 -1
  174. package/components/data/FilterBar.d.ts +2 -0
  175. package/components/data/FilterBar.jsx +10 -9
  176. package/components/data/FilterBar.vue +9 -8
  177. package/components/data/KeyValue.jsx +3 -3
  178. package/components/data/KeyValue.vue +3 -3
  179. package/components/data/ListRow.jsx +2 -2
  180. package/components/data/ListRow.vue +2 -2
  181. package/components/data/Pagination.d.ts +5 -1
  182. package/components/data/Pagination.jsx +16 -12
  183. package/components/data/Pagination.vue +14 -9
  184. package/components/data/SmartTable.d.ts +4 -1
  185. package/components/data/SmartTable.jsx +16 -14
  186. package/components/data/SmartTable.vue +25 -22
  187. package/components/data/Spreadsheet.d.ts +2 -0
  188. package/components/data/Spreadsheet.jsx +12 -11
  189. package/components/data/Spreadsheet.vue +14 -13
  190. package/components/data/Table.d.ts +47 -0
  191. package/components/data/Table.jsx +85 -0
  192. package/components/data/Table.vue +63 -0
  193. package/components/data/data.prompt.md +3 -3
  194. package/components/display/Avatar.vue +3 -2
  195. package/components/display/AvatarGroup.vue +3 -2
  196. package/components/display/Badge.vue +1 -1
  197. package/components/display/Divider.vue +3 -3
  198. package/components/display/Donut.vue +3 -2
  199. package/components/display/IconTile.vue +2 -1
  200. package/components/display/Kbd.vue +1 -1
  201. package/components/display/ProgressBar.jsx +1 -1
  202. package/components/display/ProgressBar.vue +1 -1
  203. package/components/display/StatCard.vue +1 -1
  204. package/components/display/Tag.d.ts +3 -1
  205. package/components/display/Tag.jsx +4 -4
  206. package/components/display/Tag.vue +4 -4
  207. package/components/feedback/Accordion.jsx +2 -2
  208. package/components/feedback/Accordion.vue +2 -2
  209. package/components/feedback/ActivityItem.d.ts +3 -1
  210. package/components/feedback/ActivityItem.jsx +5 -5
  211. package/components/feedback/ActivityItem.vue +5 -4
  212. package/components/feedback/Alert.jsx +4 -4
  213. package/components/feedback/Alert.vue +3 -3
  214. package/components/feedback/Banner.jsx +5 -5
  215. package/components/feedback/Banner.vue +5 -5
  216. package/components/feedback/ContextMenu.d.ts +3 -1
  217. package/components/feedback/ContextMenu.jsx +5 -5
  218. package/components/feedback/ContextMenu.vue +5 -5
  219. package/components/feedback/DotActivity.vue +3 -3
  220. package/components/feedback/Drawer.d.ts +2 -0
  221. package/components/feedback/Drawer.jsx +6 -6
  222. package/components/feedback/Drawer.vue +22 -15
  223. package/components/feedback/EmptyState.vue +2 -2
  224. package/components/feedback/LogViewer.vue +1 -1
  225. package/components/feedback/Modal.d.ts +2 -0
  226. package/components/feedback/Modal.jsx +12 -11
  227. package/components/feedback/Modal.vue +10 -8
  228. package/components/feedback/Skeleton.vue +2 -1
  229. package/components/feedback/Spinner.vue +2 -1
  230. package/components/feedback/Stepper.vue +1 -1
  231. package/components/feedback/Timeline.jsx +1 -1
  232. package/components/feedback/Timeline.vue +1 -1
  233. package/components/feedback/Toast.vue +4 -4
  234. package/components/feedback/ToastStack.vue +11 -0
  235. package/components/feedback/Tooltip.vue +19 -0
  236. package/components/feedback/feedback.prompt.md +4 -4
  237. package/components/forms/Checkbox.d.ts +3 -2
  238. package/components/forms/Checkbox.jsx +7 -7
  239. package/components/forms/Checkbox.vue +8 -8
  240. package/components/forms/Combobox.d.ts +2 -0
  241. package/components/forms/Combobox.jsx +12 -10
  242. package/components/forms/Combobox.vue +11 -10
  243. package/components/forms/DatePicker.d.ts +4 -0
  244. package/components/forms/DatePicker.jsx +16 -12
  245. package/components/forms/DatePicker.vue +15 -11
  246. package/components/forms/Dropzone.jsx +2 -2
  247. package/components/forms/Dropzone.vue +5 -5
  248. package/components/forms/Field.d.ts +5 -1
  249. package/components/forms/Field.jsx +4 -4
  250. package/components/forms/Field.vue +4 -4
  251. package/components/forms/Input.d.ts +5 -1
  252. package/components/forms/Input.jsx +7 -7
  253. package/components/forms/Input.vue +3 -3
  254. package/components/forms/MultiSelect.d.ts +2 -0
  255. package/components/forms/MultiSelect.jsx +13 -11
  256. package/components/forms/MultiSelect.vue +12 -11
  257. package/components/forms/NumberStepper.d.ts +3 -1
  258. package/components/forms/NumberStepper.jsx +7 -7
  259. package/components/forms/NumberStepper.vue +8 -7
  260. package/components/forms/Radio.d.ts +5 -3
  261. package/components/forms/Radio.jsx +13 -15
  262. package/components/forms/Radio.vue +9 -10
  263. package/components/forms/RadioGroup.vue +5 -5
  264. package/components/forms/SearchInput.d.ts +3 -1
  265. package/components/forms/SearchInput.jsx +6 -6
  266. package/components/forms/SearchInput.vue +7 -6
  267. package/components/forms/Select.d.ts +3 -2
  268. package/components/forms/Select.jsx +6 -7
  269. package/components/forms/Select.vue +6 -6
  270. package/components/forms/Slider.jsx +3 -3
  271. package/components/forms/Slider.vue +3 -3
  272. package/components/forms/Switch.d.ts +2 -2
  273. package/components/forms/Switch.jsx +20 -22
  274. package/components/forms/Switch.vue +15 -14
  275. package/components/forms/Textarea.vue +4 -4
  276. package/components/forms/forms.prompt.md +1 -1
  277. package/components/forms/input-base.js +2 -1
  278. package/components/index.d.ts +1 -2
  279. package/components/index.js +3 -4
  280. package/components/navigation/Breadcrumb.jsx +3 -3
  281. package/components/navigation/Breadcrumb.vue +4 -4
  282. package/components/navigation/DocTree.jsx +3 -3
  283. package/components/navigation/DocTree.vue +3 -3
  284. package/components/navigation/PageHeading.vue +1 -1
  285. package/components/navigation/PageToolbar.vue +7 -6
  286. package/components/navigation/Sidebar.jsx +1 -1
  287. package/components/navigation/Sidebar.vue +3 -3
  288. package/components/navigation/ThemeToggle.vue +2 -1
  289. package/components/navigation/TopBar.d.ts +3 -1
  290. package/components/navigation/TopBar.jsx +6 -6
  291. package/components/navigation/Tree.jsx +5 -5
  292. package/components/navigation/Tree.vue +5 -5
  293. package/components/pipeline/ConfigPanel.d.ts +2 -0
  294. package/components/pipeline/ConfigPanel.jsx +9 -9
  295. package/components/pipeline/ConfigPanel.vue +9 -9
  296. package/components/pipeline/DataPreview.d.ts +3 -1
  297. package/components/pipeline/DataPreview.jsx +7 -7
  298. package/components/pipeline/DataPreview.vue +7 -7
  299. package/components/pipeline/NodeCard.jsx +3 -3
  300. package/components/pipeline/NodeCard.vue +3 -3
  301. package/components/pipeline/PaletteItem.vue +2 -2
  302. package/components/pipeline/PaletteListItem.vue +1 -1
  303. package/components/pipeline/PalettePanel.jsx +2 -2
  304. package/components/pipeline/PalettePanel.vue +1 -1
  305. package/components/pipeline/Port.vue +15 -0
  306. package/components/pipeline/PortLabel.vue +15 -0
  307. package/components/pipeline/ZoomControls.vue +1 -1
  308. package/components/pipeline/port-skin.js +13 -0
  309. package/components/px.js +4 -0
  310. package/components/tabs/PanelTabs.vue +5 -4
  311. package/components/tabs/PillTabs.vue +5 -4
  312. package/components/tabs/SegmentedControl.d.ts +2 -2
  313. package/components/tabs/SegmentedControl.jsx +7 -7
  314. package/components/tabs/SegmentedControl.vue +7 -7
  315. package/components/tabs/Tabs.d.ts +5 -3
  316. package/components/tabs/Tabs.jsx +15 -15
  317. package/components/tabs/Tabs.vue +5 -5
  318. package/components/tabs/ViewToggle.d.ts +3 -1
  319. package/components/tabs/ViewToggle.jsx +5 -5
  320. package/components/tabs/ViewToggle.vue +5 -5
  321. package/components/tabs/tabs.prompt.md +1 -1
  322. package/keybricks.css +4 -0
  323. package/package.json +6 -3
  324. package/readme.md +90 -122
  325. package/readme.zh-CN.md +120 -0
  326. package/tokens/charts.css +20 -17
  327. package/tokens/colors.css +2 -2
  328. package/tokens/dark.css +2 -2
  329. package/tokens/hover.css +32 -0
  330. package/tokens/interaction.css +3 -1
  331. package/tokens/layout.css +29 -7
  332. package/tokens/tokens.d.ts +36 -0
  333. package/tokens/tokens.js +143 -89
  334. package/tokens/tokens.json +240 -106
  335. package/tokens/typography.css +27 -14
  336. package/components/_cards/brand-visual.card.html +0 -43
  337. package/components/_cards/datatable.card.html +0 -48
  338. package/components/_shared/src-brand.js +0 -1
  339. package/components/brand/BrandVisual.d.ts +0 -17
  340. package/components/brand/BrandVisual.jsx +0 -29
  341. package/components/brand/BrandVisual.vue +0 -34
  342. package/components/brand/_dir.card.html +0 -13
  343. package/components/brand/brandMotifs.js +0 -210
  344. package/components/buttons/_dir.card.html +0 -13
  345. package/components/charts/ChartKit.vue +0 -47
  346. package/components/charts/_dir.card.html +0 -7
  347. package/components/data/DataTable.d.ts +0 -28
  348. package/components/data/DataTable.jsx +0 -32
  349. package/components/data/DataTable.vue +0 -25
  350. package/components/data/_dir.card.html +0 -13
  351. package/components/display/_dir.card.html +0 -13
  352. package/components/feedback/_dir.card.html +0 -13
  353. package/components/forms/_dir.card.html +0 -13
  354. package/components/icons/_dir.card.html +0 -13
  355. package/components/navigation/_dir.card.html +0 -13
  356. package/components/pipeline/_dir.card.html +0 -13
  357. package/components/tabs/_dir.card.html +0 -13
@@ -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 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
+ 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. A size region (default inherit → md): every control in the header shares one tier. */\nexport function ConfigPanel({ title, subtitle, tabs = [\"Node Info\", \"Calc Rules\", \"Output Fields\", \"SQL Preview\"], activeTab, onTab, previewRows = 100, onPreviewRows, onSave, onPreview, children, height = \"100%\", size, style }) {\n const [tab, setTab] = React.useState(tabs[0]);\n const cur = activeTab != null ? activeTab : tab;\n return (\n <div data-size={size} 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: \"var(--leading-md)\", 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: 2 }}>{subtitle}</span>\n </div>\n <Button onClick={onSave}>Save Settings</Button>\n </div>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"var(--control-gap)\", padding: \"12px 0\" }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-secondary)\" }}>Preview rows</span>\n <NumberStepper value={previewRows} onChange={onPreviewRows} />\n <Button variant=\"primary-soft\" icon=\"eye\" 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. A size region (default inherit → md): every control in the header shares one tier.\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%' }, size: String, 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 :data-size=\"size\" :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:var(--leading-md);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:2px\">{{ subtitle }}</span>\n </div>\n <Button @click=\"emit('save')\">Save Settings</Button>\n </div>\n <div style=\"display:flex;align-items:center;gap:var(--control-gap);padding:12px 0\">\n <span style=\"font-family:var(--font-sans);font-size:var(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-secondary)\">Preview rows</span>\n <NumberStepper :model-value=\"previewRows\" @update:modelValue=\"v => emit('update:previewRows', v)\" />\n <Button variant=\"primary-soft\" icon=\"eye\" @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 { Table } from \"../data/Table.jsx\";\n\n/** Bottom data-preview drawer — 250px: 40px header (title, Debug Mode, collapse) + dense table. An sm size region: the header's controls are 24px and the table has 32px rows with 12px text. */\nexport function DataPreview({ title = \"Data Preview\", columns = [], rows = [], debug = false, onDebug, collapsed, onCollapse, size = \"sm\", style }) {\n return (\n <div data-size={size} style={{ background: \"var(--bg-surface)\", borderTop: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", height: collapsed ? \"var(--bar-card-h)\" : \"var(--drawer-h)\", overflow: \"hidden\", boxSizing: \"border-box\", flexShrink: 0, transition: \"height .15s ease\", ...style }}>\n <div style={{ height: \"var(--bar-card-h)\", display: \"flex\", alignItems: \"center\", gap: \"var(--group-gap)\", padding: \"0 var(--edge-pad)\", 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\"} muted onClick={onCollapse} label={collapsed ? \"Expand\" : \"Collapse\"} />\n </div>\n {!collapsed ? (\n <div style={{ overflowY: \"auto\", borderTop: \"1px solid var(--border-subtle)\" }}>\n <Table resizable 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 Table from '../data/Table.vue'\n// Bottom data-preview drawer — 250px: 40px header (title, Debug Mode, collapse) + dense table. An sm size region: the header's controls are 24px and the table has 32px rows with 12px text.\ndefineProps({ title: { type: String, default: 'Data Preview' }, columns: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, debug: Boolean, collapsed: Boolean, size: { type: String, default: 'sm' }, style: Object })\nconst emit = defineEmits(['update:debug', 'toggleCollapse'])\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ background: 'var(--bg-surface)', borderTop: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', height: collapsed ? 'var(--bar-card-h)' : 'var(--drawer-h)', overflow: 'hidden', boxSizing: 'border-box', flexShrink: 0, transition: 'height .15s ease', ...style }\">\n <div style=\"height:var(--bar-card-h);display:flex;align-items:center;gap:var(--group-gap);padding:0 var(--edge-pad);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'\" muted :label=\"collapsed ? 'Expand' : 'Collapse'\" @click=\"emit('toggleCollapse')\" />\n </div>\n <div v-if=\"!collapsed\" style=\"overflow-y:auto;border-top:1px solid var(--border-subtle)\">\n <Table resizable :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: \"var(--leading-base)\", 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\", alignItems: \"center\", flexShrink: 0, height: \"var(--leading-xs)\", transform: \"translateY(-0.5px)\" }}><Icon name={subtitleIcon || \"info\"} size={12} /></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: \"var(--leading-xs)\", 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:var(--leading-base);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;align-items:center;flex-shrink:0;height:var(--leading-xs);transform:translateY(-0.5px)\"><Icon :name=\"subtitleIcon || 'info'\" :size=\"12\" /></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:var(--leading-xs);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.664px', borderRadius: 'var(--radius-lg)', padding: '8px', display: 'flex', flexDirection: 'column', gap: '6px', 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-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);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: '8px', 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-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", 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)} data-wash=\"\" style={{ height: \"var(--h-subnav-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 8, padding: \"6px 8px\", boxSizing: \"border-box\", cursor: \"pointer\", background: \"var(--wash, transparent)\", transition: \"var(--t-hover)\" }}>\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)\" data-wash=\"\" 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;background:var(--wash, transparent);transition:var(--t-hover)\">\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: '2px', background: 'var(--bg-surface)', border: '1px solid var(--border-control)', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-menu)', padding: '3px', 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","pipeline/Port.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport { portSkin } from './port-skin.js'\n// Round output/input port — 22px. tone: \"default\" | \"active\" (dragging from it) | \"target\" (drop here).\n// interactive lights it on hover and shows a grab cursor. Native listeners (@mousedown, @mouseup…) fall through to the span.\ndefineProps({ tone: { type: String, default: 'default' }, interactive: Boolean, style: Object })\nconst hover = ref(false)\n</script>\n<template>\n <span @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ width: '22px', height: '22px', 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\" :stroke-width=\"2.2\" />\n </span>\n</template>\n","pipeline/PortLabel.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport { portSkin } from './port-skin.js'\n// Labeled input port pill (\"A >\", \"B >\") — 28×22, same three tones as Port.\ndefineProps({ label: { type: String, default: 'A' }, tone: { type: String, default: 'default' }, interactive: Boolean, style: Object })\nconst hover = ref(false)\n</script>\n<template>\n <span @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ 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, ...portSkin(tone, hover, interactive), ...style }\">\n <Icon v-if=\"tone === 'target'\" name=\"plus\" :size=\"12\" :stroke-width=\"2.2\" />\n <template v-else>{{ label }}<Icon name=\"chevron-right\" :size=\"9\" :stroke-width=\"2.5\" /></template>\n </span>\n</template>\n","pipeline/port-skin.js":"// Shared port skin for Port.vue / PortLabel.vue / NodeCard.vue — mirrors portSkin() in NodeCard.jsx.\n// tone: \"default\" | \"active\" (a wire is being dragged from it) | \"target\" (valid drop here).\nexport function 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"});
@@ -1 +1 @@
1
- window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"tabs/SegmentedControl.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/**\n * Segmented control — inset track, raised active segment. Switches views of the same data, applies instantly.\n * options: string[] | { label, icon?, disabled? }[] value/onChange by label\n * size \"md\" (32) | \"sm\" (26) · iconOnly hides labels (label becomes aria-label / title)\n * disabled greys the whole control · fullWidth stretches segments evenly\n * Keyboard: ← → move between enabled segments.\n */\nexport function SegmentedControl({ options = [], value, onChange, size = \"md\", iconOnly, disabled, fullWidth, style }) {\n const h = size === \"sm\" ? 26 : 32;\n const opts = options.map((o) => (typeof o === \"string\" ? { label: o } : o));\n const enabled = opts.filter((o) => !o.disabled);\n const onKey = (e) => {\n if (e.key !== \"ArrowLeft\" && e.key !== \"ArrowRight\" || disabled || !enabled.length) return;\n e.preventDefault();\n const i = enabled.findIndex((o) => o.label === value);\n const n = e.key === \"ArrowRight\" ? enabled[(i + 1) % enabled.length] : enabled[(i - 1 + enabled.length) % enabled.length];\n onChange && onChange(n.label);\n };\n return (\n <div role=\"tablist\" aria-disabled={disabled || undefined} onKeyDown={onKey} style={{ display: fullWidth ? \"flex\" : \"inline-flex\", padding: 2, gap: 2, background: \"var(--bg-inset)\", border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-md)\", boxSizing: \"border-box\", opacity: disabled ? .5 : 1, ...style }}>\n {opts.map((o) => {\n const is = value === o.label, off = disabled || o.disabled;\n return (\n <button key={o.label} type=\"button\" role=\"tab\" aria-selected={is} aria-label={iconOnly ? o.label : undefined} title={iconOnly ? o.label : undefined} disabled={off} tabIndex={is ? 0 : -1}\n onClick={() => !off && onChange && onChange(o.label)}\n style={{ height: h - 6, padding: iconOnly ? \"0 8px\" : \"0 10px\", flex: fullWidth ? 1 : \"none\", justifyContent: \"center\", display: \"inline-flex\", alignItems: \"center\", gap: 6, borderRadius: \"var(--radius-sm)\", border: \"none\", cursor: off ? \"not-allowed\" : \"pointer\", background: is ? \"var(--bg-surface)\" : \"transparent\", boxShadow: is ? \"0 1px 2px rgba(0,0,0,.08)\" : \"none\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: size === \"sm\" ? \"var(--text-xs)\" : \"var(--text-sm)\", color: is ? \"var(--text-heading)\" : \"var(--text-secondary)\", opacity: o.disabled && !disabled ? .45 : 1, whiteSpace: \"nowrap\", transition: \"var(--t-hover)\" }}>\n {o.icon ? <Icon name={o.icon} size={13} /> : null}{iconOnly ? null : o.label}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/SegmentedControl.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Segmented control — inset track, raised active segment. Switches views of the same data, applies instantly.\n// options: string[] | { label, icon?, disabled? }[] · v-model by label · size md (32) | sm (26)\n// iconOnly hides labels (label → aria-label/title) · disabled greys the control · fullWidth stretches segments · ← → keyboard.\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, size: { type: String, default: 'md' }, iconOnly: Boolean, disabled: Boolean, fullWidth: Boolean, style: Object })\nconst emit = defineEmits(['update:modelValue'])\nconst opts = computed(() => props.options.map(o => typeof o === 'string' ? { label: o } : o))\nconst enabled = computed(() => opts.value.filter(o => !o.disabled))\nconst h = computed(() => props.size === 'sm' ? 26 : 32)\nconst pick = (o) => { if (!(props.disabled || o.disabled)) emit('update:modelValue', o.label) }\nfunction onKey(e) {\n if ((e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') || props.disabled || !enabled.value.length) return\n e.preventDefault()\n const i = enabled.value.findIndex(o => o.label === props.modelValue), n = enabled.value.length\n emit('update:modelValue', (e.key === 'ArrowRight' ? enabled.value[(i + 1) % n] : enabled.value[(i - 1 + n) % n]).label)\n}\n</script>\n<template>\n <div role=\"tablist\" :aria-disabled=\"disabled || undefined\" @keydown=\"onKey\" :style=\"{ display: fullWidth ? 'flex' : 'inline-flex', padding: '2px', gap: '2px', background: 'var(--bg-inset)', border: '1px solid var(--border-control)', borderRadius: 'var(--radius-md)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...style }\">\n <button v-for=\"o in opts\" :key=\"o.label\" type=\"button\" role=\"tab\" :aria-selected=\"modelValue === o.label\" :aria-label=\"iconOnly ? o.label : undefined\" :title=\"iconOnly ? o.label : undefined\" :disabled=\"disabled || o.disabled\" :tabindex=\"modelValue === o.label ? 0 : -1\" @click=\"pick(o)\"\n :style=\"{ height: (h - 6) + 'px', padding: iconOnly ? '0 8px' : '0 10px', flex: fullWidth ? 1 : 'none', justifyContent: 'center', display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: (disabled || o.disabled) ? 'not-allowed' : 'pointer', background: modelValue === o.label ? 'var(--bg-surface)' : 'transparent', boxShadow: modelValue === o.label ? '0 1px 2px rgba(0,0,0,.08)' : 'none', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: size === 'sm' ? 'var(--text-xs)' : 'var(--text-sm)', color: modelValue === o.label ? 'var(--text-heading)' : 'var(--text-secondary)', opacity: o.disabled && !disabled ? .45 : 1, whiteSpace: 'nowrap', transition: 'var(--t-hover)' }\">\n <Icon v-if=\"o.icon\" :name=\"o.icon\" :size=\"13\" /><template v-if=\"!iconOnly\">{{ o.label }}</template>\n </button>\n </div>\n</template>\n","tabs/Tabs.jsx":"import React from \"react\";\n\n/** Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline. */\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"});
1
+ window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"tabs/SegmentedControl.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/**\n * Segmented control — inset track, raised active segment. Switches views of the same data, applies instantly.\n * options: string[] | { label, icon?, disabled? }[] value/onChange by label\n * size sm 24 | md 32 | lg 36 — omit it to inherit the region's tier · iconOnly hides labels (label becomes aria-label / title)\n * disabled greys the whole control · fullWidth stretches segments evenly\n * Keyboard: ← → move between enabled segments.\n */\nconst tierIcon = { width: \"var(--control-icon)\", height: \"var(--control-icon)\" };\nexport function SegmentedControl({ options = [], value, onChange, size, iconOnly, disabled, fullWidth, style }) {\n const opts = options.map((o) => (typeof o === \"string\" ? { label: o } : o));\n const enabled = opts.filter((o) => !o.disabled);\n const onKey = (e) => {\n if (e.key !== \"ArrowLeft\" && e.key !== \"ArrowRight\" || disabled || !enabled.length) return;\n e.preventDefault();\n const i = enabled.findIndex((o) => o.label === value);\n const n = e.key === \"ArrowRight\" ? enabled[(i + 1) % enabled.length] : enabled[(i - 1 + enabled.length) % enabled.length];\n onChange && onChange(n.label);\n };\n return (\n <div role=\"tablist\" data-size={size} aria-disabled={disabled || undefined} onKeyDown={onKey} style={{ display: fullWidth ? \"flex\" : \"inline-flex\", height: \"var(--control-h)\", padding: 2, gap: 2, background: \"var(--bg-inset)\", border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-md)\", boxSizing: \"border-box\", opacity: disabled ? .5 : 1, ...style }}>\n {opts.map((o) => {\n const is = value === o.label, off = disabled || o.disabled;\n return (\n <button key={o.label} type=\"button\" role=\"tab\" aria-selected={is} aria-label={iconOnly ? o.label : undefined} title={iconOnly ? o.label : undefined} disabled={off} tabIndex={is ? 0 : -1} data-ink={is || off ? undefined : \"\"}\n onClick={() => !off && onChange && onChange(o.label)}\n style={{ height: \"calc(var(--control-h) - 6px)\", padding: iconOnly ? \"0 calc(var(--control-px) - 4px)\" : \"0 calc(var(--control-px) - 2px)\", flex: fullWidth ? 1 : \"none\", justifyContent: \"center\", display: \"inline-flex\", alignItems: \"center\", gap: 6, borderRadius: \"var(--radius-sm)\", border: \"none\", cursor: off ? \"not-allowed\" : \"pointer\", background: is ? \"var(--bg-surface)\" : \"transparent\", boxShadow: is ? \"0 1px 2px rgba(0,0,0,.08)\" : \"none\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: is ? \"var(--text-heading)\" : \"var(--ink, var(--text-secondary))\", opacity: o.disabled && !disabled ? .45 : 1, whiteSpace: \"nowrap\", transition: \"var(--t-hover)\" }}>\n {o.icon ? <Icon name={o.icon} size={16} style={tierIcon} /> : null}{iconOnly ? null : o.label}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/SegmentedControl.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Segmented control — inset track, raised active segment. Switches views of the same data, applies instantly.\n// options: string[] | { label, icon?, disabled? }[] · v-model by label · size sm 24 | md 32 | lg 36 — omit it to inherit the region's tier\n// iconOnly hides labels (label → aria-label/title) · disabled greys the control · fullWidth stretches segments · ← → keyboard.\nconst tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, size: String, iconOnly: Boolean, disabled: Boolean, fullWidth: Boolean, style: Object })\nconst emit = defineEmits(['update:modelValue'])\nconst opts = computed(() => props.options.map(o => typeof o === 'string' ? { label: o } : o))\nconst enabled = computed(() => opts.value.filter(o => !o.disabled))\nconst pick = (o) => { if (!(props.disabled || o.disabled)) emit('update:modelValue', o.label) }\nfunction onKey(e) {\n if ((e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') || props.disabled || !enabled.value.length) return\n e.preventDefault()\n const i = enabled.value.findIndex(o => o.label === props.modelValue), n = enabled.value.length\n emit('update:modelValue', (e.key === 'ArrowRight' ? enabled.value[(i + 1) % n] : enabled.value[(i - 1 + n) % n]).label)\n}\n</script>\n<template>\n <div role=\"tablist\" :data-size=\"size\" :aria-disabled=\"disabled || undefined\" @keydown=\"onKey\" :style=\"{ display: fullWidth ? 'flex' : 'inline-flex', height: 'var(--control-h)', padding: '2px', gap: '2px', background: 'var(--bg-inset)', border: '1px solid var(--border-control)', borderRadius: 'var(--radius-md)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...style }\">\n <button v-for=\"o in opts\" :key=\"o.label\" type=\"button\" role=\"tab\" :aria-selected=\"modelValue === o.label\" :aria-label=\"iconOnly ? o.label : undefined\" :title=\"iconOnly ? o.label : undefined\" :disabled=\"disabled || o.disabled\" :tabindex=\"modelValue === o.label ? 0 : -1\" :data-ink=\"modelValue === o.label || disabled || o.disabled ? undefined : ''\" @click=\"pick(o)\"\n :style=\"{ height: 'calc(var(--control-h) - 6px)', padding: iconOnly ? '0 calc(var(--control-px) - 4px)' : '0 calc(var(--control-px) - 2px)', flex: fullWidth ? 1 : 'none', justifyContent: 'center', display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: (disabled || o.disabled) ? 'not-allowed' : 'pointer', background: modelValue === o.label ? 'var(--bg-surface)' : 'transparent', boxShadow: modelValue === o.label ? '0 1px 2px rgba(0,0,0,.08)' : 'none', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: modelValue === o.label ? 'var(--text-heading)' : 'var(--ink, var(--text-secondary))', opacity: o.disabled && !disabled ? .45 : 1, whiteSpace: 'nowrap', transition: 'var(--t-hover)' }\">\n <Icon v-if=\"o.icon\" :name=\"o.icon\" :size=\"16\" :style=\"tierIcon\" /><template v-if=\"!iconOnly\">{{ o.label }}</template>\n </button>\n </div>\n</template>\n","tabs/Tabs.jsx":"import React from \"react\";\n\n/** Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline. The bar is the region's bar height (sm 32 · md 40 · lg 44) with the tier's control text; omit `size` to inherit. */\nexport function Tabs({ tabs = [], active, onChange, size, style }) {\n return (\n <div role=\"tablist\" data-size={size} style={{ display: \"flex\", gap: 4, borderBottom: \"1px solid var(--border-control)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" role=\"tab\" aria-selected={is} data-ink={is ? undefined : \"\"} onClick={() => onChange && onChange(t)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", height: \"var(--bar-h)\", boxSizing: \"border-box\", padding: \"0 var(--control-px)\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: is ? \"var(--action-primary)\" : \"var(--ink, var(--text-body))\", whiteSpace: \"nowrap\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n\n/** Panel tabs (Node Info · Calc Rules · Output Fields · SQL Preview): a lighter underline tab bar at the region's bar height with regular-weight control text. Put the panel in an sm region for the compact 32px/12px look. */\nexport function PanelTabs({ tabs = [], active, onChange, size, style }) {\n return (\n <div role=\"tablist\" data-size={size} style={{ display: \"flex\", borderBottom: \"1px solid var(--border-subtle)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" role=\"tab\" aria-selected={is} data-ink={is ? undefined : \"\"} onClick={() => onChange && onChange(t)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", height: \"var(--bar-h)\", boxSizing: \"border-box\", padding: \"0 var(--control-px)\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: is ? \"var(--action-primary)\" : \"var(--ink, var(--text-secondary))\", whiteSpace: \"nowrap\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n\n/** Text pill filter tabs (For Review · To be Submitted · For Approval). Active = tinted pill. Pills are control-height (sm 24 · md 32 · lg 36); omit `size` to inherit. */\nexport function PillTabs({ tabs = [], active, onChange, size, style }) {\n return (\n <div role=\"tablist\" data-size={size} style={{ display: \"flex\", gap: \"var(--control-gap)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" role=\"tab\" aria-selected={is} data-ink={is ? undefined : \"\"} data-wash={is ? undefined : \"\"} onClick={() => onChange && onChange(t)}\n style={{ background: is ? \"var(--bg-active)\" : \"var(--wash, transparent)\", border: \"none\", cursor: \"pointer\", height: \"var(--control-h)\", padding: \"0 var(--control-px)\", borderRadius: \"var(--radius-md)\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: is ? \"var(--text-heading)\" : \"var(--ink, var(--text-secondary))\", whiteSpace: \"nowrap\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/Tabs.vue":"<script setup>\n// Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline. The bar is the region's bar height (sm 32 · md 40 · lg 44) with the tier's control text; omit `size` to inherit.\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, size: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div role=\"tablist\" :data-size=\"size\" :style=\"{ display: 'flex', gap: '4px', borderBottom: '1px solid var(--border-control)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" role=\"tab\" :aria-selected=\"t === active\" :data-ink=\"t === active ? undefined : ''\" @click=\"emit('change', t)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', height: 'var(--bar-h)', boxSizing: 'border-box', padding: '0 var(--control-px)', marginBottom: '-1px', borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t === active ? 'var(--action-primary)' : 'var(--ink, var(--text-body))', whiteSpace: 'nowrap' }\">{{ t }}</button>\n </div>\n</template>\n","tabs/ViewToggle.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Two-state list/grid icon toggle (palette header). Two IconButtons at the region's tier; omit `size` to inherit. */\nexport function ViewToggle({ value = \"grid\", onChange, size, style }) {\n return (\n <div data-size={size} style={{ display: \"inline-flex\", gap: 2, ...style }}>\n <IconButton icon=\"list\" active={value === \"list\"} onClick={() => onChange && onChange(\"list\")} label=\"List view\" />\n <IconButton icon=\"layout-grid\" active={value === \"grid\"} onClick={() => onChange && onChange(\"grid\")} label=\"Grid view\" />\n </div>\n );\n}\n","tabs/ViewToggle.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\n// Two-state list/grid icon toggle (palette header). Two IconButtons at the region's tier; omit `size` to inherit.\ndefineProps({ modelValue: { type: String, default: 'grid' }, size: String, style: Object })\nconst emit = defineEmits(['update:modelValue'])\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ display: 'inline-flex', gap: '2px', ...style }\">\n <IconButton icon=\"list\" :active=\"modelValue === 'list'\" label=\"List view\" @click=\"emit('update:modelValue', 'list')\" />\n <IconButton icon=\"layout-grid\" :active=\"modelValue === 'grid'\" label=\"Grid view\" @click=\"emit('update:modelValue', 'grid')\" />\n </div>\n</template>\n","tabs/PanelTabs.vue":"<script setup>\n// Panel tabs (Node Info · Calc Rules · …): a lighter underline tab bar at the region's bar height with regular-weight control text. Put the panel in an sm region for the compact 32px/12px look.\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, size: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div role=\"tablist\" :data-size=\"size\" :style=\"{ display: 'flex', borderBottom: '1px solid var(--border-subtle)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" role=\"tab\" :aria-selected=\"t === active\" :data-ink=\"t === active ? undefined : ''\" @click=\"emit('change', t)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', height: 'var(--bar-h)', boxSizing: 'border-box', padding: '0 var(--control-px)', marginBottom: '-1px', borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t === active ? 'var(--action-primary)' : 'var(--ink, var(--text-secondary))', whiteSpace: 'nowrap' }\">{{ t }}</button>\n </div>\n</template>\n","tabs/PillTabs.vue":"<script setup>\n// Text pill filter tabs. Active = tinted pill. Pills are control-height (sm 24 · md 32 · lg 36); omit `size` to inherit.\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, size: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div role=\"tablist\" :data-size=\"size\" :style=\"{ display: 'flex', gap: 'var(--control-gap)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" role=\"tab\" :aria-selected=\"t === active\" :data-ink=\"t === active ? undefined : ''\" :data-wash=\"t === active ? undefined : ''\" @click=\"emit('change', t)\"\n :style=\"{ background: t === active ? 'var(--bg-active)' : 'var(--wash, transparent)', border: 'none', cursor: 'pointer', height: 'var(--control-h)', padding: '0 var(--control-px)', borderRadius: 'var(--radius-md)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: t === active ? 'var(--text-heading)' : 'var(--ink, var(--text-secondary))', whiteSpace: 'nowrap' }\">{{ t }}</button>\n </div>\n</template>\n"});
@@ -1,9 +1,6 @@
1
1
  /* Generated barrel — do not edit by hand. Run: node scripts/build-index.mjs
2
2
  Vue 3 single-file components; your bundler needs @vitejs/plugin-vue or vue-loader. */
3
3
 
4
- /* brand/ */
5
- export { default as BrandVisual } from "../brand/BrandVisual.vue";
6
-
7
4
  /* buttons/ */
8
5
  export { default as ActionChip } from "../buttons/ActionChip.vue";
9
6
  export { default as Button } from "../buttons/Button.vue";
@@ -17,7 +14,8 @@ export { default as BarList } from "../charts/BarList.vue";
17
14
  export { default as BoxPlot } from "../charts/BoxPlot.vue";
18
15
  export { default as BulletChart } from "../charts/BulletChart.vue";
19
16
  export { default as CandlestickChart } from "../charts/CandlestickChart.vue";
20
- export { default as ChartKit } from "../charts/ChartKit.vue";
17
+ export { default as ChartLegend } from "../charts/ChartLegend.vue";
18
+ export { default as ChartTooltip } from "../charts/ChartTooltip.vue";
21
19
  export { default as ChordChart } from "../charts/ChordChart.vue";
22
20
  export { default as DonutChart } from "../charts/DonutChart.vue";
23
21
  export { default as FunnelChart } from "../charts/FunnelChart.vue";
@@ -28,6 +26,7 @@ export { default as Histogram } from "../charts/Histogram.vue";
28
26
  export { default as LineChart } from "../charts/LineChart.vue";
29
27
  export { default as ParetoChart } from "../charts/ParetoChart.vue";
30
28
  export { default as Pie3D } from "../charts/Pie3D.vue";
29
+ export { default as PlotGrid } from "../charts/PlotGrid.vue";
31
30
  export { default as RadarChart } from "../charts/RadarChart.vue";
32
31
  export { default as Ribbon3D } from "../charts/Ribbon3D.vue";
33
32
  export { default as SankeyChart } from "../charts/SankeyChart.vue";
@@ -39,10 +38,10 @@ export { default as Surface3D } from "../charts/Surface3D.vue";
39
38
  export { default as TreemapChart } from "../charts/TreemapChart.vue";
40
39
  export { default as WaterfallChart } from "../charts/WaterfallChart.vue";
41
40
  export { default as WorldMap } from "../charts/WorldMap.vue";
41
+ export { default as XLabels } from "../charts/XLabels.vue";
42
42
 
43
43
  /* data/ */
44
44
  export { default as CodeBlock } from "../data/CodeBlock.vue";
45
- export { default as DataTable } from "../data/DataTable.vue";
46
45
  export { default as DiffValue } from "../data/DiffValue.vue";
47
46
  export { default as FilterBar } from "../data/FilterBar.vue";
48
47
  export { default as KeyValue } from "../data/KeyValue.vue";
@@ -51,6 +50,7 @@ export { default as ListRow } from "../data/ListRow.vue";
51
50
  export { default as Pagination } from "../data/Pagination.vue";
52
51
  export { default as SmartTable } from "../data/SmartTable.vue";
53
52
  export { default as Spreadsheet } from "../data/Spreadsheet.vue";
53
+ export { default as Table } from "../data/Table.vue";
54
54
 
55
55
  /* display/ */
56
56
  export { default as AreaChart } from "../display/AreaChart.vue";
@@ -85,6 +85,8 @@ export { default as Spinner } from "../feedback/Spinner.vue";
85
85
  export { default as Stepper } from "../feedback/Stepper.vue";
86
86
  export { default as Timeline } from "../feedback/Timeline.vue";
87
87
  export { default as Toast } from "../feedback/Toast.vue";
88
+ export { default as ToastStack } from "../feedback/ToastStack.vue";
89
+ export { default as Tooltip } from "../feedback/Tooltip.vue";
88
90
 
89
91
  /* forms/ */
90
92
  export { default as Checkbox } from "../forms/Checkbox.vue";
@@ -125,6 +127,8 @@ export { default as PaletteItem } from "../pipeline/PaletteItem.vue";
125
127
  export { default as PaletteListItem } from "../pipeline/PaletteListItem.vue";
126
128
  export { default as PalettePanel } from "../pipeline/PalettePanel.vue";
127
129
  export { default as PipelineCanvas } from "../pipeline/PipelineCanvas.vue";
130
+ export { default as Port } from "../pipeline/Port.vue";
131
+ export { default as PortLabel } from "../pipeline/PortLabel.vue";
128
132
  export { default as ZoomControls } from "../pipeline/ZoomControls.vue";
129
133
 
130
134
  /* tabs/ */
@@ -1,6 +1,8 @@
1
1
  import * as React from "react";
2
- /** Tinted bulk-action chip (comparison-table actions). 30px, r8, 12px/500. */
2
+ /** Tinted bulk-action chip (comparison-table actions). Tier height and text (32px/14px at md), r8, weight 500. */
3
3
  export interface ActionChipProps {
4
+ /** Size tier: sm 24 · md 32 · lg 36. Omit to inherit the enclosing region's tier (the default). */
5
+ size?: "sm" | "md" | "lg";
4
6
  /** success #ecfdf5/#007a55 · warning #fff7ed/#ca3500 · neutral #f5f5f5/#525252 · info · danger */
5
7
  tone?: "success" | "warning" | "neutral" | "info" | "danger";
6
8
  onClick?: () => void;
@@ -8,12 +8,12 @@ const chipTones = {
8
8
  danger: { background: "var(--danger-bg)", color: "var(--danger-text)" },
9
9
  };
10
10
 
11
- /** Tinted bulk-action chip ("Mark as New", "Mark as Overwrite", "Skip Selected"). */
12
- export function ActionChip({ tone = "neutral", children, onClick, style }) {
11
+ /** Tinted bulk-action chip ("Mark as New", "Mark as Overwrite", "Skip Selected"). A control: it takes the region's tier height and text; `size` overrides it. */
12
+ export function ActionChip({ tone = "neutral", size, children, onClick, style }) {
13
13
  const [hover, setHover] = React.useState(false);
14
14
  return (
15
- <button type="button" onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
16
- style={{ height: 30, padding: "0 12px", borderRadius: "var(--radius-md)", border: "none", cursor: "pointer", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", display: "inline-flex", alignItems: "center", filter: hover ? "brightness(1.06) saturate(1.05)" : "none", transition: "var(--t-hover),filter var(--dur-fast) var(--ease-out)", ...chipTones[tone], ...style }}>
15
+ <button type="button" data-size={size} onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
16
+ style={{ height: "var(--control-h)", padding: "0 var(--control-px)", borderRadius: "var(--radius-md)", border: "none", cursor: "pointer", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", display: "inline-flex", alignItems: "center", filter: hover ? "brightness(1.06) saturate(1.05)" : "none", transition: "var(--t-hover),filter var(--dur-fast) var(--ease-out)", ...chipTones[tone], ...style }}>
17
17
  {children}
18
18
  </button>
19
19
  );
@@ -7,12 +7,12 @@ const chipTones = {
7
7
  info: { background: 'var(--info-bg)', color: 'var(--action-primary)' },
8
8
  danger: { background: 'var(--danger-bg)', color: 'var(--danger-text)' },
9
9
  }
10
- // Tinted bulk-action chip ("Mark as New", "Mark as Overwrite", "Skip Selected").
11
- const props = defineProps({ tone: { type: String, default: 'neutral' }, style: Object })
10
+ // Tinted bulk-action chip ("Mark as New", "Mark as Overwrite", "Skip Selected"). A control: it takes the region's tier height and text; `size` overrides it.
11
+ const props = defineProps({ tone: { type: String, default: 'neutral' }, size: String, style: Object })
12
12
  defineEmits(['click'])
13
13
  const hover = ref(false)
14
- const s = computed(() => ({ height: 30, padding: '0 12px', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', display: 'inline-flex', alignItems: 'center', filter: hover.value ? 'brightness(1.06) saturate(1.05)' : 'none', transition: 'var(--t-hover),filter var(--dur-fast) var(--ease-out)', ...chipTones[props.tone], ...props.style }))
14
+ const s = computed(() => ({ height: 'var(--control-h)', padding: '0 var(--control-px)', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', display: 'inline-flex', alignItems: 'center', filter: hover.value ? 'brightness(1.06) saturate(1.05)' : 'none', transition: 'var(--t-hover),filter var(--dur-fast) var(--ease-out)', ...chipTones[props.tone], ...props.style }))
15
15
  </script>
16
16
  <template>
17
- <button type="button" @click="$emit('click')" @mouseenter="hover = true" @mouseleave="hover = false" :style="s"><slot /></button>
17
+ <button type="button" :data-size="size" @click="$emit('click')" @mouseenter="hover = true" @mouseleave="hover = false" :style="s"><slot /></button>
18
18
  </template>
@@ -1,14 +1,17 @@
1
1
  import * as React from "react";
2
2
  /**
3
- * Keybricks button — flat, hairline, 13px default.
3
+ * Keybricks button — flat, hairline. Height, text, icon and padding come from the size tier of the region it sits in.
4
4
  */
5
5
  export interface ButtonProps {
6
6
  /** primary (blue #155dfc) | dark (#171717, e.g. "AI Assistant") | outline | ghost | primary-soft (blue tint toggle, e.g. "Data Preview") | danger (solid red, destructive) | danger-soft (red tint, e.g. "Remove") | text (no background/border ever — underlines on hover, e.g. "View all", "Cancel") */
7
7
  variant?: "primary" | "dark" | "outline" | "ghost" | "primary-soft" | "danger" | "danger-soft" | "text";
8
- /** sm 30px/12px text (panel actions) | md 32px/13px (default) | lg 35.5px r10 medium (wizard footers) */
8
+ /** Size tier: sm 24px/12px text · md 32px/14px · lg 36px/14px. Omit to inherit the tier of the enclosing region
9
+ * (toolbar, form, table, dialog — or the app root), which is the default and the normal case: controls that share
10
+ * a row must share a tier. Set it only on a control that stands alone. */
9
11
  size?: "sm" | "md" | "lg";
10
12
  /** Icon glyph name */
11
13
  icon?: string;
14
+ /** Overrides the tier's icon size (14 at sm, 16 at md/lg) */
12
15
  iconSize?: number;
13
16
  /** Where `icon` renders relative to the label (default "left") */
14
17
  iconPosition?: "left" | "right";
@@ -1,7 +1,11 @@
1
1
  import React from "react";
2
2
  import { Icon } from "../icons/Icon.jsx";
3
3
 
4
- const btnBase = { display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6, fontFamily: "var(--font-sans)", fontWeight: "var(--weight-regular)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", border: "1px solid transparent", cursor: "pointer", whiteSpace: "nowrap", boxSizing: "border-box", background: "transparent", transition: "var(--t-hover)" };
4
+ /* Every dimension comes from the size tier the button sits in (tokens/layout.css): height, text, icon and padding
5
+ switch together when a region declares data-size="sm" | "md" | "lg". A `size` prop only stamps that attribute on
6
+ the button itself — the default is to inherit, so siblings in one bar can never disagree. */
7
+ const btnBase = { display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6, height: "var(--control-h)", padding: "0 var(--control-px)", borderRadius: "var(--radius-md)", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-regular)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", border: "1px solid transparent", cursor: "pointer", whiteSpace: "nowrap", boxSizing: "border-box", background: "transparent", transition: "var(--t-hover)" };
8
+ const tierIcon = { width: "var(--control-icon)", height: "var(--control-icon)" };
5
9
 
6
10
  const variants = {
7
11
  primary: { background: "var(--action-primary)", color: "var(--on-accent)" },
@@ -16,7 +20,7 @@ const variants = {
16
20
  const hovers = {
17
21
  primary: "var(--action-primary-hover)", dark: "var(--action-dark-hover)",
18
22
  outline: "var(--bg-hover)", ghost: "var(--bg-hover)", "primary-soft": "var(--soft-hover)",
19
- danger: "var(--danger-text)", "danger-soft": "var(--danger-bg)",
23
+ danger: "var(--danger-text)", "danger-soft": "var(--danger-soft-hover)",
20
24
  };
21
25
  /** Press (mouse-down) fills — one step past hover. */
22
26
  const presses = {
@@ -26,20 +30,13 @@ const presses = {
26
30
  };
27
31
  /** Variants that change text color (+ underline) on hover instead of a background wash. */
28
32
  const colorHoverVariants = { text: "var(--text-heading)" };
29
- const sizes = {
30
- sm: { height: 30, padding: "6px 12px", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", letterSpacing: 0, borderRadius: "var(--radius-md)" },
31
- md: { height: 32, padding: "6px 12px", borderRadius: "var(--radius-md)" },
32
- lg: { height: 35.5, padding: "8px 16px", fontWeight: "var(--weight-medium)", borderRadius: "var(--radius-lg)" },
33
- };
34
- const iconOnlyPad = { sm: 0, md: 0, lg: 0 };
35
- const iconOnlyBox = { sm: 30, md: 32, lg: 36 };
36
33
 
37
34
  function Spinner({ size, color }) {
38
- return <span style={{ display: "inline-block", width: size, height: size, borderRadius: "var(--radius-full)", border: "2px solid currentColor", borderTopColor: "transparent", opacity: .9, color, animation: "kb-spin .7s linear infinite", flexShrink: 0 }} />;
35
+ return <span style={{ display: "inline-block", width: size, height: size, ...(size ? null : tierIcon), borderRadius: "var(--radius-full)", border: "2px solid currentColor", borderTopColor: "transparent", opacity: .9, color, animation: "kb-spin .7s linear infinite", flexShrink: 0 }} />;
39
36
  }
40
37
 
41
- /** Keybricks button. variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text; size: sm | md | lg. Interaction states: hover (one step darker) · press (two steps, set via mouse-down) · keyboard focus ring · loading · disabled. */
42
- export function Button({ variant = "primary", size = "md", icon, iconSize, iconPosition = "left", medium, disabled, loading, fullWidth, iconOnly, label, pressed, focused, children, style, onClick, ...rest }) {
38
+ /** Keybricks button. variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text; size: sm 24 | md 32 | lg 36 — omit it to inherit the region's tier (the default). Interaction states: hover (one step darker) · press (two steps, set via mouse-down) · keyboard focus ring · loading · disabled. */
39
+ export function Button({ variant = "primary", size, icon, iconSize, iconPosition = "left", medium, disabled, loading, fullWidth, iconOnly, label, pressed, focused, children, style, onClick, ...rest }) {
43
40
  const [hover, setHover] = React.useState(false);
44
41
  const [press, setPress] = React.useState(false);
45
42
  const [focus, setFocus] = React.useState(false);
@@ -47,10 +44,10 @@ export function Button({ variant = "primary", size = "md", icon, iconSize, iconP
47
44
  const isPressed = press || pressed;
48
45
  const isFocused = focus || focused;
49
46
  const s = {
50
- ...btnBase, ...sizes[size], ...variants[variant],
47
+ ...btnBase, ...variants[variant],
51
48
  ...(medium ? { fontWeight: "var(--weight-medium)" } : null),
52
49
  ...(fullWidth ? { width: "100%" } : null),
53
- ...(iconOnly ? { width: iconOnlyBox[size], padding: iconOnlyPad[size], gap: 0 } : null),
50
+ ...(iconOnly ? { width: "var(--control-h)", padding: 0, gap: 0 } : null),
54
51
  ...(busy ? { opacity: .55, cursor: "default" } : null),
55
52
  ...style,
56
53
  };
@@ -65,11 +62,11 @@ export function Button({ variant = "primary", size = "md", icon, iconSize, iconP
65
62
  }
66
63
  if (isFocused) s.boxShadow = "var(--ring-focus)";
67
64
  }
68
- const iSize = iconSize || (size === "sm" ? 12 : 14);
69
- const leading = loading ? <Spinner size={iSize} color={s.color} /> : (icon && iconPosition === "left" ? <Icon name={icon} size={iSize} /> : null);
70
- const trailing = !loading && icon && iconPosition === "right" ? <Icon name={icon} size={iSize} /> : null;
65
+ const iconStyle = iconSize ? null : tierIcon;
66
+ const leading = loading ? <Spinner size={iconSize} color={s.color} /> : (icon && iconPosition === "left" ? <Icon name={icon} size={iconSize || 16} style={iconStyle} /> : null);
67
+ const trailing = !loading && icon && iconPosition === "right" ? <Icon name={icon} size={iconSize || 16} style={iconStyle} /> : null;
71
68
  return (
72
- <button type="button" data-press="own" style={s} disabled={busy} aria-label={iconOnly ? label : undefined} onClick={onClick}
69
+ <button type="button" data-press="own" data-size={size} style={s} disabled={busy} aria-label={iconOnly ? label : undefined} onClick={onClick}
73
70
  onMouseEnter={() => setHover(true)} onMouseLeave={() => { setHover(false); setPress(false); }}
74
71
  onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)}
75
72
  onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") setPress(true); }} onKeyUp={() => setPress(false)}