@keyfunc/keybricks-ds 1.0.3 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/DEPLOY.md +4 -4
  2. package/DESIGN.md +154 -53
  3. package/INTEGRATION.md +176 -36
  4. package/SKILL.md +6 -4
  5. package/_adherence.oxlintrc.json +183 -72
  6. package/_scripts/build-index.mjs +9 -0
  7. package/_scripts/build-tokens.mjs +5 -2
  8. package/assets/icons/icon-index.js +1 -1
  9. package/assets/icons/icon-index.json +1 -1
  10. package/components/_cards/accordion.card.html +2 -2
  11. package/components/_cards/action-chip.card.html +8 -3
  12. package/components/_cards/activity.card.html +2 -2
  13. package/components/_cards/alert.card.html +2 -2
  14. package/components/_cards/app-chrome.card.html +4 -4
  15. package/components/_cards/banner.card.html +1 -1
  16. package/components/_cards/breadcrumb.card.html +2 -2
  17. package/components/_cards/button-1-base.card.html +2 -2
  18. package/components/_cards/button-2-states.card.html +14 -14
  19. package/components/_cards/button-3-sizes-icons.card.html +1 -1
  20. package/components/_cards/button-4-group.card.html +5 -5
  21. package/components/_cards/canvas-zoom-controls.card.html +1 -1
  22. package/components/_cards/cards-indicators.card.html +2 -2
  23. package/components/_cards/charts-1-sparkline.card.html +2 -2
  24. package/components/_cards/charts-10-gauge.card.html +4 -4
  25. package/components/_cards/charts-11-funnel.card.html +2 -2
  26. package/components/_cards/charts-12-treemap.card.html +2 -2
  27. package/components/_cards/charts-14-radar.card.html +1 -1
  28. package/components/_cards/charts-15-distribution.card.html +1 -1
  29. package/components/_cards/charts-16-pareto.card.html +2 -2
  30. package/components/_cards/charts-17-candlestick.card.html +2 -2
  31. package/components/_cards/charts-18-sankey.card.html +2 -2
  32. package/components/_cards/charts-19-chord.card.html +2 -2
  33. package/components/_cards/charts-2-line.card.html +2 -2
  34. package/components/_cards/charts-20-sunburst.card.html +2 -2
  35. package/components/_cards/charts-21-worldmap.card.html +2 -2
  36. package/components/_cards/charts-22-gantt.card.html +2 -2
  37. package/components/_cards/charts-23-bar3d.card.html +2 -2
  38. package/components/_cards/charts-24-pie3d.card.html +2 -2
  39. package/components/_cards/charts-25-surface3d.card.html +2 -2
  40. package/components/_cards/charts-26-scatter3d.card.html +2 -2
  41. package/components/_cards/charts-27-ribbon3d.card.html +2 -2
  42. package/components/_cards/charts-3-bar.card.html +2 -2
  43. package/components/_cards/charts-4-barlist.card.html +2 -2
  44. package/components/_cards/charts-5-donut.card.html +3 -3
  45. package/components/_cards/charts-6-scatter.card.html +2 -2
  46. package/components/_cards/charts-7-heatmap.card.html +2 -2
  47. package/components/_cards/charts-8-waterfall.card.html +2 -2
  48. package/components/_cards/charts-9-bullet.card.html +2 -2
  49. package/components/_cards/charts-99-fullpage.card.html +5 -5
  50. package/components/_cards/checkbox.card.html +4 -4
  51. package/components/_cards/codeblock.card.html +1 -1
  52. package/components/_cards/combobox.card.html +3 -2
  53. package/components/_cards/context-menu.card.html +3 -2
  54. package/components/_cards/date-picker.card.html +5 -2
  55. package/components/_cards/diffvalue.card.html +2 -2
  56. package/components/_cards/divider.card.html +3 -3
  57. package/components/_cards/drawer.card.html +2 -2
  58. package/components/_cards/dropdown-menu.card.html +6 -6
  59. package/components/_cards/dropzone.card.html +1 -1
  60. package/components/_cards/empty-state.card.html +5 -5
  61. package/components/_cards/filter-bar.card.html +5 -5
  62. package/components/_cards/icon-button.card.html +2 -2
  63. package/components/_cards/icons-1-core.card.html +2 -2
  64. package/components/_cards/icons-2-feather.card.html +3 -3
  65. package/components/_cards/icons-3-unicons.card.html +3 -3
  66. package/components/_cards/input.card.html +5 -2
  67. package/components/_cards/kbd.card.html +2 -2
  68. package/components/_cards/keyvalue.card.html +2 -2
  69. package/components/_cards/listrow.card.html +2 -2
  70. package/components/_cards/loading.card.html +2 -2
  71. package/components/_cards/log-viewer.card.html +2 -2
  72. package/components/_cards/modal.card.html +9 -9
  73. package/components/_cards/multi-select.card.html +3 -2
  74. package/components/_cards/number-stepper.card.html +3 -2
  75. package/components/_cards/pagination.card.html +3 -2
  76. package/components/_cards/people.card.html +3 -3
  77. package/components/_cards/pipeline-node.card.html +2 -2
  78. package/components/_cards/pipeline-palette.card.html +4 -4
  79. package/components/_cards/radio.card.html +46 -0
  80. package/components/_cards/search.card.html +5 -2
  81. package/components/_cards/segmented-control.card.html +39 -0
  82. package/components/_cards/select.card.html +4 -4
  83. package/components/_cards/slider.card.html +1 -1
  84. package/components/_cards/smart-table.card.html +1 -1
  85. package/components/_cards/spreadsheet.card.html +2 -2
  86. package/components/_cards/stepper.card.html +2 -2
  87. package/components/_cards/table.card.html +103 -0
  88. package/components/_cards/tabs.card.html +3 -2
  89. package/components/_cards/tag.card.html +5 -2
  90. package/components/_cards/textarea.card.html +2 -2
  91. package/components/_cards/timeline.card.html +1 -1
  92. package/components/_cards/toast.card.html +4 -4
  93. package/components/_cards/toggles.card.html +45 -56
  94. package/components/_cards/tooltip.card.html +5 -5
  95. package/components/_cards/tree.card.html +2 -2
  96. package/components/_shared/src-buttons.js +1 -1
  97. package/components/_shared/src-charts.js +1 -1
  98. package/components/_shared/src-data.js +1 -1
  99. package/components/_shared/src-display.js +1 -1
  100. package/components/_shared/src-feedback.js +1 -1
  101. package/components/_shared/src-forms.js +1 -1
  102. package/components/_shared/src-navigation.js +1 -1
  103. package/components/_shared/src-pipeline.js +1 -1
  104. package/components/_shared/src-tabs.js +1 -1
  105. package/components/_shared/view-source.js +9 -4
  106. package/components/_vue/index.mjs +9 -5
  107. package/components/buttons/ActionChip.d.ts +3 -1
  108. package/components/buttons/ActionChip.jsx +4 -4
  109. package/components/buttons/ActionChip.vue +4 -4
  110. package/components/buttons/Button.d.ts +5 -2
  111. package/components/buttons/Button.jsx +15 -18
  112. package/components/buttons/Button.vue +17 -18
  113. package/components/buttons/ButtonGroup.vue +1 -1
  114. package/components/buttons/IconButton.d.ts +2 -1
  115. package/components/buttons/IconButton.jsx +9 -7
  116. package/components/buttons/IconButton.vue +10 -8
  117. package/components/buttons/buttons.prompt.md +6 -5
  118. package/components/charts/Bar3D.jsx +2 -2
  119. package/components/charts/Bar3D.vue +2 -2
  120. package/components/charts/BarChart.jsx +4 -4
  121. package/components/charts/BarChart.vue +4 -4
  122. package/components/charts/BarList.jsx +1 -1
  123. package/components/charts/BarList.vue +1 -1
  124. package/components/charts/BoxPlot.jsx +1 -1
  125. package/components/charts/BoxPlot.vue +2 -2
  126. package/components/charts/BulletChart.jsx +1 -1
  127. package/components/charts/BulletChart.vue +1 -1
  128. package/components/charts/CandlestickChart.jsx +2 -2
  129. package/components/charts/CandlestickChart.vue +3 -3
  130. package/components/charts/ChartKit.jsx +5 -5
  131. package/components/charts/ChartLegend.vue +16 -0
  132. package/components/charts/ChartTooltip.vue +16 -0
  133. package/components/charts/ChordChart.jsx +2 -2
  134. package/components/charts/ChordChart.vue +2 -2
  135. package/components/charts/GanttChart.jsx +2 -2
  136. package/components/charts/GanttChart.vue +2 -2
  137. package/components/charts/GaugeChart.jsx +2 -2
  138. package/components/charts/GaugeChart.vue +2 -2
  139. package/components/charts/HeatmapChart.jsx +2 -2
  140. package/components/charts/HeatmapChart.vue +2 -2
  141. package/components/charts/Histogram.jsx +2 -2
  142. package/components/charts/Histogram.vue +3 -3
  143. package/components/charts/LineChart.jsx +1 -1
  144. package/components/charts/LineChart.vue +3 -3
  145. package/components/charts/ParetoChart.jsx +3 -3
  146. package/components/charts/ParetoChart.vue +4 -4
  147. package/components/charts/PlotGrid.vue +18 -0
  148. package/components/charts/RadarChart.jsx +1 -1
  149. package/components/charts/RadarChart.vue +1 -1
  150. package/components/charts/Ribbon3D.jsx +2 -2
  151. package/components/charts/Ribbon3D.vue +2 -2
  152. package/components/charts/SankeyChart.jsx +2 -2
  153. package/components/charts/SankeyChart.vue +2 -2
  154. package/components/charts/Scatter3D.jsx +1 -1
  155. package/components/charts/Scatter3D.vue +1 -1
  156. package/components/charts/ScatterChart.jsx +3 -3
  157. package/components/charts/ScatterChart.vue +4 -4
  158. package/components/charts/Sparkline.jsx +1 -1
  159. package/components/charts/Sparkline.vue +1 -1
  160. package/components/charts/SunburstChart.jsx +1 -1
  161. package/components/charts/SunburstChart.vue +1 -1
  162. package/components/charts/Surface3D.jsx +2 -2
  163. package/components/charts/Surface3D.vue +2 -2
  164. package/components/charts/TreemapChart.jsx +2 -2
  165. package/components/charts/TreemapChart.vue +2 -2
  166. package/components/charts/WaterfallChart.jsx +2 -2
  167. package/components/charts/WaterfallChart.vue +3 -3
  168. package/components/charts/WorldMap.jsx +2 -2
  169. package/components/charts/WorldMap.vue +2 -2
  170. package/components/charts/XLabels.vue +11 -0
  171. package/components/data/CodeBlock.jsx +1 -1
  172. package/components/data/CodeBlock.vue +1 -1
  173. package/components/data/DiffValue.jsx +1 -1
  174. package/components/data/DiffValue.vue +1 -1
  175. package/components/data/FilterBar.d.ts +2 -0
  176. package/components/data/FilterBar.jsx +10 -9
  177. package/components/data/FilterBar.vue +9 -8
  178. package/components/data/KeyValue.jsx +3 -3
  179. package/components/data/KeyValue.vue +3 -3
  180. package/components/data/ListRow.jsx +2 -2
  181. package/components/data/ListRow.vue +2 -2
  182. package/components/data/Pagination.d.ts +5 -1
  183. package/components/data/Pagination.jsx +16 -12
  184. package/components/data/Pagination.vue +14 -9
  185. package/components/data/SmartTable.d.ts +4 -1
  186. package/components/data/SmartTable.jsx +16 -14
  187. package/components/data/SmartTable.vue +25 -22
  188. package/components/data/Spreadsheet.d.ts +2 -0
  189. package/components/data/Spreadsheet.jsx +12 -11
  190. package/components/data/Spreadsheet.vue +14 -13
  191. package/components/data/Table.d.ts +47 -0
  192. package/components/data/Table.jsx +85 -0
  193. package/components/data/Table.vue +63 -0
  194. package/components/data/data.prompt.md +3 -3
  195. package/components/display/Avatar.vue +3 -2
  196. package/components/display/AvatarGroup.vue +3 -2
  197. package/components/display/Badge.vue +1 -1
  198. package/components/display/Divider.vue +3 -3
  199. package/components/display/Donut.vue +3 -2
  200. package/components/display/IconTile.vue +2 -1
  201. package/components/display/Kbd.vue +1 -1
  202. package/components/display/ProgressBar.jsx +1 -1
  203. package/components/display/ProgressBar.vue +1 -1
  204. package/components/display/StatCard.vue +1 -1
  205. package/components/display/Tag.d.ts +3 -1
  206. package/components/display/Tag.jsx +4 -4
  207. package/components/display/Tag.vue +4 -4
  208. package/components/feedback/Accordion.jsx +2 -2
  209. package/components/feedback/Accordion.vue +2 -2
  210. package/components/feedback/ActivityItem.d.ts +3 -1
  211. package/components/feedback/ActivityItem.jsx +5 -5
  212. package/components/feedback/ActivityItem.vue +5 -4
  213. package/components/feedback/Alert.jsx +4 -4
  214. package/components/feedback/Alert.vue +3 -3
  215. package/components/feedback/Banner.jsx +5 -5
  216. package/components/feedback/Banner.vue +5 -5
  217. package/components/feedback/ContextMenu.d.ts +3 -1
  218. package/components/feedback/ContextMenu.jsx +5 -5
  219. package/components/feedback/ContextMenu.vue +5 -5
  220. package/components/feedback/DotActivity.vue +3 -3
  221. package/components/feedback/Drawer.d.ts +2 -0
  222. package/components/feedback/Drawer.jsx +6 -6
  223. package/components/feedback/Drawer.vue +22 -15
  224. package/components/feedback/EmptyState.vue +2 -2
  225. package/components/feedback/LogViewer.vue +1 -1
  226. package/components/feedback/Modal.d.ts +2 -0
  227. package/components/feedback/Modal.jsx +12 -11
  228. package/components/feedback/Modal.vue +10 -8
  229. package/components/feedback/Skeleton.vue +2 -1
  230. package/components/feedback/Spinner.vue +2 -1
  231. package/components/feedback/Stepper.vue +1 -1
  232. package/components/feedback/Timeline.jsx +1 -1
  233. package/components/feedback/Timeline.vue +1 -1
  234. package/components/feedback/Toast.vue +4 -4
  235. package/components/feedback/ToastStack.vue +11 -0
  236. package/components/feedback/Tooltip.vue +19 -0
  237. package/components/feedback/feedback.prompt.md +4 -4
  238. package/components/forms/Checkbox.d.ts +3 -2
  239. package/components/forms/Checkbox.jsx +7 -7
  240. package/components/forms/Checkbox.vue +8 -8
  241. package/components/forms/Combobox.d.ts +2 -0
  242. package/components/forms/Combobox.jsx +12 -10
  243. package/components/forms/Combobox.vue +11 -10
  244. package/components/forms/DatePicker.d.ts +4 -0
  245. package/components/forms/DatePicker.jsx +16 -12
  246. package/components/forms/DatePicker.vue +15 -11
  247. package/components/forms/Dropzone.jsx +2 -2
  248. package/components/forms/Dropzone.vue +5 -5
  249. package/components/forms/Field.d.ts +5 -1
  250. package/components/forms/Field.jsx +4 -4
  251. package/components/forms/Field.vue +4 -4
  252. package/components/forms/Input.d.ts +5 -1
  253. package/components/forms/Input.jsx +7 -7
  254. package/components/forms/Input.vue +3 -3
  255. package/components/forms/MultiSelect.d.ts +2 -0
  256. package/components/forms/MultiSelect.jsx +13 -11
  257. package/components/forms/MultiSelect.vue +12 -11
  258. package/components/forms/NumberStepper.d.ts +3 -1
  259. package/components/forms/NumberStepper.jsx +7 -7
  260. package/components/forms/NumberStepper.vue +8 -7
  261. package/components/forms/Radio.d.ts +30 -0
  262. package/components/forms/Radio.jsx +65 -0
  263. package/components/forms/Radio.vue +17 -9
  264. package/components/forms/RadioGroup.vue +22 -4
  265. package/components/forms/SearchInput.d.ts +3 -1
  266. package/components/forms/SearchInput.jsx +6 -6
  267. package/components/forms/SearchInput.vue +7 -6
  268. package/components/forms/Select.d.ts +3 -2
  269. package/components/forms/Select.jsx +6 -7
  270. package/components/forms/Select.vue +6 -6
  271. package/components/forms/Slider.jsx +3 -3
  272. package/components/forms/Slider.vue +3 -3
  273. package/components/forms/Switch.d.ts +32 -24
  274. package/components/forms/Switch.jsx +85 -35
  275. package/components/forms/Switch.vue +57 -11
  276. package/components/forms/Textarea.vue +4 -4
  277. package/components/forms/forms.prompt.md +4 -1
  278. package/components/forms/input-base.js +2 -1
  279. package/components/index.d.ts +2 -2
  280. package/components/index.js +5 -5
  281. package/components/navigation/Breadcrumb.jsx +3 -3
  282. package/components/navigation/Breadcrumb.vue +4 -4
  283. package/components/navigation/DocTree.jsx +3 -3
  284. package/components/navigation/DocTree.vue +3 -3
  285. package/components/navigation/PageHeading.vue +1 -1
  286. package/components/navigation/PageToolbar.vue +7 -6
  287. package/components/navigation/Sidebar.jsx +1 -1
  288. package/components/navigation/Sidebar.vue +3 -3
  289. package/components/navigation/ThemeToggle.vue +2 -1
  290. package/components/navigation/TopBar.d.ts +3 -1
  291. package/components/navigation/TopBar.jsx +6 -6
  292. package/components/navigation/Tree.jsx +5 -5
  293. package/components/navigation/Tree.vue +5 -5
  294. package/components/pipeline/ConfigPanel.d.ts +2 -0
  295. package/components/pipeline/ConfigPanel.jsx +9 -9
  296. package/components/pipeline/ConfigPanel.vue +9 -9
  297. package/components/pipeline/DataPreview.d.ts +3 -1
  298. package/components/pipeline/DataPreview.jsx +7 -7
  299. package/components/pipeline/DataPreview.vue +7 -7
  300. package/components/pipeline/NodeCard.jsx +3 -3
  301. package/components/pipeline/NodeCard.vue +3 -3
  302. package/components/pipeline/PaletteItem.vue +2 -2
  303. package/components/pipeline/PaletteListItem.vue +1 -1
  304. package/components/pipeline/PalettePanel.jsx +2 -2
  305. package/components/pipeline/PalettePanel.vue +1 -1
  306. package/components/pipeline/Port.vue +15 -0
  307. package/components/pipeline/PortLabel.vue +15 -0
  308. package/components/pipeline/ZoomControls.vue +1 -1
  309. package/components/pipeline/port-skin.js +13 -0
  310. package/components/px.js +4 -0
  311. package/components/tabs/PanelTabs.vue +5 -4
  312. package/components/tabs/PillTabs.vue +5 -4
  313. package/components/tabs/SegmentedControl.d.ts +11 -4
  314. package/components/tabs/SegmentedControl.jsx +25 -10
  315. package/components/tabs/SegmentedControl.vue +19 -7
  316. package/components/tabs/Tabs.d.ts +5 -3
  317. package/components/tabs/Tabs.jsx +15 -15
  318. package/components/tabs/Tabs.vue +5 -5
  319. package/components/tabs/ViewToggle.d.ts +3 -1
  320. package/components/tabs/ViewToggle.jsx +5 -5
  321. package/components/tabs/ViewToggle.vue +5 -5
  322. package/components/tabs/tabs.prompt.md +4 -2
  323. package/keybricks.css +4 -0
  324. package/package.json +6 -3
  325. package/readme.md +90 -123
  326. package/readme.zh-CN.md +120 -0
  327. package/tokens/charts.css +20 -17
  328. package/tokens/colors.css +2 -2
  329. package/tokens/dark.css +2 -2
  330. package/tokens/hover.css +32 -0
  331. package/tokens/interaction.css +3 -1
  332. package/tokens/layout.css +29 -7
  333. package/tokens/tokens.d.ts +36 -0
  334. package/tokens/tokens.js +143 -89
  335. package/tokens/tokens.json +240 -106
  336. package/tokens/typography.css +27 -14
  337. package/components/_cards/brand-visual.card.html +0 -43
  338. package/components/_cards/datatable.card.html +0 -48
  339. package/components/_shared/src-brand.js +0 -1
  340. package/components/brand/BrandVisual.d.ts +0 -17
  341. package/components/brand/BrandVisual.jsx +0 -29
  342. package/components/brand/BrandVisual.vue +0 -34
  343. package/components/brand/_dir.card.html +0 -13
  344. package/components/brand/brandMotifs.js +0 -210
  345. package/components/buttons/_dir.card.html +0 -13
  346. package/components/charts/ChartKit.vue +0 -47
  347. package/components/charts/_dir.card.html +0 -7
  348. package/components/data/DataTable.d.ts +0 -28
  349. package/components/data/DataTable.jsx +0 -32
  350. package/components/data/DataTable.vue +0 -25
  351. package/components/data/_dir.card.html +0 -13
  352. package/components/display/_dir.card.html +0 -13
  353. package/components/feedback/_dir.card.html +0 -13
  354. package/components/forms/_dir.card.html +0 -13
  355. package/components/icons/_dir.card.html +0 -13
  356. package/components/navigation/_dir.card.html +0 -13
  357. package/components/pipeline/_dir.card.html +0 -13
  358. package/components/tabs/_dir.card.html +0 -13
