@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,100 @@
1
+ .toast {
2
+ display: flex;
3
+ width: min(100%, 26rem);
4
+ min-width: 0;
5
+ align-items: flex-start;
6
+ gap: 0.75rem;
7
+ padding: 0.875rem 0.875rem 0.875rem 1rem;
8
+ border: 1px solid var(--border);
9
+ border-radius: var(--radius-container); corner-shape: var(--corner-shape);
10
+ background: color-mix(in oklab, var(--surface) 94%, var(--background));
11
+ color: var(--foreground);
12
+ letter-spacing: var(--tracking-body);
13
+ box-shadow: var(--shadow-floating);
14
+ }
15
+
16
+ .icon {
17
+ display: grid;
18
+ width: 1.875rem;
19
+ height: 1.875rem;
20
+ flex: 0 0 auto;
21
+ place-items: center;
22
+ border: 1px solid color-mix(in oklab, var(--success) 24%, var(--border));
23
+ border-radius: var(--radius-full); corner-shape: round;
24
+ background: color-mix(in oklab, var(--success) 10%, var(--surface));
25
+ color: var(--success);
26
+ }
27
+
28
+ .frame {
29
+ display: block;
30
+ min-width: 0;
31
+ flex: 1;
32
+ }
33
+
34
+ .copy {
35
+ position: relative;
36
+ display: grid;
37
+ min-width: 0;
38
+ gap: 0.125rem;
39
+ /* Centers the title's first line on the 30px status icon. */
40
+ padding-top: 0.3125rem;
41
+ }
42
+
43
+ .title {
44
+ position: relative;
45
+ display: block;
46
+ min-width: 0;
47
+ font-size: var(--text-sm);
48
+ font-weight: 500;
49
+ line-height: var(--leading-body);
50
+ }
51
+
52
+ .line {
53
+ display: block;
54
+ overflow: hidden;
55
+ text-overflow: ellipsis;
56
+ white-space: nowrap;
57
+ }
58
+
59
+ .description {
60
+ display: block;
61
+ color: var(--text-secondary);
62
+ font-size: var(--text-sm);
63
+ line-height: var(--leading-body);
64
+ }
65
+
66
+ .close {
67
+ display: grid;
68
+ width: 2rem;
69
+ height: 2rem;
70
+ flex: 0 0 auto;
71
+ place-items: center;
72
+ margin: -0.0625rem -0.125rem 0 0;
73
+ border: 0;
74
+ border-radius: var(--radius-full); corner-shape: round;
75
+ background: transparent;
76
+ color: var(--text-muted);
77
+ cursor: pointer;
78
+ transition: transform var(--duration-spring) var(--ease-spring),
79
+ background-color var(--duration-fast) var(--ease-standard),
80
+ color var(--duration-fast) var(--ease-standard);
81
+ }
82
+
83
+ @media (hover: hover) and (pointer: fine) {
84
+ .close:hover {
85
+ background: var(--surface-muted);
86
+ color: var(--foreground);
87
+ }
88
+ }
89
+
90
+ /* Quick press, spring release. */
91
+ .close:active {
92
+ transform: scale(0.96);
93
+ transition-duration: var(--duration-instant);
94
+ transition-timing-function: var(--ease-standard);
95
+ }
96
+
97
+ @media (prefers-reduced-motion: reduce) {
98
+ .close { transition: none; }
99
+ .close:active { transform: none; }
100
+ }
@@ -0,0 +1,57 @@
1
+ import type { ReactNode } from "react";
2
+ export type ToastType = "success" | "info" | "warning" | "error" | "loading";
3
+ export interface ToastAction {
4
+ label: string;
5
+ /** Runs the action. The toast closes afterwards unless the handler updates it, so an Undo can morph the toast into its result. */
6
+ onClick: (id: string) => void;
7
+ }
8
+ export interface ToastOptions {
9
+ /** Reuse an id to update a toast in place instead of stacking a new one. */
10
+ id?: string;
11
+ type?: ToastType;
12
+ title: string;
13
+ description?: string;
14
+ action?: ToastAction;
15
+ /** Milliseconds before the toast closes itself. Defaults by type; loading toasts wait for an update. Pass Infinity to keep it until closed. */
16
+ duration?: number;
17
+ }
18
+ export interface ToastStackApi {
19
+ /** Shows a toast and returns its id. */
20
+ toast: (options: ToastOptions) => string;
21
+ /** Morphs a toast in place: icon, copy, and height animate to the new content and its timer restarts. Pass `action: undefined` to remove the action. */
22
+ update: (id: string, patch: Partial<Omit<ToastOptions, "id">>) => void;
23
+ /** Dismisses one toast, or every toast when called without an id. */
24
+ dismiss: (id?: string) => void;
25
+ }
26
+ export interface ToastStackProviderProps {
27
+ children: ReactNode;
28
+ /** Base lifetime in milliseconds. Warnings and errors stay 1.6 times longer. */
29
+ duration?: number;
30
+ /** Oldest toasts beyond this count are dropped from the queue. */
31
+ limit?: number;
32
+ }
33
+ /**
34
+ * The viewport for a toast stack. Render it once inside a `ToastStackProvider` and call `useToastStack()` anywhere below.
35
+ * Use it for short, non-blocking results of what someone just did: new toasts rise from the bottom edge, older ones tuck behind,
36
+ * and hovering or focusing the stack fans it out into a list. Toasts pause while the stack is open or the tab is hidden, and swipe right to dismiss.
37
+ */
38
+ export interface ToastStackProps {
39
+ /** Accessible name of the notification region. */
40
+ label?: string;
41
+ position?: "bottom-right" | "bottom-center" | "bottom-left";
42
+ /** Pins the stack inside the nearest positioned ancestor instead of the window, for panels and previews. */
43
+ contained?: boolean;
44
+ /** How many toasts show at once. Older ones wait behind and return as the front ones close. */
45
+ visibleToasts?: number;
46
+ /** Alt+T moves focus into the stack. */
47
+ hotkey?: boolean;
48
+ className?: string;
49
+ }
50
+ /** Scopes a toast queue to its subtree, so two stacks on one page never share toasts. */
51
+ export declare function ToastStackProvider({ children, duration, limit }: ToastStackProviderProps): import("react").JSX.Element;
52
+ /** Shows, updates, and dismisses toasts from anywhere inside the provider. `count` is how many toasts are queued. */
53
+ export declare function useToastStack(): ToastStackApi & {
54
+ count: number;
55
+ };
56
+ export declare function ToastStack({ label, position, contained, visibleToasts, hotkey, className }: ToastStackProps): import("react").JSX.Element;
57
+ export default ToastStack;
@@ -0,0 +1,399 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, usePresence, useReducedMotion, useTransform } from "motion/react";
5
+ import { CircleCheck, CircleX, Info, TriangleAlert, X } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./toast-stack.module.css";
8
+ /** Each tucked toast shows this many pixels above the one in front of it. */
9
+ const PEEK = 14;
10
+ /** Space between toasts when the stack is open. */
11
+ const GAP = 12;
12
+ /** Tucked toasts shrink by this much per step back. */
13
+ const STEP = .05;
14
+ /** The card's top and bottom borders, added to the measured content height. */
15
+ const BORDER = 2;
16
+ /** A swipe this far right has faded most of the way out. */
17
+ const SWIPE_FADE = 280;
18
+ const EMPTY = [];
19
+ const typeLabels = { success: "Success", info: "Info", warning: "Warning", error: "Error", loading: "In progress" };
20
+ const icons = { success: CircleCheck, info: Info, warning: TriangleAlert, error: CircleX };
21
+ const standard = [...motionTokens.ease.standard];
22
+ const enterEase = [...motionTokens.ease.enter];
23
+ const fade = { duration: motionTokens.duration.fast, ease: standard };
24
+ const enterFade = { duration: motionTokens.duration.standard, ease: enterEase };
25
+ const reducedFade = { duration: motionTokens.duration.fast, ease: standard };
26
+ const exitFast = { duration: motionTokens.duration.fast, ease: standard };
27
+ const textIn = { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` };
28
+ const textOut = { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: exitFast };
29
+ const iconIn = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };
30
+ const shown = { opacity: 1, y: "0em", scale: 1, filter: "blur(0px)" };
31
+ const fadeOnly = { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0, transition: reducedFade }, transition: reducedFade };
32
+ const textSwap = { initial: textIn, animate: shown, exit: textOut, transition: { duration: motionTokens.duration.standard, ease: enterEase } };
33
+ /** Scale rides the spring; opacity and blur tween so the blur never overshoots below zero. */
34
+ const iconSwap = { initial: iconIn, animate: shown, exit: { ...iconIn, transition: exitFast }, transition: { ...motionTokens.spring.snappy, opacity: fade, filter: fade } };
35
+ const durationFor = (type, base) => type === "loading" ? Infinity : type === "warning" || type === "error" ? base * 1.6 : base;
36
+ /** Where a release at this velocity would come to rest, using a fast scroll deceleration. */
37
+ const project = (velocity) => velocity / 1000 * .99 / (1 - .99);
38
+ /** Resistance past an edge: the pull keeps moving, but less and less, the way a scroll view overscrolls. */
39
+ const rubberBand = (distance, dimension) => (1 - 1 / (distance * .55 / dimension + 1)) * dimension;
40
+ const subscribeVisibility = (listener) => { document.addEventListener("visibilitychange", listener); return () => document.removeEventListener("visibilitychange", listener); };
41
+ const getServerSnapshot = () => EMPTY;
42
+ const focusVisible = (element) => { try {
43
+ return element.matches(":focus-visible");
44
+ }
45
+ catch {
46
+ return true;
47
+ } };
48
+ /**
49
+ * Closed, tucked toasts take the front toast's height so their edges line up; open, each takes its own and they stack with a gap.
50
+ * The list is the hover target and is sized to cover the gaps, so moving between open toasts never collapses the stack.
51
+ */
52
+ function layoutStack(toasts, heights, expanded, visibleToasts) {
53
+ const front = toasts.length ? heights[toasts[0].id] ?? 0 : 0;
54
+ const tops = toasts.reduce((sum, item, index) => [...sum, sum[index] + (index < visibleToasts ? (heights[item.id] ?? 0) + GAP : 0)], [0]);
55
+ const targets = toasts.map((item, index) => {
56
+ const visible = index < visibleToasts;
57
+ const depth = Math.min(index, visibleToasts);
58
+ return expanded
59
+ ? { y: -tops[index], scale: 1, height: heights[item.id] ?? 0, opacity: visible ? 1 : 0, content: 1 }
60
+ : { y: -PEEK * depth, scale: 1 - STEP * depth, height: index === 0 ? heights[item.id] ?? 0 : front, opacity: visible ? 1 : 0, content: index === 0 ? 1 : 0 };
61
+ });
62
+ const listHeight = !toasts.length ? 0 : expanded ? tops[toasts.length] - GAP : front + PEEK * (Math.min(toasts.length, visibleToasts) - 1);
63
+ return { targets, listHeight };
64
+ }
65
+ function createToastStore(base, limit) {
66
+ let toasts = [];
67
+ let seq = 0;
68
+ const listeners = new Set();
69
+ const commit = (next) => { toasts = next; listeners.forEach(listener => listener()); };
70
+ const update = (id, patch) => {
71
+ const found = toasts.find(item => item.id === id);
72
+ if (!found)
73
+ return;
74
+ const type = patch.type ?? found.type;
75
+ const record = { ...found, ...patch, type, duration: patch.duration ?? durationFor(type, base), version: found.version + 1 };
76
+ commit(toasts.map(item => item.id === id ? record : item));
77
+ };
78
+ const toast = ({ id, ...options }) => {
79
+ if (id && toasts.some(item => item.id === id)) {
80
+ update(id, options);
81
+ return id;
82
+ }
83
+ const type = options.type ?? "info";
84
+ seq += 1;
85
+ const record = { ...options, id: id ?? `toast-${seq}`, type, duration: options.duration ?? durationFor(type, base), seq, version: 0 };
86
+ commit([record, ...toasts].slice(0, limit));
87
+ return record.id;
88
+ };
89
+ const dismiss = id => {
90
+ if (id === undefined) {
91
+ if (toasts.length)
92
+ commit([]);
93
+ return;
94
+ }
95
+ if (toasts.some(item => item.id === id))
96
+ commit(toasts.filter(item => item.id !== id));
97
+ };
98
+ const runAction = (id) => {
99
+ const found = toasts.find(item => item.id === id);
100
+ if (!found?.action)
101
+ return;
102
+ found.action.onClick(id);
103
+ if (toasts.find(item => item.id === id)?.version === found.version)
104
+ dismiss(id);
105
+ };
106
+ return {
107
+ toast, update, dismiss, runAction,
108
+ subscribe: listener => { listeners.add(listener); return () => { listeners.delete(listener); }; },
109
+ getSnapshot: () => toasts,
110
+ };
111
+ }
112
+ const ToastStackContext = createContext(null);
113
+ function useStore() {
114
+ const store = useContext(ToastStackContext);
115
+ if (!store)
116
+ throw new Error("Render toast stack consumers inside <ToastStackProvider>.");
117
+ return store;
118
+ }
119
+ /** Scopes a toast queue to its subtree, so two stacks on one page never share toasts. */
120
+ export function ToastStackProvider({ children, duration = 5000, limit = 12 }) {
121
+ const [store] = useState(() => createToastStore(duration, limit));
122
+ return _jsx(ToastStackContext.Provider, { value: store, children: children });
123
+ }
124
+ /** Shows, updates, and dismisses toasts from anywhere inside the provider. `count` is how many toasts are queued. */
125
+ export function useToastStack() {
126
+ const store = useStore();
127
+ const count = useSyncExternalStore(store.subscribe, () => store.getSnapshot().length, () => 0);
128
+ return { toast: store.toast, update: store.update, dismiss: store.dismiss, count };
129
+ }
130
+ /** Outgoing copies are hidden from assistive tech while they fade, so the live region reads only the current text. */
131
+ function Swap(props) {
132
+ const present = useIsPresent();
133
+ return _jsx(motion.span, { ...props, "aria-hidden": present ? props["aria-hidden"] : true });
134
+ }
135
+ function ToastItem({ toast, target, expanded, front, hidden, paused, reduce, store, onMeasure, onDragChange, onTap, onHandOff }) {
136
+ const { id } = toast;
137
+ const [isPresent, safeToRemove] = usePresence();
138
+ const itemRef = useRef(null);
139
+ const cardRef = useRef(null);
140
+ const contentRef = useRef(null);
141
+ const actionRef = useRef(null);
142
+ const closeRef = useRef(null);
143
+ // Stack geometry lives in motion values, so following the stack, a finger, or a timer never re-renders React per frame.
144
+ const y = useMotionValue(0);
145
+ const scale = useMotionValue(1);
146
+ const height = useMotionValue(0);
147
+ const opacity = useMotionValue(0);
148
+ const contentOpacity = useMotionValue(1);
149
+ const x = useMotionValue(0);
150
+ const swipeFade = useTransform(x, value => value > 0 ? 1 - Math.min(value / SWIPE_FADE, 1) * .75 : 1);
151
+ /** The expanded state the last target was applied under; null until the toast has entered. */
152
+ const applied = useRef(null);
153
+ const swiped = useRef(false);
154
+ const remaining = useRef(toast.duration);
155
+ const gesture = useRef(null);
156
+ const suppressClick = useRef(false);
157
+ const pointerType = useRef("mouse");
158
+ // Measure before the first paint, so the toast enters from exactly its own height below the edge.
159
+ useLayoutEffect(() => {
160
+ const node = contentRef.current;
161
+ if (!node)
162
+ return;
163
+ onMeasure(id, node.offsetHeight + BORDER);
164
+ if (typeof ResizeObserver === "undefined")
165
+ return;
166
+ const observer = new ResizeObserver(() => onMeasure(id, node.offsetHeight + BORDER));
167
+ observer.observe(node);
168
+ return () => observer.disconnect();
169
+ }, [id, onMeasure]);
170
+ // Every stack change retargets the running springs from wherever they are, so rapid toasts and hover changes never queue.
171
+ useLayoutEffect(() => {
172
+ if (!isPresent || target.height <= 0)
173
+ return;
174
+ const entering = applied.current === null;
175
+ const morph = !entering && applied.current !== expanded;
176
+ applied.current = expanded;
177
+ if (reduce) {
178
+ y.jump(target.y);
179
+ scale.jump(target.scale);
180
+ height.jump(target.height);
181
+ animate(opacity, target.opacity, reducedFade);
182
+ animate(contentOpacity, target.content, reducedFade);
183
+ return;
184
+ }
185
+ if (entering) {
186
+ y.jump(target.y + target.height);
187
+ scale.jump(target.scale);
188
+ height.jump(target.height);
189
+ contentOpacity.jump(target.content);
190
+ opacity.jump(0);
191
+ }
192
+ const spring = morph ? motionTokens.spring.morph : motionTokens.spring.smooth;
193
+ animate(y, target.y, spring);
194
+ animate(scale, target.scale, spring);
195
+ if (!entering)
196
+ animate(height, target.height, spring);
197
+ animate(opacity, target.opacity, entering ? enterFade : fade);
198
+ animate(contentOpacity, target.content, fade);
199
+ }, [contentOpacity, expanded, height, isPresent, opacity, reduce, scale, target.content, target.height, target.opacity, target.scale, target.y, y]);
200
+ // An update restarts the clock; pausing keeps whatever time is left.
201
+ useEffect(() => { remaining.current = toast.duration; }, [toast.version, toast.duration]);
202
+ useEffect(() => {
203
+ if (!isPresent || paused || !Number.isFinite(remaining.current) || toast.duration <= 0)
204
+ return;
205
+ const started = performance.now();
206
+ const timer = window.setTimeout(() => store.dismiss(id), Math.max(remaining.current, 0));
207
+ return () => { window.clearTimeout(timer); remaining.current -= performance.now() - started; };
208
+ }, [id, isPresent, paused, store, toast.version, toast.duration]);
209
+ // Leaving: hand focus on before the toast turns inert, then fade out quickly. A closed toast sinks toward its edge; a thrown one keeps flying.
210
+ useEffect(() => {
211
+ if (isPresent)
212
+ return;
213
+ const node = itemRef.current;
214
+ if (node) {
215
+ const active = document.activeElement;
216
+ if (active instanceof HTMLElement && node.contains(active))
217
+ onHandOff(node, focusVisible(active));
218
+ node.setAttribute("inert", "");
219
+ }
220
+ const done = () => safeToRemove?.();
221
+ if (reduce) {
222
+ animate(opacity, 0, reducedFade).then(done);
223
+ return;
224
+ }
225
+ if (!swiped.current) {
226
+ animate(y, y.get() + 10, motionTokens.spring.smooth);
227
+ animate(scale, scale.get() * .96, motionTokens.spring.smooth);
228
+ }
229
+ animate(opacity, 0, { duration: swiped.current ? .22 : motionTokens.duration.exit, ease: standard }).then(done);
230
+ }, [isPresent, onHandOff, opacity, reduce, safeToRemove, scale, y]);
231
+ function onPointerDown(event) {
232
+ pointerType.current = event.pointerType;
233
+ suppressClick.current = false;
234
+ if (event.button !== 0 || !isPresent || event.target.closest("button, a"))
235
+ return;
236
+ // Catch a toast that is still springing back, from exactly where it is.
237
+ x.stop();
238
+ gesture.current = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, origin: x.get(), active: false, samples: [] };
239
+ }
240
+ function onPointerMove(event) {
241
+ const current = gesture.current;
242
+ if (!current || current.pointerId !== event.pointerId)
243
+ return;
244
+ if (!current.active) {
245
+ const dx = event.clientX - current.startX, dy = event.clientY - current.startY;
246
+ if (Math.hypot(dx, dy) < 6)
247
+ return;
248
+ if (Math.abs(dy) > Math.abs(dx)) {
249
+ gesture.current = null;
250
+ return;
251
+ }
252
+ // Start following from here, so crossing the slop never makes the toast jump.
253
+ current.active = true;
254
+ current.startX = event.clientX;
255
+ suppressClick.current = true;
256
+ event.currentTarget.setPointerCapture(event.pointerId);
257
+ event.currentTarget.dataset.dragging = "";
258
+ window.getSelection()?.removeAllRanges();
259
+ onDragChange(true);
260
+ }
261
+ const raw = current.origin + event.clientX - current.startX;
262
+ // Right follows the finger 1:1; left is the wrong way, so it resists like an overscroll.
263
+ x.set(raw >= 0 ? raw : -rubberBand(-raw, event.currentTarget.offsetWidth));
264
+ current.samples.push({ t: event.timeStamp, x: raw });
265
+ while (current.samples.length > 2 && event.timeStamp - current.samples[0].t > 100)
266
+ current.samples.shift();
267
+ }
268
+ function onPointerEnd(event, cancelled) {
269
+ const current = gesture.current;
270
+ if (!current || current.pointerId !== event.pointerId)
271
+ return;
272
+ gesture.current = null;
273
+ if (!current.active)
274
+ return;
275
+ delete event.currentTarget.dataset.dragging;
276
+ onDragChange(false);
277
+ const first = current.samples[0], last = current.samples[current.samples.length - 1];
278
+ // A finger that stopped before lifting carries no throw.
279
+ const velocity = cancelled || !first || !last || last.t === first.t || event.timeStamp - last.t > 60 ? 0 : (last.x - first.x) / (last.t - first.t) * 1000;
280
+ const width = event.currentTarget.offsetWidth;
281
+ const offset = x.get();
282
+ if (!cancelled && offset > 0 && velocity > -200 && offset + project(velocity) > width * .4) {
283
+ swiped.current = true;
284
+ if (!reduce)
285
+ animate(x, width + 40, { type: "spring", visualDuration: .32, bounce: 0, velocity: Math.max(velocity, 0) });
286
+ store.dismiss(id);
287
+ return;
288
+ }
289
+ if (reduce)
290
+ x.jump(0);
291
+ else
292
+ animate(x, 0, { ...motionTokens.spring.snappy, velocity: offset < 0 ? velocity * .3 : velocity });
293
+ }
294
+ function onCardClick(event) {
295
+ if (suppressClick.current) {
296
+ suppressClick.current = false;
297
+ return;
298
+ }
299
+ // Touch has no hover, so a tap on the card opens or closes the stack instead.
300
+ if (pointerType.current !== "mouse" && !event.target.closest("button, a"))
301
+ onTap();
302
+ }
303
+ // An action that updates its toast usually folds itself away; keyboard focus lands on the close button instead of the page.
304
+ function runAction(event) {
305
+ const keyboard = focusVisible(event.currentTarget);
306
+ store.runAction(id);
307
+ if (keyboard && !store.getSnapshot().find(item => item.id === id)?.action)
308
+ closeRef.current?.focus();
309
+ }
310
+ function onKeyDown(event) {
311
+ if (event.key !== "Escape")
312
+ return;
313
+ event.stopPropagation();
314
+ store.dismiss(id);
315
+ }
316
+ const markMoving = (moving) => { if (actionRef.current)
317
+ actionRef.current.dataset.moving = moving ? "true" : "false"; };
318
+ const Icon = toast.type === "loading" ? null : icons[toast.type];
319
+ const swap = reduce ? fadeOnly : textSwap;
320
+ return _jsx(motion.li, { ref: itemRef, className: styles.item, style: { y, scale, height, opacity, zIndex: toast.seq }, "data-front": front, "data-expanded": expanded, inert: hidden, onKeyDown: onKeyDown, children: _jsx(motion.div, { ref: cardRef, className: styles.card, style: { x, opacity: swipeFade }, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: event => onPointerEnd(event, false), onPointerCancel: event => onPointerEnd(event, true), onClick: onCardClick, children: _jsxs(motion.div, { ref: contentRef, className: styles.content, style: { opacity: contentOpacity }, children: [_jsx("span", { className: styles.icon, "data-type": toast.type, "aria-hidden": "true", children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(Swap, { className: styles.glyph, ...(reduce ? fadeOnly : iconSwap), children: Icon ? _jsx(Icon, { width: 18, height: 18, strokeWidth: 1.75 }) : _jsx("span", { className: styles.spinner }) }, toast.type) }) }), _jsxs("div", { className: styles.copy, children: [_jsxs("span", { className: styles.srOnly, children: [typeLabels[toast.type], ": "] }), _jsx("span", { className: styles.title, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(Swap, { className: styles.line, ...swap, children: toast.title }, toast.title) }) }), _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: toast.description ? _jsx(Swap, { className: styles.description, ...swap, children: toast.description }, toast.description) : null })] }), _jsx(AnimatePresence, { initial: false, children: toast.action ? _jsx(motion.div, { ref: actionRef, className: styles.actionSlot, initial: reduce ? { opacity: 0 } : { width: 0, opacity: 0 }, animate: { width: "auto", opacity: 1 }, exit: reduce ? { opacity: 0 } : { width: 0, opacity: 0 }, transition: reduce ? reducedFade : { width: motionTokens.spring.smooth, opacity: fade }, onAnimationStart: () => markMoving(true), onAnimationComplete: () => markMoving(false), children: _jsx("button", { type: "button", className: styles.action, onClick: runAction, children: toast.action.label }) }, "action") : null }), _jsx("button", { ref: closeRef, type: "button", className: styles.close, "aria-label": "Dismiss notification", onClick: () => store.dismiss(id), children: _jsx(X, { width: 16, height: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }) }) });
321
+ }
322
+ export function ToastStack({ label = "Notifications", position = "bottom-right", contained = false, visibleToasts = 3, hotkey = true, className }) {
323
+ const store = useStore();
324
+ const toasts = useSyncExternalStore(store.subscribe, store.getSnapshot, getServerSnapshot);
325
+ const reduce = !!useReducedMotion();
326
+ const pageHidden = useSyncExternalStore(subscribeVisibility, () => document.visibilityState === "hidden", () => false);
327
+ const [hovered, setHovered] = useState(false);
328
+ const [focused, setFocused] = useState(false);
329
+ const [tapped, setTapped] = useState(false);
330
+ const [dragging, setDragging] = useState(false);
331
+ const [heights, setHeights] = useState({});
332
+ const regionRef = useRef(null);
333
+ const listRef = useRef(null);
334
+ const returnFocus = useRef(null);
335
+ if (tapped && toasts.length === 0)
336
+ setTapped(false);
337
+ const expanded = toasts.length > 0 && (hovered || focused || tapped);
338
+ const paused = expanded || dragging || pageHidden;
339
+ const measure = useCallback((id, height) => setHeights(current => current[id] === height ? current : { ...current, [id]: height }), []);
340
+ const prune = useCallback(() => setHeights(current => Object.fromEntries(store.getSnapshot().filter(item => item.id in current).map(item => [item.id, current[item.id]]))), [store]);
341
+ const toggleTap = useCallback(() => setTapped(open => !open), []);
342
+ /** Keyboard users land on the next toast; with none left they return to where they were before entering the stack. */
343
+ const handOff = useCallback((leaving, keyboard) => {
344
+ const items = Array.from(listRef.current?.children ?? []);
345
+ const at = items.indexOf(leaving);
346
+ const candidates = items.filter(item => item !== leaving && !item.hasAttribute("inert"));
347
+ const next = candidates.find(item => items.indexOf(item) > at) ?? candidates[candidates.length - 1];
348
+ const button = next?.querySelector("button");
349
+ if (keyboard && button)
350
+ button.focus();
351
+ else if (keyboard && returnFocus.current?.isConnected)
352
+ returnFocus.current.focus();
353
+ else
354
+ document.activeElement?.blur();
355
+ }, []);
356
+ useEffect(() => {
357
+ if (!hotkey)
358
+ return;
359
+ const onKey = (event) => {
360
+ if (!event.altKey || event.metaKey || event.ctrlKey || event.code !== "KeyT")
361
+ return;
362
+ const target = listRef.current?.querySelector(":scope > li:not([inert]) button");
363
+ if (!target)
364
+ return;
365
+ event.preventDefault();
366
+ // Browsers skip the focus ring after a modifier shortcut, so the hotkey asks for it and opens the stack itself.
367
+ target.focus({ focusVisible: true });
368
+ setFocused(true);
369
+ };
370
+ document.addEventListener("keydown", onKey);
371
+ return () => document.removeEventListener("keydown", onKey);
372
+ }, [hotkey]);
373
+ useEffect(() => {
374
+ if (!tapped)
375
+ return;
376
+ const onDown = (event) => { if (!regionRef.current?.contains(event.target))
377
+ setTapped(false); };
378
+ document.addEventListener("pointerdown", onDown);
379
+ return () => document.removeEventListener("pointerdown", onDown);
380
+ }, [tapped]);
381
+ const { targets, listHeight } = layoutStack(toasts, heights, expanded, visibleToasts);
382
+ function onFocus(event) {
383
+ const from = event.relatedTarget;
384
+ if (!(from instanceof Node) || !event.currentTarget.contains(from))
385
+ returnFocus.current = from instanceof HTMLElement ? from : null;
386
+ // Only keyboard focus opens the stack; a mouse click on a button should not pin it open.
387
+ if (focusVisible(event.target))
388
+ setFocused(true);
389
+ }
390
+ function onBlur(event) {
391
+ if (!(event.relatedTarget instanceof Node) || !event.currentTarget.contains(event.relatedTarget))
392
+ setFocused(false);
393
+ }
394
+ return _jsx("section", { ref: regionRef, className: [styles.viewport, styles[position], contained && styles.contained, className].filter(Boolean).join(" "), "aria-label": hotkey ? `${label} (Alt+T)` : label, "aria-live": "polite", "aria-relevant": "additions text", "aria-atomic": "false", onFocus: onFocus, onBlur: onBlur, children: _jsx("ol", { ref: listRef, className: styles.list, style: { height: listHeight }, "data-expanded": expanded, onPointerEnter: event => { if (event.pointerType === "mouse")
395
+ setHovered(true); }, onPointerMove: event => { if (event.pointerType === "mouse" && !hovered)
396
+ setHovered(true); }, onPointerLeave: event => { if (event.pointerType === "mouse")
397
+ setHovered(false); }, children: _jsx(AnimatePresence, { onExitComplete: prune, children: toasts.map((item, index) => _jsx(ToastItem, { toast: item, target: targets[index], expanded: expanded, front: index === 0, hidden: index >= visibleToasts, paused: paused, reduce: reduce, store: store, onMeasure: measure, onDragChange: setDragging, onTap: toggleTap, onHandOff: handOff }, item.id)) }) }) });
398
+ }
399
+ export default ToastStack;
@@ -0,0 +1,61 @@
1
+ /* The region only holds the list; it never blocks clicks outside the toasts themselves. */
2
+ .viewport { position: fixed; z-index: 60; bottom: var(--space-section); width: min(22.5rem, calc(100% - var(--space-block))); pointer-events: none; }
3
+ .bottom-right { right: var(--space-section); }
4
+ .bottom-left { left: var(--space-section); }
5
+ .bottom-center { right: 0; left: 0; margin-inline: auto; }
6
+ .contained { position: absolute; z-index: 2; bottom: var(--space-inset); }
7
+ .contained.bottom-right { right: var(--space-inset); }
8
+ .contained.bottom-left { left: var(--space-inset); }
9
+ @media (max-width: 30rem) { .viewport:not(.contained) { right: 0; bottom: var(--space-inset); left: 0; margin-inline: auto; } }
10
+
11
+ /* Toasts hang from the bottom of the list. Its height covers the gaps between open toasts, so hover survives the space between them. */
12
+ .list { position: relative; margin: 0; padding: 0; list-style: none; pointer-events: auto; }
13
+ /* Tucked toasts scale from their top edge, so exactly the peek shows above the toast in front. */
14
+ .item { position: absolute; right: 0; bottom: 0; left: 0; transform-origin: 50% 0; }
15
+ .card { position: absolute; inset: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-raised); container-type: inline-size; touch-action: pan-y; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-standard) var(--ease-standard), border-color var(--duration-standard) var(--ease-standard); }
16
+ .item[data-front="true"] .card, .item[data-expanded="true"] .card { box-shadow: var(--shadow-floating); }
17
+ /* Tucked toasts sit a step back in tone as well as scale. */
18
+ .item[data-front="false"][data-expanded="false"] .card { background: var(--surface-muted); }
19
+ .card[data-dragging] { cursor: grabbing; user-select: none; }
20
+
21
+ .content { display: flex; align-items: flex-start; gap: var(--space-group); padding: var(--space-group) var(--space-group) var(--space-group) var(--space-inset); }
22
+ .icon { position: relative; display: grid; width: 18px; height: 20px; flex: none; place-items: center; color: var(--text-muted); transition: color var(--duration-fast) var(--ease-standard); }
23
+ .icon[data-type="success"] { color: var(--success); }
24
+ .icon[data-type="info"] { color: var(--accent); }
25
+ .icon[data-type="warning"] { color: var(--warning); }
26
+ .icon[data-type="error"] { color: var(--danger); }
27
+ .glyph { grid-area: 1 / 1; display: grid; place-items: center; }
28
+ .glyph svg { display: block; }
29
+ /* The spinner turns only while something is loading; the icon swap is its stop. */
30
+ .spinner { display: block; width: 15px; height: 15px; border: 1.75px solid currentColor; border-right-color: transparent; border-radius: var(--radius-full); corner-shape: round; animation: spin .8s linear infinite; }
31
+
32
+ .copy { position: relative; min-width: 0; flex: 1; font-size: var(--text-sm); letter-spacing: var(--tracking-body); line-height: 20px; }
33
+ .title { position: relative; display: block; font-weight: 500; }
34
+ .line { display: block; }
35
+ .description { display: block; margin-top: 2px; color: var(--text-secondary); }
36
+
37
+ /* Clips only while its width folds, so the focus ring stays whole at rest. */
38
+ .actionSlot { display: flex; flex: none; justify-content: flex-end; margin-top: -4px; }
39
+ .actionSlot[data-moving="true"] { overflow: hidden; }
40
+ .action, .close { flex: none; border: 1px solid transparent; font: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: scale var(--duration-spring) var(--ease-spring), background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
41
+ .action { height: 28px; padding: 0 var(--space-group); border-color: var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; white-space: nowrap; }
42
+ .close { display: grid; width: 28px; height: 28px; margin: -4px -2px 0 0; place-items: center; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-muted); }
43
+ @media (hover: hover) and (pointer: fine) {
44
+ .action:hover { border-color: var(--border-strong); background: var(--surface-muted); }
45
+ .close:hover { background: var(--surface-muted); color: var(--foreground); }
46
+ }
47
+ /* Quick press, spring release. */
48
+ .action:active, .close:active { scale: .96; transition-duration: var(--duration-instant); transition-timing-function: var(--ease-standard); }
49
+ /* A narrow toast keeps its copy wide: the action drops under the copy instead of squeezing it. */
50
+ @container (max-width: 21rem) {
51
+ .content { flex-wrap: wrap; row-gap: var(--space-related); }
52
+ .actionSlot { order: 1; flex-basis: 100%; justify-content: flex-start; margin-top: 0; padding-left: calc(18px + var(--space-group)); }
53
+ }
54
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
55
+
56
+ @keyframes spin { to { rotate: 360deg; } }
57
+ @media (prefers-reduced-motion: reduce) {
58
+ .card, .icon, .action, .close { transition: none; }
59
+ .action:active, .close:active { scale: none; }
60
+ .spinner { animation: none; }
61
+ }
@@ -0,0 +1,8 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ export interface TooltipProps {
3
+ content: ReactNode;
4
+ children: ReactElement;
5
+ side?: "top" | "bottom" | "left" | "right";
6
+ }
7
+ export declare function Tooltip({ content, children, side }: TooltipProps): import("react").JSX.Element;
8
+ export default Tooltip;