@pablo2410/shared-ui 0.21.0 → 0.22.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.
@@ -1,5 +1,5 @@
1
1
  // src/tour/TourShell.tsx
2
- import { useEffect, useRef } from "react";
2
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
3
3
  import {
4
4
  X,
5
5
  ChevronLeft,
@@ -7,283 +7,15 @@ import {
7
7
  ArrowRight,
8
8
  ExternalLink
9
9
  } from "lucide-react";
10
- import { jsx, jsxs } from "react/jsx-runtime";
11
- var defaultSkin = {
12
- accent: "#8C34E9",
13
- overlayBg: "rgba(10,14,26,0.72)",
14
- panelBg: "#0e1624",
15
- panelBorder: "1px solid #1e2738",
16
- mutedText: "#596475",
17
- bodyText: "#c0c6d0",
18
- softText: "#8890a0"
19
- };
20
- function TourShell({
21
- step,
22
- index,
23
- total,
24
- isFirst,
25
- isLast,
26
- onNext,
27
- onBack,
28
- onDismiss,
29
- narration,
30
- narrationPending,
31
- children,
32
- skin = defaultSkin,
33
- orb
34
- }) {
35
- const panelRef = useRef(null);
36
- useEffect(() => {
37
- const onKeyDown = (e) => {
38
- if (e.key === "Escape") onDismiss();
39
- };
40
- document.addEventListener("keydown", onKeyDown);
41
- return () => document.removeEventListener("keydown", onKeyDown);
42
- }, [onDismiss]);
43
- useEffect(() => {
44
- panelRef.current?.focus();
45
- }, [step.id]);
46
- return /* @__PURE__ */ jsx(
47
- "div",
48
- {
49
- className: "fixed inset-0 z-50 flex items-center justify-center p-4",
50
- style: { background: skin.overlayBg },
51
- onClick: (e) => {
52
- if (e.target === e.currentTarget) onDismiss();
53
- },
54
- children: /* @__PURE__ */ jsxs(
55
- "div",
56
- {
57
- ref: panelRef,
58
- tabIndex: -1,
59
- role: "dialog",
60
- "aria-modal": "true",
61
- "aria-label": step.label,
62
- className: "w-full max-w-md rounded-xl overflow-hidden flex flex-col max-h-[85vh] outline-none",
63
- style: { background: skin.panelBg, border: skin.panelBorder },
64
- children: [
65
- /* @__PURE__ */ jsxs(
66
- "div",
67
- {
68
- className: "flex items-center justify-between gap-3 px-5 py-4 shrink-0",
69
- style: {
70
- background: "linear-gradient(135deg, #2a1450 0%, #1a0e33 100%)"
71
- },
72
- children: [
73
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 min-w-0", children: [
74
- orb,
75
- /* @__PURE__ */ jsx("span", { className: "text-sm font-bold text-white truncate", children: step.label })
76
- ] }),
77
- /* @__PURE__ */ jsx(
78
- "button",
79
- {
80
- onClick: onDismiss,
81
- className: "p-1 rounded hover:bg-white/10 transition-colors shrink-0",
82
- title: "Skip tour",
83
- children: /* @__PURE__ */ jsx(X, { size: 16, className: "text-white/70" })
84
- }
85
- )
86
- ]
87
- }
88
- ),
89
- /* @__PURE__ */ jsx(
90
- "div",
91
- {
92
- className: "flex items-center gap-1.5 px-5 py-3 shrink-0",
93
- style: { borderBottom: skin.panelBorder },
94
- children: Array.from({ length: total }, (_, i) => /* @__PURE__ */ jsx(
95
- "span",
96
- {
97
- className: "h-1.5 rounded-full transition-all",
98
- style: {
99
- width: i === index ? 20 : 8,
100
- background: i === index ? skin.accent : skin.mutedText,
101
- opacity: i === index ? 1 : 0.4
102
- }
103
- },
104
- i
105
- ))
106
- }
107
- ),
108
- /* @__PURE__ */ jsxs("div", { className: "overflow-y-auto px-5 py-4 space-y-4", children: [
109
- /* @__PURE__ */ jsx(
110
- "p",
111
- {
112
- className: "text-[13px] leading-relaxed transition-opacity",
113
- style: {
114
- color: skin.bodyText,
115
- opacity: narrationPending ? 0.7 : 1
116
- },
117
- children: narration
118
- }
119
- ),
120
- step.highlights && step.highlights.length > 0 && /* @__PURE__ */ jsx("ul", { className: "space-y-1.5", children: step.highlights.map((h, i) => /* @__PURE__ */ jsxs(
121
- "li",
122
- {
123
- className: "text-[12.5px] leading-relaxed pl-3 relative",
124
- style: { color: skin.softText },
125
- children: [
126
- /* @__PURE__ */ jsx(
127
- "span",
128
- {
129
- className: "absolute left-0",
130
- style: { color: skin.mutedText },
131
- children: "\u2022"
132
- }
133
- ),
134
- h
135
- ]
136
- },
137
- i
138
- )) }),
139
- step.action && /* @__PURE__ */ jsxs(
140
- "a",
141
- {
142
- href: step.action.href,
143
- target: step.action.external ? "_blank" : void 0,
144
- rel: step.action.external ? "noopener noreferrer" : void 0,
145
- className: "inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-[13px] font-semibold transition-opacity hover:opacity-90",
146
- style: { background: `${skin.accent}20`, color: skin.accent },
147
- children: [
148
- step.action.label,
149
- step.action.external ? /* @__PURE__ */ jsx(ExternalLink, { size: 13 }) : /* @__PURE__ */ jsx(ArrowRight, { size: 13 })
150
- ]
151
- }
152
- ),
153
- children
154
- ] }),
155
- /* @__PURE__ */ jsxs(
156
- "div",
157
- {
158
- className: "flex items-center justify-between gap-2 px-5 py-3.5 shrink-0",
159
- style: { borderTop: skin.panelBorder },
160
- children: [
161
- /* @__PURE__ */ jsx(
162
- "button",
163
- {
164
- onClick: onDismiss,
165
- className: "text-[12px] font-medium transition-colors hover:underline",
166
- style: { color: skin.mutedText },
167
- children: "Skip tour"
168
- }
169
- ),
170
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
171
- !isFirst && /* @__PURE__ */ jsxs(
172
- "button",
173
- {
174
- onClick: onBack,
175
- className: "flex items-center gap-1 px-3 py-1.5 rounded-md text-[12.5px] font-medium transition-colors",
176
- style: { color: skin.bodyText, border: skin.panelBorder },
177
- children: [
178
- /* @__PURE__ */ jsx(ChevronLeft, { size: 13 }),
179
- "Back"
180
- ]
181
- }
182
- ),
183
- /* @__PURE__ */ jsxs(
184
- "button",
185
- {
186
- onClick: onNext,
187
- className: "flex items-center gap-1 px-3.5 py-1.5 rounded-md text-[12.5px] font-bold text-white transition-opacity hover:opacity-90",
188
- style: {
189
- background: "linear-gradient(135deg, #8C34E9 0%, #5B1FA6 100%)"
190
- },
191
- children: [
192
- isLast ? "Done" : "Next",
193
- !isLast && /* @__PURE__ */ jsx(ChevronRight, { size: 13 })
194
- ]
195
- }
196
- )
197
- ] })
198
- ]
199
- }
200
- )
201
- ]
202
- }
203
- )
204
- }
205
- );
206
- }
207
-
208
- // src/tour/useTour.ts
209
- import { useCallback, useState } from "react";
210
-
211
- // src/tour/tourMachine.ts
212
- function resolveInitialIndex(steps, initialStepId) {
213
- if (!initialStepId) return 0;
214
- const found = steps.findIndex((s) => s.id === initialStepId);
215
- return found === -1 ? 0 : found;
216
- }
217
- function nextIndex(steps, current) {
218
- if (current >= steps.length - 1) {
219
- return { index: current, completed: true };
220
- }
221
- return { index: current + 1, completed: false };
222
- }
223
- function backIndex(current) {
224
- return Math.max(0, current - 1);
225
- }
226
- function gotoIndex(steps, stepId, current) {
227
- const target = steps.findIndex((s) => s.id === stepId);
228
- return target === -1 ? current : target;
229
- }
230
-
231
- // src/tour/useTour.ts
232
- function useTour(steps, opts = {}) {
233
- if (steps.length === 0) {
234
- throw new Error("useTour requires at least one step");
235
- }
236
- const [index, setIndex] = useState(
237
- () => resolveInitialIndex(steps, opts.initialStepId)
238
- );
239
- const next = useCallback(() => {
240
- const result = nextIndex(steps, index);
241
- if (result.completed) {
242
- opts.onComplete?.();
243
- return;
244
- }
245
- setIndex(result.index);
246
- opts.onStepChange?.(steps[result.index], result.index);
247
- }, [index, steps]);
248
- const back = useCallback(() => {
249
- const target = backIndex(index);
250
- if (target === index) return;
251
- setIndex(target);
252
- opts.onStepChange?.(steps[target], target);
253
- }, [index, steps]);
254
- const goto = useCallback(
255
- (stepId) => {
256
- const target = gotoIndex(steps, stepId, index);
257
- if (target === index) return;
258
- setIndex(target);
259
- opts.onStepChange?.(steps[target], target);
260
- },
261
- [steps, index]
262
- );
263
- const dismiss = useCallback(() => {
264
- opts.onDismiss?.();
265
- }, []);
266
- return {
267
- step: steps[index],
268
- index,
269
- total: steps.length,
270
- isFirst: index === 0,
271
- isLast: index === steps.length - 1,
272
- next,
273
- back,
274
- goto,
275
- dismiss
276
- };
277
- }
278
10
 