@@ -22,21 +22,22 @@ export function Tooltip({ label, placement = "top", children, style }) {
22
22
  }
23
23
 
24
24
  /** Centered modal dialog with scrim. width defaults to 440. */
25
- export function Modal({ open, title, subtitle, onClose, footer, width = 440, children, style }) {
25
+ /** Modal dialog. A size region that defaults to lg (36px controls) whatever the page behind it uses — a dialog does not compete for space; pass `size` to override. 24px padding, footer buttons 8px apart. */
26
+ export function Modal({ open, title, subtitle, onClose, footer, width = 440, size = "lg", children, style }) {
26
27
  if (!open) return null;
27
28
  return (
28
29
  <div style={{ position: "fixed", inset: 0, zIndex: "var(--z-modal)", background: "var(--bg-scrim)", display: "flex", alignItems: "center", justifyContent: "center", animation: "kb-fade-in var(--dur-fast) var(--ease-out)" }} onClick={onClose}>
29
- <div onClick={(e) => e.stopPropagation()}
30
+ <div role="dialog" aria-modal="true" data-size={size} onClick={(e) => e.stopPropagation()}
30
31
  style={{ width, maxWidth: "92vw", background: "var(--bg-raised)", borderRadius: "var(--radius-lg)", border: "1px solid var(--border-card)", boxShadow: "var(--shadow-modal)", animation: "kb-rise var(--dur) var(--ease-out)", ...style }}>
31
- <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", padding: "17px 17px 0" }}>
32
- <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
32
+ <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 16, padding: "24px 24px 0" }}>
33
+ <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
33
34
  <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-semibold)", fontSize: "var(--text-md)", lineHeight: "var(--leading-md)", letterSpacing: "var(--tracking-md)", color: "var(--text-heading)" }}>{title}</span>
