@keyfunc/keybricks-ds 1.0.4 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (381) hide show
  1. package/DEPLOY.md +14 -5
  2. package/DESIGN.md +245 -59
  3. package/INTEGRATION.md +243 -49
  4. package/SKILL.md +13 -11
  5. package/_adherence.oxlintrc.json +183 -70
  6. package/_scripts/build-index.mjs +24 -4
  7. package/_scripts/build-tokens.mjs +5 -2
  8. package/assets/icons/icon-index.js +1 -1
  9. package/assets/icons/icon-index.json +1 -1
  10. package/components/_cards/accordion.card.html +2 -2
  11. package/components/_cards/action-chip.card.html +11 -9
  12. package/components/_cards/activity.card.html +3 -3
  13. package/components/_cards/alert.card.html +3 -3
  14. package/components/_cards/app-chrome.card.html +4 -4
  15. package/components/_cards/banner.card.html +1 -1
  16. package/components/_cards/breadcrumb.card.html +5 -5
  17. package/components/_cards/button-1-base.card.html +23 -26
  18. package/components/_cards/button-2-states.card.html +27 -25
  19. package/components/_cards/button-3-sizes-icons.card.html +20 -19
  20. package/components/_cards/button-4-group.card.html +14 -31
  21. package/components/_cards/canvas-zoom-controls.card.html +1 -1
  22. package/components/_cards/cards-indicators.card.html +2 -2
  23. package/components/_cards/charts-1-sparkline.card.html +5 -5
  24. package/components/_cards/charts-10-gauge.card.html +5 -5
  25. package/components/_cards/charts-11-funnel.card.html +3 -3
  26. package/components/_cards/charts-12-treemap.card.html +3 -3
  27. package/components/_cards/charts-14-radar.card.html +3 -3
  28. package/components/_cards/charts-15-distribution.card.html +2 -2
  29. package/components/_cards/charts-16-pareto.card.html +3 -3
  30. package/components/_cards/charts-17-candlestick.card.html +3 -3
  31. package/components/_cards/charts-18-sankey.card.html +3 -3
  32. package/components/_cards/charts-19-chord.card.html +3 -3
  33. package/components/_cards/charts-2-line.card.html +4 -4
  34. package/components/_cards/charts-20-sunburst.card.html +3 -3
  35. package/components/_cards/charts-21-worldmap.card.html +4 -4
  36. package/components/_cards/charts-22-gantt.card.html +3 -3
  37. package/components/_cards/charts-23-bar3d.card.html +3 -3
  38. package/components/_cards/charts-24-pie3d.card.html +3 -3
  39. package/components/_cards/charts-25-surface3d.card.html +3 -3
  40. package/components/_cards/charts-26-scatter3d.card.html +3 -3
  41. package/components/_cards/charts-27-ribbon3d.card.html +3 -3
  42. package/components/_cards/charts-3-bar.card.html +7 -7
  43. package/components/_cards/charts-4-barlist.card.html +3 -3
  44. package/components/_cards/charts-5-donut.card.html +4 -4
  45. package/components/_cards/charts-6-scatter.card.html +3 -3
  46. package/components/_cards/charts-7-heatmap.card.html +3 -3
  47. package/components/_cards/charts-8-waterfall.card.html +3 -3
  48. package/components/_cards/charts-9-bullet.card.html +3 -3
  49. package/components/_cards/charts-99-fullpage.card.html +5 -5
  50. package/components/_cards/checkbox.card.html +9 -6
  51. package/components/_cards/codeblock.card.html +2 -2
  52. package/components/_cards/combobox.card.html +6 -3
  53. package/components/_cards/context-menu.card.html +6 -3
  54. package/components/_cards/date-picker.card.html +10 -4
  55. package/components/_cards/diffvalue.card.html +3 -3
  56. package/components/_cards/divider.card.html +4 -4
  57. package/components/_cards/drawer.card.html +4 -4
  58. package/components/_cards/dropdown-menu.card.html +7 -7
  59. package/components/_cards/dropzone.card.html +5 -3
  60. package/components/_cards/empty-state.card.html +6 -6
  61. package/components/_cards/filter-bar.card.html +5 -5
  62. package/components/_cards/icon-button.card.html +14 -14
  63. package/components/_cards/icons-1-core.card.html +4 -4
  64. package/components/_cards/icons-2-feather.card.html +4 -4
  65. package/components/_cards/icons-3-unicons.card.html +4 -4
  66. package/components/_cards/input.card.html +5 -2
  67. package/components/_cards/kbd.card.html +2 -2
  68. package/components/_cards/keyvalue.card.html +2 -2
  69. package/components/_cards/listrow.card.html +2 -2
  70. package/components/_cards/loading.card.html +3 -3
  71. package/components/_cards/log-viewer.card.html +3 -3
  72. package/components/_cards/modal.card.html +9 -9
  73. package/components/_cards/multi-select.card.html +6 -3
  74. package/components/_cards/number-stepper.card.html +9 -5
  75. package/components/_cards/pagination.card.html +6 -3
  76. package/components/_cards/people.card.html +4 -4
  77. package/components/_cards/pipeline-node.card.html +8 -8
  78. package/components/_cards/pipeline-palette.card.html +5 -5
  79. package/components/_cards/radio.card.html +12 -7
  80. package/components/_cards/search.card.html +6 -3
  81. package/components/_cards/segmented-control.card.html +11 -10
  82. package/components/_cards/select.card.html +8 -4
  83. package/components/_cards/slider.card.html +2 -2
  84. package/components/_cards/smart-table.card.html +3 -3
  85. package/components/_cards/spreadsheet.card.html +3 -3
  86. package/components/_cards/stepper.card.html +3 -3
  87. package/components/_cards/table.card.html +165 -0
  88. package/components/_cards/tabs.card.html +6 -3
  89. package/components/_cards/tag.card.html +6 -3
  90. package/components/_cards/textarea.card.html +2 -2
  91. package/components/_cards/timeline.card.html +3 -3
  92. package/components/_cards/toast.card.html +10 -8
  93. package/components/_cards/toggles.card.html +15 -10
  94. package/components/_cards/tooltip.card.html +7 -7
  95. package/components/_cards/tree.card.html +3 -3
  96. package/components/_shared/card-kit.css +25 -0
  97. package/components/_shared/card-kit.js +50 -0
  98. package/components/_shared/src-buttons.js +1 -1
  99. package/components/_shared/src-charts.js +1 -1
  100. package/components/_shared/src-data.js +1 -1
  101. package/components/_shared/src-display.js +1 -1
  102. package/components/_shared/src-feedback.js +1 -1
  103. package/components/_shared/src-forms.js +1 -1
  104. package/components/_shared/src-icons.js +1 -1
  105. package/components/_shared/src-navigation.js +1 -1
  106. package/components/_shared/src-pipeline.js +1 -1
  107. package/components/_shared/src-tabs.js +1 -1
  108. package/components/_shared/view-source.js +3 -3
  109. package/components/_vue/index.mjs +16 -5
  110. package/components/buttons/ActionChip.d.ts +3 -1
  111. package/components/buttons/ActionChip.jsx +4 -4
  112. package/components/buttons/ActionChip.vue +4 -4
  113. package/components/buttons/Button.d.ts +5 -2
  114. package/components/buttons/Button.jsx +15 -18
  115. package/components/buttons/Button.vue +17 -18
  116. package/components/buttons/ButtonGroup.vue +1 -1
  117. package/components/buttons/IconButton.d.ts +2 -1
  118. package/components/buttons/IconButton.jsx +13 -9
  119. package/components/buttons/IconButton.vue +12 -9
  120. package/components/buttons/buttons.prompt.md +6 -5
  121. package/components/charts/Bar3D.jsx +2 -2
  122. package/components/charts/Bar3D.vue +3 -3
  123. package/components/charts/BarChart.jsx +4 -4
  124. package/components/charts/BarChart.vue +6 -6
  125. package/components/charts/BarList.jsx +4 -4
  126. package/components/charts/BarList.vue +4 -4
  127. package/components/charts/BoxPlot.jsx +1 -1
  128. package/components/charts/BoxPlot.vue +3 -3
  129. package/components/charts/BulletChart.jsx +6 -6
  130. package/components/charts/BulletChart.vue +6 -6
  131. package/components/charts/CandlestickChart.jsx +2 -2
  132. package/components/charts/CandlestickChart.vue +5 -5
  133. package/components/charts/ChartKit.jsx +8 -7
  134. package/components/charts/ChartLegend.vue +16 -0
  135. package/components/charts/ChartTooltip.vue +16 -0
  136. package/components/charts/ChordChart.jsx +2 -2
  137. package/components/charts/ChordChart.vue +3 -3
  138. package/components/charts/DonutChart.jsx +1 -1
  139. package/components/charts/DonutChart.vue +2 -2
  140. package/components/charts/FunnelChart.jsx +3 -3
  141. package/components/charts/FunnelChart.vue +4 -4
  142. package/components/charts/GanttChart.jsx +2 -2
  143. package/components/charts/GanttChart.vue +3 -3
  144. package/components/charts/GaugeChart.jsx +2 -2
  145. package/components/charts/GaugeChart.vue +2 -2
  146. package/components/charts/HeatmapChart.jsx +2 -2
  147. package/components/charts/HeatmapChart.vue +2 -2
  148. package/components/charts/Histogram.jsx +2 -2
  149. package/components/charts/Histogram.vue +4 -4
  150. package/components/charts/LineChart.jsx +1 -1
  151. package/components/charts/LineChart.vue +4 -4
  152. package/components/charts/ParetoChart.jsx +3 -3
  153. package/components/charts/ParetoChart.vue +5 -5
  154. package/components/charts/Pie3D.jsx +1 -1
  155. package/components/charts/Pie3D.vue +2 -2
  156. package/components/charts/PlotGrid.vue +18 -0
  157. package/components/charts/RadarChart.jsx +1 -1
  158. package/components/charts/RadarChart.vue +2 -2
  159. package/components/charts/Ribbon3D.jsx +2 -2
  160. package/components/charts/Ribbon3D.vue +3 -3
  161. package/components/charts/SankeyChart.jsx +2 -2
  162. package/components/charts/SankeyChart.vue +3 -3
  163. package/components/charts/Scatter3D.jsx +1 -1
  164. package/components/charts/Scatter3D.vue +2 -2
  165. package/components/charts/ScatterChart.jsx +3 -3
  166. package/components/charts/ScatterChart.vue +5 -5
  167. package/components/charts/Sparkline.jsx +1 -1
  168. package/components/charts/Sparkline.vue +1 -1
  169. package/components/charts/SunburstChart.jsx +1 -1
  170. package/components/charts/SunburstChart.vue +2 -2
  171. package/components/charts/Surface3D.jsx +2 -2
  172. package/components/charts/Surface3D.vue +3 -3
  173. package/components/charts/TreemapChart.jsx +2 -2
  174. package/components/charts/TreemapChart.vue +3 -3
  175. package/components/charts/WaterfallChart.jsx +2 -2
  176. package/components/charts/WaterfallChart.vue +6 -5
  177. package/components/charts/WorldMap.jsx +4 -4
  178. package/components/charts/WorldMap.vue +5 -5
  179. package/components/charts/XLabels.vue +11 -0
  180. package/components/data/CodeBlock.jsx +1 -1
  181. package/components/data/CodeBlock.vue +1 -1
  182. package/components/data/DiffValue.d.ts +1 -0
  183. package/components/data/DiffValue.jsx +1 -1
  184. package/components/data/DiffValue.vue +1 -1
  185. package/components/data/FilterBar.d.ts +2 -0
  186. package/components/data/FilterBar.jsx +10 -9
  187. package/components/data/FilterBar.vue +9 -8
  188. package/components/data/KeyValue.d.ts +1 -1
  189. package/components/data/KeyValue.jsx +4 -4
  190. package/components/data/KeyValue.vue +4 -4
  191. package/components/data/ListRow.jsx +2 -2
  192. package/components/data/ListRow.vue +2 -2
  193. package/components/data/Pagination.d.ts +5 -1
  194. package/components/data/Pagination.jsx +16 -12
  195. package/components/data/Pagination.vue +14 -9
  196. package/components/data/SmartTable.d.ts +4 -1
  197. package/components/data/SmartTable.jsx +16 -14
  198. package/components/data/SmartTable.vue +26 -23
  199. package/components/data/Spreadsheet.d.ts +2 -0
  200. package/components/data/Spreadsheet.jsx +16 -15
  201. package/components/data/Spreadsheet.vue +19 -18
  202. package/components/data/Table.d.ts +101 -0
  203. package/components/data/Table.jsx +223 -0
  204. package/components/data/Table.vue +153 -0
  205. package/components/data/data.prompt.md +3 -3
  206. package/components/display/Avatar.jsx +2 -2
  207. package/components/display/Avatar.vue +3 -2
  208. package/components/display/AvatarGroup.vue +3 -2
  209. package/components/display/Badge.d.ts +4 -1
  210. package/components/display/Badge.jsx +10 -5
  211. package/components/display/Badge.vue +1 -1
  212. package/components/display/Divider.vue +3 -3
  213. package/components/display/Donut.vue +3 -2
  214. package/components/display/IconTile.vue +10 -4
  215. package/components/display/Kbd.d.ts +1 -1
  216. package/components/display/Kbd.jsx +1 -1
  217. package/components/display/Kbd.vue +2 -2
  218. package/components/display/ProgressBar.jsx +1 -1
  219. package/components/display/ProgressBar.vue +1 -1
  220. package/components/display/StatCard.vue +1 -1
  221. package/components/display/Tag.d.ts +3 -1
  222. package/components/display/Tag.jsx +4 -4
  223. package/components/display/Tag.vue +4 -4
  224. package/components/feedback/Accordion.jsx +2 -2
  225. package/components/feedback/Accordion.vue +2 -2
  226. package/components/feedback/ActivityItem.d.ts +3 -1
  227. package/components/feedback/ActivityItem.jsx +6 -6
  228. package/components/feedback/ActivityItem.vue +5 -4
  229. package/components/feedback/Alert.jsx +4 -4
  230. package/components/feedback/Alert.vue +3 -3
  231. package/components/feedback/Banner.jsx +5 -5
  232. package/components/feedback/Banner.vue +5 -5
  233. package/components/feedback/ContextMenu.d.ts +3 -1
  234. package/components/feedback/ContextMenu.jsx +5 -5
  235. package/components/feedback/ContextMenu.vue +5 -5
  236. package/components/feedback/DotActivity.vue +3 -3
  237. package/components/feedback/Drawer.d.ts +2 -0
  238. package/components/feedback/Drawer.jsx +6 -6
  239. package/components/feedback/Drawer.vue +22 -15
  240. package/components/feedback/EmptyState.vue +2 -2
  241. package/components/feedback/LogViewer.vue +1 -1
  242. package/components/feedback/Modal.d.ts +2 -0
  243. package/components/feedback/Modal.jsx +12 -11
  244. package/components/feedback/Modal.vue +10 -8
  245. package/components/feedback/Skeleton.vue +2 -1
  246. package/components/feedback/Spinner.vue +2 -1
  247. package/components/feedback/Stepper.vue +1 -1
  248. package/components/feedback/Timeline.jsx +1 -1
  249. package/components/feedback/Timeline.vue +3 -3
  250. package/components/feedback/Toast.vue +4 -4
  251. package/components/feedback/ToastStack.vue +11 -0
  252. package/components/feedback/Tooltip.vue +19 -0
  253. package/components/feedback/feedback.prompt.md +4 -4
  254. package/components/forms/Checkbox.d.ts +3 -2
  255. package/components/forms/Checkbox.jsx +7 -7
  256. package/components/forms/Checkbox.vue +8 -8
  257. package/components/forms/Combobox.d.ts +2 -0
  258. package/components/forms/Combobox.jsx +12 -10
  259. package/components/forms/Combobox.vue +11 -10
  260. package/components/forms/DatePicker.d.ts +4 -0
  261. package/components/forms/DatePicker.jsx +16 -12
  262. package/components/forms/DatePicker.vue +15 -11
  263. package/components/forms/Dropzone.jsx +2 -2
  264. package/components/forms/Dropzone.vue +5 -5
  265. package/components/forms/Field.d.ts +5 -1
  266. package/components/forms/Field.jsx +4 -4
  267. package/components/forms/Field.vue +4 -4
  268. package/components/forms/Input.d.ts +5 -1
  269. package/components/forms/Input.jsx +8 -8
  270. package/components/forms/Input.vue +3 -3
  271. package/components/forms/MultiSelect.d.ts +2 -0
  272. package/components/forms/MultiSelect.jsx +13 -11
  273. package/components/forms/MultiSelect.vue +12 -11
  274. package/components/forms/NumberStepper.d.ts +3 -1
  275. package/components/forms/NumberStepper.jsx +7 -7
  276. package/components/forms/NumberStepper.vue +8 -7
  277. package/components/forms/Radio.d.ts +5 -3
  278. package/components/forms/Radio.jsx +13 -15
  279. package/components/forms/Radio.vue +9 -10
  280. package/components/forms/RadioGroup.vue +5 -5
  281. package/components/forms/SearchInput.d.ts +3 -1
  282. package/components/forms/SearchInput.jsx +6 -6
  283. package/components/forms/SearchInput.vue +7 -6
  284. package/components/forms/Select.d.ts +3 -2
  285. package/components/forms/Select.jsx +6 -7
  286. package/components/forms/Select.vue +6 -6
  287. package/components/forms/Slider.jsx +3 -3
  288. package/components/forms/Slider.vue +3 -3
  289. package/components/forms/Switch.d.ts +2 -2
  290. package/components/forms/Switch.jsx +20 -22
  291. package/components/forms/Switch.vue +22 -18
  292. package/components/forms/Textarea.vue +5 -5
  293. package/components/forms/forms.prompt.md +1 -1
  294. package/components/forms/input-base.js +2 -1
  295. package/components/icons/Icon.jsx +17 -1
  296. package/components/icons/Icon.prompt.md +2 -0
  297. package/components/icons/Icon.vue +1 -1
  298. package/components/icons/icon-paths.js +15 -0
  299. package/components/index.d.ts +1 -2
  300. package/components/index.js +4 -5
  301. package/components/navigation/Breadcrumb.jsx +6 -10
  302. package/components/navigation/Breadcrumb.vue +6 -12
  303. package/components/navigation/DocTree.jsx +3 -3
  304. package/components/navigation/DocTree.vue +3 -3
  305. package/components/navigation/PageHeading.vue +1 -1
  306. package/components/navigation/PageToolbar.vue +7 -6
  307. package/components/navigation/Sidebar.jsx +4 -23
  308. package/components/navigation/Sidebar.vue +5 -10
  309. package/components/navigation/ThemeToggle.vue +2 -1
  310. package/components/navigation/TopBar.d.ts +3 -1
  311. package/components/navigation/TopBar.jsx +6 -6
  312. package/components/navigation/Tree.jsx +5 -5
  313. package/components/navigation/Tree.vue +13 -8
  314. package/components/navigation/nav-model.js +23 -0
  315. package/components/navigation/theme-store.js +9 -0
  316. package/components/pipeline/ConfigPanel.d.ts +2 -0
  317. package/components/pipeline/ConfigPanel.jsx +9 -9
  318. package/components/pipeline/ConfigPanel.vue +9 -9
  319. package/components/pipeline/DataPreview.d.ts +3 -1
  320. package/components/pipeline/DataPreview.jsx +7 -7
  321. package/components/pipeline/DataPreview.vue +7 -7
  322. package/components/pipeline/NodeCard.jsx +3 -3
  323. package/components/pipeline/NodeCard.vue +3 -3
  324. package/components/pipeline/PaletteItem.vue +2 -2
  325. package/components/pipeline/PaletteListItem.vue +1 -1
  326. package/components/pipeline/PalettePanel.jsx +2 -2
  327. package/components/pipeline/PalettePanel.vue +1 -1
  328. package/components/pipeline/Port.vue +15 -0
  329. package/components/pipeline/PortLabel.vue +15 -0
  330. package/components/pipeline/ZoomControls.vue +1 -1
  331. package/components/pipeline/port-skin.js +13 -0
  332. package/components/px.js +4 -0
  333. package/components/tabs/PanelTabs.vue +5 -4
  334. package/components/tabs/PillTabs.vue +5 -4
  335. package/components/tabs/SegmentedControl.d.ts +2 -2
  336. package/components/tabs/SegmentedControl.jsx +7 -7
  337. package/components/tabs/SegmentedControl.vue +7 -7
  338. package/components/tabs/Tabs.d.ts +5 -3
  339. package/components/tabs/Tabs.jsx +15 -15
  340. package/components/tabs/Tabs.vue +5 -5
  341. package/components/tabs/ViewToggle.d.ts +3 -1
  342. package/components/tabs/ViewToggle.jsx +5 -5
  343. package/components/tabs/ViewToggle.vue +5 -5
  344. package/components/tabs/tabs.prompt.md +1 -1
  345. package/keybricks.css +4 -0
  346. package/package.json +6 -3
  347. package/readme.md +91 -122
  348. package/readme.zh-CN.md +121 -0
  349. package/tokens/alpha.css +3 -1
  350. package/tokens/charts.css +20 -17
  351. package/tokens/colors.css +3 -3
  352. package/tokens/dark.css +3 -3
  353. package/tokens/hover.css +32 -0
  354. package/tokens/interaction.css +3 -1
  355. package/tokens/layout.css +31 -7
  356. package/tokens/tokens.d.ts +58 -0
  357. package/tokens/tokens.js +179 -92
  358. package/tokens/tokens.json +327 -114
  359. package/tokens/typography.css +45 -15
  360. package/components/_cards/brand-visual.card.html +0 -43
  361. package/components/_cards/datatable.card.html +0 -48
  362. package/components/_shared/src-brand.js +0 -1
  363. package/components/brand/BrandVisual.d.ts +0 -17
  364. package/components/brand/BrandVisual.jsx +0 -29
  365. package/components/brand/BrandVisual.vue +0 -34
  366. package/components/brand/_dir.card.html +0 -13
  367. package/components/brand/brandMotifs.js +0 -210
  368. package/components/buttons/_dir.card.html +0 -13
  369. package/components/charts/ChartKit.vue +0 -47
  370. package/components/charts/_dir.card.html +0 -7
  371. package/components/data/DataTable.d.ts +0 -28
  372. package/components/data/DataTable.jsx +0 -32
  373. package/components/data/DataTable.vue +0 -25
  374. package/components/data/_dir.card.html +0 -13
  375. package/components/display/_dir.card.html +0 -13
  376. package/components/feedback/_dir.card.html +0 -13
  377. package/components/forms/_dir.card.html +0 -13
  378. package/components/icons/_dir.card.html +0 -13
  379. package/components/navigation/_dir.card.html +0 -13
  380. package/components/pipeline/_dir.card.html +0 -13
  381. package/components/tabs/_dir.card.html +0 -13
