@misoto22/design 0.7.0 → 0.8.0

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 (369) hide show
  1. package/AGENTS.md +87 -0
  2. package/CLAUDE.md +1 -0
  3. package/README.md +50 -5
  4. package/bin/misoto22-design.mjs +59 -13
  5. package/dist/agent/Accordion.md +24 -1
  6. package/dist/agent/Alert.md +23 -1
  7. package/dist/agent/AppShell.md +30 -2
  8. package/dist/agent/ArchitectureFigure.md +25 -2
  9. package/dist/agent/AreaChart.md +27 -2
  10. package/dist/agent/Article.md +26 -3
  11. package/dist/agent/AspectRatio.md +57 -0
  12. package/dist/agent/Avatar.md +25 -5
  13. package/dist/agent/Badge.md +22 -1
  14. package/dist/agent/BarChart.md +29 -2
  15. package/dist/agent/BarList.md +27 -1
  16. package/dist/agent/BigNumber.md +26 -2
  17. package/dist/agent/BoxPlot.md +24 -1
  18. package/dist/agent/Breadcrumb.md +24 -2
  19. package/dist/agent/BulletChart.md +23 -1
  20. package/dist/agent/Button.md +25 -1
  21. package/dist/agent/Calendar.md +37 -3
  22. package/dist/agent/Card.md +24 -2
  23. package/dist/agent/Checkbox.md +23 -2
  24. package/dist/agent/Code.md +50 -0
  25. package/dist/agent/CodeBlock.md +74 -0
  26. package/dist/agent/Collapsible.md +22 -1
  27. package/dist/agent/Combobox.md +29 -3
  28. package/dist/agent/Command.md +25 -2
  29. package/dist/agent/ComposedChart.md +26 -2
  30. package/dist/agent/ContextMenu.md +37 -4
  31. package/dist/agent/DataflowFigure.md +22 -1
  32. package/dist/agent/DatePicker.md +34 -6
  33. package/dist/agent/DescriptionList.md +68 -0
  34. package/dist/agent/Diagram.md +27 -3
  35. package/dist/agent/DiagramCanvas.md +24 -1
  36. package/dist/agent/DiagramExportMenu.md +29 -3
  37. package/dist/agent/DiagramInspector.md +23 -1
  38. package/dist/agent/DiagramLegend.md +22 -1
  39. package/dist/agent/DiagramMinimap.md +24 -4
  40. package/dist/agent/DiagramToolbar.md +23 -1
  41. package/dist/agent/Dialog.md +26 -3
  42. package/dist/agent/DropdownMenu.md +45 -6
  43. package/dist/agent/EmptyState.md +26 -1
  44. package/dist/agent/ErrorState.md +27 -2
  45. package/dist/agent/Facet.md +25 -1
  46. package/dist/agent/Field.md +45 -4
  47. package/dist/agent/FigureBand.md +24 -1
  48. package/dist/agent/FloatingIconButton.md +23 -1
  49. package/dist/agent/FunnelChart.md +23 -1
  50. package/dist/agent/Heading.md +64 -0
  51. package/dist/agent/Heatmap.md +25 -1
  52. package/dist/agent/Histogram.md +26 -2
  53. package/dist/agent/Input.md +22 -1
  54. package/dist/agent/Kbd.md +19 -1
  55. package/dist/agent/LifecycleFigure.md +22 -1
  56. package/dist/agent/LineChart.md +26 -2
  57. package/dist/agent/LinkArrow.md +20 -1
  58. package/dist/agent/Markdown.md +68 -0
  59. package/dist/agent/NativeSelect.md +23 -2
  60. package/dist/agent/NavItem.md +21 -1
  61. package/dist/agent/Pagination.md +28 -2
  62. package/dist/agent/PieChart.md +26 -2
  63. package/dist/agent/Popover.md +23 -1
  64. package/dist/agent/Progress.md +27 -3
  65. package/dist/agent/RadarChart.md +26 -2
  66. package/dist/agent/RadialChart.md +26 -3
  67. package/dist/agent/RadioGroup.md +23 -2
  68. package/dist/agent/SankeyChart.md +25 -2
  69. package/dist/agent/ScatterChart.md +27 -2
  70. package/dist/agent/ScrollArea.md +24 -3
  71. package/dist/agent/SearchableMenu.md +23 -1
  72. package/dist/agent/Select.md +29 -3
  73. package/dist/agent/Separator.md +28 -4
  74. package/dist/agent/SequenceFigure.md +23 -1
  75. package/dist/agent/Sheet.md +25 -2
  76. package/dist/agent/Skeleton.md +24 -2
  77. package/dist/agent/Slider.md +27 -4
  78. package/dist/agent/Sparkline.md +23 -1
  79. package/dist/agent/Spinner.md +24 -2
  80. package/dist/agent/StatusDot.md +20 -1
  81. package/dist/agent/StatusPill.md +28 -2
  82. package/dist/agent/Steps.md +24 -1
  83. package/dist/agent/Switch.md +21 -1
  84. package/dist/agent/Table.md +26 -3
  85. package/dist/agent/Tabs.md +24 -2
  86. package/dist/agent/Tag.md +40 -9
  87. package/dist/agent/Text.md +65 -0
  88. package/dist/agent/Textarea.md +20 -1
  89. package/dist/agent/Timestamp.md +64 -0
  90. package/dist/agent/Toast.md +26 -2
  91. package/dist/agent/ToggleGroup.md +24 -2
  92. package/dist/agent/Toolbar.md +71 -0
  93. package/dist/agent/Tooltip.md +23 -1
  94. package/dist/agent/TreemapChart.md +24 -1
  95. package/dist/agent/WaterfallChart.md +24 -1
  96. package/dist/agent/WorkflowFigure.md +24 -2
  97. package/dist/agent/catalog.json +5602 -745
  98. package/dist/agent/index.md +13 -4
  99. package/dist/article.css +16 -4
  100. package/dist/charts/AreaChart/AreaChart.d.ts +9 -2
  101. package/dist/charts/AreaChart/AreaChart.js +12 -13
  102. package/dist/charts/AreaChart/AreaChart.js.map +1 -1
  103. package/dist/charts/BarChart/BarChart.d.ts +14 -3
  104. package/dist/charts/BarChart/BarChart.js +20 -12
  105. package/dist/charts/BarChart/BarChart.js.map +1 -1
  106. package/dist/charts/BarList/BarList.d.ts +9 -1
  107. package/dist/charts/BarList/BarList.js +9 -1
  108. package/dist/charts/BarList/BarList.js.map +1 -1
  109. package/dist/charts/BigNumber/BigNumber.d.ts +19 -3
  110. package/dist/charts/BigNumber/BigNumber.js +25 -5
  111. package/dist/charts/BigNumber/BigNumber.js.map +1 -1
  112. package/dist/charts/BoxPlot/BoxPlot.js +4 -2
  113. package/dist/charts/BoxPlot/BoxPlot.js.map +1 -1
  114. package/dist/charts/BulletChart/BulletChart.js +28 -7
  115. package/dist/charts/BulletChart/BulletChart.js.map +1 -1
  116. package/dist/charts/ComposedChart/ComposedChart.d.ts +9 -2
  117. package/dist/charts/ComposedChart/ComposedChart.js +10 -10
  118. package/dist/charts/ComposedChart/ComposedChart.js.map +1 -1
  119. package/dist/charts/FunnelChart/FunnelChart.d.ts +7 -2
  120. package/dist/charts/FunnelChart/FunnelChart.js +6 -16
  121. package/dist/charts/FunnelChart/FunnelChart.js.map +1 -1
  122. package/dist/charts/Heatmap/Heatmap.d.ts +17 -3
  123. package/dist/charts/Heatmap/Heatmap.js +20 -11
  124. package/dist/charts/Heatmap/Heatmap.js.map +1 -1
  125. package/dist/charts/Histogram/Histogram.d.ts +24 -1
  126. package/dist/charts/Histogram/Histogram.js +43 -11
  127. package/dist/charts/Histogram/Histogram.js.map +1 -1
  128. package/dist/charts/LineChart/LineChart.d.ts +9 -2
  129. package/dist/charts/LineChart/LineChart.js +11 -12
  130. package/dist/charts/LineChart/LineChart.js.map +1 -1
  131. package/dist/charts/PieChart/PieChart.d.ts +15 -3
  132. package/dist/charts/PieChart/PieChart.js +14 -6
  133. package/dist/charts/PieChart/PieChart.js.map +1 -1
  134. package/dist/charts/RadarChart/RadarChart.d.ts +15 -3
  135. package/dist/charts/RadarChart/RadarChart.js +11 -9
  136. package/dist/charts/RadarChart/RadarChart.js.map +1 -1
  137. package/dist/charts/RadialChart/RadialChart.d.ts +22 -4
  138. package/dist/charts/RadialChart/RadialChart.js +28 -6
  139. package/dist/charts/RadialChart/RadialChart.js.map +1 -1
  140. package/dist/charts/SankeyChart/SankeyChart.d.ts +16 -4
  141. package/dist/charts/SankeyChart/SankeyChart.js +14 -6
  142. package/dist/charts/SankeyChart/SankeyChart.js.map +1 -1
  143. package/dist/charts/ScatterChart/ScatterChart.d.ts +24 -3
  144. package/dist/charts/ScatterChart/ScatterChart.js +21 -15
  145. package/dist/charts/ScatterChart/ScatterChart.js.map +1 -1
  146. package/dist/charts/Sparkline/Sparkline.js +5 -3
  147. package/dist/charts/Sparkline/Sparkline.js.map +1 -1
  148. package/dist/charts/TreemapChart/TreemapChart.d.ts +7 -2
  149. package/dist/charts/TreemapChart/TreemapChart.js +20 -11
  150. package/dist/charts/TreemapChart/TreemapChart.js.map +1 -1
  151. package/dist/charts/WaterfallChart/WaterfallChart.js +4 -2
  152. package/dist/charts/WaterfallChart/WaterfallChart.js.map +1 -1
  153. package/dist/charts/lib/export.d.ts +1 -0
  154. package/dist/charts/lib/figure.d.ts +21 -1
  155. package/dist/charts/lib/figure.js +5 -2
  156. package/dist/charts/lib/figure.js.map +1 -1
  157. package/dist/charts/lib/scale.d.ts +23 -0
  158. package/dist/charts/lib/scale.js +13 -0
  159. package/dist/charts/lib/scale.js.map +1 -0
  160. package/dist/charts/lib/selection.d.ts +20 -0
  161. package/dist/charts/lib/selection.js +18 -0
  162. package/dist/charts/lib/selection.js.map +1 -0
  163. package/dist/charts/lib/toolbar.d.ts +1 -0
  164. package/dist/components/Accordion/Accordion.js +8 -1
  165. package/dist/components/Accordion/Accordion.js.map +1 -1
  166. package/dist/components/AppShell/AppShell.d.ts +6 -0
  167. package/dist/components/AppShell/AppShell.js +34 -4
  168. package/dist/components/AppShell/AppShell.js.map +1 -1
  169. package/dist/components/Article/Article.d.ts +11 -4
  170. package/dist/components/Article/Article.js +13 -0
  171. package/dist/components/Article/Article.js.map +1 -1
  172. package/dist/components/AspectRatio/AspectRatio.d.ts +46 -0
  173. package/dist/components/AspectRatio/AspectRatio.js +33 -0
  174. package/dist/components/AspectRatio/AspectRatio.js.map +1 -0
  175. package/dist/components/Avatar/Avatar.d.ts +12 -3
  176. package/dist/components/Avatar/Avatar.js +6 -1
  177. package/dist/components/Avatar/Avatar.js.map +1 -1
  178. package/dist/components/Breadcrumb/Breadcrumb.d.ts +6 -0
  179. package/dist/components/Breadcrumb/Breadcrumb.js +15 -0
  180. package/dist/components/Breadcrumb/Breadcrumb.js.map +1 -1
  181. package/dist/components/Button/Button.js +14 -0
  182. package/dist/components/Button/Button.js.map +1 -1
  183. package/dist/components/Calendar/Calendar.d.ts +29 -3
  184. package/dist/components/Calendar/Calendar.js +60 -12
  185. package/dist/components/Calendar/Calendar.js.map +1 -1
  186. package/dist/components/Card/Card.d.ts +6 -0
  187. package/dist/components/Card/Card.js +1 -1
  188. package/dist/components/Card/Card.js.map +1 -1
  189. package/dist/components/Checkbox/Checkbox.d.ts +5 -2
  190. package/dist/components/Checkbox/Checkbox.js +12 -2
  191. package/dist/components/Checkbox/Checkbox.js.map +1 -1
  192. package/dist/components/Code/Code.d.ts +30 -0
  193. package/dist/components/Code/Code.js +21 -0
  194. package/dist/components/Code/Code.js.map +1 -0
  195. package/dist/components/CodeBlock/CodeBlock.d.ts +141 -0
  196. package/dist/components/CodeBlock/CodeBlock.js +123 -0
  197. package/dist/components/CodeBlock/CodeBlock.js.map +1 -0
  198. package/dist/components/Collapsible/Collapsible.js +10 -2
  199. package/dist/components/Collapsible/Collapsible.js.map +1 -1
  200. package/dist/components/Combobox/Combobox.d.ts +15 -1
  201. package/dist/components/Combobox/Combobox.js +25 -5
  202. package/dist/components/Combobox/Combobox.js.map +1 -1
  203. package/dist/components/Command/Command.d.ts +4 -2
  204. package/dist/components/Command/Command.js +13 -1
  205. package/dist/components/Command/Command.js.map +1 -1
  206. package/dist/components/ContextMenu/ContextMenu.d.ts +30 -5
  207. package/dist/components/ContextMenu/ContextMenu.js +32 -2
  208. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  209. package/dist/components/DatePicker/DatePicker.d.ts +36 -4
  210. package/dist/components/DatePicker/DatePicker.js +55 -12
  211. package/dist/components/DatePicker/DatePicker.js.map +1 -1
  212. package/dist/components/DescriptionList/DescriptionList.d.ts +73 -0
  213. package/dist/components/DescriptionList/DescriptionList.js +35 -0
  214. package/dist/components/DescriptionList/DescriptionList.js.map +1 -0
  215. package/dist/components/Diagram/Diagram.d.ts +8 -3
  216. package/dist/components/Diagram/Diagram.js +82 -10
  217. package/dist/components/Diagram/Diagram.js.map +1 -1
  218. package/dist/components/Dialog/Dialog.d.ts +18 -3
  219. package/dist/components/Dialog/Dialog.js +20 -2
  220. package/dist/components/Dialog/Dialog.js.map +1 -1
  221. package/dist/components/DropdownMenu/DropdownMenu.d.ts +49 -11
  222. package/dist/components/DropdownMenu/DropdownMenu.js +32 -2
  223. package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
  224. package/dist/components/EmptyState/EmptyState.d.ts +25 -1
  225. package/dist/components/EmptyState/EmptyState.js +3 -1
  226. package/dist/components/EmptyState/EmptyState.js.map +1 -1
  227. package/dist/components/ErrorState/ErrorState.d.ts +22 -1
  228. package/dist/components/ErrorState/ErrorState.js +11 -2
  229. package/dist/components/ErrorState/ErrorState.js.map +1 -1
  230. package/dist/components/Field/Field.d.ts +70 -3
  231. package/dist/components/Field/Field.js +53 -12
  232. package/dist/components/Field/Field.js.map +1 -1
  233. package/dist/components/Field/field-control.d.ts +31 -0
  234. package/dist/components/Field/field-control.js +12 -0
  235. package/dist/components/Field/field-control.js.map +1 -0
  236. package/dist/components/FloatingIconButton/FloatingIconButton.js +2 -0
  237. package/dist/components/FloatingIconButton/FloatingIconButton.js.map +1 -1
  238. package/dist/components/Heading/Heading.d.ts +62 -0
  239. package/dist/components/Heading/Heading.js +44 -0
  240. package/dist/components/Heading/Heading.js.map +1 -0
  241. package/dist/components/Markdown/Markdown.d.ts +139 -0
  242. package/dist/components/Markdown/Markdown.js +135 -0
  243. package/dist/components/Markdown/Markdown.js.map +1 -0
  244. package/dist/components/Markdown/inline.d.ts +29 -0
  245. package/dist/components/Markdown/inline.js +74 -0
  246. package/dist/components/Markdown/inline.js.map +1 -0
  247. package/dist/components/Markdown/nodes.d.ts +65 -0
  248. package/dist/components/Markdown/nodes.js +1 -0
  249. package/dist/components/Markdown/nodes.js.map +1 -0
  250. package/dist/components/Markdown/parse.d.ts +24 -0
  251. package/dist/components/Markdown/parse.js +110 -0
  252. package/dist/components/Markdown/parse.js.map +1 -0
  253. package/dist/components/Markdown/slug.d.ts +15 -0
  254. package/dist/components/Markdown/slug.js +7 -0
  255. package/dist/components/Markdown/slug.js.map +1 -0
  256. package/dist/components/NativeSelect/NativeSelect.d.ts +6 -0
  257. package/dist/components/NativeSelect/NativeSelect.js +2 -3
  258. package/dist/components/NativeSelect/NativeSelect.js.map +1 -1
  259. package/dist/components/Pagination/Pagination.d.ts +24 -1
  260. package/dist/components/Pagination/Pagination.js +6 -3
  261. package/dist/components/Pagination/Pagination.js.map +1 -1
  262. package/dist/components/Progress/Progress.d.ts +17 -5
  263. package/dist/components/Progress/Progress.js +18 -6
  264. package/dist/components/Progress/Progress.js.map +1 -1
  265. package/dist/components/RadioGroup/RadioGroup.d.ts +6 -0
  266. package/dist/components/RadioGroup/RadioGroup.js +11 -1
  267. package/dist/components/RadioGroup/RadioGroup.js.map +1 -1
  268. package/dist/components/ScrollArea/ScrollArea.d.ts +15 -0
  269. package/dist/components/ScrollArea/ScrollArea.js +2 -2
  270. package/dist/components/ScrollArea/ScrollArea.js.map +1 -1
  271. package/dist/components/SearchableMenu/SearchableMenu.d.ts +11 -2
  272. package/dist/components/SearchableMenu/SearchableMenu.js +24 -2
  273. package/dist/components/SearchableMenu/SearchableMenu.js.map +1 -1
  274. package/dist/components/Select/Select.d.ts +18 -2
  275. package/dist/components/Select/Select.js +20 -3
  276. package/dist/components/Select/Select.js.map +1 -1
  277. package/dist/components/Separator/Separator.d.ts +21 -3
  278. package/dist/components/Separator/Separator.js +9 -1
  279. package/dist/components/Separator/Separator.js.map +1 -1
  280. package/dist/components/Sheet/Sheet.d.ts +3 -0
  281. package/dist/components/Sheet/Sheet.js +11 -2
  282. package/dist/components/Sheet/Sheet.js.map +1 -1
  283. package/dist/components/Skeleton/Skeleton.d.ts +8 -1
  284. package/dist/components/Skeleton/Skeleton.js +1 -1
  285. package/dist/components/Skeleton/Skeleton.js.map +1 -1
  286. package/dist/components/Slider/Slider.d.ts +21 -3
  287. package/dist/components/Slider/Slider.js +54 -27
  288. package/dist/components/Slider/Slider.js.map +1 -1
  289. package/dist/components/Spinner/Spinner.d.ts +8 -0
  290. package/dist/components/Spinner/Spinner.js +3 -2
  291. package/dist/components/Spinner/Spinner.js.map +1 -1
  292. package/dist/components/StatusPill/StatusPill.d.ts +5 -0
  293. package/dist/components/StatusPill/StatusPill.js +6 -0
  294. package/dist/components/StatusPill/StatusPill.js.map +1 -1
  295. package/dist/components/Table/Table.d.ts +6 -0
  296. package/dist/components/Table/Table.js +3 -1
  297. package/dist/components/Table/Table.js.map +1 -1
  298. package/dist/components/Tag/Tag.d.ts +68 -9
  299. package/dist/components/Tag/Tag.js +50 -10
  300. package/dist/components/Tag/Tag.js.map +1 -1
  301. package/dist/components/Text/Text.d.ts +62 -0
  302. package/dist/components/Text/Text.js +30 -0
  303. package/dist/components/Text/Text.js.map +1 -0
  304. package/dist/components/Timestamp/Timestamp.d.ts +59 -0
  305. package/dist/components/Timestamp/Timestamp.js +56 -0
  306. package/dist/components/Timestamp/Timestamp.js.map +1 -0
  307. package/dist/components/Toast/Toast.d.ts +9 -1
  308. package/dist/components/Toast/Toast.js +27 -4
  309. package/dist/components/Toast/Toast.js.map +1 -1
  310. package/dist/components/ToggleGroup/ToggleGroup.d.ts +6 -1
  311. package/dist/components/ToggleGroup/ToggleGroup.js +11 -1
  312. package/dist/components/ToggleGroup/ToggleGroup.js.map +1 -1
  313. package/dist/components/Toolbar/Toolbar.d.ts +55 -0
  314. package/dist/components/Toolbar/Toolbar.js +43 -0
  315. package/dist/components/Toolbar/Toolbar.js.map +1 -0
  316. package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.d.ts +6 -0
  317. package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js +48 -8
  318. package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js.map +1 -1
  319. package/dist/diagrams/DataflowFigure/DataflowFigure.js +21 -2
  320. package/dist/diagrams/DataflowFigure/DataflowFigure.js.map +1 -1
  321. package/dist/diagrams/DiagramCanvas/DiagramCanvas.d.ts +14 -0
  322. package/dist/diagrams/DiagramCanvas/DiagramCanvas.js +14 -7
  323. package/dist/diagrams/DiagramCanvas/DiagramCanvas.js.map +1 -1
  324. package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.d.ts +38 -4
  325. package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js +14 -16
  326. package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js.map +1 -1
  327. package/dist/diagrams/DiagramInspector/DiagramInspector.js +2 -2
  328. package/dist/diagrams/DiagramInspector/DiagramInspector.js.map +1 -1
  329. package/dist/diagrams/DiagramMinimap/DiagramMinimap.d.ts +23 -7
  330. package/dist/diagrams/DiagramMinimap/DiagramMinimap.js +45 -18
  331. package/dist/diagrams/DiagramMinimap/DiagramMinimap.js.map +1 -1
  332. package/dist/diagrams/LifecycleFigure/LifecycleFigure.js +44 -12
  333. package/dist/diagrams/LifecycleFigure/LifecycleFigure.js.map +1 -1
  334. package/dist/diagrams/SequenceFigure/SequenceFigure.js +35 -5
  335. package/dist/diagrams/SequenceFigure/SequenceFigure.js.map +1 -1
  336. package/dist/diagrams/WorkflowFigure/WorkflowFigure.d.ts +3 -1
  337. package/dist/diagrams/WorkflowFigure/WorkflowFigure.js +76 -28
  338. package/dist/diagrams/WorkflowFigure/WorkflowFigure.js.map +1 -1
  339. package/dist/diagrams/index.d.ts +1 -0
  340. package/dist/diagrams/index.js +10 -0
  341. package/dist/diagrams/index.js.map +1 -1
  342. package/dist/diagrams/lib/dev.d.ts +61 -0
  343. package/dist/diagrams/lib/dev.js +96 -0
  344. package/dist/diagrams/lib/dev.js.map +1 -0
  345. package/dist/diagrams/lib/frame.d.ts +48 -7
  346. package/dist/diagrams/lib/frame.js +17 -2
  347. package/dist/diagrams/lib/frame.js.map +1 -1
  348. package/dist/diagrams/lib/wires.js.map +1 -1
  349. package/dist/diagrams/spec.d.ts +64 -12
  350. package/dist/index.d.ts +16 -4
  351. package/dist/index.js +9 -0
  352. package/dist/index.js.map +1 -1
  353. package/dist/keyframes.css +57 -3
  354. package/dist/lib/overlay-container.d.ts +14 -4
  355. package/dist/lib/overlay-container.js.map +1 -1
  356. package/dist/lib/warn.d.ts +73 -0
  357. package/dist/lib/warn.js +38 -0
  358. package/dist/lib/warn.js.map +1 -0
  359. package/dist/semantic.css +17 -6
  360. package/dist/styles.css +1 -1
  361. package/dist/tokens.css +20 -2
  362. package/dist/tokens.d.ts +1 -1
  363. package/dist/tokens.js +39 -36
  364. package/dist/tokens.json +39 -36
  365. package/llms.txt +35 -0
  366. package/package.json +5 -2
  367. package/skills/misoto22-design/evals/evals.json +84 -0
  368. package/skills/misoto22-design/rules/a11y.md +55 -3
  369. package/skills/misoto22-design/rules/forms.md +5 -0
