@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,37 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
5
+ import { Moon, Sun } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import { Button } from "../button/button.js";
8
+ import styles from "./theme-switch.module.css";
9
+ /** Rotation and scale ride the spring; opacity and blur tween so the blur never overshoots below zero. */
10
+ const iconSpring = { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] }, filter: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] } };
11
+ const iconExit = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };
12
+ const blur = `blur(${motionTokens.blur.subtle}px)`;
13
+ /** A stored preference usually reaches the theme prop just after hydration. That correction swaps the icon in place;
14
+ * only changes after the first painted frames animate, so a page that loads in dark mode never spins its switches. */
15
+ function useSettled() {
16
+ const [settled, setSettled] = useState(false);
17
+ useEffect(() => {
18
+ let second = 0;
19
+ const first = requestAnimationFrame(() => { second = requestAnimationFrame(() => setSettled(true)); });
20
+ return () => { cancelAnimationFrame(first); cancelAnimationFrame(second); };
21
+ }, []);
22
+ return settled;
23
+ }
24
+ /** Both icons turn the same way (clockwise into dark, back out of it), so the swap reads as one rotation rather than two fades. */
25
+ function ThemeIcon({ theme, reduced, settled }) {
26
+ const Icon = theme === "light" ? Sun : Moon;
27
+ const angle = theme === "light" ? 30 : -30;
28
+ return (_jsx(AnimatePresence, { initial: false, mode: "popLayout", children: _jsx(motion.span, { className: styles.icon, initial: !settled ? false : reduced ? { opacity: 0 } : { opacity: 0, scale: .7, rotate: angle, filter: blur }, animate: { opacity: 1, scale: 1, rotate: 0, filter: "blur(0px)" }, exit: !settled ? { opacity: 0, transition: { duration: 0 } } : reduced ? { opacity: 0, transition: { duration: motionTokens.duration.instant } } : { opacity: 0, scale: .7, rotate: angle, filter: blur, transition: iconExit }, transition: reduced ? { duration: motionTokens.duration.instant } : iconSpring, "aria-hidden": "true", children: _jsx(Icon, { size: 16, strokeWidth: 1.75 }) }, theme) }));
29
+ }
30
+ export function ThemeSwitch({ theme, variant = "reveal", onThemeChange, label, iconOnly = false }) {
31
+ const reduced = useReducedMotion() ?? false;
32
+ const settled = useSettled();
33
+ const next = theme === "light" ? "dark" : "light";
34
+ const classes = [styles.themeSwitch, styles[variant], iconOnly ? styles.iconOnly : "", settled ? styles.settled : ""].join(" ");
35
+ return (_jsxs(Button, { type: "button", size: "sm", variant: "secondary", className: classes, "data-theme": theme, "aria-label": label ?? `Switch to ${next} mode`, "aria-pressed": theme === "dark", onClick: event => onThemeChange(next, variant, event.currentTarget), children: [_jsx("span", { className: styles.iconWrap, children: _jsx(ThemeIcon, { theme: theme, reduced: reduced, settled: settled }) }), !iconOnly && _jsx("span", { className: styles.label, children: "Switch theme" })] }));
36
+ }
37
+ export default ThemeSwitch;
@@ -0,0 +1,31 @@
1
+ /* Each transition uses the same library Button, which owns the press scale; this file only animates the icon in place. */
2
+ .themeSwitch {
3
+ position: relative;
4
+ width: auto;
5
+ padding-inline: var(--space-group);
6
+ }
7
+ .iconOnly { width: 38px; min-width: 38px; height: 38px; min-height: 38px; border-radius: var(--radius-control); padding: 0; }
8
+ @media (hover: hover) and (pointer: fine) {
9
+ .themeSwitch:hover:not(:disabled) { border-color: var(--border-strong); }
10
+ .iconOnly:hover:not(:disabled) { background: var(--surface-muted); box-shadow: none; }
11
+ }
12
+ .iconWrap {
13
+ position: relative;
14
+ display: inline-grid;
15
+ width: 20px;
16
+ height: 20px;
17
+ flex: 0 0 20px;
18
+ place-items: center;
19
+ }
20
+ /* The nudge only eases once the switch has settled, so a theme that arrives during hydration lands without motion. */
21
+ .settled .iconWrap { transition: transform var(--duration-spring) var(--ease-spring); }
22
+ .icon { position: absolute; display: grid; width: 18px; height: 18px; place-items: center; }
23
+ .label { white-space: nowrap; }
24
+ /* Each variant nudges the icon the way its page transition travels, and eases back when the theme returns. */
25
+ .themeSwitch.reveal[data-theme="dark"] .iconWrap { transform: scale(1.08); }
26
+ .themeSwitch.eclipse[data-theme="dark"] .iconWrap { transform: translateX(-2px); }
27
+ .themeSwitch.split[data-theme="dark"] .iconWrap { transform: translateX(1px); }
28
+ .themeSwitch.rise[data-theme="dark"] .iconWrap { transform: translateY(-2px); }
29
+ @media (prefers-reduced-motion: reduce) {
30
+ .settled .iconWrap { transition: none; }
31
+ }
@@ -0,0 +1,13 @@
1
+ export interface TimePickerProps {
2
+ label: string;
3
+ value?: string;
4
+ defaultValue?: string;
5
+ onChange?: (value: string) => void;
6
+ description?: string;
7
+ placeholder?: string;
8
+ minuteStep?: 1 | 5 | 10 | 15 | 30;
9
+ format?: "12h" | "24h";
10
+ disabled?: boolean;
11
+ className?: string;
12
+ }
13
+ export declare function TimePicker({ label, value, defaultValue, onChange, description, placeholder, minuteStep, format, disabled, className }: TimePickerProps): import("react").JSX.Element;
@@ -0,0 +1,95 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
4
+ import { ChevronDown, Clock3 } from "lucide-react";
5
+ import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./time-picker.module.css";
8
+ const pad = (value) => String(value).padStart(2, "0");
9
+ const toMinutes = (value) => { const [h = NaN, m = NaN] = value.split(":").map(Number); return Number.isFinite(h) && Number.isFinite(m) ? h * 60 + m : 0; };
10
+ const enter = motionTokens.ease.enter;
11
+ const standard = motionTokens.ease.standard;
12
+ /** The shown time rolls like a clock face: a later time rises from below, an earlier one drops from above. */
13
+ const valueRoll = {
14
+ enter: (direction) => ({ opacity: 0, y: `${direction * 0.35}em`, filter: `blur(${motionTokens.blur.soft}px)` }),
15
+ center: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.standard, ease: enter } },
16
+ exit: (direction) => ({ opacity: 0, y: `${direction * -0.3}em`, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: standard } }),
17
+ };
18
+ /** Reduced motion keeps a short crossfade; the resting state matches valueRoll so server and client markup agree. */
19
+ const valueFade = { enter: { opacity: 0 }, center: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.instant } }, exit: { opacity: 0, transition: { duration: motionTokens.duration.instant } } };
20
+ export function TimePicker({ label, value, defaultValue = "09:00", onChange, description, placeholder = "Select a time", minuteStep = 15, format = "12h", disabled = false, className }) {
21
+ const id = useId();
22
+ const labelId = `${id}-label`;
23
+ const valueId = `${id}-value`;
24
+ const rootRef = useRef(null);
25
+ const optionRefs = useRef([]);
26
+ const centerOnOpen = useRef(false);
27
+ const [internal, setInternal] = useState(defaultValue);
28
+ const [open, setOpen] = useState(false);
29
+ const [activeIndex, setActiveIndex] = useState(-1);
30
+ const selected = value ?? internal;
31
+ const reduce = useReducedMotion();
32
+ const [previous, setPrevious] = useState(selected);
33
+ const [direction, setDirection] = useState(1);
34
+ if (previous !== selected) {
35
+ setPrevious(selected);
36
+ setDirection(toMinutes(selected) >= toMinutes(previous) ? 1 : -1);
37
+ }
38
+ const options = Array.from({ length: Math.ceil(1440 / minuteStep) }, (_, index) => { const minutes = index * minuteStep; const hour = Math.floor(minutes / 60); const minute = minutes % 60; return `${pad(hour)}:${pad(minute)}`; });
39
+ const display = (raw) => { const minutes = toMinutes(raw); const hour = Math.floor(minutes / 60); const minute = minutes % 60; return format === "24h" ? `${pad(hour)}:${pad(minute)}` : `${hour % 12 || 12}:${pad(minute)} ${hour < 12 ? "AM" : "PM"}`; };
40
+ useEffect(() => { const close = (event) => { if (!rootRef.current?.contains(event.target))
41
+ setOpen(false); }; document.addEventListener("pointerdown", close); return () => document.removeEventListener("pointerdown", close); }, []);
42
+ // Runs before paint so the menu's first frame is already centered on the selected time.
43
+ useLayoutEffect(() => {
44
+ if (!open || activeIndex < 0)
45
+ return;
46
+ // Scroll only the menu, never the page: the selected time opens centered, arrow keys then keep the highlight in view.
47
+ const option = optionRefs.current[activeIndex];
48
+ const menu = option?.parentElement;
49
+ if (!option || !menu)
50
+ return;
51
+ const top = option.offsetTop;
52
+ const bottom = top + option.offsetHeight;
53
+ if (centerOnOpen.current) {
54
+ centerOnOpen.current = false;
55
+ menu.scrollTop = top - (menu.clientHeight - option.offsetHeight) / 2;
56
+ }
57
+ else if (top < menu.scrollTop)
58
+ menu.scrollTop = top;
59
+ else if (bottom > menu.scrollTop + menu.clientHeight)
60
+ menu.scrollTop = bottom - menu.clientHeight;
61
+ }, [activeIndex, open]);
62
+ const choose = (next) => { if (value === undefined)
63
+ setInternal(next); onChange?.(next); setOpen(false); };
64
+ const openMenu = () => {
65
+ const selectedIndex = options.indexOf(selected);
66
+ setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
67
+ centerOnOpen.current = true;
68
+ setOpen(true);
69
+ };
70
+ const onKeyDown = (event) => {
71
+ if (event.key === "Escape") {
72
+ event.preventDefault();
73
+ setOpen(false);
74
+ return;
75
+ }
76
+ if (event.key === "Enter" || event.key === " ") {
77
+ event.preventDefault();
78
+ if (open && activeIndex >= 0)
79
+ choose(options[activeIndex]);
80
+ else
81
+ openMenu();
82
+ return;
83
+ }
84
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
85
+ event.preventDefault();
86
+ if (!open) {
87
+ openMenu();
88
+ return;
89
+ }
90
+ setActiveIndex(index => (index + (event.key === "ArrowDown" ? 1 : -1) + options.length) % options.length);
91
+ }
92
+ };
93
+ return _jsxs("div", { ref: rootRef, className: [styles.field, className].filter(Boolean).join(" "), children: [_jsx("span", { id: labelId, className: styles.label, children: label }), _jsxs("div", { className: styles.anchor, children: [_jsxs("button", { type: "button", className: styles.trigger, disabled: disabled, "aria-labelledby": `${labelId} ${valueId}`, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": `${id}-listbox`, onClick: () => open ? setOpen(false) : openMenu(), onKeyDown: onKeyDown, children: [_jsx(Clock3, { size: 16, "aria-hidden": "true" }), _jsx("span", { id: valueId, className: styles.srOnly, children: selected ? display(selected) : placeholder }), _jsx("span", { className: styles.valueText, "aria-hidden": "true", children: _jsx(AnimatePresence, { initial: false, custom: direction, children: _jsx(motion.span, { className: selected ? styles.value : styles.placeholder, custom: direction, variants: reduce ? valueFade : valueRoll, initial: "enter", animate: "center", exit: "exit", children: selected ? display(selected) : placeholder }, selected || "placeholder") }) }), _jsx(ChevronDown, { className: styles.chevron, size: 16, "aria-hidden": "true" })] }), _jsx(AnimatePresence, { initial: false, children: open && _jsx(motion.div, { id: `${id}-listbox`, className: styles.menu, role: "listbox", "aria-label": `${label} options`, initial: reduce ? { opacity: 0 } : { opacity: 0, y: -6, scale: .97 }, animate: { opacity: 1, y: 0, scale: 1, transition: reduce ? { duration: motionTokens.duration.instant } : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: enter } } }, exit: { opacity: 0, ...(reduce ? {} : { y: -4, scale: .98 }), transition: { duration: motionTokens.duration.instant, ease: standard } }, children: options.map((option, index) => _jsxs("button", { ref: node => { optionRefs.current[index] = node; }, id: `${id}-option-${index}`, type: "button", role: "option", "aria-selected": option === selected, "data-active": activeIndex === index || undefined, className: styles.option, onPointerMove: () => { if (activeIndex !== index)
94
+ setActiveIndex(index); }, onClick: () => choose(option), children: [display(option), option === selected && _jsx("span", { className: styles.dot, "aria-hidden": "true" })] }, option)) }) })] }), description && _jsx("span", { className: styles.description, children: description })] });
95
+ }
@@ -0,0 +1,23 @@
1
+ .field { display: grid; gap: var(--space-related); min-width: 0; }
2
+ .label { font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
3
+ .anchor { position: relative; min-width: 0; }
4
+ /* The trigger anchors the menu, so press feedback stays in color; it never scales. */
5
+ .trigger { position: relative; display: flex; width: 100%; min-height: var(--control-height-md); align-items: center; gap: 9px; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: 0 12px; background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
6
+ @media (hover: hover) and (pointer: fine) { .trigger:hover:not(:disabled) { border-color: var(--border-strong); background: var(--surface-muted); } }
7
+ .trigger:active:not(:disabled), .trigger[aria-expanded="true"] { border-color: var(--border-strong); background: var(--surface-muted); }
8
+ .trigger:disabled { opacity: .5; cursor: not-allowed; }
9
+ .trigger > svg:first-child { flex: 0 0 auto; color: var(--text-muted); }
10
+ .valueText { display: grid; flex: 1; min-width: 0; grid-template-columns: minmax(0, 1fr); }
11
+ .valueText > span { grid-area: 1 / 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
12
+ .value { font-variant-numeric: tabular-nums; }
13
+ .placeholder, .description { color: var(--text-muted); }
14
+ .srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
15
+ .chevron { flex: 0 0 auto; color: var(--text-muted); transition: transform var(--duration-spring) var(--ease-spring); }
16
+ .trigger[aria-expanded="true"] .chevron { transform: rotate(180deg); }
17
+ .description { font-size: var(--text-xs); }
18
+ .menu { position: absolute; z-index: 80; top: calc(100% + 6px); right: 0; left: 0; max-height: 250px; overflow-y: auto; overscroll-behavior: contain; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); padding: 5px; background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: top center; will-change: transform, opacity; }
19
+ /* Arrow keys move the highlight often, so it changes almost instantly. The dot marks the chosen time, so only one row is ever highlighted. */
20
+ .option { display: flex; width: 100%; min-height: 36px; align-items: center; justify-content: space-between; border: 0; border-radius: calc(var(--radius-container) - 5px); corner-shape: var(--corner-shape); padding: 0 12px; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); font-variant-numeric: tabular-nums; text-align: left; cursor: pointer; transition: background-color 80ms var(--ease-standard); }
21
+ .option[data-active="true"] { background: var(--surface-muted); }
22
+ .dot { width: 6px; height: 6px; border-radius: var(--radius-full); corner-shape: round; background: var(--foreground); }
23
+ @media (prefers-reduced-motion: reduce) { .trigger, .chevron, .option { transition: none; } }
@@ -0,0 +1,47 @@
1
+ import type { ReactNode } from "react";
2
+ export interface TimelineEvent {
3
+ id: string;
4
+ /** When it happened, as an ISO string or epoch milliseconds. */
5
+ at: string | number;
6
+ /** Who did it, shown first in the foreground color. */
7
+ actor?: string;
8
+ /** What happened, completing the actor: "merged Checkout redesign into main". */
9
+ title: string;
10
+ /** Short context under the title, such as a pull request or a build. */
11
+ meta?: string;
12
+ /** Revealed in place when the row is expanded. Rows without detail are not interactive. */
13
+ detail?: ReactNode;
14
+ /** Portrait for an event by a person. */
15
+ avatar?: string;
16
+ /** Icon for a system event, used when there is no avatar. */
17
+ icon?: ReactNode;
18
+ /** Status of a system event. Always say the outcome in the title too, so it never rests on color. */
19
+ tone?: "neutral" | "success" | "danger";
20
+ }
21
+ /**
22
+ * A vertical activity feed grouped by day, for project history, audit logs, and deploy streams. Use it when order and
23
+ * recency matter; use a table when people need to sort or compare. Day labels stay pinned while their updates scroll,
24
+ * the connecting line draws itself as rows come into view, rows expand in place, and new updates slide in at the top
25
+ * while the rest glide down. Arrow keys move between rows, Enter or Space expands one.
26
+ */
27
+ export interface TimelineProps {
28
+ /** Updates in any order; the newest shows first. */
29
+ events: TimelineEvent[];
30
+ /** Reference time for relative labels and day groups, in epoch milliseconds. Pass a ticking clock to keep labels fresh. */
31
+ now: number;
32
+ /** Accessible name for the feed. */
33
+ label: string;
34
+ /** Time zone for day groups and clock times. Fixed by default so server and client agree. */
35
+ timeZone?: string;
36
+ locale?: string;
37
+ /** Height of the scrolling area. Without it the feed grows with the page and reveals on page scroll. */
38
+ maxHeight?: number | string;
39
+ /** Scroll back to the top when a new update arrives while the feed is scrolled down. */
40
+ scrollToNew?: boolean;
41
+ defaultExpanded?: string[];
42
+ /** Heading level for the day labels. */
43
+ headingLevel?: 2 | 3 | 4 | 5 | 6;
44
+ className?: string;
45
+ }
46
+ export declare function Timeline({ events, now, label, timeZone, locale, maxHeight, scrollToNew, defaultExpanded, headingLevel, className }: TimelineProps): import("react").JSX.Element;
47
+ export default Timeline;
@@ -0,0 +1,171 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
5
+ import { ChevronDown } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./timeline.module.css";
8
+ const HOUR = 3600000;
9
+ const enter = [...motionTokens.ease.enter];
10
+ const standard = [...motionTokens.ease.standard];
11
+ /** Seconds between rows revealed together, so the line reads as drawing downward. */
12
+ const STEP = .09;
13
+ const noopSubscribe = () => () => { };
14
+ /** Reduced motion only after hydration, so the server and the first client render agree. */
15
+ function useReducedMotionSafe() {
16
+ const hydrated = useSyncExternalStore(noopSubscribe, () => true, () => false);
17
+ const reduced = useReducedMotion();
18
+ return hydrated && !!reduced;
19
+ }
20
+ const RevealClock = createContext(null);
21
+ /** Text that changes in place: the new value rises in from a soft blur while the old one lifts away a little faster. */
22
+ function RiseText({ text, reduced, direction = 1 }) {
23
+ return _jsx("span", { className: styles.rise, "aria-hidden": "true", children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, custom: direction, children: _jsx(motion.span, { className: styles.riseLine, custom: direction, variants: {
24
+ from: (dir) => reduced ? { opacity: 0 } : { opacity: 0, y: `${.3 * dir}em`, filter: `blur(${motionTokens.blur.soft}px)` },
25
+ to: { opacity: 1, y: "0em", filter: "blur(0px)" },
26
+ gone: (dir) => reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, y: `${-.3 * dir}em`, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: standard } },
27
+ }, initial: "from", animate: "to", exit: "gone", transition: { duration: reduced ? motionTokens.duration.instant : motionTokens.duration.standard, ease: enter }, children: text }, text) }) });
28
+ }
29
+ /** The day's update count rolls digit by digit in the direction it moved. */
30
+ function RollingCount({ value, reduced }) {
31
+ const [state, setState] = useState({ value, direction: 1 });
32
+ if (state.value !== value)
33
+ setState({ value, direction: value > state.value ? 1 : -1 });
34
+ const chars = [...String(value)];
35
+ return _jsx("span", { className: styles.rolling, "aria-hidden": "true", children: chars.map((char, index) => _jsx("span", { className: styles.place, children: _jsx(RiseText, { text: char, reduced: reduced, direction: state.direction }) }, chars.length - index)) });
36
+ }
37
+ function dayKey(time, timeZone) {
38
+ const parts = new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(new Date(time));
39
+ const get = (type) => parts.find(part => part.type === type)?.value ?? "";
40
+ return `${get("year")}-${get("month")}-${get("day")}`;
41
+ }
42
+ function relative(time, now) {
43
+ const minutes = Math.max(0, Math.floor((now - time) / 60000));
44
+ if (minutes < 1)
45
+ return { short: "Now", long: "just now" };
46
+ if (minutes < 60)
47
+ return { short: `${minutes}m`, long: `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago` };
48
+ const hours = Math.floor(minutes / 60);
49
+ return { short: `${hours}h`, long: `${hours} ${hours === 1 ? "hour" : "hours"} ago` };
50
+ }
51
+ /** A day's section. A new day opens from nothing and pushes the older days down; it clips only while it grows. */
52
+ function DaySection({ id, fresh, reduced, children }) {
53
+ const [entering, setEntering] = useState(fresh);
54
+ return _jsx(motion.section, { className: styles.group, "aria-labelledby": id, "data-entering": entering || undefined, initial: fresh ? { height: 0, opacity: 0 } : false, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0, transition: { duration: reduced ? motionTokens.duration.instant : motionTokens.duration.exit, ease: standard } }, transition: reduced ? { duration: .15 } : { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.standard, ease: enter } }, onAnimationComplete: () => setEntering(false), children: children });
55
+ }
56
+ function TimelineRow({ row, last, expanded, onToggle, timeLabel, timeFull }) {
57
+ const clock = useContext(RevealClock);
58
+ const { reduced } = clock;
59
+ const detailId = useId();
60
+ // The row reveals once, the first time it scrolls into view: its marker pops, then its line draws toward the next row.
61
+ const [revealDelay, setRevealDelay] = useState(null);
62
+ const fresh = clock.fresh.has(row.id);
63
+ const [entering, setEntering] = useState(fresh);
64
+ const shown = revealDelay !== null;
65
+ const delay = (revealDelay ?? 0) + (fresh ? .12 : 0);
66
+ const pop = reduced ? { duration: 0, opacity: { duration: .15 } } : { ...motionTokens.spring.morph, visualDuration: .36, bounce: .32, delay, opacity: { duration: motionTokens.duration.fast, ease: enter, delay } };
67
+ const draw = reduced ? { duration: 0 } : { ...motionTokens.spring.smooth, visualDuration: .34, delay: delay + .1 };
68
+ const tone = row.avatar ? undefined : row.tone ?? "neutral";
69
+ const Trigger = row.detail ? "button" : "div";
70
+ return _jsxs(motion.li, { className: styles.item, "data-entering": entering || undefined, initial: fresh ? { height: 0 } : false, animate: { height: "auto" }, exit: { height: 0, opacity: 0, transition: reduced ? { duration: .1 } : { height: { ...motionTokens.spring.smooth, visualDuration: .3 }, opacity: { duration: .12 } } }, transition: reduced ? { duration: 0 } : motionTokens.spring.smooth, onAnimationComplete: () => setEntering(false), onViewportEnter: () => setRevealDelay(current => current ?? clock.schedule()), viewport: { once: true, amount: .2 }, children: [_jsx(motion.span, { className: styles.marker, "data-tone": tone, "aria-hidden": "true", initial: { scale: .4, opacity: 0 }, animate: shown ? { scale: 1, opacity: 1 } : undefined, transition: pop, children: row.avatar ? _jsx("img", { src: row.avatar, alt: "", width: 28, height: 28, decoding: "async" }) : row.icon }), !last && _jsx(motion.span, { className: styles.segment, "aria-hidden": "true", initial: { scaleY: 0 }, animate: shown ? { scaleY: 1 } : undefined, transition: draw }), _jsxs(motion.div, { className: styles.content, initial: fresh ? (reduced ? { opacity: 0 } : { opacity: 0, y: -10, filter: `blur(${motionTokens.blur.soft}px)` }) : false, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, transition: reduced ? { duration: .15 } : { y: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.standard, ease: enter, delay: .05 }, filter: { duration: motionTokens.duration.standard, ease: enter, delay: .05 } }, children: [_jsxs(Trigger, { className: styles.trigger, ...(row.detail ? { type: "button", "data-timeline-trigger": "", "aria-expanded": expanded, "aria-controls": expanded ? detailId : undefined, onClick: onToggle } : {}), children: [_jsxs("span", { className: styles.text, children: [_jsxs("span", { className: styles.title, children: [row.actor && _jsx("span", { className: styles.actor, children: row.actor }), row.actor ? " " : "", row.title] }), row.meta && _jsx("span", { className: styles.meta, children: row.meta })] }), _jsxs("time", { className: styles.time, dateTime: new Date(row.time).toISOString(), title: timeFull, children: [_jsx(RiseText, { text: timeLabel, reduced: reduced }), _jsx("span", { className: styles.srOnly, children: timeFull })] }), row.detail && _jsx(motion.span, { className: styles.chevron, "aria-hidden": "true", initial: false, animate: { rotate: expanded ? 180 : 0 }, transition: reduced ? { duration: 0 } : motionTokens.spring.snappy, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) })] }), _jsx(AnimatePresence, { initial: false, children: expanded && row.detail && _jsx(motion.div, { id: detailId, className: styles.detail, initial: { height: 0 }, animate: { height: "auto" }, exit: { height: 0, transition: reduced ? { duration: 0 } : { ...motionTokens.spring.smooth, visualDuration: .28 } }, transition: reduced ? { duration: 0 } : motionTokens.spring.smooth, children: _jsx(motion.div, { className: styles.detailInner, initial: reduced ? { opacity: 0 } : { opacity: 0, y: -4, filter: `blur(${motionTokens.blur.soft}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: { opacity: 0, transition: { duration: motionTokens.duration.instant, ease: standard } }, transition: reduced ? { duration: .15 } : { duration: motionTokens.duration.standard, ease: enter, delay: .06 }, children: row.detail }) }, "detail") })] })] });
71
+ }
72
+ /** An event's time in epoch milliseconds (an unreadable time sorts as the oldest). */
73
+ function timeOf(at) {
74
+ const value = typeof at === "number" ? at : Date.parse(at);
75
+ return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
76
+ }
77
+ export function Timeline({ events, now, label, timeZone = "UTC", locale = "en-US", maxHeight, scrollToNew = true, defaultExpanded = [], headingLevel = 3, className }) {
78
+ const reduced = useReducedMotionSafe();
79
+ const id = useId();
80
+ const scrollerRef = useRef(null);
81
+ const [expanded, setExpanded] = useState(() => new Set(defaultExpanded));
82
+ // Rows newer than every row already shown are fresh: they slide in, the rest glide down, and their titles are
83
+ // announced. Older rows added below (a "Show older" page) join quietly: they are history being read, not news.
84
+ const ids = events.map(event => event.id).join("|");
85
+ const newestOf = (list) => list.reduce((newest, event) => Math.max(newest, timeOf(event.at)), Number.NEGATIVE_INFINITY);
86
+ const [known, setKnown] = useState(() => ({ ids, set: new Set(events.map(event => event.id)), newest: newestOf(events), fresh: new Set(), announcement: "" }));
87
+ if (known.ids !== ids) {
88
+ const added = events.filter(event => !known.set.has(event.id) && timeOf(event.at) > known.newest);
89
+ setKnown({
90
+ ids,
91
+ set: new Set(events.map(event => event.id)),
92
+ newest: Math.max(known.newest, newestOf(events)),
93
+ fresh: new Set([...known.fresh, ...added.map(event => event.id)]),
94
+ announcement: added.length ? `New update: ${added.map(event => [event.actor, event.title].filter(Boolean).join(" ")).join(". ")}` : known.announcement,
95
+ });
96
+ }
97
+ // The bottom edge feathers while more updates wait below, and clears once the end is in view.
98
+ const trackRef = useRef(null);
99
+ const scrolls = maxHeight !== undefined;
100
+ useEffect(() => {
101
+ const scroller = scrollerRef.current, track = trackRef.current;
102
+ if (!scrolls || !scroller || !track || typeof ResizeObserver === "undefined")
103
+ return;
104
+ const update = () => { if (scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop > 2)
105
+ scroller.dataset.more = "";
106
+ else
107
+ delete scroller.dataset.more; };
108
+ update();
109
+ scroller.addEventListener("scroll", update, { passive: true });
110
+ const observer = new ResizeObserver(update);
111
+ observer.observe(scroller);
112
+ observer.observe(track);
113
+ return () => { scroller.removeEventListener("scroll", update); observer.disconnect(); };
114
+ }, [scrolls]);
115
+ const newestId = events.reduce((latest, event) => !latest || new Date(event.at).getTime() > new Date(latest.at).getTime() ? event : latest, null)?.id;
116
+ useEffect(() => {
117
+ const scroller = scrollerRef.current;
118
+ if (!scrollToNew || !scroller || scroller.scrollTop < 1)
119
+ return;
120
+ scroller.scrollTo({ top: 0, behavior: reduced ? "auto" : "smooth" });
121
+ }, [newestId, scrollToNew, reduced]);
122
+ const [initialDays] = useState(() => new Set(events.map(event => dayKey(new Date(event.at).getTime(), timeZone))));
123
+ const groups = useMemo(() => {
124
+ const today = dayKey(now, timeZone), yesterday = dayKey(now - 24 * HOUR, timeZone);
125
+ const heading = new Intl.DateTimeFormat(locale, { weekday: "long", month: "long", day: "numeric", timeZone });
126
+ const byDay = new Map();
127
+ [...events].map(event => ({ ...event, time: new Date(event.at).getTime() })).sort((a, b) => b.time - a.time).forEach(event => {
128
+ const day = dayKey(event.time, timeZone);
129
+ byDay.set(day, [...(byDay.get(day) ?? []), { ...event, day }]);
130
+ });
131
+ return [...byDay].map(([day, rows]) => ({ day, rows, label: day === today ? "Today" : day === yesterday ? "Yesterday" : heading.format(new Date(rows[0].time)) }));
132
+ }, [events, now, timeZone, locale]);
133
+ const formats = useMemo(() => ({
134
+ clock: new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit", timeZone }),
135
+ full: new Intl.DateTimeFormat(locale, { weekday: "long", month: "long", day: "numeric", hour: "numeric", minute: "2-digit", timeZone }),
136
+ }), [locale, timeZone]);
137
+ // Rows that come into view together are spaced a beat apart, so the line reads as drawing down the feed.
138
+ const nextReveal = useRef(0);
139
+ const schedule = useCallback(() => {
140
+ if (reduced)
141
+ return 0;
142
+ const current = performance.now() / 1000;
143
+ const start = Math.min(Math.max(current, nextReveal.current), current + .45);
144
+ nextReveal.current = start + STEP;
145
+ return start - current;
146
+ }, [reduced]);
147
+ const clock = useMemo(() => ({ schedule, reduced, fresh: known.fresh }), [schedule, reduced, known.fresh]);
148
+ function toggle(id) {
149
+ setExpanded(current => { const next = new Set(current); if (next.has(id))
150
+ next.delete(id);
151
+ else
152
+ next.add(id); return next; });
153
+ }
154
+ function onKeyDown(event) {
155
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key))
156
+ return;
157
+ const triggers = [...(scrollerRef.current?.querySelectorAll("[data-timeline-trigger]") ?? [])];
158
+ const index = triggers.indexOf(document.activeElement);
159
+ if (index < 0)
160
+ return;
161
+ event.preventDefault();
162
+ const next = event.key === "Home" ? 0 : event.key === "End" ? triggers.length - 1 : Math.min(Math.max(index + (event.key === "ArrowDown" ? 1 : -1), 0), triggers.length - 1);
163
+ triggers[next]?.focus();
164
+ }
165
+ return _jsx(RevealClock.Provider, { value: clock, children: _jsxs("div", { className: [styles.root, className].filter(Boolean).join(" "), role: "region", "aria-label": label, children: [_jsx("div", { ref: scrollerRef, className: styles.scroller, "data-scrolls": scrolls || undefined, style: scrolls ? { "--timeline-height": typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight } : undefined, onKeyDown: onKeyDown, children: _jsx("div", { ref: trackRef, children: _jsx(AnimatePresence, { children: groups.map(group => _jsxs(DaySection, { id: `${id}-${group.day}`, fresh: !initialDays.has(group.day), reduced: reduced, children: [_jsxs("div", { className: styles.day, role: "heading", "aria-level": headingLevel, id: `${id}-${group.day}`, children: [_jsx("span", { children: group.label }), _jsxs("span", { className: styles.count, "aria-hidden": "true", children: [_jsx(RollingCount, { value: group.rows.length, reduced: reduced }), " ", group.rows.length === 1 ? "update" : "updates"] }), _jsx("span", { className: styles.srOnly, children: `, ${group.rows.length} ${group.rows.length === 1 ? "update" : "updates"}` })] }), _jsx("ol", { className: styles.list, children: _jsx(AnimatePresence, { children: group.rows.map((row, index) => {
166
+ const today = group.label === "Today" && now - row.time < 12 * HOUR;
167
+ const time = today ? relative(row.time, now) : { short: formats.clock.format(new Date(row.time)), long: "" };
168
+ return _jsx(TimelineRow, { row: row, last: index === group.rows.length - 1, expanded: expanded.has(row.id), onToggle: () => toggle(row.id), timeLabel: time.short, timeFull: today ? `${time.long}, ${formats.full.format(new Date(row.time))}` : formats.full.format(new Date(row.time)) }, row.id);
169
+ }) }) })] }, group.day)) }) }) }), _jsx("span", { className: styles.srOnly, role: "status", children: known.announcement })] }) });
170
+ }
171
+ export default Timeline;
@@ -0,0 +1,51 @@
1
+ /* Geometry: a 28px marker column, the line centred under it, and rows whose first text line centres on the marker. */
2
+ .root { --marker: 28px; --marker-top: 8px; --line-gap: 4px; position: relative; display: grid; min-width: 0; color: var(--foreground); }
3
+ /* Inserting at the top pushes the rows down instead of the browser holding the scroll position still. */
4
+ .scroller { min-width: 0; overflow-anchor: none; }
5
+ @property --timeline-feather { syntax: "<length>"; inherits: false; initial-value: 0px; }
6
+ .scroller[data-scrolls] { max-height: var(--timeline-height); overflow-y: auto; overscroll-behavior: contain; scrollbar-color: var(--border-strong) transparent; scrollbar-width: thin; mask-image: linear-gradient(to bottom, #000 calc(100% - var(--timeline-feather)), transparent); transition: --timeline-feather var(--duration-standard) var(--ease-standard); }
7
+ /* While more updates wait below, the last visible row fades into the edge instead of being cut. */
8
+ .scroller[data-more] { --timeline-feather: 36px; }
9
+ .group { position: relative; }
10
+ .group[data-entering] { overflow: clip; }
11
+ /* Day labels stay pinned while their updates scroll under them. Set --timeline-surface when the feed sits on another surface. */
12
+ .day { position: sticky; z-index: 2; top: 0; display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-group); padding: var(--space-related) var(--space-group) var(--space-related) 0; background: var(--timeline-surface, var(--surface)); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
13
+ .count { color: var(--text-muted); font-size: var(--text-xs); font-weight: 400; font-variant-numeric: tabular-nums; }
14
+ .rolling { display: inline-flex; }
15
+ .place { position: relative; display: inline-flex; overflow: clip; overflow-clip-margin: .15em; }
16
+ .rise { position: relative; display: inline-flex; }
17
+ .riseLine { display: inline-block; white-space: nowrap; }
18
+ .list { margin: 0 0 var(--space-group); padding: 0; list-style: none; }
19
+
20
+ .item { position: relative; padding-left: calc(var(--marker) + var(--space-related)); }
21
+ .item[data-entering] { overflow: clip; }
22
+ .marker { position: absolute; z-index: 1; top: var(--marker-top); left: 0; display: grid; width: var(--marker); height: var(--marker); place-items: center; border-radius: var(--radius-full); corner-shape: round; color: var(--text-secondary); }
23
+ .marker img { display: block; width: 100%; height: 100%; border-radius: inherit; corner-shape: inherit; object-fit: cover; }
24
+ /* A person's portrait sits on a hairline ring; a system event is a quiet node with its icon. */
25
+ .marker::after { content: ""; position: absolute; inset: 0; border: 1px solid color-mix(in oklab, var(--foreground) 10%, transparent); border-radius: inherit; corner-shape: inherit; pointer-events: none; }
26
+ .marker[data-tone] { background: var(--surface); }
27
+ .marker[data-tone="success"] { background: color-mix(in oklab, var(--success) 12%, var(--surface)); color: var(--success); }
28
+ .marker[data-tone="danger"] { background: color-mix(in oklab, var(--danger) 12%, var(--surface)); color: var(--danger); }
29
+ .marker svg { width: 14px; height: 14px; stroke-width: 2; }
30
+ /* The line runs from just under this marker to just above the next one and draws downward from its top. */
31
+ .segment { position: absolute; top: calc(var(--marker-top) + var(--marker) + var(--line-gap)); bottom: calc(var(--line-gap) - var(--marker-top)); left: calc(var(--marker) / 2 - .5px); width: 1px; background: var(--border-strong); transform-origin: 50% 0; }
32
+
33
+ .content { min-width: 0; }
34
+ .trigger { display: grid; width: 100%; grid-template-columns: minmax(0, 1fr) auto; align-items: start; column-gap: var(--space-group); margin: 0; padding: 12px var(--space-group); border: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: transparent; color: inherit; font: inherit; text-align: left; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard); }
35
+ button.trigger { grid-template-columns: minmax(0, 1fr) auto 16px; cursor: pointer; }
36
+ @media (hover: hover) and (pointer: fine) { button.trigger:hover { background: var(--surface-muted); } }
37
+ button.trigger:active { background: var(--surface-muted); }
38
+ .text { display: grid; min-width: 0; gap: 2px; }
39
+ .title { color: var(--text-secondary); font-size: var(--text-sm); line-height: 20px; }
40
+ .actor { color: var(--foreground); font-weight: 500; }
41
+ .meta { color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
42
+ /* Times keep a steady column, so "9m" becoming "10m" never rewraps the title beside it. */
43
+ .time { display: flex; min-width: 2.75rem; justify-content: flex-end; color: var(--text-muted); font-size: var(--text-xs); line-height: 20px; font-variant-numeric: tabular-nums; white-space: nowrap; }
44
+ .chevron { display: grid; height: 20px; place-items: center; color: var(--text-muted); }
45
+ .detail { overflow: clip; overflow-clip-margin: 4px; }
46
+ .detailInner { padding: 0 var(--space-group) var(--space-inset); color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); }
47
+
48
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
49
+
50
+ @media (max-width: 420px) { .trigger { column-gap: var(--space-related); padding-inline: var(--space-related); } .detailInner { padding-inline: var(--space-related); } }
51
+ @media (prefers-reduced-motion: reduce) { .trigger { transition: none; } .scroller[data-scrolls] { transition: --timeline-feather var(--duration-instant) var(--ease-standard); } }
@@ -0,0 +1,8 @@
1
+ export interface ToastProps {
2
+ title: string;
3
+ description?: string;
4
+ open?: boolean;
5
+ onOpenChange?: (open: boolean) => void;
6
+ duration?: number;
7
+ }
8
+ export default function Toast({ title, description, open, onOpenChange, duration }: ToastProps): import("react").JSX.Element;
@@ -0,0 +1,118 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion } from "motion/react";
5
+ import { X } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./toast.module.css";
8
+ const subscribeHydration = () => () => { };
9
+ const exitFast = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };
10
+ const enter = { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] };
11
+ const textIn = { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` };
12
+ const textOut = { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: exitFast };
13
+ const textShown = { opacity: 1, y: "0em", filter: "blur(0px)" };
14
+ const fadeOut = { opacity: 0, transition: { duration: motionTokens.duration.instant } };
15
+ /** A swipe past this distance (px) or speed (px/s) dismisses the toast in the direction it was thrown. */
16
+ const swipe = { distance: 80, velocity: 480 };
17
+ /** Outgoing copies are hidden from assistive tech while they fade, so the live region reads only the current text. */
18
+ function Swap(props) {
19
+ const present = useIsPresent();
20
+ return _jsx(motion.span, { ...props, "aria-hidden": present ? props["aria-hidden"] : true });
21
+ }
22
+ /** Follows its content height. After `morphKey` changes, the height springs from the old size to the new one and then returns to auto, so passive reflows (a resize, a font swap) follow instantly. It clips only while moving, so focus rings stay visible at rest. */
23
+ function HeightFrame({ reduce, morphKey, children }) {
24
+ const frame = useRef(null);
25
+ const content = useRef(null);
26
+ const height = useMotionValue("auto");
27
+ const changedAt = useRef(0);
28
+ useLayoutEffect(() => { changedAt.current = performance.now(); }, [morphKey]);
29
+ useEffect(() => {
30
+ const node = content.current;
31
+ if (!node || typeof ResizeObserver === "undefined")
32
+ return;
33
+ let last;
34
+ let controls;
35
+ const settle = () => { height.jump("auto"); if (frame.current)
36
+ Object.assign(frame.current.style, { overflow: "", height: "auto" }); };
37
+ const observer = new ResizeObserver(([entry]) => {
38
+ if (!entry)
39
+ return;
40
+ const next = entry.borderBoxSize?.[0]?.blockSize ?? node.offsetHeight;
41
+ const current = height.get();
42
+ const from = typeof current === "number" ? current : last;
43
+ last = next;
44
+ controls?.stop();
45
+ if (reduce || from === undefined || from === next || performance.now() - changedAt.current > 120)
46
+ return settle();
47
+ // Pin the old height before this frame paints, then spring to the new one.
48
+ if (frame.current)
49
+ Object.assign(frame.current.style, { overflow: "hidden", height: `${from}px` });
50
+ controls = animate(height, [from, next], { ...motionTokens.spring.smooth, onComplete: settle });
51
+ });
52
+ observer.observe(node);
53
+ return () => { observer.disconnect(); controls?.stop(); };
54
+ }, [height, reduce]);
55
+ return _jsx(motion.div, { ref: frame, className: styles.frame, style: { height }, children: _jsx("div", { ref: content, className: styles.copy, children: children }) });
56
+ }
57
+ export default function Toast({ title, description, open = true, onOpenChange, duration = 4500 }) {
58
+ const reduce = useReducedMotion();
59
+ // Toasts that mount after hydration rise in from their edge; server-rendered ones start settled.
60
+ const hydrated = useSyncExternalStore(subscribeHydration, () => true, () => false);
61
+ const [enterOnMount] = useState(hydrated);
62
+ const [dismissed, setDismissed] = useState(false);
63
+ const [throwX, setThrowX] = useState(0);
64
+ const [lastOpen, setLastOpen] = useState(open);
65
+ if (open !== lastOpen) {
66
+ setLastOpen(open);
67
+ if (open) {
68
+ setDismissed(false);
69
+ setThrowX(0);
70
+ }
71
+ }
72
+ const visible = open && !dismissed;
73
+ const surface = useRef(null);
74
+ // The drag offset is owned here, so a release hands its velocity straight to the spring home or to the throw.
75
+ const x = useMotionValue(0);
76
+ const releaseVelocity = useRef(0);
77
+ /** Closing tells the parent at once, while the exit plays out here, so a new toast can be raised mid-exit and simply turns back. */
78
+ function dismiss(to) {
79
+ setThrowX(to);
80
+ setDismissed(true);
81
+ onOpenChange?.(false);
82
+ }
83
+ useEffect(() => {
84
+ if (!visible || !onOpenChange || duration <= 0)
85
+ return;
86
+ const timer = window.setTimeout(() => { setThrowX(0); setDismissed(true); onOpenChange(false); }, duration);
87
+ return () => window.clearTimeout(timer);
88
+ }, [visible, onOpenChange, duration]);
89
+ function handleDragEnd(_, info) {
90
+ const direction = Math.sign(info.offset.x || info.velocity.x);
91
+ // A short swipe springs back with its release velocity and a slight settle instead of drifting home.
92
+ if (Math.abs(info.offset.x) < swipe.distance && Math.abs(info.velocity.x) < swipe.velocity) {
93
+ animate(x, 0, { ...motionTokens.spring.snappy, velocity: info.velocity.x });
94
+ return;
95
+ }
96
+ releaseVelocity.current = info.velocity.x;
97
+ dismiss(direction * ((surface.current?.offsetWidth ?? 320) + 48));
98
+ }
99
+ const variants = reduce ? {
100
+ hidden: { opacity: 0 },
101
+ shown: { opacity: 1, transition: { duration: motionTokens.duration.instant } },
102
+ exit: fadeOut,
103
+ } : {
104
+ // x resets here because the drag value outlives a thrown toast; the next one must rise from the center.
105
+ hidden: { opacity: 0, x: 0, y: 16, scale: .96 },
106
+ // x returns to center too, in case the toast is raised again while a throw is still leaving.
107
+ shown: { opacity: 1, x: 0, y: 0, scale: 1, transition: { ...motionTokens.spring.morph, opacity: enter } },
108
+ // A thrown toast keeps its release velocity; a closed one sinks back toward the edge it came from.
109
+ // The throw settles loosely: it is invisible once the fade ends, so no long unseen tail keeps it mounted.
110
+ exit: (to) => to
111
+ ? { x: to, opacity: 0, transition: { x: { type: "spring", visualDuration: .3, bounce: 0, velocity: releaseVelocity.current, restDelta: 2, restSpeed: 40 }, opacity: { duration: motionTokens.duration.exit } } }
112
+ : { opacity: 0, y: 8, scale: .97, transition: { duration: motionTokens.duration.exit, ease: [...motionTokens.ease.standard] } },
113
+ };
114
+ const swap = { initial: reduce ? { opacity: 0 } : textIn, animate: textShown, exit: reduce ? fadeOut : textOut, transition: reduce ? { duration: motionTokens.duration.instant } : enter };
115
+ return (_jsx(AnimatePresence, { initial: enterOnMount, custom: throwX, children: visible && _jsxs(motion.div, { ref: surface, className: styles.toast, role: "status", "aria-live": "polite", "aria-atomic": "true", custom: throwX, variants: variants, initial: "hidden", animate: "shown", exit: "exit", style: { x },
116
+ // Drag writes touch-action into the markup, so the reduced motion switch waits for hydration to keep server and client in step.
117
+ drag: reduce && hydrated ? false : "x", dragMomentum: false, onDragEnd: handleDragEnd, children: [_jsx("span", { className: styles.icon, "aria-hidden": "true", children: _jsx("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.25, strokeLinecap: "round", strokeLinejoin: "round", children: _jsx(motion.path, { d: "M4 12.5l5 5L20 6.5", initial: reduce ? false : { pathLength: 0 }, animate: { pathLength: 1 }, transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter], delay: .12 } }) }) }), _jsxs(HeightFrame, { reduce: reduce, morphKey: `${title}\n${description ?? ""}`, children: [_jsx("strong", { className: styles.title, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(Swap, { className: styles.line, ...swap, children: title }, title) }) }), _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: description ? _jsx(Swap, { className: styles.description, ...swap, children: description }, description) : null })] }), _jsx("button", { className: styles.close, type: "button", "aria-label": "Dismiss notification", onClick: () => dismiss(0), children: _jsx(X, { width: 16, height: 16, strokeWidth: 2, "aria-hidden": "true" }) })] }, "toast") }));
118
+ }