@keyfunc/keybricks-ds 2.1.1 → 2.4.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 (280) hide show
  1. package/DEPLOY.md +20 -5
  2. package/DESIGN.md +148 -20
  3. package/INTEGRATION.md +97 -24
  4. package/SKILL.md +11 -11
  5. package/_adherence.oxlintrc.json +56 -24
  6. package/_scripts/build-index.mjs +15 -4
  7. package/assets/icons/icon-index.js +1 -1
  8. package/assets/icons/icon-index.json +1 -1
  9. package/assets/logos/datasource/dat.png +0 -0
  10. package/assets/logos/datasource/excel.png +0 -0
  11. package/assets/logos/datasource/ftp.png +0 -0
  12. package/assets/logos/datasource/hierarchy.png +0 -0
  13. package/assets/logos/datasource/hive.png +0 -0
  14. package/assets/logos/datasource/json.png +0 -0
  15. package/assets/logos/datasource/mariadb.png +0 -0
  16. package/assets/logos/datasource/mongodb.png +0 -0
  17. package/assets/logos/datasource/mysql.png +0 -0
  18. package/assets/logos/datasource/odbc.png +0 -0
  19. package/assets/logos/datasource/oracle.png +0 -0
  20. package/assets/logos/datasource/postgresql.png +0 -0
  21. package/assets/logos/datasource/rest-api.png +0 -0
  22. package/assets/logos/datasource/sap-alt.png +0 -0
  23. package/assets/logos/datasource/sap.png +0 -0
  24. package/assets/logos/datasource/soap.png +0 -0
  25. package/assets/logos/datasource/sql-server.png +0 -0
  26. package/components/_cards/accordion.card.html +1 -1
  27. package/components/_cards/action-chip.card.html +11 -14
  28. package/components/_cards/activity.card.html +2 -2
  29. package/components/_cards/alert.card.html +2 -2
  30. package/components/_cards/app-chrome.card.html +34 -22
  31. package/components/_cards/banner.card.html +1 -1
  32. package/components/_cards/breadcrumb.card.html +4 -4
  33. package/components/_cards/button-1-base.card.html +23 -26
  34. package/components/_cards/button-2-states.card.html +14 -12
  35. package/components/_cards/button-3-sizes-icons.card.html +20 -19
  36. package/components/_cards/button-4-group.card.html +14 -31
  37. package/components/_cards/canvas-zoom-controls.card.html +1 -1
  38. package/components/_cards/cards-indicators.card.html +1 -1
  39. package/components/_cards/charts-1-sparkline.card.html +4 -4
  40. package/components/_cards/charts-10-gauge.card.html +2 -2
  41. package/components/_cards/charts-11-funnel.card.html +2 -2
  42. package/components/_cards/charts-12-treemap.card.html +2 -2
  43. package/components/_cards/charts-14-radar.card.html +3 -3
  44. package/components/_cards/charts-15-distribution.card.html +2 -2
  45. package/components/_cards/charts-16-pareto.card.html +2 -2
  46. package/components/_cards/charts-17-candlestick.card.html +2 -2
  47. package/components/_cards/charts-18-sankey.card.html +2 -2
  48. package/components/_cards/charts-19-chord.card.html +2 -2
  49. package/components/_cards/charts-2-line.card.html +3 -3
  50. package/components/_cards/charts-20-sunburst.card.html +2 -2
  51. package/components/_cards/charts-21-worldmap.card.html +3 -3
  52. package/components/_cards/charts-22-gantt.card.html +2 -2
  53. package/components/_cards/charts-23-bar3d.card.html +2 -2
  54. package/components/_cards/charts-24-pie3d.card.html +2 -2
  55. package/components/_cards/charts-25-surface3d.card.html +2 -2
  56. package/components/_cards/charts-26-scatter3d.card.html +2 -2
  57. package/components/_cards/charts-27-ribbon3d.card.html +2 -2
  58. package/components/_cards/charts-3-bar.card.html +6 -6
  59. package/components/_cards/charts-4-barlist.card.html +2 -2
  60. package/components/_cards/charts-5-donut.card.html +2 -2
  61. package/components/_cards/charts-6-scatter.card.html +2 -2
  62. package/components/_cards/charts-7-heatmap.card.html +2 -2
  63. package/components/_cards/charts-8-waterfall.card.html +2 -2
  64. package/components/_cards/charts-9-bullet.card.html +3 -3
  65. package/components/_cards/charts-99-fullpage.card.html +1 -1
  66. package/components/_cards/checkbox.card.html +6 -3
  67. package/components/_cards/codeblock.card.html +2 -2
  68. package/components/_cards/combobox.card.html +6 -4
  69. package/components/_cards/context-menu.card.html +12 -5
  70. package/components/_cards/date-picker.card.html +8 -5
  71. package/components/_cards/diffvalue.card.html +2 -2
  72. package/components/_cards/divider.card.html +2 -2
  73. package/components/_cards/drawer.card.html +3 -3
  74. package/components/_cards/dropdown-menu.card.html +2 -2
  75. package/components/_cards/dropzone.card.html +5 -3
  76. package/components/_cards/empty-state.card.html +2 -2
  77. package/components/_cards/filter-bar.card.html +1 -1
  78. package/components/_cards/icon-button.card.html +14 -14
  79. package/components/_cards/icons-0-usage.card.html +88 -0
  80. package/components/_cards/icons-1-core.card.html +4 -4
  81. package/components/_cards/icons-2-feather.card.html +4 -4
  82. package/components/_cards/icons-3-unicons.card.html +3 -3
  83. package/components/_cards/icons-4-datasource.card.html +5 -0
  84. package/components/_cards/input.card.html +5 -5
  85. package/components/_cards/kbd.card.html +1 -1
  86. package/components/_cards/keyvalue.card.html +1 -1
  87. package/components/_cards/listrow.card.html +1 -1
  88. package/components/_cards/loading.card.html +2 -2
  89. package/components/_cards/log-viewer.card.html +2 -2
  90. package/components/_cards/modal.card.html +1 -1
  91. package/components/_cards/multi-select.card.html +6 -4
  92. package/components/_cards/number-stepper.card.html +9 -6
  93. package/components/_cards/pagination.card.html +6 -4
  94. package/components/_cards/people.card.html +2 -2
  95. package/components/_cards/pipeline-node.card.html +8 -8
  96. package/components/_cards/pipeline-palette.card.html +2 -2
  97. package/components/_cards/radio.card.html +12 -7
  98. package/components/_cards/search.card.html +6 -6
  99. package/components/_cards/segmented-control.card.html +19 -12
  100. package/components/_cards/select.card.html +7 -3
  101. package/components/_cards/slider.card.html +2 -2
  102. package/components/_cards/smart-table.card.html +3 -3
  103. package/components/_cards/spreadsheet.card.html +2 -2
  104. package/components/_cards/stepper.card.html +2 -2
  105. package/components/_cards/table.card.html +79 -17
  106. package/components/_cards/tabs.card.html +10 -5
  107. package/components/_cards/tag.card.html +6 -6
  108. package/components/_cards/textarea.card.html +1 -1
  109. package/components/_cards/timeline.card.html +3 -3
  110. package/components/_cards/toast.card.html +7 -5
  111. package/components/_cards/toggles.card.html +14 -9
  112. package/components/_cards/tooltip.card.html +3 -3
  113. package/components/_cards/tree.card.html +2 -2
  114. package/components/_shared/card-kit.css +25 -0
  115. package/components/_shared/card-kit.js +50 -0
  116. package/components/_shared/src-buttons.js +1 -1
  117. package/components/_shared/src-charts.js +1 -1
  118. package/components/_shared/src-data.js +1 -1
  119. package/components/_shared/src-display.js +1 -1
  120. package/components/_shared/src-feedback.js +1 -1
  121. package/components/_shared/src-forms.js +1 -1
  122. package/components/_shared/src-icons.js +1 -1
  123. package/components/_shared/src-navigation.js +1 -1
  124. package/components/_shared/src-tabs.js +1 -1
  125. package/components/_shared/view-source.js +12 -12
  126. package/components/_vue/index.mjs +10 -0
  127. package/components/buttons/ActionChip.d.ts +1 -1
  128. package/components/buttons/Button.d.ts +1 -1
  129. package/components/buttons/Button.jsx +1 -1
  130. package/components/buttons/Button.vue +1 -1
  131. package/components/buttons/IconButton.d.ts +1 -1
  132. package/components/buttons/IconButton.jsx +5 -3
  133. package/components/buttons/IconButton.vue +3 -2
  134. package/components/charts/Bar3D.jsx +1 -1
  135. package/components/charts/Bar3D.vue +2 -2
  136. package/components/charts/BarChart.jsx +3 -3
  137. package/components/charts/BarChart.vue +6 -6
  138. package/components/charts/BarList.jsx +3 -3
  139. package/components/charts/BarList.vue +3 -3
  140. package/components/charts/BoxPlot.vue +2 -2
  141. package/components/charts/BulletChart.jsx +5 -5
  142. package/components/charts/BulletChart.vue +5 -5
  143. package/components/charts/CandlestickChart.vue +3 -3
  144. package/components/charts/ChartKit.jsx +4 -3
  145. package/components/charts/ChartTooltip.vue +1 -1
  146. package/components/charts/ChordChart.jsx +1 -1
  147. package/components/charts/ChordChart.vue +2 -2
  148. package/components/charts/DonutChart.jsx +1 -1
  149. package/components/charts/DonutChart.vue +2 -2
  150. package/components/charts/FunnelChart.jsx +3 -3
  151. package/components/charts/FunnelChart.vue +4 -4
  152. package/components/charts/GanttChart.vue +1 -1
  153. package/components/charts/GaugeChart.jsx +2 -2
  154. package/components/charts/GaugeChart.vue +2 -2
  155. package/components/charts/HeatmapChart.jsx +1 -1
  156. package/components/charts/HeatmapChart.vue +1 -1
  157. package/components/charts/Histogram.jsx +2 -2
  158. package/components/charts/Histogram.vue +4 -4
  159. package/components/charts/LineChart.vue +2 -2
  160. package/components/charts/ParetoChart.jsx +1 -1
  161. package/components/charts/ParetoChart.vue +3 -3
  162. package/components/charts/Pie3D.jsx +1 -1
  163. package/components/charts/Pie3D.vue +2 -2
  164. package/components/charts/PlotGrid.vue +1 -1
  165. package/components/charts/RadarChart.vue +1 -1
  166. package/components/charts/Ribbon3D.jsx +1 -1
  167. package/components/charts/Ribbon3D.vue +2 -2
  168. package/components/charts/SankeyChart.jsx +1 -1
  169. package/components/charts/SankeyChart.vue +2 -2
  170. package/components/charts/Scatter3D.vue +1 -1
  171. package/components/charts/ScatterChart.jsx +1 -1
  172. package/components/charts/ScatterChart.vue +3 -3
  173. package/components/charts/Sparkline.jsx +1 -1
  174. package/components/charts/Sparkline.vue +1 -1
  175. package/components/charts/SunburstChart.vue +1 -1
  176. package/components/charts/Surface3D.jsx +1 -1
  177. package/components/charts/Surface3D.vue +2 -2
  178. package/components/charts/TreemapChart.jsx +1 -1
  179. package/components/charts/TreemapChart.vue +2 -2
  180. package/components/charts/WaterfallChart.jsx +1 -1
  181. package/components/charts/WaterfallChart.vue +5 -4
  182. package/components/charts/WorldMap.jsx +4 -4
  183. package/components/charts/WorldMap.vue +5 -5
  184. package/components/data/DiffValue.d.ts +1 -0
  185. package/components/data/DiffValue.jsx +1 -1
  186. package/components/data/DiffValue.vue +1 -1
  187. package/components/data/KeyValue.d.ts +1 -1
  188. package/components/data/KeyValue.jsx +2 -2
  189. package/components/data/KeyValue.vue +2 -2
  190. package/components/data/Pagination.jsx +2 -2
  191. package/components/data/Pagination.vue +1 -1
  192. package/components/data/SmartTable.jsx +5 -5
  193. package/components/data/SmartTable.vue +6 -6
  194. package/components/data/Spreadsheet.jsx +5 -5
  195. package/components/data/Spreadsheet.vue +6 -6
  196. package/components/data/Table.d.ts +58 -4
  197. package/components/data/Table.jsx +153 -15
  198. package/components/data/Table.vue +105 -15
  199. package/components/data/data.prompt.md +1 -1
  200. package/components/display/Avatar.jsx +2 -2
  201. package/components/display/Avatar.vue +1 -1
  202. package/components/display/AvatarGroup.vue +1 -1
  203. package/components/display/Badge.d.ts +4 -1
  204. package/components/display/Badge.jsx +10 -5
  205. package/components/display/IconTile.vue +9 -4
  206. package/components/display/Kbd.d.ts +1 -1
  207. package/components/display/Kbd.jsx +1 -1
  208. package/components/display/Kbd.vue +1 -1
  209. package/components/feedback/ActivityItem.jsx +1 -1
  210. package/components/feedback/ContextMenu.d.ts +3 -2
  211. package/components/feedback/ContextMenu.jsx +8 -4
  212. package/components/feedback/ContextMenu.vue +9 -6
  213. package/components/feedback/Menu.d.ts +1 -1
  214. package/components/feedback/Modal.jsx +1 -1
  215. package/components/feedback/Modal.vue +1 -1
  216. package/components/feedback/Timeline.vue +2 -2
  217. package/components/feedback/feedback.prompt.md +1 -1
  218. package/components/forms/Combobox.d.ts +1 -1
  219. package/components/forms/DatePicker.d.ts +1 -1
  220. package/components/forms/Input.d.ts +1 -1
  221. package/components/forms/Input.jsx +2 -2
  222. package/components/forms/Input.vue +1 -1
  223. package/components/forms/MultiSelect.d.ts +1 -1
  224. package/components/forms/NumberStepper.d.ts +1 -1
  225. package/components/forms/SearchInput.d.ts +1 -1
  226. package/components/forms/Select.d.ts +1 -1
  227. package/components/forms/Select.jsx +1 -1
  228. package/components/forms/Select.vue +1 -1
  229. package/components/forms/Slider.jsx +1 -1
  230. package/components/forms/Slider.vue +1 -1
  231. package/components/forms/Switch.jsx +3 -3
  232. package/components/forms/Switch.vue +11 -8
  233. package/components/forms/Textarea.vue +1 -1
  234. package/components/forms/input-base.js +1 -1
  235. package/components/icons/Icon.jsx +17 -1
  236. package/components/icons/Icon.prompt.md +2 -0
  237. package/components/icons/Icon.vue +1 -1
  238. package/components/icons/icon-paths.js +15 -0
  239. package/components/index.d.ts +1 -0
  240. package/components/index.js +5 -4
  241. package/components/navigation/Breadcrumb.jsx +3 -7
  242. package/components/navigation/Breadcrumb.vue +2 -8
  243. package/components/navigation/SidePanel.d.ts +22 -0
  244. package/components/navigation/SidePanel.jsx +23 -0
  245. package/components/navigation/SidePanel.vue +21 -0
  246. package/components/navigation/Sidebar.d.ts +23 -7
  247. package/components/navigation/Sidebar.jsx +38 -56
  248. package/components/navigation/Sidebar.vue +40 -44
  249. package/components/navigation/TopBar.d.ts +28 -3
  250. package/components/navigation/TopBar.jsx +54 -7
  251. package/components/navigation/TopBar.vue +26 -8
  252. package/components/navigation/Tree.jsx +1 -1
  253. package/components/navigation/Tree.vue +9 -4
  254. package/components/navigation/UserMenu.vue +27 -0
  255. package/components/navigation/chrome-tones.js +22 -0
  256. package/components/navigation/nav-model.js +26 -0
  257. package/components/navigation/navigation.prompt.md +2 -0
  258. package/components/navigation/theme-store.js +9 -0
  259. package/components/tabs/CountBadge.vue +12 -0
  260. package/components/tabs/PanelTabs.vue +7 -3
  261. package/components/tabs/PillTabs.vue +8 -4
  262. package/components/tabs/SegmentedControl.d.ts +5 -3
  263. package/components/tabs/SegmentedControl.jsx +6 -4
  264. package/components/tabs/SegmentedControl.vue +4 -3
  265. package/components/tabs/Tabs.d.ts +14 -4
  266. package/components/tabs/Tabs.jsx +35 -20
  267. package/components/tabs/Tabs.vue +7 -3
  268. package/components/tabs/ViewToggle.d.ts +1 -1
  269. package/components/tabs/tabs.prompt.md +2 -2
  270. package/package.json +2 -2
  271. package/readme.md +10 -9
  272. package/readme.zh-CN.md +8 -7
  273. package/tokens/alpha.css +3 -1
  274. package/tokens/colors.css +1 -1
  275. package/tokens/dark.css +1 -1
  276. package/tokens/layout.css +8 -5
  277. package/tokens/tokens.d.ts +24 -0
  278. package/tokens/tokens.js +42 -6
  279. package/tokens/tokens.json +97 -11
  280. package/tokens/typography.css +26 -9
