@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
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Team of Silicons
4
+ Copyright (c) 2026 Elia Kuratli (Arc, the original work this project is based on)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,64 @@
1
- # Temporary Holding Version
1
+ # Silicon UI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React components with calm motion, built on the Silicon design tokens. By Team of Silicons.
4
+ Browse every component at [ui.teamofsilicons.com](https://ui.teamofsilicons.com).
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install @teamofsilicons/silicon-ui
10
+ ```
11
+
12
+ Requires React 19.
13
+
14
+ ## Use
15
+
16
+ Load the tokens once, in your root layout or entry file:
17
+
18
+ ```tsx
19
+ import "@teamofsilicons/silicon-ui/styles.css";
20
+ ```
21
+
22
+ Import each component from its own path:
23
+
24
+ ```tsx
25
+ import { Button } from "@teamofsilicons/silicon-ui/button";
26
+ import { Input } from "@teamofsilicons/silicon-ui/input";
27
+
28
+ export function Example() {
29
+ return <Button variant="primary">Save</Button>;
30
+ }
31
+ ```
32
+
33
+ Components carry their own styles as CSS modules, so your bundler (Next.js, Vite) only loads the CSS of the
34
+ components you use. Theme, accent and corners are set on `<html>`:
35
+
36
+ ```html
37
+ <html data-theme="light" data-accent="neutral" data-corners="squircle">
38
+ ```
39
+
40
+ `data-theme` is `light` or `dark`. `data-accent` is `neutral`, `violet`, `blue`, `green`, `amber`, `orange`,
41
+ `coral` or `rose`. `data-corners` is `squircle` or `round`.
42
+
43
+ Fonts: the tokens use `--font-geist` and `--font-inter` when set (for example by `next/font`), and fall back to
44
+ Geist Variable, Inter Variable, then sans-serif.
45
+
46
+ ## Links and images
47
+
48
+ Components render a plain `<a>` and `<img>` by default. To use your framework's, wrap the app once:
49
+
50
+ ```tsx
51
+ import Link from "next/link";
52
+ import Image from "next/image";
53
+ import { SiliconProvider } from "@teamofsilicons/silicon-ui/provider";
54
+
55
+ <SiliconProvider link={Link} image={Image}>{children}</SiliconProvider>
56
+ ```
57
+
58
+ ## Catalog
59
+
60
+ `@teamofsilicons/silicon-ui/components.json` lists every component, its import path and its exports.
61
+
62
+ ## Licence
63
+
64
+ MIT. Based on [Arc](https://github.com/kuratlielia/arc-library) by Elia Kuratli.
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from "react";
2
+ export interface AccordionItem {
3
+ title: string;
4
+ content: ReactNode;
5
+ }
6
+ export interface AccordionProps {
7
+ items: AccordionItem[];
8
+ defaultOpen?: number;
9
+ /** "lg" suits page-level FAQs: questions at the large text size, answers at body size. */
10
+ size?: "md" | "lg";
11
+ }
12
+ export declare function Accordion({ items, defaultOpen, size }: AccordionProps): import("react").JSX.Element;
13
+ export default Accordion;
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
5
+ import { ChevronDown } from "lucide-react";
6
+ import { motion, useReducedMotion } from "motion/react";
7
+ import { motionTokens } from "../../lib/motion-tokens.js";
8
+ import styles from "./accordion.module.css";
9
+ /** Height follows the content on a spring that never overshoots; closed panels leave the accessibility tree once they finish collapsing. */
10
+ const panelOpen = { height: "auto", opacity: 1, visibility: "visible" };
11
+ const panelClosed = { height: 0, opacity: 0, transitionEnd: { visibility: "hidden" } };
12
+ /** The answer settles down into place with a brief focus pull as the panel opens. */
13
+ const contentOpen = { y: 0, filter: "blur(0px)", transitionEnd: { filter: "none" } };
14
+ const contentClosed = { y: -6, filter: `blur(${motionTokens.blur.subtle}px)` };
15
+ const panelMotion = {
16
+ open: { ...panelOpen, transition: { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } } },
17
+ closed: { ...panelClosed, transition: { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } } },
18
+ };
19
+ const contentMotion = {
20
+ open: { ...contentOpen, transition: { y: motionTokens.spring.smooth, filter: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } } },
21
+ closed: { ...contentClosed, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } },
22
+ };
23
+ /** Reduced motion: same end states in one step, so server and client render identical styles. */
24
+ const panelStill = { open: { ...panelOpen, transition: { duration: 0 } }, closed: { ...panelClosed, transition: { duration: 0 } } };
25
+ const contentStill = { open: { ...contentOpen, transition: { duration: 0 } }, closed: { ...contentClosed, transition: { duration: 0 } } };
26
+ export function Accordion({ items, defaultOpen = 0, size = "md" }) {
27
+ const initialValue = defaultOpen >= 0 && defaultOpen < items.length ? String(defaultOpen) : "";
28
+ const [openValue, setOpenValue] = useState(initialValue);
29
+ const reduced = useReducedMotion();
30
+ return _jsx(AccordionPrimitive.Root, { className: [styles.accordion, size === "lg" ? styles.lg : ""].filter(Boolean).join(" "), type: "single", collapsible: true, value: openValue, onValueChange: setOpenValue, children: items.map((item, index) => {
31
+ const open = openValue === String(index);
32
+ return _jsxs(AccordionPrimitive.Item, { className: styles.item, value: String(index), children: [_jsx(AccordionPrimitive.Header, { className: styles.header, children: _jsxs(AccordionPrimitive.Trigger, { className: styles.trigger, children: [_jsx("span", { children: item.title }), _jsx(motion.span, { className: styles.icon, initial: false, animate: { rotate: open ? 180 : 0 }, transition: reduced ? { duration: 0 } : motionTokens.spring.snappy, children: _jsx(ChevronDown, { width: 17, height: 17, "aria-hidden": "true" }) })] }) }), _jsx(AccordionPrimitive.Content, { forceMount: true, asChild: true, children: _jsx(motion.div, { className: styles.panel, initial: false, animate: open ? "open" : "closed", variants: reduced ? panelStill : panelMotion, children: _jsx(motion.div, { className: styles.panelInner, variants: reduced ? contentStill : contentMotion, children: item.content }) }) })] }, `${item.title}-${index}`);
33
+ }) });
34
+ }
35
+ export default Accordion;
@@ -0,0 +1,15 @@
1
+ .accordion { width: 100%; border-top: 1px solid var(--border); }
2
+ .item { border-bottom: 1px solid var(--border); }
3
+ .header { margin: 0; }
4
+ .trigger { display: flex; width: 100%; min-height: var(--control-height-lg); align-items: center; justify-content: space-between; gap: var(--space-comfort); padding: var(--space-related) 0; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard); }
5
+ @media (hover: hover) and (pointer: fine) { .trigger:hover { color: var(--text-secondary); } .trigger:hover .icon { color: var(--foreground); } }
6
+ /* Rotation is driven by a spring in accordion.tsx; CSS only handles color. */
7
+ .icon { display: inline-flex; flex: 0 0 auto; color: var(--text-muted); transition: color var(--duration-fast) var(--ease-standard); }
8
+ .trigger[data-state="open"] .icon { color: var(--foreground); }
9
+ .panel { overflow: hidden; color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); }
10
+ .panelInner { overflow-wrap: anywhere; padding: 0 var(--space-block) var(--space-comfort) 0; }
11
+ .lg .trigger { min-height: 76px; gap: var(--space-block); padding-block: var(--space-comfort); font-size: var(--text-lg); letter-spacing: var(--tracking-body); }
12
+ .lg .panel { font-size: var(--text-base); line-height: 1.65; }
13
+ .lg .panelInner { max-width: 62ch; padding: 0 var(--space-page) var(--space-section) 0; }
14
+ @media (max-width: 520px) { .lg .trigger { min-height: 68px; font-size: var(--text-base); } .lg .panel { font-size: var(--text-sm); } .lg .panelInner { padding-right: var(--space-section); } }
15
+ @media (prefers-reduced-motion: reduce) { .trigger, .icon { transition: none; } }
@@ -0,0 +1,11 @@
1
+ import type { ButtonHTMLAttributes } from "react";
2
+ export interface ActionButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "onDrag" | "onDragEnd" | "onDragStart" | "onAnimationStart"> {
3
+ label: string;
4
+ successLabel?: string;
5
+ pendingLabel?: string;
6
+ onAction: () => void | Promise<void>;
7
+ resetAfterMs?: number;
8
+ onActionError?: (error: unknown) => void;
9
+ }
10
+ export declare function ActionButton({ label, successLabel, pendingLabel, onAction, resetAfterMs, onActionError, className, disabled, ...props }: ActionButtonProps): import("react").JSX.Element;
11
+ export default ActionButton;
@@ -0,0 +1,57 @@
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, motion, useReducedMotion } from "motion/react";
5
+ import { ArrowRight } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import { TextMorph } from "../text-morph/text-morph.js";
8
+ import styles from "./action-button.module.css";
9
+ const pressVariants = {
10
+ pressed: (button) => ({ scale: (button.current?.offsetWidth ?? 0) > 220 ? .985 : .97, transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } }),
11
+ };
12
+ const rest = { opacity: 1, y: 0, scale: 1, filter: "blur(0px)" };
13
+ const iconIn = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };
14
+ const iconOut = { ...iconIn, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } };
15
+ /** The arrow leaves in the direction of the action and returns from behind once the button resets. */
16
+ const arrowIn = { opacity: 0, x: -6, filter: `blur(${motionTokens.blur.subtle}px)` };
17
+ const arrowOut = { opacity: 0, x: 8, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } };
18
+ const iconRest = { ...rest, x: 0 };
19
+ const fadeIn = { ...rest, opacity: 0 };
20
+ const fadeOut = { opacity: 0, transition: { duration: motionTokens.duration.instant } };
21
+ /** Scale rides the spring; opacity and blur tween so blur never overshoots below zero. */
22
+ const iconEnter = { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] }, filter: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] } };
23
+ /** The success tick draws itself from its short stroke, the way a hand would write it. */
24
+ function DrawnCheck({ reduced }) {
25
+ return _jsx("svg", { className: styles.statusIcon, width: 17, height: 17, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: _jsx(motion.path, { d: "M4 12l5 5L20 6", initial: reduced ? false : { pathLength: 0, opacity: 0 }, animate: { pathLength: 1, opacity: 1 }, transition: { pathLength: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter], delay: .05 }, opacity: { duration: .05, delay: .05 } } }) });
26
+ }
27
+ export function ActionButton({ label, successLabel = "Saved", pendingLabel = "Saving", onAction, resetAfterMs = 2400, onActionError, className, disabled, ...props }) {
28
+ const [state, setState] = useState("idle");
29
+ const resetTimer = useRef(null);
30
+ const buttonRef = useRef(null);
31
+ const reduceMotion = useReducedMotion() ?? false;
32
+ const text = state === "pending" ? pendingLabel : state === "success" ? successLabel : label;
33
+ useEffect(() => () => { if (resetTimer.current)
34
+ clearTimeout(resetTimer.current); }, []);
35
+ async function run() {
36
+ if (state === "pending")
37
+ return;
38
+ if (resetTimer.current)
39
+ clearTimeout(resetTimer.current);
40
+ setState("pending");
41
+ try {
42
+ await onAction();
43
+ setState("success");
44
+ if (resetAfterMs > 0)
45
+ resetTimer.current = setTimeout(() => setState("idle"), resetAfterMs);
46
+ }
47
+ catch (error) {
48
+ setState("idle");
49
+ onActionError?.(error);
50
+ }
51
+ }
52
+ const pending = state === "pending";
53
+ const arrow = state === "idle";
54
+ // Pending stays focusable (aria-disabled instead of disabled), so a keyboard user keeps focus through the whole save.
55
+ return _jsxs(motion.button, { ...props, ref: buttonRef, tabIndex: props.tabIndex ?? 0, type: props.type ?? "button", className: [styles.button, className].filter(Boolean).join(" "), "data-variant": "primary", disabled: disabled, "aria-disabled": pending ? true : props["aria-disabled"], "aria-busy": pending, "data-state": state, onClick: run, custom: buttonRef, variants: pressVariants, whileTap: reduceMotion || disabled || pending ? undefined : "pressed", transition: motionTokens.spring.snappy, children: [_jsxs("span", { className: styles.content, "aria-hidden": "true", children: [_jsx(TextMorph, { children: text }), _jsx("span", { className: styles.iconSlot, children: _jsx(AnimatePresence, { initial: false, children: _jsx(motion.span, { className: styles.phase, initial: reduceMotion ? fadeIn : arrow ? arrowIn : iconIn, animate: iconRest, exit: reduceMotion ? fadeOut : arrow ? arrowOut : iconOut, transition: reduceMotion ? { duration: motionTokens.duration.instant } : iconEnter, children: pending ? _jsx("span", { className: styles.spinner }) : state === "success" ? _jsx(DrawnCheck, { reduced: reduceMotion }) : _jsx(ArrowRight, { className: styles.arrow, width: 17, height: 17 }) }, state) }) })] }), _jsx("span", { className: styles.visuallyHidden, children: label }), _jsx("span", { className: styles.visuallyHidden, role: "status", children: state === "pending" ? pendingLabel : state === "success" ? successLabel : "" })] });
56
+ }
57
+ export default ActionButton;
@@ -0,0 +1,15 @@
1
+ /* The button hugs its label: TextMorph morphs the text in place and springs its width, so no state ever snaps the layout. */
2
+ .button { position: relative; display: inline-flex; min-height: var(--control-height-md); align-items: center; justify-content: center; overflow: hidden; padding: 0 var(--space-comfort); border: 1px solid var(--primary); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--primary); color: var(--primary-foreground); font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
3
+ @media (hover: hover) and (pointer: fine) { .button:hover:not(:disabled) { box-shadow: var(--shadow-resting); } }
4
+ .button:disabled { cursor: not-allowed; }
5
+ .button:disabled:not([data-state="pending"]) { opacity: .7; }
6
+ .button[data-state="pending"] { cursor: progress; }
7
+ .content { display: inline-flex; min-height: 1.25rem; align-items: center; justify-content: center; gap: var(--space-related); white-space: nowrap; }
8
+ .iconSlot { display: grid; width: 17px; height: 17px; flex: 0 0 17px; place-items: center; }
9
+ .phase { grid-area: 1 / 1; display: grid; width: 17px; height: 17px; place-items: center; }
10
+ .arrow { flex: none; }
11
+ .statusIcon { color: currentColor; }
12
+ .spinner { display: inline-block; width: var(--space-inset); height: var(--space-inset); border: 1.5px solid currentColor; border-right-color: transparent; border-radius: var(--radius-full); corner-shape: round; animation: spin .7s linear infinite; }
13
+ .visuallyHidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
14
+ @keyframes spin { to { transform: rotate(360deg); } }
15
+ @media (prefers-reduced-motion: reduce) { .button { transition: none; } .spinner { animation: none; } }
@@ -0,0 +1,37 @@
1
+ import type { ReactNode } from "react";
2
+ export interface ActivityDay {
3
+ /** Calendar day as YYYY-MM-DD. */
4
+ date: string;
5
+ count: number;
6
+ }
7
+ /**
8
+ * A contribution style calendar: one square per day, weeks as columns, with four accent tints for how busy a day was.
9
+ * Use it when rhythm, streaks, and quiet weeks matter more than exact comparisons; use a bar chart when the exact
10
+ * comparison is the point. Cells wave in once on view, a tooltip glides between cells on hover or keyboard focus, and a
11
+ * new range recolors the grid in a sweep instead of redrawing it. Arrow keys move by day and week, Enter selects.
12
+ */
13
+ export interface ActivityHeatmapProps {
14
+ /** One entry per day, oldest first. Days missing inside the range count as zero. */
15
+ days: ActivityDay[];
16
+ /** Accessible name for the grid, such as "Contributions in 2025". */
17
+ label: string;
18
+ /** Finishes the summary line: "1,284 contributions in {period}". */
19
+ period: string;
20
+ /** Nouns for the count. */
21
+ unit?: {
22
+ one: string;
23
+ other: string;
24
+ };
25
+ /** Upper bounds for levels one to three; anything above the last is level four. Defaults to quarters of the busiest day. */
26
+ thresholds?: [number, number, number];
27
+ weekStartsOn?: 0 | 1;
28
+ selectedDate?: string | null;
29
+ onSelectDate?: (date: string) => void;
30
+ /** Controls beside the summary, such as a range switch. */
31
+ actions?: ReactNode;
32
+ /** Formatting locale. Fixed by default so server and client render the same labels. */
33
+ locale?: string;
34
+ className?: string;
35
+ }
36
+ export declare function ActivityHeatmap({ days, label, period, unit: unitProp, thresholds, weekStartsOn, selectedDate, onSelectDate, actions, locale, className }: ActivityHeatmapProps): import("react").JSX.Element;
37
+ export default ActivityHeatmap;
@@ -0,0 +1,303 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
+ import { AnimatePresence, animate, motion, useInView, useMotionValue, useReducedMotion } from "motion/react";
5
+ import { motionTokens } from "../../lib/motion-tokens.js";
6
+ import styles from "./activity-heatmap.module.css";
7
+ const DAY = 86400000;
8
+ const LEVELS = [0, 1, 2, 3, 4];
9
+ const enter = [...motionTokens.ease.enter];
10
+ const standard = [...motionTokens.ease.standard];
11
+ /** Total wave travel in ms, kept under the system's stagger budget however many weeks the range spans. */
12
+ const WAVE = 420;
13
+ const toUtc = (iso) => Date.parse(`${iso}T00:00:00Z`);
14
+ const toIso = (time) => new Date(time).toISOString().slice(0, 10);
15
+ function buildModel(days, weekStartsOn, thresholds, locale) {
16
+ const dates = days.map(day => toUtc(day.date)).filter(Number.isFinite);
17
+ const start = dates.length ? Math.min(...dates) : toUtc("2025-01-01");
18
+ const length = dates.length ? Math.round((Math.max(...dates) - start) / DAY) + 1 : 0;
19
+ const counts = new Array(length).fill(0);
20
+ days.forEach(day => { const index = Math.round((toUtc(day.date) - start) / DAY); if (index >= 0 && index < length)
21
+ counts[index] += Math.max(0, day.count); });
22
+ const max = counts.reduce((a, b) => Math.max(a, b), 0);
23
+ const bounds = thresholds ?? [Math.max(1, Math.ceil(max * .25)), Math.max(2, Math.ceil(max * .5)), Math.max(3, Math.ceil(max * .75))];
24
+ const levels = counts.map(count => count <= 0 ? 0 : count <= bounds[0] ? 1 : count <= bounds[1] ? 2 : count <= bounds[2] ? 3 : 4);
25
+ const lead = (new Date(start).getUTCDay() - weekStartsOn + 7) % 7;
26
+ const weeks = Math.ceil((lead + length) / 7);
27
+ const monthName = new Intl.DateTimeFormat(locale, { month: "short", timeZone: "UTC" });
28
+ const months = [];
29
+ for (let index = 0; index < length; index++) {
30
+ const date = new Date(start + index * DAY);
31
+ if (index === 0 || date.getUTCDate() === 1)
32
+ months.push({ month: date.getUTCFullYear() * 12 + date.getUTCMonth(), col: Math.floor((lead + index) / 7), label: monthName.format(date) });
33
+ }
34
+ // A partial first month keeps its label only when there is room before the next one.
35
+ if (months.length > 1 && months[1].col - months[0].col < 3)
36
+ months.shift();
37
+ const perLevel = [0, 0, 0, 0, 0];
38
+ levels.forEach(level => perLevel[level]++);
39
+ return { start, length, lead, weeks, total: counts.reduce((a, b) => a + b, 0), counts, levels, thresholds: bounds, months, perLevel };
40
+ }
41
+ const CONTRIBUTIONS = { one: "contribution", other: "contributions" };
42
+ const noun = (count, unit) => count === 1 ? unit.one : unit.other;
43
+ const noopSubscribe = () => () => { };
44
+ /** Reduced motion only after hydration, so the server and the first client render agree. */
45
+ function useReducedMotionSafe() {
46
+ const hydrated = useSyncExternalStore(noopSubscribe, () => true, () => false);
47
+ const reduced = useReducedMotion();
48
+ return hydrated && !!reduced;
49
+ }
50
+ const tipText = (reduced) => ({
51
+ from: (change) => change === "instant" ? { opacity: 1, y: "0em", filter: "blur(0px)" } : reduced ? { opacity: 0 } : { opacity: 0, y: `${.3 * (change || 1)}em`, filter: `blur(${change ? motionTokens.blur.soft : motionTokens.blur.subtle}px)` },
52
+ to: { opacity: 1, y: "0em", filter: "blur(0px)" },
53
+ gone: (change) => change === "instant" || reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, y: `${-.3 * (change || 1)}em`, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.instant, ease: standard } },
54
+ });
55
+ /** A total that rolls digit by digit in the direction it moved. Places keep their identity, so only changed digits turn,
56
+ * and when the number gains or loses a digit the width follows on a spring instead of shifting the sentence in one frame. */
57
+ function RollingNumber({ value, locale, reduced }) {
58
+ const [state, setState] = useState({ value, direction: 1 });
59
+ if (state.value !== value)
60
+ setState({ value, direction: value > state.value ? 1 : -1 });
61
+ const inner = useRef(null);
62
+ const width = useMotionValue("auto");
63
+ const armedUntil = useRef(0), lastValue = useRef(value);
64
+ useLayoutEffect(() => {
65
+ if (lastValue.current === value)
66
+ return;
67
+ lastValue.current = value;
68
+ armedUntil.current = performance.now() + 600;
69
+ }, [value]);
70
+ useEffect(() => {
71
+ const node = inner.current;
72
+ if (!node || typeof ResizeObserver === "undefined")
73
+ return;
74
+ const observer = new ResizeObserver(() => {
75
+ const next = node.getBoundingClientRect().width;
76
+ if (reduced || width.get() === "auto" || performance.now() > armedUntil.current)
77
+ width.jump(next);
78
+ else
79
+ animate(width, next, motionTokens.spring.morph);
80
+ });
81
+ observer.observe(node);
82
+ return () => observer.disconnect();
83
+ }, [reduced, width]);
84
+ const chars = [...new Intl.NumberFormat(locale).format(value)];
85
+ const variants = {
86
+ from: (direction) => reduced ? { opacity: 0 } : { opacity: 0, y: `${.3 * direction}em`, filter: `blur(${motionTokens.blur.soft}px)` },
87
+ to: { opacity: 1, y: "0em", filter: "blur(0px)" },
88
+ gone: (direction) => reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, y: `${-.3 * direction}em`, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.instant, ease: standard } },
89
+ };
90
+ return _jsx(motion.span, { className: styles.rollingFrame, style: { width }, "aria-hidden": "true", children: _jsx("span", { ref: inner, className: styles.rolling, children: chars.map((char, index) => {
91
+ const place = chars.length - index;
92
+ return _jsx("span", { className: styles.place, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, custom: state.direction, children: _jsx(motion.span, { className: styles.placeChar, custom: state.direction, variants: variants, initial: "from", animate: "to", exit: "gone", transition: reduced ? { duration: motionTokens.duration.fast } : { duration: motionTokens.duration.standard, ease: enter, delay: Math.min(place * .018, .09) }, children: char }, char) }) }, place);
93
+ }) }) });
94
+ }
95
+ /** A short word that changes rises in from a soft blur while the old one lifts away a little faster. */
96
+ function RiseText({ text, reduced, children }) {
97
+ return _jsx("span", { className: styles.rise, "aria-hidden": "true", children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(motion.span, { className: styles.riseLine, initial: reduced ? { opacity: 0 } : { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` }, animate: { opacity: 1, y: "0em", filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.instant, ease: standard } }, transition: { duration: reduced ? motionTokens.duration.fast : motionTokens.duration.standard, ease: enter }, children: children ?? text }, text) }) });
98
+ }
99
+ export function ActivityHeatmap({ days, label, period, unit: unitProp = CONTRIBUTIONS, thresholds, weekStartsOn = 0, selectedDate = null, onSelectDate, actions, locale = "en-US", className }) {
100
+ const reduced = useReducedMotionSafe();
101
+ const id = useId();
102
+ // Keyed by its words, so an inline unit object does not rebuild every cell on each render.
103
+ const unit = useMemo(() => ({ one: unitProp.one, other: unitProp.other }), [unitProp.one, unitProp.other]);
104
+ const rootRef = useRef(null);
105
+ const plotRef = useRef(null);
106
+ const gridRef = useRef(null);
107
+ const scrollerRef = useRef(null);
108
+ const ringRef = useRef(null);
109
+ const measureRef = useRef(null);
110
+ const inView = useInView(plotRef, { once: true, amount: .35 });
111
+ const model = useMemo(() => buildModel(days, weekStartsOn, thresholds, locale), [days, weekStartsOn, thresholds, locale]);
112
+ const formats = useMemo(() => ({
113
+ long: new Intl.DateTimeFormat(locale, { weekday: "long", month: "long", day: "numeric", year: "numeric", timeZone: "UTC" }),
114
+ short: new Intl.DateTimeFormat(locale, { weekday: "short", month: "short", day: "numeric", year: "numeric", timeZone: "UTC" }),
115
+ weekday: new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" }),
116
+ number: new Intl.NumberFormat(locale),
117
+ }), [locale]);
118
+ // A new range sweeps forward when it is later than the last one and back when it is earlier.
119
+ const [range, setRange] = useState({ start: model.start, direction: 1 });
120
+ if (range.start !== model.start)
121
+ setRange({ start: model.start, direction: model.start > range.start ? 1 : -1 });
122
+ const [reveal, setReveal] = useState("hidden");
123
+ useEffect(() => {
124
+ if (reveal !== "hidden" || !(inView || reduced))
125
+ return;
126
+ const frame = requestAnimationFrame(() => setReveal(reduced ? "done" : "revealing"));
127
+ return () => cancelAnimationFrame(frame);
128
+ }, [inView, reduced, reveal]);
129
+ useEffect(() => {
130
+ if (reveal !== "revealing")
131
+ return;
132
+ const timer = window.setTimeout(() => setReveal("done"), WAVE + 700);
133
+ return () => window.clearTimeout(timer);
134
+ }, [reveal]);
135
+ const selectedIndex = selectedDate ? Math.round((toUtc(selectedDate) - model.start) / DAY) : -1;
136
+ const hasSelection = selectedIndex >= 0 && selectedIndex < model.length;
137
+ const [focusIndex, setFocusIndex] = useState(null);
138
+ const tabIndexDay = focusIndex !== null && focusIndex < model.length ? focusIndex : hasSelection ? selectedIndex : model.length - 1;
139
+ // Legend: hovering or focusing a level previews it on the grid, clicking pins it. The caption says how many days sit at that level.
140
+ const [preview, setPreview] = useState(null);
141
+ const [pinned, setPinned] = useState(null);
142
+ const [legendFocus, setLegendFocus] = useState(0);
143
+ const previewTimer = useRef(0);
144
+ const highlight = preview ?? pinned;
145
+ const previewLevel = (level) => { window.clearTimeout(previewTimer.current); if (level !== null)
146
+ setPreview(level);
147
+ else
148
+ previewTimer.current = window.setTimeout(() => setPreview(null), 90); };
149
+ useEffect(() => () => window.clearTimeout(previewTimer.current), []);
150
+ // Tooltip: one bubble for the whole grid. It jumps into place when it opens and glides on a spring between cells after that.
151
+ const [tip, setTip] = useState(null);
152
+ const [open, setOpen] = useState(false);
153
+ const [change, setChange] = useState("instant");
154
+ const openRef = useRef(false);
155
+ const hideTimer = useRef(0);
156
+ const tipX = useMotionValue(0), tipY = useMotionValue(0), tipWidth = useMotionValue("auto");
157
+ function contentFor(index, anchor) {
158
+ const count = model.counts[index];
159
+ const date = new Date(model.start + index * DAY);
160
+ return { key: `day-${toIso(date.getTime())}`, primary: count ? `${formats.number.format(count)} ${noun(count, unit)}` : `No ${unit.other}`, secondary: formats.short.format(date), value: count, anchor };
161
+ }
162
+ const [a, b, c] = model.thresholds;
163
+ const ranges = [`no ${unit.other}`, a === 1 ? `1 ${unit.one}` : `1 to ${a} ${unit.other}`, `${a + 1} to ${b} ${unit.other}`, `${b + 1} to ${c} ${unit.other}`, `${c + 1} or more ${unit.other}`];
164
+ const shortRanges = [`no ${unit.other}`, a === 1 ? `1 ${unit.one}` : `1–${a} ${unit.other}`, `${a + 1}–${b} ${unit.other}`, `${b + 1}–${c} ${unit.other}`, `${c + 1}+ ${unit.other}`];
165
+ const dayCount = highlight === null ? "" : `${formats.number.format(model.perLevel[highlight])} ${model.perLevel[highlight] === 1 ? "day" : "days"}`;
166
+ const caption = highlight === null ? "" : `${dayCount} with ${shortRanges[highlight]}`;
167
+ function show(next) {
168
+ window.clearTimeout(hideTimer.current);
169
+ setChange(tip && openRef.current ? Math.sign(next.value - tip.value) : "instant");
170
+ setTip(next);
171
+ setOpen(true);
172
+ }
173
+ function hideSoon(delay = 110) {
174
+ window.clearTimeout(hideTimer.current);
175
+ hideTimer.current = window.setTimeout(() => setOpen(false), delay);
176
+ }
177
+ useEffect(() => () => window.clearTimeout(hideTimer.current), []);
178
+ useLayoutEffect(() => {
179
+ const root = rootRef.current, measure = measureRef.current;
180
+ if (!tip || !root || !measure)
181
+ return;
182
+ const place = (glide) => {
183
+ const box = root.getBoundingClientRect(), cell = tip.anchor.getBoundingClientRect();
184
+ const width = Math.ceil(measure.getBoundingClientRect().width);
185
+ const half = width / 2 + 13;
186
+ const x = Math.min(Math.max(cell.left - box.left + cell.width / 2, half + 2), box.width - half - 2);
187
+ const y = cell.top - box.top;
188
+ if (glide) {
189
+ animate(tipX, x, motionTokens.spring.snappy);
190
+ animate(tipY, y, motionTokens.spring.snappy);
191
+ animate(tipWidth, width, motionTokens.spring.morph);
192
+ }
193
+ else {
194
+ tipX.jump(x);
195
+ tipY.jump(y);
196
+ tipWidth.jump(width);
197
+ }
198
+ };
199
+ place(open && openRef.current && !reduced);
200
+ openRef.current = open;
201
+ if (!open)
202
+ return;
203
+ // The grid scrolls sideways on narrow screens; the bubble stays pinned to its cell while it does.
204
+ const scroller = scrollerRef.current;
205
+ const follow = () => place(false);
206
+ scroller?.addEventListener("scroll", follow, { passive: true });
207
+ return () => scroller?.removeEventListener("scroll", follow);
208
+ }, [tip, open, reduced, tipX, tipY, tipWidth]);
209
+ const indexOf = (element) => {
210
+ const cell = element?.closest("[data-index]");
211
+ return cell && gridRef.current?.contains(cell) ? { cell, index: Number(cell.dataset.index) } : null;
212
+ };
213
+ const focusDay = (index) => gridRef.current?.querySelector(`[data-index="${index}"]`)?.focus();
214
+ function onGridPointerOver(event) {
215
+ const hit = indexOf(event.target);
216
+ if (hit)
217
+ show(contentFor(hit.index, hit.cell));
218
+ }
219
+ function onGridPointerLeave(event) {
220
+ // A keyboard user keeps their bubble on the focused day. A tap focuses its day too, so the bubble stays until focus moves on;
221
+ // where a tap does not focus, it lingers long enough to read.
222
+ const focused = indexOf(document.activeElement);
223
+ if (focused?.cell.matches(":focus-visible"))
224
+ show(contentFor(focused.index, focused.cell));
225
+ else
226
+ hideSoon(event.pointerType === "touch" ? 2400 : 110);
227
+ }
228
+ function onGridFocus(event) {
229
+ const hit = indexOf(event.target);
230
+ if (!hit)
231
+ return;
232
+ setFocusIndex(hit.index);
233
+ show(contentFor(hit.index, hit.cell));
234
+ }
235
+ function onGridBlur(event) {
236
+ if (!gridRef.current?.contains(event.relatedTarget))
237
+ hideSoon(0);
238
+ }
239
+ function onGridKeyDown(event) {
240
+ const hit = indexOf(event.target);
241
+ if (!hit)
242
+ return;
243
+ const moves = { ArrowUp: hit.index - 1, ArrowDown: hit.index + 1, ArrowLeft: hit.index - 7, ArrowRight: hit.index + 7, Home: 0, End: model.length - 1 };
244
+ if (event.key in moves) {
245
+ event.preventDefault();
246
+ focusDay(Math.min(Math.max(moves[event.key], 0), model.length - 1));
247
+ }
248
+ else if (event.key === "Enter" || event.key === " ") {
249
+ event.preventDefault();
250
+ onSelectDate?.(toIso(model.start + hit.index * DAY));
251
+ }
252
+ else if (event.key === "Escape" && open) {
253
+ event.preventDefault();
254
+ setOpen(false);
255
+ }
256
+ }
257
+ function onLegendKeyDown(event) {
258
+ const moves = { ArrowLeft: legendFocus - 1, ArrowRight: legendFocus + 1, Home: 0, End: 4 };
259
+ if (event.key === "Escape") {
260
+ previewLevel(null);
261
+ return;
262
+ }
263
+ if (!(event.key in moves))
264
+ return;
265
+ event.preventDefault();
266
+ const next = Math.min(Math.max(moves[event.key], 0), 4);
267
+ rootRef.current?.querySelector(`[data-level-key="${next}"]`)?.focus();
268
+ }
269
+ // The selection ring glides between cells on CSS. When it first appears it should fade in where it lands, not fly in from its last spot.
270
+ const ringShown = hasSelection && reveal !== "hidden";
271
+ const ringWasShown = useRef(ringShown);
272
+ useLayoutEffect(() => {
273
+ const ring = ringRef.current;
274
+ if (ring && ringShown && !ringWasShown.current) {
275
+ ring.style.transitionProperty = "opacity";
276
+ void ring.offsetWidth;
277
+ ring.style.transitionProperty = "";
278
+ }
279
+ ringWasShown.current = ringShown;
280
+ }, [ringShown]);
281
+ const maxDiagonal = Math.max(1, model.weeks - 1 + 6);
282
+ const step = Math.min(12, WAVE / maxDiagonal);
283
+ const cells = useMemo(() => Array.from({ length: 7 }, (_, row) => _jsx("div", { role: "row", className: styles.row, children: Array.from({ length: model.weeks }, (_, col) => {
284
+ const index = col * 7 + row - model.lead;
285
+ const inRange = index >= 0 && index < model.length;
286
+ const wave = { "--wave": `${Math.round((col + row) * step)}ms`, "--wave-back": `${Math.round((maxDiagonal - col - row) * step)}ms` };
287
+ if (!inRange)
288
+ return _jsx("span", { className: styles.cell, "data-empty": "", style: wave, "aria-hidden": "true" }, col);
289
+ const count = model.counts[index];
290
+ const date = new Date(model.start + index * DAY);
291
+ return _jsx("span", { role: "gridcell", className: styles.cell, style: wave, "data-index": index, "data-level": model.levels[index], tabIndex: index === tabIndexDay ? 0 : -1, "aria-selected": onSelectDate ? index === selectedIndex : undefined, "aria-label": `${count ? formats.number.format(count) : "No"} ${noun(count, unit)}, ${formats.long.format(date)}`, onClick: () => onSelectDate?.(toIso(date.getTime())) }, col);
292
+ }) }, row)), [model, step, maxDiagonal, tabIndexDay, selectedIndex, formats, unit, onSelectDate]);
293
+ const weekdayLabels = useMemo(() => Array.from({ length: 7 }, (_, row) => {
294
+ const weekday = (row + weekStartsOn) % 7;
295
+ // January 4, 1970 was a Sunday.
296
+ return weekday % 2 === 1 ? formats.weekday.format(new Date((3 + weekday) * DAY)) : "";
297
+ }), [formats, weekStartsOn]);
298
+ const selectedCol = hasSelection ? Math.floor((selectedIndex + model.lead) / 7) : 0;
299
+ const selectedRow = hasSelection ? (selectedIndex + model.lead) % 7 : 0;
300
+ const summary = `${formats.number.format(model.total)} ${noun(model.total, unit)} in ${period}`;
301
+ return _jsxs("div", { ref: rootRef, className: [styles.root, className].filter(Boolean).join(" "), children: [_jsxs("div", { className: styles.header, children: [_jsxs("p", { className: styles.summary, children: [_jsxs("span", { className: styles.total, children: [_jsx(RollingNumber, { value: model.total, locale: locale, reduced: reduced }), " ", noun(model.total, unit)] }), " ", _jsxs("span", { className: styles.period, children: ["in ", _jsx(RiseText, { text: period, reduced: reduced })] }), _jsx("span", { className: styles.srOnly, role: "status", children: summary })] }), actions && _jsx("div", { className: styles.actions, children: actions })] }), _jsx("div", { ref: scrollerRef, className: styles.scroller, children: _jsxs("div", { className: styles.canvas, style: { "--weeks": model.weeks }, children: [_jsx("div", { className: styles.months, "aria-hidden": "true", children: model.months.map((month, order) => _jsx("span", { className: styles.month, style: { "--col": month.col }, children: month.label }, `${month.month % 12}-${model.months.findIndex(item => item.month % 12 === month.month % 12) === order ? 0 : 1}`)) }), _jsx("div", { className: styles.weekdays, "aria-hidden": "true", children: weekdayLabels.map((text, row) => _jsx("span", { className: styles.weekday, children: text }, row)) }), _jsxs("div", { ref: plotRef, className: styles.plot, children: [_jsx("div", { ref: gridRef, role: "grid", "aria-label": label, "aria-readonly": "true", "aria-describedby": `${id}-legend`, className: styles.grid, "data-reveal": reveal, "data-direction": range.direction < 0 ? "back" : undefined, "data-highlight": highlight ?? undefined, onPointerOver: onGridPointerOver, onPointerLeave: onGridPointerLeave, onFocus: onGridFocus, onBlur: onGridBlur, onKeyDown: onGridKeyDown, children: cells }), _jsx("span", { ref: ringRef, className: styles.ring, "data-shown": ringShown || undefined, style: { "--col": selectedCol, "--row": selectedRow }, "aria-hidden": "true" })] })] }) }), _jsxs("div", { className: styles.legend, children: [_jsxs("span", { id: `${id}-legend`, className: styles.srOnly, children: ["Darker squares mean more ", unit.other, ". Levels: ", ranges.join(", "), "."] }), _jsxs("span", { className: styles.caption, "aria-live": "polite", children: [_jsx(RiseText, { text: caption, reduced: reduced, children: caption && _jsxs(_Fragment, { children: [_jsx("span", { className: styles.captionLong, children: caption }), _jsx("span", { className: styles.captionShort, children: dayCount })] }) }), _jsx("span", { className: styles.srOnly, children: caption })] }), _jsxs("span", { className: styles.scale, children: [_jsx("span", { className: styles.legendText, "aria-hidden": "true", children: "Less" }), _jsx("div", { className: styles.swatches, role: "group", "aria-label": "Highlight days by level", onKeyDown: onLegendKeyDown, children: LEVELS.map(level => _jsx("button", { type: "button", className: styles.swatch, "data-level-key": level, "data-level": level, tabIndex: level === legendFocus ? 0 : -1, "aria-pressed": pinned === level, "aria-label": `Highlight days with ${ranges[level]}`, onClick: () => setPinned(current => current === level ? null : level), onPointerEnter: () => previewLevel(level), onPointerLeave: () => previewLevel(null), onFocus: () => { setLegendFocus(level); previewLevel(level); }, onBlur: () => previewLevel(null) }, level)) }), _jsx("span", { className: styles.legendText, "aria-hidden": "true", children: "More" })] })] }), _jsx(motion.div, { className: styles.tip, style: { x: tipX, y: tipY }, "aria-hidden": "true", children: _jsx(motion.div, { className: styles.bubble, style: { x: "-50%" }, initial: false, animate: open && tip ? { opacity: 1, scale: 1 } : { opacity: 0, scale: reduced ? 1 : .96 }, transition: reduced ? { duration: open ? motionTokens.duration.fast : motionTokens.duration.instant } : open ? { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: enter } } : { duration: motionTokens.duration.instant, ease: standard }, children: _jsxs(motion.span, { className: styles.tipBody, style: { width: tipWidth }, children: [_jsxs("span", { ref: measureRef, className: styles.tipMeasure, children: [_jsx("span", { className: styles.tipPrimary, children: tip?.primary }), _jsx("span", { className: styles.tipSecondary, children: tip?.secondary })] }), _jsx(AnimatePresence, { mode: "popLayout", initial: false, custom: change, children: tip && _jsxs(motion.span, { className: styles.tipLines, custom: change, variants: tipText(reduced), initial: "from", animate: "to", exit: "gone", transition: { duration: reduced ? motionTokens.duration.fast : motionTokens.duration.standard, ease: enter }, children: [_jsx("span", { className: styles.tipPrimary, children: tip.primary }), _jsx("span", { className: styles.tipSecondary, children: tip.secondary })] }, tip.key) })] }) }) })] });
302
+ }
303
+ export default ActivityHeatmap;