@ciridae/ui 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/components/blocks/comment-thread.js +2 -25
  2. package/dist/components/blocks/feedback-center-comments.d.ts +4 -2
  3. package/dist/components/blocks/feedback-center-comments.js +49 -6
  4. package/dist/components/blocks/feedback-center-sheet-parts.d.ts +4 -2
  5. package/dist/components/blocks/feedback-center-sheet-parts.js +8 -8
  6. package/dist/components/blocks/feedback-center-sheet.d.ts +3 -1
  7. package/dist/components/blocks/feedback-center-sheet.js +3 -1
  8. package/dist/components/blocks/feedback-composer.d.ts +2 -0
  9. package/dist/components/blocks/feedback-composer.js +5 -5
  10. package/dist/components/blocks/feedback-detail-sheet.js +2 -2
  11. package/dist/components/blocks/feedback-metrics.js +1 -1
  12. package/dist/components/blocks/feedback-tracking-types.d.ts +6 -1
  13. package/dist/components/blocks/feedback-tracking-types.js +2 -2
  14. package/dist/components/blocks/feedback-user-detail.js +7 -8
  15. package/dist/components/blocks/feedback-user-list.js +4 -4
  16. package/dist/components/blocks/timestamp-format.js +2 -4
  17. package/dist/components/blocks/use-reveal-thread-end.d.ts +11 -0
  18. package/dist/components/blocks/use-reveal-thread-end.js +25 -0
  19. package/dist/components/ui/app-shell-page.js +6 -2
  20. package/dist/components/ui/item.d.ts +1 -1
  21. package/dist/components/ui/sidebar/context.d.ts +6 -0
  22. package/dist/components/ui/sidebar/context.js +9 -1
  23. package/dist/components/ui/sidebar.d.ts +1 -1
  24. package/dist/components/ui/sidebar.js +1 -1
  25. package/dist/components/ui/tabs-variants.d.ts +1 -1
  26. package/dist/components/walkthrough/presentation/WalkthroughCoach.d.ts +14 -0
  27. package/dist/components/walkthrough/presentation/WalkthroughCoach.js +32 -0
  28. package/dist/components/walkthrough/presentation/WalkthroughHighlight.d.ts +5 -0
  29. package/dist/components/walkthrough/presentation/WalkthroughHighlight.js +9 -0
  30. package/dist/components/walkthrough/presentation/WalkthroughOverlay.d.ts +1 -0
  31. package/dist/components/walkthrough/presentation/WalkthroughOverlay.js +70 -0
  32. package/dist/components/walkthrough/presentation/positionWalkthroughCoach.d.ts +11 -0
  33. package/dist/components/walkthrough/presentation/positionWalkthroughCoach.js +24 -0
  34. package/dist/components/walkthrough/presentation/useWalkthroughCoachPosition.d.ts +4 -0
  35. package/dist/components/walkthrough/presentation/useWalkthroughCoachPosition.js +17 -0
  36. package/dist/components/walkthrough/runtime/WalkthroughContextDefinition.d.ts +20 -0
  37. package/dist/components/walkthrough/runtime/WalkthroughContextDefinition.js +2 -0
  38. package/dist/components/walkthrough/runtime/WalkthroughProvider.d.ts +12 -0
  39. package/dist/components/walkthrough/runtime/WalkthroughProvider.js +190 -0
  40. package/dist/components/walkthrough/runtime/useWalkthrough.d.ts +1 -0
  41. package/dist/components/walkthrough/runtime/useWalkthrough.js +8 -0
  42. package/dist/components/walkthrough/runtime/walkthroughEvents.d.ts +3 -0
  43. package/dist/components/walkthrough/runtime/walkthroughEvents.js +55 -0
  44. package/dist/components/walkthrough/runtime/walkthroughLocation.d.ts +2 -0
  45. package/dist/components/walkthrough/runtime/walkthroughLocation.js +27 -0
  46. package/dist/components/walkthrough/runtime/walkthroughStorage.d.ts +3 -0
  47. package/dist/components/walkthrough/runtime/walkthroughStorage.js +54 -0
  48. package/dist/components/walkthrough/runtime/walkthroughTypes.d.ts +32 -0
  49. package/dist/components/walkthrough/runtime/walkthroughTypes.js +1 -0
  50. package/dist/components/walkthrough/targeting/listenForOutsideWalkthroughInteraction.d.ts +2 -0
  51. package/dist/components/walkthrough/targeting/listenForOutsideWalkthroughInteraction.js +19 -0
  52. package/dist/components/walkthrough/targeting/useWalkthroughTargetRect.d.ts +15 -0
  53. package/dist/components/walkthrough/targeting/useWalkthroughTargetRect.js +54 -0
  54. package/dist/components/walkthrough/targeting/walkthroughTarget.d.ts +6 -0
  55. package/dist/components/walkthrough/targeting/walkthroughTarget.js +24 -0
  56. package/dist/components/walkthrough.d.ts +4 -0
  57. package/dist/components/walkthrough.js +2 -0
  58. package/dist/entries/walkthrough.d.ts +1 -0
  59. package/dist/entries/walkthrough.js +1 -0
  60. package/package.json +1 -1
