@flamingo-stack/openframe-frontend-core 0.0.679 → 0.0.680-snapshot.20260929023516
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/dist/{chunk-OV6YSKMP.cjs → chunk-2UTUZGMF.cjs} +17 -8
- package/dist/chunk-2UTUZGMF.cjs.map +1 -0
- package/dist/{chunk-25Y3UR3R.cjs → chunk-3YWZNB2S.cjs} +3 -3
- package/dist/{chunk-25Y3UR3R.cjs.map → chunk-3YWZNB2S.cjs.map} +1 -1
- package/dist/{chunk-UKZAW4FE.js → chunk-4DA64VEW.js} +3 -3
- package/dist/{chunk-HA3SBMR2.cjs → chunk-4NUKFUPW.cjs} +7 -7
- package/dist/{chunk-HA3SBMR2.cjs.map → chunk-4NUKFUPW.cjs.map} +1 -1
- package/dist/{chunk-TKL5JKZS.cjs → chunk-757VTCIH.cjs} +6 -6
- package/dist/{chunk-TKL5JKZS.cjs.map → chunk-757VTCIH.cjs.map} +1 -1
- package/dist/{chunk-ICGGFQQU.js → chunk-7OZ5P7Z2.js} +4 -4
- package/dist/{chunk-QU7SDFKO.cjs → chunk-AHXAD26W.cjs} +61 -61
- package/dist/{chunk-QU7SDFKO.cjs.map → chunk-AHXAD26W.cjs.map} +1 -1
- package/dist/{chunk-CTF5IQQZ.js → chunk-BGHQW5D5.js} +17 -8
- package/dist/chunk-BGHQW5D5.js.map +1 -0
- package/dist/{chunk-UL2TY6GY.cjs → chunk-BOM6MMWB.cjs} +3 -3
- package/dist/{chunk-UL2TY6GY.cjs.map → chunk-BOM6MMWB.cjs.map} +1 -1
- package/dist/{chunk-CDMZOXS7.js → chunk-C5RRTWYW.js} +19 -13
- package/dist/{chunk-CDMZOXS7.js.map → chunk-C5RRTWYW.js.map} +1 -1
- package/dist/{chunk-EEL4RGHX.js → chunk-D5QSGV3I.js} +2 -2
- package/dist/{chunk-6LN7ZNHT.js → chunk-DIWJ6NBT.js} +2 -2
- package/dist/{chunk-IPZCEAAH.js → chunk-EWZPMPQP.js} +2 -2
- package/dist/{chunk-CY3XHQM3.cjs → chunk-F7EZBJTL.cjs} +3 -3
- package/dist/{chunk-CY3XHQM3.cjs.map → chunk-F7EZBJTL.cjs.map} +1 -1
- package/dist/{chunk-UAPSU6YW.cjs → chunk-FRXIDN6T.cjs} +3 -3
- package/dist/{chunk-UAPSU6YW.cjs.map → chunk-FRXIDN6T.cjs.map} +1 -1
- package/dist/{chunk-XSSYG7KU.js → chunk-JV7SNSYT.js} +5 -5
- package/dist/{chunk-NQCID4VF.js → chunk-KLLH632Q.js} +7 -7
- package/dist/{chunk-HTNUTEXN.cjs → chunk-KQXKQ4PX.cjs} +17 -17
- package/dist/{chunk-HTNUTEXN.cjs.map → chunk-KQXKQ4PX.cjs.map} +1 -1
- package/dist/{chunk-3LW6AJMJ.cjs → chunk-KSDOCJ7B.cjs} +45 -45
- package/dist/{chunk-3LW6AJMJ.cjs.map → chunk-KSDOCJ7B.cjs.map} +1 -1
- package/dist/{chunk-XRQKJ22I.cjs → chunk-NMXL34Z2.cjs} +4 -4
- package/dist/{chunk-XRQKJ22I.cjs.map → chunk-NMXL34Z2.cjs.map} +1 -1
- package/dist/{chunk-FN7DMNA4.js → chunk-Q76YVNCQ.js} +4 -4
- package/dist/chunk-Q76YVNCQ.js.map +1 -0
- package/dist/{chunk-KNPUHPIF.cjs → chunk-QFGPSBAX.cjs} +33 -33
- package/dist/{chunk-KNPUHPIF.cjs.map → chunk-QFGPSBAX.cjs.map} +1 -1
- package/dist/{chunk-M7ILWHDA.js → chunk-RF2EYX3B.js} +2 -2
- package/dist/{chunk-CGYFCEHM.cjs → chunk-RNRAPL6V.cjs} +13 -13
- package/dist/{chunk-CGYFCEHM.cjs.map → chunk-RNRAPL6V.cjs.map} +1 -1
- package/dist/{chunk-GZIYE35W.cjs → chunk-RSDTLURW.cjs} +30 -24
- package/dist/chunk-RSDTLURW.cjs.map +1 -0
- package/dist/{chunk-L4NLXAET.js → chunk-S3OS6FIM.js} +3 -3
- package/dist/{chunk-RJYYGWJL.cjs → chunk-SJC2XUTE.cjs} +21 -21
- package/dist/{chunk-RJYYGWJL.cjs.map → chunk-SJC2XUTE.cjs.map} +1 -1
- package/dist/{chunk-4ZKZSWX4.js → chunk-ULOUTQBX.js} +3 -3
- package/dist/{chunk-ZB34GLOZ.cjs → chunk-UOZXREDM.cjs} +44 -44
- package/dist/{chunk-ZB34GLOZ.cjs.map → chunk-UOZXREDM.cjs.map} +1 -1
- package/dist/{chunk-2YMFBKYR.js → chunk-V4L2MYXY.js} +2 -2
- package/dist/{chunk-HLMMG6Q7.js → chunk-VKTQQ7TS.js} +7 -7
- package/dist/{chunk-ZRN244JV.js → chunk-WTNFYZB6.js} +2 -2
- package/dist/{chunk-FUBOKAYO.js → chunk-YC7DIARJ.js} +4 -4
- package/dist/{chunk-7WGUMITJ.js → chunk-YDJBSKLB.js} +3 -3
- package/dist/{chunk-MGSWL3L3.cjs → chunk-YFU7BULM.cjs} +23 -23
- package/dist/{chunk-MGSWL3L3.cjs.map → chunk-YFU7BULM.cjs.map} +1 -1
- package/dist/{chunk-H27EHTLX.cjs → chunk-ZFQ3ZHMR.cjs} +3 -3
- package/dist/{chunk-H27EHTLX.cjs.map → chunk-ZFQ3ZHMR.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +13 -13
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/empty-state.d.ts.map +1 -1
- package/dist/components/faq/index.cjs +4 -4
- package/dist/components/faq/index.js +3 -3
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/help-center-pages/index.cjs +54 -54
- package/dist/components/help-center-pages/index.js +16 -16
- package/dist/components/index.cjs +35 -35
- package/dist/components/index.js +15 -15
- package/dist/components/meeting-scheduler/index.cjs +33 -33
- package/dist/components/meeting-scheduler/index.js +4 -4
- package/dist/components/navigation/index.cjs +6 -6
- package/dist/components/navigation/index.js +5 -5
- package/dist/components/onboarding-guides/index.cjs +6 -6
- package/dist/components/onboarding-guides/index.js +5 -5
- package/dist/components/shared/onboarding/onboarding-walkthrough.d.ts +9 -1
- package/dist/components/shared/onboarding/onboarding-walkthrough.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +8 -8
- package/dist/components/tickets/index.js +7 -7
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/hooks/index.cjs +2 -2
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/ui/use-onboarding-state.d.ts +1 -0
- package/dist/hooks/ui/use-onboarding-state.d.ts.map +1 -1
- package/dist/index.cjs +7 -7
- package/dist/index.js +6 -6
- package/dist/utils/onboarding-storage.d.ts +5 -0
- package/dist/utils/onboarding-storage.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/empty-state-cta.test.tsx +31 -0
- package/src/components/empty-state.tsx +5 -6
- package/src/components/shared/onboarding/onboarding-walkthrough.tsx +26 -10
- package/src/hooks/ui/use-onboarding-state.ts +8 -6
- package/src/utils/__tests__/onboarding-storage.test.ts +42 -0
- package/src/utils/onboarding-storage.ts +15 -1
- package/dist/chunk-CTF5IQQZ.js.map +0 -1
- package/dist/chunk-FN7DMNA4.js.map +0 -1
- package/dist/chunk-GZIYE35W.cjs.map +0 -1
- package/dist/chunk-OV6YSKMP.cjs.map +0 -1
- /package/dist/{chunk-UKZAW4FE.js.map → chunk-4DA64VEW.js.map} +0 -0
- /package/dist/{chunk-ICGGFQQU.js.map → chunk-7OZ5P7Z2.js.map} +0 -0
- /package/dist/{chunk-EEL4RGHX.js.map → chunk-D5QSGV3I.js.map} +0 -0
- /package/dist/{chunk-6LN7ZNHT.js.map → chunk-DIWJ6NBT.js.map} +0 -0
- /package/dist/{chunk-IPZCEAAH.js.map → chunk-EWZPMPQP.js.map} +0 -0
- /package/dist/{chunk-XSSYG7KU.js.map → chunk-JV7SNSYT.js.map} +0 -0
- /package/dist/{chunk-NQCID4VF.js.map → chunk-KLLH632Q.js.map} +0 -0
- /package/dist/{chunk-M7ILWHDA.js.map → chunk-RF2EYX3B.js.map} +0 -0
- /package/dist/{chunk-L4NLXAET.js.map → chunk-S3OS6FIM.js.map} +0 -0
- /package/dist/{chunk-4ZKZSWX4.js.map → chunk-ULOUTQBX.js.map} +0 -0
- /package/dist/{chunk-2YMFBKYR.js.map → chunk-V4L2MYXY.js.map} +0 -0
- /package/dist/{chunk-HLMMG6Q7.js.map → chunk-VKTQQ7TS.js.map} +0 -0
- /package/dist/{chunk-ZRN244JV.js.map → chunk-WTNFYZB6.js.map} +0 -0
- /package/dist/{chunk-FUBOKAYO.js.map → chunk-YC7DIARJ.js.map} +0 -0
- /package/dist/{chunk-7WGUMITJ.js.map → chunk-YDJBSKLB.js.map} +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EmptyState`'s call to action is the house `Button` in its own variant, never a
|
|
3
|
+
* default accent Button repainted with classes: the repaint lost to the accent hover
|
|
4
|
+
* (green text on a green button). Each case is compared with the Button it must be.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { render, screen } from '@testing-library/react';
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { EmptyState } from '../empty-state';
|
|
10
|
+
import { Button } from '../ui/button';
|
|
11
|
+
|
|
12
|
+
function classOf(name: string): string {
|
|
13
|
+
return (screen.getByText(name).closest('a, button') as HTMLElement).className;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('EmptyState call to action', () => {
|
|
17
|
+
it.each([
|
|
18
|
+
['secondary', 'outline'],
|
|
19
|
+
['primary', 'accent'],
|
|
20
|
+
] as const)('a %s CTA renders exactly the %s Button', (ctaVariant, buttonVariant) => {
|
|
21
|
+
render(
|
|
22
|
+
<>
|
|
23
|
+
<EmptyState type="generic" ctaText="Record it" ctaHref="/record" ctaVariant={ctaVariant} />
|
|
24
|
+
<Button href="/reference" variant={buttonVariant} className="w-full">
|
|
25
|
+
Reference
|
|
26
|
+
</Button>
|
|
27
|
+
</>,
|
|
28
|
+
);
|
|
29
|
+
expect(classOf('Record it')).toBe(classOf('Reference'));
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -159,10 +159,9 @@ export function EmptyState({
|
|
|
159
159
|
// page with a link CTA must never enter that branch. Existing callers
|
|
160
160
|
// without ctaHref keep today's smart-default behavior unchanged.
|
|
161
161
|
const smartCTA = ctaHref ? null : getSmartCTA();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
: 'w-full bg-transparent border border-ods-border text-ods-text-primary hover:border-ods-accent hover:text-ods-accent transition-all duration-150 font-body font-medium';
|
|
162
|
+
// The CTA is the house Button in its own variant: painting colours over the
|
|
163
|
+
// default accent Button with classes lost to its hover (green text on green).
|
|
164
|
+
const ctaButtonVariant = ctaVariant === 'primary' ? 'accent' : 'outline';
|
|
166
165
|
|
|
167
166
|
return (
|
|
168
167
|
<div className="flex flex-col items-center justify-center px-6 py-6 text-center md:py-16">
|
|
@@ -184,7 +183,7 @@ export function EmptyState({
|
|
|
184
183
|
{/* Link CTA (ctaHref) — SSR-safe, no path sniffing */}
|
|
185
184
|
{showCTA && ctaHref && (
|
|
186
185
|
<div className="mb-3 w-full max-w-xs">
|
|
187
|
-
<Button href={ctaHref}
|
|
186
|
+
<Button href={ctaHref} variant={ctaButtonVariant} className="w-full">
|
|
188
187
|
{ctaText || 'Contact us'}
|
|
189
188
|
</Button>
|
|
190
189
|
</div>
|
|
@@ -193,7 +192,7 @@ export function EmptyState({
|
|
|
193
192
|
{/* Smart CTA Button */}
|
|
194
193
|
{showCTA && !ctaHref && smartCTA && (
|
|
195
194
|
<div className="mb-3 w-full max-w-xs">
|
|
196
|
-
<Button onClick={smartCTA.action}
|
|
195
|
+
<Button onClick={smartCTA.action} variant={ctaButtonVariant} className="w-full">
|
|
197
196
|
{smartCTA.text}
|
|
198
197
|
</Button>
|
|
199
198
|
</div>
|
|
@@ -9,6 +9,14 @@ import { OnboardingStepCard } from './onboarding-step-card';
|
|
|
9
9
|
|
|
10
10
|
export interface OnboardingWalkthroughProps {
|
|
11
11
|
steps: OnboardingStepConfig[];
|
|
12
|
+
/** Heading above the steps. Defaults to "Get Started". */
|
|
13
|
+
title?: string;
|
|
14
|
+
/**
|
|
15
|
+
* What to render once the walkthrough is dismissed. Receives `restart`,
|
|
16
|
+
* which clears every completed/skipped step and shows the walkthrough
|
|
17
|
+
* again. Omitted: a dismissed walkthrough renders nothing.
|
|
18
|
+
*/
|
|
19
|
+
renderDismissed?: (restart: () => void) => React.ReactNode;
|
|
12
20
|
onDismiss?: () => void;
|
|
13
21
|
storageKey?: string;
|
|
14
22
|
className?: string;
|
|
@@ -26,6 +34,8 @@ export interface OnboardingWalkthroughProps {
|
|
|
26
34
|
|
|
27
35
|
export function OnboardingWalkthrough({
|
|
28
36
|
steps,
|
|
37
|
+
title = 'Get Started',
|
|
38
|
+
renderDismissed,
|
|
29
39
|
onDismiss,
|
|
30
40
|
storageKey = 'openframe-onboarding-state',
|
|
31
41
|
className,
|
|
@@ -38,6 +48,7 @@ export function OnboardingWalkthrough({
|
|
|
38
48
|
markComplete,
|
|
39
49
|
markSkipped,
|
|
40
50
|
dismissOnboarding,
|
|
51
|
+
resetOnboarding,
|
|
41
52
|
isStepComplete,
|
|
42
53
|
isStepSkipped,
|
|
43
54
|
allStepsComplete,
|
|
@@ -63,8 +74,11 @@ export function OnboardingWalkthrough({
|
|
|
63
74
|
// Skip if no completion status
|
|
64
75
|
if (!completionStatus) return;
|
|
65
76
|
|
|
66
|
-
// Create a stable key from completion status to detect actual changes
|
|
67
|
-
|
|
77
|
+
// Create a stable key from completion status to detect actual changes.
|
|
78
|
+
// An empty state (first load, or after a restart) is keyed by when it was
|
|
79
|
+
// written, so a restart applies the same status again.
|
|
80
|
+
const isFreshState = state.completedSteps.length === 0 && state.skippedSteps.length === 0;
|
|
81
|
+
const statusKey = `${JSON.stringify(completionStatus)}|${isFreshState ? state.lastUpdated : ''}`;
|
|
68
82
|
|
|
69
83
|
// Skip if we've already processed this exact status
|
|
70
84
|
if (lastCompletionStatusRef.current === statusKey) return;
|
|
@@ -86,7 +100,6 @@ export function OnboardingWalkthrough({
|
|
|
86
100
|
// Mark as in progress to prevent concurrent calls
|
|
87
101
|
autoMarkingInProgressRef.current = true;
|
|
88
102
|
|
|
89
|
-
console.log('📊 Auto-marking steps as complete from hook data:', completedStepIds);
|
|
90
103
|
markMultipleComplete(completedStepIds);
|
|
91
104
|
|
|
92
105
|
// Update the last processed status key
|
|
@@ -101,14 +114,20 @@ export function OnboardingWalkthrough({
|
|
|
101
114
|
// Even if no steps to mark, update the status key to prevent re-processing
|
|
102
115
|
lastCompletionStatusRef.current = statusKey;
|
|
103
116
|
}
|
|
104
|
-
}, [
|
|
117
|
+
}, [
|
|
118
|
+
completionStatus,
|
|
119
|
+
isLoadingCompletion,
|
|
120
|
+
state.completedSteps,
|
|
121
|
+
state.skippedSteps,
|
|
122
|
+
state.lastUpdated,
|
|
123
|
+
markMultipleComplete,
|
|
124
|
+
]);
|
|
105
125
|
|
|
106
126
|
// useCallback must be called unconditionally (before any early returns)
|
|
107
127
|
const handleStepAction = useCallback(
|
|
108
128
|
async (step: OnboardingStepConfig) => {
|
|
109
129
|
// Prevent duplicate action handling for the same step
|
|
110
130
|
if (actionInProgressRef.current.has(step.id)) {
|
|
111
|
-
console.log(`⏳ Action already in progress for "${step.id}", skipping`);
|
|
112
131
|
return;
|
|
113
132
|
}
|
|
114
133
|
|
|
@@ -120,13 +139,11 @@ export function OnboardingWalkthrough({
|
|
|
120
139
|
|
|
121
140
|
// Skip if already completed (could have been auto-marked while action was running)
|
|
122
141
|
if (state.completedSteps.includes(step.id)) {
|
|
123
|
-
console.log(`✓ Step "${step.id}" already completed, skipping mark`);
|
|
124
142
|
return;
|
|
125
143
|
}
|
|
126
144
|
|
|
127
145
|
// Auto-mark Knowledge Base as complete (no verification needed)
|
|
128
146
|
if (step.id === 'knowledge-base') {
|
|
129
|
-
console.log(`🎯 Auto-completing Knowledge Base`);
|
|
130
147
|
markComplete(step.id);
|
|
131
148
|
return;
|
|
132
149
|
}
|
|
@@ -135,7 +152,6 @@ export function OnboardingWalkthrough({
|
|
|
135
152
|
if (step.checkComplete) {
|
|
136
153
|
try {
|
|
137
154
|
const isComplete = await step.checkComplete();
|
|
138
|
-
console.log(`🔍 Post-action check for "${step.id}": isComplete=${isComplete}`);
|
|
139
155
|
|
|
140
156
|
// Double-check it wasn't completed while we were checking
|
|
141
157
|
if (isComplete && !state.completedSteps.includes(step.id)) {
|
|
@@ -166,7 +182,7 @@ export function OnboardingWalkthrough({
|
|
|
166
182
|
// ALL hooks have been called above - now safe to do conditional returns
|
|
167
183
|
// Don't render if dismissed
|
|
168
184
|
if (state.dismissed) {
|
|
169
|
-
return null;
|
|
185
|
+
return renderDismissed ? <>{renderDismissed(resetOnboarding)}</> : null;
|
|
170
186
|
}
|
|
171
187
|
|
|
172
188
|
const isAllComplete = allStepsComplete(steps);
|
|
@@ -175,7 +191,7 @@ export function OnboardingWalkthrough({
|
|
|
175
191
|
<div className={cn('w-full space-y-4', className)}>
|
|
176
192
|
{/* Header - responsive: stacks on mobile */}
|
|
177
193
|
<div className="flex flex-col items-start justify-between gap-3 md:flex-row md:items-center md:gap-0">
|
|
178
|
-
<h2 className={SECTION_HEADING_CLASS}>
|
|
194
|
+
<h2 className={SECTION_HEADING_CLASS}>{title}</h2>
|
|
179
195
|
|
|
180
196
|
{isLoadingCompletion ? (
|
|
181
197
|
<div className="h-[48px] w-full animate-pulse rounded-[6px] bg-ods-border md:w-[160px]" />
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
markStepSkipped as storageMarkSkipped,
|
|
9
9
|
dismissOnboarding as storageDismiss,
|
|
10
10
|
markMultipleComplete as storageMarkMultiple,
|
|
11
|
+
resetOnboardingState as storageReset,
|
|
11
12
|
type OnboardingState,
|
|
12
13
|
} from '../../utils/onboarding-storage';
|
|
13
14
|
import { readLocalStorageUpdateDetail } from '../../utils/storage-event';
|
|
@@ -42,7 +43,6 @@ export function useOnboardingState(storageKey: string = 'openframe-onboarding-st
|
|
|
42
43
|
const newState = loadOnboardingState(storageKey);
|
|
43
44
|
setState(newState);
|
|
44
45
|
forceUpdate(prev => prev + 1);
|
|
45
|
-
console.log('🔄 State updated from storage event:', newState);
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
window.addEventListener('localStorageUpdate', handleStorageUpdate);
|
|
@@ -53,7 +53,6 @@ export function useOnboardingState(storageKey: string = 'openframe-onboarding-st
|
|
|
53
53
|
|
|
54
54
|
const markComplete = useCallback(
|
|
55
55
|
(stepId: string) => {
|
|
56
|
-
console.log(`🎯 markComplete called for: "${stepId}"`);
|
|
57
56
|
const newState = storageMarkComplete(storageKey, stepId);
|
|
58
57
|
setState(newState);
|
|
59
58
|
forceUpdate(prev => prev + 1);
|
|
@@ -63,7 +62,6 @@ export function useOnboardingState(storageKey: string = 'openframe-onboarding-st
|
|
|
63
62
|
|
|
64
63
|
const markSkipped = useCallback(
|
|
65
64
|
(stepId: string) => {
|
|
66
|
-
console.log(`⏭️ markSkipped called for: "${stepId}"`);
|
|
67
65
|
const newState = storageMarkSkipped(storageKey, stepId);
|
|
68
66
|
setState(newState);
|
|
69
67
|
forceUpdate(prev => prev + 1);
|
|
@@ -72,19 +70,22 @@ export function useOnboardingState(storageKey: string = 'openframe-onboarding-st
|
|
|
72
70
|
);
|
|
73
71
|
|
|
74
72
|
const dismissOnboarding = useCallback(() => {
|
|
75
|
-
console.log(`🚫 dismissOnboarding called`);
|
|
76
73
|
const newState = storageDismiss(storageKey);
|
|
77
74
|
setState(newState);
|
|
78
75
|
forceUpdate(prev => prev + 1);
|
|
79
76
|
}, [storageKey]);
|
|
80
77
|
|
|
78
|
+
const resetOnboarding = useCallback(() => {
|
|
79
|
+
const newState = storageReset(storageKey);
|
|
80
|
+
setState(newState);
|
|
81
|
+
forceUpdate(prev => prev + 1);
|
|
82
|
+
}, [storageKey]);
|
|
83
|
+
|
|
81
84
|
const markMultipleComplete = useCallback(
|
|
82
85
|
(stepIds: string[]) => {
|
|
83
|
-
console.log(`🎯 markMultipleComplete called for:`, stepIds);
|
|
84
86
|
const newState = storageMarkMultiple(storageKey, stepIds);
|
|
85
87
|
setState(newState);
|
|
86
88
|
forceUpdate(prev => prev + 1);
|
|
87
|
-
console.log(`📝 State after batch:`, newState);
|
|
88
89
|
},
|
|
89
90
|
[storageKey],
|
|
90
91
|
);
|
|
@@ -115,6 +116,7 @@ export function useOnboardingState(storageKey: string = 'openframe-onboarding-st
|
|
|
115
116
|
markComplete,
|
|
116
117
|
markSkipped,
|
|
117
118
|
dismissOnboarding,
|
|
119
|
+
resetOnboarding,
|
|
118
120
|
isStepComplete,
|
|
119
121
|
isStepSkipped,
|
|
120
122
|
allStepsComplete,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reset that brings a dismissed walkthrough back, round-tripped through
|
|
3
|
+
* real localStorage: reading the state back is the only proof the write stuck.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
dismissOnboarding,
|
|
8
|
+
loadOnboardingState,
|
|
9
|
+
markStepComplete,
|
|
10
|
+
markStepSkipped,
|
|
11
|
+
resetOnboardingState,
|
|
12
|
+
} from '../onboarding-storage';
|
|
13
|
+
|
|
14
|
+
const KEY = 'onboarding-storage-test';
|
|
15
|
+
|
|
16
|
+
afterEach(() => localStorage.clear());
|
|
17
|
+
|
|
18
|
+
describe('resetOnboardingState', () => {
|
|
19
|
+
it('clears completed and skipped steps and undoes a dismissal', () => {
|
|
20
|
+
markStepComplete(KEY, 'install');
|
|
21
|
+
markStepSkipped(KEY, 'video');
|
|
22
|
+
dismissOnboarding(KEY);
|
|
23
|
+
expect(loadOnboardingState(KEY).dismissed).toBe(true);
|
|
24
|
+
|
|
25
|
+
const reset = resetOnboardingState(KEY);
|
|
26
|
+
|
|
27
|
+
expect(reset).toMatchObject({ completedSteps: [], skippedSteps: [], dismissed: false });
|
|
28
|
+
expect(loadOnboardingState(KEY)).toMatchObject({ completedSteps: [], skippedSteps: [], dismissed: false });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('tells other hook instances through the localStorageUpdate event', () => {
|
|
32
|
+
const seen: string[] = [];
|
|
33
|
+
const listener = (event: Event) => seen.push((event as CustomEvent<{ key: string }>).detail.key);
|
|
34
|
+
window.addEventListener('localStorageUpdate', listener);
|
|
35
|
+
try {
|
|
36
|
+
resetOnboardingState(KEY);
|
|
37
|
+
} finally {
|
|
38
|
+
window.removeEventListener('localStorageUpdate', listener);
|
|
39
|
+
}
|
|
40
|
+
expect(seen).toEqual([KEY]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -26,7 +26,6 @@ export function saveOnboardingState(key: string, state: OnboardingState): void {
|
|
|
26
26
|
|
|
27
27
|
try {
|
|
28
28
|
localStorage.setItem(key, JSON.stringify(state));
|
|
29
|
-
console.log('💾 Saved onboarding state:', state);
|
|
30
29
|
|
|
31
30
|
// Dispatch custom event for cross-tab sync
|
|
32
31
|
if (typeof window !== 'undefined') {
|
|
@@ -110,6 +109,21 @@ export function dismissOnboarding(key: string): OnboardingState {
|
|
|
110
109
|
return newState;
|
|
111
110
|
}
|
|
112
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Reset the walkthrough to its initial state: no step completed or skipped,
|
|
114
|
+
* not dismissed. How a dismissed walkthrough is brought back.
|
|
115
|
+
*/
|
|
116
|
+
export function resetOnboardingState(key: string): OnboardingState {
|
|
117
|
+
const newState: OnboardingState = {
|
|
118
|
+
completedSteps: [],
|
|
119
|
+
skippedSteps: [],
|
|
120
|
+
dismissed: false,
|
|
121
|
+
lastUpdated: new Date().toISOString(),
|
|
122
|
+
};
|
|
123
|
+
saveOnboardingState(key, newState);
|
|
124
|
+
return newState;
|
|
125
|
+
}
|
|
126
|
+
|
|
113
127
|
/**
|
|
114
128
|
* Check if a step is complete
|
|
115
129
|
*/
|