v-uixy 1.5.0 → 1.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "v-uixy",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "private": false,
5
5
  "author": {
6
6
  "name": "Alan Haber",
@@ -1,7 +1,7 @@
1
1
  import styles from "~/utils/styles";
2
2
 
3
3
  export const buttonStyles = styles(
4
- "relative cursor-pointer h-8 font-500 transition-all duration-200 ease-in-out hover:shadow-xs flex items-center justify-center gap-2 disabled:pointer-events-none border border-transparent",
4
+ "relative cursor-pointer font-500 transition-all duration-200 ease-in-out hover:shadow-xs flex items-center justify-center gap-2 disabled:pointer-events-none border border-transparent",
5
5
  {
6
6
  variant: {
7
7
  primary:
@@ -14,9 +14,9 @@ export const buttonStyles = styles(
14
14
  "dark:hover:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-800 disabled:text-gray-700",
15
15
  },
16
16
  size: {
17
- sm: "px-5 py-1 text-button-sm",
18
- md: "px-6 py-1 text-button-md",
19
- lg: "px-7 py-1 text-button-lg",
17
+ sm: "h-7 px-4 text-button-sm",
18
+ md: "h-8 px-6 text-button-md",
19
+ lg: "h-10 px-8 text-button-lg",
20
20
  },
21
21
  rounded: {
22
22
  true: "rounded-full",
@@ -2,14 +2,15 @@
2
2
  <label :class="checkboxStyles($attrs.class as string)">
3
3
  <div class="relative flex items-center justify-center">
4
4
  <input
5
+ ref="inputRef"
5
6
  type="checkbox"
6
7
  :disabled="props.disabled"
7
8
  v-model="checked"
8
- class="size-4 cursor-pointer appearance-none rounded-1 border border-black transition-all duration-150 ease-in-out checked:bg-black hover:bg-gray-300 checked:hover:bg-gray-800 disabled:pointer-events-none disabled:border-gray-500 disabled:bg-gray-300 checked:disabled:bg-gray-500 dark:border-gray-100 dark:checked:bg-gray-100 dark:hover:bg-gray-800 dark:checked:hover:bg-gray-200 dark:disabled:border-gray-600 dark:disabled:bg-gray-900 dark:checked:disabled:bg-gray-600"
9
+ :class="inputStyles({ indeterminate: !!props.indeterminate })"
9
10
  />
10
11
  <animate-presence>
11
12
  <motion.div
12
- v-if="checked"
13
+ v-if="checked || props.indeterminate"
13
14
  :initial="{ opacity: 0 }"
14
15
  :animate="{ opacity: 1 }"
15
16
  :exit="{ opacity: 0 }"
@@ -17,11 +18,11 @@
17
18
  class="pointer-events-none absolute"
18
19
  >
19
20
  <uixy-icon
20
- name="check"
21
+ :name="props.indeterminate ? 'minus' : 'check'"
21
22
  :class="
22
23
  iconStyles({
23
24
  disabled: !!props.disabled,
24
- checked: checked,
25
+ checked: checked || props.indeterminate,
25
26
  })
26
27
  "
27
28
  />
@@ -41,9 +42,22 @@
41
42
  import { motion, AnimatePresence } from "motion-v";
42
43
  import { UixyIcon } from "../Icon";
43
44
  import type { UixyCheckboxProps } from "./Checkbox.types";
44
- import { checkboxStyles, iconStyles, labelStyles } from "./Checkbox.styles";
45
+ import {
46
+ checkboxStyles,
47
+ inputStyles,
48
+ iconStyles,
49
+ labelStyles,
50
+ } from "./Checkbox.styles";
45
51
 
46
52
  const props = defineProps<UixyCheckboxProps>();
47
53
 
48
54
  const checked = defineModel<boolean>("checked");
55
+
56
+ const inputRef = ref<HTMLInputElement | null>(null);
57
+
58
+ watchEffect(() => {
59
+ if (inputRef.value) {
60
+ inputRef.value.indeterminate = !!props.indeterminate;
61
+ }
62
+ });
49
63
  </script>
@@ -2,6 +2,16 @@ import styles from "~/utils/styles";
2
2
 
3
3
  export const checkboxStyles = styles("flex items-center gap-2");
4
4
 
5
+ export const inputStyles = styles(
6
+ "size-4 cursor-pointer appearance-none rounded-1 border border-black transition-all duration-150 ease-in-out checked:bg-black hover:bg-gray-300 checked:hover:bg-gray-800 disabled:pointer-events-none disabled:border-gray-500 disabled:bg-gray-300 checked:disabled:bg-gray-500 dark:border-gray-100 dark:checked:bg-gray-100 dark:hover:bg-gray-800 dark:checked:hover:bg-gray-200 dark:disabled:border-gray-600 dark:disabled:bg-gray-900 dark:checked:disabled:bg-gray-600",
7
+ {
8
+ indeterminate: {
9
+ true: "bg-black hover:bg-gray-800 disabled:bg-gray-300 dark:bg-gray-100 dark:hover:bg-gray-200 dark:disabled:bg-gray-900",
10
+ false: "",
11
+ },
12
+ },
13
+ );
14
+
5
15
  export const iconStyles = styles("h-3 w-3", {
6
16
  disabled: {
7
17
  true: "text-gray-500",
@@ -1,3 +1,4 @@
1
1
  export interface UixyCheckboxProps {
2
2
  disabled?: boolean;
3
+ indeterminate?: boolean;
3
4
  }
@@ -17,7 +17,7 @@
17
17
  :exit="{ opacity: 0, scale: 0.8 }"
18
18
  :transition="{ duration: 0.15, ease: 'easeInOut' }"
19
19
  >
20
- <slot />
20
+ <slot :close="handleLeave" />
21
21
  </motion.div>
22
22
  </div>
23
23
  </motion.div>
@@ -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
+ );
@@ -0,0 +1,98 @@
1
+ export type UixyTableAlign = "left" | "center" | "right";
2
+ export type UixyTableDensity = "compact" | "comfortable";
3
+ export type UixyTableSortDirection = "asc" | "desc";
4
+
5
+ export type UixyTableColumnKey<TRow> =
6
+ | Extract<keyof TRow, string>
7
+ | (string & {});
8
+
9
+ export interface UixyTableColumn<TRow = Record<string, unknown>> {
10
+ key: UixyTableColumnKey<TRow>;
11
+ header: string;
12
+ accessor?: (row: TRow) => unknown;
13
+ sortable?: boolean;
14
+ align?: UixyTableAlign;
15
+ width?: string;
16
+ minWidth?: string;
17
+ }
18
+
19
+ export interface UixyTableSort<TRow = Record<string, unknown>> {
20
+ key: UixyTableColumnKey<TRow>;
21
+ direction: UixyTableSortDirection;
22
+ }
23
+
24
+ export interface UixyTablePagination {
25
+ page: number;
26
+ pageSize: number;
27
+ total: number;
28
+ }
29
+
30
+ export interface UixyTableFilter {
31
+ key: string;
32
+ type: string;
33
+ label?: string;
34
+ active?: boolean;
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ export type UixyTableRowKey<TRow> =
39
+ | UixyTableColumnKey<TRow>
40
+ | ((row: TRow) => string | number);
41
+
42
+ export interface UixyTableProps<TRow = Record<string, unknown>> {
43
+ columns: UixyTableColumn<TRow>[];
44
+ rows: TRow[];
45
+ rowKey?: UixyTableRowKey<TRow>;
46
+ sort?: UixyTableSort<TRow> | null;
47
+ pagination?: UixyTablePagination;
48
+ filters?: UixyTableFilter[];
49
+ selectable?: boolean;
50
+ selectedRowKeys?: Array<string | number>;
51
+ summary?: Partial<Record<UixyTableColumnKey<TRow>, unknown>>;
52
+ loading?: boolean;
53
+ density?: UixyTableDensity;
54
+ stickyHeader?: boolean;
55
+ maxHeight?: string;
56
+ caption?: string;
57
+ emptyText?: string;
58
+ }
59
+
60
+ export interface UixyTableEmits<TRow = Record<string, unknown>> {
61
+ (event: "sortChange", sort: UixyTableSort<TRow> | null): void;
62
+ (event: "pageChange", page: number): void;
63
+ (event: "selectionChange", keys: Array<string | number>): void;
64
+ }
65
+
66
+ export interface UixyTableCellSlotProps<TRow = Record<string, unknown>> {
67
+ row: TRow;
68
+ value: unknown;
69
+ column: UixyTableColumn<TRow>;
70
+ index: number;
71
+ }
72
+
73
+ export interface UixyTableSummarySlotProps<TRow = Record<string, unknown>> {
74
+ column: UixyTableColumn<TRow>;
75
+ value: unknown;
76
+ }
77
+
78
+ export interface UixyTableFilterSlotProps<TRow = Record<string, unknown>> {
79
+ column: UixyTableColumn<TRow>;
80
+ filter?: UixyTableFilter;
81
+ close: () => void;
82
+ }
83
+
84
+ export interface UixyTablePaginationSlotProps {
85
+ pagination: UixyTablePagination;
86
+ pageCount: number;
87
+ goToPage: (page: number) => void;
88
+ }
89
+
90
+ export interface UixyTableSlots<TRow = Record<string, unknown>> {
91
+ empty?: (props: Record<string, never>) => unknown;
92
+ pagination?: (props: UixyTablePaginationSlotProps) => unknown;
93
+ [key: `cell-${string}`]: (props: UixyTableCellSlotProps<TRow>) => unknown;
94
+ [key: `summary-${string}`]: (
95
+ props: UixyTableSummarySlotProps<TRow>,
96
+ ) => unknown;
97
+ [key: `filter-${string}`]: (props: UixyTableFilterSlotProps<TRow>) => unknown;
98
+ }
@@ -0,0 +1,39 @@
1
+ import UixyTable from "./Table.component.vue";
2
+ import {
3
+ type UixyTableProps,
4
+ type UixyTableEmits,
5
+ type UixyTableSlots,
6
+ type UixyTableColumn,
7
+ type UixyTableColumnKey,
8
+ type UixyTableSort,
9
+ type UixyTableSortDirection,
10
+ type UixyTablePagination,
11
+ type UixyTableFilter,
12
+ type UixyTableRowKey,
13
+ type UixyTableAlign,
14
+ type UixyTableDensity,
15
+ type UixyTableCellSlotProps,
16
+ type UixyTableSummarySlotProps,
17
+ type UixyTableFilterSlotProps,
18
+ type UixyTablePaginationSlotProps,
19
+ } from "./Table.types";
20
+
21
+ export {
22
+ UixyTable,
23
+ type UixyTableProps,
24
+ type UixyTableEmits,
25
+ type UixyTableSlots,
26
+ type UixyTableColumn,
27
+ type UixyTableColumnKey,
28
+ type UixyTableSort,
29
+ type UixyTableSortDirection,
30
+ type UixyTablePagination,
31
+ type UixyTableFilter,
32
+ type UixyTableRowKey,
33
+ type UixyTableAlign,
34
+ type UixyTableDensity,
35
+ type UixyTableCellSlotProps,
36
+ type UixyTableSummarySlotProps,
37
+ type UixyTableFilterSlotProps,
38
+ type UixyTablePaginationSlotProps,
39
+ };
@@ -198,6 +198,12 @@
198
198
  "packages": ["motion-v"],
199
199
  "composables": []
200
200
  },
201
+ {
202
+ "name": "Table",
203
+ "related-components": ["Card", "Checkbox", "Icon", "Skeleton", "Popover"],
204
+ "packages": [],
205
+ "composables": []
206
+ },
201
207
  {
202
208
  "name": "TableOfContents",
203
209
  "related-components": [],
@@ -6,6 +6,8 @@ interface UsePositionOptions {
6
6
  direction?: Direction;
7
7
  }
8
8
 
9
+ const GAP = 6;
10
+
9
11
  function hasFixedParent(el: HTMLElement | null): boolean {
10
12
  while (el) {
11
13
  const style = window.getComputedStyle(el);
@@ -46,8 +48,8 @@ export function usePosition({ direction = "bottom" }: UsePositionOptions) {
46
48
 
47
49
  const spaceBelow = winH - targetRect.bottom;
48
50
  const spaceAbove = targetRect.top;
49
- const fitsBelow = spaceBelow >= tooltipRect.height + 6;
50
- const fitsAbove = spaceAbove >= tooltipRect.height + 6;
51
+ const fitsBelow = spaceBelow >= tooltipRect.height + GAP;
52
+ const fitsAbove = spaceAbove >= tooltipRect.height + GAP;
51
53
 
52
54
  const effectiveDirection =
53
55
  direction === "top"
@@ -60,8 +62,8 @@ export function usePosition({ direction = "bottom" }: UsePositionOptions) {
60
62
 
61
63
  top =
62
64
  effectiveDirection === "top"
63
- ? targetRect.top - tooltipRect.height - 6 + scrollY
64
- : targetRect.bottom + 6 + scrollY;
65
+ ? targetRect.top - tooltipRect.height - GAP + scrollY
66
+ : targetRect.bottom + GAP + scrollY;
65
67
 
66
68
  let left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2;
67
69
  const newStyles: Record<string, string | number> = {