@teamofsilicons/silicon-ui 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +81 -2
  3. package/check.mjs +236 -0
  4. package/components/accordion/accordion.d.ts +13 -0
  5. package/components/accordion/accordion.js +35 -0
  6. package/components/accordion/accordion.module.css +15 -0
  7. package/components/action-button/action-button.d.ts +11 -0
  8. package/components/action-button/action-button.js +57 -0
  9. package/components/action-button/action-button.module.css +15 -0
  10. package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
  11. package/components/activity-heatmap/activity-heatmap.js +303 -0
  12. package/components/activity-heatmap/activity-heatmap.module.css +83 -0
  13. package/components/alert/alert.d.ts +13 -0
  14. package/components/alert/alert.js +79 -0
  15. package/components/alert/alert.module.css +15 -0
  16. package/components/animated-counter/animated-counter.d.ts +13 -0
  17. package/components/animated-counter/animated-counter.js +73 -0
  18. package/components/animated-counter/animated-counter.module.css +12 -0
  19. package/components/announcement-bar/announcement-bar.d.ts +53 -0
  20. package/components/announcement-bar/announcement-bar.js +187 -0
  21. package/components/announcement-bar/announcement-bar.module.css +60 -0
  22. package/components/avatar/avatar.d.ts +8 -0
  23. package/components/avatar/avatar.js +18 -0
  24. package/components/avatar/avatar.module.css +60 -0
  25. package/components/avatar-group/avatar-group.d.ts +13 -0
  26. package/components/avatar-group/avatar-group.js +24 -0
  27. package/components/avatar-group/avatar-group.module.css +27 -0
  28. package/components/badge/badge.d.ts +10 -0
  29. package/components/badge/badge.js +66 -0
  30. package/components/badge/badge.module.css +52 -0
  31. package/components/bar-chart/bar-chart.d.ts +32 -0
  32. package/components/bar-chart/bar-chart.js +327 -0
  33. package/components/bar-chart/bar-chart.module.css +49 -0
  34. package/components/billing-toggle/billing-toggle.d.ts +35 -0
  35. package/components/billing-toggle/billing-toggle.js +83 -0
  36. package/components/billing-toggle/billing-toggle.module.css +139 -0
  37. package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
  38. package/components/bottom-sheet/bottom-sheet.js +386 -0
  39. package/components/bottom-sheet/bottom-sheet.module.css +69 -0
  40. package/components/breadcrumb/breadcrumb.d.ts +13 -0
  41. package/components/breadcrumb/breadcrumb.js +19 -0
  42. package/components/breadcrumb/breadcrumb.module.css +12 -0
  43. package/components/brush-chart/brush-chart.d.ts +45 -0
  44. package/components/brush-chart/brush-chart.js +426 -0
  45. package/components/brush-chart/brush-chart.module.css +69 -0
  46. package/components/button/button.d.ts +10 -0
  47. package/components/button/button.js +97 -0
  48. package/components/button/button.module.css +39 -0
  49. package/components/button-group/button-group.d.ts +59 -0
  50. package/components/button-group/button-group.js +280 -0
  51. package/components/button-group/button-group.module.css +87 -0
  52. package/components/button-link/button-link.d.ts +24 -0
  53. package/components/button-link/button-link.js +11 -0
  54. package/components/button-link/button-link.module.css +7 -0
  55. package/components/calendar/calendar.d.ts +22 -0
  56. package/components/calendar/calendar.js +253 -0
  57. package/components/calendar/calendar.module.css +51 -0
  58. package/components/card/card.d.ts +19 -0
  59. package/components/card/card.js +108 -0
  60. package/components/card/card.module.css +51 -0
  61. package/components/card-stack/card-stack.d.ts +34 -0
  62. package/components/card-stack/card-stack.js +291 -0
  63. package/components/card-stack/card-stack.module.css +63 -0
  64. package/components/carousel/carousel.d.ts +28 -0
  65. package/components/carousel/carousel.js +332 -0
  66. package/components/carousel/carousel.module.css +42 -0
  67. package/components/chat-thread/chat-thread.d.ts +135 -0
  68. package/components/chat-thread/chat-thread.js +406 -0
  69. package/components/chat-thread/chat-thread.module.css +242 -0
  70. package/components/checkbox/checkbox.d.ts +7 -0
  71. package/components/checkbox/checkbox.js +23 -0
  72. package/components/checkbox/checkbox.module.css +15 -0
  73. package/components/chip-group/chip-group.d.ts +24 -0
  74. package/components/chip-group/chip-group.js +196 -0
  75. package/components/chip-group/chip-group.module.css +38 -0
  76. package/components/code-block/code-block.d.ts +11 -0
  77. package/components/code-block/code-block.js +133 -0
  78. package/components/code-block/code-block.module.css +43 -0
  79. package/components/color-picker/color-picker.d.ts +40 -0
  80. package/components/color-picker/color-picker.js +449 -0
  81. package/components/color-picker/color-picker.module.css +111 -0
  82. package/components/combobox/combobox.d.ts +19 -0
  83. package/components/combobox/combobox.js +167 -0
  84. package/components/combobox/combobox.module.css +28 -0
  85. package/components/command-palette/command-palette.d.ts +20 -0
  86. package/components/command-palette/command-palette.js +151 -0
  87. package/components/command-palette/command-palette.module.css +94 -0
  88. package/components/comment-thread/comment-thread.d.ts +120 -0
  89. package/components/comment-thread/comment-thread.js +343 -0
  90. package/components/comment-thread/comment-thread.module.css +195 -0
  91. package/components/confirm-morph/confirm-morph.d.ts +65 -0
  92. package/components/confirm-morph/confirm-morph.js +355 -0
  93. package/components/confirm-morph/confirm-morph.module.css +146 -0
  94. package/components/context-menu/context-menu.d.ts +18 -0
  95. package/components/context-menu/context-menu.js +148 -0
  96. package/components/context-menu/context-menu.module.css +17 -0
  97. package/components/copy-button/copy-button.d.ts +10 -0
  98. package/components/copy-button/copy-button.js +69 -0
  99. package/components/copy-button/copy-button.module.css +21 -0
  100. package/components/countdown/countdown.d.ts +31 -0
  101. package/components/countdown/countdown.js +177 -0
  102. package/components/countdown/countdown.module.css +49 -0
  103. package/components/date-picker/date-picker.d.ts +17 -0
  104. package/components/date-picker/date-picker.js +100 -0
  105. package/components/date-picker/date-picker.module.css +34 -0
  106. package/components/date-range-picker/date-range-picker.d.ts +44 -0
  107. package/components/date-range-picker/date-range-picker.js +534 -0
  108. package/components/date-range-picker/date-range-picker.module.css +214 -0
  109. package/components/dialog/dialog.d.ts +11 -0
  110. package/components/dialog/dialog.js +52 -0
  111. package/components/dialog/dialog.module.css +36 -0
  112. package/components/donut-chart/donut-chart.d.ts +46 -0
  113. package/components/donut-chart/donut-chart.js +382 -0
  114. package/components/donut-chart/donut-chart.module.css +48 -0
  115. package/components/drawer/drawer.d.ts +20 -0
  116. package/components/drawer/drawer.js +111 -0
  117. package/components/drawer/drawer.module.css +97 -0
  118. package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
  119. package/components/dropdown-menu/dropdown-menu.js +39 -0
  120. package/components/dropdown-menu/dropdown-menu.module.css +38 -0
  121. package/components/elastic-slider/elastic-slider.d.ts +39 -0
  122. package/components/elastic-slider/elastic-slider.js +261 -0
  123. package/components/elastic-slider/elastic-slider.module.css +40 -0
  124. package/components/empty-state/empty-state.d.ts +12 -0
  125. package/components/empty-state/empty-state.js +69 -0
  126. package/components/empty-state/empty-state.module.css +13 -0
  127. package/components/expandable-card/expandable-card.d.ts +13 -0
  128. package/components/expandable-card/expandable-card.js +89 -0
  129. package/components/expandable-card/expandable-card.module.css +22 -0
  130. package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
  131. package/components/expanding-button-group/expanding-button-group.js +258 -0
  132. package/components/expanding-button-group/expanding-button-group.module.css +44 -0
  133. package/components/expanding-search/expanding-search.d.ts +38 -0
  134. package/components/expanding-search/expanding-search.js +242 -0
  135. package/components/expanding-search/expanding-search.module.css +52 -0
  136. package/components/file-dropzone/file-dropzone.d.ts +44 -0
  137. package/components/file-dropzone/file-dropzone.js +379 -0
  138. package/components/file-dropzone/file-dropzone.module.css +115 -0
  139. package/components/file-upload/file-upload.d.ts +22 -0
  140. package/components/file-upload/file-upload.js +192 -0
  141. package/components/file-upload/file-upload.module.css +49 -0
  142. package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
  143. package/components/filter-toolbar/filter-toolbar.js +446 -0
  144. package/components/filter-toolbar/filter-toolbar.module.css +70 -0
  145. package/components/floating-button-group/floating-button-group.d.ts +51 -0
  146. package/components/floating-button-group/floating-button-group.js +215 -0
  147. package/components/floating-button-group/floating-button-group.module.css +74 -0
  148. package/components/gauge/gauge.d.ts +18 -0
  149. package/components/gauge/gauge.js +101 -0
  150. package/components/gauge/gauge.module.css +22 -0
  151. package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
  152. package/components/hold-to-confirm/hold-to-confirm.js +165 -0
  153. package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
  154. package/components/hover-card/hover-card.d.ts +38 -0
  155. package/components/hover-card/hover-card.js +145 -0
  156. package/components/hover-card/hover-card.module.css +20 -0
  157. package/components/image-compare/image-compare.d.ts +27 -0
  158. package/components/image-compare/image-compare.js +267 -0
  159. package/components/image-compare/image-compare.module.css +25 -0
  160. package/components/in-view-title/in-view-title.d.ts +17 -0
  161. package/components/in-view-title/in-view-title.js +70 -0
  162. package/components/in-view-title/in-view-title.module.css +13 -0
  163. package/components/inline-edit/inline-edit.d.ts +27 -0
  164. package/components/inline-edit/inline-edit.js +315 -0
  165. package/components/inline-edit/inline-edit.module.css +73 -0
  166. package/components/input/input.d.ts +7 -0
  167. package/components/input/input.js +60 -0
  168. package/components/input/input.module.css +22 -0
  169. package/components/json-viewer/json-viewer.d.ts +48 -0
  170. package/components/json-viewer/json-viewer.js +463 -0
  171. package/components/json-viewer/json-viewer.module.css +92 -0
  172. package/components/line-chart/line-chart.d.ts +58 -0
  173. package/components/line-chart/line-chart.js +383 -0
  174. package/components/line-chart/line-chart.module.css +60 -0
  175. package/components/mention-input/mention-input.d.ts +72 -0
  176. package/components/mention-input/mention-input.js +417 -0
  177. package/components/mention-input/mention-input.module.css +134 -0
  178. package/components/metric-card/metric-card.d.ts +8 -0
  179. package/components/metric-card/metric-card.js +44 -0
  180. package/components/metric-card/metric-card.module.css +18 -0
  181. package/components/money-input/money-input.d.ts +48 -0
  182. package/components/money-input/money-input.js +503 -0
  183. package/components/money-input/money-input.module.css +77 -0
  184. package/components/morph-select/morph-select.d.ts +50 -0
  185. package/components/morph-select/morph-select.js +395 -0
  186. package/components/morph-select/morph-select.module.css +88 -0
  187. package/components/multi-select/multi-select.d.ts +18 -0
  188. package/components/multi-select/multi-select.js +105 -0
  189. package/components/multi-select/multi-select.module.css +37 -0
  190. package/components/notification-center/notification-center.d.ts +24 -0
  191. package/components/notification-center/notification-center.js +146 -0
  192. package/components/notification-center/notification-center.module.css +79 -0
  193. package/components/number-field/number-field.d.ts +41 -0
  194. package/components/number-field/number-field.js +455 -0
  195. package/components/number-field/number-field.module.css +71 -0
  196. package/components/otp-input/otp-input.d.ts +13 -0
  197. package/components/otp-input/otp-input.js +129 -0
  198. package/components/otp-input/otp-input.module.css +28 -0
  199. package/components/pagination/pagination.d.ts +7 -0
  200. package/components/pagination/pagination.js +60 -0
  201. package/components/pagination/pagination.module.css +16 -0
  202. package/components/password-field/password-field.d.ts +14 -0
  203. package/components/password-field/password-field.js +51 -0
  204. package/components/password-field/password-field.module.css +27 -0
  205. package/components/password-strength/password-strength.d.ts +107 -0
  206. package/components/password-strength/password-strength.js +161 -0
  207. package/components/password-strength/password-strength.module.css +57 -0
  208. package/components/phone-input/phone-input.d.ts +73 -0
  209. package/components/phone-input/phone-input.js +612 -0
  210. package/components/phone-input/phone-input.module.css +98 -0
  211. package/components/popover/popover.d.ts +6 -0
  212. package/components/popover/popover.js +19 -0
  213. package/components/popover/popover.module.css +32 -0
  214. package/components/progress/progress.d.ts +8 -0
  215. package/components/progress/progress.js +42 -0
  216. package/components/progress/progress.module.css +1 -0
  217. package/components/radio-cards/radio-cards.d.ts +35 -0
  218. package/components/radio-cards/radio-cards.js +110 -0
  219. package/components/radio-cards/radio-cards.module.css +84 -0
  220. package/components/radio-group/radio-group.d.ts +13 -0
  221. package/components/radio-group/radio-group.js +55 -0
  222. package/components/radio-group/radio-group.module.css +30 -0
  223. package/components/resizable-panels/resizable-panels.d.ts +37 -0
  224. package/components/resizable-panels/resizable-panels.js +338 -0
  225. package/components/resizable-panels/resizable-panels.module.css +40 -0
  226. package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
  227. package/components/rich-text-editor/rich-text-editor.js +1733 -0
  228. package/components/rich-text-editor/rich-text-editor.module.css +201 -0
  229. package/components/ridgeline/ridgeline.d.ts +37 -0
  230. package/components/ridgeline/ridgeline.js +346 -0
  231. package/components/ridgeline/ridgeline.module.css +58 -0
  232. package/components/scroll-area/scroll-area.d.ts +40 -0
  233. package/components/scroll-area/scroll-area.js +153 -0
  234. package/components/scroll-area/scroll-area.module.css +79 -0
  235. package/components/search-field/search-field.d.ts +7 -0
  236. package/components/search-field/search-field.js +20 -0
  237. package/components/search-field/search-field.module.css +17 -0
  238. package/components/segmented-control/segmented-control.d.ts +16 -0
  239. package/components/segmented-control/segmented-control.js +56 -0
  240. package/components/segmented-control/segmented-control.module.css +15 -0
  241. package/components/select/select.d.ts +15 -0
  242. package/components/select/select.js +36 -0
  243. package/components/select/select.module.css +35 -0
  244. package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
  245. package/components/shortcut-recorder/shortcut-recorder.js +380 -0
  246. package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
  247. package/components/signature-pad/signature-pad.d.ts +72 -0
  248. package/components/signature-pad/signature-pad.js +450 -0
  249. package/components/signature-pad/signature-pad.module.css +84 -0
  250. package/components/skeleton/skeleton.d.ts +13 -0
  251. package/components/skeleton/skeleton.js +53 -0
  252. package/components/skeleton/skeleton.module.css +11 -0
  253. package/components/slider/slider.d.ts +45 -0
  254. package/components/slider/slider.js +347 -0
  255. package/components/slider/slider.module.css +45 -0
  256. package/components/slope-chart/slope-chart.d.ts +38 -0
  257. package/components/slope-chart/slope-chart.js +183 -0
  258. package/components/slope-chart/slope-chart.module.css +51 -0
  259. package/components/slot-text/slot-text.d.ts +26 -0
  260. package/components/slot-text/slot-text.js +119 -0
  261. package/components/slot-text/slot-text.module.css +19 -0
  262. package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
  263. package/components/sortable-data-table/sortable-data-table.js +240 -0
  264. package/components/sortable-data-table/sortable-data-table.module.css +116 -0
  265. package/components/sparkline/sparkline.d.ts +19 -0
  266. package/components/sparkline/sparkline.js +239 -0
  267. package/components/sparkline/sparkline.module.css +26 -0
  268. package/components/split-button/split-button.d.ts +19 -0
  269. package/components/split-button/split-button.js +33 -0
  270. package/components/split-button/split-button.module.css +51 -0
  271. package/components/stepper/stepper.d.ts +36 -0
  272. package/components/stepper/stepper.js +108 -0
  273. package/components/stepper/stepper.module.css +74 -0
  274. package/components/streamgraph/streamgraph.d.ts +51 -0
  275. package/components/streamgraph/streamgraph.js +376 -0
  276. package/components/streamgraph/streamgraph.module.css +55 -0
  277. package/components/swipe-actions/swipe-actions.d.ts +35 -0
  278. package/components/swipe-actions/swipe-actions.js +330 -0
  279. package/components/swipe-actions/swipe-actions.module.css +64 -0
  280. package/components/switch/switch.d.ts +7 -0
  281. package/components/switch/switch.js +45 -0
  282. package/components/switch/switch.module.css +12 -0
  283. package/components/tabs/tabs.d.ts +8 -0
  284. package/components/tabs/tabs.js +138 -0
  285. package/components/tabs/tabs.module.css +27 -0
  286. package/components/tag-input/tag-input.d.ts +15 -0
  287. package/components/tag-input/tag-input.js +150 -0
  288. package/components/tag-input/tag-input.module.css +34 -0
  289. package/components/text-morph/text-morph.d.ts +13 -0
  290. package/components/text-morph/text-morph.js +69 -0
  291. package/components/text-morph/text-morph.module.css +8 -0
  292. package/components/text-reveal/text-reveal.d.ts +14 -0
  293. package/components/text-reveal/text-reveal.js +20 -0
  294. package/components/text-reveal/text-reveal.module.css +13 -0
  295. package/components/text-shimmer/text-shimmer.d.ts +18 -0
  296. package/components/text-shimmer/text-shimmer.js +90 -0
  297. package/components/text-shimmer/text-shimmer.module.css +9 -0
  298. package/components/textarea/textarea.d.ts +7 -0
  299. package/components/textarea/textarea.js +56 -0
  300. package/components/textarea/textarea.module.css +22 -0
  301. package/components/theme-switch/theme-switch.d.ts +11 -0
  302. package/components/theme-switch/theme-switch.js +37 -0
  303. package/components/theme-switch/theme-switch.module.css +31 -0
  304. package/components/time-picker/time-picker.d.ts +13 -0
  305. package/components/time-picker/time-picker.js +95 -0
  306. package/components/time-picker/time-picker.module.css +23 -0
  307. package/components/timeline/timeline.d.ts +47 -0
  308. package/components/timeline/timeline.js +171 -0
  309. package/components/timeline/timeline.module.css +51 -0
  310. package/components/toast/toast.d.ts +8 -0
  311. package/components/toast/toast.js +118 -0
  312. package/components/toast/toast.module.css +100 -0
  313. package/components/toast-stack/toast-stack.d.ts +57 -0
  314. package/components/toast-stack/toast-stack.js +399 -0
  315. package/components/toast-stack/toast-stack.module.css +61 -0
  316. package/components/tooltip/tooltip.d.ts +8 -0
  317. package/components/tooltip/tooltip.js +62 -0
  318. package/components/tooltip/tooltip.module.css +18 -0
  319. package/components/tree-view/tree-view.d.ts +17 -0
  320. package/components/tree-view/tree-view.js +147 -0
  321. package/components/tree-view/tree-view.module.css +22 -0
  322. package/components/treemap/treemap.d.ts +35 -0
  323. package/components/treemap/treemap.js +375 -0
  324. package/components/treemap/treemap.module.css +59 -0
  325. package/components/usage-meter/usage-meter.d.ts +24 -0
  326. package/components/usage-meter/usage-meter.js +296 -0
  327. package/components/usage-meter/usage-meter.module.css +61 -0
  328. package/components/user-menu/user-menu.d.ts +62 -0
  329. package/components/user-menu/user-menu.js +316 -0
  330. package/components/user-menu/user-menu.module.css +97 -0
  331. package/components/waffle-chart/waffle-chart.d.ts +39 -0
  332. package/components/waffle-chart/waffle-chart.js +244 -0
  333. package/components/waffle-chart/waffle-chart.module.css +52 -0
  334. package/components.json +1330 -0
  335. package/eslint.js +53 -0
  336. package/lib/escape.d.ts +19 -0
  337. package/lib/escape.js +50 -0
  338. package/lib/media.d.ts +353 -0
  339. package/lib/media.js +65 -0
  340. package/lib/menu-highlight.d.ts +28 -0
  341. package/lib/menu-highlight.js +43 -0
  342. package/lib/motion-tokens.d.ts +60 -0
  343. package/lib/motion-tokens.js +25 -0
  344. package/lib/return-focus.d.ts +9 -0
  345. package/lib/return-focus.js +53 -0
  346. package/lib/silicon-provider.d.ts +60 -0
  347. package/lib/silicon-provider.js +47 -0
  348. package/lib/use-copy-feedback.d.ts +8 -0
  349. package/lib/use-copy-feedback.js +36 -0
  350. package/lib/use-today.d.ts +2 -0
  351. package/lib/use-today.js +29 -0
  352. package/package.json +76 -4
  353. package/styles.css +218 -0
  354. package/styles.css.d.ts +1 -0
