@seekalfred/primitives 0.5.1 → 0.6.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 (215) hide show
  1. package/README.md +664 -9
  2. package/dist/accordion/accordion.js +10 -4
  3. package/dist/accordion/accordion.js.map +1 -1
  4. package/dist/action-button/action-button.d.ts +50 -0
  5. package/dist/action-button/action-button.js +159 -0
  6. package/dist/action-button/action-button.js.map +1 -0
  7. package/dist/action-button/index.d.ts +2 -0
  8. package/dist/action-button/index.js +2 -0
  9. package/dist/alert/alert.d.ts +15 -1
  10. package/dist/alert/alert.js +116 -20
  11. package/dist/alert/alert.js.map +1 -1
  12. package/dist/avatar/avatar.d.ts +6 -0
  13. package/dist/avatar/avatar.js +47 -16
  14. package/dist/avatar/avatar.js.map +1 -1
  15. package/dist/badge/badge.d.ts +11 -4
  16. package/dist/badge/badge.js +35 -5
  17. package/dist/badge/badge.js.map +1 -1
  18. package/dist/breadcrumb/breadcrumb.d.ts +0 -27
  19. package/dist/breadcrumb/breadcrumb.js +20 -6
  20. package/dist/breadcrumb/breadcrumb.js.map +1 -1
  21. package/dist/button/button.d.ts +35 -22
  22. package/dist/button/button.js +177 -34
  23. package/dist/button/button.js.map +1 -1
  24. package/dist/button-group/button-group.d.ts +76 -0
  25. package/dist/button-group/button-group.js +403 -0
  26. package/dist/button-group/button-group.js.map +1 -0
  27. package/dist/button-group/index.d.ts +2 -0
  28. package/dist/button-group/index.js +2 -0
  29. package/dist/calendar/calendar.d.ts +1 -1
  30. package/dist/calendar/calendar.js +257 -30
  31. package/dist/calendar/calendar.js.map +1 -1
  32. package/dist/card/card.d.ts +9 -2
  33. package/dist/card/card.js +15 -4
  34. package/dist/card/card.js.map +1 -1
  35. package/dist/checkbox/checkbox.d.ts +0 -7
  36. package/dist/checkbox/checkbox.js +24 -27
  37. package/dist/checkbox/checkbox.js.map +1 -1
  38. package/dist/confirm-morph/confirm-morph.d.ts +85 -0
  39. package/dist/confirm-morph/confirm-morph.js +652 -0
  40. package/dist/confirm-morph/confirm-morph.js.map +1 -0
  41. package/dist/confirm-morph/index.d.ts +2 -0
  42. package/dist/confirm-morph/index.js +2 -0
  43. package/dist/copy-button/copy-button.d.ts +32 -0
  44. package/dist/copy-button/copy-button.js +134 -0
  45. package/dist/copy-button/copy-button.js.map +1 -0
  46. package/dist/copy-button/index.d.ts +2 -0
  47. package/dist/copy-button/index.js +2 -0
  48. package/dist/date-picker/date-picker.d.ts +51 -0
  49. package/dist/date-picker/date-picker.js +241 -0
  50. package/dist/date-picker/date-picker.js.map +1 -0
  51. package/dist/date-picker/date-range-picker.d.ts +78 -0
  52. package/dist/date-picker/date-range-picker.js +1015 -0
  53. package/dist/date-picker/date-range-picker.js.map +1 -0
  54. package/dist/date-picker/index.d.ts +4 -0
  55. package/dist/date-picker/index.js +4 -0
  56. package/dist/date-picker/picker-parts.d.ts +46 -0
  57. package/dist/date-picker/picker-parts.js +135 -0
  58. package/dist/date-picker/picker-parts.js.map +1 -0
  59. package/dist/dialog/dialog.d.ts +10 -14
  60. package/dist/dialog/dialog.js +39 -11
  61. package/dist/dialog/dialog.js.map +1 -1
  62. package/dist/empty-state/empty-state.d.ts +44 -0
  63. package/dist/empty-state/empty-state.js +83 -0
  64. package/dist/empty-state/empty-state.js.map +1 -0
  65. package/dist/empty-state/index.d.ts +2 -0
  66. package/dist/empty-state/index.js +2 -0
  67. package/dist/expanding-button-group/expanding-button-group.d.ts +54 -0
  68. package/dist/expanding-button-group/expanding-button-group.js +350 -0
  69. package/dist/expanding-button-group/expanding-button-group.js.map +1 -0
  70. package/dist/expanding-button-group/index.d.ts +2 -0
  71. package/dist/expanding-button-group/index.js +2 -0
  72. package/dist/file-drop-zone/file-drop-zone.d.ts +71 -0
  73. package/dist/file-drop-zone/file-drop-zone.js +116 -0
  74. package/dist/file-drop-zone/file-drop-zone.js.map +1 -0
  75. package/dist/file-drop-zone/index.d.ts +2 -0
  76. package/dist/file-drop-zone/index.js +2 -0
  77. package/dist/filter-bar/filter-bar.d.ts +121 -0
  78. package/dist/filter-bar/filter-bar.js +300 -0
  79. package/dist/filter-bar/filter-bar.js.map +1 -0
  80. package/dist/filter-bar/index.d.ts +2 -0
  81. package/dist/filter-bar/index.js +2 -0
  82. package/dist/floating-button-group/floating-button-group.d.ts +61 -0
  83. package/dist/floating-button-group/floating-button-group.js +340 -0
  84. package/dist/floating-button-group/floating-button-group.js.map +1 -0
  85. package/dist/floating-button-group/index.d.ts +2 -0
  86. package/dist/floating-button-group/index.js +2 -0
  87. package/dist/icon-morph/icon-morph.d.ts +55 -0
  88. package/dist/icon-morph/icon-morph.js +331 -0
  89. package/dist/icon-morph/icon-morph.js.map +1 -0
  90. package/dist/icon-morph/index.d.ts +2 -0
  91. package/dist/icon-morph/index.js +2 -0
  92. package/dist/index.d.ts +24 -2
  93. package/dist/index.js +29 -7
  94. package/dist/input/input.d.ts +22 -0
  95. package/dist/input/input.js +36 -12
  96. package/dist/input/input.js.map +1 -1
  97. package/dist/lib/count-to.js +113 -0
  98. package/dist/lib/count-to.js.map +1 -0
  99. package/dist/lib/eye-morph.js +62 -0
  100. package/dist/lib/eye-morph.js.map +1 -0
  101. package/dist/lib/field-message.js +336 -0
  102. package/dist/lib/field-message.js.map +1 -0
  103. package/dist/lib/field-shell.js +81 -0
  104. package/dist/lib/field-shell.js.map +1 -0
  105. package/dist/lib/field-surface.js +4 -2
  106. package/dist/lib/field-surface.js.map +1 -1
  107. package/dist/lib/flip.js +80 -0
  108. package/dist/lib/flip.js.map +1 -0
  109. package/dist/lib/height-follow.js +59 -0
  110. package/dist/lib/height-follow.js.map +1 -0
  111. package/dist/lib/icon-swap.js +47 -0
  112. package/dist/lib/icon-swap.js.map +1 -0
  113. package/dist/lib/leaving-list.js +57 -0
  114. package/dist/lib/leaving-list.js.map +1 -0
  115. package/dist/lib/merge-refs.js +3 -3
  116. package/dist/lib/merge-refs.js.map +1 -1
  117. package/dist/lib/morph-text.js +128 -0
  118. package/dist/lib/morph-text.js.map +1 -0
  119. package/dist/lib/motion.js +162 -0
  120. package/dist/lib/motion.js.map +1 -0
  121. package/dist/lib/pausable-timeout.js +67 -0
  122. package/dist/lib/pausable-timeout.js.map +1 -0
  123. package/dist/lib/rolling-value.js +31 -0
  124. package/dist/lib/rolling-value.js.map +1 -0
  125. package/dist/lib/spring-progress.js +114 -0
  126. package/dist/lib/spring-progress.js.map +1 -0
  127. package/dist/lib/svg-id.js +10 -0
  128. package/dist/lib/svg-id.js.map +1 -0
  129. package/dist/lib/swap-text.js +77 -0
  130. package/dist/lib/swap-text.js.map +1 -0
  131. package/dist/lib/use-copy-feedback.js +36 -0
  132. package/dist/lib/use-copy-feedback.js.map +1 -0
  133. package/dist/page-header/index.d.ts +2 -0
  134. package/dist/page-header/index.js +2 -0
  135. package/dist/page-header/page-header.d.ts +75 -0
  136. package/dist/page-header/page-header.js +83 -0
  137. package/dist/page-header/page-header.js.map +1 -0
  138. package/dist/password-field/index.d.ts +2 -0
  139. package/dist/password-field/index.js +2 -0
  140. package/dist/password-field/password-field.d.ts +37 -0
  141. package/dist/password-field/password-field.js +101 -0
  142. package/dist/password-field/password-field.js.map +1 -0
  143. package/dist/password-strength/index.d.ts +2 -0
  144. package/dist/password-strength/index.js +2 -0
  145. package/dist/password-strength/password-strength.d.ts +127 -0
  146. package/dist/password-strength/password-strength.js +455 -0
  147. package/dist/password-strength/password-strength.js.map +1 -0
  148. package/dist/popover/popover.d.ts +8 -0
  149. package/dist/popover/popover.js +3 -3
  150. package/dist/popover/popover.js.map +1 -1
  151. package/dist/progress/progress.js +46 -5
  152. package/dist/progress/progress.js.map +1 -1
  153. package/dist/radio-group/radio-group.js +7 -6
  154. package/dist/radio-group/radio-group.js.map +1 -1
  155. package/dist/scroll-area/scroll-area.d.ts +10 -0
  156. package/dist/scroll-area/scroll-area.js +25 -3
  157. package/dist/scroll-area/scroll-area.js.map +1 -1
  158. package/dist/search-field/index.d.ts +2 -0
  159. package/dist/search-field/index.js +2 -0
  160. package/dist/search-field/search-field.d.ts +26 -0
  161. package/dist/search-field/search-field.js +155 -0
  162. package/dist/search-field/search-field.js.map +1 -0
  163. package/dist/select/select.js +79 -21
  164. package/dist/select/select.js.map +1 -1
  165. package/dist/sheet/sheet.js +2 -1
  166. package/dist/sheet/sheet.js.map +1 -1
  167. package/dist/side-nav/index.d.ts +2 -0
  168. package/dist/side-nav/index.js +2 -0
  169. package/dist/side-nav/side-nav.d.ts +72 -0
  170. package/dist/side-nav/side-nav.js +142 -0
  171. package/dist/side-nav/side-nav.js.map +1 -0
  172. package/dist/skeleton/skeleton.d.ts +8 -2
  173. package/dist/skeleton/skeleton.js +6 -2
  174. package/dist/skeleton/skeleton.js.map +1 -1
  175. package/dist/slider/slider.js +61 -7
  176. package/dist/slider/slider.js.map +1 -1
  177. package/dist/split-button/index.d.ts +2 -0
  178. package/dist/split-button/index.js +2 -0
  179. package/dist/split-button/split-button.d.ts +46 -0
  180. package/dist/split-button/split-button.js +122 -0
  181. package/dist/split-button/split-button.js.map +1 -0
  182. package/dist/stat-card/index.d.ts +2 -0
  183. package/dist/stat-card/index.js +2 -0
  184. package/dist/stat-card/stat-card.d.ts +81 -0
  185. package/dist/stat-card/stat-card.js +152 -0
  186. package/dist/stat-card/stat-card.js.map +1 -0
  187. package/dist/styles.css +1 -1
  188. package/dist/switch/switch.js +9 -8
  189. package/dist/switch/switch.js.map +1 -1
  190. package/dist/table/index.d.ts +2 -2
  191. package/dist/table/index.js +2 -2
  192. package/dist/table/table.d.ts +36 -5
  193. package/dist/table/table.js +112 -11
  194. package/dist/table/table.js.map +1 -1
  195. package/dist/tabs/tabs.js +142 -6
  196. package/dist/tabs/tabs.js.map +1 -1
  197. package/dist/textarea/textarea.d.ts +23 -0
  198. package/dist/textarea/textarea.js +24 -2
  199. package/dist/textarea/textarea.js.map +1 -1
  200. package/dist/theme-switch/index.d.ts +2 -0
  201. package/dist/theme-switch/index.js +2 -0
  202. package/dist/theme-switch/theme-switch.d.ts +37 -0
  203. package/dist/theme-switch/theme-switch.js +123 -0
  204. package/dist/theme-switch/theme-switch.js.map +1 -0
  205. package/dist/time-picker/index.d.ts +2 -0
  206. package/dist/time-picker/index.js +2 -0
  207. package/dist/time-picker/time-picker.d.ts +54 -0
  208. package/dist/time-picker/time-picker.js +201 -0
  209. package/dist/time-picker/time-picker.js.map +1 -0
  210. package/dist/toast/toast.js +58 -38
  211. package/dist/toast/toast.js.map +1 -1
  212. package/dist/tooltip/tooltip.d.ts +1 -1
  213. package/dist/tooltip/tooltip.js +63 -14
  214. package/dist/tooltip/tooltip.js.map +1 -1
  215. package/package.json +83 -3
