@eifi1/ui-kit 0.49.0 → 0.50.1

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 (166) hide show
  1. package/README.md +12 -8
  2. package/dist/admin/user-roster.d.ts +16 -10
  3. package/dist/admin/user-roster.js +7 -3
  4. package/dist/admin/user-roster.js.map +1 -1
  5. package/dist/auth/verify-email.d.ts +9 -1
  6. package/dist/auth/verify-email.js +3 -1
  7. package/dist/auth/verify-email.js.map +1 -1
  8. package/dist/components/amount-input.d.ts +2 -1
  9. package/dist/components/amount-input.js +3 -2
  10. package/dist/components/amount-input.js.map +1 -1
  11. package/dist/components/autocomplete.js +14 -0
  12. package/dist/components/autocomplete.js.map +1 -1
  13. package/dist/components/combobox-core.js +1 -0
  14. package/dist/components/combobox-core.js.map +1 -1
  15. package/dist/components/combobox-suggestions.js +1 -0
  16. package/dist/components/combobox-suggestions.js.map +1 -1
  17. package/dist/components/combobox.js +3 -1
  18. package/dist/components/combobox.js.map +1 -1
  19. package/dist/components/currency-select.d.ts +28 -2
  20. package/dist/components/currency-select.js +56 -10
  21. package/dist/components/currency-select.js.map +1 -1
  22. package/dist/components/data-table-mobile.d.ts +11 -2
  23. package/dist/components/data-table-mobile.js +62 -8
  24. package/dist/components/data-table-mobile.js.map +1 -1
  25. package/dist/components/data-table-row.d.ts +1 -1
  26. package/dist/components/data-table-row.js.map +1 -1
  27. package/dist/components/data-table.js +3 -1
  28. package/dist/components/data-table.js.map +1 -1
  29. package/dist/components/dropdown.js +1 -0
  30. package/dist/components/dropdown.js.map +1 -1
  31. package/dist/components/full-bleed-dialog.js +1 -0
  32. package/dist/components/full-bleed-dialog.js.map +1 -1
  33. package/dist/components/inline-entity-combobox.js +12 -0
  34. package/dist/components/inline-entity-combobox.js.map +1 -1
  35. package/dist/components/modal.js +1 -0
  36. package/dist/components/modal.js.map +1 -1
  37. package/dist/components/number-input.d.ts +2 -1
  38. package/dist/components/number-input.js +6 -2
  39. package/dist/components/number-input.js.map +1 -1
  40. package/dist/components/numpad-sheet.js +2 -0
  41. package/dist/components/numpad-sheet.js.map +1 -1
  42. package/dist/components/picker-sheet.js +2 -0
  43. package/dist/components/picker-sheet.js.map +1 -1
  44. package/dist/components/popover.js +1 -0
  45. package/dist/components/popover.js.map +1 -1
  46. package/dist/eslint/index.js +1 -1
  47. package/dist/i18n/defaults.js +2 -0
  48. package/dist/i18n/defaults.js.map +1 -1
  49. package/dist/i18n/german.js +14 -1
  50. package/dist/i18n/german.js.map +1 -1
  51. package/dist/i18n/kit-labels.d.ts +3 -0
  52. package/dist/i18n/kit-labels.js.map +1 -1
  53. package/dist/i18n/locales/en.js +16 -1
  54. package/dist/i18n/locales/en.js.map +3 -3
  55. package/dist/i18n/locales/es.js +14 -1
  56. package/dist/i18n/locales/es.js.map +1 -1
  57. package/dist/i18n/locales/fr.js +14 -1
  58. package/dist/i18n/locales/fr.js.map +1 -1
  59. package/dist/i18n/locales/hu.js +14 -1
  60. package/dist/i18n/locales/hu.js.map +1 -1
  61. package/dist/i18n/locales/it.js +14 -1
  62. package/dist/i18n/locales/it.js.map +1 -1
  63. package/dist/i18n/locales/zh.js +14 -1
  64. package/dist/i18n/locales/zh.js.map +1 -1
  65. package/dist/i18n/review.js +1 -0
  66. package/dist/i18n/review.js.map +1 -1
  67. package/dist/lib/data-attributes.d.ts +11 -0
  68. package/dist/lib/data-attributes.js +10 -0
  69. package/dist/lib/data-attributes.js.map +1 -0
  70. package/dist/lib/format.d.ts +1 -0
  71. package/dist/lib/format.js +2 -0
  72. package/dist/lib/format.js.map +1 -1
  73. package/dist/lib/minus.d.ts +10 -0
  74. package/dist/lib/minus.js.map +1 -1
  75. package/dist/lib/quiet-focus.d.ts +4 -0
  76. package/dist/lib/quiet-focus.js +19 -0
  77. package/dist/lib/quiet-focus.js.map +1 -0
  78. package/dist/testing/index.d.ts +1 -0
  79. package/dist/testing/index.js +4 -0
  80. package/dist/testing/index.js.map +1 -1
  81. package/dist/testing/react.d.ts +25 -0
  82. package/dist/testing/react.js +19 -0
  83. package/dist/testing/react.js.map +1 -0
  84. package/dist/testing/tour.d.ts +31 -0
  85. package/dist/testing/tour.js +12 -0
  86. package/dist/testing/tour.js.map +1 -0
  87. package/dist/tour/tour-dock.d.ts +42 -0
  88. package/dist/tour/tour-dock.js +80 -0
  89. package/dist/tour/tour-dock.js.map +1 -0
  90. package/dist/tour/tour-engine.d.ts +12 -0
  91. package/dist/tour/tour-engine.js +31 -0
  92. package/dist/tour/tour-engine.js.map +1 -0
  93. package/dist/tour/tour-gates.d.ts +71 -0
  94. package/dist/tour/tour-gates.js +114 -0
  95. package/dist/tour/tour-gates.js.map +1 -0
  96. package/dist/tour/tour-guide-button.d.ts +27 -0
  97. package/dist/tour/tour-guide-button.js +45 -0
  98. package/dist/tour/tour-guide-button.js.map +1 -0
  99. package/dist/tour/tour-guide-labels.d.ts +8 -0
  100. package/dist/tour/tour-guide-labels.js +9 -0
  101. package/dist/tour/tour-guide-labels.js.map +1 -0
  102. package/dist/tour/tour-labels.js +9 -1
  103. package/dist/tour/tour-labels.js.map +1 -1
  104. package/dist/tour/tour.d.ts +144 -9
  105. package/dist/tour/tour.js +598 -59
  106. package/dist/tour/tour.js.map +1 -1
  107. package/dist/tour/use-tour-url-sync.d.ts +7 -2
  108. package/dist/tour/use-tour-url-sync.js +27 -1
  109. package/dist/tour/use-tour-url-sync.js.map +1 -1
  110. package/dist/tour.d.ts +2 -0
  111. package/dist/tour.js +3 -0
  112. package/dist/tour.js.map +1 -1
  113. package/dist/wizard/stepper-nav.js +15 -2
  114. package/dist/wizard/stepper-nav.js.map +1 -1
  115. package/dist/wizard/types.d.ts +11 -0
  116. package/dist/wizard/types.js.map +1 -1
  117. package/dist/wizard/use-wizard.js +11 -5
  118. package/dist/wizard/use-wizard.js.map +1 -1
  119. package/package.json +9 -1
  120. package/src/admin/user-roster.tsx +22 -12
  121. package/src/auth/verify-email.tsx +10 -0
  122. package/src/components/amount-input.tsx +5 -4
  123. package/src/components/autocomplete.tsx +20 -0
  124. package/src/components/combobox-core.tsx +2 -0
  125. package/src/components/combobox-suggestions.tsx +2 -0
  126. package/src/components/combobox.tsx +5 -1
  127. package/src/components/currency-select.tsx +95 -6
  128. package/src/components/data-table-mobile.tsx +99 -8
  129. package/src/components/data-table-row.tsx +1 -0
  130. package/src/components/data-table.tsx +9 -1
  131. package/src/components/dropdown.tsx +2 -0
  132. package/src/components/full-bleed-dialog.tsx +3 -0
  133. package/src/components/inline-entity-combobox.tsx +17 -0
  134. package/src/components/modal.tsx +3 -0
  135. package/src/components/number-input.tsx +8 -3
  136. package/src/components/numpad-sheet.tsx +4 -0
  137. package/src/components/picker-sheet.tsx +4 -0
  138. package/src/components/popover.tsx +2 -0
  139. package/src/i18n/defaults.ts +2 -0
  140. package/src/i18n/german.ts +13 -0
  141. package/src/i18n/kit-labels.tsx +3 -0
  142. package/src/i18n/locales/es.ts +13 -0
  143. package/src/i18n/locales/fr.ts +13 -0
  144. package/src/i18n/locales/hu.ts +13 -0
  145. package/src/i18n/locales/it.ts +13 -0
  146. package/src/i18n/locales/zh.ts +13 -0
  147. package/src/i18n/review.ts +1 -0
  148. package/src/lib/data-attributes.ts +14 -0
  149. package/src/lib/format.ts +2 -0
  150. package/src/lib/minus.ts +10 -0
  151. package/src/lib/quiet-focus.ts +32 -0
  152. package/src/testing/index.ts +8 -0
  153. package/src/testing/react.ts +59 -0
  154. package/src/testing/tour.ts +41 -0
  155. package/src/tour/tour-dock.ts +132 -0
  156. package/src/tour/tour-engine.ts +41 -0
  157. package/src/tour/tour-gates.ts +195 -0
  158. package/src/tour/tour-guide-button.tsx +64 -0
  159. package/src/tour/tour-guide-labels.ts +15 -0
  160. package/src/tour/tour-labels.ts +8 -0
  161. package/src/tour/tour.tsx +989 -86
  162. package/src/tour/use-tour-url-sync.ts +43 -4
  163. package/src/tour.ts +3 -0
  164. package/src/wizard/stepper-nav.tsx +23 -3
  165. package/src/wizard/types.ts +12 -0
  166. package/src/wizard/use-wizard.ts +16 -9
