@htlkg/components 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/{AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js → AdminWrapper.vue_vue_type_script_setup_true_lang-BhnWQ-b0.js} +26 -29
  2. package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-BhnWQ-b0.js.map +1 -0
  3. package/dist/Alert.vue_vue_type_script_setup_true_lang-DxPCS-Hx.js.map +1 -1
  4. package/dist/DateRange.vue_vue_type_script_setup_true_lang-BLVg1Hah.js.map +1 -1
  5. package/dist/ProductBadge.vue_vue_type_script_setup_true_lang-Cmr2f4Cy.js.map +1 -1
  6. package/dist/components.css +4 -4
  7. package/dist/composables/index.js +23 -22
  8. package/dist/data/index.js +10 -10
  9. package/dist/{filterHelpers-DgRyoYSa.js → filterHelpers-DpHSlTuh.js} +11 -11
  10. package/dist/filterHelpers-DpHSlTuh.js.map +1 -0
  11. package/dist/index-QK97OdqQ.js.map +1 -1
  12. package/dist/index.js +34 -33
  13. package/dist/navigation/index.js +1 -1
  14. package/dist/{useAdminPage-GhgXp0x8.js → useAdminPage-AgWRvw6o.js} +150 -26
  15. package/dist/useAdminPage-AgWRvw6o.js.map +1 -0
  16. package/package.json +3 -3
  17. package/src/composables/index.ts +1 -0
  18. package/src/composables/useJsonForm.test.ts +272 -0
  19. package/src/composables/useJsonForm.ts +261 -0
  20. package/src/composables/useModal.test.ts +264 -0
  21. package/src/composables/useModal.ts +54 -8
  22. package/src/data/Chart/index.ts +2 -0
  23. package/src/data/DataList/index.ts +1 -0
  24. package/src/data/{DataTable.vue → DataTable/DataTable.vue} +2 -2
  25. package/src/data/DataTable/index.ts +8 -0
  26. package/src/data/SearchableSelect/index.ts +1 -0
  27. package/src/data/Table/index.ts +1 -0
  28. package/src/data/index.ts +5 -15
  29. package/src/domain/BrandCard/index.ts +1 -0
  30. package/src/domain/BrandSelector/index.ts +1 -0
  31. package/src/domain/ProductBadge/index.ts +1 -0
  32. package/src/domain/UserAvatar/index.ts +1 -0
  33. package/src/domain/index.ts +4 -4
  34. package/src/forms/DateRange/index.ts +2 -0
  35. package/src/forms/JsonSchemaForm/index.ts +1 -0
  36. package/src/forms/index.ts +2 -3
  37. package/src/navigation/{AdminWrapper.vue → AdminWrapper/AdminWrapper.vue} +41 -30
  38. package/src/navigation/AdminWrapper/index.ts +1 -0
  39. package/src/navigation/Breadcrumbs/index.ts +1 -0
  40. package/src/navigation/Stepper/index.ts +2 -0
  41. package/src/navigation/Tabs/index.ts +2 -0
  42. package/src/navigation/index.ts +4 -6
  43. package/src/overlays/Alert/index.ts +1 -0
  44. package/src/overlays/Drawer/index.ts +1 -0
  45. package/src/overlays/Modal/index.ts +1 -0
  46. package/src/overlays/Notification/index.ts +1 -0
  47. package/src/overlays/index.ts +4 -4
  48. package/src/patterns/DASHBOARD_PAGE.md +642 -0
  49. package/src/patterns/DETAIL_PAGE.md +446 -0
  50. package/src/patterns/FORM_PAGE.md +439 -0
  51. package/src/patterns/LIST_PAGE.md +340 -0
  52. package/src/patterns/PAGE_PATTERNS.md +110 -0
  53. package/src/patterns/WIZARD_PAGE.md +733 -0
  54. package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js.map +0 -1
  55. package/dist/filterHelpers-DgRyoYSa.js.map +0 -1
  56. package/dist/useAdminPage-GhgXp0x8.js.map +0 -1
  57. package/src/data/Table.vue +0 -295
  58. /package/src/data/{Chart.demo.vue → Chart/Chart.demo.vue} +0 -0
  59. /package/src/data/{Chart.md → Chart/Chart.md} +0 -0
  60. /package/src/data/{Chart.vue → Chart/Chart.vue} +0 -0
  61. /package/src/data/{DataList.md → DataList/DataList.md} +0 -0
  62. /package/src/data/{DataList.test.ts → DataList/DataList.test.ts} +0 -0
  63. /package/src/data/{DataList.vue → DataList/DataList.vue} +0 -0
  64. /package/src/data/{SearchableSelect.md → SearchableSelect/SearchableSelect.md} +0 -0
  65. /package/src/data/{SearchableSelect.vue → SearchableSelect/SearchableSelect.vue} +0 -0
  66. /package/src/data/{Table.demo.vue → Table/Table.demo.vue} +0 -0
  67. /package/src/data/{Table.md → Table/Table.md} +0 -0
  68. /package/src/data/{Table.property.test.ts → Table/Table.property.test.ts} +0 -0
  69. /package/src/data/{Table.test.ts → Table/Table.test.ts} +0 -0
  70. /package/src/data/{Table.unit.test.ts → Table/Table.unit.test.ts} +0 -0
  71. /package/src/domain/{BrandCard.md → BrandCard/BrandCard.md} +0 -0
  72. /package/src/domain/{BrandCard.vue → BrandCard/BrandCard.vue} +0 -0
  73. /package/src/domain/{BrandSelector.md → BrandSelector/BrandSelector.md} +0 -0
  74. /package/src/domain/{BrandSelector.vue → BrandSelector/BrandSelector.vue} +0 -0
  75. /package/src/domain/{ProductBadge.md → ProductBadge/ProductBadge.md} +0 -0
  76. /package/src/domain/{ProductBadge.vue → ProductBadge/ProductBadge.vue} +0 -0
  77. /package/src/domain/{UserAvatar.md → UserAvatar/UserAvatar.md} +0 -0
  78. /package/src/domain/{UserAvatar.vue → UserAvatar/UserAvatar.vue} +0 -0
  79. /package/src/forms/{DateRange.demo.vue → DateRange/DateRange.demo.vue} +0 -0
  80. /package/src/forms/{DateRange.md → DateRange/DateRange.md} +0 -0
  81. /package/src/forms/{DateRange.vue → DateRange/DateRange.vue} +0 -0
  82. /package/src/forms/{JsonSchemaForm.demo.vue → JsonSchemaForm/JsonSchemaForm.demo.vue} +0 -0
  83. /package/src/forms/{JsonSchemaForm.md → JsonSchemaForm/JsonSchemaForm.md} +0 -0
  84. /package/src/forms/{JsonSchemaForm.property.test.ts → JsonSchemaForm/JsonSchemaForm.property.test.ts} +0 -0
  85. /package/src/forms/{JsonSchemaForm.test.ts → JsonSchemaForm/JsonSchemaForm.test.ts} +0 -0
  86. /package/src/forms/{JsonSchemaForm.unit.test.ts → JsonSchemaForm/JsonSchemaForm.unit.test.ts} +0 -0
  87. /package/src/forms/{JsonSchemaForm.vue → JsonSchemaForm/JsonSchemaForm.vue} +0 -0
  88. /package/src/navigation/{Breadcrumbs.demo.vue → Breadcrumbs/Breadcrumbs.demo.vue} +0 -0
  89. /package/src/navigation/{Breadcrumbs.md → Breadcrumbs/Breadcrumbs.md} +0 -0
  90. /package/src/navigation/{Breadcrumbs.test.ts → Breadcrumbs/Breadcrumbs.test.ts} +0 -0
  91. /package/src/navigation/{Breadcrumbs.vue → Breadcrumbs/Breadcrumbs.vue} +0 -0
  92. /package/src/navigation/{Stepper.demo.vue → Stepper/Stepper.demo.vue} +0 -0
  93. /package/src/navigation/{Stepper.md → Stepper/Stepper.md} +0 -0
  94. /package/src/navigation/{Stepper.vue → Stepper/Stepper.vue} +0 -0
  95. /package/src/navigation/{Tabs.demo.vue → Tabs/Tabs.demo.vue} +0 -0
  96. /package/src/navigation/{Tabs.md → Tabs/Tabs.md} +0 -0
  97. /package/src/navigation/{Tabs.test.ts → Tabs/Tabs.test.ts} +0 -0
  98. /package/src/navigation/{Tabs.vue → Tabs/Tabs.vue} +0 -0
  99. /package/src/overlays/{Alert.demo.vue → Alert/Alert.demo.vue} +0 -0
  100. /package/src/overlays/{Alert.md → Alert/Alert.md} +0 -0
  101. /package/src/overlays/{Alert.test.ts → Alert/Alert.test.ts} +0 -0
  102. /package/src/overlays/{Alert.vue → Alert/Alert.vue} +0 -0
  103. /package/src/overlays/{Drawer.md → Drawer/Drawer.md} +0 -0
  104. /package/src/overlays/{Drawer.test.ts → Drawer/Drawer.test.ts} +0 -0
  105. /package/src/overlays/{Drawer.vue → Drawer/Drawer.vue} +0 -0
  106. /package/src/overlays/{Modal.demo.vue → Modal/Modal.demo.vue} +0 -0
  107. /package/src/overlays/{Modal.md → Modal/Modal.md} +0 -0
  108. /package/src/overlays/{Modal.test.ts → Modal/Modal.test.ts} +0 -0
  109. /package/src/overlays/{Modal.vue → Modal/Modal.vue} +0 -0
  110. /package/src/overlays/{Notification.md → Notification/Notification.md} +0 -0
  111. /package/src/overlays/{Notification.test.ts → Notification/Notification.test.ts} +0 -0
  112. /package/src/overlays/{Notification.vue → Notification/Notification.vue} +0 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * JsonSchemaForm Composable
