@keyfunc/keybricks-ds 1.0.0

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 (418) hide show
  1. package/DEPLOY.md +204 -0
  2. package/DESIGN.md +269 -0
  3. package/INTEGRATION.md +417 -0
  4. package/SKILL.md +32 -0
  5. package/_adherence.oxlintrc.json +1469 -0
  6. package/_scripts/build-index.mjs +80 -0
  7. package/_scripts/build-tokens.mjs +135 -0
  8. package/assets/icons/icon-index.js +1 -0
  9. package/assets/icons/icon-index.json +1 -0
  10. package/assets/icons/unicons-index.js +1 -0
  11. package/assets/icons/unicons-index.json +1 -0
  12. package/assets/keybricks-logo.png +0 -0
  13. package/components/_cards/accordion.card.html +32 -0
  14. package/components/_cards/action-chip.card.html +22 -0
  15. package/components/_cards/activity.card.html +34 -0
  16. package/components/_cards/alert.card.html +32 -0
  17. package/components/_cards/app-chrome.card.html +36 -0
  18. package/components/_cards/banner.card.html +21 -0
  19. package/components/_cards/brand-visual.card.html +43 -0
  20. package/components/_cards/breadcrumb.card.html +23 -0
  21. package/components/_cards/button-1-base.card.html +39 -0
  22. package/components/_cards/button-2-states.card.html +56 -0
  23. package/components/_cards/button-3-sizes-icons.card.html +32 -0
  24. package/components/_cards/button-4-group.card.html +47 -0
  25. package/components/_cards/canvas-zoom-controls.card.html +19 -0
  26. package/components/_cards/cards-indicators.card.html +59 -0
  27. package/components/_cards/charts-1-sparkline.card.html +55 -0
  28. package/components/_cards/charts-10-gauge.card.html +31 -0
  29. package/components/_cards/charts-11-funnel.card.html +28 -0
  30. package/components/_cards/charts-12-treemap.card.html +23 -0
  31. package/components/_cards/charts-14-radar.card.html +31 -0
  32. package/components/_cards/charts-15-distribution.card.html +32 -0
  33. package/components/_cards/charts-16-pareto.card.html +25 -0
  34. package/components/_cards/charts-17-candlestick.card.html +34 -0
  35. package/components/_cards/charts-18-sankey.card.html +29 -0
  36. package/components/_cards/charts-19-chord.card.html +31 -0
  37. package/components/_cards/charts-2-line.card.html +46 -0
  38. package/components/_cards/charts-20-sunburst.card.html +38 -0
  39. package/components/_cards/charts-21-worldmap.card.html +33 -0
  40. package/components/_cards/charts-22-gantt.card.html +37 -0
  41. package/components/_cards/charts-23-bar3d.card.html +26 -0
  42. package/components/_cards/charts-24-pie3d.card.html +32 -0
  43. package/components/_cards/charts-25-surface3d.card.html +24 -0
  44. package/components/_cards/charts-26-scatter3d.card.html +24 -0
  45. package/components/_cards/charts-27-ribbon3d.card.html +23 -0
  46. package/components/_cards/charts-3-bar.card.html +71 -0
  47. package/components/_cards/charts-4-barlist.card.html +30 -0
  48. package/components/_cards/charts-5-donut.card.html +35 -0
  49. package/components/_cards/charts-6-scatter.card.html +26 -0
  50. package/components/_cards/charts-7-heatmap.card.html +28 -0
  51. package/components/_cards/charts-8-waterfall.card.html +25 -0
  52. package/components/_cards/charts-9-bullet.card.html +31 -0
  53. package/components/_cards/charts-99-fullpage.card.html +194 -0
  54. package/components/_cards/checkbox.card.html +34 -0
  55. package/components/_cards/codeblock.card.html +19 -0
  56. package/components/_cards/combobox.card.html +25 -0
  57. package/components/_cards/context-menu.card.html +21 -0
  58. package/components/_cards/datatable.card.html +48 -0
  59. package/components/_cards/date-picker.card.html +31 -0
  60. package/components/_cards/diffvalue.card.html +21 -0
  61. package/components/_cards/divider.card.html +24 -0
  62. package/components/_cards/drawer.card.html +30 -0
  63. package/components/_cards/dropdown-menu.card.html +38 -0
  64. package/components/_cards/dropzone.card.html +22 -0
  65. package/components/_cards/empty-state.card.html +29 -0
  66. package/components/_cards/filter-bar.card.html +34 -0
  67. package/components/_cards/icon-button.card.html +27 -0
  68. package/components/_cards/icons-1-core.card.html +49 -0
  69. package/components/_cards/icons-2-feather.card.html +39 -0
  70. package/components/_cards/icons-3-unicons.card.html +44 -0
  71. package/components/_cards/input.card.html +25 -0
  72. package/components/_cards/kbd.card.html +20 -0
  73. package/components/_cards/keyvalue.card.html +23 -0
  74. package/components/_cards/listrow.card.html +30 -0
  75. package/components/_cards/loading.card.html +33 -0
  76. package/components/_cards/log-viewer.card.html +23 -0
  77. package/components/_cards/modal.card.html +35 -0
  78. package/components/_cards/multi-select.card.html +26 -0
  79. package/components/_cards/number-stepper.card.html +24 -0
  80. package/components/_cards/pagination.card.html +25 -0
  81. package/components/_cards/people.card.html +46 -0
  82. package/components/_cards/pipeline-node.card.html +76 -0
  83. package/components/_cards/pipeline-palette.card.html +54 -0
  84. package/components/_cards/search.card.html +23 -0
  85. package/components/_cards/select.card.html +23 -0
  86. package/components/_cards/slider.card.html +25 -0
  87. package/components/_cards/smart-table.card.html +37 -0
  88. package/components/_cards/spreadsheet.card.html +31 -0
  89. package/components/_cards/stepper.card.html +25 -0
  90. package/components/_cards/tabs.card.html +25 -0
  91. package/components/_cards/tag.card.html +26 -0
  92. package/components/_cards/textarea.card.html +27 -0
  93. package/components/_cards/timeline.card.html +32 -0
  94. package/components/_cards/toast.card.html +33 -0
  95. package/components/_cards/toggles.card.html +68 -0
  96. package/components/_cards/tooltip.card.html +22 -0
  97. package/components/_cards/tree.card.html +150 -0
  98. package/components/_shared/frame-theme.js +16 -0
  99. package/components/_shared/src-brand.js +1 -0
  100. package/components/_shared/src-buttons.js +1 -0
  101. package/components/_shared/src-charts.js +1 -0
  102. package/components/_shared/src-data.js +1 -0
  103. package/components/_shared/src-display.js +1 -0
  104. package/components/_shared/src-feedback.js +1 -0
  105. package/components/_shared/src-forms.js +1 -0
  106. package/components/_shared/src-icons.js +1 -0
  107. package/components/_shared/src-navigation.js +1 -0
  108. package/components/_shared/src-pipeline.js +1 -0
  109. package/components/_shared/src-tabs.js +1 -0
  110. package/components/_shared/svg-download.js +19 -0
  111. package/components/_shared/view-source.js +185 -0
  112. package/components/_vue/index.mjs +114 -0
  113. package/components/brand/BrandVisual.d.ts +17 -0
  114. package/components/brand/BrandVisual.jsx +29 -0
  115. package/components/brand/BrandVisual.vue +34 -0
  116. package/components/brand/_dir.card.html +13 -0
  117. package/components/brand/brandMotifs.js +210 -0
  118. package/components/buttons/ActionChip.d.ts +10 -0
  119. package/components/buttons/ActionChip.jsx +20 -0
  120. package/components/buttons/ActionChip.vue +18 -0
  121. package/components/buttons/Button.d.ts +40 -0
  122. package/components/buttons/Button.jsx +101 -0
  123. package/components/buttons/Button.vue +87 -0
  124. package/components/buttons/IconButton.d.ts +26 -0
  125. package/components/buttons/IconButton.jsx +28 -0
  126. package/components/buttons/IconButton.vue +21 -0
  127. package/components/buttons/_dir.card.html +13 -0
  128. package/components/buttons/buttons.prompt.md +19 -0
  129. package/components/charts/Bar3D.d.ts +15 -0
  130. package/components/charts/Bar3D.jsx +93 -0
  131. package/components/charts/Bar3D.vue +79 -0
  132. package/components/charts/BarChart.d.ts +23 -0
  133. package/components/charts/BarChart.jsx +135 -0
  134. package/components/charts/BarChart.vue +139 -0
  135. package/components/charts/BarList.d.ts +13 -0
  136. package/components/charts/BarList.jsx +31 -0
  137. package/components/charts/BarList.vue +25 -0
  138. package/components/charts/BoxPlot.d.ts +18 -0
  139. package/components/charts/BoxPlot.jsx +62 -0
  140. package/components/charts/BoxPlot.vue +58 -0
  141. package/components/charts/BulletChart.d.ts +12 -0
  142. package/components/charts/BulletChart.jsx +41 -0
  143. package/components/charts/BulletChart.vue +33 -0
  144. package/components/charts/CandlestickChart.d.ts +15 -0
  145. package/components/charts/CandlestickChart.jsx +63 -0
  146. package/components/charts/CandlestickChart.vue +62 -0
  147. package/components/charts/ChartKit.d.ts +8 -0
  148. package/components/charts/ChartKit.jsx +76 -0
  149. package/components/charts/ChartKit.vue +47 -0
  150. package/components/charts/ChordChart.d.ts +13 -0
  151. package/components/charts/ChordChart.jsx +55 -0
  152. package/components/charts/ChordChart.vue +54 -0
  153. package/components/charts/DonutChart.d.ts +15 -0
  154. package/components/charts/DonutChart.jsx +73 -0
  155. package/components/charts/DonutChart.vue +61 -0
  156. package/components/charts/FunnelChart.d.ts +11 -0
  157. package/components/charts/FunnelChart.jsx +37 -0
  158. package/components/charts/FunnelChart.vue +29 -0
  159. package/components/charts/GanttChart.d.ts +20 -0
  160. package/components/charts/GanttChart.jsx +80 -0
  161. package/components/charts/GanttChart.vue +60 -0
  162. package/components/charts/GaugeChart.d.ts +13 -0
  163. package/components/charts/GaugeChart.jsx +33 -0
  164. package/components/charts/GaugeChart.vue +29 -0
  165. package/components/charts/HeatmapChart.d.ts +13 -0
  166. package/components/charts/HeatmapChart.jsx +62 -0
  167. package/components/charts/HeatmapChart.vue +38 -0
  168. package/components/charts/Histogram.d.ts +16 -0
  169. package/components/charts/Histogram.jsx +56 -0
  170. package/components/charts/Histogram.vue +53 -0
  171. package/components/charts/LineChart.d.ts +17 -0
  172. package/components/charts/LineChart.jsx +84 -0
  173. package/components/charts/LineChart.vue +86 -0
  174. package/components/charts/ParetoChart.d.ts +13 -0
  175. package/components/charts/ParetoChart.jsx +54 -0
  176. package/components/charts/ParetoChart.vue +55 -0
  177. package/components/charts/Pie3D.d.ts +17 -0
  178. package/components/charts/Pie3D.jsx +145 -0
  179. package/components/charts/Pie3D.vue +115 -0
  180. package/components/charts/RadarChart.d.ts +14 -0
  181. package/components/charts/RadarChart.jsx +79 -0
  182. package/components/charts/RadarChart.vue +73 -0
  183. package/components/charts/Ribbon3D.d.ts +13 -0
  184. package/components/charts/Ribbon3D.jsx +89 -0
  185. package/components/charts/Ribbon3D.vue +79 -0
  186. package/components/charts/SankeyChart.d.ts +15 -0
  187. package/components/charts/SankeyChart.jsx +59 -0
  188. package/components/charts/SankeyChart.vue +53 -0
  189. package/components/charts/Scatter3D.d.ts +12 -0
  190. package/components/charts/Scatter3D.jsx +69 -0
  191. package/components/charts/Scatter3D.vue +59 -0
  192. package/components/charts/ScatterChart.d.ts +12 -0
  193. package/components/charts/ScatterChart.jsx +53 -0
  194. package/components/charts/ScatterChart.vue +57 -0
  195. package/components/charts/Sparkline.d.ts +19 -0
  196. package/components/charts/Sparkline.jsx +63 -0
  197. package/components/charts/Sparkline.vue +54 -0
  198. package/components/charts/SunburstChart.d.ts +12 -0
  199. package/components/charts/SunburstChart.jsx +61 -0
  200. package/components/charts/SunburstChart.vue +60 -0
  201. package/components/charts/Surface3D.d.ts +13 -0
  202. package/components/charts/Surface3D.jsx +70 -0
  203. package/components/charts/Surface3D.vue +63 -0
  204. package/components/charts/TreemapChart.d.ts +11 -0
  205. package/components/charts/TreemapChart.jsx +35 -0
  206. package/components/charts/TreemapChart.vue +27 -0
  207. package/components/charts/WaterfallChart.d.ts +11 -0
  208. package/components/charts/WaterfallChart.jsx +59 -0
  209. package/components/charts/WaterfallChart.vue +46 -0
  210. package/components/charts/WorldMap.d.ts +21 -0
  211. package/components/charts/WorldMap.jsx +107 -0
  212. package/components/charts/WorldMap.vue +73 -0
  213. package/components/charts/_dir.card.html +7 -0
  214. package/components/charts/chart-3d.js +84 -0
  215. package/components/charts/chart-geometry.js +223 -0
  216. package/components/charts/charts.prompt.md +24 -0
  217. package/components/data/CodeBlock.d.ts +9 -0
  218. package/components/data/CodeBlock.jsx +19 -0
  219. package/components/data/CodeBlock.vue +22 -0
  220. package/components/data/DataTable.d.ts +28 -0
  221. package/components/data/DataTable.jsx +32 -0
  222. package/components/data/DataTable.vue +25 -0
  223. package/components/data/DiffValue.d.ts +11 -0
  224. package/components/data/DiffValue.jsx +15 -0
  225. package/components/data/DiffValue.vue +13 -0
  226. package/components/data/FilterBar.d.ts +16 -0
  227. package/components/data/FilterBar.jsx +22 -0
  228. package/components/data/FilterBar.vue +23 -0
  229. package/components/data/KeyValue.d.ts +16 -0
  230. package/components/data/KeyValue.jsx +25 -0
  231. package/components/data/KeyValue.vue +26 -0
  232. package/components/data/ListRow.d.ts +14 -0
  233. package/components/data/ListRow.jsx +25 -0
  234. package/components/data/ListRow.vue +25 -0
  235. package/components/data/Pagination.d.ts +12 -0
  236. package/components/data/Pagination.jsx +27 -0
  237. package/components/data/Pagination.vue +24 -0
  238. package/components/data/SmartTable.d.ts +30 -0
  239. package/components/data/SmartTable.jsx +73 -0
  240. package/components/data/SmartTable.vue +74 -0
  241. package/components/data/Spreadsheet.d.ts +33 -0
  242. package/components/data/Spreadsheet.jsx +110 -0
  243. package/components/data/Spreadsheet.vue +95 -0
  244. package/components/data/_dir.card.html +13 -0
  245. package/components/data/data.prompt.md +33 -0
  246. package/components/display/Avatar.d.ts +21 -0
  247. package/components/display/Avatar.jsx +31 -0
  248. package/components/display/Avatar.vue +34 -0
  249. package/components/display/Badge.d.ts +27 -0
  250. package/components/display/Badge.jsx +40 -0
  251. package/components/display/Badge.vue +47 -0
  252. package/components/display/Card.d.ts +27 -0
  253. package/components/display/Card.jsx +27 -0
  254. package/components/display/Card.vue +28 -0
  255. package/components/display/Divider.d.ts +8 -0
  256. package/components/display/Divider.jsx +14 -0
  257. package/components/display/Divider.vue +13 -0
  258. package/components/display/Kbd.d.ts +7 -0
  259. package/components/display/Kbd.jsx +12 -0
  260. package/components/display/Kbd.vue +9 -0
  261. package/components/display/ProgressBar.d.ts +42 -0
  262. package/components/display/ProgressBar.jsx +70 -0
  263. package/components/display/ProgressBar.vue +69 -0
  264. package/components/display/Tag.d.ts +9 -0
  265. package/components/display/Tag.jsx +15 -0
  266. package/components/display/Tag.vue +15 -0
  267. package/components/display/_dir.card.html +13 -0
  268. package/components/display/display.prompt.md +30 -0
  269. package/components/feedback/Accordion.d.ts +13 -0
  270. package/components/feedback/Accordion.jsx +21 -0
  271. package/components/feedback/Accordion.vue +22 -0
  272. package/components/feedback/ActivityItem.d.ts +25 -0
  273. package/components/feedback/ActivityItem.jsx +39 -0
  274. package/components/feedback/ActivityItem.vue +46 -0
  275. package/components/feedback/Alert.d.ts +36 -0
  276. package/components/feedback/Alert.jsx +48 -0
  277. package/components/feedback/Alert.vue +59 -0
  278. package/components/feedback/Banner.d.ts +15 -0
  279. package/components/feedback/Banner.jsx +25 -0
  280. package/components/feedback/Banner.vue +25 -0
  281. package/components/feedback/ContextMenu.d.ts +8 -0
  282. package/components/feedback/ContextMenu.jsx +18 -0
  283. package/components/feedback/ContextMenu.vue +19 -0
  284. package/components/feedback/Drawer.d.ts +18 -0
  285. package/components/feedback/Drawer.jsx +26 -0
  286. package/components/feedback/Drawer.vue +24 -0
  287. package/components/feedback/Menu.d.ts +10 -0
  288. package/components/feedback/Menu.jsx +24 -0
  289. package/components/feedback/Menu.vue +21 -0
  290. package/components/feedback/Modal.d.ts +38 -0
  291. package/components/feedback/Modal.jsx +69 -0
  292. package/components/feedback/Modal.vue +65 -0
  293. package/components/feedback/Stepper.d.ts +9 -0
  294. package/components/feedback/Stepper.jsx +24 -0
  295. package/components/feedback/Stepper.vue +18 -0
  296. package/components/feedback/Timeline.d.ts +15 -0
  297. package/components/feedback/Timeline.jsx +27 -0
  298. package/components/feedback/Timeline.vue +21 -0
  299. package/components/feedback/_dir.card.html +13 -0
  300. package/components/feedback/feedback.prompt.md +35 -0
  301. package/components/forms/Checkbox.d.ts +13 -0
  302. package/components/forms/Checkbox.jsx +17 -0
  303. package/components/forms/Checkbox.vue +18 -0
  304. package/components/forms/Combobox.d.ts +13 -0
  305. package/components/forms/Combobox.jsx +34 -0
  306. package/components/forms/Combobox.vue +35 -0
  307. package/components/forms/DatePicker.d.ts +12 -0
  308. package/components/forms/DatePicker.jsx +48 -0
  309. package/components/forms/DatePicker.vue +46 -0
  310. package/components/forms/Dropzone.d.ts +20 -0
  311. package/components/forms/Dropzone.jsx +36 -0
  312. package/components/forms/Dropzone.vue +28 -0
  313. package/components/forms/Field.d.ts +13 -0
  314. package/components/forms/Field.jsx +13 -0
  315. package/components/forms/Field.vue +12 -0
  316. package/components/forms/Input.d.ts +18 -0
  317. package/components/forms/Input.jsx +35 -0
  318. package/components/forms/Input.vue +38 -0
  319. package/components/forms/MultiSelect.d.ts +14 -0
  320. package/components/forms/MultiSelect.jsx +33 -0
  321. package/components/forms/MultiSelect.vue +36 -0
  322. package/components/forms/NumberStepper.d.ts +13 -0
  323. package/components/forms/NumberStepper.jsx +15 -0
  324. package/components/forms/NumberStepper.vue +17 -0
  325. package/components/forms/Radio.vue +16 -0
  326. package/components/forms/RadioGroup.vue +11 -0
  327. package/components/forms/SearchInput.d.ts +12 -0
  328. package/components/forms/SearchInput.jsx +17 -0
  329. package/components/forms/SearchInput.vue +14 -0
  330. package/components/forms/Select.d.ts +14 -0
  331. package/components/forms/Select.jsx +17 -0
  332. package/components/forms/Select.vue +16 -0
  333. package/components/forms/Slider.d.ts +15 -0
  334. package/components/forms/Slider.jsx +21 -0
  335. package/components/forms/Slider.vue +23 -0
  336. package/components/forms/Switch.d.ts +31 -0
  337. package/components/forms/Switch.jsx +40 -0
  338. package/components/forms/Switch.vue +16 -0
  339. package/components/forms/_dir.card.html +13 -0
  340. package/components/forms/forms.prompt.md +23 -0
  341. package/components/icons/Icon.d.ts +19 -0
  342. package/components/icons/Icon.jsx +93 -0
  343. package/components/icons/Icon.prompt.md +8 -0
  344. package/components/icons/Icon.vue +12 -0
  345. package/components/icons/Unicon.d.ts +12 -0
  346. package/components/icons/Unicon.jsx +16 -0
  347. package/components/icons/Unicon.vue +15 -0
  348. package/components/icons/_dir.card.html +13 -0
  349. package/components/icons/feather-paths.js +2 -0
  350. package/components/icons/icon-paths.js +73 -0
  351. package/components/index.d.ts +87 -0
  352. package/components/index.js +110 -0
  353. package/components/navigation/Breadcrumb.d.ts +24 -0
  354. package/components/navigation/Breadcrumb.jsx +120 -0
  355. package/components/navigation/Breadcrumb.vue +94 -0
  356. package/components/navigation/DocTree.d.ts +18 -0
  357. package/components/navigation/DocTree.jsx +37 -0
  358. package/components/navigation/DocTree.vue +25 -0
  359. package/components/navigation/Sidebar.d.ts +29 -0
  360. package/components/navigation/Sidebar.jsx +75 -0
  361. package/components/navigation/Sidebar.vue +54 -0
  362. package/components/navigation/TopBar.d.ts +27 -0
  363. package/components/navigation/TopBar.jsx +44 -0
  364. package/components/navigation/TopBar.vue +51 -0
  365. package/components/navigation/Tree.d.ts +83 -0
  366. package/components/navigation/Tree.jsx +347 -0
  367. package/components/navigation/Tree.vue +258 -0
  368. package/components/navigation/_dir.card.html +13 -0
  369. package/components/navigation/navigation.prompt.md +22 -0
  370. package/components/pipeline/ConfigPanel.d.ts +18 -0
  371. package/components/pipeline/ConfigPanel.jsx +30 -0
  372. package/components/pipeline/ConfigPanel.vue +32 -0
  373. package/components/pipeline/DataPreview.d.ts +14 -0
  374. package/components/pipeline/DataPreview.jsx +24 -0
  375. package/components/pipeline/DataPreview.vue +21 -0
  376. package/components/pipeline/NodeCard.d.ts +47 -0
  377. package/components/pipeline/NodeCard.jsx +97 -0
  378. package/components/pipeline/NodeCard.vue +75 -0
  379. package/components/pipeline/PaletteItem.vue +15 -0
  380. package/components/pipeline/PaletteListItem.vue +15 -0
  381. package/components/pipeline/PalettePanel.d.ts +20 -0
  382. package/components/pipeline/PalettePanel.jsx +74 -0
  383. package/components/pipeline/PalettePanel.vue +45 -0
  384. package/components/pipeline/PipelineCanvas.d.ts +25 -0
  385. package/components/pipeline/PipelineCanvas.jsx +94 -0
  386. package/components/pipeline/PipelineCanvas.vue +73 -0
  387. package/components/pipeline/ZoomControls.d.ts +9 -0
  388. package/components/pipeline/ZoomControls.jsx +15 -0
  389. package/components/pipeline/ZoomControls.vue +15 -0
  390. package/components/pipeline/_dir.card.html +13 -0
  391. package/components/pipeline/pipeline.prompt.md +22 -0
  392. package/components/tabs/SegmentedControl.d.ts +11 -0
  393. package/components/tabs/SegmentedControl.jsx +21 -0
  394. package/components/tabs/SegmentedControl.vue +15 -0
  395. package/components/tabs/Tabs.d.ts +13 -0
  396. package/components/tabs/Tabs.jsx +52 -0
  397. package/components/tabs/Tabs.vue +39 -0
  398. package/components/tabs/ViewToggle.d.ts +8 -0
  399. package/components/tabs/ViewToggle.jsx +12 -0
  400. package/components/tabs/ViewToggle.vue +12 -0
  401. package/components/tabs/_dir.card.html +13 -0
  402. package/components/tabs/tabs.prompt.md +14 -0
  403. package/package.json +76 -0
  404. package/readme.md +150 -0
  405. package/styles.css +10 -0
  406. package/tokens/alpha.css +27 -0
  407. package/tokens/charts.css +55 -0
  408. package/tokens/colors-extended.css +40 -0
  409. package/tokens/colors.css +39 -0
  410. package/tokens/dark.css +96 -0
  411. package/tokens/fonts.css +4 -0
  412. package/tokens/interaction.css +48 -0
  413. package/tokens/layout.css +30 -0
  414. package/tokens/motion.css +22 -0
  415. package/tokens/tokens.d.ts +680 -0
  416. package/tokens/tokens.js +1018 -0
  417. package/tokens/tokens.json +2928 -0
  418. package/tokens/typography.css +22 -0
