@apollovisionlabs/guide-core 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -101,43 +101,55 @@ the destination.
101
101
 
102
102
  ## Persistence
103
103
 
104
- `GuideStorage` is the two-method interface the provider reads from and writes to:
104
+ `GuideStorage` is the two-method interface both `GuideProvider` and `ChecklistProvider` read from
105
+ and write to. It is generic over the stored value, so one storage serves tour progress and
106
+ checklist progress under different keys:
105
107
 
106
108
  ```ts
107
109
  interface GuideStorage {
108
- read(tourId: string): Promise<TourProgress | null>
109
- write(tourId: string, progress: TourProgress): Promise<void>
110
+ read<T>(key: string): Promise<T | null>
111
+ write<T>(key: string, value: T): Promise<void>
110
112
  }
111
113
  ```
112
114
 
115
+ `GuideProvider` reads and writes tour progress under `tour:<id>`. `ChecklistProvider` reads and
116
+ writes checklist progress under `checklist:<id>`. See [ADR 0016](docs/adr/0016-one-storage-contract-for-tours-and-checklists.md)
117
+ for why the two share one interface.
118
+
113
119
  `@apollovisionlabs/guide-core` ships `createMemoryStorage()` for tests and `createBrowserStorage(namespace?)` for
114
120
  `localStorage`. Neither talks to a server. An implementation backed by your own API looks like
115
121
  this:
116
122
 
117
123
  ```ts
118
- import type { GuideStorage, TourProgress } from '@apollovisionlabs/guide-core'
124
+ import type { GuideStorage } from '@apollovisionlabs/guide-core'
119
125
 
120
126
  function createServerStorage(): GuideStorage {
121
127
  return {
122
- async read(tourId) {
123
- const response = await fetch(`/api/tours/${tourId}/progress`)
128
+ async read<T>(key: string) {
129
+ const response = await fetch(`/api/guide/${key}`)
124
130
  if (!response.ok) return null
125
- return (await response.json()) as TourProgress
131
+ return (await response.json()) as T
126
132
  },
127
- async write(tourId, progress) {
128
- await fetch(`/api/tours/${tourId}/progress`, {
133
+ async write<T>(key: string, value: T) {
134
+ await fetch(`/api/guide/${key}`, {
129
135
  method: 'PUT',
130
136
  headers: { 'Content-Type': 'application/json' },
131
- body: JSON.stringify(progress),
137
+ body: JSON.stringify(value),
132
138
  })
133
139
  },
134
140
  }
135
141
  }
136
142
  ```
137
143
 
138
- Pass it as the `storage` prop. The provider reads on `start()` (unless an explicit `from` step
139
- index is passed, or `resume: false` is passed) and writes whenever a running tour advances or
140
- completes.
144
+ Pass it as the `storage` prop on either provider. `GuideProvider` reads on `start()` (unless an
145
+ explicit `from` step index is passed, or `resume: false` is passed) and writes whenever a running
146
+ tour advances or completes. `ChecklistProvider` reads once on mount and writes whenever an item is
147
+ ticked, completed or the checklist is dismissed.
148
+
149
+ A value read back from storage is validated before it is trusted (`isTourProgress`,
150
+ `isChecklistProgress`, both exported from `@apollovisionlabs/guide-core`): a value that does not
151
+ match the expected shape, from a hand-edited store or an older version of this library, is treated
152
+ the same as nothing stored, rather than crashing or resuming into a broken state.
141
153
 
142
154
  ## Translations
143
155
 
@@ -158,7 +170,8 @@ and nothing English remains:
158
170
 
159
171
  ## Events
160
172
 
161
- `onEvent` on `GuideProvider` receives every lifecycle event as a discriminated union:
173
+ `onEvent` on `GuideProvider` and on `ChecklistProvider` each receive their own lifecycle events, as
174
+ a discriminated union of `GuideEvent`:
162
175
 
163
176
  | Event | Payload | When |
164
177
  | --- | --- | --- |
@@ -167,6 +180,9 @@ and nothing English remains:
167
180
  | `tour:stop` | `{ tourId, stepIndex }` | The tour is stopped before completion. |
