@keyfunc/keybricks-ds 1.0.4 → 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 (357) hide show
  1. package/DEPLOY.md +4 -4
  2. package/DESIGN.md +154 -53
  3. package/INTEGRATION.md +174 -35
  4. package/SKILL.md +6 -4
  5. package/_adherence.oxlintrc.json +164 -68
  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 +2 -2
  80. package/components/_cards/search.card.html +5 -2
  81. package/components/_cards/segmented-control.card.html +2 -2
  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 +3 -3
  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/_vue/index.mjs +9 -5
  106. package/components/buttons/ActionChip.d.ts +3 -1
  107. package/components/buttons/ActionChip.jsx +4 -4
  108. package/components/buttons/ActionChip.vue +4 -4
  109. package/components/buttons/Button.d.ts +5 -2
  110. package/components/buttons/Button.jsx +15 -18
  111. package/components/buttons/Button.vue +17 -18
  112. package/components/buttons/ButtonGroup.vue +1 -1
  113. package/components/buttons/IconButton.d.ts +2 -1
  114. package/components/buttons/IconButton.jsx +9 -7
  115. package/components/buttons/IconButton.vue +10 -8
  116. package/components/buttons/buttons.prompt.md +6 -5
  117. package/components/charts/Bar3D.jsx +2 -2
  118. package/components/charts/Bar3D.vue +2 -2
  119. package/components/charts/BarChart.jsx +4 -4
  120. package/components/charts/BarChart.vue +4 -4
  121. package/components/charts/BarList.jsx +1 -1
  122. package/components/charts/BarList.vue +1 -1
  123. package/components/charts/BoxPlot.jsx +1 -1
  124. package/components/charts/BoxPlot.vue +2 -2
  125. package/components/charts/BulletChart.jsx +1 -1
  126. package/components/charts/BulletChart.vue +1 -1
  127. package/components/charts/CandlestickChart.jsx +2 -2
  128. package/components/charts/CandlestickChart.vue +3 -3
  129. package/components/charts/ChartKit.jsx +5 -5
  130. package/components/charts/ChartLegend.vue +16 -0
  131. package/components/charts/ChartTooltip.vue +16 -0
  132. package/components/charts/ChordChart.jsx +2 -2
  133. package/components/charts/ChordChart.vue +2 -2
  134. package/components/charts/GanttChart.jsx +2 -2
  135. package/components/charts/GanttChart.vue +2 -2
  136. package/components/charts/GaugeChart.jsx +2 -2
  137. package/components/charts/GaugeChart.vue +2 -2
  138. package/components/charts/HeatmapChart.jsx +2 -2
  139. package/components/charts/HeatmapChart.vue +2 -2
  140. package/components/charts/Histogram.jsx +2 -2
  141. package/components/charts/Histogram.vue +3 -3
  142. package/components/charts/LineChart.jsx +1 -1
  143. package/components/charts/LineChart.vue +3 -3
  144. package/components/charts/ParetoChart.jsx +3 -3
  145. package/components/charts/ParetoChart.vue +4 -4
  146. package/components/charts/PlotGrid.vue +18 -0
  147. package/components/charts/RadarChart.jsx +1 -1
  148. package/components/charts/RadarChart.vue +1 -1
  149. package/components/charts/Ribbon3D.jsx +2 -2
  150. package/components/charts/Ribbon3D.vue +2 -2
  151. package/components/charts/SankeyChart.jsx +2 -2
  152. package/components/charts/SankeyChart.vue +2 -2
  153. package/components/charts/Scatter3D.jsx +1 -1
  154. package/components/charts/Scatter3D.vue +1 -1
  155. package/components/charts/ScatterChart.jsx +3 -3
  156. package/components/charts/ScatterChart.vue +4 -4
  157. package/components/charts/Sparkline.jsx +1 -1
  158. package/components/charts/Sparkline.vue +1 -1
  159. package/components/charts/SunburstChart.jsx +1 -1
  160. package/components/charts/SunburstChart.vue +1 -1
  161. package/components/charts/Surface3D.jsx +2 -2
  162. package/components/charts/Surface3D.vue +2 -2
  163. package/components/charts/TreemapChart.jsx +2 -2
  164. package/components/charts/TreemapChart.vue +2 -2
  165. package/components/charts/WaterfallChart.jsx +2 -2
  166. package/components/charts/WaterfallChart.vue +3 -3
  167. package/components/charts/WorldMap.jsx +2 -2
  168. package/components/charts/WorldMap.vue +2 -2
  169. package/components/charts/XLabels.vue +11 -0
  170. package/components/data/CodeBlock.jsx +1 -1
  171. package/components/data/CodeBlock.vue +1 -1
  172. package/components/data/DiffValue.jsx +1 -1
  173. package/components/data/DiffValue.vue +1 -1
  174. package/components/data/FilterBar.d.ts +2 -0
  175. package/components/data/FilterBar.jsx +10 -9
  176. package/components/data/FilterBar.vue +9 -8
  177. package/components/data/KeyValue.jsx +3 -3
  178. package/components/data/KeyValue.vue +3 -3
  179. package/components/data/ListRow.jsx +2 -2
  180. package/components/data/ListRow.vue +2 -2
  181. package/components/data/Pagination.d.ts +5 -1
  182. package/components/data/Pagination.jsx +16 -12
  183. package/components/data/Pagination.vue +14 -9
  184. package/components/data/SmartTable.d.ts +4 -1
  185. package/components/data/SmartTable.jsx +16 -14
  186. package/components/data/SmartTable.vue +25 -22
  187. package/components/data/Spreadsheet.d.ts +2 -0
  188. package/components/data/Spreadsheet.jsx +12 -11
  189. package/components/data/Spreadsheet.vue +14 -13
  190. package/components/data/Table.d.ts +47 -0
  191. package/components/data/Table.jsx +85 -0
  192. package/components/data/Table.vue +63 -0
  193. package/components/data/data.prompt.md +3 -3
  194. package/components/display/Avatar.vue +3 -2
  195. package/components/display/AvatarGroup.vue +3 -2
  196. package/components/display/Badge.vue +1 -1
  197. package/components/display/Divider.vue +3 -3
  198. package/components/display/Donut.vue +3 -2
  199. package/components/display/IconTile.vue +2 -1
  200. package/components/display/Kbd.vue +1 -1
  201. package/components/display/ProgressBar.jsx +1 -1
  202. package/components/display/ProgressBar.vue +1 -1
  203. package/components/display/StatCard.vue +1 -1
  204. package/components/display/Tag.d.ts +3 -1
  205. package/components/display/Tag.jsx +4 -4
  206. package/components/display/Tag.vue +4 -4
  207. package/components/feedback/Accordion.jsx +2 -2
  208. package/components/feedback/Accordion.vue +2 -2
  209. package/components/feedback/ActivityItem.d.ts +3 -1
  210. package/components/feedback/ActivityItem.jsx +5 -5
  211. package/components/feedback/ActivityItem.vue +5 -4
  212. package/components/feedback/Alert.jsx +4 -4
  213. package/components/feedback/Alert.vue +3 -3
  214. package/components/feedback/Banner.jsx +5 -5
  215. package/components/feedback/Banner.vue +5 -5
  216. package/components/feedback/ContextMenu.d.ts +3 -1
  217. package/components/feedback/ContextMenu.jsx +5 -5
  218. package/components/feedback/ContextMenu.vue +5 -5
  219. package/components/feedback/DotActivity.vue +3 -3
  220. package/components/feedback/Drawer.d.ts +2 -0
  221. package/components/feedback/Drawer.jsx +6 -6
  222. package/components/feedback/Drawer.vue +22 -15
  223. package/components/feedback/EmptyState.vue +2 -2
  224. package/components/feedback/LogViewer.vue +1 -1
  225. package/components/feedback/Modal.d.ts +2 -0
  226. package/components/feedback/Modal.jsx +12 -11
  227. package/components/feedback/Modal.vue +10 -8
  228. package/components/feedback/Skeleton.vue +2 -1
  229. package/components/feedback/Spinner.vue +2 -1
  230. package/components/feedback/Stepper.vue +1 -1
  231. package/components/feedback/Timeline.jsx +1 -1
  232. package/components/feedback/Timeline.vue +1 -1
  233. package/components/feedback/Toast.vue +4 -4
  234. package/components/feedback/ToastStack.vue +11 -0
  235. package/components/feedback/Tooltip.vue +19 -0
  236. package/components/feedback/feedback.prompt.md +4 -4
  237. package/components/forms/Checkbox.d.ts +3 -2
  238. package/components/forms/Checkbox.jsx +7 -7
  239. package/components/forms/Checkbox.vue +8 -8
  240. package/components/forms/Combobox.d.ts +2 -0
  241. package/components/forms/Combobox.jsx +12 -10
  242. package/components/forms/Combobox.vue +11 -10
  243. package/components/forms/DatePicker.d.ts +4 -0
  244. package/components/forms/DatePicker.jsx +16 -12
  245. package/components/forms/DatePicker.vue +15 -11
  246. package/components/forms/Dropzone.jsx +2 -2
  247. package/components/forms/Dropzone.vue +5 -5
  248. package/components/forms/Field.d.ts +5 -1
  249. package/components/forms/Field.jsx +4 -4
  250. package/components/forms/Field.vue +4 -4
  251. package/components/forms/Input.d.ts +5 -1
  252. package/components/forms/Input.jsx +7 -7
  253. package/components/forms/Input.vue +3 -3
  254. package/components/forms/MultiSelect.d.ts +2 -0
  255. package/components/forms/MultiSelect.jsx +13 -11
  256. package/components/forms/MultiSelect.vue +12 -11
  257. package/components/forms/NumberStepper.d.ts +3 -1
  258. package/components/forms/NumberStepper.jsx +7 -7
  259. package/components/forms/NumberStepper.vue +8 -7
  260. package/components/forms/Radio.d.ts +5 -3
  261. package/components/forms/Radio.jsx +13 -15
  262. package/components/forms/Radio.vue +9 -10
  263. package/components/forms/RadioGroup.vue +5 -5
  264. package/components/forms/SearchInput.d.ts +3 -1
  265. package/components/forms/SearchInput.jsx +6 -6
  266. package/components/forms/SearchInput.vue +7 -6
  267. package/components/forms/Select.d.ts +3 -2
  268. package/components/forms/Select.jsx +6 -7
  269. package/components/forms/Select.vue +6 -6
  270. package/components/forms/Slider.jsx +3 -3
  271. package/components/forms/Slider.vue +3 -3
  272. package/components/forms/Switch.d.ts +2 -2
  273. package/components/forms/Switch.jsx +20 -22
  274. package/components/forms/Switch.vue +15 -14
  275. package/components/forms/Textarea.vue +4 -4
  276. package/components/forms/forms.prompt.md +1 -1
  277. package/components/forms/input-base.js +2 -1
  278. package/components/index.d.ts +1 -2
  279. package/components/index.js +3 -4
  280. package/components/navigation/Breadcrumb.jsx +3 -3
  281. package/components/navigation/Breadcrumb.vue +4 -4
  282. package/components/navigation/DocTree.jsx +3 -3
  283. package/components/navigation/DocTree.vue +3 -3
  284. package/components/navigation/PageHeading.vue +1 -1
  285. package/components/navigation/PageToolbar.vue +7 -6
  286. package/components/navigation/Sidebar.jsx +1 -1
  287. package/components/navigation/Sidebar.vue +3 -3
  288. package/components/navigation/ThemeToggle.vue +2 -1
  289. package/components/navigation/TopBar.d.ts +3 -1
  290. package/components/navigation/TopBar.jsx +6 -6
  291. package/components/navigation/Tree.jsx +5 -5
  292. package/components/navigation/Tree.vue +5 -5
  293. package/components/pipeline/ConfigPanel.d.ts +2 -0
  294. package/components/pipeline/ConfigPanel.jsx +9 -9
  295. package/components/pipeline/ConfigPanel.vue +9 -9
  296. package/components/pipeline/DataPreview.d.ts +3 -1
  297. package/components/pipeline/DataPreview.jsx +7 -7
  298. package/components/pipeline/DataPreview.vue +7 -7
  299. package/components/pipeline/NodeCard.jsx +3 -3
  300. package/components/pipeline/NodeCard.vue +3 -3
  301. package/components/pipeline/PaletteItem.vue +2 -2
  302. package/components/pipeline/PaletteListItem.vue +1 -1
  303. package/components/pipeline/PalettePanel.jsx +2 -2
  304. package/components/pipeline/PalettePanel.vue +1 -1
  305. package/components/pipeline/Port.vue +15 -0
  306. package/components/pipeline/PortLabel.vue +15 -0
  307. package/components/pipeline/ZoomControls.vue +1 -1
  308. package/components/pipeline/port-skin.js +13 -0
  309. package/components/px.js +4 -0
  310. package/components/tabs/PanelTabs.vue +5 -4
  311. package/components/tabs/PillTabs.vue +5 -4
  312. package/components/tabs/SegmentedControl.d.ts +2 -2
  313. package/components/tabs/SegmentedControl.jsx +7 -7
  314. package/components/tabs/SegmentedControl.vue +7 -7
  315. package/components/tabs/Tabs.d.ts +5 -3
  316. package/components/tabs/Tabs.jsx +15 -15
  317. package/components/tabs/Tabs.vue +5 -5
  318. package/components/tabs/ViewToggle.d.ts +3 -1
  319. package/components/tabs/ViewToggle.jsx +5 -5
  320. package/components/tabs/ViewToggle.vue +5 -5
  321. package/components/tabs/tabs.prompt.md +1 -1
  322. package/keybricks.css +4 -0
  323. package/package.json +6 -3
  324. package/readme.md +90 -122
  325. package/readme.zh-CN.md +120 -0
  326. package/tokens/charts.css +20 -17
  327. package/tokens/colors.css +2 -2
  328. package/tokens/dark.css +2 -2
  329. package/tokens/hover.css +32 -0
  330. package/tokens/interaction.css +3 -1
  331. package/tokens/layout.css +29 -7
  332. package/tokens/tokens.d.ts +36 -0
  333. package/tokens/tokens.js +143 -89
  334. package/tokens/tokens.json +240 -106
  335. package/tokens/typography.css +27 -14
  336. package/components/_cards/brand-visual.card.html +0 -43
  337. package/components/_cards/datatable.card.html +0 -48
  338. package/components/_shared/src-brand.js +0 -1
  339. package/components/brand/BrandVisual.d.ts +0 -17
  340. package/components/brand/BrandVisual.jsx +0 -29
  341. package/components/brand/BrandVisual.vue +0 -34
  342. package/components/brand/_dir.card.html +0 -13
  343. package/components/brand/brandMotifs.js +0 -210
  344. package/components/buttons/_dir.card.html +0 -13
  345. package/components/charts/ChartKit.vue +0 -47
  346. package/components/charts/_dir.card.html +0 -7
  347. package/components/data/DataTable.d.ts +0 -28
  348. package/components/data/DataTable.jsx +0 -32
  349. package/components/data/DataTable.vue +0 -25
  350. package/components/data/_dir.card.html +0 -13
  351. package/components/display/_dir.card.html +0 -13
  352. package/components/feedback/_dir.card.html +0 -13
  353. package/components/forms/_dir.card.html +0 -13
  354. package/components/icons/_dir.card.html +0 -13
  355. package/components/navigation/_dir.card.html +0 -13
  356. package/components/pipeline/_dir.card.html +0 -13
  357. package/components/tabs/_dir.card.html +0 -13
