@teamofsilicons/silicon-ui 0.0.0-stage → 0.1.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 (352) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +63 -2
  3. package/components/accordion/accordion.d.ts +13 -0
  4. package/components/accordion/accordion.js +35 -0
  5. package/components/accordion/accordion.module.css +15 -0
  6. package/components/action-button/action-button.d.ts +11 -0
  7. package/components/action-button/action-button.js +57 -0
  8. package/components/action-button/action-button.module.css +15 -0
  9. package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
  10. package/components/activity-heatmap/activity-heatmap.js +303 -0
  11. package/components/activity-heatmap/activity-heatmap.module.css +83 -0
  12. package/components/alert/alert.d.ts +13 -0
  13. package/components/alert/alert.js +79 -0
  14. package/components/alert/alert.module.css +15 -0
  15. package/components/animated-counter/animated-counter.d.ts +13 -0
  16. package/components/animated-counter/animated-counter.js +73 -0
  17. package/components/animated-counter/animated-counter.module.css +12 -0
  18. package/components/announcement-bar/announcement-bar.d.ts +53 -0
  19. package/components/announcement-bar/announcement-bar.js +187 -0
  20. package/components/announcement-bar/announcement-bar.module.css +60 -0
  21. package/components/avatar/avatar.d.ts +8 -0
  22. package/components/avatar/avatar.js +18 -0
  23. package/components/avatar/avatar.module.css +60 -0
  24. package/components/avatar-group/avatar-group.d.ts +13 -0
  25. package/components/avatar-group/avatar-group.js +24 -0
  26. package/components/avatar-group/avatar-group.module.css +27 -0
  27. package/components/badge/badge.d.ts +10 -0
  28. package/components/badge/badge.js +66 -0
  29. package/components/badge/badge.module.css +52 -0
  30. package/components/bar-chart/bar-chart.d.ts +32 -0
  31. package/components/bar-chart/bar-chart.js +327 -0
  32. package/components/bar-chart/bar-chart.module.css +49 -0
  33. package/components/billing-toggle/billing-toggle.d.ts +35 -0
  34. package/components/billing-toggle/billing-toggle.js +83 -0
  35. package/components/billing-toggle/billing-toggle.module.css +139 -0
  36. package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
  37. package/components/bottom-sheet/bottom-sheet.js +386 -0
  38. package/components/bottom-sheet/bottom-sheet.module.css +69 -0
  39. package/components/breadcrumb/breadcrumb.d.ts +13 -0
  40. package/components/breadcrumb/breadcrumb.js +19 -0
  41. package/components/breadcrumb/breadcrumb.module.css +12 -0
  42. package/components/brush-chart/brush-chart.d.ts +45 -0
  43. package/components/brush-chart/brush-chart.js +426 -0
  44. package/components/brush-chart/brush-chart.module.css +69 -0
  45. package/components/button/button.d.ts +10 -0
  46. package/components/button/button.js +97 -0
  47. package/components/button/button.module.css +39 -0
  48. package/components/button-group/button-group.d.ts +59 -0
  49. package/components/button-group/button-group.js +280 -0
  50. package/components/button-group/button-group.module.css +87 -0
  51. package/components/button-link/button-link.d.ts +24 -0
  52. package/components/button-link/button-link.js +11 -0
  53. package/components/button-link/button-link.module.css +7 -0
  54. package/components/calendar/calendar.d.ts +22 -0
  55. package/components/calendar/calendar.js +253 -0
  56. package/components/calendar/calendar.module.css +51 -0
  57. package/components/card/card.d.ts +19 -0
  58. package/components/card/card.js +108 -0
  59. package/components/card/card.module.css +51 -0
  60. package/components/card-stack/card-stack.d.ts +34 -0
  61. package/components/card-stack/card-stack.js +291 -0
  62. package/components/card-stack/card-stack.module.css +63 -0
  63. package/components/carousel/carousel.d.ts +28 -0
  64. package/components/carousel/carousel.js +332 -0
  65. package/components/carousel/carousel.module.css +42 -0
  66. package/components/chat-thread/chat-thread.d.ts +135 -0
  67. package/components/chat-thread/chat-thread.js +406 -0
  68. package/components/chat-thread/chat-thread.module.css +242 -0
  69. package/components/checkbox/checkbox.d.ts +7 -0
  70. package/components/checkbox/checkbox.js +23 -0
  71. package/components/checkbox/checkbox.module.css +15 -0
  72. package/components/chip-group/chip-group.d.ts +24 -0
  73. package/components/chip-group/chip-group.js +196 -0
  74. package/components/chip-group/chip-group.module.css +38 -0
  75. package/components/code-block/code-block.d.ts +11 -0
  76. package/components/code-block/code-block.js +133 -0
  77. package/components/code-block/code-block.module.css +43 -0
  78. package/components/color-picker/color-picker.d.ts +40 -0
  79. package/components/color-picker/color-picker.js +449 -0
  80. package/components/color-picker/color-picker.module.css +111 -0
  81. package/components/combobox/combobox.d.ts +19 -0
  82. package/components/combobox/combobox.js +167 -0
  83. package/components/combobox/combobox.module.css +28 -0
  84. package/components/command-palette/command-palette.d.ts +20 -0
  85. package/components/command-palette/command-palette.js +151 -0
  86. package/components/command-palette/command-palette.module.css +94 -0
  87. package/components/comment-thread/comment-thread.d.ts +120 -0
  88. package/components/comment-thread/comment-thread.js +343 -0
  89. package/components/comment-thread/comment-thread.module.css +195 -0
  90. package/components/confirm-morph/confirm-morph.d.ts +65 -0
  91. package/components/confirm-morph/confirm-morph.js +355 -0
  92. package/components/confirm-morph/confirm-morph.module.css +146 -0
  93. package/components/context-menu/context-menu.d.ts +18 -0
  94. package/components/context-menu/context-menu.js +148 -0
  95. package/components/context-menu/context-menu.module.css +17 -0
  96. package/components/copy-button/copy-button.d.ts +10 -0
  97. package/components/copy-button/copy-button.js +69 -0
  98. package/components/copy-button/copy-button.module.css +21 -0
  99. package/components/countdown/countdown.d.ts +31 -0
  100. package/components/countdown/countdown.js +177 -0
  101. package/components/countdown/countdown.module.css +49 -0
  102. package/components/date-picker/date-picker.d.ts +17 -0
  103. package/components/date-picker/date-picker.js +100 -0
  104. package/components/date-picker/date-picker.module.css +34 -0
  105. package/components/date-range-picker/date-range-picker.d.ts +44 -0
  106. package/components/date-range-picker/date-range-picker.js +534 -0
  107. package/components/date-range-picker/date-range-picker.module.css +214 -0
  108. package/components/dialog/dialog.d.ts +11 -0
  109. package/components/dialog/dialog.js +52 -0
  110. package/components/dialog/dialog.module.css +36 -0
  111. package/components/donut-chart/donut-chart.d.ts +46 -0
  112. package/components/donut-chart/donut-chart.js +382 -0
  113. package/components/donut-chart/donut-chart.module.css +48 -0
  114. package/components/drawer/drawer.d.ts +20 -0
  115. package/components/drawer/drawer.js +111 -0
  116. package/components/drawer/drawer.module.css +97 -0
  117. package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
  118. package/components/dropdown-menu/dropdown-menu.js +39 -0
  119. package/components/dropdown-menu/dropdown-menu.module.css +38 -0
  120. package/components/elastic-slider/elastic-slider.d.ts +39 -0
  121. package/components/elastic-slider/elastic-slider.js +261 -0
  122. package/components/elastic-slider/elastic-slider.module.css +40 -0
  123. package/components/empty-state/empty-state.d.ts +12 -0
  124. package/components/empty-state/empty-state.js +69 -0
  125. package/components/empty-state/empty-state.module.css +13 -0
  126. package/components/expandable-card/expandable-card.d.ts +13 -0
  127. package/components/expandable-card/expandable-card.js +89 -0
  128. package/components/expandable-card/expandable-card.module.css +22 -0
  129. package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
  130. package/components/expanding-button-group/expanding-button-group.js +258 -0
  131. package/components/expanding-button-group/expanding-button-group.module.css +44 -0
  132. package/components/expanding-search/expanding-search.d.ts +38 -0
  133. package/components/expanding-search/expanding-search.js +242 -0
  134. package/components/expanding-search/expanding-search.module.css +52 -0
  135. package/components/file-dropzone/file-dropzone.d.ts +44 -0
  136. package/components/file-dropzone/file-dropzone.js +379 -0
  137. package/components/file-dropzone/file-dropzone.module.css +115 -0
  138. package/components/file-upload/file-upload.d.ts +22 -0
  139. package/components/file-upload/file-upload.js +192 -0
  140. package/components/file-upload/file-upload.module.css +49 -0
  141. package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
  142. package/components/filter-toolbar/filter-toolbar.js +446 -0
  143. package/components/filter-toolbar/filter-toolbar.module.css +70 -0
  144. package/components/floating-button-group/floating-button-group.d.ts +51 -0
  145. package/components/floating-button-group/floating-button-group.js +215 -0
  146. package/components/floating-button-group/floating-button-group.module.css +74 -0
  147. package/components/gauge/gauge.d.ts +18 -0
  148. package/components/gauge/gauge.js +101 -0
  149. package/components/gauge/gauge.module.css +22 -0
  150. package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
  151. package/components/hold-to-confirm/hold-to-confirm.js +165 -0
  152. package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
  153. package/components/hover-card/hover-card.d.ts +38 -0
  154. package/components/hover-card/hover-card.js +145 -0
  155. package/components/hover-card/hover-card.module.css +20 -0
  156. package/components/image-compare/image-compare.d.ts +27 -0
  157. package/components/image-compare/image-compare.js +267 -0
  158. package/components/image-compare/image-compare.module.css +25 -0
  159. package/components/in-view-title/in-view-title.d.ts +17 -0
  160. package/components/in-view-title/in-view-title.js +70 -0
  161. package/components/in-view-title/in-view-title.module.css +13 -0
  162. package/components/inline-edit/inline-edit.d.ts +27 -0
  163. package/components/inline-edit/inline-edit.js +315 -0
  164. package/components/inline-edit/inline-edit.module.css +73 -0
  165. package/components/input/input.d.ts +7 -0
  166. package/components/input/input.js +60 -0
  167. package/components/input/input.module.css +22 -0
  168. package/components/json-viewer/json-viewer.d.ts +48 -0
  169. package/components/json-viewer/json-viewer.js +463 -0
  170. package/components/json-viewer/json-viewer.module.css +92 -0
  171. package/components/line-chart/line-chart.d.ts +58 -0
  172. package/components/line-chart/line-chart.js +383 -0
  173. package/components/line-chart/line-chart.module.css +60 -0
  174. package/components/mention-input/mention-input.d.ts +72 -0
  175. package/components/mention-input/mention-input.js +417 -0
  176. package/components/mention-input/mention-input.module.css +134 -0
  177. package/components/metric-card/metric-card.d.ts +8 -0
  178. package/components/metric-card/metric-card.js +44 -0
  179. package/components/metric-card/metric-card.module.css +18 -0
  180. package/components/money-input/money-input.d.ts +48 -0
  181. package/components/money-input/money-input.js +503 -0
  182. package/components/money-input/money-input.module.css +77 -0
  183. package/components/morph-select/morph-select.d.ts +50 -0
  184. package/components/morph-select/morph-select.js +395 -0
  185. package/components/morph-select/morph-select.module.css +88 -0
  186. package/components/multi-select/multi-select.d.ts +18 -0
  187. package/components/multi-select/multi-select.js +105 -0
  188. package/components/multi-select/multi-select.module.css +37 -0
  189. package/components/notification-center/notification-center.d.ts +24 -0
  190. package/components/notification-center/notification-center.js +146 -0
  191. package/components/notification-center/notification-center.module.css +79 -0
  192. package/components/number-field/number-field.d.ts +41 -0
  193. package/components/number-field/number-field.js +455 -0
  194. package/components/number-field/number-field.module.css +71 -0
  195. package/components/otp-input/otp-input.d.ts +13 -0
  196. package/components/otp-input/otp-input.js +129 -0
  197. package/components/otp-input/otp-input.module.css +28 -0
  198. package/components/pagination/pagination.d.ts +7 -0
  199. package/components/pagination/pagination.js +60 -0
  200. package/components/pagination/pagination.module.css +16 -0
  201. package/components/password-field/password-field.d.ts +14 -0
  202. package/components/password-field/password-field.js +51 -0
  203. package/components/password-field/password-field.module.css +27 -0
  204. package/components/password-strength/password-strength.d.ts +107 -0
  205. package/components/password-strength/password-strength.js +161 -0
  206. package/components/password-strength/password-strength.module.css +57 -0
  207. package/components/phone-input/phone-input.d.ts +73 -0
  208. package/components/phone-input/phone-input.js +612 -0
  209. package/components/phone-input/phone-input.module.css +98 -0
  210. package/components/popover/popover.d.ts +6 -0
  211. package/components/popover/popover.js +19 -0
  212. package/components/popover/popover.module.css +32 -0
  213. package/components/progress/progress.d.ts +8 -0
  214. package/components/progress/progress.js +42 -0
  215. package/components/progress/progress.module.css +1 -0
  216. package/components/radio-cards/radio-cards.d.ts +35 -0
  217. package/components/radio-cards/radio-cards.js +110 -0
  218. package/components/radio-cards/radio-cards.module.css +84 -0
  219. package/components/radio-group/radio-group.d.ts +13 -0
  220. package/components/radio-group/radio-group.js +55 -0
  221. package/components/radio-group/radio-group.module.css +30 -0
  222. package/components/resizable-panels/resizable-panels.d.ts +37 -0
  223. package/components/resizable-panels/resizable-panels.js +338 -0
  224. package/components/resizable-panels/resizable-panels.module.css +40 -0
  225. package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
  226. package/components/rich-text-editor/rich-text-editor.js +1733 -0
  227. package/components/rich-text-editor/rich-text-editor.module.css +201 -0
  228. package/components/ridgeline/ridgeline.d.ts +37 -0
  229. package/components/ridgeline/ridgeline.js +346 -0
  230. package/components/ridgeline/ridgeline.module.css +58 -0
  231. package/components/scroll-area/scroll-area.d.ts +40 -0
  232. package/components/scroll-area/scroll-area.js +153 -0
  233. package/components/scroll-area/scroll-area.module.css +79 -0
  234. package/components/search-field/search-field.d.ts +7 -0
  235. package/components/search-field/search-field.js +20 -0
  236. package/components/search-field/search-field.module.css +17 -0
  237. package/components/segmented-control/segmented-control.d.ts +16 -0
  238. package/components/segmented-control/segmented-control.js +56 -0
  239. package/components/segmented-control/segmented-control.module.css +15 -0
  240. package/components/select/select.d.ts +15 -0
  241. package/components/select/select.js +36 -0
  242. package/components/select/select.module.css +35 -0
  243. package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
  244. package/components/shortcut-recorder/shortcut-recorder.js +380 -0
  245. package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
  246. package/components/signature-pad/signature-pad.d.ts +72 -0
  247. package/components/signature-pad/signature-pad.js +450 -0
  248. package/components/signature-pad/signature-pad.module.css +84 -0
  249. package/components/skeleton/skeleton.d.ts +13 -0
  250. package/components/skeleton/skeleton.js +53 -0
  251. package/components/skeleton/skeleton.module.css +11 -0
  252. package/components/slider/slider.d.ts +45 -0
  253. package/components/slider/slider.js +347 -0
  254. package/components/slider/slider.module.css +45 -0
  255. package/components/slope-chart/slope-chart.d.ts +38 -0
  256. package/components/slope-chart/slope-chart.js +183 -0
  257. package/components/slope-chart/slope-chart.module.css +51 -0
  258. package/components/slot-text/slot-text.d.ts +26 -0
  259. package/components/slot-text/slot-text.js +119 -0
  260. package/components/slot-text/slot-text.module.css +19 -0
  261. package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
  262. package/components/sortable-data-table/sortable-data-table.js +240 -0
  263. package/components/sortable-data-table/sortable-data-table.module.css +116 -0
  264. package/components/sparkline/sparkline.d.ts +19 -0
  265. package/components/sparkline/sparkline.js +239 -0
  266. package/components/sparkline/sparkline.module.css +26 -0
  267. package/components/split-button/split-button.d.ts +19 -0
  268. package/components/split-button/split-button.js +33 -0
  269. package/components/split-button/split-button.module.css +51 -0
  270. package/components/stepper/stepper.d.ts +36 -0
  271. package/components/stepper/stepper.js +108 -0
  272. package/components/stepper/stepper.module.css +74 -0
  273. package/components/streamgraph/streamgraph.d.ts +51 -0
  274. package/components/streamgraph/streamgraph.js +376 -0
  275. package/components/streamgraph/streamgraph.module.css +55 -0
  276. package/components/swipe-actions/swipe-actions.d.ts +35 -0
  277. package/components/swipe-actions/swipe-actions.js +330 -0
  278. package/components/swipe-actions/swipe-actions.module.css +64 -0
  279. package/components/switch/switch.d.ts +7 -0
  280. package/components/switch/switch.js +45 -0
  281. package/components/switch/switch.module.css +12 -0
  282. package/components/tabs/tabs.d.ts +8 -0
  283. package/components/tabs/tabs.js +138 -0
  284. package/components/tabs/tabs.module.css +27 -0
  285. package/components/tag-input/tag-input.d.ts +15 -0
  286. package/components/tag-input/tag-input.js +150 -0
  287. package/components/tag-input/tag-input.module.css +34 -0
  288. package/components/text-morph/text-morph.d.ts +13 -0
  289. package/components/text-morph/text-morph.js +69 -0
  290. package/components/text-morph/text-morph.module.css +8 -0
  291. package/components/text-reveal/text-reveal.d.ts +14 -0
  292. package/components/text-reveal/text-reveal.js +20 -0
  293. package/components/text-reveal/text-reveal.module.css +13 -0
  294. package/components/text-shimmer/text-shimmer.d.ts +18 -0
  295. package/components/text-shimmer/text-shimmer.js +90 -0
  296. package/components/text-shimmer/text-shimmer.module.css +9 -0
  297. package/components/textarea/textarea.d.ts +7 -0
  298. package/components/textarea/textarea.js +56 -0
  299. package/components/textarea/textarea.module.css +22 -0
  300. package/components/theme-switch/theme-switch.d.ts +11 -0
  301. package/components/theme-switch/theme-switch.js +37 -0
  302. package/components/theme-switch/theme-switch.module.css +31 -0
  303. package/components/time-picker/time-picker.d.ts +13 -0
  304. package/components/time-picker/time-picker.js +95 -0
  305. package/components/time-picker/time-picker.module.css +23 -0
  306. package/components/timeline/timeline.d.ts +47 -0
  307. package/components/timeline/timeline.js +171 -0
  308. package/components/timeline/timeline.module.css +51 -0
  309. package/components/toast/toast.d.ts +8 -0
  310. package/components/toast/toast.js +118 -0
  311. package/components/toast/toast.module.css +100 -0
  312. package/components/toast-stack/toast-stack.d.ts +57 -0
  313. package/components/toast-stack/toast-stack.js +399 -0
  314. package/components/toast-stack/toast-stack.module.css +61 -0
  315. package/components/tooltip/tooltip.d.ts +8 -0
  316. package/components/tooltip/tooltip.js +62 -0
  317. package/components/tooltip/tooltip.module.css +18 -0
  318. package/components/tree-view/tree-view.d.ts +17 -0
  319. package/components/tree-view/tree-view.js +147 -0
  320. package/components/tree-view/tree-view.module.css +22 -0
  321. package/components/treemap/treemap.d.ts +35 -0
  322. package/components/treemap/treemap.js +375 -0
  323. package/components/treemap/treemap.module.css +59 -0
  324. package/components/usage-meter/usage-meter.d.ts +24 -0
  325. package/components/usage-meter/usage-meter.js +296 -0
  326. package/components/usage-meter/usage-meter.module.css +61 -0
  327. package/components/user-menu/user-menu.d.ts +62 -0
  328. package/components/user-menu/user-menu.js +316 -0
  329. package/components/user-menu/user-menu.module.css +97 -0
  330. package/components/waffle-chart/waffle-chart.d.ts +39 -0
  331. package/components/waffle-chart/waffle-chart.js +244 -0
  332. package/components/waffle-chart/waffle-chart.module.css +52 -0
  333. package/components.json +1330 -0
  334. package/lib/escape.d.ts +19 -0
  335. package/lib/escape.js +50 -0
  336. package/lib/media.d.ts +353 -0
  337. package/lib/media.js +65 -0
  338. package/lib/menu-highlight.d.ts +28 -0
  339. package/lib/menu-highlight.js +43 -0
  340. package/lib/motion-tokens.d.ts +60 -0
  341. package/lib/motion-tokens.js +25 -0
  342. package/lib/return-focus.d.ts +9 -0
  343. package/lib/return-focus.js +53 -0
  344. package/lib/silicon-provider.d.ts +60 -0
  345. package/lib/silicon-provider.js +47 -0
  346. package/lib/use-copy-feedback.d.ts +8 -0
  347. package/lib/use-copy-feedback.js +36 -0
  348. package/lib/use-today.d.ts +2 -0
  349. package/lib/use-today.js +29 -0
  350. package/package.json +66 -4
  351. package/styles.css +218 -0
  352. package/styles.css.d.ts +1 -0
