@teamofsilicons/silicon-ui 0.0.0-stage → 0.2.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 (354) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +81 -2
  3. package/check.mjs +236 -0
  4. package/components/accordion/accordion.d.ts +13 -0
  5. package/components/accordion/accordion.js +35 -0
  6. package/components/accordion/accordion.module.css +15 -0
  7. package/components/action-button/action-button.d.ts +11 -0
  8. package/components/action-button/action-button.js +57 -0
  9. package/components/action-button/action-button.module.css +15 -0
  10. package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
  11. package/components/activity-heatmap/activity-heatmap.js +303 -0
  12. package/components/activity-heatmap/activity-heatmap.module.css +83 -0
  13. package/components/alert/alert.d.ts +13 -0
  14. package/components/alert/alert.js +79 -0
  15. package/components/alert/alert.module.css +15 -0
  16. package/components/animated-counter/animated-counter.d.ts +13 -0
  17. package/components/animated-counter/animated-counter.js +73 -0
  18. package/components/animated-counter/animated-counter.module.css +12 -0
  19. package/components/announcement-bar/announcement-bar.d.ts +53 -0
  20. package/components/announcement-bar/announcement-bar.js +187 -0
  21. package/components/announcement-bar/announcement-bar.module.css +60 -0
  22. package/components/avatar/avatar.d.ts +8 -0
  23. package/components/avatar/avatar.js +18 -0
  24. package/components/avatar/avatar.module.css +60 -0
  25. package/components/avatar-group/avatar-group.d.ts +13 -0
  26. package/components/avatar-group/avatar-group.js +24 -0
  27. package/components/avatar-group/avatar-group.module.css +27 -0
  28. package/components/badge/badge.d.ts +10 -0
  29. package/components/badge/badge.js +66 -0
  30. package/components/badge/badge.module.css +52 -0
  31. package/components/bar-chart/bar-chart.d.ts +32 -0
  32. package/components/bar-chart/bar-chart.js +327 -0
  33. package/components/bar-chart/bar-chart.module.css +49 -0
  34. package/components/billing-toggle/billing-toggle.d.ts +35 -0
  35. package/components/billing-toggle/billing-toggle.js +83 -0
  36. package/components/billing-toggle/billing-toggle.module.css +139 -0
  37. package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
  38. package/components/bottom-sheet/bottom-sheet.js +386 -0
  39. package/components/bottom-sheet/bottom-sheet.module.css +69 -0
  40. package/components/breadcrumb/breadcrumb.d.ts +13 -0
  41. package/components/breadcrumb/breadcrumb.js +19 -0
  42. package/components/breadcrumb/breadcrumb.module.css +12 -0
  43. package/components/brush-chart/brush-chart.d.ts +45 -0
  44. package/components/brush-chart/brush-chart.js +426 -0
  45. package/components/brush-chart/brush-chart.module.css +69 -0
  46. package/components/button/button.d.ts +10 -0
  47. package/components/button/button.js +97 -0
  48. package/components/button/button.module.css +39 -0
  49. package/components/button-group/button-group.d.ts +59 -0
  50. package/components/button-group/button-group.js +280 -0
  51. package/components/button-group/button-group.module.css +87 -0
  52. package/components/button-link/button-link.d.ts +24 -0
  53. package/components/button-link/button-link.js +11 -0
  54. package/components/button-link/button-link.module.css +7 -0
  55. package/components/calendar/calendar.d.ts +22 -0
  56. package/components/calendar/calendar.js +253 -0
  57. package/components/calendar/calendar.module.css +51 -0
  58. package/components/card/card.d.ts +19 -0
  59. package/components/card/card.js +108 -0
  60. package/components/card/card.module.css +51 -0
  61. package/components/card-stack/card-stack.d.ts +34 -0
  62. package/components/card-stack/card-stack.js +291 -0
  63. package/components/card-stack/card-stack.module.css +63 -0
  64. package/components/carousel/carousel.d.ts +28 -0
  65. package/components/carousel/carousel.js +332 -0
  66. package/components/carousel/carousel.module.css +42 -0
  67. package/components/chat-thread/chat-thread.d.ts +135 -0
  68. package/components/chat-thread/chat-thread.js +406 -0
  69. package/components/chat-thread/chat-thread.module.css +242 -0
  70. package/components/checkbox/checkbox.d.ts +7 -0
  71. package/components/checkbox/checkbox.js +23 -0
  72. package/components/checkbox/checkbox.module.css +15 -0
  73. package/components/chip-group/chip-group.d.ts +24 -0
  74. package/components/chip-group/chip-group.js +196 -0
  75. package/components/chip-group/chip-group.module.css +38 -0
  76. package/components/code-block/code-block.d.ts +11 -0
  77. package/components/code-block/code-block.js +133 -0
  78. package/components/code-block/code-block.module.css +43 -0
  79. package/components/color-picker/color-picker.d.ts +40 -0
  80. package/components/color-picker/color-picker.js +449 -0
  81. package/components/color-picker/color-picker.module.css +111 -0
  82. package/components/combobox/combobox.d.ts +19 -0
  83. package/components/combobox/combobox.js +167 -0
  84. package/components/combobox/combobox.module.css +28 -0
  85. package/components/command-palette/command-palette.d.ts +20 -0
  86. package/components/command-palette/command-palette.js +151 -0
  87. package/components/command-palette/command-palette.module.css +94 -0
  88. package/components/comment-thread/comment-thread.d.ts +120 -0
  89. package/components/comment-thread/comment-thread.js +343 -0
  90. package/components/comment-thread/comment-thread.module.css +195 -0
  91. package/components/confirm-morph/confirm-morph.d.ts +65 -0
  92. package/components/confirm-morph/confirm-morph.js +355 -0
  93. package/components/confirm-morph/confirm-morph.module.css +146 -0
  94. package/components/context-menu/context-menu.d.ts +18 -0
  95. package/components/context-menu/context-menu.js +148 -0
  96. package/components/context-menu/context-menu.module.css +17 -0
  97. package/components/copy-button/copy-button.d.ts +10 -0
  98. package/components/copy-button/copy-button.js +69 -0
  99. package/components/copy-button/copy-button.module.css +21 -0
  100. package/components/countdown/countdown.d.ts +31 -0
  101. package/components/countdown/countdown.js +177 -0
  102. package/components/countdown/countdown.module.css +49 -0
  103. package/components/date-picker/date-picker.d.ts +17 -0
  104. package/components/date-picker/date-picker.js +100 -0
  105. package/components/date-picker/date-picker.module.css +34 -0
  106. package/components/date-range-picker/date-range-picker.d.ts +44 -0
  107. package/components/date-range-picker/date-range-picker.js +534 -0
  108. package/components/date-range-picker/date-range-picker.module.css +214 -0
  109. package/components/dialog/dialog.d.ts +11 -0
  110. package/components/dialog/dialog.js +52 -0
  111. package/components/dialog/dialog.module.css +36 -0
  112. package/components/donut-chart/donut-chart.d.ts +46 -0
  113. package/components/donut-chart/donut-chart.js +382 -0
  114. package/components/donut-chart/donut-chart.module.css +48 -0
  115. package/components/drawer/drawer.d.ts +20 -0
  116. package/components/drawer/drawer.js +111 -0
  117. package/components/drawer/drawer.module.css +97 -0
  118. package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
  119. package/components/dropdown-menu/dropdown-menu.js +39 -0
  120. package/components/dropdown-menu/dropdown-menu.module.css +38 -0
  121. package/components/elastic-slider/elastic-slider.d.ts +39 -0
  122. package/components/elastic-slider/elastic-slider.js +261 -0
  123. package/components/elastic-slider/elastic-slider.module.css +40 -0
  124. package/components/empty-state/empty-state.d.ts +12 -0
  125. package/components/empty-state/empty-state.js +69 -0
  126. package/components/empty-state/empty-state.module.css +13 -0
  127. package/components/expandable-card/expandable-card.d.ts +13 -0
  128. package/components/expandable-card/expandable-card.js +89 -0
  129. package/components/expandable-card/expandable-card.module.css +22 -0
  130. package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
  131. package/components/expanding-button-group/expanding-button-group.js +258 -0
  132. package/components/expanding-button-group/expanding-button-group.module.css +44 -0
  133. package/components/expanding-search/expanding-search.d.ts +38 -0
  134. package/components/expanding-search/expanding-search.js +242 -0
  135. package/components/expanding-search/expanding-search.module.css +52 -0
  136. package/components/file-dropzone/file-dropzone.d.ts +44 -0
  137. package/components/file-dropzone/file-dropzone.js +379 -0
  138. package/components/file-dropzone/file-dropzone.module.css +115 -0
  139. package/components/file-upload/file-upload.d.ts +22 -0
  140. package/components/file-upload/file-upload.js +192 -0
  141. package/components/file-upload/file-upload.module.css +49 -0
  142. package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
  143. package/components/filter-toolbar/filter-toolbar.js +446 -0
  144. package/components/filter-toolbar/filter-toolbar.module.css +70 -0
  145. package/components/floating-button-group/floating-button-group.d.ts +51 -0
  146. package/components/floating-button-group/floating-button-group.js +215 -0
  147. package/components/floating-button-group/floating-button-group.module.css +74 -0
  148. package/components/gauge/gauge.d.ts +18 -0
  149. package/components/gauge/gauge.js +101 -0
  150. package/components/gauge/gauge.module.css +22 -0
  151. package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
  152. package/components/hold-to-confirm/hold-to-confirm.js +165 -0
  153. package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
  154. package/components/hover-card/hover-card.d.ts +38 -0
  155. package/components/hover-card/hover-card.js +145 -0
  156. package/components/hover-card/hover-card.module.css +20 -0
  157. package/components/image-compare/image-compare.d.ts +27 -0
  158. package/components/image-compare/image-compare.js +267 -0
  159. package/components/image-compare/image-compare.module.css +25 -0
  160. package/components/in-view-title/in-view-title.d.ts +17 -0
  161. package/components/in-view-title/in-view-title.js +70 -0
  162. package/components/in-view-title/in-view-title.module.css +13 -0
  163. package/components/inline-edit/inline-edit.d.ts +27 -0
  164. package/components/inline-edit/inline-edit.js +315 -0
  165. package/components/inline-edit/inline-edit.module.css +73 -0
  166. package/components/input/input.d.ts +7 -0
  167. package/components/input/input.js +60 -0
  168. package/components/input/input.module.css +22 -0
  169. package/components/json-viewer/json-viewer.d.ts +48 -0
  170. package/components/json-viewer/json-viewer.js +463 -0
  171. package/components/json-viewer/json-viewer.module.css +92 -0
  172. package/components/line-chart/line-chart.d.ts +58 -0
  173. package/components/line-chart/line-chart.js +383 -0
  174. package/components/line-chart/line-chart.module.css +60 -0
  175. package/components/mention-input/mention-input.d.ts +72 -0
  176. package/components/mention-input/mention-input.js +417 -0
  177. package/components/mention-input/mention-input.module.css +134 -0
  178. package/components/metric-card/metric-card.d.ts +8 -0
  179. package/components/metric-card/metric-card.js +44 -0
  180. package/components/metric-card/metric-card.module.css +18 -0
  181. package/components/money-input/money-input.d.ts +48 -0
  182. package/components/money-input/money-input.js +503 -0
  183. package/components/money-input/money-input.module.css +77 -0
  184. package/components/morph-select/morph-select.d.ts +50 -0
  185. package/components/morph-select/morph-select.js +395 -0
  186. package/components/morph-select/morph-select.module.css +88 -0
  187. package/components/multi-select/multi-select.d.ts +18 -0
  188. package/components/multi-select/multi-select.js +105 -0
  189. package/components/multi-select/multi-select.module.css +37 -0
  190. package/components/notification-center/notification-center.d.ts +24 -0
  191. package/components/notification-center/notification-center.js +146 -0
  192. package/components/notification-center/notification-center.module.css +79 -0
  193. package/components/number-field/number-field.d.ts +41 -0
  194. package/components/number-field/number-field.js +455 -0
  195. package/components/number-field/number-field.module.css +71 -0
  196. package/components/otp-input/otp-input.d.ts +13 -0
  197. package/components/otp-input/otp-input.js +129 -0
  198. package/components/otp-input/otp-input.module.css +28 -0
  199. package/components/pagination/pagination.d.ts +7 -0
  200. package/components/pagination/pagination.js +60 -0
  201. package/components/pagination/pagination.module.css +16 -0
  202. package/components/password-field/password-field.d.ts +14 -0
  203. package/components/password-field/password-field.js +51 -0
  204. package/components/password-field/password-field.module.css +27 -0
  205. package/components/password-strength/password-strength.d.ts +107 -0
  206. package/components/password-strength/password-strength.js +161 -0
  207. package/components/password-strength/password-strength.module.css +57 -0
  208. package/components/phone-input/phone-input.d.ts +73 -0
  209. package/components/phone-input/phone-input.js +612 -0
  210. package/components/phone-input/phone-input.module.css +98 -0
  211. package/components/popover/popover.d.ts +6 -0
  212. package/components/popover/popover.js +19 -0
  213. package/components/popover/popover.module.css +32 -0
  214. package/components/progress/progress.d.ts +8 -0
  215. package/components/progress/progress.js +42 -0
  216. package/components/progress/progress.module.css +1 -0
  217. package/components/radio-cards/radio-cards.d.ts +35 -0
  218. package/components/radio-cards/radio-cards.js +110 -0
  219. package/components/radio-cards/radio-cards.module.css +84 -0
  220. package/components/radio-group/radio-group.d.ts +13 -0
  221. package/components/radio-group/radio-group.js +55 -0
  222. package/components/radio-group/radio-group.module.css +30 -0
  223. package/components/resizable-panels/resizable-panels.d.ts +37 -0
  224. package/components/resizable-panels/resizable-panels.js +338 -0
  225. package/components/resizable-panels/resizable-panels.module.css +40 -0
  226. package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
  227. package/components/rich-text-editor/rich-text-editor.js +1733 -0
  228. package/components/rich-text-editor/rich-text-editor.module.css +201 -0
  229. package/components/ridgeline/ridgeline.d.ts +37 -0
  230. package/components/ridgeline/ridgeline.js +346 -0
  231. package/components/ridgeline/ridgeline.module.css +58 -0
  232. package/components/scroll-area/scroll-area.d.ts +40 -0
  233. package/components/scroll-area/scroll-area.js +153 -0
  234. package/components/scroll-area/scroll-area.module.css +79 -0
  235. package/components/search-field/search-field.d.ts +7 -0
  236. package/components/search-field/search-field.js +20 -0
  237. package/components/search-field/search-field.module.css +17 -0
  238. package/components/segmented-control/segmented-control.d.ts +16 -0
  239. package/components/segmented-control/segmented-control.js +56 -0
  240. package/components/segmented-control/segmented-control.module.css +15 -0
  241. package/components/select/select.d.ts +15 -0
  242. package/components/select/select.js +36 -0
  243. package/components/select/select.module.css +35 -0
  244. package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
  245. package/components/shortcut-recorder/shortcut-recorder.js +380 -0
  246. package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
  247. package/components/signature-pad/signature-pad.d.ts +72 -0
  248. package/components/signature-pad/signature-pad.js +450 -0
  249. package/components/signature-pad/signature-pad.module.css +84 -0
  250. package/components/skeleton/skeleton.d.ts +13 -0
  251. package/components/skeleton/skeleton.js +53 -0
  252. package/components/skeleton/skeleton.module.css +11 -0
  253. package/components/slider/slider.d.ts +45 -0
  254. package/components/slider/slider.js +347 -0
  255. package/components/slider/slider.module.css +45 -0
  256. package/components/slope-chart/slope-chart.d.ts +38 -0
  257. package/components/slope-chart/slope-chart.js +183 -0
  258. package/components/slope-chart/slope-chart.module.css +51 -0
  259. package/components/slot-text/slot-text.d.ts +26 -0
  260. package/components/slot-text/slot-text.js +119 -0
  261. package/components/slot-text/slot-text.module.css +19 -0
  262. package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
  263. package/components/sortable-data-table/sortable-data-table.js +240 -0
  264. package/components/sortable-data-table/sortable-data-table.module.css +116 -0
  265. package/components/sparkline/sparkline.d.ts +19 -0
  266. package/components/sparkline/sparkline.js +239 -0
  267. package/components/sparkline/sparkline.module.css +26 -0
  268. package/components/split-button/split-button.d.ts +19 -0
  269. package/components/split-button/split-button.js +33 -0
  270. package/components/split-button/split-button.module.css +51 -0
  271. package/components/stepper/stepper.d.ts +36 -0
  272. package/components/stepper/stepper.js +108 -0
  273. package/components/stepper/stepper.module.css +74 -0
  274. package/components/streamgraph/streamgraph.d.ts +51 -0
  275. package/components/streamgraph/streamgraph.js +376 -0
  276. package/components/streamgraph/streamgraph.module.css +55 -0
  277. package/components/swipe-actions/swipe-actions.d.ts +35 -0
  278. package/components/swipe-actions/swipe-actions.js +330 -0
  279. package/components/swipe-actions/swipe-actions.module.css +64 -0
  280. package/components/switch/switch.d.ts +7 -0
  281. package/components/switch/switch.js +45 -0
  282. package/components/switch/switch.module.css +12 -0
  283. package/components/tabs/tabs.d.ts +8 -0
  284. package/components/tabs/tabs.js +138 -0
  285. package/components/tabs/tabs.module.css +27 -0
  286. package/components/tag-input/tag-input.d.ts +15 -0
  287. package/components/tag-input/tag-input.js +150 -0
  288. package/components/tag-input/tag-input.module.css +34 -0
  289. package/components/text-morph/text-morph.d.ts +13 -0
  290. package/components/text-morph/text-morph.js +69 -0
  291. package/components/text-morph/text-morph.module.css +8 -0
  292. package/components/text-reveal/text-reveal.d.ts +14 -0
  293. package/components/text-reveal/text-reveal.js +20 -0
  294. package/components/text-reveal/text-reveal.module.css +13 -0
  295. package/components/text-shimmer/text-shimmer.d.ts +18 -0
  296. package/components/text-shimmer/text-shimmer.js +90 -0
  297. package/components/text-shimmer/text-shimmer.module.css +9 -0
  298. package/components/textarea/textarea.d.ts +7 -0
  299. package/components/textarea/textarea.js +56 -0
  300. package/components/textarea/textarea.module.css +22 -0
  301. package/components/theme-switch/theme-switch.d.ts +11 -0
  302. package/components/theme-switch/theme-switch.js +37 -0
  303. package/components/theme-switch/theme-switch.module.css +31 -0
  304. package/components/time-picker/time-picker.d.ts +13 -0
  305. package/components/time-picker/time-picker.js +95 -0
  306. package/components/time-picker/time-picker.module.css +23 -0
  307. package/components/timeline/timeline.d.ts +47 -0
  308. package/components/timeline/timeline.js +171 -0
  309. package/components/timeline/timeline.module.css +51 -0
  310. package/components/toast/toast.d.ts +8 -0
  311. package/components/toast/toast.js +118 -0
  312. package/components/toast/toast.module.css +100 -0
  313. package/components/toast-stack/toast-stack.d.ts +57 -0
  314. package/components/toast-stack/toast-stack.js +399 -0
  315. package/components/toast-stack/toast-stack.module.css +61 -0
  316. package/components/tooltip/tooltip.d.ts +8 -0
  317. package/components/tooltip/tooltip.js +62 -0
  318. package/components/tooltip/tooltip.module.css +18 -0
  319. package/components/tree-view/tree-view.d.ts +17 -0
  320. package/components/tree-view/tree-view.js +147 -0
  321. package/components/tree-view/tree-view.module.css +22 -0
  322. package/components/treemap/treemap.d.ts +35 -0
  323. package/components/treemap/treemap.js +375 -0
  324. package/components/treemap/treemap.module.css +59 -0
  325. package/components/usage-meter/usage-meter.d.ts +24 -0
  326. package/components/usage-meter/usage-meter.js +296 -0
  327. package/components/usage-meter/usage-meter.module.css +61 -0
  328. package/components/user-menu/user-menu.d.ts +62 -0
  329. package/components/user-menu/user-menu.js +316 -0
  330. package/components/user-menu/user-menu.module.css +97 -0
  331. package/components/waffle-chart/waffle-chart.d.ts +39 -0
  332. package/components/waffle-chart/waffle-chart.js +244 -0
  333. package/components/waffle-chart/waffle-chart.module.css +52 -0
  334. package/components.json +1330 -0
  335. package/eslint.js +53 -0
  336. package/lib/escape.d.ts +19 -0
  337. package/lib/escape.js +50 -0
  338. package/lib/media.d.ts +353 -0
  339. package/lib/media.js +65 -0
  340. package/lib/menu-highlight.d.ts +28 -0
  341. package/lib/menu-highlight.js +43 -0
  342. package/lib/motion-tokens.d.ts +60 -0
  343. package/lib/motion-tokens.js +25 -0
  344. package/lib/return-focus.d.ts +9 -0
  345. package/lib/return-focus.js +53 -0
  346. package/lib/silicon-provider.d.ts +60 -0
  347. package/lib/silicon-provider.js +47 -0
  348. package/lib/use-copy-feedback.d.ts +8 -0
  349. package/lib/use-copy-feedback.js +36 -0
  350. package/lib/use-today.d.ts +2 -0
  351. package/lib/use-today.js +29 -0
  352. package/package.json +76 -4
  353. package/styles.css +218 -0
  354. package/styles.css.d.ts +1 -0