@@ -0,0 +1,14 @@
1
+ import type { WalkthroughAdvance, WalkthroughStep } from "../runtime/walkthroughTypes.js";
2
+ export type WalkthroughCoachState = "locating" | "missing" | "off-route" | "ready" | "restore";
3
+ export declare function WalkthroughCoach({ advanceType, coachState, flowTitle, onBack, onNext, onPause, onReturn, step, stepCount, stepIndex, }: {
4
+ advanceType: WalkthroughAdvance["type"];
5
+ coachState: WalkthroughCoachState;
6
+ flowTitle: string;
7
+ onBack: () => void;
8
+ onNext: () => void;
9
+ onPause: () => void;
10
+ onReturn: () => void;
11
+ step: WalkthroughStep;
12
+ stepCount: number;
13
+ stepIndex: number;
14
+ }): import("@types/react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Button } from "../../ui/button.js";
3
+ import { Card, CardContent } from "../../ui/card.js";
4
+ export function WalkthroughCoach({ advanceType, coachState, flowTitle, onBack, onNext, onPause, onReturn, step, stepCount, stepIndex, }) {
5
+ return (_jsx(Card, { className: "border-foreground/20 shadow-sm", children: _jsxs(CardContent, { className: "grid gap-4 py-4", children: [_jsxs("div", { className: "grid gap-1", children: [_jsxs("p", { className: "text-meta text-muted-foreground", children: [flowTitle, " \u00B7 Step ", stepIndex + 1, " of ", stepCount] }), _jsx("h2", { className: "font-heading text-lg text-foreground", children: step.title }), _jsx("p", { className: "text-sm leading-relaxed text-muted-foreground", children: step.body }), _jsx(CoachStatus, { advanceType: advanceType, state: coachState })] }), _jsxs("div", { className: "flex flex-wrap justify-end gap-2", children: [_jsx(Button, { onClick: onPause, size: "sm", type: "button", variant: "ghost", children: "Pause" }), _jsx(Button, { disabled: stepIndex === 0, onClick: onBack, size: "sm", type: "button", variant: "outline", children: "Back" }), _jsx(CoachPrimaryAction, { advanceType: advanceType, coachState: coachState, isLast: stepIndex === stepCount - 1, onNext: onNext, onReturn: onReturn })] })] }) }));
6
+ }
7
+ function CoachStatus({ advanceType, state, }) {
8
+ if (state === "off-route") {
9
+ return _jsx("p", { className: "text-xs text-muted-foreground", children: "This page is outside the walkthrough." });
10
+ }
11
+ if (state === "missing") {
12
+ return (_jsx("p", { className: "text-xs text-muted-foreground", children: "The required control is not available in the current app state." }));
13
+ }
14
+ if (state === "locating") {
15
+ return _jsx("p", { className: "text-xs text-muted-foreground", children: "Locating the next control\u2026" });
16
+ }
17
+ if (state === "restore") {
18
+ return (_jsx("p", { className: "text-xs text-muted-foreground", children: "Use the highlighted control to reopen this step." }));
19
+ }
20
+ if (advanceType !== "manual") {
21
+ return (_jsx("p", { className: "text-xs text-muted-foreground", children: "Use the highlighted control to continue \u00B7 click elsewhere to pause" }));
22
+ }
23
+ return (_jsx("p", { className: "text-xs text-muted-foreground", children: "Next to continue \u00B7 click elsewhere to pause" }));
24
+ }
25
+ function CoachPrimaryAction({ advanceType, coachState, isLast, onNext, onReturn, }) {
26
+ if (coachState === "off-route") {
27
+ return (_jsx(Button, { onClick: onReturn, size: "sm", type: "button", children: "Return to step" }));
28
+ }
29
+ if (advanceType !== "manual" || coachState === "restore")
30
+ return null;
31
+ return (_jsx(Button, { disabled: coachState !== "ready", onClick: onNext, size: "sm", type: "button", children: isLast ? "Finish" : "Next" }));
32
+ }
@@ -0,0 +1,5 @@
1
+ import type { WalkthroughTargetRect } from "../targeting/useWalkthroughTargetRect.js";
2
+ export declare function WalkthroughHighlight({ rect, stepNumber, }: {
3
+ rect: WalkthroughTargetRect;
4
+ stepNumber: number;
5
+ }): import("@types/react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function WalkthroughHighlight({ rect, stepNumber, }) {
3
+ const pad = 6;
4
+ const left = Math.min(window.innerWidth, Math.max(0, rect.left - pad));
5
+ const top = Math.min(window.innerHeight, Math.max(0, rect.top - pad));
6
+ const right = Math.max(left, Math.min(window.innerWidth, rect.left + rect.width + pad));
7
+ const bottom = Math.max(top, Math.min(window.innerHeight, rect.top + rect.height + pad));
8
+ return (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": true, className: "absolute inset-x-0 top-0 bg-foreground/40", style: { height: top } }), _jsx("div", { "aria-hidden": true, className: "absolute bottom-0 inset-x-0 bg-foreground/40", style: { top: bottom } }), _jsx("div", { "aria-hidden": true, className: "absolute bg-foreground/40", style: { bottom: window.innerHeight - bottom, left: 0, top, width: left } }), _jsx("div", { "aria-hidden": true, className: "absolute bg-foreground/40", style: { bottom: window.innerHeight - bottom, left: right, right: 0, top } }), _jsx("div", { "aria-hidden": true, className: "absolute rounded-md ring-2 ring-foreground", style: { height: bottom - top, left, top, width: right - left } }), _jsx("div", { "aria-hidden": true, className: "absolute z-10 grid size-7 place-items-center rounded-full bg-foreground font-mono text-xs text-background", style: { left: Math.max(8, left - 10), top: Math.max(8, top - 10) }, children: stepNumber })] }));
9
+ }
@@ -0,0 +1 @@
1
+ export declare function WalkthroughOverlay(): import("@types/react").JSX.Element | null;
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { listenForWalkthroughEvent, listenForWalkthroughTargetClick, } from "../runtime/walkthroughEvents.js";
5
+ import { walkthroughHrefMatches } from "../runtime/walkthroughLocation.js";
6
+ import { useWalkthrough } from "../runtime/useWalkthrough.js";
7
+ import { listenForOutsideWalkthroughInteraction } from "../targeting/listenForOutsideWalkthroughInteraction.js";
8
+ import { useWalkthroughTargetRect, } from "../targeting/useWalkthroughTargetRect.js";
9
+ import { WalkthroughCoach } from "./WalkthroughCoach.js";
10
+ import { WalkthroughHighlight } from "./WalkthroughHighlight.js";
11
+ import { useWalkthroughCoachPosition } from "./useWalkthroughCoachPosition.js";
12
+ export function WalkthroughOverlay() {
13
+ const walkthrough = useWalkthrough();
14
+ if (walkthrough.progress?.status !== "active" ||
15
+ !walkthrough.selectedWalkthrough ||
16
+ !walkthrough.currentStep) {
17
+ return null;
18
+ }
19
+ return (_jsx(WalkthroughStepOverlay, { advance: walkthrough.advance, currentHref: walkthrough.currentHref, flowTitle: walkthrough.selectedWalkthrough.title, goBack: walkthrough.goBack, navigateToCurrentStep: walkthrough.navigateToCurrentStep, pause: walkthrough.pauseWalkthrough, step: walkthrough.currentStep, stepCount: walkthrough.selectedWalkthrough.steps.length, stepIndex: walkthrough.progress.stepIndex }, `${walkthrough.selectedWalkthrough.id}:${walkthrough.currentStep.id}`));
20
+ }
21
+ function WalkthroughStepOverlay({ advance, currentHref, flowTitle, goBack, navigateToCurrentStep, pause, step, stepCount, stepIndex, }) {
22
+ const onStepRoute = walkthroughHrefMatches(currentHref, step.href);
23
+ const targetObservation = useWalkthroughTargetRect(onStepRoute ? step.target : null);
24
+ const restoreObservation = useWalkthroughTargetRect(onStepRoute && targetObservation.status === "missing" ? (step.restoreTarget ?? null) : null);
25
+ const advanceRef = useLatestValue(advance);
26
+ const onStepRouteRef = useLatestValue(onStepRoute);
27
+ const coachRef = useRef(null);
28
+ const coachState = resolveCoachState(onStepRoute, Boolean(step.restoreTarget), targetObservation, restoreObservation);
29
+ const observation = coachState === "restore" ? restoreObservation : targetObservation;
30
+ const coachPosition = useWalkthroughCoachPosition(observation, coachRef);
31
+ useEffect(() => listenForStepCompletion(step, () => onStepRouteRef.current, () => advanceRef.current()), [advanceRef, onStepRouteRef, step]);
32
+ useEffect(() => listenForOutsideInteractionOnStepRoute(onStepRoute, step, pause), [onStepRoute, pause, step]);
33
+ const rect = observation.status === "found" && onStepRoute ? observation.rect : null;
34
+ return createPortal(_jsxs("div", { "aria-live": "polite", className: "pointer-events-none fixed inset-0 z-[100]", children: [rect ? _jsx(WalkthroughHighlight, { rect: rect, stepNumber: stepIndex + 1 }) : null, _jsx("div", { className: "pointer-events-auto absolute w-[min(24rem,calc(100%-2rem))]", "data-walkthrough-coach": "", ref: coachRef, style: coachPosition && rect
35
+ ? { left: coachPosition.left, top: coachPosition.top }
36
+ : { bottom: 16, right: 16 }, children: _jsx(WalkthroughCoach, { advanceType: step.advance.type, coachState: coachState, flowTitle: flowTitle, onBack: goBack, onNext: advance, onPause: pause, onReturn: navigateToCurrentStep, step: step, stepCount: stepCount, stepIndex: stepIndex }) })] }), document.body);
37
+ }
38
+ function listenForOutsideInteractionOnStepRoute(onStepRoute, step, pause) {
39
+ return onStepRoute ? listenForOutsideWalkthroughInteraction(step, pause) : undefined;
40
+ }
41
+ function useLatestValue(value) {
42
+ const valueRef = useRef(value);
43
+ useLayoutEffect(() => {
44
+ valueRef.current = value;
45
+ }, [value]);
46
+ return valueRef;
47
+ }
48
+ function listenForStepCompletion(step, canComplete, advance) {
49
+ if (step.advance.type === "manual")
50
+ return () => undefined;
51
+ if (step.advance.type === "target-click") {
52
+ return listenForWalkthroughTargetClick(step.target, canComplete, advance);
53
+ }
54
+ return listenForWalkthroughEvent(step.advance.event, canComplete, advance);
55
+ }
56
+ function resolveCoachState(onStepRoute, canRestore, targetObservation, restoreObservation) {
57
+ if (!onStepRoute)
58
+ return "off-route";
59
+ if (targetObservation.status === "locating")
60
+ return "locating";
61
+ if (targetObservation.status === "found")
62
+ return "ready";
63
+ if (!canRestore)
64
+ return "missing";
65
+ if (restoreObservation.status === "found")
66
+ return "restore";
67
+ if (restoreObservation.status === "locating")
68
+ return "locating";
69
+ return "missing";
70
+ }
@@ -0,0 +1,11 @@
1
+ import type { WalkthroughTargetRect } from "../targeting/useWalkthroughTargetRect.js";
2
+ export type WalkthroughCoachPosition = {
3
+ left: number;
4
+ top: number;
5
+ };
6
+ type Size = {
7
+ height: number;
8
+ width: number;
9
+ };
10
+ export declare function positionWalkthroughCoach(target: WalkthroughTargetRect, coach: Size, viewport: Size): WalkthroughCoachPosition;
11
+ export {};
@@ -0,0 +1,24 @@
1
+ const GAP = 16;
2
+ const VIEWPORT_MARGIN = 16;
3
+ export function positionWalkthroughCoach(target, coach, viewport) {
4
+ const candidates = [
5
+ { left: target.left + target.width + GAP, top: target.top },
6
+ { left: target.left - coach.width - GAP, top: target.top },
7
+ { left: target.left, top: target.top + target.height + GAP },
8
+ { left: target.left, top: target.top - coach.height - GAP },
9
+ ];
10
+ const fitting = candidates.find((candidate) => fits(candidate, coach, viewport));
11
+ return clamp(fitting ?? candidates[2], coach, viewport);
12
+ }
13
+ function fits(position, coach, viewport) {
14
+ return (position.left >= VIEWPORT_MARGIN &&
15
+ position.top >= VIEWPORT_MARGIN &&
16
+ position.left + coach.width <= viewport.width - VIEWPORT_MARGIN &&
17
+ position.top + coach.height <= viewport.height - VIEWPORT_MARGIN);
18
+ }
19
+ function clamp(position, coach, viewport) {
20
+ return {
21
+ left: Math.max(VIEWPORT_MARGIN, Math.min(position.left, viewport.width - coach.width - VIEWPORT_MARGIN)),
22
+ top: Math.max(VIEWPORT_MARGIN, Math.min(position.top, viewport.height - coach.height - VIEWPORT_MARGIN)),
23
+ };
24
+ }
@@ -0,0 +1,4 @@
1
+ import { type RefObject } from "react";
2
+ import { type WalkthroughCoachPosition } from "./positionWalkthroughCoach.js";
3
+ import type { WalkthroughTargetObservation } from "../targeting/useWalkthroughTargetRect.js";
4
+ export declare function useWalkthroughCoachPosition(observation: WalkthroughTargetObservation, coachRef: RefObject<HTMLDivElement | null>): WalkthroughCoachPosition | null;
@@ -0,0 +1,17 @@
1
+ import { useLayoutEffect, useState } from "react";
2
+ import { positionWalkthroughCoach } from "./positionWalkthroughCoach.js";
3
+ const FALLBACK_COACH_SIZE = { height: 232, width: 384 };
4
+ export function useWalkthroughCoachPosition(observation, coachRef) {
5
+ const [position, setPosition] = useState(null);
6
+ useLayoutEffect(() => {
7
+ if (observation.status !== "found")
8
+ return;
9
+ const box = coachRef.current?.getBoundingClientRect();
10
+ const coach = box ? { height: box.height, width: box.width } : FALLBACK_COACH_SIZE;
11
+ setPosition(positionWalkthroughCoach(observation.rect, coach, {
12
+ height: window.innerHeight,
13
+ width: window.innerWidth,
14
+ }));
15
+ }, [coachRef, observation]);
16
+ return position;
17
+ }
@@ -0,0 +1,20 @@
1
+ import type { WalkthroughDefinition, WalkthroughProgress, WalkthroughStep } from "./walkthroughTypes.js";
2
+ export type WalkthroughContextValue = {
3
+ advance: () => void;
4
+ clearWalkthrough: () => void;
5
+ currentHref: string;
6
+ currentStep: WalkthroughStep | null;
7
+ emitEvent: (event: string) => void;
8
+ goBack: () => void;
9
+ navigateToCurrentStep: () => void;
10
+ pauseWalkthrough: () => void;
11
+ progress: WalkthroughProgress | null;
12
+ resumeWalkthrough: () => void;
13
+ selectedWalkthrough: WalkthroughDefinition | null;
14
+ startWalkthrough: (walkthroughId: string) => Promise<void>;
15
+ targetProps: (target: string) => {
16
+ "data-walkthrough-target": string;
17
+ };
18
+ walkthroughs: readonly WalkthroughDefinition[];
19
+ };
20
+ export declare const WalkthroughContext: import("@types/react").Context<WalkthroughContextValue | undefined>;
@@ -0,0 +1,2 @@
1
+ import { createContext } from "react";
2
+ export const WalkthroughContext = createContext(undefined);
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from "react";
2
+ import type { WalkthroughDefinition, WalkthroughFreshStartHandlers } from "./walkthroughTypes.js";
3
+ export type WalkthroughProviderProps = {
4
+ children: ReactNode;
5
+ currentHref: string;
6
+ freshStartHandlers: WalkthroughFreshStartHandlers;
7
+ navigate: (href: string) => unknown;
8
+ readCurrentHref: () => string;
9
+ storageKey: string;
10
+ walkthroughs: readonly WalkthroughDefinition[];
11
+ };
12
+ export declare function WalkthroughProvider({ children, currentHref, freshStartHandlers, navigate, readCurrentHref, storageKey, walkthroughs, }: WalkthroughProviderProps): import("@types/react").JSX.Element;
@@ -0,0 +1,190 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { WalkthroughContext } from "./WalkthroughContextDefinition.js";
4
+ import { emitWalkthroughEvent } from "./walkthroughEvents.js";
5
+ import { walkthroughHrefForNavigation, walkthroughHrefMatches } from "./walkthroughLocation.js";
6
+ import { readWalkthroughProgress, writeWalkthroughProgress } from "./walkthroughStorage.js";
7
+ import { WalkthroughOverlay } from "../presentation/WalkthroughOverlay.js";
8
+ import { walkthroughTargetProps } from "../targeting/walkthroughTarget.js";
9
+ export function WalkthroughProvider({ children, currentHref, freshStartHandlers, navigate, readCurrentHref, storageKey, walkthroughs, }) {
10
+ const startRequests = useMemo(() => createWalkthroughStartRequests(), []);
11
+ const [progress, setProgress] = useState(() => readInitialProgress(storageKey, walkthroughs));
12
+ useEffect(() => () => startRequests.cancel(), [startRequests]);
13
+ useEffect(() => {
14
+ writeWalkthroughProgress(storageKey, progress);
15
+ }, [progress, storageKey]);
16
+ const selectedWalkthrough = walkthroughById(walkthroughs, progress?.walkthroughId);
17
+ const currentStep = selectedWalkthrough
18
+ ? stepAt(selectedWalkthrough, progress?.stepIndex ?? -1)
19
+ : null;
20
+ const controller = useMemo(() => ({
21
+ freshStartHandlers,
22
+ navigate,
23
+ progress,
24
+ readCurrentHref,
25
+ selectedWalkthrough,
26
+ setProgress,
27
+ startRequests,
28
+ walkthroughs,
29
+ }), [
30
+ freshStartHandlers,
31
+ navigate,
32
+ progress,
33
+ readCurrentHref,
34
+ selectedWalkthrough,
35
+ startRequests,
36
+ walkthroughs,
37
+ ]);
38
+ const value = useMemo(() => buildContextValue(controller, currentHref, currentStep), [controller, currentHref, currentStep]);
39
+ return (_jsxs(WalkthroughContext.Provider, { value: value, children: [children, _jsx(WalkthroughOverlay, {})] }));
40
+ }
41
+ function buildContextValue(controller, currentHref, currentStep) {
42
+ return {
43
+ advance: () => {
44
+ advanceWalkthrough(controller);
45
+ },
46
+ clearWalkthrough: () => {
47
+ controller.startRequests.cancel();
48
+ controller.setProgress(null);
49
+ },
50
+ currentHref,
51
+ currentStep,
52
+ emitEvent: emitWalkthroughEvent,
53
+ goBack: () => {
54
+ goBack(controller);
55
+ },
56
+ navigateToCurrentStep: () => {
57
+ navigateToCurrentStep(controller);
58
+ },
59
+ pauseWalkthrough: () => {
60
+ pauseWalkthrough(controller);
61
+ },
62
+ progress: controller.progress,
63
+ resumeWalkthrough: () => {
64
+ resumeWalkthrough(controller);
65
+ },
66
+ selectedWalkthrough: controller.selectedWalkthrough,
67
+ startWalkthrough: (walkthroughId) => startWalkthrough(controller, walkthroughId),
68
+ targetProps: walkthroughTargetProps,
69
+ walkthroughs: controller.walkthroughs,
70
+ };
71
+ }
72
+ async function startWalkthrough(controller, walkthroughId) {
73
+ const walkthrough = walkthroughById(controller.walkthroughs, walkthroughId);
74
+ if (!walkthrough)
75
+ throw new Error(`Unknown walkthrough: ${walkthroughId}`);
76
+ if (walkthrough.steps.length === 0)
77
+ throw new Error(`Walkthrough has no steps: ${walkthroughId}`);
78
+ const freshStart = walkthrough.freshStart
79
+ ? controller.freshStartHandlers[walkthrough.freshStart]
80
+ : null;
81
+ if (walkthrough.freshStart && !freshStart) {
82
+ throw new Error(`Missing fresh-start handler: ${walkthrough.freshStart}`);
83
+ }
84
+ const startRequest = controller.startRequests.begin();
85
+ controller.setProgress(null);
86
+ if (freshStart) {
87
+ try {
88
+ await freshStart();
89
+ }
90
+ catch (error) {
91
+ controller.startRequests.finishIfLatest(startRequest);
92
+ throw error;
93
+ }
94
+ }
95
+ if (!controller.startRequests.finishIfLatest(startRequest))
96
+ return;
97
+ controller.setProgress({
98
+ status: "active",
99
+ stepIndex: 0,
100
+ version: walkthrough.version,
101
+ walkthroughId,
102
+ });
103
+ navigateToStep(controller, walkthrough, 0);
104
+ }
105
+ function createWalkthroughStartRequests() {
106
+ let latestRequest = null;
107
+ return {
108
+ begin: () => {
109
+ const request = {};
110
+ latestRequest = request;
111
+ return request;
112
+ },
113
+ cancel: () => {
114
+ latestRequest = null;
115
+ },
116
+ finishIfLatest: (request) => {
117
+ if (latestRequest !== request)
118
+ return false;
119
+ latestRequest = null;
120
+ return true;
121
+ },
122
+ };
123
+ }
124
+ function advanceWalkthrough(controller) {
125
+ const { progress, selectedWalkthrough } = controller;
126
+ if (!progress || progress.status !== "active" || !selectedWalkthrough)
127
+ return;
128
+ const nextStepIndex = progress.stepIndex + 1;
129
+ if (nextStepIndex >= selectedWalkthrough.steps.length) {
130
+ controller.setProgress({ ...progress, status: "completed" });
131
+ return;
132
+ }
133
+ controller.setProgress({ ...progress, stepIndex: nextStepIndex });
134
+ navigateToStep(controller, selectedWalkthrough, nextStepIndex);
135
+ }
136
+ function goBack(controller) {
137
+ const { progress, selectedWalkthrough } = controller;
138
+ if (!progress || progress.status !== "active" || !selectedWalkthrough)
139
+ return;
140
+ const previousStepIndex = progress.stepIndex - 1;
141
+ if (previousStepIndex < 0)
142
+ return;
143
+ controller.setProgress({ ...progress, stepIndex: previousStepIndex });
144
+ navigateToStep(controller, selectedWalkthrough, previousStepIndex);
145
+ }
146
+ function pauseWalkthrough(controller) {
147
+ const { progress } = controller;
148
+ if (progress?.status === "active") {
149
+ controller.setProgress({ ...progress, status: "paused" });
150
+ }
151
+ }
152
+ function resumeWalkthrough(controller) {
153
+ const { progress, selectedWalkthrough } = controller;
154
+ if (progress?.status !== "paused" || !selectedWalkthrough)
155
+ return;
156
+ controller.setProgress({ ...progress, status: "active" });
157
+ navigateToStep(controller, selectedWalkthrough, progress.stepIndex);
158
+ }
159
+ function navigateToCurrentStep(controller) {
160
+ if (!controller.progress || !controller.selectedWalkthrough)
161
+ return;
162
+ navigateToStep(controller, controller.selectedWalkthrough, controller.progress.stepIndex);
163
+ }
164
+ function navigateToStep(controller, walkthrough, stepIndex) {
165
+ const step = stepAt(walkthrough, stepIndex);
166
+ const currentHref = controller.readCurrentHref();
167
+ if (step && !walkthroughHrefMatches(currentHref, step.href)) {
168
+ controller.navigate(walkthroughHrefForNavigation(currentHref, step.href));
169
+ }
170
+ }
171
+ function stepAt(walkthrough, stepIndex) {
172
+ return walkthrough.steps.find((_step, index) => index === stepIndex) ?? null;
173
+ }
174
+ function readInitialProgress(storageKey, walkthroughs) {
175
+ const stored = readWalkthroughProgress(storageKey);
176
+ if (!stored)
177
+ return null;
178
+ const walkthrough = walkthroughById(walkthroughs, stored.walkthroughId);
179
+ if (!walkthrough ||
180
+ walkthrough.version !== stored.version ||
181
+ stored.stepIndex >= walkthrough.steps.length) {
182
+ return null;
183
+ }
184
+ return stored.status === "active" ? { ...stored, status: "paused" } : stored;
185
+ }
186
+ function walkthroughById(walkthroughs, walkthroughId) {
187
+ if (!walkthroughId)
188
+ return null;
189
+ return walkthroughs.find((walkthrough) => walkthrough.id === walkthroughId) ?? null;
190
+ }
@@ -0,0 +1 @@
1
+ export declare function useWalkthrough(): import("./WalkthroughContextDefinition.js").WalkthroughContextValue;
@@ -0,0 +1,8 @@
1
+ import { useContext } from "react";
2
+ import { WalkthroughContext } from "./WalkthroughContextDefinition.js";
3
+ export function useWalkthrough() {
4
+ const context = useContext(WalkthroughContext);
5
+ if (!context)
6
+ throw new Error("useWalkthrough must be used within WalkthroughProvider");
7
+ return context;
8
+ }
@@ -0,0 +1,3 @@
1
+ export declare function emitWalkthroughEvent(event: string): void;
2
+ export declare function listenForWalkthroughEvent(event: string, canComplete: () => boolean, onComplete: () => void): () => void;
3
+ export declare function listenForWalkthroughTargetClick(target: string, canComplete: () => boolean, onComplete: () => void): () => void;
@@ -0,0 +1,55 @@
1
+ import { isEnabledWalkthroughTarget, WALKTHROUGH_TARGET_ATTRIBUTE, } from "../targeting/walkthroughTarget.js";
2
+ const WALKTHROUGH_EVENT_PREFIX = "ciridae:walkthrough:";
3
+ export function emitWalkthroughEvent(event) {
4
+ window.dispatchEvent(new Event(walkthroughEventType(event)));
5
+ }
6
+ export function listenForWalkthroughEvent(event, canComplete, onComplete) {
7
+ const eventType = walkthroughEventType(event);
8
+ const eventListener = () => {
9
+ if (canComplete())
10
+ onComplete();
11
+ };
12
+ window.addEventListener(eventType, eventListener);
13
+ return () => {
14
+ window.removeEventListener(eventType, eventListener);
15
+ };
16
+ }
17
+ export function listenForWalkthroughTargetClick(target, canComplete, onComplete) {
18
+ let completionScheduled = false;
19
+ let completed = false;
20
+ const clickListener = (event) => {
21
+ if (completionScheduled || completed)
22
+ return;
23
+ if (!enabledTargetForEvent(event, target))
24
+ return;
25
+ if (!canComplete())
26
+ return;
27
+ completionScheduled = true;
28
+ queueMicrotask(() => {
29
+ completionScheduled = false;
30
+ if (completed)
31
+ return;
32
+ completed = true;
33
+ onComplete();
34
+ });
35
+ };
36
+ document.addEventListener("click", clickListener, true);
37
+ return () => {
38
+ document.removeEventListener("click", clickListener, true);
39
+ };
40
+ }
41
+ function enabledTargetForEvent(event, target) {
42
+ const element = event.target;
43
+ if (!(element instanceof Element))
44
+ return null;
45
+ const targetElement = element.closest(`[${WALKTHROUGH_TARGET_ATTRIBUTE}]`);
46
+ if (targetElement?.getAttribute(WALKTHROUGH_TARGET_ATTRIBUTE) !== target)
47
+ return null;
48
+ if (!isEnabledWalkthroughTarget(targetElement))
49
+ return null;
50
+ const disabledControl = element.closest(":disabled, [aria-disabled='true']");
51
+ return disabledControl && targetElement.contains(disabledControl) ? null : targetElement;
52
+ }
53
+ function walkthroughEventType(event) {
54
+ return `${WALKTHROUGH_EVENT_PREFIX}${event}`;
55
+ }
@@ -0,0 +1,2 @@
1
+ export declare function walkthroughHrefMatches(currentHref: string, stepHref: string): boolean;
2
+ export declare function walkthroughHrefForNavigation(currentHref: string, stepHref: string): string;
@@ -0,0 +1,27 @@
1
+ export function walkthroughHrefMatches(currentHref, stepHref) {
2
+ const current = new URL(currentHref, window.location.origin);
3
+ const step = new URL(stepHref, window.location.origin);
4
+ if (normalizePathname(current.pathname) !== normalizePathname(step.pathname))
5
+ return false;
6
+ if (step.hash && current.hash !== step.hash)
7
+ return false;
8
+ for (const [key, value] of step.searchParams) {
9
+ if (current.searchParams.get(key) !== value)
10
+ return false;
11
+ }
12
+ return true;
13
+ }
14
+ export function walkthroughHrefForNavigation(currentHref, stepHref) {
15
+ const current = new URL(currentHref, window.location.origin);
16
+ const step = new URL(stepHref, window.location.origin);
17
+ if (normalizePathname(current.pathname) !== normalizePathname(step.pathname))
18
+ return stepHref;
19
+ for (const [key, value] of step.searchParams)
20
+ current.searchParams.set(key, value);
21
+ if (step.hash)
22
+ current.hash = step.hash;
23
+ return `${current.pathname}${current.search}${current.hash}`;
24
+ }
25
+ function normalizePathname(pathname) {
26
+ return pathname.length > 1 ? pathname.replace(/\/$/, "") : pathname;
27
+ }
@@ -0,0 +1,3 @@
1
+ import type { WalkthroughProgress } from "./walkthroughTypes.js";
2
+ export declare function readWalkthroughProgress(storageKey: string): WalkthroughProgress | null;
3
+ export declare function writeWalkthroughProgress(storageKey: string, progress: WalkthroughProgress | null): void;
@@ -0,0 +1,54 @@
1
+ import { readStoredValue, removeStoredValue, writeStoredValue } from "../../../lib/browser-storage.js";
2
+ export function readWalkthroughProgress(storageKey) {
3
+ const raw = readStoredValue(storageKey);
4
+ if (!raw)
5
+ return null;
6
+ try {
7
+ const parsed = JSON.parse(raw);
8
+ return isWalkthroughProgress(parsed) ? parsed : null;
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export function writeWalkthroughProgress(storageKey, progress) {
15
+ if (!progress) {
16
+ removeStoredValue(storageKey);
17
+ return;
18
+ }
19
+ writeStoredValue(storageKey, JSON.stringify(progress));
20
+ }
21
+ function isWalkthroughProgress(value) {
22
+ if (!value || typeof value !== "object")
23
+ return false;
24
+ if (!hasWalkthroughProgressFields(value))
25
+ return false;
26
+ if (!isWalkthroughStatus(value.status))
27
+ return false;
28
+ if (!isNonnegativeInteger(value.stepIndex))
29
+ return false;
30
+ if (!isPositiveInteger(value.version))
31
+ return false;
32
+ return isNonemptyString(value.walkthroughId);
33
+ }
34
+ function isWalkthroughStatus(value) {
35
+ return value === "active" || value === "completed" || value === "paused";
36
+ }
37
+ function hasWalkthroughProgressFields(value) {
38
+ if (!("status" in value))
39
+ return false;
40
+ if (!("stepIndex" in value))
41
+ return false;
42
+ if (!("version" in value))
43
+ return false;
44
+ return "walkthroughId" in value;
45
+ }
46
+ function isNonnegativeInteger(value) {
47
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
48
+ }
49
+ function isPositiveInteger(value) {
50
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
51
+ }
52
+ function isNonemptyString(value) {
53
+ return typeof value === "string" && value.length > 0;
54
+ }
@@ -0,0 +1,32 @@
1
+ export type WalkthroughAdvance = {
2
+ type: "manual";
3
+ } | {
4
+ type: "target-click";
5
+ } | {
6
+ event: string;
7
+ type: "event";
8
+ };
9
+ export type WalkthroughStep = {
10
+ advance: WalkthroughAdvance;
11
+ body: string;
12
+ href: string;
13
+ id: string;
14
+ restoreTarget?: string;
15
+ target: string;
16
+ title: string;
17
+ };
18
+ export type WalkthroughDefinition = {
19
+ description: string;
20
+ freshStart?: string;
21
+ id: string;
22
+ steps: readonly WalkthroughStep[];
23
+ title: string;
24
+ version: number;
25
+ };
26
+ export type WalkthroughProgress = {
27
+ status: "active" | "completed" | "paused";
28
+ stepIndex: number;
29
+ version: number;
30
+ walkthroughId: string;
31
+ };
32
+ export type WalkthroughFreshStartHandlers = Readonly<Partial<Record<string, () => Promise<unknown>>>>;
@@ -0,0 +1,2 @@
1
+ import type { WalkthroughStep } from "../runtime/walkthroughTypes.js";
2
+ export declare function listenForOutsideWalkthroughInteraction(step: WalkthroughStep, pause: () => void): () => void;