@wizzard-packages/vue 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 ADDED
@@ -0,0 +1,836 @@
1
+ # @wizzard-packages/vue ๐Ÿง™โ€โ™‚๏ธ
2
+
3
+ ![npm](https://img.shields.io/npm/v/@wizzard-packages/vue)
4
+ ![downloads](https://img.shields.io/npm/dm/@wizzard-packages/vue)
5
+ ![license](https://img.shields.io/npm/l/@wizzard-packages/vue)
6
+
7
+ Vue 3 Composition API bindings for **Wizzard Stepper**.
8
+ Build flexible, headless, and strictly typed multi-step wizards with Vue 3.
9
+
10
+ ## โœจ Features
11
+
12
+ - ๐ŸŸข **Vue 3 Native**: Built with Composition API and fine-grained reactivity.
13
+ - ๐Ÿ— **Headless**: Total control over your UI/components.
14
+ - ๐Ÿงช **Strictly Typed**: Full TypeScript support with the Factory Pattern.
15
+ - โšก **Atomic Updates**: Components re-render only when the specific data they use changes.
16
+ - ๐Ÿ”Œ **Core Powered**: Uses the same battle-tested engine as `@wizzard-packages/react`.
17
+ - ๐Ÿ”„ **Feature Parity**: All React adapter features available in Vue (validation, guards, persistence, middleware).
18
+
19
+ ## ๐Ÿ“ฆ Installation
20
+
21
+ ```bash
22
+ pnpm add @wizzard-packages/vue @wizzard-packages/core
23
+ # or
24
+ npm install @wizzard-packages/vue @wizzard-packages/core
25
+ ```
26
+
27
+ Optional add-ons:
28
+
29
+ ```bash
30
+ pnpm add @wizzard-packages/persistence @wizzard-packages/middleware
31
+ pnpm add @wizzard-packages/adapter-zod zod
32
+ pnpm add @wizzard-packages/adapter-yup yup
33
+ ```
34
+
35
+ ## ๐Ÿš€ Quick Start
36
+
37
+ ### 1. Define your Schema and Factory
38
+
39
+ Create a typed wizard instance for your data schema.
40
+
41
+ ```typescript
42
+ // wizard.ts
43
+ import { createWizardFactory } from '@wizzard-packages/vue';
44
+
45
+ export interface MySchema {
46
+ user: {
47
+ name: string;
48
+ email: string;
49
+ };
50
+ plan: 'basic' | 'pro';
51
+ }
52
+
53
+ export type StepId = 'info' | 'plan' | 'review';
54
+
55
+ export const {
56
+ useProvideWizard,
57
+ useWizardActions,
58
+ useWizardValue,
59
+ useWizardState,
60
+ useWizardField,
61
+ useWizardSelector,
62
+ useWizardError
63
+ } = createWizardFactory<MySchema>();
64
+ ```
65
+
66
+ ### 2. Provide the Wizard in a Parent Component
67
+
68
+ ```vue
69
+ <script setup lang="ts">
70
+ import { useProvideWizard } from './wizard';
71
+ import type { IWizardConfig } from '@wizzard-packages/core';
72
+
73
+ const config: IWizardConfig<'info' | 'plan' | 'review', any> = {
74
+ steps: [
75
+ { id: 'info', label: 'Personal Info' },
76
+ { id: 'plan', label: 'Select Plan' },
77
+ { id: 'review', label: 'Review' }
78
+ ]
79
+ };
80
+
81
+ useProvideWizard({
82
+ config,
83
+ initialData: {
84
+ user: { name: '', email: '' },
85
+ plan: 'basic'
86
+ }
87
+ });
88
+ </script>
89
+
90
+ <template>
91
+ <div class="wizard-container">
92
+ <StepRenderer />
93
+ <NavigationControls />
94
+ </div>
95
+ </template>
96
+ ```
97
+
98
+ ### 3. Consume State in Child Components
99
+
100
+ #### Reading and Writing Values (`useWizardField`)
101
+
102
+ ```vue
103
+ <script setup lang="ts">
104
+ import { useWizardField } from './wizard';
105
+
106
+ // Reactive [ref, setter]
107
+ const [name, setName] = useWizardField('user.name');
108
+ const [email, setEmail] = useWizardField('user.email');
109
+ </script>
110
+
111
+ <template>
112
+ <div>
113
+ <input
114
+ :value="name"
115
+ @input="e => setName((e.target as HTMLInputElement).value)"
116
+ placeholder="Enter name"
117
+ />
118
+ <input
119
+ :value="email"
120
+ @input="e => setEmail((e.target as HTMLInputElement).value)"
121
+ placeholder="Enter email"
122
+ type="email"
123
+ />
124
+ </div>
125
+ </template>
126
+ ```
127
+
128
+ #### Navigation Actions (`useWizardActions`)
129
+
130
+ ```vue
131
+ <script setup lang="ts">
132
+ import { useWizardActions, useWizardState } from './wizard';
133
+
134
+ const state = useWizardState();
135
+ const { nextStep, prevStep, goToStep } = useWizardActions();
136
+ </script>
137
+
138
+ <template>
139
+ <div class="navigation">
140
+ <button @click="prevStep" :disabled="state.isFirstStep">
141
+ Back
142
+ </button>
143
+ <span>Step {{ state.currentStepIndex + 1 }} of {{ state.totalSteps }}</span>
144
+ <button @click="nextStep" :disabled="state.isLastStep">
145
+ Next
146
+ </button>
147
+ </div>
148
+ </template>
149
+ ```
150
+
151
+ ## ๐Ÿ›  API Reference
152
+
153
+ ### `createWizardFactory<TSchema>()`
154
+
155
+ Returns a set of typed hooks for your schema:
156
+
157
+ #### `useProvideWizard(options)`
158
+
159
+ Initializes and provides the wizard store to the component tree.
160
+
161
+ ```typescript
162
+ interface IWizardProviderOptions<StepId, TSchema> {
163
+ config: IWizardConfig<StepId, TSchema>;
164
+ initialData?: TSchema;
165
+ initialStepId?: StepId;
166
+ }
167
+
168
+ useProvideWizard({
169
+ config: {
170
+ steps: [
171
+ { id: 'step1', label: 'Step 1' },
172
+ { id: 'step2', label: 'Step 2' }
173
+ ],
174
+ validationMode: 'onChange', // 'onChange' | 'onBlur' | 'onSubmit'
175
+ validationDebounceTime: 300,
176
+ },
177
+ initialData: { name: '' },
178
+ initialStepId: 'step1'
179
+ });
180
+ ```
181
+
182
+ #### `useWizardState()`
183
+
184
+ Returns a reactive computed ref with the complete wizard state:
185
+
186
+ ```typescript
187
+ const state = useWizardState();
188
+
189
+ // Available properties:
190
+ state.currentStepId // Current step ID
191
+ state.currentStepIndex // Current step index (0-based)
192
+ state.totalSteps // Total number of active steps
193
+ state.isFirstStep // Boolean: is first step
194
+ state.isLastStep // Boolean: is last step
195
+ state.canGoNext // Boolean: can navigate forward
196
+ state.canGoPrev // Boolean: can navigate backward
197
+ state.visitedSteps // Set of visited step IDs
198
+ state.completedSteps // Set of completed step IDs
199
+ state.errorSteps // Set of steps with errors
200
+ state.progress // Progress percentage (0-100)
201
+ state.isBusy // Boolean: async operation in progress
202
+ state.data // Current wizard data
203
+ state.activeSteps // Array of currently active steps
204
+ ```
205
+
206
+ #### `useWizardValue(path, defaultValue?)`
207
+
208
+ Returns a reactive ref for a specific path in the wizard data:
209
+
210
+ ```typescript
211
+ const name = useWizardValue('user.name');
212
+ const email = useWizardValue('user.email', 'default@example.com');
213
+
214
+ // Automatically updates when the value changes
215
+ console.log(name.value); // 'John'
216
+ ```
217
+
218
+ #### `useWizardField(path, defaultValue?)`
219
+
220
+ Returns `[ref, setter]` tuple for a specific field:
221
+
222
+ ```typescript
223
+ const [name, setName] = useWizardField('user.name');
224
+ const [age, setAge] = useWizardField('user.age', 18);
225
+
226
+ // Use in template
227
+ <input :value="name" @input="e => setName(e.target.value)" />
228
+
229
+ // Or with v-model workaround
230
+ const nameModel = computed({
231
+ get: () => name.value,
232
+ set: (val) => setName(val)
233
+ });
234
+
235
+ <input v-model="nameModel" />
236
+ ```
237
+
238
+ #### `useWizardActions()`
239
+
240
+ Returns navigation and data manipulation methods:
241
+
242
+ ```typescript
243
+ const actions = useWizardActions();
244
+
245
+ // Navigation
246
+ actions.nextStep() // Go to next step
247
+ actions.prevStep() // Go to previous step
248
+ actions.goToStep('stepId') // Go to specific step
249
+ actions.goToStepIndex(2) // Go to step by index
250
+
251
+ // Data manipulation
252
+ actions.setData('user.name', 'John') // Set single value
253
+ actions.updateData({ // Update multiple values
254
+ user: { name: 'John', email: 'john@example.com' }
255
+ })
256
+ actions.getData('user.name') // Get value by path
257
+ actions.getData('user.name', 'Default') // With default value
258
+
259
+ // Validation
260
+ actions.validateStep('stepId') // Validate specific step
261
+ actions.validateAll() // Validate all steps
262
+
263
+ // Error management
264
+ actions.setErrors({ // Set errors for a step
265
+ stepId: {
266
+ fieldName: 'Error message'
267
+ }
268
+ })
269
+
270
+ // Reset
271
+ actions.reset() // Reset to initial state
272
+ actions.reset({ name: 'New' }) // Reset with new data
273
+ ```
274
+
275
+ #### `useWizardSelector(selector)`
276
+
277
+ Returns a reactive computed value derived from wizard state:
278
+
279
+ ```typescript
280
+ const progress = useWizardSelector(state => state.progress);
281
+ const isValid = useWizardSelector(state =>
282
+ state.errorSteps.size === 0
283
+ );
284
+
285
+ // Complex derived state
286
+ const wizardStatus = useWizardSelector(state => ({
287
+ step: state.currentStepId,
288
+ progress: state.progress,
289
+ canProceed: state.canGoNext && !state.isBusy,
290
+ hasErrors: state.errorSteps.size > 0
291
+ }));
292
+ ```
293
+
294
+ #### `useWizardError(path)`
295
+
296
+ Returns a reactive ref with the error message for a specific field:
297
+
298
+ ```typescript
299
+ const nameError = useWizardError('user.name');
300
+ const emailError = useWizardError('user.email');
301
+
302
+ // Use in template
303
+ <div v-if="nameError" class="error">{{ nameError }}</div>
304
+ ```
305
+
306
+ ## ๐Ÿ“š Advanced Examples
307
+
308
+ ### Example 1: Step Guards (beforeLeave)
309
+
310
+ Prevent navigation until validation passes:
311
+
312
+ ```typescript
313
+ const config: IWizardConfig<StepId, MySchema> = {
314
+ steps: [
315
+ {
316
+ id: 'info',
317
+ label: 'Personal Info',
318
+ beforeLeave: async (data) => {
319
+ if (!data.user.name) {
320
+ return { canLeave: false, error: 'Name is required' };
321
+ }
322
+ if (!data.user.email.includes('@')) {
323
+ return { canLeave: false, error: 'Valid email required' };
324
+ }
325
+ return { canLeave: true };
326
+ }
327
+ },
328
+ { id: 'review', label: 'Review' }
329
+ ]
330
+ };
331
+ ```
332
+
333
+ ### Example 2: Conditional Steps (isVisible)
334
+
335
+ Show/hide steps based on data:
336
+
337
+ ```typescript
338
+ const config: IWizardConfig<StepId, MySchema> = {
339
+ steps: [
340
+ { id: 'plan', label: 'Select Plan' },
341
+ {
342
+ id: 'payment',
343
+ label: 'Payment',
344
+ isVisible: (data) => data.plan === 'pro' // Only show for pro plan
345
+ },
346
+ { id: 'review', label: 'Review' }
347
+ ]
348
+ };
349
+ ```
350
+
351
+ ### Example 3: Async Step Conditions
352
+
353
+ Dynamic step visibility with async checks:
354
+
355
+ ```typescript
356
+ const config: IWizardConfig<StepId, MySchema> = {
357
+ steps: [
358
+ { id: 'account', label: 'Account' },
359
+ {
360
+ id: 'premium',
361
+ label: 'Premium Features',
362
+ isVisible: async (data) => {
363
+ const hasAccess = await checkPremiumAccess(data.user.id);
364
+ return hasAccess;
365
+ }
366
+ }
367
+ ]
368
+ };
369
+ ```
370
+
371
+ ### Example 4: Zod Validation
372
+
373
+ Integrate with Zod for schema validation:
374
+
375
+ ```vue
376
+ <script setup lang="ts">
377
+ import { z } from 'zod';
378
+ import { ZodAdapter } from '@wizzard-packages/adapter-zod';
379
+ import { useProvideWizard } from './wizard';
380
+
381
+ const userSchema = z.object({
382
+ name: z.string().min(2, 'Name must be at least 2 characters'),
383
+ email: z.string().email('Invalid email address'),
384
+ age: z.number().min(18, 'Must be 18 or older')
385
+ });
386
+
387
+ const config = {
388
+ steps: [
389
+ {
390
+ id: 'info',
391
+ label: 'Info',
392
+ validate: ZodAdapter(userSchema)
393
+ }
394
+ ]
395
+ };
396
+
397
+ useProvideWizard({ config, initialData: { name: '', email: '', age: 0 } });
398
+ </script>
399
+ ```
400
+
401
+ ### Example 5: LocalStorage Persistence
402
+
403
+ Persist wizard state across sessions:
404
+
405
+ ```typescript
406
+ import { LocalStorageAdapter } from '@wizzard-packages/persistence';
407
+
408
+ const config = {
409
+ steps: [
410
+ { id: 'step1', label: 'Step 1' },
411
+ { id: 'step2', label: 'Step 2' }
412
+ ],
413
+ persistence: LocalStorageAdapter('my-wizard-state')
414
+ };
415
+
416
+ useProvideWizard({ config, initialData: {} });
417
+ ```
418
+
419
+ ### Example 6: Middleware (Logger)
420
+
421
+ Add logging middleware for debugging:
422
+
423
+ ```typescript
424
+ import { loggerMiddleware } from '@wizzard-packages/middleware';
425
+
426
+ const config = {
427
+ steps: [
428
+ { id: 'step1', label: 'Step 1' }
429
+ ],
430
+ middlewares: [loggerMiddleware]
431
+ };
432
+
433
+ useProvideWizard({ config, initialData: {} });
434
+ ```
435
+
436
+ ### Example 7: Progress Indicator Component
437
+
438
+ ```vue
439
+ <script setup lang="ts">
440
+ import { useWizardState } from './wizard';
441
+
442
+ const state = useWizardState();
443
+ </script>
444
+
445
+ <template>
446
+ <div class="progress-bar">
447
+ <div
448
+ class="progress-fill"
449
+ :style="{ width: `${state.progress}%` }"
450
+ />
451
+ </div>
452
+ <div class="progress-text">
453
+ {{ state.progress }}% Complete
454
+ ({{ state.currentStepIndex + 1 }} / {{ state.totalSteps }})
455
+ </div>
456
+ </template>
457
+
458
+ <style scoped>
459
+ .progress-bar {
460
+ width: 100%;
461
+ height: 8px;
462
+ background: #e0e0e0;
463
+ border-radius: 4px;
464
+ overflow: hidden;
465
+ }
466
+
467
+ .progress-fill {
468
+ height: 100%;
469
+ background: linear-gradient(90deg, #4caf50, #8bc34a);
470
+ transition: width 0.3s ease;
471
+ }
472
+ </style>
473
+ ```
474
+
475
+ ### Example 8: Step Breadcrumbs
476
+
477
+ ```vue
478
+ <script setup lang="ts">
479
+ import { useWizardState, useWizardActions } from './wizard';
480
+
481
+ const state = useWizardState();
482
+ const { goToStep } = useWizardActions();
483
+
484
+ const canNavigateToStep = (stepId: string) => {
485
+ return state.visitedSteps.has(stepId) ||
486
+ state.completedSteps.has(stepId);
487
+ };
488
+ </script>
489
+
490
+ <template>
491
+ <div class="breadcrumbs">
492
+ <div
493
+ v-for="(step, index) in state.activeSteps"
494
+ :key="step.id"
495
+ class="breadcrumb"
496
+ :class="{
497
+ active: step.id === state.currentStepId,
498
+ completed: state.completedSteps.has(step.id),
499
+ error: state.errorSteps.has(step.id)
500
+ }"
501
+ @click="canNavigateToStep(step.id) && goToStep(step.id)"
502
+ >
503
+ <span class="number">{{ index + 1 }}</span>
504
+ <span class="label">{{ step.label }}</span>
505
+ </div>
506
+ </div>
507
+ </template>
508
+ ```
509
+
510
+ ### Example 9: Form with Validation Errors
511
+
512
+ ```vue
513
+ <script setup lang="ts">
514
+ import { useWizardField, useWizardError, useWizardActions } from './wizard';
515
+
516
+ const [name, setName] = useWizardField('user.name');
517
+ const [email, setEmail] = useWizardField('user.email');
518
+
519
+ const nameError = useWizardError('user.name');
520
+ const emailError = useWizardError('user.email');
521
+
522
+ const { validateStep, nextStep } = useWizardActions();
523
+
524
+ const handleSubmit = async () => {
525
+ const result = await validateStep('info');
526
+ if (result.isValid) {
527
+ await nextStep();
528
+ }
529
+ };
530
+ </script>
531
+
532
+ <template>
533
+ <form @submit.prevent="handleSubmit">
534
+ <div class="field">
535
+ <label>Name</label>
536
+ <input
537
+ :value="name"
538
+ @input="e => setName((e.target as HTMLInputElement).value)"
539
+ :class="{ error: nameError }"
540
+ />
541
+ <span v-if="nameError" class="error-message">{{ nameError }}</span>
542
+ </div>
543
+
544
+ <div class="field">
545
+ <label>Email</label>
546
+ <input
547
+ type="email"
548
+ :value="email"
549
+ @input="e => setEmail((e.target as HTMLInputElement).value)"
550
+ :class="{ error: emailError }"
551
+ />
552
+ <span v-if="emailError" class="error-message">{{ emailError }}</span>
553
+ </div>
554
+
555
+ <button type="submit">Continue</button>
556
+ </form>
557
+ </template>
558
+ ```
559
+
560
+ ### Example 10: Computed v-model Wrapper
561
+
562
+ For easier v-model integration:
563
+
564
+ ```vue
565
+ <script setup lang="ts">
566
+ import { computed } from 'vue';
567
+ import { useWizardField } from './wizard';
568
+
569
+ const [nameRef, setName] = useWizardField('user.name');
570
+ const [emailRef, setEmail] = useWizardField('user.email');
571
+
572
+ // Create computed properties for v-model
573
+ const name = computed({
574
+ get: () => nameRef.value,
575
+ set: (val) => setName(val)
576
+ });
577
+
578
+ const email = computed({
579
+ get: () => emailRef.value,
580
+ set: (val) => setEmail(val)
581
+ });
582
+ </script>
583
+
584
+ <template>
585
+ <div>
586
+ <input v-model="name" placeholder="Name" />
587
+ <input v-model="email" type="email" placeholder="Email" />
588
+ </div>
589
+ </template>
590
+ ```
591
+
592
+ ### Example 11: Complex Selector with Performance
593
+
594
+ ```vue
595
+ <script setup lang="ts">
596
+ import { useWizardSelector } from './wizard';
597
+
598
+ // Only re-renders when these specific values change
599
+ const wizardMeta = useWizardSelector(state => ({
600
+ currentStep: state.activeSteps.find(s => s.id === state.currentStepId),
601
+ isLastAndValid: state.isLastStep && state.errorSteps.size === 0,
602
+ completionRate: (state.completedSteps.size / state.totalSteps) * 100
603
+ }));
604
+ </script>
605
+
606
+ <template>
607
+ <div class="wizard-meta">
608
+ <h2>{{ wizardMeta.currentStep?.label }}</h2>
609
+ <p v-if="wizardMeta.isLastAndValid">Ready to submit!</p>
610
+ <p>{{ wizardMeta.completionRate.toFixed(0) }}% of steps completed</p>
611
+ </div>
612
+ </template>
613
+ ```
614
+
615
+ ### Example 12: Dynamic Step Configuration
616
+
617
+ ```vue
618
+ <script setup lang="ts">
619
+ import { ref, computed } from 'vue';
620
+ import { useProvideWizard } from './wizard';
621
+ import type { IStepConfig } from '@wizzard-packages/core';
622
+
623
+ const userType = ref<'personal' | 'business'>('personal');
624
+
625
+ const steps = computed(() => {
626
+ const baseSteps: IStepConfig<string, any>[] = [
627
+ { id: 'type', label: 'Account Type' }
628
+ ];
629
+
630
+ if (userType.value === 'business') {
631
+ baseSteps.push(
632
+ { id: 'company', label: 'Company Info' },
633
+ { id: 'tax', label: 'Tax Details' }
634
+ );
635
+ } else {
636
+ baseSteps.push(
637
+ { id: 'personal', label: 'Personal Info' }
638
+ );
639
+ }
640
+
641
+ baseSteps.push({ id: 'review', label: 'Review' });
642
+
643
+ return baseSteps;
644
+ });
645
+
646
+ const config = computed(() => ({
647
+ steps: steps.value
648
+ }));
649
+
650
+ useProvideWizard({
651
+ config: config.value,
652
+ initialData: { type: userType.value }
653
+ });
654
+ </script>
655
+ ```
656
+
657
+ ## ๐ŸŽฏ Best Practices
658
+
659
+ ### 1. Memoize Configuration
660
+
661
+ Use `computed` for step configuration to avoid unnecessary recalculations:
662
+
663
+ ```typescript
664
+ const config = computed(() => ({
665
+ steps: [
666
+ { id: 'step1', label: 'Step 1' }
667
+ ],
668
+ middlewares: [loggerMiddleware],
669
+ validationMode: 'onChange'
670
+ }));
671
+ ```
672
+
673
+ ### 2. Granular Subscriptions
674
+
675
+ Prefer specific hooks over full state for better performance:
676
+
677
+ ```typescript
678
+ // โœ… Good: Only re-renders when 'user.name' changes
679
+ const name = useWizardValue('user.name');
680
+
681
+ // โŒ Avoid: Re-renders on any state change
682
+ const state = useWizardState();
683
+ const name = state.data.user.name;
684
+ ```
685
+
686
+ ### 3. Use Selectors for Derived State
687
+
688
+ Create computed values with `useWizardSelector`:
689
+
690
+ ```typescript
691
+ // Only re-computes when progress or isBusy changes
692
+ const status = useWizardSelector(state => ({
693
+ progress: state.progress,
694
+ loading: state.isBusy
695
+ }));
696
+ ```
697
+
698
+ ### 4. Validation Timing
699
+
700
+ Choose appropriate validation mode:
701
+
702
+ - `onChange`: Real-time validation (best UX, more API calls)
703
+ - `onBlur`: Validate on field blur (balanced)
704
+ - `onSubmit`: Validate on step navigation (least intrusive)
705
+
706
+ ### 5. Error Handling
707
+
708
+ Always handle async errors in guards and validation:
709
+
710
+ ```typescript
711
+ {
712
+ id: 'step1',
713
+ beforeLeave: async (data) => {
714
+ try {
715
+ await validateAPI(data);
716
+ return { canLeave: true };
717
+ } catch (error) {
718
+ return {
719
+ canLeave: false,
720
+ error: error.message || 'Validation failed'
721
+ };
722
+ }
723
+ }
724
+ }
725
+ ```
726
+
727
+ ## ๐Ÿ”Œ Integration with Vue Router
728
+
729
+ ```vue
730
+ <script setup lang="ts">
731
+ import { watch } from 'vue';
732
+ import { useRoute, useRouter } from 'vue-router';
733
+ import { useWizardActions, useWizardState } from './wizard';
734
+
735
+ const route = useRoute();
736
+ const router = useRouter();
737
+ const { goToStep } = useWizardActions();
738
+ const state = useWizardState();
739
+
740
+ // Sync wizard step with route
741
+ watch(() => route.params.step, (stepId) => {
742
+ if (stepId && stepId !== state.currentStepId) {
743
+ goToStep(stepId as string);
744
+ }
745
+ });
746
+
747
+ // Sync route with wizard step
748
+ watch(() => state.currentStepId, (stepId) => {
749
+ if (route.params.step !== stepId) {
750
+ router.push({ params: { step: stepId } });
751
+ }
752
+ });
753
+ </script>
754
+ ```
755
+
756
+ ## ๐Ÿงช Testing
757
+
758
+ ```typescript
759
+ import { mount } from '@vue/test-utils';
760
+ import { useProvideWizard, useWizardState } from './wizard';
761
+
762
+ describe('Wizard', () => {
763
+ it('navigates between steps', async () => {
764
+ const wrapper = mount({
765
+ setup() {
766
+ useProvideWizard({
767
+ config: {
768
+ steps: [
769
+ { id: 'step1', label: 'Step 1' },
770
+ { id: 'step2', label: 'Step 2' }
771
+ ]
772
+ },
773
+ initialData: {}
774
+ });
775
+
776
+ const state = useWizardState();
777
+ const { nextStep } = useWizardActions();
778
+
779
+ return { state, nextStep };
780
+ },
781
+ template: '<div>{{ state.currentStepId }}</div>'
782
+ });
783
+
784
+ expect(wrapper.text()).toBe('step1');
785
+
786
+ await wrapper.vm.nextStep();
787
+ await wrapper.vm.$nextTick();
788
+
789
+ expect(wrapper.text()).toBe('step2');
790
+ });
791
+ });
792
+ ```
793
+
794
+ ## ๐ŸŒ SSR Support
795
+
796
+ The Vue adapter is SSR-friendly and works with Nuxt 3:
797
+
798
+ ```typescript
799
+ // Avoid persistence adapters that access `window` during module initialization
800
+ // Use lazy initialization instead:
801
+
802
+ const config = computed(() => ({
803
+ steps: [...],
804
+ persistence: typeof window !== 'undefined'
805
+ ? LocalStorageAdapter('key')
806
+ : undefined
807
+ }));
808
+ ```
809
+
810
+ ## ๐Ÿ“ฆ Package Structure
811
+
812
+ - **ESM**: `dist/index.js` (tree-shakeable)
813
+ - **CJS**: `dist/index.cjs` (Node.js compatibility)
814
+ - **Types**: `dist/index.d.ts` (TypeScript definitions)
815
+
816
+ ## ๐Ÿ”— How It Fits
817
+
818
+ - **Core engine**: `@wizzard-packages/core` (shared with React)
819
+ - **Optional persistence**: `@wizzard-packages/persistence`
820
+ - **Optional middleware**: `@wizzard-packages/middleware`
821
+ - **Optional validation**: `@wizzard-packages/adapter-zod` or `@wizzard-packages/adapter-yup`
822
+
823
+ ## ๐Ÿ“– Links
824
+
825
+ - **Repository**: https://github.com/ZizzX/wizzard-packages
826
+ - **Documentation**: https://zizzx.github.io/wizzard-packages/
827
+ - **Issues**: https://github.com/ZizzX/wizzard-packages/issues
828
+ - **Changelog**: [CHANGELOG.md](./CHANGELOG.md)
829
+
830
+ ## ๐Ÿ“„ License
831
+
832
+ MIT ยฉ [ZizzX](https://github.com/ZizzX)
833
+
834
+ ---
835
+
836
+ **Made with โค๏ธ for the Vue community**