@htlkg/components 0.0.1

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 (79) hide show
  1. package/dist/composables/index.js +388 -0
  2. package/dist/composables/index.js.map +1 -0
  3. package/package.json +41 -0
  4. package/src/composables/index.ts +6 -0
  5. package/src/composables/useForm.test.ts +229 -0
  6. package/src/composables/useForm.ts +130 -0
  7. package/src/composables/useFormValidation.test.ts +189 -0
  8. package/src/composables/useFormValidation.ts +83 -0
  9. package/src/composables/useModal.property.test.ts +164 -0
  10. package/src/composables/useModal.ts +43 -0
  11. package/src/composables/useNotifications.test.ts +166 -0
  12. package/src/composables/useNotifications.ts +81 -0
  13. package/src/composables/useTable.property.test.ts +198 -0
  14. package/src/composables/useTable.ts +134 -0
  15. package/src/composables/useTabs.property.test.ts +247 -0
  16. package/src/composables/useTabs.ts +101 -0
  17. package/src/data/Chart.demo.vue +340 -0
  18. package/src/data/Chart.md +525 -0
  19. package/src/data/Chart.vue +133 -0
  20. package/src/data/DataList.md +80 -0
  21. package/src/data/DataList.test.ts +69 -0
  22. package/src/data/DataList.vue +46 -0
  23. package/src/data/SearchableSelect.md +107 -0
  24. package/src/data/SearchableSelect.vue +124 -0
  25. package/src/data/Table.demo.vue +296 -0
  26. package/src/data/Table.md +588 -0
  27. package/src/data/Table.property.test.ts +548 -0
  28. package/src/data/Table.test.ts +562 -0
  29. package/src/data/Table.unit.test.ts +544 -0
  30. package/src/data/Table.vue +321 -0
  31. package/src/data/index.ts +5 -0
  32. package/src/domain/BrandCard.md +81 -0
  33. package/src/domain/BrandCard.vue +63 -0
  34. package/src/domain/BrandSelector.md +84 -0
  35. package/src/domain/BrandSelector.vue +65 -0
  36. package/src/domain/ProductBadge.md +60 -0
  37. package/src/domain/ProductBadge.vue +47 -0
  38. package/src/domain/UserAvatar.md +84 -0
  39. package/src/domain/UserAvatar.vue +60 -0
  40. package/src/domain/domain-components.property.test.ts +449 -0
  41. package/src/domain/index.ts +4 -0
  42. package/src/forms/DateRange.demo.vue +273 -0
  43. package/src/forms/DateRange.md +337 -0
  44. package/src/forms/DateRange.vue +110 -0
  45. package/src/forms/JsonSchemaForm.demo.vue +549 -0
  46. package/src/forms/JsonSchemaForm.md +112 -0
  47. package/src/forms/JsonSchemaForm.property.test.ts +817 -0
  48. package/src/forms/JsonSchemaForm.test.ts +601 -0
  49. package/src/forms/JsonSchemaForm.unit.test.ts +801 -0
  50. package/src/forms/JsonSchemaForm.vue +615 -0
  51. package/src/forms/index.ts +3 -0
  52. package/src/index.ts +17 -0
  53. package/src/navigation/Breadcrumbs.demo.vue +142 -0
  54. package/src/navigation/Breadcrumbs.md +102 -0
  55. package/src/navigation/Breadcrumbs.test.ts +69 -0
  56. package/src/navigation/Breadcrumbs.vue +58 -0
  57. package/src/navigation/Stepper.demo.vue +337 -0
  58. package/src/navigation/Stepper.md +174 -0
  59. package/src/navigation/Stepper.vue +146 -0
  60. package/src/navigation/Tabs.demo.vue +293 -0
  61. package/src/navigation/Tabs.md +163 -0
  62. package/src/navigation/Tabs.test.ts +176 -0
  63. package/src/navigation/Tabs.vue +104 -0
  64. package/src/navigation/index.ts +5 -0
  65. package/src/overlays/Alert.demo.vue +377 -0
  66. package/src/overlays/Alert.md +248 -0
  67. package/src/overlays/Alert.test.ts +166 -0
  68. package/src/overlays/Alert.vue +70 -0
  69. package/src/overlays/Drawer.md +140 -0
  70. package/src/overlays/Drawer.test.ts +92 -0
  71. package/src/overlays/Drawer.vue +76 -0
  72. package/src/overlays/Modal.demo.vue +149 -0
  73. package/src/overlays/Modal.md +385 -0
  74. package/src/overlays/Modal.test.ts +128 -0
  75. package/src/overlays/Modal.vue +86 -0
  76. package/src/overlays/Notification.md +150 -0
  77. package/src/overlays/Notification.test.ts +96 -0
  78. package/src/overlays/Notification.vue +58 -0
  79. package/src/overlays/index.ts +4 -0