34
- {subtitle ? <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", color: "var(--text-secondary)" }}>{subtitle}</span> : null}
35
+ {subtitle ? <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: "var(--text-secondary)" }}>{subtitle}</span> : null}
35
36
  </div>
36
- <IconButton icon="x" size="md" onClick={onClose} label="Close" />
37
+ <IconButton icon="x" onClick={onClose} label="Close" style={{ margin: "calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0" }} />
37
38
  </div>
38
- {children ? <div style={{ padding: "16px 17px 0" }}>{children}</div> : null}
39
- <div style={{ display: "flex", justifyContent: "flex-end", gap: 8, padding: 17 }}>{footer}</div>
39
+ {children ? <div style={{ padding: "20px 24px 0" }}>{children}</div> : null}
40
+ <div style={{ display: "flex", justifyContent: "flex-end", gap: "var(--control-gap)", padding: 24 }}>{footer}</div>
40
41
  </div>
41
42
  </div>
42
43
  );
@@ -49,12 +50,12 @@ export function Toast({ tone = "success", title, meta, onClose, style }) {
49
50
  const t = toastTones[tone];
50
51
  return (
51
52
  <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 }}>
52
- <span style={{ color: t.color, display: "inline-flex", marginTop: 1 }}><Icon name={t.icon} size={16} /></span>
53
+ <span style={{ color: t.color, display: "inline-flex", alignItems: "center", flexShrink: 0, height: "var(--leading-base)" }}><Icon name={t.icon} size={16} /></span>
53
54
  <div style={{ display: "flex", flexDirection: "column", flex: 1, minWidth: 0 }}>