@@ -2,11 +2,12 @@ import React from "react";
2
2
  import { Icon } from "../icons/Icon.jsx";
3
3
 
4
4
  /**
5
- * Switch — 36×20 pill, 16px knob, 160ms ease-out slide.
5
+ * Switch — pill track sized by the region's tier (sm 28×16 · md 36×20 · lg 44×24), knob 4px smaller, 160ms ease-out slide.
6
+ * Track, knob, glyph and label all read the tier tokens (tokens/layout.css), so a Switch in a form row matches its neighbours.
6
7
  *
7
8
  * checked / onChange(next) controlled boolean
8
9
  * disabled 50% opacity, no pointer
9
- * size "md" (36×20, default) | "lg" (44×24 — needed for knobIcon / knobText)
10
+ * size "sm" | "md" | "lg" — omit to inherit the region's tier; lg is roomy enough for knobIcon / knobText
10
11
  * tone "primary" (default, action blue) | "semantic" (red off → green on)
11
12
  * onColor / offColor explicit track colors; override tone
12
13
  * knobIcon / knobText glyph inside the knob: string | { on, off }. knobText is ONE letter or ONE CJK character.
@@ -18,27 +19,23 @@ import { Icon } from "../icons/Icon.jsx";
18
19
  * inlinePrompt (Vue-friendly alias of insideOn/insideOff pair {on,off})
19
20
  */
