@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
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,82 @@
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
+
63
+ ## Guardrails
64
+
65
+ Two checks keep an app on the system, so screens stay consistent however they are built (including by AI):
66
+
67
+ ```js
68
+ // eslint.config.mjs: raw <button>, <input>, <select>, <textarea>, <dialog> and borrowed library CSS are errors
69
+ import siliconUi from "@teamofsilicons/silicon-ui/eslint";
70
+ export default [...yourConfig, ...siliconUi()];
71
+ ```
72
+
73
+ ```sh
74
+ npx silicon-ui-check # raw spacing, radius, colour or font size in CSS modules fails
75
+ npx silicon-ui-check --update-baseline # tolerate today's legacy values; only new drift fails
76
+ ```
77
+
78
+ Full guide, including the Claude Code hook: [docs/guardrails.md](https://github.com/teamofsilicons/silicon-ui/blob/main/docs/guardrails.md).
79
+
80
+ ## Licence
81
+
82
+ MIT. Based on [Arc](https://github.com/kuratlielia/arc-library) by Elia Kuratli.
package/check.mjs ADDED
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * silicon-ui-check: keeps an app's CSS on Silicon UI's tokens.
4
+ *
5
+ * Fails a CSS file that sets spacing, a corner radius, a colour or a font size with a raw value instead of a token:
6
+ *
7
+ * spacing padding, margin, gap, inset, top/right/bottom/left, scroll-margin/padding → var(--space-*)
8
+ * radius border-radius and its corners → var(--radius-*)
9
+ * color hex, rgb(), hsl(), oklch(), oklab(), lab(), lch(), color() → var(--…)
10
+ * font-size px or rem → var(--text-*)
11
+ *
12
+ * Allowed everywhere: 0, auto, inherit and other keywords, anything inside var() (including fallbacks), and
13
+ * font-relative or layout-relative units (em, %, ch, vw, vh, cq*). Custom property definitions (--x: …) are not
14
+ * checked: tokens are defined that way, in the files you list under `ignore`. The visually hidden block (.srOnly,
15
+ * .visuallyHidden, .sr-only) and @keyframes are skipped.
16
+ *
17
+ * Configure it in package.json:
18
+ *
19
+ * "siliconUi": {
20
+ * "css": ["components/**\/*.module.css", "app/**\/*.module.css"],
21
+ * "ignore": ["styles/tokens.css"],
22
+ * "baseline": "silicon-ui-baseline.json"
23
+ * }
24
+ *
25
+ * Usage:
26
+ * silicon-ui-check check every configured file (fails only above the baseline)
27
+ * silicon-ui-check a.css b.css check these files
28
+ * silicon-ui-check --update-baseline record today's counts, so only new drift fails from now on
29
+ * silicon-ui-check --hook Claude Code PostToolUse hook: checks the edited file (CSS, and ESLint for
30
+ * .ts/.tsx/.js/.jsx), exit 2 with the problems so the agent fixes them
31
+ */
32
+ import { spawnSync } from "node:child_process";
33
+ import fs from "node:fs";
34
+ import path from "node:path";
35
+ import { fileURLToPath } from "node:url";
36
+
37
+ const RULES = {
38
+ spacing: /^(?:padding|margin|gap|row-gap|column-gap|inset|top|right|bottom|left)(?:-[a-z-]+)?$|^scroll-(?:margin|padding)(?:-[a-z-]+)?$/,
39
+ radius: /^border(?:-[a-z]+(?:-[a-z]+)?)?-radius$/,
40
+ "font-size": /^font-size$/,
41
+ };
42
+ const LENGTH = /(?<![\w.#-])(-?(?:\d*\.)?\d+)(px|rem)\b/g;
43
+ const COLOR = /#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch|color)\(/i;
44
+ const HIDDEN = /\.(?:srOnly|visuallyHidden|sr-only|visually-hidden)\b/;
45
+ const FIX = {
46
+ spacing: "use a spacing token: var(--space-tight|related|group|inset|comfort|section|block|page|stage|screen)",
47
+ radius: "use a radius token: var(--radius-detail|control|container|overlay|full)",
48
+ color: "use a colour token: var(--foreground), var(--text-secondary), var(--border), var(--accent) …",
49
+ "font-size": "use a type token: var(--text-xs|sm|base|lg|xl|2xl|3xl|4xl|5xl)",
50
+ };
51
+
52
+ /** Removes every var(...) (with nested parentheses), so tokens and their fallbacks are never flagged. */
53
+ function withoutVars(value) {
54
+ let out = "";
55
+ for (let i = 0; i < value.length; i++) {
56
+ if (value.startsWith("var(", i)) {
57
+ let depth = 0;
58
+ for (; i < value.length; i++) {
59
+ if (value[i] === "(") depth++;
60
+ else if (value[i] === ")" && --depth === 0) break;
61
+ }
62
+ out += "var()";
63
+ } else out += value[i];
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /** Declarations with their line, selector and whether they sit inside @keyframes. */
69
+ function declarations(css) {
70
+ const source = css.replace(/\/\*[\s\S]*?\*\//g, match => match.replace(/[^\n]/g, " "));
71
+ const found = [];
72
+ const stack = [];
73
+ let start = 0;
74
+ const lineAt = index => source.slice(0, index).split("\n").length;
75
+ for (let i = 0; i < source.length; i++) {
76
+ const char = source[i];
77
+ if (char === "{") {
78
+ stack.push(source.slice(start, i).trim());
79
+ start = i + 1;
80
+ } else if (char === "}" || char === ";") {
81
+ const text = source.slice(start, i);
82
+ const colon = text.indexOf(":");
83
+ if (colon > 0 && stack.length && !/[{}]/.test(text)) {
84
+ const property = text.slice(0, colon).trim().toLowerCase();
85
+ const value = text.slice(colon + 1).trim();
86
+ const offset = start + text.length - text.trimStart().length;
87
+ if (/^[a-z-]+$/.test(property)) {
88
+ found.push({ property, value, line: lineAt(offset), selector: stack[stack.length - 1], keyframes: stack.some(p => /^@(-\w+-)?keyframes\b/.test(p)) });
89
+ }
90
+ }
91
+ if (char === "}") stack.pop();
92
+ start = i + 1;
93
+ }
94
+ }
95
+ return found;
96
+ }
97
+
98
+ export function checkCss(css) {
99
+ const problems = [];
100
+ for (const { property, value, line, selector, keyframes } of declarations(css)) {
101
+ if (keyframes || property.startsWith("--") || HIDDEN.test(selector)) continue;
102
+ const bare = withoutVars(value);
103
+ for (const [rule, pattern] of Object.entries(RULES)) {
104
+ if (!pattern.test(property)) continue;
105
+ const raw = [...bare.matchAll(LENGTH)].filter(match => Number(match[1]) !== 0).map(match => match[0]);
106
+ if (raw.length) problems.push({ rule, line, property, value, raw: raw.join(", ") });
107
+ }
108
+ const color = bare.match(COLOR);
109
+ if (color) problems.push({ rule: "color", line, property, value, raw: color[0] });
110
+ }
111
+ return problems;
112
+ }
113
+
114
+ /* ---------------------------------------------------------------- files and config ------------------------------------------------------------- */
115
+
116
+ function globToRegExp(glob) {
117
+ let re = "";
118
+ for (let i = 0; i < glob.length; i++) {
119
+ const c = glob[i];
120
+ if (c === "*") {
121
+ if (glob[i + 1] === "*") { re += glob[i + 2] === "/" ? "(?:.*/)?" : ".*"; i += glob[i + 2] === "/" ? 2 : 1; }
122
+ else re += "[^/]*";
123
+ } else if (c === "?") re += "[^/]";
124
+ else if (c === "{") { const end = glob.indexOf("}", i); re += `(?:${glob.slice(i + 1, end).split(",").map(s => s.replace(/[.+^$()|[\]\\]/g, "\\$&")).join("|")})`; i = end; }
125
+ else re += c.replace(/[.+^$()|[\]\\]/g, "\\$&");
126
+ }
127
+ return new RegExp(`^${re}$`);
128
+ }
129
+ const matches = (file, globs) => globs.some(glob => globToRegExp(glob).test(file));
130
+
131
+ function walk(dir, root) {
132
+ const out = [];
133
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
134
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
135
+ const full = path.join(dir, entry.name);
136
+ if (entry.isDirectory()) out.push(...walk(full, root));
137
+ else out.push(path.relative(root, full).split(path.sep).join("/"));
138
+ }
139
+ return out;
140
+ }
141
+
142
+ function findProject(from) {
143
+ for (let dir = path.resolve(from); ; dir = path.dirname(dir)) {
144
+ const manifest = path.join(dir, "package.json");
145
+ if (fs.existsSync(manifest)) {
146
+ const config = JSON.parse(fs.readFileSync(manifest, "utf8")).siliconUi;
147
+ if (config) return { root: dir, config: { css: ["**/*.module.css"], ignore: [], baseline: "silicon-ui-baseline.json", ...config } };
148
+ }
149
+ if (path.dirname(dir) === dir) return { root: path.resolve(from), config: { css: ["**/*.module.css"], ignore: [], baseline: "silicon-ui-baseline.json" } };
150
+ }
151
+ }
152
+
153
+ const count = problems => problems.reduce((totals, { rule }) => ({ ...totals, [rule]: (totals[rule] ?? 0) + 1 }), {});
154
+ const readBaseline = file => (fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")).files ?? {} : {});
155
+
156
+ /** Problems in a file beyond what the baseline allows for it (all of them for a file the baseline does not list). */
157
+ function overBaseline(problems, allowed = {}) {
158
+ const totals = count(problems);
159
+ return Object.keys(totals).filter(rule => totals[rule] > (allowed[rule] ?? 0)).flatMap(rule => problems.filter(problem => problem.rule === rule));
160
+ }
161
+
162
+ function report(file, problems, allowed = {}) {
163
+ const lines = [`${file}`];
164
+ for (const { rule, line, property, raw } of [...problems].sort((x, y) => x.line - y.line)) lines.push(` ${line} ${rule}: ${property} uses ${raw}; ${FIX[rule]}`);
165
+ const counted = count(problems);
166
+ const listed = Object.keys(counted).filter(rule => allowed[rule] && counted[rule] > allowed[rule]).map(rule => `${rule} ${counted[rule]}, baseline ${allowed[rule]}`);
167
+ if (listed.length) lines.push(` (over the baseline: ${listed.join("; ")}. Older raw values are listed too; fix at least the new ones)`);
168
+ return lines.join("\n");
169
+ }
170
+
171
+ /* ---------------------------------------------------------------- modes ------------------------------------------------------------------------ */
172
+
173
+ function runCli(args) {
174
+ const { root, config } = findProject(process.cwd());
175
+ const update = args.includes("--update-baseline");
176
+ const named = args.filter(arg => !arg.startsWith("--"));
177
+ const files = (named.length ? named.map(file => path.relative(root, path.resolve(file)).split(path.sep).join("/")) : walk(root, root).filter(file => matches(file, config.css)))
178
+ .filter(file => !matches(file, config.ignore));
179
+ const baselinePath = path.join(root, config.baseline);
180
+ const baseline = readBaseline(baselinePath);
181
+
182
+ if (update) {
183
+ const next = {};
184
+ for (const file of files) { const totals = count(checkCss(fs.readFileSync(path.join(root, file), "utf8"))); if (Object.keys(totals).length) next[file] = totals; }
185
+ fs.writeFileSync(baselinePath, `${JSON.stringify({ note: "Raw values that predate the Silicon UI checks. Counts may only go down: fix a file, then run silicon-ui-check --update-baseline.", files: next }, null, 2)}\n`);
186
+ const total = Object.values(next).reduce((sum, totals) => sum + Object.values(totals).reduce((a, b) => a + b, 0), 0);
187
+ console.log(`silicon-ui-check: baseline written to ${config.baseline}: ${total} raw values in ${Object.keys(next).length} files.`);
188
+ return 0;
189
+ }
190
+
191
+ let failed = 0, legacy = 0;
192
+ for (const file of files) {
193
+ const problems = checkCss(fs.readFileSync(path.join(root, file), "utf8"));
194
+ const over = overBaseline(problems, baseline[file]);
195
+ legacy += problems.length - over.length;
196
+ if (over.length) { failed++; console.error(report(file, problems.length > over.length ? problems : over, baseline[file])); }
197
+ }
198
+ if (failed) {
199
+ console.error(`\nsilicon-ui-check: ${failed} file${failed === 1 ? " uses" : "s use"} raw values where Silicon UI has tokens.`);
200
+ return 1;
201
+ }
202
+ console.log(`silicon-ui-check: ${files.length} files on tokens${legacy ? ` (${legacy} older raw values tolerated by the baseline; it only goes down)` : ""}.`);
203
+ return 0;
204
+ }
205
+
206
+ function runHook() {
207
+ let input = {};
208
+ try { input = JSON.parse(fs.readFileSync(0, "utf8") || "{}"); } catch { return 0; }
209
+ const target = input.tool_input?.file_path ?? input.tool_input?.path;
210
+ if (!target || !fs.existsSync(target)) return 0;
211
+ const { root, config } = findProject(path.dirname(target));
212
+ const file = path.relative(root, path.resolve(target)).split(path.sep).join("/");
213
+ const messages = [];
214
+
215
+ if (/\.css$/.test(file) && matches(file, config.css) && !matches(file, config.ignore)) {
216
+ const problems = checkCss(fs.readFileSync(target, "utf8"));
217
+ const allowed = readBaseline(path.join(root, config.baseline))[file];
218
+ if (overBaseline(problems, allowed).length) messages.push(report(file, problems, allowed));
219
+ }
220
+ if (/\.(?:[cm]?[jt]sx?)$/.test(file)) {
221
+ const eslint = path.join(root, "node_modules", ".bin", "eslint");
222
+ if (fs.existsSync(eslint)) {
223
+ const result = spawnSync(eslint, ["--no-warn-ignored", "--max-warnings=0", file], { cwd: root, encoding: "utf8" });
224
+ if (result.status) messages.push((result.stdout || result.stderr).trim());
225
+ }
226
+ }
227
+ if (!messages.length) return 0;
228
+ console.error(`Silicon UI check failed for ${file}. Fix these before moving on (docs: https://github.com/teamofsilicons/silicon-ui/tree/main/docs):\n\n${messages.join("\n\n")}`);
229
+ return 2;
230
+ }
231
+
232
+ // Run as a command (directly or through node_modules/.bin), not when imported.
233
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
234
+ const args = process.argv.slice(2);
235
+ process.exitCode = args.includes("--hook") ? runHook() : runCli(args);
236
+ }
@@ -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;