@@ -5,6 +5,7 @@ export interface DiffValueProps {
5
5
  from?: React.ReactNode;
6
6
  to: React.ReactNode;
7
7
  tone?: "neutral" | "warning" | "success" | "danger";
8
+ /** Tabular figures (UI font — the monospace font is reserved for code). */
8
9
  mono?: boolean;
9
10
  style?: React.CSSProperties;
10
11
  }
@@ -6,7 +6,7 @@ const toneColor = { neutral: "var(--text-heading)", warning: "var(--warning-text
6
6
  /** Before → after value change ("USD → EUR"). tone colors the new value; from renders muted + struck through when both sides are given. */
7
7
  export function DiffValue({ from, to, tone = "warning", mono = true, style }) {
8
8
  return (
9
- <span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontFamily: mono ? "var(--font-mono)" : "var(--font-sans)", fontSize: "var(--control-text)", ...style }}>
9
+ <span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", ...style }}>
10
10
  {from != null ? <span style={{ color: "var(--text-muted)", textDecoration: "line-through" }}>{from}</span> : null}
11
11
  <Icon name="arrow-right" size={12} style={{ color: "var(--text-muted)" }} />
12
12
  <span style={{ color: toneColor[tone], fontWeight: "var(--weight-medium)" }}>{to}</span>
@@ -5,7 +5,7 @@ const toneColor = { neutral: 'var(--text-heading)', warning: 'var(--warning-text
5
5
  defineProps({ from: [String, Number], to: [String, Number], tone: { type: String, default: 'warning' }, mono: { type: Boolean, default: true }, style: Object })
6
6
  </script>
7
7
  <template>
8
- <span :style="{ display: 'inline-flex', alignItems: 'center', gap: '6px', fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: 'var(--control-text)', ...style }">
8
+ <span :style="{ display: 'inline-flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', ...style }">
9
9
  <span v-if="from != null" style="color:var(--text-muted);text-decoration:line-through">{{ from }}</span>
10
10
  <Icon name="arrow-right" :size="12" style="color:var(--text-muted)" />
11
11
  <span :style="{ color: toneColor[tone], fontWeight: 'var(--weight-medium)' }">{{ to }}</span>
@@ -3,7 +3,7 @@ import * as React from "react";
3
3
  export interface KeyValueProps {
4
4
  label: string;
5
5
  value?: React.ReactNode;
6
- /** Render value in Menlo (config codes) */
6
+ /** Kept for 2.x callers — values always use tabular figures in the UI font (the monospace font is reserved for code). */
7
7
  mono?: boolean;
8
8
  style?: React.CSSProperties;
9
9
  }
@@ -1,11 +1,11 @@
1
1
  import React from "react";
2
2
 
3
- /** Label/value row (config summaries, confirmation steps). mono renders the value in Menlo. */
3
+ /** Label/value row (config summaries, confirmation steps). Values always use the UI font with tabular figures; `mono` is kept for 2.x callers and changes nothing. */
4
4
  export function KeyValue({ label, value, mono, style }) {
5
5
  return (
6
6
  <div style={{ display: "flex", justifyContent: "space-between", gap: 12, padding: "6px 0", ...style }}>
7
7
  <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", color: "var(--text-secondary)" }}>{label}</span>
8
- <span style={{ fontFamily: mono ? "var(--font-mono)" : "var(--font-sans)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: mono ? 0 : "var(--tracking-base)", fontVariantNumeric: "tabular-nums", color: "var(--text-heading)", textAlign: "right" }}>{value}</span>
8
+ <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", fontVariantNumeric: "tabular-nums", color: "var(--text-heading)", textAlign: "right" }}>{value}</span>
9
9
  </div>
10
10
  );
11
11
  }
@@ -1,10 +1,10 @@
1
1
  <script setup>
2
- // Label/value row (config summaries, confirmation steps). mono renders the value in Menlo.
2
+ // Label/value row (config summaries, confirmation steps). Values always use the UI font with tabular figures; `mono` is kept for 2.x callers and changes nothing.
3
3
  defineProps({ label: String, value: [String, Number], mono: Boolean, style: Object })
4
4
  </script>
5
5
  <template>
6
6
  <div :style="{ display: 'flex', justifyContent: 'space-between', gap: '12px', padding: '6px 0', ...style }">
7
7
  <span style="font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-secondary)">{{ label }}</span>
8
- <span :style="{ fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: mono ? 0 : 'var(--tracking-base)', fontVariantNumeric: 'tabular-nums', color: 'var(--text-heading)', textAlign: 'right' }">{{ value }}</span>
8
+ <span :style="{ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', fontVariantNumeric: 'tabular-nums', color: 'var(--text-heading)', textAlign: 'right' }">{{ value }}</span>
9
9
  </div>
10
10
  </template>
@@ -3,11 +3,11 @@ import { Icon } from "../icons/Icon.jsx";
3
3
 
4
4
  const tierIcon = { width: "var(--control-icon)", height: "var(--control-icon)" };
5
5
 
6
- /** Table pagination — range label + prev/next + numbered pages, all at the region's tier (page items are control-height squares). `format(from, to, total, page, pageCount)` localises the label. */
6
+ /** Table pagination — range label + prev/next + numbered pages, all at the region's tier (page items are control-height squares with no border — the current page takes the neutral selected fill (--bg-active, weight 600) — it is a state, not an action, so it never uses the action blue; the rest show a wash on hover). `format(from, to, total, page, pageCount)` localises the label. */
7
7
  export function Pagination({ page = 1, pageCount = 1, total, pageSize = 20, size, format, onChange, style }) {
8
8
  const btn = (content, disabled, onClick, active, label) => (
9
9
  <button type="button" disabled={disabled} onClick={onClick} aria-label={label} aria-current={active ? "page" : undefined} data-wash={active || disabled ? undefined : ""}
10
- style={{ minWidth: "var(--control-h)", height: "var(--control-h)", padding: "0 6px", boxSizing: "border-box", borderRadius: "var(--radius-md)", border: `1px solid ${active ? "var(--action-primary)" : "var(--border-control)"}`, background: active ? "var(--action-primary)" : "var(--wash, var(--bg-surface))", color: active ? "var(--on-accent)" : "var(--text-body)", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", letterSpacing: "var(--control-tracking)", fontVariantNumeric: "tabular-nums", cursor: disabled ? "default" : "pointer", opacity: disabled ? .45 : 1, display: "inline-flex", alignItems: "center", justifyContent: "center", transition: "var(--t-hover)" }}>
10
+ style={{ minWidth: "var(--control-h)", height: "var(--control-h)", padding: "0 6px", boxSizing: "border-box", borderRadius: "var(--radius-md)", border: "none", background: active ? "var(--bg-active)" : "var(--wash, transparent)", color: active ? "var(--text-heading)" : "var(--text-body)", fontWeight: active ? "var(--weight-semibold)" : "var(--weight-regular)", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", letterSpacing: "var(--control-tracking)", fontVariantNumeric: "tabular-nums", cursor: disabled ? "default" : "pointer", opacity: disabled ? .45 : 1, display: "inline-flex", alignItems: "center", justifyContent: "center", transition: "var(--t-hover)" }}>
11
11
  {content}
12
12
  </button>
13
13
  );
@@ -15,7 +15,7 @@ const label = computed(() => {
15
15
  if (props.format) return props.format(from, to, props.total, props.page, props.pageCount)
16
16
  return props.total != null ? `${from}–${to} of ${props.total}` : `Page ${props.page} of ${props.pageCount}`
17
17
  })
18
- function btnStyle(active, disabled) { return { minWidth: 'var(--control-h)', height: 'var(--control-h)', padding: '0 6px', boxSizing: 'border-box', borderRadius: 'var(--radius-md)', border: `1px solid ${active ? 'var(--action-primary)' : 'var(--border-control)'}`, background: active ? 'var(--action-primary)' : 'var(--wash, var(--bg-surface))', color: active ? 'var(--on-accent)' : 'var(--text-body)', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', letterSpacing: 'var(--control-tracking)', fontVariantNumeric: 'tabular-nums', cursor: disabled ? 'default' : 'pointer', opacity: disabled ? .45 : 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', transition: 'var(--t-hover)' } }
18
+ function btnStyle(active, disabled) { return { minWidth: 'var(--control-h)', height: 'var(--control-h)', padding: '0 6px', boxSizing: 'border-box', borderRadius: 'var(--radius-md)', border: 'none', background: active ? 'var(--bg-active)' : 'var(--wash, transparent)', color: active ? 'var(--text-heading)' : 'var(--text-body)', fontWeight: active ? 'var(--weight-semibold)' : 'var(--weight-regular)', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', letterSpacing: 'var(--control-tracking)', fontVariantNumeric: 'tabular-nums', cursor: disabled ? 'default' : 'pointer', opacity: disabled ? .45 : 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', transition: 'var(--t-hover)' } }
19
19
  </script>
20
20
  <template>
21
21
  <div :data-size="size" :style="{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--group-gap)', ...style }">
@@ -9,7 +9,7 @@ const typeIcon = { text: "type", longText: "align-left", number: "hash", select:
9
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
10
 
11
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
- /* A size region that defaults to sm: a database grid is a dense surface, so rows are the sm bar height (32), cells and toolbar read the sm control text (12px) and the toolbar's buttons are 24px. Pass size="md" for a roomier grid. */
12
+ /* A size region that defaults to sm: a database grid is a dense surface, so rows are the sm bar height (36), cells and toolbar read the sm control text (12px, 13px in CJK) and the toolbar's buttons are 28px. Pass size="md" for a roomier grid. */
13
13
  export function SmartTable({ fields = [], rows = [], title = "Grid view", toolbar = true, height, rowHeight = "var(--bar-h)", size = "sm", onChange, style }) {
14
14
  const [data, setData] = React.useState(rows);
15
15
  const [hover, setHover] = React.useState(-1);
@@ -39,8 +39,8 @@ export function SmartTable({ fields = [], rows = [], title = "Grid view", toolba
39
39
  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;
40
40
  case "checkbox": return <Checkbox checked={!!v} onChange={(nv) => setCell(r, f.key, nv)} />;
41
41
  case "url": return v ? <a href={"https://" + v} onClick={(e) => e.preventDefault()} style={{ color: "var(--link)", overflow: "hidden", textOverflow: "ellipsis" }}>{v}</a> : null;
42
- 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-xs)", color: "var(--text-secondary)" }}>{v || 0}%</span></span>;
43
- case "number": return <span style={{ marginLeft: "auto", fontFamily: "var(--font-mono)", fontSize: "var(--control-text)", letterSpacing: 0, fontVariantNumeric: "tabular-nums" }}>{v}</span>;
42
+ 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-sans)", fontVariantNumeric: "tabular-nums", fontSize: "var(--text-xs)", color: "var(--text-secondary)" }}>{v || 0}%</span></span>;
43
+ case "number": return <span style={{ marginLeft: "auto", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", letterSpacing: 0, fontVariantNumeric: "tabular-nums" }}>{v}</span>;
44
44
  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>;
45
45
  }
46
46
  };
@@ -60,13 +60,13 @@ export function SmartTable({ fields = [], rows = [], title = "Grid view", toolba
60
60
  {fields.map(f => <span key={f.key} style={{ ...cellBase, background: "var(--bg-inset)", color: "var(--text-secondary)", fontWeight: "var(--weight-semibold)" }}><Icon name={typeIcon[f.type] || "type"} size={14} style={{ ...tierIcon, color: "var(--text-muted)" }} />{f.label}</span>)}
61
61
  <span style={{ ...cellBase, background: "var(--bg-inset)", justifyContent: "center", padding: 0 }}><IconButton icon="plus" muted label="Add field" /></span>
62
62
  {data.map((row, r) => [
63
- <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-xs)", letterSpacing: 0, background: hover === r ? "var(--bg-inset)" : "transparent", cursor: "pointer" }}>{hover === r ? <Icon name="maximize-2" size={14} style={{ ...tierIcon, color: "var(--action-primary)" }} /> : r + 1}</span>,
63
+ <span key={"n" + r} onMouseEnter={() => setHover(r)} onMouseLeave={() => setHover(-1)} style={{ ...cellBase, justifyContent: "center", color: "var(--text-muted)", fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", fontSize: "var(--text-xs)", letterSpacing: 0, background: hover === r ? "var(--bg-inset)" : "transparent", cursor: "pointer" }}>{hover === r ? <Icon name="maximize-2" size={14} style={{ ...tierIcon, color: "var(--action-primary)" }} /> : r + 1}</span>,
64
64
  ...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>),
65
65
  <span key={"a" + r} style={{ ...cellBase, background: hover === r ? "var(--bg-inset)" : "transparent" }}></span>,
66
66
  ])}
67
67
  <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 var(--edge-pad)", border: "none", borderBottom: "1px solid var(--border-subtle)", background: "transparent", cursor: "pointer", color: "var(--text-secondary)", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", letterSpacing: "var(--control-tracking)", 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>
68
68
  <span style={{ ...cellBase, borderBottom: "none", background: "var(--bg-inset)" }}></span>
69
- {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-xs)", letterSpacing: 0, color: "var(--text-secondary)" }}>{agg(f)}</span>)}
69
+ {fields.map(f => <span key={"g" + f.key} style={{ ...cellBase, borderBottom: "none", background: "var(--bg-inset)", justifyContent: "flex-end", fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", fontSize: "var(--text-xs)", letterSpacing: 0, color: "var(--text-secondary)" }}>{agg(f)}</span>)}
70
70
  <span style={{ ...cellBase, borderBottom: "none", background: "var(--bg-inset)" }}></span>
71
71
  </div>
72
72
  </div>
@@ -8,7 +8,7 @@ import SearchInput from '../forms/SearchInput.vue'
8
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
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
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: [Number, String], size: { type: String, default: 'sm' }, style: Object })
11
- // A size region that defaults to sm: a database grid is a dense surface, so rows are the sm bar height (32), cells and toolbar read the sm control text (12px) and the toolbar's buttons are 24px. Pass size="md" for a roomier grid.
11
+ // A size region that defaults to sm: a database grid is a dense surface, so rows are the sm bar height (36), cells and toolbar read the sm control text (12px, 13px in CJK) and the toolbar's buttons are 28px. Pass size="md" for a roomier grid.
12
12
  const rowH = computed(() => props.rowHeight == null ? 'var(--bar-h)' : (typeof props.rowHeight === 'number' ? props.rowHeight + 'px' : props.rowHeight))
13
13
  const tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }
14
14
  const emit = defineEmits(['change'])
@@ -44,7 +44,7 @@ function agg(f) {
44
44
  <span v-for="f in fields" :key="f.key" :style="{ ...cellBase, height: rowH, background: 'var(--bg-inset)', color: 'var(--text-secondary)', fontWeight: 'var(--weight-semibold)' }"><Icon :name="typeIcon[f.type] || 'type'" :size="14" :style="{ ...tierIcon, color: 'var(--text-muted)' }" />{{ f.label }}</span>
45
45
  <span :style="{ ...cellBase, height: rowH, background: 'var(--bg-inset)', justifyContent: 'center', padding: 0 }"><IconButton icon="plus" muted label="Add field" /></span>
46
46
  <template v-for="(row, r) in data" :key="r">
47
- <span @mouseenter="hover = r" @mouseleave="hover = -1" :style="{ ...cellBase, height: rowH, justifyContent: 'center', color: 'var(--text-muted)', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', letterSpacing: 0, background: hover === r ? 'var(--bg-inset)' : 'transparent', cursor: 'pointer' }">
47
+ <span @mouseenter="hover = r" @mouseleave="hover = -1" :style="{ ...cellBase, height: rowH, justifyContent: 'center', color: 'var(--text-muted)', fontFamily: 'var(--font-sans)', fontVariantNumeric: 'tabular-nums', fontSize: 'var(--text-xs)', letterSpacing: 0, background: hover === r ? 'var(--bg-inset)' : 'transparent', cursor: 'pointer' }">
48
48
  <Icon v-if="hover === r" name="maximize-2" :size="14" :style="{ ...tierIcon, color: 'var(--action-primary)' }" /><template v-else>{{ r + 1 }}</template>
49
49
  </span>
50
50
  <span v-for="f in fields" :key="r + f.key" @mouseenter="hover = r" @mouseleave="hover = -1" @dblclick="startEdit(r, f, row)" :style="{ ...cellBase, height: rowH, position: 'relative', background: hover === r ? 'var(--bg-inset)' : 'transparent' }">
@@ -53,16 +53,16 @@ function agg(f) {
53
53
  <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>
54
54
  <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>
55
55
  <template v-else-if="f.type === 'checkbox'"><Checkbox :checked="!!row[f.key]" @change="v => setCell(r, f.key, v)" /></template>
56
- <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>
57
- <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-xs);color:var(--text-secondary)">{{ row[f.key] || 0 }}%</span></span></template>
58
- <template v-else-if="f.type === 'number'"><span style="margin-left:auto;font-family:var(--font-mono);font-size:var(--control-text);letter-spacing:0;font-variant-numeric:tabular-nums">{{ row[f.key] }}</span></template>
56
+ <template v-else-if="f.type === 'url'"><a v-if="row[f.key]" href="#" @click.prevent style="color:var(--link);overflow:hidden;text-overflow:ellipsis">{{ row[f.key] }}</a></template>
57
+ <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-sans);font-variant-numeric:tabular-nums;font-size:var(--text-xs);color:var(--text-secondary)">{{ row[f.key] || 0 }}%</span></span></template>
58
+ <template v-else-if="f.type === 'number'"><span style="margin-left:auto;font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:0;font-variant-numeric:tabular-nums">{{ row[f.key] }}</span></template>
59
59
  <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>
60
60
  </span>
61
61
  <span :style="{ ...cellBase, height: rowH, background: hover === r ? 'var(--bg-inset)' : 'transparent' }" />
62
62
  </template>
63
63
  <button type="button" @click="addRow" class="kb-st-new" :style="{ gridColumn: '1 / -1', height: rowH }"><Icon name="plus" :size="14" :style="tierIcon" />New</button>
64
64
  <span :style="{ ...cellBase, height: rowH, borderBottom: 'none', background: 'var(--bg-inset)' }" />
65
- <span v-for="f in fields" :key="'g' + f.key" :style="{ ...cellBase, height: rowH, borderBottom: 'none', background: 'var(--bg-inset)', justifyContent: 'flex-end', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', letterSpacing: 0, color: 'var(--text-secondary)' }">{{ agg(f) }}</span>
65
+ <span v-for="f in fields" :key="'g' + f.key" :style="{ ...cellBase, height: rowH, borderBottom: 'none', background: 'var(--bg-inset)', justifyContent: 'flex-end', fontFamily: 'var(--font-sans)', fontVariantNumeric: 'tabular-nums', fontSize: 'var(--text-xs)', letterSpacing: 0, color: 'var(--text-secondary)' }">{{ agg(f) }}</span>
66
66
  <span :style="{ ...cellBase, height: rowH, borderBottom: 'none', background: 'var(--bg-inset)' }" />
67
67
  </div>
68
68
  </div>
@@ -76,9 +76,9 @@ export function Spreadsheet({ data = [], rows, cols, colWidths = [], defaultColW
76
76
  <Select options={["Automatic", "Number", "Currency", "Percent"]} value={{ number: "Number", currency: "Currency", percent: "Percent" }[cur.t] || "Automatic"} onChange={setType} style={{ width: 110 }} />
77
77
  </div>}
78
78
  {formulaBar && <div style={{ height: "var(--bar-h)", display: "flex", alignItems: "center", borderBottom: "1px solid var(--border-control)" }}>
79
- <span style={{ width: 72, alignSelf: "stretch", display: "flex", alignItems: "center", justifyContent: "center", borderRight: "1px solid var(--border-control)", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-heading)" }}>{multi ? colName(lo.c) + (lo.r + 1) + ":" + colName(hi.c) + (hi.r + 1) : colName(active.c) + (active.r + 1)}</span>
80
- <span style={{ padding: "0 10px", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", fontStyle: "italic", color: "var(--text-muted)", borderRight: "1px solid var(--border-subtle)", alignSelf: "stretch", display: "flex", alignItems: "center" }}>fx</span>
81
- <input value={editing === "fx" ? draft : rawOf(cur)} onFocus={() => { setEditing("fx"); setDraft(rawOf(cur)); }} onChange={(e) => setDraft(e.target.value)} onBlur={() => { if (editing === "fx") { commit(active.r, active.c, draft); setEditing(null); } }} onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") { setEditing(null); e.target.blur(); } }} style={{ flex: 1, border: "none", outline: "none", background: "transparent", padding: "0 10px", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-primary)" }} />
79
+ <span style={{ width: 72, alignSelf: "stretch", display: "flex", alignItems: "center", justifyContent: "center", borderRight: "1px solid var(--border-control)", fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", 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>
80
+ <span style={{ padding: "0 10px", fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", fontSize: "var(--text-xs)", fontStyle: "italic", color: "var(--text-muted)", borderRight: "1px solid var(--border-subtle)", alignSelf: "stretch", display: "flex", alignItems: "center" }}>fx</span>
81
+ <input value={editing === "fx" ? draft : rawOf(cur)} onFocus={() => { setEditing("fx"); setDraft(rawOf(cur)); }} onChange={(e) => setDraft(e.target.value)} onBlur={() => { if (editing === "fx") { commit(active.r, active.c, draft); setEditing(null); } }} onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") { setEditing(null); e.target.blur(); } }} style={{ flex: 1, border: "none", outline: "none", background: "transparent", padding: "0 10px", fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", fontSize: "var(--text-xs)", color: "var(--text-primary)" }} />
82
82
  </div>}
83
83
  <div style={{ overflow: "auto", maxHeight: height }}>
84
84
  <div style={{ display: "grid", gridTemplateColumns: widths.map(w => w + "px").join(" "), width: "fit-content" }}>
@@ -92,7 +92,7 @@ export function Spreadsheet({ data = [], rows, cols, colWidths = [], defaultColW
92
92
  const isActive = active.r === r && active.c === c;
93
93
  const isEdit = editing && editing !== "fx" && editing.r === r && editing.c === c;
94
94
  return <span key={r + "," + c} onMouseDown={(e) => pick(r, c, e)} onDoubleClick={() => { setEditing({ r, c }); setDraft(rawOf(cell)); }}
95
- style={{ position: "relative", height: 28, boxSizing: "border-box", borderRight: "1px solid var(--border-subtle)", borderBottom: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", justifyContent: cell.align || (isNum ? "flex-end" : "flex-start"), padding: "0 8px", cursor: "cell", background: isActive ? "var(--bg-surface)" : inRange(r, c) ? "var(--soft-bg)" : "var(--bg-surface)", boxShadow: isActive ? "inset 0 0 0 2px var(--action-primary)" : "none", zIndex: isActive ? 1 : 0, fontFamily: isNum ? "var(--font-mono)" : "var(--font-sans)", fontSize: "var(--control-text)", fontVariantNumeric: "tabular-nums", color: v === "#ERROR" ? "var(--danger)" : "var(--text-primary)", fontWeight: cell.b ? 600 : "var(--weight-regular)", fontStyle: cell.i ? "italic" : "normal", textDecoration: cell.u ? "underline" : "none", whiteSpace: "nowrap", overflow: "hidden", userSelect: "none" }}>
95
+ 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: "var(--font-sans)", fontVariantNumeric: isNum ? "tabular-nums" : "normal", fontSize: "var(--control-text)", fontVariantNumeric: "tabular-nums", color: v === "#ERROR" ? "var(--danger)" : "var(--text-primary)", fontWeight: cell.b ? 600 : "var(--weight-regular)", fontStyle: cell.i ? "italic" : "normal", textDecoration: cell.u ? "underline" : "none", whiteSpace: "nowrap", overflow: "hidden", userSelect: "none" }}>
96
96
  {isEdit ? <input autoFocus value={draft} onChange={(e) => setDraft(e.target.value)} onBlur={() => { commit(r, c, draft); setEditing(null); }} onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") setEditing(null); }} style={{ position: "absolute", inset: 0, border: "none", outline: "none", padding: "0 8px", fontFamily: "inherit", fontSize: "inherit", background: "var(--bg-surface)", color: "var(--text-primary)" }} /> : fmt(v, cell.t, currency)}
97
97
  {isActive && !isEdit && <span style={{ position: "absolute", right: -3, bottom: -3, width: 6, height: 6, background: "var(--action-primary)", border: "1px solid var(--bg-surface)", cursor: "crosshair", zIndex: 2 }}></span>}
98
98
  </span>;
@@ -104,7 +104,7 @@ export function Spreadsheet({ data = [], rows, cols, colWidths = [], defaultColW
104
104
  {sheets.map(s => <button key={s} type="button" data-ink={sheet === s ? undefined : ""} onClick={() => setSheet(s)} style={{ border: "none", borderRight: "1px solid var(--border-subtle)", padding: "0 16px", cursor: "pointer", background: sheet === s ? "var(--bg-surface)" : "transparent", color: sheet === s ? "var(--text-heading)" : "var(--ink, var(--text-secondary))", fontFamily: "var(--font-sans)", fontWeight: sheet === s ? "var(--weight-medium)" : "var(--weight-regular)", fontSize: "var(--control-text)", boxShadow: sheet === s ? "inset 0 2px 0 var(--action-primary)" : "none" }}>{s}</button>)}
105
105
  <span style={{ display: "flex", alignItems: "center", padding: "0 6px" }}><IconButton icon="plus" muted label="Add sheet" /></span>
106
106
  </div>
107
- <span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-secondary)" }}>{stats ? "Sum " + fmt(stats.sum, cur.t, currency) + " Avg " + fmt(stats.avg, cur.t, currency) + " Count " + stats.count : "100%"}</span>
107
+ <span style={{ fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", 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>
108
108
  </div>}
109
109
  </div>
110
110
  );
@@ -51,7 +51,7 @@ function commit(r, c, text) { const cell = cells.value[r][c]; delete cell.f; if
51
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 }
52
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 } }
53
53
  const gridCols = computed(() => ['46px', ...Array.from({ length: nC }, (_, c) => (props.colWidths[c] || props.defaultColWidth) + 'px')].join(' '))
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)', 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' }
55
55
  const hl = { background: 'var(--soft-bg)', color: 'var(--action-primary)' }
56
56
  </script>
57
57
  <template>
@@ -66,9 +66,9 @@ const hl = { background: 'var(--soft-bg)', color: 'var(--action-primary)' }
66
66
  <KSelect :options="['Automatic','Number','Currency','Percent']" :model-value="{ number: 'Number', currency: 'Currency', percent: 'Percent' }[cur.t] || 'Automatic'" @update:modelValue="setType" style="width:110px" />
67
67
  </div>
68
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-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>
70
- <span style="padding:0 10px;font-family:var(--font-mono);font-size:var(--text-xs);font-style:italic;color:var(--text-muted);border-right:1px solid var(--border-subtle);align-self:stretch;display:flex;align-items:center">fx</span>
71
- <input :value="editing === 'fx' ? draft : rawOf(cur)" @focus="editing = 'fx'; draft = rawOf(cur)" @input="draft = $event.target.value" @blur="editing === 'fx' && (commit(active.r, active.c, draft), editing = null)" @keydown.enter="$event.target.blur()" @keydown.esc="editing = null; $event.target.blur()" style="flex:1;border:none;outline:none;background:transparent;padding:0 10px;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-primary)" />
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)" />
72
72
  </div>
73
73
  <div :style="{ overflow: 'auto', maxHeight: typeof height === 'number' ? height + 'px' : height }">
74
74
  <div :style="{ display: 'grid', gridTemplateColumns: gridCols, width: 'fit-content' }">
@@ -77,7 +77,7 @@ const hl = { background: 'var(--soft-bg)', color: 'var(--action-primary)' }
77
77
  <template v-for="(row, r) in cells" :key="r">
78
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>
79
79
  <span v-for="(cell, c) in row" :key="r + ',' + c" @mousedown="pick(r, c, $event)" @dblclick="editing = { r, c }; draft = rawOf(cell)"
80
- :style="{ position: 'relative', height: '28px', boxSizing: 'border-box', borderRight: '1px solid var(--border-subtle)', borderBottom: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', justifyContent: cell.align || (typeof evalCell(cells, cell) === 'number' ? 'flex-end' : 'flex-start'), padding: '0 8px', cursor: 'cell', background: inRange(r, c) && !(active.r === r && active.c === c) ? 'var(--soft-bg)' : 'var(--bg-surface)', boxShadow: active.r === r && active.c === c ? 'inset 0 0 0 2px var(--action-primary)' : 'none', fontFamily: typeof evalCell(cells, cell) === 'number' ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: 'var(--control-text)', fontVariantNumeric: 'tabular-nums', color: evalCell(cells, cell) === '#ERROR' ? 'var(--danger)' : 'var(--text-primary)', fontWeight: cell.b ? 600 : 'var(--weight-regular)', fontStyle: cell.i ? 'italic' : 'normal', textDecoration: cell.u ? 'underline' : 'none', whiteSpace: 'nowrap', overflow: 'hidden', userSelect: 'none' }">
80
+ :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' }">
81
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)" />
82
82
  <template v-else>{{ fmt(evalCell(cells, cell), cell.t, currency) }}</template>
83
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" />
@@ -90,7 +90,7 @@ const hl = { background: 'var(--soft-bg)', color: 'var(--action-primary)' }
90
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
91
  <span style="display:flex;align-items:center;padding:0 6px"><IconButton icon="plus" muted label="Add sheet" /></span>
92
92
  </div>
93
- <span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-secondary)">{{ stats ? 'Sum ' + fmt(stats.sum, cur.t, currency) + ' Avg ' + fmt(stats.avg, cur.t, currency) + ' Count ' + stats.count : '100%' }}</span>
93
+ <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>
94
94
  </div>
95
95
  </div>
96
96
  </template>
@@ -1,24 +1,54 @@
1
1
  import * as React from "react";
2
2
  /**
3
3
  * Hairline table — --bg-table-header header tint, one step below --bg-app so it reads on grey pages; one rule weight (--border-subtle) for header rule and row separators alike.
4
- * The table is a size region: rows are the tier's bar height (sm 32 · md 40 · lg 44), cells and header share the tier's control text (header in weight 600, text-secondary, sentence case), and every control rendered inside a cell takes the same tier.
5
- * @startingPoint section="Data" subtitle="Comparison / preview tables with mono cells" viewport="700x300"
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
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; }
7
29
  export interface DataTableColumn {
8
30
  key: string;
9
31
  label: string;
10
32
  width?: number | string;
11
33
  /** Lower bound while resizing (default 48) */
12
34
  minWidth?: number;
13
- /** Menlo cell at the row's text size with tabular figures (config codes, amounts) */
35
+ /** Tabular figures for amounts and IDs (UI font, the row's size and colour). */
14
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;
15
45
  align?: "left" | "right" | "center";
16
46
  render?: (value: any, row: any, index: number) => React.ReactNode;
17
47
  }
18
48
  export interface DataTableProps {
19
49
  columns?: DataTableColumn[];
20
50
  rows?: Record<string, any>[];
21
- /** Size tier of the table region: sm 32px rows / 12px text (dense grids, preview drawers) · md 40px / 14px (default) · lg 44px / 14px. Omit to inherit the enclosing region. */
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. */
22
52
  size?: "sm" | "md" | "lg";
23
53
  /** Alias of size="sm", kept for 1.x callers */
24
54
  dense?: boolean;
@@ -38,6 +68,30 @@ export interface DataTableProps {
38
68
  columnWidths?: Record<string, number>;
39
69
  /** Fires on every drag frame with the new px width. Vue: `@column-resize`. */
40
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;
41
95
  /** Rendered inside the table (full-width cell) when rows is empty — an EmptyState or a muted line. Vue: `#empty` slot. */
42
96
  empty?: React.ReactNode;
43
97
  style?: React.CSSProperties;