@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,296 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { AnimatePresence, animate, cancelFrame, frame, motion, motionValue, useInView, useMotionValue, useReducedMotion, useTransform } from "motion/react";
5
+ import { CircleAlert, TriangleAlert } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./usage-meter.module.css";
8
+ const { spring, duration, ease, blur } = motionTokens;
9
+ /** Motion drops inherited velocity on time-defined springs, so values that retarget mid-flight run the same springs written as stiffness and damping. */
10
+ const physical = ({ visualDuration, bounce }, restDelta = .001) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta, restSpeed: restDelta * 2 }; };
11
+ const settle = physical(spring.smooth);
12
+ const reveal = physical({ visualDuration: duration.considered + .1, bounce: 0 });
13
+ const turn = physical(spring.smooth);
14
+ const fit = physical(spring.morph, .01);
15
+ /** A segment stops two pixels short of the next, so neighbours read apart without an outline. */
16
+ const GAP = 2;
17
+ const subscribeNothing = () => () => { };
18
+ /** Reduced motion is only known in the browser, so the first client render matches the server before it takes effect. */
19
+ function useReducedMotionSafe() {
20
+ const hydrated = useSyncExternalStore(subscribeNothing, () => true, () => false);
21
+ return { reduced: !!useReducedMotion() && hydrated, hydrated };
22
+ }
23
+ /** Starts animations on the next frame, so the render that caused them never swallows a spring's first frames. Returns a cleanup. */
24
+ function soon(start) {
25
+ let controls = null;
26
+ const run = () => { controls = start(); };
27
+ frame.update(run);
28
+ return () => { cancelFrame(run); controls?.stop(); };
29
+ }
30
+ /** Changed text rises from a soft blur and leaves a little faster than it arrives. Icons cross through a small scale instead. */
31
+ const rise = {
32
+ hidden: (direction) => ({ opacity: 0, y: `${.3 * direction}em`, scale: 1, filter: `blur(${blur.soft}px)` }),
33
+ shown: { opacity: 1, y: 0, scale: 1, filter: "blur(0px)", transition: { duration: duration.standard, ease: [...ease.enter] } },
34
+ gone: (direction) => ({ opacity: 0, y: `${-.3 * direction}em`, scale: 1, filter: `blur(${blur.subtle}px)`, transition: { duration: duration.instant, ease: [...ease.standard] } }),
35
+ };
36
+ const pop = {
37
+ hidden: { opacity: 0, y: 0, scale: .6, filter: `blur(${blur.subtle + 1}px)` },
38
+ shown: { opacity: 1, y: 0, scale: 1, filter: "blur(0px)", transition: { ...spring.snappy, opacity: { duration: duration.fast, ease: [...ease.enter] }, filter: { duration: duration.fast, ease: [...ease.enter] } } },
39
+ gone: { opacity: 0, y: 0, scale: .6, filter: `blur(${blur.subtle + 1}px)`, transition: { duration: duration.instant, ease: [...ease.standard] } },
40
+ };
41
+ const fade = { hidden: { opacity: 0, y: 0, scale: 1, filter: "blur(0px)" }, shown: { opacity: 1, y: 0, scale: 1, filter: "blur(0px)", transition: { duration: duration.fast } }, gone: { opacity: 0, y: 0, scale: 1, filter: "blur(0px)", transition: { duration: duration.instant } } };
42
+ function Swap({ text, reduced, direction = 1 }) {
43
+ return _jsx("span", { className: styles.swap, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, custom: direction, children: _jsx(motion.span, { className: styles.swapText, custom: direction, variants: reduced ? fade : rise, initial: "hidden", animate: "shown", exit: "gone", children: text }, text) }) });
44
+ }
45
+ const formats = new Map();
46
+ /** Split a number into columns keyed by place value, so 9.8 → 10.4 keeps the ones column the ones column and a new tens column slides in. */
47
+ function partsFor(value, decimals) {
48
+ let format = formats.get(decimals);
49
+ if (!format) {
50
+ format = new Intl.NumberFormat("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
51
+ formats.set(decimals, format);
52
+ }
53
+ const parts = format.formatToParts(value);
54
+ let place = parts.reduce((count, part) => count + (part.type === "integer" ? part.value.length : 0), 0), fraction = 0;
55
+ return parts.flatMap((part, index) => {
56
+ if (part.type === "integer")
57
+ return [...part.value].map(char => ({ key: `i${--place}`, digit: Number(char) }));
58
+ if (part.type === "fraction")
59
+ return [...part.value].map(char => ({ key: `f${fraction++}`, digit: Number(char) }));
60
+ return [{ key: part.type === "group" ? `g${place}` : part.type === "decimal" ? "d" : `${part.type}${index}`, text: part.value }];
61
+ });
62
+ }
63
+ const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
64
+ /** One digit on a wheel; its distance from the wheel position sets where it sits and how clearly it shows. */
65
+ function Glyph({ position, digit }) {
66
+ const offset = useTransform(position, current => ((((digit - current) % 10) + 15) % 10) - 5);
67
+ const y = useTransform(offset, current => `${current}em`);
68
+ const opacity = useTransform(offset, current => Math.max(0, 1 - Math.abs(current)));
69
+ const filter = useTransform(offset, current => Math.abs(current) < .02 || Math.abs(current) >= 1 ? "none" : `blur(${(Math.abs(current) * blur.subtle).toFixed(2)}px)`);
70
+ return _jsx(motion.span, { className: styles.glyph, style: { y, opacity, filter }, children: digit });
71
+ }
72
+ const column = { initial: { width: 0, opacity: 0 }, animate: { width: "auto", opacity: 1 }, exit: { width: 0, opacity: 0 } };
73
+ /** A digit wheel that always turns the way the whole number moved, wrapping 9 → 0 like an odometer. */
74
+ function Column({ digit, direction, reduced }) {
75
+ const position = useMotionValue(digit);
76
+ const wheel = useRef({ digit, target: digit });
77
+ const running = useRef(null);
78
+ useEffect(() => () => running.current?.(), []);
79
+ useEffect(() => {
80
+ const state = wheel.current;
81
+ if (state.digit === digit)
82
+ return;
83
+ state.target += direction < 0 ? -((state.digit - digit + 10) % 10) : (digit - state.digit + 10) % 10;
84
+ state.digit = digit;
85
+ running.current?.();
86
+ if (reduced) {
87
+ position.jump(state.target);
88
+ running.current = null;
89
+ return;
90
+ }
91
+ const target = state.target;
92
+ running.current = soon(() => animate(position, target, turn));
93
+ }, [digit, direction, position, reduced]);
94
+ return _jsxs(motion.span, { className: styles.column, ...column, transition: reduced ? { duration: 0 } : fit, children: [_jsx("span", { className: styles.sizer, children: "0" }), DIGITS.map(item => _jsx(Glyph, { position: position, digit: item }, item))] });
95
+ }
96
+ /** A number that rolls to each new value. It is decoration over text that screen readers get elsewhere. */
97
+ function Ticker({ value, decimals, reduced }) {
98
+ const [trend, setTrend] = useState({ value, direction: 1 });
99
+ if (trend.value !== value)
100
+ setTrend({ value, direction: value > trend.value ? 1 : -1 });
101
+ return _jsx("span", { className: styles.ticker, "aria-hidden": "true", children: _jsx(AnimatePresence, { initial: false, children: partsFor(value, decimals).map(part => "digit" in part
102
+ ? _jsx(Column, { digit: part.digit, direction: trend.direction, reduced: reduced }, part.key)
103
+ : _jsx(motion.span, { className: styles.symbol, ...column, transition: reduced ? { duration: 0 } : fit, children: part.text }, part.key)) }) });
104
+ }
105
+ /** The plan status as a compact mark. Its width springs to each new message and the warning icon grows in beside the words. */
106
+ function StatusBadge({ status, text, reduced }) {
107
+ const inner = useRef(null);
108
+ const width = useMotionValue("auto");
109
+ useLayoutEffect(() => {
110
+ const node = inner.current;
111
+ if (!node || typeof ResizeObserver === "undefined")
112
+ return;
113
+ let measured = false;
114
+ const observer = new ResizeObserver(() => {
115
+ const next = node.offsetWidth;
116
+ if (measured && !reduced)
117
+ animate(width, next, fit);
118
+ else
119
+ width.jump(next);
120
+ measured = next > 0;
121
+ });
122
+ observer.observe(node);
123
+ return () => observer.disconnect();
124
+ }, [reduced, width]);
125
+ const Icon = status === "over" ? TriangleAlert : CircleAlert;
126
+ return _jsx(motion.span, { className: styles.badge, "data-status": status, style: { width }, children: _jsxs("span", { ref: inner, className: styles.badgeInner, children: [_jsx(AnimatePresence, { mode: "popLayout", initial: false, children: status !== "ok" && _jsx(motion.span, { className: styles.badgeIcon, variants: reduced ? fade : pop, initial: "hidden", animate: "shown", exit: "gone", children: _jsx(Icon, { size: 14, strokeWidth: 2, "aria-hidden": "true" }) }, status) }), _jsx(Swap, { text: text, reduced: reduced })] }) });
127
+ }
128
+ function Segment({ index, amounts, span, width, highlight }) {
129
+ // Each segment starts where the ones before it end, so growth in any of them pushes the rest along in the same frame.
130
+ const x = useTransform(() => { let start = 0; for (let at = 0; at < index; at++)
131
+ start += amounts[at].get(); return (start / span.get()) * width.get(); });
132
+ const scaleX = useTransform(() => {
133
+ const w = width.get();
134
+ if (!w)
135
+ return 0;
136
+ const size = (amounts[index].get() / span.get()) * w, rest = w - x.get() - size;
137
+ return Math.max(0, size - Math.min(GAP, size, Math.max(0, rest))) / w;
138
+ });
139
+ return _jsx(motion.span, { className: styles.segment, "data-index": index, "data-highlight": highlight, style: { x, scaleX, originX: 0 } });
140
+ }
141
+ export function UsageMeter({ label, segments, limit, unit = "", decimals = 1, freeLabel = "Free", overLabel = "Over limit", warnAt = .9 }) {
142
+ const { reduced, hydrated } = useReducedMotionSafe();
143
+ const root = useRef(null);
144
+ const trackRef = useRef(null);
145
+ const inView = useInView(root, { once: true, amount: .4 });
146
+ const titleId = useId();
147
+ const hatchId = `hatch${titleId.replace(/[^a-zA-Z0-9_-]/g, "")}`;
148
+ const ready = hydrated && (inView || reduced);
149
+ const round = (value) => Math.round(value * 10 ** decimals) / 10 ** decimals;
150
+ const total = round(segments.reduce((sum, segment) => sum + segment.value, 0));
151
+ const free = round(Math.max(0, limit - total)), over = round(Math.max(0, total - limit));
152
+ const status = total > limit ? "over" : total >= limit * warnAt ? "near" : "ok";
153
+ const format = (value) => `${value.toFixed(decimals)}${unit ? ` ${unit}` : ""}`;
154
+ const limitText = format(limit).replace(/\.0+(?= |$)/, "");
155
+ // One motion value per segment id, kept across renders so a changed value springs from where it is.
156
+ const [store] = useState(() => new Map());
157
+ const amounts = segments.map(segment => { let value = store.get(segment.id); if (!value) {
158
+ value = motionValue(0);
159
+ store.set(segment.id, value);
160
+ } return value; });
161
+ const shownLimit = useMotionValue(limit);
162
+ const width = useMotionValue(0);
163
+ const used = useTransform(() => amounts.reduce((sum, amount) => sum + amount.get(), 0));
164
+ // The bar spans the limit, or everything used once that is more; both follow their springs, so crossing the limit is continuous.
165
+ const span = useTransform(() => Math.max(shownLimit.get(), used.get(), 1e-6));
166
+ const limitX = useTransform(() => Math.round((shownLimit.get() / span.get()) * width.get()));
167
+ // The limit marker and the hatch fade on their own short tween whenever usage crosses the limit, so a limit that jumps past usage never blinks them out in one frame.
168
+ const markerOpacity = useMotionValue(0);
169
+ useEffect(() => {
170
+ let over = used.get() > shownLimit.get();
171
+ markerOpacity.jump(over ? 1 : 0);
172
+ let fading;
173
+ const check = () => {
174
+ const next = used.get() > shownLimit.get();
175
+ if (next === over)
176
+ return;
177
+ over = next;
178
+ fading?.stop();
179
+ if (reduced)
180
+ markerOpacity.jump(next ? 1 : 0);
181
+ else
182
+ fading = animate(markerOpacity, next ? 1 : 0, { duration: duration.fast, ease: [...ease.standard] });
183
+ };
184
+ const stops = [used.on("change", check), shownLimit.on("change", check)];
185
+ return () => { stops.forEach(stop => stop()); fading?.stop(); };
186
+ }, [markerOpacity, reduced, shownLimit, used]);
187
+ const overClip = useTransform(() => `inset(0 0 0 ${limitX.get() + GAP}px)`);
188
+ useLayoutEffect(() => {
189
+ const node = trackRef.current;
190
+ if (!node)
191
+ return;
192
+ width.set(node.clientWidth);
193
+ if (typeof ResizeObserver === "undefined")
194
+ return;
195
+ const observer = new ResizeObserver(() => width.set(node.clientWidth));
196
+ observer.observe(node);
197
+ return () => observer.disconnect();
198
+ }, [width]);
199
+ // Segments grow into place once seen, left to right; later changes re-flow every segment on one spring each.
200
+ const values = segments.map(segment => `${segment.id}:${segment.value}`).join("|");
201
+ const revealed = useRef(false);
202
+ useEffect(() => {
203
+ if (!ready)
204
+ return;
205
+ const first = !revealed.current;
206
+ revealed.current = true;
207
+ const stops = segments.map((segment, index) => {
208
+ const amount = store.get(segment.id);
209
+ if (!amount || amount.get() === segment.value)
210
+ return null;
211
+ if (reduced) {
212
+ amount.jump(segment.value);
213
+ return null;
214
+ }
215
+ return soon(() => animate(amount, segment.value, first ? { ...reveal, delay: index * .07 } : settle));
216
+ });
217
+ return () => stops.forEach(stop => stop?.());
218
+ // `values` carries every segment id and value.
219
+ // eslint-disable-next-line react-hooks/exhaustive-deps
220
+ }, [values, ready, reduced, store]);
221
+ useEffect(() => {
222
+ if (shownLimit.get() === limit)
223
+ return;
224
+ if (reduced) {
225
+ shownLimit.jump(limit);
226
+ return;
227
+ }
228
+ return soon(() => animate(shownLimit, limit, settle));
229
+ }, [limit, reduced, shownLimit]);
230
+ // Hover previews a category, focus previews it too, and a press pins it until pressed again or Escape.
231
+ const [hovered, setHovered] = useState(null);
232
+ const [focused, setFocused] = useState(null);
233
+ const [pinned, setPinned] = useState(null);
234
+ const [roving, setRoving] = useState(0);
235
+ const items = [...segments.map(segment => ({ id: segment.id, label: segment.label, value: segment.value })), { id: "free", label: status === "over" ? overLabel : freeLabel, value: status === "over" ? over : free }];
236
+ const active = hovered ?? focused ?? pinned;
237
+ const activeItem = items.find(item => item.id === active) ?? null;
238
+ const legend = useRef(null);
239
+ const idAt = (clientX) => {
240
+ const rect = trackRef.current?.getBoundingClientRect();
241
+ if (!rect?.width)
242
+ return null;
243
+ const at = ((clientX - rect.left) / rect.width) * Math.max(limit, total);
244
+ // Past the limit the hatch stands for the overage, so pointing there highlights it rather than the category beneath.
245
+ if (at > limit)
246
+ return "free";
247
+ let end = 0;
248
+ for (const segment of segments) {
249
+ end += segment.value;
250
+ if (at <= end)
251
+ return segment.id;
252
+ }
253
+ return "free";
254
+ };
255
+ const onTrackMove = (event) => { if (event.pointerType === "mouse")
256
+ setHovered(idAt(event.clientX)); };
257
+ const onTrackTap = (event) => { if (event.pointerType !== "mouse") {
258
+ const id = idAt(event.clientX);
259
+ setPinned(current => current === id ? null : id);
260
+ } };
261
+ const onLegendKey = (event) => {
262
+ if (event.key === "Escape") {
263
+ if (pinned || focused) {
264
+ event.preventDefault();
265
+ setPinned(null);
266
+ }
267
+ return;
268
+ }
269
+ const next = { ArrowRight: roving + 1, ArrowDown: roving + 1, ArrowLeft: roving - 1, ArrowUp: roving - 1, Home: 0, End: items.length - 1 }[event.key];
270
+ if (next === undefined)
271
+ return;
272
+ event.preventDefault();
273
+ const index = (next + items.length) % items.length;
274
+ setRoving(index);
275
+ legend.current?.querySelectorAll("button")[index]?.focus();
276
+ };
277
+ const headline = ready ? (activeItem ? activeItem.value : total) : 0;
278
+ const share = activeItem ? Math.round((activeItem.value / limit) * 100) : 0;
279
+ const caption = !activeItem ? `${unit} of ${limitText} used` : activeItem.id === "free" ? `${unit} ${status === "over" ? "over the limit" : "free"}` : `${unit} in ${activeItem.label} · ${share}% of plan`;
280
+ const badge = status === "over" ? `${format(over)} over` : status === "near" ? "Almost full" : `${format(free)} free`;
281
+ const summary = `${label}: ${format(total)} of ${limitText} used. ${segments.map(segment => `${segment.label} ${format(segment.value)}`).join(", ")}. ${status === "over" ? `Over the limit by ${format(over)}.` : `${format(free)} free${status === "near" ? ", almost full" : ""}.`}`;
282
+ // Status changes are announced, and so is a growing overage; routine changes inside a status stay quiet.
283
+ const notice = status === "over" ? `over:${over}` : status;
284
+ const [announced, setAnnounced] = useState({ notice, message: "" });
285
+ if (announced.notice !== notice)
286
+ setAnnounced({ notice, message: status === "over" ? `Over the plan limit by ${format(over)}.` : status === "near" ? `Almost full. ${format(free)} free.` : `Under the limit. ${format(free)} free.` });
287
+ return _jsxs("div", { ref: root, className: styles.meter, role: "group", "aria-labelledby": titleId, "data-status": status, children: [_jsxs("div", { className: styles.top, children: [_jsx("span", { id: titleId, className: styles.title, children: label }), _jsx(StatusBadge, { status: status, text: badge, reduced: reduced })] }), _jsxs("p", { className: styles.total, children: [_jsxs("span", { className: styles.srOnly, children: [format(total), " of ", limitText, " used"] }), _jsx("span", { className: styles.amount, "aria-hidden": "true", children: _jsx(Ticker, { value: headline, decimals: decimals, reduced: reduced }) }), _jsx("span", { className: styles.caption, "aria-hidden": "true", children: _jsx(Swap, { text: caption, reduced: reduced }) })] }), _jsxs("div", { className: styles.bar, children: [_jsxs("div", { ref: trackRef, className: styles.track, role: "img", "aria-label": summary, "data-active": active ?? undefined, onPointerMove: onTrackMove, onPointerLeave: () => setHovered(null), onPointerUp: onTrackTap, children: [segments.map((segment, index) => _jsx(Segment, { index: index, amounts: amounts, span: span, width: width, highlight: active ? active === segment.id ? "on" : "off" : undefined }, segment.id)), _jsxs(motion.svg, { className: styles.overflow, "data-highlight": active ? active === "free" ? "on" : "off" : undefined, style: { clipPath: overClip, opacity: markerOpacity }, "aria-hidden": "true", focusable: "false", children: [_jsx("defs", { children: _jsx("pattern", { id: hatchId, width: 6, height: 6, patternUnits: "userSpaceOnUse", patternTransform: "rotate(45)", children: _jsx("rect", { width: 2.5, height: 6, className: styles.hatch }) }) }), _jsx("rect", { className: styles.overTint, width: "100%", height: "100%" }), _jsx("rect", { width: "100%", height: "100%", fill: `url(#${hatchId})` })] })] }), _jsx(motion.span, { className: styles.limit, style: { x: limitX, opacity: markerOpacity }, "aria-hidden": "true" })] }), _jsx("div", { ref: legend, className: styles.legend, role: "group", "aria-label": `${label} by category`, onKeyDown: onLegendKey, children: items.map((item, index) => {
288
+ const warning = item.id === "free" && status === "over";
289
+ return _jsxs("button", { type: "button", className: styles.item, "data-highlight": active ? active === item.id ? "on" : "off" : undefined, "aria-pressed": pinned === item.id, "aria-label": `${item.label}, ${format(item.value)}`, tabIndex: index === roving ? 0 : -1, onClick: () => { setRoving(index); setPinned(current => current === item.id ? null : item.id); }, onFocus: () => { setRoving(index); setFocused(item.id); }, onBlur: () => setFocused(null), onPointerEnter: event => { if (event.pointerType === "mouse")
290
+ setHovered(item.id); }, onPointerLeave: event => { if (event.pointerType === "mouse")
291
+ setHovered(null); }, children: [_jsxs("span", { className: styles.itemHead, children: [_jsx("span", { className: styles.key, "aria-hidden": "true", children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: warning
292
+ ? _jsx(motion.span, { className: styles.keyIcon, variants: reduced ? fade : pop, initial: "hidden", animate: "shown", exit: "gone", children: _jsx(TriangleAlert, { size: 12, strokeWidth: 2 }) }, "alert")
293
+ : _jsx(motion.span, { className: styles.swatch, "data-index": item.id === "free" ? "free" : index, variants: reduced ? fade : pop, initial: "hidden", animate: "shown", exit: "gone" }, "swatch") }) }), _jsx("span", { className: styles.itemLabel, children: _jsx(Swap, { text: item.label, reduced: reduced }) })] }), _jsxs("span", { className: styles.itemValue, children: [_jsx(Ticker, { value: item.value, decimals: decimals, reduced: reduced }), unit && _jsx("span", { className: styles.itemUnit, children: unit })] })] }, item.id);
294
+ }) }), _jsx("p", { className: styles.srOnly, "aria-live": "polite", "aria-atomic": "true", children: announced.message })] });
295
+ }
296
+ export default UsageMeter;
@@ -0,0 +1,61 @@
1
+ /* Categories are steps of the accent, strongest first, so the meter reads as one allowance rather than a rainbow. Free space is the track itself. */
2
+ .meter { --segment-0: var(--accent); --segment-1: color-mix(in oklab, var(--accent) 62%, var(--surface)); --segment-2: color-mix(in oklab, var(--accent) 36%, var(--surface)); --segment-3: color-mix(in oklab, var(--accent) 22%, var(--surface)); display: grid; min-width: 0; gap: var(--space-inset); color: var(--foreground); }
3
+ .top { display: flex; min-width: 0; min-height: 28px; align-items: center; justify-content: space-between; gap: var(--space-group); }
4
+ /* A long label wraps beside the status mark instead of hiding its end. */
5
+ .title { min-width: 0; color: var(--foreground); font-size: var(--text-base); font-weight: 500; letter-spacing: var(--tracking-body); line-height: var(--leading-body); text-wrap: balance; }
6
+
7
+ /* The status mark hugs its words: its width springs to each message and grows from the right edge, so the text never slides. */
8
+ .badge { display: inline-flex; flex: none; height: 28px; justify-content: flex-end; overflow: hidden; border-radius: var(--radius-full); corner-shape: round; background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); font-weight: 500; line-height: 1; transition: background-color var(--duration-standard) var(--ease-standard), color var(--duration-standard) var(--ease-standard); }
9
+ .badgeInner { display: inline-flex; flex: none; align-items: center; gap: 6px; padding: 0 var(--space-group); white-space: nowrap; }
10
+ .badgeIcon { display: grid; place-items: center; }
11
+ .badge[data-status="near"] { background: color-mix(in oklab, var(--warning) 14%, var(--surface)); color: var(--foreground); }
12
+ .badge[data-status="near"] .badgeIcon { color: var(--warning); }
13
+ .badge[data-status="over"] { background: color-mix(in oklab, var(--danger) 12%, var(--surface)); color: var(--foreground); }
14
+ .badge[data-status="over"] .badgeIcon { color: var(--danger); }
15
+
16
+ .total { display: flex; min-width: 0; align-items: baseline; gap: var(--space-related); margin: 0; }
17
+ .amount { flex: none; font-family: var(--font-display); font-size: var(--text-3xl); font-weight: 500; letter-spacing: var(--tracking-display); line-height: var(--leading-display); }
18
+ .caption { min-width: 0; flex: 1; color: var(--text-secondary); font-size: var(--text-sm); }
19
+ .swap { position: relative; display: block; min-width: 0; }
20
+ .swapText { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
21
+
22
+ /* The bar leaves room above and below the track for the limit marker. */
23
+ .bar { position: relative; padding-block: 5px; }
24
+ .track { position: relative; height: 14px; overflow: hidden; border-radius: var(--radius-full); corner-shape: round; background: color-mix(in oklab, var(--foreground) 7%, var(--surface)); isolation: isolate; -webkit-tap-highlight-color: transparent; }
25
+ .segment { position: absolute; inset: 0; transition: opacity var(--duration-fast) var(--ease-standard); }
26
+ .segment[data-index="0"], .swatch[data-index="0"] { background: var(--segment-0); }
27
+ .segment[data-index="1"], .swatch[data-index="1"] { background: var(--segment-1); }
28
+ .segment[data-index="2"], .swatch[data-index="2"] { background: var(--segment-2); }
29
+ .segment[data-index="3"], .swatch[data-index="3"] { background: var(--segment-3); }
30
+ .segment[data-highlight="off"] { opacity: .28; }
31
+ /* Past the limit the used colours stay, struck through by a hatch; highlighting the overage floods it with the danger colour. */
32
+ .overflow { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
33
+ .hatch { fill: var(--surface); }
34
+ .overTint { fill: var(--danger); opacity: 0; transition: opacity var(--duration-fast) var(--ease-standard); }
35
+ .overflow[data-highlight="on"] .overTint { opacity: 1; }
36
+ .limit { position: absolute; top: 0; bottom: 0; left: -1px; width: 2px; border-radius: var(--radius-full); corner-shape: round; background: var(--foreground); pointer-events: none; }
37
+
38
+ /* The legend doubles as the keyboard path: arrow keys move between categories, focus previews one, a press pins it. */
39
+ .legend { display: grid; grid-template-columns: repeat(auto-fit, minmax(108px, 1fr)); gap: 2px; margin: 0 calc(var(--space-related) * -1); }
40
+ .item { display: grid; min-width: 0; justify-items: start; gap: 2px; padding: var(--space-related); border: 0; border-radius: calc(var(--radius-control) - 6px); corner-shape: var(--corner-shape); background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
41
+ @media (hover: hover) and (pointer: fine) { .item:hover { background: var(--surface-muted); } }
42
+ .item[aria-pressed="true"] { background: var(--surface-muted); }
43
+ .item[data-highlight="off"] { opacity: .45; }
44
+ .itemHead { display: flex; min-width: 0; max-width: 100%; align-items: center; gap: var(--space-related); color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); }
45
+ .itemLabel { min-width: 0; flex: 1; }
46
+ .key { display: grid; width: 12px; height: 12px; flex: none; place-items: center; }
47
+ .swatch { grid-area: 1 / 1; width: 10px; height: 10px; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); }
48
+ .swatch[data-index="free"] { background: color-mix(in oklab, var(--foreground) 7%, var(--surface)); box-shadow: inset 0 0 0 1px var(--border-strong); }
49
+ .keyIcon { grid-area: 1 / 1; display: grid; place-items: center; color: var(--danger); }
50
+ .itemValue { display: flex; align-items: baseline; gap: 4px; padding-left: 20px; color: var(--foreground); font-size: var(--text-base); font-weight: 500; line-height: var(--leading-body); }
51
+ .itemUnit { color: var(--text-secondary); font-size: var(--text-sm); font-weight: 400; }
52
+
53
+ /* A rolling number: each digit is a clipped wheel that feathers at its edges, and new columns open from zero width. */
54
+ .ticker { position: relative; display: inline-flex; align-items: flex-start; white-space: nowrap; font-variant-numeric: tabular-nums; user-select: none; }
55
+ .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); }
56
+ .sizer { visibility: hidden; }
57
+ .glyph { position: absolute; inset: var(--feather) 0; text-align: center; }
58
+ .symbol { display: inline-block; overflow-x: clip; }
59
+
60
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
61
+ @media (prefers-reduced-motion: reduce) { .badge, .segment, .item, .overTint { transition: none; } }
@@ -0,0 +1,62 @@
1
+ import type { ReactNode, Ref } from "react";
2
+ export type UserStatus = "available" | "busy" | "away";
3
+ export type ThemePreference = "light" | "dark" | "system";
4
+ /** Statuses in menu order. Each one has its own dot shape as well as color: solid, barred, or hollow. */
5
+ export declare const userStatuses: {
6
+ value: UserStatus;
7
+ label: string;
8
+ }[];
9
+ export interface UserMenuUser {
10
+ name: string;
11
+ email: string;
12
+ plan?: string;
13
+ avatarSrc?: string;
14
+ avatarSrcSet?: string;
15
+ }
16
+ export interface UserMenuItem {
17
+ label: string;
18
+ icon?: ReactNode;
19
+ keys?: string[];
20
+ onSelect?: () => void;
21
+ }
22
+ /**
23
+ * The account menu behind a person's photo in an app's top bar. It holds a compact identity header, a short group of account
24
+ * destinations, an inline theme switch, and sign out. On screens narrower than 640px it opens as a bottom sheet instead.
25
+ * Theme and status choices keep the menu open; everything else closes it and returns focus to the trigger.
26
+ * When `onSignOut` returns a promise, the item shows its progress and the menu closes once it settles.
27
+ */
28
+ export interface UserMenuProps {
29
+ user: UserMenuUser;
30
+ /** Presence status. Passing `status` or `onStatusChange` shows the presence dot and an inline status switch. */
31
+ status?: UserStatus;
32
+ defaultStatus?: UserStatus;
33
+ onStatusChange?: (status: UserStatus) => void;
34
+ theme?: ThemePreference;
35
+ defaultTheme?: ThemePreference;
36
+ /** Reports the choice. Applying it to the page is up to the app. */
37
+ onThemeChange?: (theme: ThemePreference) => void;
38
+ /** Shows the inline light, dark, and system switch. */
39
+ showTheme?: boolean;
40
+ /** Account destinations with optional shortcut hints, such as ["⌘", ","]. Keep it to three or four. */
41
+ items?: UserMenuItem[];
42
+ onSignOut?: () => void | Promise<unknown>;
43
+ signOutKeys?: string[];
44
+ align?: "start" | "center" | "end";
45
+ /** Shows the name and a chevron beside the avatar from 640px up. */
46
+ showName?: boolean;
47
+ open?: boolean;
48
+ defaultOpen?: boolean;
49
+ onOpenChange?: (open: boolean) => void;
50
+ /** Renders the desktop panel in a portal on the body. Pass false to keep it inside the trigger's wrapper. */
51
+ portal?: boolean;
52
+ /** The trigger button, for returning focus after the menu is swapped out. */
53
+ ref?: Ref<HTMLButtonElement>;
54
+ className?: string;
55
+ }
56
+ /** A presence mark that morphs between statuses: the color crossfades while a hole opens for away and a bar slides in for busy. */
57
+ export declare function PresenceDot({ status, className }: {
58
+ status: UserStatus | "offline";
59
+ className?: string;
60
+ }): import("react").JSX.Element;
61
+ export declare function UserMenu({ user, status: statusProp, defaultStatus, onStatusChange, theme: themeProp, defaultTheme, onThemeChange, showTheme, items, onSignOut, signOutKeys, align, showName, open: openProp, defaultOpen, onOpenChange, portal, ref, className }: UserMenuProps): import("react").JSX.Element;
62
+ export default UserMenu;