@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,293 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue';
|
|
3
|
+
import { Tabs } from '@htlkg/components/navigation';
|
|
4
|
+
|
|
5
|
+
// Active tab state
|
|
6
|
+
const activeTab = ref('profile');
|
|
7
|
+
|
|
8
|
+
// Interactive controls
|
|
9
|
+
const showBadges = ref(true);
|
|
10
|
+
const tabCount = ref(4);
|
|
11
|
+
|
|
12
|
+
// All available tabs
|
|
13
|
+
const allTabs = [
|
|
14
|
+
{ id: 'profile', label: 'Profile', count: 0 },
|
|
15
|
+
{ id: 'settings', label: 'Settings', count: 3 },
|
|
16
|
+
{ id: 'notifications', label: 'Notifications', count: 12 },
|
|
17
|
+
{ id: 'security', label: 'Security', count: 0 },
|
|
18
|
+
{ id: 'billing', label: 'Billing', count: 1 },
|
|
19
|
+
{ id: 'team', label: 'Team', count: 5 }
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Tab configuration (computed based on controls)
|
|
23
|
+
const tabs = computed(() => {
|
|
24
|
+
const selectedTabs = allTabs.slice(0, tabCount.value);
|
|
25
|
+
return showBadges.value
|
|
26
|
+
? selectedTabs
|
|
27
|
+
: selectedTabs.map(tab => ({ ...tab, count: undefined }));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Sample data for each tab
|
|
31
|
+
const profileData = {
|
|
32
|
+
name: 'John Doe',
|
|
33
|
+
email: 'john.doe@example.com',
|
|
34
|
+
role: 'Administrator',
|
|
35
|
+
department: 'Engineering',
|
|
36
|
+
joinDate: 'January 15, 2023'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const settingsData = [
|
|
40
|
+
{ id: 1, setting: 'Email Notifications', enabled: true },
|
|
41
|
+
{ id: 2, setting: 'Push Notifications', enabled: false },
|
|
42
|
+
{ id: 3, setting: 'SMS Alerts', enabled: true }
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const notificationsData = [
|
|
46
|
+
{ id: 1, message: 'New user registered', time: '5 minutes ago', read: false },
|
|
47
|
+
{ id: 2, message: 'System update completed', time: '1 hour ago', read: false },
|
|
48
|
+
{ id: 3, message: 'Backup completed successfully', time: '2 hours ago', read: true },
|
|
49
|
+
{ id: 4, message: 'New comment on your post', time: '3 hours ago', read: false },
|
|
50
|
+
{ id: 5, message: 'Weekly report is ready', time: '1 day ago', read: true }
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const securityData = {
|
|
54
|
+
lastLogin: 'December 9, 2024 at 10:30 AM',
|
|
55
|
+
twoFactorEnabled: true,
|
|
56
|
+
activeSessions: 2,
|
|
57
|
+
passwordLastChanged: 'November 1, 2024'
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleTabChange = (tabId: string) => {
|
|
61
|
+
console.log('Tab changed to:', tabId);
|
|
62
|
+
};
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<div>
|
|
67
|
+
<!-- Interactive Controls Panel -->
|
|
68
|
+
<div class="mb-6 p-4 bg-purple-50 border border-purple-200 rounded-lg">
|
|
69
|
+
<h4 class="text-sm font-semibold text-purple-900 mb-3">🎮 Interactive Controls</h4>
|
|
70
|
+
|
|
71
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
72
|
+
<!-- Tab Count Control -->
|
|
73
|
+
<div>
|
|
74
|
+
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
75
|
+
Number of Tabs: {{ tabCount }}
|
|
76
|
+
</label>
|
|
77
|
+
<input
|
|
78
|
+
type="range"
|
|
79
|
+
v-model.number="tabCount"
|
|
80
|
+
min="2"
|
|
81
|
+
max="6"
|
|
82
|
+
class="w-full"
|
|
83
|
+
/>
|
|
84
|
+
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
|
85
|
+
<span>2</span>
|
|
86
|
+
<span>6</span>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<!-- Badge Toggle -->
|
|
91
|
+
<div>
|
|
92
|
+
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
93
|
+
Display Options:
|
|
94
|
+
</label>
|
|
95
|
+
<button
|
|
96
|
+
@click="showBadges = !showBadges"
|
|
97
|
+
:class="[
|
|
98
|
+
'px-3 py-2 text-sm rounded-md transition-colors',
|
|
99
|
+
showBadges
|
|
100
|
+
? 'bg-purple-600 text-white'
|
|
101
|
+
: 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'
|
|
102
|
+
]"
|
|
103
|
+
>
|
|
104
|
+
{{ showBadges ? '🔢 Badges ON' : '🔢 Badges OFF' }}
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Stats Display -->
|
|
111
|
+
<div class="mb-6 p-4 bg-blue-50 rounded border border-blue-200">
|
|
112
|
+
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
113
|
+
<div>
|
|
114
|
+
<span class="font-medium text-gray-700">Active Tab:</span>
|
|
115
|
+
<span class="ml-2 text-blue-600 font-semibold">{{ activeTab }}</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div>
|
|
118
|
+
<span class="font-medium text-gray-700">Total Tabs:</span>
|
|
119
|
+
<span class="ml-2 text-blue-600 font-semibold">{{ tabs.length }}</span>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- Tabs Component -->
|
|
125
|
+
<Tabs v-model="activeTab" :tabs="tabs" @tab-change="handleTabChange">
|
|
126
|
+
<!-- Profile Tab -->
|
|
127
|
+
<template #profile>
|
|
128
|
+
<div class="p-6 bg-white rounded-lg border">
|
|
129
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Profile Information</h3>
|
|
130
|
+
<div class="space-y-3">
|
|
131
|
+
<div class="flex justify-between py-2 border-b">
|
|
132
|
+
<span class="text-gray-600">Name:</span>
|
|
133
|
+
<span class="font-medium text-gray-900">{{ profileData.name }}</span>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="flex justify-between py-2 border-b">
|
|
136
|
+
<span class="text-gray-600">Email:</span>
|
|
137
|
+
<span class="font-medium text-gray-900">{{ profileData.email }}</span>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="flex justify-between py-2 border-b">
|
|
140
|
+
<span class="text-gray-600">Role:</span>
|
|
141
|
+
<span class="font-medium text-gray-900">{{ profileData.role }}</span>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="flex justify-between py-2 border-b">
|
|
144
|
+
<span class="text-gray-600">Department:</span>
|
|
145
|
+
<span class="font-medium text-gray-900">{{ profileData.department }}</span>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="flex justify-between py-2">
|
|
148
|
+
<span class="text-gray-600">Join Date:</span>
|
|
149
|
+
<span class="font-medium text-gray-900">{{ profileData.joinDate }}</span>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="mt-6">
|
|
153
|
+
<button
|
|
154
|
+
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
|
155
|
+
>
|
|
156
|
+
Edit Profile
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</template>
|
|
161
|
+
|
|
162
|
+
<!-- Settings Tab -->
|
|
163
|
+
<template #settings>
|
|
164
|
+
<div class="p-6 bg-white rounded-lg border">
|
|
165
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Notification Settings</h3>
|
|
166
|
+
<div class="space-y-4">
|
|
167
|
+
<div
|
|
168
|
+
v-for="setting in settingsData"
|
|
169
|
+
:key="setting.id"
|
|
170
|
+
class="flex items-center justify-between p-4 bg-gray-50 rounded"
|
|
171
|
+
>
|
|
172
|
+
<span class="text-gray-900">{{ setting.setting }}</span>
|
|
173
|
+
<label class="relative inline-flex items-center cursor-pointer">
|
|
174
|
+
<input
|
|
175
|
+
type="checkbox"
|
|
176
|
+
:checked="setting.enabled"
|
|
177
|
+
class="sr-only peer"
|
|
178
|
+
/>
|
|
179
|
+
<div
|
|
180
|
+
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"
|
|
181
|
+
></div>
|
|
182
|
+
</label>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="mt-6">
|
|
186
|
+
<button
|
|
187
|
+
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
|
188
|
+
>
|
|
189
|
+
Save Settings
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</template>
|
|
194
|
+
|
|
195
|
+
<!-- Notifications Tab -->
|
|
196
|
+
<template #notifications>
|
|
197
|
+
<div class="p-6 bg-white rounded-lg border">
|
|
198
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Recent Notifications</h3>
|
|
199
|
+
<div class="space-y-2">
|
|
200
|
+
<div
|
|
201
|
+
v-for="notification in notificationsData"
|
|
202
|
+
:key="notification.id"
|
|
203
|
+
class="flex items-start justify-between p-4 rounded transition-colors"
|
|
204
|
+
:class="notification.read ? 'bg-gray-50' : 'bg-blue-50'"
|
|
205
|
+
>
|
|
206
|
+
<div class="flex-1">
|
|
207
|
+
<p
|
|
208
|
+
class="text-sm"
|
|
209
|
+
:class="notification.read ? 'text-gray-600' : 'text-gray-900 font-medium'"
|
|
210
|
+
>
|
|
211
|
+
{{ notification.message }}
|
|
212
|
+
</p>
|
|
213
|
+
<p class="text-xs text-gray-500 mt-1">{{ notification.time }}</p>
|
|
214
|
+
</div>
|
|
215
|
+
<span
|
|
216
|
+
v-if="!notification.read"
|
|
217
|
+
class="ml-4 w-2 h-2 bg-blue-600 rounded-full"
|
|
218
|
+
></span>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="mt-6 flex gap-2">
|
|
222
|
+
<button
|
|
223
|
+
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
|
224
|
+
>
|
|
225
|
+
Mark All as Read
|
|
226
|
+
</button>
|
|
227
|
+
<button
|
|
228
|
+
class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition-colors"
|
|
229
|
+
>
|
|
230
|
+
Clear All
|
|
231
|
+
</button>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</template>
|
|
235
|
+
|
|
236
|
+
<!-- Security Tab -->
|
|
237
|
+
<template #security>
|
|
238
|
+
<div class="p-6 bg-white rounded-lg border">
|
|
239
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Security Settings</h3>
|
|
240
|
+
<div class="space-y-4">
|
|
241
|
+
<div class="p-4 bg-gray-50 rounded">
|
|
242
|
+
<div class="flex justify-between items-center mb-2">
|
|
243
|
+
<span class="font-medium text-gray-900">Last Login</span>
|
|
244
|
+
<span class="text-sm text-gray-600">{{ securityData.lastLogin }}</span>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
<div class="p-4 bg-gray-50 rounded">
|
|
248
|
+
<div class="flex justify-between items-center mb-2">
|
|
249
|
+
<span class="font-medium text-gray-900">Two-Factor Authentication</span>
|
|
250
|
+
<span
|
|
251
|
+
class="px-2 py-1 text-xs rounded"
|
|
252
|
+
:class="
|
|
253
|
+
securityData.twoFactorEnabled
|
|
254
|
+
? 'bg-green-100 text-green-800'
|
|
255
|
+
: 'bg-red-100 text-red-800'
|
|
256
|
+
"
|
|
257
|
+
>
|
|
258
|
+
{{ securityData.twoFactorEnabled ? 'Enabled' : 'Disabled' }}
|
|
259
|
+
</span>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="p-4 bg-gray-50 rounded">
|
|
263
|
+
<div class="flex justify-between items-center mb-2">
|
|
264
|
+
<span class="font-medium text-gray-900">Active Sessions</span>
|
|
265
|
+
<span class="text-sm text-gray-600">{{ securityData.activeSessions }}</span>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="p-4 bg-gray-50 rounded">
|
|
269
|
+
<div class="flex justify-between items-center mb-2">
|
|
270
|
+
<span class="font-medium text-gray-900">Password Last Changed</span>
|
|
271
|
+
<span class="text-sm text-gray-600">{{
|
|
272
|
+
securityData.passwordLastChanged
|
|
273
|
+
}}</span>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="mt-6 flex gap-2">
|
|
278
|
+
<button
|
|
279
|
+
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
|
280
|
+
>
|
|
281
|
+
Change Password
|
|
282
|
+
</button>
|
|
283
|
+
<button
|
|
284
|
+
class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition-colors"
|
|
285
|
+
>
|
|
286
|
+
View All Sessions
|
|
287
|
+
</button>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
</template>
|
|
291
|
+
</Tabs>
|
|
292
|
+
</div>
|
|
293
|
+
</template>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Tabs Component
|
|
2
|
+
|
|
3
|
+
A tabbed interface component for organizing content into separate views.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **v-model support**: Two-way binding for active tab
|
|
8
|
+
- **Badge counts**: Optional count badges on tabs
|
|
9
|
+
- **Slot-based content**: Named slots for each tab
|
|
10
|
+
- **Programmatic navigation**: Methods for tab control
|
|
11
|
+
- **Keyboard navigation**: Arrow keys for tab switching
|
|
12
|
+
|
|
13
|
+
## Import
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Tabs } from '@htlkg/components';
|
|
17
|
+
// or
|
|
18
|
+
import { Tabs } from '@htlkg/components/navigation';
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Props
|
|
22
|
+
|
|
23
|
+
| Prop | Type | Default | Description |
|
|
24
|
+
|------|------|---------|-------------|
|
|
25
|
+
| `tabs` | `Tab[]` | required | Tab definitions |
|
|
26
|
+
| `modelValue` | `string` | `''` | Active tab ID (v-model) |
|
|
27
|
+
|
|
28
|
+
### Tab Interface
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
interface Tab {
|
|
32
|
+
id: string;
|
|
33
|
+
label: string;
|
|
34
|
+
count?: number;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Events
|
|
39
|
+
|
|
40
|
+
| Event | Payload | Description |
|
|
41
|
+
|-------|---------|-------------|
|
|
42
|
+
| `update:modelValue` | `string` | Active tab changed (v-model) |
|
|
43
|
+
| `tab-change` | `string` | Tab changed |
|
|
44
|
+
|
|
45
|
+
## Slots
|
|
46
|
+
|
|
47
|
+
Named slots for each tab using the tab's `id` as the slot name. Each slot receives:
|
|
48
|
+
- `active-tab`: The currently active tab ID
|
|
49
|
+
|
|
50
|
+
## Exposed Methods
|
|
51
|
+
|
|
52
|
+
| Method | Description |
|
|
53
|
+
|--------|-------------|
|
|
54
|
+
| `setActiveTab(tabId)` | Set active tab |
|
|
55
|
+
| `getActiveTab()` | Get active tab ID |
|
|
56
|
+
| `nextTab()` | Navigate to next tab |
|
|
57
|
+
| `previousTab()` | Navigate to previous tab |
|
|
58
|
+
|
|
59
|
+
## Usage Examples
|
|
60
|
+
|
|
61
|
+
### Basic Tabs
|
|
62
|
+
|
|
63
|
+
```vue
|
|
64
|
+
<script setup>
|
|
65
|
+
import { ref } from 'vue';
|
|
66
|
+
import { Tabs } from '@htlkg/components';
|
|
67
|
+
|
|
68
|
+
const activeTab = ref('profile');
|
|
69
|
+
|
|
70
|
+
const tabs = [
|
|
71
|
+
{ id: 'profile', label: 'Profile' },
|
|
72
|
+
{ id: 'settings', label: 'Settings' },
|
|
73
|
+
{ id: 'notifications', label: 'Notifications' }
|
|
74
|
+
];
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<Tabs v-model="activeTab" :tabs="tabs">
|
|
79
|
+
<template #profile>
|
|
80
|
+
<div>Profile content here</div>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<template #settings>
|
|
84
|
+
<div>Settings content here</div>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<template #notifications>
|
|
88
|
+
<div>Notifications content here</div>
|
|
89
|
+
</template>
|
|
90
|
+
</Tabs>
|
|
91
|
+
</template>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Tabs with Counts
|
|
95
|
+
|
|
96
|
+
```vue
|
|
97
|
+
<script setup>
|
|
98
|
+
import { ref } from 'vue';
|
|
99
|
+
import { Tabs } from '@htlkg/components';
|
|
100
|
+
|
|
101
|
+
const activeTab = ref('all');
|
|
102
|
+
|
|
103
|
+
const tabs = [
|
|
104
|
+
{ id: 'all', label: 'All', count: 24 },
|
|
105
|
+
{ id: 'active', label: 'Active', count: 18 },
|
|
106
|
+
{ id: 'archived', label: 'Archived', count: 6 }
|
|
107
|
+
];
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<template>
|
|
111
|
+
<Tabs v-model="activeTab" :tabs="tabs">
|
|
112
|
+
<template #all>
|
|
113
|
+
<div>All items (24)</div>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<template #active>
|
|
117
|
+
<div>Active items (18)</div>
|
|
118
|
+
</template>
|
|
119
|
+
|
|
120
|
+
<template #archived>
|
|
121
|
+
<div>Archived items (6)</div>
|
|
122
|
+
</template>
|
|
123
|
+
</Tabs>
|
|
124
|
+
</template>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Programmatic Control
|
|
128
|
+
|
|
129
|
+
```vue
|
|
130
|
+
<script setup>
|
|
131
|
+
import { ref } from 'vue';
|
|
132
|
+
import { Tabs } from '@htlkg/components';
|
|
133
|
+
|
|
134
|
+
const tabsRef = ref();
|
|
135
|
+
const activeTab = ref('tab1');
|
|
136
|
+
|
|
137
|
+
const tabs = [
|
|
138
|
+
{ id: 'tab1', label: 'Tab 1' },
|
|
139
|
+
{ id: 'tab2', label: 'Tab 2' },
|
|
140
|
+
{ id: 'tab3', label: 'Tab 3' }
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const next = () => tabsRef.value?.nextTab();
|
|
144
|
+
const previous = () => tabsRef.value?.previousTab();
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<template>
|
|
148
|
+
<Tabs ref="tabsRef" v-model="activeTab" :tabs="tabs">
|
|
149
|
+
<template #tab1>Content 1</template>
|
|
150
|
+
<template #tab2>Content 2</template>
|
|
151
|
+
<template #tab3>Content 3</template>
|
|
152
|
+
</Tabs>
|
|
153
|
+
|
|
154
|
+
<div class="mt-4 space-x-2">
|
|
155
|
+
<button @click="previous">Previous</button>
|
|
156
|
+
<button @click="next">Next</button>
|
|
157
|
+
</div>
|
|
158
|
+
</template>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Demo
|
|
162
|
+
|
|
163
|
+
See the [Tabs demo page](/components/tabs) for interactive examples.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import Tabs from './Tabs.vue';
|
|
4
|
+
|
|
5
|
+
describe('Tabs Component', () => {
|
|
6
|
+
const mockTabs = [
|
|
7
|
+
{ id: 'tab1', label: 'Tab 1', count: 5 },
|
|
8
|
+
{ id: 'tab2', label: 'Tab 2', count: 10 },
|
|
9
|
+
{ id: 'tab3', label: 'Tab 3' }
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
it('renders with basic props', () => {
|
|
13
|
+
const wrapper = mount(Tabs, {
|
|
14
|
+
props: {
|
|
15
|
+
tabs: mockTabs
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(wrapper.exists()).toBe(true);
|
|
20
|
+
expect(wrapper.find('.tabs-wrapper').exists()).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('supports v-model for activeTab', async () => {
|
|
24
|
+
const wrapper = mount(Tabs, {
|
|
25
|
+
props: {
|
|
26
|
+
tabs: mockTabs,
|
|
27
|
+
activeTab: 'tab1',
|
|
28
|
+
'onUpdate:activeTab': (value: string) => wrapper.setProps({ activeTab: value })
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(wrapper.props('activeTab')).toBe('tab1');
|
|
33
|
+
|
|
34
|
+
// Change active tab
|
|
35
|
+
const component = wrapper.vm as any;
|
|
36
|
+
component.handleTabClick('tab2');
|
|
37
|
+
|
|
38
|
+
await wrapper.vm.$nextTick();
|
|
39
|
+
expect(wrapper.emitted('update:activeTab')).toBeTruthy();
|
|
40
|
+
expect(wrapper.emitted('update:activeTab')?.[0]).toEqual(['tab2']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('emits tab-change event when tab is clicked', async () => {
|
|
44
|
+
const wrapper = mount(Tabs, {
|
|
45
|
+
props: {
|
|
46
|
+
tabs: mockTabs,
|
|
47
|
+
activeTab: 'tab1'
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const component = wrapper.vm as any;
|
|
52
|
+
component.handleTabClick('tab2');
|
|
53
|
+
|
|
54
|
+
expect(wrapper.emitted('tab-change')).toBeTruthy();
|
|
55
|
+
expect(wrapper.emitted('tab-change')?.[0]).toEqual(['tab2']);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('defaults to first tab when no activeTab is provided', () => {
|
|
59
|
+
const wrapper = mount(Tabs, {
|
|
60
|
+
props: {
|
|
61
|
+
tabs: mockTabs
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const component = wrapper.vm as any;
|
|
66
|
+
expect(component.currentTab).toBe('tab1');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('exposes setActiveTab method', () => {
|
|
70
|
+
const wrapper = mount(Tabs, {
|
|
71
|
+
props: {
|
|
72
|
+
tabs: mockTabs,
|
|
73
|
+
activeTab: 'tab1'
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const component = wrapper.vm as any;
|
|
78
|
+
expect(component.setActiveTab).toBeDefined();
|
|
79
|
+
expect(typeof component.setActiveTab).toBe('function');
|
|
80
|
+
|
|
81
|
+
component.setActiveTab('tab3');
|
|
82
|
+
expect(wrapper.emitted('update:activeTab')).toBeTruthy();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('exposes getActiveTab method', () => {
|
|
86
|
+
const wrapper = mount(Tabs, {
|
|
87
|
+
props: {
|
|
88
|
+
tabs: mockTabs,
|
|
89
|
+
activeTab: 'tab2'
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const component = wrapper.vm as any;
|
|
94
|
+
expect(component.getActiveTab).toBeDefined();
|
|
95
|
+
expect(typeof component.getActiveTab).toBe('function');
|
|
96
|
+
expect(component.getActiveTab()).toBe('tab2');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('exposes nextTab method', async () => {
|
|
100
|
+
const wrapper = mount(Tabs, {
|
|
101
|
+
props: {
|
|
102
|
+
tabs: mockTabs,
|
|
103
|
+
activeTab: 'tab1'
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const component = wrapper.vm as any;
|
|
108
|
+
component.nextTab();
|
|
109
|
+
|
|
110
|
+
await wrapper.vm.$nextTick();
|
|
111
|
+
expect(wrapper.emitted('update:activeTab')).toBeTruthy();
|
|
112
|
+
expect(wrapper.emitted('update:activeTab')?.[0]).toEqual(['tab2']);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('exposes previousTab method', async () => {
|
|
116
|
+
const wrapper = mount(Tabs, {
|
|
117
|
+
props: {
|
|
118
|
+
tabs: mockTabs,
|
|
119
|
+
activeTab: 'tab2'
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const component = wrapper.vm as any;
|
|
124
|
+
component.previousTab();
|
|
125
|
+
|
|
126
|
+
await wrapper.vm.$nextTick();
|
|
127
|
+
expect(wrapper.emitted('update:activeTab')).toBeTruthy();
|
|
128
|
+
expect(wrapper.emitted('update:activeTab')?.[0]).toEqual(['tab1']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('does not go beyond last tab with nextTab', async () => {
|
|
132
|
+
const wrapper = mount(Tabs, {
|
|
133
|
+
props: {
|
|
134
|
+
tabs: mockTabs,
|
|
135
|
+
activeTab: 'tab3'
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const component = wrapper.vm as any;
|
|
140
|
+
component.nextTab();
|
|
141
|
+
|
|
142
|
+
await wrapper.vm.$nextTick();
|
|
143
|
+
// Should not emit since we're already at the last tab
|
|
144
|
+
expect(component.getActiveTab()).toBe('tab3');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('does not go before first tab with previousTab', async () => {
|
|
148
|
+
const wrapper = mount(Tabs, {
|
|
149
|
+
props: {
|
|
150
|
+
tabs: mockTabs,
|
|
151
|
+
activeTab: 'tab1'
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const component = wrapper.vm as any;
|
|
156
|
+
component.previousTab();
|
|
157
|
+
|
|
158
|
+
await wrapper.vm.$nextTick();
|
|
159
|
+
// Should not emit since we're already at the first tab
|
|
160
|
+
expect(component.getActiveTab()).toBe('tab1');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('renders tab content slot', () => {
|
|
164
|
+
const wrapper = mount(Tabs, {
|
|
165
|
+
props: {
|
|
166
|
+
tabs: mockTabs,
|
|
167
|
+
activeTab: 'tab1'
|
|
168
|
+
},
|
|
169
|
+
slots: {
|
|
170
|
+
tab1: '<div class="tab1-content">Tab 1 Content</div>'
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(wrapper.find('.tabs-content').exists()).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
});
|