@@ -10,6 +10,24 @@ interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, 'onChange'>
10
10
  /** How many numbered pages sit either side of the current one. */
11
11
  siblings?: number;
12
12
  label?: string;
13
+ /**
14
+ * Names the two step controls.
15
+ *
16
+ * Each is a chevron and nothing else, so these strings are its entire
17
+ * accessible name — the same reason `AppShell` exposes `openLabel` and
18
+ * `closeLabel` rather than baking them in.
19
+ */
20
+ previousLabel?: string;
21
+ nextLabel?: string;
22
+ /**
23
+ * Names one numbered page.
24
+ *
25
+ * A function rather than a template, because the number does not sit in the
26
+ * same place in every language and neither does the noun around it —
27
+ * `"Page 3"`, `"第 3 页"`, `"Seite 3"`. The visible digit is the same either
28
+ * way; this is what a screen reader hears instead of a bare number.
29
+ */
30
+ pageLabel?: (page: number) => string;
13
31
  }
14
32
  declare const GAP: "\u2026";
15
33
  /**
@@ -36,9 +54,14 @@ declare function paginationRange(page: number, pageCount: number, siblings?: num
36
54
  *
37
55
  * Renders nothing at one page or fewer. A pager for a single page is furniture.
38
56
  *
57
+ * Every string a reader hears is a prop. The chevrons carry no text, so
58
+ * `previousLabel` and `nextLabel` are the whole of those two controls' names,
59
+ * and `pageLabel` is a function because "Page 3" is a phrase whose parts move
60
+ * around between languages.
61
+ *
39
62
  * @example
40
63
  * <Pagination page={page} pageCount={12} onPageChange={setPage} />
41
64
  */
42
- declare function Pagination({ page, pageCount, onPageChange, siblings, label, className, ...rest }: PaginationProps): react.JSX.Element | null;
65
+ declare function Pagination({ page, pageCount, onPageChange, siblings, label, previousLabel, nextLabel, pageLabel, className, ...rest }: PaginationProps): react.JSX.Element | null;
43
66
 
44
67
  export { Pagination, type PaginationProps, Pagination as default, paginationRange };
