@htlkg/components 0.0.11 → 0.0.13

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 (112) hide show
  1. package/dist/{AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js → AdminWrapper.vue_vue_type_script_setup_true_lang-BhnWQ-b0.js} +26 -29
  2. package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-BhnWQ-b0.js.map +1 -0
  3. package/dist/Alert.vue_vue_type_script_setup_true_lang-DxPCS-Hx.js.map +1 -1
  4. package/dist/DateRange.vue_vue_type_script_setup_true_lang-BLVg1Hah.js.map +1 -1
  5. package/dist/ProductBadge.vue_vue_type_script_setup_true_lang-Cmr2f4Cy.js.map +1 -1
  6. package/dist/components.css +4 -4
  7. package/dist/composables/index.js +23 -22
  8. package/dist/data/index.js +10 -10
  9. package/dist/{filterHelpers-DgRyoYSa.js → filterHelpers-DpHSlTuh.js} +11 -11
  10. package/dist/filterHelpers-DpHSlTuh.js.map +1 -0
  11. package/dist/index-QK97OdqQ.js.map +1 -1
  12. package/dist/index.js +34 -33
  13. package/dist/navigation/index.js +1 -1
  14. package/dist/{useAdminPage-GhgXp0x8.js → useAdminPage-AgWRvw6o.js} +150 -26
  15. package/dist/useAdminPage-AgWRvw6o.js.map +1 -0
  16. package/package.json +3 -3
  17. package/src/composables/index.ts +1 -0
  18. package/src/composables/useJsonForm.test.ts +272 -0
  19. package/src/composables/useJsonForm.ts +261 -0
  20. package/src/composables/useModal.test.ts +264 -0
  21. package/src/composables/useModal.ts +54 -8
  22. package/src/data/Chart/index.ts +2 -0
  23. package/src/data/DataList/index.ts +1 -0
  24. package/src/data/{DataTable.vue → DataTable/DataTable.vue} +2 -2
  25. package/src/data/DataTable/index.ts +8 -0
  26. package/src/data/SearchableSelect/index.ts +1 -0
  27. package/src/data/Table/index.ts +1 -0
  28. package/src/data/index.ts +5 -15
  29. package/src/domain/BrandCard/index.ts +1 -0
  30. package/src/domain/BrandSelector/index.ts +1 -0
  31. package/src/domain/ProductBadge/index.ts +1 -0
  32. package/src/domain/UserAvatar/index.ts +1 -0
  33. package/src/domain/index.ts +4 -4
  34. package/src/forms/DateRange/index.ts +2 -0
  35. package/src/forms/JsonSchemaForm/index.ts +1 -0
  36. package/src/forms/index.ts +2 -3
  37. package/src/navigation/{AdminWrapper.vue → AdminWrapper/AdminWrapper.vue} +41 -30
  38. package/src/navigation/AdminWrapper/index.ts +1 -0
  39. package/src/navigation/Breadcrumbs/index.ts +1 -0
  40. package/src/navigation/Stepper/index.ts +2 -0
  41. package/src/navigation/Tabs/index.ts +2 -0
  42. package/src/navigation/index.ts +4 -6
  43. package/src/overlays/Alert/index.ts +1 -0
  44. package/src/overlays/Drawer/index.ts +1 -0
  45. package/src/overlays/Modal/index.ts +1 -0
  46. package/src/overlays/Notification/index.ts +1 -0
  47. package/src/overlays/index.ts +4 -4
  48. package/src/patterns/DASHBOARD_PAGE.md +642 -0
  49. package/src/patterns/DETAIL_PAGE.md +446 -0
  50. package/src/patterns/FORM_PAGE.md +439 -0
  51. package/src/patterns/LIST_PAGE.md +340 -0
  52. package/src/patterns/PAGE_PATTERNS.md +110 -0
  53. package/src/patterns/WIZARD_PAGE.md +733 -0
  54. package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js.map +0 -1
  55. package/dist/filterHelpers-DgRyoYSa.js.map +0 -1
  56. package/dist/useAdminPage-GhgXp0x8.js.map +0 -1
  57. package/src/data/Table.vue +0 -295
  58. /package/src/data/{Chart.demo.vue → Chart/Chart.demo.vue} +0 -0
  59. /package/src/data/{Chart.md → Chart/Chart.md} +0 -0
  60. /package/src/data/{Chart.vue → Chart/Chart.vue} +0 -0
  61. /package/src/data/{DataList.md → DataList/DataList.md} +0 -0
  62. /package/src/data/{DataList.test.ts → DataList/DataList.test.ts} +0 -0
  63. /package/src/data/{DataList.vue → DataList/DataList.vue} +0 -0
  64. /package/src/data/{SearchableSelect.md → SearchableSelect/SearchableSelect.md} +0 -0
  65. /package/src/data/{SearchableSelect.vue → SearchableSelect/SearchableSelect.vue} +0 -0
  66. /package/src/data/{Table.demo.vue → Table/Table.demo.vue} +0 -0
  67. /package/src/data/{Table.md → Table/Table.md} +0 -0
  68. /package/src/data/{Table.property.test.ts → Table/Table.property.test.ts} +0 -0
  69. /package/src/data/{Table.test.ts → Table/Table.test.ts} +0 -0
  70. /package/src/data/{Table.unit.test.ts → Table/Table.unit.test.ts} +0 -0
  71. /package/src/domain/{BrandCard.md → BrandCard/BrandCard.md} +0 -0
  72. /package/src/domain/{BrandCard.vue → BrandCard/BrandCard.vue} +0 -0
  73. /package/src/domain/{BrandSelector.md → BrandSelector/BrandSelector.md} +0 -0
  74. /package/src/domain/{BrandSelector.vue → BrandSelector/BrandSelector.vue} +0 -0
  75. /package/src/domain/{ProductBadge.md → ProductBadge/ProductBadge.md} +0 -0
  76. /package/src/domain/{ProductBadge.vue → ProductBadge/ProductBadge.vue} +0 -0
  77. /package/src/domain/{UserAvatar.md → UserAvatar/UserAvatar.md} +0 -0
  78. /package/src/domain/{UserAvatar.vue → UserAvatar/UserAvatar.vue} +0 -0
  79. /package/src/forms/{DateRange.demo.vue → DateRange/DateRange.demo.vue} +0 -0
  80. /package/src/forms/{DateRange.md → DateRange/DateRange.md} +0 -0
  81. /package/src/forms/{DateRange.vue → DateRange/DateRange.vue} +0 -0
  82. /package/src/forms/{JsonSchemaForm.demo.vue → JsonSchemaForm/JsonSchemaForm.demo.vue} +0 -0
  83. /package/src/forms/{JsonSchemaForm.md → JsonSchemaForm/JsonSchemaForm.md} +0 -0
  84. /package/src/forms/{JsonSchemaForm.property.test.ts → JsonSchemaForm/JsonSchemaForm.property.test.ts} +0 -0
  85. /package/src/forms/{JsonSchemaForm.test.ts → JsonSchemaForm/JsonSchemaForm.test.ts} +0 -0
  86. /package/src/forms/{JsonSchemaForm.unit.test.ts → JsonSchemaForm/JsonSchemaForm.unit.test.ts} +0 -0
  87. /package/src/forms/{JsonSchemaForm.vue → JsonSchemaForm/JsonSchemaForm.vue} +0 -0
  88. /package/src/navigation/{Breadcrumbs.demo.vue → Breadcrumbs/Breadcrumbs.demo.vue} +0 -0
  89. /package/src/navigation/{Breadcrumbs.md → Breadcrumbs/Breadcrumbs.md} +0 -0
  90. /package/src/navigation/{Breadcrumbs.test.ts → Breadcrumbs/Breadcrumbs.test.ts} +0 -0
  91. /package/src/navigation/{Breadcrumbs.vue → Breadcrumbs/Breadcrumbs.vue} +0 -0
  92. /package/src/navigation/{Stepper.demo.vue → Stepper/Stepper.demo.vue} +0 -0
  93. /package/src/navigation/{Stepper.md → Stepper/Stepper.md} +0 -0
  94. /package/src/navigation/{Stepper.vue → Stepper/Stepper.vue} +0 -0
  95. /package/src/navigation/{Tabs.demo.vue → Tabs/Tabs.demo.vue} +0 -0
  96. /package/src/navigation/{Tabs.md → Tabs/Tabs.md} +0 -0
  97. /package/src/navigation/{Tabs.test.ts → Tabs/Tabs.test.ts} +0 -0
  98. /package/src/navigation/{Tabs.vue → Tabs/Tabs.vue} +0 -0
  99. /package/src/overlays/{Alert.demo.vue → Alert/Alert.demo.vue} +0 -0
  100. /package/src/overlays/{Alert.md → Alert/Alert.md} +0 -0
  101. /package/src/overlays/{Alert.test.ts → Alert/Alert.test.ts} +0 -0
  102. /package/src/overlays/{Alert.vue → Alert/Alert.vue} +0 -0
  103. /package/src/overlays/{Drawer.md → Drawer/Drawer.md} +0 -0
  104. /package/src/overlays/{Drawer.test.ts → Drawer/Drawer.test.ts} +0 -0
  105. /package/src/overlays/{Drawer.vue → Drawer/Drawer.vue} +0 -0
  106. /package/src/overlays/{Modal.demo.vue → Modal/Modal.demo.vue} +0 -0
  107. /package/src/overlays/{Modal.md → Modal/Modal.md} +0 -0
  108. /package/src/overlays/{Modal.test.ts → Modal/Modal.test.ts} +0 -0
  109. /package/src/overlays/{Modal.vue → Modal/Modal.vue} +0 -0
  110. /package/src/overlays/{Notification.md → Notification/Notification.md} +0 -0
  111. /package/src/overlays/{Notification.test.ts → Notification/Notification.test.ts} +0 -0
  112. /package/src/overlays/{Notification.vue → Notification/Notification.vue} +0 -0