54
55
  <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", color: "var(--text-heading)" }}>{title}</span>
55
- {meta ? <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", color: "var(--text-secondary)" }}>{meta}</span> : null}
56
+ {meta ? <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: "var(--text-secondary)" }}>{meta}</span> : null}
56
57
  </div>
57
- {onClose ? <IconButton icon="x" size="xs" muted onClick={onClose} label="Dismiss" /> : null}
58
+ {onClose ? <IconButton icon="x" size="sm" muted onClick={onClose} label="Dismiss" style={{ margin: "-2px 0" }} /> : null}
58
59
  </div>
59
60
  );
60
61
  }
@@ -1,20 +1,22 @@
1
1
  <script setup>
2
+ import { px } from '../px.js'
2
3
  import IconButton from '../buttons/IconButton.vue'
3
- defineProps({ open: Boolean, title: String, subtitle: String, width: { type: Number, default: 440 }, style: Object })
4
+ // Modal dialog. A size region that defaults to lg (36px controls) whatever the page behind it uses — a dialog does not compete for space; pass `size` to override. 24px padding, footer buttons 8px apart.
5
+ defineProps({ open: Boolean, title: String, subtitle: String, width: { type: [Number, String], default: 440 }, size: { type: String, default: 'lg' }, style: Object })
4
6
  const emit = defineEmits(['close'])
