@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.
- package/README.md +52 -0
- package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js +367 -0
- package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js.map +1 -0
- package/dist/Alert.vue_vue_type_script_setup_true_lang-DxPCS-Hx.js +263 -0
- package/dist/Alert.vue_vue_type_script_setup_true_lang-DxPCS-Hx.js.map +1 -0
- package/dist/DateRange.vue_vue_type_script_setup_true_lang-BLVg1Hah.js +580 -0
- package/dist/DateRange.vue_vue_type_script_setup_true_lang-BLVg1Hah.js.map +1 -0
- package/dist/ProductBadge.vue_vue_type_script_setup_true_lang-Cmr2f4Cy.js +187 -0
- package/dist/ProductBadge.vue_vue_type_script_setup_true_lang-Cmr2f4Cy.js.map +1 -0
- package/dist/_plugin-vue_export-helper-1tPrXgE0.js +11 -0
- package/dist/_plugin-vue_export-helper-1tPrXgE0.js.map +1 -0
- package/dist/components.css +15 -0
- package/dist/composables/index.js +32 -573
- package/dist/composables/index.js.map +1 -1
- package/dist/data/index.js +18 -0
- package/dist/data/index.js.map +1 -0
- package/dist/domain/index.js +8 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/filterHelpers-DgRyoYSa.js +1386 -0
- package/dist/filterHelpers-DgRyoYSa.js.map +1 -0
- package/dist/forms/index.js +6 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/index-DGO_pNgG.js +79 -0
- package/dist/index-DGO_pNgG.js.map +1 -0
- package/dist/index-QK97OdqQ.js +25 -0
- package/dist/index-QK97OdqQ.js.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation/index.js +8 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/overlays/index.js +8 -0
- package/dist/overlays/index.js.map +1 -0
- package/dist/stores/index.js +14 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/useAdminPage-GhgXp0x8.js +1070 -0
- package/dist/useAdminPage-GhgXp0x8.js.map +1 -0
- package/dist/useTable-DutR1gkg.js +293 -0
- package/dist/useTable-DutR1gkg.js.map +1 -0
- package/package.json +43 -14
- package/src/composables/composables.md +109 -0
- package/src/composables/index.ts +69 -0
- package/src/composables/useAdminPage.ts +462 -0
- package/src/composables/useConfirmation.ts +358 -0
- package/src/composables/usePageContext.ts +171 -0
- package/src/composables/useStats.ts +361 -0
- package/src/composables/useTable.ts +26 -5
- package/src/composables/useWizard.ts +448 -0
- package/src/data/DataTable.vue +553 -0
- package/src/data/Table/Table.vue +295 -0
- package/src/data/columnHelpers.ts +503 -0
- package/src/data/data.md +106 -0
- package/src/data/filterHelpers.ts +358 -0
- package/src/data/index.ts +31 -0
- package/src/domain/domain.md +102 -0
- package/src/forms/JsonSchemaForm.vue +4 -1
- package/src/forms/forms.md +89 -0
- package/src/index.ts +4 -3
- package/src/navigation/navigation.md +80 -0
- package/src/overlays/overlays.md +86 -0
- package/src/stores/stores.md +82 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column Definition Helpers
|
|
3
|
+
*
|
|
4
|
+
* Pre-built column definitions for common patterns, reducing boilerplate
|
|
5
|
+
* when defining DataTable columns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DataTableColumn } from "./DataTable.vue";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Date formatting options
|
|
12
|
+
*/
|
|
13
|
+
export interface DateFormatOptions {
|
|
14
|
+
format?: "short" | "medium" | "long" | "full";
|
|
15
|
+
locale?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tag color function type
|
|
20
|
+
*/
|
|
21
|
+
export type TagColorFn<T> = (item: T) => string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pre-built column helpers
|
|
25
|
+
*/
|
|
26
|
+
export const columns = {
|
|
27
|
+
/**
|
|
28
|
+
* Simple text column
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* columns.text('name', 'Name')
|
|
32
|
+
*/
|
|
33
|
+
text: <T = any>(key: string, label: string, options?: { sortable?: boolean; width?: string }): DataTableColumn<T> => ({
|
|
34
|
+
key,
|
|
35
|
+
label,
|
|
36
|
+
type: "text",
|
|
37
|
+
sortable: options?.sortable ?? true,
|
|
38
|
+
width: options?.width,
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Number column with locale formatting
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* columns.number('amount', 'Amount')
|
|
46
|
+
* columns.number('price', 'Price', { prefix: '$', decimals: 2 })
|
|
47
|
+
*/
|
|
48
|
+
number: <T = any>(
|
|
49
|
+
key: string,
|
|
50
|
+
label: string,
|
|
51
|
+
options?: { prefix?: string; suffix?: string; decimals?: number; sortable?: boolean }
|
|
52
|
+
): DataTableColumn<T> => ({
|
|
53
|
+
key,
|
|
54
|
+
label,
|
|
55
|
+
type: "number",
|
|
56
|
+
sortable: options?.sortable ?? true,
|
|
57
|
+
render: (item: any) => {
|
|
58
|
+
const value = item[key];
|
|
59
|
+
if (value === null || value === undefined) return "";
|
|
60
|
+
|
|
61
|
+
const formatted =
|
|
62
|
+
options?.decimals !== undefined
|
|
63
|
+
? Number(value).toLocaleString(undefined, {
|
|
64
|
+
minimumFractionDigits: options.decimals,
|
|
65
|
+
maximumFractionDigits: options.decimals,
|
|
66
|
+
})
|
|
67
|
+
: Number(value).toLocaleString();
|
|
68
|
+
|
|
69
|
+
return `${options?.prefix ?? ""}${formatted}${options?.suffix ?? ""}`;
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Date column with formatting
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* columns.date('createdAt', 'Created')
|
|
78
|
+
* columns.date('updatedAt', 'Updated', { format: 'long' })
|
|
79
|
+
*/
|
|
80
|
+
date: <T = any>(key: string, label: string, options?: DateFormatOptions & { sortable?: boolean }): DataTableColumn<T> => ({
|
|
81
|
+
key,
|
|
82
|
+
label,
|
|
83
|
+
type: "date",
|
|
84
|
+
sortable: options?.sortable ?? true,
|
|
85
|
+
render: (item: any) => {
|
|
86
|
+
const value = item[key];
|
|
87
|
+
if (!value) return "";
|
|
88
|
+
|
|
89
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
90
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
91
|
+
|
|
92
|
+
const formatOptions: Intl.DateTimeFormatOptions = {
|
|
93
|
+
short: { dateStyle: "short" },
|
|
94
|
+
medium: { dateStyle: "medium" },
|
|
95
|
+
long: { dateStyle: "long" },
|
|
96
|
+
full: { dateStyle: "full" },
|
|
97
|
+
}[options?.format ?? "medium"] as Intl.DateTimeFormatOptions;
|
|
98
|
+
|
|
99
|
+
return date.toLocaleDateString(options?.locale, formatOptions);
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* DateTime column with formatting
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* columns.dateTime('createdAt', 'Created At')
|
|
108
|
+
*/
|
|
109
|
+
dateTime: <T = any>(key: string, label: string, options?: { sortable?: boolean; locale?: string }): DataTableColumn<T> => ({
|
|
110
|
+
key,
|
|
111
|
+
label,
|
|
112
|
+
type: "date",
|
|
113
|
+
sortable: options?.sortable ?? true,
|
|
114
|
+
render: (item: any) => {
|
|
115
|
+
const value = item[key];
|
|
116
|
+
if (!value) return "";
|
|
117
|
+
|
|
118
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
119
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
120
|
+
|
|
121
|
+
return date.toLocaleString(options?.locale, {
|
|
122
|
+
dateStyle: "medium",
|
|
123
|
+
timeStyle: "short",
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Tag column with optional color function
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* columns.tag('status', 'Status')
|
|
133
|
+
* columns.tag('status', 'Status', (item) => item.active ? 'green' : 'red')
|
|
134
|
+
*/
|
|
135
|
+
tag: <T = any>(key: string, label: string, colorFn?: TagColorFn<T>, options?: { sortable?: boolean }): DataTableColumn<T> => ({
|
|
136
|
+
key,
|
|
137
|
+
label,
|
|
138
|
+
type: "tag",
|
|
139
|
+
sortable: options?.sortable ?? true,
|
|
140
|
+
render: (item: T) => {
|
|
141
|
+
const value = (item as any)[key];
|
|
142
|
+
return {
|
|
143
|
+
content: value,
|
|
144
|
+
color: colorFn?.(item) ?? "gray",
|
|
145
|
+
type: "tag",
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Badge column (numeric badge style)
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* columns.badge('count', 'Count')
|
|
155
|
+
* columns.badge('brandCount', 'Brands')
|
|
156
|
+
*/
|
|
157
|
+
badge: <T = any>(key: string, label: string, options?: { sortable?: boolean }): DataTableColumn<T> => ({
|
|
158
|
+
key,
|
|
159
|
+
label,
|
|
160
|
+
type: "badge",
|
|
161
|
+
sortable: options?.sortable ?? true,
|
|
162
|
+
render: (item: any) => ({
|
|
163
|
+
content: item[key],
|
|
164
|
+
type: "badge",
|
|
165
|
+
}),
|
|
166
|
+
}),
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Boolean column rendered as tag
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* columns.boolean('active', 'Active')
|
|
173
|
+
* columns.boolean('verified', 'Verified', { trueLabel: 'Yes', falseLabel: 'No' })
|
|
174
|
+
*/
|
|
175
|
+
boolean: <T = any>(
|
|
176
|
+
key: string,
|
|
177
|
+
label: string,
|
|
178
|
+
options?: { trueLabel?: string; falseLabel?: string; trueColor?: string; falseColor?: string; sortable?: boolean }
|
|
179
|
+
): DataTableColumn<T> => ({
|
|
180
|
+
key,
|
|
181
|
+
label,
|
|
182
|
+
type: "tag",
|
|
183
|
+
sortable: options?.sortable ?? true,
|
|
184
|
+
render: (item: any) => {
|
|
185
|
+
const value = Boolean(item[key]);
|
|
186
|
+
return {
|
|
187
|
+
content: value ? (options?.trueLabel ?? "Yes") : (options?.falseLabel ?? "No"),
|
|
188
|
+
color: value ? (options?.trueColor ?? "green") : (options?.falseColor ?? "red"),
|
|
189
|
+
type: "tag",
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Email column (renders as link)
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* columns.email('email', 'Email')
|
|
199
|
+
*/
|
|
200
|
+
email: <T = any>(key: string, label: string, options?: { sortable?: boolean }): DataTableColumn<T> => ({
|
|
201
|
+
key,
|
|
202
|
+
label,
|
|
203
|
+
type: "text",
|
|
204
|
+
sortable: options?.sortable ?? true,
|
|
205
|
+
render: (item: any) => ({
|
|
206
|
+
content: item[key],
|
|
207
|
+
type: "link",
|
|
208
|
+
href: `mailto:${item[key]}`,
|
|
209
|
+
}),
|
|
210
|
+
}),
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Actions column (row actions)
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* columns.actions(['view', 'edit', 'delete'])
|
|
217
|
+
*/
|
|
218
|
+
actions: <T = any>(actions: string[]): DataTableColumn<T> => ({
|
|
219
|
+
key: "_actions",
|
|
220
|
+
label: "",
|
|
221
|
+
sortable: false,
|
|
222
|
+
render: () => actions,
|
|
223
|
+
}),
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Custom render column
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* columns.custom('fullName', 'Name', (item) => `${item.firstName} ${item.lastName}`)
|
|
230
|
+
*/
|
|
231
|
+
custom: <T = any>(
|
|
232
|
+
key: string,
|
|
233
|
+
label: string,
|
|
234
|
+
render: (item: T, index: number) => any,
|
|
235
|
+
options?: { sortable?: boolean; width?: string }
|
|
236
|
+
): DataTableColumn<T> => ({
|
|
237
|
+
key,
|
|
238
|
+
label,
|
|
239
|
+
sortable: options?.sortable ?? false,
|
|
240
|
+
width: options?.width,
|
|
241
|
+
render,
|
|
242
|
+
}),
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Truncated text column
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* columns.truncate('description', 'Description', 50)
|
|
249
|
+
*/
|
|
250
|
+
truncate: <T = any>(key: string, label: string, maxLength: number, options?: { sortable?: boolean }): DataTableColumn<T> => ({
|
|
251
|
+
key,
|
|
252
|
+
label,
|
|
253
|
+
type: "text",
|
|
254
|
+
sortable: options?.sortable ?? true,
|
|
255
|
+
render: (item: any) => {
|
|
256
|
+
const value = String(item[key] ?? "");
|
|
257
|
+
if (value.length <= maxLength) return value;
|
|
258
|
+
return `${value.substring(0, maxLength)}...`;
|
|
259
|
+
},
|
|
260
|
+
}),
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Image column
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* columns.image('avatar', 'Avatar', { size: 32 })
|
|
267
|
+
*/
|
|
268
|
+
image: <T = any>(key: string, label: string, options?: { size?: number; fallback?: string }): DataTableColumn<T> => ({
|
|
269
|
+
key,
|
|
270
|
+
label,
|
|
271
|
+
sortable: false,
|
|
272
|
+
render: (item: any) => ({
|
|
273
|
+
content: item[key] || options?.fallback,
|
|
274
|
+
type: "image",
|
|
275
|
+
size: options?.size ?? 32,
|
|
276
|
+
}),
|
|
277
|
+
}),
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Progress column
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* columns.progress('completion', 'Progress')
|
|
284
|
+
*/
|
|
285
|
+
progress: <T = any>(key: string, label: string, options?: { max?: number; sortable?: boolean }): DataTableColumn<T> => ({
|
|
286
|
+
key,
|
|
287
|
+
label,
|
|
288
|
+
type: "number",
|
|
289
|
+
sortable: options?.sortable ?? true,
|
|
290
|
+
render: (item: any) => ({
|
|
291
|
+
content: item[key],
|
|
292
|
+
type: "progress",
|
|
293
|
+
max: options?.max ?? 100,
|
|
294
|
+
}),
|
|
295
|
+
}),
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Clickable link column that emits an action
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* // Simple link with action
|
|
302
|
+
* columns.link('name', 'Campaign Name', (item) => ({
|
|
303
|
+
* action: 'view',
|
|
304
|
+
* data: { id: item.id }
|
|
305
|
+
* }))
|
|
306
|
+
*
|
|
307
|
+
* // Link with custom emit handler in parent
|
|
308
|
+
* columns.link('title', 'Title', (item) => ({
|
|
309
|
+
* action: 'openDetails',
|
|
310
|
+
* data: item
|
|
311
|
+
* }))
|
|
312
|
+
*/
|
|
313
|
+
link: <T = any>(
|
|
314
|
+
key: string,
|
|
315
|
+
label: string,
|
|
316
|
+
emitConfig: (item: T) => { action: string; data: any },
|
|
317
|
+
options?: { sortable?: boolean; width?: string }
|
|
318
|
+
): DataTableColumn<T> => ({
|
|
319
|
+
key,
|
|
320
|
+
label,
|
|
321
|
+
sortable: options?.sortable ?? true,
|
|
322
|
+
width: options?.width,
|
|
323
|
+
render: (item: T) => ({
|
|
324
|
+
content: (item as any)[key],
|
|
325
|
+
type: "link",
|
|
326
|
+
emits: emitConfig(item),
|
|
327
|
+
}),
|
|
328
|
+
}),
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Link to external URL
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* columns.externalLink('website', 'Website')
|
|
335
|
+
* columns.externalLink('url', 'URL', { newTab: true })
|
|
336
|
+
*/
|
|
337
|
+
externalLink: <T = any>(
|
|
338
|
+
key: string,
|
|
339
|
+
label: string,
|
|
340
|
+
options?: { sortable?: boolean; newTab?: boolean; displayKey?: string }
|
|
341
|
+
): DataTableColumn<T> => ({
|
|
342
|
+
key,
|
|
343
|
+
label,
|
|
344
|
+
sortable: options?.sortable ?? true,
|
|
345
|
+
render: (item: any) => {
|
|
346
|
+
const url = item[key];
|
|
347
|
+
const display = options?.displayKey ? item[options.displayKey] : url;
|
|
348
|
+
return {
|
|
349
|
+
content: display,
|
|
350
|
+
type: "link",
|
|
351
|
+
href: url,
|
|
352
|
+
target: options?.newTab ? "_blank" : undefined,
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
}),
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Relative time column (e.g., "2 hours ago")
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* columns.relativeTime('updatedAt', 'Last Updated')
|
|
362
|
+
*/
|
|
363
|
+
relativeTime: <T = any>(key: string, label: string, options?: { sortable?: boolean; locale?: string }): DataTableColumn<T> => ({
|
|
364
|
+
key,
|
|
365
|
+
label,
|
|
366
|
+
type: "date",
|
|
367
|
+
sortable: options?.sortable ?? true,
|
|
368
|
+
render: (item: any) => {
|
|
369
|
+
const value = item[key];
|
|
370
|
+
if (!value) return "";
|
|
371
|
+
|
|
372
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
373
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
374
|
+
|
|
375
|
+
const now = new Date();
|
|
376
|
+
const diffMs = now.getTime() - date.getTime();
|
|
377
|
+
const diffSecs = Math.floor(diffMs / 1000);
|
|
378
|
+
const diffMins = Math.floor(diffSecs / 60);
|
|
379
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
380
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
381
|
+
|
|
382
|
+
if (diffSecs < 60) return "Just now";
|
|
383
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
384
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
385
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
386
|
+
|
|
387
|
+
return date.toLocaleDateString(options?.locale);
|
|
388
|
+
},
|
|
389
|
+
}),
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* User/Avatar column with name and optional subtitle
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* columns.user('assignee', 'Assigned To', { avatarKey: 'avatar', subtitleKey: 'email' })
|
|
396
|
+
*/
|
|
397
|
+
user: <T = any>(
|
|
398
|
+
key: string,
|
|
399
|
+
label: string,
|
|
400
|
+
options?: { avatarKey?: string; subtitleKey?: string; sortable?: boolean }
|
|
401
|
+
): DataTableColumn<T> => ({
|
|
402
|
+
key,
|
|
403
|
+
label,
|
|
404
|
+
sortable: options?.sortable ?? true,
|
|
405
|
+
render: (item: any) => ({
|
|
406
|
+
content: item[key],
|
|
407
|
+
type: "guest",
|
|
408
|
+
avatar: options?.avatarKey ? item[options.avatarKey] : undefined,
|
|
409
|
+
subtitle: options?.subtitleKey ? item[options.subtitleKey] : undefined,
|
|
410
|
+
}),
|
|
411
|
+
}),
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Currency column with formatting
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* columns.currency('amount', 'Total', { currency: 'EUR' })
|
|
418
|
+
* columns.currency('price', 'Price', { currency: 'USD', locale: 'en-US' })
|
|
419
|
+
*/
|
|
420
|
+
currency: <T = any>(
|
|
421
|
+
key: string,
|
|
422
|
+
label: string,
|
|
423
|
+
options?: { currency?: string; locale?: string; sortable?: boolean }
|
|
424
|
+
): DataTableColumn<T> => ({
|
|
425
|
+
key,
|
|
426
|
+
label,
|
|
427
|
+
type: "number",
|
|
428
|
+
sortable: options?.sortable ?? true,
|
|
429
|
+
render: (item: any) => {
|
|
430
|
+
const value = item[key];
|
|
431
|
+
if (value === null || value === undefined) return "";
|
|
432
|
+
|
|
433
|
+
return new Intl.NumberFormat(options?.locale ?? "en-US", {
|
|
434
|
+
style: "currency",
|
|
435
|
+
currency: options?.currency ?? "EUR",
|
|
436
|
+
}).format(Number(value));
|
|
437
|
+
},
|
|
438
|
+
}),
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Percentage column
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* columns.percentage('openRate', 'Open Rate')
|
|
445
|
+
* columns.percentage('completion', 'Progress', { decimals: 0 })
|
|
446
|
+
*/
|
|
447
|
+
percentage: <T = any>(
|
|
448
|
+
key: string,
|
|
449
|
+
label: string,
|
|
450
|
+
options?: { decimals?: number; sortable?: boolean; multiply?: boolean }
|
|
451
|
+
): DataTableColumn<T> => ({
|
|
452
|
+
key,
|
|
453
|
+
label,
|
|
454
|
+
type: "number",
|
|
455
|
+
sortable: options?.sortable ?? true,
|
|
456
|
+
render: (item: any) => {
|
|
457
|
+
const value = item[key];
|
|
458
|
+
if (value === null || value === undefined) return "";
|
|
459
|
+
|
|
460
|
+
// If multiply is true, assume value is 0-1 and needs *100
|
|
461
|
+
const numValue = options?.multiply ? Number(value) * 100 : Number(value);
|
|
462
|
+
return `${numValue.toFixed(options?.decimals ?? 1)}%`;
|
|
463
|
+
},
|
|
464
|
+
}),
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Status color mappings for common status values
|
|
469
|
+
*/
|
|
470
|
+
export const statusColors: Record<string, string> = {
|
|
471
|
+
active: "green",
|
|
472
|
+
inactive: "gray",
|
|
473
|
+
pending: "yellow",
|
|
474
|
+
approved: "green",
|
|
475
|
+
rejected: "red",
|
|
476
|
+
draft: "gray",
|
|
477
|
+
published: "green",
|
|
478
|
+
archived: "gray",
|
|
479
|
+
error: "red",
|
|
480
|
+
success: "green",
|
|
481
|
+
warning: "yellow",
|
|
482
|
+
info: "blue",
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Get color for a status value
|
|
487
|
+
*
|
|
488
|
+
* @example
|
|
489
|
+
* columns.tag('status', 'Status', (item) => getStatusColor(item.status))
|
|
490
|
+
*/
|
|
491
|
+
export function getStatusColor(status: string): string {
|
|
492
|
+
return statusColors[status?.toLowerCase()] ?? "gray";
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Create a status color function for a specific key
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* columns.tag('status', 'Status', createStatusColorFn('status'))
|
|
500
|
+
*/
|
|
501
|
+
export function createStatusColorFn<T>(key: keyof T): TagColorFn<T> {
|
|
502
|
+
return (item: T) => getStatusColor(String(item[key]));
|
|
503
|
+
}
|
package/src/data/data.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Data Module
|
|
2
|
+
|
|
3
|
+
Components for displaying and managing data.
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
|
|
7
|
+
### Table
|
|
8
|
+
|
|
9
|
+
Comprehensive data table with pagination, sorting, and filtering.
|
|
10
|
+
|
|
11
|
+
```vue
|
|
12
|
+
<script setup>
|
|
13
|
+
import { ref } from 'vue';
|
|
14
|
+
import { Table } from '@htlkg/components/data';
|
|
15
|
+
|
|
16
|
+
const currentPage = ref(1);
|
|
17
|
+
const columns = [
|
|
18
|
+
{ name: 'Name', value: 'name', sortable: true },
|
|
19
|
+
{ name: 'Email', value: 'email' },
|
|
20
|
+
{ name: 'Status', value: 'status' },
|
|
21
|
+
];
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<Table
|
|
26
|
+
v-model:currentPage="currentPage"
|
|
27
|
+
:items="users"
|
|
28
|
+
:columns="columns"
|
|
29
|
+
:loading="loading"
|
|
30
|
+
selectable
|
|
31
|
+
@row-click="handleRowClick"
|
|
32
|
+
@selection-change="handleSelection"
|
|
33
|
+
/>
|
|
34
|
+
</template>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### DataList
|
|
38
|
+
|
|
39
|
+
Simple list component for displaying collections.
|
|
40
|
+
|
|
41
|
+
```vue
|
|
42
|
+
<script setup>
|
|
43
|
+
import { DataList } from '@htlkg/components/data';
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<DataList :items="items" :loading="loading">
|
|
48
|
+
<template #item="{ item }">
|
|
49
|
+
<div>{{ item.name }}</div>
|
|
50
|
+
</template>
|
|
51
|
+
<template #empty>
|
|
52
|
+
<p>No items found</p>
|
|
53
|
+
</template>
|
|
54
|
+
</DataList>
|
|
55
|
+
</template>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### SearchableSelect
|
|
59
|
+
|
|
60
|
+
Searchable dropdown with single/multiple selection.
|
|
61
|
+
|
|
62
|
+
```vue
|
|
63
|
+
<script setup>
|
|
64
|
+
import { ref } from 'vue';
|
|
65
|
+
import { SearchableSelect } from '@htlkg/components/data';
|
|
66
|
+
|
|
67
|
+
const selected = ref(null);
|
|
68
|
+
const options = [
|
|
69
|
+
{ value: '1', label: 'Option 1' },
|
|
70
|
+
{ value: '2', label: 'Option 2' },
|
|
71
|
+
];
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<SearchableSelect
|
|
76
|
+
v-model="selected"
|
|
77
|
+
:options="options"
|
|
78
|
+
placeholder="Select..."
|
|
79
|
+
searchable
|
|
80
|
+
clearable
|
|
81
|
+
/>
|
|
82
|
+
</template>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Chart
|
|
86
|
+
|
|
87
|
+
Data visualization with multiple chart types.
|
|
88
|
+
|
|
89
|
+
```vue
|
|
90
|
+
<script setup>
|
|
91
|
+
import { Chart } from '@htlkg/components/data';
|
|
92
|
+
|
|
93
|
+
const data = {
|
|
94
|
+
labels: ['Jan', 'Feb', 'Mar'],
|
|
95
|
+
datasets: [{ data: [10, 20, 30] }],
|
|
96
|
+
};
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<template>
|
|
100
|
+
<Chart
|
|
101
|
+
type="line"
|
|
102
|
+
:data="data"
|
|
103
|
+
:options="{ responsive: true }"
|
|
104
|
+
/>
|
|
105
|
+
</template>
|
|
106
|
+
```
|