@@ -0,0 +1,733 @@
1
+ # Wizard Page Pattern
2
+
3
+ Multi-step forms for complex entity creation with validation, progress tracking, and step-by-step navigation.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────┐
9
+ │ AdminLayout │
10
+ ├─────────────────────────────────────────────────────────────┤
11
+ │ Page Header (title + breadcrumbs) │
12
+ ├─────────────────────────────────────────────────────────────┤
13
+ │ Back Button │
14
+ ├─────────────────────────────────────────────────────────────┤
15
+ │ Stepper (progress indicator) │
16
+ │ ├── Step 1: Complete ✓ │
17
+ │ ├── Step 2: Current → │
18
+ │ ├── Step 3: Upcoming │
19
+ │ └── Step N: Upcoming │
20
+ ├─────────────────────────────────────────────────────────────┤
21
+ │ Step Content Container (lg:w-2/3 xl:w-1/2) │
22
+ │ ├── Step Title/Description │
23
+ │ ├── Form Fields (JsonSchemaForm or custom) │
24
+ │ └── Navigation Buttons (Back / Next / Complete) │
25
+ ├─────────────────────────────────────────────────────────────┤
26
+ │ Final Step: Review & Submit │
27
+ │ ├── Summary Card │
28
+ │ ├── Missing Fields Alert (if any) │
29
+ │ └── Create/Submit Button │
30
+ └─────────────────────────────────────────────────────────────┘
31
+ ```
32
+
33
+ ## CRM Template Reference
34
+
35
+ Based on: `ui/src/components/Templates/CRM/CampaignsCreate.vue`
36
+
37
+ ```vue
38
+ <!-- CRM Template Structure (raw @hotelinking/ui) -->
39
+ <uiWrapper :sidebar :topbar>
40
+ <uiViewHeader :pages="pages" title="Campaigns - Create" />
41
+
42
+ <!-- Back Button -->
43
+ <div class="flex justify-between items-center mb-6">
44
+ <uiButton :icon="ArrowLeftIcon" @click="handleBack">Back to Campaigns</uiButton>
45
+ </div>
46
+
47
+ <!-- Stepper -->
48
+ <uiStepsV4
49
+ :steps="stepsV4"
50
+ @stepClick="handleStepClick"
51
+ @stepCompleted="handleStepCompleted"
52
+ @stepCurrent="handleStepCurrent"
53
+ />
54
+
55
+ <!-- Step Content -->
56
+ <div v-if="currentStepIndex === 0" class="lg:w-2/3 xl:w-1/2">
57
+ <uiInput label="Campaign Name" v-model="campaignName" />
58
+ <uiTextArea label="Description" v-model="description" />
59
+
60
+ <!-- Navigation -->
61
+ <div class="flex justify-end gap-4 mt-8">
62
+ <uiButton v-if="currentStepIndex > 0" :icon="ArrowLeftIcon" color="gray" @click="goToPreviousStep">
63
+ Back
64
+ </uiButton>
65
+ <uiButton :icon="ArrowRightIcon" color="primary" @click="goToNextStep">
66
+ Next
67
+ </uiButton>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Review Step -->
72
+ <div v-else-if="currentStepIndex === lastStepIndex" class="lg:w-2/3 xl:w-1/2">
73
+ <uiAlert v-if="missingFields.length" title="Missing Fields" type="danger" />
74
+ <uiStripedCard :title="campaignName" :items="reviewItems" />
75
+ <uiButton color="primary" @click="handleCreate">Create Campaign</uiButton>
76
+ </div>
77
+ </uiWrapper>
78
+ ```
79
+
80
+ ## Enhanced Pattern (with @htlkg/*)
81
+
82
+ ### Astro Page
83
+
84
+ ```astro
85
+ ---
86
+ // src/pages/[brandId]/admin/campaigns/create.astro
87
+ import Layout from '@/layouts/Layout.astro';
88
+ import CampaignWizard from '@/components/admin/CampaignWizard.vue';
89
+
90
+ export const prerender = false;
91
+
92
+ const { brandId } = Astro.params;
93
+
94
+ const layoutProps = {
95
+ title: 'Create Campaign',
96
+ breadcrumbs: [
97
+ { name: 'Campaigns', href: `/${brandId}/admin/campaigns` },
98
+ { name: 'Create', current: true },
99
+ ],
100
+ };
101
+ ---
102
+
103
+ <Layout {...layoutProps}>
104
+ <CampaignWizard client:load brandId={brandId} />
105
+ </Layout>
106
+ ```
107
+
108
+ ### Vue Component with useWizard
109
+
110
+ ```vue
111
+ <!-- src/components/admin/CampaignWizard.vue -->
112
+ <script setup lang="ts">
113
+ import { computed } from 'vue';
114
+ import { Stepper } from '@htlkg/components/navigation';
115
+ import { JsonSchemaForm } from '@htlkg/components/forms';
116
+ import { useWizard, useNotifications } from '@htlkg/components/composables';
117
+ import { uiButton, uiAlert, uiStripedCard } from '@hotelinking/ui';
118
+ import {
119
+ ArrowLeftIcon,
120
+ ArrowRightIcon,
121
+ CheckIcon,
122
+ } from '@heroicons/vue/24/outline';
123
+
124
+ interface CampaignData {
125
+ name: string;
126
+ description: string;
127
+ segments: string[];
128
+ tags: string[];
129
+ templateId: string;
130
+ sendOption: 'immediately' | 'schedule';
131
+ scheduleDate?: string;
132
+ subjectLine: string;
133
+ senderName: string;
134
+ senderEmail: string;
135
+ }
136
+
137
+ interface Props {
138
+ brandId: string;
139
+ }
140
+
141
+ const props = defineProps<Props>();
142
+ const emit = defineEmits<{
143
+ back: [];
144
+ created: [campaign: CampaignData];
145
+ }>();
146
+
147
+ const { notify } = useNotifications();
148
+
149
+ // Define wizard steps with JSON Schema for each step
150
+ const steps = [
151
+ {
152
+ id: 'details',
153
+ label: 'Campaign Details',
154
+ description: 'Basic campaign information',
155
+ schema: {
156
+ type: 'object',
157
+ properties: {
158
+ name: {
159
+ type: 'string',
160
+ title: 'Campaign Name',
161
+ minLength: 1,
162
+ },
163
+ description: {
164
+ type: 'string',
165
+ title: 'Description',
166
+ minLength: 100, // Will auto-use textarea
167
+ },
168
+ },
169
+ required: ['name'],
170
+ },
171
+ },
172
+ {
173
+ id: 'audience',
174
+ label: 'Audience Selection',
175
+ description: 'Select target segments and tags',
176
+ // Custom validation - no schema
177
+ validate: (data: Partial<CampaignData>) => {
178
+ return (data.segments?.length ?? 0) > 0 || (data.tags?.length ?? 0) > 0;
179
+ },
180
+ },
181
+ {
182
+ id: 'template',
183
+ label: 'Email Design',
184
+ description: 'Select email template',
185
+ validate: (data: Partial<CampaignData>) => {
186
+ return !!data.templateId;
187
+ },
188
+ },
189
+ {
190
+ id: 'schedule',
191
+ label: 'Scheduling',
192
+ description: 'Set send date and time',
193
+ schema: {
194
+ type: 'object',
195
+ properties: {
196
+ sendOption: {
197
+ type: 'string',
198
+ title: 'Send Option',
199
+ enum: ['immediately', 'schedule'],
200
+ default: 'schedule',
201
+ },
202
+ scheduleDate: {
203
+ type: 'string',
204
+ title: 'Schedule Date',
205
+ format: 'date-time',
206
+ },
207
+ },
208
+ required: ['sendOption'],
209
+ },
210
+ },
211
+ {
212
+ id: 'sender',
213
+ label: 'Subject & Sender',
214
+ description: 'Configure email subject and sender',
215
+ schema: {
216
+ type: 'object',
217
+ properties: {
218
+ subjectLine: {
219
+ type: 'string',
220
+ title: 'Subject Line',
221
+ minLength: 1,
222
+ },
223
+ senderName: {
224
+ type: 'string',
225
+ title: 'Sender Name',
226
+ minLength: 1,
227
+ },
228
+ senderEmail: {
229
+ type: 'string',
230
+ title: 'Sender Email',
231
+ format: 'email',
232
+ },
233
+ },
234
+ required: ['subjectLine', 'senderName', 'senderEmail'],
235
+ },
236
+ },
237
+ {
238
+ id: 'review',
239
+ label: 'Review & Send',
240
+ description: 'Review campaign before creating',
241
+ optional: true, // Review step doesn't need validation
242
+ },
243
+ ];
244
+
245
+ // Initialize wizard
246
+ const wizard = useWizard<CampaignData>({
247
+ steps,
248
+ initialData: {
249
+ name: '',
250
+ description: '',
251
+ segments: [],
252
+ tags: [],
253
+ templateId: '',
254
+ sendOption: 'schedule',
255
+ subjectLine: '',
256
+ senderName: '',
257
+ senderEmail: '',
258
+ },
259
+ validateOnNext: true,
260
+ onComplete: async (data) => {
261
+ try {
262
+ await createCampaign(props.brandId, data);
263
+ notify({ type: 'success', message: 'Campaign created successfully' });
264
+ emit('created', data as CampaignData);
265
+ } catch (error) {
266
+ notify({ type: 'error', message: 'Failed to create campaign' });
267
+ throw error;
268
+ }
269
+ },
270
+ onStepChange: (from, to, data) => {
271
+ console.log(`Step changed from ${from} to ${to}`);
272
+ },
273
+ });
274
+
275
+ // Review items for final step
276
+ const reviewItems = computed(() => {
277
+ const data = wizard.data.value;
278
+ return [
279
+ { title: 'Campaign Name', text: data.name || 'Not set' },
280
+ { title: 'Description', text: data.description || 'Not set' },
281
+ { title: 'Segments', text: data.segments?.join(', ') || 'None selected' },
282
+ { title: 'Tags', text: data.tags?.join(', ') || 'None selected' },
283
+ { title: 'Template', text: data.templateId || 'Not selected' },
284
+ { title: 'Send Option', text: data.sendOption === 'immediately' ? 'Send Now' : `Scheduled: ${data.scheduleDate}` },
285
+ { title: 'Subject Line', text: data.subjectLine || 'Not set' },
286
+ { title: 'Sender', text: `${data.senderName} <${data.senderEmail}>` || 'Not set' },
287
+ ];
288
+ });
289
+
290
+ // Missing fields for review step
291
+ const missingFields = computed(() => {
292
+ const missing: string[] = [];
293
+ const data = wizard.data.value;
294
+
295
+ if (!data.name) missing.push('Campaign Name');
296
+ if ((data.segments?.length ?? 0) === 0 && (data.tags?.length ?? 0) === 0) {
297
+ missing.push('At least one segment or tag');
298
+ }
299
+ if (!data.templateId) missing.push('Email Template');
300
+ if (!data.subjectLine) missing.push('Subject Line');
301
+ if (!data.senderName) missing.push('Sender Name');
302
+ if (!data.senderEmail) missing.push('Sender Email');
303
+
304
+ return missing;
305
+ });
306
+
307
+ // Handle form submission for schema-based steps
308
+ function handleStepSubmit(formData: Partial<CampaignData>) {
309
+ wizard.updateData(formData);
310
+ wizard.setStepValid(wizard.currentStep.value.id, true);
311
+ }
312
+
313
+ // Handle form validation errors
314
+ function handleValidationError(errors: Array<{ field: string; message: string }>) {
315
+ wizard.setStepValid(wizard.currentStep.value.id, false);
316
+ if (errors.length > 0) {
317
+ wizard.setStepError(wizard.currentStep.value.id, errors[0].message);
318
+ }
319
+ }
320
+ </script>
321
+
322
+ <template>
323
+ <!-- Back Button -->
324
+ <div class="flex justify-between items-center mb-6">
325
+ <uiButton :icon="ArrowLeftIcon" @click="emit('back')">
326
+ Back to Campaigns
327
+ </uiButton>
328
+ </div>
329
+
330
+ <!-- Stepper -->
331
+ <Stepper
332
+ :steps="wizard.stepperSteps.value"
333
+ :current-step="wizard.currentStepIndex.value"
334
+ @step-click="wizard.goToStep"
335
+ />
336
+
337
+ <!-- Step Content -->
338
+ <div class="mt-8">
339
+ <!-- Step 1: Campaign Details -->
340
+ <div v-if="wizard.currentStep.value.id === 'details'" class="lg:w-2/3 xl:w-1/2">
341
+ <JsonSchemaForm
342
+ v-model="wizard.data.value"
343
+ :schema="wizard.currentStep.value.schema"
344
+ @submit="handleStepSubmit"
345
+ @validation-error="handleValidationError"
346
+ >
347
+ <template #actions>
348
+ <div class="flex justify-end gap-4 mt-8">
349
+ <uiButton
350
+ :icon="ArrowRightIcon"
351
+ color="primary"
352
+ @click="wizard.goToNext"
353
+ >
354
+ Next
355
+ </uiButton>
356
+ </div>
357
+ </template>
358
+ </JsonSchemaForm>
359
+ </div>
360
+
361
+ <!-- Step 2: Audience Selection (Custom UI) -->
362
+ <div v-else-if="wizard.currentStep.value.id === 'audience'" class="lg:w-2/3 xl:w-1/2">
363
+ <!-- Segments Checkboxes -->
364
+ <div class="mb-8">
365
+ <h3 class="text-lg font-semibold mb-4">Select Segments</h3>
366
+ <!-- Custom segment selection UI here -->
367
+ </div>
368
+
369
+ <!-- Tags Checkboxes -->
370
+ <div class="mb-8">
371
+ <h3 class="text-lg font-semibold mb-4">Select Tags</h3>
372
+ <!-- Custom tag selection UI here -->
373
+ </div>
374
+
375
+ <!-- Navigation -->
376
+ <div class="flex justify-end gap-4 mt-8">
377
+ <uiButton
378
+ :icon="ArrowLeftIcon"
379
+ color="gray"
380
+ @click="wizard.goToPrevious"
381
+ >
382
+ Back
383
+ </uiButton>
384
+ <uiButton
385
+ :icon="ArrowRightIcon"
386
+ color="primary"
387
+ @click="wizard.goToNext"
388
+ >
389
+ Next
390
+ </uiButton>
391
+ </div>
392
+ </div>
393
+
394
+ <!-- Step 3: Template Selection (Custom UI) -->
395
+ <div v-else-if="wizard.currentStep.value.id === 'template'" class="mt-8">
396
+ <!-- Template Grid -->
397
+ <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 mb-6">
398
+ <!-- Template cards here -->
399
+ </div>
400
+
401
+ <!-- Navigation -->
402
+ <div class="flex justify-end gap-4 mt-8">
403
+ <uiButton
404
+ :icon="ArrowLeftIcon"
405
+ color="gray"
406
+ @click="wizard.goToPrevious"
407
+ >
408
+ Back
409
+ </uiButton>
410
+ <uiButton
411
+ :icon="ArrowRightIcon"
412
+ color="primary"
413
+ @click="wizard.goToNext"
414
+ >
415
+ Next
416
+ </uiButton>
417
+ </div>
418
+ </div>
419
+
420
+ <!-- Step 4: Scheduling (JsonSchemaForm) -->
421
+ <div v-else-if="wizard.currentStep.value.id === 'schedule'" class="lg:w-2/3 xl:w-1/2">
422
+ <JsonSchemaForm
423
+ v-model="wizard.data.value"
424
+ :schema="wizard.currentStep.value.schema"
425
+ @submit="handleStepSubmit"
426
+ @validation-error="handleValidationError"
427
+ >
428
+ <template #actions>
429
+ <div class="flex justify-end gap-4 mt-8">
430
+ <uiButton
431
+ :icon="ArrowLeftIcon"
432
+ color="gray"
433
+ @click="wizard.goToPrevious"
434
+ >
435
+ Back
436
+ </uiButton>
437
+ <uiButton
438
+ :icon="ArrowRightIcon"
439
+ color="primary"
440
+ @click="wizard.goToNext"
441
+ >
442
+ Next
443
+ </uiButton>
444
+ </div>
445
+ </template>
446
+ </JsonSchemaForm>
447
+ </div>
448
+
449
+ <!-- Step 5: Subject & Sender (JsonSchemaForm) -->
450
+ <div v-else-if="wizard.currentStep.value.id === 'sender'" class="lg:w-2/3 xl:w-1/2">
451
+ <JsonSchemaForm
452
+ v-model="wizard.data.value"
453
+ :schema="wizard.currentStep.value.schema"
454
+ @submit="handleStepSubmit"
455
+ @validation-error="handleValidationError"
456
+ >
457
+ <template #actions>
458
+ <div class="flex justify-end gap-4 mt-8">
459
+ <uiButton
460
+ :icon="ArrowLeftIcon"
461
+ color="gray"
462
+ @click="wizard.goToPrevious"
463
+ >
464
+ Back
465
+ </uiButton>
466
+ <uiButton
467
+ :icon="ArrowRightIcon"
468
+ color="primary"
469
+ @click="wizard.goToNext"
470
+ >
471
+ Next
472
+ </uiButton>
473
+ </div>
474
+ </template>
475
+ </JsonSchemaForm>
476
+ </div>
477
+
478
+ <!-- Step 6: Review -->
479
+ <div v-else-if="wizard.currentStep.value.id === 'review'" class="lg:w-2/3 xl:w-1/2">
480
+ <!-- Missing Fields Alert -->
481
+ <uiAlert
482
+ v-if="missingFields.length > 0"
483
+ title="Missing Required Fields"
484
+ type="danger"
485
+ class="mb-6"
486
+ >
487
+ <p class="text-sm mb-2">Please complete the following before creating:</p>
488
+ <ul class="list-disc list-inside text-sm">
489
+ <li v-for="field in missingFields" :key="field">{{ field }}</li>
490
+ </ul>
491
+ </uiAlert>
492
+
493
+ <!-- Review Summary -->
494
+ <uiStripedCard
495
+ :title="wizard.data.value.name || 'New Campaign'"
496
+ :items="reviewItems"
497
+ class="mb-8"
498
+ />
499
+
500
+ <!-- Navigation -->
501
+ <div class="flex justify-end gap-4 mt-8">
502
+ <uiButton
503
+ :icon="ArrowLeftIcon"
504
+ color="gray"
505
+ @click="wizard.goToPrevious"
506
+ >
507
+ Back
508
+ </uiButton>
509
+ <uiButton
510
+ :icon="CheckIcon"
511
+ color="primary"
512
+ :loading="wizard.isCompleting.value"
513
+ :disabled="missingFields.length > 0"
514
+ @click="wizard.complete"
515
+ >
516
+ Create Campaign
517
+ </uiButton>
518
+ </div>
519
+ </div>
520
+ </div>
521
+
522
+ <!-- Progress Indicator -->
523
+ <div class="mt-4 text-sm text-gray-500 text-center">
524
+ Step {{ wizard.currentStepIndex.value + 1 }} of {{ wizard.stepperSteps.value.length }}
525
+ ({{ wizard.progress.value }}% complete)
526
+ </div>
527
+ </template>
528
+ ```
529
+
530
+ ## Key Components
531
+
532
+ ### useWizard Composable
533
+
534
+ ```typescript
535
+ import { useWizard } from '@htlkg/components/composables';
536
+
537
+ const wizard = useWizard<FormData>({
538
+ // Step definitions
539
+ steps: [
540
+ {
541
+ id: 'step1',
542
+ label: 'Step Label',
543
+ description: 'Optional description',
544
+ schema: jsonSchema, // JSON Schema for JsonSchemaForm
545
+ validate: (data) => true, // Custom validation function
546
+ optional: false, // Whether step can be skipped
547
+ },
548
+ ],
549
+
550
+ // Initial form data
551
+ initialData: { field1: '', field2: '' },
552
+
553
+ // Validation behavior
554
+ validateOnNext: true, // Validate before advancing
555
+ allowBackWithoutValidation: true, // Allow going back without validation
556
+
557
+ // Callbacks
558
+ onComplete: async (data) => { /* Handle completion */ },
559
+ onStepChange: (from, to, data) => { /* Handle step change */ },
560
+ });
561
+
562
+ // Returned state and methods
563
+ wizard.currentStepIndex // Current step index (ref)
564
+ wizard.currentStep // Current step object (computed)
565
+ wizard.data // Form data (ref)
566
+ wizard.isFirstStep // Is first step (computed)
567
+ wizard.isLastStep // Is last step (computed)
568
+ wizard.isCompleting // Is completing (ref)
569
+ wizard.stepperSteps // Steps for Stepper component (computed)
570
+ wizard.stepValidation // Validation state per step (ref)
571
+ wizard.currentStepValid // Is current step valid (computed)
572
+ wizard.allStepsValid // Are all steps valid (computed)
573
+ wizard.stepErrors // Error messages per step (ref)
574
+ wizard.progress // Progress percentage (computed)
575
+
576
+ // Navigation
577
+ wizard.goToNext() // Go to next step (validates current)
578
+ wizard.goToPrevious() // Go to previous step
579
+ wizard.goToStep(index) // Go to specific step
580
+ wizard.canGoToStep(index) // Check if navigation is allowed
581
+
582
+ // Data management
583
+ wizard.updateData(data) // Update form data
584
+ wizard.updateStepData(stepId, data) // Update data for specific step
585
+ wizard.setStepValid(stepId, valid) // Set step validation status
586
+ wizard.setStepError(stepId, error) // Set step error message
587
+ wizard.resetStep(stepId) // Reset specific step
588
+ wizard.reset() // Reset entire wizard
589
+
590
+ // Completion
591
+ wizard.complete() // Validate all and complete
592
+
593
+ // Utilities
594
+ wizard.getStepIndex(stepId) // Get step index by ID
595
+ wizard.getStep(stepId) // Get step by ID
596
+ ```
597
+
598
+ ### Step Types
599
+
600
+ ```typescript
601
+ interface WizardStep<T> {
602
+ /** Unique step identifier */
603
+ id: string;
604
+ /** Display label for the step */
605
+ label: string;
606
+ /** Optional description */
607
+ description?: string;
608
+ /** JSON Schema for JsonSchemaForm (optional) */
609
+ schema?: JsonSchema;
610
+ /** Custom validation function (optional) */
611
+ validate?: (data: T) => boolean | Promise<boolean>;
612
+ /** Icon component for the step (optional) */
613
+ icon?: Component;
614
+ /** Whether step can be skipped */
615
+ optional?: boolean;
616
+ }
617
+ ```
618
+
619
+ ### Step Status
620
+
621
+ ```typescript
622
+ type StepStatus = 'complete' | 'current' | 'upcoming' | 'error';
623
+ ```
624
+
625
+ ## Patterns
626
+
627
+ ### Schema-Based Steps
628
+
629
+ For steps with simple form fields, use JSON Schema:
630
+
631
+ ```typescript
632
+ {
633
+ id: 'details',
634
+ label: 'Details',
635
+ schema: {
636
+ type: 'object',
637
+ properties: {
638
+ name: { type: 'string', title: 'Name', minLength: 1 },
639
+ email: { type: 'string', title: 'Email', format: 'email' },
640
+ },
641
+ required: ['name', 'email'],
642
+ },
643
+ }
644
+ ```
645
+
646
+ ### Custom Validation Steps
647
+
648
+ For steps with complex UI (grids, cards, custom selections):
649
+
650
+ ```typescript
651
+ {
652
+ id: 'selection',
653
+ label: 'Select Items',
654
+ validate: (data) => data.selectedItems?.length > 0,
655
+ }
656
+ ```
657
+
658
+ ### Optional Steps
659
+
660
+ For steps that can be skipped:
661
+
662
+ ```typescript
663
+ {
664
+ id: 'extras',
665
+ label: 'Additional Options',
666
+ optional: true,
667
+ }
668
+ ```
669
+
670
+ ### Review Step
671
+
672
+ Always add a final review step:
673
+
674
+ ```typescript
675
+ {
676
+ id: 'review',
677
+ label: 'Review',
678
+ optional: true, // No validation needed
679
+ }
680
+ ```
681
+
682
+ ## Navigation Buttons Pattern
683
+
684
+ ```vue
685
+ <div class="flex justify-end gap-4 mt-8">
686
+ <!-- Back Button (hide on first step) -->
687
+ <uiButton
688
+ v-if="!wizard.isFirstStep.value"
689
+ :icon="ArrowLeftIcon"
690
+ color="gray"
691
+ @click="wizard.goToPrevious"
692
+ >
693
+ Back
694
+ </uiButton>
695
+
696
+ <!-- Next Button (show on non-last steps) -->
697
+ <uiButton
698
+ v-if="!wizard.isLastStep.value"
699
+ :icon="ArrowRightIcon"
700
+ color="primary"
701
+ @click="wizard.goToNext"
702
+ >
703
+ Next
704
+ </uiButton>
705
+
706
+ <!-- Complete Button (show on last step) -->
707
+ <uiButton
708
+ v-else
709
+ :icon="CheckIcon"
710
+ color="primary"
711
+ :loading="wizard.isCompleting.value"
712
+ :disabled="!wizard.allStepsValid.value"
713
+ @click="wizard.complete"
714
+ >
715
+ Create
716
+ </uiButton>
717
+ </div>
718
+ ```
719
+
720
+ ## Checklist
721
+
722
+ - [ ] Astro page with layout
723
+ - [ ] Vue component with `useWizard` composable
724
+ - [ ] Step definitions with schemas or custom validation
725
+ - [ ] `Stepper` component for progress visualization
726
+ - [ ] `JsonSchemaForm` for schema-based steps
727
+ - [ ] Custom UI for complex selection steps
728
+ - [ ] Navigation buttons (Back/Next/Complete)
729
+ - [ ] Review step with summary card
730
+ - [ ] Missing fields alert on review step
731
+ - [ ] Loading state during completion
732
+ - [ ] `useNotifications` for success/error messages
733
+ - [ ] Progress indicator (optional)