@keyfunc/keybricks-ds 1.0.3 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/DEPLOY.md +4 -4
  2. package/DESIGN.md +154 -53
  3. package/INTEGRATION.md +176 -36
  4. package/SKILL.md +6 -4
  5. package/_adherence.oxlintrc.json +183 -72
  6. package/_scripts/build-index.mjs +9 -0
  7. package/_scripts/build-tokens.mjs +5 -2
  8. package/assets/icons/icon-index.js +1 -1
  9. package/assets/icons/icon-index.json +1 -1
  10. package/components/_cards/accordion.card.html +2 -2
  11. package/components/_cards/action-chip.card.html +8 -3
  12. package/components/_cards/activity.card.html +2 -2
  13. package/components/_cards/alert.card.html +2 -2
  14. package/components/_cards/app-chrome.card.html +4 -4
  15. package/components/_cards/banner.card.html +1 -1
  16. package/components/_cards/breadcrumb.card.html +2 -2
  17. package/components/_cards/button-1-base.card.html +2 -2
  18. package/components/_cards/button-2-states.card.html +14 -14
  19. package/components/_cards/button-3-sizes-icons.card.html +1 -1
  20. package/components/_cards/button-4-group.card.html +5 -5
  21. package/components/_cards/canvas-zoom-controls.card.html +1 -1
  22. package/components/_cards/cards-indicators.card.html +2 -2
  23. package/components/_cards/charts-1-sparkline.card.html +2 -2
  24. package/components/_cards/charts-10-gauge.card.html +4 -4
  25. package/components/_cards/charts-11-funnel.card.html +2 -2
  26. package/components/_cards/charts-12-treemap.card.html +2 -2
  27. package/components/_cards/charts-14-radar.card.html +1 -1
  28. package/components/_cards/charts-15-distribution.card.html +1 -1
  29. package/components/_cards/charts-16-pareto.card.html +2 -2
  30. package/components/_cards/charts-17-candlestick.card.html +2 -2
  31. package/components/_cards/charts-18-sankey.card.html +2 -2
  32. package/components/_cards/charts-19-chord.card.html +2 -2
  33. package/components/_cards/charts-2-line.card.html +2 -2
  34. package/components/_cards/charts-20-sunburst.card.html +2 -2
  35. package/components/_cards/charts-21-worldmap.card.html +2 -2
  36. package/components/_cards/charts-22-gantt.card.html +2 -2
  37. package/components/_cards/charts-23-bar3d.card.html +2 -2
  38. package/components/_cards/charts-24-pie3d.card.html +2 -2
  39. package/components/_cards/charts-25-surface3d.card.html +2 -2
  40. package/components/_cards/charts-26-scatter3d.card.html +2 -2
  41. package/components/_cards/charts-27-ribbon3d.card.html +2 -2
  42. package/components/_cards/charts-3-bar.card.html +2 -2
  43. package/components/_cards/charts-4-barlist.card.html +2 -2
  44. package/components/_cards/charts-5-donut.card.html +3 -3
  45. package/components/_cards/charts-6-scatter.card.html +2 -2
  46. package/components/_cards/charts-7-heatmap.card.html +2 -2
  47. package/components/_cards/charts-8-waterfall.card.html +2 -2
  48. package/components/_cards/charts-9-bullet.card.html +2 -2
  49. package/components/_cards/charts-99-fullpage.card.html +5 -5
  50. package/components/_cards/checkbox.card.html +4 -4
  51. package/components/_cards/codeblock.card.html +1 -1
  52. package/components/_cards/combobox.card.html +3 -2
  53. package/components/_cards/context-menu.card.html +3 -2
  54. package/components/_cards/date-picker.card.html +5 -2
  55. package/components/_cards/diffvalue.card.html +2 -2
  56. package/components/_cards/divider.card.html +3 -3
  57. package/components/_cards/drawer.card.html +2 -2
  58. package/components/_cards/dropdown-menu.card.html +6 -6
  59. package/components/_cards/dropzone.card.html +1 -1
  60. package/components/_cards/empty-state.card.html +5 -5
  61. package/components/_cards/filter-bar.card.html +5 -5
  62. package/components/_cards/icon-button.card.html +2 -2
  63. package/components/_cards/icons-1-core.card.html +2 -2
  64. package/components/_cards/icons-2-feather.card.html +3 -3
  65. package/components/_cards/icons-3-unicons.card.html +3 -3
  66. package/components/_cards/input.card.html +5 -2
  67. package/components/_cards/kbd.card.html +2 -2
  68. package/components/_cards/keyvalue.card.html +2 -2
  69. package/components/_cards/listrow.card.html +2 -2
  70. package/components/_cards/loading.card.html +2 -2
  71. package/components/_cards/log-viewer.card.html +2 -2
  72. package/components/_cards/modal.card.html +9 -9
  73. package/components/_cards/multi-select.card.html +3 -2
  74. package/components/_cards/number-stepper.card.html +3 -2
  75. package/components/_cards/pagination.card.html +3 -2
  76. package/components/_cards/people.card.html +3 -3
  77. package/components/_cards/pipeline-node.card.html +2 -2
  78. package/components/_cards/pipeline-palette.card.html +4 -4
  79. package/components/_cards/radio.card.html +46 -0
  80. package/components/_cards/search.card.html +5 -2
  81. package/components/_cards/segmented-control.card.html +39 -0
  82. package/components/_cards/select.card.html +4 -4
  83. package/components/_cards/slider.card.html +1 -1
  84. package/components/_cards/smart-table.card.html +1 -1
  85. package/components/_cards/spreadsheet.card.html +2 -2
  86. package/components/_cards/stepper.card.html +2 -2
  87. package/components/_cards/table.card.html +103 -0
  88. package/components/_cards/tabs.card.html +3 -2
  89. package/components/_cards/tag.card.html +5 -2
  90. package/components/_cards/textarea.card.html +2 -2
  91. package/components/_cards/timeline.card.html +1 -1
  92. package/components/_cards/toast.card.html +4 -4
  93. package/components/_cards/toggles.card.html +45 -56
  94. package/components/_cards/tooltip.card.html +5 -5
  95. package/components/_cards/tree.card.html +2 -2
  96. package/components/_shared/src-buttons.js +1 -1
  97. package/components/_shared/src-charts.js +1 -1
  98. package/components/_shared/src-data.js +1 -1
  99. package/components/_shared/src-display.js +1 -1
  100. package/components/_shared/src-feedback.js +1 -1
  101. package/components/_shared/src-forms.js +1 -1
  102. package/components/_shared/src-navigation.js +1 -1
  103. package/components/_shared/src-pipeline.js +1 -1
  104. package/components/_shared/src-tabs.js +1 -1
  105. package/components/_shared/view-source.js +9 -4
  106. package/components/_vue/index.mjs +9 -5
  107. package/components/buttons/ActionChip.d.ts +3 -1
  108. package/components/buttons/ActionChip.jsx +4 -4
  109. package/components/buttons/ActionChip.vue +4 -4
  110. package/components/buttons/Button.d.ts +5 -2
  111. package/components/buttons/Button.jsx +15 -18
  112. package/components/buttons/Button.vue +17 -18
  113. package/components/buttons/ButtonGroup.vue +1 -1
  114. package/components/buttons/IconButton.d.ts +2 -1
  115. package/components/buttons/IconButton.jsx +9 -7
  116. package/components/buttons/IconButton.vue +10 -8
  117. package/components/buttons/buttons.prompt.md +6 -5
  118. package/components/charts/Bar3D.jsx +2 -2
  119. package/components/charts/Bar3D.vue +2 -2
  120. package/components/charts/BarChart.jsx +4 -4
  121. package/components/charts/BarChart.vue +4 -4
  122. package/components/charts/BarList.jsx +1 -1
  123. package/components/charts/BarList.vue +1 -1
  124. package/components/charts/BoxPlot.jsx +1 -1
  125. package/components/charts/BoxPlot.vue +2 -2
  126. package/components/charts/BulletChart.jsx +1 -1
  127. package/components/charts/BulletChart.vue +1 -1
  128. package/components/charts/CandlestickChart.jsx +2 -2
  129. package/components/charts/CandlestickChart.vue +3 -3
  130. package/components/charts/ChartKit.jsx +5 -5
  131. package/components/charts/ChartLegend.vue +16 -0
  132. package/components/charts/ChartTooltip.vue +16 -0
  133. package/components/charts/ChordChart.jsx +2 -2
  134. package/components/charts/ChordChart.vue +2 -2
  135. package/components/charts/GanttChart.jsx +2 -2
  136. package/components/charts/GanttChart.vue +2 -2
  137. package/components/charts/GaugeChart.jsx +2 -2
  138. package/components/charts/GaugeChart.vue +2 -2
  139. package/components/charts/HeatmapChart.jsx +2 -2
  140. package/components/charts/HeatmapChart.vue +2 -2
  141. package/components/charts/Histogram.jsx +2 -2
  142. package/components/charts/Histogram.vue +3 -3
  143. package/components/charts/LineChart.jsx +1 -1
  144. package/components/charts/LineChart.vue +3 -3
  145. package/components/charts/ParetoChart.jsx +3 -3
  146. package/components/charts/ParetoChart.vue +4 -4
  147. package/components/charts/PlotGrid.vue +18 -0
  148. package/components/charts/RadarChart.jsx +1 -1
  149. package/components/charts/RadarChart.vue +1 -1
  150. package/components/charts/Ribbon3D.jsx +2 -2
  151. package/components/charts/Ribbon3D.vue +2 -2
  152. package/components/charts/SankeyChart.jsx +2 -2
  153. package/components/charts/SankeyChart.vue +2 -2
  154. package/components/charts/Scatter3D.jsx +1 -1
  155. package/components/charts/Scatter3D.vue +1 -1
  156. package/components/charts/ScatterChart.jsx +3 -3
  157. package/components/charts/ScatterChart.vue +4 -4
  158. package/components/charts/Sparkline.jsx +1 -1
  159. package/components/charts/Sparkline.vue +1 -1
  160. package/components/charts/SunburstChart.jsx +1 -1
  161. package/components/charts/SunburstChart.vue +1 -1
  162. package/components/charts/Surface3D.jsx +2 -2
  163. package/components/charts/Surface3D.vue +2 -2
  164. package/components/charts/TreemapChart.jsx +2 -2
  165. package/components/charts/TreemapChart.vue +2 -2
  166. package/components/charts/WaterfallChart.jsx +2 -2
  167. package/components/charts/WaterfallChart.vue +3 -3
  168. package/components/charts/WorldMap.jsx +2 -2
  169. package/components/charts/WorldMap.vue +2 -2
  170. package/components/charts/XLabels.vue +11 -0
  171. package/components/data/CodeBlock.jsx +1 -1
  172. package/components/data/CodeBlock.vue +1 -1
  173. package/components/data/DiffValue.jsx +1 -1
  174. package/components/data/DiffValue.vue +1 -1
  175. package/components/data/FilterBar.d.ts +2 -0
  176. package/components/data/FilterBar.jsx +10 -9
  177. package/components/data/FilterBar.vue +9 -8
  178. package/components/data/KeyValue.jsx +3 -3
  179. package/components/data/KeyValue.vue +3 -3
  180. package/components/data/ListRow.jsx +2 -2
  181. package/components/data/ListRow.vue +2 -2
  182. package/components/data/Pagination.d.ts +5 -1
  183. package/components/data/Pagination.jsx +16 -12
  184. package/components/data/Pagination.vue +14 -9
  185. package/components/data/SmartTable.d.ts +4 -1
  186. package/components/data/SmartTable.jsx +16 -14
  187. package/components/data/SmartTable.vue +25 -22
  188. package/components/data/Spreadsheet.d.ts +2 -0
  189. package/components/data/Spreadsheet.jsx +12 -11
  190. package/components/data/Spreadsheet.vue +14 -13
  191. package/components/data/Table.d.ts +47 -0
  192. package/components/data/Table.jsx +85 -0
  193. package/components/data/Table.vue +63 -0
  194. package/components/data/data.prompt.md +3 -3
  195. package/components/display/Avatar.vue +3 -2
  196. package/components/display/AvatarGroup.vue +3 -2
  197. package/components/display/Badge.vue +1 -1
  198. package/components/display/Divider.vue +3 -3
  199. package/components/display/Donut.vue +3 -2
  200. package/components/display/IconTile.vue +2 -1
  201. package/components/display/Kbd.vue +1 -1
  202. package/components/display/ProgressBar.jsx +1 -1
  203. package/components/display/ProgressBar.vue +1 -1
  204. package/components/display/StatCard.vue +1 -1
  205. package/components/display/Tag.d.ts +3 -1
  206. package/components/display/Tag.jsx +4 -4
  207. package/components/display/Tag.vue +4 -4
  208. package/components/feedback/Accordion.jsx +2 -2
  209. package/components/feedback/Accordion.vue +2 -2
  210. package/components/feedback/ActivityItem.d.ts +3 -1
  211. package/components/feedback/ActivityItem.jsx +5 -5
  212. package/components/feedback/ActivityItem.vue +5 -4
  213. package/components/feedback/Alert.jsx +4 -4
  214. package/components/feedback/Alert.vue +3 -3
  215. package/components/feedback/Banner.jsx +5 -5
  216. package/components/feedback/Banner.vue +5 -5
  217. package/components/feedback/ContextMenu.d.ts +3 -1
  218. package/components/feedback/ContextMenu.jsx +5 -5
  219. package/components/feedback/ContextMenu.vue +5 -5
  220. package/components/feedback/DotActivity.vue +3 -3
  221. package/components/feedback/Drawer.d.ts +2 -0
  222. package/components/feedback/Drawer.jsx +6 -6
  223. package/components/feedback/Drawer.vue +22 -15
  224. package/components/feedback/EmptyState.vue +2 -2
  225. package/components/feedback/LogViewer.vue +1 -1
  226. package/components/feedback/Modal.d.ts +2 -0
  227. package/components/feedback/Modal.jsx +12 -11
  228. package/components/feedback/Modal.vue +10 -8
  229. package/components/feedback/Skeleton.vue +2 -1
  230. package/components/feedback/Spinner.vue +2 -1
  231. package/components/feedback/Stepper.vue +1 -1
  232. package/components/feedback/Timeline.jsx +1 -1
  233. package/components/feedback/Timeline.vue +1 -1
  234. package/components/feedback/Toast.vue +4 -4
  235. package/components/feedback/ToastStack.vue +11 -0
  236. package/components/feedback/Tooltip.vue +19 -0
  237. package/components/feedback/feedback.prompt.md +4 -4
  238. package/components/forms/Checkbox.d.ts +3 -2
  239. package/components/forms/Checkbox.jsx +7 -7
  240. package/components/forms/Checkbox.vue +8 -8
  241. package/components/forms/Combobox.d.ts +2 -0
  242. package/components/forms/Combobox.jsx +12 -10
  243. package/components/forms/Combobox.vue +11 -10
  244. package/components/forms/DatePicker.d.ts +4 -0
  245. package/components/forms/DatePicker.jsx +16 -12
  246. package/components/forms/DatePicker.vue +15 -11
  247. package/components/forms/Dropzone.jsx +2 -2
  248. package/components/forms/Dropzone.vue +5 -5
  249. package/components/forms/Field.d.ts +5 -1
  250. package/components/forms/Field.jsx +4 -4
  251. package/components/forms/Field.vue +4 -4
  252. package/components/forms/Input.d.ts +5 -1
  253. package/components/forms/Input.jsx +7 -7
  254. package/components/forms/Input.vue +3 -3
  255. package/components/forms/MultiSelect.d.ts +2 -0
  256. package/components/forms/MultiSelect.jsx +13 -11
  257. package/components/forms/MultiSelect.vue +12 -11
  258. package/components/forms/NumberStepper.d.ts +3 -1
  259. package/components/forms/NumberStepper.jsx +7 -7
  260. package/components/forms/NumberStepper.vue +8 -7
  261. package/components/forms/Radio.d.ts +30 -0
  262. package/components/forms/Radio.jsx +65 -0
  263. package/components/forms/Radio.vue +17 -9
  264. package/components/forms/RadioGroup.vue +22 -4
  265. package/components/forms/SearchInput.d.ts +3 -1
  266. package/components/forms/SearchInput.jsx +6 -6
  267. package/components/forms/SearchInput.vue +7 -6
  268. package/components/forms/Select.d.ts +3 -2
  269. package/components/forms/Select.jsx +6 -7
  270. package/components/forms/Select.vue +6 -6
  271. package/components/forms/Slider.jsx +3 -3
  272. package/components/forms/Slider.vue +3 -3
  273. package/components/forms/Switch.d.ts +32 -24
  274. package/components/forms/Switch.jsx +85 -35
  275. package/components/forms/Switch.vue +57 -11
  276. package/components/forms/Textarea.vue +4 -4
  277. package/components/forms/forms.prompt.md +4 -1
  278. package/components/forms/input-base.js +2 -1
  279. package/components/index.d.ts +2 -2
  280. package/components/index.js +5 -5
  281. package/components/navigation/Breadcrumb.jsx +3 -3
  282. package/components/navigation/Breadcrumb.vue +4 -4
  283. package/components/navigation/DocTree.jsx +3 -3
  284. package/components/navigation/DocTree.vue +3 -3
  285. package/components/navigation/PageHeading.vue +1 -1
  286. package/components/navigation/PageToolbar.vue +7 -6
  287. package/components/navigation/Sidebar.jsx +1 -1
  288. package/components/navigation/Sidebar.vue +3 -3
  289. package/components/navigation/ThemeToggle.vue +2 -1
  290. package/components/navigation/TopBar.d.ts +3 -1
  291. package/components/navigation/TopBar.jsx +6 -6
  292. package/components/navigation/Tree.jsx +5 -5
  293. package/components/navigation/Tree.vue +5 -5
  294. package/components/pipeline/ConfigPanel.d.ts +2 -0
  295. package/components/pipeline/ConfigPanel.jsx +9 -9
  296. package/components/pipeline/ConfigPanel.vue +9 -9
  297. package/components/pipeline/DataPreview.d.ts +3 -1
  298. package/components/pipeline/DataPreview.jsx +7 -7
  299. package/components/pipeline/DataPreview.vue +7 -7
  300. package/components/pipeline/NodeCard.jsx +3 -3
  301. package/components/pipeline/NodeCard.vue +3 -3
  302. package/components/pipeline/PaletteItem.vue +2 -2
  303. package/components/pipeline/PaletteListItem.vue +1 -1
  304. package/components/pipeline/PalettePanel.jsx +2 -2
  305. package/components/pipeline/PalettePanel.vue +1 -1
  306. package/components/pipeline/Port.vue +15 -0
  307. package/components/pipeline/PortLabel.vue +15 -0
  308. package/components/pipeline/ZoomControls.vue +1 -1
  309. package/components/pipeline/port-skin.js +13 -0
  310. package/components/px.js +4 -0
  311. package/components/tabs/PanelTabs.vue +5 -4
  312. package/components/tabs/PillTabs.vue +5 -4
  313. package/components/tabs/SegmentedControl.d.ts +11 -4
  314. package/components/tabs/SegmentedControl.jsx +25 -10
  315. package/components/tabs/SegmentedControl.vue +19 -7
  316. package/components/tabs/Tabs.d.ts +5 -3
  317. package/components/tabs/Tabs.jsx +15 -15
  318. package/components/tabs/Tabs.vue +5 -5
  319. package/components/tabs/ViewToggle.d.ts +3 -1
  320. package/components/tabs/ViewToggle.jsx +5 -5
  321. package/components/tabs/ViewToggle.vue +5 -5
  322. package/components/tabs/tabs.prompt.md +4 -2
  323. package/keybricks.css +4 -0
  324. package/package.json +6 -3
  325. package/readme.md +90 -123
  326. package/readme.zh-CN.md +120 -0
  327. package/tokens/charts.css +20 -17
  328. package/tokens/colors.css +2 -2
  329. package/tokens/dark.css +2 -2
  330. package/tokens/hover.css +32 -0
  331. package/tokens/interaction.css +3 -1
  332. package/tokens/layout.css +29 -7
  333. package/tokens/tokens.d.ts +36 -0
  334. package/tokens/tokens.js +143 -89
  335. package/tokens/tokens.json +240 -106
  336. package/tokens/typography.css +27 -14
  337. package/components/_cards/brand-visual.card.html +0 -43
  338. package/components/_cards/datatable.card.html +0 -48
  339. package/components/_shared/src-brand.js +0 -1
  340. package/components/brand/BrandVisual.d.ts +0 -17
  341. package/components/brand/BrandVisual.jsx +0 -29
  342. package/components/brand/BrandVisual.vue +0 -34
  343. package/components/brand/_dir.card.html +0 -13
  344. package/components/brand/brandMotifs.js +0 -210
  345. package/components/buttons/_dir.card.html +0 -13
  346. package/components/charts/ChartKit.vue +0 -47
  347. package/components/charts/_dir.card.html +0 -7
  348. package/components/data/DataTable.d.ts +0 -28
  349. package/components/data/DataTable.jsx +0 -32
  350. package/components/data/DataTable.vue +0 -25
  351. package/components/data/_dir.card.html +0 -13
  352. package/components/display/_dir.card.html +0 -13
  353. package/components/feedback/_dir.card.html +0 -13
  354. package/components/forms/_dir.card.html +0 -13
  355. package/components/icons/_dir.card.html +0 -13
  356. package/components/navigation/_dir.card.html +0 -13
  357. package/components/pipeline/_dir.card.html +0 -13
  358. package/components/tabs/_dir.card.html +0 -13