@@ -34,6 +34,9 @@ function Pagination({
34
34
  onPageChange,
35
35
  siblings = 1,
36
36
  label = "Pagination",
37
+ previousLabel = "Previous page",
38
+ nextLabel = "Next page",
39
+ pageLabel = (page2) => `Page ${page2}`,
37
40
  className,
38
41
  ...rest
39
42
  }) {
@@ -45,7 +48,7 @@ function Pagination({
45
48
  "button",
46
49
  {
47
50
  type: "button",
48
- "aria-label": "Previous page",
51
+ "aria-label": previousLabel,
49
52
  disabled: page <= 1,
50
53
  onClick: () => onPageChange(page - 1),
51
54
  className: cn(STEP, "border-(--rule-2) text-(--ink-2) hover:border-(--ink) hover:text-(--ink)"),
@@ -74,7 +77,7 @@ function Pagination({
74
77
  {
75
78
  type: "button",
76
79
  "aria-current": entry === page ? "page" : void 0,
77
- "aria-label": `Page ${entry}`,
80
+ "aria-label": pageLabel(entry),
78
81
  "data-indicator-active": entry === page ? "true" : void 0,
79
82
  onClick: () => onPageChange(entry),
80
83
  className: cn(
@@ -91,7 +94,7 @@ function Pagination({
91
94
  "button",
92
95
  {
93
96
  type: "button",
94
- "aria-label": "Next page",
97
+ "aria-label": nextLabel,
95
98
  disabled: page >= pageCount,
96
99
  onClick: () => onPageChange(page + 1),
97
100
  className: cn(STEP, "border-(--rule-2) text-(--ink-2) hover:border-(--ink) hover:text-(--ink)"),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["'use client'\n\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport type { HTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\nimport { useSelectionIndicator } from '../../lib/useSelectionIndicator'\n\nexport interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {\n /** 1-based. */\n page: number\n /** Total pages. A value of 1 or less renders nothing. */\n pageCount: number\n onPageChange: (page: number) => void\n /** How many numbered pages sit either side of the current one. */\n siblings?: number\n label?: string\n}\n\nconst GAP = '…' as const\n\n/**\n * Builds the visible page list: always the first and last page, a window around\n * the current one, and an ellipsis wherever the sequence skips.\n *\n * Returned as numbers and a literal ellipsis rather than as pre-rendered nodes,\n * so the shape is testable without a DOM — the off-by-one at the window edges\n * is the whole difficulty of this component.\n */\nexport function paginationRange(page: number, pageCount: number, siblings = 1): (number | typeof GAP)[] {\n const first = 1\n const last = pageCount\n\n // Below the width the elided form would occupy — first, last, the window and\n // two ellipses — printing every page is both shorter and more useful. Without\n // this, a five-page list renders \"1 2 … 5\", which hides a page to save\n // nothing.\n const widest = 2 * siblings + 5\n if (pageCount <= widest) {\n return Array.from({ length: pageCount }, (_, index) => index + 1)\n }\n\n const start = Math.max(first, page - siblings)\n const end = Math.min(last, page + siblings)\n\n const pages: (number | typeof GAP)[] = []\n if (start > first) {\n pages.push(first)\n // A single skipped page is printed, not elided: \"1 … 3\" is longer than\n // \"1 2 3\" and tells the reader less.\n if (start > first + 2) pages.push(GAP)\n else if (start === first + 2) pages.push(first + 1)\n }\n for (let n = start; n <= end; n += 1) pages.push(n)\n if (end < last) {\n if (end < last - 2) pages.push(GAP)\n else if (end === last - 2) pages.push(last - 1)\n pages.push(last)\n }\n return pages\n}\n\nconst STEP =\n 'inline-flex size-(--control-h-sm) items-center justify-center rounded-(--radius-pill) border transition-colors duration-(--duration-fast) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none'\n\n/**\n * Numbered pagination.\n *\n * The current page is marked by one filled pill that TRAVELS between the\n * numbers rather than by a background switching off on one and on on another.\n * Two backgrounds cross-fading reads as two things changing; a shape moving\n * reads as the one thing that actually did.\n *\n * A `<nav>` wrapping a list, and the current page is a `<button aria-current>`\n * rather than a styled `<span>` — a reader jumping by landmark needs to find\n * the control, and a reader on the current page needs to be told they are\n * already there.\n *\n * Renders nothing at one page or fewer. A pager for a single page is furniture.\n *\n * @example\n * <Pagination page={page} pageCount={12} onPageChange={setPage} />\n */\nexport function Pagination({\n page,\n pageCount,\n onPageChange,\n siblings = 1,\n label = 'Pagination',\n className,\n ...rest\n}: PaginationProps) {\n // Called before the early return, because a hook cannot be conditional — and\n // keyed on the page so the pill re-measures when the elided window shifts and\n // the numbers under it change.\n const [listRef, indicator] = useSelectionIndicator<HTMLOListElement>(String(page))\n\n if (pageCount <= 1) return null\n const pages = paginationRange(page, pageCount, siblings)\n\n return (\n <nav aria-label={label} className={cn('flex items-center gap-1.5', className)} {...rest}>\n <button\n type=\"button\"\n aria-label=\"Previous page\"\n disabled={page <= 1}\n onClick={() => onPageChange(page - 1)}\n className={cn(STEP, 'border-(--rule-2) text-(--ink-2) hover:border-(--ink) hover:text-(--ink)')}\n >\n <ChevronLeft size={16} strokeWidth={1.5} aria-hidden />\n </button>\n\n <ol ref={listRef} className=\"relative m-0 flex list-none items-center gap-1 p-0\">\n {indicator.ready && (\n <span\n aria-hidden\n data-m22-animated\n className=\"absolute rounded-(--radius-pill) bg-(--accent) transition-[transform,width] duration-(--duration-base) ease-(--ease-out-expo) motion-reduce:transition-none\"\n style={{\n transform: `translate(${indicator.offset}px, ${indicator.top}px)`,\n width: indicator.width,\n height: indicator.height,\n insetInlineStart: 0,\n top: 0,\n }}\n />\n )}\n {pages.map((entry, index) =>\n entry === GAP ? (\n <li key={`gap-${index}`} aria-hidden=\"true\" className=\"px-1 mono-meta text-(--ink-3-aa)\">\n {GAP}\n </li>\n ) : (\n <li key={entry} className=\"relative z-1\">\n <button\n type=\"button\"\n aria-current={entry === page ? 'page' : undefined}\n aria-label={`Page ${entry}`}\n data-indicator-active={entry === page ? 'true' : undefined}\n onClick={() => onPageChange(entry)}\n className={cn(\n STEP,\n 'mono-meta tabular-nums',\n entry === page\n ? 'border-transparent text-(--accent-foreground)'\n : 'border-transparent text-(--ink-2) hover:border-(--rule-2) hover:text-(--ink)',\n )}\n >\n {entry}\n </button>\n </li>\n ),\n )}\n </ol>\n\n <button\n type=\"button\"\n aria-label=\"Next page\"\n disabled={page >= pageCount}\n onClick={() => onPageChange(page + 1)}\n className={cn(STEP, 'border-(--rule-2) text-(--ink-2) hover:border-(--ink) hover:text-(--ink)')}\n >\n <ChevronRight size={16} strokeWidth={1.5} aria-hidden />\n </button>\n </nav>\n )\n}\n\nexport default Pagination\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["'use client'\n\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport type { HTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\nimport { useSelectionIndicator } from '../../lib/useSelectionIndicator'\n\nexport interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {\n /** 1-based. */\n page: number\n /** Total pages. A value of 1 or less renders nothing. */\n pageCount: number\n onPageChange: (page: number) => void\n /** How many numbered pages sit either side of the current one. */\n siblings?: number\n label?: string\n /**\n * Names the two step controls.\n *\n * Each is a chevron and nothing else, so these strings are its entire\n * accessible name — the same reason `AppShell` exposes `openLabel` and\n * `closeLabel` rather than baking them in.\n */\n previousLabel?: string\n nextLabel?: string\n /**\n * Names one numbered page.\n *\n * A function rather than a template, because the number does not sit in the\n * same place in every language and neither does the noun around it —\n * `\"Page 3\"`, `\"第 3 页\"`, `\"Seite 3\"`. The visible digit is the same either\n * way; this is what a screen reader hears instead of a bare number.\n */\n pageLabel?: (page: number) => string\n}\n\nconst GAP = '…' as const\n\n/**\n * Builds the visible page list: always the first and last page, a window around\n * the current one, and an ellipsis wherever the sequence skips.\n *\n * Returned as numbers and a literal ellipsis rather than as pre-rendered nodes,\n * so the shape is testable without a DOM — the off-by-one at the window edges\n * is the whole difficulty of this component.\n */\nexport function paginationRange(page: number, pageCount: number, siblings = 1): (number | typeof GAP)[] {\n const first = 1\n const last = pageCount\n\n // Below the width the elided form would occupy — first, last, the window and\n // two ellipses — printing every page is both shorter and more useful. Without\n // this, a five-page list renders \"1 2 … 5\", which hides a page to save\n // nothing.\n const widest = 2 * siblings + 5\n if (pageCount <= widest) {\n return Array.from({ length: pageCount }, (_, index) => index + 1)\n }\n\n const start = Math.max(first, page - siblings)\n const end = Math.min(last, page + siblings)\n\n const pages: (number | typeof GAP)[] = []\n if (start > first) {\n pages.push(first)\n // A single skipped page is printed, not elided: \"1 … 3\" is longer than\n // \"1 2 3\" and tells the reader less.\n if (start > first + 2) pages.push(GAP)\n else if (start === first + 2) pages.push(first + 1)\n }\n for (let n = start; n <= end; n += 1) pages.push(n)\n if (end < last) {\n if (end < last - 2) pages.push(GAP)\n else if (end === last - 2) pages.push(last - 1)\n pages.push(last)\n }\n return pages\n}\n\nconst STEP =\n 'inline-flex size-(--control-h-sm) items-center justify-center rounded-(--radius-pill) border transition-colors duration-(--duration-fast) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none'\n\n/**\n * Numbered pagination.\n *\n * The current page is marked by one filled pill that TRAVELS between the\n * numbers rather than by a background switching off on one and on on another.\n * Two backgrounds cross-fading reads as two things changing; a shape moving\n * reads as the one thing that actually did.\n *\n * A `<nav>` wrapping a list, and the current page is a `<button aria-current>`\n * rather than a styled `<span>` — a reader jumping by landmark needs to find\n * the control, and a reader on the current page needs to be told they are\n * already there.\n *\n * Renders nothing at one page or fewer. A pager for a single page is furniture.\n *\n * Every string a reader hears is a prop. The chevrons carry no text, so\n * `previousLabel` and `nextLabel` are the whole of those two controls' names,\n * and `pageLabel` is a function because \"Page 3\" is a phrase whose parts move\n * around between languages.\n *\n * @example\n * <Pagination page={page} pageCount={12} onPageChange={setPage} />\n */\nexport function Pagination({\n page,\n pageCount,\n onPageChange,\n siblings = 1,\n label = 'Pagination',\n previousLabel = 'Previous page',\n nextLabel = 'Next page',\n pageLabel = (page) => `Page ${page}`,\n className,\n ...rest\n}: PaginationProps) {\n // Called before the early return, because a hook cannot be conditional — and\n // keyed on the page so the pill re-measures when the elided window shifts and\n // the numbers under it change.\n const [listRef, indicator] = useSelectionIndicator<HTMLOListElement>(String(page))\n\n if (pageCount <= 1) return null\n const pages = paginationRange(page, pageCount, siblings)\n\n return (\n <nav aria-label={label} className={cn('flex items-center gap-1.5', className)} {...rest}>\n <button\n type=\"button\"\n aria-label={previousLabel}\n disabled={page <= 1}\n onClick={() => onPageChange(page - 1)}\n className={cn(STEP, 'border-(--rule-2) text-(--ink-2) hover:border-(--ink) hover:text-(--ink)')}\n >\n <ChevronLeft size={16} strokeWidth={1.5} aria-hidden />\n </button>\n\n <ol ref={listRef} className=\"relative m-0 flex list-none items-center gap-1 p-0\">\n {indicator.ready && (\n <span\n aria-hidden\n data-m22-animated\n className=\"absolute rounded-(--radius-pill) bg-(--accent) transition-[transform,width] duration-(--duration-base) ease-(--ease-out-expo) motion-reduce:transition-none\"\n style={{\n transform: `translate(${indicator.offset}px, ${indicator.top}px)`,\n width: indicator.width,\n height: indicator.height,\n insetInlineStart: 0,\n top: 0,\n }}\n />\n )}\n {pages.map((entry, index) =>\n entry === GAP ? (\n <li key={`gap-${index}`} aria-hidden=\"true\" className=\"px-1 mono-meta text-(--ink-3-aa)\">\n {GAP}\n </li>\n ) : (\n <li key={entry} className=\"relative z-1\">\n <button\n type=\"button\"\n aria-current={entry === page ? 'page' : undefined}\n aria-label={pageLabel(entry)}\n data-indicator-active={entry === page ? 'true' : undefined}\n onClick={() => onPageChange(entry)}\n className={cn(\n STEP,\n 'mono-meta tabular-nums',\n entry === page\n ? 'border-transparent text-(--accent-foreground)'\n : 'border-transparent text-(--ink-2) hover:border-(--rule-2) hover:text-(--ink)',\n )}\n >\n {entry}\n </button>\n </li>\n ),\n )}\n </ol>\n\n <button\n type=\"button\"\n aria-label={nextLabel}\n disabled={page >= pageCount}\n onClick={() => onPageChange(page + 1)}\n className={cn(STEP, 'border-(--rule-2) text-(--ink-2) hover:border-(--ink) hover:text-(--ink)')}\n >\n <ChevronRight size={16} strokeWidth={1.5} aria-hidden />\n </button>\n </nav>\n )\n}\n\nexport default Pagination\n"],"mappings":"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","names":["page"]}
@@ -2,13 +2,25 @@ import * as react from 'react';
2
2
  import { ComponentProps } from 'react';
3
3
  import * as ProgressPrimitive from '@radix-ui/react-progress';
4
4
 
5
- interface ProgressProps extends Omit<ComponentProps<typeof ProgressPrimitive.Root>, 'value'> {
5
+ interface ProgressProps extends Omit<ComponentProps<typeof ProgressPrimitive.Root>, 'value' | 'max'> {
6
6
  /**
7
- * 0100. Omit (or pass `null`) when the duration is genuinely unknown — the
8
- * bar then sweeps instead of filling, and Radix drops `aria-valuenow` so a
9
- * screen reader is told "indeterminate" rather than a number that is a guess.
7
+ * 0 to `max`, which is 100 unless you say otherwise. Omit (or pass `null`)
8
+ * when the duration is genuinely unknown the bar then sweeps instead of
9
+ * filling, and Radix drops `aria-valuenow` so a screen reader is told
10
+ * "indeterminate" rather than a number that is a guess.
10
11
  */
11
12
  value?: number | null;
13
+ /**
14
+ * What `value` is measured against. 100 by default.
15
+ *
16
+ * The width is computed from it as well as announced from it. It used to be
17
+ * only announced: `max` reached Radix through `...rest` while the width was
18
+ * `value` clamped to 100, so `max={500}` with `value={100}` painted a full
19
+ * bar and told a screen reader "100 of 500". A max that is not a positive
20
+ * number is refused the way Radix refuses it — Radix warns, and both fall
21
+ * back to 100 rather than drawing one picture and announcing another.
22
+ */
23
+ max?: number;
12
24
  /** Names what is progressing. Required: a bare bar announces nothing. */
13
25
  label: string;
14
26
  /** Prints the percentage above the bar. Only meaningful when `value` is set. */
@@ -26,6 +38,6 @@ interface ProgressProps extends Omit<ComponentProps<typeof ProgressPrimitive.Roo
26
38
  * @example
27
39
  * <Progress label="Indexing" />
28
40
  */
29
- declare function Progress({ value, label, showValue, className, ...rest }: ProgressProps): react.JSX.Element;
41
+ declare function Progress({ value, max, label, showValue, className, ...rest }: ProgressProps): react.JSX.Element;
30
42
 
31
43
  export { Progress, type ProgressProps, Progress as default };
@@ -2,21 +2,33 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as ProgressPrimitive from "@radix-ui/react-progress";
4
4
  import { cn } from "../../lib/cn.js";
5
- function Progress({ value = null, label, showValue = false, className, ...rest }) {
5
+ import { warnBlankName } from "../../lib/warn.js";
6
+ function Progress({
7
+ value = null,
8
+ max,
9
+ label,
10
+ showValue = false,
11
+ className,
12
+ ...rest
13
+ }) {
14
+ warnBlankName("Progress", "label", label, "the progressbar is announced with no name and its percentage describes nothing");
6
15
  const indeterminate = value === null || value === void 0;
7
- const clamped = indeterminate ? 0 : Math.min(100, Math.max(0, value));
16
+ const ceiling = typeof max === "number" && !Number.isNaN(max) && max > 0 ? max : 100;
17
+ const bounded = indeterminate ? 0 : Math.min(ceiling, Math.max(0, value));
18
+ const percent = bounded / ceiling * 100;
8
19
  return /* @__PURE__ */ jsxs("div", { className: cn("flex w-full flex-col gap-2", className), children: [
9
20
  showValue && !indeterminate && /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between mono-meta text-(--ink-3-aa)", children: [
10
21
  /* @__PURE__ */ jsx("span", { children: label }),
11
22
  /* @__PURE__ */ jsxs("span", { className: "tabular-nums", children: [
12
- Math.round(clamped),
23
+ Math.round(percent),
13
24
  "%"
14
25
  ] })
15
26
  ] }),
16
27
  /* @__PURE__ */ jsx(
17
28
  ProgressPrimitive.Root,
18
29
  {
19
- value: indeterminate ? null : clamped,
30
+ value: indeterminate ? null : bounded,
31
+ max,
20
32
  "aria-label": label,
21
33
  className: "relative h-1 w-full overflow-hidden rounded-(--radius-pill) bg-(--stone)",
22
34
  ...rest,
@@ -24,13 +36,13 @@ function Progress({ value = null, label, showValue = false, className, ...rest }
24
36
  "span",
25
37
  {
26
38
  "data-m22-animated": true,
27
- className: "absolute inset-y-0 start-0 w-1/4 rounded-(--radius-pill) bg-(--accent) rtl:-scale-x-100 motion-safe:animate-[m22-sweep_1.4s_var(--ease)_infinite] motion-reduce:w-full motion-reduce:opacity-40"
39
+ className: "absolute inset-y-0 start-0 w-1/4 rounded-(--radius-pill) bg-(--accent) rtl:-scale-x-100 motion-safe:animate-[m22-sweep_1.4s_var(--ease)_infinite]"
28
40
  }
29
41
  ) : /* @__PURE__ */ jsx(
30
42
  ProgressPrimitive.Indicator,
31
43
  {
32
44
  className: "h-full rounded-(--radius-pill) bg-(--accent) transition-[width] duration-(--duration-slow) ease-(--ease-out-expo)",
33
- style: { width: `${clamped}%` }
45
+ style: { width: `${percent}%` }
34
46
  }
35
47
  )
36
48
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/Progress/Progress.tsx"],"sourcesContent":["'use client'\n\nimport * as ProgressPrimitive from '@radix-ui/react-progress'\nimport type { ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface ProgressProps\n extends Omit<ComponentProps<typeof ProgressPrimitive.Root>, 'value'> {\n /**\n * 0100. Omit (or pass `null`) when the duration is genuinely unknown — the\n * bar then sweeps instead of filling, and Radix drops `aria-valuenow` so a\n * screen reader is told \"indeterminate\" rather than a number that is a guess.\n */\n value?: number | null\n /** Names what is progressing. Required: a bare bar announces nothing. */\n label: string\n /** Prints the percentage above the bar. Only meaningful when `value` is set. */\n showValue?: boolean\n}\n\n/**\n * A bar that fills, or sweeps when the end is unknown.\n *\n * Flat: a track in `--stone`, a fill in `--ink`. The White Reset has no\n * gradient and no glow, so the only thing carrying the reading is the boundary\n * between the two.\n *\n * @example\n * <Progress value={62} label=\"Uploading photos\" showValue />\n * @example\n * <Progress label=\"Indexing\" />\n */\nexport function Progress({ value = null, label, showValue = false, className, ...rest }: ProgressProps) {\n const indeterminate = value === null || value === undefined\n const clamped = indeterminate ? 0 : Math.min(100, Math.max(0, value))\n\n return (\n <div className={cn('flex w-full flex-col gap-2', className)}>\n {showValue && !indeterminate && (\n <div className=\"flex items-baseline justify-between mono-meta text-(--ink-3-aa)\">\n <span>{label}</span>\n <span className=\"tabular-nums\">{Math.round(clamped)}%</span>\n </div>\n )}\n <ProgressPrimitive.Root\n value={indeterminate ? null : clamped}\n aria-label={label}\n className=\"relative h-1 w-full overflow-hidden rounded-(--radius-pill) bg-(--stone)\"\n {...rest}\n >\n {indeterminate ? (\n <span\n data-m22-animated\n // A quarter-width bar travelling the track. `transform` only, so it\n // runs on the compositor and never triggers layout. `rtl:-scale-x-100`\n // mirrors the whole motion rather than needing a second keyframe:\n // in a right-to-left document the sweep has to run the other way, or\n // it reads as progress running backwards.\n className=\"absolute inset-y-0 start-0 w-1/4 rounded-(--radius-pill) bg-(--accent) rtl:-scale-x-100 motion-safe:animate-[m22-sweep_1.4s_var(--ease)_infinite] motion-reduce:w-full motion-reduce:opacity-40\"\n />\n ) : (\n <ProgressPrimitive.Indicator\n // Width, not a translate. A `translateX(-N%)` fill has to be negated\n // in a right-to-left document, and an inline style cannot carry a\n // direction variant — so the bar grew from the wrong edge. Width\n // grows from the inline start on its own, in either direction.\n className=\"h-full rounded-(--radius-pill) bg-(--accent) transition-[width] duration-(--duration-slow) ease-(--ease-out-expo)\"\n style={{ width: `${clamped}%` }}\n />\n )}\n </ProgressPrimitive.Root>\n </div>\n )\n}\n\nexport default Progress\n"],"mappings":";AAwCU,cACA,YADA;AAtCV,YAAY,uBAAuB;AAEnC,SAAS,UAAU;AA4BZ,SAAS,SAAS,EAAE,QAAQ,MAAM,OAAO,YAAY,OAAO,WAAW,GAAG,KAAK,GAAkB;AACtG,QAAM,gBAAgB,UAAU,QAAQ,UAAU;AAClD,QAAM,UAAU,gBAAgB,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,KAAK,CAAC;AAEpE,SACE,qBAAC,SAAI,WAAW,GAAG,8BAA8B,SAAS,GACvD;AAAA,iBAAa,CAAC,iBACb,qBAAC,SAAI,WAAU,mEACb;AAAA,0BAAC,UAAM,iBAAM;AAAA,MACb,qBAAC,UAAK,WAAU,gBAAgB;AAAA,aAAK,MAAM,OAAO;AAAA,QAAE;AAAA,SAAC;AAAA,OACvD;AAAA,IAEF;AAAA,MAAC,kBAAkB;AAAA,MAAlB;AAAA,QACC,OAAO,gBAAgB,OAAO;AAAA,QAC9B,cAAY;AAAA,QACZ,WAAU;AAAA,QACT,GAAG;AAAA,QAEH,0BACC;AAAA,UAAC;AAAA;AAAA,YACC,qBAAiB;AAAA,YAMjB,WAAU;AAAA;AAAA,QACZ,IAEA;AAAA,UAAC,kBAAkB;AAAA,UAAlB;AAAA,YAKC,WAAU;AAAA,YACV,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI;AAAA;AAAA,QAChC;AAAA;AAAA,IAEJ;AAAA,KACF;AAEJ;AAEA,IAAO,mBAAQ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/Progress/Progress.tsx"],"sourcesContent":["'use client'\n\nimport * as ProgressPrimitive from '@radix-ui/react-progress'\nimport type { ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\nimport { warnBlankName } from '../../lib/warn'\n\nexport interface ProgressProps\n extends Omit<ComponentProps<typeof ProgressPrimitive.Root>, 'value' | 'max'> {\n /**\n * 0 to `max`, which is 100 unless you say otherwise. Omit (or pass `null`)\n * when the duration is genuinely unknown — the bar then sweeps instead of\n * filling, and Radix drops `aria-valuenow` so a screen reader is told\n * \"indeterminate\" rather than a number that is a guess.\n */\n value?: number | null\n /**\n * What `value` is measured against. 100 by default.\n *\n * The width is computed from it as well as announced from it. It used to be\n * only announced: `max` reached Radix through `...rest` while the width was\n * `value` clamped to 100, so `max={500}` with `value={100}` painted a full\n * bar and told a screen reader \"100 of 500\". A max that is not a positive\n * number is refused the way Radix refuses it — Radix warns, and both fall\n * back to 100 rather than drawing one picture and announcing another.\n */\n max?: number\n /** Names what is progressing. Required: a bare bar announces nothing. */\n label: string\n /** Prints the percentage above the bar. Only meaningful when `value` is set. */\n showValue?: boolean\n}\n\n/**\n * A bar that fills, or sweeps when the end is unknown.\n *\n * Flat: a track in `--stone`, a fill in `--ink`. The White Reset has no\n * gradient and no glow, so the only thing carrying the reading is the boundary\n * between the two.\n *\n * @example\n * <Progress value={62} label=\"Uploading photos\" showValue />\n * @example\n * <Progress label=\"Indexing\" />\n */\nexport function Progress({\n value = null,\n max,\n label,\n showValue = false,\n className,\n ...rest\n}: ProgressProps) {\n warnBlankName('Progress', 'label', label, 'the progressbar is announced with no name and its percentage describes nothing')\n const indeterminate = value === null || value === undefined\n // Radix's own test for a usable max, so the fallback below is the same\n // fallback it applies — and `max` is forwarded raw, so Radix still prints the\n // warning rather than this component swallowing a bad prop quietly.\n const ceiling = typeof max === 'number' && !Number.isNaN(max) && max > 0 ? max : 100\n const bounded = indeterminate ? 0 : Math.min(ceiling, Math.max(0, value))\n const percent = (bounded / ceiling) * 100\n\n return (\n <div className={cn('flex w-full flex-col gap-2', className)}>\n {showValue && !indeterminate && (\n <div className=\"flex items-baseline justify-between mono-meta text-(--ink-3-aa)\">\n <span>{label}</span>\n <span className=\"tabular-nums\">{Math.round(percent)}%</span>\n </div>\n )}\n <ProgressPrimitive.Root\n value={indeterminate ? null : bounded}\n max={max}\n aria-label={label}\n className=\"relative h-1 w-full overflow-hidden rounded-(--radius-pill) bg-(--stone)\"\n {...rest}\n >\n {indeterminate ? (\n <span\n data-m22-animated\n // A quarter-width bar travelling the track. `transform` only, so it\n // runs on the compositor and never triggers layout. `rtl:-scale-x-100`\n // mirrors the whole motion rather than needing a second keyframe:\n // in a right-to-left document the sweep has to run the other way, or\n // it reads as progress running backwards.\n //\n // Nothing overrides it under `motion-reduce`, and that is the fix\n // rather than an omission: it used to become `w-full` at 40%\n // opacity, which is the shape of a FINISHED bar at rest, so a\n // reader who asked for less motion was told the operation was done.\n // Still, it is a quarter of the track at the inline start — the\n // same answer Spinner gives, where a ring that has stopped turning\n // still reads as unfinished because it is not a whole circle.\n className=\"absolute inset-y-0 start-0 w-1/4 rounded-(--radius-pill) bg-(--accent) rtl:-scale-x-100 motion-safe:animate-[m22-sweep_1.4s_var(--ease)_infinite]\"\n />\n ) : (\n <ProgressPrimitive.Indicator\n // Width, not a translate. A `translateX(-N%)` fill has to be negated\n // in a right-to-left document, and an inline style cannot carry a\n // direction variant — so the bar grew from the wrong edge. Width\n // grows from the inline start on its own, in either direction.\n className=\"h-full rounded-(--radius-pill) bg-(--accent) transition-[width] duration-(--duration-slow) ease-(--ease-out-expo)\"\n style={{ width: `${percent}%` }}\n />\n )}\n </ProgressPrimitive.Root>\n </div>\n )\n}\n\nexport default Progress\n"],"mappings":";AAkEU,cACA,YADA;AAhEV,YAAY,uBAAuB;AAEnC,SAAS,UAAU;AACnB,SAAS,qBAAqB;AAwCvB,SAAS,SAAS;AAAA,EACvB,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,gBAAc,YAAY,SAAS,OAAO,gFAAgF;AAC1H,QAAM,gBAAgB,UAAU,QAAQ,UAAU;AAIlD,QAAM,UAAU,OAAO,QAAQ,YAAY,CAAC,OAAO,MAAM,GAAG,KAAK,MAAM,IAAI,MAAM;AACjF,QAAM,UAAU,gBAAgB,IAAI,KAAK,IAAI,SAAS,KAAK,IAAI,GAAG,KAAK,CAAC;AACxE,QAAM,UAAW,UAAU,UAAW;AAEtC,SACE,qBAAC,SAAI,WAAW,GAAG,8BAA8B,SAAS,GACvD;AAAA,iBAAa,CAAC,iBACb,qBAAC,SAAI,WAAU,mEACb;AAAA,0BAAC,UAAM,iBAAM;AAAA,MACb,qBAAC,UAAK,WAAU,gBAAgB;AAAA,aAAK,MAAM,OAAO;AAAA,QAAE;AAAA,SAAC;AAAA,OACvD;AAAA,IAEF;AAAA,MAAC,kBAAkB;AAAA,MAAlB;AAAA,QACC,OAAO,gBAAgB,OAAO;AAAA,QAC9B;AAAA,QACA,cAAY;AAAA,QACZ,WAAU;AAAA,QACT,GAAG;AAAA,QAEH,0BACC;AAAA,UAAC;AAAA;AAAA,YACC,qBAAiB;AAAA,YAcjB,WAAU;AAAA;AAAA,QACZ,IAEA;AAAA,UAAC,kBAAkB;AAAA,UAAlB;AAAA,YAKC,WAAU;AAAA,YACV,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI;AAAA;AAAA,QAChC;AAAA;AAAA,IAEJ;AAAA,KACF;AAEJ;AAEA,IAAO,mBAAQ;","names":[]}
@@ -11,6 +11,12 @@ type RadioGroupProps = ComponentProps<typeof RadioGroupPrimitive.Root>;
11
11
  * requires and what a stack of hand-rolled `<input type="radio">` wrappers
12
12
  * usually gets wrong.
13
13
  *
14
+ * Inside a `Field` the group takes its name from that label, by pointing back
15
+ * at it: the root is a `<div role="radiogroup">` and `<label for>` does not bind
16
+ * to one, so the words above it click through no more than a `<legend>` does.
17
+ * Standing alone, it still needs an `aria-label` of its own — an unnamed group
18
+ * is three unlabelled radios.
19
+ *
14
20
  * @example
15
21
  * <RadioGroup defaultValue="light" aria-label="Theme">
16
22
  * <RadioGroupItem value="light">Light</RadioGroupItem>
@@ -3,9 +3,19 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
4
4
  import { useEffect, useRef } from "react";
5
5
  import { cn } from "../../lib/cn.js";
6
+ import { useFieldControl } from "../Field/field-control.js";
6
7
  const ARROW_GRACE_MS = 300;
7
8
  function RadioGroup({ className, ...props }) {
8
- return /* @__PURE__ */ jsx(RadioGroupPrimitive.Root, { className: cn("flex flex-col gap-2.5", className), ...props });
9
+ const field = useFieldControl();
10
+ const named = props["aria-label"] != null || props["aria-labelledby"] != null;
11
+ return /* @__PURE__ */ jsx(
12
+ RadioGroupPrimitive.Root,
13
+ {
14
+ "aria-labelledby": named ? void 0 : field?.labelId,
15
+ className: cn("flex flex-col gap-2.5", className),
16
+ ...props
17
+ }
18
+ );
9
19
  }
10
20
  function RadioGroupItem({ children, className, id, onFocus, ...props }) {
11
21
  const arrowPressedAt = useRef(0);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/RadioGroup/RadioGroup.tsx"],"sourcesContent":["'use client'\n\nimport * as RadioGroupPrimitive from '@radix-ui/react-radio-group'\nimport { useEffect, useRef, type ComponentProps, type FocusEvent, type ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\n/**\n * How recently an arrow key was pressed, in milliseconds, for selection to\n * still count as \"caused by\" it.\n *\n * A boolean cleared on `keyup` — which is how the upstream primitive tracks\n * this — loses the race: the roving focus moves through a React state update,\n * and by the time the commit lands and the element takes focus, a normal\n * keypress has already released. The result is a group whose arrow keys move\n * the outline and select nothing, which is half the ARIA radiogroup pattern\n * missing. A timestamp cannot be cleared out from under the focus handler.\n */\nconst ARROW_GRACE_MS = 300\n\nexport type RadioGroupProps = ComponentProps<typeof RadioGroupPrimitive.Root>\n\n/**\n * A set of mutually exclusive choices.\n *\n * Radix owns the roving tabindex, so the whole group is ONE tab stop and the\n * arrow keys move between options — which is what the ARIA radiogroup pattern\n * requires and what a stack of hand-rolled `<input type=\"radio\">` wrappers\n * usually gets wrong.\n *\n * @example\n * <RadioGroup defaultValue=\"light\" aria-label=\"Theme\">\n * <RadioGroupItem value=\"light\">Light</RadioGroupItem>\n * <RadioGroupItem value=\"dark\">Dark</RadioGroupItem>\n * </RadioGroup>\n */\nexport function RadioGroup({ className, ...props }: RadioGroupProps) {\n return <RadioGroupPrimitive.Root className={cn('flex flex-col gap-2.5', className)} {...props} />\n}\n\nexport interface RadioGroupItemProps\n extends Omit<ComponentProps<typeof RadioGroupPrimitive.Item>, 'children'> {\n /** The visible label. Rendered inside the `<label>` that wraps the control. */\n children: ReactNode\n}\n\n/**\n * One option, and its label, as a single click target.\n *\n * The `<label>` wraps both, so the whole row is clickable — a bare 18px circle\n * is below every pointer-target guideline and is miserable on a phone. It is\n * also what gives the control its accessible name: remove the wrapper and the\n * radio has no name at all.\n *\n * Selection follows focus, which is the half of the pattern that makes a\n * radiogroup usable from the keyboard: moving to an option chooses it, so\n * nobody has to press an extra key to commit. That is implemented here rather\n * than inherited, because the upstream primitive gates it on a flag cleared by\n * `keyup` and loses the race against its own focus move — see ARROW_GRACE_MS.\n * Re-selecting an already-selected option is a no-op, so this stays correct\n * even where the upstream path does fire.\n */\nexport function RadioGroupItem({ children, className, id, onFocus, ...props }: RadioGroupItemProps) {\n const arrowPressedAt = useRef(0)\n\n useEffect(() => {\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key.startsWith('Arrow')) arrowPressedAt.current = Date.now()\n }\n document.addEventListener('keydown', onKeyDown)\n return () => document.removeEventListener('keydown', onKeyDown)\n }, [])\n\n const selectOnArrowFocus = (event: FocusEvent<HTMLButtonElement>) => {\n onFocus?.(event)\n if (Date.now() - arrowPressedAt.current < ARROW_GRACE_MS) event.currentTarget.click()\n }\n\n return (\n <label className={cn('flex cursor-pointer items-center gap-2.5 text-sm text-(--ink-2)', className)}>\n <RadioGroupPrimitive.Item\n id={id}\n onFocus={selectOnArrowFocus}\n className=\"grid size-[18px] shrink-0 place-items-center rounded-full border border-(--rule-2) bg-(--paper) transition-colors duration-(--duration-fast) data-[state=checked]:border-(--accent) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none\"\n {...props}\n >\n <RadioGroupPrimitive.Indicator className=\"size-2.5 rounded-full bg-(--accent)\" />\n </RadioGroupPrimitive.Item>\n {children}\n </label>\n )\n}\n\nexport default RadioGroup\n"],"mappings":";AAoCS,cA0CL,YA1CK;AAlCT,YAAY,yBAAyB;AACrC,SAAS,WAAW,cAAoE;AACxF,SAAS,UAAU;AAanB,MAAM,iBAAiB;AAkBhB,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AACnE,SAAO,oBAAC,oBAAoB,MAApB,EAAyB,WAAW,GAAG,yBAAyB,SAAS,GAAI,GAAG,OAAO;AACjG;AAwBO,SAAS,eAAe,EAAE,UAAU,WAAW,IAAI,SAAS,GAAG,MAAM,GAAwB;AAClG,QAAM,iBAAiB,OAAO,CAAC;AAE/B,YAAU,MAAM;AACd,UAAM,YAAY,CAAC,UAAyB;AAC1C,UAAI,MAAM,IAAI,WAAW,OAAO,EAAG,gBAAe,UAAU,KAAK,IAAI;AAAA,IACvE;AACA,aAAS,iBAAiB,WAAW,SAAS;AAC9C,WAAO,MAAM,SAAS,oBAAoB,WAAW,SAAS;AAAA,EAChE,GAAG,CAAC,CAAC;AAEL,QAAM,qBAAqB,CAAC,UAAyC;AACnE,cAAU,KAAK;AACf,QAAI,KAAK,IAAI,IAAI,eAAe,UAAU,eAAgB,OAAM,cAAc,MAAM;AAAA,EACtF;AAEA,SACE,qBAAC,WAAM,WAAW,GAAG,mEAAmE,SAAS,GAC/F;AAAA;AAAA,MAAC,oBAAoB;AAAA,MAApB;AAAA,QACC;AAAA,QACA,SAAS;AAAA,QACT,WAAU;AAAA,QACT,GAAG;AAAA,QAEJ,8BAAC,oBAAoB,WAApB,EAA8B,WAAU,uCAAsC;AAAA;AAAA,IACjF;AAAA,IACC;AAAA,KACH;AAEJ;AAEA,IAAO,qBAAQ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/RadioGroup/RadioGroup.tsx"],"sourcesContent":["'use client'\n\nimport * as RadioGroupPrimitive from '@radix-ui/react-radio-group'\nimport { useEffect, useRef, type ComponentProps, type FocusEvent, type ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport { useFieldControl } from '../Field/field-control'\n\n/**\n * How recently an arrow key was pressed, in milliseconds, for selection to\n * still count as \"caused by\" it.\n *\n * A boolean cleared on `keyup` — which is how the upstream primitive tracks\n * this — loses the race: the roving focus moves through a React state update,\n * and by the time the commit lands and the element takes focus, a normal\n * keypress has already released. The result is a group whose arrow keys move\n * the outline and select nothing, which is half the ARIA radiogroup pattern\n * missing. A timestamp cannot be cleared out from under the focus handler.\n */\nconst ARROW_GRACE_MS = 300\n\nexport type RadioGroupProps = ComponentProps<typeof RadioGroupPrimitive.Root>\n\n/**\n * A set of mutually exclusive choices.\n *\n * Radix owns the roving tabindex, so the whole group is ONE tab stop and the\n * arrow keys move between options — which is what the ARIA radiogroup pattern\n * requires and what a stack of hand-rolled `<input type=\"radio\">` wrappers\n * usually gets wrong.\n *\n * Inside a `Field` the group takes its name from that label, by pointing back\n * at it: the root is a `<div role=\"radiogroup\">` and `<label for>` does not bind\n * to one, so the words above it click through no more than a `<legend>` does.\n * Standing alone, it still needs an `aria-label` of its own — an unnamed group\n * is three unlabelled radios.\n *\n * @example\n * <RadioGroup defaultValue=\"light\" aria-label=\"Theme\">\n * <RadioGroupItem value=\"light\">Light</RadioGroupItem>\n * <RadioGroupItem value=\"dark\">Dark</RadioGroupItem>\n * </RadioGroup>\n */\nexport function RadioGroup({ className, ...props }: RadioGroupProps) {\n const field = useFieldControl()\n const named = props['aria-label'] != null || props['aria-labelledby'] != null\n\n return (\n <RadioGroupPrimitive.Root\n aria-labelledby={named ? undefined : field?.labelId}\n className={cn('flex flex-col gap-2.5', className)}\n {...props}\n />\n )\n}\n\nexport interface RadioGroupItemProps\n extends Omit<ComponentProps<typeof RadioGroupPrimitive.Item>, 'children'> {\n /** The visible label. Rendered inside the `<label>` that wraps the control. */\n children: ReactNode\n}\n\n/**\n * One option, and its label, as a single click target.\n *\n * The `<label>` wraps both, so the whole row is clickable — a bare 18px circle\n * is below every pointer-target guideline and is miserable on a phone. It is\n * also what gives the control its accessible name: remove the wrapper and the\n * radio has no name at all.\n *\n * Selection follows focus, which is the half of the pattern that makes a\n * radiogroup usable from the keyboard: moving to an option chooses it, so\n * nobody has to press an extra key to commit. That is implemented here rather\n * than inherited, because the upstream primitive gates it on a flag cleared by\n * `keyup` and loses the race against its own focus move — see ARROW_GRACE_MS.\n * Re-selecting an already-selected option is a no-op, so this stays correct\n * even where the upstream path does fire.\n */\nexport function RadioGroupItem({ children, className, id, onFocus, ...props }: RadioGroupItemProps) {\n const arrowPressedAt = useRef(0)\n\n useEffect(() => {\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key.startsWith('Arrow')) arrowPressedAt.current = Date.now()\n }\n document.addEventListener('keydown', onKeyDown)\n return () => document.removeEventListener('keydown', onKeyDown)\n }, [])\n\n const selectOnArrowFocus = (event: FocusEvent<HTMLButtonElement>) => {\n onFocus?.(event)\n if (Date.now() - arrowPressedAt.current < ARROW_GRACE_MS) event.currentTarget.click()\n }\n\n return (\n <label className={cn('flex cursor-pointer items-center gap-2.5 text-sm text-(--ink-2)', className)}>\n <RadioGroupPrimitive.Item\n id={id}\n onFocus={selectOnArrowFocus}\n className=\"grid size-[18px] shrink-0 place-items-center rounded-full border border-(--rule-2) bg-(--paper) transition-colors duration-(--duration-fast) data-[state=checked]:border-(--accent) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none\"\n {...props}\n >\n <RadioGroupPrimitive.Indicator className=\"size-2.5 rounded-full bg-(--accent)\" />\n </RadioGroupPrimitive.Item>\n {children}\n </label>\n )\n}\n\nexport default RadioGroup\n"],"mappings":";AA+CI,cA+CA,YA/CA;AA7CJ,YAAY,yBAAyB;AACrC,SAAS,WAAW,cAAoE;AACxF,SAAS,UAAU;AACnB,SAAS,uBAAuB;AAahC,MAAM,iBAAiB;AAwBhB,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AACnE,QAAM,QAAQ,gBAAgB;AAC9B,QAAM,QAAQ,MAAM,YAAY,KAAK,QAAQ,MAAM,iBAAiB,KAAK;AAEzE,SACE;AAAA,IAAC,oBAAoB;AAAA,IAApB;AAAA,MACC,mBAAiB,QAAQ,SAAY,OAAO;AAAA,MAC5C,WAAW,GAAG,yBAAyB,SAAS;AAAA,MAC/C,GAAG;AAAA;AAAA,EACN;AAEJ;AAwBO,SAAS,eAAe,EAAE,UAAU,WAAW,IAAI,SAAS,GAAG,MAAM,GAAwB;AAClG,QAAM,iBAAiB,OAAO,CAAC;AAE/B,YAAU,MAAM;AACd,UAAM,YAAY,CAAC,UAAyB;AAC1C,UAAI,MAAM,IAAI,WAAW,OAAO,EAAG,gBAAe,UAAU,KAAK,IAAI;AAAA,IACvE;AACA,aAAS,iBAAiB,WAAW,SAAS;AAC9C,WAAO,MAAM,SAAS,oBAAoB,WAAW,SAAS;AAAA,EAChE,GAAG,CAAC,CAAC;AAEL,QAAM,qBAAqB,CAAC,UAAyC;AACnE,cAAU,KAAK;AACf,QAAI,KAAK,IAAI,IAAI,eAAe,UAAU,eAAgB,OAAM,cAAc,MAAM;AAAA,EACtF;AAEA,SACE,qBAAC,WAAM,WAAW,GAAG,mEAAmE,SAAS,GAC/F;AAAA;AAAA,MAAC,oBAAoB;AAAA,MAApB;AAAA,QACC;AAAA,QACA,SAAS;AAAA,QACT,WAAU;AAAA,QACT,GAAG;AAAA,QAEJ,8BAAC,oBAAoB,WAApB,EAA8B,WAAU,uCAAsC;AAAA;AAAA,IACjF;AAAA,IACC;AAAA,KACH;AAEJ;AAEA,IAAO,qBAAQ;","names":[]}
@@ -8,6 +8,15 @@ interface ScrollAreaProps extends ComponentProps<typeof ScrollAreaPrimitive.Root
8
8
  * keyboard stop, and an unnamed stop announces "group" and nothing else.
9
9
  */
10
10
  label: string;
11
+ /**
12
+ * Which axes get a bar — and therefore which axes scroll at all. Radix sets
13
+ * the viewport's overflow from the bars that are mounted, so an axis without
14
+ * one is `hidden`: what is past that edge is not merely unmarked, it is
15
+ * unreachable by every key and every gesture.
16
+ *
17
+ * `both` is the default for that reason. Narrow it only when clipping the
18
+ * other axis is the thing you meant.
19
+ */
11
20
  orientation?: 'vertical' | 'horizontal' | 'both';
12
21
  }
13
22
  /**
@@ -23,6 +32,12 @@ interface ScrollAreaProps extends ComponentProps<typeof ScrollAreaPrimitive.Root
23
32
  * needs no component. This is for a bounded panel: a long option list, a log,
24
33
  * a sidebar that outgrows its column.
25
34
  *
35
+ * Both axes scroll unless a caller says otherwise. The old vertical default
36
+ * read as a statement about which bar to draw and was in fact a statement about
37
+ * which half of the content existed: Radix sets the viewport's overflow from
38
+ * the mounted bars, so the axis without one was `hidden` and everything past it
39
+ * was unreachable — silently, and with the content perfectly well rendered.
40
+ *
26
41
  * @example
27
42
  * <ScrollArea label="Deploy log" className="h-48">…</ScrollArea>
28
43
  */
@@ -4,7 +4,7 @@ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
4
4
  import { cn } from "../../lib/cn.js";
5
5
  function ScrollArea({
6
6
  label,
7
- orientation = "vertical",
7
+ orientation = "both",
8
8
  className,
9
9
  children,
10
10
  ...props
@@ -22,7 +22,7 @@ function ScrollArea({
22
22
  tabIndex: 0,
23
23
  role: "region",
24
24
  "aria-label": label,
25
- className: "size-full rounded-[inherit]",
25
+ className: "relative size-full rounded-[inherit]",
26
26
  children
27
27
  }
28
28
  ),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ScrollArea/ScrollArea.tsx"],"sourcesContent":["'use client'\n\nimport * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'\nimport type { ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface ScrollAreaProps extends ComponentProps<typeof ScrollAreaPrimitive.Root> {\n /**\n * Names the region. Required, and not decoration: a scroll container is a\n * keyboard stop, and an unnamed stop announces \"group\" and nothing else.\n */\n label: string\n orientation?: 'vertical' | 'horizontal' | 'both'\n}\n\n/**\n * A box that scrolls, with a scrollbar that looks the same on every platform.\n *\n * The reason to reach for this over `overflow-auto` is not the scrollbar — it\n * is that Radix keeps the viewport focusable and the bar operable, which a bare\n * overflow container does not. A scrollable region whose contents are not\n * themselves focusable is unreachable by keyboard: there is nothing to Tab to,\n * so everything past the fold does not exist without a mouse.\n *\n * For a page-level or prose scroll, the `scroll-slim` utility is lighter and\n * needs no component. This is for a bounded panel: a long option list, a log,\n * a sidebar that outgrows its column.\n *\n * @example\n * <ScrollArea label=\"Deploy log\" className=\"h-48\">…</ScrollArea>\n */\nexport function ScrollArea({\n label,\n orientation = 'vertical',\n className,\n children,\n ...props\n}: ScrollAreaProps) {\n return (\n <ScrollAreaPrimitive.Root\n type=\"hover\"\n className={cn('relative overflow-hidden', className)}\n {...props}\n >\n <ScrollAreaPrimitive.Viewport\n tabIndex={0}\n role=\"region\"\n aria-label={label}\n className=\"size-full rounded-[inherit]\"\n >\n {children}\n </ScrollAreaPrimitive.Viewport>\n {(orientation === 'vertical' || orientation === 'both') && <Bar orientation=\"vertical\" />}\n {(orientation === 'horizontal' || orientation === 'both') && <Bar orientation=\"horizontal\" />}\n <ScrollAreaPrimitive.Corner />\n </ScrollAreaPrimitive.Root>\n )\n}\n\nfunction Bar({ orientation }: { orientation: 'vertical' | 'horizontal' }) {\n return (\n <ScrollAreaPrimitive.Scrollbar\n orientation={orientation}\n className={cn(\n 'flex touch-none select-none p-0.5 transition-colors duration-(--duration-fast)',\n orientation === 'vertical' ? 'h-full w-2' : 'h-2 flex-col',\n )}\n >\n <ScrollAreaPrimitive.Thumb className=\"relative flex-1 rounded-(--radius-pill) bg-(--rule-2)\" />\n </ScrollAreaPrimitive.Scrollbar>\n )\n}\n\nexport default ScrollArea\n"],"mappings":";AAuCI,SAKE,KALF;AArCJ,YAAY,yBAAyB;AAErC,SAAS,UAAU;AA2BZ,SAAS,WAAW;AAAA,EACzB;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,SACE;AAAA,IAAC,oBAAoB;AAAA,IAApB;AAAA,MACC,MAAK;AAAA,MACL,WAAW,GAAG,4BAA4B,SAAS;AAAA,MAClD,GAAG;AAAA,MAEJ;AAAA;AAAA,UAAC,oBAAoB;AAAA,UAApB;AAAA,YACC,UAAU;AAAA,YACV,MAAK;AAAA,YACL,cAAY;AAAA,YACZ,WAAU;AAAA,YAET;AAAA;AAAA,QACH;AAAA,SACE,gBAAgB,cAAc,gBAAgB,WAAW,oBAAC,OAAI,aAAY,YAAW;AAAA,SACrF,gBAAgB,gBAAgB,gBAAgB,WAAW,oBAAC,OAAI,aAAY,cAAa;AAAA,QAC3F,oBAAC,oBAAoB,QAApB,EAA2B;AAAA;AAAA;AAAA,EAC9B;AAEJ;AAEA,SAAS,IAAI,EAAE,YAAY,GAA+C;AACxE,SACE;AAAA,IAAC,oBAAoB;AAAA,IAApB;AAAA,MACC;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA,gBAAgB,aAAa,eAAe;AAAA,MAC9C;AAAA,MAEA,8BAAC,oBAAoB,OAApB,EAA0B,WAAU,yDAAwD;AAAA;AAAA,EAC/F;AAEJ;AAEA,IAAO,qBAAQ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ScrollArea/ScrollArea.tsx"],"sourcesContent":["'use client'\n\nimport * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'\nimport type { ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface ScrollAreaProps extends ComponentProps<typeof ScrollAreaPrimitive.Root> {\n /**\n * Names the region. Required, and not decoration: a scroll container is a\n * keyboard stop, and an unnamed stop announces \"group\" and nothing else.\n */\n label: string\n /**\n * Which axes get a bar — and therefore which axes scroll at all. Radix sets\n * the viewport's overflow from the bars that are mounted, so an axis without\n * one is `hidden`: what is past that edge is not merely unmarked, it is\n * unreachable by every key and every gesture.\n *\n * `both` is the default for that reason. Narrow it only when clipping the\n * other axis is the thing you meant.\n */\n orientation?: 'vertical' | 'horizontal' | 'both'\n}\n\n/**\n * A box that scrolls, with a scrollbar that looks the same on every platform.\n *\n * The reason to reach for this over `overflow-auto` is not the scrollbar — it\n * is that Radix keeps the viewport focusable and the bar operable, which a bare\n * overflow container does not. A scrollable region whose contents are not\n * themselves focusable is unreachable by keyboard: there is nothing to Tab to,\n * so everything past the fold does not exist without a mouse.\n *\n * For a page-level or prose scroll, the `scroll-slim` utility is lighter and\n * needs no component. This is for a bounded panel: a long option list, a log,\n * a sidebar that outgrows its column.\n *\n * Both axes scroll unless a caller says otherwise. The old vertical default\n * read as a statement about which bar to draw and was in fact a statement about\n * which half of the content existed: Radix sets the viewport's overflow from\n * the mounted bars, so the axis without one was `hidden` and everything past it\n * was unreachable — silently, and with the content perfectly well rendered.\n *\n * @example\n * <ScrollArea label=\"Deploy log\" className=\"h-48\">…</ScrollArea>\n */\nexport function ScrollArea({\n label,\n orientation = 'both',\n className,\n children,\n ...props\n}: ScrollAreaProps) {\n return (\n <ScrollAreaPrimitive.Root\n type=\"hover\"\n className={cn('relative overflow-hidden', className)}\n {...props}\n >\n <ScrollAreaPrimitive.Viewport\n tabIndex={0}\n role=\"region\"\n aria-label={label}\n // Positioned, so an absolutely-positioned descendant — an `sr-only`\n // label, a pinned marker — resolves against the thing that scrolls and\n // travels with the content, rather than against the root and hanging\n // still over it.\n className=\"relative size-full rounded-[inherit]\"\n >\n {children}\n </ScrollAreaPrimitive.Viewport>\n {(orientation === 'vertical' || orientation === 'both') && <Bar orientation=\"vertical\" />}\n {(orientation === 'horizontal' || orientation === 'both') && <Bar orientation=\"horizontal\" />}\n <ScrollAreaPrimitive.Corner />\n </ScrollAreaPrimitive.Root>\n )\n}\n\nfunction Bar({ orientation }: { orientation: 'vertical' | 'horizontal' }) {\n return (\n <ScrollAreaPrimitive.Scrollbar\n orientation={orientation}\n className={cn(\n 'flex touch-none select-none p-0.5 transition-colors duration-(--duration-fast)',\n orientation === 'vertical' ? 'h-full w-2' : 'h-2 flex-col',\n )}\n >\n <ScrollAreaPrimitive.Thumb className=\"relative flex-1 rounded-(--radius-pill) bg-(--rule-2)\" />\n </ScrollAreaPrimitive.Scrollbar>\n )\n}\n\nexport default ScrollArea\n"],"mappings":";AAsDI,SAKE,KALF;AApDJ,YAAY,yBAAyB;AAErC,SAAS,UAAU;AA0CZ,SAAS,WAAW;AAAA,EACzB;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,SACE;AAAA,IAAC,oBAAoB;AAAA,IAApB;AAAA,MACC,MAAK;AAAA,MACL,WAAW,GAAG,4BAA4B,SAAS;AAAA,MAClD,GAAG;AAAA,MAEJ;AAAA;AAAA,UAAC,oBAAoB;AAAA,UAApB;AAAA,YACC,UAAU;AAAA,YACV,MAAK;AAAA,YACL,cAAY;AAAA,YAKZ,WAAU;AAAA,YAET;AAAA;AAAA,QACH;AAAA,SACE,gBAAgB,cAAc,gBAAgB,WAAW,oBAAC,OAAI,aAAY,YAAW;AAAA,SACrF,gBAAgB,gBAAgB,gBAAgB,WAAW,oBAAC,OAAI,aAAY,cAAa;AAAA,QAC3F,oBAAC,oBAAoB,QAApB,EAA2B;AAAA;AAAA;AAAA,EAC9B;AAEJ;AAEA,SAAS,IAAI,EAAE,YAAY,GAA+C;AACxE,SACE;AAAA,IAAC,oBAAoB;AAAA,IAApB;AAAA,MACC;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA,gBAAgB,aAAa,eAAe;AAAA,MAC9C;AAAA,MAEA,8BAAC,oBAAoB,OAApB,EAA0B,WAAU,yDAAwD;AAAA;AAAA,EAC/F;AAEJ;AAEA,IAAO,qBAAQ;","names":[]}
@@ -2,10 +2,19 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
 
4
4
  interface MenuAction {
5
- /** Stable id, and what the filter matches on. */
5
+ /**
6
+ * Stable identity for the row. Not what the filter matches — the label is,
7
+ * and an id a reader would never type is exactly what this used to be.
8
+ */
6
9
  id: string;
10
+ /** What the row prints. Its text is the first thing the filter matches. */
7
11
  label: ReactNode;
8
- /** Plain text the filter should match beyond the label. */
12
+ /**
13
+ * More text the filter should match, beyond the label's own words.
14
+ *
15
+ * The synonym a reader reaches for first: "download" for Export, "bin" for
16
+ * Delete. Required in practice only when the label prints no text at all.
17
+ */
9
18
  keywords?: string[];
10
19
  /** Printed at the end of the row. */
11
20
  shortcut?: string;
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { ChevronDown } from "lucide-react";
4
- import { useState } from "react";
4
+ import { isValidElement, useState } from "react";
5
5
  import { cn } from "../../lib/cn.js";
6
+ import { DEV, warn } from "../../lib/warn.js";
6
7
  import {
7
8
  Command,
8
9
  CommandEmpty,
@@ -13,6 +14,27 @@ import {
13
14
  CommandSeparator
14
15
  } from "../Command/Command.js";
15
16
  import { Popover, PopoverContent, PopoverTrigger } from "../Popover/Popover.js";
17
+ function textOf(node) {
18
+ if (typeof node === "string") return node;
19
+ if (typeof node === "number") return String(node);
20
+ if (Array.isArray(node)) return node.map(textOf).join(" ");
21
+ if (isValidElement(node)) return textOf(node.props.children);
22
+ return "";
23
+ }
24
+ function filterKeywords(action) {
25
+ const label = textOf(action.label).trim();
26
+ const extra = action.keywords ?? [];
27
+ if (DEV && !label && extra.length === 0) {
28
+ warn({
29
+ code: "SEARCHABLE_MENU_LABEL_UNREADABLE",
30
+ problem: "A SearchableMenu action has a label with no text in it, so the filter has nothing to match and the row can never be found by typing.",
31
+ field: "actions[].label",
32
+ fix: "Give the action keywords with the words a reader would type for it, or put text in the label.",
33
+ component: "SearchableMenu"
34
+ });
35
+ }
36
+ return label ? [label, ...extra] : extra;
37
+ }
16
38
  function SearchableMenu({
17
39
  actions,
18
40
  children,
@@ -54,7 +76,7 @@ function SearchableMenu({
54
76
  CommandItem,
55
77
  {
56
78
  value: action.id,
57
- keywords: action.keywords,
79
+ keywords: filterKeywords(action),
58
80
  disabled: action.disabled,
59
81
  shortcut: action.shortcut,
60
82
  onSelect: () => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/SearchableMenu/SearchableMenu.tsx"],"sourcesContent":["'use client'\n\nimport { ChevronDown } from 'lucide-react'\nimport { useState, type ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from '../Command/Command'\nimport { Popover, PopoverContent, PopoverTrigger } from '../Popover/Popover'\n\nexport interface MenuAction {\n /** Stable id, and what the filter matches on. */\n id: string\n label: ReactNode\n /** Plain text the filter should match beyond the label. */\n keywords?: string[]\n /** Printed at the end of the row. */\n shortcut?: string\n /** Optional group heading. Rows with the same value are shown together. */\n group?: string\n disabled?: boolean\n destructive?: boolean\n onSelect: () => void\n}\n\nexport interface SearchableMenuProps {\n actions: MenuAction[]\n /** The trigger's text. */\n children: ReactNode\n /** Names the menu for assistive tech. */\n label: string\n searchPlaceholder?: string\n emptyMessage?: string\n align?: 'start' | 'center' | 'end'\n className?: string\n}\n\n/**\n * A menu of actions you can type into.\n *\n * The fourth corner of a square the system otherwise had three of, and the\n * distinction is worth stating because reaching for the wrong one is easy:\n *\n * | | few options | many options |\n * |---|---|---|\n * | **sets a value** | `Select` | `Combobox` |\n * | **runs an action** | `DropdownMenu` | `SearchableMenu` |\n *\n * A `DropdownMenu` past about a dozen rows stops being scannable, and the\n * usual response — nesting submenus — makes it worse. This is the same list\n * with a filter over it.\n *\n * Not the same thing as `Command`: that is a page-level palette, usually modal\n * and usually bound to ⌘K. This is anchored to a control, like the menu it\n * replaces.\n *\n * The rows are `option`s inside a listbox rather than `menuitem`s, because that\n * is what the filtering pattern requires — the highlight moves through\n * `aria-activedescendant` while focus stays in the input, and a menu cannot do\n * that. The trade is deliberate: a menu that cannot be filtered is worse for\n * the reader than a listbox that runs actions.\n *\n * @example\n * <SearchableMenu label=\"Actions\" actions={ACTIONS}>Actions</SearchableMenu>\n */\nexport function SearchableMenu({\n actions,\n children,\n label,\n searchPlaceholder = 'Filter…',\n emptyMessage = 'Nothing matches.',\n align = 'start',\n className,\n}: SearchableMenuProps) {\n const [open, setOpen] = useState(false)\n\n // Grouped in declaration order, so a caller controls the arrangement by the\n // order they list actions rather than by an extra prop.\n const groups: { name: string | undefined; items: MenuAction[] }[] = []\n for (const action of actions) {\n const last = groups.at(-1)\n if (last && last.name === action.group) last.items.push(action)\n else groups.push({ name: action.group, items: [action] })\n }\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger\n aria-label={label}\n className={cn(\n 'inline-flex min-h-(--control-h-sm) items-center gap-2 rounded-(--radius-pill) border border-(--rule-2) px-4 text-sm text-(--ink) transition-colors duration-(--duration-fast) hover:border-(--ink)',\n className,\n )}\n >\n {children}\n <ChevronDown size={14} strokeWidth={1.5} aria-hidden className=\"shrink-0 text-(--ink-3-aa)\" />\n </PopoverTrigger>\n <PopoverContent label={label} align={align} className=\"w-64 overflow-hidden p-0\">\n <Command label={`${label}: ${searchPlaceholder}`} className=\"rounded-none border-0\">\n <CommandInput placeholder={searchPlaceholder} />\n <CommandList>\n <CommandEmpty>{emptyMessage}</CommandEmpty>\n {groups.map((group, index) => (\n <div key={group.name ?? index}>\n {index > 0 && <CommandSeparator />}\n <CommandGroup heading={group.name}>\n {group.items.map((action) => (\n <CommandItem\n key={action.id}\n value={action.id}\n keywords={action.keywords}\n disabled={action.disabled}\n shortcut={action.shortcut}\n onSelect={() => {\n setOpen(false)\n action.onSelect()\n }}\n className={action.destructive ? 'text-(--danger)' : undefined}\n >\n {action.label}\n </CommandItem>\n ))}\n </CommandGroup>\n </div>\n ))}\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n )\n}\n\nexport default SearchableMenu\n"],"mappings":";AA6FM,SAQE,KARF;AA3FN,SAAS,mBAAmB;AAC5B,SAAS,gBAAgC;AACzC,SAAS,UAAU;AACnB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,SAAS,gBAAgB,sBAAsB;AAyDjD,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB,eAAe;AAAA,EACf,QAAQ;AAAA,EACR;AACF,GAAwB;AACtB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AAItC,QAAM,SAA8D,CAAC;AACrE,aAAW,UAAU,SAAS;AAC5B,UAAM,OAAO,OAAO,GAAG,EAAE;AACzB,QAAI,QAAQ,KAAK,SAAS,OAAO,MAAO,MAAK,MAAM,KAAK,MAAM;AAAA,QACzD,QAAO,KAAK,EAAE,MAAM,OAAO,OAAO,OAAO,CAAC,MAAM,EAAE,CAAC;AAAA,EAC1D;AAEA,SACE,qBAAC,WAAQ,MAAY,cAAc,SACjC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,cAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QAEC;AAAA;AAAA,UACD,oBAAC,eAAY,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC,WAAU,8BAA6B;AAAA;AAAA;AAAA,IAC9F;AAAA,IACA,oBAAC,kBAAe,OAAc,OAAc,WAAU,4BACpD,+BAAC,WAAQ,OAAO,GAAG,KAAK,KAAK,iBAAiB,IAAI,WAAU,yBAC1D;AAAA,0BAAC,gBAAa,aAAa,mBAAmB;AAAA,MAC9C,qBAAC,eACC;AAAA,4BAAC,gBAAc,wBAAa;AAAA,QAC3B,OAAO,IAAI,CAAC,OAAO,UAClB,qBAAC,SACE;AAAA,kBAAQ,KAAK,oBAAC,oBAAiB;AAAA,UAChC,oBAAC,gBAAa,SAAS,MAAM,MAC1B,gBAAM,MAAM,IAAI,CAAC,WAChB;AAAA,YAAC;AAAA;AAAA,cAEC,OAAO,OAAO;AAAA,cACd,UAAU,OAAO;AAAA,cACjB,UAAU,OAAO;AAAA,cACjB,UAAU,OAAO;AAAA,cACjB,UAAU,MAAM;AACd,wBAAQ,KAAK;AACb,uBAAO,SAAS;AAAA,cAClB;AAAA,cACA,WAAW,OAAO,cAAc,oBAAoB;AAAA,cAEnD,iBAAO;AAAA;AAAA,YAXH,OAAO;AAAA,UAYd,CACD,GACH;AAAA,aAnBQ,MAAM,QAAQ,KAoBxB,CACD;AAAA,SACH;AAAA,OACF,GACF;AAAA,KACF;AAEJ;AAEA,IAAO,yBAAQ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/SearchableMenu/SearchableMenu.tsx"],"sourcesContent":["'use client'\n\nimport { ChevronDown } from 'lucide-react'\nimport { isValidElement, useState, type ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport { DEV, warn } from '../../lib/warn'\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from '../Command/Command'\nimport { Popover, PopoverContent, PopoverTrigger } from '../Popover/Popover'\n\nexport interface MenuAction {\n /**\n * Stable identity for the row. Not what the filter matches the label is,\n * and an id a reader would never type is exactly what this used to be.\n */\n id: string\n /** What the row prints. Its text is the first thing the filter matches. */\n label: ReactNode\n /**\n * More text the filter should match, beyond the label's own words.\n *\n * The synonym a reader reaches for first: \"download\" for Export, \"bin\" for\n * Delete. Required in practice only when the label prints no text at all.\n */\n keywords?: string[]\n /** Printed at the end of the row. */\n shortcut?: string\n /** Optional group heading. Rows with the same value are shown together. */\n group?: string\n disabled?: boolean\n destructive?: boolean\n onSelect: () => void\n}\n\nexport interface SearchableMenuProps {\n actions: MenuAction[]\n /** The trigger's text. */\n children: ReactNode\n /** Names the menu for assistive tech. */\n label: string\n searchPlaceholder?: string\n emptyMessage?: string\n align?: 'start' | 'center' | 'end'\n className?: string\n}\n\n/** The plain text a label prints, gathered through whatever wraps it. */\nfunction textOf(node: ReactNode): string {\n if (typeof node === 'string') return node\n if (typeof node === 'number') return String(node)\n if (Array.isArray(node)) return node.map(textOf).join(' ')\n if (isValidElement<{ children?: ReactNode }>(node)) return textOf(node.props.children)\n return ''\n}\n\n/**\n * What the filter matches this row on.\n *\n * cmdk derives an item's value from the first string in\n * `[props.value, props.children, ref]`, and only falls back to the row's\n * `textContent` when `value` is absent. This component passes the id as the\n * value, because that is the row's identity and two rows may print the same\n * words — so with the opaque ids an application actually has, the label a\n * reader can SEE scored zero and the menu showed its empty state. Lifting the\n * label's text into the keywords is what makes the visible words match, while\n * the id stays the identity.\n */\nfunction filterKeywords(action: MenuAction): string[] {\n const label = textOf(action.label).trim()\n const extra = action.keywords ?? []\n\n if (DEV && !label && extra.length === 0) {\n warn({\n code: 'SEARCHABLE_MENU_LABEL_UNREADABLE',\n problem:\n 'A SearchableMenu action has a label with no text in it, so the filter has nothing to match and the row can never be found by typing.',\n field: 'actions[].label',\n fix: 'Give the action keywords with the words a reader would type for it, or put text in the label.',\n component: 'SearchableMenu',\n })\n }\n\n return label ? [label, ...extra] : extra\n}\n\n/**\n * A menu of actions you can type into.\n *\n * The fourth corner of a square the system otherwise had three of, and the\n * distinction is worth stating because reaching for the wrong one is easy:\n *\n * | | few options | many options |\n * |---|---|---|\n * | **sets a value** | `Select` | `Combobox` |\n * | **runs an action** | `DropdownMenu` | `SearchableMenu` |\n *\n * A `DropdownMenu` past about a dozen rows stops being scannable, and the\n * usual response — nesting submenus — makes it worse. This is the same list\n * with a filter over it.\n *\n * Not the same thing as `Command`: that is a page-level palette, usually modal\n * and usually bound to ⌘K. This is anchored to a control, like the menu it\n * replaces.\n *\n * The rows are `option`s inside a listbox rather than `menuitem`s, because that\n * is what the filtering pattern requires — the highlight moves through\n * `aria-activedescendant` while focus stays in the input, and a menu cannot do\n * that. The trade is deliberate: a menu that cannot be filtered is worse for\n * the reader than a listbox that runs actions.\n *\n * @example\n * <SearchableMenu label=\"Actions\" actions={ACTIONS}>Actions</SearchableMenu>\n */\nexport function SearchableMenu({\n actions,\n children,\n label,\n searchPlaceholder = 'Filter…',\n emptyMessage = 'Nothing matches.',\n align = 'start',\n className,\n}: SearchableMenuProps) {\n const [open, setOpen] = useState(false)\n\n // Grouped in declaration order, so a caller controls the arrangement by the\n // order they list actions rather than by an extra prop.\n const groups: { name: string | undefined; items: MenuAction[] }[] = []\n for (const action of actions) {\n const last = groups.at(-1)\n if (last && last.name === action.group) last.items.push(action)\n else groups.push({ name: action.group, items: [action] })\n }\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger\n aria-label={label}\n className={cn(\n 'inline-flex min-h-(--control-h-sm) items-center gap-2 rounded-(--radius-pill) border border-(--rule-2) px-4 text-sm text-(--ink) transition-colors duration-(--duration-fast) hover:border-(--ink)',\n className,\n )}\n >\n {children}\n <ChevronDown size={14} strokeWidth={1.5} aria-hidden className=\"shrink-0 text-(--ink-3-aa)\" />\n </PopoverTrigger>\n <PopoverContent label={label} align={align} className=\"w-64 overflow-hidden p-0\">\n <Command label={`${label}: ${searchPlaceholder}`} className=\"rounded-none border-0\">\n <CommandInput placeholder={searchPlaceholder} />\n <CommandList>\n <CommandEmpty>{emptyMessage}</CommandEmpty>\n {groups.map((group, index) => (\n <div key={group.name ?? index}>\n {index > 0 && <CommandSeparator />}\n <CommandGroup heading={group.name}>\n {group.items.map((action) => (\n <CommandItem\n key={action.id}\n value={action.id}\n keywords={filterKeywords(action)}\n disabled={action.disabled}\n shortcut={action.shortcut}\n onSelect={() => {\n setOpen(false)\n action.onSelect()\n }}\n className={action.destructive ? 'text-(--danger)' : undefined}\n >\n {action.label}\n </CommandItem>\n ))}\n </CommandGroup>\n </div>\n ))}\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n )\n}\n\nexport default SearchableMenu\n"],"mappings":";AA8IM,SAQE,KARF;AA5IN,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB,gBAAgC;AACzD,SAAS,UAAU;AACnB,SAAS,KAAK,YAAY;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,SAAS,gBAAgB,sBAAsB;AAuCxD,SAAS,OAAO,MAAyB;AACvC,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,MAAI,OAAO,SAAS,SAAU,QAAO,OAAO,IAAI;AAChD,MAAI,MAAM,QAAQ,IAAI,EAAG,QAAO,KAAK,IAAI,MAAM,EAAE,KAAK,GAAG;AACzD,MAAI,eAAyC,IAAI,EAAG,QAAO,OAAO,KAAK,MAAM,QAAQ;AACrF,SAAO;AACT;AAcA,SAAS,eAAe,QAA8B;AACpD,QAAM,QAAQ,OAAO,OAAO,KAAK,EAAE,KAAK;AACxC,QAAM,QAAQ,OAAO,YAAY,CAAC;AAElC,MAAI,OAAO,CAAC,SAAS,MAAM,WAAW,GAAG;AACvC,SAAK;AAAA,MACH,MAAM;AAAA,MACN,SACE;AAAA,MACF,OAAO;AAAA,MACP,KAAK;AAAA,MACL,WAAW;AAAA,IACb,CAAC;AAAA,EACH;AAEA,SAAO,QAAQ,CAAC,OAAO,GAAG,KAAK,IAAI;AACrC;AA8BO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB,eAAe;AAAA,EACf,QAAQ;AAAA,EACR;AACF,GAAwB;AACtB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AAItC,QAAM,SAA8D,CAAC;AACrE,aAAW,UAAU,SAAS;AAC5B,UAAM,OAAO,OAAO,GAAG,EAAE;AACzB,QAAI,QAAQ,KAAK,SAAS,OAAO,MAAO,MAAK,MAAM,KAAK,MAAM;AAAA,QACzD,QAAO,KAAK,EAAE,MAAM,OAAO,OAAO,OAAO,CAAC,MAAM,EAAE,CAAC;AAAA,EAC1D;AAEA,SACE,qBAAC,WAAQ,MAAY,cAAc,SACjC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,cAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QAEC;AAAA;AAAA,UACD,oBAAC,eAAY,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC,WAAU,8BAA6B;AAAA;AAAA;AAAA,IAC9F;AAAA,IACA,oBAAC,kBAAe,OAAc,OAAc,WAAU,4BACpD,+BAAC,WAAQ,OAAO,GAAG,KAAK,KAAK,iBAAiB,IAAI,WAAU,yBAC1D;AAAA,0BAAC,gBAAa,aAAa,mBAAmB;AAAA,MAC9C,qBAAC,eACC;AAAA,4BAAC,gBAAc,wBAAa;AAAA,QAC3B,OAAO,IAAI,CAAC,OAAO,UAClB,qBAAC,SACE;AAAA,kBAAQ,KAAK,oBAAC,oBAAiB;AAAA,UAChC,oBAAC,gBAAa,SAAS,MAAM,MAC1B,gBAAM,MAAM,IAAI,CAAC,WAChB;AAAA,YAAC;AAAA;AAAA,cAEC,OAAO,OAAO;AAAA,cACd,UAAU,eAAe,MAAM;AAAA,cAC/B,UAAU,OAAO;AAAA,cACjB,UAAU,OAAO;AAAA,cACjB,UAAU,MAAM;AACd,wBAAQ,KAAK;AACb,uBAAO,SAAS;AAAA,cAClB;AAAA,cACA,WAAW,OAAO,cAAc,oBAAoB;AAAA,cAEnD,iBAAO;AAAA;AAAA,YAXH,OAAO;AAAA,UAYd,CACD,GACH;AAAA,aAnBQ,MAAM,QAAQ,KAoBxB,CACD;AAAA,SACH;AAAA,OACF,GACF;AAAA,KACF;AAEJ;AAEA,IAAO,yBAAQ;","names":[]}
@@ -15,13 +15,29 @@ interface SelectProps extends ComponentProps<typeof SelectPrimitive.Root> {
15
15
  * picker over its own calendar, say.
16
16
  */
17
17
  contentClassName?: string;
18
- /** Names the control. Required — the trigger shows a value, and a value is not a name. */
18
+ /**
19
+ * Names the control. Required — the trigger shows a value, and a value is not
20
+ * a name.
21
+ *
22
+ * It is announced together with the value, not instead of it: the trigger is
23
+ * named by the label and by its own text, so a reader hears "Region,
24
+ * Australia". Inside a `Field` with a label, that label is used and this one
25
+ * is not repeated.
26
+ */
19
27
  label: string;
20
28
  placeholder?: string;
21
29
  /** Paints the resting border with `--danger` and reflects `aria-invalid`. */
22
30
  invalid?: boolean;
23
31
  disabled?: boolean;
24
32
  className?: string;
33
+ /** The TRIGGER's id — the element a label points at. A `Field` sets it. */
34
+ id?: string;
35
+ /** Ids of the copy describing the control. A `Field` sets it from hint, error and description. */
36
+ 'aria-describedby'?: string;
37
+ /** The spelling a form library sets; read together with `invalid`. */
38
+ 'aria-invalid'?: boolean | 'true' | 'false';
39
+ /** Announced on the trigger. A `Field` sets it from `required`. */
40
+ 'aria-required'?: boolean;
25
41
  /** `SelectItem`s, optionally wrapped in `SelectGroup` with a `SelectLabel`. */
26
42
  children: ReactNode;
27
43
  }
@@ -53,7 +69,7 @@ interface SelectProps extends ComponentProps<typeof SelectPrimitive.Root> {
53
69
  * </Select>
54
70
  * </Field>
55
71
  */
56
- declare function Select({ label, placeholder, invalid, disabled, className, contentClassName, children, ...props }: SelectProps): react.JSX.Element;
72
+ declare function Select({ label, placeholder, invalid, disabled, className, contentClassName, children, id, 'aria-describedby': describedBy, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, ...props }: SelectProps): react.JSX.Element;
57
73
  type SelectItemProps = ComponentProps<typeof SelectPrimitive.Item>;
58
74
  /** One option. The tick marks the chosen one; the fill marks the highlighted one. */
59
75
  declare function SelectItem({ className, children, ...props }: SelectItemProps): react.JSX.Element;
@@ -2,8 +2,11 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as SelectPrimitive from "@radix-ui/react-select";
4
4
  import { Check, ChevronDown, ChevronUp } from "lucide-react";
5
+ import { useId } from "react";
5
6
  import { cn } from "../../lib/cn.js";
7
+ import { warnBlankName } from "../../lib/warn.js";
6
8
  import { useOverlayContainer } from "../../lib/overlay-container.js";
9
+ import { useFieldControl } from "../Field/field-control.js";
7
10
  import { CONTROL_BASE, CONTROL_BORDER, isInvalid } from "../../lib/control.js";
8
11
  const SelectRoot = SelectPrimitive.Root;
9
12
  const SelectGroup = SelectPrimitive.Group;
@@ -15,15 +18,28 @@ function Select({
15
18
  className,
16
19
  contentClassName,
17
20
  children,
21
+ id,
22
+ "aria-describedby": describedBy,
23
+ "aria-invalid": ariaInvalid,
24
+ "aria-required": ariaRequired,
18
25
  ...props
19
26
  }) {
20
- const bad = isInvalid(invalid);
27
+ warnBlankName("Select", "label", label, "the trigger is announced with no name");
28
+ const bad = isInvalid(invalid, ariaInvalid);
29
+ const field = useFieldControl();
30
+ const generated = useId();
31
+ const triggerId = id ?? generated;
32
+ const valueId = `${triggerId}-value`;
33
+ const nameId = field?.labelId ?? `${triggerId}-name`;
21
34
  const container = useOverlayContainer();
22
35
  return /* @__PURE__ */ jsxs(SelectPrimitive.Root, { disabled, ...props, children: [
23
36
  /* @__PURE__ */ jsxs(
24
37
  SelectPrimitive.Trigger,
25
38
  {
26
- "aria-label": label,
39
+ id: triggerId,
40
+ "aria-labelledby": `${nameId} ${valueId}`,
41
+ "aria-describedby": describedBy,
42
+ "aria-required": ariaRequired,
27
43
  "aria-invalid": bad || void 0,
28
44
  className: cn(
29
45
  CONTROL_BASE,
@@ -32,7 +48,8 @@ function Select({
32
48
  className
33
49
  ),
34
50
  children: [
35
- /* @__PURE__ */ jsx(SelectPrimitive.Value, { placeholder }),
51
+ field?.labelId == null && /* @__PURE__ */ jsx("span", { id: nameId, className: "sr-only", children: label }),
52
+ /* @__PURE__ */ jsx("span", { id: valueId, className: "truncate", children: /* @__PURE__ */ jsx(SelectPrimitive.Value, { placeholder }) }),
36
53
  /* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(
37
54
  ChevronDown,
38
55
  {