@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,134 @@
1
+ import { ref, computed, type Ref, type ComputedRef } from 'vue';
2
+
3
+ export interface UseTableOptions<T> {
4
+ items: T[];
5
+ pageSize?: number;
6
+ sortKey?: string;
7
+ sortOrder?: 'asc' | 'desc';
8
+ }
9
+
10
+ export interface UseTableReturn<T> {
11
+ currentPage: Ref<number>;
12
+ pageSize: Ref<number>;
13
+ sortKey: Ref<string>;
14
+ sortOrder: Ref<'asc' | 'desc'>;
15
+ selectedItems: Ref<T[]>;
16
+ paginatedItems: ComputedRef<T[]>;
17
+ totalPages: ComputedRef<number>;
18
+ sortedItems: ComputedRef<T[]>;
19
+ setPage: (page: number) => void;
20
+ setPageSize: (size: number) => void;
21
+ setSorting: (key: string, order?: 'asc' | 'desc') => void;
22
+ selectItem: (item: T) => void;
23
+ deselectItem: (item: T) => void;
24
+ selectAll: () => void;
25
+ clearSelection: () => void;
26
+ isSelected: (item: T) => boolean;
27
+ }
28
+
29
+ export function useTable<T extends Record<string, any>>(
30
+ options: UseTableOptions<T>
31
+ ): UseTableReturn<T> {
32
+ const currentPage = ref(1);
33
+ const pageSize = ref(options.pageSize ?? 10);
34
+ const sortKey = ref(options.sortKey ?? '');
35
+ const sortOrder = ref<'asc' | 'desc'>(options.sortOrder ?? 'asc');
36
+ const selectedItems = ref<T[]>([]);
37
+
38
+ const sortedItems = computed(() => {
39
+ if (!sortKey.value) return options.items;
40
+
41
+ return [...options.items].sort((a, b) => {
42
+ const aVal = a[sortKey.value];
43
+ const bVal = b[sortKey.value];
44
+ const order = sortOrder.value === 'asc' ? 1 : -1;
45
+
46
+ if (aVal === bVal) return 0;
47
+ if (aVal == null) return 1;
48
+ if (bVal == null) return -1;
49
+
50
+ return aVal > bVal ? order : -order;
51
+ });
52
+ });
53
+
54
+ const paginatedItems = computed(() => {
55
+ const start = (currentPage.value - 1) * pageSize.value;
56
+ const end = start + pageSize.value;
57
+ return sortedItems.value.slice(start, end);
58
+ });
59
+
60
+ const totalPages = computed(() =>
61
+ Math.ceil(options.items.length / pageSize.value)
62
+ );
63
+
64
+ function setPage(page: number) {
65
+ if (page >= 1 && page <= totalPages.value) {
66
+ currentPage.value = page;
67
+ }
68
+ }
69
+
70
+ function setPageSize(size: number) {
71
+ pageSize.value = size;
72
+ currentPage.value = 1; // Reset to first page
73
+ }
74
+
75
+ function setSorting(key: string, order: 'asc' | 'desc' = 'asc') {
76
+ if (sortKey.value === key) {
77
+ // Toggle order if same key
78
+ sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
79
+ } else {
80
+ sortKey.value = key;
81
+ sortOrder.value = order;
82
+ }
83
+ }
84
+
85
+ function selectItem(item: T) {
86
+ if (!isSelected(item)) {
87
+ selectedItems.value.push(item);
88
+ }
89
+ }
90
+
91
+ function deselectItem(item: T) {
92
+ // Use deep comparison for objects
93
+ const index = selectedItems.value.findIndex(i =>
94
+ JSON.stringify(i) === JSON.stringify(item)
95
+ );
96
+ if (index !== -1) {
97
+ selectedItems.value.splice(index, 1);
98
+ }
99
+ }
100
+
101
+ function selectAll() {
102
+ selectedItems.value = [...options.items];
103
+ }
104
+
105
+ function clearSelection() {
106
+ selectedItems.value = [];
107
+ }
108
+
109
+ function isSelected(item: T): boolean {
110
+ // Use deep comparison for objects
111
+ return selectedItems.value.some(i =>
112
+ JSON.stringify(i) === JSON.stringify(item)
113
+ );
114
+ }
115
+
116
+ return {
117
+ currentPage,
118
+ pageSize,
119
+ sortKey,
120
+ sortOrder,
121
+ selectedItems,
122
+ paginatedItems,
123
+ totalPages,
124
+ sortedItems,
125
+ setPage,
126
+ setPageSize,
127
+ setSorting,
128
+ selectItem,
129
+ deselectItem,
130
+ selectAll,
131
+ clearSelection,
132
+ isSelected
133
+ };
134
+ }
@@ -0,0 +1,247 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as fc from 'fast-check';
3
+ import { useTabs, type Tab } from './useTabs';
4
+
5
+ /**
6
+ * Feature: htlkg-modular-architecture, Property 9: Components support v-model
7
+ * Validates: Requirements 12.3
8
+ */
9
+ describe('useTabs property tests', () => {
10
+ it('should manage active tab state reactively', () => {
11
+ fc.assert(
12
+ fc.property(
13
+ fc.array(
14
+ fc.record({
15
+ id: fc.string({ minLength: 1 }),
16
+ label: fc.string(),
17
+ disabled: fc.boolean()
18
+ }),
19
+ { minLength: 1, maxLength: 10 }
20
+ ),
21
+ (tabs) => {
22
+ // Ensure unique IDs
23
+ const uniqueTabs = tabs.map((tab, index) => ({
24
+ ...tab,
25
+ id: `tab-${index}`
26
+ }));
27
+
28
+ const { activeTab, setActiveTab, isTabActive } = useTabs({
29
+ tabs: uniqueTabs
30
+ });
31
+
32
+ // Property: Initial active tab should be the first tab
33
+ expect(activeTab.value).toBe(uniqueTabs[0].id);
34
+ expect(isTabActive(uniqueTabs[0].id)).toBe(true);
35
+
36
+ // Property: Setting active tab should update state
37
+ if (uniqueTabs.length > 1) {
38
+ const targetTab = uniqueTabs[1];
39
+ if (!targetTab.disabled) {
40
+ setActiveTab(targetTab.id);
41
+ expect(activeTab.value).toBe(targetTab.id);
42
+ expect(isTabActive(targetTab.id)).toBe(true);
43
+ }
44
+ }
45
+ }
46
+ ),
47
+ { numRuns: 100 }
48
+ );
49
+ });
50
+
51
+ it('should navigate between tabs correctly', () => {
52
+ fc.assert(
53
+ fc.property(
54
+ fc.integer({ min: 2, max: 10 }),
55
+ (tabCount) => {
56
+ const tabs: Tab[] = Array.from({ length: tabCount }, (_, i) => ({
57
+ id: `tab-${i}`,
58
+ label: `Tab ${i}`,
59
+ disabled: false
60
+ }));
61
+
62
+ const { activeTab, nextTab, previousTab, isFirstTab, isLastTab } = useTabs({
63
+ tabs
64
+ });
65
+
66
+ // Property: Initially on first tab
67
+ expect(activeTab.value).toBe(tabs[0].id);
68
+ expect(isFirstTab.value).toBe(true);
69
+ expect(isLastTab.value).toBe(false);
70
+
71
+ // Property: nextTab should move to next tab
72
+ nextTab();
73
+ expect(activeTab.value).toBe(tabs[1].id);
74
+ expect(isFirstTab.value).toBe(false);
75
+
76
+ // Property: previousTab should move back
77
+ previousTab();
78
+ expect(activeTab.value).toBe(tabs[0].id);
79
+ expect(isFirstTab.value).toBe(true);
80
+
81
+ // Property: Navigate to last tab
82
+ for (let i = 0; i < tabCount - 1; i++) {
83
+ nextTab();
84
+ }
85
+ expect(activeTab.value).toBe(tabs[tabCount - 1].id);
86
+ expect(isLastTab.value).toBe(true);
87
+
88
+ // Property: nextTab on last tab should not change state
89
+ const lastTabId = activeTab.value;
90
+ nextTab();
91
+ expect(activeTab.value).toBe(lastTabId);
92
+ }
93
+ ),
94
+ { numRuns: 100 }
95
+ );
96
+ });
97
+
98
+ it('should handle disabled tabs correctly', () => {
99
+ fc.assert(
100
+ fc.property(
101
+ fc.integer({ min: 3, max: 10 }),
102
+ fc.integer({ min: 1, max: 8 }),
103
+ (tabCount, disabledIndex) => {
104
+ const validDisabledIndex = Math.min(disabledIndex, tabCount - 1);
105
+
106
+ const tabs: Tab[] = Array.from({ length: tabCount }, (_, i) => ({
107
+ id: `tab-${i}`,
108
+ label: `Tab ${i}`,
109
+ disabled: i === validDisabledIndex
110
+ }));
111
+
112
+ const { setActiveTab, activeTab, isTabDisabled } = useTabs({
113
+ tabs
114
+ });
115
+
116
+ // Property: Disabled tab should be marked as disabled
117
+ expect(isTabDisabled(tabs[validDisabledIndex].id)).toBe(true);
118
+
119
+ // Property: Setting active tab to disabled tab should not change state
120
+ const initialTab = activeTab.value;
121
+ setActiveTab(tabs[validDisabledIndex].id);
122
+ expect(activeTab.value).toBe(initialTab);
123
+
124
+ // Property: Setting active tab to enabled tab should work
125
+ const enabledTabIndex = validDisabledIndex === 0 ? 1 : 0;
126
+ setActiveTab(tabs[enabledTabIndex].id);
127
+ expect(activeTab.value).toBe(tabs[enabledTabIndex].id);
128
+ }
129
+ ),
130
+ { numRuns: 100 }
131
+ );
132
+ });
133
+
134
+ it('should maintain tab index consistency', () => {
135
+ fc.assert(
136
+ fc.property(
137
+ fc.array(
138
+ fc.record({
139
+ id: fc.string({ minLength: 1 }),
140
+ label: fc.string(),
141
+ disabled: fc.boolean()
142
+ }),
143
+ { minLength: 1, maxLength: 10 }
144
+ ),
145
+ (tabs) => {
146
+ // Ensure unique IDs
147
+ const uniqueTabs = tabs.map((tab, index) => ({
148
+ ...tab,
149
+ id: `tab-${index}`
150
+ }));
151
+
152
+ const { activeTab, currentTabIndex, setActiveTab } = useTabs({
153
+ tabs: uniqueTabs
154
+ });
155
+
156
+ // Property: Current tab index should match active tab position
157
+ const expectedIndex = uniqueTabs.findIndex(t => t.id === activeTab.value);
158
+ expect(currentTabIndex.value).toBe(expectedIndex);
159
+
160
+ // Property: After setting active tab, index should update
161
+ if (uniqueTabs.length > 1) {
162
+ const targetIndex = uniqueTabs.length > 2 ? 2 : 1;
163
+ const targetTab = uniqueTabs[targetIndex];
164
+ if (!targetTab.disabled) {
165
+ setActiveTab(targetTab.id);
166
+ expect(currentTabIndex.value).toBe(targetIndex);
167
+ }
168
+ }
169
+ }
170
+ ),
171
+ { numRuns: 100 }
172
+ );
173
+ });
174
+
175
+ it('should support v-model pattern with reactive updates', () => {
176
+ fc.assert(
177
+ fc.property(
178
+ fc.integer({ min: 2, max: 10 }),
179
+ fc.integer({ min: 0, max: 9 }),
180
+ (tabCount, targetIndex) => {
181
+ const tabs: Tab[] = Array.from({ length: tabCount }, (_, i) => ({
182
+ id: `tab-${i}`,
183
+ label: `Tab ${i}`,
184
+ disabled: false
185
+ }));
186
+
187
+ const validTargetIndex = Math.min(targetIndex, tabCount - 1);
188
+
189
+ const { activeTab, setActiveTab, isTabActive } = useTabs({
190
+ tabs
191
+ });
192
+
193
+ // Property: Directly setting activeTab.value should update state (v-model pattern)
194
+ activeTab.value = tabs[validTargetIndex].id;
195
+ expect(activeTab.value).toBe(tabs[validTargetIndex].id);
196
+ expect(isTabActive(tabs[validTargetIndex].id)).toBe(true);
197
+
198
+ // Property: Methods should still work after direct state change
199
+ if (validTargetIndex > 0) {
200
+ setActiveTab(tabs[0].id);
201
+ expect(activeTab.value).toBe(tabs[0].id);
202
+ }
203
+ }
204
+ ),
205
+ { numRuns: 100 }
206
+ );
207
+ });
208
+
209
+ it('should handle onChange callback correctly', () => {
210
+ fc.assert(
211
+ fc.property(
212
+ fc.integer({ min: 3, max: 10 }),
213
+ (tabCount) => {
214
+ const tabs: Tab[] = Array.from({ length: tabCount }, (_, i) => ({
215
+ id: `tab-${i}`,
216
+ label: `Tab ${i}`,
217
+ disabled: false
218
+ }));
219
+
220
+ let changeCallCount = 0;
221
+ let lastChangedTab = '';
222
+
223
+ const { setActiveTab, nextTab } = useTabs({
224
+ tabs,
225
+ onChange: (tabId) => {
226
+ changeCallCount++;
227
+ lastChangedTab = tabId;
228
+ }
229
+ });
230
+
231
+ const initialCallCount = changeCallCount;
232
+
233
+ // Property: setActiveTab should trigger onChange
234
+ setActiveTab(tabs[1].id);
235
+ expect(changeCallCount).toBe(initialCallCount + 1);
236
+ expect(lastChangedTab).toBe(tabs[1].id);
237
+
238
+ // Property: nextTab should trigger onChange (we're at tab 1, so can move to tab 2)
239
+ nextTab();
240
+ expect(changeCallCount).toBe(initialCallCount + 2);
241
+ expect(lastChangedTab).toBe(tabs[2].id);
242
+ }
243
+ ),
244
+ { numRuns: 100 }
245
+ );
246
+ });
247
+ });
@@ -0,0 +1,101 @@
1
+ import { ref, computed, type Ref, type ComputedRef } from 'vue';
2
+
3
+ export interface Tab {
4
+ id: string;
5
+ label: string;
6
+ disabled?: boolean;
7
+ }
8
+
9
+ export interface UseTabsOptions {
10
+ tabs: Tab[];
11
+ initialTab?: string;
12
+ onChange?: (tabId: string) => void;
13
+ }
14
+
15
+ export interface UseTabsReturn {
16
+ activeTab: Ref<string>;
17
+ tabs: Ref<Tab[]>;
18
+ currentTabIndex: ComputedRef<number>;
19
+ isFirstTab: ComputedRef<boolean>;
20
+ isLastTab: ComputedRef<boolean>;
21
+ setActiveTab: (tabId: string) => void;
22
+ nextTab: () => void;
23
+ previousTab: () => void;
24
+ isTabActive: (tabId: string) => boolean;
25
+ isTabDisabled: (tabId: string) => boolean;
26
+ }
27
+
28
+ export function useTabs(options: UseTabsOptions): UseTabsReturn {
29
+ const tabs = ref(options.tabs);
30
+ const activeTab = ref(
31
+ options.initialTab || (tabs.value.length > 0 ? tabs.value[0].id : '')
32
+ );
33
+
34
+ const currentTabIndex = computed(() =>
35
+ tabs.value.findIndex(tab => tab.id === activeTab.value)
36
+ );
37
+
38
+ const isFirstTab = computed(() => currentTabIndex.value === 0);
39
+
40
+ const isLastTab = computed(() =>
41
+ currentTabIndex.value === tabs.value.length - 1
42
+ );
43
+
44
+ function setActiveTab(tabId: string) {
45
+ const tab = tabs.value.find(t => t.id === tabId);
46
+ if (tab && !tab.disabled) {
47
+ activeTab.value = tabId;
48
+ options.onChange?.(tabId);
49
+ }
50
+ }
51
+
52
+ function nextTab() {
53
+ if (!isLastTab.value) {
54
+ const nextIndex = currentTabIndex.value + 1;
55
+ const nextTab = tabs.value[nextIndex];
56
+ if (nextTab && !nextTab.disabled) {
57
+ setActiveTab(nextTab.id);
58
+ } else if (nextIndex < tabs.value.length - 1) {
59
+ // Skip disabled tab and try next
60
+ activeTab.value = nextTab.id;
61
+ nextTab();
62
+ }
63
+ }
64
+ }
65
+
66
+ function previousTab() {
67
+ if (!isFirstTab.value) {
68
+ const prevIndex = currentTabIndex.value - 1;
69
+ const prevTab = tabs.value[prevIndex];
70
+ if (prevTab && !prevTab.disabled) {
71
+ setActiveTab(prevTab.id);
72
+ } else if (prevIndex > 0) {
73
+ // Skip disabled tab and try previous
74
+ activeTab.value = prevTab.id;
75
+ previousTab();
76
+ }
77
+ }
78
+ }
79
+
80
+ function isTabActive(tabId: string): boolean {
81
+ return activeTab.value === tabId;
82
+ }
83
+
84
+ function isTabDisabled(tabId: string): boolean {
85
+ const tab = tabs.value.find(t => t.id === tabId);
86
+ return tab?.disabled ?? false;
87
+ }
88
+
89
+ return {
90
+ activeTab,
91
+ tabs,
92
+ currentTabIndex,
93
+ isFirstTab,
94
+ isLastTab,
95
+ setActiveTab,
96
+ nextTab,
97
+ previousTab,
98
+ isTabActive,
99
+ isTabDisabled
100
+ };
101
+ }