@@ -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>
@@ -1,12 +1,13 @@
1
1
  import * as React from "react";
2
- /** Radio — 16px (sm: 13px) circle; filled action-blue dot when selected. Hover darkens the ring, focus shows the 2px ring. */
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
3
  export interface RadioProps {
4
4
  checked?: boolean;
5
5
  onChange?: (checked: true) => void;
6
6
  label?: string;
7
7
  /** Muted second line under the label */
8
8
  description?: string;
9
- size?: "sm" | "md";
9
+ /** Size tier. Omit to inherit the enclosing region's tier (the default). */
10
+ size?: "sm" | "md" | "lg";
10
11
  disabled?: boolean;
11
12
  style?: React.CSSProperties;
12
13
  }
@@ -18,7 +19,8 @@ export interface RadioGroupProps {
18
19
  onChange?: (value: string) => void;
19
20
  /** Horizontal layout */
20
21
  row?: boolean;
21
- size?: "sm" | "md";
22
+ /** Size tier for the whole group. Omit to inherit (the default). */
23
+ size?: "sm" | "md" | "lg";
22
24
  /** plain (default) · cards — each option is a bordered selectable card */
23
25
  variant?: "plain" | "cards";
24
26
  /** Disables the whole group */
@@ -1,30 +1,28 @@
1
1
  import React from "react";
2
2
  import { Icon } from "../icons/Icon.jsx";
3
3
 
4
- const radioDims = { sm: { box: 13, dot: 6, font: "var(--text-xs)" }, md: { box: 16, dot: 8, font: "var(--text-sm)" } };
5
-
6
4
  /**
7
- * Radio — 16px (sm: 13px) circle, 1.5px border, filled action-blue dot when selected.
5
+ * Radio — circle at the region's tier (sm 14 · md/lg 16, --check-size), 1.5px border, filled action-blue dot when selected.
8
6
  * hover darkens the ring; keyboard focus shows the 2px focus ring; disabled = 50% opacity.
9
7
  * description renders a muted second line under the label (settings-style choices).
10
8
  */
11
- export function Radio({ checked, onChange, label, description, size = "md", disabled, style }) {
12
- const d = radioDims[size] || radioDims.md;
9
+ export function Radio({ checked, onChange, label, description, size, disabled, style }) {
10
+ const d = { box: "var(--check-size)", dot: "calc(var(--check-size) / 2)" };
13
11
  const [hover, setHover] = React.useState(false);
14
12
  const [focus, setFocus] = React.useState(false);
15
13
  const ring = checked ? "var(--action-primary)" : hover && !disabled ? "var(--text-muted)" : "var(--checkbox-border)";
16
14
  return (
17
- <label role="radio" aria-checked={!!checked} aria-disabled={disabled || undefined} tabIndex={disabled ? -1 : 0}
15
+ <label role="radio" data-size={size} aria-checked={!!checked} aria-disabled={disabled || undefined} tabIndex={disabled ? -1 : 0}
18
16
  onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
19
17
  onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") { e.preventDefault(); !disabled && onChange && onChange(true); } }}
20
- style={{ display: "inline-flex", alignItems: description ? "flex-start" : "center", verticalAlign: "middle", lineHeight: "var(--leading-sm)", gap: 8, cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .5 : 1, outline: "none", ...style }}
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 }}
21
19
  onClick={() => !disabled && onChange && onChange(true)}>
22
- <span style={{ width: d.box, height: d.box, marginTop: description ? 1 : 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)" }}>
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)" }}>
23
21
  {checked ? <span style={{ width: d.dot, height: d.dot, borderRadius: "var(--radius-full)", background: "var(--action-primary)" }}></span> : null}
24
22
  </span>
25
23
  {label || description ? <span style={{ display: "flex", flexDirection: "column", gap: 2 }}>
26
- {label ? <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: d.font, lineHeight: "var(--leading-sm)", color: "var(--text-body)" }}>{label}</span> : null}
27
- {description ? <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "16px", color: "var(--text-muted)" }}>{description}</span> : null}
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}
28
26
  </span> : null}
