@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,446 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
4
+ import { Check, ChevronLeft, ChevronRight, Plus, X } from "lucide-react";
5
+ import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion, useTransform } from "motion/react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./filter-toolbar.module.css";
8
+ const enter = { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] };
9
+ const leave = { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] };
10
+ const still = { duration: 0 };
11
+ const fade = { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] };
12
+ const blur = (px) => `blur(${px}px)`;
13
+ const clamp = (value) => Math.min(1, Math.max(0, value));
14
+ const toOption = (entry) => typeof entry === "string" ? { value: entry } : entry;
15
+ const optionText = (entry) => entry.label ?? entry.value;
16
+ const textMotion = (reduced) => ({
17
+ initial: reduced ? { opacity: 0 } : { opacity: 0, y: "0.3em", filter: blur(motionTokens.blur.soft) },
18
+ animate: { opacity: 1, y: 0, filter: blur(0) },
19
+ exit: reduced ? { opacity: 0, transition: still } : { opacity: 0, y: "-0.3em", filter: blur(motionTokens.blur.subtle), transition: leave },
20
+ transition: reduced ? fade : enter,
21
+ });
22
+ /** Text that swaps with a rise and blur while its box springs to the new width, so nothing beside it snaps. */
23
+ function MorphText({ text }) {
24
+ const reduced = useReducedMotion() ?? false;
25
+ const measure = useRef(null);
26
+ const width = useMotionValue("auto");
27
+ useLayoutEffect(() => {
28
+ const node = measure.current;
29
+ if (!node || typeof ResizeObserver === "undefined")
30
+ return;
31
+ let last = null;
32
+ // The observer reports the layout size, so an entering chip's scale never shrinks the measurement.
33
+ const observer = new ResizeObserver(([entry]) => {
34
+ if (!entry)
35
+ return;
36
+ const next = Math.ceil(entry.borderBoxSize?.[0]?.inlineSize ?? node.offsetWidth), changed = last !== null && last !== node.textContent;
37
+ last = node.textContent;
38
+ // Only a new value morphs; the first measure and font swaps settle at once.
39
+ if (changed && !reduced)
40
+ animate(width, next, motionTokens.spring.morph);
41
+ else
42
+ width.jump(next);
43
+ });
44
+ observer.observe(node);
45
+ return () => observer.disconnect();
46
+ }, [reduced, width]);
47
+ return _jsxs(motion.span, { className: styles.morphText, style: { width }, children: [_jsx("span", { ref: measure, className: styles.measure, "aria-hidden": "true", children: text }), _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(motion.span, { className: styles.morphValue, ...textMotion(reduced), children: text }, text) })] });
48
+ }
49
+ /** Springs a wrapper to the height of its content while filters change; other resizes follow at once. */
50
+ function useFollowHeight(list, armed, reduced) {
51
+ const height = useMotionValue("auto");
52
+ useLayoutEffect(() => {
53
+ const node = list.current;
54
+ if (!node || typeof ResizeObserver === "undefined")
55
+ return;
56
+ let known = false;
57
+ const observer = new ResizeObserver(() => {
58
+ const next = node.offsetHeight;
59
+ if (!known || reduced || performance.now() > armed.current) {
60
+ known = true;
61
+ height.jump(next);
62
+ return;
63
+ }
64
+ animate(height, next, motionTokens.spring.smooth);
65
+ });
66
+ observer.observe(node);
67
+ return () => observer.disconnect();
68
+ }, [armed, height, list, reduced]);
69
+ return height;
70
+ }
71
+ /** Chips glide with the slot widths, but a chip that wraps to another line would teleport. Resize observers run after
72
+ * layout and before paint, so each jump is offset in the same frame and springs back to zero from where it was seen. */
73
+ function useReflowGlide(list, armed, reduced) {
74
+ useLayoutEffect(() => {
75
+ const node = list.current;
76
+ if (!node || typeof ResizeObserver === "undefined")
77
+ return;
78
+ const tracks = new Map();
79
+ const check = () => {
80
+ const now = performance.now();
81
+ for (const child of Array.from(node.children)) {
82
+ const x = child.offsetLeft, y = child.offsetTop, track = tracks.get(child);
83
+ if (!track) {
84
+ tracks.set(child, { x, y, dx: 0, dy: 0 });
85
+ continue;
86
+ }
87
+ const jx = track.x - x, jy = track.y - y;
88
+ track.x = x;
89
+ track.y = y;
90
+ if (reduced || now > armed.current || (Math.abs(jy) < 1 && Math.abs(jx) < 40))
91
+ continue;
92
+ track.stop?.();
93
+ const fromX = track.dx + jx, fromY = track.dy + jy;
94
+ const apply = (progress) => { track.dx = fromX * (1 - progress); track.dy = fromY * (1 - progress); child.style.translate = progress >= 1 ? "" : `${track.dx}px ${track.dy}px`; };
95
+ apply(0);
96
+ track.stop = animate(0, 1, { ...motionTokens.spring.smooth, onUpdate: apply, onComplete: () => apply(1) }).stop;
97
+ }
98
+ for (const child of tracks.keys())
99
+ if (child.parentElement !== node)
100
+ tracks.delete(child);
101
+ };
102
+ const observer = new ResizeObserver(check);
103
+ const watch = () => { for (const child of Array.from(node.children))
104
+ observer.observe(child); };
105
+ const mutations = new MutationObserver(() => { watch(); check(); });
106
+ observer.observe(node);
107
+ watch();
108
+ mutations.observe(node, { childList: true });
109
+ return () => { observer.disconnect(); mutations.disconnect(); for (const track of tracks.values())
110
+ track.stop?.(); };
111
+ }, [armed, list, reduced]);
112
+ }
113
+ function describeChange(before, after) {
114
+ const text = (filter) => filter.value ? `${filter.label}: ${filter.value}` : filter.label;
115
+ const removed = before.filter(old => !after.some(filter => filter.id === old.id));
116
+ if (!after.length && removed.length > 1)
117
+ return "All filters cleared";
118
+ const added = after.filter(filter => !before.some(old => old.id === filter.id));
119
+ const changed = after.filter(filter => before.some(old => old.id === filter.id && old.value !== filter.value));
120
+ return [...added.map(filter => `Added ${text(filter)}`), ...changed.map(filter => `${filter.label} changed to ${filter.value ?? "any"}`), ...removed.map(filter => `Removed ${text(filter)}`)].join(". ");
121
+ }
122
+ /** A roving list with one highlight that glides under the pointer and jumps with the keyboard. */
123
+ function MenuList({ rows, labelledBy, onChoose, onBack, onLeave, radio }) {
124
+ const reduced = useReducedMotion() ?? false;
125
+ const present = useIsPresent();
126
+ const list = useRef(null);
127
+ const pointer = useRef(false);
128
+ const typed = useRef({ text: "", at: 0 });
129
+ const [current, setCurrent] = useState(() => Math.max(0, rows.findIndex(row => row.checked)));
130
+ const [highlight, setHighlight] = useState(null);
131
+ const items = () => Array.from(list.current?.querySelectorAll("[data-row]") ?? []);
132
+ const move = (item) => {
133
+ const node = list.current;
134
+ if (!item || !node)
135
+ return;
136
+ item.focus({ preventScroll: true });
137
+ if (item.offsetTop < node.scrollTop)
138
+ node.scrollTop = item.offsetTop;
139
+ else if (item.offsetTop + item.offsetHeight > node.scrollTop + node.clientHeight)
140
+ node.scrollTop = item.offsetTop + item.offsetHeight - node.clientHeight;
141
+ };
142
+ function onKeyDown(event) {
143
+ const all = items(), index = all.indexOf(document.activeElement);
144
+ const go = (next) => { event.preventDefault(); pointer.current = false; move(all[next]); };
145
+ if (event.key === "ArrowDown")
146
+ return go(index < 0 ? 0 : (index + 1) % all.length);
147
+ if (event.key === "ArrowUp")
148
+ return go(index < 0 ? all.length - 1 : (index - 1 + all.length) % all.length);
149
+ if (event.key === "Home")
150
+ return go(0);
151
+ if (event.key === "End")
152
+ return go(all.length - 1);
153
+ if (event.key === "ArrowRight" && index >= 0 && rows[index]?.drill) {
154
+ event.preventDefault();
155
+ onChoose(rows[index], true);
156
+ return;
157
+ }
158
+ if (event.key === "ArrowLeft" && onBack) {
159
+ event.preventDefault();
160
+ onBack(true);
161
+ return;
162
+ }
163
+ if (event.key.length === 1 && /\S/.test(event.key) && !event.metaKey && !event.ctrlKey && !event.altKey) {
164
+ const now = performance.now();
165
+ typed.current = { text: now - typed.current.at < 700 ? typed.current.text + event.key.toLowerCase() : event.key.toLowerCase(), at: now };
166
+ const order = [...all.slice(index + (typed.current.text.length > 1 ? 0 : 1)), ...all.slice(0, index + (typed.current.text.length > 1 ? 0 : 1))];
167
+ const match = order.find(item => (item.dataset.text ?? "").toLowerCase().startsWith(typed.current.text));
168
+ if (match)
169
+ go(all.indexOf(match));
170
+ }
171
+ }
172
+ function onFocus(event) {
173
+ const item = event.target.closest("[data-row]");
174
+ if (!item)
175
+ return;
176
+ setCurrent(items().indexOf(item));
177
+ // Keyboard focus also draws the focus ring inside the highlight, where the scrolling list cannot clip it.
178
+ const glide = pointer.current && !reduced, ring = !pointer.current && item.matches(":focus-visible");
179
+ setHighlight(previous => ({ top: item.offsetTop, height: item.offsetHeight, glide: glide && !!previous?.shown, ring, shown: true }));
180
+ }
181
+ function onBlur(event) {
182
+ // The highlight fades where it is; it keeps its place so the fade is visible.
183
+ if (!list.current?.contains(event.relatedTarget))
184
+ setHighlight(previous => previous && { ...previous, glide: false, ring: false, shown: false });
185
+ }
186
+ return _jsxs("div", { ref: list, className: styles.list, role: "menu", "aria-labelledby": labelledBy, "data-current": present ? "" : undefined, onKeyDown: onKeyDown, onFocus: onFocus, onBlur: onBlur, onPointerLeave: () => { if (pointer.current && list.current?.contains(document.activeElement))
187
+ onLeave(); }, children: [_jsx(motion.span, { className: styles.highlight, "data-ring": highlight?.ring || undefined, "aria-hidden": "true", initial: false, animate: highlight ? { y: highlight.top, height: highlight.height, opacity: highlight.shown ? 1 : 0 } : { opacity: 0 }, transition: { default: highlight?.glide ? motionTokens.spring.snappy : still, opacity: { duration: reduced ? 0 : .08 } } }), rows.map((row, index) => _jsxs("button", { type: "button", className: styles.item, role: radio ? "menuitemradio" : "menuitem", "aria-checked": radio ? !!row.checked : undefined, tabIndex: index === current ? 0 : -1, "data-row": row.key, "data-text": row.label, onPointerMove: event => { pointer.current = true; if (document.activeElement !== event.currentTarget)
188
+ event.currentTarget.focus({ preventScroll: true }); }, onClick: (event) => onChoose(row, event.detail === 0), children: [row.icon ? _jsx("span", { className: styles.itemIcon, "aria-hidden": "true", children: row.icon }) : null, _jsx("span", { className: styles.itemLabel, children: row.label }), row.meta ? _jsx("span", { className: styles.itemMeta, children: row.meta }) : null] }, row.key))] });
189
+ }
190
+ /** The room the panel may use: the viewport, narrowed on each axis by any ancestor that clips that axis, so a panel
191
+ * inside a scrolling or clipped container flips and shifts within what is actually visible. */
192
+ function roomFor(node) {
193
+ const room = { left: 0, top: 0, right: document.documentElement.clientWidth, bottom: window.innerHeight };
194
+ for (let parent = node.parentElement; parent && parent !== document.body; parent = parent.parentElement) {
195
+ const style = getComputedStyle(parent);
196
+ if (style.overflowX === "visible" && style.overflowY === "visible")
197
+ continue;
198
+ const box = parent.getBoundingClientRect();
199
+ if (style.overflowX !== "visible") {
200
+ room.left = Math.max(room.left, box.left);
201
+ room.right = Math.min(room.right, box.right);
202
+ }
203
+ if (style.overflowY !== "visible") {
204
+ room.top = Math.max(room.top, box.top);
205
+ room.bottom = Math.min(room.bottom, box.bottom);
206
+ }
207
+ }
208
+ return room;
209
+ }
210
+ /** An Add filter button that grows into its own menu: pick a field, then a value. The shape springs between the
211
+ * button and the panel, and the panel renders in place, so keep its ancestors free of overflow clipping. */
212
+ export function FilterMenu({ fields, onSelect, active = [], label = "Add filter", align = "end" }) {
213
+ const reduced = useReducedMotion() ?? false;
214
+ const id = useId(), panelId = `${id}panel`, titleId = `${id}title`;
215
+ const root = useRef(null), trigger = useRef(null), panel = useRef(null);
216
+ const phaseRef = useRef("closed"), run = useRef(0), focusNext = useRef(null);
217
+ const [phase, setPhase] = useState("closed");
218
+ const [fieldId, setFieldId] = useState(null);
219
+ const [direction, setDirection] = useState(1);
220
+ const [up, setUp] = useState(false);
221
+ // The content scales up from the trigger's center, so it reads as growing out of the button.
222
+ const [origin, setOrigin] = useState("100% 0");
223
+ // The surface is the button's own rounded rectangle. At rest it fills the trigger; while open it springs to the panel.
224
+ const width = useMotionValue("100%"), height = useMotionValue("100%");
225
+ const left = useMotionValue(0), target = useMotionValue(0);
226
+ // The panel content stays put at its final position while the surface edge travels across it.
227
+ const panelX = useTransform(() => target.get() - left.get());
228
+ // How far the shape has grown from the button (0) to the panel (1). The label and the content read it, so they
229
+ // reveal with the morph, never ahead of the growing edge, and reverse with it when a close interrupts an open.
230
+ const from = useMotionValue(0), to = useMotionValue(0);
231
+ const progress = useTransform(() => { const now = width.get(), start = from.get(), end = to.get(); return typeof now !== "number" ? 0 : end > start ? clamp((now - start) / (end - start)) : 1; });
232
+ const faceOut = useTransform(() => clamp(progress.get() / .35)), contentIn = useTransform(() => clamp((progress.get() - .4) / .5));
233
+ // Reduced motion jumps the shape, so progress goes 0 to 1 in one frame and these in-between values never paint.
234
+ const faceStyle = { opacity: useTransform(() => 1 - faceOut.get()), scale: useTransform(() => 1 - faceOut.get() * .04), filter: useTransform(() => faceOut.get() ? blur(faceOut.get() * motionTokens.blur.subtle) : "none") };
235
+ const contentStyle = { opacity: contentIn, transformOrigin: origin, scale: useTransform(() => .96 + contentIn.get() * .04), y: useTransform(() => (1 - contentIn.get()) * (up ? -6 : 6)), filter: useTransform(() => contentIn.get() === 1 ? "none" : blur((1 - contentIn.get()) * motionTokens.blur.soft)) };
236
+ const open = phase === "open", field = fields.find(entry => entry.id === fieldId) ?? null;
237
+ const openMenu = useCallback((focus) => {
238
+ const button = trigger.current, content = panel.current;
239
+ if (!button || !content)
240
+ return;
241
+ const rect = button.getBoundingClientRect(), panelWidth = content.offsetWidth + 2, panelHeight = content.offsetHeight + 2;
242
+ const room = roomFor(root.current ?? button), gutter = 16;
243
+ const min = room.left + gutter - rect.left, max = room.right - gutter - rect.left - panelWidth, end = rect.width - panelWidth;
244
+ let x = align === "end" ? (end >= min ? end : 0 <= max ? 0 : end) : (0 <= max ? 0 : end >= min ? end : 0);
245
+ x = Math.round(Math.min(Math.max(x, min), Math.max(min, max)));
246
+ const below = room.bottom - rect.top, above = rect.bottom - room.top;
247
+ const nextUp = below < panelHeight + gutter && above > below;
248
+ if (phaseRef.current === "closed") {
249
+ width.jump(rect.width);
250
+ height.jump(rect.height);
251
+ left.jump(0);
252
+ }
253
+ target.jump(x);
254
+ from.jump(rect.width);
255
+ to.jump(panelWidth);
256
+ phaseRef.current = "open";
257
+ run.current += 1;
258
+ focusNext.current = focus;
259
+ setPhase("open");
260
+ setUp(nextUp);
261
+ setFieldId(null);
262
+ setDirection(-1);
263
+ setOrigin(`${Math.round(rect.width / 2 - x)}px ${nextUp ? "100%" : "0"}`);
264
+ const transition = reduced ? still : motionTokens.spring.morph;
265
+ animate(left, x, transition);
266
+ animate(width, panelWidth, transition);
267
+ animate(height, panelHeight, transition);
268
+ }, [align, from, height, left, reduced, target, to, width]);
269
+ const closeMenu = useCallback((restore) => {
270
+ const button = trigger.current;
271
+ if (phaseRef.current !== "open" || !button)
272
+ return;
273
+ phaseRef.current = "closing";
274
+ const token = (run.current += 1);
275
+ setPhase("closing");
276
+ // Focus returns to the trigger; its ring shows only when the menu was closed from the keyboard.
277
+ if (restore)
278
+ button.focus({ preventScroll: true, focusVisible: restore === "keyboard" });
279
+ // Closing is quicker than opening: the snappy spring lands the shape back on the button.
280
+ const transition = reduced ? still : motionTokens.spring.snappy;
281
+ animate(left, 0, transition);
282
+ const rect = button.getBoundingClientRect();
283
+ animate(height, rect.height, transition);
284
+ animate(width, rect.width, { ...transition, onComplete: () => {
285
+ if (token !== run.current)
286
+ return;
287
+ phaseRef.current = "closed";
288
+ width.jump("100%");
289
+ height.jump("100%");
290
+ setPhase("closed");
291
+ setFieldId(null);
292
+ } });
293
+ }, [height, left, reduced, width]);
294
+ // A new step changes the panel height; the surface follows on the smooth spring instead of snapping.
295
+ useLayoutEffect(() => {
296
+ const content = panel.current;
297
+ if (!content || typeof ResizeObserver === "undefined")
298
+ return;
299
+ const observer = new ResizeObserver(() => {
300
+ if (phaseRef.current !== "open")
301
+ return;
302
+ animate(height, content.offsetHeight + 2, reduced ? still : motionTokens.spring.smooth);
303
+ });
304
+ observer.observe(content);
305
+ return () => observer.disconnect();
306
+ }, [height, reduced]);
307
+ // Focus moves into the panel once it is interactive: an item for keyboard users, the dialog itself for the pointer.
308
+ useLayoutEffect(() => {
309
+ const request = focusNext.current, content = panel.current;
310
+ if (!request || !content || phase !== "open")
311
+ return;
312
+ focusNext.current = null;
313
+ if (request === "panel") {
314
+ content.focus({ preventScroll: true });
315
+ return;
316
+ }
317
+ const items = Array.from(content.querySelectorAll("[data-current] [data-row]"));
318
+ const next = request === "first" ? items[0] : request === "last" ? items.at(-1) : request === "checked" ? items.find(item => item.getAttribute("aria-checked") === "true") ?? items[0] : items.find(item => item.dataset.row === request.key) ?? items[0];
319
+ next?.focus({ preventScroll: true });
320
+ }, [phase, fieldId]);
321
+ useEffect(() => {
322
+ if (phase !== "open")
323
+ return;
324
+ const onPointerDown = (event) => {
325
+ const node = root.current, hit = event.target;
326
+ if (!node || (hit && node.contains(hit)))
327
+ return;
328
+ closeMenu(false);
329
+ // A press on empty space hands focus back to the trigger; a press on another control keeps its own focus.
330
+ if (!hit?.closest?.("button, a[href], input, select, textarea, [tabindex], [contenteditable]"))
331
+ window.setTimeout(() => {
332
+ const focused = document.activeElement;
333
+ if (!focused || focused === document.body || node.contains(focused))
334
+ trigger.current?.focus({ preventScroll: true, focusVisible: false });
335
+ }, 0);
336
+ };
337
+ document.addEventListener("pointerdown", onPointerDown, true);
338
+ return () => document.removeEventListener("pointerdown", onPointerDown, true);
339
+ }, [closeMenu, phase]);
340
+ function onPanelKeyDown(event) {
341
+ if (event.defaultPrevented)
342
+ return;
343
+ const items = Array.from(panel.current?.querySelectorAll("[data-current] [data-row]") ?? []);
344
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
345
+ event.preventDefault();
346
+ (event.key === "ArrowDown" ? items[0] : items.at(-1))?.focus({ preventScroll: true });
347
+ }
348
+ if (event.key === "ArrowLeft" && field) {
349
+ event.preventDefault();
350
+ goBack(true);
351
+ }
352
+ }
353
+ function goBack(keyboard) {
354
+ if (!field)
355
+ return;
356
+ focusNext.current = keyboard ? { key: field.id } : "panel";
357
+ setDirection(-1);
358
+ setFieldId(null);
359
+ }
360
+ function chooseField(row, keyboard) {
361
+ focusNext.current = keyboard ? "checked" : "panel";
362
+ setDirection(1);
363
+ setFieldId(row.key);
364
+ }
365
+ function chooseValue(row, keyboard) {
366
+ if (!field)
367
+ return;
368
+ onSelect({ id: field.id, label: field.label, value: row.label }, field);
369
+ closeMenu(keyboard ? "keyboard" : "pointer");
370
+ }
371
+ const current = (entry) => active.find(filter => filter.id === entry.id)?.value;
372
+ const fieldRows = fields.map(entry => ({ key: entry.id, label: entry.label, icon: entry.icon, drill: true,
373
+ meta: _jsxs(_Fragment, { children: [current(entry) ? _jsx("span", { className: styles.metaValue, children: current(entry) }) : null, _jsx(ChevronRight, { size: 15, strokeWidth: 1.8, "aria-hidden": "true" })] }) }));
374
+ const valueRows = field ? field.options.map(toOption).map(entry => {
375
+ const text = optionText(entry), checked = current(field) === text;
376
+ return { key: entry.value, label: text, icon: entry.icon, checked, meta: entry.hint != null || checked ? _jsxs(_Fragment, { children: [entry.hint != null ? _jsx("span", { className: styles.hint, children: entry.hint }) : null, _jsx("span", { className: styles.check, "data-on": checked || undefined, children: _jsx(Check, { size: 15, strokeWidth: 2, "aria-hidden": "true" }) })] }) : null };
377
+ }) : [];
378
+ // Steps travel in the direction of the move: forward slides in from the end edge, back from the start edge.
379
+ const step = {
380
+ enter: (dir) => reduced ? { opacity: 0 } : { opacity: 0, x: dir * 18, filter: blur(motionTokens.blur.soft) },
381
+ center: { opacity: 1, x: 0, filter: blur(0), transition: reduced ? fade : { ...enter, x: motionTokens.spring.smooth } },
382
+ exit: (dir) => reduced ? { opacity: 0, transition: still } : { opacity: 0, x: dir * -18, filter: blur(motionTokens.blur.subtle), transition: leave },
383
+ };
384
+ return _jsxs("div", { ref: root, className: styles.menu, "data-state": phase, "data-y": up ? "up" : "down", onKeyDown: event => { if (event.key === "Escape" && phaseRef.current === "open") {
385
+ event.preventDefault();
386
+ event.stopPropagation();
387
+ closeMenu("keyboard");
388
+ } }, onBlur: event => { const next = event.relatedTarget; if (phaseRef.current === "open" && next && !root.current?.contains(next))
389
+ closeMenu(false); }, children: [_jsx("button", { ref: trigger, type: "button", className: styles.trigger, "data-filter-trigger": "", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": panelId, tabIndex: open ? -1 : undefined, onClick: event => { if (phaseRef.current !== "open")
390
+ openMenu(event.detail === 0 ? "first" : "panel"); }, onKeyDown: event => { if (event.key === "ArrowDown" || event.key === "ArrowUp") {
391
+ event.preventDefault();
392
+ openMenu(event.key === "ArrowDown" ? "first" : "last");
393
+ } }, children: _jsxs(motion.span, { className: styles.face, style: faceStyle, children: [_jsx(Plus, { size: 15, strokeWidth: 1.8, "aria-hidden": "true" }), label] }) }), _jsx(motion.div, { className: styles.surface, style: { left, width, height }, children: _jsx(motion.div, { ref: panel, id: panelId, className: styles.panel, role: "dialog", "aria-labelledby": titleId, tabIndex: -1, inert: !open, style: { x: panelX }, onKeyDown: onPanelKeyDown, children: _jsxs(motion.div, { style: contentStyle, children: [_jsxs("div", { className: styles.head, children: [_jsx(AnimatePresence, { initial: false, children: field ? _jsx(motion.button, { type: "button", className: styles.back, "aria-label": "Back to fields", onClick: event => goBack(event.detail === 0), initial: reduced ? { opacity: 0, width: 28, marginRight: 2 } : { opacity: 0, width: 0, marginRight: 0, scale: .6 }, animate: { opacity: 1, width: 28, marginRight: 2, scale: 1 }, exit: reduced ? { opacity: 0, transition: still } : { opacity: 0, width: 0, marginRight: 0, scale: .6, transition: { ...motionTokens.spring.snappy, opacity: leave } }, transition: reduced ? fade : { ...motionTokens.spring.snappy, opacity: enter }, children: _jsx(ChevronLeft, { size: 16, strokeWidth: 1.8, "aria-hidden": "true" }) }, "back") : null }), _jsx("span", { className: styles.title, id: titleId, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(motion.span, { className: styles.titleText, ...textMotion(reduced), children: field ? field.label : label }, field?.id ?? "fields") }) })] }), _jsx("div", { className: styles.stage, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, custom: direction, children: _jsx(motion.div, { className: styles.step, custom: direction, variants: step, initial: "enter", animate: "center", exit: "exit", children: field
394
+ ? _jsx(MenuList, { rows: valueRows, labelledBy: titleId, radio: true, onChoose: chooseValue, onBack: goBack, onLeave: () => panel.current?.focus({ preventScroll: true }) })
395
+ : _jsx(MenuList, { rows: fieldRows, labelledBy: titleId, onChoose: chooseField, onLeave: () => panel.current?.focus({ preventScroll: true }) }) }, field?.id ?? "fields") }) })] }) }) })] });
396
+ }
397
+ export function FilterToolbar({ filters, onRemove, onClearAll, children, label = "Active filters", addFilter }) {
398
+ const reduced = useReducedMotion() ?? false;
399
+ const root = useRef(null), list = useRef(null);
400
+ const armed = useRef(0), pendingFocus = useRef(null);
401
+ const [seen, setSeen] = useState(filters);
402
+ const [message, setMessage] = useState("");
403
+ if (seen !== filters) {
404
+ setSeen(filters);
405
+ const next = describeChange(seen, filters);
406
+ if (next)
407
+ setMessage(next);
408
+ }
409
+ const signature = filters.map(filter => `${filter.id}:${filter.value ?? ""}`).join("|");
410
+ // Layout motion runs only for a moment after the filters change, so window resizes and font swaps settle at once.
411
+ useLayoutEffect(() => { armed.current = performance.now() + 900; }, [signature]);
412
+ const frameHeight = useFollowHeight(list, armed, reduced);
413
+ useReflowGlide(list, armed, reduced);
414
+ // A removed chip hands focus to its neighbor, or to the Add filter trigger once the list is empty.
415
+ useLayoutEffect(() => {
416
+ const index = pendingFocus.current, node = root.current;
417
+ if (index === null || !node)
418
+ return;
419
+ pendingFocus.current = null;
420
+ const next = filters[Math.min(index, filters.length - 1)];
421
+ const target = next ? node.querySelector(`[data-chip-remove="${CSS.escape(next.id)}"]`) : node.querySelector("[data-filter-trigger]");
422
+ target?.focus({ preventScroll: true });
423
+ }, [filters]);
424
+ const still = { duration: 0 };
425
+ // Each chip sits in a slot whose width grows or collapses, so the chips beside it, the toolbar, and its
426
+ // actions all follow in the normal flow. While the slot moves it clips, and the chip keeps its natural width.
427
+ // (Popping chips out of the flow would glide the chips but snap the width of a content-sized toolbar.)
428
+ const slot = (gap) => ({
429
+ initial: reduced ? false : { width: 0, marginRight: -gap, overflow: "clip", "--slot-moving": 1 },
430
+ animate: { width: "auto", marginRight: 0, transitionEnd: { overflow: "visible", "--slot-moving": 0 } },
431
+ exit: reduced ? { opacity: 0, transition: still } : { width: 0, marginRight: -gap, overflow: "clip", "--slot-moving": 1, pointerEvents: "none", transition: { ...motionTokens.spring.smooth, "--slot-moving": still } },
432
+ transition: reduced ? still : motionTokens.spring.smooth,
433
+ });
434
+ const chip = {
435
+ initial: reduced ? false : { opacity: 0, scale: .9, filter: blur(motionTokens.blur.soft) },
436
+ animate: { opacity: 1, scale: 1, filter: blur(0) },
437
+ exit: reduced ? { opacity: 0, transition: still } : { opacity: 0, scale: .9, filter: blur(motionTokens.blur.subtle), transition: leave },
438
+ transition: reduced ? still : { ...enter, scale: motionTokens.spring.snappy },
439
+ };
440
+ const text = reduced ? { ...textMotion(true), initial: false } : textMotion(false);
441
+ // The note waits for the leaving chips to fade before it rises in.
442
+ const emptyText = reduced ? text : { ...text, transition: { ...enter, delay: motionTokens.duration.instant * .75 } };
443
+ return _jsxs("div", { ref: root, className: styles.toolbar, role: "group", "aria-label": label, children: [_jsx(motion.div, { className: styles.frame, style: { height: frameHeight }, children: _jsxs("div", { ref: list, className: styles.chips, children: [_jsx(AnimatePresence, { initial: false, children: filters.map((filter, index) => _jsx(motion.span, { className: styles.slot, ...slot(8), children: _jsxs(motion.span, { className: styles.chip, ...chip, children: [_jsxs("span", { className: styles.chipLabel, children: [filter.label, filter.value ? _jsxs("span", { className: styles.value, children: [_jsx("span", { className: styles.separator, children: " \u00B7 " }), _jsx(MorphText, { text: filter.value })] }) : null] }), _jsx("button", { type: "button", "data-chip-remove": filter.id, "aria-label": `Remove ${filter.label}${filter.value ? `: ${filter.value}` : ""}`, onClick: event => { if (document.activeElement === event.currentTarget)
444
+ pendingFocus.current = index; onRemove(filter.id); }, children: _jsx(X, { size: 14, strokeWidth: 1.8, "aria-hidden": "true" }) })] }) }, filter.id)) }), _jsx(AnimatePresence, { initial: false, children: filters.length ? null : _jsx(motion.span, { className: styles.empty, ...emptyText, children: "No filters applied" }, "empty") })] }) }), _jsxs("div", { className: styles.actions, children: [addFilter ? _jsx(FilterMenu, { fields: addFilter.fields, onSelect: addFilter.onAdd, active: filters, label: addFilter.label, align: addFilter.align }) : null, children, _jsx(AnimatePresence, { initial: false, children: filters.length > 0 ? _jsx(motion.span, { className: styles.slot, ...slot(12), children: _jsx("button", { className: styles.clear, type: "button", onClick: () => { pendingFocus.current = -1; onClearAll?.(); }, children: _jsx(motion.span, { className: styles.clearText, ...text, children: "Clear all" }) }) }, "clear") : null })] }), _jsx("span", { className: styles.srOnly, role: "status", children: message })] });
445
+ }
446
+ export default FilterToolbar;
@@ -0,0 +1,70 @@
1
+ .toolbar { position: relative; display: flex; min-height: var(--control-height-lg); align-items: flex-start; justify-content: space-between; gap: var(--space-inset); padding: var(--space-group) var(--space-inset); border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface); }
2
+ /* The frame springs to the chip list height, so a new line of chips opens the toolbar instead of snapping it. */
3
+ .frame { min-width: 0; flex: 1; }
4
+ .chips { position: relative; display: flex; min-width: 0; min-height: var(--control-height-sm); flex-wrap: wrap; align-items: center; gap: var(--space-related); }
5
+ /* The slot clips with a pill radius while it opens, so a growing chip reads as a pill stretching out, never as a hard cut. */
6
+ .slot { display: inline-flex; max-width: 100%; min-width: 0; flex: none; border-radius: var(--radius-full); corner-shape: round; }
7
+ /* --slot-moving is 1 while the slot animates: the chip then ignores the slot width instead of squeezing. */
8
+ .chip { display: inline-flex; max-width: calc(100% + var(--slot-moving, 0) * 100vw); flex: none; min-height: var(--control-height-sm); align-items: center; gap: var(--space-related); padding: 0 var(--space-related) 0 var(--space-group); border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); }
9
+ .chipLabel { overflow-x: clip; overflow-y: visible; text-overflow: ellipsis; white-space: nowrap; }
10
+ .value { color: var(--foreground); font-weight: 500; }
11
+ .separator { color: var(--text-secondary); font-weight: 400; }
12
+ .morphText { position: relative; display: inline-flex; overflow-x: clip; overflow-y: visible; white-space: nowrap; vertical-align: bottom; }
13
+ .morphValue { display: inline-block; flex: none; white-space: nowrap; }
14
+ .measure { position: absolute; top: 0; left: 0; visibility: hidden; white-space: nowrap; pointer-events: none; }
15
+ .chip button { display: grid; width: 24px; height: 24px; flex: 0 0 24px; place-items: center; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-muted); cursor: pointer; transition: background var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
16
+ .chip button:active { transform: scale(.96); transition-duration: var(--duration-fast), var(--duration-fast), var(--duration-instant); transition-timing-function: var(--ease-standard), var(--ease-standard), var(--ease-standard); }
17
+ .empty { position: absolute; top: 0; bottom: 0; left: 0; display: flex; max-width: 100%; align-items: center; overflow: hidden; color: var(--text-muted); font-size: var(--text-sm); white-space: nowrap; pointer-events: none; }
18
+ .actions { display: inline-flex; flex: 0 0 auto; align-items: center; gap: var(--space-group); }
19
+ .clear { min-height: var(--control-height-sm); padding: 0 var(--space-group); border: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: transparent; color: var(--text-muted); font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); letter-spacing: var(--tracking-body); white-space: nowrap; cursor: pointer; transition: background var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
20
+ .clearText { display: block; }
21
+ .clear:active { transform: scale(.97); transition-duration: var(--duration-fast), var(--duration-fast), var(--duration-instant); transition-timing-function: var(--ease-standard), var(--ease-standard), var(--ease-standard); }
22
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
23
+
24
+ /* Add filter: the trigger is transparent and the surface behind it is the button's rounded rectangle.
25
+ Opening springs that same surface to the panel size, so the button itself becomes the menu. */
26
+ .menu { position: relative; display: inline-flex; flex: none; isolation: isolate; }
27
+ .menu:not([data-state="closed"]) { z-index: 30; }
28
+ .trigger { position: relative; z-index: 1; display: inline-flex; min-height: var(--control-height-sm); align-items: center; padding: 0 var(--space-group); border: 1px solid transparent; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); letter-spacing: var(--tracking-body); white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; }
29
+ .menu[data-state="open"] .trigger { pointer-events: none; }
30
+ .face { display: inline-flex; align-items: center; gap: var(--space-related); }
31
+ .face svg { flex: none; }
32
+ /* clip, not hidden: focus and scripts cannot scroll the panel inside the growing shape. */
33
+ .surface { position: absolute; top: 0; z-index: 0; box-sizing: border-box; overflow: clip; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); transition: background-color var(--duration-fast) var(--ease-standard), border-radius var(--duration-spring) var(--ease-spring), box-shadow var(--duration-standard) var(--ease-standard); }
34
+ .menu[data-y="up"] .surface { top: auto; bottom: 0; }
35
+ .menu[data-state="open"] .surface { border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); box-shadow: var(--shadow-floating); }
36
+ /* The trigger anchors the panel, so a press answers in color and never scales. */
37
+ .menu[data-state="closed"] .trigger:active + .surface { background: color-mix(in oklab, var(--surface-muted), var(--border) 55%); }
38
+ @media (hover: hover) and (pointer: fine) {
39
+ .menu[data-state="closed"] .trigger:hover + .surface { background: var(--surface-muted); box-shadow: var(--shadow-resting); }
40
+ }
41
+ .panel { position: absolute; top: 0; left: 0; width: min(var(--filter-menu-width, 16rem), calc(100vw - var(--space-block))); padding: 6px 6px 6px; outline: none; }
42
+ .menu[data-y="up"] .panel { top: auto; bottom: 0; }
43
+ .menu[data-state="closed"] .panel { visibility: hidden; }
44
+ .head { display: flex; height: 32px; align-items: center; padding: 0 var(--space-related) 0 10px; }
45
+ .menu[data-y="up"] .head { margin-top: 4px; }
46
+ .back { display: grid; width: 28px; height: 28px; flex: none; place-items: center; margin-left: -6px; padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-secondary); cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
47
+ .back:active { background: var(--surface-muted); color: var(--foreground); }
48
+ .title { position: relative; display: flex; min-width: 0; color: var(--foreground); font-size: var(--text-sm); font-weight: 500; letter-spacing: var(--tracking-body); white-space: nowrap; }
49
+ .titleText { display: block; }
50
+ .stage { position: relative; margin-top: 2px; }
51
+ .step { width: 100%; }
52
+ .list { position: relative; display: grid; max-height: min(20rem, 55vh); overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
53
+ .highlight { position: absolute; top: 0; right: 0; left: 0; border-radius: calc(var(--radius-container) - 6px); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--foreground) 6%, transparent); opacity: 0; pointer-events: none; }
54
+ .item:focus-visible { outline: none; }
55
+ .item { position: relative; display: flex; min-width: 0; min-height: var(--control-height-sm); align-items: center; gap: 10px; padding: 0 10px; border: 0; border-radius: calc(var(--radius-container) - 6px); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); letter-spacing: var(--tracking-body); text-align: left; cursor: pointer; outline: none; -webkit-tap-highlight-color: transparent; }
56
+ .itemIcon { display: inline-flex; min-width: 16px; flex: none; align-items: center; justify-content: center; color: var(--text-secondary); }
57
+ .itemLabel { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
58
+ .itemMeta { display: inline-flex; min-width: 0; flex: none; align-items: center; gap: 6px; max-width: 50%; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
59
+ .metaValue { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
60
+ .hint { min-width: 2ch; text-align: right; }
61
+ .check { display: inline-flex; width: 15px; color: var(--foreground); opacity: 0; }
62
+ .check[data-on] { opacity: 1; }
63
+ @media (hover: hover) and (pointer: fine) {
64
+ .chip button:hover { background: var(--border); color: var(--foreground); }
65
+ .clear:hover { background: var(--surface-muted); color: var(--foreground); }
66
+ .back:hover { background: var(--surface-muted); color: var(--foreground); }
67
+ }
68
+ /* In a column the frame must size from its height, not a zero flex basis, or its spring is ignored and a new line of chips snaps. */
69
+ @media (max-width: 520px) { .toolbar { align-items: stretch; flex-direction: column; } .frame { flex: none; } .actions { justify-content: flex-end; } }
70
+ @media (prefers-reduced-motion: reduce) { .chip button, .clear, .surface, .back { transition: none; } .chip button:active, .clear:active { transform: none; } }
@@ -0,0 +1,51 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ export type FloatingButtonGroupVariant = "muted" | "floating";
3
+ export type FloatingButtonGroupSize = "sm" | "md";
4
+ export type FloatingButtonGroupOrientation = "horizontal" | "vertical";
5
+ export interface FloatingButtonGroupAction {
6
+ type?: "action";
7
+ /** Stable key, also passed to onAction. */
8
+ id: string;
9
+ /** Visible label, and the accessible name when the item shows only its icon. */
10
+ label: string;
11
+ icon?: ReactNode;
12
+ /** Shows only the icon. The label moves into a tooltip and the accessible name. Overrides the group's iconOnly. */
13
+ iconOnly?: boolean;
14
+ onSelect?: () => void;
15
+ disabled?: boolean;
16
+ /** Marks an action that switches a mode on, such as Present. Sets aria-pressed and keeps a quiet tint while on. */
17
+ pressed?: boolean;
18
+ /** A shortcut hint shown in the tooltip, such as "⌘Z". The component does not bind the key. */
19
+ shortcut?: string;
20
+ /** Other labels this item can switch to, such as ["Copied"] for Share. The button keeps the width of the widest, so a label change never moves anything. */
21
+ reserveLabels?: readonly string[];
22
+ }
23
+ export interface FloatingButtonGroupSeparator {
24
+ type: "separator";
25
+ id?: string;
26
+ }
27
+ export type FloatingButtonGroupItem = FloatingButtonGroupAction | FloatingButtonGroupSeparator;
28
+ export interface FloatingButtonGroupProps {
29
+ items: readonly FloatingButtonGroupItem[];
30
+ /** Accessible name of the toolbar, such as "Board actions". */
31
+ label: string;
32
+ /** muted sits quietly on a page; floating raises the tray with the floating shadow for overlays such as a selection toolbar. */
33
+ variant?: FloatingButtonGroupVariant;
34
+ size?: FloatingButtonGroupSize;
35
+ orientation?: FloatingButtonGroupOrientation;
36
+ /** Shows every item as an icon only, with its label in a tooltip. Items with their own iconOnly win. */
37
+ iconOnly?: boolean;
38
+ /** Side of the item tooltips. Defaults to top for a row and right for a column. */
39
+ tooltipSide?: "top" | "bottom" | "left" | "right";
40
+ /** Called with the item id after the item's own onSelect. */
41
+ onAction?: (id: string) => void;
42
+ className?: string;
43
+ style?: CSSProperties;
44
+ }
45
+ /**
46
+ * Separate soft buttons in a quiet tray. One shared highlight morphs, in position and size, to whichever button the pointer
47
+ * or keyboard focus is on, fades when the pointer leaves the tray, and settles darker in place while a button is pressed.
48
+ * It is the toolbar's only hover and focus indicator. Arrow keys move focus, Home and End jump to the ends, Enter and Space act.
49
+ */
50
+ export declare function FloatingButtonGroup({ items, label, variant, size, orientation, iconOnly: groupIconOnly, tooltipSide, onAction, className, style }: FloatingButtonGroupProps): import("react").JSX.Element;
51
+ export default FloatingButtonGroup;