v-uixy 1.5.0 → 1.7.0

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 (26) hide show
  1. package/package.json +1 -1
  2. package/templates/components/Alert/Alert.component.vue +53 -11
  3. package/templates/components/Alert/Alert.types.ts +2 -0
  4. package/templates/components/Alert/AlertItem.component.vue +21 -2
  5. package/templates/components/Button/Button.styles.ts +4 -4
  6. package/templates/components/Checkbox/Checkbox.component.vue +19 -5
  7. package/templates/components/Checkbox/Checkbox.styles.ts +10 -0
  8. package/templates/components/Checkbox/Checkbox.types.ts +1 -0
  9. package/templates/components/DatePicker/DatePicker.component.vue +23 -3
  10. package/templates/components/DatePicker/DatePicker.types.ts +0 -1
  11. package/templates/components/DateRangePicker/DateRangePicker.component.vue +29 -4
  12. package/templates/components/DateRangePicker/DateRangePicker.types.ts +0 -1
  13. package/templates/components/FileUpload/FileUpload.component.vue +23 -13
  14. package/templates/components/Input/Input.component.vue +23 -13
  15. package/templates/components/Input/Input.types.ts +0 -1
  16. package/templates/components/Popover/Popover.component.vue +1 -1
  17. package/templates/components/Select/Select.component.vue +23 -14
  18. package/templates/components/Select/Select.types.ts +0 -1
  19. package/templates/components/Table/Table.component.vue +507 -0
  20. package/templates/components/Table/Table.styles.ts +140 -0
  21. package/templates/components/Table/Table.types.ts +98 -0
  22. package/templates/components/Table/index.ts +39 -0
  23. package/templates/components/Textarea/Textarea.component.vue +20 -14
  24. package/templates/components/Textarea/Textarea.types.ts +0 -1
  25. package/templates/components.json +6 -0
  26. package/templates/composables/usePosition.ts +6 -4