29
27
  </label>
30
28
  );
@@ -36,7 +34,7 @@ export function Radio({ checked, onChange, label, description, size = "md", disa
36
34
  * Arrow keys move the selection between enabled options (WAI-ARIA radiogroup).
37
35
  * variant="cards" renders each option as a bordered selectable card (plan pickers, migration strategies).
38
36
  */
39
- export function RadioGroup({ options = [], value, onChange, row, size = "md", variant = "plain", disabled, style }) {
37
+ export function RadioGroup({ options = [], value, onChange, row, size, variant = "plain", disabled, style }) {
40
38
  const opts = options.map((o) => (typeof o === "string" ? { label: o, value: o } : o));
41
39
  const enabled = opts.filter((o) => !o.disabled && !disabled);
42
40
  const onKey = (e) => {
@@ -48,20 +46,20 @@ export function RadioGroup({ options = [], value, onChange, row, size = "md", va
48
46
  };
49
47
  if (variant === "cards") {
50
48
  return (
51
- <div role="radiogroup" onKeyDown={onKey} style={{ display: "flex", flexDirection: row ? "row" : "column", gap: 8, ...style }}>
49
+ <div role="radiogroup" data-size={size} onKeyDown={onKey} style={{ display: "flex", flexDirection: row ? "row" : "column", gap: 8, ...style }}>
52
50
  {opts.map((o) => {
53
51
  const on = value === o.value, off = o.disabled || disabled;
54
52
  return <div key={o.value} onClick={() => !off && onChange && onChange(o.value)}
55
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)" }}>
56
- <Radio checked={on} onChange={() => onChange && onChange(o.value)} label={o.label} description={o.description} size={size} disabled={off} style={{ opacity: 1, cursor: "inherit" }} />
54
+ <Radio checked={on} onChange={() => onChange && onChange(o.value)} label={o.label} description={o.description} disabled={off} style={{ opacity: 1, cursor: "inherit" }} />
57
55
  </div>;
58
56
  })}
59
57
  </div>
60
58
  );
61
59
  }
62
60
  return (
63
- <div role="radiogroup" onKeyDown={onKey} style={{ display: "flex", flexDirection: row ? "row" : "column", gap: row ? 16 : 10, ...style }}>
64
- {opts.map((o) => <Radio key={o.value} label={o.label} description={o.description} size={size} disabled={o.disabled || disabled} checked={value === o.value} onChange={() => onChange && onChange(o.value)} />)}
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)} />)}
65
63
  </div>
66
64
  );
67
65
  }