20
21
 
21
- const SIZES = {
22
- sm: { w: 28, h: 16, knob: 12, pad: 2, inside: 9, glyph: 8, label: "var(--text-xs)" },
23
- md: { w: 36, h: 20, knob: 16, pad: 2, inside: 10, glyph: 10, label: "var(--text-sm)" },
24
- lg: { w: 44, h: 24, knob: 20, pad: 2, inside: 11, glyph: 12, label: "var(--text-sm)" }
25
- };
22
+ /* Geometry in CSS so it follows the tier without JavaScript: knob = track height − 4, glyph = half the track height. */
23
+ const H = "var(--switch-h)", W = "var(--switch-w)", KNOB = "calc(var(--switch-h) - 4px)", GLYPH = "calc(var(--switch-h) * .5)", TEXT = "max(calc(var(--switch-h) * .5), var(--text-glyph-min))" /* text never drops below the glyph floor (12px in CJK) */;
26
24
 
27
25
  const pick = (v, on) => (v && typeof v === "object" ? (on ? v.on : v.off) : v);
28
26
 
29
- export function Switch({ checked = false, onChange, disabled, size = "md", tone = "primary", onColor, offColor, knobIcon, knobText, insideOn, insideOff, inlinePrompt, label, labelOn, labelOff, loading: loadingProp, beforeChange, style }) {
27
+ export function Switch({ checked = false, onChange, disabled, size, tone = "primary", onColor, offColor, knobIcon, knobText, insideOn, insideOff, inlinePrompt, label, labelOn, labelOff, loading: loadingProp, beforeChange, style }) {
30
28
  // `label` = single caption to the right, shown in text-body regardless of state (settings-row usage)
31
29
  const single = label != null && labelOn == null && labelOff == null;
32
30
  const [busy, setBusy] = React.useState(false);
33
31
  const alive = React.useRef(true);
34
32
  React.useEffect(() => () => { alive.current = false; }, []);
35
33
  const loading = loadingProp || busy;
36
- const s = SIZES[size] || SIZES.md;
37
34
  if (inlinePrompt) { insideOn = insideOn ?? inlinePrompt.on; insideOff = insideOff ?? inlinePrompt.off; }
38
35
  const hasInside = insideOn != null || insideOff != null;
39
- // inside text needs a wider track so the knob never covers the word
40
- const insideExtra = hasInside ? Math.max(0, Math.max(String(insideOn ?? "").length, String(insideOff ?? "").length) * (s.inside * .68) + 6 - (s.w - s.knob - s.pad * 2)) : 0;
41
- const w = s.w + insideExtra;
36
+ // inside text needs a wider track so the knob never covers the word: text runs at half the track height, ~.68em per character
37
+ const insideChars = hasInside ? Math.max(String(insideOn ?? "").length, String(insideOff ?? "").length) : 0;
38
+ const w = hasInside ? `max(${W}, calc(${H} * ${(1 + insideChars * .34).toFixed(2)} + 6px))` : W;
42
39
 
43
40
  const onTrack = onColor || (tone === "semantic" ? "var(--success)" : "var(--action-primary)");
44
41
  const offTrack = offColor || (tone === "semantic" ? "var(--danger)" : "var(--border-strong)");
@@ -61,30 +58,31 @@ export function Switch({ checked = false, onChange, disabled, size = "md", tone
61
58
 
62
59
  const glyphIcon = pick(knobIcon, checked);
63
60
  const glyphText = pick(knobText, checked);
64
- const knobX = checked ? w - s.knob - s.pad : s.pad;
61
+ const knobX = checked ? `calc(100% - ${H} + 2px)` : 2;
65
62
 
66
- const knob = <span aria-hidden="true" style={{ position: "absolute", top: s.pad, left: knobX, width: s.knob, height: s.knob, borderRadius: "var(--radius-full)", background: "var(--bg-surface)", boxShadow: "0 1px 2px rgba(0,0,0,.18)", transition: "left 160ms ease-out", display: "flex", alignItems: "center", justifyContent: "center", color: track, fontSize: s.glyph, lineHeight: 1, fontWeight: 600, fontFamily: "var(--font-sans)", overflow: "hidden" }}>
63
+ const knob = <span aria-hidden="true" style={{ position: "absolute", top: 2, left: knobX, width: KNOB, height: KNOB, borderRadius: "var(--radius-full)", background: "var(--bg-surface)", boxShadow: "0 1px 2px rgba(0,0,0,.18)", transition: "left 160ms ease-out", display: "flex", alignItems: "center", justifyContent: "center", color: track, fontSize: TEXT, lineHeight: 1, fontWeight: "var(--weight-semibold)", fontFamily: "var(--font-sans)", overflow: "hidden" }}>
67
64
  {loading
68
- ? <span style={{ width: s.glyph, height: s.glyph, borderRadius: "50%", border: `1.5px solid ${track}`, borderRightColor: "transparent", animation: "kb-spin 720ms linear infinite" }}></span>
69
- : glyphIcon ? <Icon name={glyphIcon} size={s.glyph} strokeWidth={2.5} />
65
+ ? <span style={{ width: GLYPH, height: GLYPH, borderRadius: "50%", border: `1.5px solid ${track}`, borderRightColor: "transparent", animation: "kb-spin 720ms linear infinite" }}></span>
66
+ : glyphIcon ? <Icon name={glyphIcon} size={10} strokeWidth={2.5} style={{ width: GLYPH, height: GLYPH }} />
70
67
  : glyphText ? <span>{String(glyphText).slice(0, 1)}</span> : null}
71
68
  </span>;
72
69
 
73
- const insideStyle = (side) => ({ position: "absolute", top: 0, bottom: 0, [side]: s.pad + 4, display: "flex", alignItems: "center", fontSize: s.inside, lineHeight: 1, fontWeight: 600, fontFamily: "var(--font-sans)", letterSpacing: ".2px", color: "var(--on-accent)", transition: "opacity 160ms ease-out", pointerEvents: "none", whiteSpace: "nowrap" });
70
+ const insideStyle = (side) => ({ position: "absolute", top: 0, bottom: 0, [side]: 6, display: "flex", alignItems: "center", fontSize: TEXT, lineHeight: 1, fontWeight: "var(--weight-semibold)", fontFamily: "var(--font-sans)", letterSpacing: ".2px", color: "var(--on-accent)", transition: "opacity 160ms ease-out", pointerEvents: "none", whiteSpace: "nowrap" });
74
71
 
75
72
  const pill = <span role="switch" aria-checked={checked} aria-busy={loading || undefined} aria-disabled={disabled || undefined} tabIndex={inert ? -1 : 0} onClick={flip}
76
73
  onKeyDown={e => { if (e.key === " " || e.key === "Enter") { e.preventDefault(); flip(); } }}
77
- style={{ position: "relative", display: "inline-block", width: w, height: s.h, borderRadius: "var(--radius-full)", background: track, transition: "background 160ms ease-out", cursor: inert ? (loading ? "progress" : "not-allowed") : "pointer", flexShrink: 0, verticalAlign: "middle", outline: "none" }}>
74
+ style={{ position: "relative", display: "inline-block", width: w, height: H, borderRadius: "var(--radius-full)", "--sw-track": track, background: "var(--sw-bg, var(--sw-track))", transition: "background 160ms ease-out", cursor: inert ? (loading ? "progress" : "not-allowed") : "pointer", flexShrink: 0, verticalAlign: "middle", outline: "none" }}>
78
75
  {hasInside && <span style={{ ...insideStyle("left"), opacity: checked ? 1 : 0 }}>{insideOn}</span>}
79
76
  {hasInside && <span style={{ ...insideStyle("right"), opacity: checked ? 0 : 1 }}>{insideOff}</span>}
80
77
  {knob}
81
78
  </span>;
82
79
 
83
- if (single) return <span style={{ display: "inline-flex", alignItems: "center", gap: 8, opacity: disabled ? .5 : 1, ...style }}>{pill}<span onClick={flip} style={{ fontSize: s.label, lineHeight: 1, fontFamily: "var(--font-sans)", fontWeight: 500, color: "var(--text-body)", cursor: inert ? "default" : "pointer", userSelect: "none" }}>{label}</span></span>;
84
- if (labelOn == null && labelOff == null) return <span style={{ display: "inline-flex", alignItems: "center", opacity: disabled ? .5 : 1, ...style }}>{pill}</span>;
80
+ const capBase = { fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", fontFamily: "var(--font-sans)", cursor: inert ? "default" : "pointer", userSelect: "none" };
81
+ if (single) return <span data-size={size} data-sw="" style={{ display: "inline-flex", alignItems: "center", gap: "var(--control-gap)", opacity: disabled ? .5 : 1, ...style }}>{pill}<span onClick={flip} style={{ ...capBase, fontWeight: "var(--weight-regular)", color: "var(--text-body)" }}>{label}</span></span>;
82
+ if (labelOn == null && labelOff == null) return <span data-size={size} style={{ display: "inline-flex", alignItems: "center", opacity: disabled ? .5 : 1, ...style }}>{pill}</span>;
85
83
 
86
- const cap = (text, active) => text == null ? null : <span style={{ fontSize: s.label, lineHeight: 1, fontFamily: "var(--font-sans)", fontWeight: active ? 500 : 400, color: active ? "var(--text-heading)" : "var(--text-muted)", transition: "color 160ms ease-out", cursor: inert ? "default" : "pointer", userSelect: "none" }} onClick={flip}>{text}</span>;
87
- return <span style={{ display: "inline-flex", alignItems: "center", gap: 8, opacity: disabled ? .5 : 1, ...style }}>
84
+ const cap = (text, active) => text == null ? null : <span style={{ ...capBase, fontWeight: active ? "var(--weight-medium)" : "var(--weight-regular)", color: active ? "var(--text-heading)" : "var(--text-muted)", transition: "color 160ms ease-out" }} onClick={flip}>{text}</span>;
85
+ return <span data-size={size} data-sw="" style={{ display: "inline-flex", alignItems: "center", gap: "var(--control-gap)", opacity: disabled ? .5 : 1, ...style }}>
88
86
  {cap(labelOff, !checked)}{pill}{cap(labelOn, checked)}
89
87
  </span>;
90
88
  }
@@ -1,32 +1,36 @@
1
1
  <script setup>
2
2
  import { ref, computed, onBeforeUnmount } from 'vue'
3
3
  import Icon from '../icons/Icon.vue'
4
- // Switch — 36×20 pill (lg 44×24), 16/20px knob, 160ms slide. v-model:checked.
4
+ // Switch — pill track sized by the region's tier (sm 28×16 · md 36×20 · lg 44×24), knob 4px smaller, 160ms slide. v-model:checked.
5
+ // Geometry is CSS (tokens/layout.css --switch-w/-h), so it follows the tier without JavaScript; `size` stamps data-size to override.
5
6
  // tone "primary" | "semantic" (red off → green on); onColor/offColor override.
6
7
  // knobIcon / knobText: string | { on, off } — knobText is ONE letter or ONE CJK char.
7
8
  // insideOn / insideOff: text inside the pill opposite the knob. labelOn / labelOff: captions beside the pill.
8
9
  // loading: controlled spinner. beforeChange(next) → Promise|boolean gates the flip and shows loading meanwhile.
9
10
  const props = defineProps({
10
11
  checked: Boolean, disabled: Boolean, loading: Boolean,
11
- size: { type: String, default: 'md' }, tone: { type: String, default: 'primary' },
12
+ size: String, tone: { type: String, default: 'primary' },
12
13
  onColor: String, offColor: String,
13
14
  knobIcon: [String, Object], knobText: [String, Object],
14
- insideOn: String, insideOff: String, label: String, labelOn: String, labelOff: String,
15
+ insideOn: String, insideOff: String, inlinePrompt: Object, label: String, labelOn: String, labelOff: String,
15
16
  beforeChange: Function, style: Object
16
17
  })
17
18
  const emit = defineEmits(['update:checked', 'change'])
18
- const SIZES = { sm: { w: 28, h: 16, knob: 12, pad: 2, inside: 9, glyph: 8 }, md: { w: 36, h: 20, knob: 16, pad: 2, inside: 10, glyph: 10 }, lg: { w: 44, h: 24, knob: 20, pad: 2, inside: 11, glyph: 12 } }
19
- const s = computed(() => SIZES[props.size] || SIZES.md)
19
+ const H = 'var(--switch-h)', W = 'var(--switch-w)', KNOB = 'calc(var(--switch-h) - 4px)', GLYPH = 'calc(var(--switch-h) * .5)', TEXT = 'max(calc(var(--switch-h) * .5), var(--text-glyph-min))' /* text never drops below the glyph floor (12px in CJK) */
20
20
  const busy = ref(false); let alive = true; onBeforeUnmount(() => { alive = false })
21
21
  const isLoading = computed(() => props.loading || busy.value)
22
22
  const inert = computed(() => props.disabled || isLoading.value)
23
- const hasInside = computed(() => props.insideOn != null || props.insideOff != null)
24
- const width = computed(() => { if (!hasInside.value) return s.value.w; const n = Math.max((props.insideOn || '').length, (props.insideOff || '').length); return s.value.w + Math.max(0, n * (s.value.inside * .68) + 6 - (s.value.w - s.value.knob - s.value.pad * 2)) })
23
+ // inlinePrompt { on, off } is an alias of the insideOn / insideOff pair (same as Switch.jsx)
24
+ const inOn = computed(() => props.insideOn ?? (props.inlinePrompt ? props.inlinePrompt.on : undefined))
25
+ const inOff = computed(() => props.insideOff ?? (props.inlinePrompt ? props.inlinePrompt.off : undefined))
26
+ const hasInside = computed(() => inOn.value != null || inOff.value != null)
27
+ // inside text needs a wider track so the knob never covers the word: text runs at half the track height, ~.68em per character
28
+ const width = computed(() => { if (!hasInside.value) return W; const n = Math.max((inOn.value || '').length, (inOff.value || '').length); return `max(${W}, calc(${H} * ${(1 + n * .34).toFixed(2)} + 6px))` })
25
29
  const track = computed(() => props.checked ? (props.onColor || (props.tone === 'semantic' ? 'var(--success)' : 'var(--action-primary)')) : (props.offColor || (props.tone === 'semantic' ? 'var(--danger)' : 'var(--border-strong)')))
26
30
  const pick = (v) => v && typeof v === 'object' ? (props.checked ? v.on : v.off) : v
27
31
  const glyphIcon = computed(() => pick(props.knobIcon))
28
32
  const glyphText = computed(() => { const t = pick(props.knobText); return t ? String(t).slice(0, 1) : '' })
29
- const knobX = computed(() => props.checked ? width.value - s.value.knob - s.value.pad : s.value.pad)
33
+ const knobX = computed(() => props.checked ? `calc(100% - ${H} + 2px)` : '2px')
30
34
  async function flip() {
31
35
  if (inert.value) return
32
36
  const next = !props.checked
@@ -39,23 +43,23 @@ async function flip() {
39
43
  }
40
44
  emit('update:checked', next); emit('change', next)
41
45
  }
42
- const insideStyle = (side, on) => ({ position: 'absolute', top: 0, bottom: 0, [side]: (s.value.pad + 4) + 'px', display: 'flex', alignItems: 'center', fontSize: s.value.inside + 'px', lineHeight: 1, fontWeight: 600, fontFamily: 'var(--font-sans)', letterSpacing: '.2px', color: 'var(--on-accent)', transition: 'opacity 160ms ease-out', pointerEvents: 'none', whiteSpace: 'nowrap', opacity: on === props.checked ? 1 : 0 })
43
- const cap = (active) => ({ fontSize: props.size === 'sm' ? 'var(--text-xs)' : 'var(--text-sm)', lineHeight: 1, fontFamily: 'var(--font-sans)', fontWeight: active ? 500 : 400, color: active ? 'var(--text-heading)' : 'var(--text-muted)', transition: 'color 160ms ease-out', cursor: inert.value ? 'default' : 'pointer', userSelect: 'none' })
46
+ const insideStyle = (side, on) => ({ position: 'absolute', top: 0, bottom: 0, [side]: '6px', display: 'flex', alignItems: 'center', fontSize: TEXT, lineHeight: 1, fontWeight: 'var(--weight-semibold)', fontFamily: 'var(--font-sans)', letterSpacing: '.2px', color: 'var(--on-accent)', transition: 'opacity 160ms ease-out', pointerEvents: 'none', whiteSpace: 'nowrap', opacity: on === props.checked ? 1 : 0 })
47
+ const cap = (active) => ({ fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', fontFamily: 'var(--font-sans)', fontWeight: active ? 'var(--weight-medium)' : 'var(--weight-regular)', color: active ? 'var(--text-heading)' : 'var(--text-muted)', transition: 'color 160ms ease-out', cursor: inert.value ? 'default' : 'pointer', userSelect: 'none' })
44
48
  </script>
45
49
  <template>
46
- <span :style="{ display: 'inline-flex', alignItems: 'center', gap: '8px', opacity: disabled ? .5 : 1, ...style }">
50
+ <span :data-size="size" data-sw="" :style="{ display: 'inline-flex', alignItems: 'center', gap: 'var(--control-gap)', opacity: disabled ? .5 : 1, ...style }">
47
51
  <span v-if="labelOff != null" :style="cap(!checked)" @click="flip">{{ labelOff }}</span>
48
52
  <span role="switch" :aria-checked="checked" :aria-busy="isLoading || undefined" :aria-disabled="disabled || undefined" :tabindex="inert ? -1 : 0" @click="flip" @keydown.space.prevent="flip" @keydown.enter.prevent="flip"
49
- :style="{ position: 'relative', display: 'inline-block', width: width + 'px', height: s.h + 'px', borderRadius: 'var(--radius-full)', background: track, transition: 'background 160ms ease-out', cursor: inert ? (isLoading ? 'progress' : 'not-allowed') : 'pointer', flexShrink: 0, verticalAlign: 'middle', outline: 'none' }">
50
- <span v-if="hasInside" :style="insideStyle('left', true)">{{ insideOn }}</span>
51
- <span v-if="hasInside" :style="insideStyle('right', false)">{{ insideOff }}</span>
52
- <span aria-hidden="true" :style="{ position: 'absolute', top: s.pad + 'px', left: knobX + 'px', width: s.knob + 'px', height: s.knob + 'px', borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', boxShadow: '0 1px 2px rgba(0,0,0,.18)', transition: 'left 160ms ease-out', display: 'flex', alignItems: 'center', justifyContent: 'center', color: track, fontSize: s.glyph + 'px', lineHeight: 1, fontWeight: 600, fontFamily: 'var(--font-sans)', overflow: 'hidden' }">
53
- <span v-if="isLoading" :style="{ width: s.glyph + 'px', height: s.glyph + 'px', borderRadius: '50%', border: '1.5px solid ' + track, borderRightColor: 'transparent', animation: 'kb-spin 720ms linear infinite' }"></span>
54
- <Icon v-else-if="glyphIcon" :name="glyphIcon" :size="s.glyph" :stroke-width="2.5" />
53
+ :style="{ position: 'relative', display: 'inline-block', width: width, height: H, borderRadius: 'var(--radius-full)', '--sw-track': track, background: 'var(--sw-bg, var(--sw-track))', transition: 'background 160ms ease-out', cursor: inert ? (isLoading ? 'progress' : 'not-allowed') : 'pointer', flexShrink: 0, verticalAlign: 'middle', outline: 'none' }">
54
+ <span v-if="hasInside" :style="insideStyle('left', true)">{{ inOn }}</span>
55
+ <span v-if="hasInside" :style="insideStyle('right', false)">{{ inOff }}</span>
56
+ <span aria-hidden="true" :style="{ position: 'absolute', top: '2px', left: knobX, width: KNOB, height: KNOB, borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', boxShadow: '0 1px 2px rgba(0,0,0,.18)', transition: 'left 160ms ease-out', display: 'flex', alignItems: 'center', justifyContent: 'center', color: track, fontSize: TEXT, lineHeight: 1, fontWeight: 'var(--weight-semibold)', fontFamily: 'var(--font-sans)', overflow: 'hidden' }">
57
+ <span v-if="isLoading" :style="{ width: GLYPH, height: GLYPH, borderRadius: '50%', border: '1.5px solid ' + track, borderRightColor: 'transparent', animation: 'kb-spin 720ms linear infinite' }"></span>
58
+ <Icon v-else-if="glyphIcon" :name="glyphIcon" :size="10" :stroke-width="2.5" :style="{ width: GLYPH, height: GLYPH }" />
55
59
  <span v-else-if="glyphText">{{ glyphText }}</span>
56
60
  </span>
57
61
  </span>
58
62
  <span v-if="labelOn != null" :style="cap(checked)" @click="flip">{{ labelOn }}</span>
59
- <span v-if="label != null && labelOn == null && labelOff == null" :style="{ fontSize: size === 'sm' ? 'var(--text-xs)' : 'var(--text-sm)', lineHeight: 1, fontFamily: 'var(--font-sans)', fontWeight: 500, color: 'var(--text-body)', cursor: inert ? 'default' : 'pointer', userSelect: 'none' }" @click="flip">{{ label }}</span>
63
+ <span v-if="label != null && labelOn == null && labelOff == null" :style="{ fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-regular)', color: 'var(--text-body)', cursor: inert ? 'default' : 'pointer', userSelect: 'none' }" @click="flip">{{ label }}</span>
60
64
  </span>
61
65
  </template>
@@ -1,20 +1,20 @@
1
1
  <script setup>
2
2
  import { ref, computed } from 'vue'
3
3
  import { inputBase } from './input-base.js'
4
- // Multiline textarea, r8, default 90px tall. v-model. showCount + maxLength renders a bottom-right "n/max" counter (red past the limit).
5
- const props = defineProps({ modelValue: String, rows: { type: Number, default: 4 }, error: Boolean, disabled: Boolean, showCount: Boolean, maxLength: Number, style: Object })
4
+ // Multiline textarea, r8, default 90px tall; text follows the region's tier. v-model. showCount + maxLength renders a bottom-right "n/max" counter (red past the limit).
5
+ const props = defineProps({ modelValue: String, rows: { type: Number, default: 4 }, error: Boolean, disabled: Boolean, showCount: Boolean, maxLength: Number, size: String, style: Object })
6
6
  defineEmits(['update:modelValue'])
7
7
  const focus = ref(false)
8
8
  const len = computed(() => (props.modelValue || '').length)
9
9
  const over = computed(() => props.maxLength != null && len.value > props.maxLength)
10
- const s = computed(() => ({ ...inputBase, height: '90px', resize: 'vertical', paddingBottom: props.showCount ? '22px' : '8px',
10
+ const s = computed(() => ({ ...inputBase, height: '90px', resize: 'vertical', paddingTop: '8px', paddingBottom: props.showCount ? '22px' : '8px',
11
11
  ...((props.error || over.value) ? { borderColor: 'var(--danger)' } : null),
12
12
  ...(focus.value && !props.disabled ? { borderColor: (props.error || over.value) ? 'var(--danger)' : 'var(--focus-border)', boxShadow: `0 0 0 2px ${(props.error || over.value) ? 'var(--danger-bg)' : 'var(--focus-ring)'}` } : null),
13
13
  ...(props.disabled ? { opacity: .5, cursor: 'not-allowed', background: 'var(--bg-inset)' } : null), ...props.style }))
14
14
  </script>
15
15
  <template>
16
16
  <div style="position:relative">
17
- <textarea :rows="rows" :disabled="disabled" :maxlength="showCount ? undefined : maxLength" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" @focus="focus = true" @blur="focus = false" :style="s"></textarea>
18
- <span v-if="showCount" :style="{ position: 'absolute', right: '10px', bottom: '7px', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: over ? 'var(--danger)' : 'var(--text-muted)' }">{{ len }}{{ maxLength != null ? `/${maxLength}` : '' }}</span>
17
+ <textarea :rows="rows" :disabled="disabled" :data-size="size" :data-field="disabled ? undefined : ''" :maxlength="showCount ? undefined : maxLength" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" @focus="focus = true" @blur="focus = false" :style="s"></textarea>
18
+ <span v-if="showCount" :style="{ position: 'absolute', right: '10px', bottom: '7px', fontFamily: 'var(--font-sans)', fontVariantNumeric: 'tabular-nums', fontSize: 'var(--text-xs)', color: over ? 'var(--danger)' : 'var(--text-muted)' }">{{ len }}{{ maxLength != null ? `/${maxLength}` : '' }}</span>
19
19
  </div>
20
20
  </template>
@@ -15,7 +15,7 @@ Form controls, split by component (each with its own specimen card covering defa
15
15
  Everything is 12px text in 36px controls; readOnly inputs go #fafafa; disabled = 50% opacity; error = red border + red helper text via `Field`'s `error` prop.
16
16
 
17
17
 
18
- `Switch` / `Radio` / `RadioGroup` (additions, not in the source file). Switch: default 36×20 blue-on / border-strong-off; `size="lg"` (44×24) whenever the knob carries a glyph — `knobIcon` (string or `{on,off}`) or `knobText` (ONE letter / ONE CJK char); `insideOn`/`insideOff` put 1–3 characters inside the pill; `labelOn`/`labelOff` put captions on both sides with the active one in text-heading; `tone="semantic"` = danger off → success on, only for states with consequence (a live integration, a kill switch); `beforeChange` async-gates the flip with a spinner in the knob, `loading` does the same under your control.
18
+ `Switch` / `Radio` / `RadioGroup` (additions, not in the source file). Switch: the track follows the size tier (sm 28×16 · md 36×20 · lg 44×24), blue-on / border-strong-off; a knob glyph needs the lg tier (`size="lg"` only when the switch stands alone) — `knobIcon` (string or `{on,off}`) or `knobText` (ONE letter / ONE CJK char); `insideOn`/`insideOff` put 1–3 characters inside the pill; `labelOn`/`labelOff` put captions on both sides with the active one in text-heading; `tone="semantic"` = danger off → success on, only for states with consequence (a live integration, a kill switch); `beforeChange` async-gates the flip with a spinner in the knob, `loading` does the same under your control.
19
19
 
20
20
  ```jsx
21
21
  <Switch label="Debug Mode" checked={on} onChange={setOn}/>
@@ -1,3 +1,4 @@
1
1
  // Shared base style for text-like controls (Input, Textarea, Combobox, DatePicker).
2
2
  // Lives in a plain module because <script setup> cannot export values.
3
- export const inputBase = { height: 'var(--h-input)', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-control)', background: 'var(--bg-surface)', padding: '8px 12px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: 'var(--text-primary)', outline: 'none', boxSizing: 'border-box', width: '100%' }
3
+ // Height, text and padding come from the size tier of the enclosing region (tokens/layout.css): sm 28 · md 32 · lg 40.
4
+ export const inputBase = { height: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: '1px solid var(--field-border, var(--border-control))', background: 'var(--bg-surface)', padding: '0 var(--control-px)', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-primary)', outline: 'none', boxSizing: 'border-box', width: '100%' }
@@ -1,7 +1,8 @@
1
1
  import React from "react";
2
2
  // Keybricks icon set — the source file uses Lucide stroke glyphs (24-grid, stroke 2,
3
3
  // round caps/joins, currentColor) rendered at 14/16px. Path data below is from the
4
- // public Lucide set, matching the glyphs used across Keyfunc UI v8.
4
+ // public Lucide set, matching the glyphs used across Keyfunc UI v8, plus thirteen Keybricks
5
+ // glyphs (event … time-restatement, ai, ai-sparkles) drawn to the same rules.
5
6
  export const ICON_PATHS = {
6
7
  search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
7
8
  bell: '<path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/>',
@@ -74,6 +75,21 @@ export const ICON_PATHS = {
74
75
  info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
75
76
  "triangle-alert": '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 20h16a2 2 0 0 0 1.73-2Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
76
77
  calendar: '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/>',
78
+ // Keybricks glyphs — drawn to the core-set rules (see readme.md › Iconography)
79
+ event: '<path d="M17 2 22 12 17 22 12 12Z"/><path d="M7 2 2 12l5 10"/><path d="M12 2 7 12l5 10"/>',
80
+ consolidation: '<rect width="4" height="4" x="2" y="17" rx="1"/><rect width="4" height="4" x="10" y="17" rx="1"/><rect width="4" height="4" x="18" y="17" rx="1"/><path d="M4 17v-5a2 2 0 0 1 2-2h2"/><path d="M20 17v-5a2 2 0 0 0-2-2h-2"/><path d="M12 17v-4"/><path d="M12 3v6"/><path d="M9 6h6"/>',
81
+ journal: '<rect width="20" height="16" x="2" y="4" rx="2"/><path d="M2 8h20"/><path d="M12 8v12"/><path d="M6 12h2"/><path d="M16 16h2"/>',
82
+ reconciliation: '<path d="M3 6h8"/><path d="m15 4 2 2 4-4"/><path d="M3 10h18"/><path d="M3 18h8"/><path d="m15 16 2 2 4-4"/><path d="M3 22h18"/>',
83
+ "view-tree": '<rect width="4" height="4" x="4" y="2" rx="1"/><rect width="4" height="4" x="16" y="10" rx="1"/><rect width="4" height="4" x="16" y="18" rx="1"/><path d="M6 6v4a2 2 0 0 0 2 2h8"/><path d="M6 10v8a2 2 0 0 0 2 2h8"/>',
84
+ "drilldown": '<path d="M12 2v11"/><path d="m8 9 4 4 4-4"/><path d="M19 17H5"/><path d="M16 21H8"/>',
85
+ "add-below": '<path d="M11 2v10"/><path d="M6 7h10"/><path d="M22 9v5a4 4 0 0 1-4 4H2"/><path d="m6 14-4 4 4 4"/>',
86
+ "copy-data": '<path d="M14 22h5a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-5"/><path d="m11 16 4-4-4-4"/><path d="M3 22v-6a4 4 0 0 1 4-4h8"/>',
87
+ "task-execution": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M8 14h8"/><path d="m13 11 3 3-3 3"/>',
88
+ "time-future": '<path d="M20 6A10 10 0 1 0 22 12"/><path d="M12 7v5h5"/><path d="M21 2v5h-5"/>',
89
+ "time-restatement": '<path d="M2 12A10 10 0 1 0 4 6"/><path d="M12 7v5h5"/><path d="M3 2v5h5"/>',
90
+ // AI glyphs (2.3.0) — traced from the two reference glyphs, redrawn to core rules (stroke 2, no fill, integer endpoints, >=4 between separate elements)
91
+ ai: '<path d="M9 4H6a4 4 0 0 0-4 4v10a4 4 0 0 0 4 4h10a4 4 0 0 0 4-4v-3"/><path d="M16 2c1.2 3 3 4.8 6 6c-3 1.2-4.8 3-6 6c-1.2-3-3-4.8-6-6c3-1.2 4.8-3 6-6Z"/><path d="M9 12c.6 1.5 1.5 2.4 3 3c-1.5.6-2.4 1.5-3 3c-.6-1.5-1.5-2.4-3-3c1.5-.6 2.4-1.5 3-3Z"/>',
92
+ "ai-sparkles": '<path d="M9 2c1.4 3.5 3.5 5.6 7 7c-3.5 1.4-5.6 3.5-7 7c-1.4-3.5-3.5-5.6-7-7c3.5-1.4 5.6-3.5 7-7Z"/><path d="M17 13c.8 2 2 3.2 4 4c-2 .8-3.2 2-4 4c-.8-2-2-3.2-4-4c2-.8 3.2-2 4-4Z"/>',
77
93
  };
78
94
 
79
95
  import { FEATHER_PATHS } from "./feather-paths.js";
@@ -6,3 +6,5 @@ Stroke icon component embedding the Lucide glyphs used across Keybricks; use for
6
6
  ```
7
7
 
8
8
  Sizes in the product: 14 (dense chrome), 16 (nav/buttons), 18 (palette tiles), 22 (node tiles). Color inherits `currentColor`. Categories pair hue + tint bg: source `database/table/layers/grid-3x3`, join `text-search/shuffle`, transform `sliders-horizontal/split/arrow-right-left/menu/funnel/chart-no-axes-column/trending-up/code`, merge `database/merge`, output `table/layers/grid-3x3`.
9
+
10
+ Keybricks glyphs (drawn to the same grid rules): modules `reconciliation`, `consolidation`, `journal`, `event`, `view-tree`; actions `drilldown`, `add-below`, `copy-data`, `task-execution`, `time-future`, `time-restatement`. For hierarchy use `layers`; for change use `repeat`.
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  // Keybricks icon set — Lucide stroke glyphs (24-grid, stroke 2, round caps/joins, currentColor), rendered at 14/16px.
3
- // Full path dictionary mirrors icons/Icon.jsx's ICON_PATHS (80+ entries) — see that file for the complete list.
3
+ // Full path dictionary mirrors icons/Icon.jsx's ICON_PATHS (81 entries) — see that file for the complete list.
4
4
  import { VUE_ICON_PATHS } from './icon-paths.js'
5
5
  import { FEATHER_PATHS } from './feather-paths.js'
6
6
  const props = defineProps({ name: String, size: { type: Number, default: 16 }, strokeWidth: { type: Number, default: 2 }, color: String, style: Object })
@@ -70,4 +70,19 @@ export const VUE_ICON_PATHS = {
70
70
  info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
71
71
  "triangle-alert": '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 20h16a2 2 0 0 0 1.73-2Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
72
72
  calendar: '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/>',
73
+ // Keybricks glyphs — drawn to the core-set rules (see readme.md › Iconography)
74
+ event: '<path d="M17 2 22 12 17 22 12 12Z"/><path d="M7 2 2 12l5 10"/><path d="M12 2 7 12l5 10"/>',
75
+ consolidation: '<rect width="4" height="4" x="2" y="17" rx="1"/><rect width="4" height="4" x="10" y="17" rx="1"/><rect width="4" height="4" x="18" y="17" rx="1"/><path d="M4 17v-5a2 2 0 0 1 2-2h2"/><path d="M20 17v-5a2 2 0 0 0-2-2h-2"/><path d="M12 17v-4"/><path d="M12 3v6"/><path d="M9 6h6"/>',
76
+ journal: '<rect width="20" height="16" x="2" y="4" rx="2"/><path d="M2 8h20"/><path d="M12 8v12"/><path d="M6 12h2"/><path d="M16 16h2"/>',
77
+ reconciliation: '<path d="M3 6h8"/><path d="m15 4 2 2 4-4"/><path d="M3 10h18"/><path d="M3 18h8"/><path d="m15 16 2 2 4-4"/><path d="M3 22h18"/>',
78
+ "view-tree": '<rect width="4" height="4" x="4" y="2" rx="1"/><rect width="4" height="4" x="16" y="10" rx="1"/><rect width="4" height="4" x="16" y="18" rx="1"/><path d="M6 6v4a2 2 0 0 0 2 2h8"/><path d="M6 10v8a2 2 0 0 0 2 2h8"/>',
79
+ "drilldown": '<path d="M12 2v11"/><path d="m8 9 4 4 4-4"/><path d="M19 17H5"/><path d="M16 21H8"/>',
80
+ "add-below": '<path d="M11 2v10"/><path d="M6 7h10"/><path d="M22 9v5a4 4 0 0 1-4 4H2"/><path d="m6 14-4 4 4 4"/>',
81
+ "copy-data": '<path d="M14 22h5a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-5"/><path d="m11 16 4-4-4-4"/><path d="M3 22v-6a4 4 0 0 1 4-4h8"/>',
82
+ "task-execution": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M8 14h8"/><path d="m13 11 3 3-3 3"/>',
83
+ "time-future": '<path d="M20 6A10 10 0 1 0 22 12"/><path d="M12 7v5h5"/><path d="M21 2v5h-5"/>',
84
+ "time-restatement": '<path d="M2 12A10 10 0 1 0 4 6"/><path d="M12 7v5h5"/><path d="M3 2v5h5"/>',
85
+ // AI glyphs (2.3.0) — traced from the two reference glyphs, redrawn to core rules (stroke 2, no fill, integer endpoints, >=4 between separate elements)
86
+ ai: '<path d="M9 4H6a4 4 0 0 0-4 4v10a4 4 0 0 0 4 4h10a4 4 0 0 0 4-4v-3"/><path d="M16 2c1.2 3 3 4.8 6 6c-3 1.2-4.8 3-6 6c-1.2-3-3-4.8-6-6c3-1.2 4.8-3 6-6Z"/><path d="M9 12c.6 1.5 1.5 2.4 3 3c-1.5.6-2.4 1.5-3 3c-.6-1.5-1.5-2.4-3-3c1.5-.6 2.4-1.5 3-3Z"/>',
87
+ "ai-sparkles": '<path d="M9 2c1.4 3.5 3.5 5.6 7 7c-3.5 1.4-5.6 3.5-7 7c-1.4-3.5-3.5-5.6-7-7c3.5-1.4 5.6-3.5 7-7Z"/><path d="M17 13c.8 2 2 3.2 4 4c-2 .8-3.2 2-4 4c-.8-2-2-3.2-4-4c2-.8 3.2-2 4-4Z"/>',
73
88
  };
@@ -1,5 +1,4 @@
1
1
  /* Generated barrel — do not edit by hand. */
2
- export * from "./brand/BrandVisual";
3
2
  export * from "./buttons/ActionChip";
4
3
  export * from "./buttons/Button";
5
4
  export * from "./buttons/IconButton";
@@ -32,7 +31,6 @@ export * from "./charts/TreemapChart";
32
31
  export * from "./charts/WaterfallChart";
33
32
  export * from "./charts/WorldMap";
34
33
  export * from "./data/CodeBlock";
35
- export * from "./data/DataTable";
36
34
  export * from "./data/DiffValue";
37
35
  export * from "./data/FilterBar";
38
36
  export * from "./data/KeyValue";
@@ -40,6 +38,7 @@ export * from "./data/ListRow";
40
38
  export * from "./data/Pagination";
41
39
  export * from "./data/SmartTable";
42
40
  export * from "./data/Spreadsheet";
41
+ export * from "./data/Table";
43
42
  export * from "./display/Avatar";
44
43
  export * from "./display/Badge";
45
44
  export * from "./display/Card";
@@ -1,9 +1,6 @@
1
1
  /* Generated barrel — do not edit by hand. Run: node scripts/build-index.mjs
2
2
  Source-only package: consumers compile these .jsx files with their own toolchain. */
3
3
 
4
- /* brand/ */
5
- export { BrandVisual } from "./brand/BrandVisual.jsx";
6
-
7
4
  /* buttons/ */
8
5
  export { ActionChip } from "./buttons/ActionChip.jsx";
9
6
  export { Button, ButtonGroup } from "./buttons/Button.jsx";
@@ -41,7 +38,6 @@ export { WorldMap } from "./charts/WorldMap.jsx";
41
38
 
42
39
  /* data/ */
43
40
  export { CodeBlock } from "./data/CodeBlock.jsx";
44
- export { DataTable } from "./data/DataTable.jsx";
45
41
  export { DiffValue } from "./data/DiffValue.jsx";
46
42
  export { FilterBar } from "./data/FilterBar.jsx";
47
43
  export { KeyValue, KeyValueList } from "./data/KeyValue.jsx";
@@ -49,6 +45,9 @@ export { ListRow } from "./data/ListRow.jsx";
49
45
  export { Pagination } from "./data/Pagination.jsx";
50
46
  export { SmartTable } from "./data/SmartTable.jsx";
51
47
  export { Spreadsheet } from "./data/Spreadsheet.jsx";
48
+ export { Table } from "./data/Table.jsx";
49
+ /** @deprecated renamed to Table in 1.0.6 — still exported in 2.x, removed in 3.0 */
50
+ export { Table as DataTable } from "./data/Table.jsx";
52
51
 
53
52
  /* display/ */
54
53
  export { Avatar, AvatarGroup } from "./display/Avatar.jsx";
@@ -91,7 +90,7 @@ export { ICON_PATHS, Icon } from "./icons/Icon.jsx";
91
90
  export { Unicon } from "./icons/Unicon.jsx";
92
91
 
93
92
  /* navigation/ */
94
- export { Breadcrumb, ThemeToggle } from "./navigation/Breadcrumb.jsx";
93
+ export { Breadcrumb, ThemeToggle, applyStoredTheme } from "./navigation/Breadcrumb.jsx";
95
94
  export { DocTree } from "./navigation/DocTree.jsx";
96
95
  export { KEYBRICKS_NAV, Sidebar } from "./navigation/Sidebar.jsx";
97
96
  export { PageHeading, PageToolbar, TopBar } from "./navigation/TopBar.jsx";
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { Icon } from "../icons/Icon.jsx";
3
+ import { applyStoredTheme } from "./theme-store.js";
3
4
 
4
5
  function Crumb({ label, last, hovered, onSelect }) {
5
6
  const [hover, setHover] = React.useState(false);
@@ -9,7 +10,7 @@ function Crumb({ label, last, hovered, onSelect }) {
9
10
  onClick={() => !last && onSelect && onSelect(label)}
10
11
  onKeyDown={(e) => { if (!last && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); onSelect && onSelect(label); } }}
11
12
  onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
12
- style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", color: last || on ? "var(--text-heading)" : "var(--text-secondary)", fontWeight: last ? "var(--weight-medium)" : "var(--weight-regular)", cursor: last ? "default" : "pointer", borderRadius: "var(--radius-sm)", transition: "var(--t-hover)" }}>{label}</span>
13
+ style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", color: last || on ? "var(--text-heading)" : "var(--text-secondary)", fontWeight: last ? "var(--weight-medium)" : "var(--weight-regular)", cursor: last ? "default" : "pointer", borderRadius: "var(--radius-sm)", transition: "var(--t-hover)" }}>{label}</span>
13
14
  );
14
15
  }
15
16
 
@@ -34,7 +35,7 @@ function Overflow({ items, onSelect }) {
34
35
  onClick={() => setOpen(!open)}
35
36
  onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpen(!open); } if (e.key === "Escape") setOpen(false); }}
36
37
  onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
37
- style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", width: 20, height: 18, borderRadius: "var(--radius-sm)", cursor: "pointer", color: open || hover ? "var(--text-heading)" : "var(--text-muted)", background: open ? "var(--bg-active)" : hover ? "var(--bg-hover)" : "transparent", transition: "var(--t-hover)" }}>
38
+ style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", width: 24, height: 24, borderRadius: "var(--radius-sm)", cursor: "pointer", color: open || hover ? "var(--text-heading)" : "var(--text-muted)", background: open ? "var(--bg-active)" : hover ? "var(--bg-hover)" : "transparent", transition: "var(--t-hover)" }}>
38
39
  <Icon name="ellipsis" size={14} />
39
40
  </span>
40
41
  {open ? (
@@ -52,7 +53,7 @@ function OverflowItem({ label, depth, onSelect }) {
52
53
  <span role="menuitem" tabIndex={0} onClick={() => onSelect(label)}
53
54
  onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onSelect(label); } }}
54
55
  onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
55
- style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 9px", paddingLeft: 9 + depth * 12, borderRadius: "var(--radius-sm)", cursor: "pointer", background: hover ? "var(--bg-hover)" : "transparent", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", color: "var(--text-heading)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", transition: "var(--t-hover)" }}>
56
+ style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 9px", paddingLeft: 9 + depth * 12, borderRadius: "var(--radius-sm)", cursor: "pointer", background: hover ? "var(--bg-hover)" : "transparent", fontFamily: "var(--font-sans)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", color: "var(--text-heading)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", transition: "var(--t-hover)" }}>
56
57
  {depth > 0 ? <span style={{ color: "var(--text-muted)", display: "inline-flex", flexShrink: 0 }}><Icon name="chevron-right" size={11} /></span> : null}
57
58
  {label}
58
59
  </span>
@@ -111,10 +112,5 @@ export function ThemeToggle({ size = 34, dark: darkProp, onToggle, style }) {
111
112
  );
112
113
  }
113
114
 
114
- /** Reads the persisted theme and applies it before first paint. Call once at app start. */
115
- export function applyStoredTheme() {
116
- if (typeof document === "undefined") return;
117
- let t = "light";
118
- try { t = localStorage.getItem("kb-theme") || "light"; } catch (e) {}
119
- document.documentElement.dataset.theme = t;
120
- }
115
+ /** Reads the persisted theme and applies it before first paint. Call once at app start. Defined in theme-store.js, shared with the Vue entry. */
116
+ export { applyStoredTheme };
@@ -24,10 +24,10 @@ function pickHidden(it) { open.value = false; emit('select', it) }
24
24
  function onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }
25
25
  watch(open, (v) => { if (v) document.addEventListener('mousedown', onDoc); else document.removeEventListener('mousedown', onDoc) })
26
26
  onUnmounted(() => document.removeEventListener('mousedown', onDoc))
27
- const crumbStyle = (i, it) => ({ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: isLast(it) || on(i, it) ? 'var(--text-heading)' : 'var(--text-secondary)', fontWeight: isLast(it) ? 'var(--weight-medium)' : 'var(--weight-regular)', cursor: isLast(it) ? 'default' : 'pointer', borderRadius: 'var(--radius-sm)', transition: 'var(--t-hover)' })
27
+ const crumbStyle = (i, it) => ({ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', color: isLast(it) || on(i, it) ? 'var(--text-heading)' : 'var(--text-secondary)', fontWeight: isLast(it) ? 'var(--weight-medium)' : 'var(--weight-regular)', cursor: isLast(it) ? 'default' : 'pointer', borderRadius: 'var(--radius-sm)', transition: 'var(--t-hover)' })
28
28
  </script>
29
29
  <template>
30
- <nav :style="{ display: 'flex', alignItems: 'center', gap: 4, ...style }">
30
+ <nav :style="{ display: 'flex', alignItems: 'center', gap: '4px', ...style }">
31
31
  <template v-for="(it, i) in head" :key="it">
32
32
  <span v-if="i > 0" style="color:var(--text-muted);display:inline-flex"><Icon name="chevron-right" :size="13" /></span>
33
33
  <span :role="isLast(it) ? undefined : 'button'" :tabindex="isLast(it) ? undefined : 0"
@@ -40,14 +40,14 @@ const crumbStyle = (i, it) => ({ fontFamily: 'var(--font-sans)', fontSize: 'var(
40
40
  <span role="button" tabindex="0" :aria-label="`Show ${hidden.length} hidden levels`" :aria-expanded="open"
41
41
  @click="open = !open" @keydown.enter.prevent="open = !open" @keydown.space.prevent="open = !open" @keydown.esc="open = false"
42
42
  @mouseenter="ovHover = true" @mouseleave="ovHover = false"
43
- :style="{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '20px', height: '18px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', color: open || ovHover ? 'var(--text-heading)' : 'var(--text-muted)', background: open ? 'var(--bg-active)' : ovHover ? 'var(--bg-hover)' : 'transparent', transition: 'var(--t-hover)' }">
43
+ :style="{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '24px', height: '24px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', color: open || ovHover ? 'var(--text-heading)' : 'var(--text-muted)', background: open ? 'var(--bg-active)' : ovHover ? 'var(--bg-hover)' : 'transparent', transition: 'var(--t-hover)' }">
44
44
  <Icon name="ellipsis" :size="14" />
45
45
  </span>
46
46
  <span v-if="open" style="position:absolute;top:calc(100% + 5px);left:0;z-index:var(--z-menu);min-width:168px;max-width:260px;background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;display:flex;flex-direction:column">
47
47
  <span v-for="(h, hi) in hidden" :key="h" role="menuitem" tabindex="0"
48
48
  @click="pickHidden(h)" @keydown.enter.prevent="pickHidden(h)" @keydown.space.prevent="pickHidden(h)"
49
49
  @mouseenter="menuHover = hi" @mouseleave="menuHover = -1"
50
- :style="{ display: 'flex', alignItems: 'center', gap: '6px', padding: '6px 9px', paddingLeft: (9 + hi * 12) + 'px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: menuHover === hi ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', transition: 'var(--t-hover)' }">
50
+ :style="{ display: 'flex', alignItems: 'center', gap: '6px', padding: '6px 9px', paddingLeft: (9 + hi * 12) + 'px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: menuHover === hi ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', color: 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', transition: 'var(--t-hover)' }">
51
51
  <span v-if="hi > 0" style="color:var(--text-muted);display:inline-flex;flex-shrink:0"><Icon name="chevron-right" :size="11" /></span>{{ h }}
52
52
  </span>
53
53
  </span>
@@ -62,11 +62,5 @@ const crumbStyle = (i, it) => ({ fontFamily: 'var(--font-sans)', fontSize: 'var(
62
62
  </nav>
63
63
  </template>
64
64
 
65
- <!-- applyStoredTheme() — reads the persisted theme and applies it before first paint. Call once at app start (main.js): -->
66
- <!--
67
- export function applyStoredTheme() {
68
- let t = 'light'
69
- try { t = localStorage.getItem('kb-theme') || 'light' } catch (e) {}
70
- document.documentElement.dataset.theme = t
71
- }
72
- -->
65
+ <!-- applyStoredTheme() lives in ./theme-store.js (shared with React) and is exported from the Vue entry.
66
+ Call it once in main.js before mounting. -->
@@ -20,14 +20,14 @@ export function DocTree({ title = "Documents", items = [], active, onSelect, sty
20
20
  {items.map((it) => {
21
21
  const is = active === it.label;
22
22
  return (
23
- <div key={it.label} role="treeitem" tabIndex={0} onClick={() => onSelect && onSelect(it.label)}
24
- style={{ height: "var(--h-tree-item)", borderRadius: "var(--radius-md)", display: "flex", alignItems: "center", gap: 8, padding: "6px 8px", boxSizing: "border-box", cursor: "pointer", background: is ? "var(--bg-active)" : "transparent" }}
23
+ <div key={it.label} role="treeitem" tabIndex={0} data-wash={is ? undefined : ""} onClick={() => onSelect && onSelect(it.label)}
24
+ style={{ height: "var(--h-tree-item)", borderRadius: "var(--radius-md)", display: "flex", alignItems: "center", gap: 8, padding: "6px 8px", boxSizing: "border-box", cursor: "pointer", background: is ? "var(--bg-active)" : "var(--wash, transparent)", transition: "var(--t-hover)" }}
25
25
  onMouseEnter={(e) => { if (!is) e.currentTarget.style.background = "var(--bg-hover)"; }}
26
26
  onMouseLeave={(e) => { if (!is) e.currentTarget.style.background = "transparent"; }}>
27
27
  <span style={{ color: it.kind === "folder" ? "var(--text-muted)" : docHues[it.hue || "gray"], display: "inline-flex" }}>
28
28
  <Icon name={it.icon || (it.kind === "folder" ? "folder" : "file-text")} size={16} />
29
29
  </span>
30
- <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", color: "var(--text-heading)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{it.label}</span>
30
+ <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", color: "var(--text-heading)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{it.label}</span>
31
31
  </div>
32
32
  );
33
33
  })}
@@ -15,10 +15,10 @@ const emit = defineEmits(['select'])
15
15
  </div>
16
16
  <div style="padding:12px 16px 8px;border-bottom:1px solid var(--border-subtle)"><SearchInput bare placeholder="Search..." style="gap:8px" /></div>
17
17
  <div style="padding:8px;display:flex;flex-direction:column;gap:2px;overflow-y:auto">
18
- <div v-for="it in items" :key="it.label" @click="emit('select', it.label)"
19
- :style="{ height: 'var(--h-tree-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: 8, padding: '6px 8px', boxSizing: 'border-box', cursor: 'pointer', background: active === it.label ? 'var(--bg-active)' : 'transparent' }">
18
+ <div v-for="it in items" :key="it.label" role="treeitem" tabindex="0" @click="emit('select', it.label)" @keydown.enter="emit('select', it.label)"
19
+ :style="{ height: 'var(--h-tree-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 8px', boxSizing: 'border-box', cursor: 'pointer', background: active === it.label ? 'var(--bg-active)' : 'var(--wash, transparent)', transition: 'var(--t-hover)' }" :data-wash="active === it.label ? undefined : ''">
20
20
  <span :style="{ color: it.kind === 'folder' ? 'var(--text-muted)' : docHues[it.hue || 'gray'], display: 'inline-flex' }"><Icon :name="it.icon || (it.kind === 'folder' ? 'folder' : 'file-text')" :size="16" /></span>
21
- <span style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ it.label }}</span>
21
+ <span style="font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ it.label }}</span>
22
22
  </div>
23
23
  </div>
24
24
  </div>
@@ -2,7 +2,7 @@
2
2
  defineProps({ title: String, subtitle: String, style: Object })
3
3
  </script>
4
4
  <template>
5
- <div :style="{ display: 'flex', flexDirection: 'column', gap: 4, ...style }">
5
+ <div :style="{ display: 'flex', flexDirection: 'column', gap: '4px', ...style }">
6
6
  <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-lg);line-height:var(--leading-lg);letter-spacing:var(--tracking-lg);color:var(--text-primary)">{{ title }}</span>
7
7
  <span v-if="subtitle" 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)">{{ subtitle }}</span>
8
8
  </div>
@@ -1,15 +1,16 @@
1
1
  <script setup>
2
2
  import IconButton from '../buttons/IconButton.vue'
3
- defineProps({ title: String, status: { type: String, default: '• Editing' }, showBack: Boolean, style: Object })
3
+ // Page toolbar under the top bar: back · title · status, actions on the right. A size region (default inherit → md).
4
+ defineProps({ title: String, status: { type: String, default: '• Editing' }, showBack: Boolean, size: String, style: Object })
4
5
  const emit = defineEmits(['back'])
5
6
  </script>
6
7
  <template>
7
- <div :style="{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 16px', boxSizing: 'border-box', flexShrink: 0, ...style }">
8
- <div style="display:flex;align-items:center;gap:8px">
9
- <IconButton v-if="showBack" icon="chevron-left" size="md" @click="emit('back')" />
8
+ <div :data-size="size" :style="{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 var(--edge-pad)', boxSizing: 'border-box', flexShrink: 0, ...style }">
9
+ <div style="display:flex;align-items:center;gap:var(--control-gap)">
10
+ <IconButton v-if="showBack" icon="chevron-left" label="Back" @click="emit('back')" />
10
11
  <span style="font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)">{{ title }}</span>
11
- <span v-if="status" style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-muted);margin-left:6px">{{ status }}</span>
12
+ <span v-if="status" style="font-family:var(--font-sans);font-size:var(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-muted);margin-left:6px">{{ status }}</span>
12
13
  </div>
13
- <div style="display:flex;align-items:center;gap:8px"><slot name="actions" /></div>
14
+ <div style="display:flex;align-items:center;gap:var(--control-gap)"><slot name="actions" /></div>
14
15
  </div>
15
16
  </template>