@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
@@ -43,7 +43,8 @@ function fmt(val, t, currency) {
43
43
  }
44
44
 
45
45
  /** Excel-style spreadsheet: toolbar, formula bar (name box + fx), lettered/numbered frozen headers, click + shift-click range selection, double-click editing, =SUM()/AVERAGE()-style formulas, sheet tabs and a Sum/Avg/Count status bar. */
46
- export function Spreadsheet({ data = [], rows, cols, colWidths = [], defaultColWidth = 92, currency = "$", defaultActive = "A1", sheets = ["Sheet1"], activeSheet, toolbar = true, formulaBar = true, statusBar = true, height, onChange, style }) {
46
+ /* A size region that defaults to sm: a spreadsheet is the densest surface in the system — 24px toolbar controls, 12px text, 28px cells. */
47
+ export function Spreadsheet({ data = [], rows, cols, colWidths = [], defaultColWidth = 92, currency = "$", defaultActive = "A1", sheets = ["Sheet1"], activeSheet, toolbar = true, formulaBar = true, statusBar = true, height, size = "sm", onChange, style }) {
47
48
  const nR = Math.max(rows || 0, data.length, 8), nC = Math.max(cols || 0, ...data.map(r => r.length), 6);
48
49
  const [cells, setCells] = React.useState(() => Array.from({ length: nR }, (_, r) => Array.from({ length: nC }, (_, c) => norm(data[r] && data[r][c]))));
49
50
  const a0 = refRC(defaultActive) || { r: 0, c: 0 };
@@ -64,17 +65,17 @@ export function Spreadsheet({ data = [], rows, cols, colWidths = [], defaultColW
64
65
  const setType = (label) => { const t = { Automatic: undefined, Number: "number", Currency: "currency", Percent: "percent" }[label]; const next = cells.map(row => row.map(cell => ({ ...cell }))); for (let r = lo.r; r <= hi.r; r++) for (let c = lo.c; c <= hi.c; c++) { if (t) next[r][c].t = t; else delete next[r][c].t; } setCells(next); };
65
66
  const widths = [46]; for (let c = 0; c < nC; c++) widths.push(colWidths[c] || defaultColWidth);
66
67
  const hdr = { position: "sticky", top: 0, zIndex: 3, background: "var(--bg-inset)", borderRight: "1px solid var(--border-control)", borderBottom: "1px solid var(--border-control)", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-xs)", letterSpacing: "var(--tracking-xs)", color: "var(--text-secondary)", height: 26, boxSizing: "border-box", userSelect: "none" };
67
- const tbBtn = (label, k, st) => <button key={label} type="button" onClick={() => setFlag(k)} style={{ width: 24, height: 24, border: "none", borderRadius: "var(--radius-sm)", cursor: "pointer", background: cur[k] ? "var(--bg-active)" : "transparent", color: "var(--text-body)", fontFamily: "var(--font-sans)", fontSize: 12.5, ...st }}>{label}</button>;
68
+ const tbBtn = (label, k, st) => <button key={label} type="button" data-wash={cur[k] ? undefined : ""} onClick={() => setFlag(k)} style={{ width: "var(--control-h)", height: "var(--control-h)", border: "none", borderRadius: "var(--radius-md)", cursor: "pointer", background: cur[k] ? "var(--bg-active)" : "var(--wash, transparent)", color: "var(--text-body)", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", ...st }}>{label}</button>;
68
69
  return (
69
- <div style={{ border: "1px solid var(--border-control)", borderRadius: "var(--radius-lg)", overflow: "hidden", background: "var(--bg-surface)", boxSizing: "border-box", ...style }}>
70
- {toolbar && <div style={{ height: 36, display: "flex", alignItems: "center", gap: 2, padding: "0 8px", borderBottom: "1px solid var(--border-subtle)" }}>
71
- <IconButton icon="undo-2" size="sm" muted label="Undo" /><IconButton icon="redo-2" size="sm" muted label="Redo" />
70
+ <div data-size={size} style={{ border: "1px solid var(--border-control)", borderRadius: "var(--radius-lg)", overflow: "hidden", background: "var(--bg-surface)", boxSizing: "border-box", ...style }}>
71
+ {toolbar && <div style={{ height: "var(--bar-card-h)", display: "flex", alignItems: "center", gap: 2, padding: "0 var(--edge-pad)", boxSizing: "border-box", borderBottom: "1px solid var(--border-subtle)" }}>
72
+ <IconButton icon="undo-2" muted label="Undo" /><IconButton icon="redo-2" muted label="Redo" />
72
73
  <span style={{ width: 1, height: 16, background: "var(--border-control)", margin: "0 6px" }}></span>
73
74
  {tbBtn("B", "b", { fontWeight: 700 })}{tbBtn("I", "i", { fontStyle: "italic" })}{tbBtn("U", "u", { textDecoration: "underline" })}
74
75
  <span style={{ width: 1, height: 16, background: "var(--border-control)", margin: "0 6px" }}></span>
75
- <Select size="sm" options={["Automatic", "Number", "Currency", "Percent"]} value={{ number: "Number", currency: "Currency", percent: "Percent" }[cur.t] || "Automatic"} onChange={setType} style={{ width: 110 }} />
76
+ <Select options={["Automatic", "Number", "Currency", "Percent"]} value={{ number: "Number", currency: "Currency", percent: "Percent" }[cur.t] || "Automatic"} onChange={setType} style={{ width: 110 }} />
76
77
  </div>}
77
- {formulaBar && <div style={{ height: 32, display: "flex", alignItems: "center", borderBottom: "1px solid var(--border-control)" }}>
78
+ {formulaBar && <div style={{ height: "var(--bar-h)", display: "flex", alignItems: "center", borderBottom: "1px solid var(--border-control)" }}>
78
79
  <span style={{ width: 72, alignSelf: "stretch", display: "flex", alignItems: "center", justifyContent: "center", borderRight: "1px solid var(--border-control)", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-heading)" }}>{multi ? colName(lo.c) + (lo.r + 1) + ":" + colName(hi.c) + (hi.r + 1) : colName(active.c) + (active.r + 1)}</span>
79
80
  <span style={{ padding: "0 10px", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", fontStyle: "italic", color: "var(--text-muted)", borderRight: "1px solid var(--border-subtle)", alignSelf: "stretch", display: "flex", alignItems: "center" }}>fx</span>
80
81
  <input value={editing === "fx" ? draft : rawOf(cur)} onFocus={() => { setEditing("fx"); setDraft(rawOf(cur)); }} onChange={(e) => setDraft(e.target.value)} onBlur={() => { if (editing === "fx") { commit(active.r, active.c, draft); setEditing(null); } }} onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") { setEditing(null); e.target.blur(); } }} style={{ flex: 1, border: "none", outline: "none", background: "transparent", padding: "0 10px", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-primary)" }} />
@@ -91,17 +92,17 @@ export function Spreadsheet({ data = [], rows, cols, colWidths = [], defaultColW
91
92
  const isActive = active.r === r && active.c === c;
92
93
  const isEdit = editing && editing !== "fx" && editing.r === r && editing.c === c;
93
94
  return <span key={r + "," + c} onMouseDown={(e) => pick(r, c, e)} onDoubleClick={() => { setEditing({ r, c }); setDraft(rawOf(cell)); }}
94
- style={{ position: "relative", height: 28, boxSizing: "border-box", borderRight: "1px solid var(--border-subtle)", borderBottom: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", justifyContent: cell.align || (isNum ? "flex-end" : "flex-start"), padding: "0 8px", cursor: "cell", background: isActive ? "var(--bg-surface)" : inRange(r, c) ? "var(--soft-bg)" : "var(--bg-surface)", boxShadow: isActive ? "inset 0 0 0 2px var(--action-primary)" : "none", zIndex: isActive ? 1 : 0, fontFamily: isNum ? "var(--font-mono)" : "var(--font-sans)", fontSize: isNum ? 11.5 : "var(--text-sm)", color: v === "#ERROR" ? "var(--danger)" : "var(--text-primary)", fontWeight: cell.b ? 600 : "var(--weight-regular)", fontStyle: cell.i ? "italic" : "normal", textDecoration: cell.u ? "underline" : "none", whiteSpace: "nowrap", overflow: "hidden", userSelect: "none" }}>
95
+ style={{ position: "relative", height: 28, boxSizing: "border-box", borderRight: "1px solid var(--border-subtle)", borderBottom: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", justifyContent: cell.align || (isNum ? "flex-end" : "flex-start"), padding: "0 8px", cursor: "cell", background: isActive ? "var(--bg-surface)" : inRange(r, c) ? "var(--soft-bg)" : "var(--bg-surface)", boxShadow: isActive ? "inset 0 0 0 2px var(--action-primary)" : "none", zIndex: isActive ? 1 : 0, fontFamily: isNum ? "var(--font-mono)" : "var(--font-sans)", fontSize: "var(--control-text)", fontVariantNumeric: "tabular-nums", color: v === "#ERROR" ? "var(--danger)" : "var(--text-primary)", fontWeight: cell.b ? 600 : "var(--weight-regular)", fontStyle: cell.i ? "italic" : "normal", textDecoration: cell.u ? "underline" : "none", whiteSpace: "nowrap", overflow: "hidden", userSelect: "none" }}>
95
96
  {isEdit ? <input autoFocus value={draft} onChange={(e) => setDraft(e.target.value)} onBlur={() => { commit(r, c, draft); setEditing(null); }} onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") setEditing(null); }} style={{ position: "absolute", inset: 0, border: "none", outline: "none", padding: "0 8px", fontFamily: "inherit", fontSize: "inherit", background: "var(--bg-surface)", color: "var(--text-primary)" }} /> : fmt(v, cell.t, currency)}
96
97
  {isActive && !isEdit && <span style={{ position: "absolute", right: -3, bottom: -3, width: 6, height: 6, background: "var(--action-primary)", border: "1px solid var(--bg-surface)", cursor: "crosshair", zIndex: 2 }}></span>}
97
98
  </span>;
98
99
  })])}
99
100
  </div>
100
101
  </div>
101
- {statusBar && <div style={{ height: 32, display: "flex", alignItems: "center", justifyContent: "space-between", borderTop: "1px solid var(--border-control)", background: "var(--bg-inset)", paddingRight: 12 }}>
102
+ {statusBar && <div style={{ height: "var(--bar-h)", display: "flex", alignItems: "center", justifyContent: "space-between", borderTop: "1px solid var(--border-control)", background: "var(--bg-inset)", paddingRight: 12 }}>
102
103
  <div style={{ display: "flex", alignSelf: "stretch", alignItems: "stretch" }}>
103
- {sheets.map(s => <button key={s} type="button" onClick={() => setSheet(s)} style={{ border: "none", borderRight: "1px solid var(--border-subtle)", padding: "0 16px", cursor: "pointer", background: sheet === s ? "var(--bg-surface)" : "transparent", color: sheet === s ? "var(--text-heading)" : "var(--text-secondary)", fontFamily: "var(--font-sans)", fontWeight: sheet === s ? "var(--weight-medium)" : "var(--weight-regular)", fontSize: "var(--text-sm)", boxShadow: sheet === s ? "inset 0 2px 0 var(--action-primary)" : "none" }}>{s}</button>)}
104
- <span style={{ display: "flex", alignItems: "center", padding: "0 6px" }}><IconButton icon="plus" size="xs" muted label="Add sheet" /></span>
104
+ {sheets.map(s => <button key={s} type="button" data-ink={sheet === s ? undefined : ""} onClick={() => setSheet(s)} style={{ border: "none", borderRight: "1px solid var(--border-subtle)", padding: "0 16px", cursor: "pointer", background: sheet === s ? "var(--bg-surface)" : "transparent", color: sheet === s ? "var(--text-heading)" : "var(--ink, var(--text-secondary))", fontFamily: "var(--font-sans)", fontWeight: sheet === s ? "var(--weight-medium)" : "var(--weight-regular)", fontSize: "var(--control-text)", boxShadow: sheet === s ? "inset 0 2px 0 var(--action-primary)" : "none" }}>{s}</button>)}
105
+ <span style={{ display: "flex", alignItems: "center", padding: "0 6px" }}><IconButton icon="plus" muted label="Add sheet" /></span>
105
106
  </div>
106
107
  <span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-secondary)" }}>{stats ? "Sum " + fmt(stats.sum, cur.t, currency) + " Avg " + fmt(stats.avg, cur.t, currency) + " Count " + stats.count : "100%"}</span>
107
108
  </div>}
@@ -4,7 +4,8 @@ import Icon from '../icons/Icon.vue'
4
4
  import IconButton from '../buttons/IconButton.vue'
5
5
  import KSelect from '../forms/Select.vue'
6
6
  // Excel-style spreadsheet: toolbar, formula bar (name box + fx), frozen lettered/numbered headers, click + shift-click range selection, double-click editing, =SUM()/AVERAGE() formulas, sheet tabs, Sum/Avg/Count status bar.
7
- const props = defineProps({ data: { type: Array, default: () => [] }, rows: Number, cols: Number, colWidths: { type: Array, default: () => [] }, defaultColWidth: { type: Number, default: 92 }, currency: { type: String, default: '$' }, defaultActive: { type: String, default: 'A1' }, sheets: { type: Array, default: () => ['Sheet1'] }, toolbar: { type: Boolean, default: true }, formulaBar: { type: Boolean, default: true }, statusBar: { type: Boolean, default: true }, height: [Number, String], style: Object })
7
+ const props = defineProps({ data: { type: Array, default: () => [] }, rows: Number, cols: Number, colWidths: { type: Array, default: () => [] }, defaultColWidth: { type: Number, default: 92 }, currency: { type: String, default: '$' }, defaultActive: { type: String, default: 'A1' }, sheets: { type: Array, default: () => ['Sheet1'] }, toolbar: { type: Boolean, default: true }, formulaBar: { type: Boolean, default: true }, statusBar: { type: Boolean, default: true }, height: [Number, String], size: { type: String, default: 'sm' }, style: Object })
8
+ // A size region that defaults to sm: a spreadsheet is the densest surface in the system — 24px toolbar controls, 12px text, 28px cells.
8
9
  const emit = defineEmits(['change'])
9
10
 
10
11
  const colName = (i) => { let s = ''; i++; while (i > 0) { s = String.fromCharCode(64 + ((i - 1) % 26 + 1)) + s; i = Math.floor((i - 1) / 26) } return s }
@@ -54,17 +55,17 @@ const hdr = { position: 'sticky', top: 0, zIndex: 3, background: 'var(--bg-inset
54
55
  const hl = { background: 'var(--soft-bg)', color: 'var(--action-primary)' }
55
56
  </script>
56
57
  <template>
57
- <div :style="{ border: '1px solid var(--border-control)', borderRadius: 'var(--radius-lg)', overflow: 'hidden', background: 'var(--bg-surface)', boxSizing: 'border-box', ...style }">
58
- <div v-if="toolbar" style="height:36px;display:flex;align-items:center;gap:2px;padding:0 8px;border-bottom:1px solid var(--border-subtle)">
59
- <IconButton icon="undo-2" size="sm" muted label="Undo" /><IconButton icon="redo-2" size="sm" muted label="Redo" />
58
+ <div :data-size="size" :style="{ border: '1px solid var(--border-control)', borderRadius: 'var(--radius-lg)', overflow: 'hidden', background: 'var(--bg-surface)', boxSizing: 'border-box', ...style }">
59
+ <div v-if="toolbar" style="height:var(--bar-card-h);display:flex;align-items:center;gap:2px;padding:0 var(--edge-pad);box-sizing:border-box;border-bottom:1px solid var(--border-subtle)">
60
+ <IconButton icon="undo-2" muted label="Undo" /><IconButton icon="redo-2" muted label="Redo" />
60
61
  <span style="width:1px;height:16px;background:var(--border-control);margin:0 6px" />
61
- <button type="button" @click="setFlag('b')" :style="{ width: '24px', height: '24px', border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: cur.b ? 'var(--bg-active)' : 'transparent', color: 'var(--text-body)', fontWeight: 700 }">B</button>
62
- <button type="button" @click="setFlag('i')" :style="{ width: '24px', height: '24px', border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: cur.i ? 'var(--bg-active)' : 'transparent', color: 'var(--text-body)', fontStyle: 'italic' }">I</button>
63
- <button type="button" @click="setFlag('u')" :style="{ width: '24px', height: '24px', border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: cur.u ? 'var(--bg-active)' : 'transparent', color: 'var(--text-body)', textDecoration: 'underline' }">U</button>
62
+ <button type="button" :data-wash="cur.b ? undefined : ''" @click="setFlag('b')" :style="{ width: 'var(--control-h)', height: 'var(--control-h)', border: 'none', borderRadius: 'var(--radius-md)', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', background: cur.b ? 'var(--bg-active)' : 'var(--wash, transparent)', color: 'var(--text-body)', fontWeight: 700 }">B</button>
63
+ <button type="button" :data-wash="cur.i ? undefined : ''" @click="setFlag('i')" :style="{ width: 'var(--control-h)', height: 'var(--control-h)', border: 'none', borderRadius: 'var(--radius-md)', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', background: cur.i ? 'var(--bg-active)' : 'var(--wash, transparent)', color: 'var(--text-body)', fontStyle: 'italic' }">I</button>
64
+ <button type="button" :data-wash="cur.u ? undefined : ''" @click="setFlag('u')" :style="{ width: 'var(--control-h)', height: 'var(--control-h)', border: 'none', borderRadius: 'var(--radius-md)', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', background: cur.u ? 'var(--bg-active)' : 'var(--wash, transparent)', color: 'var(--text-body)', textDecoration: 'underline' }">U</button>
64
65
  <span style="width:1px;height:16px;background:var(--border-control);margin:0 6px" />
65
- <KSelect size="sm" :options="['Automatic','Number','Currency','Percent']" :model-value="{ number: 'Number', currency: 'Currency', percent: 'Percent' }[cur.t] || 'Automatic'" @update:modelValue="setType" style="width:110px" />
66
+ <KSelect :options="['Automatic','Number','Currency','Percent']" :model-value="{ number: 'Number', currency: 'Currency', percent: 'Percent' }[cur.t] || 'Automatic'" @update:modelValue="setType" style="width:110px" />
66
67
  </div>
67
- <div v-if="formulaBar" style="height:32px;display:flex;align-items:center;border-bottom:1px solid var(--border-control)">
68
+ <div v-if="formulaBar" style="height:var(--bar-h);display:flex;align-items:center;border-bottom:1px solid var(--border-control)">
68
69
  <span style="width:72px;align-self:stretch;display:flex;align-items:center;justify-content:center;border-right:1px solid var(--border-control);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ multi ? colName(lo.c) + (lo.r + 1) + ':' + colName(hi.c) + (hi.r + 1) : colName(active.c) + (active.r + 1) }}</span>
69
70
  <span style="padding:0 10px;font-family:var(--font-mono);font-size:var(--text-xs);font-style:italic;color:var(--text-muted);border-right:1px solid var(--border-subtle);align-self:stretch;display:flex;align-items:center">fx</span>
70
71
  <input :value="editing === 'fx' ? draft : rawOf(cur)" @focus="editing = 'fx'; draft = rawOf(cur)" @input="draft = $event.target.value" @blur="editing === 'fx' && (commit(active.r, active.c, draft), editing = null)" @keydown.enter="$event.target.blur()" @keydown.esc="editing = null; $event.target.blur()" style="flex:1;border:none;outline:none;background:transparent;padding:0 10px;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-primary)" />
@@ -76,7 +77,7 @@ const hl = { background: 'var(--soft-bg)', color: 'var(--action-primary)' }
76
77
  <template v-for="(row, r) in cells" :key="r">
77
78
  <span :style="{ ...hdr, position: 'sticky', left: 0, top: 'auto', zIndex: 2, height: '28px', ...(r >= lo.r && r <= hi.r ? hl : null) }">{{ r + 1 }}</span>
78
79
  <span v-for="(cell, c) in row" :key="r + ',' + c" @mousedown="pick(r, c, $event)" @dblclick="editing = { r, c }; draft = rawOf(cell)"
79
- :style="{ position: 'relative', height: '28px', boxSizing: 'border-box', borderRight: '1px solid var(--border-subtle)', borderBottom: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', justifyContent: cell.align || (typeof evalCell(cells, cell) === 'number' ? 'flex-end' : 'flex-start'), padding: '0 8px', cursor: 'cell', background: inRange(r, c) && !(active.r === r && active.c === c) ? 'var(--soft-bg)' : 'var(--bg-surface)', boxShadow: active.r === r && active.c === c ? 'inset 0 0 0 2px var(--action-primary)' : 'none', fontFamily: typeof evalCell(cells, cell) === 'number' ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: typeof evalCell(cells, cell) === 'number' ? '11.5px' : 'var(--text-sm)', color: evalCell(cells, cell) === '#ERROR' ? 'var(--danger)' : 'var(--text-primary)', fontWeight: cell.b ? 600 : 'var(--weight-regular)', fontStyle: cell.i ? 'italic' : 'normal', textDecoration: cell.u ? 'underline' : 'none', whiteSpace: 'nowrap', overflow: 'hidden', userSelect: 'none' }">
80
+ :style="{ position: 'relative', height: '28px', boxSizing: 'border-box', borderRight: '1px solid var(--border-subtle)', borderBottom: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', justifyContent: cell.align || (typeof evalCell(cells, cell) === 'number' ? 'flex-end' : 'flex-start'), padding: '0 8px', cursor: 'cell', background: inRange(r, c) && !(active.r === r && active.c === c) ? 'var(--soft-bg)' : 'var(--bg-surface)', boxShadow: active.r === r && active.c === c ? 'inset 0 0 0 2px var(--action-primary)' : 'none', fontFamily: typeof evalCell(cells, cell) === 'number' ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: 'var(--control-text)', fontVariantNumeric: 'tabular-nums', color: evalCell(cells, cell) === '#ERROR' ? 'var(--danger)' : 'var(--text-primary)', fontWeight: cell.b ? 600 : 'var(--weight-regular)', fontStyle: cell.i ? 'italic' : 'normal', textDecoration: cell.u ? 'underline' : 'none', whiteSpace: 'nowrap', overflow: 'hidden', userSelect: 'none' }">
80
81
  <input v-if="editing && editing !== 'fx' && editing.r === r && editing.c === c" autofocus :value="draft" @input="draft = $event.target.value" @blur="commit(r, c, draft); editing = null" @keydown.enter="$event.target.blur()" @keydown.esc="editing = null" style="position:absolute;inset:0;border:none;outline:none;padding:0 8px;font:inherit;background:var(--bg-surface);color:var(--text-primary)" />
81
82
  <template v-else>{{ fmt(evalCell(cells, cell), cell.t, currency) }}</template>
82
83
  <span v-if="active.r === r && active.c === c && !editing" style="position:absolute;right:-3px;bottom:-3px;width:6px;height:6px;background:var(--action-primary);border:1px solid var(--bg-surface);cursor:crosshair;z-index:2" />
@@ -84,10 +85,10 @@ const hl = { background: 'var(--soft-bg)', color: 'var(--action-primary)' }
84
85
  </template>
85
86
  </div>
86
87
  </div>
87
- <div v-if="statusBar" style="height:32px;display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--border-control);background:var(--bg-inset);padding-right:12px">
88
+ <div v-if="statusBar" style="height:var(--bar-h);display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--border-control);background:var(--bg-inset);padding-right:12px">
88
89
  <div style="display:flex;align-self:stretch;align-items:stretch">
89
- <button v-for="s in sheets" :key="s" type="button" @click="sheet = s" :style="{ border: 'none', borderRight: '1px solid var(--border-subtle)', padding: '0 16px', cursor: 'pointer', background: sheet === s ? 'var(--bg-surface)' : 'transparent', color: sheet === s ? 'var(--text-heading)' : 'var(--text-secondary)', fontFamily: 'var(--font-sans)', fontWeight: sheet === s ? 'var(--weight-medium)' : 'var(--weight-regular)', fontSize: 'var(--text-sm)', boxShadow: sheet === s ? 'inset 0 2px 0 var(--action-primary)' : 'none' }">{{ s }}</button>
90
- <span style="display:flex;align-items:center;padding:0 6px"><IconButton icon="plus" size="xs" muted label="Add sheet" /></span>
90
+ <button v-for="s in sheets" :key="s" type="button" :data-ink="sheet === s ? undefined : ''" @click="sheet = s" :style="{ border: 'none', borderRight: '1px solid var(--border-subtle)', padding: '0 16px', cursor: 'pointer', background: sheet === s ? 'var(--bg-surface)' : 'transparent', color: sheet === s ? 'var(--text-heading)' : 'var(--ink, var(--text-secondary))', fontFamily: 'var(--font-sans)', fontWeight: sheet === s ? 'var(--weight-medium)' : 'var(--weight-regular)', fontSize: 'var(--control-text)', boxShadow: sheet === s ? 'inset 0 2px 0 var(--action-primary)' : 'none' }">{{ s }}</button>
91
+ <span style="display:flex;align-items:center;padding:0 6px"><IconButton icon="plus" muted label="Add sheet" /></span>
91
92
  </div>
92
93
  <span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-secondary)">{{ stats ? 'Sum ' + fmt(stats.sum, cur.t, currency) + ' Avg ' + fmt(stats.avg, cur.t, currency) + ' Count ' + stats.count : '100%' }}</span>
93
94
  </div>
@@ -0,0 +1,47 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Hairline table — --bg-table-header header tint, one step below --bg-app so it reads on grey pages; one rule weight (--border-subtle) for header rule and row separators alike.
4
+ * The table is a size region: rows are the tier's bar height (sm 32 · md 40 · lg 44), cells and header share the tier's control text (header in weight 600, text-secondary, sentence case), and every control rendered inside a cell takes the same tier.
5
+ * @startingPoint section="Data" subtitle="Comparison / preview tables with mono cells" viewport="700x300"
6
+ */
7
+ export interface DataTableColumn {
8
+ key: string;
9
+ label: string;
10
+ width?: number | string;
11
+ /** Lower bound while resizing (default 48) */
12
+ minWidth?: number;
13
+ /** Menlo cell at the row's text size with tabular figures (config codes, amounts) */
14
+ mono?: boolean;
15
+ align?: "left" | "right" | "center";
16
+ render?: (value: any, row: any, index: number) => React.ReactNode;
17
+ }
18
+ export interface DataTableProps {
19
+ columns?: DataTableColumn[];
20
+ rows?: Record<string, any>[];
21
+ /** Size tier of the table region: sm 32px rows / 12px text (dense grids, preview drawers) · md 40px / 14px (default) · lg 44px / 14px. Omit to inherit the enclosing region. */
22
+ size?: "sm" | "md" | "lg";
23
+ /** Alias of size="sm", kept for 1.x callers */
24
+ dense?: boolean;
25
+ /** Leading checkbox column */
26
+ selectable?: boolean;
27
+ selected?: number[];
28
+ /** onToggle("all"|rowIndex, checked) */
29
+ onToggle?: (which: number | "all", checked: boolean) => void;
30
+ headerBorder?: boolean;
31
+ /** Draws its own rounded card frame (border-card, radius-lg) and drops the header-top and last-row rules that would double it. Use instead of wrapping the table in a bordered div. */
32
+ bordered?: boolean;
33
+ /** For a table placed flush inside a container that already has a border (Card pad={false}): drops only the header's top rule. */
34
+ framed?: boolean;
35
+ /** Short divider between header cells; hover darkens it, drag sets that column's width. Uses table-layout: fixed. */
36
+ resizable?: boolean;
37
+ /** Controlled widths by column key (px). Omit to let the table keep them locally. */
38
+ columnWidths?: Record<string, number>;
39
+ /** Fires on every drag frame with the new px width. Vue: `@column-resize`. */
40
+ onColumnResize?: (key: string, width: number) => void;
41
+ /** Rendered inside the table (full-width cell) when rows is empty — an EmptyState or a muted line. Vue: `#empty` slot. */
42
+ empty?: React.ReactNode;
43
+ style?: React.CSSProperties;
44
+ }
45
+ export declare function Table(props: DataTableProps): JSX.Element;
46
+ /** @deprecated renamed to Table in 1.0.6 — still exported in 2.x, removed in 3.0 */
47
+ export declare const DataTable: typeof Table;
@@ -0,0 +1,85 @@
1
+ import React from "react";
2
+ import { Checkbox } from "../forms/Checkbox.jsx";
3
+
4
+ /**
5
+ * Keybricks table. columns: [{key,label,width,minWidth,mono,align,render}].
6
+ * The table is a size region: `size` ("sm" | "md" | "lg", default inherit → md) sets its rows, header, cell padding and every
7
+ * control inside the cells from the tier tokens — rows are the tier's bar height (sm 32 · md 40 · lg 44), cells read the tier's
8
+ * control text, header cells are the same size in weight 600 / text-secondary, sentence case, no letter-spacing.
9
+ * `dense` is kept as an alias of size="sm" (the data-preview drawer).
10
+ * Every rule in the table is one weight (--border-subtle); the header is set apart by its --bg-table-header tint, never by a heavier line.
11
+ * bordered — the table draws its own rounded card frame (border-card, radius-lg) and drops the edge rules that would double it.
12
+ * framed — the table sits flush inside a container that already has a border (Card pad={false}); drops only the header's top rule.
13
+ * resizable — a short divider between header cells (hover darkens it, cursor col-resize); drag to set that column's width.
14
+ * Widths live in local state unless you pass columnWidths/onColumnResize to control them. Min width = column.minWidth ?? 48.
15
+ */
16
+ export function Table({ columns = [], rows = [], size, dense, selectable, selected = [], onToggle, headerBorder = true, bordered, framed, resizable, columnWidths, onColumnResize, empty, style }) {
17
+ const tier = size || (dense ? "sm" : undefined);
18
+ const flushTop = bordered || framed;
19
+ const colCount = columns.length + (selectable ? 1 : 0);
20
+ const [localW, setLocalW] = React.useState({});
21
+ const [drag, setDrag] = React.useState(null); // key being dragged
22
+ const [hot, setHot] = React.useState(null); // key whose handle is hovered
23
+ const thRefs = React.useRef({});
24
+ const widths = columnWidths || localW;
25
+ const widthOf = (c) => widths[c.key] ?? c.width;
26
+
27
+ const startDrag = (e, c) => {
28
+ e.preventDefault();
29
+ const th = thRefs.current[c.key];
30
+ const startX = e.clientX, startW = th ? th.getBoundingClientRect().width : (parseFloat(widthOf(c)) || 120);
31
+ const min = c.minWidth ?? 48;
32
+ setDrag(c.key);
33
+ const move = (ev) => {
34
+ const w = Math.max(min, Math.round(startW + ev.clientX - startX));
35
+ if (onColumnResize) onColumnResize(c.key, w); else setLocalW((s) => ({ ...s, [c.key]: w }));
36
+ };
37
+ const up = () => { setDrag(null); window.removeEventListener("mousemove", move); window.removeEventListener("mouseup", up); document.body.style.cursor = ""; document.body.style.userSelect = ""; };
38
+ document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none";
39
+ window.addEventListener("mousemove", move); window.addEventListener("mouseup", up);
40
+ };
41
+
42
+ const cellFont = (c) => c.mono
43
+ ? { fontFamily: "var(--font-mono)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: 0, fontVariantNumeric: "tabular-nums", color: "var(--text-heading)" }
44
+ : { fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-primary)" };
45
+ // first and last cells keep the card's edge padding; every other cell the tier's cell padding
46
+ const cellPad = (i) => ({ paddingTop: 0, paddingBottom: 0, paddingLeft: i === 0 && !selectable ? "var(--edge-pad)" : "var(--cell-px)", paddingRight: i === columns.length - 1 ? "var(--edge-pad)" : "var(--cell-px)" });
47
+
48
+ const table = (
49
+ <table data-size={tier} style={{ width: "100%", borderCollapse: "collapse", tableLayout: resizable ? "fixed" : "auto", ...(bordered ? null : style) }}>
50
+ <thead>
51
+ <tr style={{ background: "var(--bg-table-header)", borderTop: headerBorder && !flushTop ? "1px solid var(--border-subtle)" : "none", borderBottom: headerBorder ? "1px solid var(--border-subtle)" : "none", height: "var(--bar-h)" }}>
52
+ {selectable ? <th style={{ width: "calc(var(--edge-pad) + var(--check-size) + var(--cell-px))", padding: "0 0 0 var(--edge-pad)", textAlign: "left" }}><Checkbox checked={rows.length > 0 && selected.length === rows.length} indeterminate={selected.length > 0 && selected.length < rows.length} onChange={(v) => onToggle && onToggle("all", v)} /></th> : null}
53
+ {columns.map((c, i) => {
54
+ const last = i === columns.length - 1;
55
+ const active = drag === c.key, lit = active || hot === c.key;
56
+ return (
57
+ <th key={c.key} ref={(el) => { thRefs.current[c.key] = el; }} style={{ position: "relative", textAlign: c.align || "left", ...cellPad(i), width: widthOf(c), fontFamily: "var(--font-sans)", fontWeight: "var(--weight-semibold)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-secondary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
58
+ {c.label}
59
+ {resizable && !last ? (
60
+ <span aria-hidden="true" onMouseDown={(e) => startDrag(e, c)} onMouseEnter={() => setHot(c.key)} onMouseLeave={() => setHot(null)}
61
+ style={{ position: "absolute", top: 0, right: -5, width: 11, height: "100%", cursor: "col-resize", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1 }}>
62
+ <span style={{ width: active ? 2 : 1, height: "50%", background: lit ? "var(--action-primary)" : "var(--border-control)", borderRadius: 1, transition: "var(--t-hover)" }}></span>
63
+ </span>
64
+ ) : null}
65
+ </th>
66
+ );
67
+ })}
68
+ </tr>
69
+ </thead>
70
+ <tbody>
71
+ {rows.map((r, ri) => (
72
+ <tr key={ri} data-row="" style={{ height: "var(--bar-h)", background: "var(--row-wash, transparent)", transition: "var(--t-hover)", borderBottom: bordered && ri === rows.length - 1 ? "none" : "1px solid var(--border-subtle)" }}>
73
+ {selectable ? <td style={{ padding: "0 0 0 var(--edge-pad)" }}><Checkbox checked={selected.includes(ri)} onChange={(v) => onToggle && onToggle(ri, v)} /></td> : null}
74
+ {columns.map((c, i) => (
75
+ <td key={c.key} style={{ ...cellPad(i), textAlign: c.align || "left", whiteSpace: "nowrap", ...(resizable ? { overflow: "hidden", textOverflow: "ellipsis" } : null), ...cellFont(c) }}>{c.render ? c.render(r[c.key], r, ri) : r[c.key]}</td>
76
+ ))}
77
+ </tr>
78
+ ))}
79
+ {rows.length === 0 && empty ? <tr><td colSpan={colCount} style={{ padding: 0 }}>{empty}</td></tr> : null}
80
+ </tbody>
81
+ </table>
82
+ );
83
+ if (!bordered) return table;
84
+ return <div data-size={tier} style={{ background: "var(--bg-surface)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-lg)", overflow: "hidden", ...style }}>{table}</div>;
85
+ }
@@ -0,0 +1,63 @@
1
+ <script setup>
2
+ import { px } from '../px.js'
3
+ import { ref, computed, reactive } from 'vue'
4
+ import Checkbox from '../forms/Checkbox.vue'
5
+ // Keybricks table. columns: [{key,label,width,minWidth,mono,align,render}].
6
+ // The table is a size region: `size` ("sm" | "md" | "lg", default inherit → md) sets its rows, header, cell padding and every control
7
+ // inside the cells from the tier tokens — rows are the tier's bar height (sm 32 · md 40 · lg 44), cells read the tier's control text,
8
+ // header cells are the same size in weight 600 / text-secondary, sentence case, no letter-spacing. `dense` is an alias of size="sm".
9
+ // Every rule in the table is one weight (--border-subtle); the header is set apart by its --bg-table-header tint, never by a heavier line.
10
+ // bordered — the table draws its own rounded card frame (border-card, radius-lg) and drops the edge rules that would double it.
11
+ // framed — the table sits flush inside a container that already has a border (Card :pad="false"); drops only the header's top rule.
12
+ // resizable — a short divider between header cells (hover darkens it, cursor col-resize); drag to set that column's width.
13
+ // Widths live in local state unless you pass columnWidths + listen to @column-resize(key, px). Min width = column.minWidth ?? 48.
14
+ // #empty slot renders inside the table when rows is empty.
15
+ const props = defineProps({ columns: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, size: String, dense: Boolean, selectable: Boolean, selected: { type: Array, default: () => [] }, headerBorder: { type: Boolean, default: true }, bordered: Boolean, framed: Boolean, resizable: Boolean, columnWidths: Object, style: Object })
16
+ const emit = defineEmits(['toggle', 'columnResize'])
17
+ const tier = computed(() => props.size || (props.dense ? 'sm' : undefined))
18
+ const flushTop = computed(() => props.bordered || props.framed)
19
+ const colCount = computed(() => props.columns.length + (props.selectable ? 1 : 0))
20
+ const frame = computed(() => props.bordered ? { background: 'var(--bg-surface)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', overflow: 'hidden', ...props.style } : { display: 'contents' })
21
+ const localW = reactive({})
22
+ const drag = ref(null), hot = ref(null)
23
+ const thEls = {}
24
+ const widthOf = (c) => px((props.columnWidths || localW)[c.key] ?? c.width)
25
+ function startDrag(e, c) {
26
+ e.preventDefault()
27
+ const th = thEls[c.key]
28
+ const startX = e.clientX, startW = th ? th.getBoundingClientRect().width : (parseFloat(widthOf(c)) || 120), min = c.minWidth ?? 48
29
+ drag.value = c.key
30
+ const move = (ev) => { const w = Math.max(min, Math.round(startW + ev.clientX - startX)); if (props.columnWidths) emit('columnResize', c.key, w); else localW[c.key] = w }
31
+ const up = () => { drag.value = null; window.removeEventListener('mousemove', move); window.removeEventListener('mouseup', up); document.body.style.cursor = ''; document.body.style.userSelect = '' }
32
+ document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'
33
+ window.addEventListener('mousemove', move); window.addEventListener('mouseup', up)
34
+ }
35
+ function cellFont(c) { return c.mono ? { fontFamily: 'var(--font-mono)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 0, fontVariantNumeric: 'tabular-nums', color: 'var(--text-heading)' } : { fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-primary)' } }
36
+ // first and last cells keep the card's edge padding; every other cell the tier's cell padding
37
+ function cellPad(i) { return { paddingTop: 0, paddingBottom: 0, paddingLeft: i === 0 && !props.selectable ? 'var(--edge-pad)' : 'var(--cell-px)', paddingRight: i === props.columns.length - 1 ? 'var(--edge-pad)' : 'var(--cell-px)' } }
38
+ </script>
39
+ <template>
40
+ <div :style="frame" :data-size="bordered ? tier : undefined">
41
+ <table :data-size="tier" :style="{ width: '100%', borderCollapse: 'collapse', tableLayout: resizable ? 'fixed' : 'auto', ...(bordered ? null : style) }">
42
+ <thead>
43
+ <tr :style="{ background: 'var(--bg-table-header)', borderTop: (headerBorder && !flushTop) ? '1px solid var(--border-subtle)' : 'none', borderBottom: headerBorder ? '1px solid var(--border-subtle)' : 'none', height: 'var(--bar-h)' }">
44
+ <th v-if="selectable" style="width:calc(var(--edge-pad) + var(--check-size) + var(--cell-px));padding:0 0 0 var(--edge-pad);text-align:left"><Checkbox :checked="rows.length > 0 && selected.length === rows.length" :indeterminate="selected.length > 0 && selected.length < rows.length" @change="v => emit('toggle', 'all', v)" /></th>
45
+ <th v-for="(c, i) in columns" :key="c.key" :ref="el => { thEls[c.key] = el }" :style="{ position: 'relative', textAlign: c.align || 'left', ...cellPad(i), width: widthOf(c), fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-semibold)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-secondary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }">
46
+ {{ c.label }}
47
+ <span v-if="resizable && i < columns.length - 1" aria-hidden="true" @mousedown="e => startDrag(e, c)" @mouseenter="hot = c.key" @mouseleave="hot = null"
48
+ style="position:absolute;top:0;right:-5px;width:11px;height:100%;cursor:col-resize;display:flex;align-items:center;justify-content:center;z-index:1">
49
+ <span :style="{ width: drag === c.key ? '2px' : '1px', height: '50%', background: (drag === c.key || hot === c.key) ? 'var(--action-primary)' : 'var(--border-control)', borderRadius: '1px', transition: 'var(--t-hover)' }"></span>
50
+ </span>
51
+ </th>
52
+ </tr>
53
+ </thead>
54
+ <tbody>
55
+ <tr v-for="(r, ri) in rows" :key="ri" data-row="" :style="{ height: 'var(--bar-h)', background: 'var(--row-wash, transparent)', transition: 'var(--t-hover)', borderBottom: bordered && ri === rows.length - 1 ? 'none' : '1px solid var(--border-subtle)' }">
56
+ <td v-if="selectable" style="padding:0 0 0 var(--edge-pad)"><Checkbox :checked="selected.includes(ri)" @change="v => emit('toggle', ri, v)" /></td>
57
+ <td v-for="(c, i) in columns" :key="c.key" :style="{ ...cellPad(i), textAlign: c.align || 'left', whiteSpace: 'nowrap', ...(resizable ? { overflow: 'hidden', textOverflow: 'ellipsis' } : null), ...cellFont(c) }">{{ c.render ? c.render(r[c.key], r, ri) : r[c.key] }}</td>
58
+ </tr>
59
+ <tr v-if="rows.length === 0 && $slots.empty"><td :colspan="colCount" style="padding:0"><slot name="empty" /></td></tr>
60
+ </tbody>
61
+ </table>
62
+ </div>
63
+ </template>
@@ -1,7 +1,7 @@
1
- Data lists: `DataTable` (hairline table, mono cells, selectable, dense preview mode) and `ListRow` (expandable workflow rows in a `Card pad={false}`).
1
+ Data lists: `Table` (hairline table, mono cells, selectable; a size region — `size="sm"` for preview grids) and `ListRow` (expandable workflow rows in a `Card pad={false}`).
2
2
 
3
3
  ```jsx
4
- <DataTable selectable columns={[
4
+ <Table selectable columns={[
5
5
  {key:"module",label:"Module"},
6
6
  {key:"code",label:"Code",mono:true},
7
7
  {key:"cur",label:"Current Value"},
@@ -10,7 +10,7 @@ Data lists: `DataTable` (hairline table, mono cells, selectable, dense preview m
10
10
  <ListRow icon="chart-line" title="Business Analysis" meta="1 document" trailing="1 item"/>
11
11
  ```
12
12
 
13
- dense mode = 29px rows/11px headers for the pipeline data-preview drawer.
13
+ `size="sm"` (the 1.x `dense`) = 32px rows / 12px text for the pipeline data-preview drawer; md = 40px rows / 14px text; lg = 44 / 14. Header cells are the same size as body cells in weight 600, sentence case, no letter-spacing; mono cells keep the row's size with tabular figures.
14
14
 
15
15
  `Pagination` (addition) sits under a table:
16
16
 
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import { px } from '../px.js'
2
3
  import { computed } from 'vue'
3
4
  const palette = ['var(--cat-join)', 'var(--cat-source)', 'var(--cat-transform)', 'var(--cat-merge)', 'var(--cat-output)']
4
5
  function initials(name = '') {
@@ -12,6 +13,6 @@ const props = defineProps({ name: { type: String, default: '' }, src: String, si
12
13
  const hue = computed(() => palette[[...props.name].reduce((a, c) => a + c.charCodeAt(0), 0) % palette.length])
13
14
  </script>
14
15
  <template>
15
- <img v-if="src" :src="src" :alt="name" :style="{ width: size, height: size, borderRadius: 'var(--radius-full)', objectFit: 'cover', flexShrink: 0, ...style }" />
16
- <span v-else :title="name" :style="{ width: size, height: size, borderRadius: 'var(--radius-full)', background: 'var(--bg-hover)', color: hue, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: Math.max(9, Math.round(size * .4)), letterSpacing: 0, flexShrink: 0, boxSizing: 'border-box', border: '1px solid var(--border-card)', ...style }">{{ initials(name) }}</span>
16
+ <img v-if="src" :src="src" :alt="name" :style="{ width: px(size), height: px(size), borderRadius: 'var(--radius-full)', objectFit: 'cover', flexShrink: 0, ...style }" />
17
+ <span v-else :title="name" :style="{ width: px(size), height: px(size), borderRadius: 'var(--radius-full)', background: 'var(--bg-hover)', color: hue, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: px(Math.max(9, Math.round(size * .4))), letterSpacing: 0, flexShrink: 0, boxSizing: 'border-box', border: '1px solid var(--border-card)', ...style }">{{ initials(name) }}</span>
17
18
  </template>
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import { px } from '../px.js'
2
3
  import { computed } from 'vue'
3
4
  import Avatar from './Avatar.vue'
4
5
  const props = defineProps({ names: { type: Array, default: () => [] }, size: { type: Number, default: 24 }, max: { type: Number, default: 4 }, style: Object })
@@ -7,7 +8,7 @@ const rest = computed(() => props.names.length - shown.value.length)
7
8
  </script>
8
9
  <template>
9
10
  <div :style="{ display: 'flex', alignItems: 'center', ...style }">
10
- <Avatar v-for="(n, i) in shown" :key="n + i" :name="n" :size="size" :style="{ marginLeft: i ? -8 : 0, boxShadow: 'var(--ring-surface)' }" />
11
- <span v-if="rest > 0" :style="{ marginLeft: -8, width: size, height: size, borderRadius: 'var(--radius-full)', background: 'var(--bg-active)', color: 'var(--text-secondary)', boxShadow: 'var(--ring-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontSize: Math.max(9, Math.round(size * .38)) }">+{{ rest }}</span>
11
+ <Avatar v-for="(n, i) in shown" :key="n + i" :name="n" :size="size" :style="{ marginLeft: i ? '-8px' : 0, boxShadow: 'var(--ring-surface)' }" />
12
+ <span v-if="rest > 0" :style="{ marginLeft: '-8px', width: px(size), height: px(size), borderRadius: 'var(--radius-full)', background: 'var(--bg-active)', color: 'var(--text-secondary)', boxShadow: 'var(--ring-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontSize: px(Math.max(9, Math.round(size * .38))) }">+{{ rest }}</span>
12
13
  </div>
13
14
  </template>
@@ -9,5 +9,5 @@ const tones = {
9
9
  defineProps({ tone: { type: String, default: 'neutral' }, style: Object })
10
10
  </script>
11
11
  <template>
12
- <span :style="{ display: 'inline-flex', alignItems: 'center', height: 20, padding: '0 8px', borderRadius: 'var(--radius-full)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-xs)', lineHeight: 1, letterSpacing: 'var(--tracking-xs)', ...tones[tone], ...style }"><slot /></span>
12
+ <span :style="{ display: 'inline-flex', alignItems: 'center', height: '20px', padding: '0 8px', borderRadius: 'var(--radius-full)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-xs)', lineHeight: 1, letterSpacing: 'var(--tracking-xs)', ...tones[tone], ...style }"><slot /></span>
13
13
  </template>
@@ -3,9 +3,9 @@
3
3
  defineProps({ label: String, vertical: Boolean, style: Object })
4
4
  </script>
5
5
  <template>
6
- <span v-if="vertical" :style="{ width: 1, alignSelf: 'stretch', background: 'var(--border-subtle)', ...style }" />
7
- <div v-else-if="!label" :style="{ height: 1, background: 'var(--border-subtle)', ...style }" />
8
- <div v-else :style="{ display: 'flex', alignItems: 'center', gap: 12, ...style }">
6
+ <span v-if="vertical" :style="{ width: '1px', alignSelf: 'stretch', background: 'var(--border-subtle)', ...style }" />
7
+ <div v-else-if="!label" :style="{ height: '1px', background: 'var(--border-subtle)', ...style }" />
8
+ <div v-else :style="{ display: 'flex', alignItems: 'center', gap: '12px', ...style }">
9
9
  <div style="flex:1;height:1px;background:var(--border-subtle)" />
10
10
  <span style="font-family:var(--font-sans);font-size:var(--text-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted);white-space:nowrap">{{ label }}</span>
11
11
  <div style="flex:1;height:1px;background:var(--border-subtle)" />
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import { px } from '../px.js'
2
3
  import { computed } from 'vue'
3
4
  const props = defineProps({ size: { type: Number, default: 128 }, thickness: { type: Number, default: 7.68 }, segments: Array, value: { type: Number, default: 75 }, label: [String, Number], style: Object })
4
5
  const R = computed(() => props.size / 2 - props.thickness / 2 - props.size * .02)
@@ -7,11 +8,11 @@ const segs = computed(() => props.segments || [{ value: props.value, color: 'var
7
8
  const arcs = computed(() => { let offset = 0; return segs.value.map(s => { const len = (s.value / 100) * C.value; const a = { ...s, len, offset }; offset += len; return a }) })
8
9
  </script>
9
10
  <template>
10
- <div :style="{ position: 'relative', width: size, height: size, ...style }">
11
+ <div :style="{ position: 'relative', width: px(size), height: px(size), ...style }">
11
12
  <svg :width="size" :height="size" style="transform:rotate(-90deg)">
12
13
  <circle :cx="size / 2" :cy="size / 2" :r="R" fill="none" stroke="var(--track)" :stroke-width="thickness" />
13
14
  <circle v-for="(s, i) in arcs" :key="i" :cx="size / 2" :cy="size / 2" :r="R" fill="none" :stroke="s.color" :stroke-width="thickness" :stroke-dasharray="`${s.len} ${C - s.len}`" :stroke-dashoffset="-s.offset" stroke-linecap="butt" />
14
15
  </svg>
15
- <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:var(--font-sans);font-weight:var(--weight-light);font-size:20px;letter-spacing:var(--tracking-xl);color:var(--text-primary)">{{ label != null ? label : `${value}%` }}</div>
16
+ <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:var(--font-sans);font-weight:var(--weight-light);font-size:var(--text-lg);letter-spacing:var(--tracking-lg);color:var(--text-primary)">{{ label != null ? label : `${value}%` }}</div>
16
17
  </div>
17
18
  </template>
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import { px } from '../px.js'
2
3
  import { computed } from 'vue'
3
4
  import Icon from '../icons/Icon.vue'
4
5
  const dims = { palette: { box: 40, r: 'var(--radius-tile)', icon: 18 }, node: { box: 48, r: 'var(--radius-node)', icon: 22 }, row: { box: 32, r: 'var(--radius-md)', icon: 16 } }
@@ -7,7 +8,7 @@ const t = computed(() => dims[props.kind])
7
8
  const tinted = computed(() => props.kind !== 'row')
8
9
  </script>
9
10
  <template>
10
- <span :style="{ width: t.box, height: t.box, borderRadius: t.r, background: tinted ? `var(--cat-${hue}-bg)` : 'var(--bg-surface)', border: tinted ? 'none' : '1px solid var(--border-control)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: tinted ? `var(--cat-${hue})` : 'var(--text-body)', flexShrink: 0, boxSizing: 'border-box', ...style }">
11
+ <span :style="{ width: px(t.box), height: px(t.box), borderRadius: px(t.r), background: tinted ? `var(--cat-${hue}-bg)` : 'var(--bg-surface)', border: tinted ? 'none' : '1px solid var(--border-control)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: tinted ? `var(--cat-${hue})` : 'var(--text-body)', flexShrink: 0, boxSizing: 'border-box', ...style }">
11
12
  <Icon :name="icon" :size="t.icon" :stroke-width="2" />
12
13
  </span>
13
14
  </template>
@@ -3,7 +3,7 @@
3
3
  defineProps({ keys: { type: Array, default: () => [] }, style: Object })
4
4
  </script>
5
5
  <template>
6
- <span :style="{ display: 'inline-flex', gap: 3, ...style }">
6
+ <span :style="{ display: 'inline-flex', gap: '3px', ...style }">
7
7
  <span v-for="(k, i) in keys" :key="i" style="display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 5px;border-radius:var(--radius-sm);background:var(--bg-inset);border:1px solid var(--border-control);box-sizing:border-box;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-secondary)">{{ k }}</span>
8
8
  </span>
9
9
  </template>
@@ -35,7 +35,7 @@ export function Donut({ size = 128, thickness = 7.68, segments, value = 75, labe
35
35
  return el;
36
36
  })}
37
37
  </svg>
38
- <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-light)", fontSize: 20, letterSpacing: "var(--tracking-xl)", color: "var(--text-primary)" }}>{label != null ? label : `${value}%`}</div>
38
+ <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-light)", fontSize: "var(--text-lg)", letterSpacing: "var(--tracking-lg)", color: "var(--text-primary)" }}>{label != null ? label : `${value}%`}</div>
39
39
  </div>
40
40
  );
41
41
  }
@@ -3,7 +3,7 @@
3
3
  defineProps({ label: String, value: { type: Number, default: 50 }, color: { type: String, default: 'var(--chart-blue)' }, showValue: { type: Boolean, default: true }, style: Object })
4
4
  </script>
5
5
  <template>
6
- <div :style="{ display: 'flex', flexDirection: 'column', gap: 4, ...style }">
6
+ <div :style="{ display: 'flex', flexDirection: 'column', gap: '4px', ...style }">
7
7
  <div v-if="label != null" style="display:flex;justify-content:space-between;font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base)">
8
8
  <span style="color:var(--text-body)">{{ label }}</span>
9
9
  <span v-if="showValue" style="color:var(--text-heading)">{{ value }}%</span>
@@ -3,7 +3,7 @@ import Card from './Card.vue'
3
3
  defineProps({ label: String, value: [String, Number], dense: Boolean, style: Object })
4
4
  </script>
5
5
  <template>
6
- <Card :dense="dense" :style="{ display: 'flex', flexDirection: 'column', gap: 12, ...style }">
6
+ <Card :dense="dense" :style="{ display: 'flex', flexDirection: 'column', gap: '12px', ...style }">
7
7
  <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-secondary)">{{ label }}</span>
8
8
  <slot />
9
9
  <span v-if="value != null" style="font-family:var(--font-sans);font-weight:var(--weight-light);font-size:var(--text-num);line-height:var(--leading-num);letter-spacing:var(--tracking-num);color:var(--text-primary)">{{ value }}</span>
@@ -1,7 +1,9 @@
1
1
  import * as React from "react";
2
- /** Small removable filter tag (22px, full radius). Addition — distinct from Badge/ActionChip. */
2
+ /** Small removable filter tag (full radius; sm 20 · md 24 · lg 28 tall, 12px text). Addition — distinct from Badge/ActionChip. */
3
3
  export interface TagProps {
4
4
  children?: React.ReactNode;
5
+ /** Size tier. Omit to inherit the enclosing region's tier (the default). */
6
+ size?: "sm" | "md" | "lg";
5
7
  onRemove?: () => void;
6
8
  tone?: "neutral" | "info";
7
9
  style?: React.CSSProperties;
@@ -3,13 +3,13 @@ import { Icon } from "../icons/Icon.jsx";
3
3
 
4
4
  const tones = { neutral: { bg: "var(--bg-hover)", fg: "var(--text-body)" }, info: { bg: "var(--info-bg)", fg: "var(--info-fg)" } };
5
5
 
6
- /** Small removable filter tag — distinct from Badge (status) and ActionChip (bulk action). */
7
- export function Tag({ children, onRemove, tone = "neutral", style }) {
6
+ /** Small removable filter tag — distinct from Badge (status) and ActionChip (bulk action). Height follows the region's tier (sm 20 · md 24 · lg 28, --tag-h); text stays 12px. */
7
+ export function Tag({ children, onRemove, tone = "neutral", size, style }) {
8
8
  const t = tones[tone] || tones.neutral;
9
9
  return (
10
- <span style={{ display: "inline-flex", alignItems: "center", gap: 4, height: 22, padding: onRemove ? "0 6px 0 10px" : "0 10px", borderRadius: "var(--radius-full)", background: t.bg, color: t.fg, fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", fontWeight: "var(--weight-medium)", letterSpacing: "var(--tracking-xs)", ...style }}>
10
+ <span data-size={size} style={{ display: "inline-flex", alignItems: "center", gap: 4, height: "var(--tag-h)", padding: onRemove ? "0 6px 0 10px" : "0 10px", borderRadius: "var(--radius-full)", background: t.bg, color: t.fg, fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", fontWeight: "var(--weight-medium)", letterSpacing: "var(--tracking-xs)", whiteSpace: "nowrap", ...style }}>
11
11
  {children}
12
- {onRemove ? <button type="button" onClick={onRemove} style={{ border: "none", background: "transparent", cursor: "pointer", color: "inherit", opacity: .7, display: "flex", padding: 0 }}><Icon name="x" size={11} /></button> : null}
12
+ {onRemove ? <button type="button" onClick={onRemove} aria-label="Remove" data-ink="" style={{ border: "none", background: "transparent", cursor: "pointer", color: "inherit", opacity: "var(--ink-op, .7)", display: "flex", padding: 0 }}><Icon name="x" size={12} /></button> : null}
13
13
  </span>
14
14
  );
15
15
  }
@@ -2,14 +2,14 @@
2
2
  import { computed } from 'vue'
3
3
  import Icon from '../icons/Icon.vue'
4
4
  const tones = { neutral: { bg: 'var(--bg-hover)', fg: 'var(--text-body)' }, info: { bg: 'var(--info-bg)', fg: 'var(--info-fg)' } }
5
- // Small removable filter tag — distinct from Badge (status) and ActionChip (bulk action).
6
- const props = defineProps({ tone: { type: String, default: 'neutral' }, removable: Boolean, style: Object })
5
+ // Small removable filter tag — distinct from Badge (status) and ActionChip (bulk action). Height follows the region's tier (sm 20 · md 24 · lg 28, --tag-h); text stays 12px.
6
+ const props = defineProps({ tone: { type: String, default: 'neutral' }, removable: Boolean, size: String, style: Object })
7
7
  defineEmits(['remove'])
8
8
  const t = computed(() => tones[props.tone] || tones.neutral)
9
9
  </script>
10
10
  <template>
11
- <span :style="{ display: 'inline-flex', alignItems: 'center', gap: 4, height: 22, padding: removable ? '0 6px 0 10px' : '0 10px', borderRadius: 'var(--radius-full)', background: t.bg, color: t.fg, fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', fontWeight: 'var(--weight-medium)', letterSpacing: 'var(--tracking-xs)', ...style }">
11
+ <span :data-size="size" :style="{ display: 'inline-flex', alignItems: 'center', gap: '4px', height: 'var(--tag-h)', padding: removable ? '0 6px 0 10px' : '0 10px', borderRadius: 'var(--radius-full)', background: t.bg, color: t.fg, fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', fontWeight: 'var(--weight-medium)', letterSpacing: 'var(--tracking-xs)', whiteSpace: 'nowrap', ...style }">
12
12
  <slot />
13
- <button v-if="removable" type="button" @click="$emit('remove')" style="border:none;background:transparent;cursor:pointer;color:inherit;opacity:.7;display:flex;padding:0"><Icon name="x" :size="11" /></button>
13
+ <button v-if="removable" type="button" aria-label="Remove" data-ink="" @click="$emit('remove')" style="border:none;background:transparent;cursor:pointer;color:inherit;opacity:var(--ink-op, .7);display:flex;padding:0"><Icon name="x" :size="12" /></button>
14
14
  </span>
15
15
  </template>