@htlkg/components 0.0.2 → 0.0.3

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.
@@ -0,0 +1,295 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue';
3
+ import {
4
+ type TableItemType,
5
+ type UiDropdownItemType,
6
+ type UiModalInterface,
7
+ type UiTableInterface,
8
+ uiTable
9
+ } from '@hotelinking/ui';
10
+
11
+ interface Props {
12
+ // Data
13
+ items: TableItemType[];
14
+ header: UiTableInterface['header'];
15
+
16
+ // State
17
+ loading?: boolean;
18
+ currentPage?: number;
19
+ totalPages?: number;
20
+ totalItems?: number;
21
+ pageSize?: number;
22
+ orderedBy?: string;
23
+ orderDirection?: 'asc' | 'desc';
24
+
25
+ // Selection
26
+ actions?: Array<{ name: string; id: string }>;
27
+ selectAllItemsModal?: UiModalInterface;
28
+ resetSelected?: boolean;
29
+
30
+ // Filters (now integrated in uiTable)
31
+ smartFilterCategories?: Array<{
32
+ id: string;
33
+ name: string;
34
+ componentType: 'uiInput' | 'uiSelect';
35
+ defaultProps: any;
36
+ }>;
37
+ filterLiterals?: {
38
+ filters: string;
39
+ contains: string;
40
+ is: string;
41
+ and: string;
42
+ or: string;
43
+ deleteAll: string;
44
+ filter: string;
45
+ };
46
+ filters?: {
47
+ logicOperator: string;
48
+ filters: Array<{
49
+ name: string;
50
+ label: string;
51
+ type: 'uiInput' | 'uiSelect';
52
+ value: string | undefined;
53
+ }>;
54
+ };
55
+
56
+ // Additional features
57
+ tableActionsDropdown?: { items: UiDropdownItemType[]; loading?: boolean };
58
+ tableActionButtons?: Array<{
59
+ text: string;
60
+ id: string;
61
+ color?: string;
62
+ size?: string;
63
+ disabled?: boolean;
64
+ icon?: any;
65
+ loading?: boolean;
66
+ }>;
67
+ noResults?: {
68
+ title: string;
69
+ message: string;
70
+ actions?: Array<{ action: string; text: string }>;
71
+ items?: UiDropdownItemType[];
72
+ select?: UiDropdownItemType;
73
+ };
74
+ hiddenColumns?: number[];
75
+ }
76
+
77
+ const props = withDefaults(defineProps<Props>(), {
78
+ loading: false,
79
+ currentPage: 1,
80
+ totalPages: 1,
81
+ totalItems: 0,
82
+ pageSize: 10,
83
+ orderedBy: '',
84
+ orderDirection: 'asc',
85
+ resetSelected: false,
86
+ filterLiterals: () => ({
87
+ filters: 'Smart Filters',
88
+ contains: 'contains',
89
+ is: 'is',
90
+ and: 'and',
91
+ or: 'or',
92
+ deleteAll: 'Delete All',
93
+ filter: 'Filter'
94
+ })
95
+ });
96
+
97
+ const emit = defineEmits<{
98
+ 'update:currentPage': [page: number];
99
+ 'update:pageSize': [size: number];
100
+ 'update:selected': [items: any[]];
101
+ 'update:orderedBy': [key: string];
102
+ 'update:orderDirection': [direction: 'asc' | 'desc'];
103
+ 'update:resetSelected': [value: boolean];
104
+ 'table-action': [data: { action: string; items: Array<string | number> }];
105
+ 'table-action-selected': [item: any];
106
+ 'table-action-button-clicked': [data: { id: string; text: string }];
107
+ 'smart-filters-sent': [filters: any];
108
+ 'smart-filters-cleared': [];
109
+ 'smart-filter-deleted': [index: number];
110
+ 'custom-emit': [data: any];
111
+ 'modal-action': [data: { modal: string; action: string }];
112
+ 'select-all-items': [];
113
+ 'deselect-all-items': [];
114
+ 'no-results-action': [action: string];
115
+ 'no-results-option-selected': [item: any];
116
+ 'columns-visibility-changed': [data: { index: number; hidden: boolean }];
117
+ 'order-by': [data: { value: string; orderDirection: 'asc' | 'desc' }];
118
+ 'change-page': [page: number];
119
+ 'change-page-size': [size: number | string];
120
+ }>();
121
+
122
+ // State management
123
+ const internalHiddenColumns = ref<number[]>(props.hiddenColumns || []);
124
+ const selectedItemIds = ref<Set<string | number>>(new Set());
125
+
126
+ // Page size options for uiTable
127
+ const pageSizeOptions = computed(() => [
128
+ { name: '10', value: '10', active: props.pageSize === 10 },
129
+ { name: '25', value: '25', active: props.pageSize === 25 },
130
+ { name: '50', value: '50', active: props.pageSize === 50 },
131
+ { name: '100', value: '100', active: props.pageSize === 100 }
132
+ ]);
133
+
134
+ // Event handlers
135
+ function handleOrderBy(event: { value: string; orderDirection: 'asc' | 'desc' }) {
136
+ emit('update:orderedBy', event.value);
137
+ emit('update:orderDirection', event.orderDirection);
138
+ emit('order-by', event);
139
+ }
140
+
141
+ function handleChangePage(page: number) {
142
+ emit('update:currentPage', page);
143
+ emit('change-page', page);
144
+ }
145
+
146
+ function handleChangePageSize(size: number | string) {
147
+ const numSize = typeof size === 'string' ? Number(size) : size;
148
+ emit('update:pageSize', numSize);
149
+ emit('change-page-size', size);
150
+ emit('update:currentPage', 1);
151
+ }
152
+
153
+ function handleTableAction(data: { action: string; items: Array<string | number> }) {
154
+ selectedItemIds.value = new Set(data.items);
155
+ updateSelectedItems();
156
+ emit('table-action', data);
157
+ }
158
+
159
+ function resetSelectedItems() {
160
+ selectedItemIds.value.clear();
161
+ updateSelectedItems();
162
+ emit('update:resetSelected', true);
163
+ }
164
+
165
+ function handleTableActionSelected(item: any) {
166
+ emit('table-action-selected', item);
167
+ }
168
+
169
+ function handleTableActionButtonClicked(data: { id: string; text: string }) {
170
+ emit('table-action-button-clicked', data);
171
+ }
172
+
173
+ function handleSmartFiltersSent(filters: any) {
174
+ emit('smart-filters-sent', filters);
175
+ }
176
+
177
+ function handleSmartFiltersCleared() {
178
+ emit('smart-filters-cleared');
179
+ }
180
+
181
+ function handleSmartFilterDeleted(index: number) {
182
+ emit('smart-filter-deleted', index);
183
+ }
184
+
185
+ function handleCustomEmit(data: any) {
186
+ emit('custom-emit', data);
187
+ }
188
+
189
+ function handleColumnsVisibilityChanged(event: { index: number; hidden: boolean }) {
190
+ if (event.hidden) {
191
+ if (!internalHiddenColumns.value.includes(event.index)) {
192
+ internalHiddenColumns.value.push(event.index);
193
+ }
194
+ } else {
195
+ const index = internalHiddenColumns.value.indexOf(event.index);
196
+ if (index > -1) {
197
+ internalHiddenColumns.value.splice(index, 1);
198
+ }
199
+ }
200
+ emit('columns-visibility-changed', event);
201
+ }
202
+
203
+ function handleModalAction(data: { modal: string; action: string }) {
204
+ emit('modal-action', data);
205
+ }
206
+
207
+ function handleSelectAllItems() {
208
+ selectedItemIds.value = new Set(props.items.map(item => item.id));
209
+ updateSelectedItems();
210
+ emit('select-all-items');
211
+ }
212
+
213
+ function handleDeselectAllItems() {
214
+ selectedItemIds.value.clear();
215
+ updateSelectedItems();
216
+ emit('deselect-all-items');
217
+ }
218
+
219
+ function handleNoResultsAction(action: string) {
220
+ emit('no-results-action', action);
221
+ }
222
+
223
+ function handleNoResultsOptionSelected(item: any) {
224
+ emit('no-results-option-selected', item);
225
+ }
226
+
227
+ function handleSelectedItemsDeleted() {
228
+ emit('update:resetSelected', false);
229
+ }
230
+
231
+ function updateSelectedItems() {
232
+ const selected = props.items.filter(item => item.id && selectedItemIds.value.has(item.id));
233
+ emit('update:selected', selected);
234
+ }
235
+
236
+ // Expose methods for parent components
237
+ defineExpose({
238
+ clearSelection: () => {
239
+ emit('update:resetSelected', true);
240
+ selectedItemIds.value.clear();
241
+ updateSelectedItems();
242
+ },
243
+ getHiddenColumns: () => internalHiddenColumns.value,
244
+ getSelectedItems: () => Array.from(selectedItemIds.value),
245
+ getSelectedItemsData: () => props.items.filter(item => item.id && selectedItemIds.value.has(item.id))
246
+ });
247
+ </script>
248
+
249
+ <template>
250
+ <uiTable
251
+ :loading="loading"
252
+ :header="header"
253
+ :items="items"
254
+ :ordered-by="orderedBy"
255
+ :order-direction="orderDirection"
256
+ :actions="(actions as any)"
257
+ :hidden-columns="internalHiddenColumns"
258
+ :reset-selected="resetSelected"
259
+ :select-all-items-modal="(selectAllItemsModal as any)"
260
+ :smart-filter-categories="smartFilterCategories"
261
+ :filter-literals="filterLiterals"
262
+ :filters="filters"
263
+ :table-actions-dropdown="tableActionsDropdown"
264
+ :table-action-buttons="tableActionButtons"
265
+ :pagination-current="currentPage"
266
+ :pagination-total="totalPages"
267
+ :pagination-total-items="totalItems"
268
+ :page-size-options="pageSizeOptions"
269
+ :current-page-size="pageSize"
270
+ :no-results="noResults"
271
+ @order-by="handleOrderBy"
272
+ @table-action="handleTableAction"
273
+ @table-action-selected="handleTableActionSelected"
274
+ @table-action-button-clicked="handleTableActionButtonClicked"
275
+ @smart-filters-sent="handleSmartFiltersSent"
276
+ @smart-filters-cleared="handleSmartFiltersCleared"
277
+ @smart-filter-deleted="handleSmartFilterDeleted"
278
+ @change-page="handleChangePage"
279
+ @change-page-size="handleChangePageSize"
280
+ @custom-emit="handleCustomEmit"
281
+ @columns-visibility-changed="handleColumnsVisibilityChanged"
282
+ @selected-items-deleted="handleSelectedItemsDeleted"
283
+ @modal-action="handleModalAction"
284
+ @select-all-items="handleSelectAllItems"
285
+ @deselect-all-items="handleDeselectAllItems"
286
+ @no-results-action="handleNoResultsAction"
287
+ @no-results-option-selected="handleNoResultsOptionSelected"
288
+ />
289
+ </template>
290
+
291
+ <style scoped>
292
+ .table-component {
293
+ width: 100%;
294
+ }
295
+ </style>
@@ -0,0 +1,334 @@
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
+ /**
299
+ * Status color mappings for common status values
300
+ */
301
+ export const statusColors: Record<string, string> = {
302
+ active: "green",
303
+ inactive: "gray",
304
+ pending: "yellow",
305
+ approved: "green",
306
+ rejected: "red",
307
+ draft: "gray",
308
+ published: "green",
309
+ archived: "gray",
310
+ error: "red",
311
+ success: "green",
312
+ warning: "yellow",
313
+ info: "blue",
314
+ };
315
+
316
+ /**
317
+ * Get color for a status value
318
+ *
319
+ * @example
320
+ * columns.tag('status', 'Status', (item) => getStatusColor(item.status))
321
+ */
322
+ export function getStatusColor(status: string): string {
323
+ return statusColors[status?.toLowerCase()] ?? "gray";
324
+ }
325
+
326
+ /**
327
+ * Create a status color function for a specific key
328
+ *
329
+ * @example
330
+ * columns.tag('status', 'Status', createStatusColorFn('status'))
331
+ */
332
+ export function createStatusColorFn<T>(key: keyof T): TagColorFn<T> {
333
+ return (item: T) => getStatusColor(String(item[key]));
334
+ }