@@ -1,25 +1,24 @@
1
1
  <script setup>
2
2
  import { ref, computed } from 'vue'
3
- // Radio — 16px (sm: 13px) circle, 1.5px border, filled dot when selected. Hover darkens the ring, focus shows the 2px ring, disabled = 50% opacity.
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
4
  // description renders a muted second line under the label.
5
- const dims = { sm: { box: 13, dot: 6, font: 'var(--text-xs)' }, md: { box: 16, dot: 8, font: 'var(--text-sm)' } }
6
- const props = defineProps({ checked: Boolean, label: String, description: String, size: { type: String, default: 'md' }, disabled: Boolean, style: Object })
5
+ const props = defineProps({ checked: Boolean, label: String, description: String, size: String, disabled: Boolean, style: Object })
7
6
  const emit = defineEmits(['change'])
8
- const d = computed(() => dims[props.size] || dims.md)
7
+ const d = { box: 'var(--check-size)', dot: 'calc(var(--check-size) / 2)' }
9
8
  const hover = ref(false), focus = ref(false)
10
9
  const ring = computed(() => props.checked ? 'var(--action-primary)' : hover.value && !props.disabled ? 'var(--text-muted)' : 'var(--checkbox-border)')
11
10
  const pick = () => { if (!props.disabled) emit('change', true) }