168
181
  | `step:show` | `{ tourId, stepIndex, target }` | A step's target is resolved and the step becomes visible. |
169
182
  | `target:missing` | `{ tourId, stepIndex, target }` | A step's target didn't appear within `targetTimeoutMs`. |
183
+ | `checklist:item-complete` | `{ checklistId, itemId }` | An item is completed, by finishing its linked tour or by a manual tick. Not emitted for an item already complete. |
184
+ | `checklist:complete` | `{ checklistId }` | The last incomplete item in a checklist is completed. Fires on every transition into the complete state, so unticking an item and reticking it emits a second time. Deduplicate downstream if you count completions. |
185
+ | `checklist:dismiss` | `{ checklistId }` | `dismiss()` is called. |
170
186
 
171
187
  ## Accessibility
172
188
 
@@ -190,6 +206,88 @@ happens: `'skip'` moves to the next step, `'error'` stops the tour, and `'wait'`
190
206
  pauses and resumes automatically if the target appears later, for instance after a slow async
191
207
  render.
192
208
 
209
+ ## Checklist
210
+
211
+ A checklist is a separate feature from the tour: a fixed list of items, each completed by
212
+ finishing a linked tour or by a manual tick. An item can also carry an `href`, which navigates
213
+ and nothing more. `ChecklistProvider` holds
214
+ its state the way `GuideProvider` holds tour state, and nests inside it so that an item can launch
215
+ a tour:
216
+
217
+ ```tsx
218
+ import { GuideProvider, ChecklistProvider, type Tour, type Checklist } from '@apollovisionlabs/guide-core'
219
+ import { GuideTour } from '@apollovisionlabs/guide-mui'
220
+ import { ChecklistLauncher } from '@apollovisionlabs/guide-mui'
221
+
222
+ const tour: Tour = {
223
+ id: 'welcome',
224
+ steps: [{ target: 'sidebar.projects', title: 'Your projects', body: 'Grouped under a project.' }],
225
+ }
226
+
227
+ const onboarding: Checklist = {
228
+ id: 'onboarding',
229
+ items: [
230
+ { id: 'tour', title: 'Take the tour', body: 'Two minutes.', tourId: 'welcome' },
231
+ { id: 'projects', title: 'Open your projects', body: 'See the list.', href: '/projects' },
232
+ { id: 'profile', title: 'Set your name', body: 'Manual, ticked by hand.' },
233
+ ],
234
+ }
235
+
236
+ function App() {
237
+ return (
238
+ <GuideProvider tours={[tour]} navigate={(path) => router.push(path)}>
239
+ <ChecklistProvider checklists={[onboarding]} navigate={(path) => router.push(path)}>
240
+ <Sidebar />
241
+ <GuideTour />
242
+ <ChecklistLauncher checklistId="onboarding" title="Get started" />
243
+ </ChecklistProvider>
244
+ </GuideProvider>
245
+ )
246
+ }
247
+ ```
248
+
249
+ An item with an `href`, or with neither `tourId` nor `href`, is completed by a manual tick only:
250
+ activating an `href` item navigates and stops there, since arriving on a page is not evidence that
251
+ anyone did anything on it. An item with `tourId`
252
+ is completed automatically when that tour is finished (`next()` called on its last step); it can
253
+ also be ticked by hand before that. Completion is idempotent: ticking an already-complete item, or
254
+ finishing a tour whose item is already ticked, does nothing and emits no event.
255
+
256
+ ### `ChecklistProvider` props
257
+
258
+ | Prop | Type | Default | Description |
259
+ | --- | --- | --- | --- |
260
+ | `checklists` | `Checklist[]` | none | The checklists available to `useChecklist`. |
261
+ | `children` | `ReactNode` | none | Your application. |
262
+ | `storage` | `GuideStorage` | none | Persists checklist progress under `checklist:<id>`. See "Persistence". |
263
+ | `translate` | `(key: string) => string` | none | Resolves `titleKey` / `bodyKey` on items. See "Translations". |
264
+ | `navigate` | `(path: string) => void` | none | Called when an item with `href` is activated. |
265
+ | `onEvent` | `(event: GuideEvent) => void` | none | Called for `checklist:item-complete`, `checklist:complete`, `checklist:dismiss`. See "Events". |
266
+
267
+ ### `useChecklist(checklistId)`
268
+
269
+ Returns `{ items, completedCount, total, isComplete, dismissed, activate, toggle, complete, dismiss, reset }`.
270
+ `items` is `ResolvedChecklistItem[]`: `{ id, title, body, completed, tourId?, href? }`, with
271
+ `title` / `body` already resolved through `translate`. `activate(itemId)` runs an item's default
272
+ action (start its tour, navigate to its `href`, or toggle it if it has neither); `toggle` and
273
+ `complete` change completion directly; `dismiss()` and `reset()` act on the whole checklist.
274
+
275
+ ### `Checklist` and `ChecklistLauncher` (`@apollovisionlabs/guide-mui`)
276
+
277
+ `Checklist` renders the list inline: a progress bar, one row per item with a checkbox and a
278
+ dismiss button. `ChecklistLauncher` wraps it behind a floating action button showing
279
+ `completedCount/total`, opened as a popover. The popover stays open while items are ticked one
280
+ after another, and closes when an item hands off to something that needs the screen: launching a
281
+ tour or navigating to an `href`. It does not close on a plain tick.
282
+
283
+ ```tsx
284
+ import { Checklist, ChecklistLauncher } from '@apollovisionlabs/guide-mui'
285
+
286
+ <Checklist checklistId="onboarding" title="Get started" />
287
+ // or, as a floating launcher:
288
+ <ChecklistLauncher checklistId="onboarding" title="Get started" placement="bottom-right" />
289
+ ```
290
+
193
291
  ## Compatibility