@@ -0,0 +1,242 @@
1
+ .root {
2
+ display: flex;
3
+ height: 100%;
4
+ min-height: 0;
5
+ flex-direction: column;
6
+ color: var(--foreground);
7
+ font-family: var(--font-body);
8
+ font-size: var(--text-sm);
9
+ letter-spacing: var(--tracking-body);
10
+ line-height: var(--leading-body);
11
+ }
12
+
13
+ .viewport { position: relative; display: flex; min-height: 0; flex: 1; flex-direction: column; }
14
+ .scroller { min-height: 0; flex: 1; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-gutter: stable; }
15
+ /* The content springs up from where it was when the thread grows; the clip keeps that travel out of the scroll range. */
16
+ .track { overflow: clip; }
17
+ .content { padding: 12px 16px 14px; will-change: transform; }
18
+
19
+ .list { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
20
+
21
+ .day { display: flex; align-items: center; gap: 12px; padding: 20px 4px 4px; color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; }
22
+ .day::before, .day::after { height: 1px; flex: 1; background: var(--border-subtle); content: ""; }
23
+ .day:first-child { padding-top: 4px; }
24
+ .day > span:empty::before { content: "\00a0"; }
25
+
26
+ .row { position: relative; display: flex; align-items: flex-end; gap: 8px; margin-top: 2px; }
27
+ .row[data-first] { margin-top: 14px; }
28
+ .day + .row { margin-top: 10px; }
29
+ .row[data-mine] { justify-content: flex-end; }
30
+
31
+ .gutter { width: 28px; flex: none; align-self: flex-end; }
32
+ .stack { display: flex; min-width: 0; max-width: min(78%, 34rem); flex-direction: column; align-items: flex-start; gap: 4px; }
33
+ .row[data-mine] .stack { align-items: flex-end; }
34
+
35
+ .meta { display: flex; align-items: baseline; gap: 6px; margin: 0 0 2px; padding: 0 12px; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
36
+ .author { color: var(--text-secondary); font-weight: 500; }
37
+
38
+ .line { position: relative; display: flex; max-width: 100%; align-items: center; gap: 6px; }
39
+ /* The author's face sits in the gutter, level with the bottom of their last bubble rather than the reactions under it. */
40
+ .face { position: absolute; bottom: 0; left: -36px; display: flex; }
41
+ .row[data-mine] .line { flex-direction: row-reverse; }
42
+ .parts { display: flex; min-width: 0; flex-direction: column; align-items: flex-start; gap: 4px; }
43
+ .row[data-mine] .parts { align-items: flex-end; }
44
+
45
+ /* Bubbles in a group share a soft edge on the author's side, so a run of messages reads as one voice. */
46
+ .bubble {
47
+ --r: 18px;
48
+ --tight: 6px;
49
+ max-width: 100%;
50
+ padding: 8px 13px 9px;
51
+ border-radius: var(--r);
52
+ background: var(--surface-muted);
53
+ color: var(--foreground);
54
+ font-size: var(--text-sm);
55
+ overflow-wrap: anywhere;
56
+ white-space: pre-wrap;
57
+ }
58
+ .row:not([data-mine]):not([data-first]) .bubble { border-top-left-radius: var(--tight); }
59
+ .row:not([data-mine]):not([data-last]) .bubble { border-bottom-left-radius: var(--tight); }
60
+ .row[data-mine] .bubble { background: var(--accent); color: var(--accent-foreground); }
61
+ :global(:root[data-theme="dark"]) .row:not([data-mine]) .bubble { background: var(--surface-raised); }
62
+ .row[data-mine]:not([data-first]) .bubble { border-top-right-radius: var(--tight); }
63
+ .row[data-mine]:not([data-last]) .bubble { border-bottom-right-radius: var(--tight); }
64
+ .row[data-failed] .bubble { opacity: .6; }
65
+
66
+ .gallery { display: grid; width: min(240px, 100%); gap: 3px; grid-template-columns: 1fr; }
67
+ .gallery[data-count="2"],
68
+ .gallery[data-count="3"],
69
+ .gallery[data-count="4"] { grid-template-columns: 1fr 1fr; }
70
+ .gallery[data-count="3"] > :first-child { grid-column: span 2; aspect-ratio: 2 / 1; }
71
+ .image { position: relative; display: block; overflow: hidden; aspect-ratio: 1; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface-muted); }
72
+ .gallery[data-count="1"] .image { aspect-ratio: 4 / 3; }
73
+ .gallery:not([data-count="1"]) .image { border-radius: var(--radius-detail); corner-shape: var(--corner-shape); }
74
+ .gallery:not([data-count="1"]) { overflow: hidden; border-radius: var(--radius-control); corner-shape: var(--corner-shape); }
75
+ .image img { display: block; width: 100%; height: 100%; object-fit: cover; }
76
+ .more { position: absolute; inset: 0; display: grid; place-items: center; background: oklch(0% 0 0 / .45); color: #fff; font-size: var(--text-lg); font-weight: 500; }
77
+
78
+ .file {
79
+ display: flex; max-width: 100%; align-items: center; gap: 10px; padding: 9px 14px 9px 11px;
80
+ border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); color: var(--foreground); text-decoration: none;
81
+ transition: background-color var(--duration-fast) var(--ease-standard);
82
+ }
83
+ .file > svg { flex: none; color: var(--text-secondary); }
84
+ .fileText { display: grid; min-width: 0; }
85
+ .fileName { overflow: hidden; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
86
+ .fileSize { color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
87
+ @media (hover: hover) and (pointer: fine) { .file:hover { background: var(--surface-muted); } }
88
+
89
+ /* The reaction button waits beside the bubble and shows on hover or keyboard focus. */
90
+ /* The picker positions against the whole line, so it opens over the bubble and stays inside the thread. */
91
+ .pickerWrap { position: static; flex: none; }
92
+ .reactButton {
93
+ display: grid; width: 28px; height: 28px; place-items: center; padding: 0;
94
+ border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-muted); cursor: pointer; opacity: 0;
95
+ -webkit-tap-highlight-color: transparent;
96
+ transition: opacity var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
97
+ }
98
+ .row:hover .reactButton,
99
+ .row:focus-within .reactButton,
100
+ .pickerWrap[data-open] .reactButton { opacity: 1; }
101
+ .pickerWrap[data-open] .reactButton { background: var(--surface-muted); color: var(--foreground); }
102
+ @media (hover: hover) and (pointer: fine) { .reactButton:hover { background: var(--surface-muted); color: var(--foreground); } }
103
+ @media (hover: none) { .reactButton { opacity: .7; } }
104
+
105
+ .picker {
106
+ position: absolute; z-index: 5; bottom: calc(100% + 6px); right: -4px;
107
+ display: flex; gap: 2px; padding: 4px;
108
+ border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-raised); box-shadow: var(--shadow-floating);
109
+ transform-origin: calc(100% - 18px) 100%;
110
+ }
111
+ .picker[data-mine] { right: auto; left: -4px; transform-origin: 18px 100%; }
112
+ .pickerWrap[data-below] .picker { top: calc(100% + 6px); bottom: auto; transform-origin: calc(100% - 18px) 0; }
113
+ .pickerWrap[data-below] .picker[data-mine] { transform-origin: 18px 0; }
114
+ .emoji {
115
+ display: grid; width: 34px; height: 34px; place-items: center; padding: 0;
116
+ border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; cursor: pointer; font-size: 19px; line-height: 1;
117
+ -webkit-tap-highlight-color: transparent;
118
+ transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
119
+ }
120
+ .emoji:focus-visible { background: var(--surface-muted); }
121
+ .emoji:active { transform: scale(.9); }
122
+ @media (hover: hover) and (pointer: fine) { .emoji:hover { background: var(--surface-muted); transform: scale(1.12); } }
123
+
124
+ .reactionClip { overflow: hidden; margin: -2px -4px; padding: 2px 4px 2px 8px; }
125
+ .row[data-mine] .reactionClip { padding: 2px 8px 2px 4px; }
126
+ .reactions { display: flex; flex-wrap: wrap; gap: 4px; margin: 0; padding: 0; list-style: none; }
127
+ .row[data-mine] .reactions { justify-content: flex-end; }
128
+ .reaction {
129
+ display: inline-flex; height: 26px; align-items: center; gap: 5px; padding: 0 9px 0 7px;
130
+ border: 1px solid var(--border-subtle); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); color: var(--text-secondary); cursor: pointer;
131
+ font: inherit; font-size: var(--text-xs); font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums;
132
+ -webkit-tap-highlight-color: transparent;
133
+ transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
134
+ }
135
+ .reaction > span:first-child { font-size: 14px; }
136
+ .reaction[aria-pressed="true"] { border-color: color-mix(in oklab, var(--accent) 28%, transparent); background: color-mix(in oklab, var(--accent) 8%, var(--surface)); color: var(--foreground); }
137
+ @media (hover: hover) and (pointer: fine) { .reaction:hover { border-color: var(--border-strong); } }
138
+ .reaction:active { transform: scale(.95); }
139
+ .countRoll { display: inline-grid; overflow: hidden; font-variant-numeric: tabular-nums; }
140
+ .countRoll > span { grid-area: 1 / 1; }
141
+
142
+ .receipts { display: flex; min-height: 16px; align-items: center; gap: 6px; padding: 1px 6px 0; color: var(--text-muted); font-size: var(--text-xs); }
143
+ .readers { display: inline-flex; }
144
+ .reader { display: inline-flex; border-radius: 50%; corner-shape: round; box-shadow: 0 0 0 2px var(--surface); }
145
+ .reader + .reader { margin-left: -4px; }
146
+ .status[data-status="failed"] { display: inline-flex; align-items: center; gap: 8px; color: var(--danger); }
147
+ .retry {
148
+ display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px;
149
+ border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); color: var(--foreground); cursor: pointer;
150
+ font: inherit; font-weight: 500; line-height: 16px;
151
+ -webkit-tap-highlight-color: transparent;
152
+ transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard);
153
+ }
154
+ .retry > svg { color: var(--text-secondary); }
155
+ @media (hover: hover) and (pointer: fine) { .retry:hover { background: var(--surface-muted); border-color: var(--border-strong); } }
156
+
157
+ .avatar { display: inline-grid; flex: none; overflow: hidden; place-items: center; border-radius: 50%; corner-shape: round; background: var(--surface-muted); color: var(--text-secondary); font-weight: 500; }
158
+ .avatar img { width: 100%; height: 100%; object-fit: cover; }
159
+
160
+ .typing { display: grid; height: 36px; box-sizing: border-box; place-items: center; padding: 0 14px; }
161
+ .dots { display: inline-flex; gap: 4px; }
162
+ .dots i { width: 6px; height: 6px; border-radius: 50%; corner-shape: round; background: var(--text-muted); animation: breathe 1.2s var(--ease-in-out) infinite; }
163
+ .dots i:nth-child(2) { animation-delay: .16s; }
164
+ .dots i:nth-child(3) { animation-delay: .32s; }
165
+ @keyframes breathe {
166
+ 0%, 60%, 100% { opacity: .35; transform: translateY(0); }
167
+ 30% { opacity: 1; transform: translateY(-3px); }
168
+ }
169
+ @media (prefers-reduced-motion: reduce) { .dots i { animation: none; opacity: .6; } }
170
+
171
+ .pill {
172
+ position: absolute; z-index: 6; bottom: 12px; left: 50%; translate: -50% 0;
173
+ display: inline-flex; height: 32px; align-items: center; gap: 6px; padding: 0 14px 0 11px;
174
+ border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-raised); box-shadow: var(--shadow-floating); color: var(--foreground); cursor: pointer;
175
+ font: inherit; font-size: var(--text-xs); font-weight: 500; white-space: nowrap;
176
+ -webkit-tap-highlight-color: transparent;
177
+ }
178
+ .pill > span { display: inline-flex; gap: .3em; }
179
+ .pill > .pillIcon { display: inline-grid; place-items: center; }
180
+ .pill:active { scale: .97; }
181
+
182
+ /* Composer. */
183
+ .composer { display: grid; padding: 10px 12px 12px; border-top: 1px solid var(--border-subtle); transition: background-color var(--duration-fast) var(--ease-standard); }
184
+ .composer[data-dragging] { background: var(--accent-subtle); }
185
+ .trayClip { overflow: hidden; }
186
+ .tray { display: flex; gap: 8px; margin: 0; padding: 8px 8px 10px 4px; overflow-x: auto; list-style: none; scrollbar-width: none; }
187
+ .pending { position: relative; flex: none; }
188
+ .pendingImage { display: block; width: 56px; height: 56px; border-radius: var(--radius-control); corner-shape: var(--corner-shape); object-fit: cover; }
189
+ .pendingFile { display: grid; width: 168px; height: 56px; box-sizing: border-box; align-content: center; column-gap: 8px; padding: 0 12px; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); grid-template-columns: auto minmax(0, 1fr); }
190
+ .pendingFile > svg { grid-row: span 2; align-self: center; color: var(--text-secondary); }
191
+ .pendingName { overflow: hidden; font-size: var(--text-xs); font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
192
+ .pendingSize { color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
193
+ .remove {
194
+ position: absolute; top: -6px; right: -6px; display: grid; width: 20px; height: 20px; place-items: center; padding: 0;
195
+ border: 2px solid var(--surface); border-radius: 50%; corner-shape: round; background: var(--foreground); color: var(--background); cursor: pointer;
196
+ }
197
+
198
+ .inputRow { display: flex; align-items: flex-end; gap: 6px; }
199
+ .attach {
200
+ display: grid; width: 40px; height: 40px; flex: none; place-items: center; margin-bottom: 1px; padding: 0;
201
+ border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-secondary); cursor: pointer;
202
+ -webkit-tap-highlight-color: transparent;
203
+ transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
204
+ }
205
+ .attach:active { transform: scale(.94); }
206
+ @media (hover: hover) and (pointer: fine) { .attach:hover { background: var(--surface-muted); color: var(--foreground); } }
207
+
208
+ /* The field and the send button share one rounded box; the button rides the bottom edge as the text grows. */
209
+ .box {
210
+ position: relative; display: flex; min-width: 0; flex: 1; align-items: flex-end;
211
+ border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface);
212
+ transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard);
213
+ }
214
+ .box:focus-within { border-color: var(--border-strong); }
215
+ .composer[data-dragging] .box { border-color: var(--accent); border-style: dashed; }
216
+ .field { position: relative; min-width: 0; flex: 1; overflow: hidden; border-radius: var(--radius-control); corner-shape: var(--corner-shape); }
217
+ .mirror,
218
+ .textarea {
219
+ box-sizing: border-box; width: 100%; margin: 0; padding: 9px 6px 9px 16px; border: 0;
220
+ font: inherit; font-size: var(--text-base); letter-spacing: var(--tracking-body); line-height: 22px;
221
+ overflow-wrap: break-word; white-space: pre-wrap; word-break: break-word;
222
+ }
223
+ .mirror { position: absolute; top: 0; left: 0; visibility: hidden; pointer-events: none; }
224
+ .textarea { position: absolute; inset: 0; height: 100%; display: block; resize: none; overflow-y: auto; background: transparent; color: var(--foreground); outline: none; scrollbar-width: none; }
225
+ .textarea::placeholder { color: var(--text-muted); }
226
+
227
+ .send {
228
+ position: relative; display: grid; width: 32px; height: 32px; flex: none; place-items: center; margin: 0 4px 4px 0; overflow: hidden; padding: 0;
229
+ border: 0; border-radius: 50%; corner-shape: round; background: transparent; color: var(--text-muted); cursor: default;
230
+ -webkit-tap-highlight-color: transparent;
231
+ transition: background-color var(--duration-standard) var(--ease-standard), color var(--duration-standard) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
232
+ }
233
+ .send[data-ready] { background: var(--accent); color: var(--accent-foreground); cursor: pointer; }
234
+ .send[data-ready]:active { transform: scale(.92); }
235
+ .sendIcon { display: grid; grid-area: 1 / 1; place-items: center; }
236
+
237
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
238
+
239
+ @media (max-width: 420px) {
240
+ .content { padding-inline: 12px; }
241
+ .stack { max-width: 84%; }
242
+ }
@@ -0,0 +1,7 @@
1
+ import type { ComponentPropsWithoutRef } from "react";
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
+ export interface CheckboxProps extends ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> {
4
+ label?: string;
5
+ description?: string;
6
+ }
7
+ export declare const Checkbox: import("react").ForwardRefExoticComponent<CheckboxProps & import("react").RefAttributes<HTMLButtonElement>>;
@@ -0,0 +1,23 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useId, useState } from "react";
4
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5
+ import { motion, useReducedMotion } from "motion/react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./checkbox.module.css";
8
+ /** Both marks share three points, so the check morphs into the dash and back instead of swapping. */
9
+ const checkPath = "M4.25 9.25 L7.25 12.25 L13.75 5.75";
10
+ const dashPath = "M4.75 9 L9 9 L13.25 9";
11
+ export const Checkbox = forwardRef(function Checkbox({ label, description, id, className, checked, defaultChecked, onCheckedChange, ...props }, ref) {
12
+ const generatedId = useId();
13
+ const controlId = id ?? generatedId;
14
+ const reduced = useReducedMotion();
15
+ const [internal, setInternal] = useState(defaultChecked ?? false);
16
+ const state = checked ?? internal;
17
+ const on = state !== false;
18
+ const change = (next) => { if (checked === undefined)
19
+ setInternal(next); onCheckedChange?.(next); };
20
+ const fade = { duration: on ? motionTokens.duration.instant : motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };
21
+ return _jsxs("div", { className: styles.field, children: [_jsx(CheckboxPrimitive.Root, { ...props, id: controlId, ref: ref, checked: state, onCheckedChange: change, className: [styles.box, className].filter(Boolean).join(" "), "aria-describedby": description ? `${controlId}-description` : undefined, "aria-label": props["aria-label"] ?? (label ? undefined : "Checkbox"), children: _jsxs("span", { className: styles.visual, "aria-hidden": "true", children: [_jsx(motion.span, { className: styles.fill, initial: false, animate: { opacity: on ? 1 : 0, scale: on ? 1 : .6 }, transition: reduced ? { duration: 0 } : { scale: motionTokens.spring.snappy, opacity: fade } }), _jsx("svg", { className: styles.mark, viewBox: "0 0 18 18", fill: "none", focusable: "false", children: _jsx(motion.path, { initial: false, animate: { d: state === "indeterminate" ? dashPath : checkPath, pathLength: on ? 1 : 0, opacity: on ? 1 : 0 }, transition: reduced ? { duration: 0 } : { d: motionTokens.spring.morph, pathLength: motionTokens.spring.snappy, opacity: fade }, stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", strokeLinejoin: "round" }) })] }) }), label || description ? _jsxs("div", { className: styles.copy, children: [label ? _jsx("label", { htmlFor: controlId, children: label }) : null, description ? _jsx("span", { id: `${controlId}-description`, children: description }) : null] }) : null] });
22
+ });
23
+ Checkbox.displayName = "Checkbox";
@@ -0,0 +1,15 @@
1
+ .field { display: inline-flex; align-items: flex-start; gap: 0; min-width: 0; }
2
+ .box { position: relative; flex: 0 0 auto; display: grid; place-items: center; width: var(--control-height-md); height: var(--control-height-md); border: 0; border-radius: var(--radius-control); padding: 0; background: transparent; color: var(--control-glyph); cursor: pointer; -webkit-tap-highlight-color: transparent; }
3
+ /* The square is the only part that reacts to a press, so the hit area and label never move. */
4
+ .visual { position: relative; display: block; width: 18px; height: 18px; border: 1px solid var(--border-strong); border-radius: var(--radius-detail); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
5
+ .fill { position: absolute; inset: -1px; border-radius: inherit; corner-shape: inherit; background: var(--control-on); }
6
+ .mark { position: absolute; inset: -1px; width: 18px; height: 18px; overflow: visible; }
7
+ .box[data-state="checked"] .visual, .box[data-state="indeterminate"] .visual { border-color: var(--control-on); }
8
+ @media (hover: hover) and (pointer: fine) { .box[data-state="unchecked"]:hover:not(:disabled) .visual { border-color: var(--text-muted); } }
9
+ .box:active:not(:disabled) .visual { transform: scale(.95); transition: border-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
10
+ .box:disabled { opacity: .5; cursor: not-allowed; }
11
+ /* The label's first line centers on the square: (44px box - 19.6px line) / 2. */
12
+ .copy { display: grid; gap: 2px; min-width: 0; margin-left: 1px; padding-top: calc((var(--control-height-md) - var(--text-sm) * 1.4) / 2); }
13
+ .copy label { color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); cursor: pointer; }
14
+ .copy span { color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
15
+ @media (prefers-reduced-motion: reduce) { .visual, .box:active:not(:disabled) .visual { transition: none; transform: none; } }
@@ -0,0 +1,24 @@
1
+ export interface ChipOption {
2
+ value: string;
3
+ label: string;
4
+ }
5
+ /**
6
+ * Selectable filter chips for narrowing a list by facets people toggle often, such as topics, statuses, or tags.
7
+ * Selecting morphs the chip: a check grows in, the label slides over, and the chip's edge follows on a spring while its
8
+ * neighbours glide to their new places, even across lines. Long sets fold behind a “+N more” chip that opens with a height morph.
9
+ * Arrow keys move between chips, Space or Enter toggles, and each chip is a pressed or unpressed button.
10
+ */
11
+ export interface ChipGroupProps {
12
+ options: ChipOption[];
13
+ value: string[];
14
+ onValueChange: (value: string[]) => void;
15
+ /** Accessible name of the group, such as “Topics”. */
16
+ label: string;
17
+ /** Allow several chips at once. In single mode the selected chip can still be cleared. */
18
+ multiple?: boolean;
19
+ /** Chips shown before the rest fold behind a “+N more” chip. Chips selected when it folds stay in view. */
20
+ maxVisible?: number;
21
+ className?: string;
22
+ }
23
+ export declare function ChipGroup({ options, value, onValueChange, label, multiple, maxVisible, className }: ChipGroupProps): import("react").JSX.Element;
24
+ export default ChipGroup;
@@ -0,0 +1,196 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
4
+ import { AnimatePresence, LayoutGroup, animate, motion, useIsPresent, useMotionValue, useReducedMotion, useTransform } from "motion/react";
5
+ import { motionTokens } from "../../lib/motion-tokens.js";
6
+ import styles from "./chip-group.module.css";
7
+ /** Roving-focus key of the overflow chip; the NUL keeps it from colliding with any option value. */
8
+ const MORE = "\u0000more";
9
+ const standard = [...motionTokens.ease.standard];
10
+ const enterEase = [...motionTokens.ease.enter];
11
+ const fade = { duration: motionTokens.duration.fast, ease: standard };
12
+ const reducedFade = { duration: .15, ease: standard };
13
+ const exitFast = { duration: motionTokens.duration.fast, ease: standard };
14
+ const shown = { opacity: 1, y: "0em", scale: 1, filter: "blur(0px)" };
15
+ const textIn = { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` };
16
+ const textOut = { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: exitFast };
17
+ /**
18
+ * A chip's layout width changes in one frame (so the row can re-flow once and neighbours can glide), but what you see must not.
19
+ * `lag` is how far the visible edge trails the layout edge: it jumps by the change, then springs back to zero.
20
+ * Passive resizes, like a late web font, only re-baseline it.
21
+ */
22
+ function useWidthLag(node, key, reduce) {
23
+ const lag = useMotionValue(0);
24
+ const width = useRef(null);
25
+ useLayoutEffect(() => {
26
+ const element = node.current;
27
+ if (!element)
28
+ return;
29
+ const next = element.offsetWidth, previous = width.current;
30
+ width.current = next;
31
+ if (previous === null || previous === next)
32
+ return;
33
+ if (reduce) {
34
+ lag.jump(0);
35
+ return;
36
+ }
37
+ // Retarget from wherever the edge is now and keep its speed, so fast toggling never snaps.
38
+ const velocity = lag.getVelocity();
39
+ lag.jump(lag.get() + previous - next);
40
+ animate(lag, 0, { ...motionTokens.spring.morph, velocity });
41
+ }, [key, lag, node, reduce]);
42
+ useEffect(() => {
43
+ const element = node.current;
44
+ if (!element || typeof ResizeObserver === "undefined")
45
+ return;
46
+ const observer = new ResizeObserver(() => { width.current = element.offsetWidth; });
47
+ observer.observe(element);
48
+ return () => observer.disconnect();
49
+ }, [node]);
50
+ return lag;
51
+ }
52
+ /** Follows the chips' height. After `morphKey` changes (a selection, opening the overflow) it springs from the old height to the new one, then returns to auto. It clips only while moving, so nothing is cut off at rest. */
53
+ function HeightFrame({ morphKey, reduce, children }) {
54
+ const frame = useRef(null);
55
+ const content = useRef(null);
56
+ const height = useMotionValue("auto");
57
+ const changedAt = useRef(0);
58
+ const last = useRef(undefined);
59
+ const animating = useRef(false);
60
+ /* Pin the old height in the same commit as the change, before Motion measures the chips' new layout. Otherwise the chips
61
+ are measured against an unpinned frame, the centred demo shifts by half the growth for that measurement, and the first row
62
+ sinks and floats back while the height springs. */
63
+ useLayoutEffect(() => {
64
+ changedAt.current = performance.now();
65
+ const node = frame.current;
66
+ if (reduce || !node || last.current === undefined)
67
+ return;
68
+ const current = height.get();
69
+ const from = typeof current === "number" ? current : last.current;
70
+ height.jump(from);
71
+ Object.assign(node.style, { overflow: "clip", height: `${from}px`, minHeight: `${from}px` });
72
+ // If the change leaves the height alone, no resize follows, so release the pin after a frame.
73
+ let inner = 0;
74
+ const outer = requestAnimationFrame(() => { inner = requestAnimationFrame(() => { if (animating.current)
75
+ return; height.jump("auto"); if (frame.current)
76
+ Object.assign(frame.current.style, { overflow: "", height: "auto", minHeight: "" }); }); });
77
+ return () => { cancelAnimationFrame(outer); cancelAnimationFrame(inner); };
78
+ }, [morphKey, reduce, height]);
79
+ useEffect(() => {
80
+ const node = content.current;
81
+ if (!node || typeof ResizeObserver === "undefined")
82
+ return;
83
+ let controls;
84
+ const settle = () => { animating.current = false; height.jump("auto"); if (frame.current)
85
+ Object.assign(frame.current.style, { overflow: "", height: "auto", minHeight: "" }); };
86
+ // The minimum follows the moving height, so a flex parent short on room cannot squeeze the frame mid-morph and snap it when the morph ends.
87
+ const unfollow = height.on("change", value => { if (frame.current)
88
+ frame.current.style.minHeight = typeof value === "number" ? `${value}px` : ""; });
89
+ const observer = new ResizeObserver(([entry]) => {
90
+ if (!entry)
91
+ return;
92
+ const next = entry.borderBoxSize?.[0]?.blockSize ?? node.offsetHeight;
93
+ const current = height.get();
94
+ const from = typeof current === "number" ? current : last.current;
95
+ last.current = next;
96
+ controls?.stop();
97
+ if (reduce || from === undefined || Math.abs(from - next) < .5 || performance.now() - changedAt.current > 160)
98
+ return settle();
99
+ // Pin the old height before this frame paints, then spring to the new one.
100
+ if (frame.current)
101
+ Object.assign(frame.current.style, { overflow: "clip", height: `${from}px`, minHeight: `${from}px` });
102
+ animating.current = true;
103
+ controls = animate(height, [from, next], { ...motionTokens.spring.smooth, onComplete: settle });
104
+ });
105
+ observer.observe(node);
106
+ return () => { observer.disconnect(); unfollow(); controls?.stop(); };
107
+ }, [height, reduce]);
108
+ return _jsx(motion.div, { ref: frame, className: styles.frame, style: { height }, children: _jsx("div", { ref: content, className: styles.content, children: children }) });
109
+ }
110
+ /** Outgoing copies are hidden from assistive tech while they fade, so the button reads only its current text.
111
+ * `follow` is the centring offset of the current line; a leaving line holds the offset it had when it left, so the new line's centring never drags it sideways. */
112
+ function Swap({ follow, style, ...props }) {
113
+ const present = useIsPresent();
114
+ const held = useMotionValue(0);
115
+ // Runs before the chip re-measures its width in the same commit, so this reads the offset from before the change.
116
+ useLayoutEffect(() => { if (!present && follow)
117
+ held.jump(follow.get()); }, [present, follow, held]);
118
+ return _jsx(motion.span, { ...props, style: follow ? { ...style, x: present ? follow : held } : style, "aria-hidden": present ? props["aria-hidden"] : true });
119
+ }
120
+ /** The width the check takes from the label: a 14px glyph and the space after it. */
121
+ const SLOT = 18;
122
+ function Chip({ option, selected, tabbable, reduce, delay, onToggle, onFocusChip, ref }) {
123
+ const present = useIsPresent();
124
+ const body = useRef(null);
125
+ const lag = useWidthLag(body, String(selected), reduce);
126
+ /* How far the check has grown: the slot it was given, less the width the label still lags behind. It follows both inputs
127
+ explicitly, so a jump with no animation (reduced motion) still lands the check in its final state. */
128
+ const slot = useRef(selected ? SLOT : 0);
129
+ const grown = useMotionValue(selected ? 1 : 0);
130
+ const edge = useTransform(lag, value => -value);
131
+ // The check grows from zero width in exactly the gap the label opens as it slides over, so the two never overlap, and deselecting runs it backwards.
132
+ const checkScale = useTransform(grown, value => Math.min(value, 1.1));
133
+ const checkOpacity = useTransform(grown, value => Math.min(1, value * 1.4));
134
+ const checkBlur = useTransform(grown, value => value >= 1 ? "none" : `blur(${((1 - value) * motionTokens.blur.subtle).toFixed(2)}px)`);
135
+ /* The stroke draws in step with the growth, so the check writes itself as the label makes room. */
136
+ const checkDraw = useTransform(grown, value => Math.min(1, Math.max(.001, value)));
137
+ // Declared after the transforms so they are already subscribed when a reduced-motion selection jumps straight to the end.
138
+ useLayoutEffect(() => {
139
+ slot.current = selected ? SLOT : 0;
140
+ const update = () => grown.set(Math.max(0, (slot.current + lag.get()) / SLOT));
141
+ update();
142
+ return lag.on("change", update);
143
+ }, [selected, lag, grown]);
144
+ // Revealed chips grow in with a short stagger; chips that fold away leave faster than they came.
145
+ const enter = reduce ? { layout: { duration: 0 }, default: reducedFade } : { layout: motionTokens.spring.morph, default: { ...motionTokens.spring.snappy, delay }, opacity: { ...fade, delay } };
146
+ return _jsx(motion.button, { ref: ref, type: "button", className: styles.chip, "data-chip": option.value, "aria-pressed": selected, "aria-hidden": present ? undefined : true, tabIndex: present && tabbable ? 0 : -1, onClick: () => onToggle(option.value), onFocus: () => onFocusChip(option.value), layout: "position", initial: reduce ? { opacity: 0 } : { opacity: 0, scale: .9 }, animate: { opacity: 1, scale: 1 }, exit: reduce ? { opacity: 0, transition: reducedFade } : { opacity: 0, scale: .9, transition: { duration: motionTokens.duration.instant, ease: standard } }, transition: enter, children: _jsxs("span", { ref: body, className: styles.body, "data-selected": selected, children: [_jsx(motion.span, { className: styles.surface, style: { right: edge }, "aria-hidden": "true" }), _jsx(motion.span, { className: styles.check, style: { scale: checkScale, opacity: checkOpacity, filter: checkBlur }, "aria-hidden": "true", children: _jsx("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", children: _jsx(motion.path, { d: "M4 12.5 9.5 18 20 6.5", style: { pathLength: checkDraw } }) }) }), _jsx("span", { className: styles.slot, "aria-hidden": "true" }), _jsx(motion.span, { className: styles.label, style: { x: lag }, children: option.label })] }) });
147
+ }
148
+ function MoreChip({ hidden, expanded, tabbable, reduce, onToggle, onFocusChip, ref }) {
149
+ const body = useRef(null);
150
+ const text = expanded ? "Show less" : `+${hidden} more`;
151
+ const lag = useWidthLag(body, text, reduce);
152
+ const edge = useTransform(lag, value => -value);
153
+ // The text stays centred on the visible surface while its edge catches up.
154
+ const centre = useTransform(lag, value => value / 2);
155
+ return _jsx(motion.button, { ref: ref, type: "button", className: `${styles.chip} ${styles.more}`, "data-more": "", "aria-expanded": expanded, tabIndex: tabbable ? 0 : -1, onClick: onToggle, onFocus: () => onFocusChip(MORE), layout: "position", transition: { layout: reduce ? { duration: 0 } : motionTokens.spring.morph }, children: _jsxs("span", { ref: body, className: styles.body, children: [_jsx(motion.span, { className: styles.surface, style: { right: edge }, "aria-hidden": "true" }), _jsx("span", { className: styles.moreText, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(Swap, { follow: centre, className: styles.moreLine, initial: reduce ? { opacity: 0 } : textIn, animate: shown, exit: reduce ? { opacity: 0, transition: reducedFade } : textOut, transition: reduce ? reducedFade : { duration: motionTokens.duration.standard, ease: enterEase }, children: text }, text) }) })] }) });
156
+ }
157
+ export function ChipGroup({ options, value, onValueChange, label, multiple = true, maxVisible = Infinity, className }) {
158
+ const id = useId();
159
+ const reduce = !!useReducedMotion();
160
+ const [expanded, setExpanded] = useState(false);
161
+ // Chips selected when the overflow folds stay in view, so a selection is never hidden and deselecting never makes a chip vanish.
162
+ const [pinned, setPinned] = useState(value);
163
+ const [active, setActive] = useState(null);
164
+ const foldable = options.length > maxVisible;
165
+ const visible = !foldable || expanded ? options : options.filter((option, index) => index < maxVisible || pinned.includes(option.value) || value.includes(option.value));
166
+ const hidden = options.length - visible.length;
167
+ const showMore = foldable && (expanded || hidden > 0);
168
+ const keys = [...visible.map(option => option.value), ...(showMore ? [MORE] : [])];
169
+ // One chip holds the tab stop: the last one focused, else the first selected, else the first.
170
+ const tabStop = active !== null && keys.includes(active) ? active : visible.find(option => value.includes(option.value))?.value ?? keys[0];
171
+ function toggle(next) {
172
+ const on = value.includes(next);
173
+ if (!multiple)
174
+ return onValueChange(on ? [] : [next]);
175
+ onValueChange(options.filter(option => option.value === next ? !on : value.includes(option.value)).map(option => option.value));
176
+ }
177
+ function toggleMore() {
178
+ if (expanded)
179
+ setPinned(value);
180
+ setExpanded(!expanded);
181
+ }
182
+ function onKeyDown(event) {
183
+ const buttons = Array.from(event.currentTarget.querySelectorAll(":is(button[data-chip], button[data-more]):not([aria-hidden='true'])"));
184
+ const index = buttons.indexOf(document.activeElement);
185
+ if (index < 0)
186
+ return;
187
+ const last = buttons.length - 1;
188
+ const moves = { ArrowRight: index === last ? 0 : index + 1, ArrowDown: index === last ? 0 : index + 1, ArrowLeft: index === 0 ? last : index - 1, ArrowUp: index === 0 ? last : index - 1, Home: 0, End: last };
189
+ if (!(event.key in moves))
190
+ return;
191
+ event.preventDefault();
192
+ buttons[moves[event.key]].focus();
193
+ }
194
+ return _jsx(LayoutGroup, { id: id, children: _jsx(HeightFrame, { morphKey: `${expanded}|${value.join(",")}`, reduce: reduce, children: _jsx("div", { className: [styles.group, className].filter(Boolean).join(" "), role: "group", "aria-label": label, onKeyDown: onKeyDown, children: _jsxs(AnimatePresence, { mode: "popLayout", initial: false, children: [visible.map((option, index) => _jsx(Chip, { option: option, selected: value.includes(option.value), tabbable: tabStop === option.value, reduce: reduce, delay: expanded ? Math.min(Math.max(0, index - maxVisible) * motionTokens.stagger.item, .3) : 0, onToggle: toggle, onFocusChip: setActive }, option.value)), showMore ? _jsx(MoreChip, { hidden: hidden, expanded: expanded, tabbable: tabStop === MORE, reduce: reduce, onToggle: toggleMore, onFocusChip: setActive }, MORE) : null] }) }) }) });
195
+ }
196
+ export default ChipGroup;
@@ -0,0 +1,38 @@
1
+ /* The frame follows the chip rows' height on a spring; the rows re-flow once and every chip glides to its new place. */
2
+ .frame { min-width: 0; }
3
+ .content { position: relative; }
4
+ .group { position: relative; isolation: isolate; display: flex; flex-wrap: wrap; gap: var(--space-related); }
5
+
6
+ /* The button is only the hit area and the layout box. What you see is the surface, which trails the box on a spring while the width morphs. */
7
+ .chip { position: relative; display: inline-flex; max-width: 100%; padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: none; color: var(--text-secondary); font: inherit; font-size: var(--text-sm); font-weight: 500; letter-spacing: var(--tracking-body); line-height: 20px; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard); }
8
+ /* Press answers on the body with the individual scale property, so it never fights the layout transform on the button. */
9
+ .body { position: relative; isolation: isolate; display: inline-flex; min-width: 0; height: var(--control-height-sm); align-items: center; padding: 0 14px; transition: scale var(--duration-spring) var(--ease-spring); }
10
+ .chip:active .body { scale: .97; transition-duration: var(--duration-instant); transition-timing-function: var(--ease-standard); }
11
+ .surface { position: absolute; z-index: -1; top: 0; bottom: 0; left: 0; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard); }
12
+ /* The check grows from its left edge where the label started; the slot hands its width to the label in one layout step, and the label's offset springs it over. */
13
+ .check { position: absolute; top: calc(50% - 7px); left: 12px; display: grid; width: 14px; height: 14px; place-items: center; color: var(--accent-strong); transform-origin: 0 50%; }
14
+ .check svg { display: block; overflow: visible; }
15
+ .slot { width: 0; flex: none; }
16
+ .body[data-selected="true"] .slot { width: 18px; }
17
+ .label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
18
+
19
+ /* Selected reads as a quiet tint of the accent (neutral by default) plus the check, never color alone. */
20
+ .body[data-selected="true"] .surface { border-color: color-mix(in oklab, var(--accent) 42%, var(--border)); background: color-mix(in oklab, var(--accent) 11%, var(--surface)); }
21
+ .chip[aria-pressed="true"] { color: var(--foreground); }
22
+ @media (hover: hover) and (pointer: fine) {
23
+ .chip:hover { color: var(--foreground); }
24
+ .chip:hover .surface { border-color: var(--border-strong); }
25
+ .chip[aria-pressed="true"]:hover .surface { border-color: color-mix(in oklab, var(--accent) 60%, var(--border)); background: color-mix(in oklab, var(--accent) 16%, var(--surface)); }
26
+ }
27
+
28
+ /* The overflow chip travels under the chips it reveals, so they seem to come out of it. */
29
+ .chip:not(.more) { z-index: 1; }
30
+ .more { color: var(--text-muted); }
31
+ .more .surface { border-color: var(--border); background: var(--surface-muted); }
32
+ .moreText { position: relative; display: inline-flex; justify-content: center; font-variant-numeric: tabular-nums; }
33
+ .moreLine { display: block; white-space: nowrap; }
34
+
35
+ @media (prefers-reduced-motion: reduce) {
36
+ .chip, .body, .surface { transition: none; }
37
+ .chip:active .body { scale: none; }
38
+ }
@@ -0,0 +1,11 @@
1
+ export interface CodeBlockProps {
2
+ /** Source shown in the block and copied by the action. */
3
+ code: string;
4
+ /** File name displayed in the header. */
5
+ filename?: string;
6
+ /** Used for the header label and lightweight syntax highlighting. */
7
+ language?: string;
8
+ /** Collapse longer sources to this many lines, with a toggle that expands the rest in place. */
9
+ maxLines?: number;
10
+ }
11
+ export declare function CodeBlock({ code, filename, language, maxLines }: CodeBlockProps): import("react").JSX.Element;