@@ -0,0 +1,25 @@
1
+ <script setup>
2
+ import { ref } from 'vue'
3
+ import Icon from '../icons/Icon.vue'
4
+ import { IconTile } from '../display/Badge.vue'
5
+ // Expandable workflow list row: chevron + bordered 32px icon tile + title/meta + right count.
6
+ defineProps({ icon: { type: String, default: 'file-text' }, title: String, meta: String, trailing: [String, Number], expanded: Boolean, style: Object })
7
+ defineEmits(['click'])
8
+ const hover = ref(false)
9
+ </script>
10
+ <template>
11
+ <div :style="{ borderBottom: '1px solid var(--border-card)', ...style }">
12
+ <div @click="$emit('click')" @mouseenter="hover = true" @mouseleave="hover = false" :style="{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', cursor: 'pointer', background: hover ? 'var(--bg-inset)' : 'transparent', transition: 'var(--t-hover)' }">
13
+ <div style="display:flex;align-items:center;gap:12px">
14
+ <span :style="{ color: 'var(--text-muted)', transform: expanded ? 'rotate(90deg)' : 'none', transition: 'transform .15s ease', display: 'inline-flex' }"><Icon name="chevron-right" :size="16" /></span>
15
+ <IconTile :icon="icon" kind="row" />
16
+ <div style="display:flex;flex-direction:column">
17
+ <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)">{{ title }}</span>
18
+ <span v-if="meta" style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)">{{ meta }}</span>
19
+ </div>
20
+ </div>
21
+ <span v-if="trailing" style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)">{{ trailing }}</span>
22
+ </div>
23
+ <div v-if="expanded" style="padding:0 16px 12px 72px"><slot /></div>
24
+ </div>
25
+ </template>
@@ -0,0 +1,12 @@
1
+ import * as React from "react";
2
+ /** Table pagination (26px controls, r4). Addition — not in the source file. */
3
+ export interface PaginationProps {
4
+ page?: number;
5
+ pageCount?: number;
6
+ /** Total rows; shows an "1–20 of 128" range label when given */
7
+ total?: number;
8
+ pageSize?: number;
9
+ onChange?: (page: number) => void;
10
+ style?: React.CSSProperties;
11
+ }
12
+ export declare function Pagination(props: PaginationProps): JSX.Element;
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import { Icon } from "../icons/Icon.jsx";
3
+
4
+ /** Table pagination — 12px range label + prev/next + numbered pages. */
5
+ export function Pagination({ page = 1, pageCount = 1, total, pageSize = 20, onChange, style }) {
6
+ const btn = (content, disabled, onClick, active) => (
7
+ <button type="button" disabled={disabled} onClick={onClick}
8
+ style={{ minWidth: 26, height: 26, padding: "0 6px", borderRadius: "var(--radius-sm)", border: `1px solid ${active ? "var(--action-primary)" : "var(--border-control)"}`, background: active ? "var(--action-primary)" : "var(--bg-surface)", color: active ? "var(--on-accent)" : "var(--text-body)", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", cursor: disabled ? "default" : "pointer", opacity: disabled ? .45 : 1, display: "inline-flex", alignItems: "center", justifyContent: "center", transition: "var(--t-hover)" }}>
9
+ {content}
10
+ </button>
11
+ );
12
+ const pages = [];
13
+ const from = Math.max(1, Math.min(page - 1, pageCount - 2));
14
+ for (let i = from; i < from + 3 && i <= pageCount; i++) pages.push(i);
15
+ return (
16
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, ...style }}>
17
+ <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", color: "var(--text-secondary)" }}>
18
+ {total != null ? `${(page - 1) * pageSize + 1}–${Math.min(page * pageSize, total)} of ${total}` : `Page ${page} of ${pageCount}`}
19
+ </span>
20
+ <div style={{ display: "flex", gap: 4 }}>
21
+ {btn(<Icon name="chevron-left" size={13} />, page <= 1, () => onChange && onChange(page - 1))}
22
+ {pages.map((p) => <React.Fragment key={p}>{btn(p, false, () => onChange && onChange(p), p === page)}</React.Fragment>)}
23
+ {btn(<Icon name="chevron-right" size={13} />, page >= pageCount, () => onChange && onChange(page + 1))}
24
+ </div>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,24 @@
1
+ <script setup>
2
+ import { computed } from 'vue'
3
+ import Icon from '../icons/Icon.vue'
4
+ // Table pagination — 12px range label + prev/next + numbered pages.
5
+ const props = defineProps({ page: { type: Number, default: 1 }, pageCount: { type: Number, default: 1 }, total: Number, pageSize: { type: Number, default: 20 }, style: Object })
6
+ const emit = defineEmits(['change'])
7
+ const pages = computed(() => {
8
+ const from = Math.max(1, Math.min(props.page - 1, props.pageCount - 2)), out = []
9
+ for (let i = from; i < from + 3 && i <= props.pageCount; i++) out.push(i)
10
+ return out
11
+ })
12
+ const label = computed(() => props.total != null ? `${(props.page - 1) * props.pageSize + 1}–${Math.min(props.page * props.pageSize, props.total)} of ${props.total}` : `Page ${props.page} of ${props.pageCount}`)
13
+ function btnStyle(active, disabled) { return { minWidth: 26, height: 26, padding: '0 6px', borderRadius: 'var(--radius-sm)', border: `1px solid ${active ? 'var(--action-primary)' : 'var(--border-control)'}`, background: active ? 'var(--action-primary)' : 'var(--bg-surface)', color: active ? 'var(--on-accent)' : 'var(--text-body)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', cursor: disabled ? 'default' : 'pointer', opacity: disabled ? .45 : 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', transition: 'var(--t-hover)' } }
14
+ </script>
15
+ <template>
16
+ <div :style="{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, ...style }">
17
+ <span style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)">{{ label }}</span>
18
+ <div style="display:flex;gap:4px">
19
+ <button type="button" :disabled="page <= 1" @click="emit('change', page - 1)" :style="btnStyle(false, page <= 1)"><Icon name="chevron-left" :size="13" /></button>
20
+ <button v-for="p in pages" :key="p" type="button" @click="emit('change', p)" :style="btnStyle(p === page, false)">{{ p }}</button>
21
+ <button type="button" :disabled="page >= pageCount" @click="emit('change', page + 1)" :style="btnStyle(false, page >= pageCount)"><Icon name="chevron-right" :size="13" /></button>
22
+ </div>
23
+ </div>
24
+ </template>
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+
3
+ export interface SmartTableField {
4
+ key: string;
5
+ label: string;
6
+ /** Cell type: text | longText | number | select | multiSelect | date | person | checkbox | url | progress */
7
+ type?: string;
8
+ /** Column width px (default 140) */
9
+ width?: number;
10
+ /** select/multiSelect: option label → hue (gray|blue|green|orange|amber|teal|cyan|purple|pink|red) */
11
+ options?: Record<string, string>;
12
+ /** Footer aggregate: count | sum | avg */
13
+ aggregate?: "count" | "sum" | "avg";
14
+ /** Primary field renders medium-weight */
15
+ primary?: boolean;
16
+ }
17
+
18
+ /** Notion/Airtable-style database grid: field-type header icons, hue pills, person/date/checkbox/url/progress cells, row hover + expand, inline editing, + New row, footer aggregates, view toolbar. */
19
+ export interface SmartTableProps {
20
+ fields?: SmartTableField[];
21
+ rows?: Record<string, any>[];
22
+ /** View name in the toolbar (default "Grid view") */
23
+ title?: string;
24
+ toolbar?: boolean;
25
+ /** Scroll-area max height */
26
+ height?: number | string;
27
+ rowHeight?: number;
28
+ onChange?: (rows: object[]) => void;
29
+ style?: React.CSSProperties;
30
+ }
@@ -0,0 +1,73 @@
1
+ import React from "react";
2
+ import { Icon } from "../icons/Icon.jsx";
3
+ import { IconButton } from "../buttons/IconButton.jsx";
4
+ import { Checkbox } from "../forms/Checkbox.jsx";
5
+ import { Avatar } from "../display/Avatar.jsx";
6
+ import { SearchInput } from "../forms/SearchInput.jsx";
7
+
8
+ const typeIcon = { text: "type", longText: "align-left", number: "hash", select: "chevron-down", multiSelect: "tag", date: "calendar", person: "user", checkbox: "check-square", url: "link", progress: "bar-chart-2" };
9
+ const Pill = ({ hue = "gray", children }) => <span style={{ display: "inline-flex", alignItems: "center", height: 20, padding: "0 8px", borderRadius: "var(--radius-full)", background: `var(--hue-${hue}-bg)`, color: `var(--hue-${hue}-fg)`, fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-xs)", lineHeight: 1, whiteSpace: "nowrap" }}>{children}</span>;
10
+
11
+ /** Notion/Airtable-style database grid ("smart table"): field-type icons in headers, colored select pills, person/date/checkbox/url/progress cells, row hover + expand, inline text/number editing, + New row, add-field column, per-column footer aggregates, and a view toolbar (Hide fields · Filter · Group · Sort · search). */
12
+ export function SmartTable({ fields = [], rows = [], title = "Grid view", toolbar = true, height, rowHeight = 32, onChange, style }) {
13
+ const [data, setData] = React.useState(rows);
14
+ const [hover, setHover] = React.useState(-1);
15
+ const [editing, setEditing] = React.useState(null);
16
+ const [draft, setDraft] = React.useState("");
17
+ const setCell = (r, key, v) => { const next = data.map((row, i) => i === r ? { ...row, [key]: v } : row); setData(next); onChange && onChange(next); };
18
+ const widths = ["40px", ...fields.map(f => (f.width || 140) + "px"), "40px"];
19
+ const grid = { display: "grid", gridTemplateColumns: widths.join(" "), width: "fit-content", minWidth: "100%" };
20
+ const cellBase = { display: "flex", alignItems: "center", gap: 5, padding: "0 8px", borderRight: "1px solid var(--border-subtle)", borderBottom: "1px solid var(--border-subtle)", height: rowHeight, boxSizing: "border-box", overflow: "hidden", whiteSpace: "nowrap", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: "var(--text-primary)" };
21
+ const agg = (f) => {
22
+ if (!f.aggregate) return "";
23
+ const vals = data.map(r => r[f.key]);
24
+ if (f.aggregate === "count") return "COUNT " + vals.filter(v => v === true || (v !== "" && v != null && v !== false)).length;
25
+ const nums = vals.map(Number).filter(n => !isNaN(n));
26
+ if (f.aggregate === "sum") return "SUM " + nums.reduce((a, b) => a + b, 0);
27
+ if (f.aggregate === "avg") return "AVG " + (nums.length ? Math.round(nums.reduce((a, b) => a + b, 0) / nums.length * 10) / 10 : 0);
28
+ return "";
29
+ };
30
+ const renderCell = (f, row, r) => {
31
+ const v = row[f.key];
32
+ const isEdit = editing && editing.r === r && editing.key === f.key;
33
+ if (isEdit) return <input autoFocus value={draft} onChange={(e) => setDraft(e.target.value)} onBlur={() => { setCell(r, f.key, f.type === "number" && draft !== "" && !isNaN(Number(draft)) ? Number(draft) : draft); setEditing(null); }} onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") setEditing(null); }} style={{ position: "absolute", inset: 0, border: "2px solid var(--action-primary)", borderRadius: 2, outline: "none", padding: "0 6px", font: "inherit", background: "var(--bg-surface)", color: "var(--text-primary)", boxSizing: "border-box" }} />;
34
+ switch (f.type) {
35
+ case "select": { const hue = (f.options || {})[v] || "gray"; return v ? <Pill hue={hue}>{v}</Pill> : null; }
36
+ case "multiSelect": return <span style={{ display: "flex", gap: 4, overflow: "hidden" }}>{(v || []).map(t => <Pill key={t} hue={(f.options || {})[t] || "gray"}>{t}</Pill>)}</span>;
37
+ case "person": return v ? <span style={{ display: "inline-flex", alignItems: "center", gap: 6, minWidth: 0 }}><Avatar name={v} size={20} /><span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{v}</span></span> : null;
38
+ case "checkbox": return <Checkbox size="sm" checked={!!v} onChange={(nv) => setCell(r, f.key, nv)} />;
39
+ case "url": return v ? <a href={"https://" + v} onClick={(e) => e.preventDefault()} style={{ color: "var(--link)", overflow: "hidden", textOverflow: "ellipsis" }}>{v}</a> : null;
40
+ case "progress": return <span style={{ display: "flex", alignItems: "center", gap: 6, width: "100%" }}><span style={{ flex: 1, height: 4, borderRadius: "var(--radius-full)", background: "var(--track)", overflow: "hidden" }}><span style={{ display: "block", width: (v || 0) + "%", height: "100%", background: "var(--chart-blue)" }}></span></span><span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--text-2xs)", color: "var(--text-secondary)" }}>{v || 0}%</span></span>;
41
+ case "number": return <span style={{ marginLeft: "auto", fontFamily: "var(--font-mono)", fontSize: 11.5 }}>{v}</span>;
42
+ default: return <span style={{ overflow: "hidden", textOverflow: "ellipsis", fontWeight: f.primary ? "var(--weight-medium)" : "var(--weight-regular)", color: f.primary ? "var(--text-heading)" : "var(--text-primary)" }}>{v}</span>;
43
+ }
44
+ };
45
+ const tbBtn = (icon, label) => <button key={label} type="button" style={{ display: "inline-flex", alignItems: "center", gap: 5, height: 26, padding: "0 8px", border: "none", borderRadius: "var(--radius-sm)", background: "transparent", cursor: "pointer", color: "var(--text-body)", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)" }} onMouseEnter={(e) => e.currentTarget.style.background = "var(--bg-hover)"} onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}><Icon name={icon} size={13} />{label}</button>;
46
+ return (
47
+ <div style={{ border: "1px solid var(--border-control)", borderRadius: "var(--radius-lg)", background: "var(--bg-surface)", overflow: "hidden", boxSizing: "border-box", ...style }}>
48
+ {toolbar && <div style={{ display: "flex", alignItems: "center", gap: 4, padding: "6px 10px", borderBottom: "1px solid var(--border-subtle)" }}>
49
+ <button type="button" style={{ display: "inline-flex", alignItems: "center", gap: 6, height: 26, padding: "0 8px", border: "none", borderRadius: "var(--radius-sm)", background: "var(--bg-active)", cursor: "pointer", color: "var(--text-heading)", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-sm)" }}><Icon name="layout-grid" size={13} />{title}<Icon name="chevron-down" size={12} /></button>
50
+ <span style={{ width: 1, height: 16, background: "var(--border-control)", margin: "0 4px" }}></span>
51
+ {tbBtn("eye-off", "Hide fields")}{tbBtn("funnel", "Filter")}{tbBtn("layers", "Group")}{tbBtn("arrow-down", "Sort")}
52
+ <span style={{ flex: 1 }}></span>
53
+ <SearchInput bare placeholder="Search..." style={{ width: 140, gap: 6 }} />
54
+ </div>}
55
+ <div style={{ overflow: "auto", maxHeight: height }}>
56
+ <div style={grid}>
57
+ <span style={{ ...cellBase, background: "var(--bg-inset)", justifyContent: "center" }}></span>
58
+ {fields.map(f => <span key={f.key} style={{ ...cellBase, background: "var(--bg-inset)", color: "var(--text-secondary)", fontSize: "var(--text-xs)", fontWeight: "var(--weight-medium)", letterSpacing: "var(--tracking-xs)" }}><Icon name={typeIcon[f.type] || "type"} size={12} style={{ color: "var(--text-muted)" }} />{f.label}</span>)}
59
+ <span style={{ ...cellBase, background: "var(--bg-inset)", justifyContent: "center" }}><IconButton icon="plus" size="xs" muted label="Add field" /></span>
60
+ {data.map((row, r) => [
61
+ <span key={"n" + r} onMouseEnter={() => setHover(r)} onMouseLeave={() => setHover(-1)} style={{ ...cellBase, justifyContent: "center", color: "var(--text-muted)", fontFamily: "var(--font-mono)", fontSize: "var(--text-2xs)", background: hover === r ? "var(--bg-inset)" : "transparent", cursor: "pointer" }}>{hover === r ? <Icon name="maximize-2" size={12} style={{ color: "var(--action-primary)" }} /> : r + 1}</span>,
62
+ ...fields.map(f => <span key={r + f.key} onMouseEnter={() => setHover(r)} onMouseLeave={() => setHover(-1)} onDoubleClick={() => { if (f.type === "text" || f.type === "number" || f.type === "date" || f.type === "url") { setEditing({ r, key: f.key }); setDraft(String(row[f.key] == null ? "" : row[f.key])); } }} style={{ ...cellBase, position: "relative", background: hover === r ? "var(--bg-inset)" : "transparent" }}>{renderCell(f, row, r)}</span>),
63
+ <span key={"a" + r} style={{ ...cellBase, background: hover === r ? "var(--bg-inset)" : "transparent" }}></span>,
64
+ ])}
65
+ <button type="button" onClick={() => { const empty = {}; fields.forEach(f => empty[f.key] = f.type === "multiSelect" ? [] : f.type === "checkbox" ? false : ""); const next = [...data, empty]; setData(next); onChange && onChange(next); }} style={{ gridColumn: "1 / -1", display: "flex", alignItems: "center", gap: 6, height: rowHeight, padding: "0 10px", border: "none", borderBottom: "1px solid var(--border-subtle)", background: "transparent", cursor: "pointer", color: "var(--text-secondary)", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", textAlign: "left" }} onMouseEnter={(e) => e.currentTarget.style.background = "var(--bg-inset)"} onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}><Icon name="plus" size={13} />New</button>
66
+ <span style={{ ...cellBase, borderBottom: "none", background: "var(--bg-inset)" }}></span>
67
+ {fields.map(f => <span key={"g" + f.key} style={{ ...cellBase, borderBottom: "none", background: "var(--bg-inset)", justifyContent: "flex-end", fontFamily: "var(--font-mono)", fontSize: "var(--text-2xs)", color: "var(--text-secondary)" }}>{agg(f)}</span>)}
68
+ <span style={{ ...cellBase, borderBottom: "none", background: "var(--bg-inset)" }}></span>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,74 @@
1
+ <script setup>
2
+ import { ref, computed } from 'vue'
3
+ import Icon from '../icons/Icon.vue'
4
+ import IconButton from '../buttons/IconButton.vue'
5
+ import Checkbox from '../forms/Checkbox.vue'
6
+ import Avatar from '../display/Avatar.vue'
7
+ import SearchInput from '../forms/SearchInput.vue'
8
+ // Notion/Airtable-style database grid: field-type header icons, hue pills, person/date/checkbox/url/progress cells, row hover + expand, inline editing, + New row, footer aggregates, view toolbar.
9
+ const typeIcon = { text: 'type', longText: 'align-left', number: 'hash', select: 'chevron-down', multiSelect: 'tag', date: 'calendar', person: 'user', checkbox: 'check-square', url: 'link', progress: 'bar-chart-2' }
10
+ const props = defineProps({ fields: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, title: { type: String, default: 'Grid view' }, toolbar: { type: Boolean, default: true }, height: [Number, String], rowHeight: { type: Number, default: 32 }, style: Object })
11
+ const emit = defineEmits(['change'])
12
+ const data = ref(props.rows.map(r => ({ ...r })))
13
+ const hover = ref(-1), editing = ref(null), draft = ref('')
14
+ const gridCols = computed(() => ['40px', ...props.fields.map(f => (f.width || 140) + 'px'), '40px'].join(' '))
15
+ const cellBase = { display: 'flex', alignItems: 'center', gap: '5px', padding: '0 8px', borderRight: '1px solid var(--border-subtle)', borderBottom: '1px solid var(--border-subtle)', boxSizing: 'border-box', overflow: 'hidden', whiteSpace: 'nowrap', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', color: 'var(--text-primary)' }
16
+ function setCell(r, key, v) { data.value[r][key] = v; emit('change', data.value) }
17
+ function commitEdit(r, f) { setCell(r, f.key, f.type === 'number' && draft.value !== '' && !isNaN(Number(draft.value)) ? Number(draft.value) : draft.value); editing.value = null }
18
+ function startEdit(r, f, row) { if (['text', 'number', 'date', 'url'].includes(f.type || 'text')) { editing.value = { r, key: f.key }; draft.value = String(row[f.key] == null ? '' : row[f.key]) } }
19
+ function addRow() { const empty = {}; props.fields.forEach(f => empty[f.key] = f.type === 'multiSelect' ? [] : f.type === 'checkbox' ? false : ''); data.value.push(empty); emit('change', data.value) }
20
+ function agg(f) {
21
+ if (!f.aggregate) return ''
22
+ const vals = data.value.map(r => r[f.key])
23
+ if (f.aggregate === 'count') return 'COUNT ' + vals.filter(v => v === true || (v !== '' && v != null && v !== false)).length
24
+ const nums = vals.map(Number).filter(n => !isNaN(n))
25
+ if (f.aggregate === 'sum') return 'SUM ' + nums.reduce((a, b) => a + b, 0)
26
+ return 'AVG ' + (nums.length ? Math.round(nums.reduce((a, b) => a + b, 0) / nums.length * 10) / 10 : 0)
27
+ }
28
+ </script>
29
+ <template>
30
+ <div :style="{ border: '1px solid var(--border-control)', borderRadius: 'var(--radius-lg)', background: 'var(--bg-surface)', overflow: 'hidden', boxSizing: 'border-box', ...style }">
31
+ <div v-if="toolbar" style="display:flex;align-items:center;gap:4px;padding:6px 10px;border-bottom:1px solid var(--border-subtle)">
32
+ <button type="button" style="display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 8px;border:none;border-radius:var(--radius-sm);background:var(--bg-active);cursor:pointer;color:var(--text-heading);font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm)"><Icon name="layout-grid" :size="13" />{{ title }}<Icon name="chevron-down" :size="12" /></button>
33
+ <span style="width:1px;height:16px;background:var(--border-control);margin:0 4px" />
34
+ <button v-for="b in [['eye-off','Hide fields'],['funnel','Filter'],['layers','Group'],['arrow-down','Sort']]" :key="b[1]" type="button" class="kb-st-btn"><Icon :name="b[0]" :size="13" />{{ b[1] }}</button>
35
+ <span style="flex:1" />
36
+ <SearchInput bare placeholder="Search..." style="width:140px;gap:6px" />
37
+ </div>
38
+ <div :style="{ overflow: 'auto', maxHeight: typeof height === 'number' ? height + 'px' : height }">
39
+ <div :style="{ display: 'grid', gridTemplateColumns: gridCols, width: 'fit-content', minWidth: '100%' }">
40
+ <span :style="{ ...cellBase, height: rowHeight + 'px', background: 'var(--bg-inset)' }" />
41
+ <span v-for="f in fields" :key="f.key" :style="{ ...cellBase, height: rowHeight + 'px', background: 'var(--bg-inset)', color: 'var(--text-secondary)', fontSize: 'var(--text-xs)', fontWeight: 'var(--weight-medium)' }"><Icon :name="typeIcon[f.type] || 'type'" :size="12" style="color:var(--text-muted)" />{{ f.label }}</span>
42
+ <span :style="{ ...cellBase, height: rowHeight + 'px', background: 'var(--bg-inset)', justifyContent: 'center' }"><IconButton icon="plus" size="xs" muted label="Add field" /></span>
43
+ <template v-for="(row, r) in data" :key="r">
44
+ <span @mouseenter="hover = r" @mouseleave="hover = -1" :style="{ ...cellBase, height: rowHeight + 'px', justifyContent: 'center', color: 'var(--text-muted)', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-2xs)', background: hover === r ? 'var(--bg-inset)' : 'transparent', cursor: 'pointer' }">
45
+ <Icon v-if="hover === r" name="maximize-2" :size="12" style="color:var(--action-primary)" /><template v-else>{{ r + 1 }}</template>
46
+ </span>
47
+ <span v-for="f in fields" :key="r + f.key" @mouseenter="hover = r" @mouseleave="hover = -1" @dblclick="startEdit(r, f, row)" :style="{ ...cellBase, height: rowHeight + 'px', position: 'relative', background: hover === r ? 'var(--bg-inset)' : 'transparent' }">
48
+ <input v-if="editing && editing.r === r && editing.key === f.key" autofocus :value="draft" @input="draft = $event.target.value" @blur="commitEdit(r, f)" @keydown.enter="$event.target.blur()" @keydown.esc="editing = null" style="position:absolute;inset:0;border:2px solid var(--action-primary);border-radius:2px;outline:none;padding:0 6px;font:inherit;background:var(--bg-surface);color:var(--text-primary);box-sizing:border-box" />
49
+ <template v-else-if="f.type === 'select'"><span v-if="row[f.key]" class="kb-pill" :style="{ background: `var(--hue-${(f.options || {})[row[f.key]] || 'gray'}-bg)`, color: `var(--hue-${(f.options || {})[row[f.key]] || 'gray'}-fg)` }">{{ row[f.key] }}</span></template>
50
+ <template v-else-if="f.type === 'multiSelect'"><span style="display:flex;gap:4px;overflow:hidden"><span v-for="t in row[f.key] || []" :key="t" class="kb-pill" :style="{ background: `var(--hue-${(f.options || {})[t] || 'gray'}-bg)`, color: `var(--hue-${(f.options || {})[t] || 'gray'}-fg)` }">{{ t }}</span></span></template>
51
+ <template v-else-if="f.type === 'person'"><span v-if="row[f.key]" style="display:inline-flex;align-items:center;gap:6px;min-width:0"><Avatar :name="row[f.key]" :size="20" /><span style="overflow:hidden;text-overflow:ellipsis">{{ row[f.key] }}</span></span></template>
52
+ <template v-else-if="f.type === 'checkbox'"><Checkbox size="sm" :checked="!!row[f.key]" @change="v => setCell(r, f.key, v)" /></template>
53
+ <template v-else-if="f.type === 'url'"><a v-if="row[f.key]" href="#" @click.prevent style="overflow:hidden;text-overflow:ellipsis">{{ row[f.key] }}</a></template>
54
+ <template v-else-if="f.type === 'progress'"><span style="display:flex;align-items:center;gap:6px;width:100%"><span style="flex:1;height:4px;border-radius:var(--radius-full);background:var(--track);overflow:hidden"><span :style="{ display: 'block', width: (row[f.key] || 0) + '%', height: '100%', background: 'var(--chart-blue)' }" /></span><span style="font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--text-secondary)">{{ row[f.key] || 0 }}%</span></span></template>
55
+ <template v-else-if="f.type === 'number'"><span style="margin-left:auto;font-family:var(--font-mono);font-size:11.5px">{{ row[f.key] }}</span></template>
56
+ <template v-else><span :style="{ overflow: 'hidden', textOverflow: 'ellipsis', fontWeight: f.primary ? 'var(--weight-medium)' : 'var(--weight-regular)', color: f.primary ? 'var(--text-heading)' : 'var(--text-primary)' }">{{ row[f.key] }}</span></template>
57
+ </span>
58
+ <span :style="{ ...cellBase, height: rowHeight + 'px', background: hover === r ? 'var(--bg-inset)' : 'transparent' }" />
59
+ </template>
60
+ <button type="button" @click="addRow" class="kb-st-new" :style="{ gridColumn: '1 / -1', height: rowHeight + 'px' }"><Icon name="plus" :size="13" />New</button>
61
+ <span :style="{ ...cellBase, height: rowHeight + 'px', borderBottom: 'none', background: 'var(--bg-inset)' }" />
62
+ <span v-for="f in fields" :key="'g' + f.key" :style="{ ...cellBase, height: rowHeight + 'px', borderBottom: 'none', background: 'var(--bg-inset)', justifyContent: 'flex-end', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-2xs)', color: 'var(--text-secondary)' }">{{ agg(f) }}</span>
63
+ <span :style="{ ...cellBase, height: rowHeight + 'px', borderBottom: 'none', background: 'var(--bg-inset)' }" />
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </template>
68
+ <style scoped>
69
+ .kb-pill{display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:var(--radius-full);font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-xs);line-height:1;white-space:nowrap}
70
+ .kb-st-btn{display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 8px;border:none;border-radius:var(--radius-sm);background:transparent;cursor:pointer;color:var(--text-body);font-family:var(--font-sans);font-size:var(--text-sm)}
71
+ .kb-st-btn:hover{background:var(--bg-hover)}
72
+ .kb-st-new{display:flex;align-items:center;gap:6px;padding:0 10px;border:none;border-bottom:1px solid var(--border-subtle);background:transparent;cursor:pointer;color:var(--text-secondary);font-family:var(--font-sans);font-size:var(--text-sm);text-align:left}
73
+ .kb-st-new:hover{background:var(--bg-inset)}
74
+ </style>
@@ -0,0 +1,33 @@
1
+ import * as React from "react";
2
+
3
+ export type SpreadsheetCell = string | number | {
4
+ /** Literal value */
5
+ v?: string | number;
6
+ /** Formula string, e.g. "=SUM(B2:E2)" — supports SUM/AVERAGE/COUNT/MIN/MAX over a range plus + - * / with cell refs */
7
+ f?: string;
8
+ /** Display format */
9
+ t?: "number" | "currency" | "percent";
10
+ /** Bold / italic / underline */
11
+ b?: boolean; i?: boolean; u?: boolean;
12
+ align?: "flex-start" | "center" | "flex-end";
13
+ };
14
+
15
+ /** Excel-style spreadsheet: toolbar (undo/redo, B/I/U, format), formula bar with name box + fx, frozen lettered/numbered headers, click + shift-click range selection, double-click cell editing, live formula evaluation, sheet tabs, Sum/Avg/Count status bar. */
16
+ export interface SpreadsheetProps {
17
+ /** 2D cell array (row-major). Primitives or cell objects. */
18
+ data?: SpreadsheetCell[][];
19
+ /** Minimum grid size (padded with empty cells) */
20
+ rows?: number; cols?: number;
21
+ /** Per-column pixel widths; default 92 */
22
+ colWidths?: number[]; defaultColWidth?: number;
23
+ /** Currency symbol for t:"currency" cells */
24
+ currency?: string;
25
+ /** Initially selected cell ref, e.g. "F4" */
26
+ defaultActive?: string;
27
+ sheets?: string[]; activeSheet?: string;
28
+ toolbar?: boolean; formulaBar?: boolean; statusBar?: boolean;
29
+ /** Scroll-area max height */
30
+ height?: number | string;
31
+ onChange?: (cells: object[][]) => void;
32
+ style?: React.CSSProperties;
33
+ }
@@ -0,0 +1,110 @@
1
+ import React from "react";
2
+ import { Icon } from "../icons/Icon.jsx";
3
+ import { IconButton } from "../buttons/IconButton.jsx";
4
+ import { Select } from "../forms/Select.jsx";
5
+
6
+ export 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; };
7
+ const norm = (cell) => cell == null ? { v: "" } : (typeof cell === "object" ? { ...cell } : { v: cell });
8
+ const refRC = (ref) => { const m = /^([A-Z]+)(\d+)$/.exec(String(ref).trim().toUpperCase()); if (!m) return null; let c = 0; for (const ch of m[1]) c = c * 26 + (ch.charCodeAt(0) - 64); return { r: +m[2] - 1, c: c - 1 }; };
9
+
10
+ function evalCell(cells, cell, depth = 0) {
11
+ if (!cell || depth > 12) return "";
12
+ const raw = cell.f != null ? cell.f : cell.v;
13
+ if (typeof raw === "string" && raw.startsWith("=")) return evalFormula(cells, raw, depth);
14
+ return raw;
15
+ }
16
+ function numAt(cells, r, c, depth) { const v = evalCell(cells, cells[r] && cells[r][c], depth + 1); const n = typeof v === "number" ? v : parseFloat(String(v).replace(/[^0-9.eE+-]/g, "")); return isNaN(n) ? null : n; }
17
+ function evalFormula(cells, f, depth = 0) {
18
+ try {
19
+ let expr = f.slice(1).toUpperCase().replace(/\s+/g, "");
20
+ expr = expr.replace(/(SUM|AVERAGE|AVG|COUNT|MIN|MAX)\(([A-Z]+\d+):([A-Z]+\d+)\)/g, (m, fn, a, b) => {
21
+ const A = refRC(a), B = refRC(b), vals = [];
22
+ for (let r = Math.min(A.r, B.r); r <= Math.max(A.r, B.r); r++) for (let c = Math.min(A.c, B.c); c <= Math.max(A.c, B.c); c++) { const n = numAt(cells, r, c, depth); if (n != null) vals.push(n); }
23
+ if (fn === "COUNT") return String(vals.length);
24
+ if (!vals.length) return "0";
25
+ if (fn === "SUM") return String(vals.reduce((x, y) => x + y, 0));
26
+ if (fn === "MIN") return String(Math.min(...vals));
27
+ if (fn === "MAX") return String(Math.max(...vals));
28
+ return String(vals.reduce((x, y) => x + y, 0) / vals.length);
29
+ });
30
+ expr = expr.replace(/[A-Z]+\d+/g, (ref) => { const p = refRC(ref); const n = p ? numAt(cells, p.r, p.c, depth) : null; return String(n == null ? 0 : n); });
31
+ if (!/^[-+*/().\d\seE]*$/.test(expr)) return "#ERROR";
32
+ const out = Function('"use strict";return (' + expr + ")")();
33
+ return typeof out === "number" && isFinite(out) ? out : "#ERROR";
34
+ } catch (e) { return "#ERROR"; }
35
+ }
36
+ function fmt(val, t, currency) {
37
+ if (val === "" || val == null) return "";
38
+ if (typeof val !== "number") return String(val);
39
+ const r2 = Math.round(val * 100) / 100;
40
+ if (t === "percent") return (Math.round(val * 1000) / 10) + "%";
41
+ if (t === "currency") return currency + r2.toLocaleString();
42
+ return r2.toLocaleString();
43
+ }
44
+
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 }) {
47
+ const nR = Math.max(rows || 0, data.length, 8), nC = Math.max(cols || 0, ...data.map(r => r.length), 6);
48
+ const [cells, setCells] = React.useState(() => Array.from({ length: nR }, (_, r) => Array.from({ length: nC }, (_, c) => norm(data[r] && data[r][c]))));
49
+ const a0 = refRC(defaultActive) || { r: 0, c: 0 };
50
+ const [active, setActive] = React.useState(a0);
51
+ const [anchor, setAnchor] = React.useState(a0);
52
+ const [editing, setEditing] = React.useState(null);
53
+ const [draft, setDraft] = React.useState("");
54
+ const [sheet, setSheet] = React.useState(activeSheet || sheets[0]);
55
+ const cur = cells[active.r][active.c];
56
+ const rawOf = (cell) => cell.f != null ? cell.f : (cell.v == null ? "" : String(cell.v));
57
+ const commit = (r, c, text) => { const next = cells.map(row => row.map(cell => ({ ...cell }))); const cell = next[r][c]; delete cell.f; if (String(text).startsWith("=")) { cell.f = text; cell.v = ""; } else { const n = text !== "" && !isNaN(Number(text)) ? Number(text) : text; cell.v = n; } setCells(next); onChange && onChange(next); };
58
+ const lo = { r: Math.min(active.r, anchor.r), c: Math.min(active.c, anchor.c) }, hi = { r: Math.max(active.r, anchor.r), c: Math.max(active.c, anchor.c) };
59
+ const multi = lo.r !== hi.r || lo.c !== hi.c;
60
+ const inRange = (r, c) => r >= lo.r && r <= hi.r && c >= lo.c && c <= hi.c;
61
+ const stats = React.useMemo(() => { if (!multi) return null; const vals = []; for (let r = lo.r; r <= hi.r; r++) for (let c = lo.c; c <= hi.c; c++) { const v = evalCell(cells, cells[r][c]); const n = typeof v === "number" ? v : NaN; if (!isNaN(n)) vals.push(n); } if (!vals.length) return null; const sum = vals.reduce((x, y) => x + y, 0); return { sum: Math.round(sum * 100) / 100, avg: Math.round(sum / vals.length * 100) / 100, count: vals.length }; }, [cells, active, anchor]);
62
+ const pick = (r, c, e) => { setActive({ r, c }); if (!(e && e.shiftKey)) setAnchor({ r, c }); setEditing(null); };
63
+ const setFlag = (k) => { 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++) next[r][c][k] = !cur[k]; setCells(next); };
64
+ 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
+ const widths = [46]; for (let c = 0; c < nC; c++) widths.push(colWidths[c] || defaultColWidth);
66
+ 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
+ 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" />
72
+ <span style={{ width: 1, height: 16, background: "var(--border-control)", margin: "0 6px" }}></span>
73
+ {tbBtn("B", "b", { fontWeight: 700 })}{tbBtn("I", "i", { fontStyle: "italic" })}{tbBtn("U", "u", { textDecoration: "underline" })}
74
+ <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
+ </div>}
77
+ {formulaBar && <div style={{ height: 32, display: "flex", alignItems: "center", borderBottom: "1px solid var(--border-control)" }}>
78
+ <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
+ <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
+ <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)" }} />
81
+ </div>}
82
+ <div style={{ overflow: "auto", maxHeight: height }}>
83
+ <div style={{ display: "grid", gridTemplateColumns: widths.map(w => w + "px").join(" "), width: "fit-content" }}>
84
+ <span style={{ ...hdr, zIndex: 5, left: 0 }}><span style={{ width: 0, height: 0, borderStyle: "solid", borderWidth: "0 0 10px 10px", borderColor: "transparent transparent var(--border-control) transparent", alignSelf: "flex-end", marginLeft: "auto", marginRight: 3, marginBottom: 3 }}></span></span>
85
+ {Array.from({ length: nC }, (_, c) => <span key={c} style={{ ...hdr, ...(c >= lo.c && c <= hi.c ? { background: "var(--soft-bg)", color: "var(--action-primary)" } : null) }}>{colName(c)}</span>)}
86
+ {cells.map((row, r) => [
87
+ <span key={"h" + r} style={{ ...hdr, position: "sticky", left: 0, top: "auto", zIndex: 2, height: 28, ...(r >= lo.r && r <= hi.r ? { background: "var(--soft-bg)", color: "var(--action-primary)" } : null) }}>{r + 1}</span>,
88
+ ...row.map((cell, c) => {
89
+ const v = evalCell(cells, cell);
90
+ const isNum = typeof v === "number";
91
+ const isActive = active.r === r && active.c === c;
92
+ const isEdit = editing && editing !== "fx" && editing.r === r && editing.c === c;
93
+ 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
+ {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
+ {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
+ </span>;
98
+ })])}
99
+ </div>
100
+ </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
+ <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>
105
+ </div>
106
+ <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
+ </div>}
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,95 @@
1
+ <script setup>
2
+ import { ref, computed } from 'vue'
3
+ import Icon from '../icons/Icon.vue'
4
+ import IconButton from '../buttons/IconButton.vue'
5
+ import KSelect from '../forms/Select.vue'
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 })
8
+ const emit = defineEmits(['change'])
9
+
10
+ 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 }
11
+ const refRC = (ref) => { const m = /^([A-Z]+)(\d+)$/.exec(String(ref).trim().toUpperCase()); if (!m) return null; let c = 0; for (const ch of m[1]) c = c * 26 + (ch.charCodeAt(0) - 64); return { r: +m[2] - 1, c: c - 1 } }
12
+ const norm = (cell) => cell == null ? { v: '' } : (typeof cell === 'object' ? { ...cell } : { v: cell })
13
+ function evalCell(cells, cell, depth = 0) { if (!cell || depth > 12) return ''; const raw = cell.f != null ? cell.f : cell.v; if (typeof raw === 'string' && raw.startsWith('=')) return evalFormula(cells, raw, depth); return raw }
14
+ function numAt(cells, r, c, depth) { const v = evalCell(cells, cells[r] && cells[r][c], depth + 1); const n = typeof v === 'number' ? v : parseFloat(String(v).replace(/[^0-9.eE+-]/g, '')); return isNaN(n) ? null : n }
15
+ function evalFormula(cells, f, depth = 0) {
16
+ try {
17
+ let expr = f.slice(1).toUpperCase().replace(/\s+/g, '')
18
+ expr = expr.replace(/(SUM|AVERAGE|AVG|COUNT|MIN|MAX)\(([A-Z]+\d+):([A-Z]+\d+)\)/g, (m, fn, a, b) => {
19
+ const A = refRC(a), B = refRC(b), vals = []
20
+ for (let r = Math.min(A.r, B.r); r <= Math.max(A.r, B.r); r++) for (let c = Math.min(A.c, B.c); c <= Math.max(A.c, B.c); c++) { const n = numAt(cells, r, c, depth); if (n != null) vals.push(n) }
21
+ if (fn === 'COUNT') return String(vals.length)
22
+ if (!vals.length) return '0'
23
+ if (fn === 'SUM') return String(vals.reduce((x, y) => x + y, 0))
24
+ if (fn === 'MIN') return String(Math.min(...vals))
25
+ if (fn === 'MAX') return String(Math.max(...vals))
26
+ return String(vals.reduce((x, y) => x + y, 0) / vals.length)
27
+ })
28
+ expr = expr.replace(/[A-Z]+\d+/g, (ref) => { const p = refRC(ref); const n = p ? numAt(cells, p.r, p.c, depth) : null; return String(n == null ? 0 : n) })
29
+ if (!/^[-+*/().\d\seE]*$/.test(expr)) return '#ERROR'
30
+ const out = Function('"use strict";return (' + expr + ')')()
31
+ return typeof out === 'number' && isFinite(out) ? out : '#ERROR'
32
+ } catch (e) { return '#ERROR' }
33
+ }
34
+ function fmt(val, t, currency) { if (val === '' || val == null) return ''; if (typeof val !== 'number') return String(val); const r2 = Math.round(val * 100) / 100; if (t === 'percent') return (Math.round(val * 1000) / 10) + '%'; if (t === 'currency') return currency + r2.toLocaleString(); return r2.toLocaleString() }
35
+
36
+ const nR = Math.max(props.rows || 0, props.data.length, 8)
37
+ const nC = Math.max(props.cols || 0, ...props.data.map(r => r.length), 6)
38
+ const cells = ref(Array.from({ length: nR }, (_, r) => Array.from({ length: nC }, (_, c) => norm(props.data[r] && props.data[r][c]))))
39
+ const a0 = refRC(props.defaultActive) || { r: 0, c: 0 }
40
+ const active = ref(a0), anchor = ref(a0), editing = ref(null), draft = ref(''), sheet = ref(props.sheets[0])
41
+ const cur = computed(() => cells.value[active.value.r][active.value.c])
42
+ const rawOf = (cell) => cell.f != null ? cell.f : (cell.v == null ? '' : String(cell.v))
43
+ const lo = computed(() => ({ r: Math.min(active.value.r, anchor.value.r), c: Math.min(active.value.c, anchor.value.c) }))
44
+ const hi = computed(() => ({ r: Math.max(active.value.r, anchor.value.r), c: Math.max(active.value.c, anchor.value.c) }))
45
+ const multi = computed(() => lo.value.r !== hi.value.r || lo.value.c !== hi.value.c)
46
+ const inRange = (r, c) => r >= lo.value.r && r <= hi.value.r && c >= lo.value.c && c <= hi.value.c
47
+ const stats = computed(() => { if (!multi.value) return null; const vals = []; for (let r = lo.value.r; r <= hi.value.r; r++) for (let c = lo.value.c; c <= hi.value.c; c++) { const v = evalCell(cells.value, cells.value[r][c]); if (typeof v === 'number') vals.push(v) } if (!vals.length) return null; const sum = vals.reduce((x, y) => x + y, 0); return { sum: Math.round(sum * 100) / 100, avg: Math.round(sum / vals.length * 100) / 100, count: vals.length } })
48
+ function pick(r, c, e) { active.value = { r, c }; if (!(e && e.shiftKey)) anchor.value = { r, c }; editing.value = null }
49
+ function commit(r, c, text) { const cell = cells.value[r][c]; delete cell.f; if (String(text).startsWith('=')) { cell.f = text; cell.v = '' } else cell.v = text !== '' && !isNaN(Number(text)) ? Number(text) : text; emit('change', cells.value) }
50
+ 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
+ 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
+ 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 hl = { background: 'var(--soft-bg)', color: 'var(--action-primary)' }
55
+ </script>
56
+ <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" />
60
+ <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>
64
+ <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
+ </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)" />
71
+ </div>
72
+ <div :style="{ overflow: 'auto', maxHeight: typeof height === 'number' ? height + 'px' : height }">
73
+ <div :style="{ display: 'grid', gridTemplateColumns: gridCols, width: 'fit-content' }">
74
+ <span :style="{ ...hdr, zIndex: 5, left: 0 }" />
75
+ <span v-for="c in nC" :key="'c' + c" :style="{ ...hdr, ...((c - 1) >= lo.c && (c - 1) <= hi.c ? hl : null) }">{{ colName(c - 1) }}</span>
76
+ <template v-for="(row, r) in cells" :key="r">
77
+ <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
+ <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
+ <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
+ <template v-else>{{ fmt(evalCell(cells, cell), cell.t, currency) }}</template>
82
+ <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" />
83
+ </span>
84
+ </template>
85
+ </div>
86
+ </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 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>
91
+ </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
+ </div>
94
+ </div>
95
+ </template>
@@ -0,0 +1,13 @@
1
+ <!-- @dsCard group="Directory Index" viewport="700x120" name="Data dir" subtitle="Full cards: Components → Data Table / List Row / etc." -->
2
+ <!DOCTYPE html>
3
+ <html><head><meta charset="utf-8"><link rel="stylesheet" href="../../styles.css">
4
+ <script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
5
+ <script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
6
+ <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
7
+ <script src="../../_ds_bundle.js"></script>
8
+ <style>body{margin:0;background:var(--bg-surface);padding:16px;font-family:var(--font-sans)}</style></head>
9
+ <body><div id="root"></div>
10
+ <script type="text/babel">
11
+ const { DataTable } = window.KeybricksDesignSystem_71231a;
12
+ ReactDOM.createRoot(document.getElementById("root")).render(<DataTable dense columns={[{key:"id",label:"ID",mono:true},{key:"entity",label:"Entity",mono:true}]} rows={[{id:"528563",entity:"HK"}]}/>);
13
+ </script></body></html>