package/dist/tour/tour.js CHANGED
@@ -5,19 +5,42 @@ import {
5
5
  useCallback,
6
6
  useContext,
7
7
  useEffect,
8
+ useId,
8
9
  useLayoutEffect,
10
+ useMemo,
9
11
  useRef,
10
- useState
12
+ useState,
13
+ useSyncExternalStore
11
14
  } from "react";
12
15
  import { createPortal } from "react-dom";
13
16
  import { cn } from "../lib/cn.js";
14
17
  import { useKitLabels } from "../i18n/kit-labels.js";
15
18
  import { Button } from "../components/button.js";
16
19
  import { useFocusTrap } from "../hooks/use-focus-trap.js";
20
+ import { useDismissLayer } from "../hooks/use-dismiss.js";
17
21
  import { sameRect } from "../hooks/use-anchored-rect.js";
18
22
  import { dirOf, horizontalStep } from "../lib/direction.js";
23
+ import { isComposing } from "../lib/ime.js";
24
+ import { focusQuietly } from "../lib/quiet-focus.js";
19
25
  import { DEFAULT_TOUR_LABELS } from "./tour-labels.js";
20
26
  import { completeStored, rememberAccount, rememberMigrate } from "./use-tour-progress.js";
27
+ import { createGateRegistry, devWarnings, TourInternalContext } from "./tour-gates.js";
28
+ import { liveEngine } from "./tour-engine.js";
29
+ import { DOCK_MAX_WIDTH, dockEdge, dockStyle, readBand, scrollIntoFreeHalf } from "./tour-dock.js";
30
+ import { useTourGate, useTourGateRelease } from "./tour-gates.js";
31
+ const ENGINE_KEYS = [
32
+ "active",
33
+ "activeId",
34
+ "start",
35
+ "stop",
36
+ "next",
37
+ "prev",
38
+ "goToStep",
39
+ "index",
40
+ "total",
41
+ "position",
42
+ "count"
43
+ ];
21
44
  const TourContext = createContext(null);