5
7
  </script>
6
8
  <template>
7
9
  <div v-if="open" style="position:fixed;inset:0;z-index:var(--z-modal);background:var(--bg-scrim);display:flex;align-items:center;justify-content:center;animation:kb-fade-in var(--dur-fast) var(--ease-out)" @click="emit('close')">
8
- <div @click.stop :style="{ width, maxWidth: '92vw', background: 'var(--bg-raised)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-card)', boxShadow: 'var(--shadow-modal)', animation: 'kb-rise var(--dur) var(--ease-out)', ...style }">
9
- <div style="display:flex;align-items:flex-start;justify-content:space-between;padding:17px 17px 0">
10
- <div style="display:flex;flex-direction:column;gap:4px">
10
+ <div role="dialog" aria-modal="true" :data-size="size" @click.stop :style="{ width: px(width), maxWidth: '92vw', background: 'var(--bg-raised)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-card)', boxShadow: 'var(--shadow-modal)', animation: 'kb-rise var(--dur) var(--ease-out)', ...style }">
11
+ <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:24px 24px 0">
12
+ <div style="display:flex;flex-direction:column;gap:2px">
11
13
  <span style="font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)">{{ title }}</span>
12
- <span v-if="subtitle" style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)">{{ subtitle }}</span>
14
+ <span v-if="subtitle" style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)">{{ subtitle }}</span>
13
15
  </div>