@@ -0,0 +1,507 @@
1
+ <template>
2
+ <uixy-card :class="cardClass" v-bind="filteredAttrs">
3
+ <uixy-card class="w-full overflow-hidden p-0">
4
+ <div
5
+ :class="scrollAreaStyles({ scrollY: !!props.maxHeight })"
6
+ :style="props.maxHeight ? { maxHeight: props.maxHeight } : undefined"
7
+ >
8
+ <table :class="tableStyles()">
9
+ <caption v-if="props.caption" :class="captionStyles()">
10
+ {{
11
+ props.caption
12
+ }}
13
+ </caption>
14
+
15
+ <colgroup>
16
+ <col v-if="props.selectable" style="width: 44px" />
17
+ <col
18
+ v-for="column in props.columns"
19
+ :key="column.key"
20
+ :style="{ width: column.width, minWidth: column.minWidth }"
21
+ />
22
+ </colgroup>
23
+
24
+ <thead>
25
+ <tr>
26
+ <th
27
+ v-if="props.selectable"
28
+ :class="
29
+ thStyles(
30
+ {
31
+ align: 'left',
32
+ density: props.density,
33
+ sticky: !!props.stickyHeader,
34
+ },
35
+ 'w-11',
36
+ )
37
+ "
38
+ >
39
+ <uixy-checkbox
40
+ :checked="allSelected"
41
+ :indeterminate="someSelected"
42
+ :disabled="!props.rows.length"
43
+ @update:checked="toggleSelectAll"
44
+ />
45
+ </th>
46
+ <th
47
+ v-for="column in props.columns"
48
+ :key="column.key"
49
+ scope="col"
50
+ :class="
51
+ thStyles({
52
+ align: column.align ?? 'left',
53
+ density: props.density,
54
+ sticky: !!props.stickyHeader,
55
+ })
56
+ "
57
+ >
58
+ <span :class="thContentStyles()">
59
+ <span
60
+ :class="thLabelStyles({ align: column.align ?? 'left' })"
61
+ >
62
+ <span
63
+ v-if="column.sortable"
64
+ :class="sortButtonStyles()"
65
+ @click="handleSort(column)"
66
+ >
67
+ {{ column.header }}
68
+ <uixy-icon
69
+ :name="sortIconName(column)"
70
+ :class="sortIconStyles({ active: isSorted(column) })"
71
+ />
72
+ </span>
73
+ <span v-else>{{ column.header }}</span>
74
+ </span>
75
+ <uixy-popover
76
+ v-if="hasFilterSlot(column)"
77
+ :group="filterGroupId"
78
+ >
79
+ <template #trigger="triggerProps">
80
+ <button
81
+ v-bind="triggerProps"
82
+ type="button"
83
+ :class="
84
+ filterTriggerStyles({
85
+ active: isFilterActive(column),
86
+ })
87
+ "
88
+ >
89
+ <uixy-icon name="sliders-2" class="h-3.5 w-3.5" />
90
+ </button>
91
+ </template>
92
+ <template #default="{ close }">
93
+ <uixy-card :class="filterPanelStyles()">
94
+ <slot
95
+ :name="`filter-${column.key}`"
96
+ :column="column"
97
+ :filter="filterFor(column)"
98
+ :close="close"
99
+ />
100
+ </uixy-card>
101
+ </template>
102
+ </uixy-popover>
103
+ </span>
104
+ </th>
105
+ </tr>
106
+ </thead>
107
+
108
+ <tbody>
109
+ <template v-if="props.loading">
110
+ <tr v-for="n in skeletonRowCount" :key="`skeleton-${n}`">
111
+ <td
112
+ v-if="props.selectable"
113
+ :class="
114
+ tdStyles({
115
+ align: 'left',
116
+ density: props.density,
117
+ last: n === skeletonRowCount,
118
+ })
119
+ "
120
+ >
121
+ <uixy-skeleton class="h-4 w-4 rounded-1" />
122
+ </td>
123
+ <td
124
+ v-for="column in props.columns"
125
+ :key="column.key"
126
+ :class="
127
+ tdStyles({
128
+ align: column.align ?? 'left',
129
+ density: props.density,
130
+ last: n === skeletonRowCount,
131
+ })
132
+ "
133
+ >
134
+ <uixy-skeleton class="h-4 w-full max-w-32 rounded-1" />
135
+ </td>
136
+ </tr>
137
+ </template>
138
+
139
+ <template v-else-if="!props.rows.length">
140
+ <tr>
141
+ <td :colspan="totalColumnCount" class="p-0">
142
+ <div :class="emptyStyles()">
143
+ <slot name="empty">
144
+ <span>{{ props.emptyText ?? "No data available." }}</span>
145
+ </slot>
146
+ </div>
147
+ </td>
148
+ </tr>
149
+ </template>
150
+
151
+ <template v-else>
152
+ <tr
153
+ v-for="(row, index) in props.rows"
154
+ :key="keyOf(row, index)"
155
+ :class="rowStyles({ selected: isSelected(row, index) })"
156
+ >
157
+ <td
158
+ v-if="props.selectable"
159
+ :class="
160
+ tdStyles({
161
+ align: 'left',
162
+ density: props.density,
163
+ last: index === props.rows.length - 1,
164
+ })
165
+ "
166
+ >
167
+ <uixy-checkbox
168
+ :checked="isSelected(row, index)"
169
+ @update:checked="
170
+ (checked) => toggleRow(row, index, checked)
171
+ "
172
+ />
173
+ </td>
174
+ <td
175
+ v-for="column in props.columns"
176
+ :key="column.key"
177
+ :class="
178
+ tdStyles({
179
+ align: column.align ?? 'left',
180
+ density: props.density,
181
+ last: index === props.rows.length - 1,
182
+ })
183
+ "
184
+ >
185
+ <slot
186
+ :name="`cell-${column.key}`"
187
+ :row="row"
188
+ :value="cellValue(row, column)"
189
+ :column="column"
190
+ :index="index"
191
+ >
192
+ {{ cellValue(row, column) }}
193
+ </slot>
194
+ </td>
195
+ </tr>
196
+ </template>
197
+ </tbody>
198
+
199
+ <tfoot v-if="props.summary">
200
+ <tr>
201
+ <td
202
+ v-if="props.selectable"
203
+ :class="
204
+ footerCellStyles({ align: 'left', density: props.density })
205
+ "
206
+ />
207
+ <td
208
+ v-for="column in props.columns"
209
+ :key="column.key"
210
+ :class="
211
+ footerCellStyles({
212
+ align: column.align ?? 'left',
213
+ density: props.density,
214
+ })
215
+ "
216
+ >
217
+ <slot
218
+ :name="`summary-${column.key}`"
219
+ :column="column"
220
+ :value="props.summary?.[column.key]"
221
+ >
222
+ {{ props.summary?.[column.key] ?? "" }}
223
+ </slot>
224
+ </td>
225
+ </tr>
226
+ </tfoot>
227
+ </table>
228
+ </div>
229
+ </uixy-card>
230
+
231
+ <div v-if="props.pagination" :class="paginationBarStyles()">
232
+ <slot
233
+ name="pagination"
234
+ :pagination="props.pagination"
235
+ :page-count="pageCount"
236
+ :go-to-page="goToPage"
237
+ >
238
+ <span :class="paginationInfoStyles()">{{ paginationRangeText }}</span>
239
+ <div :class="paginationControlsStyles()">
240
+ <button
241
+ type="button"
242
+ :class="paginationButtonStyles()"
243
+ :disabled="props.pagination.page <= 1"
244
+ @click="goToPage(props.pagination.page - 1)"
245
+ >
246
+ <uixy-icon name="chevron-left" class="h-4 w-4" />
247
+ </button>
248
+ <template v-for="(item, i) in paginationItems" :key="i">
249
+ <span
250
+ v-if="item === 'ellipsis'"
251
+ :class="paginationEllipsisStyles()"
252
+ >
253
+ …
254
+ </span>
255
+ <button
256
+ v-else
257
+ type="button"
258
+ :class="
259
+ paginationButtonStyles({
260
+ active: item === props.pagination.page,
261
+ })
262
+ "
263
+ @click="goToPage(item)"
264
+ >
265
+ {{ item }}
266
+ </button>
267
+ </template>
268
+ <button
269
+ type="button"
270
+ :class="paginationButtonStyles()"
271
+ :disabled="props.pagination.page >= pageCount"
272
+ @click="goToPage(props.pagination.page + 1)"
273
+ >
274
+ <uixy-icon name="chevron-right" class="h-4 w-4" />
275
+ </button>
276
+ </div>
277
+ </slot>
278
+ </div>
279
+ </uixy-card>
280
+ </template>
281
+
282
+ <script
283
+ setup
284
+ lang="ts"
285
+ generic="TRow extends Record<string, any> = Record<string, any>"
286
+ >
287
+ import { UixyCard } from "../Card";
288
+ import { UixyCheckbox } from "../Checkbox";
289
+ import { UixyIcon } from "../Icon";
290
+ import { UixyPopover } from "../Popover";
291
+ import { UixySkeleton } from "../Skeleton";
292
+ import type {
293
+ UixyTableProps,
294
+ UixyTableEmits,
295
+ UixyTableSlots,
296
+ UixyTableColumn,
297
+ UixyTableSort,
298
+ } from "./Table.types";
299
+ import {
300
+ scrollAreaStyles,
301
+ tableStyles,
302
+ captionStyles,
303
+ thStyles,
304
+ thContentStyles,
305
+ thLabelStyles,
306
+ sortButtonStyles,
307
+ sortIconStyles,
308
+ filterTriggerStyles,
309
+ filterPanelStyles,
310
+ rowStyles,
311
+ tdStyles,
312
+ footerCellStyles,
313
+ emptyStyles,
314
+ paginationBarStyles,
315
+ paginationInfoStyles,
316
+ paginationControlsStyles,
317
+ paginationButtonStyles,
318
+ paginationEllipsisStyles,
319
+ } from "./Table.styles";
320
+
321
+ const props = withDefaults(defineProps<UixyTableProps<TRow>>(), {
322
+ rowKey: "id",
323
+ density: "comfortable",
324
+ sort: null,
325
+ selectedRowKeys: () => [],
326
+ loading: false,
327
+ selectable: false,
328
+ stickyHeader: false,
329
+ });
330
+
331
+ const emit = defineEmits<UixyTableEmits<TRow>>();
332
+
333
+ defineSlots<UixyTableSlots<TRow>>();
334
+
335
+ defineOptions({
336
+ inheritAttrs: false,
337
+ });
338
+
339
+ const attrs = useAttrs();
340
+ const slots = useSlots();
341
+ const filterGroupId = useId();
342
+
343
+ const filteredAttrs = computed(() => {
344
+ const { class: _cls, ...rest } = attrs;
345
+ return rest;
346
+ });
347
+
348
+ const cardClass = computed(() =>
349
+ ["w-full p-1 bg-gray-100 dark:bg-black", attrs.class]
350
+ .filter(Boolean)
351
+ .join(" "),
352
+ );
353
+
354
+ const totalColumnCount = computed(
355
+ () => props.columns.length + (props.selectable ? 1 : 0),
356
+ );
357
+
358
+ function keyOf(row: TRow, index: number): string | number {
359
+ const rk = props.rowKey;
360
+ const value =
361
+ typeof rk === "function"
362
+ ? rk(row)
363
+ : (row as Record<string, unknown>)[(rk ?? "id") as string];
364
+ return (value as string | number | undefined) ?? index;
365
+ }
366
+
367
+ function cellValue(row: TRow, column: UixyTableColumn<TRow>): unknown {
368
+ if (column.accessor) return column.accessor(row);
369
+ return (row as Record<string, unknown>)[column.key];
370
+ }
371
+
372
+ function filterFor(column: UixyTableColumn<TRow>) {
373
+ return props.filters?.find((filter) => filter.key === column.key);
374
+ }
375
+
376
+ function hasFilterSlot(column: UixyTableColumn<TRow>) {
377
+ return !!slots[`filter-${column.key}`];
378
+ }
379
+
380
+ function isFilterActive(column: UixyTableColumn<TRow>) {
381
+ return !!filterFor(column)?.active;
382
+ }
383
+
384
+ function isSorted(column: UixyTableColumn<TRow>) {
385
+ return props.sort?.key === column.key;
386
+ }
387
+
388
+ function sortIconName(column: UixyTableColumn<TRow>) {
389
+ if (!isSorted(column)) return "chevrons-vertical" as const;
390
+ return props.sort?.direction === "asc"
391
+ ? ("chevron-up" as const)
392
+ : ("chevron-down" as const);
393
+ }
394
+
395
+ function handleSort(column: UixyTableColumn<TRow>) {
396
+ if (!column.sortable) return;
397
+
398
+ const current = props.sort;
399
+ let next: UixyTableSort<TRow> | null;
400
+
401
+ if (!current || current.key !== column.key) {
402
+ next = { key: column.key, direction: "asc" };
403
+ } else if (current.direction === "asc") {
404
+ next = { key: column.key, direction: "desc" };
405
+ } else {
406
+ next = null;
407
+ }
408
+
409
+ emit("sortChange", next);
410
+ }
411
+
412
+ const selectedKeySet = computed(() => new Set(props.selectedRowKeys ?? []));
413
+
414
+ function isSelected(row: TRow, index: number) {
415
+ return selectedKeySet.value.has(keyOf(row, index));
416
+ }
417
+
418
+ const allSelected = computed(
419
+ () =>
420
+ props.rows.length > 0 &&
421
+ props.rows.every((row, index) => isSelected(row, index)),
422
+ );
423
+
424
+ const someSelected = computed(
425
+ () =>
426
+ !allSelected.value &&
427
+ props.rows.some((row, index) => isSelected(row, index)),
428
+ );
429
+
430
+ function toggleRow(row: TRow, index: number, checked: boolean | undefined) {
431
+ const key = keyOf(row, index);
432
+ const next = new Set(selectedKeySet.value);
433
+
434
+ if (checked) next.add(key);
435
+ else next.delete(key);
436
+
437
+ emit("selectionChange", Array.from(next));
438
+ }
439
+
440
+ function toggleSelectAll(checked: boolean | undefined) {
441
+ const next = new Set(selectedKeySet.value);
442
+
443
+ props.rows.forEach((row, index) => {
444
+ const key = keyOf(row, index);
445
+ if (checked) next.add(key);
446
+ else next.delete(key);
447
+ });
448
+
449
+ emit("selectionChange", Array.from(next));
450
+ }
451
+
452
+ const skeletonRowCount = computed(() =>
453
+ Math.min(props.pagination?.pageSize ?? 8, 10),
454
+ );
455
+
456
+ const pageCount = computed(() => {
457
+ if (!props.pagination) return 1;
458
+ return Math.max(
459
+ 1,
460
+ Math.ceil(props.pagination.total / props.pagination.pageSize),
461
+ );
462
+ });
463
+
464
+ const paginationRangeText = computed(() => {
465
+ if (!props.pagination) return "";
466
+
467
+ const { page, pageSize, total } = props.pagination;
468
+ if (total === 0) return "0 results";
469
+
470
+ const from = (page - 1) * pageSize + 1;
471
+ const to = Math.min(page * pageSize, total);
472
+
473
+ return `${from}–${to} of ${total}`;
474
+ });
475
+
476
+ function goToPage(page: number) {
477
+ if (!props.pagination) return;
478
+
479
+ const clamped = Math.min(Math.max(page, 1), pageCount.value);
480
+
481
+ if (clamped === props.pagination.page) return;
482
+
483
+ emit("pageChange", clamped);
484
+ }
485
+
486
+ type PaginationItem = number | "ellipsis";
487
+
488
+ const paginationItems = computed<PaginationItem[]>(() => {
489
+ const count = pageCount.value;
490
+ const current = props.pagination?.page ?? 1;
491
+ const siblings = 1;
492
+ const items: PaginationItem[] = [1];
493
+
494
+ const start = Math.max(2, current - siblings);
495
+ const end = Math.min(count - 1, current + siblings);
496
+
497
+ if (start > 2) items.push("ellipsis");
498
+
499
+ for (let i = start; i <= end; i++) items.push(i);
500
+
501
+ if (end < count - 1) items.push("ellipsis");
502
+
503
+ if (count > 1) items.push(count);
504
+
505
+ return items;
506
+ });
507
+ </script>
@@ -0,0 +1,140 @@
1
+ import styles from "~/utils/styles";
2
+
3
+ export const scrollAreaStyles = styles("scrollbar w-full overflow-x-auto", {
4
+ scrollY: {
5
+ true: "overflow-y-auto",
6
+ false: "",
7
+ },
8
+ });
9
+
10
+ export const tableStyles = styles(
11
+ "w-full caption-bottom border-separate border-spacing-0 text-xs",
12
+ );
13
+
14
+ export const captionStyles = styles(
15
+ "px-4 py-3 text-left text-xs text-gray-600 dark:text-gray-500",
16
+ );
17
+
18
+ export const thStyles = styles(
19
+ "whitespace-nowrap border-b border-gray-300 bg-white align-middle text-xs font-500 text-gray-600 dark:border-gray-900 dark:bg-gray-1000 dark:text-gray-500",
20
+ {
21
+ align: {
22
+ left: "text-left",
23
+ center: "text-center",
24
+ right: "text-right",
25
+ },
26
+ density: {
27
+ compact: "h-9 px-3",
28
+ comfortable: "h-10 px-4",
29
+ },
30
+ sticky: {
31
+ true: "sticky top-0 z-10",
32
+ false: "",
33
+ },
34
+ },
35
+ );
36
+
37
+ export const thContentStyles = styles("flex w-full items-center gap-2");
38
+
39
+ export const thLabelStyles = styles("flex min-w-0 flex-1 items-center gap-1", {
40
+ align: {
41
+ left: "justify-start",
42
+ center: "justify-center",
43
+ right: "justify-end",
44
+ },
45
+ });
46
+
47
+ export const sortButtonStyles = styles(
48
+ "inline-flex cursor-pointer select-none items-center gap-1 transition-colors hover:text-black dark:hover:text-white",
49
+ );
50
+
51
+ export const sortIconStyles = styles(
52
+ "h-3.5 w-3.5 shrink-0 text-gray-500 dark:text-gray-600",
53
+ {
54
+ active: {
55
+ true: "text-black dark:text-white",
56
+ false: "",
57
+ },
58
+ },
59
+ );
60
+
61
+ export const filterTriggerStyles = styles(
62
+ "flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-1 transition-colors hover:bg-gray-200 dark:hover:bg-gray-800",
63
+ {
64
+ active: {
65
+ true: "text-black dark:text-white",
66
+ false: "text-gray-500 dark:text-gray-600",
67
+ },
68
+ },
69
+ );
70
+
71
+ export const filterPanelStyles = styles("flex w-56 flex-col gap-2 p-2");
72
+
73
+ export const rowStyles = styles("transition-colors", {
74
+ selected: {
75
+ true: "bg-gray-100 dark:bg-gray-900",
76
+ false: "hover:bg-gray-100 dark:hover:bg-gray-900",
77
+ },
78
+ });
79
+
80
+ export const tdStyles = styles(
81
+ "border-b border-gray-300 align-middle text-gray-900 dark:border-gray-900 dark:text-gray-100",
82
+ {
83
+ align: {
84
+ left: "text-left",
85
+ center: "text-center",
86
+ right: "text-right",
87
+ },
88
+ density: {
89
+ compact: "px-3 py-2",
90
+ comfortable: "px-4 py-3",
91
+ },
92
+ last: {
93
+ true: "border-b-0",
94
+ false: "",
95
+ },
96
+ },
97
+ );
98
+
99
+ export const footerCellStyles = styles(
100
+ "border-t border-gray-300 bg-gray-100 align-middle font-500 text-gray-800 dark:border-gray-900 dark:bg-gray-900 dark:text-gray-300",
101
+ {
102
+ align: {
103
+ left: "text-left",
104
+ center: "text-center",
105
+ right: "text-right",
106
+ },
107
+ density: {
108
+ compact: "px-3 py-2",
109
+ comfortable: "px-4 py-3",
110
+ },
111
+ },
112
+ );
113
+
114
+ export const emptyStyles = styles(
115
+ "flex flex-col items-center justify-center gap-2 py-16 text-xs text-gray-500 dark:text-gray-500",
116
+ );
117
+
118
+ export const paginationBarStyles = styles(
119
+ "flex flex-wrap items-center justify-between gap-3 px-2 pt-1 pb-0 text-xs",
120
+ );
121
+
122
+ export const paginationInfoStyles = styles(
123
+ "text-xs text-gray-600 dark:text-gray-500",
124
+ );
125
+
126
+ export const paginationControlsStyles = styles("flex items-center gap-1");
127
+
128
+ export const paginationButtonStyles = styles(
129
+ "flex h-6 min-w-6 cursor-pointer items-center justify-center rounded-1 px-1.5 text-xs transition-colors hover:bg-gray-200 disabled:pointer-events-none disabled:cursor-default disabled:text-gray-400 dark:hover:bg-gray-800 dark:disabled:text-gray-700",
130
+ {
131
+ active: {
132
+ true: "bg-gray-800 text-gray-100 hover:bg-gray-700 dark:bg-gray-300 dark:text-black dark:hover:bg-gray-200",
133
+ false: "text-gray-800 dark:text-gray-300",
134
+ },
135
+ },
136
+ );
137
+
138
+ export const paginationEllipsisStyles = styles(
139
+ "flex h-6 min-w-6 items-center justify-center text-gray-500 dark:text-gray-600",
140
+ );