@innertia-solutions/nuxt-theme-spark 0.1.141 → 0.1.143
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.
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
IconChevronDown,
|
|
7
7
|
IconReload,
|
|
8
8
|
IconBolt,
|
|
9
|
+
IconPin,
|
|
9
10
|
} from '@tabler/icons-vue'
|
|
10
11
|
|
|
11
12
|
const props = defineProps({
|
|
@@ -22,6 +23,7 @@ const props = defineProps({
|
|
|
22
23
|
clickRowToOpen: { type: Boolean, default: false },
|
|
23
24
|
previewRowId: { type: [String, Number], default: null },
|
|
24
25
|
previewMode: { type: Boolean, default: false },
|
|
26
|
+
pinnedColumns: { type: Object, default: null }, // { left?: string[], right?: string[] }
|
|
25
27
|
})
|
|
26
28
|
|
|
27
29
|
const emit = defineEmits(['update:search', 'row-click', 'loaded', 'page-change', 'per-page-change'])
|
|
@@ -55,6 +57,7 @@ const columnOrder = ref([])
|
|
|
55
57
|
const columnSizing = ref({})
|
|
56
58
|
const columnSizingInfo = ref({})
|
|
57
59
|
const rowSelection = ref({})
|
|
60
|
+
const columnPinning = ref({ left: [], right: [] })
|
|
58
61
|
const isCustomPerPage = ref(false)
|
|
59
62
|
|
|
60
63
|
const makeUpdater = (stateRef) => (updater) => {
|
|
@@ -111,6 +114,7 @@ const table = useVueTable({
|
|
|
111
114
|
get columnSizing() { return columnSizing.value },
|
|
112
115
|
get columnSizingInfo() { return columnSizingInfo.value },
|
|
113
116
|
get rowSelection() { return rowSelection.value },
|
|
117
|
+
get columnPinning() { return columnPinning.value },
|
|
114
118
|
},
|
|
115
119
|
onPaginationChange: makeUpdater(pagination),
|
|
116
120
|
onSortingChange: makeUpdater(sorting),
|
|
@@ -120,9 +124,11 @@ const table = useVueTable({
|
|
|
120
124
|
onColumnSizingChange: makeUpdater(columnSizing),
|
|
121
125
|
onColumnSizingInfoChange: makeUpdater(columnSizingInfo),
|
|
122
126
|
onRowSelectionChange: makeUpdater(rowSelection),
|
|
127
|
+
onColumnPinningChange: makeUpdater(columnPinning),
|
|
123
128
|
getCoreRowModel: getCoreRowModel(),
|
|
124
129
|
columnResizeMode: 'onChange',
|
|
125
130
|
enableColumnResizing: true,
|
|
131
|
+
enableColumnPinning: true,
|
|
126
132
|
manualPagination: true,
|
|
127
133
|
manualSorting: true,
|
|
128
134
|
manualFiltering: true,
|
|
@@ -131,6 +137,58 @@ const table = useVueTable({
|
|
|
131
137
|
enableRowSelection: true,
|
|
132
138
|
})
|
|
133
139
|
|
|
140
|
+
// ─── Column pinning helpers ───────────────────────────────────────────────────
|
|
141
|
+
const pinColumn = (key, position) => {
|
|
142
|
+
table.getColumn(key)?.pin(position)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const getPinnedStyles = (column, isHeader = false) => {
|
|
146
|
+
const pinned = column.getIsPinned()
|
|
147
|
+
if (!pinned) return {}
|
|
148
|
+
const z = isHeader ? 2 : 1
|
|
149
|
+
const w = column.getSize() + 'px'
|
|
150
|
+
// Headers always use the solid card background.
|
|
151
|
+
// Body cells use --row-bg, which is set to solid colors only (normal + hover) via <style scoped>.
|
|
152
|
+
// Selected/preview rows intentionally don't set --row-bg so sticky cells fall back to --card,
|
|
153
|
+
// which prevents semi-transparent tints from bleeding through the sticky cell.
|
|
154
|
+
const bg = isHeader ? 'var(--card, #fff)' : 'var(--row-bg, var(--card, #fff))'
|
|
155
|
+
const base = {
|
|
156
|
+
position: 'sticky',
|
|
157
|
+
zIndex: z,
|
|
158
|
+
background: bg,
|
|
159
|
+
width: w,
|
|
160
|
+
minWidth: w,
|
|
161
|
+
maxWidth: w,
|
|
162
|
+
}
|
|
163
|
+
// inset box-shadow: paints inside the cell so it can't be covered by adjacent cells
|
|
164
|
+
// and always follows the visual sticky position (unlike border or outset box-shadow)
|
|
165
|
+
if (pinned === 'left') return { ...base, left: column.getStart('left') + 'px', boxShadow: 'inset -1px 0 0 0 var(--card-line, #e5e7eb)' }
|
|
166
|
+
if (pinned === 'right') return { ...base, right: column.getAfter('right') + 'px', boxShadow: 'inset 1px 0 0 0 var(--card-line, #e5e7eb)' }
|
|
167
|
+
return {}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
// Initialize pinning from prop if provided
|
|
172
|
+
onMounted(() => {
|
|
173
|
+
if (props.pinnedColumns) {
|
|
174
|
+
columnPinning.value = {
|
|
175
|
+
left: props.pinnedColumns.left ?? [],
|
|
176
|
+
right: props.pinnedColumns.right ?? [],
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// Keep 'select' always first in the left pinning array.
|
|
182
|
+
// Fires synchronously so the colgroup/headers never render in wrong order.
|
|
183
|
+
watch(() => columnPinning.value.left, (left) => {
|
|
184
|
+
if (props.checkable && left.includes('select') && left[0] !== 'select') {
|
|
185
|
+
columnPinning.value = {
|
|
186
|
+
...columnPinning.value,
|
|
187
|
+
left: ['select', ...left.filter(id => id !== 'select')],
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}, { flush: 'sync' })
|
|
191
|
+
|
|
134
192
|
// ─── Fetch ────────────────────────────────────────────────────────────────────
|
|
135
193
|
const buildRequestParams = () => {
|
|
136
194
|
const { sort, ...otherParams } = props.params
|
|
@@ -457,6 +515,23 @@ const handleRowKeydown = (row, e) => {
|
|
|
457
515
|
emit('row-click', row.original, e)
|
|
458
516
|
}
|
|
459
517
|
|
|
518
|
+
// Compute --row-bg for pinned (sticky) cells.
|
|
519
|
+
// Non-pinned cells get background from Tailwind classes on <tr>.
|
|
520
|
+
// Pinned cells inherit --row-bg which must be a solid opaque color (no transparency → no bleed-through).
|
|
521
|
+
// color-mix() blends the Tailwind tint with the card color to produce an opaque equivalent.
|
|
522
|
+
// For normal/hover rows the <style scoped> CSS rule handles it; selected/preview override via inline style.
|
|
523
|
+
const pinnedRowStyle = (row) => {
|
|
524
|
+
if (props.previewRowId && row.original.id === props.previewRowId) {
|
|
525
|
+
// !bg-indigo-50 → solid indigo-50
|
|
526
|
+
return { '--row-bg': 'color-mix(in srgb, #eef2ff 100%, var(--card, #fff))' }
|
|
527
|
+
}
|
|
528
|
+
if (row.getIsSelected()) {
|
|
529
|
+
// bg-indigo-50/40 → 40% indigo-50 blended with card
|
|
530
|
+
return { '--row-bg': 'color-mix(in srgb, #eef2ff 40%, var(--card, #fff))' }
|
|
531
|
+
}
|
|
532
|
+
return {}
|
|
533
|
+
}
|
|
534
|
+
|
|
460
535
|
// ─── Expose ───────────────────────────────────────────────────────────────────
|
|
461
536
|
const reloadTable = () => {
|
|
462
537
|
clearCache()
|
|
@@ -475,6 +550,8 @@ defineExpose({
|
|
|
475
550
|
isDataFromCache,
|
|
476
551
|
cached: computed(() => props.cached),
|
|
477
552
|
paginationBarRef,
|
|
553
|
+
columnPinning,
|
|
554
|
+
pinColumn,
|
|
478
555
|
})
|
|
479
556
|
</script>
|
|
480
557
|
|
|
@@ -488,8 +565,9 @@ defineExpose({
|
|
|
488
565
|
:style="{ tableLayout: 'fixed', width: table.getTotalSize() + 'px', minWidth: '100%' }"
|
|
489
566
|
>
|
|
490
567
|
<colgroup>
|
|
568
|
+
<!-- Must use pinning order (left|center|right) — same as getHeaderGroups() and row.getVisibleCells() -->
|
|
491
569
|
<col
|
|
492
|
-
v-for="col in table.
|
|
570
|
+
v-for="col in [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns(), ...table.getRightVisibleLeafColumns()]"
|
|
493
571
|
:key="col.id"
|
|
494
572
|
:style="{ width: col.getSize() + 'px' }"
|
|
495
573
|
>
|
|
@@ -497,24 +575,23 @@ defineExpose({
|
|
|
497
575
|
<thead class="relative z-20 bg-card">
|
|
498
576
|
<template v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
|
499
577
|
<!-- Main header row -->
|
|
500
|
-
<tr
|
|
501
|
-
class="divide-x divide-card-line"
|
|
502
|
-
>
|
|
578
|
+
<tr class="bg-card">
|
|
503
579
|
<th
|
|
504
580
|
v-for="header in headerGroup.headers"
|
|
505
581
|
:key="header.id"
|
|
506
582
|
scope="col"
|
|
507
|
-
:draggable="header.id !== 'select' && resizeHoverId !== header.id"
|
|
508
|
-
@dragstart="header.id !== 'select' && resizeHoverId !== header.id && onHeaderDragStart(header.id)"
|
|
583
|
+
:draggable="header.id !== 'select' && resizeHoverId !== header.id && !header.column.getIsPinned()"
|
|
584
|
+
@dragstart="header.id !== 'select' && resizeHoverId !== header.id && !header.column.getIsPinned() && onHeaderDragStart(header.id)"
|
|
509
585
|
@dragover="header.id !== 'select' && onHeaderDragOver($event, header.id)"
|
|
510
586
|
@dragleave="onHeaderDragLeave"
|
|
511
587
|
@drop="header.id !== 'select' && onHeaderDrop(header.id)"
|
|
512
|
-
class="relative
|
|
588
|
+
class="relative"
|
|
513
589
|
:class="[
|
|
514
590
|
header.id === 'select' ? 'text-center' : '',
|
|
515
591
|
dragOverHeaderId === header.id ? 'bg-indigo-50 dark:bg-indigo-900/20' : '',
|
|
516
592
|
header.column.getCanSort() ? 'cursor-pointer select-none' : '',
|
|
517
593
|
]"
|
|
594
|
+
:style="getPinnedStyles(header.column, true)"
|
|
518
595
|
@click="header.column.getCanSort() && header.column.toggleSorting()"
|
|
519
596
|
>
|
|
520
597
|
<!-- Select all checkbox -->
|
|
@@ -529,8 +606,12 @@ defineExpose({
|
|
|
529
606
|
</template>
|
|
530
607
|
<!-- Regular column header -->
|
|
531
608
|
<template v-else>
|
|
532
|
-
<div
|
|
533
|
-
|
|
609
|
+
<div
|
|
610
|
+
class="px-4 py-3 flex items-center gap-x-1 text-xs font-medium w-full overflow-hidden"
|
|
611
|
+
:class="header.column.getIsPinned() ? 'text-foreground' : 'text-muted-foreground'"
|
|
612
|
+
>
|
|
613
|
+
<IconPin v-if="header.column.getIsPinned()" class="size-3 shrink-0 text-indigo-400 dark:text-indigo-500" />
|
|
614
|
+
<span class="truncate">{{ header.column.columnDef.meta?.label ?? header.id }}</span>
|
|
534
615
|
<span v-if="header.column.getCanSort()">
|
|
535
616
|
<IconSelector v-if="!header.column.getIsSorted()" class="size-4 opacity-40" />
|
|
536
617
|
<IconChevronDown v-else-if="header.column.getIsSorted() === 'desc'" class="size-4" />
|
|
@@ -563,12 +644,15 @@ defineExpose({
|
|
|
563
644
|
<!-- Column filter row -->
|
|
564
645
|
<tr
|
|
565
646
|
v-if="hasFilterableColumns"
|
|
566
|
-
class="
|
|
647
|
+
class="border-b border-card-line bg-muted/50"
|
|
567
648
|
>
|
|
568
649
|
<th
|
|
569
650
|
v-for="header in headerGroup.headers"
|
|
570
651
|
:key="'f-' + header.id"
|
|
571
|
-
:class="
|
|
652
|
+
:class="[
|
|
653
|
+
header.id === 'select' ? 'w-12' : 'px-3 py-1.5',
|
|
654
|
+
]"
|
|
655
|
+
:style="getPinnedStyles(header.column, true)"
|
|
572
656
|
>
|
|
573
657
|
<input
|
|
574
658
|
v-if="header.column.getCanFilter()"
|
|
@@ -588,13 +672,15 @@ defineExpose({
|
|
|
588
672
|
v-if="loading"
|
|
589
673
|
v-for="(_, i) in skeletonRows"
|
|
590
674
|
:key="'sk-' + i"
|
|
591
|
-
class="animate-pulse
|
|
675
|
+
class="animate-pulse bg-card"
|
|
592
676
|
>
|
|
593
677
|
<td
|
|
594
678
|
v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
|
|
595
679
|
:key="'skc-' + header.id"
|
|
596
|
-
:class="
|
|
597
|
-
|
|
680
|
+
:class="[
|
|
681
|
+
header.id === 'select' ? 'text-center w-12' : 'px-4 overflow-hidden',
|
|
682
|
+
]"
|
|
683
|
+
:style="{ height: lastRowHeight + 'px', ...getPinnedStyles(header.column) }"
|
|
598
684
|
>
|
|
599
685
|
<div v-if="header.id === 'select'" class="w-4 h-4 bg-surface-1 rounded mx-auto"></div>
|
|
600
686
|
<div v-else class="h-4 w-[50%] rounded bg-surface-1"></div>
|
|
@@ -606,12 +692,12 @@ defineExpose({
|
|
|
606
692
|
v-if="loading && skeletonRows.length < pagination.pageSize"
|
|
607
693
|
v-for="i in (pagination.pageSize - skeletonRows.length)"
|
|
608
694
|
:key="'lf-' + i"
|
|
609
|
-
class="
|
|
695
|
+
class="bg-card"
|
|
610
696
|
>
|
|
611
697
|
<td
|
|
612
698
|
v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
|
|
613
699
|
:key="'lfc-' + header.id"
|
|
614
|
-
:style="{ height: lastRowHeight + 'px' }"
|
|
700
|
+
:style="{ height: lastRowHeight + 'px', ...getPinnedStyles(header.column) }"
|
|
615
701
|
/>
|
|
616
702
|
</tr>
|
|
617
703
|
|
|
@@ -620,12 +706,12 @@ defineExpose({
|
|
|
620
706
|
v-if="!loading && tableData.length === 0"
|
|
621
707
|
v-for="i in pagination.pageSize"
|
|
622
708
|
:key="'esk-' + i"
|
|
623
|
-
class="
|
|
709
|
+
class="bg-card"
|
|
624
710
|
>
|
|
625
711
|
<td
|
|
626
712
|
v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
|
|
627
713
|
:key="'eskc-' + header.id"
|
|
628
|
-
:style="{ height: lastRowHeight + 'px' }"
|
|
714
|
+
:style="{ height: lastRowHeight + 'px', ...getPinnedStyles(header.column) }"
|
|
629
715
|
/>
|
|
630
716
|
</tr>
|
|
631
717
|
|
|
@@ -638,12 +724,13 @@ defineExpose({
|
|
|
638
724
|
@click="(e) => handleRowClick(row, e)"
|
|
639
725
|
@keydown="(e) => handleRowKeydown(row, e)"
|
|
640
726
|
:tabindex="isRowClickEnabled ? 0 : undefined"
|
|
641
|
-
class="
|
|
727
|
+
class="bg-card hover:bg-layer-hover transition-colors"
|
|
642
728
|
:class="{
|
|
643
729
|
'cursor-pointer': isRowClickEnabled,
|
|
644
730
|
'bg-indigo-50/40 dark:bg-indigo-900/10 hover:bg-indigo-50/60': row.getIsSelected(),
|
|
645
731
|
'!bg-indigo-50 dark:!bg-indigo-900/20 ring-1 ring-inset ring-indigo-200 dark:ring-indigo-700': previewRowId && row.original.id === previewRowId,
|
|
646
732
|
}"
|
|
733
|
+
:style="pinnedRowStyle(row)"
|
|
647
734
|
>
|
|
648
735
|
<td
|
|
649
736
|
v-for="cell in row.getVisibleCells()"
|
|
@@ -651,10 +738,11 @@ defineExpose({
|
|
|
651
738
|
:data-col-id="cell.column.id"
|
|
652
739
|
:class="[
|
|
653
740
|
cell.column.id === 'select'
|
|
654
|
-
? 'text-center w-12'
|
|
655
|
-
: 'px-4 py-3 text-sm text-muted-foreground-1',
|
|
741
|
+
? 'text-center w-12 overflow-hidden'
|
|
742
|
+
: 'px-4 py-3 text-sm text-muted-foreground-1 overflow-hidden',
|
|
656
743
|
cell.column.id !== 'select' ? cell.column.columnDef.meta?.class ?? '' : '',
|
|
657
744
|
]"
|
|
745
|
+
:style="getPinnedStyles(cell.column)"
|
|
658
746
|
>
|
|
659
747
|
<!-- Select checkbox -->
|
|
660
748
|
<template v-if="cell.column.id === 'select'">
|
|
@@ -682,12 +770,12 @@ defineExpose({
|
|
|
682
770
|
v-if="!loading && tableData.length > 0 && tableData.length < pagination.pageSize"
|
|
683
771
|
v-for="i in (pagination.pageSize - tableData.length)"
|
|
684
772
|
:key="'fill-' + i"
|
|
685
|
-
class="
|
|
773
|
+
class="bg-card"
|
|
686
774
|
>
|
|
687
775
|
<td
|
|
688
776
|
v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
|
|
689
777
|
:key="'fillc-' + header.id"
|
|
690
|
-
:style="{ height: lastRowHeight + 'px' }"
|
|
778
|
+
:style="{ height: lastRowHeight + 'px', ...getPinnedStyles(header.column) }"
|
|
691
779
|
/>
|
|
692
780
|
</tr>
|
|
693
781
|
</tbody>
|
|
@@ -870,3 +958,17 @@ defineExpose({
|
|
|
870
958
|
</div>
|
|
871
959
|
</div>
|
|
872
960
|
</template>
|
|
961
|
+
|
|
962
|
+
<style scoped>
|
|
963
|
+
/* --row-bg drives the background of sticky (pinned) body cells.
|
|
964
|
+
Only solid, opaque values here — semi-transparent tints for selected/preview
|
|
965
|
+
rows intentionally do NOT override --row-bg, so pinned cells stay opaque
|
|
966
|
+
and text from scrolling content can't bleed through. */
|
|
967
|
+
tbody tr {
|
|
968
|
+
--row-bg: var(--card, #fff);
|
|
969
|
+
}
|
|
970
|
+
tbody tr:hover {
|
|
971
|
+
--row-bg: var(--layer-hover, #f8fafc);
|
|
972
|
+
}
|
|
973
|
+
</style>
|
|
974
|
+
|
|
@@ -26,8 +26,8 @@ const toggleColumn = (key) => {
|
|
|
26
26
|
if (idx >= 0) selectedColumns.value.splice(idx, 1)
|
|
27
27
|
else selectedColumns.value.push(key)
|
|
28
28
|
}
|
|
29
|
-
const allSelected
|
|
30
|
-
const toggleAll
|
|
29
|
+
const allSelected = computed(() => selectedColumns.value.length === props.columns.length)
|
|
30
|
+
const toggleAll = () => {
|
|
31
31
|
selectedColumns.value = allSelected.value ? [] : props.columns.map(c => c.key)
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -43,12 +43,12 @@ const doExport = () => {
|
|
|
43
43
|
isOpen.value = false
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const panelRef
|
|
46
|
+
const panelRef = ref(null)
|
|
47
47
|
const triggerRef = ref(null)
|
|
48
48
|
|
|
49
49
|
const onOutsideClick = (e) => {
|
|
50
50
|
if (
|
|
51
|
-
panelRef.value
|
|
51
|
+
panelRef.value && !panelRef.value.contains(e.target) &&
|
|
52
52
|
triggerRef.value && !triggerRef.value.contains(e.target)
|
|
53
53
|
) {
|
|
54
54
|
isOpen.value = false
|
|
@@ -65,19 +65,21 @@ defineExpose({ open: () => { isOpen.value = true } })
|
|
|
65
65
|
|
|
66
66
|
<template>
|
|
67
67
|
<div class="relative">
|
|
68
|
+
|
|
69
|
+
<!-- Trigger — icon-only, igual al botón de columnas -->
|
|
68
70
|
<button
|
|
69
71
|
ref="triggerRef"
|
|
70
72
|
type="button"
|
|
71
73
|
@click="isOpen = !isOpen"
|
|
74
|
+
title="Exportar"
|
|
72
75
|
:class="[
|
|
73
|
-
'
|
|
76
|
+
'p-1.5 inline-flex items-center justify-center rounded-lg border transition-colors',
|
|
74
77
|
isOpen
|
|
75
|
-
? 'border-
|
|
76
|
-
: 'border-
|
|
78
|
+
? 'border-primary/40 bg-primary/10 text-primary'
|
|
79
|
+
: 'border-transparent text-muted-foreground hover:border-card-line hover:bg-muted-hover hover:text-foreground'
|
|
77
80
|
]"
|
|
78
81
|
>
|
|
79
82
|
<IconDownload class="size-4" stroke="1.5" />
|
|
80
|
-
Exportar
|
|
81
83
|
</button>
|
|
82
84
|
|
|
83
85
|
<Transition
|
|
@@ -105,7 +107,7 @@ defineExpose({ open: () => { isOpen.value = true } })
|
|
|
105
107
|
:class="[
|
|
106
108
|
'flex flex-col items-center gap-1 py-2 rounded-lg border text-xs font-medium transition-colors',
|
|
107
109
|
format === f.value
|
|
108
|
-
? 'border-
|
|
110
|
+
? 'border-primary/40 bg-primary/10 text-primary'
|
|
109
111
|
: 'border-card-line text-muted-foreground-1 hover:bg-muted-hover'
|
|
110
112
|
]"
|
|
111
113
|
>
|
|
@@ -124,7 +126,7 @@ defineExpose({ open: () => { isOpen.value = true } })
|
|
|
124
126
|
<input
|
|
125
127
|
v-model="filename"
|
|
126
128
|
type="text"
|
|
127
|
-
class="flex-1 rounded-lg border border-card-line bg-card text-foreground py-1.5 px-2.5 text-xs focus:outline-none focus:ring-1 focus:ring-
|
|
129
|
+
class="flex-1 rounded-lg border border-card-line bg-card text-foreground py-1.5 px-2.5 text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 min-w-0"
|
|
128
130
|
/>
|
|
129
131
|
<span class="text-xs text-muted-foreground shrink-0">.{{ format }}</span>
|
|
130
132
|
</div>
|
|
@@ -134,7 +136,7 @@ defineExpose({ open: () => { isOpen.value = true } })
|
|
|
134
136
|
<div v-if="columns.length > 0" class="mb-3 px-1">
|
|
135
137
|
<div class="flex items-center justify-between mb-1.5">
|
|
136
138
|
<label class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest">Columnas</label>
|
|
137
|
-
<button type="button" @click="toggleAll" class="text-[10px] text-
|
|
139
|
+
<button type="button" @click="toggleAll" class="text-[10px] text-primary hover:underline">
|
|
138
140
|
{{ allSelected ? 'Ninguna' : 'Todas' }}
|
|
139
141
|
</button>
|
|
140
142
|
</div>
|
|
@@ -148,7 +150,7 @@ defineExpose({ open: () => { isOpen.value = true } })
|
|
|
148
150
|
type="checkbox"
|
|
149
151
|
:checked="selectedColumns.includes(col.key)"
|
|
150
152
|
@change="toggleColumn(col.key)"
|
|
151
|
-
class="rounded border-card-line bg-surface shrink-0 cursor-pointer"
|
|
153
|
+
class="rounded border-card-line bg-surface shrink-0 cursor-pointer text-primary"
|
|
152
154
|
/>
|
|
153
155
|
<span class="text-xs text-foreground truncate">{{ col.label }}</span>
|
|
154
156
|
</label>
|
|
@@ -159,7 +161,7 @@ defineExpose({ open: () => { isOpen.value = true } })
|
|
|
159
161
|
<button
|
|
160
162
|
type="button"
|
|
161
163
|
@click="doExport"
|
|
162
|
-
class="w-full py-1.5 px-3 rounded-lg bg-
|
|
164
|
+
class="w-full py-1.5 px-3 rounded-lg bg-primary hover:bg-primary/90 text-white text-sm font-medium transition-colors inline-flex items-center justify-center gap-2"
|
|
163
165
|
>
|
|
164
166
|
<IconDownload class="size-4" stroke="1.5" />
|
|
165
167
|
Exportar
|
package/package.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useDebounceFn } from '@vueuse/core'
|
|
2
|
+
|
|
3
|
+
export interface TablePreferences {
|
|
4
|
+
pinning?: { left: string[], right: string[] }
|
|
5
|
+
visibility?: Record<string, boolean>
|
|
6
|
+
order?: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useTablePreferences(tableName: string) {
|
|
10
|
+
const api = useApi()
|
|
11
|
+
const prefKey = `table:${tableName}:columns`
|
|
12
|
+
|
|
13
|
+
const preferences = ref<TablePreferences>({})
|
|
14
|
+
|
|
15
|
+
const load = async () => {
|
|
16
|
+
try {
|
|
17
|
+
const data = await api.get(`auth/me/preferences/${prefKey}`)
|
|
18
|
+
preferences.value = data?.value ?? {}
|
|
19
|
+
} catch {
|
|
20
|
+
preferences.value = {}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const save = useDebounceFn(async (value: TablePreferences) => {
|
|
25
|
+
try {
|
|
26
|
+
await api.put(`auth/me/preferences/${prefKey}`, { value, cast: 'json' })
|
|
27
|
+
} catch {
|
|
28
|
+
// silent fail — preferencias no son críticas
|
|
29
|
+
}
|
|
30
|
+
}, 800)
|
|
31
|
+
|
|
32
|
+
return { preferences, load, save }
|
|
33
|
+
}
|