@@ -0,0 +1,337 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue';
3
+ import { Stepper } from '@htlkg/components/navigation';
4
+ import { JsonSchemaForm } from '@htlkg/components/forms';
5
+
6
+ // Step interface (matches Stepper component)
7
+ interface Step {
8
+ id?: string | number;
9
+ label: string;
10
+ status?: 'complete' | 'current' | 'upcoming';
11
+ valid?: boolean;
12
+ }
13
+
14
+ // Current step
15
+ const currentStep = ref(0);
16
+
17
+ // Form data for each step
18
+ const step1Data = ref({ username: '', email: '', password: '' });
19
+ const step2Data = ref({ firstName: '', lastName: '', phone: '' });
20
+ const step3Data = ref({ language: 'en', timezone: 'UTC', notifications: true });
21
+
22
+ // Form schemas for each step
23
+ const step1Schema = {
24
+ type: 'object',
25
+ title: 'Account Information',
26
+ properties: {
27
+ username: { type: 'string', title: 'Username', minLength: 3 },
28
+ email: { type: 'string', title: 'Email', format: 'email' },
29
+ password: { type: 'string', title: 'Password', minLength: 6 }
30
+ },
31
+ required: ['username', 'email', 'password']
32
+ };
33
+
34
+ const step2Schema = {
35
+ type: 'object',
36
+ title: 'Personal Details',
37
+ properties: {
38
+ firstName: { type: 'string', title: 'First Name', minLength: 2 },
39
+ lastName: { type: 'string', title: 'Last Name', minLength: 2 },
40
+ phone: { type: 'string', title: 'Phone Number' }
41
+ },
42
+ required: ['firstName', 'lastName']
43
+ };
44
+
45
+ const step3Schema = {
46
+ type: 'object',
47
+ title: 'Preferences',
48
+ properties: {
49
+ language: {
50
+ type: 'string',
51
+ title: 'Language',
52
+ enum: ['en', 'es', 'fr', 'de']
53
+ },
54
+ timezone: {
55
+ type: 'string',
56
+ title: 'Timezone',
57
+ enum: ['UTC', 'EST', 'PST', 'CET']
58
+ },
59
+ notifications: { type: 'boolean', title: 'Enable Notifications' }
60
+ },
61
+ required: ['language', 'timezone']
62
+ };
63
+
64
+ // Form refs to access validation
65
+ const step1FormRef = ref<any>(null);
66
+ const step2FormRef = ref<any>(null);
67
+ const step3FormRef = ref<any>(null);
68
+
69
+ // Validate each step
70
+ const isStep1Valid = computed(() => {
71
+ return step1Data.value.username.length >= 3 &&
72
+ step1Data.value.email.includes('@') &&
73
+ step1Data.value.password.length >= 6;
74
+ });
75
+
76
+ const isStep2Valid = computed(() => {
77
+ return step2Data.value.firstName.length >= 2 &&
78
+ step2Data.value.lastName.length >= 2;
79
+ });
80
+
81
+ const isStep3Valid = computed(() => {
82
+ return !!(step3Data.value.language && step3Data.value.timezone);
83
+ });
84
+
85
+ // Steps configuration with validation
86
+ const steps = computed<Step[]>(() => [
87
+ { id: '01', label: 'Account', valid: isStep1Valid.value },
88
+ { id: '02', label: 'Personal', valid: isStep2Valid.value },
89
+ { id: '03', label: 'Preferences', valid: isStep3Valid.value },
90
+ { id: '04', label: 'Review', valid: true }
91
+ ]);
92
+
93
+ // Event log
94
+ const eventLog = ref<string[]>([]);
95
+
96
+ const logEvent = (message: string) => {
97
+ eventLog.value.unshift(`[${new Date().toLocaleTimeString()}] ${message}`);
98
+ if (eventLog.value.length > 5) {
99
+ eventLog.value = eventLog.value.slice(0, 5);
100
+ }
101
+ };
102
+
103
+ // Navigation handlers
104
+ const goToNext = () => {
105
+ if (currentStep.value < steps.value.length - 1) {
106
+ currentStep.value++;
107
+ logEvent(`Advanced to step ${currentStep.value + 1}`);
108
+ }
109
+ };
110
+
111
+ const goToPrevious = () => {
112
+ if (currentStep.value > 0) {
113
+ currentStep.value--;
114
+ logEvent(`Went back to step ${currentStep.value + 1}`);
115
+ }
116
+ };
117
+
118
+ const handleValidationFailed = (step: Step) => {
119
+ logEvent(`❌ Cannot proceed: "${step.label}" step has invalid data`);
120
+ };
121
+
122
+ const handleStepClick = (step: Step) => {
123
+ logEvent(`Clicked on step: ${step.label}`);
124
+ };
125
+
126
+ // Final submission
127
+ const handleFinalSubmit = () => {
128
+ logEvent('✅ Form submitted successfully!');
129
+ alert('Registration complete! Check the console for submitted data.');
130
+ console.log('Submitted data:', {
131
+ account: step1Data.value,
132
+ personal: step2Data.value,
133
+ preferences: step3Data.value
134
+ });
135
+ };
136
+ </script>
137
+
138
+ <template>
139
+ <div>
140
+ <!-- Info Banner -->
141
+ <div class="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
142
+ <p class="text-sm text-blue-800">
143
+ <strong>🔒 Validation Enabled:</strong> Fill out all required fields in each step to proceed.
144
+ You can only advance if the current step's form is valid.
145
+ </p>
146
+ </div>
147
+
148
+ <!-- Stepper Component -->
149
+ <div class="mb-8">
150
+ <Stepper
151
+ v-model:current-step="currentStep"
152
+ :steps="steps"
153
+ :validate-on-next="true"
154
+ @step-click="handleStepClick"
155
+ @validation-failed="handleValidationFailed"
156
+ />
157
+ </div>
158
+
159
+ <!-- Step Content -->
160
+ <div class="bg-white p-6 rounded-lg border mb-6">
161
+ <!-- Step 1: Account Information -->
162
+ <div v-if="currentStep === 0">
163
+ <JsonSchemaForm
164
+ ref="step1FormRef"
165
+ v-model="step1Data"
166
+ :schema="step1Schema"
167
+ :loading="false"
168
+ >
169
+ <template #actions>
170
+ <div class="flex justify-end">
171
+ <button
172
+ type="button"
173
+ @click="goToNext"
174
+ :disabled="!isStep1Valid"
175
+ class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
176
+ >
177
+ Next →
178
+ </button>
179
+ </div>
180
+ </template>
181
+ </JsonSchemaForm>
182
+ </div>
183
+
184
+ <!-- Step 2: Personal Details -->
185
+ <div v-else-if="currentStep === 1">
186
+ <JsonSchemaForm
187
+ ref="step2FormRef"
188
+ v-model="step2Data"
189
+ :schema="step2Schema"
190
+ :loading="false"
191
+ >
192
+ <template #actions>
193
+ <div class="flex justify-between">
194
+ <button
195
+ type="button"
196
+ @click="goToPrevious"
197
+ class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
198
+ >
199
+ ← Previous
200
+ </button>
201
+ <button
202
+ type="button"
203
+ @click="goToNext"
204
+ :disabled="!isStep2Valid"
205
+ class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
206
+ >
207
+ Next →
208
+ </button>
209
+ </div>
210
+ </template>
211
+ </JsonSchemaForm>
212
+ </div>
213
+
214
+ <!-- Step 3: Preferences -->
215
+ <div v-else-if="currentStep === 2">
216
+ <JsonSchemaForm
217
+ ref="step3FormRef"
218
+ v-model="step3Data"
219
+ :schema="step3Schema"
220
+ :loading="false"
221
+ >
222
+ <template #actions>
223
+ <div class="flex justify-between">
224
+ <button
225
+ type="button"
226
+ @click="goToPrevious"
227
+ class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
228
+ >
229
+ ← Previous
230
+ </button>
231
+ <button
232
+ type="button"
233
+ @click="goToNext"
234
+ :disabled="!isStep3Valid"
235
+ class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
236
+ >
237
+ Next →
238
+ </button>
239
+ </div>
240
+ </template>
241
+ </JsonSchemaForm>
242
+ </div>
243
+
244
+ <!-- Step 4: Review & Submit -->
245
+ <div v-else-if="currentStep === 3">
246
+ <h3 class="text-xl font-bold text-gray-900 mb-4">Review Your Information</h3>
247
+
248
+ <div class="space-y-6">
249
+ <!-- Account Info Review -->
250
+ <div class="p-4 bg-gray-50 rounded border">
251
+ <h4 class="font-semibold text-gray-900 mb-3">Account Information</h4>
252
+ <div class="space-y-2 text-sm">
253
+ <div><span class="text-gray-600">Username:</span> <span class="font-medium">{{ step1Data.username }}</span></div>
254
+ <div><span class="text-gray-600">Email:</span> <span class="font-medium">{{ step1Data.email }}</span></div>
255
+ <div><span class="text-gray-600">Password:</span> <span class="font-medium">••••••••</span></div>
256
+ </div>
257
+ </div>
258
+
259
+ <!-- Personal Info Review -->
260
+ <div class="p-4 bg-gray-50 rounded border">
261
+ <h4 class="font-semibold text-gray-900 mb-3">Personal Details</h4>
262
+ <div class="space-y-2 text-sm">
263
+ <div><span class="text-gray-600">First Name:</span> <span class="font-medium">{{ step2Data.firstName }}</span></div>
264
+ <div><span class="text-gray-600">Last Name:</span> <span class="font-medium">{{ step2Data.lastName }}</span></div>
265
+ <div><span class="text-gray-600">Phone:</span> <span class="font-medium">{{ step2Data.phone || 'Not provided' }}</span></div>
266
+ </div>
267
+ </div>
268
+
269
+ <!-- Preferences Review -->
270
+ <div class="p-4 bg-gray-50 rounded border">
271
+ <h4 class="font-semibold text-gray-900 mb-3">Preferences</h4>
272
+ <div class="space-y-2 text-sm">
273
+ <div><span class="text-gray-600">Language:</span> <span class="font-medium">{{ step3Data.language }}</span></div>
274
+ <div><span class="text-gray-600">Timezone:</span> <span class="font-medium">{{ step3Data.timezone }}</span></div>
275
+ <div><span class="text-gray-600">Notifications:</span> <span class="font-medium">{{ step3Data.notifications ? 'Enabled' : 'Disabled' }}</span></div>
276
+ </div>
277
+ </div>
278
+ </div>
279
+
280
+ <div class="flex justify-between mt-6">
281
+ <button
282
+ type="button"
283
+ @click="goToPrevious"
284
+ class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors"
285
+ >
286
+ ← Previous
287
+ </button>
288
+ <button
289
+ type="button"
290
+ @click="handleFinalSubmit"
291
+ class="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
292
+ >
293
+ ✓ Submit Registration
294
+ </button>
295
+ </div>
296
+ </div>
297
+ </div>
298
+
299
+ <!-- Validation Status -->
300
+ <div class="mb-6 p-4 bg-gray-50 rounded border">
301
+ <h4 class="text-sm font-semibold text-gray-900 mb-3">Step Validation Status:</h4>
302
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-3">
303
+ <div
304
+ v-for="(step, index) in steps"
305
+ :key="index"
306
+ :class="[
307
+ 'p-3 rounded border text-center',
308
+ step.valid
309
+ ? 'bg-green-50 border-green-300'
310
+ : 'bg-red-50 border-red-300'
311
+ ]"
312
+ >
313
+ <div class="text-2xl mb-1">{{ step.valid ? '✅' : '❌' }}</div>
314
+ <div class="text-xs font-medium text-gray-700">{{ step.label }}</div>
315
+ <div class="text-xs text-gray-500">{{ step.valid ? 'Valid' : 'Invalid' }}</div>
316
+ </div>
317
+ </div>
318
+ </div>
319
+
320
+ <!-- Event Log -->
321
+ <div class="p-4 bg-gray-50 rounded border">
322
+ <h4 class="text-sm font-semibold text-gray-900 mb-2">Event Log:</h4>
323
+ <div class="space-y-1">
324
+ <p
325
+ v-for="(event, index) in eventLog"
326
+ :key="index"
327
+ class="text-xs text-gray-700"
328
+ >
329
+ {{ event }}
330
+ </p>
331
+ <p v-if="eventLog.length === 0" class="text-xs text-gray-500 italic">
332
+ No events yet. Try navigating between steps or filling out forms.
333
+ </p>
334
+ </div>
335
+ </div>
336
+ </div>
337
+ </template>
@@ -0,0 +1,174 @@
1
+ # Stepper Component
2
+
3
+ A multi-step progress indicator for wizards and multi-page forms. Wraps `@hotelinking/ui`'s `uiStepsV4` component with validation support.
4
+
5
+ ## Features
6
+
7
+ - **Step validation**: Prevent navigation to invalid steps
8
+ - **v-model support**: Two-way binding for current step
9
+ - **Status tracking**: Complete, current, upcoming states
10
+ - **Skip control**: Allow or prevent skipping steps
11
+ - **Programmatic navigation**: Methods for step control
12
+ - **Event system**: Track step changes and validation
13
+
14
+ ## Import
15
+
16
+ ```typescript
17
+ import { Stepper } from '@htlkg/components';
18
+ // or
19
+ import { Stepper } from '@htlkg/components/navigation';
20
+ ```
21
+
22
+ ## Props
23
+
24
+ | Prop | Type | Default | Description |
25
+ |------|------|---------|-------------|
26
+ | `steps` | `Step[]` | required | Step definitions |
27
+ | `currentStep` | `number` | `0` | Current step index (v-model) |
28
+ | `validateOnNext` | `boolean` | `false` | Validate before moving forward |
29
+ | `allowSkip` | `boolean` | `false` | Allow skipping to any step |
30
+
31
+ ### Step Interface
32
+
33
+ ```typescript
34
+ interface Step {
35
+ id?: string | number;
36
+ label: string;
37
+ status?: 'complete' | 'current' | 'upcoming';
38
+ valid?: boolean;
39
+ }
40
+ ```
41
+
42
+ ## Events
43
+
44
+ | Event | Payload | Description |
45
+ |-------|---------|-------------|
46
+ | `update:currentStep` | `number` | Current step changed (v-model) |
47
+ | `step-click` | `Step, number` | Step clicked |
48
+ | `step-completed` | `Step` | Step marked as complete |
49
+ | `step-current` | `Step` | Step marked as current |
50
+ | `step-upcoming` | `Step` | Step marked as upcoming |
51
+ | `validation-failed` | `Step, number` | Validation failed for step |
52
+
53
+ ## Exposed Methods
54
+
55
+ | Method | Description |
56
+ |--------|-------------|
57
+ | `getCurrentStep()` | Get current step index |
58
+ | `getSteps()` | Get all steps |
59
+ | `goToStep(index)` | Navigate to specific step |
60
+ | `goToNext()` | Navigate to next step |
61
+ | `goToPrevious()` | Navigate to previous step |
62
+ | `validateCurrentStep()` | Check if current step is valid |
63
+ | `canNavigateToStep(index)` | Check if navigation is allowed |
64
+
65
+ ## Usage Examples
66
+
67
+ ### Basic Stepper
68
+
69
+ ```vue
70
+ <script setup>
71
+ import { ref } from 'vue';
72
+ import { Stepper } from '@htlkg/components';
73
+
74
+ const currentStep = ref(0);
75
+
76
+ const steps = [
77
+ { label: 'Account Info' },
78
+ { label: 'Personal Details' },
79
+ { label: 'Confirmation' }
80
+ ];
81
+ </script>
82
+
83
+ <template>
84
+ <Stepper
85
+ v-model:currentStep="currentStep"
86
+ :steps="steps"
87
+ />
88
+ </template>
89
+ ```
90
+
91
+ ### With Validation
92
+
93
+ ```vue
94
+ <script setup>
95
+ import { ref } from 'vue';
96
+ import { Stepper } from '@htlkg/components';
97
+
98
+ const currentStep = ref(0);
99
+ const formData = ref({
100
+ email: '',
101
+ name: '',
102
+ terms: false
103
+ });
104
+
105
+ const steps = ref([
106
+ { label: 'Email', valid: false },
107
+ { label: 'Profile', valid: false },
108
+ { label: 'Review', valid: true }
109
+ ]);
110
+
111
+ // Validate step 1
112
+ watch(() => formData.value.email, (email) => {
113
+ steps.value[0].valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
114
+ });
115
+
116
+ // Validate step 2
117
+ watch(() => formData.value.name, (name) => {
118
+ steps.value[1].valid = name.length >= 3;
119
+ });
120
+
121
+ const handleValidationFailed = (step, index) => {
122
+ alert(`Please complete ${step.label} before proceeding`);
123
+ };
124
+ </script>
125
+
126
+ <template>
127
+ <Stepper
128
+ v-model:currentStep="currentStep"
129
+ :steps="steps"
130
+ :validateOnNext="true"
131
+ @validation-failed="handleValidationFailed"
132
+ />
133
+ </template>
134
+ ```
135
+
136
+ ### Programmatic Navigation
137
+
138
+ ```vue
139
+ <script setup>
140
+ import { ref } from 'vue';
141
+ import { Stepper } from '@htlkg/components';
142
+
143
+ const stepperRef = ref();
144
+ const currentStep = ref(0);
145
+
146
+ const steps = [
147
+ { label: 'Step 1' },
148
+ { label: 'Step 2' },
149
+ { label: 'Step 3' }
150
+ ];
151
+
152
+ const next = () => stepperRef.value?.goToNext();
153
+ const previous = () => stepperRef.value?.goToPrevious();
154
+ const goTo = (index) => stepperRef.value?.goToStep(index);
155
+ </script>
156
+
157
+ <template>
158
+ <Stepper
159
+ ref="stepperRef"
160
+ v-model:currentStep="currentStep"
161
+ :steps="steps"
162
+ />
163
+
164
+ <div class="mt-4 space-x-2">
165
+ <button @click="previous">Previous</button>
166
+ <button @click="next">Next</button>
167
+ <button @click="goTo(2)">Skip to End</button>
168
+ </div>
169
+ </template>
170
+ ```
171
+
172
+ ## Demo
173
+
174
+ See the [Stepper demo page](/components/stepper) for interactive examples.
@@ -0,0 +1,146 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { uiStepsV4 } from '@hotelinking/ui';
4
+
5
+ export interface Step {
6
+ id?: string | number;
7
+ label: string;
8
+ status?: 'complete' | 'current' | 'upcoming';
9
+ valid?: boolean; // Validation state for the step
10
+ }
11
+
12
+ interface Props {
13
+ steps: Step[];
14
+ currentStep?: number;
15
+ validateOnNext?: boolean; // Enable validation when moving forward
16
+ allowSkip?: boolean; // Allow skipping to any step regardless of validation
17
+ }
18
+
19
+ const props = withDefaults(defineProps<Props>(), {
20
+ currentStep: 0,
21
+ validateOnNext: false,
22
+ allowSkip: false
23
+ });
24
+
25
+ const emit = defineEmits<{
26
+ 'step-click': [step: Step, index: number];
27
+ 'step-completed': [step: Step];
28
+ 'step-current': [step: Step];
29
+ 'step-upcoming': [step: Step];
30
+ 'validation-failed': [step: Step, index: number];
31
+ 'update:currentStep': [index: number]; // v-model support
32
+ }>();
33
+
34
+ // Transform steps to uiStepsV4 format
35
+ const uiSteps = computed(() =>
36
+ props.steps.map((step, index) => ({
37
+ id: step.id || String(index + 1).padStart(2, '0'),
38
+ name: step.label,
39
+ status: step.status || (
40
+ index < props.currentStep ? 'complete' :
41
+ index === props.currentStep ? 'current' :
42
+ 'upcoming'
43
+ )
44
+ }))
45
+ );
46
+
47
+ // Check if navigation to a step is allowed
48
+ function canNavigateToStep(targetIndex: number): boolean {
49
+ if (props.allowSkip) return true;
50
+ if (!props.validateOnNext) return true;
51
+ if (targetIndex < props.currentStep) return true;
52
+
53
+ const currentStepData = props.steps[props.currentStep];
54
+ if (targetIndex > props.currentStep && currentStepData.valid === false) {
55
+ return false;
56
+ }
57
+
58
+ for (let i = props.currentStep; i < targetIndex; i++) {
59
+ if (props.steps[i].valid === false) {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ return true;
65
+ }
66
+
67
+ // Handle uiStepsV4 events
68
+ function handleStepClick(step: any, index: number) {
69
+ const originalStep = props.steps[index];
70
+
71
+ if (!canNavigateToStep(index)) {
72
+ const currentStepData = props.steps[props.currentStep];
73
+ emit('validation-failed', currentStepData, props.currentStep);
74
+ return;
75
+ }
76
+
77
+ emit('step-click', originalStep, index);
78
+ emit('update:currentStep', index);
79
+ }
80
+
81
+ function handleStepCompleted(step: any) {
82
+ const index = uiSteps.value.findIndex(s => s.id === step.id);
83
+ if (index !== -1) {
84
+ emit('step-completed', props.steps[index]);
85
+ }
86
+ }
87
+
88
+ function handleStepCurrent(step: any) {
89
+ const index = uiSteps.value.findIndex(s => s.id === step.id);
90
+ if (index !== -1) {
91
+ emit('step-current', props.steps[index]);
92
+ }
93
+ }
94
+
95
+ function handleStepUpcoming(step: any) {
96
+ const index = uiSteps.value.findIndex(s => s.id === step.id);
97
+ if (index !== -1) {
98
+ emit('step-upcoming', props.steps[index]);
99
+ }
100
+ }
101
+
102
+ // Public methods for programmatic navigation
103
+ function goToStep(index: number): boolean {
104
+ if (index < 0 || index >= props.steps.length) return false;
105
+ if (!canNavigateToStep(index)) {
106
+ const currentStepData = props.steps[props.currentStep];
107
+ emit('validation-failed', currentStepData, props.currentStep);
108
+ return false;
109
+ }
110
+ emit('update:currentStep', index);
111
+ return true;
112
+ }
113
+
114
+ function goToNext(): boolean {
115
+ return goToStep(props.currentStep + 1);
116
+ }
117
+
118
+ function goToPrevious(): boolean {
119
+ return goToStep(props.currentStep - 1);
120
+ }
121
+
122
+ function validateCurrentStep(): boolean {
123
+ const currentStepData = props.steps[props.currentStep];
124
+ return currentStepData.valid !== false;
125
+ }
126
+
127
+ defineExpose({
128
+ getCurrentStep: () => props.currentStep,
129
+ getSteps: () => props.steps,
130
+ goToStep,
131
+ goToNext,
132
+ goToPrevious,
133
+ validateCurrentStep,
134
+ canNavigateToStep
135
+ });
136
+ </script>
137
+
138
+ <template>
139
+ <uiStepsV4
140
+ :steps="uiSteps"
141
+ @stepClick="handleStepClick"
142
+ @stepCompleted="handleStepCompleted"
143
+ @stepCurrent="handleStepCurrent"
144
+ @stepUpcoming="handleStepUpcoming"
145
+ />
146
+ </template>