@apollovisionlabs/guide-core 0.1.0 → 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 +112 -14
- package/dist/index.cjs +318 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +92 -4
- package/dist/index.d.ts +92 -4
- package/dist/index.mjs +320 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,43 +101,55 @@ the destination.
|
|
|
101
101
|
|
|
102
102
|
## Persistence
|
|
103
103
|
|
|
104
|
-
`GuideStorage` is the two-method interface
|
|
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(
|
|
109
|
-
write(
|
|
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
|
|
124
|
+
import type { GuideStorage } from '@apollovisionlabs/guide-core'
|
|
119
125
|
|
|
120
126
|
function createServerStorage(): GuideStorage {
|
|
121
127
|
return {
|
|
122
|
-
async read(
|
|
123
|
-
const response = await fetch(`/api/
|
|
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
|
|
131
|
+
return (await response.json()) as T
|
|
126
132
|
},
|
|
127
|
-
async write(
|
|
128
|
-
await fetch(`/api/
|
|
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(
|
|
137
|
+
body: JSON.stringify(value),
|
|
132
138
|
})
|
|
133
139
|
},
|
|
134
140
|
}
|
|
135
141
|
}
|
|
136
142
|
```
|
|
137
143
|
|
|
138
|
-
Pass it as the `storage` prop
|
|
139
|
-
index is passed, or `resume: false` is passed) and writes whenever a running
|
|
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`
|
|
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(
|
|
48
|
-
return store.get(
|
|
53
|
+
async read(key) {
|
|
54
|
+
return store.get(key) ?? null;
|
|
49
55
|
},
|
|
50
|
-
async write(
|
|
51
|
-
store.set(
|
|
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 = (
|
|
62
|
+
const key = (storageKey) => `${namespace}:${storageKey}`;
|
|
57
63
|
const available = () => typeof window !== "undefined" && !!window.localStorage;
|
|
58
64
|
return {
|
|
59
|
-
async read(
|
|
65
|
+
async read(storageKey) {
|
|
60
66
|
if (!available()) return null;
|
|
61
67
|
try {
|
|
62
|
-
const raw = window.localStorage.getItem(key(
|
|
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(
|
|
74
|
+
async write(storageKey, value) {
|
|
69
75
|
if (!available()) return;
|
|
70
76
|
try {
|
|
71
|
-
window.localStorage.setItem(key(
|
|
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/
|
|
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
|
-
|
|
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
|
|
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,
|