@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,642 @@
1
+ # Dashboard Page Pattern
2
+
3
+ Display key metrics, statistics, and charts with date range filtering and tabbed navigation.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────┐
9
+ │ AdminLayout │
10
+ ├─────────────────────────────────────────────────────────────┤
11
+ │ Page Header (title + breadcrumbs) │
12
+ ├─────────────────────────────────────────────────────────────┤
13
+ │ Date Range Filter (right-aligned) │
14
+ ├─────────────────────────────────────────────────────────────┤
15
+ │ Tabs (Overview | Campaigns | Revenue | Contacts | ...) │
16
+ ├─────────────────────────────────────────────────────────────┤
17
+ │ Stats Grid (1-4 columns) │
18
+ │ ├── Stat 1: Active Campaigns │ Stat 2: Total Contacts │
19
+ │ └── Stat 3: Reservations │ Stat 4: Revenue │
20
+ ├─────────────────────────────────────────────────────────────┤
21
+ │ Section Title (e.g., "Email Performance") │
22
+ ├─────────────────────────────────────────────────────────────┤
23
+ │ Stats Grid (Secondary metrics) │
24
+ │ ├── Email Delivered │ Open Rate │ CTR │ Deliverability │
25
+ ├─────────────────────────────────────────────────────────────┤
26
+ │ Charts Grid (2 columns) │
27
+ │ ├── Chart 1: Database Growth │ Chart 2: Database Health │
28
+ │ └── Chart 3: Engagement │ Chart 4: Revenue │
29
+ └─────────────────────────────────────────────────────────────┘
30
+ ```
31
+
32
+ ## CRM Template Reference
33
+
34
+ Based on: `ui/src/components/Templates/CRM/Dashboard.vue`
35
+
36
+ ```vue
37
+ <!-- CRM Template Structure (raw @hotelinking/ui) -->
38
+ <uiWrapper :sidebar :topbar>
39
+ <uiViewHeader :pages="pages" title="Dashboard - Overview" :loading="loadingHeader" />
40
+
41
+ <!-- Date Range Filter -->
42
+ <div class="flex justify-end items-center mb-4 w-full">
43
+ <uiDateRange
44
+ id="charts-date-range"
45
+ :literals="{ from: 'From', to: 'To', search: 'Search' }"
46
+ :values="dateRangeValues"
47
+ :loading="loadingDateRange"
48
+ @uiDateRangeButtonClicked="handleDateRangeSearch"
49
+ />
50
+ </div>
51
+
52
+ <!-- Tabs -->
53
+ <uiTabs :tabs="tabs" @tabClicked="handleTabClick" />
54
+
55
+ <!-- Primary Stats Grid -->
56
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 my-8">
57
+ <uiStats
58
+ v-for="(stat, index) in stats"
59
+ :key="stat.id"
60
+ :item="stat"
61
+ :loading="loadingStats[index]"
62
+ @statClick="handleStatClick"
63
+ />
64
+ </div>
65
+
66
+ <!-- Section Title -->
67
+ <uiSectionTitle title="Email Performance Overview" />
68
+
69
+ <!-- Secondary Stats Grid -->
70
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
71
+ <uiStats
72
+ v-for="(stat, index) in emailStats"
73
+ :key="stat.id"
74
+ :item="stat"
75
+ :loading="loadingEmailStats[index]"
76
+ />
77
+ </div>
78
+
79
+ <!-- Charts Grid -->
80
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
81
+ <uiChart
82
+ id="database-growth"
83
+ title="Database Growth"
84
+ type="area"
85
+ :series="series"
86
+ :options="options"
87
+ :loading="loadingChart"
88
+ />
89
+ <!-- More charts... -->
90
+ </div>
91
+ </uiWrapper>
92
+ ```
93
+
94
+ ## Enhanced Pattern (with @htlkg/*)
95
+
96
+ ### Astro Page
97
+
98
+ ```astro
99
+ ---
100
+ // src/pages/[brandId]/admin/dashboard.astro
101
+ import Layout from '@/layouts/Layout.astro';
102
+ import DashboardView from '@/components/admin/DashboardView.vue';
103
+ import { getDashboardData } from '@/services/dashboard';
104
+
105
+ export const prerender = false;
106
+
107
+ const { brandId } = Astro.params;
108
+ const initialData = await getDashboardData(brandId);
109
+
110
+ const layoutProps = {
111
+ title: 'Dashboard',
112
+ breadcrumbs: [
113
+ { name: 'Dashboard', current: true },
114
+ ],
115
+ };
116
+ ---
117
+
118
+ <Layout {...layoutProps}>
119
+ <DashboardView client:load brandId={brandId} initialData={initialData} />
120
+ </Layout>
121
+ ```
122
+
123
+ ### Vue Component with useStats
124
+
125
+ ```vue
126
+ <!-- src/components/admin/DashboardView.vue -->
127
+ <script setup lang="ts">
128
+ import { ref, computed } from 'vue';
129
+ import { Chart } from '@htlkg/components/data';
130
+ import { DateRange } from '@htlkg/components/forms';
131
+ import { useStats, useTabs, countStat, sumStat, percentageStat } from '@htlkg/components/composables';
132
+ import { uiStats, uiSectionTitle } from '@hotelinking/ui';
133
+ import {
134
+ MegaphoneIcon,
135
+ UsersIcon,
136
+ CalendarIcon,
137
+ CurrencyDollarIcon,
138
+ EnvelopeIcon,
139
+ EyeIcon,
140
+ CursorArrowRaysIcon,
141
+ CheckCircleIcon,
142
+ } from '@heroicons/vue/24/outline';
143
+
144
+ interface DashboardData {
145
+ campaigns: Campaign[];
146
+ contacts: Contact[];
147
+ reservations: Reservation[];
148
+ emailStats: EmailStats;
149
+ }
150
+
151
+ interface Props {
152
+ brandId: string;
153
+ initialData: DashboardData;
154
+ }
155
+
156
+ const props = defineProps<Props>();
157
+
158
+ // Date range state
159
+ const dateRange = ref({
160
+ from: '',
161
+ to: new Date().toISOString().slice(0, 16),
162
+ });
163
+
164
+ // Tabs
165
+ const { tabs, activeTab, setActiveTab } = useTabs({
166
+ tabs: [
167
+ { id: 'overview', label: 'Overview' },
168
+ { id: 'campaigns', label: 'Campaigns' },
169
+ { id: 'revenue', label: 'Revenue' },
170
+ { id: 'contacts', label: 'Contacts' },
171
+ { id: 'insights', label: 'Insights' },
172
+ ],
173
+ initialTab: 'overview',
174
+ });
175
+
176
+ // Data refs (would come from data hooks in real app)
177
+ const campaigns = ref(props.initialData.campaigns);
178
+ const contacts = ref(props.initialData.contacts);
179
+ const reservations = ref(props.initialData.reservations);
180
+ const loading = ref(false);
181
+
182
+ // Primary Stats with useStats
183
+ const { statsWithLoading: primaryStats } = useStats({
184
+ data: computed(() => ({
185
+ campaigns: campaigns.value,
186
+ contacts: contacts.value,
187
+ reservations: reservations.value,
188
+ })),
189
+ loading,
190
+ definitions: [
191
+ {
192
+ id: 'active-campaigns',
193
+ name: 'Active Campaigns',
194
+ icon: MegaphoneIcon,
195
+ color: 'green',
196
+ compute: (data) => data.campaigns.filter(c => c.status === 'active').length,
197
+ computeChange: () => '+12.5%',
198
+ computeChangeType: () => 'increase',
199
+ showFooter: true,
200
+ actionText: 'View all',
201
+ },
202
+ {
203
+ id: 'total-contacts',
204
+ name: 'Total Contacts',
205
+ icon: UsersIcon,
206
+ color: 'green',
207
+ compute: (data) => data.contacts.length,
208
+ computeChange: () => '+8.2%',
209
+ computeChangeType: () => 'increase',
210
+ showFooter: true,
211
+ actionText: 'View all',
212
+ },
213
+ {
214
+ id: 'active-reservations',
215
+ name: 'Active Reservations',
216
+ icon: CalendarIcon,
217
+ color: 'green',
218
+ compute: (data) => data.reservations.filter(r => r.status === 'active').length,
219
+ computeChange: () => '-3.1%',
220
+ computeChangeType: () => 'decrease',
221
+ showFooter: true,
222
+ actionText: 'View all',
223
+ },
224
+ {
225
+ id: 'revenue-month',
226
+ name: 'Revenue This Month',
227
+ icon: CurrencyDollarIcon,
228
+ color: 'green',
229
+ compute: (data) => {
230
+ const total = data.reservations.reduce((sum, r) => sum + r.amount, 0);
231
+ return `$${total.toLocaleString()}`;
232
+ },
233
+ computeChange: () => '+20.1%',
234
+ computeChangeType: () => 'increase',
235
+ },
236
+ ],
237
+ staggerDelay: 100,
238
+ });
239
+
240
+ // Email Stats
241
+ const emailStats = ref([
242
+ {
243
+ id: 'emails-delivered',
244
+ name: 'Emails Delivered',
245
+ stat: '2.4M',
246
+ icon: EnvelopeIcon,
247
+ color: 'green',
248
+ change: '+156K this month',
249
+ changeType: 'increase',
250
+ },
251
+ {
252
+ id: 'average-open-rate',
253
+ name: 'Average Open Rate',
254
+ stat: '24.8%',
255
+ icon: EyeIcon,
256
+ color: 'green',
257
+ change: '+2.1% vs last month',
258
+ changeType: 'increase',
259
+ },
260
+ {
261
+ id: 'average-ctr',
262
+ name: 'Average CTR',
263
+ stat: '3.2%',
264
+ icon: CursorArrowRaysIcon,
265
+ color: 'green',
266
+ change: '+0.4% vs last month',
267
+ changeType: 'increase',
268
+ },
269
+ {
270
+ id: 'deliverability-rate',
271
+ name: 'Deliverability Rate',
272
+ stat: '98.1%',
273
+ icon: CheckCircleIcon,
274
+ color: 'green',
275
+ change: '+0.3% vs last month',
276
+ changeType: 'increase',
277
+ },
278
+ ]);
279
+
280
+ // Chart data
281
+ const databaseGrowthSeries = ref([
282
+ {
283
+ name: 'Total Contacts',
284
+ data: [1200, 1350, 1500, 1680, 1850, 2100, 2350, 2600, 2850, 3100, 3350, 3600],
285
+ },
286
+ {
287
+ name: 'Active Users',
288
+ data: [800, 920, 1050, 1180, 1320, 1500, 1680, 1850, 2020, 2200, 2380, 2550],
289
+ },
290
+ ]);
291
+
292
+ const chartOptions = {
293
+ xaxis: {
294
+ categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
295
+ },
296
+ };
297
+
298
+ // Handlers
299
+ function handleDateRangeChange(dates: { from: string; to: string }) {
300
+ dateRange.value = dates;
301
+ // Refetch data with new date range
302
+ fetchDashboardData();
303
+ }
304
+
305
+ function handleStatClick(statId: string) {
306
+ if (statId === 'active-campaigns') {
307
+ window.location.href = `/${props.brandId}/admin/campaigns`;
308
+ } else if (statId === 'total-contacts') {
309
+ window.location.href = `/${props.brandId}/admin/contacts`;
310
+ } else if (statId === 'active-reservations') {
311
+ window.location.href = `/${props.brandId}/admin/reservations`;
312
+ }
313
+ }
314
+
315
+ async function fetchDashboardData() {
316
+ loading.value = true;
317
+ try {
318
+ // Fetch data with date range
319
+ const data = await getDashboardData(props.brandId, dateRange.value);
320
+ campaigns.value = data.campaigns;
321
+ contacts.value = data.contacts;
322
+ reservations.value = data.reservations;
323
+ } finally {
324
+ loading.value = false;
325
+ }
326
+ }
327
+ </script>
328
+
329
+ <template>
330
+ <!-- Date Range Filter -->
331
+ <div class="flex justify-end items-center mb-4 w-full">
332
+ <DateRange
333
+ id="dashboard-date-range"
334
+ :values="dateRange"
335
+ :loading="loading"
336
+ @search="handleDateRangeChange"
337
+ />
338
+ </div>
339
+
340
+ <!-- Tabs -->
341
+ <div class="border-b border-gray-200 mb-4">
342
+ <nav class="-mb-px flex space-x-8">
343
+ <button
344
+ v-for="tab in tabs"
345
+ :key="tab.id"
346
+ :class="[
347
+ 'py-4 px-1 border-b-2 font-medium text-sm',
348
+ activeTab === tab.id
349
+ ? 'border-indigo-500 text-indigo-600'
350
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
351
+ ]"
352
+ @click="setActiveTab(tab.id)"
353
+ >
354
+ {{ tab.label }}
355
+ </button>
356
+ </nav>
357
+ </div>
358
+
359
+ <!-- Primary Stats Grid -->
360
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 my-8">
361
+ <uiStats
362
+ v-for="stat in primaryStats"
363
+ :key="stat.id"
364
+ :item="stat"
365
+ :loading="stat.loading"
366
+ @statClick="handleStatClick"
367
+ />
368
+ </div>
369
+
370
+ <!-- Email Performance Section -->
371
+ <uiSectionTitle title="Email Performance Overview" />
372
+
373
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
374
+ <uiStats
375
+ v-for="stat in emailStats"
376
+ :key="stat.id"
377
+ :item="stat"
378
+ :loading="loading"
379
+ />
380
+ </div>
381
+
382
+ <!-- Charts Grid -->
383
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
384
+ <Chart
385
+ id="database-growth"
386
+ title="Database Growth"
387
+ type="area"
388
+ :series="databaseGrowthSeries"
389
+ :options="chartOptions"
390
+ :loading="loading"
391
+ />
392
+ <Chart
393
+ id="database-status"
394
+ title="Database Health"
395
+ type="bar"
396
+ :series="databaseStatusSeries"
397
+ :options="chartOptions"
398
+ :stacked="true"
399
+ :loading="loading"
400
+ />
401
+ <Chart
402
+ id="monthly-engagement"
403
+ title="Monthly Engagement Metrics"
404
+ type="area"
405
+ :series="monthlyEngagementSeries"
406
+ :options="chartOptions"
407
+ :loading="loading"
408
+ />
409
+ <Chart
410
+ id="revenue-booking"
411
+ title="Revenue and Booking Performance"
412
+ type="area"
413
+ :series="revenueBookingSeries"
414
+ :options="chartOptions"
415
+ :loading="loading"
416
+ />
417
+ </div>
418
+ </template>
419
+ ```
420
+
421
+ ## Key Components
422
+
423
+ ### useStats Composable
424
+
425
+ Derives statistics from reactive data with staggered loading animation.
426
+
427
+ ```typescript
428
+ import { useStats, countStat, sumStat, percentageStat } from '@htlkg/components/composables';
429
+
430
+ const { stats, loadingStates, statsWithLoading, allLoaded, refreshAnimation, getStat } = useStats({
431
+ // Reactive data source
432
+ data: campaigns,
433
+ // Loading state
434
+ loading: campaignsLoading,
435
+ // Stat definitions
436
+ definitions: [
437
+ {
438
+ id: 'active',
439
+ name: 'Active Campaigns',
440
+ icon: CheckCircleIcon,
441
+ color: 'green',
442
+ compute: (items) => items.filter(c => c.status === 'active').length,
443
+ computeChange: (items, prev) => `+${items.length - (prev?.length ?? 0)}`,
444
+ computeChangeType: () => 'increase',
445
+ showFooter: true,
446
+ actionText: 'View all',
447
+ },
448
+ ],
449
+ // Animation settings
450
+ staggerDelay: 100, // Delay between each stat reveal
451
+ initialDelay: 0, // Delay before first stat reveals
452
+ // Optional: Previous data for computing changes
453
+ previousData: previousCampaigns,
454
+ });
455
+ ```
456
+
457
+ ### Stat Definition Helpers
458
+
459
+ ```typescript
460
+ // Simple count stat
461
+ countStat<Campaign>(
462
+ 'active',
463
+ 'Active Campaigns',
464
+ CheckCircleIcon,
465
+ (c) => c.status === 'active',
466
+ { color: 'green' }
467
+ );
468
+
469
+ // Sum stat
470
+ sumStat<Campaign>(
471
+ 'total-sent',
472
+ 'Total Emails Sent',
473
+ PaperAirplaneIcon,
474
+ (c) => c.sentCount,
475
+ { color: 'blue', format: (val) => val.toLocaleString() }
476
+ );
477
+
478
+ // Percentage stat
479
+ percentageStat<Contact>(
480
+ 'active-rate',
481
+ 'Active Rate',
482
+ ChartBarIcon,
483
+ (c) => c.status === 'active',
484
+ { color: 'green', decimals: 1 }
485
+ );
486
+
487
+ // Average stat
488
+ averageStat<Campaign>(
489
+ 'avg-open-rate',
490
+ 'Avg Open Rate',
491
+ EyeIcon,
492
+ (c) => c.openRate,
493
+ { color: 'blue', decimals: 1, suffix: '%' }
494
+ );
495
+ ```
496
+
497
+ ### Stat Item Interface
498
+
499
+ ```typescript
500
+ interface StatItem {
501
+ id: string;
502
+ name: string;
503
+ stat: string | number;
504
+ icon: Component;
505
+ color: 'green' | 'red' | 'yellow' | 'blue' | 'gray' | 'purple' | 'orange' | 'pink';
506
+ change?: string; // e.g., "+12.5%", "+156K this month"
507
+ changeType?: 'increase' | 'decrease' | 'neutral';
508
+ showFooter?: boolean;
509
+ actionText?: string; // e.g., "View all"
510
+ }
511
+ ```
512
+
513
+ ### Chart Component
514
+
515
+ ```vue
516
+ <Chart
517
+ id="unique-chart-id"
518
+ title="Chart Title"
519
+ type="area" // area, bar, line, donut
520
+ :series="chartSeries"
521
+ :options="chartOptions"
522
+ :stacked="false" // For bar charts
523
+ :loading="loading"
524
+ />
525
+ ```
526
+
527
+ ### DateRange Component
528
+
529
+ ```vue
530
+ <DateRange
531
+ id="date-range-id"
532
+ :values="{ from: '', to: '' }"
533
+ :loading="loading"
534
+ @search="handleDateRangeSearch"
535
+ />
536
+ ```
537
+
538
+ ## Grid Layouts
539
+
540
+ ### Stats Grid
541
+
542
+ ```vue
543
+ <!-- 4 columns on large screens, 2 on medium, 1 on mobile -->
544
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
545
+ <uiStats v-for="stat in stats" :key="stat.id" :item="stat" />
546
+ </div>
547
+ ```
548
+
549
+ ### Charts Grid
550
+
551
+ ```vue
552
+ <!-- 2 columns on large screens, 1 on smaller -->
553
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
554
+ <Chart ... />
555
+ <Chart ... />
556
+ </div>
557
+ ```
558
+
559
+ ## Staggered Loading Pattern
560
+
561
+ The CRM template shows staggered loading where each stat reveals after a delay:
562
+
563
+ ```typescript
564
+ // Manual approach (from CRM template)
565
+ const loadingStats = ref([true, true, true, true]);
566
+ setTimeout(() => loadingStats.value[0] = false, 300);
567
+ setTimeout(() => loadingStats.value[1] = false, 400);
568
+ setTimeout(() => loadingStats.value[2] = false, 500);
569
+ setTimeout(() => loadingStats.value[3] = false, 600);
570
+
571
+ // Enhanced approach with useStats (automatic)
572
+ const { statsWithLoading } = useStats({
573
+ data: campaigns,
574
+ loading: campaignsLoading,
575
+ definitions: [...],
576
+ staggerDelay: 100, // 100ms between each stat
577
+ });
578
+
579
+ // statsWithLoading includes `loading` property for each stat
580
+ <uiStats
581
+ v-for="stat in statsWithLoading"
582
+ :key="stat.id"
583
+ :item="stat"
584
+ :loading="stat.loading"
585
+ />
586
+ ```
587
+
588
+ ## Tab-Based Views
589
+
590
+ For dashboards with multiple views (Overview, Campaigns, Revenue, etc.):
591
+
592
+ ```vue
593
+ <script setup>
594
+ import { useTabs } from '@htlkg/components/composables';
595
+
596
+ const { tabs, activeTab, setActiveTab } = useTabs({
597
+ tabs: [
598
+ { id: 'overview', label: 'Overview' },
599
+ { id: 'campaigns', label: 'Campaigns' },
600
+ { id: 'revenue', label: 'Revenue' },
601
+ ],
602
+ initialTab: 'overview',
603
+ onTabChange: (tabId) => {
604
+ // Load tab-specific data
605
+ fetchTabData(tabId);
606
+ },
607
+ });
608
+ </script>
609
+
610
+ <template>
611
+ <!-- Overview Tab Content -->
612
+ <div v-if="activeTab === 'overview'">
613
+ <!-- Stats and charts -->
614
+ </div>
615
+
616
+ <!-- Campaigns Tab Content -->
617
+ <div v-else-if="activeTab === 'campaigns'">
618
+ <!-- Campaign-specific metrics -->
619
+ </div>
620
+
621
+ <!-- Revenue Tab Content -->
622
+ <div v-else-if="activeTab === 'revenue'">
623
+ <!-- Revenue-specific charts -->
624
+ </div>
625
+ </template>
626
+ ```
627
+
628
+ ## Checklist
629
+
630
+ - [ ] Astro page with layout
631
+ - [ ] Vue component with stats and charts
632
+ - [ ] `useStats` composable for derived statistics
633
+ - [ ] Staggered loading animation
634
+ - [ ] Date range filter
635
+ - [ ] Tab navigation (if multiple views)
636
+ - [ ] Stats grid (4 columns)
637
+ - [ ] Section titles for grouping
638
+ - [ ] Charts grid (2 columns)
639
+ - [ ] Chart types: area, bar, line, donut
640
+ - [ ] Stat click handlers for navigation
641
+ - [ ] Loading states for all components
642
+ - [ ] Responsive grid layouts