@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,50 @@
1
+ import type { ReactNode } from "react";
2
+ export interface MorphSelectOption {
3
+ value: string;
4
+ label: string;
5
+ /** A 20px mark shown before the label, in the list and in the trigger. An avatar or a plain icon. */
6
+ icon?: ReactNode;
7
+ /** Short trailing detail such as an offset or a count. Rendered with tabular numerals. */
8
+ meta?: string;
9
+ /** Extra words that match this option when searching. */
10
+ keywords?: string;
11
+ disabled?: boolean;
12
+ }
13
+ /** A labelled run of options. */
14
+ export interface MorphSelectGroup {
15
+ label: string;
16
+ options: MorphSelectOption[];
17
+ }
18
+ export type MorphSelectItem = MorphSelectOption | MorphSelectGroup;
19
+ /**
20
+ * A select whose trigger is the list. Opening grows the trigger itself into the options surface, a highlight glides between options,
21
+ * and the chosen option's label lifts out of the list and settles into the trigger while the trigger springs to its new width.
22
+ * Supports groups, type-ahead, search for long lists, and the full listbox keyboard model.
23
+ * Use it for compact property pickers and form fields with up to a few hundred options. Prefer a combobox when people mostly type free text.
24
+ */
25
+ export interface MorphSelectProps {
26
+ /** Visible label, also the accessible name of the trigger and the list. */
27
+ label: string;
28
+ /** Keeps the label for assistive technology only. */
29
+ hideLabel?: boolean;
30
+ items: MorphSelectItem[];
31
+ value?: string | null;
32
+ defaultValue?: string | null;
33
+ onValueChange?: (value: string, option: MorphSelectOption) => void;
34
+ placeholder?: string;
35
+ /** Shows a search field in place of the trigger when open. "auto" turns it on above eight options. */
36
+ searchable?: boolean | "auto";
37
+ searchPlaceholder?: string;
38
+ /** Width of the open surface in px. It never gets narrower than the trigger. Defaults to 272. */
39
+ panelWidth?: number;
40
+ /** Tallest the option list gets before it scrolls, in px. Defaults to 296. */
41
+ maxListHeight?: number;
42
+ /** Which edge of the trigger stays put while the surface grows. */
43
+ align?: "start" | "end";
44
+ /** Adds a hidden input for native form submission. */
45
+ name?: string;
46
+ disabled?: boolean;
47
+ className?: string;
48
+ }
49
+ export declare function MorphSelect({ label, hideLabel, items, value, defaultValue, onValueChange, placeholder, searchable, searchPlaceholder, panelWidth, maxListHeight, align, name, disabled, className }: MorphSelectProps): import("react").JSX.Element;
50
+ export default MorphSelect;
@@ -0,0 +1,395 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
+ import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion } from "motion/react";
5
+ import { Check, ChevronDown, Search, X } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./morph-select.module.css";
8
+ const { blur } = motionTokens;
9
+ const enter = [...motionTokens.ease.enter];
10
+ const standard = [...motionTokens.ease.standard];
11
+ /** Duration springs restated as stiffness and damping, so a retarget mid flight keeps the velocity it already has. */
12
+ const physical = (visualDuration, bounce) => {
13
+ const root = 2 * Math.PI / (visualDuration * 1.2);
14
+ return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };
15
+ };
16
+ const GROW = physical(.4, .12), SHRINK = physical(.34, 0), GLIDE = physical(.28, .08), WIDTH = physical(.42, .16);
17
+ /** Width that narrows never overshoots: dipping even half a pixel under the closed width ellipsizes the label and nudges the chevron. */
18
+ const NARROW = physical(.42, 0);
19
+ /** The lifted label travels a touch faster than the surface folds, so the closing edge never catches it. */
20
+ const FLY = physical(.3, .06);
21
+ const OPEN_RADIUS = 22;
22
+ const TYPEAHEAD_RESET = 600, PAGE = 8;
23
+ const subscribe = () => () => { };
24
+ function useReducedFlag() {
25
+ const hydrated = useSyncExternalStore(subscribe, () => true, () => false);
26
+ return !!useReducedMotion() && hydrated;
27
+ }
28
+ const isGroup = (item) => "options" in item;
29
+ /** Loose options between groups gather into unlabelled sections, so the list keeps the order it was given. */
30
+ function normalize(items) {
31
+ const out = [];
32
+ items.forEach((item, index) => {
33
+ if (isGroup(item)) {
34
+ out.push({ key: `group-${index}`, label: item.label, options: item.options });
35
+ return;
36
+ }
37
+ const last = out[out.length - 1];
38
+ if (last && !last.label)
39
+ last.options.push(item);
40
+ else
41
+ out.push({ key: `loose-${index}`, options: [item] });
42
+ });
43
+ return out;
44
+ }
45
+ function matches(option, needle) {
46
+ return `${option.label} ${option.meta ?? ""} ${option.keywords ?? ""}`.toLowerCase().includes(needle);
47
+ }
48
+ /** Lid values roll with the list: a later option rises from below, an earlier one drops from above. */
49
+ const valueVariants = {
50
+ enter: (change) => change.fly ? { opacity: 1, x: change.fly.x, y: change.fly.y, filter: "blur(0px)" } : { opacity: 0, x: 0, y: `${change.dir * .5}em`, filter: `blur(${blur.soft}px)` },
51
+ rest: { opacity: 1, x: 0, y: 0, filter: "blur(0px)" },
52
+ exit: (change) => ({ opacity: 0, x: 0, y: change.fly ? "-0.3em" : `${change.dir * -.45}em`, filter: `blur(${blur.subtle}px)`, transition: { duration: .12, ease: standard } }),
53
+ };
54
+ const valueFade = { enter: { opacity: 0 }, rest: { opacity: 1, x: 0, y: 0, filter: "blur(0px)" }, exit: { opacity: 0, transition: { duration: .1 } } };
55
+ /** The closed trigger's width, rounded up. offsetWidth rounds to the nearest pixel, and half a pixel short is enough to ellipsize the label. */
56
+ const triggerWidth = (node) => {
57
+ const exact = node.getBoundingClientRect().width, rounded = node.offsetWidth;
58
+ // Under a scaled ancestor the rect is scaled too, so fall back to the layout width with a pixel of slack.
59
+ return Math.abs(exact - rounded) <= .5 ? Math.ceil(exact - .01) : rounded + 1;
60
+ };
61
+ export function MorphSelect({ label, hideLabel = false, items, value, defaultValue = null, onValueChange, placeholder = "Select", searchable = "auto", searchPlaceholder, panelWidth = 272, maxListHeight = 296, align = "start", name, disabled = false, className }) {
62
+ const reduced = useReducedFlag();
63
+ const uid = useId();
64
+ const labelId = `${uid}-label`, listId = `${uid}-list`, optionId = (index) => `${uid}-option-${index}`;
65
+ const rootRef = useRef(null);
66
+ const measureRef = useRef(null);
67
+ const listFaceRef = useRef(null);
68
+ const scrollRef = useRef(null);
69
+ const triggerRef = useRef(null);
70
+ const inputRef = useRef(null);
71
+ const slotRef = useRef(null);
72
+ const optionRefs = useRef(new Map());
73
+ const sections = useMemo(() => normalize(items), [items]);
74
+ const all = useMemo(() => sections.flatMap(section => section.options), [sections]);
75
+ const indexOf = useMemo(() => new Map(all.map((option, index) => [option.value, index])), [all]);
76
+ const canSearch = searchable === "auto" ? all.length > 8 : searchable;
77
+ const [inner, setInner] = useState(defaultValue);
78
+ const selected = value !== undefined ? value : inner;
79
+ const selectedOption = selected === null ? undefined : all.find(option => option.value === selected);
80
+ const [open, setOpen] = useState(false);
81
+ const [query, setQuery] = useState("");
82
+ const [active, setActive] = useState(null);
83
+ const [change, setChange] = useState({ key: 0, value: null, dir: 1, fly: null });
84
+ const needle = query.trim().toLowerCase();
85
+ const visibleSections = useMemo(() => needle
86
+ ? sections.map(section => ({ ...section, options: section.options.filter(option => matches(option, needle)) })).filter(section => section.options.length)
87
+ : sections, [needle, sections]);
88
+ const enabled = useMemo(() => visibleSections.flatMap(section => section.options).filter(option => !option.disabled), [visibleSections]);
89
+ const current = active !== null && enabled.some(option => option.value === active) ? active : enabled[0]?.value ?? null;
90
+ const resultCount = visibleSections.reduce((sum, section) => sum + section.options.length, 0);
91
+ /* The shape: one surface whose width, height, and corners spring between the trigger and the open list. */
92
+ const anchorW = useMotionValue("auto");
93
+ const shapeW = useMotionValue("100%"), shapeH = useMotionValue("100%"), radius = useMotionValue(22);
94
+ const sizes = useRef({ trigger: 0, lid: 0, listW: 0, listH: 0 });
95
+ const live = useRef({ open: false, reduced: false, measured: false });
96
+ useLayoutEffect(() => { live.current.reduced = reduced; }, [reduced]);
97
+ const place = useCallback((animated) => {
98
+ const { trigger, lid, listW, listH } = sizes.current;
99
+ if (!trigger || !lid)
100
+ return;
101
+ const isOpen = live.current.open;
102
+ const next = isOpen ? { w: Math.max(trigger, listW), h: lid + listH, r: OPEN_RADIUS } : { w: trigger, h: lid, r: lid / 2 };
103
+ if (!animated || live.current.reduced || !live.current.measured) {
104
+ anchorW.jump(trigger);
105
+ shapeW.jump(next.w);
106
+ shapeH.jump(next.h);
107
+ radius.jump(next.r);
108
+ live.current.measured = true;
109
+ return;
110
+ }
111
+ const lastW = typeof shapeW.get() === "number" ? shapeW.get() : next.w;
112
+ const lastH = typeof shapeH.get() === "number" ? shapeH.get() : next.h;
113
+ const lastAnchor = typeof anchorW.get() === "number" ? anchorW.get() : trigger;
114
+ const transition = next.w * next.h >= lastW * lastH ? GROW : SHRINK;
115
+ animate(anchorW, trigger, trigger < lastAnchor ? NARROW : WIDTH);
116
+ animate(shapeW, next.w, isOpen ? transition : next.w < lastW ? NARROW : WIDTH);
117
+ animate(shapeH, next.h, transition);
118
+ animate(radius, next.r, transition);
119
+ }, [anchorW, radius, shapeH, shapeW]);
120
+ useLayoutEffect(() => {
121
+ const measure = measureRef.current, face = listFaceRef.current, root = rootRef.current;
122
+ if (!measure || !face || !root)
123
+ return;
124
+ const read = () => {
125
+ const previous = sizes.current;
126
+ // The open list is never narrower than the trigger; the width goes to CSS directly so a new label never waits on a render.
127
+ root.style.setProperty("--ms-trigger-w", `${triggerWidth(measure)}px`);
128
+ const next = { trigger: triggerWidth(measure), lid: measure.offsetHeight, listW: face.offsetWidth, listH: face.offsetHeight };
129
+ const changed = next.trigger !== previous.trigger || next.lid !== previous.lid || (live.current.open && (next.listW !== previous.listW || next.listH !== previous.listH));
130
+ sizes.current = next;
131
+ if (changed)
132
+ place(live.current.measured);
133
+ };
134
+ read();
135
+ root.dataset.ready = "";
136
+ if (typeof ResizeObserver === "undefined")
137
+ return;
138
+ const observer = new ResizeObserver(read);
139
+ observer.observe(measure);
140
+ observer.observe(face);
141
+ return () => observer.disconnect();
142
+ }, [place]);
143
+ useLayoutEffect(() => {
144
+ if (live.current.open === open)
145
+ return;
146
+ live.current.open = open;
147
+ // A pick changes the label and closes in the same commit, so both faces are read fresh before the shape moves.
148
+ const face = listFaceRef.current, measure = measureRef.current;
149
+ if (face && measure) {
150
+ rootRef.current?.style.setProperty("--ms-trigger-w", `${triggerWidth(measure)}px`);
151
+ sizes.current = { trigger: triggerWidth(measure), lid: measure.offsetHeight, listW: face.offsetWidth, listH: face.offsetHeight };
152
+ }
153
+ place(true);
154
+ }, [open, place]);
155
+ /* The highlight glides between options on its own spring and fades when nothing is active. */
156
+ const hy = useMotionValue(0), hh = useMotionValue(0), ho = useMotionValue(0);
157
+ const scrollIntent = useRef(false);
158
+ useLayoutEffect(() => {
159
+ const node = open && current !== null ? optionRefs.current.get(current) : undefined;
160
+ if (!node) {
161
+ animate(ho, 0, { duration: reduced || !open ? 0 : .12, ease: standard });
162
+ return;
163
+ }
164
+ const top = node.offsetTop, height = node.offsetHeight;
165
+ if (ho.get() < .05 || reduced) {
166
+ hy.jump(top);
167
+ hh.jump(height);
168
+ }
169
+ else {
170
+ animate(hy, top, GLIDE);
171
+ animate(hh, height, GLIDE);
172
+ }
173
+ animate(ho, 1, { duration: reduced ? 0 : .12, ease: enter });
174
+ const scroller = scrollRef.current;
175
+ if (scroller && scrollIntent.current) {
176
+ scrollIntent.current = false;
177
+ const pad = 6;
178
+ if (top < scroller.scrollTop + pad)
179
+ scroller.scrollTop = top - pad;
180
+ else if (top + height > scroller.scrollTop + scroller.clientHeight - pad)
181
+ scroller.scrollTop = top + height - scroller.clientHeight + pad;
182
+ }
183
+ }, [current, hh, ho, hy, open, reduced, visibleSections]);
184
+ /* Focus lands in the lid: the search field when there is one, otherwise the trigger that keeps the active option. */
185
+ const pendingFocus = useRef(null);
186
+ useLayoutEffect(() => {
187
+ const target = pendingFocus.current;
188
+ pendingFocus.current = null;
189
+ if (target === "input")
190
+ inputRef.current?.focus({ preventScroll: true });
191
+ if (target === "trigger")
192
+ triggerRef.current?.focus({ preventScroll: true });
193
+ }, [open]);
194
+ const typeahead = useRef({ buffer: "", at: 0 });
195
+ /** Space opens and picks on keydown; the click a button fires on the matching keyup must not undo it. */
196
+ const suppressClick = useRef(false);
197
+ const [announcement, setAnnouncement] = useState("");
198
+ const openList = (focus, seed = "") => {
199
+ if (disabled || open)
200
+ return;
201
+ setQuery(seed);
202
+ setActive(focus !== undefined ? focus : selected);
203
+ scrollIntent.current = true;
204
+ pendingFocus.current = canSearch ? "input" : "trigger";
205
+ setAnnouncement("");
206
+ setOpen(true);
207
+ };
208
+ const close = useCallback((restoreFocus) => {
209
+ typeahead.current.buffer = "";
210
+ if (restoreFocus)
211
+ pendingFocus.current = "trigger";
212
+ setOpen(false);
213
+ }, []);
214
+ const commit = (option, source) => {
215
+ if (!option || option.disabled)
216
+ return;
217
+ if (option.value !== selected) {
218
+ let fly = null;
219
+ const slot = slotRef.current;
220
+ const part = source?.querySelector("[data-part='value']");
221
+ if (!reduced && part && slot) {
222
+ const from = part.getBoundingClientRect(), to = slot.getBoundingClientRect();
223
+ fly = { x: from.left - to.left, y: from.top + from.height / 2 - (to.top + to.height / 2) };
224
+ }
225
+ const from = selected === null ? -1 : indexOf.get(selected) ?? -1, to = indexOf.get(option.value) ?? 0;
226
+ setChange(last => ({ key: last.key + 1, value: option.value, dir: from < 0 ? 1 : Math.sign(to - from) || 1, fly }));
227
+ if (value === undefined)
228
+ setInner(option.value);
229
+ onValueChange?.(option.value, option);
230
+ }
231
+ close(true);
232
+ };
233
+ const typeTo = (char) => {
234
+ const now = performance.now(), state = typeahead.current;
235
+ state.buffer = now - state.at > TYPEAHEAD_RESET ? char : state.buffer + char;
236
+ state.at = now;
237
+ const buffer = state.buffer.toLowerCase();
238
+ const repeated = buffer.split("").every(letter => letter === buffer[0]);
239
+ const search = repeated ? buffer[0] : buffer;
240
+ const pool = all.filter(option => !option.disabled);
241
+ const from = open ? current : selected;
242
+ const at = pool.findIndex(option => option.value === from);
243
+ const offset = repeated || buffer.length === 1 ? 1 : 0;
244
+ const ordered = [...pool.slice(at + offset), ...pool.slice(0, at + offset)];
245
+ return ordered.find(option => option.label.toLowerCase().startsWith(search));
246
+ };
247
+ const move = (to) => {
248
+ if (to === undefined || to === null)
249
+ return;
250
+ scrollIntent.current = true;
251
+ setActive(to);
252
+ };
253
+ const onKey = (event, fromInput) => {
254
+ const { key } = event;
255
+ if (key === " " && !fromInput)
256
+ suppressClick.current = true;
257
+ const printable = key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey;
258
+ const typing = performance.now() - typeahead.current.at < TYPEAHEAD_RESET && typeahead.current.buffer.length > 0;
259
+ if (!open) {
260
+ if (key === "ArrowDown" || key === "ArrowUp" || key === "Enter" || (key === " " && !typing)) {
261
+ event.preventDefault();
262
+ openList();
263
+ return;
264
+ }
265
+ if (key === "Home" || key === "End") {
266
+ event.preventDefault();
267
+ const pool = all.filter(option => !option.disabled);
268
+ openList((key === "Home" ? pool[0] : pool[pool.length - 1])?.value ?? null);
269
+ return;
270
+ }
271
+ if (printable) {
272
+ event.preventDefault();
273
+ if (canSearch)
274
+ openList(undefined, key);
275
+ else {
276
+ const hit = typeTo(key);
277
+ openList(hit?.value ?? selected);
278
+ }
279
+ }
280
+ return;
281
+ }
282
+ const at = enabled.findIndex(option => option.value === current);
283
+ const caretKeys = fromInput && query.length > 0;
284
+ switch (key) {
285
+ case "ArrowDown":
286
+ event.preventDefault();
287
+ move(enabled[Math.min(enabled.length - 1, at + 1)]?.value);
288
+ return;
289
+ case "ArrowUp":
290
+ event.preventDefault();
291
+ if (event.altKey) {
292
+ commit(enabled[at], optionRefs.current.get(current ?? ""));
293
+ return;
294
+ }
295
+ move(enabled[Math.max(0, at - 1)]?.value);
296
+ return;
297
+ case "PageDown":
298
+ event.preventDefault();
299
+ move(enabled[Math.min(enabled.length - 1, at + PAGE)]?.value);
300
+ return;
301
+ case "PageUp":
302
+ event.preventDefault();
303
+ move(enabled[Math.max(0, at - PAGE)]?.value);
304
+ return;
305
+ case "Home":
306
+ if (caretKeys)
307
+ return;
308
+ event.preventDefault();
309
+ move(enabled[0]?.value);
310
+ return;
311
+ case "End":
312
+ if (caretKeys)
313
+ return;
314
+ event.preventDefault();
315
+ move(enabled[enabled.length - 1]?.value);
316
+ return;
317
+ case "Enter":
318
+ event.preventDefault();
319
+ commit(enabled[at], optionRefs.current.get(current ?? ""));
320
+ return;
321
+ case "Escape":
322
+ event.preventDefault();
323
+ event.stopPropagation();
324
+ close(true);
325
+ return;
326
+ case "Tab":
327
+ close(false);
328
+ return;
329
+ }
330
+ if (fromInput)
331
+ return;
332
+ if (key === " " && !typing) {
333
+ event.preventDefault();
334
+ commit(enabled[at], optionRefs.current.get(current ?? ""));
335
+ return;
336
+ }
337
+ if (printable) {
338
+ event.preventDefault();
339
+ move(typeTo(key)?.value);
340
+ }
341
+ };
342
+ // A press outside or focus leaving the control folds the list back into the trigger.
343
+ useEffect(() => {
344
+ if (!open)
345
+ return;
346
+ const down = (event) => { if (!rootRef.current?.contains(event.target))
347
+ close(false); };
348
+ document.addEventListener("pointerdown", down);
349
+ return () => document.removeEventListener("pointerdown", down);
350
+ }, [close, open]);
351
+ const onBlur = (event) => {
352
+ const next = event.relatedTarget;
353
+ if (open && next && !event.currentTarget.contains(next))
354
+ close(false);
355
+ };
356
+ const onQuery = (next) => {
357
+ setQuery(next);
358
+ setActive(null);
359
+ scrollIntent.current = true;
360
+ if (scrollRef.current)
361
+ scrollRef.current.scrollTop = 0;
362
+ const text = next.trim().toLowerCase();
363
+ const count = text ? all.filter(option => matches(option, text)).length : all.length;
364
+ setAnnouncement(text ? count ? `${count} ${count === 1 ? "option" : "options"}` : "No matches" : "");
365
+ };
366
+ const renderValue = (option) => option
367
+ ? _jsxs(_Fragment, { children: [option.icon && _jsx("span", { className: styles.icon, "aria-hidden": "true", children: option.icon }), _jsx("span", { className: styles.valueText, children: option.label })] })
368
+ : _jsx("span", { className: styles.placeholder, children: placeholder });
369
+ const showSearch = open && canSearch;
370
+ const layerKey = selected ?? "__empty";
371
+ const changeForLayer = change.value === selected ? change : { ...change, fly: null };
372
+ const rootStyle = { "--ms-panel-w": `${panelWidth}px`, "--ms-list-max": `${maxListHeight}px` };
373
+ return _jsxs("div", { ref: rootRef, className: [styles.root, className].filter(Boolean).join(" "), style: rootStyle, "data-open": open || undefined, "data-align": align, "data-disabled": disabled || undefined, onBlur: onBlur, children: [_jsx("span", { id: labelId, className: hideLabel ? styles.srOnly : styles.label, children: label }), _jsxs(motion.div, { className: styles.anchor, style: { width: anchorW }, children: [_jsx("span", { ref: measureRef, className: `${styles.trigger} ${styles.measure}`, "aria-hidden": "true", children: renderValue(selectedOption) }), _jsxs(motion.div, { className: styles.shape, style: { width: shapeW, height: shapeH, borderRadius: radius }, children: [_jsxs("div", { className: styles.lid, children: [_jsx("button", { ref: triggerRef, type: "button", className: styles.trigger, role: "combobox", "aria-labelledby": labelId, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": listId, "aria-activedescendant": open && !canSearch && current !== null ? optionId(indexOf.get(current) ?? 0) : undefined, disabled: disabled, inert: showSearch || undefined, tabIndex: showSearch ? -1 : 0, onClick: () => { if (suppressClick.current) {
374
+ suppressClick.current = false;
375
+ return;
376
+ } if (open)
377
+ close(true);
378
+ else
379
+ openList(); }, onKeyDown: event => onKey(event, false), onKeyUp: event => { if (event.key === " ")
380
+ window.setTimeout(() => { suppressClick.current = false; }); }, children: _jsx("span", { ref: slotRef, className: styles.slot, "data-hidden": showSearch || undefined, children: _jsx(AnimatePresence, { initial: false, custom: changeForLayer, children: _jsx(motion.span, { className: styles.layer, custom: changeForLayer, variants: reduced ? valueFade : valueVariants, initial: "enter", animate: "rest", exit: "exit", transition: reduced ? { duration: .12 } : changeForLayer.fly ? { x: FLY, y: FLY, opacity: { duration: 0 } } : { y: GLIDE, opacity: { duration: .2, ease: enter }, filter: { duration: .22, ease: enter } }, children: renderValue(selectedOption) }, layerKey) }) }) }), canSearch && _jsxs(motion.div, { className: styles.searchRow, inert: !showSearch || undefined, initial: false, animate: showSearch ? { opacity: 1, filter: "blur(0px)" } : { opacity: 0, filter: reduced ? "blur(0px)" : `blur(${blur.subtle}px)` }, transition: showSearch ? { duration: .18, ease: enter, delay: reduced ? 0 : .04 } : { duration: .1, ease: standard }, children: [_jsx(Search, { className: styles.searchIcon, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("input", { ref: inputRef, className: styles.input, type: "text", role: "combobox", "aria-label": `Search ${label.toLowerCase()}`, "aria-expanded": open, "aria-controls": listId, "aria-autocomplete": "list", "aria-activedescendant": open && current !== null ? optionId(indexOf.get(current) ?? 0) : undefined, placeholder: searchPlaceholder ?? selectedOption?.label ?? "Search", value: query, autoComplete: "off", spellCheck: false, onChange: event => onQuery(event.target.value), onKeyDown: event => onKey(event, true) }), _jsx(AnimatePresence, { initial: false, children: query && _jsx(motion.button, { type: "button", className: styles.clear, "aria-label": "Clear search", onPointerDown: event => event.preventDefault(), onClick: () => { onQuery(""); inputRef.current?.focus(); }, initial: { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: .6, transition: { duration: .1 } }, transition: reduced ? { duration: 0 } : GLIDE, children: _jsx(X, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) }, "clear") }), _jsx("button", { type: "button", className: styles.closeHit, "aria-label": `Close ${label.toLowerCase()}`, onClick: () => close(true) })] }), _jsx(motion.span, { className: styles.chevron, "aria-hidden": "true", initial: false, animate: { rotate: open ? 180 : 0 }, transition: reduced ? { duration: 0 } : GLIDE, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) })] }), _jsx(motion.div, { ref: listFaceRef, className: styles.listFace, inert: !open || undefined, "aria-hidden": !open || undefined, initial: false, animate: open ? { opacity: 1, y: 0, filter: "blur(0px)" } : { opacity: 0, y: reduced ? 0 : -6, filter: reduced ? "blur(0px)" : `blur(${blur.subtle}px)` }, transition: open ? { y: GROW, opacity: { duration: .2, ease: enter, delay: reduced ? 0 : .04 }, filter: { duration: .22, ease: enter, delay: .04 } } : { duration: .1, ease: standard }, children: _jsx("div", { ref: scrollRef, className: styles.scroll, children: _jsxs("div", { id: listId, className: styles.options, role: "listbox", "aria-labelledby": labelId, children: [_jsx(motion.span, { className: styles.highlight, style: { y: hy, height: hh, opacity: ho }, "aria-hidden": "true" }), visibleSections.map(section => {
381
+ const rows = section.options.map(option => {
382
+ const index = indexOf.get(option.value) ?? 0;
383
+ const isSelected = option.value === selected, isActive = option.value === current;
384
+ return _jsxs("div", { id: optionId(index), role: "option", "aria-selected": isSelected, "aria-disabled": option.disabled || undefined, className: styles.option, "data-active": isActive || undefined, ref: node => { if (node)
385
+ optionRefs.current.set(option.value, node);
386
+ else
387
+ optionRefs.current.delete(option.value); }, onPointerMove: event => { if (event.pointerType === "mouse" && !option.disabled && option.value !== current)
388
+ setActive(option.value); }, onPointerDown: event => event.preventDefault(), onClick: event => commit(option, event.currentTarget), children: [_jsxs("span", { className: styles.optionValue, "data-part": "value", children: [option.icon && _jsx("span", { className: styles.icon, "aria-hidden": "true", children: option.icon }), _jsx("span", { className: styles.valueText, children: option.label })] }), option.meta && _jsx("span", { className: styles.meta, children: option.meta }), _jsx("span", { className: styles.check, "data-on": isSelected || undefined, "aria-hidden": "true", children: _jsx(Check, { size: 16, strokeWidth: 1.75 }) })] }, option.value);
389
+ });
390
+ return section.label
391
+ ? _jsxs("div", { role: "group", "aria-labelledby": `${uid}-${section.key}`, className: styles.group, children: [_jsx("div", { id: `${uid}-${section.key}`, className: styles.groupLabel, role: "presentation", children: section.label }), rows] }, section.key)
392
+ : _jsx("div", { role: "presentation", className: styles.group, children: rows }, section.key);
393
+ }), resultCount === 0 && _jsxs("p", { className: styles.empty, children: ["No matches for \u201C", query.trim(), "\u201D"] })] }) }) })] })] }), name && _jsx("input", { type: "hidden", name: name, value: selected ?? "" }), _jsx("span", { className: styles.srOnly, role: "status", "aria-live": "polite", children: announcement })] });
394
+ }
395
+ export default MorphSelect;
@@ -0,0 +1,88 @@
1
+ /* The root lays out the label and the anchor. The anchor holds the closed trigger's place; the shape floats over it and grows past it when open. */
2
+ .root { --ms-h: var(--control-height-md); --ms-trigger-w: 0px; position: relative; display: inline-grid; justify-items: start; gap: var(--space-related); min-width: 0; color: var(--foreground); font-family: var(--font-body); font-size: var(--text-sm); letter-spacing: var(--tracking-body); line-height: var(--leading-body); }
3
+ .root[data-align="end"] { justify-items: end; }
4
+ .root[data-open] { z-index: 30; }
5
+ .label { color: var(--text-secondary); font-size: var(--text-sm); font-weight: 500; }
6
+
7
+ .anchor { position: relative; height: var(--ms-h); max-width: 100%; }
8
+
9
+ /* One material for both states. The border is an overlay so it never changes a measurement. */
10
+ .shape { position: absolute; top: 0; left: 0; overflow: clip; background: var(--surface); box-shadow: var(--shadow-resting); transition: box-shadow var(--duration-considered) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard); }
11
+ .shape::after { content: ""; position: absolute; inset: 0; z-index: 2; border: 1px solid var(--border); border-radius: inherit; corner-shape: inherit; pointer-events: none; transition: border-color var(--duration-fast) var(--ease-standard); }
12
+ .root[data-align="end"] .shape { right: 0; left: auto; }
13
+ .shape { corner-shape: round; }
14
+ .root[data-open] .shape { corner-shape: var(--corner-shape); }
15
+ .root[data-open] .shape { background: var(--surface-raised); box-shadow: var(--shadow-floating); }
16
+ .root[data-disabled] .shape { opacity: .55; }
17
+ @media (hover: hover) and (pointer: fine) {
18
+ .root:not([data-open], [data-disabled]) .shape:hover { background: var(--surface-muted); }
19
+ .root:not([data-open], [data-disabled]) .shape:hover::after { border-color: var(--border-strong); }
20
+ }
21
+
22
+ /* The lid is the trigger row. It stays at the top of the surface in both states and sits above the list, so a lifted label passes over it. */
23
+ .lid { position: absolute; z-index: 1; top: 0; right: 0; left: 0; height: var(--ms-h); }
24
+
25
+ .trigger { box-sizing: border-box; display: flex; height: var(--ms-h); align-items: center; gap: var(--space-related); padding: 0 36px 0 14px; font-size: var(--text-sm); font-weight: 500; white-space: nowrap; }
26
+ .lid .trigger { position: absolute; inset: 0; width: 100%; margin: 0; border: 0; background: transparent; color: inherit; cursor: pointer; font-family: inherit; letter-spacing: inherit; text-align: left; -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
27
+ .lid .trigger:disabled { cursor: not-allowed; }
28
+ /* An invisible copy that says how wide the closed trigger wants to be. It sits in flow until the first measurement lands, so the server render already has the right size. */
29
+ .measure { position: static; visibility: hidden; pointer-events: none; width: max-content; max-width: min(20rem, calc(100vw - 2rem)); }
30
+ .root[data-ready] .measure { position: absolute; top: 0; left: 0; }
31
+ .root[data-align="end"][data-ready] .measure { right: 0; left: auto; }
32
+
33
+ .slot { position: relative; display: block; min-width: 0; height: 100%; flex: 1; }
34
+ .slot[data-hidden] { opacity: 0; transition: opacity var(--duration-instant) var(--ease-standard); }
35
+ .layer { position: absolute; inset: 0; display: flex; align-items: center; gap: var(--space-related); min-width: 0; }
36
+ .valueText { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
37
+ .placeholder { color: var(--text-muted); font-weight: 400; }
38
+ .icon { display: grid; width: 20px; height: 20px; flex: none; place-items: center; color: var(--text-secondary); }
39
+ .icon > svg { width: 16px; height: 16px; }
40
+ .icon > img { width: 20px; height: 20px; border-radius: 50%; corner-shape: round; object-fit: cover; }
41
+
42
+ .chevron { position: absolute; top: 50%; right: 12px; display: grid; width: 16px; height: 16px; margin-top: -8px; place-items: center; color: var(--text-muted); pointer-events: none; }
43
+
44
+ /* Search takes the lid's place when the list is long. */
45
+ .searchRow { position: absolute; inset: 0; display: flex; align-items: center; gap: var(--space-related); padding: 0 2px 0 14px; }
46
+ .searchRow[inert] { pointer-events: none; }
47
+ .searchIcon { flex: none; color: var(--text-muted); }
48
+ .input { min-width: 0; height: 100%; flex: 1; padding: 0; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); letter-spacing: var(--tracking-body); outline: none; }
49
+ .input::placeholder { color: var(--text-muted); }
50
+ .clear { display: grid; width: 24px; height: 24px; flex: none; place-items: center; padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: color-mix(in oklab, var(--foreground) 8%, transparent); color: var(--text-secondary); cursor: pointer; -webkit-tap-highlight-color: transparent; }
51
+ /* Sits exactly over the shared chevron, so the chevron closes the list in both lid states. */
52
+ .closeHit { width: 36px; height: 36px; flex: none; padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard); }
53
+ @media (hover: hover) and (pointer: fine) {
54
+ .closeHit:hover, .clear:hover { background: color-mix(in oklab, var(--foreground) 8%, transparent); }
55
+ .clear:hover { color: var(--foreground); }
56
+ }
57
+
58
+ /* The list hangs under the lid at its own width, so it measures the same whatever size the surface is mid spring. */
59
+ .listFace { position: absolute; top: var(--ms-h); left: 0; box-sizing: border-box; width: max(var(--ms-trigger-w), min(var(--ms-panel-w), calc(100vw - 2rem))); padding: 0 6px 6px; }
60
+ .root[data-align="end"] .listFace { right: 0; left: auto; }
61
+ .listFace::before { content: ""; position: absolute; top: 0; right: 14px; left: 14px; height: 1px; background: var(--border-subtle); }
62
+ .listFace[inert] { pointer-events: none; }
63
+ .scroll { max-height: var(--ms-list-max); overflow-y: auto; overscroll-behavior: contain; padding-top: 6px; scrollbar-width: thin; }
64
+ .options { position: relative; display: grid; gap: 2px; }
65
+ .highlight { position: absolute; top: 0; right: 0; left: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--foreground) 6.5%, transparent); pointer-events: none; }
66
+
67
+ .group { display: grid; gap: 2px; }
68
+ .group + .group { margin-top: 4px; }
69
+ .groupLabel { padding: 8px 10px 4px; color: var(--text-muted); font-size: var(--text-xs); font-weight: 400; }
70
+
71
+ .option { position: relative; display: flex; min-height: 36px; box-sizing: border-box; align-items: center; gap: var(--space-related); padding: 0 10px 0 8px; border-radius: var(--radius-control); corner-shape: var(--corner-shape); color: var(--text-secondary); cursor: pointer; user-select: none; -webkit-user-select: none; transition: color var(--duration-fast) var(--ease-standard); }
72
+ .option[data-active], .option[aria-selected="true"] { color: var(--foreground); }
73
+ .option[aria-disabled="true"] { color: var(--text-muted); cursor: not-allowed; opacity: .6; }
74
+ /* Lid padding minus list inset, so a lifted label starts exactly level with where it lands. */
75
+ .optionValue { display: flex; min-width: 0; flex: 1; align-items: center; gap: var(--space-related); font-weight: 500; }
76
+ .meta { flex: none; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
77
+ .check { display: grid; width: 16px; height: 16px; flex: none; place-items: center; color: var(--foreground); opacity: 0; transform: scale(.6); transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
78
+ .check[data-on] { opacity: 1; transform: none; }
79
+ .empty { margin: 0; padding: 12px 10px 14px; color: var(--text-muted); font-size: var(--text-sm); }
80
+
81
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
82
+
83
+ @media (prefers-reduced-motion: reduce) {
84
+ .shape, .check, .slot { transition-duration: 0ms; }
85
+ }
86
+ @media (prefers-contrast: more) {
87
+ .shape::after { border-color: var(--border-strong); }
88
+ }
@@ -0,0 +1,18 @@
1
+ export interface MultiSelectOption {
2
+ value: string;
3
+ label: string;
4
+ disabled?: boolean;
5
+ }
6
+ export interface MultiSelectProps {
7
+ label: string;
8
+ options: MultiSelectOption[];
9
+ value?: string[];
10
+ defaultValue?: string[];
11
+ onValueChange?: (value: string[]) => void;
12
+ placeholder?: string;
13
+ description?: string;
14
+ maxVisible?: number;
15
+ disabled?: boolean;
16
+ className?: string;
17
+ }
18
+ export declare function MultiSelect({ label, options, value, defaultValue, onValueChange, placeholder, description, maxVisible, disabled, className }: MultiSelectProps): import("react").JSX.Element;