@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.
Files changed (120) hide show
  1. package/dist/{chunk-OV6YSKMP.cjs → chunk-2UTUZGMF.cjs} +17 -8
  2. package/dist/chunk-2UTUZGMF.cjs.map +1 -0
  3. package/dist/{chunk-25Y3UR3R.cjs → chunk-3YWZNB2S.cjs} +3 -3
  4. package/dist/{chunk-25Y3UR3R.cjs.map → chunk-3YWZNB2S.cjs.map} +1 -1
  5. package/dist/{chunk-UKZAW4FE.js → chunk-4DA64VEW.js} +3 -3
  6. package/dist/{chunk-HA3SBMR2.cjs → chunk-4NUKFUPW.cjs} +7 -7
  7. package/dist/{chunk-HA3SBMR2.cjs.map → chunk-4NUKFUPW.cjs.map} +1 -1
  8. package/dist/{chunk-TKL5JKZS.cjs → chunk-757VTCIH.cjs} +6 -6
  9. package/dist/{chunk-TKL5JKZS.cjs.map → chunk-757VTCIH.cjs.map} +1 -1
  10. package/dist/{chunk-ICGGFQQU.js → chunk-7OZ5P7Z2.js} +4 -4
  11. package/dist/{chunk-QU7SDFKO.cjs → chunk-AHXAD26W.cjs} +61 -61
  12. package/dist/{chunk-QU7SDFKO.cjs.map → chunk-AHXAD26W.cjs.map} +1 -1
  13. package/dist/{chunk-CTF5IQQZ.js → chunk-BGHQW5D5.js} +17 -8
  14. package/dist/chunk-BGHQW5D5.js.map +1 -0
  15. package/dist/{chunk-UL2TY6GY.cjs → chunk-BOM6MMWB.cjs} +3 -3
  16. package/dist/{chunk-UL2TY6GY.cjs.map → chunk-BOM6MMWB.cjs.map} +1 -1
  17. package/dist/{chunk-CDMZOXS7.js → chunk-C5RRTWYW.js} +19 -13
  18. package/dist/{chunk-CDMZOXS7.js.map → chunk-C5RRTWYW.js.map} +1 -1
  19. package/dist/{chunk-EEL4RGHX.js → chunk-D5QSGV3I.js} +2 -2
  20. package/dist/{chunk-6LN7ZNHT.js → chunk-DIWJ6NBT.js} +2 -2
  21. package/dist/{chunk-IPZCEAAH.js → chunk-EWZPMPQP.js} +2 -2
  22. package/dist/{chunk-CY3XHQM3.cjs → chunk-F7EZBJTL.cjs} +3 -3
  23. package/dist/{chunk-CY3XHQM3.cjs.map → chunk-F7EZBJTL.cjs.map} +1 -1
  24. package/dist/{chunk-UAPSU6YW.cjs → chunk-FRXIDN6T.cjs} +3 -3
  25. package/dist/{chunk-UAPSU6YW.cjs.map → chunk-FRXIDN6T.cjs.map} +1 -1
  26. package/dist/{chunk-XSSYG7KU.js → chunk-JV7SNSYT.js} +5 -5
  27. package/dist/{chunk-NQCID4VF.js → chunk-KLLH632Q.js} +7 -7
  28. package/dist/{chunk-HTNUTEXN.cjs → chunk-KQXKQ4PX.cjs} +17 -17
  29. package/dist/{chunk-HTNUTEXN.cjs.map → chunk-KQXKQ4PX.cjs.map} +1 -1
  30. package/dist/{chunk-3LW6AJMJ.cjs → chunk-KSDOCJ7B.cjs} +45 -45
  31. package/dist/{chunk-3LW6AJMJ.cjs.map → chunk-KSDOCJ7B.cjs.map} +1 -1
  32. package/dist/{chunk-XRQKJ22I.cjs → chunk-NMXL34Z2.cjs} +4 -4
  33. package/dist/{chunk-XRQKJ22I.cjs.map → chunk-NMXL34Z2.cjs.map} +1 -1
  34. package/dist/{chunk-FN7DMNA4.js → chunk-Q76YVNCQ.js} +4 -4
  35. package/dist/chunk-Q76YVNCQ.js.map +1 -0
  36. package/dist/{chunk-KNPUHPIF.cjs → chunk-QFGPSBAX.cjs} +33 -33
  37. package/dist/{chunk-KNPUHPIF.cjs.map → chunk-QFGPSBAX.cjs.map} +1 -1
  38. package/dist/{chunk-M7ILWHDA.js → chunk-RF2EYX3B.js} +2 -2
  39. package/dist/{chunk-CGYFCEHM.cjs → chunk-RNRAPL6V.cjs} +13 -13
  40. package/dist/{chunk-CGYFCEHM.cjs.map → chunk-RNRAPL6V.cjs.map} +1 -1
  41. package/dist/{chunk-GZIYE35W.cjs → chunk-RSDTLURW.cjs} +30 -24
  42. package/dist/chunk-RSDTLURW.cjs.map +1 -0
  43. package/dist/{chunk-L4NLXAET.js → chunk-S3OS6FIM.js} +3 -3
  44. package/dist/{chunk-RJYYGWJL.cjs → chunk-SJC2XUTE.cjs} +21 -21
  45. package/dist/{chunk-RJYYGWJL.cjs.map → chunk-SJC2XUTE.cjs.map} +1 -1
  46. package/dist/{chunk-4ZKZSWX4.js → chunk-ULOUTQBX.js} +3 -3
  47. package/dist/{chunk-ZB34GLOZ.cjs → chunk-UOZXREDM.cjs} +44 -44
  48. package/dist/{chunk-ZB34GLOZ.cjs.map → chunk-UOZXREDM.cjs.map} +1 -1
  49. package/dist/{chunk-2YMFBKYR.js → chunk-V4L2MYXY.js} +2 -2
  50. package/dist/{chunk-HLMMG6Q7.js → chunk-VKTQQ7TS.js} +7 -7
  51. package/dist/{chunk-ZRN244JV.js → chunk-WTNFYZB6.js} +2 -2
  52. package/dist/{chunk-FUBOKAYO.js → chunk-YC7DIARJ.js} +4 -4
  53. package/dist/{chunk-7WGUMITJ.js → chunk-YDJBSKLB.js} +3 -3
  54. package/dist/{chunk-MGSWL3L3.cjs → chunk-YFU7BULM.cjs} +23 -23
  55. package/dist/{chunk-MGSWL3L3.cjs.map → chunk-YFU7BULM.cjs.map} +1 -1
  56. package/dist/{chunk-H27EHTLX.cjs → chunk-ZFQ3ZHMR.cjs} +3 -3
  57. package/dist/{chunk-H27EHTLX.cjs.map → chunk-ZFQ3ZHMR.cjs.map} +1 -1
  58. package/dist/components/case-studies/index.cjs +13 -13
  59. package/dist/components/case-studies/index.js +3 -3
  60. package/dist/components/contact/index.cjs +4 -4
  61. package/dist/components/contact/index.js +3 -3
  62. package/dist/components/docs/index.cjs +7 -7
  63. package/dist/components/docs/index.js +6 -6
  64. package/dist/components/embeds/index.cjs +4 -4
  65. package/dist/components/embeds/index.js +3 -3
  66. package/dist/components/empty-state.d.ts.map +1 -1
  67. package/dist/components/faq/index.cjs +4 -4
  68. package/dist/components/faq/index.js +3 -3
  69. package/dist/components/features/index.cjs +3 -3
  70. package/dist/components/features/index.js +2 -2
  71. package/dist/components/help-center-pages/index.cjs +54 -54
  72. package/dist/components/help-center-pages/index.js +16 -16
  73. package/dist/components/index.cjs +35 -35
  74. package/dist/components/index.js +15 -15
  75. package/dist/components/meeting-scheduler/index.cjs +33 -33
  76. package/dist/components/meeting-scheduler/index.js +4 -4
  77. package/dist/components/navigation/index.cjs +6 -6
  78. package/dist/components/navigation/index.js +5 -5
  79. package/dist/components/onboarding-guides/index.cjs +6 -6
  80. package/dist/components/onboarding-guides/index.js +5 -5
  81. package/dist/components/shared/onboarding/onboarding-walkthrough.d.ts +9 -1
  82. package/dist/components/shared/onboarding/onboarding-walkthrough.d.ts.map +1 -1
  83. package/dist/components/tickets/index.cjs +8 -8
  84. package/dist/components/tickets/index.js +7 -7
  85. package/dist/components/ui/index.cjs +3 -3
  86. package/dist/components/ui/index.js +2 -2
  87. package/dist/hooks/index.cjs +2 -2
  88. package/dist/hooks/index.js +1 -1
  89. package/dist/hooks/ui/use-onboarding-state.d.ts +1 -0
  90. package/dist/hooks/ui/use-onboarding-state.d.ts.map +1 -1
  91. package/dist/index.cjs +7 -7
  92. package/dist/index.js +6 -6
  93. package/dist/utils/onboarding-storage.d.ts +5 -0
  94. package/dist/utils/onboarding-storage.d.ts.map +1 -1
  95. package/package.json +1 -1
  96. package/src/components/__tests__/empty-state-cta.test.tsx +31 -0
  97. package/src/components/empty-state.tsx +5 -6
  98. package/src/components/shared/onboarding/onboarding-walkthrough.tsx +26 -10
  99. package/src/hooks/ui/use-onboarding-state.ts +8 -6
  100. package/src/utils/__tests__/onboarding-storage.test.ts +42 -0
  101. package/src/utils/onboarding-storage.ts +15 -1
  102. package/dist/chunk-CTF5IQQZ.js.map +0 -1
  103. package/dist/chunk-FN7DMNA4.js.map +0 -1
  104. package/dist/chunk-GZIYE35W.cjs.map +0 -1
  105. package/dist/chunk-OV6YSKMP.cjs.map +0 -1
  106. /package/dist/{chunk-UKZAW4FE.js.map → chunk-4DA64VEW.js.map} +0 -0
  107. /package/dist/{chunk-ICGGFQQU.js.map → chunk-7OZ5P7Z2.js.map} +0 -0
  108. /package/dist/{chunk-EEL4RGHX.js.map → chunk-D5QSGV3I.js.map} +0 -0
  109. /package/dist/{chunk-6LN7ZNHT.js.map → chunk-DIWJ6NBT.js.map} +0 -0
  110. /package/dist/{chunk-IPZCEAAH.js.map → chunk-EWZPMPQP.js.map} +0 -0
  111. /package/dist/{chunk-XSSYG7KU.js.map → chunk-JV7SNSYT.js.map} +0 -0
  112. /package/dist/{chunk-NQCID4VF.js.map → chunk-KLLH632Q.js.map} +0 -0
  113. /package/dist/{chunk-M7ILWHDA.js.map → chunk-RF2EYX3B.js.map} +0 -0
  114. /package/dist/{chunk-L4NLXAET.js.map → chunk-S3OS6FIM.js.map} +0 -0
  115. /package/dist/{chunk-4ZKZSWX4.js.map → chunk-ULOUTQBX.js.map} +0 -0
  116. /package/dist/{chunk-2YMFBKYR.js.map → chunk-V4L2MYXY.js.map} +0 -0
  117. /package/dist/{chunk-HLMMG6Q7.js.map → chunk-VKTQQ7TS.js.map} +0 -0
  118. /package/dist/{chunk-ZRN244JV.js.map → chunk-WTNFYZB6.js.map} +0 -0
  119. /package/dist/{chunk-FUBOKAYO.js.map → chunk-YC7DIARJ.js.map} +0 -0
  120. /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
- const ctaClassName =
163
- ctaVariant === 'primary'
164
- ? 'w-full bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover transition-all duration-150 font-body font-medium'
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} className={ctaClassName}>
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} className={ctaClassName}>
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
- const statusKey = JSON.stringify(completionStatus);
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
- }, [completionStatus, isLoadingCompletion, state.completedSteps, markMultipleComplete]);
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}>Get Started</h2>
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
  */