22
45
  function useTour() {
23
46
  const ctx = useContext(TourContext);
@@ -27,25 +50,28 @@ function useTour() {
27
50
  function useTourOptional() {
28
51
  return useContext(TourContext);
29
52
  }
30
- function devWarnings() {
31
- return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
32
- }
33
53
  function TourProvider({
34
54
  children,
35
55
  labels,
36
56
  progress,
37
- onComplete
57
+ onComplete,
58
+ launch,
59
+ dockOnPhones = true
38
60
  }) {
39
61
  const merged = useKitLabels("tour", DEFAULT_TOUR_LABELS, labels);
40
62
  const [steps, setSteps] = useState(null);
41
- const [index, setIndex] = useState(0);
63
+ const [nav, setNav] = useState(NO_NAV);
64
+ const index = nav.index;
65
+ const [refusal, setRefusal] = useState(null);
42
66
  const [activeId, setActiveId] = useState(null);
43
67
  const optsRef = useRef({});
44
68
  const progressRef = useRef(progress);
45
69
  const onCompleteRef = useRef(onComplete);
70
+ const launchRef = useRef(launch);
46
71
  useLayoutEffect(() => {
47
72
  progressRef.current = progress;
48
73
  onCompleteRef.current = onComplete;
74
+ launchRef.current = launch;
49
75
  });
50
76
  const storageKey = progress?.storageKey;
51
77
  const migrate = progress?.migrate;
@@ -61,15 +87,89 @@ function TourProvider({
61
87
  stepsRef.current = steps;
62
88
  indexRef.current = index;
63
89
  }, [steps, index]);
90
+ const floorRef = useRef(0);
64
91
  const active = !!steps && steps.length > 0;
65
92
  const total = steps?.length ?? 0;
66
- const start = useCallback((s, opts) => {
67
- optsRef.current = opts ?? {};
68
- optsRef.current.onStart?.();
69
- setIndex(Math.min(Math.max(0, opts?.startIndex ?? 0), Math.max(0, s.length - 1)));
70
- setActiveId(opts?.id ?? opts?.tour?.id ?? null);
71
- setSteps(s);
72
- }, []);
93
+ const [gates] = useState(createGateRegistry);
94
+ useLayoutEffect(() => {
95
+ gates.setActive(active);
96
+ }, [gates, active]);
97
+ const canLaunch = !!launch;
98
+ const internals = useMemo(() => ({ gates, canLaunch }), [gates, canLaunch]);
99
+ const shown = useCallback(
100
+ (s, i, via) => isShown(s[i], stepContext(gates, via)),
101
+ [gates]
102
+ );
103
+ const arrive = useCallback(
104
+ (s, i, via) => {
105
+ const ctx = stepContext(gates, via);
106
+ let position = 0;
107
+ let count = 0;
108
+ for (let j = floorRef.current; j < s.length; j++) {
109
+ if (j !== i && !isShown(s[j], ctx)) continue;
110
+ count++;
111
+ if (j <= i) position++;
112
+ }
113
+ indexRef.current = i;
114
+ setRefusal(null);
115
+ setNav({ index: i, floor: floorRef.current, via, position, count });
116
+ },
117
+ [gates]
118
+ );
119
+ const forward = useCallback(
120
+ (s, from, via) => {
121
+ for (let j = Math.max(from, floorRef.current); j < s.length; j++) if (shown(s, j, via)) return j;
122
+ return -1;
123
+ },
124
+ [shown]
125
+ );
126
+ const backward = useCallback(
127
+ (s, from, via) => {
128
+ for (let j = Math.min(from, s.length - 1); j >= floorRef.current; j--) if (shown(s, j, via)) return j;
129
+ return -1;
130
+ },
131
+ [shown]
132
+ );
133
+ const startSteps = useCallback(
134
+ (s, opts) => {
135
+ optsRef.current = opts ?? {};
136
+ optsRef.current.onStart?.();
137
+ const last = Math.max(0, s.length - 1);
138
+ floorRef.current = opts?.at === void 0 ? 0 : resolveAt(s, opts.at);
139
+ const via = opts?.at === void 0 ? "start" : "at";
140
+ const wanted = Math.min(Math.max(floorRef.current, opts?.startIndex ?? floorRef.current), last);
141
+ let to = forward(s, wanted, via);
142
+ if (to < 0) to = backward(s, wanted, via);
143
+ if (to < 0) return;
144
+ arrive(s, to, via);
145
+ setActiveId(opts?.id ?? opts?.tour?.id ?? null);
146
+ setSteps(s);
147
+ },
148
+ [arrive, forward, backward]
149
+ );
150
+ const ownStartRef = useRef(null);
151
+ const [live] = useState(() => liveEngine(ENGINE_KEYS));
152
+ const engine = live.engine;
153
+ const start = useCallback(
154
+ (what, opts) => {
155
+ if (typeof what !== "string") {
156
+ startSteps(what, opts);
157
+ return;
158
+ }
159
+ const run = launchRef.current;
160
+ if (!run) {
161
+ if (devWarnings()) console.warn(`[ui-kit] start("${what}") needs TourProvider's \`launch\` prop.`);
162
+ return;
163
+ }
164
+ void Promise.resolve(run(what, { at: opts?.at }, ownStartRef.current, engine)).catch(
165
+ (error) => console.error(error)
166
+ );
167
+ },
168
+ [startSteps, engine]
169
+ );
170
+ useLayoutEffect(() => {
171
+ ownStartRef.current = start;
172
+ }, [start]);
73
173
  const stop = useCallback(() => {
74
174
  setSteps(null);
75
175
  setActiveId(null);
@@ -103,34 +203,117 @@ function TourProvider({
103
203
  setSteps(null);
104
204
  setActiveId(null);
105
205
  }, []);
106
- const goToStep = useCallback((i) => {
107
- const len = stepsRef.current?.length ?? 0;
108
- if (len === 0) return;
109
- setIndex(Math.min(Math.max(0, i), len - 1));
110
- }, []);
206
+ const goToStep = useCallback(
207
+ (i) => {
208
+ const s = stepsRef.current;
209
+ if (!s || s.length === 0) return;
210
+ const wanted = Math.min(Math.max(floorRef.current, i), s.length - 1);
211
+ let to = forward(s, wanted, "goto");
212
+ if (to < 0) to = backward(s, wanted, "goto");
213
+ if (to >= 0) arrive(s, to, "goto");
214
+ },
215
+ [arrive, forward, backward]
216
+ );
111
217
  const next = useCallback(() => {
112
- const len = stepsRef.current?.length ?? 0;
113
- if (len === 0) return;
114
- if (indexRef.current >= len - 1) finish();
115
- else setIndex(indexRef.current + 1);
116
- }, [finish]);
218
+ const s = stepsRef.current;
219
+ if (!s || s.length === 0) return;
220
+ const to = forward(s, indexRef.current + 1, "next");
221
+ if (to < 0) finish();
222
+ else arrive(s, to, "next");
223
+ }, [arrive, forward, finish]);
117
224
  const prev = useCallback(() => {
118
- if (indexRef.current > 0) setIndex(indexRef.current - 1);
119
- }, []);
120
- return /* @__PURE__ */ jsxs(TourContext.Provider, { value: { active, activeId, start, stop, next, prev, goToStep, index, total }, children: [
225
+ const s = stepsRef.current;
226
+ if (!s) return;
227
+ const to = backward(s, indexRef.current - 1, "back");
228
+ if (to >= 0) arrive(s, to, "back");
229
+ }, [arrive, backward]);
230
+ useEffect(() => {
231
+ if (!active) return;
232
+ let seq = 0;
233
+ return gates.listen((signal) => {
234
+ const s = stepsRef.current;
235
+ if (!s) return;
236
+ const i = indexRef.current;
237
+ if (signal.kind === "refuse") {
238
+ if (s[i]?.awaitGate === signal.key) setRefusal({ step: s[i], reason: signal.reason, seq: ++seq });
239
+ return;
240
+ }
241
+ const awaiting = s[i]?.awaitGate === signal.key ? i : s.findIndex((st, k) => k > i && st.awaitGate === signal.key);
242
+ if (awaiting < 0) return;
243
+ const to = forward(s, awaiting + 1, "next");
244
+ if (to < 0) finish();
245
+ else arrive(s, to, "next");
246
+ });
247
+ }, [active, gates, arrive, forward, finish]);
248
+ const value = {
249
+ active,
250
+ activeId,
251
+ start,
252
+ stop,
253
+ next,
254
+ prev,
255
+ goToStep,
256
+ index,
257
+ total,
258
+ position: active ? nav.position : 0,
259
+ count: active ? nav.count : 0
260
+ };
261
+ useLayoutEffect(() => {
262
+ live.set(value);
263
+ });
264
+ return /* @__PURE__ */ jsx(TourInternalContext.Provider, { value: internals, children: /* @__PURE__ */ jsxs(TourContext.Provider, { value, children: [
121
265
  children,
122
266
  active && /* @__PURE__ */ jsx(
123
267
  TourOverlay,
124
268
  {
125
269
  steps,
126
270
  index,
271
+ via: nav.via,
272
+ floor: nav.floor,
273
+ position: nav.position,
274
+ count: nav.count,
275
+ refusal: refusal?.step === steps[index] ? refusal : null,
276
+ onGoTo: goToStep,
127
277
  labels: merged,
278
+ gates,
279
+ dockOnPhones,
128
280
  onNext: next,
129
281
  onPrev: prev,
130
282
  onSkip: skip
131
283
  }
132
284
  )
133
- ] });
285
+ ] }) });
286
+ }
287
+ const NO_NAV = { index: 0, floor: 0, via: "start", position: 0, count: 0 };
288
+ const NOOP = () => {
289
+ };
290
+ function stepContext(gates, via) {
291
+ return { gate: (key) => gates.get(key), via };
292
+ }
293
+ function resolveAt(steps, at) {
294
+ if (typeof at === "number") return Math.min(Math.max(0, Math.floor(at)), Math.max(0, steps.length - 1));
295
+ const anchor = `data-tour="${at}"`;
296
+ const found = steps.findIndex((step) => step.target?.includes(anchor));
297
+ if (found >= 0) return found;
298
+ if (devWarnings()) console.warn(`[ui-kit] A tour was started at "${at}", but no step targets [${anchor}]: it starts at 0.`);
299
+ return 0;
300
+ }
301
+ function firstEmptied(steps, floor, index, gates) {
302
+ const ctx = stepContext(gates, "goto");
303
+ for (let j = floor; j < index; j++) {
304
+ const key = steps[j].gate;
305
+ if (key && gates.get(key) === false && isShown(steps[j], ctx)) return j;
306
+ }
307
+ return -1;
308
+ }
309
+ function isShown(step, ctx) {
310
+ if (!step.when) return true;
311
+ try {
312
+ return step.when(ctx) === true;
313
+ } catch (error) {
314
+ console.error(error);
315
+ return false;
316
+ }
134
317
  }