14
- <IconButton icon="x" size="md" label="Close" @click="emit('close')" />
16
+ <IconButton icon="x" label="Close" :style="{ margin: 'calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0' }" @click="emit('close')" />
15
17
  </div>
16
- <div v-if="$slots.default" style="padding:16px 17px 0"><slot /></div>
17
- <div style="display:flex;justify-content:flex-end;gap:8px;padding:17px"><slot name="footer" /></div>
18
+ <div v-if="$slots.default" style="padding:20px 24px 0"><slot /></div>
19
+ <div style="display:flex;justify-content:flex-end;gap:var(--control-gap);padding:24px"><slot name="footer" /></div>
18
20
  </div>
19
21
  </div>
20
22
  </template>
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
+ import { px } from '../px.js'
2
3
  defineProps({ width: { type: [String, Number], default: '100%' }, height: { type: [String, Number], default: 12 }, radius: { type: String, default: 'var(--radius-sm)' }, style: Object })
3
4
  </script>
4
5
  <template>
5
- <span :style="{ display: 'block', width, height, borderRadius: radius, background: 'var(--skeleton)', animation: 'kb-pulse 1.4s var(--ease-in-out) infinite', ...style }" />
6
+ <span :style="{ display: 'block', width, height, borderRadius: px(radius), background: 'var(--skeleton)', animation: 'kb-pulse 1.4s var(--ease-in-out) infinite', ...style }" />
6
7
  </template>
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
+ import { px } from '../px.js'
2
3
  defineProps({ size: { type: Number, default: 16 }, color: { type: String, default: 'var(--action-primary)' }, style: Object })
