@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
@@ -12,17 +12,17 @@ function stop(e) { e.preventDefault(); e.stopPropagation() }
12
12
  function onDrop(e) { stop(e); dragging.value = false; emit('dropFiles', e.dataTransfer.files) }
13
13
  </script>
14
14
  <template>
15
- <div v-if="file" :style="{ height, borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-control)', background: 'var(--bg-inset)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8, boxSizing: 'border-box', ...style }">
15
+ <div v-if="file" :style="{ height, borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-control)', background: 'var(--bg-inset)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px', boxSizing: 'border-box', ...style }">
16
16
  <span style="color:var(--success-strong)"><Icon name="file-check" :size="22" :stroke-width="1.5" /></span>
17
17
  <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);color:var(--text-heading)">{{ file }}</span>
18
- <button type="button" @click="$emit('remove')" style="display:flex;align-items:center;gap:4px;border:none;background:transparent;cursor:pointer;color:var(--danger);font-family:var(--font-sans);font-size:var(--text-sm);padding:0"><Icon name="trash-2" :size="13" />Remove</button>
18
+ <button type="button" data-link="" @click="$emit('remove')" style="display:flex;align-items:center;gap:4px;border:none;background:transparent;cursor:pointer;color:var(--danger);font-family:var(--font-sans);font-size:var(--text-base);padding:0;text-decoration-line:var(--link-line, none);text-underline-offset:2px"><Icon name="trash-2" :size="13" />Remove</button>
19
19
  </div>
20
20
  <div v-else @click="!disabled && $emit('click')" @mouseenter="hover = true" @mouseleave="hover = false"
21
21
  @dragenter="!disabled && ((e) => { stop(e); dragging = true })($event)" @dragover="!disabled && stop($event)"
22
22
  @dragleave="!disabled && ((e) => { stop(e); dragging = false })($event)" @drop="!disabled && onDrop($event)"
23
- :style="{ height, borderRadius: 'var(--radius-lg)', border: `1px dashed ${toneColor[tone]}`, background: error ? 'var(--danger-bg)' : dragActive ? 'var(--soft-bg)' : 'transparent', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 4, cursor: disabled ? 'not-allowed' : 'pointer', boxSizing: 'border-box', opacity: disabled ? .5 : 1, transition: 'var(--t-hover)', ...style }">
24
- <span :style="{ color: error ? 'var(--danger)' : dragActive ? 'var(--action-primary)' : 'var(--text-muted)', marginBottom: 6 }"><Icon :name="error ? 'triangle-alert' : 'upload'" :size="22" :stroke-width="1.5" /></span>
23
+ :style="{ height, borderRadius: 'var(--radius-lg)', border: `1px dashed ${toneColor[tone]}`, background: error ? 'var(--danger-bg)' : dragActive ? 'var(--soft-bg)' : 'transparent', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', boxSizing: 'border-box', opacity: disabled ? .5 : 1, transition: 'var(--t-hover)', ...style }">
24
+ <span :style="{ color: error ? 'var(--danger)' : dragActive ? 'var(--action-primary)' : 'var(--text-muted)', marginBottom: '6px' }"><Icon :name="error ? 'triangle-alert' : 'upload'" :size="22" :stroke-width="1.5" /></span>
25
25
  <span :style="{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', color: error ? 'var(--danger-text)' : dragActive ? 'var(--action-primary)' : 'var(--text-heading)' }">{{ error || (dragActive ? 'Drop file to upload' : title) }}</span>
26
- <span :style="{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: dragActive ? 'var(--action-primary)' : 'var(--text-secondary)' }">{{ hint }}</span>
26
+ <span :style="{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', color: dragActive ? 'var(--action-primary)' : 'var(--text-secondary)' }">{{ hint }}</span>
27
27
  </div>
28
28
  </template>
@@ -1,8 +1,12 @@
1
1
  import * as React from "react";