135
318
  function isVisible(el) {
136
319
  const r = el.getBoundingClientRect();
@@ -149,29 +332,142 @@ function rectOf(el) {
149
332
  const r = el.getBoundingClientRect();
150
333
  return { top: r.top, left: r.left, width: r.width, height: r.height };
151
334
  }
335
+ const RING_BAND = 6;
336
+ function spotRectOf(el, step) {
337
+ const own = rectOf(el);
338
+ if (!isFieldStep(step)) return own;
339
+ const ids = /* @__PURE__ */ new Set();
340
+ const holders = [el, ...Array.from(el.querySelectorAll("[aria-describedby]"))];
341
+ for (const holder of holders) {
342
+ for (const id of (holder.getAttribute("aria-describedby") ?? "").split(/\s+/)) if (id) ids.add(id);
343
+ }
344
+ if (ids.size === 0) return own;
345
+ const lines = [];
346
+ for (const id of ids) {
347
+ const line = document.getElementById(id);
348
+ if (!line || el.contains(line) || line.closest('[role="tooltip"]') || !isVisible(line)) continue;
349
+ const r = line.getBoundingClientRect();
350
+ if (r.width > 1 && r.height > 1) lines.push(r);
351
+ }
352
+ let top = own.top;
353
+ let left = own.left;
354
+ let right = own.left + own.width;
355
+ let bottom = own.top + own.height;
356
+ const reach = (step.padding ?? 8) + RING_BAND;
357
+ for (let grew = true; grew; ) {
358
+ grew = false;
359
+ for (let i = lines.length - 1; i >= 0; i--) {
360
+ const r = lines[i];
361
+ const across = r.left < right && r.right > left;
362
+ const gap = Math.max(r.top - bottom, top - r.bottom);
363
+ if (!across || gap > reach) continue;
364
+ top = Math.min(top, r.top);
365
+ left = Math.min(left, r.left);
366
+ right = Math.max(right, r.right);
367
+ bottom = Math.max(bottom, r.bottom);
368
+ lines.splice(i, 1);
369
+ grew = true;
370
+ }
371
+ }
372
+ return { top, left, width: right - left, height: bottom - top };
373
+ }
374
+ function layerHost(el) {
375
+ return el.closest("[data-kit-layer]");
376
+ }
377
+ function coversViewport(host, el) {
378
+ let surface = host;
379
+ if (host.getAttribute("data-kit-layer") === "backdrop") {
380
+ for (const child of Array.from(host.children)) {
381
+ if (child.contains(el)) {
382
+ surface = child;
383
+ break;
384
+ }
385
+ }
386
+ }
387
+ const r = surface.getBoundingClientRect();
388
+ const vv = window.visualViewport;
389
+ const top = vv?.offsetTop ?? 0;
390
+ const bottom = top + (vv?.height ?? window.innerHeight);
391
+ const width = vv?.width ?? window.innerWidth;
392
+ return r.width > 0 && r.left <= 1 && r.top <= top + 1 && r.right >= width - 1 && r.bottom >= bottom - 1;
393
+ }
394
+ function docks(step, dockOnPhones) {
395
+ if (typeof window === "undefined" || window.innerWidth >= DOCK_MAX_WIDTH || !step.target) return false;
396
+ return step.placement === "dock" || step.placement === void 0 && dockOnPhones && isFieldStep(step);
397
+ }
398
+ function isFieldStep(step) {
399
+ return !!(step.interactive || step.awaitInput || step.gate || step.awaitGate);
400
+ }
401
+ function firstFocusable(el) {
402
+ if (el instanceof HTMLElement && el.matches(FOCUSABLE_CONTROL)) return el;
403
+ return el.querySelector(FOCUSABLE_CONTROL);
404
+ }
405
+ function tabStops(el) {
406
+ const all = el.matches(FOCUSABLE_CONTROL) ? [el] : [];
407
+ return all.concat(
408
+ Array.from(el.querySelectorAll(FOCUSABLE_CONTROL)).filter(
409
+ (x) => x.tabIndex >= 0 && !x.closest("[hidden],[inert]")
410
+ )
411
+ );
412
+ }
413
+ const FOCUSABLE_CONTROL = 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
414
+ function coarsePointer() {
415
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(pointer: coarse)").matches : false;
416
+ }
152
417
  function TourOverlay({
153
418
  steps,
154
419
  index,
420
+ via,
421
+ floor,
422
+ position,
423
+ count,
424
+ refusal,
425
+ onGoTo,
155
426
  labels,
427
+ gates,
428
+ dockOnPhones,
156
429
  onNext,
157
430
  onPrev,
158
431
  onSkip
159
432
  }) {
160
433
  const step = steps[index];
161
- const isFirst = index === 0;
162
- const isLast = index === steps.length - 1;
434
+ const isFirst = position <= 1;
435
+ const isLast = position >= count;
163
436
  const [located, setLocated] = useState(null);
164
437
  const current = located !== null && located.index === index && located.step === step ? located : null;
165
438
  const ready = current !== null;
166
439
  const rect = current?.rect ?? null;
167
440
  const dir = current?.dir ?? "ltr";
168
441
  const rootRef = useRef(null);
442
+ const cardRef = useRef(null);
443
+ const bodyId = useId();
444
+ const hintId = useId();
445
+ const gateVersion = useSyncExternalStore(gates.subscribe, gates.version, gates.version);
446
+ const gateKey = step.gate;
447
+ const enforced = !!gateKey && gates.has(gateKey);
448
+ const locked = enforced && gates.get(gateKey) !== true;
449
+ const [polite, setPolite] = useState("");
450
+ const announce = useCallback((text) => {
451
+ setPolite((prev) => prev === text ? `${text}\u200B` : text);
452
+ }, []);
169
453
  useEffect(() => {
170
454
  let cancelled = false;
171
455
  let raf = 0;
172
- const settle = (el) => setLocated({ index, step, rect: el ? rectOf(el) : null, dir: dirOf(el) });
456
+ const settle = (el) => {
457
+ let layer2 = null;
458
+ let fullScreen = false;
459
+ if (el && isFieldStep(step)) {
460
+ const host = layerHost(el);
461
+ layer2 = document.createElement("div");
462
+ layer2.setAttribute("data-tour-layer", "");
463
+ if (host) host.after(layer2);
464
+ else document.body.appendChild(layer2);
465
+ fullScreen = !!host && coversViewport(host, el);
466
+ }
467
+ setLocated({ index, step, rect: el ? spotRectOf(el, step) : null, dir: dirOf(el), layer: layer2, fullScreen });
468
+ };
173
469
  void (async () => {
174
- await step.beforeStep?.();
470
+ await step.beforeStep?.(stepContext(gates, via));
175
471
  if (cancelled) return;
176
472
  if (!step.target) {
177
473
  settle(null);
@@ -182,7 +478,8 @@ function TourOverlay({
182
478
  if (cancelled) return;
183
479
  const el = queryVisibleTarget(step.target);
184
480
  if (el) {
185
- el.scrollIntoView({ block: "center", behavior: "smooth" });
481
+ if (docks(step, dockOnPhones)) scrollIntoFreeHalf(el, null, null);
482
+ else el.scrollIntoView({ block: "center", behavior: "smooth" });
186
483
  settle(el);
187
484
  return;
188
485
  }
@@ -201,10 +498,15 @@ function TourOverlay({
201
498
  const update = () => {
202
499
  const el = queryVisibleTarget(step.target);
203
500
  if (el) {
204
- const next = rectOf(el);
205
- setLocated(
206
- (prev) => prev && prev.index === index && prev.step === step && !sameRect(prev.rect, next) ? { ...prev, rect: next } : prev
207
- );
501
+ const next = spotRectOf(el, step);
502
+ const host = layerHost(el);
503
+ const full = !!host && coversViewport(host, el);
504
+ setLocated((prev) => {
505
+ if (!prev || prev.index !== index || prev.step !== step) return prev;
506
+ const fullScreen = prev.layer ? full : prev.fullScreen;
507
+ if (sameRect(prev.rect, next) && fullScreen === prev.fullScreen) return prev;
508
+ return { ...prev, rect: next, fullScreen };
509
+ });
208
510
  }
209
511
  };
210
512
  window.addEventListener("scroll", update, true);
@@ -216,6 +518,23 @@ function TourOverlay({
216
518
  clearInterval(id);
217
519
  };
218
520
  }, [index, step]);
521
+ const layer = located?.layer ?? null;
522
+ useEffect(() => {
523
+ if (!layer) return;
524
+ return () => layer.remove();
525
+ }, [layer]);
526
+ useDismissLayer(
527
+ ready,
528
+ (e) => {
529
+ if (fieldStepNow && step.target && e.target instanceof Node) {
530
+ const el = queryVisibleTarget(step.target);
531
+ if (el?.contains(e.target)) return;
532
+ }
533
+ onSkip();
534
+ },
535
+ // Not an overlay to `useOverlayOpen`: a floating launcher a tour spotlights stays.
536
+ { overlay: false }
537
+ );
219
538
  const spotted = rect !== null;
220
539
  useEffect(() => {
221
540
  if (!ready || !spotted || !step.awaitClick || !step.target) return;
@@ -225,7 +544,112 @@ function TourOverlay({
225
544
  el.addEventListener("click", onClick, { once: true });
226
545
  return () => el.removeEventListener("click", onClick);
227
546
  }, [ready, spotted, index, step, onNext]);
228
- const interactive = spotted && !!step.target && !!(step.interactive || step.awaitInput);
547
+ const interactive = spotted && !!step.target && !!(step.interactive || step.awaitInput || step.gate || step.awaitGate);
548
+ const guided = interactive && !!(step.gate || step.awaitInput || step.awaitGate);
549
+ const fieldStepNow = interactive;
550
+ const awaitsRelease = spotted && !!step.target && !!step.awaitGate;
551
+ const warnedGateRef = useRef(null);
552
+ useEffect(() => {
553
+ if (!ready || !gateKey || gates.has(gateKey) || !devWarnings() || warnedGateRef.current === step) return;
554
+ warnedGateRef.current = step;
555
+ console.warn(
556
+ `[ui-kit] Tour step "${step.title}" waits for the gate "${gateKey}", which nothing publishes: its Next is open. Publish it with useTourGate beside the field, or the form has closed under the tour.`
557
+ );
558
+ }, [ready, gateKey, gates, step, gateVersion]);
559
+ useEffect(() => {
560
+ if (!ready || !step.optional || step.gate || !devWarnings()) return;
561
+ console.warn(
562
+ `[ui-kit] Tour step "${step.title}" is \`optional\` without a \`gate\`: it shows the Optional chip and a plain Next. "Leave empty" needs a gate to compare.`
563
+ );
564
+ }, [ready, step]);
565
+ const wasLockedRef = useRef(null);
566
+ useEffect(() => {
567
+ if (!ready || !gateKey) return;
568
+ const before = wasLockedRef.current;
569
+ if (before && before.step === step && before.locked && !locked && enforced) announce(labels.gateReady);
570
+ wasLockedRef.current = { step, locked };
571
+ }, [ready, gateKey, step, locked, enforced, announce, labels.gateReady]);
572
+ useEffect(() => {
573
+ if (!ready || !guided || !step.target) return;
574
+ const el = queryVisibleTarget(step.target);
575
+ const control = el ? firstFocusable(el) : null;
576
+ const previous = control ? control.getAttribute("aria-describedby") : null;
577
+ if (control) control.setAttribute("aria-describedby", previous ? `${previous} ${bodyId}` : bodyId);
578
+ if (coarsePointer() || !control) cardRef.current?.focus();
579
+ else if (!el.contains(document.activeElement)) focusQuietly(control);
580
+ if (!control) return;
581
+ return () => {
582
+ if (previous === null) control.removeAttribute("aria-describedby");
583
+ else control.setAttribute("aria-describedby", previous);
584
+ };
585
+ }, [ready, guided, index, step, bodyId]);
586
+ useEffect(() => {
587
+ if (!ready || !guided || !step.target) return;
588
+ const onTab = (e) => {
589
+ if (e.key !== "Tab") return;
590
+ const el = queryVisibleTarget(step.target);
591
+ const card = cardRef.current;
592
+ if (!el || !card) return;
593
+ const active = document.activeElement;
594
+ const fieldStops = tabStops(el);
595
+ const cardStops = tabStops(card);
596
+ if (fieldStops.length === 0 || cardStops.length === 0) return;
597
+ let to = null;
598
+ if (active instanceof Node && el.contains(active)) {
599
+ if (!e.shiftKey && active === fieldStops[fieldStops.length - 1]) to = cardStops[0];
600
+ else if (e.shiftKey && active === fieldStops[0]) to = cardStops[cardStops.length - 1];
601
+ } else if (active instanceof Node && card.contains(active)) {
602
+ if (!e.shiftKey && active === cardStops[cardStops.length - 1]) to = fieldStops[0];
603
+ else if (e.shiftKey && (active === cardStops[0] || active === card)) to = fieldStops[fieldStops.length - 1];
604
+ }
605
+ if (!to) return;
606
+ e.preventDefault();
607
+ e.stopPropagation();
608
+ to.focus();
609
+ };
610
+ document.addEventListener("keydown", onTab, true);
611
+ return () => document.removeEventListener("keydown", onTab, true);
612
+ }, [ready, guided, index, step]);
613
+ useEffect(() => {
614
+ if (!ready || !spotted || !gateKey || !step.target) return;
615
+ const el = queryVisibleTarget(step.target);
616
+ if (!el) return;
617
+ const satisfied = () => !gates.has(gateKey) || gates.get(gateKey) === true;
618
+ let timer = 0;
619
+ let armed = el.contains(document.activeElement);
620
+ const check = () => {
621
+ window.clearTimeout(timer);
622
+ timer = window.setTimeout(() => {
623
+ if (satisfied()) onNext();
624
+ }, 0);
625
+ };
626
+ const onFocusIn = () => {
627
+ armed = true;
628
+ };
629
+ const onFocusOut = (e) => {
630
+ const to = e.relatedTarget;
631
+ if (to instanceof Node && el.contains(to)) return;
632
+ const wasArmed = armed;
633
+ armed = false;
634
+ if (!wasArmed) return;
635
+ if (to === null || to instanceof Node && rootRef.current?.contains(to)) check();
636
+ };
637
+ const onKeyDown = (e) => {
638
+ if (e.key !== "Enter" || !(e.target instanceof Node) || !el.contains(e.target)) return;
639
+ if (e.defaultPrevented || isComposing(e)) return;
640
+ if (e.target instanceof HTMLTextAreaElement && !(e.ctrlKey || e.metaKey)) return;
641
+ check();
642
+ };
643
+ el.addEventListener("focusin", onFocusIn);
644
+ el.addEventListener("focusout", onFocusOut);
645
+ document.addEventListener("keydown", onKeyDown);
646
+ return () => {
647
+ window.clearTimeout(timer);
648
+ el.removeEventListener("focusin", onFocusIn);
649
+ el.removeEventListener("focusout", onFocusOut);
650
+ document.removeEventListener("keydown", onKeyDown);
651
+ };
652
+ }, [ready, spotted, gateKey, gates, index, step, onNext]);
229
653
  useEffect(() => {
230
654
  if (!ready || !spotted || !step.awaitInput || !step.target) return;
231
655
  const el = queryVisibleTarget(step.target);
@@ -259,34 +683,44 @@ function TourOverlay({
259
683
  el.removeEventListener("change", check);
260
684
  };
261
685
  }, [ready, spotted, index, step, onNext]);
686
+ const gateHint = step.gateHint ?? labels.gateHint;
687
+ const lockedNext = useCallback(() => {
688
+ const el = step.target ? queryVisibleTarget(step.target) : null;
689
+ const control = el ? firstFocusable(el) : null;
690
+ if (control) focusQuietly(control);
691
+ announce(gateHint);
692
+ }, [step.target, announce, gateHint]);
693
+ const tryNext = awaitsRelease ? NOOP : locked ? lockedNext : onNext;
694
+ const backTo = refusal ? firstEmptied(steps, floor, index, gates) : -1;
262
695
  useEffect(() => {
263
696
  const onKey = (e) => {
264
697
  if (interactive && e.target instanceof Node && e.target !== document.body && !rootRef.current?.contains(e.target)) {
265
698
  return;
266
699
  }
267
700
  if (e.key === "Escape") {
268
- onSkip();
701
+ if (!ready) onSkip();
269
702
  return;
270
703
  }
271
704
  if (e.key === "Enter") {
272
705
  if (step.awaitClick || step.awaitInput || ownsKey(e.target, "Enter")) return;
273
706
  e.preventDefault();
274
- onNext();
707
+ tryNext();
275
708
  return;
276
709
  }
277
710
  if (ownsKey(e.target, e.key)) return;
278
711
  const delta = horizontalStep(e.key, rootRef.current);
279
712
  if (delta === 1) {
280
713
  e.preventDefault();
281
- onNext();
714
+ tryNext();
282
715
  } else if (delta === -1) {
283
716
  if (!isFirst) onPrev();
284
717
  }
285
718
  };
286
719
  document.addEventListener("keydown", onKey);
287
720
  return () => document.removeEventListener("keydown", onKey);
288
- }, [isFirst, step, interactive, onNext, onPrev, onSkip]);
721
+ }, [isFirst, step, interactive, tryNext, onPrev, onSkip, ready]);
289
722
  if (!ready) return null;
723
+ const bare = interactive && !!current?.fullScreen;
290
724
  const pad = step.padding ?? 8;
291
725
  const spot = rect ? { top: rect.top - pad, left: rect.left - pad, width: rect.width + pad * 2, height: rect.height + pad * 2 } : null;
292
726
  return createPortal(
@@ -294,15 +728,16 @@ function TourOverlay({
294
728
  "div",
295
729
  {
296
730
  ref: rootRef,
297
- className: cn("pointer-events-none fixed inset-0", interactive ? "z-[45]" : "z-[60]"),
731
+ className: cn("pointer-events-none fixed inset-0", interactive ? "z-50" : "z-[60]"),
298
732
  role: "dialog",
299
733
  "aria-modal": interactive ? void 0 : "true",
300
734
  "data-tour-overlay": "",
301
735
  "data-tour-interactive": interactive ? "" : void 0,
736
+ "data-tour-bare": bare ? "" : void 0,
302
737
  "aria-label": step.title,
303
738
  dir,
304
739
  children: [
305
- spot && /* @__PURE__ */ jsx(SpotlightShields, { spot }),
740
+ spot && !bare && /* @__PURE__ */ jsx(SpotlightShields, { spot }),
306
741
  spot ? /* @__PURE__ */ jsx(
307
742
  "div",
308
743
  {
@@ -330,7 +765,7 @@ function TourOverlay({
330
765
  "0 0 0 3px rgba(255,255,255,0.98)",
331
766
  "0 0 0 5px rgba(15,23,42,0.92)",
332
767
  "0 0 18px 6px rgba(15,23,42,0.45)",
333
- "0 0 0 9999px rgba(15,23,42,0.70)"
768
+ ...bare ? [] : ["0 0 0 9999px rgba(15,23,42,0.70)"]
334
769
  ].join(", ")
335
770
  }
336
771
  }
@@ -338,23 +773,36 @@ function TourOverlay({
338
773
  /* @__PURE__ */ jsx(
339
774
  TourCard,
340
775
  {
776
+ cardRef,
341
777
  step,
342
778
  index,
343
- total: steps.length,
779
+ position,
780
+ total: count,
344
781
  labels,
345
782
  isFirst,
346
783
  isLast,
347
784
  rect: spot,
348
785
  dir,
786
+ trap: !guided,
787
+ bodyId,
788
+ hintId,
789
+ locked,
790
+ gateHint,
349
791
  onBack: onPrev,
350
- onNext,
351
- onSkip
792
+ onNext: tryNext,
793
+ onLeaveEmpty: onNext,
794
+ onSkip,
795
+ awaitsRelease,
796
+ docked: spot !== null && docks(step, dockOnPhones),
797
+ refusal,
798
+ backTo: backTo >= 0 ? { title: steps[backTo].title, go: () => onGoTo(backTo) } : null
352
799
  }
353
- )
800
+ ),
801
+ /* @__PURE__ */ jsx("div", { className: "sr-only", "aria-live": "polite", "aria-atomic": "true", children: polite })
354
802
  ]
355
803
  }
356
804
  ),
357
- document.body
805
+ interactive && current?.layer || document.body
358
806
  );
359
807
  }
360
808
  function fieldValues(el) {
@@ -433,38 +881,108 @@ function placeCard(rect, cw, ch, vw, vh, m, placement, dir = "ltr") {
433
881
  }
434
882
  }
435
883
  function TourCard({
884
+ cardRef: ref,
436
885
  step,
437
886
  index,
887
+ position,
438
888
  total,
439
889
  labels,
440
890
  isFirst,
441
891
  isLast,
442
892
  rect,
443
893
  dir,
894
+ trap,
895
+ bodyId,
896
+ hintId,
897
+ locked,
898
+ gateHint,
444
899
  onBack,
445
900
  onNext,
446
- onSkip
901
+ onLeaveEmpty,
902
+ onSkip,
903
+ awaitsRelease,
904
+ docked,
905
+ refusal,
906
+ backTo
447
907
  }) {
448
- const ref = useRef(null);
449
908
  const [style, setStyle] = useState({ opacity: 0 });
450
- useFocusTrap(ref, { active: true });
909
+ useFocusTrap(ref, { active: trap });
910
+ const edgeRef = useRef(null);
911
+ const [bandTick, setBandTick] = useState(0);
912
+ const [edge, setEdge] = useState(null);
913
+ useEffect(() => {
914
+ if (!docked) return;
915
+ let last = "";
916
+ const look = () => {
917
+ const target = step.target ? queryVisibleTarget(step.target) : null;
918
+ const band = readBand(target, ref.current);
919
+ const key = `${band.top}:${band.bottom}`;
920
+ if (key === last) return;
921
+ last = key;
922
+ if (target) {
923
+ const r = target.getBoundingClientRect();
924
+ if (r.top < band.top || r.bottom > band.bottom) {
925
+ edgeRef.current = scrollIntoFreeHalf(target, ref.current, edgeRef.current);
926
+ }
927
+ }
928
+ setBandTick((t) => t + 1);
929
+ };
930
+ const vv = window.visualViewport;
931
+ vv?.addEventListener("resize", look);
932
+ vv?.addEventListener("scroll", look);
933
+ window.addEventListener("resize", look);
934
+ const id = window.setInterval(look, 250);
935
+ return () => {
936
+ vv?.removeEventListener("resize", look);
937
+ vv?.removeEventListener("scroll", look);
938
+ window.removeEventListener("resize", look);
939
+ clearInterval(id);
940
+ };
941
+ }, [docked, step.target, ref]);
451
942
  useLayoutEffect(() => {
452
943
  const card = ref.current;
453
944
  if (!card) return;
945
+ if (docked) {
946
+ const target = step.target ? queryVisibleTarget(step.target) : null;
947
+ const band = readBand(target, card);
948
+ const next = rect ? dockEdge(rect, band, edgeRef.current, card.offsetHeight) : "bottom";
949
+ edgeRef.current = next;
950
+ setEdge(next);
951
+ setStyle(dockStyle(next, rect, band));
952
+ return;
953
+ }
954
+ setEdge(null);
454
955
  setStyle(
455
956
  placeCard(rect, card.offsetWidth, card.offsetHeight, window.innerWidth, window.innerHeight, 12, step.placement, dir)
456
957
  );
457
- }, [rect, index, step.placement, dir]);
958
+ }, [ref, rect, index, step.placement, step.target, dir, docked, bandTick]);
959
+ const awaitsTarget = (step.awaitClick || step.awaitInput) && step.target && rect || awaitsRelease;
458
960
  return /* @__PURE__ */ jsxs(
459
961
  "div",
460
962
  {
461
963
  ref,
462
964
  tabIndex: -1,
463
965
  style,
464
- className: "pointer-events-auto fixed z-[61] w-80 max-w-[calc(100vw-1.5rem)] rounded-lg border border-subtle bg-surface p-4 shadow-xl outline-none transition-all duration-200 ease-out",
966
+ "data-tour-card": "",
967
+ "data-tour-docked": edge ?? void 0,
968
+ className: cn(
969
+ "pointer-events-auto fixed z-[61] w-80 max-w-[calc(100vw-1.5rem)] rounded-lg border border-subtle bg-surface p-4 shadow-xl outline-none transition-all duration-200 ease-out",
970
+ // Docked, the height is capped: the body scrolls, the title and buttons stay.
971
+ docked && "flex flex-col"
972
+ ),
465
973
  children: [
466
- /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold text-primary", children: step.title }),
467
- /* @__PURE__ */ jsx("div", { className: "mt-1 text-sm leading-relaxed text-secondary", children: step.body }),
974
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 text-sm font-semibold text-primary", children: [
975
+ /* @__PURE__ */ jsx("span", { children: step.title }),
976
+ step.optional && /* @__PURE__ */ jsx("span", { className: "rounded-full border border-subtle px-1.5 py-px text-xs font-normal text-muted", children: labels.optional })
977
+ ] }),
978
+ /* @__PURE__ */ jsx(
979
+ "div",
980
+ {
981
+ id: bodyId,
982
+ className: cn("mt-1 text-sm leading-relaxed text-secondary", docked && "min-h-0 shrink overflow-y-auto"),
983
+ children: step.body
984
+ }
985
+ ),
468
986
  step.action && /* @__PURE__ */ jsx("div", { className: "mt-3", children: step.action }),
469
987
  /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-2", children: [
470
988
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
@@ -478,12 +996,31 @@ function TourCard({
478
996
  }
479
997
  ),
480
998
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
481
- /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap text-xs tabular-nums text-placeholder", children: labels.step(index + 1, total) }),
999
+ /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap text-xs tabular-nums text-placeholder", children: labels.step(position, total) }),
482
1000
  !isFirst && /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onBack, className: "px-2.5 py-1 text-xs", children: labels.back }),
483
- !((step.awaitClick || step.awaitInput) && step.target && rect) && /* @__PURE__ */ jsx(Button, { variant: "accent", onClick: onNext, className: cn("px-2.5 py-1 text-xs"), children: isLast ? labels.done : labels.next })
1001
+ step.optional && locked && !awaitsTarget && /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onLeaveEmpty, className: "px-2.5 py-1 text-xs", children: labels.leaveEmpty }),
1002
+ !awaitsTarget && /* @__PURE__ */ jsx(
1003
+ Button,
1004
+ {
1005
+ variant: "accent",
1006
+ onClick: onNext,
1007
+ "aria-disabled": locked || void 0,
1008
+ "aria-describedby": locked ? hintId : void 0,
1009
+ className: cn("px-2.5 py-1 text-xs", locked && "opacity-60"),
1010
+ children: isLast ? labels.done : labels.next
1011
+ }
1012
+ )
484
1013
  ] })
485
1014
  ] }),
486
- (step.awaitClick || step.awaitInput) && step.target && rect && /* @__PURE__ */ jsx("div", { className: "text-center text-xs italic text-placeholder", children: step.awaitInput && !step.awaitClick ? labels.awaitInputHint : labels.awaitClickHint })
1015
+ awaitsTarget && /* @__PURE__ */ jsx("div", { className: "text-center text-xs italic text-placeholder", children: awaitsRelease ? step.awaitGateHint ?? labels.awaitGateHint : step.awaitInput && !step.awaitClick ? labels.awaitInputHint : labels.awaitClickHint }),
1016
+ awaitsRelease && /* @__PURE__ */ jsxs("div", { "aria-live": "assertive", "aria-atomic": "true", className: "space-y-1", children: [
1017
+ refusal && /* @__PURE__ */ jsxs("p", { "data-tour-refused": "", className: "text-xs text-danger", children: [
1018
+ refusal.reason ?? labels.awaitGateRefused,
1019
+ refusal.seq % 2 === 0 ? "\u200B" : ""
1020
+ ] }),
1021
+ refusal && backTo && /* @__PURE__ */ jsx(Button, { variant: "link", onClick: backTo.go, className: "text-xs", children: labels.backToStep(backTo.title) })
1022
+ ] }),
1023
+ locked && !awaitsTarget && /* @__PURE__ */ jsx("div", { id: hintId, className: "text-center text-xs italic text-placeholder", children: gateHint })
487
1024
  ] })
488
1025
  ]
489
1026
  }
@@ -493,6 +1030,8 @@ export {
493
1030
  DEFAULT_TOUR_LABELS,
494
1031
  TourProvider,
495
1032
  useTour,
1033
+ useTourGate,
1034
+ useTourGateRelease,
496
1035
  useTourOptional
497
1036
  };
498
1037
  //# sourceMappingURL=tour.js.map