@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
@@ -8,12 +8,12 @@ const emit = defineEmits(['close'])
8
8
  const t = computed(() => toastTones[props.tone])
9
9
  </script>
10
10
  <template>
11
- <div :style="{ display: 'flex', alignItems: 'flex-start', gap: 10, minWidth: 280, maxWidth: 380, background: 'var(--bg-raised)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-menu)', padding: '12px 12px 12px 14px', animation: 'kb-toast-in var(--dur) var(--ease-out)', ...style }">
12
- <span :style="{ color: t.color, display: 'inline-flex', marginTop: 1 }"><Icon :name="t.icon" :size="16" /></span>
11
+ <div :style="{ display: 'flex', alignItems: 'flex-start', gap: '10px', minWidth: '280px', maxWidth: '380px', background: 'var(--bg-raised)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-menu)', padding: '12px 12px 12px 14px', animation: 'kb-toast-in var(--dur) var(--ease-out)', ...style }">
12
+ <span :style="{ color: t.color, display: 'inline-flex', alignItems: 'center', flexShrink: 0, height: 'var(--leading-base)' }"><Icon :name="t.icon" :size="16" /></span>
13
13
  <div style="display:flex;flex-direction:column;flex:1;min-width:0">
14
14
  <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)">{{ title }}</span>
15
- <span v-if="meta" style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)">{{ meta }}</span>
15
+ <span v-if="meta" style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)">{{ meta }}</span>
16
16
  </div>
17
- <IconButton v-if="dismissible" icon="x" size="xs" muted label="Dismiss" @click="emit('close')" />
17
+ <IconButton v-if="dismissible" icon="x" size="sm" muted label="Dismiss" :style="{ margin: '-2px 0' }" @click="emit('close')" />
18
18
  </div>
19
19
  </template>
@@ -0,0 +1,11 @@
1
+ <script setup>
2
+ import Toast from './Toast.vue'
3
+ // Fixed bottom-right stack of toasts. toasts: [{ id, tone, title, meta }]. Emits dismiss(id).
4
+ defineProps({ toasts: { type: Array, default: () => [] }, style: Object })
5
+ const emit = defineEmits(['dismiss'])
6
+ </script>
7
+ <template>
8
+ <div :style="{ position: 'fixed', right: '24px', bottom: '24px', zIndex: 'var(--z-toast)', display: 'flex', flexDirection: 'column', gap: '8px', ...style }">
9
+ <Toast v-for="t in toasts" :key="t.id" :tone="t.tone" :title="t.title" :meta="t.meta" dismissible @close="emit('dismiss', t.id)" />
10
+ </div>
11
+ </template>
@@ -0,0 +1,19 @@
1
+ <script setup>
2
+ import { ref, computed } from 'vue'
3
+ // Hover tooltip around any inline content. placement: top | bottom | left | right. Dark pill, 11px, fades in.
4
+ const props = defineProps({ label: String, placement: { type: String, default: 'top' }, style: Object })
5
+ const show = ref(false)
6
+ const POS = {
7
+ top: { bottom: '100%', left: '50%', transform: 'translateX(-50%)', marginBottom: '6px' },
8
+ bottom: { top: '100%', left: '50%', transform: 'translateX(-50%)', marginTop: '6px' },
9
+ left: { right: '100%', top: '50%', transform: 'translateY(-50%)', marginRight: '6px' },
10
+ right: { left: '100%', top: '50%', transform: 'translateY(-50%)', marginLeft: '6px' }
11
+ }
12
+ const pos = computed(() => POS[props.placement] || POS.top)
13
+ </script>
14
+ <template>
15
+ <span :style="{ position: 'relative', display: 'inline-flex', ...style }" @mouseenter="show = true" @mouseleave="show = false">
16
+ <slot />
17
+ <span v-if="show" role="tooltip" :style="{ position: 'absolute', zIndex: 'var(--z-tooltip)', whiteSpace: 'nowrap', background: 'var(--tooltip-bg)', color: 'var(--tooltip-fg)', borderRadius: 'var(--radius-md)', padding: '5px 8px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: 'var(--tracking-xs)', boxShadow: 'var(--shadow-menu)', pointerEvents: 'none', animation: 'kb-fade-in var(--dur-fast) var(--ease-out)', ...pos }">{{ label }}</span>
18
+ </span>
19
+ </template>
@@ -12,17 +12,17 @@ Additions (not in the source file): `Tooltip`, `Modal`, `Toast`/`ToastStack`, `A
12
12
  ```jsx
13
13
  <Tooltip label="Run a test execution"><Button variant="ghost" icon="play">Run Test</Button></Tooltip>
14
14
  <Modal open={o} title="Delete node?" subtitle="This removes its 2 edges." onClose={close}
