@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.
- package/dist/composables/index.js +388 -0
- package/dist/composables/index.js.map +1 -0
- package/package.json +41 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/useForm.test.ts +229 -0
- package/src/composables/useForm.ts +130 -0
- package/src/composables/useFormValidation.test.ts +189 -0
- package/src/composables/useFormValidation.ts +83 -0
- package/src/composables/useModal.property.test.ts +164 -0
- package/src/composables/useModal.ts +43 -0
- package/src/composables/useNotifications.test.ts +166 -0
- package/src/composables/useNotifications.ts +81 -0
- package/src/composables/useTable.property.test.ts +198 -0
- package/src/composables/useTable.ts +134 -0
- package/src/composables/useTabs.property.test.ts +247 -0
- package/src/composables/useTabs.ts +101 -0
- package/src/data/Chart.demo.vue +340 -0
- package/src/data/Chart.md +525 -0
- package/src/data/Chart.vue +133 -0
- package/src/data/DataList.md +80 -0
- package/src/data/DataList.test.ts +69 -0
- package/src/data/DataList.vue +46 -0
- package/src/data/SearchableSelect.md +107 -0
- package/src/data/SearchableSelect.vue +124 -0
- package/src/data/Table.demo.vue +296 -0
- package/src/data/Table.md +588 -0
- package/src/data/Table.property.test.ts +548 -0
- package/src/data/Table.test.ts +562 -0
- package/src/data/Table.unit.test.ts +544 -0
- package/src/data/Table.vue +321 -0
- package/src/data/index.ts +5 -0
- package/src/domain/BrandCard.md +81 -0
- package/src/domain/BrandCard.vue +63 -0
- package/src/domain/BrandSelector.md +84 -0
- package/src/domain/BrandSelector.vue +65 -0
- package/src/domain/ProductBadge.md +60 -0
- package/src/domain/ProductBadge.vue +47 -0
- package/src/domain/UserAvatar.md +84 -0
- package/src/domain/UserAvatar.vue +60 -0
- package/src/domain/domain-components.property.test.ts +449 -0
- package/src/domain/index.ts +4 -0
- package/src/forms/DateRange.demo.vue +273 -0
- package/src/forms/DateRange.md +337 -0
- package/src/forms/DateRange.vue +110 -0
- package/src/forms/JsonSchemaForm.demo.vue +549 -0
- package/src/forms/JsonSchemaForm.md +112 -0
- package/src/forms/JsonSchemaForm.property.test.ts +817 -0
- package/src/forms/JsonSchemaForm.test.ts +601 -0
- package/src/forms/JsonSchemaForm.unit.test.ts +801 -0
- package/src/forms/JsonSchemaForm.vue +615 -0
- package/src/forms/index.ts +3 -0
- package/src/index.ts +17 -0
- package/src/navigation/Breadcrumbs.demo.vue +142 -0
- package/src/navigation/Breadcrumbs.md +102 -0
- package/src/navigation/Breadcrumbs.test.ts +69 -0
- package/src/navigation/Breadcrumbs.vue +58 -0
- package/src/navigation/Stepper.demo.vue +337 -0
- package/src/navigation/Stepper.md +174 -0
- package/src/navigation/Stepper.vue +146 -0
- package/src/navigation/Tabs.demo.vue +293 -0
- package/src/navigation/Tabs.md +163 -0
- package/src/navigation/Tabs.test.ts +176 -0
- package/src/navigation/Tabs.vue +104 -0
- package/src/navigation/index.ts +5 -0
- package/src/overlays/Alert.demo.vue +377 -0
- package/src/overlays/Alert.md +248 -0
- package/src/overlays/Alert.test.ts +166 -0
- package/src/overlays/Alert.vue +70 -0
- package/src/overlays/Drawer.md +140 -0
- package/src/overlays/Drawer.test.ts +92 -0
- package/src/overlays/Drawer.vue +76 -0
- package/src/overlays/Modal.demo.vue +149 -0
- package/src/overlays/Modal.md +385 -0
- package/src/overlays/Modal.test.ts +128 -0
- package/src/overlays/Modal.vue +86 -0
- package/src/overlays/Notification.md +150 -0
- package/src/overlays/Notification.test.ts +96 -0
- package/src/overlays/Notification.vue +58 -0
- 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>
|