2
- /** Label + control wrapper. Label 12px/500 #525252; required appends a red " *" after the label. */
2
+ /** Label + control wrapper. The label uses the tier's control text at weight 500 (text-body); required appends a red " *". */
3
3
  export interface FieldProps {
4
4
  label: string;
5
5
  required?: boolean;
6
+ /** Size tier for the label and the control inside. Omit to inherit the enclosing region's tier (the default). */
7
+ size?: "sm" | "md" | "lg";
8
+ /** Label beside the control instead of above it — the filter-bar layout */
9
+ inline?: boolean;
6
10
  /** Muted helper text below the control; hidden when `error` is set */
7
11
  hint?: string;
8
12
  /** Red error text below the control; also expected to drive the control's own error look */
@@ -1,10 +1,10 @@
1
1
  import React from "react";
2
2
 
3
- /** Form field wrapper: 12px/500 label (red + * when required) above the control, optional hint/error below. */
4
- export function Field({ label, required, hint, error, children, style }) {
3
+ /** Form field wrapper: label (tier control text, weight 500; red + * when required) above the control, optional hint/error below. `size` sets the tier for the whole field; `inline` puts the label beside the control for filter bars. */
4
+ export function Field({ label, required, hint, error, size, inline, children, style }) {
5
5
  return (
6
- <div style={{ display: "flex", flexDirection: "column", ...style }}>
7
- <label style={{ paddingBottom: 6, fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", color: "var(--text-body)" }}>{label}{required ? <span style={{ color: "var(--danger)" }}> *</span> : null}</label>
6
+ <div data-size={size} style={{ display: "flex", flexDirection: inline ? "row" : "column", ...(inline ? { alignItems: "center", gap: "var(--control-gap)" } : null), ...style }}>
7
+ <label style={{ paddingBottom: inline ? 0 : 8, whiteSpace: inline ? "nowrap" : undefined, fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-body)" }}>{label}{required ? <span style={{ color: "var(--danger)" }}> *</span> : null}</label>
8
8
  {children}
9
9
  {error ? <span style={{ paddingTop: 5, fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: "var(--danger)" }}>{error}</span>
10
10
  : hint ? <span style={{ paddingTop: 5, fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: "var(--text-muted)" }}>{hint}</span> : null}
@@ -1,10 +1,10 @@
1
1
  <script setup>
2
- // Form field wrapper: 12px/500 label (red + * when required) above the control, optional hint/error below.
3
- defineProps({ label: String, required: Boolean, hint: String, error: String, style: Object })
2
+ // Form field wrapper: label (tier control text, weight 500; red + * when required) above the control, optional hint/error below. `size` sets the tier for the whole field; `inline` puts the label beside the control for filter bars.
3
+ defineProps({ label: String, required: Boolean, hint: String, error: String, size: String, inline: Boolean, style: Object })
4
4
  </script>
5
5
  <template>
6
- <div :style="{ display: 'flex', flexDirection: 'column', ...style }">
7
- <label style="padding-bottom:6px;font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-body)">{{ label }}<span v-if="required" style="color:var(--danger)"> *</span></label>
6
+ <div :data-size="size" :style="{ display: 'flex', flexDirection: inline ? 'row' : 'column', ...(inline ? { alignItems: 'center', gap: 'var(--control-gap)' } : null), ...style }">
7
+ <label :style="{ paddingBottom: inline ? 0 : '8px', whiteSpace: inline ? 'nowrap' : undefined, fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-body)' }">{{ label }}<span v-if="required" style="color:var(--danger)"> *</span></label>
8
8
  <slot />
9
9
  <span v-if="error" style="padding-top:5px;font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--danger)">{{ error }}</span>
10
10
  <span v-else-if="hint" style="padding-top:5px;font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-muted)">{{ hint }}</span>
@@ -1,6 +1,8 @@
1
1
  import * as React from "react";
2
- /** 36px r8 hairline text input; readOnly = #fafafa inset, disabled = 50% opacity, error = red border+ring. Extends native input props. */
2
+ /** r8 hairline text input at the region's tier height (sm 24 · md 32 · lg 36); readOnly = inset fill, disabled = 50% opacity, error = red border+ring. Extends native input props. */
3
3
  export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
4
+ /** Size tier. Omit to inherit the enclosing region's tier (the default); controls that share a row must share a tier. */
5
+ size?: "sm" | "md" | "lg";
4
6
  readOnly?: boolean;
5
7
  disabled?: boolean;
6
8
  error?: boolean;
@@ -9,6 +11,8 @@ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>
9
11
  export declare function Input(props: InputProps): JSX.Element;
10
12
  /** r8 hairline textarea (default 90px). showCount + maxLength renders a live "n/max" character counter that turns red past the limit. */
11
13
  export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
14
+ /** Size tier for the text; omit to inherit */
15
+ size?: "sm" | "md" | "lg";
12
16
  error?: boolean;
13
17
  disabled?: boolean;
14
18
  showCount?: boolean;
@@ -3,18 +3,18 @@ import React from "react";
3
3
  import { inputBase } from "./input-base.js";
4
4
  export { inputBase };
5
5
 
6
- /** Text input, 36px r8. readOnly = #fafafa inset; disabled = 50% opacity; error = red border + ring. */
7
- export function Input({ readOnly, disabled, error, style, ...rest }) {
6
+ /** Text input, r8, tier height (sm 24 · md 32 · lg 36 — omit `size` to inherit the region's tier). readOnly = inset fill; disabled = 50% opacity; error = red border + ring. */
7
+ export function Input({ readOnly, disabled, error, size, style, ...rest }) {
8
8
  const [focus, setFocus] = React.useState(false);
9
- return <input readOnly={readOnly} disabled={disabled} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
9
+ return <input readOnly={readOnly} disabled={disabled} data-size={size} data-field={readOnly || disabled ? undefined : ""} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
10
10
  style={{ ...inputBase, ...(readOnly ? { background: "var(--bg-inset)", color: "var(--text-secondary)" } : null),
11
11
  ...(error ? { borderColor: "var(--danger)" } : null),
12
12
  ...(focus && !readOnly && !disabled ? { borderColor: error ? "var(--danger)" : "var(--focus-border)", boxShadow: `0 0 0 2px ${error ? "var(--danger-bg)" : "var(--focus-ring)"}` } : null),
13
13
  ...(disabled ? { opacity: .5, cursor: "not-allowed", background: "var(--bg-inset)" } : null), ...style }} {...rest} />;
14
14
  }
15
15
 
16
- /** Multiline textarea, r8, default 90px tall. showCount + maxLength renders a bottom-right "n/max" counter (red past the limit). */
17
- export function Textarea({ style, rows = 4, error, disabled, showCount, maxLength, value, defaultValue, onChange, ...rest }) {
16
+ /** Multiline textarea, r8, default 90px tall; text follows the region's tier. showCount + maxLength renders a bottom-right "n/max" counter (red past the limit). */
17
+ export function Textarea({ style, rows = 4, error, disabled, showCount, maxLength, size, value, defaultValue, onChange, ...rest }) {
18
18
  const [focus, setFocus] = React.useState(false);
19
19
  const [inner, setInner] = React.useState(defaultValue || "");
20
20
  const val = value != null ? value : inner;
@@ -22,11 +22,11 @@ export function Textarea({ style, rows = 4, error, disabled, showCount, maxLengt
22
22
  const over = maxLength != null && len > maxLength;
23
23
  return (
24
24
  <div style={{ position: "relative" }}>
25
- <textarea rows={rows} disabled={disabled} maxLength={showCount ? undefined : maxLength}
25
+ <textarea rows={rows} disabled={disabled} data-size={size} data-field={disabled ? undefined : ""} maxLength={showCount ? undefined : maxLength}
26
26
  value={value} defaultValue={value == null ? defaultValue : undefined}
27
27
  onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
28
28
  onChange={(e) => { if (value == null) setInner(e.target.value); onChange && onChange(e); }}
29
- style={{ ...inputBase, height: 90, resize: "vertical", paddingBottom: showCount ? 22 : 8,
29
+ style={{ ...inputBase, height: 90, resize: "vertical", paddingTop: 8, paddingBottom: showCount ? 22 : 8,
30
30
  ...(error || over ? { borderColor: "var(--danger)" } : null),
31
31
  ...(focus && !disabled ? { borderColor: error || over ? "var(--danger)" : "var(--focus-border)", boxShadow: `0 0 0 2px ${error || over ? "var(--danger-bg)" : "var(--focus-ring)"}` } : null),
32
32
  ...(disabled ? { opacity: .5, cursor: "not-allowed", background: "var(--bg-inset)" } : null), ...style }} {...rest} />
@@ -1,9 +1,9 @@
1
1
  <script setup>
2
2
  import { ref, computed } from 'vue'
3
3
  import { inputBase } from './input-base.js'
4
- // Text input, 36px r8. v-model. readonly = inset fill; disabled = 50% opacity; error = red border + ring.
4
+ // Text input, r8, tier height (sm 24 · md 32 · lg 36 — omit `size` to inherit the region's tier). v-model. readonly = inset fill; disabled = 50% opacity; error = red border + ring.
5
5
  // Multiline variant lives in Textarea.vue.
6
- const props = defineProps({ modelValue: String, readonly: Boolean, disabled: Boolean, error: Boolean, style: Object })
6
+ const props = defineProps({ modelValue: String, readonly: Boolean, disabled: Boolean, error: Boolean, size: String, style: Object })
7
7
  defineEmits(['update:modelValue'])
8
8
  const focus = ref(false)
9
9
  const s = computed(() => ({ ...inputBase, ...(props.readonly ? { background: 'var(--bg-inset)', color: 'var(--text-secondary)' } : null),
@@ -12,5 +12,5 @@ const s = computed(() => ({ ...inputBase, ...(props.readonly ? { background: 'va
12
12
  ...(props.disabled ? { opacity: .5, cursor: 'not-allowed', background: 'var(--bg-inset)' } : null), ...props.style }))
13
13
  </script>
14
14
  <template>
15
- <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" :readonly="readonly" :disabled="disabled" @focus="focus = true" @blur="focus = false" :style="s" />
15
+ <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" :readonly="readonly" :disabled="disabled" :data-size="size" :data-field="readonly || disabled ? undefined : ''" @focus="focus = true" @blur="focus = false" :style="s" />
16
16
  </template>
@@ -9,6 +9,8 @@ export interface MultiSelectProps {
9
9
  maxTags?: number;
10
10
  disabled?: boolean;
11
11
  width?: number | string;
12
+ /** Size tier: sm 24 · md 32 · lg 36. Omit to inherit the enclosing region's tier (the default). */
13
+ size?: "sm" | "md" | "lg";
12
14
  onChange?: (value: string[]) => void;
13
15
  style?: React.CSSProperties;
14
16
  }
@@ -3,29 +3,31 @@ import { Icon } from "../icons/Icon.jsx";
3
3
  import { Checkbox } from "./Checkbox.jsx";
4
4
  import { Tag } from "../display/Tag.jsx";
5
5
 
6
- /** Multi-select dropdown: checkbox option list, selected values render as removable tags in the field (overflow collapses to "+n"), clear-all ×. */
7
- export function MultiSelect({ options = [], value = [], placeholder = "Select...", maxTags = 2, disabled, width, onChange, style }) {
6
+ const tierIcon = { width: "var(--control-icon)", height: "var(--control-icon)" };
7
+
8
+ /** Multi-select dropdown: checkbox option list, selected values render as removable tags in the field (overflow collapses to "+n"), clear-all ×. Field, tags and option rows take the region's tier (omit `size` to inherit). */
9
+ export function MultiSelect({ options = [], value = [], placeholder = "Select...", maxTags = 2, disabled, width, size, onChange, style }) {
8
10
  const [open, setOpen] = React.useState(false);
9
11
  const root = React.useRef(null);
10
12
  React.useEffect(() => { const h = (e) => { if (root.current && !root.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []);
11
13
  const toggle = (o) => { const next = value.includes(o) ? value.filter(x => x !== o) : [...value, o]; onChange && onChange(next); };
12
14
  const shown = value.slice(0, maxTags), extra = value.length - shown.length;
13
15
  return (
14
- <div ref={root} style={{ position: "relative", width, ...style }}>
15
- <div onClick={() => !disabled && setOpen(!open)} style={{ minHeight: "var(--h-input)", borderRadius: "var(--radius-md)", border: `1px solid ${open ? "var(--focus-border)" : "var(--border-control)"}`, boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", background: "var(--bg-surface)", padding: "5px 30px 5px 8px", boxSizing: "border-box", display: "flex", alignItems: "center", flexWrap: "wrap", gap: 4, cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .5 : 1, position: "relative" }}>
16
- {value.length === 0 && <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: "var(--text-muted)", paddingLeft: 4 }}>{placeholder}</span>}
16
+ <div ref={root} data-size={size} style={{ position: "relative", width, ...style }}>
17
+ <div data-field={disabled ? undefined : ""} onClick={() => !disabled && setOpen(!open)} style={{ minHeight: "var(--control-h)", borderRadius: "var(--radius-md)", border: `1px solid ${open ? "var(--focus-border)" : "var(--field-border, var(--border-control))"}`, boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", background: "var(--bg-surface)", padding: "calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) + var(--control-icon) * 2 + 8px) calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) - 4px)", boxSizing: "border-box", display: "flex", alignItems: "center", flexWrap: "wrap", gap: 4, cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .5 : 1, position: "relative" }}>
18
+ {value.length === 0 && <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-muted)", paddingLeft: 4 }}>{placeholder}</span>}
17
19
  {shown.map(v => <Tag key={v} removable onRemove={() => toggle(v)}>{v}</Tag>)}
18
20
  {extra > 0 && <Tag>+{extra}</Tag>}
19
- <span style={{ position: "absolute", right: 8, top: "50%", transform: "translateY(-50%)", display: "flex", alignItems: "center", gap: 2, color: "var(--text-muted)" }}>
20
- {value.length > 0 && <button type="button" onClick={(e) => { e.stopPropagation(); onChange && onChange([]); }} style={{ border: "none", background: "transparent", cursor: "pointer", color: "inherit", display: "flex", padding: 0 }}><Icon name="x" size={13} /></button>}
21
- <span style={{ display: "inline-flex", transform: open ? "rotate(180deg)" : "none", transition: "transform var(--dur-fast) var(--ease-out)" }}><Icon name="chevron-down" size={14} /></span>
21
+ <span style={{ position: "absolute", right: "var(--control-px)", top: "50%", transform: "translateY(-50%)", display: "flex", alignItems: "center", gap: 4, color: "var(--text-muted)" }}>
22
+ {value.length > 0 && <button type="button" aria-label="Clear" data-ink="" onClick={(e) => { e.stopPropagation(); onChange && onChange([]); }} 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>}
23
+ <span style={{ display: "inline-flex", transform: open ? "rotate(180deg)" : "none", transition: "transform var(--dur-fast) var(--ease-out)" }}><Icon name="chevron-down" size={16} style={tierIcon} /></span>
22
24
  </span>
23
25
  </div>
24
26
  {open && <div style={{ position: "absolute", top: "calc(100% + 4px)", left: 0, right: 0, zIndex: "var(--z-menu)", background: "var(--bg-surface)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-md)", boxShadow: "var(--shadow-menu)", padding: 4, maxHeight: 208, overflowY: "auto" }}>
25
- {options.map(o => <div key={o} role="option" aria-selected={value.includes(o)} tabIndex={0} onClick={() => toggle(o)} style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 10px", borderRadius: "var(--radius-sm)", cursor: "pointer", background: "transparent" }}
27
+ {options.map(o => <div key={o} role="option" aria-selected={value.includes(o)} tabIndex={0} onClick={() => toggle(o)} style={{ display: "flex", alignItems: "center", gap: 8, height: "var(--control-h)", padding: "0 10px", boxSizing: "border-box", borderRadius: "var(--radius-sm)", cursor: "pointer", background: "transparent" }}
26
28
  onMouseEnter={(e) => e.currentTarget.style.background = "var(--bg-hover)"} onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}>
27
- <Checkbox size="sm" checked={value.includes(o)} onChange={() => toggle(o)} />
28
- <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: "var(--text-heading)" }}>{o}</span>
29
+ <Checkbox checked={value.includes(o)} onChange={() => toggle(o)} />
30
+ <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", letterSpacing: "var(--control-tracking)", color: "var(--text-heading)" }}>{o}</span>
29
31
  </div>)}
30
32
  </div>}
31
33
  </div>
@@ -3,8 +3,9 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
3
3
  import Icon from '../icons/Icon.vue'
4
4
  import Checkbox from './Checkbox.vue'
5
5
  import Tag from '../display/Tag.vue'
6
- // Multi-select dropdown: checkbox options; picks render as removable tags with "+n" overflow and clear-all ×.
7
- const props = defineProps({ options: { type: Array, default: () => [] }, modelValue: { type: Array, default: () => [] }, placeholder: { type: String, default: 'Select...' }, maxTags: { type: Number, default: 2 }, disabled: Boolean, width: [Number, String], style: Object })
6
+ // Multi-select dropdown: checkbox options; picks render as removable tags with "+n" overflow and clear-all ×. Field, tags and option rows take the region's tier (omit `size` to inherit).
7
+ const props = defineProps({ options: { type: Array, default: () => [] }, modelValue: { type: Array, default: () => [] }, placeholder: { type: String, default: 'Select...' }, maxTags: { type: Number, default: 2 }, disabled: Boolean, width: [Number, String], size: String, style: Object })
8
+ const tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }
8
9
  const emit = defineEmits(['update:modelValue'])
9
10
  const open = ref(false), root = ref(null)
10
11
  function toggle(o) { emit('update:modelValue', props.modelValue.includes(o) ? props.modelValue.filter(x => x !== o) : [...props.modelValue, o]) }
@@ -15,20 +16,20 @@ onMounted(() => document.addEventListener('mousedown', onDoc))
15
16
  onUnmounted(() => document.removeEventListener('mousedown', onDoc))
16
17
  </script>
17
18
  <template>
18
- <div ref="root" :style="{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }">
19
- <div @click="!disabled && (open = !open)" :style="{ minHeight: 'var(--h-input)', borderRadius: 'var(--radius-md)', border: `1px solid ${open ? 'var(--focus-border)' : 'var(--border-control)'}`, boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', background: 'var(--bg-surface)', padding: '5px 30px 5px 8px', boxSizing: 'border-box', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, position: 'relative' }">
20
- <span v-if="modelValue.length === 0" style="font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-muted);padding-left:4px">{{ placeholder }}</span>
19
+ <div ref="root" :data-size="size" :style="{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }">
20
+ <div :data-field="disabled ? undefined : ''" @click="!disabled && (open = !open)" :style="{ minHeight: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: `1px solid ${open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))'}`, boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', background: 'var(--bg-surface)', padding: 'calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) + var(--control-icon) * 2 + 8px) calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) - 4px)', boxSizing: 'border-box', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, position: 'relative' }">
21
+ <span v-if="modelValue.length === 0" 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);padding-left:4px">{{ placeholder }}</span>
21
22
  <Tag v-for="v in shown" :key="v" removable @remove="toggle(v)">{{ v }}</Tag>
22
23
  <Tag v-if="extra > 0">+{{ extra }}</Tag>
23
- <span style="position:absolute;right:8px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:2px;color:var(--text-muted)">
24
- <button v-if="modelValue.length" type="button" @click.stop="emit('update:modelValue', [])" style="border:none;background:transparent;cursor:pointer;color:inherit;display:flex;padding:0"><Icon name="x" :size="13" /></button>
25
- <span :style="{ display: 'inline-flex', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }"><Icon name="chevron-down" :size="14" /></span>
24
+ <span style="position:absolute;right:var(--control-px);top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:4px;color:var(--text-muted)">
25
+ <button v-if="modelValue.length" type="button" aria-label="Clear" data-ink="" @click.stop="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>
26
+ <span :style="{ display: 'inline-flex', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }"><Icon name="chevron-down" :size="16" :style="tierIcon" /></span>
26
27
  </span>
27
28
  </div>
28
29
  <div v-if="open" style="position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:var(--z-menu);background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;max-height:208px;overflow-y:auto">
29
- <div v-for="o in options" :key="o" role="option" :aria-selected="modelValue.includes(o)" tabindex="0" @click="toggle(o)" class="kb-ms-row" style="display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:var(--radius-sm);cursor:pointer">
30
- <Checkbox size="sm" :checked="modelValue.includes(o)" @change="toggle(o)" />
31
- <span style="font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-heading)">{{ o }}</span>
30
+ <div v-for="o in options" :key="o" role="option" :aria-selected="modelValue.includes(o)" tabindex="0" @click="toggle(o)" class="kb-ms-row" style="display:flex;align-items:center;gap:8px;height:var(--control-h);padding:0 10px;box-sizing:border-box;border-radius:var(--radius-sm);cursor:pointer">
31
+ <Checkbox :checked="modelValue.includes(o)" @change="toggle(o)" />
32
+ <span style="font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:var(--control-tracking);color:var(--text-heading)">{{ o }}</span>
32
33
  </div>
33
34
  </div>
34
35
  </div>
@@ -1,6 +1,8 @@
1
1
  import * as React from "react";
2
- /** Segmented −/value/+ number stepper (25.5px, r4) — "Preview rows" control. */
2
+ /** Segmented −/value/+ number stepper at the region's tier height, r8 — "Preview rows" control. */
3
3
  export interface NumberStepperProps {
4
+ /** Size tier: sm 24 · md 32 · lg 36. Omit to inherit the enclosing region's tier (the default). */
5
+ size?: "sm" | "md" | "lg";
4
6
  value?: number;
5
7
  onChange?: (next: number) => void;
6
8
  step?: number;
@@ -1,15 +1,15 @@
1
1
  import React from "react";
2
2
  import { Icon } from "../icons/Icon.jsx";
3
3
 
4
- /** Segmented − / value / + stepper, 25.5px tall, r4 ("Preview rows"). Buttons disable themselves at min/max. */
5
- export function NumberStepper({ value = 100, onChange, step = 10, min = 0, max, disabled, style }) {
4
+ /** Segmented − / value / + stepper, r8, tier height ("Preview rows" — omit `size` to inherit the region's tier). Buttons disable themselves at min/max. */
5
+ export function NumberStepper({ value = 100, onChange, step = 10, min = 0, max, disabled, size, style }) {
6
6
  const atMin = value <= min, atMax = max != null && value >= max;
7
- const seg = (dis) => ({ width: 30, display: "flex", alignItems: "center", justifyContent: "center", cursor: dis ? "not-allowed" : "pointer", color: dis ? "var(--text-muted)" : "var(--text-body)", alignSelf: "stretch", background: "transparent", border: "none", padding: 0 });
7
+ const seg = (dis) => ({ width: "var(--control-h)", display: "flex", alignItems: "center", justifyContent: "center", cursor: dis ? "not-allowed" : "pointer", color: dis ? "var(--text-muted)" : "var(--text-body)", alignSelf: "stretch", background: dis ? "transparent" : "var(--wash, transparent)", transition: "var(--t-hover)", border: "none", padding: 0 });
8
8
  return (
9
- <div style={{ display: "inline-flex", alignItems: "center", height: 25.5, borderRadius: "var(--radius-sm)", border: "1px solid var(--border-control)", overflow: "hidden", background: "var(--bg-surface)", boxSizing: "border-box", opacity: disabled ? .5 : 1, ...style }}>
10
- <button type="button" disabled={disabled || atMin} style={seg(disabled || atMin)} onClick={() => onChange && onChange(Math.max(min, value - step))}><Icon name="minus" size={12} /></button>
11
- <span style={{ minWidth: 34, textAlign: "center", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: "var(--text-primary)", borderLeft: "1px solid var(--border-control)", borderRight: "1px solid var(--border-control)", alignSelf: "stretch", display: "inline-flex", alignItems: "center", justifyContent: "center", padding: "0 6px" }}>{value}</span>
12
- <button type="button" disabled={disabled || atMax} style={seg(disabled || atMax)} onClick={() => onChange && onChange(max != null ? Math.min(max, value + step) : value + step)}><Icon name="plus" size={12} /></button>
9
+ <div data-size={size} data-field={disabled ? undefined : ""} style={{ display: "inline-flex", alignItems: "center", height: "var(--control-h)", borderRadius: "var(--radius-md)", border: "1px solid var(--field-border, var(--border-control))", overflow: "hidden", background: "var(--bg-surface)", boxSizing: "border-box", opacity: disabled ? .5 : 1, ...style }}>
10
+ <button type="button" disabled={disabled || atMin} data-wash="" aria-label="Decrease" style={seg(disabled || atMin)} onClick={() => onChange && onChange(Math.max(min, value - step))}><Icon name="minus" size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)" }} /></button>
11
+ <span style={{ minWidth: 34, textAlign: "center", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", letterSpacing: "var(--control-tracking)", fontVariantNumeric: "tabular-nums", color: "var(--text-primary)", borderLeft: "1px solid var(--border-control)", borderRight: "1px solid var(--border-control)", alignSelf: "stretch", display: "inline-flex", alignItems: "center", justifyContent: "center", padding: "0 6px" }}>{value}</span>
12
+ <button type="button" disabled={disabled || atMax} data-wash="" aria-label="Increase" style={seg(disabled || atMax)} onClick={() => onChange && onChange(max != null ? Math.min(max, value + step) : value + step)}><Icon name="plus" size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)" }} /></button>
13
13
  </div>
14
14
  );
15
15
  }
@@ -1,17 +1,18 @@
1
1
  <script setup>
2
2
  import { computed } from 'vue'
3
3
  import Icon from '../icons/Icon.vue'
4
- // Segmented − / value / + stepper, 25.5px tall, r4 ("Preview rows"). Buttons disable themselves at min/max.
5
- const props = defineProps({ modelValue: { type: Number, default: 100 }, step: { type: Number, default: 10 }, min: { type: Number, default: 0 }, max: Number, disabled: Boolean, style: Object })
4
+ // Segmented − / value / + stepper, r8, tier height ("Preview rows" — omit `size` to inherit the region's tier). Buttons disable themselves at min/max.
5
+ const props = defineProps({ modelValue: { type: Number, default: 100 }, step: { type: Number, default: 10 }, min: { type: Number, default: 0 }, max: Number, disabled: Boolean, size: String, style: Object })
6
+ const tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }
6
7
  const emit = defineEmits(['update:modelValue'])
7
8
  const atMin = computed(() => props.modelValue <= props.min)
8
9
  const atMax = computed(() => props.max != null && props.modelValue >= props.max)
9
- function seg(dis) { return { width: 30, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: dis ? 'not-allowed' : 'pointer', color: dis ? 'var(--text-muted)' : 'var(--text-body)', alignSelf: 'stretch', background: 'transparent', border: 'none', padding: 0 } }
10
+ function seg(dis) { return { width: 'var(--control-h)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: dis ? 'not-allowed' : 'pointer', color: dis ? 'var(--text-muted)' : 'var(--text-body)', alignSelf: 'stretch', background: dis ? 'transparent' : 'var(--wash, transparent)', transition: 'var(--t-hover)', border: 'none', padding: 0 } }
10
11
  </script>
11
12
  <template>
12
- <div :style="{ display: 'inline-flex', alignItems: 'center', height: 25.5, borderRadius: 'var(--radius-sm)', border: '1px solid var(--border-control)', overflow: 'hidden', background: 'var(--bg-surface)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...style }">
13
- <button type="button" :disabled="disabled || atMin" :style="seg(disabled || atMin)" @click="emit('update:modelValue', Math.max(min, modelValue - step))"><Icon name="minus" :size="12" /></button>
14
- <span style="min-width:34px;text-align:center;font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-primary);border-left:1px solid var(--border-control);border-right:1px solid var(--border-control);align-self:stretch;display:inline-flex;align-items:center;justify-content:center;padding:0 6px">{{ modelValue }}</span>
15
- <button type="button" :disabled="disabled || atMax" :style="seg(disabled || atMax)" @click="emit('update:modelValue', max != null ? Math.min(max, modelValue + step) : modelValue + step)"><Icon name="plus" :size="12" /></button>
13
+ <div :data-size="size" :data-field="disabled ? undefined : ''" :style="{ display: 'inline-flex', alignItems: 'center', height: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: '1px solid var(--field-border, var(--border-control))', overflow: 'hidden', background: 'var(--bg-surface)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...style }">
14
+ <button type="button" :disabled="disabled || atMin" data-wash="" aria-label="Decrease" :style="seg(disabled || atMin)" @click="emit('update:modelValue', Math.max(min, modelValue - step))"><Icon name="minus" :size="16" :style="tierIcon" /></button>
15
+ <span style="min-width:34px;text-align:center;font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:var(--control-tracking);font-variant-numeric:tabular-nums;color:var(--text-primary);border-left:1px solid var(--border-control);border-right:1px solid var(--border-control);align-self:stretch;display:inline-flex;align-items:center;justify-content:center;padding:0 6px">{{ modelValue }}</span>
16
+ <button type="button" :disabled="disabled || atMax" data-wash="" aria-label="Increase" :style="seg(disabled || atMax)" @click="emit('update:modelValue', max != null ? Math.min(max, modelValue + step) : modelValue + step)"><Icon name="plus" :size="16" :style="tierIcon" /></button>
16
17
  </div>
17
18
  </template>
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+ /** Radio — circle at the region's tier (sm 14 · md/lg 16); filled action-blue dot when selected. Hover darkens the ring, focus shows the 2px ring. */
3
+ export interface RadioProps {
4
+ checked?: boolean;
5
+ onChange?: (checked: true) => void;
6
+ label?: string;
7
+ /** Muted second line under the label */
8
+ description?: string;
9
+ /** Size tier. Omit to inherit the enclosing region's tier (the default). */
10
+ size?: "sm" | "md" | "lg";
11
+ disabled?: boolean;
12
+ style?: React.CSSProperties;
13
+ }
14
+ export declare function Radio(props: RadioProps): JSX.Element;
15
+ /** Single choice from a small set. Arrow keys move between enabled options. */
16
+ export interface RadioGroupProps {
17
+ options?: Array<string | { label: string; value: string; description?: string; disabled?: boolean }>;
18
+ value?: string;
19
+ onChange?: (value: string) => void;
20
+ /** Horizontal layout */
21
+ row?: boolean;
22
+ /** Size tier for the whole group. Omit to inherit (the default). */
23
+ size?: "sm" | "md" | "lg";
24
+ /** plain (default) · cards — each option is a bordered selectable card */
25
+ variant?: "plain" | "cards";
26
+ /** Disables the whole group */
27
+ disabled?: boolean;
28
+ style?: React.CSSProperties;
29
+ }
30
+ export declare function RadioGroup(props: RadioGroupProps): JSX.Element;
@@ -0,0 +1,65 @@
1
+ import React from "react";
2
+ import { Icon } from "../icons/Icon.jsx";
3
+
4
+ /**
5
+ * Radio — circle at the region's tier (sm 14 · md/lg 16, --check-size), 1.5px border, filled action-blue dot when selected.
6
+ * hover darkens the ring; keyboard focus shows the 2px focus ring; disabled = 50% opacity.
7
+ * description renders a muted second line under the label (settings-style choices).
8
+ */
9
+ export function Radio({ checked, onChange, label, description, size, disabled, style }) {
10
+ const d = { box: "var(--check-size)", dot: "calc(var(--check-size) / 2)" };
11
+ const [hover, setHover] = React.useState(false);
12
+ const [focus, setFocus] = React.useState(false);
13
+ const ring = checked ? "var(--action-primary)" : hover && !disabled ? "var(--text-muted)" : "var(--checkbox-border)";
14
+ return (
15
+ <label role="radio" data-size={size} aria-checked={!!checked} aria-disabled={disabled || undefined} tabIndex={disabled ? -1 : 0}
16
+ onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
17
+ onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") { e.preventDefault(); !disabled && onChange && onChange(true); } }}
18
+ style={{ display: "inline-flex", alignItems: description ? "flex-start" : "center", verticalAlign: "middle", lineHeight: "var(--control-leading)", gap: "var(--control-gap)", cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .5 : 1, outline: "none", ...style }}
19
+ onClick={() => !disabled && onChange && onChange(true)}>
20
+ <span style={{ width: d.box, height: d.box, marginTop: description ? 2 : 0, borderRadius: "var(--radius-full)", boxSizing: "border-box", border: `1.5px solid ${ring}`, background: "var(--bg-surface)", boxShadow: focus && !disabled ? "0 0 0 2px var(--focus-ring)" : "none", display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0, transition: "var(--t-hover)" }}>
21
+ {checked ? <span style={{ width: d.dot, height: d.dot, borderRadius: "var(--radius-full)", background: "var(--action-primary)" }}></span> : null}
22
+ </span>
23
+ {label || description ? <span style={{ display: "flex", flexDirection: "column", gap: 2 }}>
24
+ {label ? <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-regular)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-body)" }}>{label}</span> : null}
25
+ {description ? <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: "var(--text-muted)" }}>{description}</span> : null}
26
+ </span> : null}
27
+ </label>
28
+ );
29
+ }
30
+
31
+ /**
32
+ * RadioGroup — single choice from a small set. Vertical (default) or `row`.
33
+ * options: string[] | { label, value, description?, disabled? }[]
34
+ * Arrow keys move the selection between enabled options (WAI-ARIA radiogroup).
35
+ * variant="cards" renders each option as a bordered selectable card (plan pickers, migration strategies).
36
+ */
37
+ export function RadioGroup({ options = [], value, onChange, row, size, variant = "plain", disabled, style }) {
38
+ const opts = options.map((o) => (typeof o === "string" ? { label: o, value: o } : o));
39
+ const enabled = opts.filter((o) => !o.disabled && !disabled);
40
+ const onKey = (e) => {
41
+ if (!["ArrowDown", "ArrowRight", "ArrowUp", "ArrowLeft"].includes(e.key) || !enabled.length) return;
42
+ e.preventDefault();
43
+ const i = enabled.findIndex((o) => o.value === value);
44
+ const next = e.key === "ArrowDown" || e.key === "ArrowRight" ? enabled[(i + 1) % enabled.length] : enabled[(i - 1 + enabled.length) % enabled.length];
45
+ onChange && onChange(next.value);
46
+ };
47
+ if (variant === "cards") {
48
+ return (
49
+ <div role="radiogroup" data-size={size} onKeyDown={onKey} style={{ display: "flex", flexDirection: row ? "row" : "column", gap: 8, ...style }}>
50
+ {opts.map((o) => {
51
+ const on = value === o.value, off = o.disabled || disabled;
52
+ return <div key={o.value} onClick={() => !off && onChange && onChange(o.value)}
53
+ style={{ flex: row ? 1 : "none", padding: "10px 12px", borderRadius: "var(--radius-md)", border: `1px solid ${on ? "var(--action-primary)" : "var(--border-card)"}`, background: on ? "var(--soft-bg)" : "var(--bg-surface)", cursor: off ? "not-allowed" : "pointer", opacity: off ? .5 : 1, transition: "var(--t-hover)" }}>
54
+ <Radio checked={on} onChange={() => onChange && onChange(o.value)} label={o.label} description={o.description} disabled={off} style={{ opacity: 1, cursor: "inherit" }} />
55
+ </div>;
56
+ })}
57
+ </div>
58
+ );
59
+ }
60
+ return (
61
+ <div role="radiogroup" data-size={size} onKeyDown={onKey} style={{ display: "flex", flexDirection: row ? "row" : "column", gap: row ? "var(--group-gap)" : 10, ...style }}>
62
+ {opts.map((o) => <Radio key={o.value} label={o.label} description={o.description} disabled={o.disabled || disabled} checked={value === o.value} onChange={() => onChange && onChange(o.value)} />)}
63
+ </div>
64
+ );
65
+ }
@@ -1,16 +1,24 @@
1
1
  <script setup>
2
- import { computed } from 'vue'
3
- // Radio — 16px (sm: 13px) circle, 1.5px border, filled dot when selected.
4
- const dims = { sm: { box: 13, dot: 6 }, md: { box: 16, dot: 8 } }
5
- const props = defineProps({ checked: Boolean, label: String, size: { type: String, default: 'md' }, disabled: Boolean, style: Object })
2
+ import { ref, computed } from 'vue'
3
+ // Radio — circle at the region's tier (sm 14 · md/lg 16, --check-size), 1.5px border, filled dot when selected. Hover darkens the ring, focus shows the 2px ring, disabled = 50% opacity.
4
+ // description renders a muted second line under the label.
5
+ const props = defineProps({ checked: Boolean, label: String, description: String, size: String, disabled: Boolean, style: Object })
6
6
  const emit = defineEmits(['change'])
7
- const d = computed(() => dims[props.size] || dims.md)
7
+ const d = { box: 'var(--check-size)', dot: 'calc(var(--check-size) / 2)' }
8
+ const hover = ref(false), focus = ref(false)
9
+ const ring = computed(() => props.checked ? 'var(--action-primary)' : hover.value && !props.disabled ? 'var(--text-muted)' : 'var(--checkbox-border)')
10
+ const pick = () => { if (!props.disabled) emit('change', true) }
8
11
  </script>
9
12
  <template>
10
- <label :style="{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--leading-sm)', gap: 6, cursor: disabled ? 'default' : 'pointer', opacity: disabled ? .5 : 1, ...style }" @click="!disabled && emit('change', true)">
11
- <span :style="{ width: d.box + 'px', height: d.box + 'px', 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)' }">
12
- <span v-if="checked" :style="{ width: d.dot + 'px', height: d.dot + 'px', borderRadius: 'var(--radius-full)', background: 'var(--action-primary)' }" />
13
+ <label role="radio" :data-size="size" :aria-checked="checked" :aria-disabled="disabled || undefined" :tabindex="disabled ? -1 : 0"
14
+ @mouseenter="hover = true" @mouseleave="hover = false" @focus="focus = true" @blur="focus = false" @click="pick" @keydown.space.prevent="pick" @keydown.enter.prevent="pick"
15
+ :style="{ display: 'inline-flex', alignItems: description ? 'flex-start' : 'center', verticalAlign: 'middle', lineHeight: 'var(--control-leading)', gap: 'var(--control-gap)', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, outline: 'none', ...style }">
16
+ <span :style="{ width: d.box, height: d.box, marginTop: description ? '2px' : 0, borderRadius: 'var(--radius-full)', boxSizing: 'border-box', border: `1.5px solid ${ring}`, background: 'var(--bg-surface)', boxShadow: focus && !disabled ? '0 0 0 2px var(--focus-ring)' : 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, transition: 'var(--t-hover)' }">
17
+ <span v-if="checked" :style="{ width: d.dot, height: d.dot, borderRadius: 'var(--radius-full)', background: 'var(--action-primary)' }"></span>
18
+ </span>
19
+ <span v-if="label || description" style="display:flex;flex-direction:column;gap:2px">
20
+ <span v-if="label" style="font-family:var(--font-sans);font-weight:var(--weight-regular);font-size:var(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-body)">{{ label }}</span>
21
+ <span v-if="description" style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-muted)">{{ description }}</span>
13
22
  </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
23
  </label>
16
24
  </template>
@@ -1,11 +1,29 @@
1
1
  <script setup>
2
+ import { computed } from 'vue'
2
3
  import Radio from './Radio.vue'
3
- // Vertical or horizontal group of Radios.
4
- defineProps({ options: { type: Array, default: () => [] }, modelValue: String, row: Boolean, size: { type: String, default: 'md' }, style: Object })
4
+ // RadioGroup — single choice. Vertical (default) or `row`. options: string[] | { label, value, description?, disabled? }[].
5
+ // Arrow keys move between enabled options. variant="cards" renders bordered selectable cards. v-model.
6
+ const props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, row: Boolean, size: String, variant: { type: String, default: 'plain' }, disabled: Boolean, style: Object })
5
7
  const emit = defineEmits(['update:modelValue'])
8
+ const opts = computed(() => props.options.map(o => typeof o === 'string' ? { label: o, value: o } : o))
9
+ const enabled = computed(() => opts.value.filter(o => !o.disabled && !props.disabled))
10
+ function onKey(e) {
11
+ if (!['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft'].includes(e.key) || !enabled.value.length) return
12
+ e.preventDefault()
13
+ const i = enabled.value.findIndex(o => o.value === props.modelValue), n = enabled.value.length
14
+ const next = (e.key === 'ArrowDown' || e.key === 'ArrowRight') ? enabled.value[(i + 1) % n] : enabled.value[(i - 1 + n) % n]
15
+ emit('update:modelValue', next.value)
16
+ }
17
+ const pick = (o) => { if (!(o.disabled || props.disabled)) emit('update:modelValue', o.value) }
6
18
  </script>
7
19
  <template>
8
- <div :style="{ display: 'flex', flexDirection: row ? 'row' : 'column', gap: (row ? 16 : 10) + 'px', ...style }">
9
- <Radio v-for="o in options" :key="o" :label="o" :size="size" :checked="modelValue === o" @change="emit('update:modelValue', o)" />
20
+ <div v-if="variant === 'cards'" role="radiogroup" :data-size="size" @keydown="onKey" :style="{ display: 'flex', flexDirection: row ? 'row' : 'column', gap: '8px', ...style }">
21
+ <div v-for="o in opts" :key="o.value" @click="pick(o)"
22
+ :style="{ flex: row ? 1 : 'none', padding: '10px 12px', borderRadius: 'var(--radius-md)', border: `1px solid ${modelValue === o.value ? 'var(--action-primary)' : 'var(--border-card)'}`, background: modelValue === o.value ? 'var(--soft-bg)' : 'var(--bg-surface)', cursor: (o.disabled || disabled) ? 'not-allowed' : 'pointer', opacity: (o.disabled || disabled) ? .5 : 1, transition: 'var(--t-hover)' }">
23
+ <Radio :checked="modelValue === o.value" :label="o.label" :description="o.description" :disabled="o.disabled || disabled" :style="{ opacity: 1, cursor: 'inherit' }" @change="pick(o)" />
24
+ </div>
25
+ </div>
26
+ <div v-else role="radiogroup" :data-size="size" @keydown="onKey" :style="{ display: 'flex', flexDirection: row ? 'row' : 'column', gap: row ? 'var(--group-gap)' : '10px', ...style }">
27
+ <Radio v-for="o in opts" :key="o.value" :label="o.label" :description="o.description" :disabled="o.disabled || disabled" :checked="modelValue === o.value" @change="pick(o)" />
10
28
  </div>
11
29
  </template>
@@ -1,8 +1,10 @@
1
1
  import * as React from "react";
2
- /** Search field. Default: r10 hairline card filter. bare: borderless 13px top-bar flavor. Shows a clear (×) button once there's a value; uncontrolled if `value` is omitted. */
2
+ /** Search field at the region's tier height. Default: r8 hairline card filter. bare: borderless top-bar flavor. Shows a clear (×) button once there's a value; uncontrolled if `value` is omitted. */
3
3
  export interface SearchInputProps {
4
4
  placeholder?: string;
5
5
  bare?: boolean;
6
+ /** Size tier: sm 24 · md 32 · lg 36. Omit to inherit the enclosing region's tier (the default). */
7
+ size?: "sm" | "md" | "lg";
6
8
  value?: string;
7
9
  /** Receives the new string value (consistent with Select / Combobox / MultiSelect) */
8
10
  onChange?: (value: string) => void;
@@ -1,17 +1,17 @@
1
1
  import React from "react";
2
2
  import { Icon } from "../icons/Icon.jsx";
3
3
 
4
- /** 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. */
5
- export function SearchInput({ placeholder = "Search...", bare, value, onChange, disabled, style }) {
4
+ /** 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. */
5
+ export function SearchInput({ placeholder = "Search...", bare, size, value, onChange, disabled, style }) {
6
6
  const [inner, setInner] = React.useState("");
7
7
  const val = value != null ? value : inner;
8
8
  const set = (v) => { if (value == null) setInner(v); onChange && onChange(v); };
9
9
  return (
10
- <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 }}>
11
- <span style={{ color: "var(--text-muted)" }}><Icon name="search" size={bare ? 16 : 14} /></span>
10
+ <div data-size={size} data-field={bare || disabled ? undefined : ""} style={{ display: "flex", alignItems: "center", gap: bare ? 12 : 8, 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 }}>
11
+ <span style={{ color: "var(--text-muted)", display: "inline-flex" }}><Icon name="search" size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)" }} /></span>
12
12
  <input value={val} disabled={disabled} onChange={(e) => set(e.target.value)} placeholder={placeholder}
13
- 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" }} />
14
- {val && !disabled ? <button type="button" onClick={() => set("")} style={{ border: "none", background: "transparent", cursor: "pointer", color: "var(--text-muted)", display: "flex", padding: 0 }}><Icon name="x" size={13} /></button> : null}
13
+ 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" }} />
14
+ {val && !disabled ? <button type="button" onClick={() => set("")} aria-label="Clear" data-ink="" style={{ border: "none", background: "transparent", cursor: "pointer", color: "var(--ink, var(--text-muted))", display: "flex", padding: 0 }}><Icon name="x" size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)" }} /></button> : null}
15
15
  </div>
16
16
  );
17
17
  }