12
11
  </script>
13
12
  <template>
14
- <label role="radio" :aria-checked="checked" :aria-disabled="disabled || undefined" :tabindex="disabled ? -1 : 0"
13
+ <label role="radio" :data-size="size" :aria-checked="checked" :aria-disabled="disabled || undefined" :tabindex="disabled ? -1 : 0"
15
14
  @mouseenter="hover = true" @mouseleave="hover = false" @focus="focus = true" @blur="focus = false" @click="pick" @keydown.space.prevent="pick" @keydown.enter.prevent="pick"
16
- :style="{ display: 'inline-flex', alignItems: description ? 'flex-start' : 'center', verticalAlign: 'middle', lineHeight: 'var(--leading-sm)', gap: '8px', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, outline: 'none', ...style }">
17
- <span :style="{ width: d.box + 'px', height: d.box + 'px', marginTop: description ? '1px' : 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)' }">
18
- <span v-if="checked" :style="{ width: d.dot + 'px', height: d.dot + 'px', borderRadius: 'var(--radius-full)', background: 'var(--action-primary)' }"></span>
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>
19
18
  </span>
20
19
  <span v-if="label || description" style="display:flex;flex-direction:column;gap:2px">
21
- <span v-if="label" :style="{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: d.font, lineHeight: 'var(--leading-sm)', color: 'var(--text-body)' }">{{ label }}</span>
22
- <span v-if="description" style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:16px;color:var(--text-muted)">{{ description }}</span>
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>
23
22
  </span>