194
292
 
195
293
  | | Supported |
package/dist/index.cjs CHANGED
@@ -21,16 +21,22 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
+ ChecklistContext: () => ChecklistContext,
25
+ ChecklistProvider: () => ChecklistProvider,
24
26
  GuideContext: () => GuideContext,
25
27
  GuideProvider: () => GuideProvider,
26
28
  createBrowserStorage: () => createBrowserStorage,
27
29
  createMemoryStorage: () => createMemoryStorage,
28
30
  findMissingTargets: () => findMissingTargets,
29
31
  initialTourState: () => initialTourState,
32
+ isChecklistProgress: () => isChecklistProgress,
30
33
  isLiteralRoute: () => isLiteralRoute,
34
+ isTourProgress: () => isTourProgress,
31
35
  matchRoute: () => matchRoute,
36
+ resolveText: () => resolveText,
32
37
  tourReducer: () => tourReducer,
33
38
  useAnnouncer: () => useAnnouncer,
39
+ useChecklist: () => useChecklist,
34
40
  useElementRect: () => useElementRect,
35
41
  useFocusTrap: () => useFocusTrap,
36
42
  useGuideStep: () => useGuideStep,
@@ -44,36 +50,46 @@ module.exports = __toCommonJS(index_exports);
44
50
  function createMemoryStorage(initial = {}) {
45
51
  const store = new Map(Object.entries(initial));
46
52
  return {
47
- async read(tourId) {
48
- return store.get(tourId) ?? null;
53
+ async read(key) {
54
+ return store.get(key) ?? null;
49
55
  },
50
- async write(tourId, progress) {
51
- store.set(tourId, progress);
56
+ async write(key, value) {
57
+ store.set(key, value);
52
58
  }
53
59
  };
54
60
  }
55
61
  function createBrowserStorage(namespace = "guide") {
56
- const key = (tourId) => `${namespace}:${tourId}`;
62
+ const key = (storageKey) => `${namespace}:${storageKey}`;
57
63
  const available = () => typeof window !== "undefined" && !!window.localStorage;
58
64
  return {
59
- async read(tourId) {
65
+ async read(storageKey) {
60
66
  if (!available()) return null;
61
67
  try {
62
- const raw = window.localStorage.getItem(key(tourId));
68
+ const raw = window.localStorage.getItem(key(storageKey));
63
69
  return raw ? JSON.parse(raw) : null;
64
70
  } catch {
65
71
  return null;
66
72
  }
67
73
  },
68
- async write(tourId, progress) {
74
+ async write(storageKey, value) {
69
75
  if (!available()) return;
70
76
  try {
71
- window.localStorage.setItem(key(tourId), JSON.stringify(progress));
77
+ window.localStorage.setItem(key(storageKey), JSON.stringify(value));
72
78
  } catch {
73
79
  }
74
80
  }
75
81
  };
76
82
  }
83
+ function isTourProgress(value) {
84
+ if (typeof value !== "object" || value === null) return false;
85
+ const candidate = value;
86
+ return typeof candidate.stepIndex === "number" && Number.isInteger(candidate.stepIndex) && candidate.stepIndex >= 0 && (candidate.status === "in-progress" || candidate.status === "completed");
87
+ }
88
+ function isChecklistProgress(value) {
89
+ if (typeof value !== "object" || value === null) return false;
90
+ const candidate = value;
91
+ return Array.isArray(candidate.completed) && candidate.completed.every((entry) => typeof entry === "string") && typeof candidate.dismissed === "boolean";
92
+ }
77
93
 
78
94
  // src/matchRoute.ts
79
95
  function segments(value) {
@@ -295,14 +311,16 @@ function findMissingTargets(tour, location, attribute = "data-guide") {
295
311
  return tour.steps.filter((step) => !step.route || location === void 0 || matchRoute(step.route, location)).map((step) => step.target).filter((target) => !document.querySelector(targetSelector(target, attribute)));
296
312
  }
297
313
 
298
- // src/GuideProvider.tsx
299
- var import_jsx_runtime = require("react/jsx-runtime");
300
- var GuideContext = (0, import_react4.createContext)(null);
314
+ // src/resolveText.ts
301
315
  function resolveText(value, key, translate) {
302
316
  if (value !== void 0) return value;
303
317
  if (key === void 0) return "";
304
318
  return translate ? translate(key) : key;
305
319
  }
320
+
321
+ // src/GuideProvider.tsx
322
+ var import_jsx_runtime = require("react/jsx-runtime");
323
+ var GuideContext = (0, import_react4.createContext)(null);
306
324
  function GuideProvider({
307
325
  tours,
308
326
  children,
@@ -386,7 +404,8 @@ function GuideProvider({
386
404
  if (options?.from === void 0 && options?.resume !== false && storage) {
387
405
  let progress = null;
388
406
  try {
389
- progress = await storage.read(tourId);
407
+ const stored = await storage.read(`tour:${tourId}`);
408
+ progress = isTourProgress(stored) ? stored : null;
390
409
  } catch (error) {
391
410
  warnStorageFailure(error);
392
411
  }
@@ -453,7 +472,7 @@ function GuideProvider({
453
472
  if (!status) return;
454
473
  try {
455
474
  void Promise.resolve(
456
- storage.write(state.tourId, { status, stepIndex: state.stepIndex })
475
+ storage.write(`tour:${state.tourId}`, { status, stepIndex: state.stepIndex })
457
476
  ).catch(warnStorageFailure);
458
477
  } catch (error) {
459
478
  warnStorageFailure(error);
@@ -518,18 +537,303 @@ function useGuideStep() {
518
537
  if (!context) throw new Error("[guide] useGuideStep must be used inside a GuideProvider");
519
538
  return context.activeStep;
520
539
  }
540
+
541
+ // src/ChecklistProvider.tsx
542
+ var import_react7 = require("react");
543
+ var import_jsx_runtime2 = require("react/jsx-runtime");
544
+ var ChecklistContext = (0, import_react7.createContext)(null);
545
+ var emptyProgress = { completed: [], dismissed: false };
546
+ function ChecklistProvider({
547
+ checklists,
548
+ children,
549
+ storage,
550
+ translate,
551
+ navigate,
552
+ onEvent
553
+ }) {
554
+ const checklistsById = (0, import_react7.useMemo)(() => {
555
+ const map = /* @__PURE__ */ new Map();
556
+ for (const candidate of checklists) map.set(candidate.id, candidate);
557
+ return map;
558
+ }, [checklists]);
559
+ const [progress, setProgress] = (0, import_react7.useState)(() => {
560
+ const initial = {};
561
+ for (const candidate of checklists) initial[candidate.id] = emptyProgress;
562
+ return initial;
563
+ });
564
+ const progressRef = (0, import_react7.useRef)(progress);
565
+ const guide = (0, import_react7.useContext)(GuideContext);
566
+ const storageWarnedRef = (0, import_react7.useRef)(false);
567
+ const warnStorageFailure = (0, import_react7.useCallback)((error) => {
568
+ if (storageWarnedRef.current) return;
569
+ storageWarnedRef.current = true;
570
+ console.warn("[guide] storage failed; checklist progress will not be persisted", error);
571
+ }, []);
572
+ const noGuideWarnedRef = (0, import_react7.useRef)(false);
573
+ const warnNoGuide = (0, import_react7.useCallback)(() => {
574
+ if (noGuideWarnedRef.current) return;
575
+ noGuideWarnedRef.current = true;
576
+ console.warn("[guide] a checklist item needs a GuideProvider to launch a tour");
577
+ }, []);
578
+ const tourStartFailedWarnedRef = (0, import_react7.useRef)(false);
579
+ const warnTourStartFailure = (0, import_react7.useCallback)((error) => {
580
+ if (tourStartFailedWarnedRef.current) return;
581
+ tourStartFailedWarnedRef.current = true;
582
+ console.warn("[guide] starting a tour for a checklist item failed", error);
583
+ }, []);
584
+ const noNavigateWarnedRef = (0, import_react7.useRef)(false);
585
+ const warnNoNavigate = (0, import_react7.useCallback)(() => {
586
+ if (noNavigateWarnedRef.current) return;
587
+ noNavigateWarnedRef.current = true;
588
+ console.warn("[guide] a checklist item declares an href but no navigate function was provided");
589
+ }, []);
590
+ const onEventRef = (0, import_react7.useRef)(onEvent);
591
+ onEventRef.current = onEvent;
592
+ const emit = (0, import_react7.useCallback)((event) => onEventRef.current?.(event), []);
593
+ (0, import_react7.useEffect)(() => {
594
+ if (!storage) return;
595
+ let cancelled = false;
596
+ void (async () => {
597
+ const restored = {};
598
+ for (const candidate of checklists) {
599
+ try {
600
+ const stored = await storage.read(`checklist:${candidate.id}`);
601
+ if (isChecklistProgress(stored)) restored[candidate.id] = stored;
602
+ } catch (error) {
603
+ warnStorageFailure(error);
604
+ }
605
+ }
606
+ if (!cancelled && Object.keys(restored).length > 0) {
607
+ const merged = { ...progressRef.current };
608
+ for (const [checklistId, stored] of Object.entries(restored)) {
609
+ const live = merged[checklistId] ?? emptyProgress;
610
+ merged[checklistId] = {
611
+ completed: live.completed.concat(
612
+ stored.completed.filter((id) => !live.completed.includes(id))
613
+ ),
614
+ dismissed: live.dismissed || stored.dismissed
615
+ };
616
+ }
617
+ progressRef.current = merged;
618
+ setProgress(merged);
619
+ }
620
+ })();
621
+ return () => {
622
+ cancelled = true;
623
+ };
624
+ }, [storage]);
625
+ const applyProgress = (0, import_react7.useCallback)(
626
+ (checklistId, next) => {
627
+ const merged = { ...progressRef.current, [checklistId]: next };
628
+ progressRef.current = merged;
629
+ setProgress(merged);
630
+ if (!storage) return;
631
+ try {
632
+ void Promise.resolve(storage.write(`checklist:${checklistId}`, next)).catch(
633
+ warnStorageFailure
634
+ );
635
+ } catch (error) {
636
+ warnStorageFailure(error);
637
+ }
638
+ },
639
+ [storage, warnStorageFailure]
640
+ );
641
+ const resolveChecklist = (0, import_react7.useCallback)(
642
+ (checklistId) => {
643
+ const checklist = checklistsById.get(checklistId);
644
+ if (!checklist) {
645
+ console.warn(`[guide] unknown checklist "${checklistId}"`);
646
+ return null;
647
+ }
648
+ return checklist;
649
+ },
650
+ [checklistsById]
651
+ );
652
+ const resolveItem = (0, import_react7.useCallback)(
653
+ (checklistId, itemId) => {
654
+ const checklist = resolveChecklist(checklistId);
655
+ if (!checklist) return null;
656
+ const item = checklist.items.find((candidate) => candidate.id === itemId);
657
+ if (!item) {
658
+ console.warn(`[guide] unknown checklist item "${itemId}"`);
659
+ return null;
660
+ }
661
+ return { checklist, item };
662
+ },
663
+ [resolveChecklist]
664
+ );
665
+ const complete = (0, import_react7.useCallback)(
666
+ (checklistId, itemId) => {
667
+ const resolved = resolveItem(checklistId, itemId);
668
+ if (!resolved) return;
669
+ const { checklist } = resolved;
670
+ const current = progressRef.current[checklistId] ?? emptyProgress;
671
+ if (current.completed.includes(itemId)) return;
672
+ const wasComplete = checklist.items.length > 0 && checklist.items.every((candidate) => current.completed.includes(candidate.id));
673
+ const nextCompleted = [...current.completed, itemId];
674
+ applyProgress(checklistId, { ...current, completed: nextCompleted });
675
+ emit({ type: "checklist:item-complete", checklistId, itemId });
676
+ const isNowComplete = checklist.items.length > 0 && checklist.items.every((candidate) => nextCompleted.includes(candidate.id));
677
+ if (isNowComplete && !wasComplete) emit({ type: "checklist:complete", checklistId });
678
+ },
679
+ [resolveItem, applyProgress, emit]
680
+ );
681
+ const completeItemsForTour = (0, import_react7.useCallback)(
682
+ (tourId) => {
683
+ for (const candidate of checklists) {
684
+ for (const item of candidate.items) {
685
+ if (item.tourId === tourId) complete(candidate.id, item.id);
686
+ }
687
+ }
688
+ },
689
+ [checklists, complete]
690
+ );
691
+ const handledCompletionRef = (0, import_react7.useRef)(null);
692
+ (0, import_react7.useEffect)(() => {
693
+ const state = guide?.state;
694
+ if (!state || state.status !== "completed" || !state.tourId) {
695
+ handledCompletionRef.current = null;
696
+ return;
697
+ }
698
+ if (handledCompletionRef.current === state.tourId) return;
699
+ handledCompletionRef.current = state.tourId;
700
+ completeItemsForTour(state.tourId);
701
+ }, [guide?.state, completeItemsForTour]);
702
+ const toggle = (0, import_react7.useCallback)(
703
+ (checklistId, itemId) => {
704
+ const resolved = resolveItem(checklistId, itemId);
705
+ if (!resolved) return;
706
+ const current = progressRef.current[checklistId] ?? emptyProgress;
707
+ if (current.completed.includes(itemId)) {
708
+ const nextCompleted = current.completed.filter((id) => id !== itemId);
709
+ applyProgress(checklistId, { ...current, completed: nextCompleted });
710
+ return;
711
+ }
712
+ complete(checklistId, itemId);
713
+ },
714
+ [resolveItem, applyProgress, complete]
715
+ );
716
+ const dismiss = (0, import_react7.useCallback)(
717
+ (checklistId) => {
718
+ if (!resolveChecklist(checklistId)) return;
719
+ const current = progressRef.current[checklistId] ?? emptyProgress;
720
+ applyProgress(checklistId, { ...current, dismissed: true });
721
+ emit({ type: "checklist:dismiss", checklistId });
722
+ },
723
+ [resolveChecklist, applyProgress, emit]
724
+ );
725
+ const reset = (0, import_react7.useCallback)(
726
+ (checklistId) => {
727
+ if (!resolveChecklist(checklistId)) return;
728
+ applyProgress(checklistId, { completed: [], dismissed: false });
729
+ },
730
+ [resolveChecklist, applyProgress]
731
+ );
732
+ const activate = (0, import_react7.useCallback)(
733
+ (checklistId, itemId) => {
734
+ const resolved = resolveItem(checklistId, itemId);
735
+ if (!resolved) return;
736
+ const { item } = resolved;
737
+ if (item.tourId) {
738
+ if (!guide) {
739
+ warnNoGuide();
740
+ return;
741
+ }
742
+ void guide.start(item.tourId).catch(warnTourStartFailure);
743
+ return;
744
+ }
745
+ if (item.href) {
746
+ if (!navigate) {
747
+ warnNoNavigate();
748
+ return;
749
+ }
750
+ navigate(item.href);
751
+ return;
752
+ }
753
+ toggle(checklistId, itemId);
754
+ },
755
+ [resolveItem, guide, navigate, toggle, warnNoGuide, warnNoNavigate, warnTourStartFailure]
756
+ );
757
+ const value = (0, import_react7.useMemo)(
758
+ () => ({ checklists, progress, translate, activate, toggle, complete, dismiss, reset }),
759
+ [checklists, progress, translate, activate, toggle, complete, dismiss, reset]
760
+ );
761
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ChecklistContext.Provider, { value, children });
762
+ }
763
+
764
+ // src/useChecklist.ts
765
+ var import_react8 = require("react");
766
+ function useChecklist(checklistId) {
767
+ const context = (0, import_react8.useContext)(ChecklistContext);
768
+ if (!context)
769
+ throw new Error("[guide] useChecklist must be used inside a ChecklistProvider");
770
+ const checklist = context.checklists.find((entry) => entry.id === checklistId);
771
+ if (!checklist) throw new Error(`[guide] unknown checklist "${checklistId}"`);
772
+ const progress = context.progress[checklistId];
773
+ const completed = progress?.completed ?? [];
774
+ const dismissed = progress?.dismissed ?? false;
775
+ const translate = context.translate;
776
+ const items = (0, import_react8.useMemo)(
777
+ () => checklist.items.map((item) => ({
778
+ id: item.id,
779
+ title: resolveText(item.title, item.titleKey, translate),
780
+ body: resolveText(item.body, item.bodyKey, translate),
781
+ completed: completed.includes(item.id),
782
+ tourId: item.tourId,
783
+ href: item.href
784
+ })),
785
+ [checklist, completed, translate]
786
+ );
787
+ const total = checklist.items.length;
788
+ const completedCount = items.filter((item) => item.completed).length;
789
+ const isComplete = total > 0 && completedCount === total;
790
+ const { activate, toggle, complete, dismiss, reset } = context;
791
+ return (0, import_react8.useMemo)(
792
+ () => ({
793
+ items,
794
+ completedCount,
795
+ total,
796
+ isComplete,
797
+ dismissed,
798
+ activate: (itemId) => activate(checklistId, itemId),
799
+ toggle: (itemId) => toggle(checklistId, itemId),
800
+ complete: (itemId) => complete(checklistId, itemId),
801
+ dismiss: () => dismiss(checklistId),
802
+ reset: () => reset(checklistId)
803
+ }),
804
+ [
805
+ items,
806
+ completedCount,
807
+ total,
808
+ isComplete,
809
+ dismissed,
810
+ activate,
811
+ toggle,
812
+ complete,
813
+ dismiss,
814
+ reset,
815
+ checklistId
816
+ ]
817
+ );
818
+ }
521
819
  // Annotate the CommonJS export names for ESM import in node:
522
820
  0 && (module.exports = {
821
+ ChecklistContext,
822
+ ChecklistProvider,
523
823
  GuideContext,
524
824
  GuideProvider,
525
825
  createBrowserStorage,
526
826
  createMemoryStorage,
527
827
  findMissingTargets,
528
828
  initialTourState,
829
+ isChecklistProgress,
529
830
  isLiteralRoute,
831
+ isTourProgress,
530
832
  matchRoute,
833
+ resolveText,
531
834
  tourReducer,
532
835
  useAnnouncer,
836
+ useChecklist,
533
837
  useElementRect,
534
838
  useFocusTrap,
535
839
  useGuideStep,