3
4
  </script>
4
5
  <template>
5
- <span :style="{ display: 'inline-block', width: size, height: size, borderRadius: 'var(--radius-full)', border: '2px solid var(--track)', borderTopColor: color, animation: 'kb-spin .7s linear infinite', ...style }" />
6
+ <span :style="{ display: 'inline-block', width: px(size), height: px(size), borderRadius: 'var(--radius-full)', border: '2px solid var(--track)', borderTopColor: color, animation: 'kb-spin .7s linear infinite', ...style }" />
6
7
  </template>
@@ -8,7 +8,7 @@ defineProps({ steps: { type: Array, default: () => [] }, current: { type: Number
8
8
  <template v-for="(s, i) in steps" :key="s.label">
9
9
  <div v-if="i > 0" style="flex:1;height:2px;background:var(--track);margin-top:15.25px;margin-left:12px;margin-right:12px;min-width:24px" />
10
10
  <div style="display:flex;flex-direction:column;align-items:center;gap:8px;flex-shrink:0">
11
- <span :style="{ width: 32, height: 32, borderRadius: 'var(--radius-full)', boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: i < current ? 'var(--success)' : i === current ? 'var(--action-primary)' : 'var(--bg-surface)', border: (i < current || i === current) ? 'none' : '2px solid var(--track)', color: (i < current || i === current) ? 'var(--on-accent)' : 'var(--text-muted)' }">
11
+ <span :style="{ width: '32px', height: '32px', borderRadius: 'var(--radius-full)', boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: i < current ? 'var(--success)' : i === current ? 'var(--action-primary)' : 'var(--bg-surface)', border: (i < current || i === current) ? 'none' : '2px solid var(--track)', color: (i < current || i === current) ? 'var(--on-accent)' : 'var(--text-muted)' }">
12
12
  <Icon :name="i < current ? 'check' : (s.icon || 'file')" :size="16" />
13
13
  </span>
14
14
  <span :style="{ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: 'var(--tracking-xs)', fontWeight: i === current ? 'var(--weight-medium)' : 'var(--weight-regular)', color: i === current ? 'var(--text-heading)' : 'var(--text-secondary)', whiteSpace: 'nowrap' }">{{ s.label }}</span>
@@ -18,7 +18,7 @@ export function Timeline({ items = [], style }) {
18
18
  <div style={{ display: "flex", flexDirection: "column", gap: 2, paddingBottom: i < items.length - 1 ? 18 : 0, minWidth: 0, paddingTop: 3 }}>
19
19
  <span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", color: "var(--text-heading)" }}>{it.title}</span>
20
20
  {it.meta && <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", letterSpacing: "var(--tracking-xs)", color: "var(--text-muted)" }}>{it.meta}</span>}
21
- {it.body && <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", color: "var(--text-body)", paddingTop: 4 }}>{it.body}</span>}
21
+ {it.body && <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", color: "var(--text-body)", paddingTop: 4 }}>{it.body}</span>}
22
22
  </div>
23
23
  </div>
24
24
  ))}
@@ -14,7 +14,7 @@ defineProps({ items: { type: Array, default: () => [] }, style: Object })
14
14
  <div :style="{ display: 'flex', flexDirection: 'column', gap: '2px', paddingBottom: i < items.length - 1 ? '18px' : 0, minWidth: 0, paddingTop: '3px' }">
15
15
  <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);color:var(--text-heading)">{{ it.title }}</span>
16
16
  <span v-if="it.meta" style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-muted)">{{ it.meta }}</span>
17
- <span v-if="it.body" style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-body);padding-top:4px">{{ it.body }}</span>
17
+ <span v-if="it.body" style="font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-body);padding-top:4px">{{ it.body }}</span>
18
18
  </div>
19
19
  </div>
20
20
  </div>
@@ -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
  }