@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,355 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useMotionValueEvent, useReducedMotion, useTransform } from "motion/react";
5
+ import { CircleAlert, LoaderCircle } from "lucide-react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./confirm-morph.module.css";
8
+ /** A press this soon after the question appears is the tail of the press that opened it, not an answer. */
9
+ const ARM_MS = 280;
10
+ const { blur } = motionTokens;
11
+ const enter = [...motionTokens.ease.enter];
12
+ const standard = [...motionTokens.ease.standard];
13
+ /** Duration springs restated as stiffness and damping, so a retarget mid-flight keeps the velocity it already has. */
14
+ const physical = ({ visualDuration, bounce }) => {
15
+ const root = 2 * Math.PI / (visualDuration * 1.2);
16
+ return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };
17
+ };
18
+ /** Growing follows new content with a little life; shrinking settles without overshoot. */
19
+ const GROW = physical(motionTokens.spring.morph), SHRINK = physical(motionTokens.spring.smooth);
20
+ const subscribe = () => () => { };
21
+ function useReducedFlag() {
22
+ const hydrated = useSyncExternalStore(subscribe, () => true, () => false);
23
+ return !!useReducedMotion() && hydrated;
24
+ }
25
+ /*
26
+ * Faces never move and never scale: each one sits at its natural width, centred in the pill, so the baseline stays put and
27
+ * the pill's clip reveals it as the width springs. The old face fades out quickly; the new one's parts arrive in reading
28
+ * order, each clearing a small blur.
29
+ */
30
+ const faceVariants = {
31
+ hidden: { opacity: 1 },
32
+ shown: { opacity: 1, transition: { staggerChildren: motionTokens.stagger.item, delayChildren: .05 } },
33
+ gone: { opacity: 0, filter: `blur(${blur.subtle}px)`, transition: { duration: motionTokens.duration.instant, ease: standard } },
34
+ };
35
+ const partVariants = {
36
+ hidden: { opacity: 0, filter: `blur(${blur.soft}px)` },
37
+ shown: { opacity: 1, filter: "blur(0px)", transition: { duration: .26, ease: enter } },
38
+ };
39
+ const reducedFace = { hidden: { opacity: 0 }, shown: { opacity: 1, transition: { duration: .14 } }, gone: { opacity: 0, transition: { duration: .1 } } };
40
+ const reducedPart = { hidden: { opacity: 1 }, shown: { opacity: 1 } };
41
+ function Face({ id, reduced, onSize, children, labelledBy }) {
42
+ const ref = useRef(null);
43
+ const present = useIsPresent();
44
+ useLayoutEffect(() => {
45
+ const node = ref.current;
46
+ if (!node || !present)
47
+ return;
48
+ // Fractional widths, so the spring lands exactly where the pill's natural width is and the hand back to auto is seamless.
49
+ const report = () => onSize(id, node.getBoundingClientRect().width);
50
+ report();
51
+ if (typeof ResizeObserver === "undefined")
52
+ return;
53
+ const observer = new ResizeObserver(report);
54
+ observer.observe(node);
55
+ return () => observer.disconnect();
56
+ }, [id, onSize, present]);
57
+ return _jsx(motion.div, { ref: ref, className: styles.face, "data-face": id, role: labelledBy ? "group" : undefined, "aria-labelledby": labelledBy, variants: reduced ? reducedFace : faceVariants, initial: "hidden", animate: "shown", exit: "gone", inert: !present || undefined, children: children });
58
+ }
59
+ function Part({ reduced, className, children }) {
60
+ return _jsx(motion.span, { className: [styles.part, className].filter(Boolean).join(" "), variants: reduced ? reducedPart : partVariants, children: children });
61
+ }
62
+ /** A success disc that pops in with a tick drawing across it, the moment the action lands. */
63
+ function Check({ reduced }) {
64
+ return _jsxs("svg", { className: styles.check, viewBox: "0 0 18 18", fill: "none", "aria-hidden": "true", children: [_jsx(motion.circle, { className: styles.checkDisc, cx: "9", cy: "9", r: "8", style: { transformOrigin: "9px 9px" }, initial: reduced ? false : { scale: .5, opacity: 0 }, animate: { scale: 1, opacity: 1 }, transition: { scale: physical(motionTokens.spring.snappy), opacity: { duration: .12 } } }), _jsx(motion.path, { className: styles.checkTick, d: "M5.6 9.3 7.8 11.4 12.4 6.7", strokeWidth: "1.9", strokeLinecap: "round", strokeLinejoin: "round", initial: reduced ? false : { pathLength: 0 }, animate: { pathLength: 1 }, transition: { duration: .3, ease: enter, delay: .14 } })] });
65
+ }
66
+ /** A ring that drains with the countdown. */
67
+ function Ring({ progress }) {
68
+ return _jsxs("svg", { className: styles.ring, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [_jsx("circle", { className: styles.ringTrack, cx: "8", cy: "8", r: "6.25", strokeWidth: "1.75" }), _jsx(motion.circle, { className: styles.ringFill, cx: "8", cy: "8", r: "6.25", strokeWidth: "1.75", strokeLinecap: "round", style: { pathLength: progress, rotate: -90, transformOrigin: "8px 8px" } })] });
69
+ }
70
+ /** Seconds that swap in place: the old digit fades up and out, the new one rises in, on tabular figures so the width holds. */
71
+ function Seconds({ value, reduced }) {
72
+ return _jsx("span", { className: styles.seconds, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: _jsx(motion.span, { className: styles.digit, initial: reduced ? { opacity: 0 } : { opacity: 0, y: 6, filter: `blur(${blur.subtle}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: reduced ? { opacity: 0 } : { opacity: 0, y: -6, filter: `blur(${blur.subtle}px)` }, transition: { duration: .22, ease: enter }, children: value }, value) }) });
73
+ }
74
+ export function ConfirmMorph({ label, icon, prompt, confirmLabel = "Delete", cancelLabel = "Cancel", pendingLabel = "Deleting", doneLabel = "Deleted", errorLabel = "Couldn’t finish", retryLabel = "Retry", undoLabel = "Undo", undoingLabel = "Restoring", tone = "danger", confirmMode = "press", holdDuration = 1000, countdown = 3, countdownLabel, onConfirm, onUndo, onCancel, state: stateProp, defaultState = "idle", onStateChange, confirmTimeout = 6000, resultTimeout = 5000, cancelOnOutsidePress = true, disabled = false, className, ref, }) {
75
+ const reduced = useReducedFlag();
76
+ const uid = useId();
77
+ const promptId = `${uid}-prompt`;
78
+ const holdHintId = `${uid}-hold`;
79
+ const rootRef = useRef(null);
80
+ useImperativeHandle(ref, () => rootRef.current, []);
81
+ const [inner, setInner] = useState(defaultState);
82
+ const state = stateProp ?? inner;
83
+ const [working, setWorking] = useState("confirm");
84
+ const [counting, setCounting] = useState(false);
85
+ const [announcement, setAnnouncement] = useState("");
86
+ const faceKey = state === "confirming" && counting ? "counting" : state;
87
+ const live = useRef({ state, onStateChange, controlled: stateProp !== undefined });
88
+ useLayoutEffect(() => { live.current = { state, onStateChange, controlled: stateProp !== undefined }; });
89
+ const pendingFocus = useRef(false);
90
+ const run = useRef(0);
91
+ const askedAt = useRef(0);
92
+ const keepFocus = useCallback(() => {
93
+ const root = rootRef.current;
94
+ // Focus follows the control between faces, but only when it was already inside; a timeout never steals focus from elsewhere.
95
+ pendingFocus.current = !!root && (root.contains(document.activeElement) || document.activeElement === document.body);
96
+ }, []);
97
+ const go = useCallback((next) => {
98
+ if (next !== "confirming")
99
+ setCounting(false);
100
+ if (next === live.current.state)
101
+ return;
102
+ keepFocus();
103
+ if (next === "confirming")
104
+ askedAt.current = performance.now();
105
+ if (!live.current.controlled)
106
+ setInner(next);
107
+ live.current.state = next;
108
+ live.current.onStateChange?.(next);
109
+ }, [keepFocus]);
110
+ // A controlled parent can move the state on its own; the countdown never survives that.
111
+ const [seenState, setSeenState] = useState(state);
112
+ if (seenState !== state) {
113
+ setSeenState(state);
114
+ if (state !== "confirming")
115
+ setCounting(false);
116
+ }
117
+ const toIdle = useCallback(() => { run.current++; go("idle"); }, [go]);
118
+ const perform = useCallback(async (kind) => {
119
+ const handler = kind === "confirm" ? onConfirm : onUndo;
120
+ const token = ++run.current;
121
+ setWorking(kind);
122
+ setCounting(false);
123
+ let result;
124
+ try {
125
+ result = handler?.();
126
+ }
127
+ catch {
128
+ go("error");
129
+ setAnnouncement(errorLabel);
130
+ return;
131
+ }
132
+ if (result && typeof result.then === "function") {
133
+ go("pending");
134
+ setAnnouncement(kind === "confirm" ? pendingLabel : undoingLabel);
135
+ try {
136
+ await result;
137
+ }
138
+ catch {
139
+ if (token !== run.current)
140
+ return;
141
+ go("error");
142
+ setAnnouncement(errorLabel);
143
+ return;
144
+ }
145
+ if (token !== run.current)
146
+ return;
147
+ }
148
+ if (kind === "undo") {
149
+ go("idle");
150
+ setAnnouncement("Undone");
151
+ return;
152
+ }
153
+ go("done");
154
+ setAnnouncement(onUndo ? `${doneLabel}. ${undoLabel} is available.` : doneLabel);
155
+ }, [doneLabel, errorLabel, go, onConfirm, onUndo, pendingLabel, undoLabel, undoingLabel]);
156
+ const cancel = useCallback(() => { onCancel?.(); toIdle(); setAnnouncement("Cancelled"); }, [onCancel, toIdle]);
157
+ const expire = useRef(() => { });
158
+ useLayoutEffect(() => { expire.current = () => { if (live.current.state === "confirming")
159
+ cancel();
160
+ else
161
+ toIdle(); }; });
162
+ /* The shape: one pill whose width springs to whichever face is current. At rest it is auto, so it renders right before hydration. */
163
+ const width = useMotionValue("auto");
164
+ const target = useRef(0);
165
+ const flight = useRef(0);
166
+ // Only the present face reports (a leaving face stops observing), so every report is the current face's width.
167
+ const onFaceSize = useCallback((_id, w) => {
168
+ if (Math.abs(w - target.current) < .25)
169
+ return;
170
+ const from = target.current;
171
+ target.current = w;
172
+ if (!from || reduced) {
173
+ flight.current++;
174
+ width.jump("auto");
175
+ return;
176
+ }
177
+ const now = width.get();
178
+ if (now === "auto")
179
+ width.jump(from);
180
+ const token = ++flight.current;
181
+ animate(width, w, w > (typeof now === "number" ? now : from) ? GROW : SHRINK).then(() => { if (token === flight.current)
182
+ width.jump("auto"); });
183
+ }, [reduced, width]);
184
+ /* The timeout runs as an invisible clock. A pointer resting on the control holds it, and so does a hidden tab. */
185
+ const drain = useMotionValue(1);
186
+ const clock = useRef(null);
187
+ const holds = useRef({ hover: false, hidden: false });
188
+ const timeout = state === "confirming" && !counting ? confirmTimeout : state === "done" || state === "error" ? resultTimeout : 0;
189
+ const sync = useCallback(() => {
190
+ const control = clock.current;
191
+ if (!control)
192
+ return;
193
+ const held = holds.current.hover || holds.current.hidden;
194
+ if (held)
195
+ control.pause();
196
+ else
197
+ control.play();
198
+ }, []);
199
+ useEffect(() => {
200
+ if (!timeout)
201
+ return;
202
+ drain.jump(1);
203
+ const control = animate(drain, 0, { duration: timeout / 1000, ease: "linear" });
204
+ clock.current = control;
205
+ control.then(() => { if (clock.current === control) {
206
+ clock.current = null;
207
+ expire.current();
208
+ } });
209
+ sync();
210
+ return () => { if (clock.current === control)
211
+ clock.current = null; control.stop(); };
212
+ }, [drain, state, sync, timeout]);
213
+ useEffect(() => {
214
+ const onVisibility = () => { holds.current.hidden = document.hidden; sync(); };
215
+ document.addEventListener("visibilitychange", onVisibility);
216
+ return () => document.removeEventListener("visibilitychange", onVisibility);
217
+ }, [sync]);
218
+ /* Countdown: a short grace period after the answer, drawn as a draining ring and ticking seconds. */
219
+ const remaining = useMotionValue(1);
220
+ const [seconds, setSeconds] = useState(countdown);
221
+ const performRef = useRef(perform);
222
+ useLayoutEffect(() => { performRef.current = perform; });
223
+ useMotionValueEvent(remaining, "change", value => setSeconds(Math.max(1, Math.ceil(value * countdown))));
224
+ useEffect(() => {
225
+ if (!counting)
226
+ return;
227
+ remaining.jump(1);
228
+ const control = animate(remaining, 0, { duration: countdown, ease: "linear" });
229
+ control.then(() => { void performRef.current("confirm"); });
230
+ return () => control.stop();
231
+ }, [counting, countdown, remaining]);
232
+ /* Hold: the confirm button fills with the danger colour from the left while pressed, and drains back if let go early. */
233
+ const hold = useMotionValue(0);
234
+ const holding = useRef(null);
235
+ const fillClip = useTransform(hold, value => `inset(0 ${(1 - value) * 100}% 0 0 round 999px)`);
236
+ const startHold = () => {
237
+ if (holding.current)
238
+ return;
239
+ const control = animate(hold, 1, { duration: holdDuration / 1000 * (1 - hold.get()), ease: "linear" });
240
+ holding.current = control;
241
+ control.then(() => {
242
+ if (holding.current !== control)
243
+ return;
244
+ holding.current = null;
245
+ void perform("confirm");
246
+ });
247
+ };
248
+ const releaseHold = () => {
249
+ const control = holding.current;
250
+ if (!control)
251
+ return;
252
+ holding.current = null;
253
+ control.stop();
254
+ if (hold.get() < 1) {
255
+ animate(hold, 0, reduced ? { duration: 0 } : { type: "spring", visualDuration: .3, bounce: 0 });
256
+ setAnnouncement(`Keep holding to ${confirmLabel.toLowerCase()}`);
257
+ }
258
+ };
259
+ useEffect(() => { if (state !== "confirming") {
260
+ holding.current?.stop();
261
+ holding.current = null;
262
+ hold.jump(0);
263
+ } }, [hold, state]);
264
+ // A distinct keyboard activation is deliberate; only suppress the opening pointer press tail.
265
+ const answer = (keyboard = false) => {
266
+ if (!keyboard && performance.now() - askedAt.current < ARM_MS)
267
+ return;
268
+ if (confirmMode === "countdown") {
269
+ keepFocus();
270
+ setCounting(true);
271
+ setAnnouncement(`${countdownLabel ?? `${pendingLabel} in`} ${countdown} seconds`);
272
+ return;
273
+ }
274
+ void perform("confirm");
275
+ };
276
+ // An outside press answers the question with no.
277
+ useEffect(() => {
278
+ if (state !== "confirming" || !cancelOnOutsidePress)
279
+ return;
280
+ const down = (event) => { if (!rootRef.current?.contains(event.target))
281
+ cancel(); };
282
+ document.addEventListener("pointerdown", down);
283
+ return () => document.removeEventListener("pointerdown", down);
284
+ }, [cancel, cancelOnOutsidePress, state]);
285
+ // Focus moves with the morph: Confirm while asking, the root while counting or working, Undo or Retry on a result, the trigger at rest.
286
+ useLayoutEffect(() => {
287
+ if (!pendingFocus.current)
288
+ return;
289
+ pendingFocus.current = false;
290
+ const root = rootRef.current;
291
+ if (!root)
292
+ return;
293
+ const face = root.querySelector(`[data-face="${faceKey}"]`);
294
+ const autofocus = face?.querySelector("[data-autofocus]:not(:disabled)");
295
+ (autofocus ?? root).focus({ preventScroll: true });
296
+ }, [faceKey]);
297
+ const onKeyDown = (event) => {
298
+ if (event.key === "Escape") {
299
+ if (state === "confirming") {
300
+ event.preventDefault();
301
+ event.stopPropagation();
302
+ cancel();
303
+ }
304
+ else if (state === "done" || state === "error") {
305
+ event.preventDefault();
306
+ event.stopPropagation();
307
+ toIdle();
308
+ }
309
+ return;
310
+ }
311
+ // Enter answers yes from anywhere in the question except a button of its own (Cancel keeps meaning Cancel).
312
+ if (event.key === "Enter" && state === "confirming" && !event.repeat && !(event.target instanceof HTMLButtonElement)) {
313
+ event.preventDefault();
314
+ if (counting)
315
+ void perform("confirm");
316
+ else if (confirmMode !== "hold")
317
+ answer(true);
318
+ }
319
+ };
320
+ const holdKeys = (event, down) => {
321
+ if (event.key !== "Enter" && event.key !== " ")
322
+ return;
323
+ event.preventDefault();
324
+ if (down) {
325
+ if (!event.repeat)
326
+ startHold();
327
+ }
328
+ else
329
+ releaseHold();
330
+ };
331
+ const holdPointer = (event) => {
332
+ if (event.button !== 0)
333
+ return;
334
+ event.currentTarget.setPointerCapture?.(event.pointerId);
335
+ startHold();
336
+ };
337
+ const shownPrompt = prompt ?? _jsxs(_Fragment, { children: [label, "?"] });
338
+ const shownCountdown = countdownLabel ?? `${pendingLabel} in`;
339
+ const confirmButton = confirmMode === "hold"
340
+ ? _jsxs("button", { type: "button", className: styles.confirm, "data-autofocus": true, "aria-describedby": holdHintId, onPointerDown: holdPointer, onPointerUp: releaseHold, onPointerCancel: releaseHold, onLostPointerCapture: releaseHold, onKeyDown: event => holdKeys(event, true), onKeyUp: event => holdKeys(event, false), onBlur: releaseHold, onContextMenu: event => event.preventDefault(), children: [_jsx("span", { children: confirmLabel }), _jsx(motion.span, { className: styles.fill, style: { clipPath: fillClip }, "aria-hidden": "true", children: _jsx("span", { children: confirmLabel }) })] })
341
+ : _jsx("button", { type: "button", className: styles.confirm, "data-autofocus": true, onClick: event => answer(event.detail === 0), onKeyDown: event => { if (event.repeat)
342
+ event.preventDefault(); }, children: confirmLabel });
343
+ const face = (() => {
344
+ switch (faceKey) {
345
+ case "confirming": return _jsxs(_Fragment, { children: [_jsx(Part, { reduced: reduced, className: styles.prompt, children: _jsx("span", { id: promptId, children: shownPrompt }) }), _jsx(Part, { reduced: reduced, children: _jsx("button", { type: "button", className: styles.secondary, onClick: cancel, children: cancelLabel }) }), _jsx(Part, { reduced: reduced, children: confirmButton }), confirmMode === "hold" && _jsx("span", { id: holdHintId, className: styles.srOnly, children: "Press and hold" })] });
346
+ case "counting": return _jsxs(_Fragment, { children: [_jsxs(Part, { reduced: reduced, className: styles.status, children: [_jsx(Ring, { progress: remaining }), _jsxs("span", { className: styles.statusText, children: [shownCountdown, " ", _jsx(Seconds, { value: seconds, reduced: reduced })] })] }), _jsx(Part, { reduced: reduced, children: _jsx("button", { type: "button", className: styles.secondary, onClick: cancel, children: cancelLabel }) })] });
347
+ case "pending": return _jsxs(Part, { reduced: reduced, className: styles.status, children: [_jsx(LoaderCircle, { className: styles.spinner, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("span", { className: styles.statusText, children: working === "undo" ? undoingLabel : pendingLabel })] });
348
+ case "done": return _jsxs(_Fragment, { children: [_jsxs(Part, { reduced: reduced, className: styles.status, children: [_jsx(Check, { reduced: reduced }), _jsx("span", { className: styles.statusText, children: doneLabel })] }), onUndo && _jsx(Part, { reduced: reduced, children: _jsx("button", { type: "button", className: styles.secondary, "data-autofocus": true, onClick: () => void perform("undo"), children: undoLabel }) })] });
349
+ case "error": return _jsxs(_Fragment, { children: [_jsxs(Part, { reduced: reduced, className: styles.status, children: [_jsx(CircleAlert, { className: styles.alert, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("span", { className: styles.statusText, children: errorLabel })] }), _jsx(Part, { reduced: reduced, children: _jsx("button", { type: "button", className: styles.secondary, "data-autofocus": true, onClick: () => void perform(working), children: retryLabel }) })] });
350
+ default: return _jsx(Part, { reduced: reduced, children: _jsxs("button", { type: "button", className: styles.trigger, "data-autofocus": true, disabled: disabled, onClick: () => { setAnnouncement(typeof shownPrompt === "string" ? shownPrompt : ""); go("confirming"); }, children: [icon && _jsx("span", { className: styles.icon, "aria-hidden": "true", children: icon }), _jsx("span", { children: label })] }) });
351
+ }
352
+ })();
353
+ return _jsxs("div", { ref: rootRef, className: [styles.root, className].filter(Boolean).join(" "), "data-state": state, "data-face": faceKey, "data-tone": tone, "data-mode": confirmMode, "data-disabled": disabled || undefined, tabIndex: -1, onKeyDown: onKeyDown, "aria-busy": state === "pending" || undefined, onPointerEnter: () => { holds.current.hover = true; sync(); }, onPointerLeave: () => { holds.current.hover = false; sync(); }, onPointerCancel: () => { holds.current.hover = false; sync(); }, children: [_jsx(motion.div, { className: styles.surface, style: { width }, children: _jsx(AnimatePresence, { initial: false, children: _jsx(Face, { id: faceKey, reduced: reduced, onSize: onFaceSize, labelledBy: faceKey === "confirming" ? promptId : undefined, children: face }, faceKey) }) }), _jsx("span", { className: styles.srOnly, role: "status", "aria-live": "polite", children: announcement })] });
354
+ }
355
+ export default ConfirmMorph;
@@ -0,0 +1,146 @@
1
+ /* The root holds the control in the flow of the page; the pill inside it springs between faces. */
2
+ .root {
3
+ position: relative;
4
+ display: inline-flex;
5
+ min-width: 0;
6
+ max-width: 100%;
7
+ vertical-align: middle;
8
+ color: var(--foreground);
9
+ font-family: var(--font-body);
10
+ font-size: var(--text-sm);
11
+ letter-spacing: var(--tracking-body);
12
+ line-height: 20px;
13
+ -webkit-tap-highlight-color: transparent;
14
+ }
15
+
16
+ /*
17
+ * One neutral pill for every state. Only its width moves; corners, text and border never scale, so nothing distorts.
18
+ * Every face sits at its natural width against the pill's leading edge, so text never slides or squeezes: the question
19
+ * starts where the label was, and the springing edge reveals the rest. The border is an overlay so it never changes a measurement.
20
+ */
21
+ .surface {
22
+ position: relative;
23
+ display: flex;
24
+ height: var(--control-height-sm);
25
+ max-width: 100%;
26
+ justify-content: flex-start;
27
+ overflow: clip;
28
+ border-radius: var(--radius-full); corner-shape: round;
29
+ background: var(--surface-raised);
30
+ box-shadow: var(--shadow-resting);
31
+ transition: background-color var(--duration-standard) var(--ease-standard), box-shadow var(--duration-standard) var(--ease-standard);
32
+ }
33
+ .surface::after { content: ""; position: absolute; inset: 0; border: 1px solid var(--border); border-radius: inherit; corner-shape: inherit; pointer-events: none; transition: border-color var(--duration-standard) var(--ease-standard); }
34
+ .root[data-disabled][data-state="idle"] .surface { box-shadow: none; }
35
+ .root[data-disabled][data-state="idle"] .surface::after { border-color: var(--border-subtle); }
36
+
37
+ .face { display: flex; flex: none; height: 100%; align-items: center; gap: 2px; padding: 0 4px; white-space: nowrap; }
38
+ .face[data-face="idle"] { padding: 0; }
39
+ /* A leaving face steps out of the flow, exactly where it was, and fades while the pill springs to the next one. */
40
+ .face[inert] { position: absolute; top: 0; inset-inline-start: 0; pointer-events: none; }
41
+ .part { display: inline-flex; height: 100%; align-items: center; }
42
+
43
+ .trigger {
44
+ display: inline-flex;
45
+ height: 100%;
46
+ align-items: center;
47
+ gap: 8px;
48
+ padding: 0 16px 0 14px;
49
+ border: 0;
50
+ border-radius: var(--radius-full); corner-shape: round;
51
+ background: transparent;
52
+ color: var(--foreground);
53
+ cursor: pointer;
54
+ font: inherit;
55
+ font-weight: 500;
56
+ transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
57
+ }
58
+ .trigger:active:not(:disabled) { background: color-mix(in oklab, var(--foreground) 9%, transparent); transition-duration: 0ms; }
59
+ .trigger:disabled { color: var(--text-muted); cursor: not-allowed; }
60
+ .trigger:disabled .icon { opacity: .7; }
61
+ .icon { display: grid; width: 16px; height: 16px; place-items: center; color: var(--text-secondary); }
62
+ .trigger:disabled .icon { color: inherit; }
63
+ .icon > svg { width: 16px; height: 16px; }
64
+
65
+ .prompt { max-width: 16rem; overflow: hidden; padding: 0 8px 0 12px; color: var(--foreground); font-weight: 500; font-variant-numeric: tabular-nums; }
66
+ .prompt > span { overflow: hidden; text-overflow: ellipsis; }
67
+
68
+ .secondary,
69
+ .confirm {
70
+ position: relative;
71
+ display: inline-flex;
72
+ flex: none;
73
+ height: calc(var(--control-height-sm) - 8px);
74
+ align-items: center;
75
+ justify-content: center;
76
+ padding: 0 12px;
77
+ border: 0;
78
+ border-radius: var(--radius-full); corner-shape: round;
79
+ cursor: pointer;
80
+ font: inherit;
81
+ font-weight: 500;
82
+ transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
83
+ touch-action: manipulation;
84
+ }
85
+ .secondary { background: transparent; color: var(--text-secondary); }
86
+ .face:is([data-face="done"], [data-face="error"]) .secondary { color: var(--foreground); }
87
+ .secondary:active { background: color-mix(in oklab, var(--foreground) 11%, transparent); color: var(--foreground); transition-duration: 0ms; }
88
+
89
+ /* The only coloured thing in the control: danger for a destructive answer, the accent for a reversible one. */
90
+ .confirm { overflow: clip; background: color-mix(in oklab, var(--danger) 13%, transparent); color: var(--danger); }
91
+ .confirm:active { background: color-mix(in oklab, var(--danger) 22%, transparent); transition-duration: 0ms; }
92
+ .root[data-tone="neutral"] .confirm { background: var(--accent); color: var(--accent-foreground); }
93
+ .root[data-tone="neutral"] .confirm:active { background: var(--accent-strong); }
94
+
95
+ /* Hold: a solid copy of the button, label and all, revealed from the left as the hold fills; the label inverts as the edge passes. */
96
+ .root[data-mode="hold"] .confirm { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
97
+ .fill { position: absolute; inset: 0; display: inline-flex; align-items: center; justify-content: center; background: var(--danger); color: var(--background); pointer-events: none; }
98
+ .root[data-tone="neutral"] .fill { background: var(--accent-strong); color: var(--accent-foreground); }
99
+
100
+ .status { gap: 8px; padding: 0 10px 0 12px; color: var(--foreground); font-weight: 500; font-variant-numeric: tabular-nums; }
101
+ .status > svg { flex: none; }
102
+ .statusText { display: inline-flex; align-items: baseline; gap: .25em; }
103
+ .face[data-face="pending"] .status { padding-inline: 12px 16px; color: var(--text-secondary); }
104
+ .face[data-face="done"]:not(:has(.secondary)) .status { padding-right: 14px; }
105
+ .face[data-face="counting"] .status { padding-right: 6px; color: var(--text-secondary); }
106
+ .alert { color: var(--danger); }
107
+ .check { width: 18px; height: 18px; }
108
+ .checkDisc { fill: var(--success); }
109
+ .checkTick { stroke: var(--background); }
110
+ .spinner { animation: spin .7s linear infinite; }
111
+ @keyframes spin { to { transform: rotate(360deg); } }
112
+
113
+ .ring { width: 16px; height: 16px; }
114
+ .ringTrack { stroke: var(--border); }
115
+ .ringFill { stroke: var(--danger); }
116
+ .root[data-tone="neutral"] .ringFill { stroke: var(--accent); }
117
+ /* Seconds keep one tabular width while the digits swap. */
118
+ .seconds { display: inline-grid; min-width: 1ch; color: var(--foreground); font-variant-numeric: tabular-nums; }
119
+ .digit { grid-area: 1 / 1; }
120
+
121
+ /* No focus rings anywhere in the control; keyboard focus reads through fill (below). */
122
+ .root, .root :focus { outline: none; }
123
+
124
+ .srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
125
+
126
+ @media (hover: hover) and (pointer: fine) {
127
+ .trigger:hover:not(:disabled):not(:active) { background: color-mix(in oklab, var(--foreground) 5%, transparent); }
128
+ .secondary:hover:not(:active) { background: color-mix(in oklab, var(--foreground) 7%, transparent); color: var(--foreground); }
129
+ .confirm:hover:not(:active) { background: color-mix(in oklab, var(--danger) 18%, transparent); }
130
+ .root[data-tone="neutral"] .confirm:hover:not(:active) { background: var(--accent-strong); }
131
+ }
132
+ /* Keyboard focus reads through fill, never a ring. */
133
+ .trigger:focus-visible { background: color-mix(in oklab, var(--foreground) 6%, transparent); }
134
+ .secondary:focus-visible { background: color-mix(in oklab, var(--foreground) 8%, transparent); color: var(--foreground); }
135
+ .confirm:focus-visible { background: color-mix(in oklab, var(--danger) 20%, transparent); }
136
+ .root[data-tone="neutral"] .confirm:focus-visible { background: var(--accent-strong); }
137
+
138
+ @media (prefers-reduced-motion: reduce) {
139
+ .surface, .surface::after, .trigger, .secondary, .confirm { transition-duration: 0ms; }
140
+ .spinner { animation-duration: 1.6s; }
141
+ }
142
+
143
+ @media (prefers-contrast: more) {
144
+ .surface::after { border-color: var(--border-strong); }
145
+ .confirm { background: color-mix(in oklab, var(--danger) 22%, transparent); }
146
+ }
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from "react";
2
+ export interface ContextMenuItem {
3
+ id: string;
4
+ label: string;
5
+ icon?: ReactNode;
6
+ onSelect?: () => void;
7
+ disabled?: boolean;
8
+ destructive?: boolean;
9
+ checked?: boolean;
10
+ }
11
+ export interface ContextMenuProps {
12
+ children: ReactNode;
13
+ items: ContextMenuItem[];
14
+ label?: string;
15
+ }
16
+ export declare function ContextMenu({ children, items, label }: ContextMenuProps): import("react").JSX.Element;
17
+ export declare const contextMenuExampleItems: ContextMenuItem[];
18
+ export default ContextMenu;