279
11
  // src/tour/useSpotlight.ts
280
- import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
12
+ import { useEffect, useRef, useState } from "react";
281
13
  var RESIZE_DEBOUNCE_MS = 100;
282
14
  function useSpotlight(selector) {
283
- const [rect, setRect] = useState2(null);
284
- const [found, setFound] = useState2(false);
285
- const scrolledForSelector = useRef2(null);
286
- useEffect2(() => {
15
+ const [rect, setRect] = useState(null);
16
+ const [found, setFound] = useState(false);
17
+ const scrolledForSelector = useRef(null);
18
+ useEffect(() => {
287
19
  if (!selector) {
288
20
  setRect(null);
289
21
  setFound(false);
@@ -433,6 +165,440 @@ function fits(pos, panelSize, viewport) {
433
165
  function clamp(value, min, max) {
434
166
  return Math.min(Math.max(value, min), max);
435
167
  }
168
+
169
+ // src/tour/TourShell.tsx
170
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
171
+ var defaultSkin = {
172
+ accent: "#8C34E9",
173
+ overlayBg: "rgba(10,14,26,0.72)",
174
+ panelBg: "#0e1624",
175
+ panelBorder: "1px solid #1e2738",
176
+ mutedText: "#596475",
177
+ bodyText: "#c0c6d0",
178
+ softText: "#8890a0"
179
+ };
180
+ var SPOTLIGHT_PANEL_SIZE = { width: 320, height: 280 };
181
+ function TourShell({
182
+ step,
183
+ index,
184
+ total,
185
+ isFirst,
186
+ isLast,
187
+ onNext,
188
+ onBack,
189
+ onDismiss,
190
+ narration,
191
+ narrationPending,
192
+ children,
193
+ skin = defaultSkin,
194
+ orb,
195
+ onSpotlightTargetActivated
196
+ }) {
197
+ const panelRef = useRef2(null);
198
+ const { rect, found } = useSpotlight(step.spotlight?.selector ?? null);
199
+ const isSpotlight = Boolean(step.spotlight) && found && rect !== null;
200
+ useEffect2(() => {
201
+ const onKeyDown = (e) => {
202
+ if (e.key === "Escape") onDismiss();
203
+ };
204
+ document.addEventListener("keydown", onKeyDown);
205
+ return () => document.removeEventListener("keydown", onKeyDown);
206
+ }, [onDismiss]);
207
+ useEffect2(() => {
208
+ panelRef.current?.focus();
209
+ }, [step.id]);
210
+ useEffect2(() => {
211
+ if (!isSpotlight || !onSpotlightTargetActivated || !step.spotlight) return;
212
+ const selector = step.spotlight.selector;
213
+ const onClickCapture = (e) => {
214
+ const target = e.target;
215
+ if (target?.closest(`[data-tour-target="${selector}"]`)) {
216
+ onSpotlightTargetActivated();
217
+ }
218
+ };
219
+ document.addEventListener("click", onClickCapture, { capture: true });
220
+ return () => document.removeEventListener("click", onClickCapture, {
221
+ capture: true
222
+ });
223
+ }, [isSpotlight, onSpotlightTargetActivated, step.spotlight]);
224
+ if (isSpotlight && rect) {
225
+ const viewport = { width: window.innerWidth, height: window.innerHeight };
226
+ const frame = computeSpotlightFrame(rect, viewport);
227
+ const panelPos = computePanelPosition(
228
+ rect,
229
+ SPOTLIGHT_PANEL_SIZE,
230
+ viewport,
231
+ step.spotlight?.placement ?? "auto"
232
+ );
233
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
234
+ ["top", "bottom", "left", "right"].map((side) => {
235
+ const segment = frame[side];
236
+ if (segment.width <= 0 || segment.height <= 0) return null;
237
+ return /* @__PURE__ */ jsx(
238
+ "div",
239
+ {
240
+ className: "fixed z-50",
241
+ style: {
242
+ top: segment.top,
243
+ left: segment.left,
244
+ width: segment.width,
245
+ height: segment.height,
246
+ background: skin.overlayBg
247
+ },
248
+ onClick: onDismiss
249
+ },
250
+ side
251
+ );
252
+ }),
253
+ /* @__PURE__ */ jsx(
254
+ "div",
255
+ {
256
+ className: "fixed z-50 rounded-xl pointer-events-none transition-all",
257
+ style: {
258
+ top: rect.top - 4,
259
+ left: rect.left - 4,
260
+ width: rect.width + 8,
261
+ height: rect.height + 8,
262
+ boxShadow: `0 0 0 2px ${skin.accent}, 0 0 24px 4px ${skin.accent}66`
263
+ }
264
+ }
265
+ ),
266
+ /* @__PURE__ */ jsxs(
267
+ "div",
268
+ {
269
+ ref: panelRef,
270
+ tabIndex: -1,
271
+ role: "dialog",
272
+ "aria-modal": "true",
273
+ "aria-label": step.label,
274
+ className: "fixed z-50 rounded-xl overflow-hidden flex flex-col outline-none",
275
+ style: {
276
+ top: panelPos.top,
277
+ left: panelPos.left,
278
+ width: SPOTLIGHT_PANEL_SIZE.width,
279
+ maxHeight: SPOTLIGHT_PANEL_SIZE.height,
280
+ background: skin.panelBg,
281
+ border: skin.panelBorder
282
+ },
283
+ children: [
284
+ /* @__PURE__ */ jsx(TourShellHeader, { step, orb, onDismiss }),
285
+ /* @__PURE__ */ jsx(TourShellStepDots, { total, index, skin }),
286
+ /* @__PURE__ */ jsx("div", { className: "overflow-y-auto px-4 py-3 space-y-3", children: /* @__PURE__ */ jsx(
287
+ TourShellBody,
288
+ {
289
+ step,
290
+ narration,
291
+ narrationPending,
292
+ skin,
293
+ children
294
+ }
295
+ ) }),
296
+ /* @__PURE__ */ jsx(
297
+ TourShellFooter,
298
+ {
299
+ isFirst,
300
+ isLast,
301
+ onNext,
302
+ onBack,
303
+ onDismiss,
304
+ skin
305
+ }
306
+ )
307
+ ]
308
+ }
309
+ )
310
+ ] });
311
+ }
312
+ return /* @__PURE__ */ jsx(
313
+ "div",
314
+ {
315
+ className: "fixed inset-0 z-50 flex items-center justify-center p-4",
316
+ style: { background: skin.overlayBg },
317
+ onClick: (e) => {
318
+ if (e.target === e.currentTarget) onDismiss();
319
+ },
320
+ children: /* @__PURE__ */ jsxs(
321
+ "div",
322
+ {
323
+ ref: panelRef,
324
+ tabIndex: -1,
325
+ role: "dialog",
326
+ "aria-modal": "true",
327
+ "aria-label": step.label,
328
+ className: "w-full max-w-md rounded-xl overflow-hidden flex flex-col max-h-[85vh] outline-none",
329
+ style: { background: skin.panelBg, border: skin.panelBorder },
330
+ children: [
331
+ /* @__PURE__ */ jsx(TourShellHeader, { step, orb, onDismiss }),
332
+ /* @__PURE__ */ jsx(TourShellStepDots, { total, index, skin }),
333
+ /* @__PURE__ */ jsx("div", { className: "overflow-y-auto px-5 py-4 space-y-4", children: /* @__PURE__ */ jsx(
334
+ TourShellBody,
335
+ {
336
+ step,
337
+ narration,
338
+ narrationPending,
339
+ skin,
340
+ children
341
+ }
342
+ ) }),
343
+ /* @__PURE__ */ jsx(
344
+ TourShellFooter,
345
+ {
346
+ isFirst,
347
+ isLast,
348
+ onNext,
349
+ onBack,
350
+ onDismiss,
351
+ skin
352
+ }
353
+ )
354
+ ]
355
+ }
356
+ )
357
+ }
358
+ );
359
+ }
360
+ function TourShellHeader({
361
+ step,
362
+ orb,
363
+ onDismiss
364
+ }) {
365
+ return /* @__PURE__ */ jsxs(
366
+ "div",
367
+ {
368
+ className: "flex items-center justify-between gap-3 px-5 py-4 shrink-0",
369
+ style: {
370
+ background: "linear-gradient(135deg, #2a1450 0%, #1a0e33 100%)"
371
+ },
372
+ children: [
373
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 min-w-0", children: [
374
+ orb,
375
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-bold text-white truncate", children: step.label })
376
+ ] }),
377
+ /* @__PURE__ */ jsx(
378
+ "button",
379
+ {
380
+ onClick: onDismiss,
381
+ className: "p-1 rounded hover:bg-white/10 transition-colors shrink-0",
382
+ title: "Skip tour",
383
+ children: /* @__PURE__ */ jsx(X, { size: 16, className: "text-white/70" })
384
+ }
385
+ )
386
+ ]
387
+ }
388
+ );
389
+ }
390
+ function TourShellStepDots({
391
+ total,
392
+ index,
393
+ skin
394
+ }) {
395
+ return /* @__PURE__ */ jsx(
396
+ "div",
397
+ {
398
+ className: "flex items-center gap-1.5 px-5 py-3 shrink-0",
399
+ style: { borderBottom: skin.panelBorder },
400
+ children: Array.from({ length: total }, (_, i) => /* @__PURE__ */ jsx(
401
+ "span",
402
+ {
403
+ className: "h-1.5 rounded-full transition-all",
404
+ style: {
405
+ width: i === index ? 20 : 8,
406
+ background: i === index ? skin.accent : skin.mutedText,
407
+ opacity: i === index ? 1 : 0.4
408
+ }
409
+ },
410
+ i
411
+ ))
412
+ }
413
+ );
414
+ }
415
+ function TourShellBody({
416
+ step,
417
+ narration,
418
+ narrationPending,
419
+ skin,
420
+ children
421
+ }) {
422
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
423
+ /* @__PURE__ */ jsx(
424
+ "p",
425
+ {
426
+ className: "text-[13px] leading-relaxed transition-opacity",
427
+ style: {
428
+ color: skin.bodyText,
429
+ opacity: narrationPending ? 0.7 : 1
430
+ },
431
+ children: narration
432
+ }
433
+ ),
434
+ step.features && step.features.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "space-y-2", children: step.features.map((f, i) => /* @__PURE__ */ jsxs("li", { className: "text-[12.5px] leading-relaxed", children: [
435
+ /* @__PURE__ */ jsx("span", { className: "font-semibold", style: { color: skin.accent }, children: f.name }),
436
+ /* @__PURE__ */ jsxs("span", { style: { color: skin.softText }, children: [
437
+ " \u2014 ",
438
+ f.detail
439
+ ] })
440
+ ] }, i)) }) : step.highlights && step.highlights.length > 0 && /* @__PURE__ */ jsx("ul", { className: "space-y-1.5", children: step.highlights.map((h, i) => /* @__PURE__ */ jsxs(
441
+ "li",
442
+ {
443
+ className: "text-[12.5px] leading-relaxed pl-3 relative",
444
+ style: { color: skin.softText },
445
+ children: [
446
+ /* @__PURE__ */ jsx(
447
+ "span",
448
+ {
449
+ className: "absolute left-0",
450
+ style: { color: skin.mutedText },
451
+ children: "\u2022"
452
+ }
453
+ ),
454
+ h
455
+ ]
456
+ },
457
+ i
458
+ )) }),
459
+ step.action && /* @__PURE__ */ jsxs(
460
+ "a",
461
+ {
462
+ href: step.action.href,
463
+ target: step.action.external ? "_blank" : void 0,
464
+ rel: step.action.external ? "noopener noreferrer" : void 0,
465
+ className: "inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-[13px] font-semibold transition-opacity hover:opacity-90",
466
+ style: { background: `${skin.accent}20`, color: skin.accent },
467
+ children: [
468
+ step.action.label,
469
+ step.action.external ? /* @__PURE__ */ jsx(ExternalLink, { size: 13 }) : /* @__PURE__ */ jsx(ArrowRight, { size: 13 })
470
+ ]
471
+ }
472
+ ),
473
+ children
474
+ ] });
475
+ }
476
+ function TourShellFooter({
477
+ isFirst,
478
+ isLast,
479
+ onNext,
480
+ onBack,
481
+ onDismiss,
482
+ skin
483
+ }) {
484
+ return /* @__PURE__ */ jsxs(
485
+ "div",
486
+ {
487
+ className: "flex items-center justify-between gap-2 px-5 py-3.5 shrink-0",
488
+ style: { borderTop: skin.panelBorder },
489
+ children: [
490
+ /* @__PURE__ */ jsx(
491
+ "button",
492
+ {
493
+ onClick: onDismiss,
494
+ className: "text-[12px] font-medium transition-colors hover:underline",
495
+ style: { color: skin.mutedText },
496
+ children: "Skip tour"
497
+ }
498
+ ),
499
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
500
+ !isFirst && /* @__PURE__ */ jsxs(
501
+ "button",
502
+ {
503
+ onClick: onBack,
504
+ className: "flex items-center gap-1 px-3 py-1.5 rounded-md text-[12.5px] font-medium transition-colors",
505
+ style: { color: skin.bodyText, border: skin.panelBorder },
506
+ children: [
507
+ /* @__PURE__ */ jsx(ChevronLeft, { size: 13 }),
508
+ "Back"
509
+ ]
510
+ }
511
+ ),
512
+ /* @__PURE__ */ jsxs(
513
+ "button",
514
+ {
515
+ onClick: onNext,
516
+ className: "flex items-center gap-1 px-3.5 py-1.5 rounded-md text-[12.5px] font-bold text-white transition-opacity hover:opacity-90",
517
+ style: {
518
+ background: "linear-gradient(135deg, #8C34E9 0%, #5B1FA6 100%)"
519
+ },
520
+ children: [
521
+ isLast ? "Done" : "Next",
522
+ !isLast && /* @__PURE__ */ jsx(ChevronRight, { size: 13 })
523
+ ]
524
+ }
525
+ )
526
+ ] })
527
+ ]
528
+ }
529
+ );
530
+ }
531
+
532
+ // src/tour/useTour.ts
533
+ import { useCallback, useState as useState2 } from "react";
534
+
535
+ // src/tour/tourMachine.ts
536
+ function resolveInitialIndex(steps, initialStepId) {
537
+ if (!initialStepId) return 0;
538
+ const found = steps.findIndex((s) => s.id === initialStepId);
539
+ return found === -1 ? 0 : found;
540
+ }
541
+ function nextIndex(steps, current) {
542
+ if (current >= steps.length - 1) {
543
+ return { index: current, completed: true };
544
+ }
545
+ return { index: current + 1, completed: false };
546
+ }
547
+ function backIndex(current) {
548
+ return Math.max(0, current - 1);
549
+ }
550
+ function gotoIndex(steps, stepId, current) {
551
+ const target = steps.findIndex((s) => s.id === stepId);
552
+ return target === -1 ? current : target;
553
+ }
554
+
555
+ // src/tour/useTour.ts
556
+ function useTour(steps, opts = {}) {
557
+ if (steps.length === 0) {
558
+ throw new Error("useTour requires at least one step");
559
+ }
560
+ const [index, setIndex] = useState2(
561
+ () => resolveInitialIndex(steps, opts.initialStepId)
562
+ );
563
+ const next = useCallback(() => {
564
+ const result = nextIndex(steps, index);
565
+ if (result.completed) {
566
+ opts.onComplete?.();
567
+ return;
568
+ }
569
+ setIndex(result.index);
570
+ opts.onStepChange?.(steps[result.index], result.index);
571
+ }, [index, steps]);
572
+ const back = useCallback(() => {
573
+ const target = backIndex(index);
574
+ if (target === index) return;
575
+ setIndex(target);
576
+ opts.onStepChange?.(steps[target], target);
577
+ }, [index, steps]);
578
+ const goto = useCallback(
579
+ (stepId) => {
580
+ const target = gotoIndex(steps, stepId, index);
581
+ if (target === index) return;
582
+ setIndex(target);
583
+ opts.onStepChange?.(steps[target], target);
584
+ },
585
+ [steps, index]
586
+ );
587
+ const dismiss = useCallback(() => {
588
+ opts.onDismiss?.();
589
+ }, []);
590
+ return {
591
+ step: steps[index],
592
+ index,
593
+ total: steps.length,
594
+ isFirst: index === 0,
595
+ isLast: index === steps.length - 1,
596
+ next,
597
+ back,
598
+ goto,
599
+ dismiss
600
+ };
601
+ }
436
602
  export {
437
603
  TourShell,
438
604
  computePanelPosition,