@@ -1,14 +1,15 @@
1
1
  <script setup>
2
2
  import Icon from '../icons/Icon.vue'
3
- // Bordered search field (r10) with leading search glyph; bare=true renders the borderless top-bar flavor. Shows a clear (×) button once there's a value.
4
- defineProps({ modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Search...' }, bare: Boolean, disabled: Boolean, style: Object })
3
+ // Bordered search field (r8, tier height — omit `size` to inherit the region's tier) with leading search glyph; bare=true renders the borderless top-bar flavor. Shows a clear (×) button once there's a value.
4
+ defineProps({ modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Search...' }, bare: Boolean, size: String, disabled: Boolean, style: Object })
5
+ const tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }
5
6
  const emit = defineEmits(['update:modelValue'])
6
7
  </script>
7
8
  <template>
8
- <div :style="{ display: 'flex', alignItems: 'center', gap: bare ? 12 : 8, opacity: disabled ? .5 : 1, ...(bare ? null : { border: '1px solid var(--border-control)', borderRadius: 'var(--radius-lg)', padding: '6px 12px', background: 'var(--bg-surface)' }), ...style }">
9
- <span style="color:var(--text-muted)"><Icon name="search" :size="bare ? 16 : 14" /></span>
9
+ <div :data-size="size" :data-field="bare || disabled ? undefined : ''" :style="{ display: 'flex', alignItems: 'center', gap: bare ? '12px' : '8px', height: 'var(--control-h)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...(bare ? null : { border: '1px solid var(--field-border, var(--border-control))', borderRadius: 'var(--radius-md)', padding: '0 var(--control-px)', background: 'var(--bg-surface)' }), ...style }">
10
+ <span style="color:var(--text-muted);display:inline-flex"><Icon name="search" :size="16" :style="tierIcon" /></span>
10
11
  <input :value="modelValue" :disabled="disabled" @input="emit('update:modelValue', $event.target.value)" :placeholder="placeholder"
11
- :style="{ border: 'none', outline: 'none', background: 'transparent', flex: 1, minWidth: 0, fontFamily: 'var(--font-sans)', fontSize: bare ? 'var(--text-base)' : 'var(--text-sm)', letterSpacing: bare ? 'var(--tracking-base)' : 0, color: 'var(--text-primary)', cursor: disabled ? 'not-allowed' : 'text' }" />
12
- <button v-if="modelValue && !disabled" type="button" @click="emit('update:modelValue', '')" style="border:none;background:transparent;cursor:pointer;color:var(--text-muted);display:flex;padding:0"><Icon name="x" :size="13" /></button>
12
+ :style="{ border: 'none', outline: 'none', background: 'transparent', flex: 1, minWidth: 0, padding: 0, fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-primary)', cursor: disabled ? 'not-allowed' : 'text' }" />
13
+ <button v-if="modelValue && !disabled" type="button" aria-label="Clear" data-ink="" @click="emit('update:modelValue', '')" style="border:none;background:transparent;cursor:pointer;color:var(--ink, var(--text-muted));display:flex;padding:0"><Icon name="x" :size="16" :style="tierIcon" /></button>
13
14
  </div>
14
15
  </template>
@@ -1,8 +1,9 @@
1
1
  import * as React from "react";
2
- /** Hairline select. md 36/r8 for filters, sm 25/r4 inside table rows. disabled fills #fafafa inset like Input's readOnly. */
2
+ /** Hairline select, r8, at the region's tier height (sm 24 · md 32 · lg 36). disabled fills inset like Input's readOnly. */
3
3
  export interface SelectProps {
4
4
  options?: string[];
5
- size?: "md" | "sm";
5
+ /** Size tier. Omit to inherit the enclosing region's tier (the default); controls that share a row must share a tier. */
6
+ size?: "sm" | "md" | "lg";
6
7
  value?: string;
7
8
  onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
8
9
  /** Shown as a disabled, muted-color first option when no value is set */
@@ -1,17 +1,16 @@
1
1
  import React from "react";
2
2
  import { Icon } from "../icons/Icon.jsx";
3
3
 
4
- /** Native select styled per Keybricks. size md 36/r8 · sm 25/r4 (table rows). disabled = #fafafa inset fill (matches Input readOnly), 50% opacity; error = red border. */
5
- export function Select({ options = [], size = "md", value, onChange, placeholder, disabled, error, style }) {
6
- const sm = size === "sm";
4
+ /** Native select styled per Keybricks: r8, tier height (sm 24 · md 32 · lg 36 — omit `size` to inherit the region's tier). disabled = inset fill (matches Input readOnly), 50% opacity; error = red border. */
5
+ export function Select({ options = [], size, value, onChange, placeholder, disabled, error, style }) {
7
6
  return (
8
- <div style={{ position: "relative", display: "inline-flex", ...style }}>
9
- <select value={value} onChange={onChange} disabled={disabled}
10
- style={{ appearance: "none", WebkitAppearance: "none", height: sm ? 25 : 36, borderRadius: sm ? "var(--radius-sm)" : "var(--radius-md)", border: `1px solid ${error ? "var(--danger)" : "var(--border-control)"}`, background: disabled ? "var(--bg-inset)" : "var(--bg-surface)", padding: sm ? "0 26px 0 10px" : "0 32px 0 12px", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: disabled ? "var(--text-secondary)" : value ? "var(--text-primary)" : "var(--text-muted)", outline: "none", cursor: disabled ? "not-allowed" : "pointer", width: "100%", opacity: disabled ? .5 : 1 }}>
7
+ <div data-size={size} style={{ position: "relative", display: "inline-flex", ...style }}>
8
+ <select value={value} onChange={onChange} disabled={disabled} data-field={disabled ? undefined : ""}
9
+ style={{ appearance: "none", WebkitAppearance: "none", height: "var(--control-h)", borderRadius: "var(--radius-md)", border: `1px solid ${error ? "var(--danger)" : "var(--field-border, var(--border-control))"}`, background: disabled ? "var(--bg-inset)" : "var(--bg-surface)", padding: "0 calc(var(--control-px) + var(--control-icon) + 6px) 0 var(--control-px)", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", letterSpacing: "var(--control-tracking)", color: disabled ? "var(--text-secondary)" : value ? "var(--text-primary)" : "var(--text-muted)", outline: "none", cursor: disabled ? "not-allowed" : "pointer", width: "100%", opacity: disabled ? .5 : 1 }}>
11
10
  {placeholder ? <option value="" disabled hidden>{placeholder}</option> : null}
12
11
  {options.map((o) => <option key={o} value={o}>{o}</option>)}
13
12
  </select>
14
- <span style={{ position: "absolute", right: sm ? 8 : 10, top: "50%", transform: "translateY(-50%)", pointerEvents: "none", color: "var(--text-muted)" }}><Icon name="chevron-down" size={sm ? 12 : 14} /></span>
13
+ <span style={{ position: "absolute", right: "var(--control-px)", top: "50%", transform: "translateY(-50%)", pointerEvents: "none", color: "var(--text-muted)", display: "inline-flex" }}><Icon name="chevron-down" size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)" }} /></span>
15
14
  </div>
16
15
  );
17
16
  }
@@ -1,16 +1,16 @@
1
1
  <script setup>
2
2
  import Icon from '../icons/Icon.vue'
3
- // Native select styled per Keybricks. size md 36/r8 · sm 25/r4 (table rows). disabled = inset fill (matches Input readonly), 50% opacity; error = red border.
4
- const props = defineProps({ modelValue: String, options: { type: Array, default: () => [] }, size: { type: String, default: 'md' }, placeholder: String, disabled: Boolean, error: Boolean, style: Object })
3
+ // Native select styled per Keybricks: r8, tier height (sm 24 · md 32 · lg 36 — omit `size` to inherit the region's tier). disabled = inset fill (matches Input readonly), 50% opacity; error = red border.
4
+ const props = defineProps({ modelValue: String, options: { type: Array, default: () => [] }, size: String, placeholder: String, disabled: Boolean, error: Boolean, style: Object })
5
5
  defineEmits(['update:modelValue'])
6
6
  </script>
7
7
  <template>
8
- <div :style="{ position: 'relative', display: 'inline-flex', ...style }">
9
- <select :value="modelValue" @change="$emit('update:modelValue', $event.target.value)" :disabled="disabled"
10
- :style="{ appearance: 'none', height: size === 'sm' ? 25 : 36, borderRadius: size === 'sm' ? 'var(--radius-sm)' : 'var(--radius-md)', border: `1px solid ${error ? 'var(--danger)' : 'var(--border-control)'}`, background: disabled ? 'var(--bg-inset)' : 'var(--bg-surface)', padding: size === 'sm' ? '0 26px 0 10px' : '0 32px 0 12px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', color: disabled ? 'var(--text-secondary)' : modelValue ? 'var(--text-primary)' : 'var(--text-muted)', outline: 'none', cursor: disabled ? 'not-allowed' : 'pointer', width: '100%', opacity: disabled ? .5 : 1 }">
8
+ <div :data-size="size" :style="{ position: 'relative', display: 'inline-flex', ...style }">
9
+ <select :value="modelValue" @change="$emit('update:modelValue', $event.target.value)" :disabled="disabled" :data-field="disabled ? undefined : ''"
10
+ :style="{ appearance: 'none', height: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: `1px solid ${error ? 'var(--danger)' : 'var(--field-border, var(--border-control))'}`, background: disabled ? 'var(--bg-inset)' : 'var(--bg-surface)', padding: '0 calc(var(--control-px) + var(--control-icon) + 6px) 0 var(--control-px)', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', letterSpacing: 'var(--control-tracking)', color: disabled ? 'var(--text-secondary)' : modelValue ? 'var(--text-primary)' : 'var(--text-muted)', outline: 'none', cursor: disabled ? 'not-allowed' : 'pointer', width: '100%', opacity: disabled ? .5 : 1 }">
11
11
  <option v-if="placeholder" value="" disabled hidden>{{ placeholder }}</option>
12
12
  <option v-for="o in options" :key="o" :value="o">{{ o }}</option>
13
13
  </select>
14
- <span :style="{ position: 'absolute', right: size === 'sm' ? 8 : 10, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none', color: 'var(--text-muted)' }"><Icon name="chevron-down" :size="size === 'sm' ? 12 : 14" /></span>
14
+ <span :style="{ position: 'absolute', right: 'var(--control-px)', top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none', color: 'var(--text-muted)', display: 'inline-flex' }"><Icon name="chevron-down" :size="16" :style="{ width: 'var(--control-icon)', height: 'var(--control-icon)' }" /></span>
15
15
  </div>
16
16
  </template>
@@ -9,11 +9,11 @@ export function Slider({ value = 50, min = 0, max = 100, step = 1, labels, showV
9
9
  React.useEffect(() => { if (!drag) return; const mv = (e) => set(e.clientX), up = () => setDrag(false); document.addEventListener("mousemove", mv); document.addEventListener("mouseup", up); return () => { document.removeEventListener("mousemove", mv); document.removeEventListener("mouseup", up); }; }, [drag]);
10
10
  return (
11
11
  <div style={{ width, opacity: disabled ? .5 : 1, ...style }}>
12
- <div ref={track} onMouseDown={disabled ? undefined : (e) => { setDrag(true); set(e.clientX); }} style={{ position: "relative", height: 20, display: "flex", alignItems: "center", cursor: disabled ? "not-allowed" : "pointer" }}>
12
+ <div ref={track} data-thumb={disabled ? undefined : ""} onMouseDown={disabled ? undefined : (e) => { setDrag(true); set(e.clientX); }} style={{ position: "relative", height: 20, display: "flex", alignItems: "center", cursor: disabled ? "not-allowed" : "pointer" }}>
13
13
  <div style={{ position: "absolute", left: 0, right: 0, height: 6, borderRadius: "var(--radius-full)", background: "var(--track)" }}></div>
14
14
  <div style={{ position: "absolute", left: 0, width: pct + "%", height: 6, borderRadius: "var(--radius-full)", background: "var(--action-primary)" }}></div>
15
- <span style={{ position: "absolute", left: pct + "%", transform: "translateX(-50%)", width: 16, height: 16, borderRadius: "var(--radius-full)", background: "var(--bg-surface)", border: "2px solid var(--action-primary)", boxSizing: "border-box", boxShadow: "0 1px 2px rgba(0,0,0,.15)" }}></span>
16
- {(drag || showValue) && <span style={{ position: "absolute", left: pct + "%", transform: "translateX(-50%)", bottom: 22, background: "var(--tooltip-bg)", color: "var(--tooltip-fg)", borderRadius: "var(--radius-sm)", padding: "2px 6px", fontFamily: "var(--font-mono)", fontSize: "var(--text-2xs)", whiteSpace: "nowrap", pointerEvents: "none" }}>{value}{unit}</span>}
15
+ <span style={{ position: "absolute", left: pct + "%", transform: "translateX(-50%)", width: 16, height: 16, borderRadius: "var(--radius-full)", background: "var(--bg-surface)", border: "2px solid var(--action-primary)", boxSizing: "border-box", boxShadow: drag ? "0 0 0 4px var(--focus-ring), 0 1px 2px rgba(0,0,0,.15)" : "var(--thumb-ring, 0 1px 2px rgba(0,0,0,.15))", transition: "box-shadow var(--dur-fast) var(--ease-out)" }}></span>
16
+ {(drag || showValue) && <span style={{ position: "absolute", left: pct + "%", transform: "translateX(-50%)", bottom: 22, background: "var(--tooltip-bg)", color: "var(--tooltip-fg)", borderRadius: "var(--radius-sm)", padding: "2px 6px", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", whiteSpace: "nowrap", pointerEvents: "none" }}>{value}{unit}</span>}
17
17
  </div>
18
18
  {labels && <div style={{ display: "flex", justifyContent: "space-between", paddingTop: 4, fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", color: "var(--text-muted)" }}><span>{min}{unit}</span><span>{max}{unit}</span></div>}
19
19
  </div>
@@ -12,11 +12,11 @@ onUnmounted(up)
12
12
  </script>
13
13
  <template>
14
14
  <div :style="{ width: typeof width === 'number' ? width + 'px' : width, opacity: disabled ? .5 : 1, ...style }">
15
- <div ref="track" @mousedown="!disabled && (drag = true, set($event.clientX))" :style="{ position: 'relative', height: '20px', display: 'flex', alignItems: 'center', cursor: disabled ? 'not-allowed' : 'pointer' }">
15
+ <div ref="track" :data-thumb="disabled ? undefined : ''" @mousedown="!disabled && (drag = true, set($event.clientX))" :style="{ position: 'relative', height: '20px', display: 'flex', alignItems: 'center', cursor: disabled ? 'not-allowed' : 'pointer' }">
16
16
  <div style="position:absolute;left:0;right:0;height:6px;border-radius:var(--radius-full);background:var(--track)" />
17
17
  <div :style="{ position: 'absolute', left: 0, width: pct + '%', height: '6px', borderRadius: 'var(--radius-full)', background: 'var(--action-primary)' }" />
18
- <span :style="{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', width: '16px', height: '16px', borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', border: '2px solid var(--action-primary)', boxSizing: 'border-box', boxShadow: '0 1px 2px rgba(0,0,0,.15)' }" />
19
- <span v-if="drag || showValue" :style="{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', bottom: '22px', background: 'var(--tooltip-bg)', color: 'var(--tooltip-fg)', borderRadius: 'var(--radius-sm)', padding: '2px 6px', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-2xs)', whiteSpace: 'nowrap', pointerEvents: 'none' }">{{ modelValue }}{{ unit }}</span>
18
+ <span :style="{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', width: '16px', height: '16px', borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', border: '2px solid var(--action-primary)', boxSizing: 'border-box', boxShadow: drag ? '0 0 0 4px var(--focus-ring), 0 1px 2px rgba(0,0,0,.15)' : 'var(--thumb-ring, 0 1px 2px rgba(0,0,0,.15))', transition: 'box-shadow var(--dur-fast) var(--ease-out)' }" />
19
+ <span v-if="drag || showValue" :style="{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', bottom: '22px', background: 'var(--tooltip-bg)', color: 'var(--tooltip-fg)', borderRadius: 'var(--radius-sm)', padding: '2px 6px', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', whiteSpace: 'nowrap', pointerEvents: 'none' }">{{ modelValue }}{{ unit }}</span>
20
20
  </div>
21
21
  <div v-if="labels" style="display:flex;justify-content:space-between;padding-top:4px;font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-muted)"><span>{{ min }}{{ unit }}</span><span>{{ max }}{{ unit }}</span></div>
22
22
  </div>
@@ -1,31 +1,39 @@
1
1
  import * as React from "react";
2
- /** 34×20 switch for settings-style booleans. Addition — not in the source file. */
2
+ /**
3
+ * Switch — pill track at the region's tier (sm 28×16 · md 36×20 · lg 44×24), knob 4px smaller, 160ms ease-out slide. Controlled.
4
+ * Variants: knob glyph (icon or one character), text inside the pill, captions beside it,
5
+ * a semantic red→green tone, and an async gate that shows a spinner in the knob.
6
+ */
3
7
  export interface SwitchProps {
4
8
  checked?: boolean;
5
- onChange?: (checked: boolean) => void;
6
- label?: string;
7
- size?: "md" | "sm";
9
+ onChange?(next: boolean): void;
8
10
  disabled?: boolean;
9
- style?: React.CSSProperties;
10
- }
11
- export declare function Switch(props: SwitchProps): JSX.Element;
12
- /** 16px (sm: 13px) radio. Addition — not in the source file. */
13
- export interface RadioProps {
14
- checked?: boolean;
15
- onChange?: (checked: boolean) => void;
11
+ /** Size tier: sm 28×16 · md 36×20 · lg 44×24. Omit to inherit the enclosing region's tier (the default); lg is roomy enough for a knob glyph */
12
+ size?: "sm" | "md" | "lg";
13
+ /** primary = action blue on / border-strong off · semantic = success on / danger off */
14
+ tone?: "primary" | "semantic";
15
+ /** Explicit track colors; override tone. Prefer tokens. */
16
+ onColor?: string;
17
+ offColor?: string;
18
+ /** Icon inside the knob, or a pair for each state */
19
+ knobIcon?: string | { on: string; off: string };
20
+ /** ONE letter or ONE CJK character inside the knob, or a pair */
21
+ knobText?: string | { on: string; off: string };
22
+ /** Text inside the pill opposite the knob — keep it to 1–3 characters */
23
+ insideOn?: string;
24
+ insideOff?: string;
25
+ /** Alias for the insideOn/insideOff pair */
26
+ inlinePrompt?: { on: string; off: string };
27
+ /** Single caption to the right (settings rows) — text-body, state-independent */
16
28
  label?: string;
17
- size?: "md" | "sm";
18
- disabled?: boolean;
19
- style?: React.CSSProperties;
20
- }
21
- export declare function Radio(props: RadioProps): JSX.Element;
22
- /** Radio set; `row` lays them horizontally. */
23
- export interface RadioGroupProps {
24
- options?: string[];
25
- value?: string;
26
- onChange?: (value: string) => void;
27
- row?: boolean;
28
- size?: "md" | "sm";
29
+ /** Captions beside the pill; the active side takes text-heading */
30
+ labelOn?: string;
31
+ labelOff?: string;
32
+ /** Controlled loading: spinner in the knob, input inert */
33
+ loading?: boolean;
34
+ /** Async gate — Switch shows loading until it settles; flips only on truthy */
35
+ beforeChange?(next: boolean): Promise<boolean> | boolean;
29
36
  style?: React.CSSProperties;
30
37
  }
31
- export declare function RadioGroup(props: RadioGroupProps): JSX.Element;
38
+ export declare function Switch(props: SwitchProps): JSX.Element;
39
+ export default Switch;
@@ -1,40 +1,90 @@
1
1
  import React from "react";
2
+ import { Icon } from "../icons/Icon.jsx";
2
3
 
3
- const switchDims = { sm: { w: 28, h: 16, knob: 12 }, md: { w: 34, h: 20, knob: 16 } };
4
- /** Switch — 34×20 (sm: 28×16) track, knob. Keybricks has no switch in the source file; this follows the button radii + action color. */
5
- export function Switch({ checked, onChange, label, size = "md", disabled, style }) {
6
- const d = switchDims[size] || switchDims.md;
7
- return (
8
- <label style={{ display: "inline-flex", alignItems: "center", verticalAlign: "middle", lineHeight: "var(--leading-sm)", gap: 8, cursor: disabled ? "default" : "pointer", opacity: disabled ? .5 : 1, ...style }}
9
- onClick={() => !disabled && onChange && onChange(!checked)}>
10
- <span style={{ width: d.w, height: d.h, borderRadius: "var(--radius-full)", background: checked ? "var(--action-primary)" : "var(--track)", position: "relative", flexShrink: 0, transition: "var(--t-hover)" }}>
11
- <span style={{ position: "absolute", top: 2, left: checked ? d.w - d.knob - 2 : 2, width: d.knob, height: d.knob, borderRadius: "var(--radius-full)", background: "var(--on-accent)", boxShadow: "0 1px 2px rgba(0,0,0,.2)", transition: "left var(--dur-fast) var(--ease-out)" }} />
12
- </span>
13
- {label ? <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: size === "sm" ? "var(--text-xs)" : "var(--text-sm)", lineHeight: "var(--leading-sm)", color: "var(--text-body)" }}>{label}</span> : null}
14
- </label>
15
- );
16
- }
4
+ /**
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.
7
+ *
8
+ * checked / onChange(next) controlled boolean
9
+ * disabled 50% opacity, no pointer
10
+ * size "sm" | "md" | "lg" — omit to inherit the region's tier; lg is roomy enough for knobIcon / knobText
11
+ * tone "primary" (default, action blue) | "semantic" (red off → green on)
12
+ * onColor / offColor explicit track colors; override tone
13
+ * knobIcon / knobText glyph inside the knob: string | { on, off }. knobText is ONE letter or ONE CJK character.
14
+ * insideOn / insideOff text inside the pill on the far side of the knob (short — "ON"/"OFF", "开"/"关", "Y"/"N")
15
+ * labelOn / labelOff captions beside the pill; active caption takes text-heading, inactive takes text-muted
16
+ * loading controlled: knob shows a spinner, input is inert
17
+ * beforeChange(next) => Promise|boolean
18
+ * async gate — Switch enters loading, flips only if the promise resolves truthy
19
+ * inlinePrompt (Vue-friendly alias of insideOn/insideOff pair {on,off})
20
+ */
17
21
 
18
- const radioDims = { sm: { box: 13, dot: 6 }, md: { box: 16, dot: 8 } };
19
- /** Radio — 16px (sm: 13px) circle, 1.5px border, filled dot when selected. */
20
- export function Radio({ checked, onChange, label, size = "md", disabled, style }) {
21
- const d = radioDims[size] || radioDims.md;
22
- return (
23
- <label style={{ display: "inline-flex", alignItems: "center", verticalAlign: "middle", lineHeight: "var(--leading-sm)", gap: 6, cursor: disabled ? "default" : "pointer", opacity: disabled ? .5 : 1, ...style }}
24
- onClick={() => !disabled && onChange && onChange(true)}>
25
- <span style={{ width: d.box, height: d.box, borderRadius: "var(--radius-full)", boxSizing: "border-box", border: `1.5px solid ${checked ? "var(--action-primary)" : "var(--checkbox-border)"}`, background: "var(--bg-surface)", display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0, transition: "var(--t-hover)" }}>
26
- {checked ? <span style={{ width: d.dot, height: d.dot, borderRadius: "var(--radius-full)", background: "var(--action-primary)" }} /> : null}
27
- </span>
28
- {label ? <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: size === "sm" ? "var(--text-xs)" : "var(--text-sm)", lineHeight: "var(--leading-sm)", color: "var(--text-body)" }}>{label}</span> : null}
29
- </label>
30
- );
31
- }
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)";
24
+
25
+ const pick = (v, on) => (v && typeof v === "object" ? (on ? v.on : v.off) : v);
26
+
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 }) {
28
+ // `label` = single caption to the right, shown in text-body regardless of state (settings-row usage)
29
+ const single = label != null && labelOn == null && labelOff == null;
30
+ const [busy, setBusy] = React.useState(false);
31
+ const alive = React.useRef(true);
32
+ React.useEffect(() => () => { alive.current = false; }, []);
33
+ const loading = loadingProp || busy;
34
+ if (inlinePrompt) { insideOn = insideOn ?? inlinePrompt.on; insideOff = insideOff ?? inlinePrompt.off; }
35
+ const hasInside = insideOn != null || insideOff != null;
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;
39
+
40
+ const onTrack = onColor || (tone === "semantic" ? "var(--success)" : "var(--action-primary)");
41
+ const offTrack = offColor || (tone === "semantic" ? "var(--danger)" : "var(--border-strong)");
42
+ const track = checked ? onTrack : offTrack;
43
+ const inert = disabled || loading;
44
+
45
+ const flip = async () => {
46
+ if (inert) return;
47
+ const next = !checked;
48
+ if (beforeChange) {
49
+ setBusy(true);
50
+ let ok = false;
51
+ try { ok = await beforeChange(next); } catch (e) { ok = false; }
52
+ if (!alive.current) return;
53
+ setBusy(false);
54
+ if (!ok) return;
55
+ }
56
+ onChange && onChange(next);
57
+ };
32
58
 
33
- /** Vertical or horizontal group of Radios. */
34
- export function RadioGroup({ options = [], value, onChange, row, size = "md", style }) {
35
- return (
36
- <div style={{ display: "flex", flexDirection: row ? "row" : "column", gap: row ? 16 : 10, ...style }}>
37
- {options.map((o) => <Radio key={o} label={o} size={size} checked={value === o} onChange={() => onChange && onChange(o)} />)}
38
- </div>
39
- );
59
+ const glyphIcon = pick(knobIcon, checked);
60
+ const glyphText = pick(knobText, checked);
61
+ const knobX = checked ? `calc(100% - ${H} + 2px)` : 2;
62
+
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: GLYPH, lineHeight: 1, fontWeight: "var(--weight-semibold)", fontFamily: "var(--font-sans)", overflow: "hidden" }}>
64
+ {loading
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 }} />
67
+ : glyphText ? <span>{String(glyphText).slice(0, 1)}</span> : null}
68
+ </span>;
69
+
70
+ const insideStyle = (side) => ({ position: "absolute", top: 0, bottom: 0, [side]: 6, display: "flex", alignItems: "center", fontSize: GLYPH, 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" });
71
+
72
+ const pill = <span role="switch" aria-checked={checked} aria-busy={loading || undefined} aria-disabled={disabled || undefined} tabIndex={inert ? -1 : 0} onClick={flip}
73
+ onKeyDown={e => { if (e.key === " " || e.key === "Enter") { e.preventDefault(); flip(); } }}
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" }}>
75
+ {hasInside && <span style={{ ...insideStyle("left"), opacity: checked ? 1 : 0 }}>{insideOn}</span>}
76
+ {hasInside && <span style={{ ...insideStyle("right"), opacity: checked ? 0 : 1 }}>{insideOff}</span>}
77
+ {knob}
78
+ </span>;
79
+
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>;
83
+
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 }}>
86
+ {cap(labelOff, !checked)}{pill}{cap(labelOn, checked)}
87
+ </span>;
40
88
  }
89
+
90
+ export default Switch;
@@ -1,16 +1,62 @@
1
1
  <script setup>
2
- import { computed } from 'vue'
3
- // Switch — 34×20 (sm: 28×16) track, knob.
4
- const dims = { sm: { w: 28, h: 16, knob: 12 }, md: { w: 34, h: 20, knob: 16 } }
5
- const props = defineProps({ modelValue: Boolean, label: String, size: { type: String, default: 'md' }, disabled: Boolean, style: Object })
6
- const emit = defineEmits(['update:modelValue'])
7
- const d = computed(() => dims[props.size] || dims.md)
2
+ import { ref, computed, onBeforeUnmount } from 'vue'
3
+ import Icon from '../icons/Icon.vue'
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.
6
+ // tone "primary" | "semantic" (red off → green on); onColor/offColor override.
7
+ // knobIcon / knobText: string | { on, off } — knobText is ONE letter or ONE CJK char.
8
+ // insideOn / insideOff: text inside the pill opposite the knob. labelOn / labelOff: captions beside the pill.
9
+ // loading: controlled spinner. beforeChange(next) → Promise|boolean gates the flip and shows loading meanwhile.
10
+ const props = defineProps({
11
+ checked: Boolean, disabled: Boolean, loading: Boolean,
12
+ size: String, tone: { type: String, default: 'primary' },
13
+ onColor: String, offColor: String,
14
+ knobIcon: [String, Object], knobText: [String, Object],
15
+ insideOn: String, insideOff: String, label: String, labelOn: String, labelOff: String,
16
+ beforeChange: Function, style: Object
17
+ })
18
+ const emit = defineEmits(['update:checked', 'change'])
19
+ const H = 'var(--switch-h)', W = 'var(--switch-w)', KNOB = 'calc(var(--switch-h) - 4px)', GLYPH = 'calc(var(--switch-h) * .5)'
20
+ const busy = ref(false); let alive = true; onBeforeUnmount(() => { alive = false })
21
+ const isLoading = computed(() => props.loading || busy.value)
22
+ const inert = computed(() => props.disabled || isLoading.value)
23
+ const hasInside = computed(() => props.insideOn != null || props.insideOff != null)
24
+ // inside text needs a wider track so the knob never covers the word: text runs at half the track height, ~.68em per character
25
+ const width = computed(() => { if (!hasInside.value) return W; const n = Math.max((props.insideOn || '').length, (props.insideOff || '').length); return `max(${W}, calc(${H} * ${(1 + n * .34).toFixed(2)} + 6px))` })
26
+ 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)')))
27
+ const pick = (v) => v && typeof v === 'object' ? (props.checked ? v.on : v.off) : v
28
+ const glyphIcon = computed(() => pick(props.knobIcon))
29
+ const glyphText = computed(() => { const t = pick(props.knobText); return t ? String(t).slice(0, 1) : '' })
30
+ const knobX = computed(() => props.checked ? `calc(100% - ${H} + 2px)` : '2px')
31
+ async function flip() {
32
+ if (inert.value) return
33
+ const next = !props.checked
34
+ if (props.beforeChange) {
35
+ busy.value = true; let ok = false
36
+ try { ok = await props.beforeChange(next) } catch (e) { ok = false }
37
+ if (!alive) return
38
+ busy.value = false
39
+ if (!ok) return
40
+ }
41
+ emit('update:checked', next); emit('change', next)
42
+ }
43
+ const insideStyle = (side, on) => ({ position: 'absolute', top: 0, bottom: 0, [side]: '6px', display: 'flex', alignItems: 'center', fontSize: GLYPH, 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 })
44
+ 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' })
8
45
  </script>
9
46
  <template>
10
- <label :style="{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--leading-sm)', gap: 8, cursor: disabled ? 'default' : 'pointer', opacity: disabled ? .5 : 1, ...style }" @click="!disabled && emit('update:modelValue', !modelValue)">
11
- <span :style="{ width: d.w + 'px', height: d.h + 'px', borderRadius: 'var(--radius-full)', background: modelValue ? 'var(--action-primary)' : 'var(--track)', position: 'relative', flexShrink: 0, transition: 'var(--t-hover)' }">
12
- <span :style="{ position: 'absolute', top: '2px', left: (modelValue ? d.w - d.knob - 2 : 2) + 'px', width: d.knob + 'px', height: d.knob + 'px', borderRadius: 'var(--radius-full)', background: 'var(--on-accent)', boxShadow: '0 1px 2px rgba(0,0,0,.2)', transition: 'left var(--dur-fast) var(--ease-out)' }" />
47
+ <span :data-size="size" data-sw="" :style="{ display: 'inline-flex', alignItems: 'center', gap: 'var(--control-gap)', opacity: disabled ? .5 : 1, ...style }">
48
+ <span v-if="labelOff != null" :style="cap(!checked)" @click="flip">{{ labelOff }}</span>
49
+ <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"
50
+ :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' }">
51
+ <span v-if="hasInside" :style="insideStyle('left', true)">{{ insideOn }}</span>
52
+ <span v-if="hasInside" :style="insideStyle('right', false)">{{ insideOff }}</span>
53
+ <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: GLYPH, lineHeight: 1, fontWeight: 'var(--weight-semibold)', fontFamily: 'var(--font-sans)', overflow: 'hidden' }">
54
+ <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>
55
+ <Icon v-else-if="glyphIcon" :name="glyphIcon" :size="10" :stroke-width="2.5" :style="{ width: GLYPH, height: GLYPH }" />
56
+ <span v-else-if="glyphText">{{ glyphText }}</span>
57
+ </span>
13
58
  </span>
14
- <span v-if="label" :style="{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: size === 'sm' ? 'var(--text-xs)' : 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: 'var(--text-body)' }">{{ label }}</span>
15
- </label>
59
+ <span v-if="labelOn != null" :style="cap(checked)" @click="flip">{{ labelOn }}</span>
60
+ <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>
61
+ </span>
16
62
  </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>
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
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>
19
19
  </div>
20
20
  </template>
@@ -15,9 +15,12 @@ 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):
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}/>
22
+ <Switch size="lg" tone="semantic" knobIcon={{on:"check",off:"x"}} checked={live} beforeChange={n=>api.setLive(n)}/>
23
+ <Switch labelOff="Monthly" labelOn="Annual" checked={annual} onChange={setAnnual}/>
22
24
  <RadioGroup options={["New","Overwrite","Skip"]} value={v} onChange={setV}/>
25
+ <RadioGroup variant="cards" row value={m} onChange={setM} options={[{label:"Merge",value:"merge",description:"Keep target values, add new keys"},{label:"Overwrite",value:"overwrite",description:"Target takes source values"}]}/>
23
26
  ```
@@ -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 24 · md 32 · lg 36.
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,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";
@@ -65,6 +64,7 @@ export * from "./forms/Field";
65
64
  export * from "./forms/Input";
66
65
  export * from "./forms/MultiSelect";
67
66
  export * from "./forms/NumberStepper";
67
+ export * from "./forms/Radio";
68
68
  export * from "./forms/SearchInput";
69
69
  export * from "./forms/Select";
70
70
  export * from "./forms/Slider";
@@ -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";
@@ -80,10 +79,11 @@ export { Field } from "./forms/Field.jsx";
80
79
  export { Input, Textarea } from "./forms/Input.jsx";
81
80
  export { MultiSelect } from "./forms/MultiSelect.jsx";
82
81
  export { NumberStepper } from "./forms/NumberStepper.jsx";
82
+ export { Radio, RadioGroup } from "./forms/Radio.jsx";
83
83
  export { SearchInput } from "./forms/SearchInput.jsx";
84
84
  export { Select } from "./forms/Select.jsx";
85
85
  export { Slider } from "./forms/Slider.jsx";
86
- export { Radio, RadioGroup, Switch } from "./forms/Switch.jsx";
86
+ export { Switch } from "./forms/Switch.jsx";
87
87
 
88
88
  /* icons/ */
89
89
  export { ICON_PATHS, Icon } from "./icons/Icon.jsx";
@@ -9,7 +9,7 @@ function Crumb({ label, last, hovered, onSelect }) {
9
9
  onClick={() => !last && onSelect && onSelect(label)}
10
10
  onKeyDown={(e) => { if (!last && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); onSelect && onSelect(label); } }}
11
11
  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>
12
+ 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
13
  );
14
14
  }
15
15
 
@@ -34,7 +34,7 @@ function Overflow({ items, onSelect }) {
34
34
  onClick={() => setOpen(!open)}
35
35
  onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpen(!open); } if (e.key === "Escape") setOpen(false); }}
36
36
  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)" }}>
37
+ 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
38
  <Icon name="ellipsis" size={14} />
39
39
  </span>
40
40
  {open ? (
@@ -52,7 +52,7 @@ function OverflowItem({ label, depth, onSelect }) {
52
52
  <span role="menuitem" tabIndex={0} onClick={() => onSelect(label)}
53
53
  onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onSelect(label); } }}
54
54
  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)" }}>
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-base)", lineHeight: "var(--leading-base)", color: "var(--text-heading)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", transition: "var(--t-hover)" }}>
56
56
  {depth > 0 ? <span style={{ color: "var(--text-muted)", display: "inline-flex", flexShrink: 0 }}><Icon name="chevron-right" size={11} /></span> : null}
57
57
  {label}
58
58
  </span>