3
+ *
4
+ * Wraps JsonSchemaForm component with additional features:
5
+ * - isDirty tracking (has unsaved changes)
6
+ * - confirmOnLeave (warn before navigation with unsaved changes)
7
+ * - Form ref management
8
+ */
9
+
10
+ import { ref, computed, watch, onMounted, onUnmounted, type Ref, type ComputedRef } from 'vue';
11
+
12
+ /**
13
+ * Options for useJsonForm
14
+ */
15
+ export interface UseJsonFormOptions<T extends Record<string, unknown>> {
16
+ /** Initial form values */
17
+ initialValues?: T;
18
+ /** Warn before leaving with unsaved changes */
19
+ confirmOnLeave?: boolean;
20
+ /** Message to show in browser confirm dialog */
21
+ confirmMessage?: string;
22
+ /** Auto-save configuration */
23
+ autoSave?: {
24
+ enabled: boolean;
25
+ debounceMs?: number;
26
+ onAutoSave: (values: T) => Promise<void>;
27
+ };
28
+ /** Callback when values change */
29
+ onChange?: (values: T) => void;
30
+ }
31
+
32
+ /**
33
+ * Return type for useJsonForm
34
+ */
35
+ export interface UseJsonFormReturn<T extends Record<string, unknown>> {
36
+ /** Ref to attach to JsonSchemaForm component */
37
+ formRef: Ref<JsonSchemaFormInstance | null>;
38
+ /** Current form values (v-model) */
39
+ values: Ref<T>;
40
+ /** Original values (for dirty checking) */
41
+ originalValues: Ref<T>;
42
+ /** Whether form has unsaved changes */
43
+ isDirty: ComputedRef<boolean>;
44
+ /** Trigger form validation */
45
+ validate: () => Array<{ field: string; message: string }>;
46
+ /** Reset form to original values */
47
+ reset: () => void;
48
+ /** Update both current and original values (after save) */
49
+ setValues: (newValues: Partial<T>) => void;
50
+ /** Mark form as saved (updates original values to current) */
51
+ markAsSaved: () => void;
52
+ }
53
+
54
+ /**
55
+ * JsonSchemaForm component instance interface
56
+ */
57
+ interface JsonSchemaFormInstance {
58
+ validate: () => Array<{ field: string; message: string }>;
59
+ reset: () => void;
60
+ formData: Ref<Record<string, unknown>>;
61
+ errors: Ref<Record<string, string>>;
62
+ touched: Ref<Record<string, boolean>>;
63
+ }
64
+
65
+ /**
66
+ * Deep equality check for objects
67
+ */
68
+ function deepEqual(a: unknown, b: unknown): boolean {
69
+ if (a === b) return true;
70
+ if (a == null || b == null) return false;
71
+ if (typeof a !== 'object' || typeof b !== 'object') return false;
72
+
73
+ const keysA = Object.keys(a as object);
74
+ const keysB = Object.keys(b as object);
75
+
76
+ if (keysA.length !== keysB.length) return false;
77
+
78
+ for (const key of keysA) {
79
+ if (!keysB.includes(key)) return false;
80
+ if (!deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ return true;
86
+ }
87
+
88
+ /**
89
+ * Deep clone an object
90
+ */
91
+ function deepClone<T>(obj: T): T {
92
+ if (obj === null || typeof obj !== 'object') return obj;
93
+ if (Array.isArray(obj)) return obj.map(deepClone) as unknown as T;
94
+ const clone = {} as T;
95
+ for (const key in obj) {
96
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
97
+ clone[key] = deepClone(obj[key]);
98
+ }
99
+ }
100
+ return clone;
101
+ }
102
+
103
+ /**
104
+ * Debounce function
105
+ */
106
+ function debounce<T extends (...args: unknown[]) => unknown>(fn: T, delay: number): T {
107
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
108
+ return ((...args: unknown[]) => {
109
+ if (timeoutId) clearTimeout(timeoutId);
110
+ timeoutId = setTimeout(() => fn(...args), delay);
111
+ }) as T;
112
+ }
113
+
114
+ /**
115
+ * useJsonForm composable
116
+ *
117
+ * @example
118
+ * ```vue
119
+ * <script setup>
120
+ * import { JsonSchemaForm } from '@htlkg/components/forms';
121
+ * import { useJsonForm } from '@htlkg/components/composables';
122
+ *
123
+ * const { formRef, values, isDirty, validate, reset } = useJsonForm({
124
+ * initialValues: props.brand,
125
+ * confirmOnLeave: true,
126
+ * });
127
+ *
128
+ * const schema = {
129
+ * type: 'object',
130
+ * properties: {
131
+ * name: { type: 'string', title: 'Name', minLength: 1 },
132
+ * },
133
+ * required: ['name'],
134
+ * };
135
+ *
136
+ * async function handleSubmit(data) {
137
+ * await saveBrand(data);
138
+ * // Update original values so isDirty becomes false
139
+ * setValues(data);
140
+ * }
141
+ * </script>
142
+ *
143
+ * <template>
144
+ * <div v-if="isDirty" class="warning">You have unsaved changes</div>
145
+ * <JsonSchemaForm
146
+ * ref="formRef"
147
+ * v-model="values"
148
+ * :schema="schema"
149
+ * @submit="handleSubmit"
150
+ * />
151
+ * </template>
152
+ * ```
153
+ */
154
+ export function useJsonForm<T extends Record<string, unknown>>(
155
+ options: UseJsonFormOptions<T> = {}
156
+ ): UseJsonFormReturn<T> {
157
+ const {
158
+ initialValues = {} as T,
159
+ confirmOnLeave = false,
160
+ confirmMessage = 'You have unsaved changes. Are you sure you want to leave?',
161
+ autoSave,
162
+ onChange,
163
+ } = options;
164
+
165
+ // Form ref
166
+ const formRef = ref<JsonSchemaFormInstance | null>(null);
167
+
168
+ // Current values
169
+ const values = ref<T>(deepClone(initialValues)) as Ref<T>;
170
+
171
+ // Original values (for dirty checking)
172
+ const originalValues = ref<T>(deepClone(initialValues)) as Ref<T>;
173
+
174
+ // Computed dirty state
175
+ const isDirty = computed(() => !deepEqual(values.value, originalValues.value));
176
+
177
+ // Validate form
178
+ function validate(): Array<{ field: string; message: string }> {
179
+ if (formRef.value?.validate) {
180
+ return formRef.value.validate();
181
+ }
182
+ return [];
183
+ }
184
+
185
+ // Reset to original values
186
+ function reset(): void {
187
+ values.value = deepClone(originalValues.value);
188
+ if (formRef.value?.reset) {
189
+ formRef.value.reset();
190
+ }
191
+ }
192
+
193
+ // Update both current and original values
194
+ function setValues(newValues: Partial<T>): void {
195
+ const merged = { ...values.value, ...newValues } as T;
196
+ values.value = merged;
197
+ originalValues.value = deepClone(merged);
198
+ }
199
+
200
+ // Mark current state as saved
201
+ function markAsSaved(): void {
202
+ originalValues.value = deepClone(values.value);
203
+ }
204
+
205
+ // Handle beforeunload for confirmOnLeave
206
+ function handleBeforeUnload(e: BeforeUnloadEvent): string | undefined {
207
+ if (isDirty.value) {
208
+ e.preventDefault();
209
+ e.returnValue = confirmMessage;
210
+ return confirmMessage;
211
+ }
212
+ return undefined;
213
+ }
214
+
215
+ // Setup auto-save
216
+ const debouncedAutoSave = autoSave?.enabled
217
+ ? debounce(async (newValues: T) => {
218
+ try {
219
+ await autoSave.onAutoSave(newValues);
220
+ } catch (error) {
221
+ console.error('Auto-save failed:', error);
222
+ }
223
+ }, autoSave.debounceMs ?? 2000)
224
+ : null;
225
+
226
+ // Watch for value changes
227
+ watch(
228
+ values,
229
+ (newValues) => {
230
+ onChange?.(newValues);
231
+ if (debouncedAutoSave && isDirty.value) {
232
+ debouncedAutoSave(newValues);
233
+ }
234
+ },
235
+ { deep: true }
236
+ );
237
+
238
+ // Setup/cleanup beforeunload listener
239
+ onMounted(() => {
240
+ if (confirmOnLeave) {
241
+ window.addEventListener('beforeunload', handleBeforeUnload);
242
+ }
243
+ });
244
+
245
+ onUnmounted(() => {
246
+ if (confirmOnLeave) {
247
+ window.removeEventListener('beforeunload', handleBeforeUnload);
248
+ }
249
+ });
250
+
251
+ return {
252
+ formRef,
253
+ values,
254
+ originalValues,
255
+ isDirty,
256
+ validate,
257
+ reset,
258
+ setValues,
259
+ markAsSaved,
260
+ };
261
+ }
@@ -0,0 +1,264 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { useModal } from './useModal';
3
+
4
+ /**
5
+ * Unit tests for useModal composable
6
+ * Tests data context support and type safety
7
+ */
8
+ describe('useModal composable', () => {
9
+ describe('data context', () => {
10
+ it('initializes data as null by default', () => {
11
+ const { data } = useModal();
12
+ expect(data.value).toBe(null);
13
+ });
14
+
15
+ it('initializes with provided initialData', () => {
16
+ const initialData = { id: 1, name: 'Test' };
17
+ const { data } = useModal({ initialData });
18
+ expect(data.value).toEqual(initialData);
19
+ });
20
+
21
+ it('sets data when opening with context', () => {
22
+ interface Contact {
23
+ id: number;
24
+ name: string;
25
+ }
26
+
27
+ const modal = useModal<Contact>();
28
+ const contact = { id: 1, name: 'John' };
29
+
30
+ modal.open(contact);
31
+
32
+ expect(modal.isOpen.value).toBe(true);
33
+ expect(modal.data.value).toEqual(contact);
34
+ });
35
+
36
+ it('opens without data when no context provided', () => {
37
+ const modal = useModal<{ id: number }>();
38
+
39
+ modal.open();
40
+
41
+ expect(modal.isOpen.value).toBe(true);
42
+ expect(modal.data.value).toBe(null);
43
+ });
44
+
45
+ it('preserves existing data when opening without context', () => {
46
+ const modal = useModal<{ id: number }>({
47
+ initialData: { id: 1 },
48
+ });
49
+
50
+ modal.open();
51
+
52
+ expect(modal.data.value).toEqual({ id: 1 });
53
+ });
54
+
55
+ it('clears data when closing', () => {
56
+ const modal = useModal<{ id: number }>();
57
+
58
+ modal.open({ id: 1 });
59
+ expect(modal.data.value).toEqual({ id: 1 });
60
+
61
+ modal.close();
62
+ expect(modal.data.value).toBe(null);
63
+ });
64
+
65
+ it('passes context to onOpen callback', () => {
66
+ const onOpen = vi.fn();
67
+ const modal = useModal<{ id: number }>({ onOpen });
68
+ const context = { id: 1 };
69
+
70
+ modal.open(context);
71
+
72
+ expect(onOpen).toHaveBeenCalledWith(context);
73
+ });
74
+
75
+ it('calls onOpen without context when opening without data', () => {
76
+ const onOpen = vi.fn();
77
+ const modal = useModal<{ id: number }>({ onOpen });
78
+
79
+ modal.open();
80
+
81
+ expect(onOpen).toHaveBeenCalledWith(undefined);
82
+ });
83
+ });
84
+
85
+ describe('setData', () => {
86
+ it('updates data without changing open state', () => {
87
+ const modal = useModal<{ id: number; name?: string }>();
88
+
89
+ modal.open({ id: 1 });
90
+ expect(modal.isOpen.value).toBe(true);
91
+
92
+ modal.setData({ id: 1, name: 'Updated' });
93
+
94
+ expect(modal.isOpen.value).toBe(true);
95
+ expect(modal.data.value).toEqual({ id: 1, name: 'Updated' });
96
+ });
97
+
98
+ it('can set data to null', () => {
99
+ const modal = useModal<{ id: number }>();
100
+
101
+ modal.open({ id: 1 });
102
+ modal.setData(null);
103
+
104
+ expect(modal.isOpen.value).toBe(true);
105
+ expect(modal.data.value).toBe(null);
106
+ });
107
+
108
+ it('works when modal is closed', () => {
109
+ const modal = useModal<{ id: number }>();
110
+
111
+ modal.setData({ id: 1 });
112
+
113
+ expect(modal.isOpen.value).toBe(false);
114
+ expect(modal.data.value).toEqual({ id: 1 });
115
+ });
116
+ });
117
+
118
+ describe('toggle with data', () => {
119
+ it('clears data when toggling closed', () => {
120
+ const modal = useModal<{ id: number }>();
121
+
122
+ modal.open({ id: 1 });
123
+ expect(modal.data.value).toEqual({ id: 1 });
124
+
125
+ modal.toggle(); // closes
126
+ expect(modal.isOpen.value).toBe(false);
127
+ expect(modal.data.value).toBe(null);
128
+ });
129
+
130
+ it('preserves null data when toggling open', () => {
131
+ const modal = useModal<{ id: number }>();
132
+
133
+ modal.toggle(); // opens
134
+ expect(modal.isOpen.value).toBe(true);
135
+ expect(modal.data.value).toBe(null);
136
+ });
137
+ });
138
+
139
+ describe('type safety', () => {
140
+ it('supports generic type parameter', () => {
141
+ interface User {
142
+ id: number;
143
+ name: string;
144
+ email: string;
145
+ }
146
+
147
+ const modal = useModal<User>();
148
+
149
+ const user: User = { id: 1, name: 'John', email: 'john@example.com' };
150
+ modal.open(user);
151
+
152
+ // TypeScript should infer correct type
153
+ expect(modal.data.value?.id).toBe(1);
154
+ expect(modal.data.value?.name).toBe('John');
155
+ expect(modal.data.value?.email).toBe('john@example.com');
156
+ });
157
+
158
+ it('works without generic type', () => {
159
+ const modal = useModal();
160
+
161
+ modal.open({ anyProperty: 'value' });
162
+ expect(modal.data.value).toEqual({ anyProperty: 'value' });
163
+ });
164
+ });
165
+
166
+ describe('basic operations', () => {
167
+ it('opens and closes correctly', () => {
168
+ const modal = useModal();
169
+
170
+ expect(modal.isOpen.value).toBe(false);
171
+
172
+ modal.open();
173
+ expect(modal.isOpen.value).toBe(true);
174
+
175
+ modal.close();
176
+ expect(modal.isOpen.value).toBe(false);
177
+ });
178
+
179
+ it('toggles correctly', () => {
180
+ const modal = useModal();
181
+
182
+ modal.toggle();
183
+ expect(modal.isOpen.value).toBe(true);
184
+
185
+ modal.toggle();
186
+ expect(modal.isOpen.value).toBe(false);
187
+ });
188
+
189
+ it('respects initialOpen option', () => {
190
+ const modal = useModal({ initialOpen: true });
191
+ expect(modal.isOpen.value).toBe(true);
192
+ });
193
+
194
+ it('calls onClose callback', () => {
195
+ const onClose = vi.fn();
196
+ const modal = useModal({ onClose });
197
+
198
+ modal.open();
199
+ modal.close();
200
+
201
+ expect(onClose).toHaveBeenCalled();
202
+ });
203
+ });
204
+
205
+ describe('use case: edit modal', () => {
206
+ interface Contact {
207
+ id: number;
208
+ name: string;
209
+ email: string;
210
+ }
211
+
212
+ it('handles edit workflow correctly', () => {
213
+ const editModal = useModal<Contact>();
214
+
215
+ // Click edit on contact
216
+ const contactToEdit = { id: 1, name: 'John', email: 'john@example.com' };
217
+ editModal.open(contactToEdit);
218
+
219
+ expect(editModal.isOpen.value).toBe(true);
220
+ expect(editModal.data.value).toEqual(contactToEdit);
221
+
222
+ // User closes modal
223
+ editModal.close();
224
+
225
+ expect(editModal.isOpen.value).toBe(false);
226
+ expect(editModal.data.value).toBe(null);
227
+
228
+ // Edit different contact
229
+ const anotherContact = { id: 2, name: 'Jane', email: 'jane@example.com' };
230
+ editModal.open(anotherContact);
231
+
232
+ expect(editModal.data.value).toEqual(anotherContact);
233
+ });
234
+ });
235
+
236
+ describe('use case: confirmation modal', () => {
237
+ interface ConfirmationData {
238
+ title: string;
239
+ message: string;
240
+ itemId: number;
241
+ }
242
+
243
+ it('handles confirmation workflow correctly', () => {
244
+ const confirmModal = useModal<ConfirmationData>();
245
+
246
+ // Show confirmation for delete
247
+ confirmModal.open({
248
+ title: 'Delete Item',
249
+ message: 'Are you sure?',
250
+ itemId: 123,
251
+ });
252
+
253
+ expect(confirmModal.isOpen.value).toBe(true);
254
+ expect(confirmModal.data.value?.itemId).toBe(123);
255
+
256
+ // User confirms
257
+ const itemToDelete = confirmModal.data.value?.itemId;
258
+ confirmModal.close();
259
+
260
+ expect(itemToDelete).toBe(123);
261
+ expect(confirmModal.isOpen.value).toBe(false);
262
+ });
263
+ });
264
+ });
@@ -1,28 +1,68 @@
1
1
  import { ref, type Ref } from 'vue';
2
2
 
3
- export interface UseModalOptions {
3
+ export interface UseModalOptions<T = unknown> {
4
4
  initialOpen?: boolean;
5
- onOpen?: () => void;
5
+ initialData?: T | null;
6
+ onOpen?: (data?: T) => void;
6
7
  onClose?: () => void;
7
8
  }
8
9
 
9
- export interface UseModalReturn {
10
+ export interface UseModalReturn<T = unknown> {
11
+ /** Whether the modal is open */
10
12
  isOpen: Ref<boolean>;
11
- open: () => void;
13
+ /** Data context passed when opening the modal */
14
+ data: Ref<T | null>;
15
+ /** Open the modal, optionally with data context */
16
+ open: (context?: T) => void;
17
+ /** Close the modal and clear data */
12
18
  close: () => void;
19
+ /** Toggle modal open/closed state */
13
20
  toggle: () => void;
21
+ /** Update the data without changing open state */
22
+ setData: (newData: T | null) => void;
14
23
  }
15
24
 
16
- export function useModal(options: UseModalOptions = {}): UseModalReturn {
25
+ /**
26
+ * Modal state management composable with data context support.
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * // Type-safe modal for editing contacts
31
+ * const contactModal = useModal<Contact>();
32
+ *
33
+ * // Open with data
34
+ * contactModal.open(selectedContact);
35
+ *
36
+ * // Access in template
37
+ * <Modal :open="contactModal.isOpen.value">
38
+ * <p>Editing: {{ contactModal.data.value?.name }}</p>
39
+ * </Modal>
40
+ * ```
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * // With callbacks
45
+ * const modal = useModal({
46
+ * onOpen: (data) => console.log('Opened with:', data),
47
+ * onClose: () => console.log('Closed'),
48
+ * });
49
+ * ```
50
+ */
51
+ export function useModal<T = unknown>(options: UseModalOptions<T> = {}): UseModalReturn<T> {
17
52
  const isOpen = ref(options.initialOpen ?? false);
53
+ const data = ref<T | null>(options.initialData ?? null) as Ref<T | null>;
18
54
 
19
- function open() {
55
+ function open(context?: T) {
56
+ if (context !== undefined) {
57
+ data.value = context;
58
+ }
20
59
  isOpen.value = true;
21
- options.onOpen?.();
60
+ options.onOpen?.(context);
22
61
  }
23
62
 
24
63
  function close() {
25
64
  isOpen.value = false;
65
+ data.value = null;
26
66
  options.onClose?.();
27
67
  }
28
68
 
@@ -34,10 +74,16 @@ export function useModal(options: UseModalOptions = {}): UseModalReturn {
34
74
  }
35
75
  }
36
76
 
77
+ function setData(newData: T | null) {
78
+ data.value = newData;
79
+ }
80
+
37
81
  return {
38
82
  isOpen,
83
+ data,
39
84
  open,
40
85
  close,
41
- toggle
86
+ toggle,
87
+ setData,
42
88
  };
43
89
  }
@@ -0,0 +1,2 @@
1
+ export { default as Chart } from './Chart.vue';
2
+ export type { ChartSeries, ChartOptions, ChartDateRange, ChartAnnotation, NpsLiterals } from './Chart.vue';
@@ -0,0 +1 @@
1
+ export { default as DataList } from './DataList.vue';
@@ -7,8 +7,8 @@
7
7
  */
8
8
 
9
9
  import { computed, toRef, watch } from "vue";
10
- import Table from "./Table.vue";
11
- import { useTable, type SmartFilter } from "../composables/useTable";
10
+ import Table from "../Table/Table.vue";
11
+ import { useTable, type SmartFilter } from "../../composables/useTable";
12
12
 
13
13
  /**
14
14
  * Column definition for DataTable
@@ -0,0 +1,8 @@
1
+ export { default as DataTable } from './DataTable.vue';
2
+ export type {
3
+ DataTableColumn,
4
+ DataTableFilter,
5
+ BulkAction,
6
+ RowAction,
7
+ NoResultsConfig,
8
+ } from './DataTable.vue';
@@ -0,0 +1 @@
1
+ export { default as SearchableSelect } from './SearchableSelect.vue';
@@ -0,0 +1 @@
1
+ export { default as Table } from './Table.vue';
package/src/data/index.ts CHANGED
@@ -1,18 +1,8 @@
1
- export { default as Table } from './Table.vue';
2
- export { default as DataList } from './DataList.vue';
3
- export { default as SearchableSelect } from './SearchableSelect.vue';
4
- export { default as Chart } from './Chart.vue';
5
- export type { ChartSeries, ChartOptions, ChartDateRange, ChartAnnotation, NpsLiterals } from './Chart.vue';
6
-
7
- // DataTable - Simplified table with automatic useTable integration
8
- export { default as DataTable } from './DataTable.vue';
9
- export type {
10
- DataTableColumn,
11
- DataTableFilter,
12
- BulkAction,
13
- RowAction,
14
- NoResultsConfig,
15
- } from './DataTable.vue';
1
+ export * from './Table';
2
+ export * from './DataList';
3
+ export * from './SearchableSelect';
4
+ export * from './Chart';
5
+ export * from './DataTable';
16
6
 
17
7
  // Column definition helpers
18
8
  export {
@@ -0,0 +1 @@
1
+ export { default as BrandCard } from './BrandCard.vue';
@@ -0,0 +1 @@
1
+ export { default as BrandSelector } from './BrandSelector.vue';
@@ -0,0 +1 @@
1
+ export { default as ProductBadge } from './ProductBadge.vue';
@@ -0,0 +1 @@
1
+ export { default as UserAvatar } from './UserAvatar.vue';
@@ -1,4 +1,4 @@
1
- export { default as BrandSelector } from './BrandSelector.vue';
2
- export { default as BrandCard } from './BrandCard.vue';
3
- export { default as UserAvatar } from './UserAvatar.vue';
4
- export { default as ProductBadge } from './ProductBadge.vue';
1
+ export * from './BrandSelector';
2
+ export * from './BrandCard';
3
+ export * from './UserAvatar';
4
+ export * from './ProductBadge';
@@ -0,0 +1,2 @@
1
+ export { default as DateRange } from './DateRange.vue';
2
+ export type { DateRangeValue, DateRangeLiterals } from './DateRange.vue';