@keyfunc/keybricks-ds 1.0.4 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (381) hide show
  1. package/DEPLOY.md +14 -5
  2. package/DESIGN.md +245 -59
  3. package/INTEGRATION.md +243 -49
  4. package/SKILL.md +13 -11
  5. package/_adherence.oxlintrc.json +183 -70
  6. package/_scripts/build-index.mjs +24 -4
  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 +11 -9
  12. package/components/_cards/activity.card.html +3 -3
  13. package/components/_cards/alert.card.html +3 -3
  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 +5 -5
  17. package/components/_cards/button-1-base.card.html +23 -26
  18. package/components/_cards/button-2-states.card.html +27 -25
  19. package/components/_cards/button-3-sizes-icons.card.html +20 -19
  20. package/components/_cards/button-4-group.card.html +14 -31
  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 +5 -5
  24. package/components/_cards/charts-10-gauge.card.html +5 -5
  25. package/components/_cards/charts-11-funnel.card.html +3 -3
  26. package/components/_cards/charts-12-treemap.card.html +3 -3
  27. package/components/_cards/charts-14-radar.card.html +3 -3
  28. package/components/_cards/charts-15-distribution.card.html +2 -2
  29. package/components/_cards/charts-16-pareto.card.html +3 -3
  30. package/components/_cards/charts-17-candlestick.card.html +3 -3
  31. package/components/_cards/charts-18-sankey.card.html +3 -3
  32. package/components/_cards/charts-19-chord.card.html +3 -3
  33. package/components/_cards/charts-2-line.card.html +4 -4
  34. package/components/_cards/charts-20-sunburst.card.html +3 -3
  35. package/components/_cards/charts-21-worldmap.card.html +4 -4
  36. package/components/_cards/charts-22-gantt.card.html +3 -3
  37. package/components/_cards/charts-23-bar3d.card.html +3 -3
  38. package/components/_cards/charts-24-pie3d.card.html +3 -3
  39. package/components/_cards/charts-25-surface3d.card.html +3 -3
  40. package/components/_cards/charts-26-scatter3d.card.html +3 -3
  41. package/components/_cards/charts-27-ribbon3d.card.html +3 -3
  42. package/components/_cards/charts-3-bar.card.html +7 -7
  43. package/components/_cards/charts-4-barlist.card.html +3 -3
  44. package/components/_cards/charts-5-donut.card.html +4 -4
  45. package/components/_cards/charts-6-scatter.card.html +3 -3
  46. package/components/_cards/charts-7-heatmap.card.html +3 -3
  47. package/components/_cards/charts-8-waterfall.card.html +3 -3
  48. package/components/_cards/charts-9-bullet.card.html +3 -3
  49. package/components/_cards/charts-99-fullpage.card.html +5 -5
  50. package/components/_cards/checkbox.card.html +9 -6
  51. package/components/_cards/codeblock.card.html +2 -2
  52. package/components/_cards/combobox.card.html +6 -3
  53. package/components/_cards/context-menu.card.html +6 -3
  54. package/components/_cards/date-picker.card.html +10 -4
  55. package/components/_cards/diffvalue.card.html +3 -3
  56. package/components/_cards/divider.card.html +4 -4
  57. package/components/_cards/drawer.card.html +4 -4
  58. package/components/_cards/dropdown-menu.card.html +7 -7
  59. package/components/_cards/dropzone.card.html +5 -3
  60. package/components/_cards/empty-state.card.html +6 -6
  61. package/components/_cards/filter-bar.card.html +5 -5
  62. package/components/_cards/icon-button.card.html +14 -14
  63. package/components/_cards/icons-1-core.card.html +4 -4
  64. package/components/_cards/icons-2-feather.card.html +4 -4
  65. package/components/_cards/icons-3-unicons.card.html +4 -4
  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 +3 -3
  71. package/components/_cards/log-viewer.card.html +3 -3
  72. package/components/_cards/modal.card.html +9 -9
  73. package/components/_cards/multi-select.card.html +6 -3
  74. package/components/_cards/number-stepper.card.html +9 -5
  75. package/components/_cards/pagination.card.html +6 -3
  76. package/components/_cards/people.card.html +4 -4
  77. package/components/_cards/pipeline-node.card.html +8 -8
  78. package/components/_cards/pipeline-palette.card.html +5 -5
  79. package/components/_cards/radio.card.html +12 -7
  80. package/components/_cards/search.card.html +6 -3
  81. package/components/_cards/segmented-control.card.html +11 -10
  82. package/components/_cards/select.card.html +8 -4
  83. package/components/_cards/slider.card.html +2 -2
  84. package/components/_cards/smart-table.card.html +3 -3
  85. package/components/_cards/spreadsheet.card.html +3 -3
  86. package/components/_cards/stepper.card.html +3 -3
  87. package/components/_cards/table.card.html +165 -0
  88. package/components/_cards/tabs.card.html +6 -3
  89. package/components/_cards/tag.card.html +6 -3
  90. package/components/_cards/textarea.card.html +2 -2
  91. package/components/_cards/timeline.card.html +3 -3
  92. package/components/_cards/toast.card.html +10 -8
  93. package/components/_cards/toggles.card.html +15 -10
  94. package/components/_cards/tooltip.card.html +7 -7
  95. package/components/_cards/tree.card.html +3 -3
  96. package/components/_shared/card-kit.css +25 -0
  97. package/components/_shared/card-kit.js +50 -0
  98. package/components/_shared/src-buttons.js +1 -1
  99. package/components/_shared/src-charts.js +1 -1
  100. package/components/_shared/src-data.js +1 -1
  101. package/components/_shared/src-display.js +1 -1
  102. package/components/_shared/src-feedback.js +1 -1
  103. package/components/_shared/src-forms.js +1 -1
  104. package/components/_shared/src-icons.js +1 -1
  105. package/components/_shared/src-navigation.js +1 -1
  106. package/components/_shared/src-pipeline.js +1 -1
  107. package/components/_shared/src-tabs.js +1 -1
  108. package/components/_shared/view-source.js +3 -3
  109. package/components/_vue/index.mjs +16 -5
  110. package/components/buttons/ActionChip.d.ts +3 -1
  111. package/components/buttons/ActionChip.jsx +4 -4
  112. package/components/buttons/ActionChip.vue +4 -4
  113. package/components/buttons/Button.d.ts +5 -2
  114. package/components/buttons/Button.jsx +15 -18
  115. package/components/buttons/Button.vue +17 -18
  116. package/components/buttons/ButtonGroup.vue +1 -1
  117. package/components/buttons/IconButton.d.ts +2 -1
  118. package/components/buttons/IconButton.jsx +13 -9
  119. package/components/buttons/IconButton.vue +12 -9
  120. package/components/buttons/buttons.prompt.md +6 -5
  121. package/components/charts/Bar3D.jsx +2 -2
  122. package/components/charts/Bar3D.vue +3 -3
  123. package/components/charts/BarChart.jsx +4 -4
  124. package/components/charts/BarChart.vue +6 -6
  125. package/components/charts/BarList.jsx +4 -4
  126. package/components/charts/BarList.vue +4 -4
  127. package/components/charts/BoxPlot.jsx +1 -1
  128. package/components/charts/BoxPlot.vue +3 -3
  129. package/components/charts/BulletChart.jsx +6 -6
  130. package/components/charts/BulletChart.vue +6 -6
  131. package/components/charts/CandlestickChart.jsx +2 -2
  132. package/components/charts/CandlestickChart.vue +5 -5
  133. package/components/charts/ChartKit.jsx +8 -7
  134. package/components/charts/ChartLegend.vue +16 -0
  135. package/components/charts/ChartTooltip.vue +16 -0
  136. package/components/charts/ChordChart.jsx +2 -2
  137. package/components/charts/ChordChart.vue +3 -3
  138. package/components/charts/DonutChart.jsx +1 -1
  139. package/components/charts/DonutChart.vue +2 -2
  140. package/components/charts/FunnelChart.jsx +3 -3
  141. package/components/charts/FunnelChart.vue +4 -4
  142. package/components/charts/GanttChart.jsx +2 -2
  143. package/components/charts/GanttChart.vue +3 -3
  144. package/components/charts/GaugeChart.jsx +2 -2
  145. package/components/charts/GaugeChart.vue +2 -2
  146. package/components/charts/HeatmapChart.jsx +2 -2
  147. package/components/charts/HeatmapChart.vue +2 -2
  148. package/components/charts/Histogram.jsx +2 -2
  149. package/components/charts/Histogram.vue +4 -4
  150. package/components/charts/LineChart.jsx +1 -1
  151. package/components/charts/LineChart.vue +4 -4
  152. package/components/charts/ParetoChart.jsx +3 -3
  153. package/components/charts/ParetoChart.vue +5 -5
  154. package/components/charts/Pie3D.jsx +1 -1
  155. package/components/charts/Pie3D.vue +2 -2
  156. package/components/charts/PlotGrid.vue +18 -0
  157. package/components/charts/RadarChart.jsx +1 -1
  158. package/components/charts/RadarChart.vue +2 -2
  159. package/components/charts/Ribbon3D.jsx +2 -2
  160. package/components/charts/Ribbon3D.vue +3 -3
  161. package/components/charts/SankeyChart.jsx +2 -2
  162. package/components/charts/SankeyChart.vue +3 -3
  163. package/components/charts/Scatter3D.jsx +1 -1
  164. package/components/charts/Scatter3D.vue +2 -2
  165. package/components/charts/ScatterChart.jsx +3 -3
  166. package/components/charts/ScatterChart.vue +5 -5
  167. package/components/charts/Sparkline.jsx +1 -1
  168. package/components/charts/Sparkline.vue +1 -1
  169. package/components/charts/SunburstChart.jsx +1 -1
  170. package/components/charts/SunburstChart.vue +2 -2
  171. package/components/charts/Surface3D.jsx +2 -2
  172. package/components/charts/Surface3D.vue +3 -3
  173. package/components/charts/TreemapChart.jsx +2 -2
  174. package/components/charts/TreemapChart.vue +3 -3
  175. package/components/charts/WaterfallChart.jsx +2 -2
  176. package/components/charts/WaterfallChart.vue +6 -5
  177. package/components/charts/WorldMap.jsx +4 -4
  178. package/components/charts/WorldMap.vue +5 -5
  179. package/components/charts/XLabels.vue +11 -0
  180. package/components/data/CodeBlock.jsx +1 -1
  181. package/components/data/CodeBlock.vue +1 -1
  182. package/components/data/DiffValue.d.ts +1 -0
  183. package/components/data/DiffValue.jsx +1 -1
  184. package/components/data/DiffValue.vue +1 -1
  185. package/components/data/FilterBar.d.ts +2 -0
  186. package/components/data/FilterBar.jsx +10 -9
  187. package/components/data/FilterBar.vue +9 -8
  188. package/components/data/KeyValue.d.ts +1 -1
  189. package/components/data/KeyValue.jsx +4 -4
  190. package/components/data/KeyValue.vue +4 -4
  191. package/components/data/ListRow.jsx +2 -2
  192. package/components/data/ListRow.vue +2 -2
  193. package/components/data/Pagination.d.ts +5 -1
  194. package/components/data/Pagination.jsx +16 -12
  195. package/components/data/Pagination.vue +14 -9
  196. package/components/data/SmartTable.d.ts +4 -1
  197. package/components/data/SmartTable.jsx +16 -14
  198. package/components/data/SmartTable.vue +26 -23
  199. package/components/data/Spreadsheet.d.ts +2 -0
  200. package/components/data/Spreadsheet.jsx +16 -15
  201. package/components/data/Spreadsheet.vue +19 -18
  202. package/components/data/Table.d.ts +101 -0
  203. package/components/data/Table.jsx +223 -0
  204. package/components/data/Table.vue +153 -0
  205. package/components/data/data.prompt.md +3 -3
  206. package/components/display/Avatar.jsx +2 -2
  207. package/components/display/Avatar.vue +3 -2
  208. package/components/display/AvatarGroup.vue +3 -2
  209. package/components/display/Badge.d.ts +4 -1
  210. package/components/display/Badge.jsx +10 -5
  211. package/components/display/Badge.vue +1 -1
  212. package/components/display/Divider.vue +3 -3
  213. package/components/display/Donut.vue +3 -2
  214. package/components/display/IconTile.vue +10 -4
  215. package/components/display/Kbd.d.ts +1 -1
  216. package/components/display/Kbd.jsx +1 -1
  217. package/components/display/Kbd.vue +2 -2
  218. package/components/display/ProgressBar.jsx +1 -1
  219. package/components/display/ProgressBar.vue +1 -1
  220. package/components/display/StatCard.vue +1 -1
  221. package/components/display/Tag.d.ts +3 -1
  222. package/components/display/Tag.jsx +4 -4
  223. package/components/display/Tag.vue +4 -4
  224. package/components/feedback/Accordion.jsx +2 -2
  225. package/components/feedback/Accordion.vue +2 -2
  226. package/components/feedback/ActivityItem.d.ts +3 -1
  227. package/components/feedback/ActivityItem.jsx +6 -6
  228. package/components/feedback/ActivityItem.vue +5 -4
  229. package/components/feedback/Alert.jsx +4 -4
  230. package/components/feedback/Alert.vue +3 -3
  231. package/components/feedback/Banner.jsx +5 -5
  232. package/components/feedback/Banner.vue +5 -5
  233. package/components/feedback/ContextMenu.d.ts +3 -1
  234. package/components/feedback/ContextMenu.jsx +5 -5
  235. package/components/feedback/ContextMenu.vue +5 -5
  236. package/components/feedback/DotActivity.vue +3 -3
  237. package/components/feedback/Drawer.d.ts +2 -0
  238. package/components/feedback/Drawer.jsx +6 -6
  239. package/components/feedback/Drawer.vue +22 -15
  240. package/components/feedback/EmptyState.vue +2 -2
  241. package/components/feedback/LogViewer.vue +1 -1
  242. package/components/feedback/Modal.d.ts +2 -0
  243. package/components/feedback/Modal.jsx +12 -11
  244. package/components/feedback/Modal.vue +10 -8
  245. package/components/feedback/Skeleton.vue +2 -1
  246. package/components/feedback/Spinner.vue +2 -1
  247. package/components/feedback/Stepper.vue +1 -1
  248. package/components/feedback/Timeline.jsx +1 -1
  249. package/components/feedback/Timeline.vue +3 -3
  250. package/components/feedback/Toast.vue +4 -4
  251. package/components/feedback/ToastStack.vue +11 -0
  252. package/components/feedback/Tooltip.vue +19 -0
  253. package/components/feedback/feedback.prompt.md +4 -4
  254. package/components/forms/Checkbox.d.ts +3 -2
  255. package/components/forms/Checkbox.jsx +7 -7
  256. package/components/forms/Checkbox.vue +8 -8
  257. package/components/forms/Combobox.d.ts +2 -0
  258. package/components/forms/Combobox.jsx +12 -10
  259. package/components/forms/Combobox.vue +11 -10
  260. package/components/forms/DatePicker.d.ts +4 -0
  261. package/components/forms/DatePicker.jsx +16 -12
  262. package/components/forms/DatePicker.vue +15 -11
  263. package/components/forms/Dropzone.jsx +2 -2
  264. package/components/forms/Dropzone.vue +5 -5
  265. package/components/forms/Field.d.ts +5 -1
  266. package/components/forms/Field.jsx +4 -4
  267. package/components/forms/Field.vue +4 -4
  268. package/components/forms/Input.d.ts +5 -1
  269. package/components/forms/Input.jsx +8 -8
  270. package/components/forms/Input.vue +3 -3
  271. package/components/forms/MultiSelect.d.ts +2 -0
  272. package/components/forms/MultiSelect.jsx +13 -11
  273. package/components/forms/MultiSelect.vue +12 -11
  274. package/components/forms/NumberStepper.d.ts +3 -1
  275. package/components/forms/NumberStepper.jsx +7 -7
  276. package/components/forms/NumberStepper.vue +8 -7
  277. package/components/forms/Radio.d.ts +5 -3
  278. package/components/forms/Radio.jsx +13 -15
  279. package/components/forms/Radio.vue +9 -10
  280. package/components/forms/RadioGroup.vue +5 -5
  281. package/components/forms/SearchInput.d.ts +3 -1
  282. package/components/forms/SearchInput.jsx +6 -6
  283. package/components/forms/SearchInput.vue +7 -6
  284. package/components/forms/Select.d.ts +3 -2
  285. package/components/forms/Select.jsx +6 -7
  286. package/components/forms/Select.vue +6 -6
  287. package/components/forms/Slider.jsx +3 -3
  288. package/components/forms/Slider.vue +3 -3
  289. package/components/forms/Switch.d.ts +2 -2
  290. package/components/forms/Switch.jsx +20 -22
  291. package/components/forms/Switch.vue +22 -18
  292. package/components/forms/Textarea.vue +5 -5
  293. package/components/forms/forms.prompt.md +1 -1
  294. package/components/forms/input-base.js +2 -1
  295. package/components/icons/Icon.jsx +17 -1
  296. package/components/icons/Icon.prompt.md +2 -0
  297. package/components/icons/Icon.vue +1 -1
  298. package/components/icons/icon-paths.js +15 -0
  299. package/components/index.d.ts +1 -2
  300. package/components/index.js +4 -5
  301. package/components/navigation/Breadcrumb.jsx +6 -10
  302. package/components/navigation/Breadcrumb.vue +6 -12
  303. package/components/navigation/DocTree.jsx +3 -3
  304. package/components/navigation/DocTree.vue +3 -3
  305. package/components/navigation/PageHeading.vue +1 -1
  306. package/components/navigation/PageToolbar.vue +7 -6
  307. package/components/navigation/Sidebar.jsx +4 -23
  308. package/components/navigation/Sidebar.vue +5 -10
  309. package/components/navigation/ThemeToggle.vue +2 -1
  310. package/components/navigation/TopBar.d.ts +3 -1
  311. package/components/navigation/TopBar.jsx +6 -6
  312. package/components/navigation/Tree.jsx +5 -5
  313. package/components/navigation/Tree.vue +13 -8
  314. package/components/navigation/nav-model.js +23 -0
  315. package/components/navigation/theme-store.js +9 -0
  316. package/components/pipeline/ConfigPanel.d.ts +2 -0
  317. package/components/pipeline/ConfigPanel.jsx +9 -9
  318. package/components/pipeline/ConfigPanel.vue +9 -9
  319. package/components/pipeline/DataPreview.d.ts +3 -1
  320. package/components/pipeline/DataPreview.jsx +7 -7
  321. package/components/pipeline/DataPreview.vue +7 -7
  322. package/components/pipeline/NodeCard.jsx +3 -3
  323. package/components/pipeline/NodeCard.vue +3 -3
  324. package/components/pipeline/PaletteItem.vue +2 -2
  325. package/components/pipeline/PaletteListItem.vue +1 -1
  326. package/components/pipeline/PalettePanel.jsx +2 -2
  327. package/components/pipeline/PalettePanel.vue +1 -1
  328. package/components/pipeline/Port.vue +15 -0
  329. package/components/pipeline/PortLabel.vue +15 -0
  330. package/components/pipeline/ZoomControls.vue +1 -1
  331. package/components/pipeline/port-skin.js +13 -0
  332. package/components/px.js +4 -0
  333. package/components/tabs/PanelTabs.vue +5 -4
  334. package/components/tabs/PillTabs.vue +5 -4
  335. package/components/tabs/SegmentedControl.d.ts +2 -2
  336. package/components/tabs/SegmentedControl.jsx +7 -7
  337. package/components/tabs/SegmentedControl.vue +7 -7
  338. package/components/tabs/Tabs.d.ts +5 -3
  339. package/components/tabs/Tabs.jsx +15 -15
  340. package/components/tabs/Tabs.vue +5 -5
  341. package/components/tabs/ViewToggle.d.ts +3 -1
  342. package/components/tabs/ViewToggle.jsx +5 -5
  343. package/components/tabs/ViewToggle.vue +5 -5
  344. package/components/tabs/tabs.prompt.md +1 -1
  345. package/keybricks.css +4 -0
  346. package/package.json +6 -3
  347. package/readme.md +91 -122
  348. package/readme.zh-CN.md +121 -0
  349. package/tokens/alpha.css +3 -1
  350. package/tokens/charts.css +20 -17
  351. package/tokens/colors.css +3 -3
  352. package/tokens/dark.css +3 -3
  353. package/tokens/hover.css +32 -0
  354. package/tokens/interaction.css +3 -1
  355. package/tokens/layout.css +31 -7
  356. package/tokens/tokens.d.ts +58 -0
  357. package/tokens/tokens.js +179 -92
  358. package/tokens/tokens.json +327 -114
  359. package/tokens/typography.css +45 -15
  360. package/components/_cards/brand-visual.card.html +0 -43
  361. package/components/_cards/datatable.card.html +0 -48
  362. package/components/_shared/src-brand.js +0 -1
  363. package/components/brand/BrandVisual.d.ts +0 -17
  364. package/components/brand/BrandVisual.jsx +0 -29
  365. package/components/brand/BrandVisual.vue +0 -34
  366. package/components/brand/_dir.card.html +0 -13
  367. package/components/brand/brandMotifs.js +0 -210
  368. package/components/buttons/_dir.card.html +0 -13
  369. package/components/charts/ChartKit.vue +0 -47
  370. package/components/charts/_dir.card.html +0 -7
  371. package/components/data/DataTable.d.ts +0 -28
  372. package/components/data/DataTable.jsx +0 -32
  373. package/components/data/DataTable.vue +0 -25
  374. package/components/data/_dir.card.html +0 -13
  375. package/components/display/_dir.card.html +0 -13
  376. package/components/feedback/_dir.card.html +0 -13
  377. package/components/forms/_dir.card.html +0 -13
  378. package/components/icons/_dir.card.html +0 -13
  379. package/components/navigation/_dir.card.html +0 -13
  380. package/components/pipeline/_dir.card.html +0 -13
  381. package/components/tabs/_dir.card.html +0 -13