@@ -0,0 +1,83 @@
1
+ /* Levels are tints of the accent over a neutral empty square, so the grid follows the selected accent and both themes. */
2
+ .root { --level-0: color-mix(in oklab, var(--foreground) 6.5%, var(--surface)); --level-1: color-mix(in oklab, var(--accent) 26%, var(--level-0)); --level-2: color-mix(in oklab, var(--accent) 50%, var(--level-0)); --level-3: color-mix(in oklab, var(--accent) 76%, var(--level-0)); --level-4: var(--accent); container-type: inline-size; position: relative; display: grid; min-width: 0; gap: var(--space-inset); color: var(--foreground); }
3
+ .header { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-group) var(--space-inset); }
4
+ .summary { display: flex; min-width: 0; flex-wrap: wrap; align-items: baseline; column-gap: .3em; margin: 0; color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); font-variant-numeric: tabular-nums; }
5
+ .total { color: var(--foreground); font-weight: 500; white-space: nowrap; }
6
+ .period { white-space: nowrap; }
7
+ .actions { display: flex; flex: none; align-items: center; }
8
+ .rollingFrame { display: inline-flex; justify-content: flex-end; overflow: clip; overflow-clip-margin: .2em; }
9
+ .rolling { display: inline-flex; flex: none; }
10
+ /* Each place is its own window, so a turning digit clips at the line instead of brushing the next line of text. */
11
+ .place { position: relative; display: inline-flex; overflow: clip; overflow-clip-margin: .15em; }
12
+ .placeChar, .riseLine { display: inline-block; }
13
+ .rise { position: relative; display: inline-flex; }
14
+
15
+ /* The scroller starts at its right edge on narrow screens (the most recent weeks) without script, through its writing direction. */
16
+ .scroller { direction: rtl; overflow-x: auto; overflow-y: hidden; margin: -6px; padding: 6px; overscroll-behavior-x: contain; scrollbar-color: var(--border-strong) transparent; scrollbar-width: thin; }
17
+ .canvas { --gap: 3px; --label: 30px; --cell: clamp(9px, calc((100cqi - var(--label) - var(--gap) * 53) / 53), 15px); --step: calc(var(--cell) + var(--gap)); direction: ltr; display: grid; width: max-content; grid-template-columns: var(--label) auto; grid-template-rows: auto auto; column-gap: var(--gap); row-gap: var(--space-related); }
18
+ .months { position: relative; grid-row: 1; grid-column: 2; height: 1rem; color: var(--text-muted); font-size: var(--text-xs); line-height: 1rem; }
19
+ /* Month labels glide to their new week when a range starts on a different weekday. */
20
+ .month { position: absolute; top: 0; left: 0; white-space: nowrap; transform: translateX(calc(var(--col) * var(--step))); transition: transform var(--duration-spring) var(--ease-spring); }
21
+ /* Weekday labels stay pinned while a narrow grid scrolls under them. Set --heatmap-surface when the heatmap sits on another surface. */
22
+ .weekdays { position: sticky; z-index: 1; left: -6px; display: grid; grid-row: 1 / 3; grid-column: 1; grid-template-rows: repeat(7, var(--cell)); align-content: end; row-gap: var(--gap); margin: 0 calc(var(--gap) * -1) 0 -6px; padding: 0 var(--gap) 0 6px; background: var(--heatmap-surface, var(--surface)); color: var(--text-muted); font-size: var(--text-xs); }
23
+ .weekday { display: flex; height: var(--cell); align-items: center; white-space: nowrap; line-height: 1; }
24
+ .plot { position: relative; grid-row: 2; grid-column: 2; }
25
+ .grid { display: grid; gap: var(--gap); outline: none; }
26
+ .row { display: flex; gap: var(--gap); }
27
+
28
+ /* One square per day. The wave delay rides a custom property so a reveal and a recolor sweep the same diagonal. */
29
+ .cell { --delay: var(--wave); display: block; width: var(--cell); height: var(--cell); flex: none; border-radius: calc(var(--cell) * .28); background: var(--level-0); outline: 1.5px solid transparent; outline-offset: 1px; cursor: default; -webkit-tap-highlight-color: transparent; transition: background-color 320ms var(--ease-standard) var(--delay), transform var(--duration-spring) var(--ease-spring) var(--delay), opacity var(--duration-fast) var(--ease-standard); }
30
+ .grid[data-direction="back"] .cell { --delay: var(--wave-back); }
31
+ .cell[data-level="1"] { background: var(--level-1); }
32
+ .cell[data-level="2"] { background: var(--level-2); }
33
+ .cell[data-level="3"] { background: var(--level-3); }
34
+ .cell[data-level="4"] { background: var(--level-4); }
35
+ /* Days outside the range shrink away in the sweep and grow back when a range needs them. */
36
+ .cell[data-empty] { transform: scale(0); background: var(--level-0); }
37
+ .grid[data-reveal="hidden"] .cell { opacity: 0; transform: scale(.5); transition: none; }
38
+ .grid[data-reveal="hidden"] .cell[data-empty] { transform: scale(0); }
39
+ .grid[data-reveal="revealing"] .cell { transition: background-color 320ms var(--ease-standard) var(--delay), transform var(--duration-spring) var(--ease-spring) var(--delay), opacity 360ms var(--ease-enter) var(--delay); }
40
+ .grid[data-highlight] .cell:not([data-empty]) { opacity: .2; }
41
+ .grid[data-highlight="0"] .cell[data-level="0"], .grid[data-highlight="1"] .cell[data-level="1"], .grid[data-highlight="2"] .cell[data-level="2"], .grid[data-highlight="3"] .cell[data-level="3"], .grid[data-highlight="4"] .cell[data-level="4"] { opacity: 1; }
42
+ @media (hover: hover) and (pointer: fine) { .cell:not([data-empty]):hover { outline-color: color-mix(in oklab, var(--foreground) 45%, transparent); } }
43
+
44
+ /* The selection ring glides between days, and follows its day when the range changes. */
45
+ .ring { position: absolute; top: -3px; left: -3px; width: calc(var(--cell) + 6px); height: calc(var(--cell) + 6px); border: 1.5px solid var(--foreground); border-radius: calc(var(--cell) * .28 + 3px); opacity: 0; pointer-events: none; transform: translate(calc(var(--col) * var(--step)), calc(var(--row) * var(--step))); transition: transform var(--duration-spring) var(--ease-spring), opacity var(--duration-fast) var(--ease-standard); }
46
+ .ring[data-shown] { opacity: 1; }
47
+
48
+ .legend { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: var(--space-group); color: var(--text-muted); font-size: var(--text-xs); }
49
+ /* The caption names the previewed level; it sits apart from the scale so its changing width never nudges the swatches. */
50
+ .caption { position: relative; min-width: 0; overflow: hidden; color: var(--text-secondary); font-variant-numeric: tabular-nums; white-space: nowrap; text-overflow: ellipsis; }
51
+ .scale { display: flex; flex: none; align-items: center; gap: var(--space-related); }
52
+ .captionShort { display: none; }
53
+ /* Narrow cards keep the day count and let the highlighted squares say which level it is. */
54
+ @container (max-width: 440px) { .captionLong { display: none; } .captionShort { display: inline; } }
55
+ .legendText { line-height: 1; }
56
+ .swatches { display: flex; }
57
+ /* The visible square stays the size of a day while the button keeps a comfortable target around it. */
58
+ .swatch { position: relative; display: grid; width: 15px; height: 20px; place-items: center; padding: 0; border: 0; border-radius: var(--radius-detail); background: transparent; cursor: pointer; -webkit-tap-highlight-color: transparent; }
59
+ .swatch::before { content: ""; width: 11px; height: 11px; border-radius: var(--radius-detail); background: var(--level-0); }
60
+ .swatch[data-level="1"]::before { background: var(--level-1); }
61
+ .swatch[data-level="2"]::before { background: var(--level-2); }
62
+ .swatch[data-level="3"]::before { background: var(--level-3); }
63
+ .swatch[data-level="4"]::before { background: var(--level-4); }
64
+ .swatch[aria-pressed="true"] { box-shadow: inset 0 0 0 1.5px var(--foreground); }
65
+
66
+ /* One bubble for the whole grid. The outer layer glides between cells; the bubble fades and scales from its tail. */
67
+ .tip { position: absolute; z-index: 5; top: 0; left: 0; width: 0; height: 0; pointer-events: none; }
68
+ .bubble { position: absolute; bottom: 8px; left: 0; display: block; padding: 7px 12px 8px; border: 1px solid color-mix(in oklab, var(--background) 14%, var(--foreground)); border-radius: var(--radius-control); background: var(--foreground); color: var(--background); box-shadow: var(--shadow-raised); transform-origin: 50% 100%; will-change: transform, opacity; }
69
+ .tipBody { position: relative; display: block; overflow: clip; overflow-clip-margin: 8px; }
70
+ .tipMeasure { position: absolute; top: 0; left: 0; display: grid; width: max-content; visibility: hidden; }
71
+ .tipLines { display: grid; width: max-content; }
72
+ .tipMeasure > span, .tipLines > span { white-space: nowrap; }
73
+ .tipPrimary { font-size: var(--text-sm); font-weight: 500; line-height: 1.3; font-variant-numeric: tabular-nums; }
74
+ .tipSecondary { color: color-mix(in oklab, var(--background) 68%, var(--foreground)); font-size: var(--text-xs); line-height: 1.35; }
75
+
76
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
77
+
78
+ @media (prefers-reduced-motion: reduce) {
79
+ .cell, .grid[data-reveal="revealing"] .cell { transition: background-color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
80
+ .grid[data-reveal="hidden"] .cell { transform: none; }
81
+ .cell[data-empty], .grid[data-reveal="hidden"] .cell[data-empty] { opacity: 0; transform: none; }
82
+ .month, .ring { transition: opacity var(--duration-fast) var(--ease-standard); }
83
+ }
@@ -0,0 +1,13 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ export type AlertTone = "info" | "success" | "warning" | "danger";
3
+ export interface AlertProps extends HTMLAttributes<HTMLDivElement> {
4
+ tone?: AlertTone;
5
+ title: string;
6
+ children?: ReactNode;
7
+ /** Controls presence. Hiding the alert collapses its height and fades it out. */
8
+ open?: boolean;
9
+ /** Shows a dismiss button. An uncontrolled alert collapses first, then calls this. */
10
+ onDismiss?: () => void;
11
+ }
12
+ export declare function Alert({ open, onDismiss, ...props }: AlertProps): import("react").JSX.Element;
13
+ export default Alert;
@@ -0,0 +1,79 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion } from "motion/react";
5
+ import { Check, Info as InfoCircle, TriangleAlert as WarningTriangle, CircleX as XmarkCircle, X } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./alert.module.css";
8
+ const icons = { info: InfoCircle, success: Check, warning: WarningTriangle, danger: XmarkCircle };
9
+ const exitFast = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };
10
+ const textIn = { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` };
11
+ const textOut = { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: exitFast };
12
+ const iconIn = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };
13
+ const shown = { opacity: 1, y: "0em", scale: 1, filter: "blur(0px)" };
14
+ const fadeOut = { opacity: 0, transition: { duration: motionTokens.duration.instant } };
15
+ /** Outgoing copies are hidden from assistive tech while they fade, so the live region reads only the current text. */
16
+ function Swap(props) {
17
+ const present = useIsPresent();
18
+ return _jsx(motion.span, { ...props, "aria-hidden": present ? props["aria-hidden"] : true });
19
+ }
20
+ /** 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. */
21
+ function HeightFrame({ className, reduce, morphKey, children }) {
22
+ const frame = useRef(null);
23
+ const content = useRef(null);
24
+ const height = useMotionValue("auto");
25
+ const changedAt = useRef(0);
26
+ useLayoutEffect(() => { changedAt.current = performance.now(); }, [morphKey]);
27
+ useEffect(() => {
28
+ const node = content.current;
29
+ if (!node || typeof ResizeObserver === "undefined")
30
+ return;
31
+ let last;
32
+ let controls;
33
+ const settle = () => { height.jump("auto"); if (frame.current)
34
+ Object.assign(frame.current.style, { overflow: "", height: "auto" }); };
35
+ const observer = new ResizeObserver(([entry]) => {
36
+ if (!entry)
37
+ return;
38
+ const next = entry.borderBoxSize?.[0]?.blockSize ?? node.offsetHeight;
39
+ const current = height.get();
40
+ const from = typeof current === "number" ? current : last;
41
+ last = next;
42
+ controls?.stop();
43
+ if (reduce || from === undefined || from === next || performance.now() - changedAt.current > 120)
44
+ return settle();
45
+ // Pin the old height before this frame paints, then spring to the new one.
46
+ if (frame.current)
47
+ Object.assign(frame.current.style, { overflow: "hidden", height: `${from}px` });
48
+ controls = animate(height, [from, next], { ...motionTokens.spring.smooth, onComplete: settle });
49
+ });
50
+ observer.observe(node);
51
+ return () => { observer.disconnect(); controls?.stop(); };
52
+ }, [height, reduce]);
53
+ return _jsx(motion.div, { ref: frame, className: className, style: { height }, children: _jsx("div", { ref: content, className: styles.content, children: children }) });
54
+ }
55
+ function AlertBox({ tone = "info", title, children, className, reduce, onDismiss, animateIcon = false, ...props }) {
56
+ const Icon = icons[tone];
57
+ const text = typeof children === "string" || typeof children === "number" ? String(children) : null;
58
+ const hasDetails = children !== undefined && children !== null && children !== false && children !== "";
59
+ const enter = reduce ? { duration: motionTokens.duration.instant } : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] };
60
+ const swap = { initial: reduce ? { opacity: 0 } : textIn, animate: shown, exit: reduce ? fadeOut : textOut, transition: enter };
61
+ return _jsxs("div", { ...props, className: [styles.alert, styles[tone], className].filter(Boolean).join(" "), role: tone === "danger" ? "alert" : "status", children: [_jsx(motion.span, { className: styles.icon, initial: animateIcon && !reduce ? iconIn : false, animate: shown, transition: motionTokens.spring.snappy, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(Swap, { className: styles.glyph, initial: reduce ? { opacity: 0 } : iconIn, animate: shown, exit: reduce ? fadeOut : { ...iconIn, transition: exitFast }, transition: reduce ? enter : motionTokens.spring.snappy, children: _jsx(Icon, { width: 18, height: 18, "aria-hidden": "true" }) }, tone) }) }), _jsxs(HeightFrame, { className: styles.copy, reduce: reduce, morphKey: `${title}\n${hasDetails}\n${text ?? ""}`, 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: hasDetails && _jsx(motion.p, { className: styles.description, ...swap, children: text === null ? children : _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(Swap, { className: styles.line, ...swap, children: text }, text) }) }, "details") })] }), onDismiss && _jsx("button", { type: "button", className: styles.dismiss, "aria-label": `Dismiss: ${title}`, onClick: onDismiss, children: _jsx(X, { width: 16, height: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] });
62
+ }
63
+ export function Alert({ open, onDismiss, ...props }) {
64
+ const reduce = useReducedMotion();
65
+ const [dismissed, setDismissed] = useState(false);
66
+ const shell = useRef(null);
67
+ const setClip = (value) => { if (shell.current)
68
+ shell.current.style.overflow = value; };
69
+ if (open === undefined && !onDismiss)
70
+ return _jsx(AlertBox, { ...props, reduce: reduce });
71
+ const isOpen = open ?? !dismissed;
72
+ const dismiss = onDismiss && (() => open === undefined ? setDismissed(true) : onDismiss());
73
+ const presence = reduce ? { duration: 0 } : { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } };
74
+ const exit = { height: 0, opacity: 0, transition: reduce ? { duration: 0 } : { height: motionTokens.spring.smooth, opacity: exitFast } };
75
+ // Presence collapses the height with the fade, so the content below closes the gap instead of jumping.
76
+ return _jsx(AnimatePresence, { initial: false, onExitComplete: () => { if (open === undefined)
77
+ onDismiss?.(); }, children: isOpen && _jsx(motion.div, { className: styles.presence, initial: { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: exit, transition: presence, ref: shell, onAnimationStart: () => setClip("hidden"), onAnimationComplete: () => setClip(""), children: _jsx(AlertBox, { ...props, reduce: reduce, onDismiss: dismiss, animateIcon: true }) }, "alert") });
78
+ }
79
+ export default Alert;
@@ -0,0 +1,15 @@
1
+ .presence { min-width: 0; }
2
+ .alert { display: flex; align-items: start; gap: var(--space-group); border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: var(--space-inset); background: var(--surface); box-shadow: var(--shadow-resting); }
3
+ .icon, .glyph { position: relative; display: grid; flex: 0 0 auto; place-items: center; }
4
+ .icon { width: 18px; height: 18px; margin-top: 1px; }
5
+ .icon svg { display: block; }
6
+ .copy { flex: 1; min-width: 0; }
7
+ .content { position: relative; }
8
+ .title { position: relative; display: block; font-size: var(--text-sm); font-weight: 500; }
9
+ .line { display: block; }
10
+ .description { position: relative; margin: var(--space-tight) 0 0; color: var(--text-secondary); font-size: var(--text-xs); }
11
+ .info .icon { color: var(--accent); } .success .icon { color: var(--success); } .warning .icon { color: var(--warning); } .danger .icon { color: var(--danger); }
12
+ .dismiss { display: grid; width: 28px; height: 28px; flex: 0 0 auto; place-items: center; margin: -5px -6px -5px 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-muted); cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
13
+ .dismiss:active { transform: scale(.96); transition-duration: var(--duration-instant); transition-timing-function: var(--ease-standard); }
14
+ @media (hover: hover) and (pointer: fine) { .dismiss:hover { background: var(--surface-muted); color: var(--foreground); } }
15
+ @media (prefers-reduced-motion: reduce) { .dismiss { transition: none; } .dismiss:active { transform: none; } }
@@ -0,0 +1,13 @@
1
+ export interface AnimatedCounterProps {
2
+ value: number;
3
+ label?: string;
4
+ prefix?: string;
5
+ suffix?: string;
6
+ decimals?: number;
7
+ /** Roll every digit up from zero the first time the counter scrolls into view. */
8
+ animateOnView?: boolean;
9
+ /** Formatting locale. Fixed by default so server and client render the same digits. */
10
+ locale?: string;
11
+ }
12
+ export declare function AnimatedCounter({ value, label, prefix, suffix, decimals, animateOnView, locale }: AnimatedCounterProps): import("react").JSX.Element;
13
+ export default AnimatedCounter;
@@ -0,0 +1,73 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { AnimatePresence, animate, motion, useInView, useMotionValue, useReducedMotion, useTransform } from "motion/react";
5
+ import { motionTokens } from "../../lib/motion-tokens.js";
6
+ import styles from "./animated-counter.module.css";
7
+ const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
8
+ const rise = { hidden: { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` }, shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } }, gone: { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } } };
9
+ const fade = { hidden: { opacity: 0, y: 0, filter: "blur(0px)" }, shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.instant } }, gone: { opacity: 0, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.instant } } };
10
+ const reveal = { ...motionTokens.spring.smooth, visualDuration: motionTokens.duration.considered };
11
+ /** Split a formatted number into columns keyed by place value, so 999 → 1,000 keeps the ones column the ones column. */
12
+ function partsFor(value, decimals, locale) {
13
+ const parts = new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals, numberingSystem: "latn" }).formatToParts(value);
14
+ let place = parts.reduce((count, part) => count + (part.type === "integer" ? part.value.length : 0), 0);
15
+ let fraction = 0;
16
+ let order = 0;
17
+ return parts.flatMap((part, index) => {
18
+ if (part.type === "integer")
19
+ return [...part.value].map(char => ({ key: `i${--place}`, digit: Number(char), order: order++ }));
20
+ if (part.type === "fraction")
21
+ return [...part.value].map(char => ({ key: `f${fraction++}`, digit: Number(char), order: order++ }));
22
+ return [{ key: part.type === "group" ? `g${place}` : part.type === "decimal" ? "d" : `${part.type}${index}`, text: part.value }];
23
+ });
24
+ }
25
+ /** One digit on the wheel. Its offset from the wheel position decides where it sits and how visible it is. */
26
+ function Glyph({ position, digit }) {
27
+ const offset = useTransform(position, current => ((((digit - current) % 10) + 15) % 10) - 5);
28
+ const y = useTransform(offset, current => `${current}em`);
29
+ const opacity = useTransform(offset, current => Math.max(0, 1 - Math.abs(current)));
30
+ const visibility = useTransform(offset, current => Math.abs(current) >= 1 ? "hidden" : "visible");
31
+ const filter = useTransform(offset, current => Math.abs(current) < .02 || Math.abs(current) >= 1 ? "none" : `blur(${(Math.abs(current) * motionTokens.blur.subtle).toFixed(2)}px)`);
32
+ return _jsx(motion.span, { className: styles.glyph, style: { y, opacity, filter, visibility }, children: digit });
33
+ }
34
+ const presence = { initial: { width: 0, opacity: 0 }, animate: { width: "auto", opacity: 1 }, exit: { width: 0, opacity: 0 } };
35
+ /** A digit wheel. It always turns in the direction the whole number moved, wrapping 9 → 0 like an odometer. */
36
+ function Column({ digit, direction, armed, delay, reduceMotion }) {
37
+ const position = useMotionValue(armed ? 0 : digit);
38
+ const wheel = useRef({ digit: armed ? 0 : digit, target: armed ? 0 : digit, revealed: !armed });
39
+ useEffect(() => {
40
+ const state = wheel.current;
41
+ if (armed || state.digit === digit) {
42
+ if (!armed)
43
+ state.revealed = true;
44
+ return;
45
+ }
46
+ state.target += direction < 0 && state.revealed ? -((state.digit - digit + 10) % 10) : (digit - state.digit + 10) % 10;
47
+ state.digit = digit;
48
+ if (reduceMotion)
49
+ position.jump(state.target);
50
+ else
51
+ animate(position, state.target, state.revealed ? motionTokens.spring.smooth : { ...reveal, delay });
52
+ state.revealed = true;
53
+ }, [armed, delay, digit, direction, position, reduceMotion]);
54
+ return _jsxs(motion.span, { className: styles.column, ...presence, transition: reduceMotion ? { duration: 0 } : motionTokens.spring.morph, children: [_jsx("span", { className: styles.sizer, children: "0" }), DIGITS.map(item => _jsx(Glyph, { position: position, digit: item }, item))] });
55
+ }
56
+ export function AnimatedCounter({ value, label, prefix = "", suffix = "", decimals = 0, animateOnView = false, locale = "en-US" }) {
57
+ const ref = useRef(null);
58
+ const inView = useInView(ref, { once: true, amount: .6 });
59
+ const reduceMotion = !!useReducedMotion();
60
+ const [previous, setPrevious] = useState(value);
61
+ const [direction, setDirection] = useState(1);
62
+ if (value !== previous) {
63
+ setPrevious(value);
64
+ setDirection(value > previous ? 1 : -1);
65
+ }
66
+ const parts = partsFor(value, decimals, locale);
67
+ const text = `${prefix}${parts.map(part => "text" in part ? part.text : part.digit).join("")}${suffix}`;
68
+ const armed = animateOnView && !inView;
69
+ return _jsxs("span", { ref: ref, className: styles.counter, children: [label && _jsx("span", { className: styles.label, children: _jsx("span", { className: styles.labelSwap, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(motion.span, { className: styles.labelText, variants: reduceMotion ? fade : rise, initial: "hidden", animate: "shown", exit: "gone", children: label }, label) }) }) }), _jsx("span", { className: styles.srOnly, children: text }), _jsxs("span", { className: styles.value, "aria-hidden": "true", children: [prefix && _jsx("span", { className: styles.symbol, children: prefix }), _jsx(AnimatePresence, { initial: false, children: parts.map(part => "digit" in part
70
+ ? _jsx(Column, { digit: part.digit, direction: direction, armed: armed, delay: Math.min(part.order * motionTokens.stagger.item, .25), reduceMotion: reduceMotion }, part.key)
71
+ : _jsx(motion.span, { className: styles.symbol, ...presence, transition: reduceMotion ? { duration: 0 } : motionTokens.spring.morph, children: part.text }, part.key)) }), suffix && _jsx("span", { className: styles.symbol, children: suffix })] })] });
72
+ }
73
+ export default AnimatedCounter;
@@ -0,0 +1,12 @@
1
+ .counter { position: relative; display: inline-flex; flex-direction: column; color: var(--foreground); font-family: var(--font-display); font-size: var(--text-3xl); font-weight: 500; letter-spacing: var(--tracking-display); line-height: 1; font-variant-numeric: tabular-nums; }
2
+ .label { margin-bottom: var(--space-related); color: var(--text-muted); font-family: var(--font-body); font-size: var(--text-xs); font-weight: 400; letter-spacing: var(--tracking-body); line-height: var(--leading-body); }
3
+ /* Each digit is a clipped wheel; separators clip sideways only so a comma keeps its tail. */
4
+ .value { display: inline-flex; align-items: flex-start; white-space: nowrap; user-select: none; }
5
+ /* The wheel's window reaches a little past the line box and feathers out, so a turning digit fades at the edge instead of being cut. */
6
+ .column { --feather: .16em; position: relative; display: inline-block; overflow: hidden; margin-block: calc(var(--feather) * -1); padding-block: var(--feather); mask-image: linear-gradient(to bottom, transparent, #000 calc(var(--feather) * 1.5), #000 calc(100% - var(--feather) * 1.5), transparent); }
7
+ .sizer { visibility: hidden; }
8
+ .glyph { position: absolute; inset: var(--feather) 0; text-align: center; }
9
+ .labelText { display: inline-block; }
10
+ .labelSwap { position: relative; display: block; }
11
+ .symbol { display: inline-block; overflow-x: clip; }
12
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@@ -0,0 +1,53 @@
1
+ import type { ReactNode } from "react";
2
+ export interface AnnouncementAction {
3
+ label: string;
4
+ href?: string;
5
+ onClick?: () => void;
6
+ }
7
+ export interface Announcement {
8
+ /** Stable id, reported to callbacks. */
9
+ id: string;
10
+ message: ReactNode;
11
+ action?: AnnouncementAction;
12
+ /** Counts down to a moment, for example the end of a sale. */
13
+ countdown?: {
14
+ to: Date | string | number;
15
+ label?: string;
16
+ };
17
+ }
18
+ /**
19
+ * A slim bar for the top of a page. It can rotate several messages, each rising in from below while the bar springs to
20
+ * the new height; rotation pauses on hover, focus, or a hidden tab, and `controls` adds previous, next, and a pause ring. Messages can carry a
21
+ * call to action and a live countdown. Dismissing collapses the height smoothly so the page below eases up, and with an
22
+ * `id` the dismissal is remembered in `localStorage`.
23
+ */
24
+ export interface AnnouncementBarProps {
25
+ messages: Announcement[];
26
+ /** Remembers dismissal under this id. Change the id to show a new campaign to everyone again. */
27
+ id?: string;
28
+ /** Whether the bar is shown. Leave it out to let the component manage it. */
29
+ open?: boolean;
30
+ defaultOpen?: boolean;
31
+ onOpenChange?: (open: boolean) => void;
32
+ /** Index of the visible message. */
33
+ index?: number;
34
+ defaultIndex?: number;
35
+ onIndexChange?: (index: number) => void;
36
+ /** Milliseconds each message stays before the next one. Defaults to 6000. */
37
+ interval?: number;
38
+ /** Rotate automatically. Pauses on hover, focus, or a hidden tab, and is turned off when the visitor prefers reduced motion. Defaults to true. */
39
+ autoPlay?: boolean;
40
+ /** Show previous, next, and pause controls when there are several messages. Defaults to false: only the close button shows. */
41
+ controls?: boolean;
42
+ dismissible?: boolean;
43
+ tone?: "neutral" | "inverted";
44
+ onAction?: (announcement: Announcement) => void;
45
+ onCountdownEnd?: (announcement: Announcement) => void;
46
+ /** Accessible name of the region. */
47
+ label?: string;
48
+ className?: string;
49
+ }
50
+ /** Forgets a remembered dismissal so the bar with this id shows again. */
51
+ export declare function clearAnnouncementDismissal(id: string): void;
52
+ export declare const AnnouncementBar: import("react").ForwardRefExoticComponent<AnnouncementBarProps & import("react").RefAttributes<HTMLElement>>;
53
+ export default AnnouncementBar;
@@ -0,0 +1,187 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { forwardRef, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
4
+ import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion, useTransform } from "motion/react";
5
+ import { ArrowRight, ChevronDown, ChevronUp, Pause, Play, X } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./announcement-bar.module.css";
8
+ const storageKey = (id) => `silicon-announcement:${id}`;
9
+ /** Forgets a remembered dismissal so the bar with this id shows again. */
10
+ export function clearAnnouncementDismissal(id) {
11
+ try {
12
+ window.localStorage.removeItem(storageKey(id));
13
+ }
14
+ catch { /* Storage can be blocked. */ }
15
+ }
16
+ const enter = [...motionTokens.ease.enter];
17
+ const standard = [...motionTokens.ease.standard];
18
+ const physical = (visualDuration, bounce) => {
19
+ const root = 2 * Math.PI / (visualDuration * 1.2);
20
+ return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };
21
+ };
22
+ const RISE = physical(.46, .1), HEIGHT = physical(.42, 0), COLLAPSE = physical(.44, 0);
23
+ /** Next rises from below and leaves upward; previous runs the other way. */
24
+ const faceVariants = {
25
+ hidden: (direction) => ({ opacity: 0, y: `${direction * 70}%`, filter: `blur(${motionTokens.blur.soft}px)` }),
26
+ shown: { opacity: 1, y: "0%", filter: "blur(0px)", transition: { y: RISE, opacity: { duration: motionTokens.duration.standard, ease: enter }, filter: { duration: motionTokens.duration.standard, ease: enter } } },
27
+ gone: (direction) => ({ opacity: 0, y: `${direction * -60}%`, filter: `blur(${motionTokens.blur.soft}px)`, transition: { y: RISE, opacity: { duration: motionTokens.duration.fast, ease: standard }, filter: { duration: motionTokens.duration.fast, ease: standard } } }),
28
+ };
29
+ const fadeVariants = { hidden: { opacity: 0 }, shown: { opacity: 1, transition: { duration: motionTokens.duration.standard, ease: standard } }, gone: { opacity: 0, transition: { duration: motionTokens.duration.instant, ease: standard } } };
30
+ function parts(ms) {
31
+ const total = Math.max(0, Math.floor(ms / 1000));
32
+ return { d: Math.floor(total / 86400), h: Math.floor(total % 86400 / 3600), m: Math.floor(total % 3600 / 60), s: total % 60 };
33
+ }
34
+ const pad = (value) => String(value).padStart(2, "0");
35
+ /**
36
+ * One character cell. A changed digit drops in from above while the old one falls away, like a counter winding down.
37
+ * The invisible sizer keeps the cell in the text flow, so it shares the baseline of the words around it.
38
+ */
39
+ function Digit({ char, reduced }) {
40
+ return _jsxs("span", { className: styles.digit, children: [_jsx("span", { className: styles.digitSizer, "aria-hidden": "true", children: "0" }), _jsx(AnimatePresence, { initial: false, children: _jsx(motion.span, { className: styles.digitFace, initial: reduced ? { opacity: 0 } : { y: "-70%", opacity: 0 }, animate: { y: "0%", opacity: 1 }, exit: reduced ? { opacity: 0 } : { y: "70%", opacity: 0 }, transition: reduced ? { duration: motionTokens.duration.instant } : { y: physical(.32, .08), opacity: { duration: motionTokens.duration.fast, ease: standard } }, children: char }, char) })] });
41
+ }
42
+ function Countdown({ to, label, reduced, onEnd }) {
43
+ const target = new Date(to).getTime();
44
+ const [now, setNow] = useState(null);
45
+ const ended = useRef(false);
46
+ const endRef = useRef(onEnd);
47
+ useEffect(() => { endRef.current = onEnd; }, [onEnd]);
48
+ useEffect(() => {
49
+ let timer = 0;
50
+ const tick = () => {
51
+ const current = Date.now();
52
+ setNow(current);
53
+ if (current >= target) {
54
+ if (!ended.current) {
55
+ ended.current = true;
56
+ endRef.current?.();
57
+ }
58
+ return;
59
+ }
60
+ // Wake just after each whole second so digits change on the beat.
61
+ timer = window.setTimeout(tick, 1000 - (current % 1000) + 8);
62
+ };
63
+ timer = window.setTimeout(tick, 0);
64
+ return () => window.clearTimeout(timer);
65
+ }, [target]);
66
+ const { d, h, m, s } = parts(now === null ? 0 : target - now);
67
+ const pending = now === null;
68
+ const spoken = pending ? "" : `${d ? `${d} days ` : ""}${h} hours ${m} minutes`;
69
+ const digits = (value, key) => value.split("").map((char, index) => _jsx(Digit, { char: pending ? "0" : char, reduced: reduced }, `${key}${value.length - index}`));
70
+ return _jsxs("span", { className: styles.countdown, children: [label ? _jsx("span", { className: styles.countdownLabel, children: label }) : null, _jsxs("span", { role: "timer", "aria-live": "off", "aria-label": spoken, className: styles.time, "data-pending": pending || undefined, children: [d ? _jsxs(_Fragment, { children: [_jsxs("span", { className: styles.unit, children: [digits(String(d), "d"), _jsx("span", { className: styles.unitMark, "aria-hidden": "true", children: "d" })] }), _jsx("span", { className: styles.gap, "aria-hidden": "true" })] }) : null, digits(pad(h), "h"), _jsx("span", { className: styles.sep, "aria-hidden": "true", children: ":" }), digits(pad(m), "m"), _jsx("span", { className: styles.sep, "aria-hidden": "true", children: ":" }), digits(pad(s), "s")] })] });
71
+ }
72
+ function Face({ announcement, direction, reduced, position, total, onSize, onAction, onCountdownEnd }) {
73
+ const ref = useRef(null);
74
+ const present = useIsPresent();
75
+ useLayoutEffect(() => {
76
+ const node = ref.current;
77
+ if (!node || !present)
78
+ return;
79
+ const report = () => onSize(node.offsetHeight);
80
+ report();
81
+ if (typeof ResizeObserver === "undefined")
82
+ return;
83
+ const observer = new ResizeObserver(report);
84
+ observer.observe(node);
85
+ return () => observer.disconnect();
86
+ }, [onSize, present]);
87
+ const { action, countdown } = announcement;
88
+ const cta = action ? action.href
89
+ ? _jsxs("a", { className: styles.cta, href: action.href, onClick: () => { action.onClick?.(); onAction?.(announcement); }, children: [action.label, _jsx(ArrowRight, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] })
90
+ : _jsxs("button", { type: "button", className: styles.cta, onClick: () => { action.onClick?.(); onAction?.(announcement); }, children: [action.label, _jsx(ArrowRight, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }) : null;
91
+ return _jsx(motion.div, { ref: ref, className: styles.face, custom: direction, variants: reduced ? fadeVariants : faceVariants, initial: "hidden", animate: "shown", exit: "gone", role: total > 1 ? "group" : undefined, "aria-roledescription": total > 1 ? "slide" : undefined, "aria-label": total > 1 ? `${position} of ${total}` : undefined, inert: !present || undefined, children: _jsxs("p", { className: styles.message, children: [_jsx("span", { className: styles.text, children: announcement.message }), countdown ? _jsx(Countdown, { to: countdown.to, label: countdown.label, reduced: reduced, onEnd: () => onCountdownEnd?.(announcement) }) : null, cta] }) });
92
+ }
93
+ const RING = 2 * Math.PI * 8;
94
+ export const AnnouncementBar = forwardRef(function AnnouncementBar({ messages, id, open: openProp, defaultOpen = true, onOpenChange, index: indexProp, defaultIndex = 0, onIndexChange, interval = 6000, autoPlay = true, controls = false, dismissible = true, tone = "neutral", onAction, onCountdownEnd, label = "Announcements", className, }, ref) {
95
+ const reduced = !!useReducedMotion();
96
+ const uid = useId();
97
+ const total = messages.length;
98
+ /* Open state: controlled, or internal and hidden at once when this id was dismissed before. */
99
+ const [openInternal, setOpenInternal] = useState(defaultOpen);
100
+ const [remembered, setRemembered] = useState(false);
101
+ useLayoutEffect(() => {
102
+ if (!id || openProp !== undefined)
103
+ return;
104
+ let stored = false;
105
+ try {
106
+ stored = window.localStorage.getItem(storageKey(id)) === "dismissed";
107
+ }
108
+ catch { /* Storage can be blocked. */ }
109
+ // Storage is only readable on the client; hide before paint so a dismissed bar never flashes.
110
+ // eslint-disable-next-line react-hooks/set-state-in-effect
111
+ if (stored)
112
+ setRemembered(true);
113
+ }, [id, openProp]);
114
+ const open = openProp ?? (openInternal && !remembered);
115
+ const dismiss = () => {
116
+ if (id) {
117
+ try {
118
+ window.localStorage.setItem(storageKey(id), "dismissed");
119
+ }
120
+ catch { /* Storage can be blocked. */ }
121
+ }
122
+ if (openProp === undefined)
123
+ setOpenInternal(false);
124
+ onOpenChange?.(false);
125
+ };
126
+ /* Rotation. */
127
+ const [indexInternal, setIndexInternal] = useState(defaultIndex);
128
+ const current = total ? ((indexProp ?? indexInternal) % total + total) % total : 0;
129
+ const [direction, setDirection] = useState(1);
130
+ const go = useCallback((step) => {
131
+ if (total < 2)
132
+ return;
133
+ const next = ((current + step) % total + total) % total;
134
+ setDirection(step > 0 ? 1 : -1);
135
+ if (indexProp === undefined)
136
+ setIndexInternal(next);
137
+ onIndexChange?.(next);
138
+ }, [current, indexProp, onIndexChange, total]);
139
+ const [userPaused, setUserPaused] = useState(false);
140
+ const [hovered, setHovered] = useState(false);
141
+ const [focused, setFocused] = useState(false);
142
+ const [hidden, setHidden] = useState(false);
143
+ useEffect(() => {
144
+ const read = () => setHidden(document.visibilityState === "hidden");
145
+ document.addEventListener("visibilitychange", read);
146
+ return () => document.removeEventListener("visibilitychange", read);
147
+ }, []);
148
+ const rotating = autoPlay && !reduced && total > 1;
149
+ const running = rotating && open && !userPaused && !hovered && !focused && !hidden;
150
+ /* The ring is the clock: its progress runs to one, then advances. Pausing stops it where it is; resuming finishes the rest. */
151
+ const progress = useMotionValue(0);
152
+ const dash = useTransform(progress, value => RING * (1 - value));
153
+ const goRef = useRef(go);
154
+ useEffect(() => { goRef.current = go; }, [go]);
155
+ useEffect(() => { progress.jump(0); }, [current, progress]);
156
+ useEffect(() => {
157
+ if (!running)
158
+ return;
159
+ let controls = animate(progress, 1, {
160
+ duration: interval / 1000 * (1 - progress.get()), ease: "linear",
161
+ onComplete: () => { controls = null; goRef.current(1); },
162
+ });
163
+ return () => controls?.stop();
164
+ }, [running, current, interval, progress]);
165
+ /* The viewport springs to the height of the current message, which can wrap on narrow screens. */
166
+ const height = useMotionValue("auto");
167
+ const measured = useRef(0);
168
+ const onSize = useCallback((next) => {
169
+ if (Math.abs(next - measured.current) < .5)
170
+ return;
171
+ const first = measured.current === 0;
172
+ measured.current = next;
173
+ if (first || reduced)
174
+ height.jump(next);
175
+ else
176
+ animate(height, next, HEIGHT);
177
+ }, [height, reduced]);
178
+ const announcement = messages[current];
179
+ if (!announcement)
180
+ return null;
181
+ const navigable = controls && total > 1;
182
+ const controlCount = (navigable ? 2 + (rotating ? 1 : 0) : 0) + (dismissible ? 1 : 0);
183
+ return _jsx(AnimatePresence, { initial: false, children: open ? _jsx(motion.section, { ref: ref, className: [styles.collapse, className].filter(Boolean).join(" "), "aria-label": label, "aria-roledescription": total > 1 ? "carousel" : undefined, initial: reduced ? { opacity: 0 } : { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: reduced ? { opacity: 0, transition: { duration: motionTokens.duration.fast } } : { height: 0, opacity: 0, transition: { height: COLLAPSE, opacity: { duration: motionTokens.duration.exit, ease: standard } } }, transition: reduced ? { duration: motionTokens.duration.fast } : { height: COLLAPSE, opacity: { duration: motionTokens.duration.standard, ease: enter } }, onPointerEnter: event => { if (event.pointerType === "mouse")
184
+ setHovered(true); }, onPointerLeave: () => setHovered(false), onFocus: () => setFocused(true), onBlur: event => { if (!event.currentTarget.contains(event.relatedTarget))
185
+ setFocused(false); }, children: _jsxs("div", { className: styles.bar, "data-tone": tone, style: { "--controls": controlCount }, children: [_jsx("div", { className: styles.side, "aria-hidden": "true" }), _jsx(motion.div, { className: styles.viewport, style: { height }, id: `${uid}-slides`, "aria-live": running ? "off" : "polite", "aria-atomic": "false", children: _jsx(AnimatePresence, { initial: false, custom: direction, children: _jsx(Face, { announcement: announcement, direction: direction, reduced: reduced, position: current + 1, total: total, onSize: onSize, onAction: onAction, onCountdownEnd: onCountdownEnd }, announcement.id) }) }), _jsxs("div", { className: styles.controls, children: [navigable ? _jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: styles.icon, "aria-label": "Previous announcement", "aria-controls": `${uid}-slides`, onClick: () => go(-1), children: _jsx(ChevronUp, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), rotating ? _jsxs("button", { type: "button", className: styles.ring, "aria-label": userPaused ? "Resume announcements" : "Pause announcements", "aria-pressed": userPaused, onClick: () => setUserPaused(paused => !paused), children: [_jsxs("svg", { viewBox: "0 0 20 20", "aria-hidden": "true", children: [_jsx("circle", { cx: "10", cy: "10", r: "8", className: styles.ringTrack }), _jsx(motion.circle, { cx: "10", cy: "10", r: "8", className: styles.ringFill, strokeDasharray: RING, style: { strokeDashoffset: dash } })] }), _jsx("span", { className: styles.ringIcon, children: userPaused ? _jsx(Play, { size: 9, strokeWidth: 2.4, "aria-hidden": "true" }) : _jsx(Pause, { size: 9, strokeWidth: 2.4, "aria-hidden": "true" }) })] }) : null, _jsx("button", { type: "button", className: styles.icon, "aria-label": "Next announcement", "aria-controls": `${uid}-slides`, onClick: () => go(1), children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }) : null, dismissible ? _jsx("button", { type: "button", className: styles.icon, "aria-label": "Dismiss", onClick: dismiss, children: _jsx(X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) : null] })] }) }, "bar") : null });
186
+ });
187
+ export default AnnouncementBar;
@@ -0,0 +1,60 @@
1
+ /* The outer section animates its height to zero on dismiss, so whatever sits below eases up with it. */
2
+ .collapse { overflow: hidden; container-type: inline-size; font-family: var(--font-body); letter-spacing: var(--tracking-body); }
3
+
4
+ /* The side column mirrors the controls, so the message stays centered on the page rather than in the leftover space. */
5
+ .bar { --icon: 32px; --control-gap: 2px; --muted: var(--text-secondary); display: grid; grid-template-columns: calc(var(--controls, 0) * var(--icon) + max(var(--controls, 0) - 1, 0) * var(--control-gap)) minmax(0, 1fr) auto; align-items: center; gap: var(--space-group); padding: 6px 8px 6px 16px; border-bottom: 1px solid var(--border); background: var(--surface-muted); color: var(--foreground); }
6
+ .bar[data-tone="inverted"] { --muted: color-mix(in oklab, var(--background) 66%, transparent); border-bottom-color: transparent; background: var(--foreground); color: var(--background); }
7
+
8
+ .side { min-width: 0; }
9
+ .viewport { position: relative; min-height: 32px; overflow: hidden; }
10
+
11
+ /* Faces overlap in place; the entering one reports its height and the viewport springs to it. */
12
+ .face { position: absolute; top: 0; right: 0; left: 0; display: flex; min-height: 32px; align-items: center; justify-content: center; }
13
+ .message { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: center; gap: 2px 12px; margin: 0; padding: 6px 0; font-size: var(--text-sm); line-height: var(--leading-body); text-align: center; text-wrap: balance; }
14
+ .text { min-width: 0; }
15
+
16
+ /* The countdown is one run of text: every cell shares the line box, so digits, colons, and the day mark sit on the message baseline. */
17
+ .countdown { display: inline-flex; align-items: baseline; gap: 6px; color: var(--muted); white-space: nowrap; }
18
+ .countdownLabel { color: inherit; }
19
+ .time { display: inline-flex; align-items: baseline; color: var(--foreground); font-variant-numeric: tabular-nums; font-weight: 500; line-height: inherit; }
20
+ .time[data-pending] { opacity: 0; }
21
+ .bar[data-tone="inverted"] .time { color: var(--background); }
22
+ .digit { position: relative; display: inline-block; overflow: hidden; overflow: clip; text-align: center; }
23
+ .digitSizer { visibility: hidden; }
24
+ .digitFace { position: absolute; inset: 0; display: block; }
25
+ .unit { display: inline-flex; align-items: baseline; }
26
+ .unitMark { margin-left: 1px; color: var(--muted); font-weight: 400; }
27
+ /* "case" lifts the colon to the middle of the figures, which it loses once the digits are split into cells. */
28
+ .sep { display: inline-block; padding-inline: 1px; color: var(--muted); font-feature-settings: "case"; font-weight: 400; }
29
+ .gap { display: inline-block; width: .4em; }
30
+
31
+ .cta { display: inline-flex; align-items: center; gap: 4px; padding: 0; border: 0; background: none; color: inherit; cursor: pointer; font: inherit; font-weight: 500; text-decoration: none; white-space: nowrap; -webkit-tap-highlight-color: transparent; }
32
+ .cta svg { transition: transform var(--duration-fast) var(--ease-standard); }
33
+
34
+ .controls { display: flex; align-items: center; gap: var(--control-gap); }
35
+ .icon, .ring { position: relative; display: grid; width: var(--icon); height: var(--icon); flex: none; place-items: center; padding: 0; border: 0; border-radius: 50%; corner-shape: round; background: transparent; color: var(--muted); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard); }
36
+ .icon:active, .ring:active { background: color-mix(in oklab, currentColor 12%, transparent); color: inherit; }
37
+ .ring svg { width: 20px; height: 20px; overflow: visible; transform: rotate(-90deg); }
38
+ .ringTrack { fill: none; stroke: currentColor; stroke-opacity: .22; stroke-width: 1.75; }
39
+ .ringFill { fill: none; stroke: currentColor; stroke-linecap: round; stroke-width: 1.75; }
40
+ .ringIcon { position: absolute; inset: 0; display: grid; place-items: center; }
41
+ .ringIcon svg { width: 9px; height: 9px; transform: none; fill: currentColor; }
42
+ .bar[data-tone="inverted"] .icon, .bar[data-tone="inverted"] .ring { color: var(--muted); }
43
+
44
+ @media (hover: hover) and (pointer: fine) {
45
+ .icon:hover, .ring:hover { background: color-mix(in oklab, currentColor 10%, transparent); }
46
+ .bar .icon:hover, .bar .ring:hover { color: var(--foreground); }
47
+ .bar[data-tone="inverted"] .icon:hover, .bar[data-tone="inverted"] .ring:hover { color: var(--background); }
48
+ .cta:hover svg { transform: translateX(2px); }
49
+ }
50
+
51
+ /* Narrow bars drop the mirror column and read from the start edge. */
52
+ @container (max-width: 560px) {
53
+ .bar { grid-template-columns: 0 minmax(0, 1fr) auto; gap: 0 var(--space-related); padding-left: 14px; }
54
+ .face { justify-content: flex-start; }
55
+ .message { justify-content: flex-start; column-gap: 10px; text-align: left; }
56
+ }
57
+
58
+ @media (prefers-reduced-motion: reduce) {
59
+ .icon, .ring, .cta svg { transition: none; }
60
+ }
@@ -0,0 +1,8 @@
1
+ import { type HTMLAttributes } from "react";
2
+ export interface AvatarProps extends HTMLAttributes<HTMLSpanElement> {
3
+ name: string;
4
+ src?: string;
5
+ size?: "sm" | "md" | "lg" | "xl";
6
+ status?: "online" | "offline";
7
+ }
8
+ export declare function Avatar({ name, src, size, status, className, ...props }: AvatarProps): import("react").JSX.Element;