15
- footer={<><Button variant="outline" size="sm" onClick={close}>Cancel</Button><Button size="sm" style={{background:"var(--danger)"}}>Delete Node</Button></>}/>
15
+ footer={<><Button variant="outline" onClick={close}>Cancel</Button><Button variant="danger">Delete Node</Button></>}/> // Modal is an lg region: 36px buttons
16
16
  <ToastStack toasts={toasts} onDismiss={dismiss}/>
17
17
  <Alert tone="warning" title="4 configurations differ">Review before continuing.</Alert>
18
- <EmptyState icon="file-text" title="No documents yet" hint="..." action={<Button icon="plus" size="sm">New Document</Button>}/>
19
- <Skeleton width={140} height={13}/> <Spinner/>
18
+ <EmptyState icon="file-text" title="No documents yet" hint="..." action={<Button icon="plus">New Document</Button>}/>
19
+ <Skeleton width={140} height={14}/> <Spinner/>
20
20
  ```
21
21
 
22
22
  `Menu` (addition) — trigger-anchored dropdown (vs. `ContextMenu`, which opens at a click point):
23
23
 
24
24
  ```jsx
25
- <Menu trigger={<Button variant="dark" icon="chevron-down" size="sm">Show</Button>}
25
+ <Menu trigger={<Button variant="dark" icon="chevron-down">Show</Button>}
26
26
  items={[{label:"Preview",icon:"eye"},{label:"Export",icon:"download"},{label:"Delete",icon:"trash-2",danger:true}]}/>