@@ -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 }
@@ -50,24 +51,24 @@ function commit(r, c, text) { const cell = cells.value[r][c]; delete cell.f; if
50
51
  function setFlag(k) { const on = !cur.value[k]; for (let r = lo.value.r; r <= hi.value.r; r++) for (let c = lo.value.c; c <= hi.value.c; c++) cells.value[r][c][k] = on }
51
52
  function setType(label) { const t = { Automatic: undefined, Number: 'number', Currency: 'currency', Percent: 'percent' }[label]; for (let r = lo.value.r; r <= hi.value.r; r++) for (let c = lo.value.c; c <= hi.value.c; c++) { if (t) cells.value[r][c].t = t; else delete cells.value[r][c].t } }
52
53
  const gridCols = computed(() => ['46px', ...Array.from({ length: nC }, (_, c) => (props.colWidths[c] || props.defaultColWidth) + 'px')].join(' '))
53
- 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)', color: 'var(--text-secondary)', height: '26px', boxSizing: 'border-box', userSelect: 'none' }
54
+ 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: '26px', boxSizing: 'border-box', userSelect: 'none' }
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
- <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
- <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
- <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)" />
68
+ <div v-if="formulaBar" style="height:var(--bar-h);display:flex;align-items:center;border-bottom:1px solid var(--border-control)">
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-sans);font-variant-numeric:tabular-nums;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>
70
+ <span style="padding:0 10px;font-family:var(--font-sans);font-variant-numeric:tabular-nums;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>
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-sans);font-variant-numeric:tabular-nums;font-size:var(--text-xs);color:var(--text-primary)" />
71
72
  </div>
