@htlkg/components 0.0.2 → 0.0.4

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 (60) hide show
  1. package/README.md +52 -0
  2. package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js +367 -0
  3. package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js.map +1 -0
  4. package/dist/Alert.vue_vue_type_script_setup_true_lang-DxPCS-Hx.js +263 -0
  5. package/dist/Alert.vue_vue_type_script_setup_true_lang-DxPCS-Hx.js.map +1 -0
  6. package/dist/DateRange.vue_vue_type_script_setup_true_lang-BLVg1Hah.js +580 -0
  7. package/dist/DateRange.vue_vue_type_script_setup_true_lang-BLVg1Hah.js.map +1 -0
  8. package/dist/ProductBadge.vue_vue_type_script_setup_true_lang-Cmr2f4Cy.js +187 -0
  9. package/dist/ProductBadge.vue_vue_type_script_setup_true_lang-Cmr2f4Cy.js.map +1 -0
  10. package/dist/_plugin-vue_export-helper-1tPrXgE0.js +11 -0
  11. package/dist/_plugin-vue_export-helper-1tPrXgE0.js.map +1 -0
  12. package/dist/components.css +15 -0
  13. package/dist/composables/index.js +32 -573
  14. package/dist/composables/index.js.map +1 -1
  15. package/dist/data/index.js +18 -0
  16. package/dist/data/index.js.map +1 -0
  17. package/dist/domain/index.js +8 -0
  18. package/dist/domain/index.js.map +1 -0
  19. package/dist/filterHelpers-DgRyoYSa.js +1386 -0
  20. package/dist/filterHelpers-DgRyoYSa.js.map +1 -0
  21. package/dist/forms/index.js +6 -0
  22. package/dist/forms/index.js.map +1 -0
  23. package/dist/index-DGO_pNgG.js +79 -0
  24. package/dist/index-DGO_pNgG.js.map +1 -0
  25. package/dist/index-QK97OdqQ.js +25 -0
  26. package/dist/index-QK97OdqQ.js.map +1 -0
  27. package/dist/index.js +67 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/navigation/index.js +8 -0
  30. package/dist/navigation/index.js.map +1 -0
  31. package/dist/overlays/index.js +8 -0
  32. package/dist/overlays/index.js.map +1 -0
  33. package/dist/stores/index.js +14 -0
  34. package/dist/stores/index.js.map +1 -0
  35. package/dist/useAdminPage-GhgXp0x8.js +1070 -0
  36. package/dist/useAdminPage-GhgXp0x8.js.map +1 -0
  37. package/dist/useTable-DutR1gkg.js +293 -0
  38. package/dist/useTable-DutR1gkg.js.map +1 -0
  39. package/package.json +43 -14
  40. package/src/composables/composables.md +109 -0
  41. package/src/composables/index.ts +69 -0
  42. package/src/composables/useAdminPage.ts +462 -0
  43. package/src/composables/useConfirmation.ts +358 -0
  44. package/src/composables/usePageContext.ts +171 -0
  45. package/src/composables/useStats.ts +361 -0
  46. package/src/composables/useTable.ts +26 -5
  47. package/src/composables/useWizard.ts +448 -0
  48. package/src/data/DataTable.vue +553 -0
  49. package/src/data/Table/Table.vue +295 -0
  50. package/src/data/columnHelpers.ts +503 -0
  51. package/src/data/data.md +106 -0
  52. package/src/data/filterHelpers.ts +358 -0
  53. package/src/data/index.ts +31 -0
  54. package/src/domain/domain.md +102 -0
  55. package/src/forms/JsonSchemaForm.vue +4 -1
  56. package/src/forms/forms.md +89 -0
  57. package/src/index.ts +4 -3
  58. package/src/navigation/navigation.md +80 -0
  59. package/src/overlays/overlays.md +86 -0
  60. package/src/stores/stores.md +82 -0
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Stats Composable
3
+ *
4
+ * Derives statistics from reactive data with staggered loading animation.
5
+ * Integrates with uiStats component from @hotelinking/ui.
6
+ */
7
+
8
+ import { ref, computed, watch, type Ref, type ComputedRef, type Component } from 'vue';
9
+
10
+ /**
11
+ * Color options matching @hotelinking/ui
12
+ */
13
+ export type StatColor = 'green' | 'red' | 'yellow' | 'blue' | 'gray' | 'purple' | 'orange' | 'pink';
14
+
15
+ /**
16
+ * Change type for stat trends
17
+ */
18
+ export type ChangeType = 'increase' | 'decrease' | 'neutral';
19
+
20
+ /**
21
+ * Definition for a single stat
22
+ */
23
+ export interface StatDefinition<T> {
24
+ /** Unique identifier */
25
+ id: string;
26
+ /** Display name */
27
+ name: string;
28
+ /** Icon component (Heroicons) */
29
+ icon: Component;
30
+ /** Color theme */
31
+ color?: StatColor;
32
+ /** Function to compute stat value from data */
33
+ compute: (items: T[]) => string | number;
34
+ /** Optional: Compute change percentage/value */
35
+ computeChange?: (items: T[], previousItems?: T[]) => string;
36
+ /** Optional: Compute change type (increase/decrease/neutral) */
37
+ computeChangeType?: (items: T[], previousItems?: T[]) => ChangeType;
38
+ /** Show footer with action text */
39
+ showFooter?: boolean;
40
+ /** Action text for footer */
41
+ actionText?: string;
42
+ }
43
+
44
+ /**
45
+ * Stat item formatted for uiStats component
46
+ */
47
+ export interface StatItem {
48
+ id: string;
49
+ name: string;
50
+ stat: string | number;
51
+ icon: Component;
52
+ color: StatColor;
53
+ change?: string;
54
+ changeType?: ChangeType;
55
+ showFooter?: boolean;
56
+ actionText?: string;
57
+ }
58
+
59
+ /**
60
+ * Options for useStats
61
+ */
62
+ export interface UseStatsOptions<T> {
63
+ /** Reactive data source */
64
+ data: Ref<T[]> | ComputedRef<T[]>;
65
+ /** Loading state of the data source */
66
+ loading: Ref<boolean> | ComputedRef<boolean>;
67
+ /** Stat definitions */
68
+ definitions: StatDefinition<T>[];
69
+ /** Delay between each stat reveal (ms) */
70
+ staggerDelay?: number;
71
+ /** Initial delay before first stat reveals (ms) */
72
+ initialDelay?: number;
73
+ /** Previous data for computing changes (optional) */
74
+ previousData?: Ref<T[]> | ComputedRef<T[]>;
75
+ }
76
+
77
+ /**
78
+ * Return type for useStats
79
+ */
80
+ export interface UseStatsReturn<T> {
81
+ /** Computed stats array for uiStats */
82
+ stats: ComputedRef<StatItem[]>;
83
+ /** Individual loading states for each stat */
84
+ loadingStates: Ref<boolean[]>;
85
+ /** Combined stats with loading property */
86
+ statsWithLoading: ComputedRef<Array<StatItem & { loading: boolean }>>;
87
+ /** Whether all stats are loaded */
88
+ allLoaded: ComputedRef<boolean>;
89
+ /** Force refresh loading animation */
90
+ refreshAnimation: () => void;
91
+ /** Get a single stat by ID */
92
+ getStat: (id: string) => StatItem | undefined;
93
+ }
94
+
95
+ /**
96
+ * Creates a stats composable for deriving statistics from reactive data
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * import { CheckCircleIcon, ClockIcon, XCircleIcon } from '@heroicons/vue/24/outline';
101
+ *
102
+ * const { statsWithLoading } = useStats({
103
+ * data: campaigns,
104
+ * loading: campaignsLoading,
105
+ * definitions: [
106
+ * {
107
+ * id: 'active',
108
+ * name: 'Active Campaigns',
109
+ * icon: CheckCircleIcon,
110
+ * color: 'green',
111
+ * compute: (items) => items.filter(c => c.status === 'active').length,
112
+ * },
113
+ * {
114
+ * id: 'pending',
115
+ * name: 'Pending',
116
+ * icon: ClockIcon,
117
+ * color: 'yellow',
118
+ * compute: (items) => items.filter(c => c.status === 'pending').length,
119
+ * },
120
+ * {
121
+ * id: 'total-sent',
122
+ * name: 'Total Sent',
123
+ * icon: PaperAirplaneIcon,
124
+ * color: 'blue',
125
+ * compute: (items) => items.reduce((sum, c) => sum + c.sentCount, 0).toLocaleString(),
126
+ * computeChange: (items) => '+12%',
127
+ * computeChangeType: () => 'increase',
128
+ * },
129
+ * ],
130
+ * staggerDelay: 100,
131
+ * });
132
+ *
133
+ * // In template
134
+ * <div class="grid grid-cols-4 gap-4">
135
+ * <uiStats
136
+ * v-for="stat in statsWithLoading"
137
+ * :key="stat.id"
138
+ * :item="stat"
139
+ * :loading="stat.loading"
140
+ * @statClick="handleStatClick"
141
+ * />
142
+ * </div>
143
+ * ```
144
+ */
145
+ export function useStats<T>(options: UseStatsOptions<T>): UseStatsReturn<T> {
146
+ const {
147
+ data,
148
+ loading,
149
+ definitions,
150
+ staggerDelay = 100,
151
+ initialDelay = 0,
152
+ previousData,
153
+ } = options;
154
+
155
+ // Individual loading states for staggered animation
156
+ const loadingStates = ref<boolean[]>(definitions.map(() => true));
157
+
158
+ // Timers for cleanup
159
+ let timers: ReturnType<typeof setTimeout>[] = [];
160
+
161
+ // Clear all pending timers
162
+ function clearTimers(): void {
163
+ timers.forEach(timer => clearTimeout(timer));
164
+ timers = [];
165
+ }
166
+
167
+ // Trigger staggered loading animation
168
+ function triggerStaggeredReveal(): void {
169
+ clearTimers();
170
+
171
+ // Reset all to loading
172
+ loadingStates.value = definitions.map(() => true);
173
+
174
+ // Staggered reveal
175
+ definitions.forEach((_, index) => {
176
+ const timer = setTimeout(() => {
177
+ loadingStates.value[index] = false;
178
+ }, initialDelay + staggerDelay * (index + 1));
179
+ timers.push(timer);
180
+ });
181
+ }
182
+
183
+ // Watch for loading state changes
184
+ watch(
185
+ () => loading.value,
186
+ (isLoading, wasLoading) => {
187
+ if (wasLoading && !isLoading) {
188
+ // Data just finished loading, trigger reveal
189
+ triggerStaggeredReveal();
190
+ } else if (isLoading && !wasLoading) {
191
+ // Started loading, reset states
192
+ clearTimers();
193
+ loadingStates.value = definitions.map(() => true);
194
+ }
195
+ },
196
+ { immediate: true }
197
+ );
198
+
199
+ // If data is already loaded on mount, trigger reveal
200
+ if (!loading.value) {
201
+ triggerStaggeredReveal();
202
+ }
203
+
204
+ // Computed: Stats derived from data
205
+ const stats = computed<StatItem[]>(() => {
206
+ const items = data.value;
207
+ const prevItems = previousData?.value;
208
+
209
+ return definitions.map(def => {
210
+ const stat: StatItem = {
211
+ id: def.id,
212
+ name: def.name,
213
+ stat: def.compute(items),
214
+ icon: def.icon,
215
+ color: def.color ?? 'gray',
216
+ };
217
+
218
+ // Add change information if provided
219
+ if (def.computeChange) {
220
+ stat.change = def.computeChange(items, prevItems);
221
+ }
222
+
223
+ if (def.computeChangeType) {
224
+ stat.changeType = def.computeChangeType(items, prevItems);
225
+ }
226
+
227
+ // Footer options
228
+ if (def.showFooter !== undefined) {
229
+ stat.showFooter = def.showFooter;
230
+ }
231
+
232
+ if (def.actionText) {
233
+ stat.actionText = def.actionText;
234
+ }
235
+
236
+ return stat;
237
+ });
238
+ });
239
+
240
+ // Computed: Stats with loading property included
241
+ const statsWithLoading = computed(() =>
242
+ stats.value.map((stat, index) => ({
243
+ ...stat,
244
+ loading: loading.value || loadingStates.value[index],
245
+ }))
246
+ );
247
+
248
+ // Computed: All stats loaded
249
+ const allLoaded = computed(() =>
250
+ !loading.value && loadingStates.value.every(s => !s)
251
+ );
252
+
253
+ // Get a single stat by ID
254
+ function getStat(id: string): StatItem | undefined {
255
+ return stats.value.find(s => s.id === id);
256
+ }
257
+
258
+ // Manually refresh animation
259
+ function refreshAnimation(): void {
260
+ triggerStaggeredReveal();
261
+ }
262
+
263
+ return {
264
+ stats,
265
+ loadingStates,
266
+ statsWithLoading,
267
+ allLoaded,
268
+ refreshAnimation,
269
+ getStat,
270
+ };
271
+ }
272
+
273
+ /**
274
+ * Helper: Create a simple count stat definition
275
+ */
276
+ export function countStat<T>(
277
+ id: string,
278
+ name: string,
279
+ icon: Component,
280
+ filter: (item: T) => boolean,
281
+ options?: { color?: StatColor }
282
+ ): StatDefinition<T> {
283
+ return {
284
+ id,
285
+ name,
286
+ icon,
287
+ color: options?.color ?? 'gray',
288
+ compute: (items) => items.filter(filter).length,
289
+ };
290
+ }
291
+
292
+ /**
293
+ * Helper: Create a sum stat definition
294
+ */
295
+ export function sumStat<T>(
296
+ id: string,
297
+ name: string,
298
+ icon: Component,
299
+ getValue: (item: T) => number,
300
+ options?: { color?: StatColor; format?: (value: number) => string }
301
+ ): StatDefinition<T> {
302
+ return {
303
+ id,
304
+ name,
305
+ icon,
306
+ color: options?.color ?? 'gray',
307
+ compute: (items) => {
308
+ const sum = items.reduce((total, item) => total + getValue(item), 0);
309
+ return options?.format ? options.format(sum) : sum.toLocaleString();
310
+ },
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Helper: Create an average stat definition
316
+ */
317
+ export function averageStat<T>(
318
+ id: string,
319
+ name: string,
320
+ icon: Component,
321
+ getValue: (item: T) => number,
322
+ options?: { color?: StatColor; decimals?: number; suffix?: string }
323
+ ): StatDefinition<T> {
324
+ return {
325
+ id,
326
+ name,
327
+ icon,
328
+ color: options?.color ?? 'gray',
329
+ compute: (items) => {
330
+ if (items.length === 0) return '0';
331
+ const sum = items.reduce((total, item) => total + getValue(item), 0);
332
+ const avg = sum / items.length;
333
+ const formatted = avg.toFixed(options?.decimals ?? 1);
334
+ return options?.suffix ? `${formatted}${options.suffix}` : formatted;
335
+ },
336
+ };
337
+ }
338
+
339
+ /**
340
+ * Helper: Create a percentage stat definition
341
+ */
342
+ export function percentageStat<T>(
343
+ id: string,
344
+ name: string,
345
+ icon: Component,
346
+ filter: (item: T) => boolean,
347
+ options?: { color?: StatColor; decimals?: number }
348
+ ): StatDefinition<T> {
349
+ return {
350
+ id,
351
+ name,
352
+ icon,
353
+ color: options?.color ?? 'gray',
354
+ compute: (items) => {
355
+ if (items.length === 0) return '0%';
356
+ const count = items.filter(filter).length;
357
+ const percentage = (count / items.length) * 100;
358
+ return `${percentage.toFixed(options?.decimals ?? 1)}%`;
359
+ },
360
+ };
361
+ }
@@ -122,11 +122,13 @@ export function useTable<T extends Record<string, any>>(
122
122
 
123
123
  // Computed - Filtering
124
124
  const filteredItems = computed(() => {
125
- if (activeFilters.value.length === 0) return items.value;
125
+ // Ensure activeFilters is always an array
126
+ const filters = Array.isArray(activeFilters.value) ? activeFilters.value : [];
127
+ if (filters.length === 0) return items.value;
126
128
 
127
129
  return items.value.filter(item => {
128
130
  // All filters must pass (AND logic by default)
129
- return activeFilters.value.every(filter => {
131
+ return filters.every(filter => {
130
132
  const { category, operator, value } = filter;
131
133
 
132
134
  if (value === undefined || value === null || value === '') return true;
@@ -297,8 +299,27 @@ export function useTable<T extends Record<string, any>>(
297
299
  }
298
300
 
299
301
  // Methods - Filtering
300
- function applyFilters(filters: SmartFilter[]) {
301
- activeFilters.value = filters;
302
+ function applyFilters(filters: SmartFilter[] | Record<string, any>) {
303
+ // Handle various filter formats from UI components
304
+ if (Array.isArray(filters)) {
305
+ activeFilters.value = filters;
306
+ } else if (filters && typeof filters === 'object') {
307
+ // Handle object format like { filters: [...] } or extract values
308
+ if ('filters' in filters && Array.isArray(filters.filters)) {
309
+ activeFilters.value = filters.filters;
310
+ } else {
311
+ // Convert object to SmartFilter array
312
+ activeFilters.value = Object.entries(filters)
313
+ .filter(([_, value]) => value !== undefined && value !== null && value !== '')
314
+ .map(([key, value]) => ({
315
+ category: key,
316
+ operator: 'contains',
317
+ value,
318
+ }));
319
+ }
320
+ } else {
321
+ activeFilters.value = [];
322
+ }
302
323
  currentPage.value = 1; // Reset to first page when filters change
303
324
  }
304
325
 
@@ -314,7 +335,7 @@ export function useTable<T extends Record<string, any>>(
314
335
  }
315
336
  }
316
337
 
317
- function handleSmartFiltersApplied(filters: SmartFilter[]) {
338
+ function handleSmartFiltersApplied(filters: SmartFilter[] | Record<string, any>) {
318
339
  applyFilters(filters);
319
340
  }
320
341