@@ -0,0 +1,652 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { prefersReducedMotion, useExitBackstop, useIsomorphicLayoutEffect, useLeavingItems } from "../lib/motion.js";
4
+ import { usePausableTimeout } from "../lib/pausable-timeout.js";
5
+ import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useRef, useState } from "react";
6
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/confirm-morph/confirm-morph.tsx
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
+ /** Whether the parts of the current face arrive with motion: not on first render, only on a change of face. */
11
+ const PartsAnimate = createContext(false);
12
+ /**
13
+ * One piece of a face. The parts of a new face arrive in reading order, 35ms
14
+ * apart after a 50ms beat, each clearing a small blur (--part-delay).
15
+ */
16
+ function Part({ index, className, children }) {
17
+ const animate = useContext(PartsAnimate);
18
+ return /* @__PURE__ */ jsx("span", {
19
+ style: { "--part-delay": 50 + index * 35 + "ms" },
20
+ className: cn("inline-flex h-full items-center " + (animate ? "motion-safe:animate-part-in motion-safe:[animation-delay:var(--part-delay)] " : "") + (className ?? "")),
21
+ children
22
+ });
23
+ }
24
+ /**
25
+ * Faces never move and never scale: each one sits at its natural width,
26
+ * against the pill's leading edge, so the baseline stays put and the pill's clip
27
+ * reveals it as the width springs. The present face reports its width so the
28
+ * pill can spring to it.
29
+ */
30
+ function Face({ id, onSize, labelledBy, animated, children }) {
31
+ const ref = useRef(null);
32
+ useIsomorphicLayoutEffect(() => {
33
+ const node = ref.current;
34
+ if (!node) return;
35
+ const report = () => onSize(node.getBoundingClientRect().width);
36
+ report();
37
+ if (typeof ResizeObserver === "undefined") return;
38
+ const observer = new ResizeObserver(report);
39
+ observer.observe(node);
40
+ return () => observer.disconnect();
41
+ }, [id, onSize]);
42
+ return /* @__PURE__ */ jsx("div", {
43
+ ref,
44
+ "data-face": id,
45
+ role: labelledBy ? "group" : void 0,
46
+ "aria-labelledby": labelledBy,
47
+ className: cn("flex h-full flex-none items-center gap-0.5 px-1 whitespace-nowrap data-[face=idle]:px-0" + (animated ? " motion-reduce:animate-face-in-soft" : "")),
48
+ children: /* @__PURE__ */ jsx(PartsAnimate.Provider, {
49
+ value: animated,
50
+ children
51
+ })
52
+ });
53
+ }
54
+ /** A leaving face steps out of the flow, exactly where it was, and fades while the pill springs to the next one. */
55
+ function LeavingFace({ id, onDone, children }) {
56
+ const remove = useCallback(() => onDone(id), [onDone, id]);
57
+ useExitBackstop(true, remove);
58
+ return /* @__PURE__ */ jsx("div", {
59
+ "aria-hidden": "true",
60
+ ref: (node) => node?.setAttribute("inert", ""),
61
+ onAnimationEnd: (event) => {
62
+ if (event.target === event.currentTarget) remove();
63
+ },
64
+ className: "pointer-events-none absolute start-0 top-0 flex h-full flex-none items-center gap-0.5 px-1 whitespace-nowrap motion-safe:animate-face-out motion-reduce:animate-fade-out",
65
+ children: /* @__PURE__ */ jsx(PartsAnimate.Provider, {
66
+ value: false,
67
+ children
68
+ })
69
+ });
70
+ }
71
+ /** A success disc that pops in with a tick drawing across it, the moment the action lands. */
72
+ function Check() {
73
+ return /* @__PURE__ */ jsxs("svg", {
74
+ className: "size-4.5 flex-none",
75
+ viewBox: "0 0 18 18",
76
+ fill: "none",
77
+ "aria-hidden": "true",
78
+ children: [/* @__PURE__ */ jsx("circle", {
79
+ cx: "9",
80
+ cy: "9",
81
+ r: "8",
82
+ className: "fill-success-strong [transform-box:fill-box] [transform-origin:center] motion-safe:animate-disc-in"
83
+ }), /* @__PURE__ */ jsx("path", {
84
+ d: "M5.6 9.3 7.8 11.4 12.4 6.7",
85
+ pathLength: 1,
86
+ strokeWidth: "1.9",
87
+ strokeLinecap: "round",
88
+ strokeLinejoin: "round",
89
+ className: "stroke-background [stroke-dasharray:1] motion-safe:animate-tick-draw"
90
+ })]
91
+ });
92
+ }
93
+ /** A ring that drains with the countdown. */
94
+ function Ring({ seconds, tone }) {
95
+ return /* @__PURE__ */ jsxs("svg", {
96
+ className: "size-4 flex-none",
97
+ viewBox: "0 0 16 16",
98
+ fill: "none",
99
+ "aria-hidden": "true",
100
+ children: [/* @__PURE__ */ jsx("circle", {
101
+ cx: "8",
102
+ cy: "8",
103
+ r: "6.25",
104
+ strokeWidth: "1.75",
105
+ className: "stroke-border"
106
+ }), /* @__PURE__ */ jsx("circle", {
107
+ cx: "8",
108
+ cy: "8",
109
+ r: "6.25",
110
+ strokeWidth: "1.75",
111
+ strokeLinecap: "round",
112
+ pathLength: 1,
113
+ style: { animationDuration: seconds + "s" },
114
+ className: cn("[stroke-dasharray:1] [transform-box:fill-box] [transform-origin:center] -rotate-90 motion-safe:animate-ring-drain " + (tone === "neutral" ? "stroke-primary" : "stroke-destructive"))
115
+ })]
116
+ });
117
+ }
118
+ /** Seconds that swap in place: the old digit fades up and out, the new one rises in, on tabular figures so the width holds. */
119
+ function Seconds({ value }) {
120
+ const ref = useRef(null);
121
+ const { leaving, changed, remove } = useLeavingItems(String(value), value, () => ref.current);
122
+ return /* @__PURE__ */ jsxs("span", {
123
+ className: "inline-grid min-w-2 text-foreground tabular-nums",
124
+ children: [leaving.map((item) => /* @__PURE__ */ jsx(LeavingDigit, {
125
+ id: item.id,
126
+ onDone: remove,
127
+ children: item.value
128
+ }, item.id)), /* @__PURE__ */ jsx("span", {
129
+ ref,
130
+ className: cn("col-start-1 row-start-1" + (changed ? " motion-safe:animate-digit-in motion-reduce:animate-fade-in" : "")),
131
+ children: value
132
+ }, value)]
133
+ });
134
+ }
135
+ function LeavingDigit({ id, onDone, children }) {
136
+ const remove = useCallback(() => onDone(id), [onDone, id]);
137
+ useExitBackstop(true, remove);
138
+ return /* @__PURE__ */ jsx("span", {
139
+ "aria-hidden": "true",
140
+ onAnimationEnd: (event) => {
141
+ if (event.target === event.currentTarget) remove();
142
+ },
143
+ className: "col-start-1 row-start-1 motion-safe:animate-digit-out motion-reduce:animate-fade-out",
144
+ children
145
+ });
146
+ }
147
+ function Spinner() {
148
+ return /* @__PURE__ */ jsx("svg", {
149
+ className: "size-4 flex-none motion-safe:animate-spin-fast motion-reduce:[animation:spin_1.6s_linear_infinite]",
150
+ viewBox: "0 0 24 24",
151
+ fill: "none",
152
+ stroke: "currentColor",
153
+ strokeWidth: 1.75,
154
+ strokeLinecap: "round",
155
+ "aria-hidden": "true",
156
+ children: /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
157
+ });
158
+ }
159
+ function AlertIcon() {
160
+ return /* @__PURE__ */ jsxs("svg", {
161
+ className: "size-4 flex-none text-destructive-strong",
162
+ viewBox: "0 0 24 24",
163
+ fill: "none",
164
+ stroke: "currentColor",
165
+ strokeWidth: 1.75,
166
+ strokeLinecap: "round",
167
+ strokeLinejoin: "round",
168
+ "aria-hidden": "true",
169
+ children: [/* @__PURE__ */ jsx("circle", {
170
+ cx: "12",
171
+ cy: "12",
172
+ r: "10"
173
+ }), /* @__PURE__ */ jsx("path", { d: "M12 8v4M12 16h.01" })]
174
+ });
175
+ }
176
+ const focusRing = "outline-none focus-visible:outline-solid focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset) focus-visible:outline-(--color-ring)";
177
+ const trigger = cn("inline-flex h-full cursor-pointer items-center gap-2 rounded-pill border-0 bg-transparent py-0 ps-3.5 pe-4 font-sans font-medium text-foreground motion-safe:transition-colors motion-safe:duration-(--duration-quick) motion-safe:ease-settle hover:not-active:not-disabled:bg-foreground/5 active:not-disabled:bg-foreground/10 active:not-disabled:duration-0 focus-visible:bg-foreground/6 disabled:cursor-not-allowed disabled:text-muted-foreground " + focusRing);
178
+ const secondary = "relative inline-flex confirm-button-h flex-none cursor-pointer touch-manipulation items-center justify-center rounded-pill border-0 bg-transparent px-3 font-sans font-medium text-muted-foreground motion-safe:transition-colors motion-safe:duration-(--duration-quick) motion-safe:ease-settle hover:not-active:bg-foreground/7 hover:not-active:text-foreground active:bg-foreground/10 active:text-foreground active:duration-0 focus-visible:bg-foreground/8 focus-visible:text-foreground " + focusRing;
179
+ const confirmDanger = "bg-destructive-subtle text-destructive-subtle-foreground hover:not-active:bg-[color-mix(in_oklab,var(--color-destructive)_18%,transparent)] active:bg-[color-mix(in_oklab,var(--color-destructive)_22%,transparent)] focus-visible:bg-[color-mix(in_oklab,var(--color-destructive)_20%,transparent)]";
180
+ const confirmNeutral = "bg-primary text-primary-foreground hover:not-active:bg-primary-hover active:bg-primary-pressed focus-visible:bg-primary-hover";
181
+ /**
182
+ * A button for an action that deserves a second thought, without a dialog. It
183
+ * asks in place: the pill morphs into a question with Cancel and Confirm, then
184
+ * a spinner, then a result with Undo, its width springing to each face while the
185
+ * faces crossfade at their natural width.
186
+ *
187
+ * Escape, a press outside, or the timeout answer the question with no. Focus
188
+ * follows the control between faces, but only when it was already inside, so a
189
+ * timeout never steals it. A live region announces each step.
190
+ *
191
+ * @example
192
+ * <ConfirmMorph label="Delete project" onConfirm={() => deleteProject()} onUndo={() => restore()} />
193
+ * <ConfirmMorph label="Delete project" confirmMode="hold" onConfirm={deleteProject} />
194
+ */
195
+ const ConfirmMorph = forwardRef(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 = 1e3, countdown = 3, countdownLabel, onConfirm, onUndo, onCancel, state: stateProp, defaultState = "idle", onStateChange, confirmTimeout = 6e3, resultTimeout = 5e3, cancelOnOutsidePress = true, isDisabled = false, className }, ref) {
196
+ const uid = useId();
197
+ const promptId = uid + "-prompt";
198
+ const holdHintId = uid + "-hold";
199
+ const rootRef = useRef(null);
200
+ useImperativeHandle(ref, () => rootRef.current, []);
201
+ const [inner, setInner] = useState(defaultState);
202
+ const state = stateProp ?? inner;
203
+ const [working, setWorking] = useState("confirm");
204
+ const [counting, setCounting] = useState(false);
205
+ const [announcement, setAnnouncement] = useState("");
206
+ const faceKey = state === "confirming" && counting ? "counting" : state;
207
+ const live = useRef({
208
+ state,
209
+ onStateChange,
210
+ controlled: stateProp !== void 0
211
+ });
212
+ useIsomorphicLayoutEffect(() => {
213
+ live.current = {
214
+ state,
215
+ onStateChange,
216
+ controlled: stateProp !== void 0
217
+ };
218
+ });
219
+ const pendingFocus = useRef(false);
220
+ const run = useRef(0);
221
+ const askedAt = useRef(0);
222
+ const keepFocus = useCallback(() => {
223
+ const root = rootRef.current;
224
+ pendingFocus.current = !!root && (root.contains(document.activeElement) || document.activeElement === document.body);
225
+ }, []);
226
+ const go = useCallback((next) => {
227
+ if (next !== "confirming") setCounting(false);
228
+ if (next === live.current.state) return;
229
+ keepFocus();
230
+ if (next === "confirming") askedAt.current = performance.now();
231
+ if (!live.current.controlled) setInner(next);
232
+ live.current.state = next;
233
+ live.current.onStateChange?.(next);
234
+ }, [keepFocus]);
235
+ const [seenState, setSeenState] = useState(state);
236
+ if (seenState !== state) {
237
+ setSeenState(state);
238
+ if (state !== "confirming") setCounting(false);
239
+ }
240
+ const toIdle = useCallback(() => {
241
+ run.current++;
242
+ go("idle");
243
+ }, [go]);
244
+ const perform = useCallback(async (kind) => {
245
+ const handler = kind === "confirm" ? onConfirm : onUndo;
246
+ const token = ++run.current;
247
+ setWorking(kind);
248
+ setCounting(false);
249
+ let result;
250
+ try {
251
+ result = handler?.();
252
+ } catch {
253
+ go("error");
254
+ setAnnouncement(errorLabel);
255
+ return;
256
+ }
257
+ if (result && typeof result.then === "function") {
258
+ go("pending");
259
+ setAnnouncement(kind === "confirm" ? pendingLabel : undoingLabel);
260
+ try {
261
+ await result;
262
+ } catch {
263
+ if (token !== run.current) return;
264
+ go("error");
265
+ setAnnouncement(errorLabel);
266
+ return;
267
+ }
268
+ if (token !== run.current) return;
269
+ }
270
+ if (kind === "undo") {
271
+ go("idle");
272
+ setAnnouncement("Undone");
273
+ return;
274
+ }
275
+ go("done");
276
+ setAnnouncement(onUndo ? doneLabel + ". " + undoLabel + " is available." : doneLabel);
277
+ }, [
278
+ doneLabel,
279
+ errorLabel,
280
+ go,
281
+ onConfirm,
282
+ onUndo,
283
+ pendingLabel,
284
+ undoLabel,
285
+ undoingLabel
286
+ ]);
287
+ const cancel = useCallback(() => {
288
+ onCancel?.();
289
+ toIdle();
290
+ setAnnouncement("Cancelled");
291
+ }, [onCancel, toIdle]);
292
+ const expire = useRef(() => {});
293
+ useIsomorphicLayoutEffect(() => {
294
+ expire.current = () => {
295
+ if (live.current.state === "confirming") cancel();
296
+ else toIdle();
297
+ };
298
+ });
299
+ const surfaceRef = useRef(null);
300
+ const target = useRef(0);
301
+ const flight = useRef(0);
302
+ const onFaceSize = useCallback((width) => {
303
+ if (Math.abs(width - target.current) < .25) return;
304
+ const from = target.current;
305
+ target.current = width;
306
+ const el = surfaceRef.current;
307
+ if (!el) return;
308
+ if (!from || prefersReducedMotion()) {
309
+ flight.current++;
310
+ el.style.width = "";
311
+ el.style.transition = "";
312
+ return;
313
+ }
314
+ const start = el.style.width ? el.getBoundingClientRect().width : from;
315
+ const token = ++flight.current;
316
+ const curve = width > start ? "morph" : "smooth";
317
+ el.style.transition = "none";
318
+ el.style.width = start + "px";
319
+ el.offsetWidth;
320
+ el.style.transition = "width var(--duration-" + curve + ") var(--ease-" + curve + "), background-color var(--duration-standard) var(--ease-settle), box-shadow var(--duration-standard) var(--ease-settle)";
321
+ el.style.width = width + "px";
322
+ const done = (event) => {
323
+ if (event.target !== el || event.propertyName !== "width") return;
324
+ el.removeEventListener("transitionend", done);
325
+ if (token !== flight.current) return;
326
+ el.style.width = "";
327
+ el.style.transition = "";
328
+ };
329
+ el.addEventListener("transitionend", done);
330
+ }, []);
331
+ const setHover = usePausableTimeout(state === "confirming" && !counting ? confirmTimeout : state === "done" || state === "error" ? resultTimeout : 0, state, () => expire.current());
332
+ const [seconds, setSeconds] = useState(countdown);
333
+ const performRef = useRef(perform);
334
+ useIsomorphicLayoutEffect(() => {
335
+ performRef.current = perform;
336
+ });
337
+ useEffect(() => {
338
+ if (!counting) return;
339
+ const startedAt = performance.now();
340
+ const timer = setInterval(() => {
341
+ const elapsed = (performance.now() - startedAt) / 1e3;
342
+ if (elapsed >= countdown) {
343
+ clearInterval(timer);
344
+ performRef.current("confirm");
345
+ } else setSeconds(Math.max(1, Math.ceil(countdown - elapsed)));
346
+ }, 100);
347
+ return () => {
348
+ clearInterval(timer);
349
+ setSeconds(countdown);
350
+ };
351
+ }, [counting, countdown]);
352
+ const fillRef = useRef(null);
353
+ const holdRef = useRef(null);
354
+ const progress = useRef(0);
355
+ const setFill = (value, transition) => {
356
+ const el = fillRef.current;
357
+ if (!el) return;
358
+ el.style.transition = transition;
359
+ el.style.clipPath = "inset(0 " + (1 - value) * 100 + "% 0 0 round 999px)";
360
+ };
361
+ const startHold = () => {
362
+ if (holdRef.current) return;
363
+ const from = progress.current;
364
+ const remaining = holdDuration * (1 - from);
365
+ setFill(from, "none");
366
+ fillRef.current?.offsetWidth;
367
+ setFill(1, "clip-path " + remaining + "ms linear");
368
+ const timer = setTimeout(() => {
369
+ holdRef.current = null;
370
+ progress.current = 1;
371
+ perform("confirm");
372
+ }, remaining);
373
+ holdRef.current = {
374
+ from,
375
+ at: performance.now(),
376
+ timer
377
+ };
378
+ };
379
+ const releaseHold = () => {
380
+ const held = holdRef.current;
381
+ if (!held) return;
382
+ holdRef.current = null;
383
+ clearTimeout(held.timer);
384
+ const value = Math.min(1, held.from + (performance.now() - held.at) / holdDuration);
385
+ setFill(value, "none");
386
+ fillRef.current?.offsetWidth;
387
+ progress.current = 0;
388
+ setFill(0, prefersReducedMotion() ? "none" : "clip-path 540ms var(--ease-smooth)");
389
+ setAnnouncement("Keep holding to " + confirmLabel.toLowerCase());
390
+ };
391
+ useEffect(() => () => {
392
+ if (holdRef.current) clearTimeout(holdRef.current.timer);
393
+ }, []);
394
+ useEffect(() => {
395
+ if (state !== "confirming") {
396
+ if (holdRef.current) clearTimeout(holdRef.current.timer);
397
+ holdRef.current = null;
398
+ progress.current = 0;
399
+ setFill(0, "none");
400
+ }
401
+ }, [state]);
402
+ const answer = () => {
403
+ if (performance.now() - askedAt.current < ARM_MS) return;
404
+ if (confirmMode === "countdown") {
405
+ keepFocus();
406
+ setCounting(true);
407
+ setAnnouncement((countdownLabel ?? pendingLabel + " in") + " " + countdown + " seconds");
408
+ return;
409
+ }
410
+ perform("confirm");
411
+ };
412
+ useEffect(() => {
413
+ if (state !== "confirming" || !cancelOnOutsidePress) return;
414
+ const down = (event) => {
415
+ if (!rootRef.current?.contains(event.target)) cancel();
416
+ };
417
+ document.addEventListener("pointerdown", down);
418
+ return () => document.removeEventListener("pointerdown", down);
419
+ }, [
420
+ cancel,
421
+ cancelOnOutsidePress,
422
+ state
423
+ ]);
424
+ useIsomorphicLayoutEffect(() => {
425
+ if (!pendingFocus.current) return;
426
+ pendingFocus.current = false;
427
+ const root = rootRef.current;
428
+ if (!root) return;
429
+ (root.querySelector("[data-face=\"" + faceKey + "\"]")?.querySelector("[data-autofocus]:not(:disabled)") ?? root).focus({ preventScroll: true });
430
+ }, [faceKey]);
431
+ const onKeyDown = (event) => {
432
+ if (event.key === "Escape") {
433
+ if (state === "confirming") {
434
+ event.preventDefault();
435
+ event.stopPropagation();
436
+ cancel();
437
+ } else if (state === "done" || state === "error") {
438
+ event.preventDefault();
439
+ event.stopPropagation();
440
+ toIdle();
441
+ }
442
+ return;
443
+ }
444
+ if (event.key === "Enter" && state === "confirming" && !event.repeat && !(event.target instanceof HTMLButtonElement)) {
445
+ event.preventDefault();
446
+ if (counting) perform("confirm");
447
+ else if (confirmMode !== "hold") answer();
448
+ }
449
+ };
450
+ const holdKeys = (event, down) => {
451
+ if (event.key !== "Enter" && event.key !== " ") return;
452
+ event.preventDefault();
453
+ if (down) {
454
+ if (!event.repeat) startHold();
455
+ } else releaseHold();
456
+ };
457
+ const holdPointer = (event) => {
458
+ if (event.button !== 0) return;
459
+ event.currentTarget.setPointerCapture?.(event.pointerId);
460
+ startHold();
461
+ };
462
+ const shownPrompt = prompt ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [label, "?"] });
463
+ const shownCountdown = countdownLabel ?? pendingLabel + " in";
464
+ const confirmClasses = cn("relative inline-flex confirm-button-h flex-none cursor-pointer touch-manipulation items-center justify-center overflow-clip rounded-pill border-0 px-3 font-sans font-medium motion-safe:transition-colors motion-safe:duration-(--duration-quick) motion-safe:ease-settle active:duration-0 outline-none focus-visible:outline-solid focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset) focus-visible:outline-(--color-ring) " + (tone === "neutral" ? confirmNeutral : confirmDanger));
465
+ const confirmButton = confirmMode === "hold" ? /* @__PURE__ */ jsxs("button", {
466
+ type: "button",
467
+ className: cn(confirmClasses + " select-none [-webkit-touch-callout:none]"),
468
+ "data-autofocus": true,
469
+ "aria-describedby": holdHintId,
470
+ onPointerDown: holdPointer,
471
+ onPointerUp: releaseHold,
472
+ onPointerCancel: releaseHold,
473
+ onLostPointerCapture: releaseHold,
474
+ onKeyDown: (event) => holdKeys(event, true),
475
+ onKeyUp: (event) => holdKeys(event, false),
476
+ onBlur: releaseHold,
477
+ onContextMenu: (event) => event.preventDefault(),
478
+ children: [/* @__PURE__ */ jsx("span", { children: confirmLabel }), /* @__PURE__ */ jsx("span", {
479
+ ref: fillRef,
480
+ "aria-hidden": "true",
481
+ style: { clipPath: "inset(0 100% 0 0 round 999px)" },
482
+ className: cn("pointer-events-none absolute inset-0 inline-flex items-center justify-center " + (tone === "neutral" ? "bg-primary-strong text-primary-foreground" : "bg-destructive text-destructive-foreground")),
483
+ children: /* @__PURE__ */ jsx("span", { children: confirmLabel })
484
+ })]
485
+ }) : /* @__PURE__ */ jsx("button", {
486
+ type: "button",
487
+ className: confirmClasses,
488
+ "data-autofocus": true,
489
+ onClick: answer,
490
+ onKeyDown: (event) => {
491
+ if (event.repeat) event.preventDefault();
492
+ },
493
+ children: confirmLabel
494
+ });
495
+ const statusPart = "gap-2 ps-3 pe-2.5 font-medium text-foreground tabular-nums";
496
+ const face = (() => {
497
+ switch (faceKey) {
498
+ case "confirming": return /* @__PURE__ */ jsxs(Fragment$1, { children: [
499
+ /* @__PURE__ */ jsx(Part, {
500
+ index: 0,
501
+ className: "max-w-64 overflow-hidden ps-3 pe-2 font-medium text-foreground tabular-nums",
502
+ children: /* @__PURE__ */ jsx("span", {
503
+ id: promptId,
504
+ className: "overflow-hidden text-ellipsis",
505
+ children: shownPrompt
506
+ })
507
+ }),
508
+ /* @__PURE__ */ jsx(Part, {
509
+ index: 1,
510
+ children: /* @__PURE__ */ jsx("button", {
511
+ type: "button",
512
+ className: secondary,
513
+ onClick: cancel,
514
+ children: cancelLabel
515
+ })
516
+ }),
517
+ /* @__PURE__ */ jsx(Part, {
518
+ index: 2,
519
+ children: confirmButton
520
+ }),
521
+ confirmMode === "hold" ? /* @__PURE__ */ jsx("span", {
522
+ id: holdHintId,
523
+ className: "sr-only",
524
+ children: "Press and hold"
525
+ }) : null
526
+ ] });
527
+ case "counting": return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Part, {
528
+ index: 0,
529
+ className: cn(statusPart + " pe-1.5 text-muted-foreground"),
530
+ children: [/* @__PURE__ */ jsx(Ring, {
531
+ seconds: countdown,
532
+ tone
533
+ }), /* @__PURE__ */ jsxs("span", {
534
+ className: "inline-flex items-baseline gap-1",
535
+ children: [
536
+ shownCountdown,
537
+ " ",
538
+ /* @__PURE__ */ jsx(Seconds, { value: seconds })
539
+ ]
540
+ })]
541
+ }), /* @__PURE__ */ jsx(Part, {
542
+ index: 1,
543
+ children: /* @__PURE__ */ jsx("button", {
544
+ type: "button",
545
+ className: secondary,
546
+ onClick: cancel,
547
+ children: cancelLabel
548
+ })
549
+ })] });
550
+ case "pending": return /* @__PURE__ */ jsxs(Part, {
551
+ index: 0,
552
+ className: cn(statusPart + " ps-3 pe-4 text-muted-foreground"),
553
+ children: [/* @__PURE__ */ jsx(Spinner, {}), /* @__PURE__ */ jsx("span", {
554
+ className: "inline-flex items-baseline gap-1",
555
+ children: working === "undo" ? undoingLabel : pendingLabel
556
+ })]
557
+ });
558
+ case "done": return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Part, {
559
+ index: 0,
560
+ className: cn(statusPart + (onUndo ? "" : " pe-3.5")),
561
+ children: [/* @__PURE__ */ jsx(Check, {}), /* @__PURE__ */ jsx("span", {
562
+ className: "inline-flex items-baseline gap-1",
563
+ children: doneLabel
564
+ })]
565
+ }), onUndo ? /* @__PURE__ */ jsx(Part, {
566
+ index: 1,
567
+ children: /* @__PURE__ */ jsx("button", {
568
+ type: "button",
569
+ className: cn(secondary + " text-foreground"),
570
+ "data-autofocus": true,
571
+ onClick: () => void perform("undo"),
572
+ children: undoLabel
573
+ })
574
+ }) : null] });
575
+ case "error": return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Part, {
576
+ index: 0,
577
+ className: statusPart,
578
+ children: [/* @__PURE__ */ jsx(AlertIcon, {}), /* @__PURE__ */ jsx("span", {
579
+ className: "inline-flex items-baseline gap-1",
580
+ children: errorLabel
581
+ })]
582
+ }), /* @__PURE__ */ jsx(Part, {
583
+ index: 1,
584
+ children: /* @__PURE__ */ jsx("button", {
585
+ type: "button",
586
+ className: cn(secondary + " text-foreground"),
587
+ "data-autofocus": true,
588
+ onClick: () => void perform(working),
589
+ children: retryLabel
590
+ })
591
+ })] });
592
+ default: return /* @__PURE__ */ jsx(Part, {
593
+ index: 0,
594
+ children: /* @__PURE__ */ jsxs("button", {
595
+ type: "button",
596
+ className: trigger,
597
+ "data-autofocus": true,
598
+ disabled: isDisabled,
599
+ onClick: () => {
600
+ setAnnouncement(typeof shownPrompt === "string" ? shownPrompt : "");
601
+ go("confirming");
602
+ },
603
+ children: [icon ? /* @__PURE__ */ jsx("span", {
604
+ className: "grid size-4 place-items-center text-muted-foreground [&>svg]:size-4",
605
+ "aria-hidden": "true",
606
+ children: icon
607
+ }) : null, /* @__PURE__ */ jsx("span", { children: label })]
608
+ })
609
+ });
610
+ }
611
+ })();
612
+ const { leaving, changed, remove } = useLeavingItems(faceKey, face, () => null);
613
+ return /* @__PURE__ */ jsxs("div", {
614
+ ref: rootRef,
615
+ "data-state": state,
616
+ "data-face": faceKey,
617
+ "data-tone": tone,
618
+ "data-mode": confirmMode,
619
+ "data-disabled": isDisabled || void 0,
620
+ tabIndex: -1,
621
+ "aria-busy": state === "pending" || void 0,
622
+ onKeyDown,
623
+ onPointerEnter: () => setHover(true),
624
+ onPointerLeave: () => setHover(false),
625
+ onPointerCancel: () => setHover(false),
626
+ className: cn("relative inline-flex max-w-full min-w-0 align-middle font-sans text-sm leading-5 text-foreground outline-none [-webkit-tap-highlight-color:transparent]", className),
627
+ children: [/* @__PURE__ */ jsxs("div", {
628
+ ref: surfaceRef,
629
+ className: cn("relative flex h-(--density-control-h-sm) max-w-full justify-start overflow-clip rounded-pill bg-card shadow-resting after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:border-(length:--border-width) after:border-border after:content-[''] motion-safe:transition-[background-color,box-shadow] motion-safe:duration-(--duration-standard) motion-safe:ease-settle " + (isDisabled && state === "idle" ? "shadow-none" : "")),
630
+ children: [leaving.map((item) => /* @__PURE__ */ jsx(LeavingFace, {
631
+ id: item.id,
632
+ onDone: remove,
633
+ children: item.value
634
+ }, item.id)), /* @__PURE__ */ jsx(Face, {
635
+ id: faceKey,
636
+ onSize: onFaceSize,
637
+ labelledBy: faceKey === "confirming" ? promptId : void 0,
638
+ animated: changed,
639
+ children: face
640
+ }, faceKey)]
641
+ }), /* @__PURE__ */ jsx("span", {
642
+ className: "sr-only",
643
+ role: "status",
644
+ "aria-live": "polite",
645
+ children: announcement
646
+ })]
647
+ });
648
+ });
649
+ //#endregion
650
+ export { ConfirmMorph };
651
+
652
+ //# sourceMappingURL=confirm-morph.js.map