27
27
  ```
28
28
 
@@ -1,12 +1,13 @@
1
1
  import * as React from "react";
2
- /** Checkbox with optional 12px/500 label ("Debug Mode"). md 16px · sm 13px (tables). */
2
+ /** Checkbox with optional label ("Debug Mode"). Box sm 14 · md/lg 16; the label uses the tier's control text and is the click target. */
3
3
  export interface CheckboxProps {
4
4
  checked?: boolean;
5
5
  /** Dash glyph, same fill as checked — for "some of N selected" parent rows */
6
6
  indeterminate?: boolean;
7
7
  onChange?: (checked: boolean) => void;
8
8
  label?: string;
9
- size?: "md" | "sm";
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
  }
@@ -1,17 +1,17 @@
1
1
  import React from "react";
2
2
  import { Icon } from "../icons/Icon.jsx";
3
3
 
4
- /** 16px (sm: 13px) checkbox — 1.5px #d1d5db border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. */
5
- export function Checkbox({ checked, indeterminate, onChange, label, size = "md", disabled, style }) {
6
- const box = size === "sm" ? 13 : 16;
4
+ /** Checkbox — box at the region's tier (sm 14 · md/lg 16, tokens/layout.css --check-size), 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. The label is the click target and uses the tier's control text. */
5
+ export function Checkbox({ checked, indeterminate, onChange, label, size, disabled, style }) {
6
+ const box = "var(--check-size)", glyph = { width: "calc(var(--check-size) - 4px)", height: "calc(var(--check-size) - 4px)" };
7
7
  const filled = checked || indeterminate;
8
8
  return (
9
- <label style={{ display: "inline-flex", alignItems: "center", verticalAlign: "middle", lineHeight: "var(--leading-sm)", gap: 6, cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .5 : 1, ...style }}
9
+ <label data-size={size} data-check={disabled ? undefined : ""} style={{ display: "inline-flex", alignItems: "center", verticalAlign: "middle", lineHeight: "var(--control-leading)", gap: "var(--control-gap)", cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .5 : 1, ...style }}
10
10
  onClick={() => !disabled && onChange && onChange(!checked)}>
11
- <span style={{ width: box, height: box, borderRadius: 4, boxSizing: "border-box", border: filled ? "none" : "1.5px solid var(--checkbox-border)", background: filled ? "var(--action-primary)" : "var(--bg-surface)", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "var(--on-accent)", flexShrink: 0, transition: "var(--t-hover)" }}>
12
- {checked ? <Icon name="check" size={box - 4} strokeWidth={3} /> : indeterminate ? <Icon name="minus" size={box - 4} strokeWidth={3} /> : null}
11
+ <span style={{ width: box, height: box, borderRadius: 4, boxSizing: "border-box", border: filled ? "none" : "1.5px solid var(--check-border, var(--checkbox-border))", background: filled ? "var(--action-primary)" : "var(--bg-surface)", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "var(--on-accent)", flexShrink: 0, transition: "var(--t-hover)" }}>
12
+ {checked ? <Icon name="check" size={12} strokeWidth={3} style={glyph} /> : indeterminate ? <Icon name="minus" size={12} strokeWidth={3} style={glyph} /> : null}
13
13
  </span>
14
- {label ? <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", color: "var(--text-body)" }}>{label}</span> : null}
14
+ {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}
15
15
  </label>
16
16
  );
17
17
  }
@@ -1,18 +1,18 @@
1
1
  <script setup>
2
2
  import { computed } from 'vue'
3
3
  import Icon from '../icons/Icon.vue'
4
- // 16px (sm: 13px) checkbox — 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity.
5
- const props = defineProps({ checked: Boolean, indeterminate: Boolean, label: String, size: { type: String, default: 'md' }, disabled: Boolean, style: Object })
4
+ // Checkbox — box at the region's tier (sm 14 · md/lg 16, tokens/layout.css --check-size), 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. The label is the click target and uses the tier's control text.
5
+ const props = defineProps({ checked: Boolean, indeterminate: Boolean, label: String, size: String, disabled: Boolean, style: Object })
6
6
  const emit = defineEmits(['change'])
7
- const box = computed(() => props.size === 'sm' ? 13 : 16)
7
+ const box = 'var(--check-size)', glyph = { width: 'calc(var(--check-size) - 4px)', height: 'calc(var(--check-size) - 4px)' }
8
8
  const filled = computed(() => props.checked || props.indeterminate)
9
9
  </script>
10
10
  <template>
11
- <label :style="{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--leading-sm)', gap: 6, cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, ...style }" @click="!disabled && emit('change', !checked)">
12
- <span :style="{ width: box, height: box, borderRadius: 4, boxSizing: 'border-box', border: filled ? 'none' : '1.5px solid var(--checkbox-border)', background: filled ? 'var(--action-primary)' : 'var(--bg-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--on-accent)', flexShrink: 0, transition: 'var(--t-hover)' }">
13
- <Icon v-if="checked" name="check" :size="box - 4" :stroke-width="3" />
14
- <Icon v-else-if="indeterminate" name="minus" :size="box - 4" :stroke-width="3" />
11
+ <label :data-size="size" :data-check="disabled ? undefined : ''" :style="{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--control-leading)', gap: 'var(--control-gap)', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, ...style }" @click="!disabled && emit('change', !checked)">
12
+ <span :style="{ width: box, height: box, borderRadius: '4px', boxSizing: 'border-box', border: filled ? 'none' : '1.5px solid var(--check-border, var(--checkbox-border))', background: filled ? 'var(--action-primary)' : 'var(--bg-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--on-accent)', flexShrink: 0, transition: 'var(--t-hover)' }">
13
+ <Icon v-if="checked" name="check" :size="12" :stroke-width="3" :style="glyph" />
14
+ <Icon v-else-if="indeterminate" name="minus" :size="12" :stroke-width="3" :style="glyph" />
15
15
  </span>
16
- <span v-if="label" style="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>
16
+ <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>
17
17
  </label>
18
18
  </template>
@@ -8,6 +8,8 @@ export interface ComboboxProps {
8
8
  disabled?: boolean;
9
9
  error?: boolean;
10
10
  width?: number | string;
11
+ /** Size tier: sm 24 · md 32 · lg 36. Omit to inherit the enclosing region's tier (the default). */
12
+ size?: "sm" | "md" | "lg";
11
13
  onChange?: (value: string) => void;
12
14
  style?: React.CSSProperties;
13
15
  }
@@ -2,8 +2,10 @@ import React from "react";
2
2
  import { Icon } from "../icons/Icon.jsx";
3
3
  import { inputBase } from "./Input.jsx";
4
4
 
5
- /** Typeahead single-select: type to filter options, click or Enter to pick. Chevron toggles the list; shows "No results" when the filter matches nothing. */
6
- export function Combobox({ options = [], value = "", placeholder = "Select...", disabled, error, width, onChange, style }) {
5
+ const tierIcon = { width: "var(--control-icon)", height: "var(--control-icon)" };
6
+
7
+ /** Typeahead single-select: type to filter options, click or Enter to pick. Chevron toggles the list; shows "No results" when the filter matches nothing. Field and option rows take the region's tier height (omit `size` to inherit). */
8
+ export function Combobox({ options = [], value = "", placeholder = "Select...", disabled, error, width, size, onChange, style }) {
7
9
  const [open, setOpen] = React.useState(false);
8
10
  const [query, setQuery] = React.useState(null);
9
11
  const [hov, setHov] = React.useState(-1);
@@ -12,21 +14,21 @@ export function Combobox({ options = [], value = "", placeholder = "Select...",
12
14
  const shown = query == null ? options : options.filter(o => o.toLowerCase().includes(query.toLowerCase()));
13
15
  const pick = (o) => { onChange && onChange(o); setOpen(false); setQuery(null); };
14
16
  return (
15
- <div ref={root} style={{ position: "relative", width, ...style }}>
16
- <div style={{ position: "relative" }}>
17
+ <div ref={root} data-size={size} style={{ position: "relative", width, ...style }}>
18
+ <div data-field={disabled ? undefined : ""} style={{ position: "relative" }}>
17
19
  <input value={query != null ? query : value} placeholder={placeholder} disabled={disabled}
18
20
  onFocus={() => setOpen(true)} onChange={(e) => { setQuery(e.target.value); setOpen(true); setHov(0); }}
19
21
  onKeyDown={(e) => { if (e.key === "ArrowDown") setHov(Math.min(hov + 1, shown.length - 1)); if (e.key === "ArrowUp") setHov(Math.max(hov - 1, 0)); if (e.key === "Enter" && shown[hov]) pick(shown[hov]); if (e.key === "Escape") { setOpen(false); setQuery(null); } }}
20
- style={{ ...inputBase, paddingRight: 32, borderColor: error ? "var(--danger)" : open ? "var(--focus-border)" : "var(--border-control)", boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", opacity: disabled ? .5 : 1, cursor: disabled ? "not-allowed" : "text" }} />
21
- <button type="button" disabled={disabled} onClick={() => { setOpen(!open); setQuery(null); }} style={{ position: "absolute", right: 4, top: "50%", transform: "translateY(-50%)", width: 26, height: 26, border: "none", background: "transparent", cursor: "pointer", color: "var(--text-muted)", display: "flex", alignItems: "center", justifyContent: "center", padding: 0 }}>
22
- <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>
22
+ style={{ ...inputBase, paddingRight: "calc(var(--control-px) + var(--control-icon) + 6px)", borderColor: error ? "var(--danger)" : open ? "var(--focus-border)" : "var(--field-border, var(--border-control))", boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", opacity: disabled ? .5 : 1, cursor: disabled ? "not-allowed" : "text" }} />
23
+ <button type="button" disabled={disabled} onClick={() => { setOpen(!open); setQuery(null); }} style={{ position: "absolute", right: 1, top: "50%", transform: "translateY(-50%)", width: "calc(var(--control-h) - 2px)", height: "calc(var(--control-h) - 2px)", border: "none", background: "transparent", cursor: "pointer", color: "var(--text-muted)", display: "flex", alignItems: "center", justifyContent: "center", padding: 0 }}>
24
+ <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>
23
25
  </button>
24
26
  </div>
25
27
  {open && !disabled && <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" }}>
26
- {shown.length === 0 && <div style={{ padding: "8px 10px", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: "var(--text-muted)" }}>No results</div>}
28
+ {shown.length === 0 && <div style={{ height: "var(--control-h)", display: "flex", alignItems: "center", padding: "0 10px", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", letterSpacing: "var(--control-tracking)", color: "var(--text-muted)" }}>No results</div>}
27
29
  {shown.map((o, i) => <button key={o} type="button" onClick={() => pick(o)} onMouseEnter={() => setHov(i)}
28
- style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", padding: "7px 10px", borderRadius: "var(--radius-sm)", border: "none", cursor: "pointer", background: hov === i ? "var(--bg-hover)" : "transparent", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: "var(--text-heading)", textAlign: "left" }}>
29
- {o}{o === value && <span style={{ color: "var(--action-primary)", display: "inline-flex" }}><Icon name="check" size={13} /></span>}
30
+ style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", height: "var(--control-h)", padding: "0 10px", borderRadius: "var(--radius-sm)", border: "none", cursor: "pointer", background: hov === i ? "var(--bg-hover)" : "transparent", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", letterSpacing: "var(--control-tracking)", color: "var(--text-heading)", textAlign: "left" }}>
31
+ {o}{o === value && <span style={{ color: "var(--action-primary)", display: "inline-flex" }}><Icon name="check" size={16} style={tierIcon} /></span>}
30
32
  </button>)}
31
33
  </div>}
32
34
  </div>
@@ -2,8 +2,9 @@
2
2
  import { ref, computed, onMounted, onUnmounted } from 'vue'
3
3
  import Icon from '../icons/Icon.vue'
4
4
  import { inputBase } from './input-base.js'
5
- // Typeahead single-select: type to filter options, click or Enter to pick; "No results" empty row.
6
- const props = defineProps({ options: { type: Array, default: () => [] }, modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select...' }, disabled: Boolean, error: Boolean, width: [Number, String], style: Object })
5
+ // Typeahead single-select: type to filter options, click or Enter to pick; "No results" empty row. Field and option rows take the region's tier height (omit `size` to inherit).
6
+ const props = defineProps({ options: { type: Array, default: () => [] }, modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select...' }, disabled: Boolean, error: Boolean, width: [Number, String], size: String, style: Object })
7
+ const tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }
7
8
  const emit = defineEmits(['update:modelValue'])
8
9
  const open = ref(false), query = ref(null), hov = ref(-1), root = ref(null)
9
10
  const shown = computed(() => query.value == null ? props.options : props.options.filter(o => o.toLowerCase().includes(query.value.toLowerCase())))
@@ -13,22 +14,22 @@ onMounted(() => document.addEventListener('mousedown', onDoc))
13
14
  onUnmounted(() => document.removeEventListener('mousedown', onDoc))
14
15
  </script>
15
16
  <template>
16
- <div ref="root" :style="{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }">
17
- <div style="position:relative">
17
+ <div ref="root" :data-size="size" :style="{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }">
18
+ <div :data-field="disabled ? undefined : ''" style="position:relative">
18
19
  <input :value="query != null ? query : modelValue" :placeholder="placeholder" :disabled="disabled"
19
20
  @focus="open = true" @input="query = $event.target.value; open = true; hov = 0"
20
21
  @keydown.down="hov = Math.min(hov + 1, shown.length - 1)" @keydown.up="hov = Math.max(hov - 1, 0)"
21
22
  @keydown.enter="shown[hov] && pick(shown[hov])" @keydown.esc="open = false; query = null"
22
- :style="{ ...inputBase, paddingRight: '32px', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-border)' : 'var(--border-control)', boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', opacity: disabled ? .5 : 1 }" />
23
- <button type="button" :disabled="disabled" @click="open = !open; query = null" style="position:absolute;right:4px;top:50%;transform:translateY(-50%);width:26px;height:26px;border:none;background:transparent;cursor:pointer;color:var(--text-muted);display:flex;align-items:center;justify-content:center;padding:0">
24
- <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>
23
+ :style="{ ...inputBase, paddingRight: 'calc(var(--control-px) + var(--control-icon) + 6px)', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))', boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', opacity: disabled ? .5 : 1 }" />
24
+ <button type="button" :disabled="disabled" @click="open = !open; query = null" style="position:absolute;right:1px;top:50%;transform:translateY(-50%);width:calc(var(--control-h) - 2px);height:calc(var(--control-h) - 2px);border:none;background:transparent;cursor:pointer;color:var(--text-muted);display:flex;align-items:center;justify-content:center;padding:0">
25
+ <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>
25
26
  </button>
26
27
  </div>
27
28
  <div v-if="open && !disabled" 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">
28
- <div v-if="shown.length === 0" style="padding:8px 10px;font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-muted)">No results</div>
29
+ <div v-if="shown.length === 0" style="height:var(--control-h);display:flex;align-items:center;padding:0 10px;font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:var(--control-tracking);color:var(--text-muted)">No results</div>
29
30
  <button v-for="(o, i) in shown" :key="o" type="button" @click="pick(o)" @mouseenter="hov = i"
30
- :style="{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', padding: '7px 10px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: 'pointer', background: hov === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', color: 'var(--text-heading)', textAlign: 'left' }">
31
- {{ o }}<span v-if="o === modelValue" style="color:var(--action-primary);display:inline-flex"><Icon name="check" :size="13" /></span>
31
+ :style="{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', height: 'var(--control-h)', padding: '0 10px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: 'pointer', background: hov === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-heading)', textAlign: 'left' }">
32
+ {{ o }}<span v-if="o === modelValue" style="color:var(--action-primary);display:inline-flex"><Icon name="check" :size="16" :style="tierIcon" /></span>
32
33
  </button>
33
34
  </div>
34
35
  </div>
@@ -7,6 +7,10 @@ export interface DatePickerProps {
7
7
  disabled?: boolean;
8
8
  error?: boolean;
9
9
  width?: number | string;
10
+ /** Size tier: sm 24 · md 32 · lg 36. Omit to inherit the enclosing region's tier (the default). */
11
+ size?: "sm" | "md" | "lg";
12
+ /** BCP 47 locale for the month title, weekday initials and the field label (default "en-US"; e.g. "zh-CN", "ja-JP") */
13
+ locale?: string;
10
14
  onChange?: (isoDate: string) => void;
11
15
  style?: React.CSSProperties;
12
16
  }
@@ -3,12 +3,15 @@ import { Icon } from "../icons/Icon.jsx";
3
3
  import { IconButton } from "../buttons/IconButton.jsx";
4
4
  import { inputBase } from "./Input.jsx";
5
5
 
6
- const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
7
6
  const pad = (n) => String(n).padStart(2, "0");
8
7
  const iso = (d) => d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate());
8
+ const tierIcon = { width: "var(--control-icon)", height: "var(--control-icon)" };
9
+ /** Month title, weekday initials and the field label come from Intl for the given locale (en-US, zh-CN, ja-JP…), so the picker reads right in every site language. */
10
+ const fmt = (locale, opts) => { try { return new Intl.DateTimeFormat(locale, opts); } catch (e) { return new Intl.DateTimeFormat("en-US", opts); } };
11
+ const weekdays = (locale) => Array.from({ length: 7 }, (_, i) => fmt(locale, { weekday: "short" }).format(new Date(2024, 0, 1 + i))); // 2024-01-01 is a Monday
9
12
 
10
- /** Date picker: input with calendar glyph opening a month-grid popover — chevron month nav, today outlined, selected day filled blue. value/onChange use "YYYY-MM-DD". */
11
- export function DatePicker({ value = "", placeholder = "Select date", disabled, error, width = 180, onChange, style }) {
13
+ /** Date picker: input with calendar glyph opening a month-grid popover — chevron month nav, today outlined, selected day filled blue. value/onChange use "YYYY-MM-DD"; `locale` localises the labels; the field takes the region's tier (omit `size` to inherit). */
14
+ export function DatePicker({ value = "", placeholder = "Select date", disabled, error, width = 180, size, locale = "en-US", onChange, style }) {
12
15
  const [open, setOpen] = React.useState(false);
13
16
  const sel = value ? new Date(value + "T00:00:00") : null;
14
17
  const [view, setView] = React.useState(() => { const d = sel || new Date(); return { y: d.getFullYear(), m: d.getMonth() }; });
@@ -19,26 +22,27 @@ export function DatePicker({ value = "", placeholder = "Select date", disabled,
19
22
  const days = new Date(view.y, view.m + 1, 0).getDate();
20
23
  const todayIso = iso(new Date());
21
24
  const nav = (d) => { let m = view.m + d, y = view.y; if (m < 0) { m = 11; y--; } if (m > 11) { m = 0; y++; } setView({ y, m }); };
22
- const label = sel ? MONTHS[sel.getMonth()].slice(0, 3) + " " + sel.getDate() + ", " + sel.getFullYear() : "";
25
+ const label = sel ? fmt(locale, { year: "numeric", month: "short", day: "numeric" }).format(sel) : "";
26
+ const monthTitle = fmt(locale, { year: "numeric", month: "long" }).format(new Date(view.y, view.m, 1));
23
27
  return (
24
- <div ref={root} style={{ position: "relative", width, ...style }}>
25
- <div onClick={() => !disabled && setOpen(!open)} style={{ ...inputBase, display: "flex", alignItems: "center", justifyContent: "space-between", cursor: disabled ? "not-allowed" : "pointer", borderColor: error ? "var(--danger)" : open ? "var(--focus-border)" : "var(--border-control)", boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", opacity: disabled ? .5 : 1, color: label ? "var(--text-primary)" : "var(--text-muted)" }}>
26
- {label || placeholder}<span style={{ color: "var(--text-muted)", display: "inline-flex" }}><Icon name="calendar" size={14} /></span>
28
+ <div ref={root} data-size={size} style={{ position: "relative", width, ...style }}>
29
+ <div data-field={disabled ? undefined : ""} onClick={() => !disabled && setOpen(!open)} style={{ ...inputBase, display: "flex", alignItems: "center", justifyContent: "space-between", cursor: disabled ? "not-allowed" : "pointer", borderColor: error ? "var(--danger)" : open ? "var(--focus-border)" : "var(--field-border, var(--border-control))", boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", opacity: disabled ? .5 : 1, color: label ? "var(--text-primary)" : "var(--text-muted)" }}>
30
+ {label || placeholder}<span style={{ color: "var(--text-muted)", display: "inline-flex" }}><Icon name="calendar" size={16} style={tierIcon} /></span>
27
31
  </div>
28
32
  {open && <div style={{ position: "absolute", top: "calc(100% + 4px)", left: 0, zIndex: "var(--z-menu)", width: 252, background: "var(--bg-surface)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-lg)", boxShadow: "var(--shadow-menu)", padding: 12, boxSizing: "border-box" }}>
29
33
  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", paddingBottom: 8 }}>
30
- <IconButton icon="chevron-left" size="sm" label="Previous month" onClick={() => nav(-1)} />
31
- <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-sm)", color: "var(--text-heading)" }}>{MONTHS[view.m]} {view.y}</span>
32
- <IconButton icon="chevron-right" size="sm" label="Next month" onClick={() => nav(1)} />
34
+ <IconButton icon="chevron-left" label="Previous month" onClick={() => nav(-1)} />
35
+ <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--control-text)", letterSpacing: "var(--control-tracking)", color: "var(--text-heading)" }}>{monthTitle}</span>
36
+ <IconButton icon="chevron-right" label="Next month" onClick={() => nav(1)} />
33
37
  </div>
34
38
  <div style={{ display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 2 }}>
35
- {["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"].map(d => <span key={d} style={{ textAlign: "center", fontFamily: "var(--font-sans)", fontSize: "var(--text-2xs)", letterSpacing: "var(--tracking-2xs)", color: "var(--text-muted)", padding: "2px 0" }}>{d}</span>)}
39
+ {weekdays(locale).map((d, i) => <span key={i} style={{ textAlign: "center", fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", letterSpacing: "var(--tracking-xs)", color: "var(--text-muted)", padding: "2px 0", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{d}</span>)}
36
40
  {Array.from({ length: start }, (_, i) => <span key={"e" + i}></span>)}
37
41
  {Array.from({ length: days }, (_, i) => {
38
42
  const dIso = view.y + "-" + pad(view.m + 1) + "-" + pad(i + 1);
39
43
  const isSel = value === dIso, isToday = todayIso === dIso;
40
44
  return <button key={i} type="button" onClick={() => { onChange && onChange(dIso); setOpen(false); }}
41
- style={{ height: 28, borderRadius: "var(--radius-sm)", border: isToday && !isSel ? "1px solid var(--action-primary)" : "1px solid transparent", cursor: "pointer", background: isSel ? "var(--action-primary)" : "transparent", color: isSel ? "var(--on-accent)" : "var(--text-body)", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", padding: 0, boxSizing: "border-box" }}
45
+ style={{ height: 28, borderRadius: "var(--radius-sm)", border: isToday && !isSel ? "1px solid var(--action-primary)" : "1px solid transparent", cursor: "pointer", background: isSel ? "var(--action-primary)" : "transparent", color: isSel ? "var(--on-accent)" : "var(--text-body)", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", letterSpacing: "var(--control-tracking)", fontVariantNumeric: "tabular-nums", padding: 0, boxSizing: "border-box" }}
42
46
  onMouseEnter={(e) => { if (!isSel) e.currentTarget.style.background = "var(--bg-hover)"; }} onMouseLeave={(e) => { if (!isSel) e.currentTarget.style.background = "transparent"; }}>{i + 1}</button>;
43
47
  })}
44
48
  </div>
@@ -4,9 +4,11 @@ import Icon from '../icons/Icon.vue'
4
4
  import IconButton from '../buttons/IconButton.vue'
5
5
  import { inputBase } from './input-base.js'
6
6
  // Date picker: input with calendar glyph opening a month-grid popover — Monday-first, today outlined, selected filled. v-model is "YYYY-MM-DD".
7
- const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']
7
+ // `locale` localises month title, weekday initials and the field label through Intl; the field takes the region's tier (omit `size` to inherit).
8
8
  const pad = (n) => String(n).padStart(2, '0')
9
- const props = defineProps({ modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select date' }, disabled: Boolean, error: Boolean, width: { type: [Number, String], default: 180 }, style: Object })
9
+ const tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }
10
+ const fmt = (locale, opts) => { try { return new Intl.DateTimeFormat(locale, opts) } catch (e) { return new Intl.DateTimeFormat('en-US', opts) } }
11
+ const props = defineProps({ modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select date' }, disabled: Boolean, error: Boolean, width: { type: [Number, String], default: 180 }, size: String, locale: { type: String, default: 'en-US' }, style: Object })
10
12
  const emit = defineEmits(['update:modelValue'])
11
13
  const open = ref(false), root = ref(null)
12
14
  const sel = computed(() => props.modelValue ? new Date(props.modelValue + 'T00:00:00') : null)
@@ -15,7 +17,9 @@ const view = ref({ y: d0.getFullYear(), m: d0.getMonth() })
15
17
  const start = computed(() => (new Date(view.value.y, view.value.m, 1).getDay() + 6) % 7)
16
18
  const days = computed(() => new Date(view.value.y, view.value.m + 1, 0).getDate())
17
19
  const todayIso = (() => { const d = new Date(); return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) })()
18
- const label = computed(() => sel.value ? MONTHS[sel.value.getMonth()].slice(0, 3) + ' ' + sel.value.getDate() + ', ' + sel.value.getFullYear() : '')
20
+ const label = computed(() => sel.value ? fmt(props.locale, { year: 'numeric', month: 'short', day: 'numeric' }).format(sel.value) : '')
21
+ const monthTitle = computed(() => fmt(props.locale, { year: 'numeric', month: 'long' }).format(new Date(view.value.y, view.value.m, 1)))
22
+ const weekdays = computed(() => Array.from({ length: 7 }, (_, i) => fmt(props.locale, { weekday: 'short' }).format(new Date(2024, 0, 1 + i)))) // 2024-01-01 is a Monday
19
23
  function nav(d) { let m = view.value.m + d, y = view.value.y; if (m < 0) { m = 11; y-- } if (m > 11) { m = 0; y++ } view.value = { y, m } }
20
24
  function dayIso(i) { return view.value.y + '-' + pad(view.value.m + 1) + '-' + pad(i) }
21
25
  function pick(i) { emit('update:modelValue', dayIso(i)); open.value = false }
@@ -24,21 +28,21 @@ onMounted(() => document.addEventListener('mousedown', onDoc))
24
28
  onUnmounted(() => document.removeEventListener('mousedown', onDoc))
25
29
  </script>
26
30
  <template>
27
- <div ref="root" :style="{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }">
28
- <div @click="!disabled && (open = !open)" :style="{ ...inputBase, display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: disabled ? 'not-allowed' : 'pointer', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-border)' : 'var(--border-control)', boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', opacity: disabled ? .5 : 1, color: label ? 'var(--text-primary)' : 'var(--text-muted)' }">
29
- {{ label || placeholder }}<span style="color:var(--text-muted);display:inline-flex"><Icon name="calendar" :size="14" /></span>
31
+ <div ref="root" :data-size="size" :style="{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }">
32
+ <div :data-field="disabled ? undefined : ''" @click="!disabled && (open = !open)" :style="{ ...inputBase, display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: disabled ? 'not-allowed' : 'pointer', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))', boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', opacity: disabled ? .5 : 1, color: label ? 'var(--text-primary)' : 'var(--text-muted)' }">
33
+ {{ label || placeholder }}<span style="color:var(--text-muted);display:inline-flex"><Icon name="calendar" :size="16" :style="tierIcon" /></span>
30
34
  </div>
31
35
  <div v-if="open" style="position:absolute;top:calc(100% + 4px);left:0;z-index:var(--z-menu);width:252px;background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);padding:12px;box-sizing:border-box">
32
36
  <div style="display:flex;align-items:center;justify-content:space-between;padding-bottom:8px">
33
- <IconButton icon="chevron-left" size="sm" label="Previous month" @click="nav(-1)" />
34
- <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);color:var(--text-heading)">{{ MONTHS[view.m] }} {{ view.y }}</span>
35
- <IconButton icon="chevron-right" size="sm" label="Next month" @click="nav(1)" />
37
+ <IconButton icon="chevron-left" label="Previous month" @click="nav(-1)" />
38
+ <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--control-text);letter-spacing:var(--control-tracking);color:var(--text-heading)">{{ monthTitle }}</span>
39
+ <IconButton icon="chevron-right" label="Next month" @click="nav(1)" />
36
40
  </div>
37
41
  <div style="display:grid;grid-template-columns:repeat(7,1fr);gap:2px">
38
- <span v-for="d in ['Mo','Tu','We','Th','Fr','Sa','Su']" :key="d" style="text-align:center;font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding:2px 0">{{ d }}</span>
42
+ <span v-for="(d, i) in weekdays" :key="i" style="text-align:center;font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted);padding:2px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ d }}</span>
39
43
  <span v-for="i in start" :key="'e' + i" />
40
44
  <button v-for="i in days" :key="i" type="button" @click="pick(i)" class="kb-dp-day"
41
- :style="{ height: '28px', borderRadius: 'var(--radius-sm)', border: todayIso === dayIso(i) && modelValue !== dayIso(i) ? '1px solid var(--action-primary)' : '1px solid transparent', cursor: 'pointer', background: modelValue === dayIso(i) ? 'var(--action-primary)' : 'transparent', color: modelValue === dayIso(i) ? 'var(--on-accent)' : 'var(--text-body)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', padding: 0, boxSizing: 'border-box' }">{{ i }}</button>
45
+ :style="{ height: '28px', borderRadius: 'var(--radius-sm)', border: todayIso === dayIso(i) && modelValue !== dayIso(i) ? '1px solid var(--action-primary)' : '1px solid transparent', cursor: 'pointer', background: modelValue === dayIso(i) ? 'var(--action-primary)' : 'transparent', color: modelValue === dayIso(i) ? 'var(--on-accent)' : 'var(--text-body)', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', letterSpacing: 'var(--control-tracking)', fontVariantNumeric: 'tabular-nums', padding: 0, boxSizing: 'border-box' }">{{ i }}</button>
42
46
  </div>
43
47
  </div>
44
48
  </div>
@@ -13,7 +13,7 @@ export function Dropzone({ title = "Click to upload or drag and drop", hint = "J
13
13
  <div 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 }}>
14
14
  <span style={{ color: "var(--success-strong)" }}><Icon name="file-check" size={22} strokeWidth={1.5} /></span>
15
15
  <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", color: "var(--text-heading)" }}>{file}</span>
16
- <button type="button" onClick={onRemove} style={{ display: "flex", alignItems: "center", gap: 4, border: "none", background: "transparent", cursor: "pointer", color: "var(--danger)", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", padding: 0 }}>
16
+ <button type="button" onClick={onRemove} data-link="" style={{ display: "flex", alignItems: "center", gap: 4, border: "none", background: "transparent", cursor: "pointer", color: "var(--danger)", fontFamily: "var(--font-sans)", fontSize: "var(--text-base)", padding: 0, textDecorationLine: "var(--link-line, none)", textUnderlineOffset: 2 }}>
17
17
  <Icon name="trash-2" size={13} />Remove
18
18
  </button>
19
19
  </div>
@@ -30,7 +30,7 @@ export function Dropzone({ title = "Click to upload or drag and drop", hint = "J
30
30
  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 }}>
31
31
  <span style={{ color: error ? "var(--danger)" : dragActive ? "var(--action-primary)" : "var(--text-muted)", marginBottom: 6 }}><Icon name={error ? "triangle-alert" : "upload"} size={22} strokeWidth={1.5} /></span>
32
32
  <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>
33
- <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>
33
+ <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>
34
34
  </div>
35
35
  );
36
36
  }
@@ -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} />