72
73
  <div :style="{ overflow: 'auto', maxHeight: typeof height === 'number' ? height + 'px' : height }">
73
74
  <div :style="{ display: 'grid', gridTemplateColumns: gridCols, width: 'fit-content' }">
@@ -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: 'var(--font-sans)', fontVariantNumeric: typeof evalCell(cells, cell) === 'number' ? 'tabular-nums' : 'normal', 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,12 +85,12 @@ 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
- <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
+ <span style="font-family:var(--font-sans);font-variant-numeric:tabular-nums;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>
94
95
  </div>
95
96
  </template>
@@ -0,0 +1,101 @@
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 36 · md 40 · lg 48), 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 code/amount cells" viewport="700x300"
6
+ */
7
+ /** Field name on the row, or an accessor (row, index) => value. */
8
+ export type DataTableAccessor<T> = string | ((row: any, index: number) => T);
9
+ /** A status mark beside the title: "favorite" draws a filled 14px star in --warning; an object draws a 14px outline Icon. */
10
+ export type DataTableTitleMark = "favorite" | { icon: string; color?: string; label: string } | null | undefined;
11
+ /**
12
+ * Two-line title cell — the name column of a document list.
13
+ * Left to right: IconTile (kind "row", tinted by hue) · title over description · sm ⋯ IconButton.
14
+ * md: 32px tile r8 · 12px tile→text · title 14/20 --text-heading · description 12/16 --text-secondary (13/18 CJK) · 2px between · 10px row padding → 58px row.
15
+ * sm: 28px tile · 8px · 12/16 title · 2px · 8px padding → 50px. lg: 40px tile r10 · 12px · 14/20 title · 4px · 14px padding → 68px.
16
+ * Each field defaults to the row key of the same name (icon, hue, description, mark).
17
+ */
18
+ export interface DataTableTwoLine {
19
+ icon?: DataTableAccessor<string | null | undefined>;
20
+ /** source | join | transform | merge | output | neutral (default neutral — the grey folder tile) */
21
+ hue?: DataTableAccessor<"source" | "join" | "transform" | "merge" | "output" | "neutral" | null | undefined>;
22
+ description?: DataTableAccessor<React.ReactNode>;
23
+ mark?: DataTableAccessor<DataTableTitleMark>;
24
+ /** Trailing ⋯ button (default true). Fires the table's onRowMore. */
25
+ more?: boolean;
26
+ }
27
+ export interface DataTableSort { key: string; direction: "asc" | "desc"; }
28
+ export interface DataTableLabels { sortAsc?: string; sortDesc?: string; sortClear?: string; more?: string; expand?: string; collapse?: string; }
29
+ export interface DataTableColumn {
30
+ key: string;
31
+ label: string;
32
+ width?: number | string;
33
+ /** Lower bound while resizing (default 48) */
34
+ minWidth?: number;
35
+ /** Tabular figures for amounts and IDs (UI font, the row's size and colour). */
36
+ mono?: boolean;
37
+ /** Config code (ALL_CAPS_SNAKE) shown as a code chip: Menlo --text-code (12px, 11px in sm) on --bg-inset. */
38
+ code?: boolean;
39
+ /** Two-line title cell (see DataTableTwoLine); `true` reads icon/hue/description/mark from the row. Give the column a width; when the table is tight it shrinks to minWidth (default 180), and text truncates to whatever width it gets. */
40
+ twoLine?: boolean | DataTableTwoLine;
41
+ /** Header sort button: muted sm IconButton at the header cell's right end (in line with a two-line row's ⋯), shown on hover (arrow-up), kept while sorted (arrow-up / arrow-down, label --text-heading). Click cycles ascending → descending → none. Omit on action columns. */
42
+ sortable?: boolean;
43
+ /** Custom comparison for this column (ascending order). Default: numbers (commas allowed) numerically, text by locale with numeric collation, empty values last. */
44
+ compare?: (a: any, b: any, rowA: any, rowB: any) => number;
45
+ align?: "left" | "right" | "center";
46
+ render?: (value: any, row: any, index: number) => React.ReactNode;
47
+ }
48
+ export interface DataTableProps {
49
+ columns?: DataTableColumn[];
50
+ rows?: Record<string, any>[];
51
+ /** Size tier of the table region: sm 36px rows / 12px text, 13px in CJK (dense grids, preview drawers) · md 40px / 14px (default) · lg 48px / 14px. Omit to inherit the enclosing region. */
52
+ size?: "sm" | "md" | "lg";
53
+ /** Alias of size="sm", kept for 1.x callers */
54
+ dense?: boolean;
55
+ /** Leading checkbox column */
56
+ selectable?: boolean;
57
+ selected?: number[];
58
+ /** onToggle("all"|rowIndex, checked) */
59
+ onToggle?: (which: number | "all", checked: boolean) => void;
60
+ headerBorder?: boolean;
61
+ /** 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. */
62
+ bordered?: boolean;
63
+ /** For a table placed flush inside a container that already has a border (Card pad={false}): drops only the header's top rule. */
64
+ framed?: boolean;
65
+ /** Short divider between header cells; hover darkens it, drag sets that column's width. Uses table-layout: fixed. */
66
+ resizable?: boolean;
67
+ /** Controlled widths by column key (px). Omit to let the table keep them locally. */
68
+ columnWidths?: Record<string, number>;
69
+ /** Fires on every drag frame with the new px width. Vue: `@column-resize`. */
70
+ onColumnResize?: (key: string, width: number) => void;
71
+ /** ⋯ in a twoLine cell was clicked. Vue: `@row-more`. */
72
+ onRowMore?: (row: any, index: number, event: React.MouseEvent) => void;
73
+ /** Opens a row from its twoLine cell: the icon tile and the title get a hand cursor and call this on click (the title also on Enter / Space). Vue: `@row-open`. */
74
+ onRowOpen?: (row: any, index: number, event: React.SyntheticEvent) => void;
75
+ /** Tree rows: a row nests through `children` (or sets `hasChildren: true` to show the arrow before its children are loaded). The twoLine column indents 24px per level and shows an expand arrow (chevron-right, turned down when open). Sorting reorders siblings only. Selection and event indices are depth-first over every row, so they never move. */
76
+ tree?: boolean;
77
+ /** Row field that identifies a row in expandedKeys (default "id"; falls back to the row's index). */
78
+ rowKey?: string;
79
+ /** Controlled expanded rows (keys from rowKey). Omit to let the table keep them. Vue: `v-model:expandedKeys`. */
80
+ expandedKeys?: (string | number)[];
81
+ /** Initially expanded rows when uncontrolled. */
82
+ defaultExpandedKeys?: (string | number)[];
83
+ /** An arrow was clicked: the row, whether it is now expanded, and the next key list. Load `children` here for `hasChildren` rows. Vue: `@expand-change`. */
84
+ onExpandChange?: (row: any, expanded: boolean, expandedKeys: (string | number)[]) => void;
85
+ /** Every word the table draws itself (English by default). The sort tooltip names the next step: unsorted → sortAsc, ascending → sortDesc, descending → sortClear. */
86
+ labels?: DataTableLabels;
87
+ /** Controlled sort state (one column at a time); null = unsorted. Omit to let the table keep it. Vue: `v-model:sort`. */
88
+ sort?: DataTableSort | null;
89
+ /** Initial sort when uncontrolled. */
90
+ defaultSort?: DataTableSort | null;
91
+ /** Fires with the next state: { key, direction: "asc" } → "desc" → null; a different column starts at "asc". Vue: `@sort-change`. */
92
+ onSortChange?: (sort: DataTableSort | null) => void;
93
+ /** Rows are already ordered (server-side sorting): the table shows the sort state but does not reorder. */
94
+ manualSort?: boolean;
95
+ /** Rendered inside the table (full-width cell) when rows is empty — an EmptyState or a muted line. Vue: `#empty` slot. */
96
+ empty?: React.ReactNode;
97
+ style?: React.CSSProperties;
98
+ }
99
+ export declare function Table(props: DataTableProps): JSX.Element;
100
+ /** @deprecated renamed to Table in 1.0.6 — still exported in 2.x, removed in 3.0 */
101
+ export declare const DataTable: typeof Table;
@@ -0,0 +1,223 @@
1
+ import React from "react";
2
+ import { Checkbox } from "../forms/Checkbox.jsx";
3
+ import { IconTile } from "../display/Badge.jsx";
4
+ import { IconButton } from "../buttons/IconButton.jsx";
5
+ import { Icon } from "../icons/Icon.jsx";
6
+
7
+ /* Selection checkbox: sits in the first column's leading padding, not in a column of its own, so the gap to the
8
+ first column's text equals the edge padding (16px; 12px in sm) whatever the table width or column widths (a separate column took a share
9
+ of any spare width). */
10
+ const CHECK_PAD = "calc(var(--edge-pad) * 2 + var(--check-size))";
11
+ /* Config-code chip (`code: true` columns): Menlo at --text-code on the inset fill — the one place the code font appears in a table.
12
+ Only text and numbers get the chip; an element in the cell (a Skeleton while loading, a link) renders as is. */
13
+ const codeChip = { fontFamily: "var(--font-mono)", fontSize: "var(--text-code)", lineHeight: "var(--leading-code)", letterSpacing: 0, background: "var(--bg-inset)", border: "1px solid var(--border-subtle)", borderRadius: 4, padding: "1px 6px", color: "var(--text-primary)", whiteSpace: "nowrap" };
14
+ const checkSlot = { position: "absolute", left: "var(--edge-pad)", top: 0, bottom: 0, display: "flex", alignItems: "center" };
15
+
16
+ /* Two-line title cell (`twoLine` column): icon tile · title over description · trailing ⋯. Every measure is a --title-cell-* tier token:
17
+ md 32px tile (r8) · 12px to the text · 14px title / 12px description · 2px between · 10px row padding → 58px row. */
18
+ const T_TILE = { width: "var(--title-cell-tile)", height: "var(--title-cell-tile)", borderRadius: "var(--title-cell-tile-r)" };
19
+ const T_ICON = { width: "var(--title-cell-icon)", height: "var(--title-cell-icon)" };
20
+ const T_ONE = { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 };
21
+ const T_TITLE = { ...T_ONE, fontFamily: "var(--font-sans)", fontWeight: "var(--weight-regular)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-heading)" };
22
+ const T_DESC = { ...T_ONE, fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", letterSpacing: "var(--tracking-xs)", color: "var(--text-secondary)" };
23
+ const STAR_FILLED = "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z";
24
+ // field name (string) or accessor (row, index) => value
25
+ const pick = (a, r, ri) => typeof a === "function" ? a(r, ri) : a != null ? r[a] : undefined;
26
+ /* Sorting: one column at a time. Click cycles ascending → descending → none; another column starts at ascending.
27
+ The tooltip names the next step. Rows are reordered here unless manualSort (server-side: rows arrive ordered). */
28
+ // the label is capped at the declared width minus the button (28) and its gap (4), so a long label truncates and the cell never
29
+ // shrinks below label + button — the button is never pushed out
30
+ const SORT_BTN = 32;
31
+ const isEmpty = (v) => v == null || v === "" || v === "–";
32
+ /* Every word the table draws itself. Pass `labels` to translate; missing keys fall back to these. The sort tooltip names the NEXT step:
33
+ unsorted → sortAsc, ascending → sortDesc, descending → sortClear. */
34
+ const DEFAULT_LABELS = { sortAsc: "Sort ascending", sortDesc: "Sort descending", sortClear: "Clear sorting", more: "More actions", expand: "Expand", collapse: "Collapse" };
35
+ const sortLabel = (L, sorted) => sorted === "asc" ? L.sortDesc : sorted === "desc" ? L.sortClear : L.sortAsc;
36
+ /* Tree (`tree` on the table, `children` on a row): the twoLine column indents 24px per level and reserves a 20px arrow slot + 4px,
37
+ so a child's arrow sits exactly under its parent's tile. */
38
+ const TREE_INDENT = 24, TREE_TOGGLE = 20;
39
+ /* Builds the visible rows. index = depth-first position over EVERY row (expanded or not) — for a flat table that is the position in
40
+ `rows`. It is assigned before sorting, so selection, onRowMore and onRowOpen indices never move when the user sorts or expands.
41
+ Sorting reorders siblings only; children stay under their parent. */
42
+ function buildView(rows, { tree, rowKey, expanded, col, dir, manualSort }) {
43
+ const idx = new Map(); let n = 0;
44
+ const number = (list) => list.forEach((r) => { idx.set(r, n++); if (tree && Array.isArray(r.children)) number(r.children); });
45
+ number(rows);
46
+ const cmp = col && !manualSort ? (a, b) => {
47
+ const va = a[col.key], vb = b[col.key], ea = isEmpty(va), eb = isEmpty(vb);
48
+ if (ea || eb) return ea && eb ? idx.get(a) - idx.get(b) : ea ? 1 : -1; // empty values last in both directions
49
+ return (col.compare ? col.compare(va, vb, a, b) : compareValues(va, vb)) * dir || idx.get(a) - idx.get(b);
50
+ } : null;
51
+ const out = [];
52
+ const walk = (list, level) => (cmp ? list.slice().sort(cmp) : list).forEach((r) => {
53
+ const ri = idx.get(r), key = r[rowKey] != null ? r[rowKey] : String(ri);
54
+ const kids = !!tree && ((Array.isArray(r.children) && r.children.length > 0) || !!r.hasChildren);
55
+ const open = kids && expanded.has(key);
56
+ out.push({ r, ri, key, level, kids, open });
57
+ if (open && Array.isArray(r.children)) walk(r.children, level + 1);
58
+ });
59
+ walk(rows, 0);
60
+ return { list: out, total: n };
61
+ }
62
+ const nextSort = (cur, key) => !cur || cur.key !== key ? { key, direction: "asc" } : cur.direction === "asc" ? { key, direction: "desc" } : null;
63
+ const num = (v) => typeof v === "number" ? v : typeof v === "string" && /^\s*-?[\d,]+(\.\d+)?\s*$/.test(v) ? parseFloat(v.replace(/,/g, "")) : NaN;
64
+ function compareValues(a, b) {
65
+ const ea = a == null || a === "" || a === "–", eb = b == null || b === "" || b === "–";
66
+ if (ea || eb) return ea && eb ? 0 : ea ? 1 : -1;
67
+ const na = num(a), nb = num(b);
68
+ if (!isNaN(na) && !isNaN(nb)) return na - nb;
69
+ return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: "base" });
70
+ }
71
+ function TitleMark({ m }) {
72
+ if (m === "favorite") return <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinejoin="round" role="img" aria-label="Favorite" style={{ display: "block", flexShrink: 0, color: "var(--warning)" }}><path d={STAR_FILLED} /></svg>;
73
+ return <span role="img" aria-label={m.label} style={{ display: "inline-flex", flexShrink: 0, color: m.color || "var(--text-muted)" }}><Icon name={m.icon} size={14} /></span>;
74
+ }
75
+ function TreeToggle({ open, label, onClick }) {
76
+ const [hover, setHover] = React.useState(false);
77
+ return (
78
+ <button type="button" aria-expanded={open} aria-label={label} title={label} onClick={(e) => { e.stopPropagation(); onClick(); }} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
79
+ style={{ width: TREE_TOGGLE, height: TREE_TOGGLE, padding: 0, border: "none", borderRadius: "var(--radius-sm)", display: "inline-flex", alignItems: "center", justifyContent: "center", cursor: "pointer", background: hover ? "var(--overlay-hover)" : "transparent", color: hover ? "var(--text-body)" : "var(--text-muted)", transition: "var(--t-hover)" }}>
80
+ <Icon name="chevron-right" size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)", transform: open ? "rotate(90deg)" : "none", transition: "transform .15s ease" }} />
81
+ </button>
82
+ );
83
+ }
84
+ function TitleCell({ c, item, onRowMore, onRowOpen, labels, tree, onToggleRow }) {
85
+ const { r, ri } = item;
86
+ const o = c.twoLine === true ? {} : c.twoLine;
87
+ const icon = pick(o.icon ?? "icon", r, ri), hue = pick(o.hue ?? "hue", r, ri), desc = pick(o.description ?? "description", r, ri), mark = pick(o.mark ?? "mark", r, ri);
88
+ // onRowOpen: the tile and the title open the row (hand cursor); the title is also reachable by keyboard (Enter / Space)
89
+ const open = onRowOpen ? (e) => onRowOpen(r, ri, e) : null;
90
+ return (
91
+ <div style={{ display: "flex", alignItems: "center", minWidth: c.minWidth ?? 180, ...(tree ? { paddingLeft: item.level * TREE_INDENT } : null) }}>
92
+ {tree ? <span style={{ width: TREE_TOGGLE, flexShrink: 0, marginRight: TREE_INDENT - TREE_TOGGLE, display: "inline-flex" }}>{item.kids ? <TreeToggle open={item.open} label={item.open ? labels.collapse : labels.expand} onClick={() => onToggleRow(item)} /> : null}</span> : null}
93
+ <div style={{ display: "flex", alignItems: "center", gap: "var(--title-cell-tile-gap)", flex: 1, minWidth: 0 }}>
94
+ {icon ? <span onClick={open || undefined} style={{ display: "inline-flex", flexShrink: 0, cursor: open ? "pointer" : undefined }}><IconTile icon={icon} hue={hue || "neutral"} kind="row" style={T_TILE} iconStyle={T_ICON} /></span> : null}
95
+ <div style={{ display: "flex", flexDirection: "column", gap: "var(--title-cell-gap)", flex: 1, minWidth: 0 }}>
96
+ <div style={{ display: "flex", alignItems: "center", gap: 4, minWidth: 0 }}>
97
+ <span style={{ ...T_TITLE, ...(open ? { cursor: "pointer" } : null) }} {...(open ? { role: "button", tabIndex: 0, onClick: open, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); open(e); } } } : null)}>{c.render ? c.render(r[c.key], r, ri) : r[c.key]}</span>
98
+ {mark ? <TitleMark m={mark} /> : null}
99
+ </div>
100
+ {desc ? <span style={T_DESC}>{desc}</span> : null}
101
+ </div>
102
+ {o.more !== false ? <IconButton icon="ellipsis" size="sm" muted label={labels.more} title={labels.more} onClick={(e) => { if (e && e.stopPropagation) e.stopPropagation(); if (onRowMore) onRowMore(r, ri, e); }} /> : null}
103
+ </div>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Keybricks table. columns: [{key,label,width,minWidth,mono,code,twoLine,sortable,compare,align,render}].
110
+ * sortable — hover the header for a muted sm sort button at the cell's right end (in line with a two-line row's ⋯; arrow-up); click: ascending → descending → none.
111
+ * While sorted the button stays and the label turns --text-heading. One column at a time. sort / onSortChange control it
112
+ * (defaultSort to seed it uncontrolled); compare(a, b, rowA, rowB) overrides the default comparison; manualSort keeps rows as given.
113
+ * twoLine — the name column of a document list: icon tile, title over a description, optional mark ("favorite" = filled star), trailing ⋯
114
+ * (onRowMore(row, index, event)); onRowOpen(row, index, event) makes the tile and the title open the row (hand cursor, Enter / Space on the title).
115
+ * tree — rows nest through `children` (or `hasChildren` for rows you load on demand); the twoLine column indents 24px per level with an
116
+ * expand arrow; expandedKeys / defaultExpandedKeys / onExpandChange(row, expanded, keys); rowKey names the key field (default "id").
117
+ * labels — { sortAsc, sortDesc, sortClear, more, expand, collapse } for every word the table draws (English by default).
118
+ * Give it a width; it shrinks to minWidth (default 180) before the other columns give way, and title and description truncate to whatever it gets. Row grows to 58px in md (50 sm · 68 lg).
119
+ * The table is a size region: `size` ("sm" | "md" | "lg", default inherit → md) sets its rows, header, cell padding and every
120
+ * control inside the cells from the tier tokens — rows are the tier's bar height (sm 36 · md 40 · lg 48), cells read the tier's
121
+ * control text, header cells are the same size in weight 600 / text-secondary, sentence case, no letter-spacing.
122
+ * `dense` is kept as an alias of size="sm" (the data-preview drawer).
123
+ * 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.
124
+ * bordered — the table draws its own rounded card frame (border-card, radius-lg) and drops the edge rules that would double it.
125
+ * framed — the table sits flush inside a container that already has a border (Card pad={false}); drops only the header's top rule.
126
+ * resizable — a short divider between header cells (hover darkens it, cursor col-resize); drag to set that column's width.
127
+ * Widths live in local state unless you pass columnWidths/onColumnResize to control them. Min width = column.minWidth ?? 48.
128
+ */
129
+ export function Table({ columns = [], rows = [], size, dense, selectable, selected = [], onToggle, headerBorder = true, bordered, framed, resizable, columnWidths, onColumnResize, onRowMore, onRowOpen, sort, defaultSort = null, onSortChange, manualSort, tree, rowKey = "id", expandedKeys, defaultExpandedKeys, onExpandChange, labels, empty, style }) {
130
+ const L = { ...DEFAULT_LABELS, ...labels };
131
+ const [localExp, setLocalExp] = React.useState(defaultExpandedKeys || []);
132
+ const expKeys = expandedKeys !== undefined ? expandedKeys : localExp;
133
+ const [localSort, setLocalSort] = React.useState(defaultSort);
134
+ const sortState = sort !== undefined ? sort : localSort;
135
+ const [hotTh, setHotTh] = React.useState(null); // header under the pointer or holding focus
136
+ const cycleSort = (key) => { const n = nextSort(sortState, key); if (sort === undefined) setLocalSort(n); if (onSortChange) onSortChange(n); };
137
+ const { list: view, total } = React.useMemo(() => buildView(rows, {
138
+ tree, rowKey, expanded: new Set(expKeys), manualSort,
139
+ col: sortState && columns.find((x) => x.key === sortState.key), dir: sortState && sortState.direction === "desc" ? -1 : 1,
140
+ }), [rows, columns, sortState, manualSort, tree, rowKey, expKeys]);
141
+ const toggleRow = (item) => {
142
+ const next = item.open ? expKeys.filter((k) => k !== item.key) : [...expKeys, item.key];
143
+ if (expandedKeys === undefined) setLocalExp(next);
144
+ if (onExpandChange) onExpandChange(item.r, !item.open, next);
145
+ };
146
+ const tier = size || (dense ? "sm" : undefined);
147
+ const flushTop = bordered || framed;
148
+ const colCount = columns.length || 1;
149
+ const [localW, setLocalW] = React.useState({});
150
+ const [drag, setDrag] = React.useState(null); // key being dragged
151
+ const [hot, setHot] = React.useState(null); // key whose handle is hovered
152
+ const thRefs = React.useRef({});
153
+ const widths = columnWidths || localW;
154
+ const widthOf = (c) => widths[c.key] ?? c.width;
155
+
156
+ const startDrag = (e, c) => {
157
+ e.preventDefault();
158
+ const th = thRefs.current[c.key];
159
+ const startX = e.clientX, startW = th ? th.getBoundingClientRect().width : (parseFloat(widthOf(c)) || 120);
160
+ const min = c.minWidth ?? 48;
161
+ setDrag(c.key);
162
+ const move = (ev) => {
163
+ const w = Math.max(min, Math.round(startW + ev.clientX - startX));
164
+ if (onColumnResize) onColumnResize(c.key, w); else setLocalW((s) => ({ ...s, [c.key]: w }));
165
+ };
166
+ const up = () => { setDrag(null); window.removeEventListener("mousemove", move); window.removeEventListener("mouseup", up); document.body.style.cursor = ""; document.body.style.userSelect = ""; };
167
+ document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none";
168
+ window.addEventListener("mousemove", move); window.addEventListener("mouseup", up);
169
+ };
170
+
171
+ // `mono` columns (codes, IDs, amounts) use the UI font with tabular figures; the monospace font is for code only
172
+ const cellFont = (c) => c.mono
173
+ ? { fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", fontVariantNumeric: "tabular-nums", color: "var(--text-primary)" }
174
+ : { fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-primary)" };
175
+ // first and last cells keep the card's edge padding; every other cell the tier's cell padding
176
+ const cellPad = (i) => ({ ...(i === 0 && selectable ? { position: "relative" } : null), paddingTop: 0, paddingBottom: 0, paddingLeft: i === 0 ? (selectable ? CHECK_PAD : "var(--edge-pad)") : "var(--cell-px)", paddingRight: i === columns.length - 1 ? "var(--edge-pad)" : "var(--cell-px)" });
177
+
178
+ const table = (
179
+ <table data-size={tier} style={{ width: "100%", borderCollapse: "collapse", tableLayout: resizable ? "fixed" : "auto", ...(bordered ? null : style) }}>
180
+ <thead>
181
+ <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)" }}>
182
+ {columns.map((c, i) => {
183
+ const last = i === columns.length - 1;
184
+ const active = drag === c.key, lit = active || hot === c.key;
185
+ const sorted = sortState && sortState.key === c.key ? sortState.direction : null;
186
+ const showSort = c.sortable && (sorted || hotTh === c.key);
187
+ return (
188
+ <th key={c.key} ref={(el) => { thRefs.current[c.key] = el; }} aria-sort={c.sortable ? (sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none") : undefined}
189
+ onMouseEnter={c.sortable ? () => setHotTh(c.key) : undefined} onMouseLeave={c.sortable ? () => setHotTh(null) : undefined} onFocus={c.sortable ? () => setHotTh(c.key) : undefined} onBlur={c.sortable ? () => setHotTh(null) : undefined}
190
+ 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: sorted ? "var(--text-heading)" : "var(--text-secondary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
191
+ {selectable && i === 0 ? <span style={checkSlot}><Checkbox checked={total > 0 && selected.length === total} indeterminate={selected.length > 0 && selected.length < total} onChange={(v) => onToggle && onToggle("all", v)} /></span> : null}
192
+ {c.sortable ? (
193
+ <span style={{ display: "flex", alignItems: "center", gap: 4 }}>
194
+ <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0, ...(c.align === "right" ? { marginLeft: "auto" } : c.align === "center" ? { margin: "0 auto" } : null), ...(typeof widthOf(c) === "number" ? { maxWidth: widthOf(c) - SORT_BTN } : null) }}>{c.label}</span>
195
+ <IconButton icon={sorted === "desc" ? "arrow-down" : "arrow-up"} size="sm" muted label={sortLabel(L, sorted)} title={sortLabel(L, sorted)} onClick={() => cycleSort(c.key)} style={{ opacity: showSort ? 1 : 0, ...(c.align === "right" ? null : { marginLeft: "auto" }) }} />
196
+ </span>
197
+ ) : c.label}
198
+ {resizable && !last ? (
199
+ <span aria-hidden="true" onMouseDown={(e) => startDrag(e, c)} onMouseEnter={() => setHot(c.key)} onMouseLeave={() => setHot(null)}
200
+ style={{ position: "absolute", top: 0, right: -5, width: 11, height: "100%", cursor: "col-resize", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1 }}>
201
+ <span style={{ width: active ? 2 : 1, height: "50%", background: lit ? "var(--action-primary)" : "var(--border-split)", borderRadius: 1, transition: "var(--t-hover)" }}></span>
202
+ </span>
203
+ ) : null}
204
+ </th>
205
+ );
206
+ })}
207
+ </tr>
208
+ </thead>
209
+ <tbody>
210
+ {view.map((item, pos) => { const { r, ri } = item; return (
211
+ <tr key={item.key} data-row="" aria-level={tree ? item.level + 1 : undefined} aria-expanded={item.kids ? item.open : undefined} style={{ height: "var(--bar-h)", background: "var(--row-wash, transparent)", transition: "var(--t-hover)", borderBottom: bordered && pos === view.length - 1 ? "none" : "1px solid var(--border-subtle)" }}>
212
+ {columns.map((c, i) => (
213
+ <td key={c.key} style={{ ...cellPad(i), ...(c.twoLine ? { paddingTop: "var(--title-cell-py)", paddingBottom: "var(--title-cell-py)", maxWidth: 0 } : null), textAlign: c.align || "left", whiteSpace: "nowrap", ...(resizable ? { overflow: "hidden", textOverflow: "ellipsis" } : null), ...cellFont(c) }}>{selectable && i === 0 ? <span style={checkSlot}><Checkbox checked={selected.includes(ri)} onChange={(v) => onToggle && onToggle(ri, v)} /></span> : null}{c.twoLine ? <TitleCell c={c} item={item} onRowMore={onRowMore} onRowOpen={onRowOpen} labels={L} tree={tree && c === columns.find((x) => x.twoLine)} onToggleRow={toggleRow} /> : (c.code && (typeof r[c.key] === "string" || typeof r[c.key] === "number") && r[c.key] !== "") ? <code style={codeChip}>{c.render ? c.render(r[c.key], r, ri) : r[c.key]}</code> : c.render ? c.render(r[c.key], r, ri) : r[c.key]}</td>
214
+ ))}
215
+ </tr>
216
+ ); })}
217
+ {rows.length === 0 && empty ? <tr><td colSpan={colCount} style={{ padding: 0 }}>{empty}</td></tr> : null}
218
+ </tbody>
219
+ </table>
220
+ );
221
+ if (!bordered) return table;
222
+ 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>;
223
+ }