24
23
  </label>
25
24
  </template>
@@ -3,7 +3,7 @@ import { computed } from 'vue'
3
3
  import Radio from './Radio.vue'
4
4
  // RadioGroup — single choice. Vertical (default) or `row`. options: string[] | { label, value, description?, disabled? }[].
5
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: { type: String, default: 'md' }, variant: { type: String, default: 'plain' }, disabled: Boolean, style: Object })
6
+ const props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, row: Boolean, size: String, variant: { type: String, default: 'plain' }, disabled: Boolean, style: Object })
7
7
  const emit = defineEmits(['update:modelValue'])
8
8
  const opts = computed(() => props.options.map(o => typeof o === 'string' ? { label: o, value: o } : o))
9
9
  const enabled = computed(() => opts.value.filter(o => !o.disabled && !props.disabled))
@@ -17,13 +17,13 @@ function onKey(e) {
17
17
  const pick = (o) => { if (!(o.disabled || props.disabled)) emit('update:modelValue', o.value) }
18
18
  </script>
19
19
  <template>
20
- <div v-if="variant === 'cards'" role="radiogroup" @keydown="onKey" :style="{ display: 'flex', flexDirection: row ? 'row' : 'column', gap: '8px', ...style }">
20
+ <div v-if="variant === 'cards'" role="radiogroup" :data-size="size" @keydown="onKey" :style="{ display: 'flex', flexDirection: row ? 'row' : 'column', gap: '8px', ...style }">
21
21
  <div v-for="o in opts" :key="o.value" @click="pick(o)"
22
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" :size="size" :disabled="o.disabled || disabled" :style="{ opacity: 1, cursor: 'inherit' }" @change="pick(o)" />
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
24
  </div>
25
25
  </div>
26
- <div v-else role="radiogroup" @keydown="onKey" :style="{ display: 'flex', flexDirection: row ? 'row' : 'column', gap: (row ? 16 : 10) + 'px', ...style }">
27
- <Radio v-for="o in opts" :key="o.value" :label="o.label" :description="o.description" :size="size" :disabled="o.disabled || disabled" :checked="modelValue === o.value" @change="pick(o)" />
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)" />
28
28
  </div>
29
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
  }
@@ -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>