@@ -0,0 +1,40 @@
1
+ import type { CSSProperties, HTMLAttributes, ReactNode, Ref, UIEvent } from "react";
2
+ /** Which edges still have content beyond them. */
3
+ export interface ScrollAreaEdges {
4
+ top: boolean;
5
+ bottom: boolean;
6
+ left: boolean;
7
+ right: boolean;
8
+ }
9
+ /**
10
+ * A native scroll container with thin overlay scrollbars that appear while scrolling or on hover and fade away at rest.
11
+ * Each edge fades out only when there is more content beyond it, in proportion to how much. Scrolling stays native, so
12
+ * keyboard, touch momentum, and scroll snap all behave as the platform expects.
13
+ */
14
+ export interface ScrollAreaProps extends Omit<HTMLAttributes<HTMLDivElement>, "onScroll"> {
15
+ children?: ReactNode;
16
+ /** Axes that scroll. Defaults to vertical. */
17
+ orientation?: "vertical" | "horizontal" | "both";
18
+ /** Length of the edge fade in px. 0 turns the fades off. */
19
+ fade?: number;
20
+ /** "auto" shows scrollbars while scrolling or hovering; "always" keeps them visible when content overflows. */
21
+ scrollbars?: "auto" | "always";
22
+ /** How long scrollbars linger after scrolling stops, in ms. */
23
+ hideDelay?: number;
24
+ /** Maximum height of the viewport, for vertical areas that should grow with their content. */
25
+ maxHeight?: CSSProperties["maxHeight"];
26
+ /** Passed to the viewport's `scroll-snap-type`, for example "x mandatory". Children set their own `scroll-snap-align`. */
27
+ snap?: CSSProperties["scrollSnapType"];
28
+ /** Accessible name. The viewport becomes a labelled, focusable region that arrow and page keys scroll. */
29
+ label?: string;
30
+ /** Turns vertical wheel movement into horizontal scrolling for horizontal areas. Defaults to true. */
31
+ wheelToHorizontal?: boolean;
32
+ viewportClassName?: string;
33
+ viewportStyle?: CSSProperties;
34
+ viewportRef?: Ref<HTMLDivElement>;
35
+ onScroll?: (event: UIEvent<HTMLDivElement>) => void;
36
+ /** Called when content starts or stops extending past an edge. */
37
+ onEdgeChange?: (edges: ScrollAreaEdges) => void;
38
+ }
39
+ export declare const ScrollArea: import("react").ForwardRefExoticComponent<ScrollAreaProps & import("react").RefAttributes<HTMLDivElement>>;
40
+ export default ScrollArea;
@@ -0,0 +1,153 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef } from "react";
4
+ import styles from "./scroll-area.module.css";
5
+ const MIN_THUMB = 28;
6
+ export const ScrollArea = forwardRef(function ScrollArea({ children, orientation = "vertical", fade = 28, scrollbars = "auto", hideDelay = 900, maxHeight, snap, label, wheelToHorizontal = true, viewportClassName, viewportStyle, viewportRef, onScroll, onEdgeChange, className, ...rest }, forwardedRef) {
7
+ const rootRef = useRef(null);
8
+ const viewport = useRef(null);
9
+ const tracks = useRef({ x: null, y: null });
10
+ const thumbs = useRef({ x: null, y: null });
11
+ const hideTimer = useRef(undefined);
12
+ const lastEdges = useRef("");
13
+ const onEdgeRef = useRef(onEdgeChange);
14
+ useImperativeHandle(forwardedRef, () => rootRef.current);
15
+ useImperativeHandle(viewportRef, () => viewport.current);
16
+ useEffect(() => { onEdgeRef.current = onEdgeChange; }, [onEdgeChange]);
17
+ const vertical = orientation !== "horizontal", horizontal = orientation !== "vertical";
18
+ /* Everything visual is written straight to the DOM on each scroll frame, so scrolling never re-renders React. */
19
+ const sync = useCallback(() => {
20
+ const node = viewport.current, root = rootRef.current;
21
+ if (!node || !root)
22
+ return;
23
+ const { scrollTop, scrollLeft, scrollHeight, scrollWidth, clientHeight, clientWidth } = node;
24
+ const maxY = Math.max(0, scrollHeight - clientHeight), maxX = Math.max(0, scrollWidth - clientWidth);
25
+ // RTL layouts report negative scrollLeft; distance from the start edge is its magnitude.
26
+ const left = Math.abs(scrollLeft);
27
+ const edges = { top: vertical && scrollTop > .5, bottom: vertical && maxY - scrollTop > .5, left: horizontal && left > .5, right: horizontal && maxX - left > .5 };
28
+ if (fade > 0) {
29
+ node.style.setProperty("--fade-top", `${vertical ? Math.min(fade, scrollTop) : 0}px`);
30
+ node.style.setProperty("--fade-bottom", `${vertical ? Math.min(fade, maxY - scrollTop) : 0}px`);
31
+ node.style.setProperty("--fade-left", `${horizontal ? Math.min(fade, left) : 0}px`);
32
+ node.style.setProperty("--fade-right", `${horizontal ? Math.min(fade, maxX - left) : 0}px`);
33
+ }
34
+ const place = (axis, scroll, max, client, total) => {
35
+ const track = tracks.current[axis], thumb = thumbs.current[axis];
36
+ if (!track || !thumb)
37
+ return;
38
+ const overflowing = max > .5;
39
+ track.toggleAttribute("data-hidden", !overflowing);
40
+ if (!overflowing)
41
+ return;
42
+ const length = axis === "y" ? track.clientHeight : track.clientWidth;
43
+ const size = Math.max(MIN_THUMB, length * client / total);
44
+ const offset = (length - size) * (scroll / max);
45
+ thumb.style[axis === "y" ? "height" : "width"] = `${size}px`;
46
+ thumb.style.transform = axis === "y" ? `translate3d(0, ${offset}px, 0)` : `translate3d(${offset}px, 0, 0)`;
47
+ };
48
+ if (vertical)
49
+ place("y", scrollTop, maxY, clientHeight, scrollHeight);
50
+ if (horizontal)
51
+ place("x", left, maxX, clientWidth, scrollWidth);
52
+ root.toggleAttribute("data-overflow", maxY > .5 || maxX > .5);
53
+ const key = `${edges.top}${edges.bottom}${edges.left}${edges.right}`;
54
+ if (key !== lastEdges.current) {
55
+ lastEdges.current = key;
56
+ onEdgeRef.current?.(edges);
57
+ }
58
+ }, [fade, horizontal, vertical]);
59
+ useLayoutEffect(() => {
60
+ const node = viewport.current;
61
+ if (!node)
62
+ return;
63
+ sync();
64
+ if (typeof ResizeObserver === "undefined")
65
+ return;
66
+ const observer = new ResizeObserver(() => sync());
67
+ observer.observe(node);
68
+ Array.from(node.children).forEach(child => observer.observe(child));
69
+ const mutations = new MutationObserver(() => { observer.disconnect(); observer.observe(node); Array.from(node.children).forEach(child => observer.observe(child)); sync(); });
70
+ mutations.observe(node, { childList: true });
71
+ return () => { observer.disconnect(); mutations.disconnect(); };
72
+ }, [sync]);
73
+ useEffect(() => () => window.clearTimeout(hideTimer.current), []);
74
+ const wake = () => {
75
+ const root = rootRef.current;
76
+ if (!root)
77
+ return;
78
+ root.setAttribute("data-scrolling", "");
79
+ window.clearTimeout(hideTimer.current);
80
+ hideTimer.current = window.setTimeout(() => root.removeAttribute("data-scrolling"), hideDelay);
81
+ };
82
+ const handleScroll = (event) => {
83
+ sync();
84
+ wake();
85
+ onScroll?.(event);
86
+ };
87
+ // A mouse wheel over a horizontal strip moves it sideways, but only while there is room, so the page still scrolls at the ends.
88
+ useEffect(() => {
89
+ const node = viewport.current;
90
+ if (!node || orientation !== "horizontal" || !wheelToHorizontal)
91
+ return;
92
+ const wheel = (event) => {
93
+ if (event.ctrlKey || Math.abs(event.deltaX) >= Math.abs(event.deltaY))
94
+ return;
95
+ const max = node.scrollWidth - node.clientWidth, at = Math.abs(node.scrollLeft);
96
+ if (max <= 0 || (event.deltaY < 0 && at <= 0) || (event.deltaY > 0 && at >= max - .5))
97
+ return;
98
+ event.preventDefault();
99
+ node.scrollLeft += event.deltaY * (getComputedStyle(node).direction === "rtl" ? -1 : 1);
100
+ };
101
+ node.addEventListener("wheel", wheel, { passive: false });
102
+ return () => node.removeEventListener("wheel", wheel);
103
+ }, [orientation, wheelToHorizontal]);
104
+ /* Dragging a thumb maps pointer travel to scroll distance; pressing the track pages toward the pointer. */
105
+ const drag = useRef(null);
106
+ const onThumbDown = (event, axis) => {
107
+ const node = viewport.current, track = tracks.current[axis], thumb = thumbs.current[axis];
108
+ if (!node || !track || !thumb || event.button !== 0)
109
+ return;
110
+ event.preventDefault();
111
+ event.stopPropagation();
112
+ event.currentTarget.setPointerCapture(event.pointerId);
113
+ const length = axis === "y" ? track.clientHeight : track.clientWidth, size = axis === "y" ? thumb.offsetHeight : thumb.offsetWidth;
114
+ const max = axis === "y" ? node.scrollHeight - node.clientHeight : node.scrollWidth - node.clientWidth;
115
+ drag.current = { axis, start: axis === "y" ? event.clientY : event.clientX, scroll: axis === "y" ? node.scrollTop : node.scrollLeft, ratio: max / Math.max(1, length - size) };
116
+ rootRef.current?.setAttribute("data-dragging", axis);
117
+ node.style.scrollSnapType = "none";
118
+ };
119
+ const onThumbMove = (event) => {
120
+ const state = drag.current, node = viewport.current;
121
+ if (!state || !node)
122
+ return;
123
+ const delta = ((state.axis === "y" ? event.clientY : event.clientX) - state.start) * state.ratio;
124
+ if (state.axis === "y")
125
+ node.scrollTop = state.scroll + delta;
126
+ else
127
+ node.scrollLeft = state.scroll + delta;
128
+ };
129
+ const onThumbUp = (event) => {
130
+ if (!drag.current)
131
+ return;
132
+ drag.current = null;
133
+ if (event.currentTarget.hasPointerCapture(event.pointerId))
134
+ event.currentTarget.releasePointerCapture(event.pointerId);
135
+ rootRef.current?.removeAttribute("data-dragging");
136
+ if (viewport.current)
137
+ viewport.current.style.scrollSnapType = "";
138
+ wake();
139
+ };
140
+ const onTrackDown = (event, axis) => {
141
+ const node = viewport.current, thumb = thumbs.current[axis];
142
+ if (!node || !thumb || event.button !== 0 || event.target !== event.currentTarget)
143
+ return;
144
+ const rect = thumb.getBoundingClientRect();
145
+ const before = axis === "y" ? event.clientY < rect.top : event.clientX < rect.left;
146
+ const page = (axis === "y" ? node.clientHeight : node.clientWidth) * .9 * (before ? -1 : 1);
147
+ const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
148
+ node.scrollBy({ [axis === "y" ? "top" : "left"]: page, behavior: reduce ? "auto" : "smooth" });
149
+ };
150
+ const bar = (axis) => _jsx("div", { ref: node => { tracks.current[axis] = node; }, className: styles.track, "data-axis": axis, "data-hidden": "", "aria-hidden": "true", onPointerDown: event => onTrackDown(event, axis), children: _jsx("div", { ref: node => { thumbs.current[axis] = node; }, className: styles.thumb, onPointerDown: event => onThumbDown(event, axis), onPointerMove: onThumbMove, onPointerUp: onThumbUp, onPointerCancel: onThumbUp }) });
151
+ return _jsxs("div", { ref: rootRef, className: [styles.root, className].filter(Boolean).join(" "), "data-orientation": orientation, "data-scrollbars": scrollbars, ...rest, children: [_jsx("div", { ref: viewport, className: [styles.viewport, viewportClassName].filter(Boolean).join(" "), "data-fade": fade > 0 ? orientation : undefined, style: { maxHeight, scrollSnapType: snap, ...viewportStyle }, tabIndex: 0, role: label ? "region" : undefined, "aria-label": label, onScroll: handleScroll, children: children }), vertical && bar("y"), horizontal && bar("x")] });
152
+ });
153
+ export default ScrollArea;
@@ -0,0 +1,79 @@
1
+ .root { position: relative; display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; }
2
+
3
+ /* Native scrolling with the platform scrollbars hidden; the overlay bars below replace them. */
4
+ .viewport {
5
+ --fade-top: 0px; --fade-bottom: 0px; --fade-left: 0px; --fade-right: 0px;
6
+ min-width: 0;
7
+ min-height: 0;
8
+ flex: 1 1 auto;
9
+ overscroll-behavior-x: contain;
10
+ scrollbar-width: none;
11
+ -webkit-overflow-scrolling: touch;
12
+ }
13
+ .viewport::-webkit-scrollbar { display: none; }
14
+ .root[data-orientation="vertical"] > .viewport { overflow-x: hidden; overflow-y: auto; }
15
+ .root[data-orientation="horizontal"] > .viewport { overflow-x: auto; overflow-y: hidden; }
16
+ .root[data-orientation="both"] > .viewport { overflow: auto; }
17
+
18
+ /* Each fade is exactly as long as the content hidden past that edge, up to the fade size, so it grows in with the first pixels of scroll. */
19
+ .viewport[data-fade="vertical"] {
20
+ -webkit-mask-image: linear-gradient(to bottom, transparent, #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent);
21
+ mask-image: linear-gradient(to bottom, transparent, #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent);
22
+ }
23
+ .viewport[data-fade="horizontal"] {
24
+ -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade-left), #000 calc(100% - var(--fade-right)), transparent);
25
+ mask-image: linear-gradient(to right, transparent, #000 var(--fade-left), #000 calc(100% - var(--fade-right)), transparent);
26
+ }
27
+ .viewport[data-fade="both"] {
28
+ -webkit-mask-image: linear-gradient(to bottom, transparent, #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent), linear-gradient(to right, transparent, #000 var(--fade-left), #000 calc(100% - var(--fade-right)), transparent);
29
+ -webkit-mask-composite: source-in;
30
+ mask-image: linear-gradient(to bottom, transparent, #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent), linear-gradient(to right, transparent, #000 var(--fade-left), #000 calc(100% - var(--fade-right)), transparent);
31
+ mask-composite: intersect;
32
+ }
33
+
34
+ /* Overlay scrollbars: a generous hit area around a thin pill that thickens under the pointer. */
35
+ .track {
36
+ position: absolute;
37
+ z-index: 1;
38
+ opacity: 0;
39
+ pointer-events: none;
40
+ touch-action: none;
41
+ transition: opacity var(--duration-considered) var(--ease-standard);
42
+ }
43
+ .track[data-axis="y"] { top: 3px; right: 1px; bottom: 3px; width: 11px; }
44
+ .track[data-axis="x"] { right: 3px; bottom: 1px; left: 3px; height: 11px; }
45
+ .root[data-orientation="both"] > .track[data-axis="y"] { bottom: 13px; }
46
+ .root[data-orientation="both"] > .track[data-axis="x"] { right: 13px; }
47
+ .track[data-hidden] { display: none; }
48
+
49
+ .thumb { position: absolute; top: 0; left: 0; will-change: transform; }
50
+ .track[data-axis="y"] .thumb { width: 100%; }
51
+ .track[data-axis="x"] .thumb { height: 100%; }
52
+ .thumb::after {
53
+ content: "";
54
+ position: absolute;
55
+ border-radius: var(--radius-full); corner-shape: round;
56
+ background: color-mix(in oklab, var(--foreground) 30%, transparent);
57
+ transition: width var(--duration-fast) var(--ease-standard), height var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard);
58
+ }
59
+ .track[data-axis="y"] .thumb::after { top: 0; right: 3px; bottom: 0; width: 5px; }
60
+ .track[data-axis="x"] .thumb::after { right: 0; bottom: 3px; left: 0; height: 5px; }
61
+
62
+ /* Visible while scrolling, while the pointer is over the area, and always when asked. Appearing is quick; leaving lingers. */
63
+ .root[data-scrolling] > .track,
64
+ .root[data-dragging] > .track,
65
+ .root[data-scrollbars="always"] > .track { opacity: 1; pointer-events: auto; transition-duration: var(--duration-fast); }
66
+
67
+ @media (hover: hover) and (pointer: fine) {
68
+ .root:hover > .track { opacity: 1; pointer-events: auto; transition-duration: var(--duration-fast); }
69
+ .track:hover .thumb::after,
70
+ .root[data-dragging] .thumb::after { background: color-mix(in oklab, var(--foreground) 46%, transparent); }
71
+ .track[data-axis="y"]:hover .thumb::after,
72
+ .root[data-dragging="y"] .track[data-axis="y"] .thumb::after { width: 7px; right: 2px; }
73
+ .track[data-axis="x"]:hover .thumb::after,
74
+ .root[data-dragging="x"] .track[data-axis="x"] .thumb::after { height: 7px; bottom: 2px; }
75
+ }
76
+
77
+ @media (prefers-reduced-motion: reduce) {
78
+ .track, .thumb::after { transition: none; }
79
+ }
@@ -0,0 +1,7 @@
1
+ import type { InputHTMLAttributes } from "react";
2
+ export interface SearchFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
3
+ label: string;
4
+ value: string;
5
+ onValueChange: (value: string) => void;
6
+ }
7
+ export declare const SearchField: import("react").ForwardRefExoticComponent<SearchFieldProps & import("react").RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useId, useRef } from "react";
4
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
5
+ import { Search, X as Xmark } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./search-field.module.css";
8
+ export const SearchField = forwardRef(function SearchField({ label, value, onValueChange, id, className, ...props }, ref) {
9
+ const generated = useId();
10
+ const controlId = id ?? generated;
11
+ const reduced = useReducedMotion();
12
+ const inputRef = useRef(null);
13
+ const setRefs = (node) => { inputRef.current = node; if (typeof ref === "function")
14
+ ref(node);
15
+ else if (ref)
16
+ ref.current = node; };
17
+ // Clearing returns focus to the field, since the clear button unmounts under the pointer.
18
+ function clear() { onValueChange(""); inputRef.current?.focus(); }
19
+ return _jsxs("div", { className: styles.field, children: [_jsx("label", { htmlFor: controlId, children: label }), _jsxs("div", { className: styles.shell, "data-filled": value ? "true" : undefined, children: [_jsx(Search, { width: 18, height: 18, "aria-hidden": "true" }), _jsx("input", { ...props, ref: setRefs, id: controlId, type: "search", value: value, onChange: event => onValueChange(event.target.value), className: [styles.input, className].filter(Boolean).join(" ") }), _jsx("span", { className: styles.clearSlot, children: _jsx(AnimatePresence, { initial: false, children: value ? _jsx(motion.button, { type: "button", tabIndex: 0, onClick: clear, "aria-label": "Clear search", initial: reduced ? { opacity: 0 } : { opacity: 0, scale: .8, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, scale: 1, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, scale: .8, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } }, whileTap: reduced ? undefined : { scale: .96, transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } }, transition: reduced ? { duration: motionTokens.duration.instant } : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast }, filter: { duration: motionTokens.duration.fast } }, children: _jsx(Xmark, { width: 16, height: 16, "aria-hidden": "true" }) }, "clear") : null }) })] })] });
20
+ });
@@ -0,0 +1,17 @@
1
+ .field { display: grid; gap: var(--space-related); min-width: 0; }
2
+ .field label { font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); letter-spacing: var(--tracking-body); }
3
+ /* Focus darkens the border; the shell never changes size. */
4
+ .shell { display: flex; min-height: var(--control-height-md); align-items: center; gap: var(--space-related); border: 1px solid var(--border-strong); border-radius: var(--radius-control); padding: 0 var(--space-group); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); }
5
+ @media (hover: hover) and (pointer: fine) { .shell:hover:not(:focus-within) { border-color: var(--foreground); } }
6
+ .shell:focus-within { border-color: var(--foreground); }
7
+ /* The glass wakes up with the field: muted at rest, full contrast while searching. */
8
+ .shell > svg { flex: 0 0 auto; color: var(--text-muted); transition: color var(--duration-fast) var(--ease-standard); }
9
+ .shell:focus-within > svg, .shell[data-filled="true"] > svg { color: var(--foreground); }
10
+ .input { width: 100%; min-width: 0; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); letter-spacing: var(--tracking-body); outline: 0; }
11
+ .input:focus-visible { outline: 0; }
12
+ .input::placeholder { color: var(--text-muted); }
13
+ .input::-webkit-search-cancel-button { display: none; }
14
+ .clearSlot { display: grid; width: 24px; height: 24px; flex: 0 0 auto; place-items: center; }
15
+ .shell button { display: grid; width: 24px; height: 24px; flex: 0 0 auto; place-items: center; border: 0; border-radius: var(--radius-control); background: transparent; color: var(--text-muted); transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
16
+ @media (hover: hover) and (pointer: fine) { .shell button:hover { background: var(--surface-muted); color: var(--foreground); } }
17
+ @media (prefers-reduced-motion: reduce) { .shell, .shell > svg, .shell button { transition: none; } }
@@ -0,0 +1,16 @@
1
+ import { type ReactNode } from "react";
2
+ export interface Segment {
3
+ value: string;
4
+ label: string; /** Optional content after the label, such as a badge. */
5
+ accessory?: ReactNode;
6
+ }
7
+ export interface SegmentedControlProps {
8
+ options: Segment[];
9
+ value: string;
10
+ onValueChange: (value: string) => void;
11
+ label?: string;
12
+ /** Called when the pointer or focus reaches an option, before it is chosen. Use it to start loading what that option shows. */
13
+ onOptionIntent?: (value: string) => void;
14
+ className?: string;
15
+ }
16
+ export default function SegmentedControl({ options, value, onValueChange, label, onOptionIntent, className }: SegmentedControlProps): import("react").JSX.Element;
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useLayoutEffect, useRef } from "react";
4
+ import { LayoutGroup, motion, useReducedMotion } from "motion/react";
5
+ import { motionTokens } from "../../lib/motion-tokens.js";
6
+ import styles from "./segmented-control.module.css";
7
+ export default function SegmentedControl({ options, value, onValueChange, label, onOptionIntent, className }) {
8
+ const id = useId();
9
+ const reduced = useReducedMotion();
10
+ const track = useRef(null);
11
+ // When the options are wider than the container, the track scrolls inside itself. Edges fade only on the side with more to see.
12
+ useLayoutEffect(() => {
13
+ const node = track.current;
14
+ if (!node)
15
+ return;
16
+ const edges = () => {
17
+ const rest = node.scrollWidth - node.clientWidth - node.scrollLeft;
18
+ node.toggleAttribute("data-fade-start", node.scrollLeft > 1);
19
+ node.toggleAttribute("data-fade-end", rest > 1);
20
+ };
21
+ edges();
22
+ node.addEventListener("scroll", edges, { passive: true });
23
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(edges);
24
+ observer?.observe(node);
25
+ return () => { node.removeEventListener("scroll", edges); observer?.disconnect(); };
26
+ }, [options.length]);
27
+ // The selected option is always scrolled fully into view, with a little room so it clears the fade.
28
+ const first = useRef(true);
29
+ useEffect(() => {
30
+ const node = track.current;
31
+ const button = node?.querySelector('[aria-pressed="true"]');
32
+ if (!node || !button || node.scrollWidth <= node.clientWidth) {
33
+ first.current = false;
34
+ return;
35
+ }
36
+ const room = 20, start = button.offsetLeft - room, end = button.offsetLeft + button.offsetWidth + room - node.clientWidth;
37
+ const left = node.scrollLeft > start ? start : node.scrollLeft < end ? end : node.scrollLeft;
38
+ if (left !== node.scrollLeft)
39
+ node.scrollTo({ left: Math.max(0, left), behavior: first.current || reduced ? "auto" : "smooth" });
40
+ first.current = false;
41
+ }, [value, reduced]);
42
+ // Arrow keys, Home and End move the selection like a tab list; only the selected option is a tab stop.
43
+ const selectedIndex = Math.max(0, options.findIndex(option => option.value === value));
44
+ const onKeyDown = (event) => {
45
+ const last = options.length - 1;
46
+ const target = event.key === "ArrowRight" || event.key === "ArrowDown" ? (selectedIndex === last ? 0 : selectedIndex + 1)
47
+ : event.key === "ArrowLeft" || event.key === "ArrowUp" ? (selectedIndex === 0 ? last : selectedIndex - 1)
48
+ : event.key === "Home" ? 0 : event.key === "End" ? last : -1;
49
+ if (target < 0 || !options[target])
50
+ return;
51
+ event.preventDefault();
52
+ onValueChange(options[target].value);
53
+ track.current?.querySelector(`[data-value="${CSS.escape(options[target].value)}"]`)?.focus({ preventScroll: true });
54
+ };
55
+ return _jsx("div", { className: `${styles.root} ${className ?? ""}`, role: "group", "aria-label": label, children: _jsx(LayoutGroup, { id: id, children: _jsx(motion.div, { ref: track, layoutScroll: true, className: styles.track, children: options.map((option, index) => _jsxs("button", { id: `${id}-${option.value}`, className: styles.button, type: "button", "data-value": option.value, "aria-pressed": value === option.value, tabIndex: index === selectedIndex ? 0 : -1, onClick: () => onValueChange(option.value), onKeyDown: onKeyDown, onPointerEnter: onOptionIntent ? () => onOptionIntent(option.value) : undefined, onFocus: onOptionIntent ? () => onOptionIntent(option.value) : undefined, children: [value === option.value && _jsx(motion.span, { className: styles.selection, layoutId: "selection", layoutDependency: value, transition: reduced ? { duration: 0 } : motionTokens.spring.morph, "aria-hidden": "true" }), _jsxs("span", { className: styles.label, children: [option.label, option.accessory] })] }, option.value)) }) }) });
56
+ }
@@ -0,0 +1,15 @@
1
+ /* The frame can shrink below its content inside flex and grid parents; the track then scrolls inside it. */
2
+ .root { display: inline-flex; min-width: 0; max-width: 100%; flex-shrink: 1; border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--surface-muted); }
3
+ /* The track owns the stacking context so the gliding highlight passes under every label, not over earlier ones. */
4
+ .track { --fade-start: 0px; --fade-end: 0px; isolation: isolate; display: flex; min-width: 0; gap: 2px; overflow-x: auto; overscroll-behavior-x: contain; padding: 3px; border-radius: inherit; corner-shape: inherit; scrollbar-width: none; -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade-start), #000 calc(100% - var(--fade-end)), transparent); mask-image: linear-gradient(to right, transparent, #000 var(--fade-start), #000 calc(100% - var(--fade-end)), transparent); }
5
+ .track::-webkit-scrollbar { display: none; }
6
+ .track[data-fade-start] { --fade-start: 20px; }
7
+ .track[data-fade-end] { --fade-end: 20px; }
8
+ /* Items never scale or change weight; only the highlight travels and the label color follows it. */
9
+ .button { position: relative; flex: 0 0 auto; min-height: var(--control-height-sm); border: 0; border-radius: calc(var(--radius-control) - 4px); padding: 0 12px; background: transparent; color: var(--text-muted); font: inherit; font-size: var(--text-sm); font-weight: 500; white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard); }
10
+ @media (hover: hover) and (pointer: fine) { .button:hover { color: var(--foreground); } }
11
+ .button:active, .button[aria-pressed="true"] { color: var(--foreground); }
12
+ .selection { position: absolute; z-index: -1; inset: 0; border: 1px solid var(--border); border-radius: inherit; corner-shape: inherit; background: var(--surface); box-shadow: var(--shadow-resting); }
13
+ .label { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 6px; }
14
+ @media (max-width: 380px) { .button { padding: 0 10px; } }
15
+ @media (prefers-reduced-motion: reduce) { .button { transition: none; } }
@@ -0,0 +1,15 @@
1
+ import type { ComponentPropsWithoutRef } from "react";
2
+ import * as SelectPrimitive from "@radix-ui/react-select";
3
+ export interface SelectProps extends Omit<ComponentPropsWithoutRef<typeof SelectPrimitive.Root>, "children"> {
4
+ label: string;
5
+ description?: string;
6
+ placeholder?: string;
7
+ id?: string;
8
+ className?: string;
9
+ options: {
10
+ value: string;
11
+ label: string;
12
+ disabled?: boolean;
13
+ }[];
14
+ }
15
+ export declare const Select: import("react").ForwardRefExoticComponent<SelectProps & import("react").RefAttributes<HTMLButtonElement>>;
@@ -0,0 +1,36 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useId, useState } from "react";
4
+ import * as SelectPrimitive from "@radix-ui/react-select";
5
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
6
+ import { Check, ChevronDown, ChevronUp } from "lucide-react";
7
+ import { MenuHighlight, useMenuHighlight } from "../../lib/menu-highlight.js";
8
+ import { motionTokens } from "../../lib/motion-tokens.js";
9
+ import styles from "./select.module.css";
10
+ /** The shown value rolls in the direction of the list: a later option rises from below, an earlier one drops from above. */
11
+ const valueRoll = {
12
+ enter: (direction) => ({ opacity: 0, y: `${direction * 0.35}em`, filter: `blur(${motionTokens.blur.soft}px)` }),
13
+ center: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } },
14
+ exit: (direction) => ({ opacity: 0, y: `${direction * -0.3}em`, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } }),
15
+ };
16
+ /** Reduced motion keeps a short crossfade; the resting state matches valueRoll so server and client markup agree. */
17
+ const valueFade = { enter: { opacity: 0 }, center: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.instant } }, exit: { opacity: 0, transition: { duration: motionTokens.duration.instant } } };
18
+ export const Select = forwardRef(function Select({ label, description, placeholder = "Select an option", options, id, className, disabled, onValueChange, ...rootProps }, ref) {
19
+ const generatedId = useId();
20
+ const controlId = id ?? generatedId;
21
+ const hintId = description ? `${controlId}-description` : undefined;
22
+ const reduceMotion = useReducedMotion();
23
+ const { highlight, reset, contentProps } = useMenuHighlight();
24
+ const [uncontrolledValue, setUncontrolledValue] = useState(rootProps.defaultValue ?? "");
25
+ const currentValue = rootProps.value ?? uncontrolledValue;
26
+ const index = options.findIndex((option) => option.value === currentValue);
27
+ const shown = currentValue ? options[index]?.label ?? "" : placeholder;
28
+ const [previousIndex, setPreviousIndex] = useState(index);
29
+ const [direction, setDirection] = useState(1);
30
+ if (previousIndex !== index) {
31
+ setPreviousIndex(index);
32
+ setDirection(index > previousIndex ? 1 : -1);
33
+ }
34
+ return (_jsxs("div", { className: styles.field, children: [_jsx("label", { htmlFor: controlId, children: label }), _jsxs(SelectPrimitive.Root, { ...rootProps, disabled: disabled, onOpenChange: (open) => { if (open)
35
+ reset(); rootProps.onOpenChange?.(open); }, onValueChange: (next) => { setUncontrolledValue(next); onValueChange?.(next); }, children: [_jsxs(SelectPrimitive.Trigger, { ref: ref, id: controlId, "aria-describedby": hintId, className: [styles.trigger, className].filter(Boolean).join(" "), children: [_jsx("span", { className: styles.srOnly, children: _jsx(SelectPrimitive.Value, { placeholder: placeholder }) }), _jsx("span", { className: styles.valueText, "aria-hidden": "true", children: _jsx(AnimatePresence, { initial: false, custom: direction, children: _jsx(motion.span, { "data-placeholder": currentValue ? undefined : "", custom: direction, variants: reduceMotion ? valueFade : valueRoll, initial: "enter", animate: "center", exit: "exit", children: shown }, currentValue ? `value-${currentValue}` : "placeholder") }) }), _jsx(SelectPrimitive.Icon, { className: styles.chevron, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }), _jsx(SelectPrimitive.Portal, { children: _jsxs(SelectPrimitive.Content, { className: styles.content, position: "popper", sideOffset: 4, collisionPadding: 12, ...contentProps, children: [_jsx(SelectPrimitive.ScrollUpButton, { className: styles.scrollButton, children: _jsx(ChevronUp, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsxs(SelectPrimitive.Viewport, { className: styles.viewport, children: [_jsx(MenuHighlight, { state: highlight, className: styles.highlight }), options.map((option) => (_jsxs(SelectPrimitive.Item, { value: option.value, disabled: option.disabled, className: styles.item, children: [_jsx(SelectPrimitive.ItemText, { children: option.label }), _jsx(SelectPrimitive.ItemIndicator, { className: styles.indicator, children: _jsx(Check, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }, option.value)))] }), _jsx(SelectPrimitive.ScrollDownButton, { className: styles.scrollButton, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }) })] }), description && _jsx("span", { id: hintId, className: styles.hint, children: description })] }));
36
+ });
@@ -0,0 +1,35 @@
1
+ .field { display: grid; gap: var(--space-related); min-width: 0; }
2
+ .field label { font-size: var(--text-sm); font-weight: 500; }
3
+ /* The trigger anchors the floating menu. Radix opens on pointerdown and measures this box, so it never scales: press feedback is color only. */
4
+ .trigger { position: relative; box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: var(--space-group); width: 100%; min-height: var(--control-height-md); border: 1px solid var(--border); border-radius: var(--radius-control); padding: 0 var(--space-group); background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
5
+ @media (hover: hover) and (pointer: fine) { .trigger:hover:not([data-disabled]) { border-color: var(--border-strong); background: var(--surface-muted); } }
6
+ .trigger:active:not([data-disabled]), .trigger[data-state="open"] { border-color: var(--border-strong); background: var(--surface-muted); }
7
+ .trigger:focus-visible { border-color: var(--border-strong); }
8
+ .trigger[data-disabled] { opacity: .5; cursor: not-allowed; }
9
+ .valueText { display: grid; flex: 1; min-width: 0; grid-template-columns: minmax(0, 1fr); }
10
+ .valueText > span { grid-area: 1 / 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
11
+ .valueText > [data-placeholder] { color: var(--text-muted); }
12
+ .srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
13
+ .chevron { display: inline-flex; flex: 0 0 auto; color: var(--text-muted); transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard); }
14
+ .trigger[data-state="open"] .chevron { transform: rotate(180deg); }
15
+ .hint { color: var(--text-muted); font-size: var(--text-xs); }
16
+ /* Radix holds the animation until the menu is positioned, so it always grows from the trigger edge. */
17
+ .content { --menu-x: 0px; --menu-y: -6px; box-sizing: border-box; z-index: 1000; width: var(--radix-select-trigger-width); min-width: var(--radix-select-trigger-width); max-width: min(24rem, calc(100vw - 20px)); max-height: min(320px, var(--radix-select-content-available-height)); overflow: hidden; border: 1px solid var(--border); border-radius: calc(var(--radius-control) + 2px); padding: 6px; background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); transform-origin: var(--radix-select-content-transform-origin); animation: select-in var(--duration-standard) var(--ease-enter) both; }
18
+ .content[data-side="top"] { --menu-y: 6px; }
19
+ .content[data-side="left"] { --menu-x: 6px; --menu-y: 0px; }
20
+ .content[data-side="right"] { --menu-x: -6px; --menu-y: 0px; }
21
+ .content[data-state="closed"] { animation: select-out var(--duration-instant) var(--ease-standard) both; }
22
+ .viewport { position: relative; padding: 2px 0; }
23
+ /* One highlight glides between options for the pointer and jumps instantly for the keyboard (lib/menu-highlight). */
24
+ .highlight { position: absolute; top: 0; right: 0; left: 0; border-radius: calc(var(--radius-control) - 4px); background: var(--surface-muted); opacity: 0; pointer-events: none; }
25
+ .item { position: relative; display: flex; align-items: center; min-height: 36px; border-radius: calc(var(--radius-control) - 4px); padding: 0 34px 0 11px; color: var(--foreground); font-size: var(--text-sm); cursor: default; outline: none; user-select: none; transition: background-color var(--duration-instant) var(--ease-standard), color var(--duration-instant) var(--ease-standard); }
26
+ .item[data-disabled] { opacity: .45; }
27
+ .indicator { position: absolute; right: 10px; display: inline-flex; align-items: center; color: var(--foreground); animation: indicator-in var(--duration-standard) var(--ease-enter) 40ms both; }
28
+ .scrollButton { display: grid; height: 28px; place-items: center; color: var(--text-muted); }
29
+ @keyframes select-in { from { opacity: 0; transform: translate(var(--menu-x), var(--menu-y)) scale(.97); } }
30
+ @keyframes select-out { to { opacity: 0; transform: translate(calc(var(--menu-x) / 2), calc(var(--menu-y) / 2)) scale(.98); } }
31
+ @keyframes indicator-in { from { opacity: 0; transform: scale(.6); filter: blur(2px); } }
32
+ @media (prefers-reduced-transparency: reduce) { .content { background: var(--surface-raised); backdrop-filter: none; -webkit-backdrop-filter: none; } }
33
+ @media (prefers-reduced-motion: reduce) { .trigger, .chevron, .item { transition: none; } .content { animation: select-fade var(--duration-instant) var(--ease-standard) both; } .content[data-state="closed"] { animation: select-fade-out var(--duration-instant) var(--ease-standard) both; } .indicator { animation: none; } }
34
+ @keyframes select-fade { from { opacity: 0; } }
35
+ @keyframes select-fade-out { to { opacity: 0; } }
@@ -0,0 +1,108 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ export type Platform = "mac" | "other";
3
+ export interface ShortcutToken {
4
+ label: string; /** Physical key id, matched against held keys. */
5
+ id: string;
6
+ spoken: string;
7
+ }
8
+ /** The physical key id for an event: "k", "1", "enter", "meta", "shift"… `null` for keys that never make a shortcut, such as Caps Lock. */
9
+ export declare function keyId(event: Pick<KeyboardEvent, "key" | "code">): string | null;
10
+ /** One spelling per combination: on a Mac, Command is `mod`; elsewhere, Ctrl is. Modifiers come in a fixed order. */
11
+ export declare function normalizeShortcut(shortcut: string, platform: Platform): string;
12
+ /** Reads a keydown into a shortcut string, or `null` while only modifiers are down. */
13
+ export declare function shortcutFromEvent(event: Pick<KeyboardEvent, "key" | "code" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey">, platform: Platform): string | null;
14
+ /** True when a keydown is this shortcut. Use it to run the action a recorded shortcut is bound to. */
15
+ export declare function matchesShortcut(event: Pick<KeyboardEvent, "key" | "code" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey">, shortcut: string, platform: Platform): boolean;
16
+ /** Key caps for a shortcut in platform order: ⌃ ⌥ ⇧ ⌘ on a Mac, Ctrl Alt Shift Win elsewhere. */
17
+ export declare function shortcutTokens(shortcut: string, platform: Platform): ShortcutToken[];
18
+ /** "⌘⇧K" on a Mac, "Ctrl+Shift+K" elsewhere, plus a spoken form for assistive technology. */
19
+ export declare function formatShortcut(shortcut: string, platform: Platform): {
20
+ text: string;
21
+ spoken: string;
22
+ };
23
+ /** The viewer's platform. Server renders assume a Mac and settle on the first client render. */
24
+ export declare function usePlatform(override?: Platform): Platform;
25
+ /** Keys held down right now, by physical id. Clears on window blur, and after Command lifts (macOS drops the other keyups). */
26
+ export declare function usePressedKeys(enabled?: boolean): ReadonlySet<string>;
27
+ export interface ShortcutKbdProps extends HTMLAttributes<HTMLElement> {
28
+ children: ReactNode;
29
+ /** Draws the key pushed down and lit, such as while it is held. */
30
+ pressed?: boolean;
31
+ size?: "sm" | "md";
32
+ }
33
+ /** A key cap. Use it inline in copy, in menus, or through `ShortcutKeys` for a whole combination. */
34
+ export declare const ShortcutKbd: import("react").ForwardRefExoticComponent<ShortcutKbdProps & import("react").RefAttributes<HTMLElement>>;
35
+ /** @deprecated Renamed to `ShortcutKbd` so it never clashes with the `kbd` component's `Kbd` in a shared index.ts. */
36
+ export declare const Kbd: import("react").ForwardRefExoticComponent<ShortcutKbdProps & import("react").RefAttributes<HTMLElement>>;
37
+ /** @deprecated Renamed to `ShortcutKbdProps`. */
38
+ export type KbdProps = ShortcutKbdProps;
39
+ export interface ShortcutKeysProps {
40
+ shortcut: string;
41
+ platform?: Platform;
42
+ /** Held key ids from `usePressedKeys`; matching caps light up. */
43
+ pressed?: ReadonlySet<string>;
44
+ size?: "sm" | "md";
45
+ className?: string;
46
+ }
47
+ /** A shortcut as key caps, with a spoken label for assistive technology. */
48
+ export declare function ShortcutKeys({ shortcut, platform: platformProp, pressed, size, className }: ShortcutKeysProps): import("react").JSX.Element;
49
+ export interface ShortcutBinding {
50
+ shortcut: string;
51
+ label: string;
52
+ }
53
+ export interface ShortcutRecorderProps {
54
+ label: string;
55
+ hideLabel?: boolean;
56
+ value?: string | null;
57
+ defaultValue?: string | null;
58
+ /** `replaced` is the binding the new shortcut was taken from after "Use anyway", so the caller can unbind it. */
59
+ onValueChange?: (value: string | null, details: {
60
+ replaced?: ShortcutBinding;
61
+ }) => void;
62
+ /** What the reset button restores. Defaults to `defaultValue`. */
63
+ resetValue?: string | null;
64
+ /** Shortcuts already in use. Recording one of them asks before taking it. */
65
+ bindings?: ShortcutBinding[];
66
+ /** Also warn about combinations the browser or system keeps, such as ⌘W and ⌘C. On by default. */
67
+ warnReserved?: boolean;
68
+ /** Require ⌘, Ctrl, or ⌥ (function keys are always allowed). On by default. */
69
+ requireModifier?: boolean;
70
+ platform?: Platform;
71
+ placeholder?: string;
72
+ description?: string;
73
+ disabled?: boolean;
74
+ id?: string;
75
+ className?: string;
76
+ }
77
+ /**
78
+ * A field that records a keyboard shortcut. Press it, then press the combination: held modifiers light up as caps while you
79
+ * build the chord, the finished shortcut settles into place, and a combination that is already taken asks before it is used.
80
+ * Escape cancels, Backspace clears, and the reset button restores the default. Caps follow the platform: ⌘ on a Mac, Ctrl elsewhere.
81
+ */
82
+ export declare function ShortcutRecorder({ label, hideLabel, value, defaultValue, onValueChange, resetValue, bindings, warnReserved, requireModifier, platform: platformProp, placeholder, description, disabled, id, className, }: ShortcutRecorderProps): import("react").JSX.Element;
83
+ export interface ShortcutListItem {
84
+ label: string;
85
+ shortcut: string; /** Extra words that match when searching. */
86
+ keywords?: string;
87
+ }
88
+ export interface ShortcutListGroup {
89
+ label: string;
90
+ items: ShortcutListItem[];
91
+ }
92
+ export interface ShortcutListProps {
93
+ groups: ShortcutListGroup[];
94
+ /** Accessible name of the list. */
95
+ label?: string;
96
+ searchable?: boolean;
97
+ searchPlaceholder?: string;
98
+ /** Light up caps as keys are held, and fade rows that do not use the held modifiers. On by default. */
99
+ highlightPressed?: boolean;
100
+ platform?: Platform;
101
+ className?: string;
102
+ }
103
+ /**
104
+ * A searchable, grouped cheatsheet. Holding a modifier lights that cap in every row and fades rows that do not use it, so people
105
+ * can explore by holding keys; a row whose whole combination is down lights up. Search matches names, groups, and key names such as "cmd k".
106
+ */
107
+ export declare function ShortcutList({ groups, label, searchable, searchPlaceholder, highlightPressed, platform: platformProp, className }: ShortcutListProps): import("react").JSX.Element;
108
+ export default ShortcutRecorder;