@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,20 @@
1
+ /* The button hugs its label; the fill is a second, inverted copy of the face revealed by clip-path, so the text flips colour exactly at the edge. */
2
+ /* The default tone follows the selected accent; danger stays available for a truly irreversible action. */
3
+ .button { --hold-ink: var(--foreground); --hold-fill: var(--accent); --hold-on-fill: var(--accent-foreground); --hold-border: var(--border-strong); position: relative; display: inline-flex; min-height: var(--control-height-md); align-items: center; justify-content: center; padding: 0 var(--space-comfort); border: 1px solid var(--hold-border); border-radius: var(--radius-control); background: var(--surface); color: var(--hold-ink); font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); cursor: pointer; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
4
+ .button[data-tone="danger"] { --hold-ink: var(--danger); --hold-fill: var(--danger); --hold-on-fill: var(--background); --hold-border: color-mix(in oklab, var(--danger) 32%, var(--border)); }
5
+ .button[data-tone="neutral"] { --hold-ink: var(--foreground); --hold-fill: var(--foreground); --hold-on-fill: var(--background); --hold-border: var(--border-strong); }
6
+ @media (hover: hover) and (pointer: fine) { .button:not(:disabled):not([aria-disabled="true"]):hover { background: color-mix(in oklab, var(--hold-fill) 6%, var(--surface)); } }
7
+ .button[aria-disabled="true"] { cursor: default; }
8
+ .button:disabled { cursor: not-allowed; opacity: .5; }
9
+ .face { display: inline-flex; align-items: center; gap: var(--space-related); white-space: nowrap; }
10
+ /* Covers the border too, and keeps its own rounded corners; the clip only ever cuts a straight leading edge. */
11
+ .fill { position: absolute; inset: -1px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-control); background: var(--hold-fill); color: var(--hold-on-fill); pointer-events: none; }
12
+ .iconSlot { display: grid; width: 18px; height: 18px; flex: none; place-items: center; }
13
+ .iconPhase { display: grid; grid-area: 1 / 1; place-items: center; }
14
+ .iconPhase svg { width: 18px; height: 18px; }
15
+ /* Clips the outgoing label while the frame narrows, with room for the rise and the soft blur. */
16
+ .labelFrame { position: relative; display: inline-flex; min-width: 0; clip-path: inset(-.6em -3px); }
17
+ .label { display: block; white-space: nowrap; }
18
+ .measure { position: absolute; top: 0; left: 0; visibility: hidden; white-space: nowrap; pointer-events: none; }
19
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
20
+ @media (prefers-reduced-motion: reduce) { .button { transition: none; } }
@@ -0,0 +1,38 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ /**
3
+ * A rich preview of a person or link that opens after a short hover or keyboard focus, for mentions, avatars, and references where a click should stay free for navigation.
4
+ * It waits half a second, opens almost at once when moving between triggers, stays open while the pointer travels into it, and closes fast on leave or Escape.
5
+ * On touch, a tap toggles it. Keep the content read only; anything actionable belongs behind the trigger itself.
6
+ */
7
+ export interface HoverCardProps {
8
+ /** The trigger, such as a mention button or a link. It must accept a ref and be focusable. */
9
+ children: ReactElement;
10
+ /** The preview. `HoverCardProfile` covers people; any read only content works. */
11
+ content: ReactNode;
12
+ side?: "top" | "bottom" | "left" | "right";
13
+ align?: "start" | "center" | "end";
14
+ /** Milliseconds of hover before the first card opens. */
15
+ openDelay?: number;
16
+ /** Milliseconds of grace after the pointer leaves, so it can travel into the card. */
17
+ closeDelay?: number;
18
+ className?: string;
19
+ }
20
+ export declare function HoverCard({ children, content, side, align, openDelay, closeDelay, className }: HoverCardProps): import("react").JSX.Element;
21
+ export interface HoverCardStat {
22
+ label: string;
23
+ value: string | number;
24
+ }
25
+ /** A person preview for `HoverCard`: portrait, name, role, a short bio, a couple of numbers, and a quiet footer line. Its rows settle in with a tiny stagger. */
26
+ export interface HoverCardProfileProps {
27
+ name: string;
28
+ /** Role and team, such as "Product designer, Payments". */
29
+ role?: string;
30
+ /** A portrait URL, or your own image element such as next/image. Initials show when it is missing. */
31
+ avatar?: string | ReactNode;
32
+ bio?: string;
33
+ stats?: HoverCardStat[];
34
+ /** A footer line, such as a location and local time. */
35
+ meta?: ReactNode;
36
+ }
37
+ export declare function HoverCardProfile({ name, role, avatar, bio, stats, meta }: HoverCardProfileProps): import("react").JSX.Element;
38
+ export default HoverCard;
@@ -0,0 +1,145 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useCallback, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
5
+ import { AnimatePresence, animate, motion, useMotionValue, usePresence, useReducedMotion } from "motion/react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./hover-card.module.css";
8
+ const OPEN_DELAY = 500;
9
+ const CLOSE_DELAY = 140;
10
+ /** Moving to another trigger within this window opens its card almost at once. */
11
+ const SKIP_WINDOW = 300;
12
+ /** Even warm, a trigger waits this long, so a pointer crossing it on the way into an open card does not steal the card. */
13
+ const INTENT = 80;
14
+ /** How far the card starts toward its trigger, in px. */
15
+ const TRAVEL = 4;
16
+ const enter = [...motionTokens.ease.enter];
17
+ const standard = [...motionTokens.ease.standard];
18
+ const IDLE = { id: null, instant: false };
19
+ let snapshot = IDLE;
20
+ let closedAt = -Infinity;
21
+ const listeners = new Set();
22
+ /** One hover card is open at a time on the page. While one is open, or just after it closes, the next one opens without waiting and only fades. */
23
+ const cards = {
24
+ subscribe(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; },
25
+ get: () => snapshot,
26
+ server: () => IDLE,
27
+ warm: () => snapshot.id !== null || performance.now() - closedAt < SKIP_WINDOW,
28
+ open(id) { if (snapshot.id === id)
29
+ return; snapshot = { id, instant: cards.warm() }; listeners.forEach(listener => listener()); },
30
+ close(id) { if (snapshot.id !== id)
31
+ return; snapshot = IDLE; closedAt = performance.now(); listeners.forEach(listener => listener()); },
32
+ };
33
+ /** Grows from the trigger edge: scale from .96 around the placed origin and a 4px offset toward the trigger on one smooth spring, with its own fade.
34
+ * Both retarget from where they are if the pointer comes back mid-exit, so the card turns around instead of jumping. */
35
+ const Surface = forwardRef(function Surface({ instant, reduced, className, style, children, ...props }, forwardedRef) {
36
+ const [isPresent, safeToRemove] = usePresence();
37
+ const node = useRef(null);
38
+ const progress = useMotionValue(0);
39
+ const x = useMotionValue(0), y = useMotionValue(0), scale = useMotionValue(1), opacity = useMotionValue(0);
40
+ // Decided when the card mounts: a card revived mid-exit keeps the path it came in on, even though the page is now warm.
41
+ const [still] = useState(() => instant || reduced);
42
+ const setRef = useCallback((element) => {
43
+ node.current = element;
44
+ if (typeof forwardedRef === "function")
45
+ forwardedRef(element);
46
+ else if (forwardedRef)
47
+ forwardedRef.current = element;
48
+ }, [forwardedRef]);
49
+ useEffect(() => {
50
+ let cancelled = false;
51
+ // The side is read from the placed card every frame, so a card that flips above its trigger still grows from the trigger edge.
52
+ const apply = (value) => {
53
+ const side = node.current?.dataset.side;
54
+ const travel = still ? 0 : (1 - value) * TRAVEL;
55
+ x.set(side === "left" ? travel : side === "right" ? -travel : 0);
56
+ y.set(side === "top" ? travel : side === "bottom" ? -travel : 0);
57
+ scale.set(still ? 1 : .96 + .04 * value);
58
+ };
59
+ const unsubscribe = progress.on("change", apply);
60
+ apply(progress.get());
61
+ const controls = isPresent
62
+ ? animate(progress, 1, still ? { duration: .14, ease: enter } : motionTokens.spring.smooth)
63
+ : animate(progress, 0, { duration: .12, ease: standard });
64
+ // The fade leads the grow on the way in and runs alongside it on the way out; it starts from the current opacity either way.
65
+ const fade = isPresent
66
+ ? animate(opacity, 1, { duration: reduced ? .15 : still ? .09 : .16, ease: enter })
67
+ : animate(opacity, 0, { duration: reduced ? .1 : .12, ease: standard });
68
+ if (!isPresent)
69
+ fade.then(() => { if (!cancelled)
70
+ safeToRemove?.(); });
71
+ return () => { cancelled = true; unsubscribe(); controls.stop(); fade.stop(); };
72
+ }, [isPresent, still, reduced, progress, x, y, scale, opacity, safeToRemove]);
73
+ return _jsx(motion.div, { ...props, ref: setRef, className: [styles.card, className].filter(Boolean).join(" "), style: { ...style, x, y, scale, opacity }, children: children });
74
+ });
75
+ export function HoverCard({ children, content, side = "bottom", align = "start", openDelay = OPEN_DELAY, closeDelay = CLOSE_DELAY, className }) {
76
+ const id = useId();
77
+ const cardId = `${id}-card`;
78
+ const state = useSyncExternalStore(cards.subscribe, cards.get, cards.server);
79
+ const open = state.id === id;
80
+ const reduced = useReducedMotion() ?? false;
81
+ const triggerRef = useRef(null);
82
+ const timers = useRef({ open: 0, close: 0 });
83
+ const inside = useRef({ trigger: false, card: false, focus: false });
84
+ /** Set by Escape, so a pointer resting on the trigger does not reopen what was just dismissed. */
85
+ const dismissed = useRef(false);
86
+ const pointerType = useRef("");
87
+ useEffect(() => {
88
+ const pending = timers.current;
89
+ return () => { window.clearTimeout(pending.open); window.clearTimeout(pending.close); cards.close(id); };
90
+ }, [id]);
91
+ const clearTimers = () => { window.clearTimeout(timers.current.open); window.clearTimeout(timers.current.close); };
92
+ const show = () => { clearTimers(); dismissed.current = false; cards.open(id); };
93
+ const hide = () => { clearTimers(); cards.close(id); };
94
+ const scheduleOpen = () => {
95
+ window.clearTimeout(timers.current.close);
96
+ if (dismissed.current || cards.get().id === id)
97
+ return;
98
+ window.clearTimeout(timers.current.open);
99
+ timers.current.open = window.setTimeout(() => cards.open(id), cards.warm() ? INTENT : openDelay);
100
+ };
101
+ const scheduleClose = () => {
102
+ window.clearTimeout(timers.current.open);
103
+ const { trigger, card, focus } = inside.current;
104
+ if (trigger || card || focus)
105
+ return;
106
+ window.clearTimeout(timers.current.close);
107
+ timers.current.close = window.setTimeout(() => cards.close(id), closeDelay);
108
+ };
109
+ const mouse = (event) => event.pointerType !== "touch";
110
+ return _jsxs(PopoverPrimitive.Root, { open: open, onOpenChange: next => { if (!next)
111
+ hide(); }, children: [_jsx(PopoverPrimitive.Anchor, { asChild: true, ref: triggerRef, "aria-describedby": open ? cardId : undefined, "data-hover-card": open ? "open" : "closed", onPointerEnter: (event) => { if (!mouse(event))
112
+ return; inside.current.trigger = true; scheduleOpen(); }, onPointerLeave: (event) => { if (!mouse(event))
113
+ return; inside.current.trigger = false; dismissed.current = false; scheduleClose(); }, onPointerDown: (event) => { pointerType.current = event.pointerType; }, onClick: () => {
114
+ // A tap toggles the card on touch; a click or Enter opens it without the wait.
115
+ const isOpen = cards.get().id === id;
116
+ if (pointerType.current === "touch") {
117
+ if (isOpen)
118
+ hide();
119
+ else
120
+ show();
121
+ }
122
+ else if (!isOpen)
123
+ show();
124
+ pointerType.current = "";
125
+ }, onFocus: (event) => { if (!event.currentTarget.matches(":focus-visible"))
126
+ return; inside.current.focus = true; show(); }, onBlur: () => { inside.current.focus = false; dismissed.current = false; scheduleClose(); }, children: children }), _jsx(AnimatePresence, { children: open && _jsx(PopoverPrimitive.Portal, { forceMount: true, children: _jsx(PopoverPrimitive.Content, { forceMount: true, asChild: true, id: cardId, role: "tooltip", side: side, align: align, sideOffset: 8, collisionPadding: 12, onOpenAutoFocus: event => event.preventDefault(), onCloseAutoFocus: event => event.preventDefault(), onEscapeKeyDown: () => { dismissed.current = true; }, onInteractOutside: event => { if (triggerRef.current?.contains(event.target))
127
+ event.preventDefault(); }, onPointerEnter: (event) => { if (!mouse(event))
128
+ return; inside.current.card = true; window.clearTimeout(timers.current.close); }, onPointerLeave: (event) => { if (!mouse(event))
129
+ return; inside.current.card = false; scheduleClose(); }, children: _jsx(Surface, { instant: state.instant, reduced: reduced, className: className, children: content }) }) }, "card") })] });
130
+ }
131
+ function Portrait({ src }) {
132
+ const image = useRef(null);
133
+ // A cached portrait shows at once; one still downloading fades in from a soft blur.
134
+ useLayoutEffect(() => { const node = image.current; if (node && !node.complete)
135
+ node.dataset.loading = ""; }, [src]);
136
+ // eslint-disable-next-line @next/next/no-img-element -- registry components stay framework agnostic; pass next/image through `avatar` instead.
137
+ return _jsx("img", { ref: image, src: src, alt: "", width: 48, height: 48, decoding: "async", onLoad: event => { delete event.currentTarget.dataset.loading; } });
138
+ }
139
+ export function HoverCardProfile({ name, role, avatar, bio, stats = [], meta }) {
140
+ const reduced = useReducedMotion() ?? false;
141
+ const initials = name.trim().split(/\s+/).slice(0, 2).map(part => part[0]?.toUpperCase()).join("");
142
+ const settle = (order) => reduced ? {} : { initial: { opacity: 0, y: 4 }, animate: { opacity: 1, y: 0 }, transition: { duration: motionTokens.duration.standard, ease: enter, delay: .04 + order * motionTokens.stagger.item } };
143
+ return _jsxs("div", { className: styles.profile, children: [_jsxs(motion.div, { className: styles.head, ...settle(0), children: [_jsx("span", { className: styles.avatar, "aria-hidden": "true", children: typeof avatar === "string" ? _jsx(Portrait, { src: avatar }) : avatar ?? _jsx("span", { className: styles.initials, children: initials }) }), _jsxs("span", { className: styles.identity, children: [_jsx("span", { className: styles.name, children: name }), role && _jsx("span", { className: styles.role, children: role })] })] }), bio && _jsx(motion.p, { className: styles.bio, ...settle(1), children: bio }), stats.length > 0 && _jsx(motion.dl, { className: styles.stats, ...settle(2), children: stats.map(stat => _jsxs("div", { className: styles.stat, children: [_jsx("dt", { children: stat.label }), _jsx("dd", { children: stat.value })] }, stat.label)) }), meta && _jsx(motion.div, { className: styles.meta, ...settle(3), children: meta })] });
144
+ }
145
+ export default HoverCard;
@@ -0,0 +1,20 @@
1
+ /* A floating layer, so it earns the floating shadow. Transforms pivot on the placed origin, the corner nearest the trigger. */
2
+ .card { z-index: 60; box-sizing: border-box; width: 18.5rem; max-width: min(18.5rem, var(--radix-popover-content-available-width, calc(100vw - 24px))); padding: var(--space-inset); border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); font-family: var(--font-body); letter-spacing: var(--tracking-body); transform-origin: var(--radix-popover-content-transform-origin); outline: none; }
3
+ .profile { display: grid; gap: var(--space-group); }
4
+ .head { display: flex; min-width: 0; align-items: center; gap: var(--space-group); }
5
+ .avatar { position: relative; display: grid; width: 48px; height: 48px; flex: none; place-items: center; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-muted); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; }
6
+ .avatar img { display: block; width: 100%; height: 100%; object-fit: cover; transition: opacity var(--duration-standard) var(--ease-enter), filter var(--duration-standard) var(--ease-enter); }
7
+ .avatar img[data-loading] { opacity: 0; filter: blur(4px); transition: none; }
8
+ .identity { display: grid; min-width: 0; }
9
+ .name, .role { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
10
+ .name { color: var(--foreground); font-size: var(--text-base); font-weight: 500; line-height: var(--leading-body); }
11
+ .role { color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); }
12
+ .bio { margin: 0; color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); }
13
+ /* Numbers lead and labels follow, while the markup keeps label then value for screen readers. */
14
+ .stats { display: flex; flex-wrap: wrap; gap: var(--space-related) var(--space-section); margin: 0; padding-top: var(--space-group); border-top: 1px solid var(--border-subtle); }
15
+ .stat { display: flex; flex-direction: column-reverse; }
16
+ .stat dt { color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
17
+ .stat dd { margin: 0; color: var(--foreground); font-size: var(--text-base); font-variant-numeric: tabular-nums; font-weight: 500; line-height: var(--leading-body); }
18
+ .meta { display: flex; align-items: center; gap: 6px; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
19
+ .meta svg { flex: none; }
20
+ @media (prefers-reduced-motion: reduce) { .avatar img { transition: none; } .avatar img[data-loading] { filter: none; } }
@@ -0,0 +1,27 @@
1
+ import { type ReactNode } from "react";
2
+ export interface ImageCompareProps {
3
+ /** The original, shown on the left (or top). Pass an image with its own alt text; it is sized to cover the frame. */
4
+ before: ReactNode;
5
+ /** The result, shown on the right (or bottom). */
6
+ after: ReactNode;
7
+ /** Where the divider sits, in percent from the left (or top). */
8
+ position?: number;
9
+ defaultPosition?: number;
10
+ onPositionChange?: (position: number) => void;
11
+ /** Vertical stacks the images top and bottom. Changing it swings the divider a quarter turn instead of swapping layouts. */
12
+ orientation?: "horizontal" | "vertical";
13
+ /** Captions over each side. They fade as the divider reaches them. Pass false to hide them. */
14
+ labels?: [string, string] | false;
15
+ /** Accessible name of the divider. */
16
+ label?: string;
17
+ /** Frame proportions, as a CSS aspect-ratio. */
18
+ aspectRatio?: string;
19
+ className?: string;
20
+ }
21
+ /**
22
+ * Before and after, split by a divider. Drag anywhere to move it: the handle follows 1:1, rubber-bands past the edges, and stays where it
23
+ * is released. A press on the image springs the divider there; on touch the press waits to see a sideways drag so the page still scrolls.
24
+ * Arrow keys step 1% (Shift for 10%), Home and End reveal one side fully, and a double-click on the handle springs it back to the middle.
25
+ */
26
+ export declare function ImageCompare({ before, after, position, defaultPosition, onPositionChange, orientation, labels, label, aspectRatio, className }: ImageCompareProps): import("react").JSX.Element;
27
+ export default ImageCompare;
@@ -0,0 +1,267 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import { animate, motion, useMotionValue, useReducedMotion, useTransform } from "motion/react";
5
+ import { ChevronLeft, ChevronRight } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./image-compare.module.css";
8
+ const { spring } = motionTokens;
9
+ /** Motion drops velocity on time-defined springs, so hand-offs run the same springs written as stiffness and damping, settling far below a pixel. */
10
+ const physical = ({ visualDuration, bounce }) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta: .002, restSpeed: .02 }; };
11
+ const moveSpring = physical(spring.snappy);
12
+ const turnSpring = physical(spring.morph);
13
+ const kickSpring = physical(spring.morph);
14
+ /** Rubber-band travel past an edge, the handle's inset from the frame, how far it gives inside that inset, and a key press at a limit. */
15
+ const STRETCH = 9, MARGIN = 42, GIVE = 12, BUMP_PX = 150, INSET = 12;
16
+ const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
17
+ const lerp = (from, to, amount) => from + (to - from) * amount;
18
+ /** iOS style resistance: travel past a limit gives less and less, and never more than `limit` pixels. */
19
+ const rubber = (distance, limit = STRETCH) => Math.sign(distance) * (1 - 1 / (Math.abs(distance) * .55 / limit + 1)) * limit;
20
+ /** The handle keeps the line's pace in the middle and slows smoothly near an edge, so it is never cut off and never stops dead. */
21
+ const give = (distance) => GIVE * (1 - 1 / (distance / GIVE + 1));
22
+ const soften = (at, size) => at < MARGIN ? MARGIN - give(MARGIN - at) : at > size - MARGIN ? size - MARGIN + give(at - size + MARGIN) : at;
23
+ const fade = (room) => clamp(room / 28, 0, 1);
24
+ /** The before side of a line through `center` at `angle`, cut from the frame: the clip while the divider turns. */
25
+ function halfPlane(w, h, center, angle) {
26
+ const nx = -Math.cos(angle), ny = -Math.sin(angle);
27
+ const side = ([x, y]) => (x - center[0]) * nx + (y - center[1]) * ny;
28
+ const corners = [[0, 0], [w, 0], [w, h], [0, h]];
29
+ const points = [];
30
+ corners.forEach((corner, index) => {
31
+ const next = corners[(index + 1) % 4], a = side(corner), b = side(next);
32
+ if (a >= 0)
33
+ points.push(corner);
34
+ if ((a >= 0) !== (b >= 0)) {
35
+ const t = a / (a - b);
36
+ points.push([corner[0] + (next[0] - corner[0]) * t, corner[1] + (next[1] - corner[1]) * t]);
37
+ }
38
+ });
39
+ return points.length ? `polygon(${points.map(([x, y]) => `${x.toFixed(2)}px ${y.toFixed(2)}px`).join(", ")})` : "inset(50%)";
40
+ }
41
+ /**
42
+ * Before and after, split by a divider. Drag anywhere to move it: the handle follows 1:1, rubber-bands past the edges, and stays where it
43
+ * is released. A press on the image springs the divider there; on touch the press waits to see a sideways drag so the page still scrolls.
44
+ * Arrow keys step 1% (Shift for 10%), Home and End reveal one side fully, and a double-click on the handle springs it back to the middle.
45
+ */
46
+ export function ImageCompare({ before, after, position, defaultPosition = 50, onPositionChange, orientation = "horizontal", labels = ["Before", "After"], label = "Before and after", aspectRatio = "3 / 2", className }) {
47
+ const reduced = useReducedMotion();
48
+ const vertical = orientation === "vertical";
49
+ const [internal, setInternal] = useState(defaultPosition);
50
+ const current = clamp(position ?? internal, 0, 100);
51
+ const afterShare = Math.round(100 - current);
52
+ const rootRef = useRef(null);
53
+ const handleRef = useRef(null);
54
+ const beforeChip = useRef(null);
55
+ const afterChip = useRef(null);
56
+ const latest = useRef(current);
57
+ const goal = useRef(current);
58
+ const drag = useRef(null);
59
+ const pressedHandle = useRef(false);
60
+ const [dragging, setDragging] = useState(false);
61
+ const [quiet, setQuiet] = useState(false);
62
+ // The divider is a committed position plus a catch-up offset: a press springs the offset to zero while the pointer moves the position
63
+ // 1:1, so the divider glides to the finger and never trails it.
64
+ const pos = useMotionValue(current), lag = useMotionValue(0);
65
+ const theta = useMotionValue(vertical ? 90 : 0);
66
+ const metrics = useMotionValue({ w: 0, h: 0, bw: 0, bh: 0, aw: 0, ah: 0 });
67
+ const shown = useTransform(() => pos.get() + lag.get());
68
+ const turn = useTransform(() => clamp(theta.get() / 90, 0, 1));
69
+ /** The divider's pivot in pixels, blending from the horizontal to the vertical layout as it turns. */
70
+ const center = (p, m) => [lerp((p / 100) * m.w, m.w / 2, turn.get()), lerp(m.h / 2, (p / 100) * m.h, turn.get())];
71
+ // At rest every style is a percentage, so the server renders it and a resize never lags; only a turn in progress uses pixels.
72
+ const clipPath = useTransform(() => {
73
+ const angle = theta.get(), p = clamp(shown.get(), 0, 100), m = metrics.get();
74
+ if (angle === 0 || !m.w)
75
+ return angle >= 45 ? `inset(0 0 ${(100 - p).toFixed(3)}% 0)` : `inset(0 ${(100 - p).toFixed(3)}% 0 0)`;
76
+ if (angle === 90)
77
+ return `inset(0 0 ${(100 - p).toFixed(3)}% 0)`;
78
+ return halfPlane(m.w, m.h, center(p, m), (angle * Math.PI) / 180);
79
+ });
80
+ const rest = (angle, m) => angle === 0 || angle === 90 || !m.w;
81
+ const pivotX = useTransform(() => { const angle = theta.get(), m = metrics.get(); return rest(angle, m) ? (angle >= 45 ? "50%" : `${shown.get()}%`) : `${center(shown.get(), m)[0]}px`; });
82
+ const pivotY = useTransform(() => { const angle = theta.get(), m = metrics.get(); return rest(angle, m) ? (angle >= 45 ? `${shown.get()}%` : "50%") : `${center(shown.get(), m)[1]}px`; });
83
+ /** How far the handle sits from the line along the drag axis, so it stays whole near an edge. */
84
+ const offset = () => { const m = metrics.get(); if (!m.w)
85
+ return 0; const p = shown.get(), x = (p / 100) * m.w, y = (p / 100) * m.h; return lerp(soften(x, m.w) - x, soften(y, m.h) - y, turn.get()); };
86
+ const handleX = useTransform(() => offset() * Math.cos((theta.get() * Math.PI) / 180));
87
+ const handleY = useTransform(() => offset() * Math.sin((theta.get() * Math.PI) / 180));
88
+ // Captions fade as the divider reaches them, and hand over mid-turn when the after caption changes corner.
89
+ const beforeOpacity = useTransform(() => {
90
+ const m = metrics.get(), k = turn.get(), p = clamp(shown.get(), 0, 100), across = clamp(1 - k * 3, 0, 1), down = clamp(k * 3 - 2, 0, 1);
91
+ if (!m.w)
92
+ return across + down;
93
+ return across * fade((p / 100) * m.w - (INSET + m.bw + 8)) + down * fade((p / 100) * m.h - (INSET + m.bh + 8));
94
+ });
95
+ const afterAcross = useTransform(() => { const m = metrics.get(), k = clamp(1 - turn.get() * 3, 0, 1); return m.w ? k * fade(m.w - INSET - m.aw - 8 - (clamp(shown.get(), 0, 100) / 100) * m.w) : k; });
96
+ const afterDown = useTransform(() => { const m = metrics.get(), k = clamp(turn.get() * 3 - 2, 0, 1); return m.w ? k * fade(m.h - INSET - m.ah - 8 - (clamp(shown.get(), 0, 100) / 100) * m.h) : k; });
97
+ useLayoutEffect(() => {
98
+ const root = rootRef.current;
99
+ if (!root || typeof ResizeObserver === "undefined")
100
+ return;
101
+ const measure = () => metrics.set({ w: root.clientWidth, h: root.clientHeight, bw: beforeChip.current?.offsetWidth ?? 0, bh: beforeChip.current?.offsetHeight ?? 0, aw: afterChip.current?.offsetWidth ?? 0, ah: afterChip.current?.offsetHeight ?? 0 });
102
+ const observer = new ResizeObserver(measure);
103
+ [root, beforeChip.current, afterChip.current].forEach(node => node && observer.observe(node));
104
+ measure();
105
+ return () => observer.disconnect();
106
+ }, [metrics]);
107
+ // A new orientation swings the divider a quarter turn about its pivot while the clip follows it.
108
+ useEffect(() => {
109
+ const target = vertical ? 90 : 0;
110
+ if (theta.get() === target)
111
+ return;
112
+ if (reduced)
113
+ theta.jump(target);
114
+ else
115
+ animate(theta, target, turnSpring);
116
+ }, [reduced, theta, vertical]);
117
+ // Positions that arrive from outside a drag (props, keys, a double-click) spring the divider there.
118
+ useLayoutEffect(() => {
119
+ latest.current = current;
120
+ if (drag.current?.started || goal.current === current)
121
+ return;
122
+ goal.current = current;
123
+ const from = pos.get() + lag.get();
124
+ lag.jump(0);
125
+ pos.jump(from);
126
+ if (reduced)
127
+ pos.jump(current);
128
+ else
129
+ animate(pos, current, moveSpring);
130
+ });
131
+ function commit(next, live = false) {
132
+ if (live && Math.round(next) === Math.round(latest.current))
133
+ return;
134
+ const value = Number(next.toFixed(2));
135
+ if (value === latest.current)
136
+ return;
137
+ latest.current = value;
138
+ if (position === undefined)
139
+ setInternal(value);
140
+ onPositionChange?.(value);
141
+ }
142
+ const percentAt = (event) => {
143
+ const rect = rootRef.current.getBoundingClientRect();
144
+ return vertical ? ((event.clientY - rect.top) / (rect.height || 1)) * 100 : ((event.clientX - rect.left) / (rect.width || 1)) * 100;
145
+ };
146
+ const axisSize = () => { const rect = rootRef.current?.getBoundingClientRect(); return (vertical ? rect?.height : rect?.width) || 1; };
147
+ /** Focus that follows a pointer keeps the handle quiet: no ring until a key is pressed. */
148
+ function focusQuietly() {
149
+ const node = handleRef.current;
150
+ if (!node || document.activeElement === node)
151
+ return;
152
+ setQuiet(true);
153
+ node.focus({ preventScroll: true });
154
+ }
155
+ function start(state, at) {
156
+ const from = pos.get() + lag.get();
157
+ state.started = true;
158
+ state.grab = state.press ? 0 : at - from;
159
+ lag.jump(0);
160
+ pos.jump(from);
161
+ setDragging(true);
162
+ focusQuietly();
163
+ follow(state, at, true);
164
+ }
165
+ /** Puts the line under the pointer, with resistance past the edges, and commits the position once per whole percent. */
166
+ function follow(state, at, first = false) {
167
+ const raw = at - state.grab, size = axisSize();
168
+ const edge = clamp(raw, 0, 100);
169
+ const placed = reduced ? edge : edge + (rubber(((raw - edge) / 100) * size) / size) * 100;
170
+ state.raw = raw;
171
+ if (first && state.press && !reduced) {
172
+ const from = pos.get();
173
+ pos.jump(placed);
174
+ lag.jump(from - placed);
175
+ animate(lag, 0, moveSpring);
176
+ }
177
+ else
178
+ pos.set(placed);
179
+ commit(edge, true);
180
+ }
181
+ function onPointerDown(event) {
182
+ if (event.button !== 0 || !event.isPrimary || !rootRef.current)
183
+ return;
184
+ const onHandle = !!event.target.closest("[data-handle]");
185
+ pressedHandle.current = onHandle;
186
+ event.currentTarget.setPointerCapture(event.pointerId);
187
+ const state = { pointer: event.pointerId, grab: 0, press: !onHandle, started: false, x: event.clientX, y: event.clientY, raw: 0 };
188
+ drag.current = state;
189
+ // A finger on the photo might be scrolling the page, so it waits for a sideways drag; a mouse or the handle answers at once.
190
+ if (event.pointerType !== "touch" || onHandle)
191
+ start(state, percentAt(event));
192
+ }
193
+ function onPointerMove(event) {
194
+ const state = drag.current;
195
+ if (!state || state.pointer !== event.pointerId)
196
+ return;
197
+ if (!state.started) {
198
+ const along = Math.abs(vertical ? event.clientY - state.y : event.clientX - state.x), across = Math.abs(vertical ? event.clientX - state.x : event.clientY - state.y);
199
+ if (along < 6 && across < 6)
200
+ return;
201
+ if (across > along) {
202
+ drag.current = null;
203
+ return;
204
+ }
205
+ start(state, percentAt(event));
206
+ return;
207
+ }
208
+ follow(state, percentAt(event));
209
+ }
210
+ function onPointerEnd(event) {
211
+ const state = drag.current;
212
+ if (!state || state.pointer !== event.pointerId)
213
+ return;
214
+ drag.current = null;
215
+ // A tap on the photo moves the divider there.
216
+ if (!state.started) {
217
+ if (event.type === "pointerup") {
218
+ focusQuietly();
219
+ commit(clamp(percentAt(event), 0, 100));
220
+ }
221
+ return;
222
+ }
223
+ // The divider stays where it was let go. Past an edge, or still catching up to a press, it springs on with the speed it had.
224
+ const from = pos.get() + lag.get(), velocity = lag.getVelocity() + (from > 100 || from < 0 ? pos.getVelocity() : 0);
225
+ const target = clamp(lag.get() !== 0 ? state.raw : from, 0, 100);
226
+ lag.jump(0);
227
+ pos.jump(from);
228
+ goal.current = Number(target.toFixed(2));
229
+ if (from !== target) {
230
+ if (reduced)
231
+ pos.jump(target);
232
+ else
233
+ animate(pos, target, { ...moveSpring, velocity });
234
+ }
235
+ commit(target);
236
+ setDragging(false);
237
+ }
238
+ function onKeyDown(event) {
239
+ const large = event.shiftKey ? 10 : 1;
240
+ // Keys move the divider the way they point along its axis; the other two follow the value, where up and right mean more of the after image.
241
+ const toward = vertical ? { ArrowUp: -large, ArrowDown: large, ArrowRight: -large, ArrowLeft: large, PageUp: -10, PageDown: 10 } : { ArrowLeft: -large, ArrowRight: large, ArrowUp: -large, ArrowDown: large, PageUp: -10, PageDown: 10 };
242
+ let next;
243
+ if (event.key === "Home")
244
+ next = 100;
245
+ else if (event.key === "End")
246
+ next = 0;
247
+ else if (event.key in toward)
248
+ next = Math.round(latest.current) + toward[event.key];
249
+ else
250
+ return;
251
+ event.preventDefault();
252
+ setQuiet(false);
253
+ const clamped = clamp(next, 0, 100);
254
+ if (clamped === latest.current) {
255
+ if (!reduced && next !== clamped)
256
+ animate(pos, goal.current, { ...kickSpring, velocity: (Math.sign(next - clamped) * BUMP_PX / axisSize()) * 100 });
257
+ return;
258
+ }
259
+ commit(clamped);
260
+ }
261
+ const capsule = dragging ? (vertical ? { width: 36, height: 56 } : { width: 56, height: 36 }) : { width: 40, height: 40 };
262
+ return _jsxs("div", { ref: rootRef, className: [styles.root, className].filter(Boolean).join(" "), "data-orientation": orientation, "data-dragging": dragging || undefined, "data-frame-keep": "", style: { aspectRatio }, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerEnd, onPointerCancel: onPointerEnd, onLostPointerCapture: onPointerEnd, onMouseDown: event => event.preventDefault(), onDragStart: event => event.preventDefault(),
263
+ // Pointer capture sends the double-click to the frame, so it checks where the presses began.
264
+ onDoubleClick: () => { if (pressedHandle.current && latest.current !== 50)
265
+ commit(50); }, children: [_jsx("div", { className: styles.layer, children: after }), _jsx(motion.div, { className: styles.layer, style: { clipPath }, children: before }), labels && _jsxs(_Fragment, { children: [_jsx(motion.span, { ref: beforeChip, className: styles.chip, style: { opacity: beforeOpacity }, "aria-hidden": "true", children: labels[0] }), _jsx(motion.span, { ref: afterChip, className: styles.chip, "data-place": "end", style: { opacity: afterAcross }, "aria-hidden": "true", children: labels[1] }), _jsx(motion.span, { className: styles.chip, "data-place": "bottom", style: { opacity: afterDown }, "aria-hidden": "true", children: labels[1] })] }), _jsxs(motion.div, { className: styles.pivot, style: { x: pivotX, y: pivotY }, children: [_jsx(motion.span, { className: styles.rotor, style: { rotate: theta }, children: _jsx("span", { className: styles.line }) }), _jsx(motion.span, { className: styles.handleSlot, style: { x: handleX, y: handleY }, children: _jsx(motion.div, { ref: handleRef, "data-handle": "", "data-quiet": quiet || undefined, className: styles.handle, role: "slider", tabIndex: 0, "aria-label": label, "aria-orientation": orientation, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": afterShare, "aria-valuetext": `${afterShare}% after`, initial: false, animate: capsule, transition: reduced ? { duration: 0 } : spring.morph, onKeyDown: onKeyDown, onBlur: () => setQuiet(false), children: _jsxs(motion.span, { className: styles.chevrons, style: { rotate: theta }, "aria-hidden": "true", children: [_jsx(motion.span, { className: styles.chevron, initial: false, animate: { x: dragging ? -4 : 0 }, transition: reduced ? { duration: 0 } : spring.morph, children: _jsx(ChevronLeft, { size: 16, strokeWidth: 1.75 }) }), _jsx(motion.span, { className: styles.chevron, initial: false, animate: { x: dragging ? 4 : 0 }, transition: reduced ? { duration: 0 } : spring.morph, children: _jsx(ChevronRight, { size: 16, strokeWidth: 1.75 }) })] }) }) })] })] });
266
+ }
267
+ export default ImageCompare;
@@ -0,0 +1,25 @@
1
+ /* The frame is a size container, so the divider's line can span any angle with container units. Vertical pans still scroll the page on touch. */
2
+ .root { position: relative; display: block; width: 100%; overflow: clip; container-type: size; isolation: isolate; border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-muted); cursor: ew-resize; touch-action: pan-y; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; }
3
+ .root[data-orientation="vertical"] { cursor: ns-resize; touch-action: pan-x; }
4
+ /* A hairline keeps light photos from bleeding into the page. */
5
+ .root::after { content: ""; position: absolute; z-index: 3; inset: 0; border-radius: inherit; corner-shape: inherit; box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--foreground) 8%, transparent); pointer-events: none; }
6
+ .layer { position: absolute; inset: 0; }
7
+ .layer :where(img, picture, video) { display: block; width: 100%; height: 100%; object-fit: cover; pointer-events: none; -webkit-user-drag: none; }
8
+ /* Captions sit on a frosted chip so they read over any photo, below the divider. */
9
+ .chip { position: absolute; z-index: 1; top: var(--space-group); left: var(--space-group); padding: 3px 10px; border-radius: var(--radius-full); corner-shape: round; background: color-mix(in oklab, var(--background) 74%, transparent); color: var(--foreground); font-size: var(--text-xs); font-weight: 500; line-height: var(--leading-body); letter-spacing: var(--tracking-body); pointer-events: none; -webkit-backdrop-filter: blur(12px) saturate(1.4); backdrop-filter: blur(12px) saturate(1.4); }
10
+ .chip[data-place="end"] { right: var(--space-group); left: auto; }
11
+ .chip[data-place="bottom"] { top: auto; bottom: var(--space-group); }
12
+ /* The pivot is a frame-sized layer moved by a percentage of itself; its top-left corner is the divider's center. */
13
+ .pivot { position: absolute; z-index: 2; inset: 0; pointer-events: none; }
14
+ .rotor { position: absolute; top: 0; left: 0; width: 0; height: 0; }
15
+ .line { position: absolute; top: calc((100cqw + 100cqh) * -1); left: -1px; width: 2px; height: calc((100cqw + 100cqh) * 2); background: var(--surface-raised); box-shadow: 0 0 0 .5px oklch(0% 0 0 / .14); }
16
+ /* The line sits on photography, not on the theme, so it stays light in dark mode too. */
17
+ :global(:root[data-theme="dark"]) .line { background: var(--foreground); }
18
+ .handleSlot { position: absolute; top: 0; left: 0; }
19
+ /* Centered with the translate property; Motion only animates its width and height, so the shape morphs without scaling the icons. */
20
+ .handle { position: absolute; top: 0; left: 0; display: grid; box-sizing: border-box; width: 40px; height: 40px; place-items: center; translate: -50% -50%; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-raised); box-shadow: var(--shadow-raised); color: var(--foreground); pointer-events: auto; outline: 0 solid transparent; transition: border-color var(--duration-fast) var(--ease-standard); }
21
+ .handle::after { content: ""; position: absolute; inset: -8px; border-radius: inherit; corner-shape: inherit; }
22
+ @media (hover: hover) and (pointer: fine) { .root:hover .handle { border-color: var(--border-strong); } }
23
+ .chevrons { display: flex; align-items: center; }
24
+ .chevron { display: grid; margin-inline: -2.5px; place-items: center; }
25
+ @media (prefers-reduced-motion: reduce) { .handle { transition: none; } }
@@ -0,0 +1,17 @@
1
+ export type InViewTitleVariant = "word" | "line" | "blur" | "tracking" | "wipe";
2
+ /**
3
+ * A section title that reveals itself once it scrolls into view. `blur` sharpens word by word and suits most headings; `word` and `line` rise out of a clip for editorial pages;
4
+ * `tracking` opens tight letter spacing; `wipe` uncovers the title from left to right through a soft edge. Pass `lines` to set the breaks for `line`, and `once={false}` to replay on every entry.
5
+ * Titles that are already above the viewport show immediately, and server rendered titles appear on their own if scripts are slow.
6
+ */
7
+ export interface InViewTitleProps {
8
+ text: string;
9
+ variant?: InViewTitleVariant;
10
+ as?: "h1" | "h2" | "h3";
11
+ lines?: string[];
12
+ className?: string;
13
+ id?: string;
14
+ once?: boolean;
15
+ }
16
+ export declare function InViewTitle({ text, variant, as, lines, className, id, once }: InViewTitleProps): import("react").JSX.Element;
17
+ export default InViewTitle;