@innertia-solutions/nuxt-theme-spark 0.1.140 → 0.1.142
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/components/Table/Standard.vue +322 -169
- package/components/Table.vue +28 -65
- package/components/TableExportable.vue +15 -13
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { IconSearch, IconLayoutColumns, IconGripVertical, IconMinus, IconMaximize, IconX, IconPlus, IconChevronLeft, IconCheck, IconChevronDown } from '@tabler/icons-vue'
|
|
2
|
+
import { IconSearch, IconLayoutColumns, IconGripVertical, IconMinus, IconMaximize, IconX, IconPlus, IconChevronLeft, IconCheck, IconChevronDown, IconExternalLink, IconTrash } from '@tabler/icons-vue'
|
|
3
3
|
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
table: { type: Object, default: null },
|
|
@@ -16,14 +16,16 @@ const props = defineProps({
|
|
|
16
16
|
showFilters: { type: Boolean, default: true },
|
|
17
17
|
showExport: { type: Boolean, default: true },
|
|
18
18
|
filters: { type: Array, default: () => [] },
|
|
19
|
-
splitRatio: { type: Number,
|
|
20
|
-
autoClosePreview: { type: Boolean,
|
|
19
|
+
splitRatio: { type: Number, default: 60 },
|
|
20
|
+
autoClosePreview: { type: Boolean, default: true },
|
|
21
|
+
previewHref: { type: [String, Function], default: null }, // url fija o (row) => url
|
|
22
|
+
previewDeletable: { type: Boolean, default: false },
|
|
21
23
|
})
|
|
22
24
|
|
|
23
25
|
const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
|
|
24
26
|
const resolvedName = computed(() => props.table?.name ?? props.name)
|
|
25
27
|
|
|
26
|
-
const emit = defineEmits(['row-click', 'loaded'])
|
|
28
|
+
const emit = defineEmits(['row-click', 'loaded', 'preview-delete'])
|
|
27
29
|
const slots = useSlots()
|
|
28
30
|
const forwardedSlots = computed(() => {
|
|
29
31
|
const excluded = new Set(['toolbar', 'preview'])
|
|
@@ -51,6 +53,20 @@ const filterMenuRef = ref(null)
|
|
|
51
53
|
const filterAddBtnRef = ref(null)
|
|
52
54
|
const filterMenuStyle = ref({})
|
|
53
55
|
|
|
56
|
+
const pendingOperator = ref('contains')
|
|
57
|
+
const pendingValueInputRef = ref(null)
|
|
58
|
+
|
|
59
|
+
const textOps = [
|
|
60
|
+
{ value: 'contains', label: 'contiene' },
|
|
61
|
+
{ value: 'starts_with', label: 'empieza con' },
|
|
62
|
+
{ value: 'equals', label: 'es igual a' },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const selectOps = [
|
|
66
|
+
{ value: 'is', label: 'es' },
|
|
67
|
+
{ value: 'is_not', label: 'no es' },
|
|
68
|
+
]
|
|
69
|
+
|
|
54
70
|
const dateOps = [
|
|
55
71
|
{ value: 'before', label: 'antes de' },
|
|
56
72
|
{ value: 'after', label: 'después de' },
|
|
@@ -61,8 +77,9 @@ const activeFilterList = computed(() =>
|
|
|
61
77
|
filtersConfig.value
|
|
62
78
|
.filter(col => {
|
|
63
79
|
const v = activeFilters.value[col.key]
|
|
80
|
+
if (!v) return false
|
|
64
81
|
if (col.filterType === 'daterange') return v?.from || v?.to
|
|
65
|
-
return v !== null && v !== undefined && v !== ''
|
|
82
|
+
return v?.value !== null && v?.value !== undefined && v?.value !== ''
|
|
66
83
|
})
|
|
67
84
|
.map(col => {
|
|
68
85
|
const v = activeFilters.value[col.key]
|
|
@@ -72,10 +89,13 @@ const activeFilterList = computed(() =>
|
|
|
72
89
|
else if (v.from) { displayOp = 'después de'; displayVal = v.from }
|
|
73
90
|
else { displayOp = 'antes de'; displayVal = v.to }
|
|
74
91
|
} else if (col.filterType === 'select') {
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
const op = selectOps.find(o => o.value === v.operator) ?? selectOps[0]
|
|
93
|
+
displayOp = op.label
|
|
94
|
+
displayVal = col.filterOptions?.find(o => o.value === v.value)?.label ?? v.value
|
|
77
95
|
} else {
|
|
78
|
-
|
|
96
|
+
const op = textOps.find(o => o.value === v.operator) ?? textOps[0]
|
|
97
|
+
displayOp = op.label
|
|
98
|
+
displayVal = v.value
|
|
79
99
|
}
|
|
80
100
|
return { key: col.key, label: col.label, displayOp, displayVal, col }
|
|
81
101
|
})
|
|
@@ -83,22 +103,54 @@ const activeFilterList = computed(() =>
|
|
|
83
103
|
|
|
84
104
|
const activeFilterCount = computed(() => activeFilterList.value.length)
|
|
85
105
|
|
|
106
|
+
// Columns NOT yet filtered — what appears in the picker (already-active columns are hidden)
|
|
107
|
+
const availableFilterColumns = computed(() =>
|
|
108
|
+
filtersConfig.value.filter(col => {
|
|
109
|
+
const v = activeFilters.value[col.key]
|
|
110
|
+
if (!v) return true
|
|
111
|
+
if (col.filterType === 'daterange') return !v.from && !v.to
|
|
112
|
+
return !v.value
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// Convert activeFilters to enriched [{field, operator, value}] for the backend DataTable
|
|
117
|
+
const enrichedFilters = computed(() => {
|
|
118
|
+
const result = []
|
|
119
|
+
for (const col of filtersConfig.value) {
|
|
120
|
+
const v = activeFilters.value[col.key]
|
|
121
|
+
if (!v) continue
|
|
122
|
+
if (col.filterType === 'daterange') {
|
|
123
|
+
if (!v.from && !v.to) continue
|
|
124
|
+
if (v.from) result.push({ field: col.key, operator: 'after', value: v.from })
|
|
125
|
+
if (v.to) result.push({ field: col.key, operator: 'before', value: v.to })
|
|
126
|
+
} else {
|
|
127
|
+
if (!v.value) continue
|
|
128
|
+
result.push({ field: col.key, operator: v.operator ?? 'contains', value: v.value })
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return result
|
|
132
|
+
})
|
|
133
|
+
|
|
86
134
|
const mergedParams = computed(() => ({
|
|
87
135
|
...props.params,
|
|
88
|
-
...
|
|
136
|
+
...(enrichedFilters.value.length ? { filters: enrichedFilters.value } : {}),
|
|
89
137
|
}))
|
|
90
138
|
|
|
91
139
|
const removeFilter = (key) => {
|
|
92
140
|
const u = { ...activeFilters.value }; delete u[key]; activeFilters.value = u
|
|
93
141
|
}
|
|
94
142
|
|
|
143
|
+
const updateFilterMenuPosition = () => {
|
|
144
|
+
const rect = filterAddBtnRef.value?.getBoundingClientRect()
|
|
145
|
+
if (rect) filterMenuStyle.value = { top: rect.bottom + 4 + 'px', left: rect.left + 'px' }
|
|
146
|
+
}
|
|
147
|
+
|
|
95
148
|
const openFilterMenu = async () => {
|
|
96
149
|
filterMenuStep.value = 'columns'
|
|
97
150
|
pendingCol.value = null
|
|
98
151
|
showFilterPanel.value = true
|
|
99
152
|
await nextTick()
|
|
100
|
-
|
|
101
|
-
if (rect) filterMenuStyle.value = { top: rect.bottom + 4 + 'px', left: rect.left + 'px' }
|
|
153
|
+
updateFilterMenuPosition()
|
|
102
154
|
}
|
|
103
155
|
|
|
104
156
|
const toggleFilterMenu = async () => {
|
|
@@ -121,7 +173,8 @@ const selectFilterColumn = (col) => {
|
|
|
121
173
|
else if (existing?.to) { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: existing.to, from: '', to: '' } }
|
|
122
174
|
else { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: '', from: '', to: '' } }
|
|
123
175
|
} else {
|
|
124
|
-
|
|
176
|
+
pendingOperator.value = existing?.operator ?? (col.filterType === 'select' ? 'is' : 'contains')
|
|
177
|
+
pendingValue.value = existing?.value ?? ''
|
|
125
178
|
}
|
|
126
179
|
filterMenuStep.value = 'value'
|
|
127
180
|
}
|
|
@@ -135,9 +188,9 @@ const applyPendingFilter = () => {
|
|
|
135
188
|
else if (pendingDateOp.value === 'after') v = { from: pendingValue.value.singleDate }
|
|
136
189
|
else v = { to: pendingValue.value.singleDate }
|
|
137
190
|
} else {
|
|
138
|
-
v = pendingValue.value
|
|
191
|
+
v = { value: pendingValue.value, operator: pendingOperator.value }
|
|
139
192
|
}
|
|
140
|
-
activeFilters.value = { ...activeFilters.value, [col.key]: v
|
|
193
|
+
activeFilters.value = { ...activeFilters.value, [col.key]: v }
|
|
141
194
|
closeFilterMenu()
|
|
142
195
|
}
|
|
143
196
|
|
|
@@ -156,8 +209,30 @@ const onFilterMenuOutsideClick = (e) => {
|
|
|
156
209
|
}
|
|
157
210
|
}
|
|
158
211
|
watch(showFilterPanel, v => {
|
|
159
|
-
if (v)
|
|
160
|
-
|
|
212
|
+
if (v) {
|
|
213
|
+
document.addEventListener('mousedown', onFilterMenuOutsideClick)
|
|
214
|
+
window.addEventListener('scroll', updateFilterMenuPosition, true)
|
|
215
|
+
window.addEventListener('resize', updateFilterMenuPosition)
|
|
216
|
+
} else {
|
|
217
|
+
document.removeEventListener('mousedown', onFilterMenuOutsideClick)
|
|
218
|
+
window.removeEventListener('scroll', updateFilterMenuPosition, true)
|
|
219
|
+
window.removeEventListener('resize', updateFilterMenuPosition)
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
watch(filterMenuStep, async (step) => {
|
|
224
|
+
if (step === 'value' && pendingCol.value?.filterType === 'text') {
|
|
225
|
+
await nextTick()
|
|
226
|
+
pendingValueInputRef.value?.focus()
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
// ─── Preview href helper ───────────────────────────────────────────────────────
|
|
231
|
+
const resolvedPreviewHref = computed(() => {
|
|
232
|
+
if (!props.previewHref || !previewRow.value) return null
|
|
233
|
+
return typeof props.previewHref === 'function'
|
|
234
|
+
? props.previewHref(previewRow.value)
|
|
235
|
+
: props.previewHref
|
|
161
236
|
})
|
|
162
237
|
|
|
163
238
|
// ─── Preview panel ─────────────────────────────────────────────────────────────
|
|
@@ -182,12 +257,19 @@ const resolvedHistoryEndpoint = computed(() => {
|
|
|
182
257
|
return `history/${tableMeta.value.entity_type}/${previewRow.value.id}`
|
|
183
258
|
})
|
|
184
259
|
|
|
185
|
-
watch(previewRow, () => {
|
|
260
|
+
watch(previewRow, async (row) => {
|
|
261
|
+
previewTab.value = 'datos'
|
|
262
|
+
if (row) {
|
|
263
|
+
await nextTick()
|
|
264
|
+
if (typeof window !== 'undefined') window.HSStaticMethods?.autoInit?.(['Tooltip'])
|
|
265
|
+
}
|
|
266
|
+
})
|
|
186
267
|
|
|
187
268
|
const handleRowClick = (row) => {
|
|
188
269
|
if (previewEnabled.value) {
|
|
270
|
+
if (previewRow.value?.id === row.id) return // ya está abierto, no pestañear
|
|
189
271
|
collapseDock()
|
|
190
|
-
previewRow.value =
|
|
272
|
+
previewRow.value = row
|
|
191
273
|
} else {
|
|
192
274
|
emit('row-click', row)
|
|
193
275
|
}
|
|
@@ -369,19 +451,22 @@ const onColumnPanelOutsideClick = (e) => {
|
|
|
369
451
|
}
|
|
370
452
|
}
|
|
371
453
|
|
|
454
|
+
const updateColumnPanelPosition = () => {
|
|
455
|
+
const rect = columnButtonRef.value?.getBoundingClientRect()
|
|
456
|
+
if (rect) columnPanelStyle.value = { top: rect.bottom + 6 + 'px', right: window.innerWidth - rect.right + 'px' }
|
|
457
|
+
}
|
|
458
|
+
|
|
372
459
|
watch(showColumnPanel, async (v) => {
|
|
373
460
|
if (v) {
|
|
374
461
|
await nextTick()
|
|
375
|
-
|
|
376
|
-
if (rect) {
|
|
377
|
-
columnPanelStyle.value = {
|
|
378
|
-
top: rect.bottom + 6 + 'px',
|
|
379
|
-
right: window.innerWidth - rect.right + 'px',
|
|
380
|
-
}
|
|
381
|
-
}
|
|
462
|
+
updateColumnPanelPosition()
|
|
382
463
|
document.addEventListener('mousedown', onColumnPanelOutsideClick)
|
|
464
|
+
window.addEventListener('scroll', updateColumnPanelPosition, true)
|
|
465
|
+
window.addEventListener('resize', updateColumnPanelPosition)
|
|
383
466
|
} else {
|
|
384
467
|
document.removeEventListener('mousedown', onColumnPanelOutsideClick)
|
|
468
|
+
window.removeEventListener('scroll', updateColumnPanelPosition, true)
|
|
469
|
+
window.removeEventListener('resize', updateColumnPanelPosition)
|
|
385
470
|
}
|
|
386
471
|
})
|
|
387
472
|
|
|
@@ -413,7 +498,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
413
498
|
:class="[
|
|
414
499
|
'inline-flex items-center gap-1.5 py-1.5 px-3 text-sm font-medium rounded-lg border transition-colors',
|
|
415
500
|
activeFilterList.length
|
|
416
|
-
? 'border-
|
|
501
|
+
? 'border-primary/40 bg-primary/10 text-primary'
|
|
417
502
|
: 'border-card-line bg-card text-muted-foreground-1 hover:bg-muted-hover'
|
|
418
503
|
]"
|
|
419
504
|
>
|
|
@@ -425,23 +510,25 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
425
510
|
<!-- Slot for custom toolbar buttons -->
|
|
426
511
|
<slot name="toolbar" />
|
|
427
512
|
|
|
428
|
-
<!--
|
|
429
|
-
<
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
'
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
513
|
+
<!-- Secondary actions: pushed to the right, icon-only style -->
|
|
514
|
+
<div class="ml-auto flex items-center gap-1">
|
|
515
|
+
<button
|
|
516
|
+
ref="columnButtonRef"
|
|
517
|
+
type="button"
|
|
518
|
+
@click="showColumnPanel = !showColumnPanel"
|
|
519
|
+
:title="'Columnas'"
|
|
520
|
+
:class="[
|
|
521
|
+
'p-1.5 inline-flex items-center justify-center rounded-lg border transition-colors',
|
|
522
|
+
showColumnPanel
|
|
523
|
+
? 'border-primary/40 bg-primary/10 text-primary'
|
|
524
|
+
: 'border-transparent text-muted-foreground hover:border-card-line hover:bg-muted-hover hover:text-foreground'
|
|
525
|
+
]"
|
|
526
|
+
>
|
|
527
|
+
<IconLayoutColumns class="size-4" />
|
|
528
|
+
</button>
|
|
443
529
|
|
|
444
|
-
|
|
530
|
+
<TableExportable v-if="showExport" :table-ref="tableRef" :name="resolvedName" :columns="columns" />
|
|
531
|
+
</div>
|
|
445
532
|
</div>
|
|
446
533
|
|
|
447
534
|
<!-- Filter chips row (shown when filters active) -->
|
|
@@ -456,7 +543,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
456
543
|
<button
|
|
457
544
|
type="button"
|
|
458
545
|
@click.stop="openEditFilter(chip.col)"
|
|
459
|
-
class="inline-flex items-center gap-1 px-2 py-1 text-
|
|
546
|
+
class="inline-flex items-center gap-1 px-2 py-1 text-primary font-medium hover:bg-primary/10 transition-colors border-x border-card-line"
|
|
460
547
|
>
|
|
461
548
|
{{ chip.displayVal }}
|
|
462
549
|
<IconChevronDown class="size-3 opacity-60" />
|
|
@@ -471,10 +558,8 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
471
558
|
</div>
|
|
472
559
|
</div>
|
|
473
560
|
|
|
474
|
-
<!--
|
|
475
|
-
<div class="
|
|
476
|
-
|
|
477
|
-
<!-- Tabla -->
|
|
561
|
+
<!-- Tabla -->
|
|
562
|
+
<div class="overflow-hidden border-t border-b border-card-line">
|
|
478
563
|
<Table
|
|
479
564
|
ref="tableRef"
|
|
480
565
|
:endpoint="resolvedEndpoint"
|
|
@@ -497,100 +582,134 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
497
582
|
<slot :name="name" v-bind="slotProps ?? {}" />
|
|
498
583
|
</template>
|
|
499
584
|
</Table>
|
|
585
|
+
</div>
|
|
500
586
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
587
|
+
<!-- Preview panel — overlay deslizante desde la derecha (cubre todo el componente) -->
|
|
588
|
+
<Transition
|
|
589
|
+
:enter-active-class="previewFromCache ? '' : 'transition ease-out duration-200'"
|
|
590
|
+
:enter-from-class="previewFromCache ? '' : 'opacity-0 translate-x-6'"
|
|
591
|
+
:enter-to-class="previewFromCache ? '' : 'opacity-100 translate-x-0'"
|
|
592
|
+
leave-active-class="transition ease-in duration-150"
|
|
593
|
+
leave-from-class="opacity-100 translate-x-0"
|
|
594
|
+
leave-to-class="opacity-0 translate-x-6"
|
|
595
|
+
>
|
|
596
|
+
<div
|
|
597
|
+
v-if="previewRow && previewEnabled"
|
|
598
|
+
ref="previewPanelRef"
|
|
599
|
+
class="absolute top-0 right-0 bottom-0 z-30 flex flex-col bg-card border border-card-line rounded-2xl shadow-xl overflow-hidden"
|
|
600
|
+
:style="{ width: (100 - currentRatio) + '%' }"
|
|
509
601
|
>
|
|
602
|
+
<!-- Resize handle — thin pill on left edge -->
|
|
510
603
|
<div
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
<!-- Preview -->
|
|
522
|
-
<div class="flex flex-col flex-1 overflow-hidden">
|
|
604
|
+
class="absolute left-1 top-1/2 -translate-y-1/2 h-12 w-1 cursor-col-resize rounded-full bg-border hover:bg-primary/50 transition-colors z-10"
|
|
605
|
+
@mousedown="startResize"
|
|
606
|
+
/>
|
|
607
|
+
|
|
608
|
+
<!-- Barra de acciones del preview -->
|
|
609
|
+
<div class="shrink-0 flex items-center gap-3 px-5 py-4 border-b border-card-line">
|
|
610
|
+
<!-- Título (reemplazable via slot) -->
|
|
611
|
+
<div class="flex-1 min-w-0">
|
|
612
|
+
<slot name="preview-header" :row="previewRow" :close="closePreview" />
|
|
613
|
+
</div>
|
|
523
614
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
type="button"
|
|
540
|
-
class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
541
|
-
title="Cerrar"
|
|
542
|
-
@click.stop="closePreview"
|
|
543
|
-
>
|
|
544
|
-
<IconX class="size-3.5" />
|
|
545
|
-
</button>
|
|
546
|
-
</div>
|
|
615
|
+
<!-- Botones de acción — todos icon-only, mismo tamaño y estilo -->
|
|
616
|
+
<div class="flex items-center gap-0.5 shrink-0">
|
|
617
|
+
|
|
618
|
+
<!-- Acciones extra configurables desde el padre -->
|
|
619
|
+
<slot name="preview-actions" :row="previewRow" :close="closePreview" />
|
|
620
|
+
|
|
621
|
+
<!-- Abrir (configurable vía :preview-href) -->
|
|
622
|
+
<div v-if="resolvedPreviewHref" class="hs-tooltip [--placement:top] inline-block">
|
|
623
|
+
<NuxtLink
|
|
624
|
+
:to="resolvedPreviewHref"
|
|
625
|
+
class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
626
|
+
>
|
|
627
|
+
<IconExternalLink class="size-3.5" />
|
|
628
|
+
</NuxtLink>
|
|
629
|
+
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-md shadow-2xs" role="tooltip">Abrir</span>
|
|
547
630
|
</div>
|
|
548
631
|
|
|
549
|
-
<!--
|
|
550
|
-
<div class="
|
|
551
|
-
<
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
632
|
+
<!-- Eliminar (configurable vía :preview-deletable) -->
|
|
633
|
+
<div v-if="previewDeletable" class="hs-tooltip [--placement:top] inline-block">
|
|
634
|
+
<button
|
|
635
|
+
type="button"
|
|
636
|
+
class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-md text-muted-foreground hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
|
637
|
+
@click.stop="emit('preview-delete', previewRow)"
|
|
638
|
+
>
|
|
639
|
+
<IconTrash class="size-3.5" />
|
|
640
|
+
</button>
|
|
641
|
+
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-md shadow-2xs" role="tooltip">Eliminar</span>
|
|
556
642
|
</div>
|
|
557
643
|
|
|
558
|
-
<!--
|
|
559
|
-
<div
|
|
644
|
+
<!-- Minimizar (siempre) -->
|
|
645
|
+
<div class="hs-tooltip [--placement:top] inline-block">
|
|
560
646
|
<button
|
|
561
647
|
type="button"
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
'flex-1 py-2.5 text-xs font-semibold transition-colors border-r border-card-line border-t-2 -mt-px',
|
|
565
|
-
previewTab === 'datos'
|
|
566
|
-
? 'border-t-card text-foreground'
|
|
567
|
-
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
568
|
-
]"
|
|
648
|
+
class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
649
|
+
@click.stop="minimizePreview"
|
|
569
650
|
>
|
|
570
|
-
|
|
651
|
+
<IconMinus class="size-3.5" />
|
|
571
652
|
</button>
|
|
653
|
+
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-md shadow-2xs" role="tooltip">Minimizar</span>
|
|
654
|
+
</div>
|
|
655
|
+
|
|
656
|
+
<!-- Cerrar (siempre) -->
|
|
657
|
+
<div class="hs-tooltip [--placement:top] inline-block">
|
|
572
658
|
<button
|
|
573
659
|
type="button"
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
:class="[
|
|
577
|
-
'flex-1 py-2.5 text-xs font-semibold transition-colors border-t-2 -mt-px',
|
|
578
|
-
!resolvedHistoryEndpoint
|
|
579
|
-
? 'border-t-transparent text-muted-foreground/40 cursor-not-allowed'
|
|
580
|
-
: previewTab === 'bitacora'
|
|
581
|
-
? 'border-t-card text-foreground'
|
|
582
|
-
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
583
|
-
]"
|
|
660
|
+
class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
661
|
+
@click.stop="closePreview"
|
|
584
662
|
>
|
|
585
|
-
|
|
663
|
+
<IconX class="size-3.5" />
|
|
586
664
|
</button>
|
|
665
|
+
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-md shadow-2xs" role="tooltip">Cerrar</span>
|
|
587
666
|
</div>
|
|
588
667
|
|
|
589
668
|
</div>
|
|
590
669
|
</div>
|
|
591
|
-
</Transition>
|
|
592
670
|
|
|
593
|
-
|
|
671
|
+
<!-- Scrollable content -->
|
|
672
|
+
<div class="flex-1 overflow-y-auto min-h-0">
|
|
673
|
+
<slot v-if="previewTab === 'datos'" name="preview" :row="previewRow" :close="closePreview" />
|
|
674
|
+
<Table.PreviewTimeline
|
|
675
|
+
v-else-if="previewTab === 'bitacora' && resolvedHistoryEndpoint"
|
|
676
|
+
:endpoint="resolvedHistoryEndpoint"
|
|
677
|
+
/>
|
|
678
|
+
</div>
|
|
679
|
+
|
|
680
|
+
<!-- Tabs — bottom -->
|
|
681
|
+
<div v-if="hasHistory" class="shrink-0 flex border-t border-card-line">
|
|
682
|
+
<button
|
|
683
|
+
type="button"
|
|
684
|
+
@click="previewTab = 'datos'"
|
|
685
|
+
:class="[
|
|
686
|
+
'flex-1 py-2.5 text-xs font-semibold transition-colors border-r border-card-line border-t-2 -mt-px',
|
|
687
|
+
previewTab === 'datos'
|
|
688
|
+
? 'border-t-card text-foreground'
|
|
689
|
+
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
690
|
+
]"
|
|
691
|
+
>
|
|
692
|
+
Datos
|
|
693
|
+
</button>
|
|
694
|
+
<button
|
|
695
|
+
type="button"
|
|
696
|
+
@click="resolvedHistoryEndpoint && (previewTab = 'bitacora')"
|
|
697
|
+
:disabled="!resolvedHistoryEndpoint"
|
|
698
|
+
:class="[
|
|
699
|
+
'flex-1 py-2.5 text-xs font-semibold transition-colors border-t-2 -mt-px',
|
|
700
|
+
!resolvedHistoryEndpoint
|
|
701
|
+
? 'border-t-transparent text-muted-foreground/40 cursor-not-allowed'
|
|
702
|
+
: previewTab === 'bitacora'
|
|
703
|
+
? 'border-t-card text-foreground'
|
|
704
|
+
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
705
|
+
]"
|
|
706
|
+
>
|
|
707
|
+
Bitácora
|
|
708
|
+
</button>
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
</div>
|
|
712
|
+
</Transition>
|
|
594
713
|
|
|
595
714
|
<!-- ── Floating mini-preview (dock expand, estilo Gmail) ── -->
|
|
596
715
|
<Teleport to="body">
|
|
@@ -645,105 +764,139 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
645
764
|
<div
|
|
646
765
|
v-if="showFilterPanel"
|
|
647
766
|
ref="filterMenuRef"
|
|
648
|
-
class="fixed z-[60] bg-dropdown border border-dropdown-line rounded-xl shadow-2xl
|
|
767
|
+
class="fixed z-[60] bg-dropdown border border-dropdown-line rounded-xl shadow-2xl overflow-hidden"
|
|
649
768
|
:style="filterMenuStyle"
|
|
650
769
|
>
|
|
651
770
|
|
|
652
771
|
<!-- Step 1: column picker -->
|
|
653
772
|
<template v-if="filterMenuStep === 'columns'">
|
|
654
|
-
<p class="text-[10px] font-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
773
|
+
<p class="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest px-3 pt-3 pb-1.5">
|
|
774
|
+
Filtrar por
|
|
775
|
+
</p>
|
|
776
|
+
<div class="pb-2 min-w-48">
|
|
777
|
+
<template v-if="availableFilterColumns.length">
|
|
778
|
+
<button
|
|
779
|
+
v-for="col in availableFilterColumns"
|
|
780
|
+
:key="col.key"
|
|
781
|
+
type="button"
|
|
782
|
+
@click.stop="selectFilterColumn(col)"
|
|
783
|
+
class="w-full flex items-center justify-between gap-3 px-3 py-2 text-sm text-foreground hover:bg-muted-hover transition-colors text-left group"
|
|
784
|
+
>
|
|
785
|
+
<span>{{ col.label }}</span>
|
|
786
|
+
<span class="text-[11px] text-muted-foreground-2 group-hover:text-muted-foreground transition-colors capitalize">
|
|
787
|
+
{{ col.filterType === 'daterange' ? 'fecha' : col.filterType === 'select' ? 'opción' : 'texto' }}
|
|
788
|
+
</span>
|
|
789
|
+
</button>
|
|
790
|
+
</template>
|
|
791
|
+
<p v-else class="px-3 py-3 text-xs text-muted-foreground italic">
|
|
792
|
+
Todos los filtros están configurados
|
|
793
|
+
</p>
|
|
666
794
|
</div>
|
|
667
795
|
</template>
|
|
668
796
|
|
|
669
|
-
<!-- Step 2: value
|
|
797
|
+
<!-- Step 2: value config -->
|
|
670
798
|
<template v-else-if="filterMenuStep === 'value' && pendingCol">
|
|
671
|
-
|
|
799
|
+
|
|
800
|
+
<!-- Header: back + field name + operator selector -->
|
|
801
|
+
<div class="flex items-center gap-1.5 px-2 py-2 border-b border-dropdown-line">
|
|
672
802
|
<button
|
|
673
803
|
type="button"
|
|
674
804
|
@click.stop="filterMenuStep = 'columns'"
|
|
675
|
-
class="text-muted-foreground hover:text-foreground transition-colors"
|
|
805
|
+
class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors shrink-0"
|
|
676
806
|
>
|
|
677
807
|
<IconChevronLeft class="size-4" />
|
|
678
808
|
</button>
|
|
679
809
|
<span class="text-sm font-medium text-foreground">{{ pendingCol.label }}</span>
|
|
810
|
+
|
|
811
|
+
<!-- Operator: native select for text (3 options) -->
|
|
812
|
+
<select
|
|
813
|
+
v-if="pendingCol.filterType === 'text'"
|
|
814
|
+
v-model="pendingOperator"
|
|
815
|
+
class="ml-auto text-xs bg-dropdown border border-dropdown-line rounded-md px-2 py-1 text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/50 cursor-pointer"
|
|
816
|
+
>
|
|
817
|
+
<option v-for="op in textOps" :key="op.value" :value="op.value">{{ op.label }}</option>
|
|
818
|
+
</select>
|
|
819
|
+
|
|
820
|
+
<!-- Operator: segmented toggle for select (es / no es) -->
|
|
821
|
+
<div v-else-if="pendingCol.filterType === 'select'" class="ml-auto flex rounded-md border border-dropdown-line overflow-hidden text-xs">
|
|
822
|
+
<button
|
|
823
|
+
v-for="op in selectOps"
|
|
824
|
+
:key="op.value"
|
|
825
|
+
type="button"
|
|
826
|
+
@click.stop="pendingOperator = op.value"
|
|
827
|
+
:class="[
|
|
828
|
+
'px-2.5 py-1 transition-colors',
|
|
829
|
+
pendingOperator === op.value
|
|
830
|
+
? 'bg-primary/10 text-primary'
|
|
831
|
+
: 'text-muted-foreground hover:bg-muted-hover'
|
|
832
|
+
]"
|
|
833
|
+
>
|
|
834
|
+
{{ op.label }}
|
|
835
|
+
</button>
|
|
836
|
+
</div>
|
|
837
|
+
|
|
838
|
+
<!-- Operator: inline select for daterange -->
|
|
839
|
+
<select
|
|
840
|
+
v-else-if="pendingCol.filterType === 'daterange'"
|
|
841
|
+
v-model="pendingDateOp"
|
|
842
|
+
class="ml-auto text-xs bg-dropdown border border-dropdown-line rounded-md px-2 py-1 text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/50 cursor-pointer"
|
|
843
|
+
>
|
|
844
|
+
<option v-for="op in dateOps" :key="op.value" :value="op.value">{{ op.label }}</option>
|
|
845
|
+
</select>
|
|
680
846
|
</div>
|
|
681
|
-
<div class="p-3 space-y-2.5">
|
|
682
847
|
|
|
683
|
-
|
|
848
|
+
<div class="p-3 min-w-56 space-y-2">
|
|
849
|
+
|
|
850
|
+
<!-- ── TEXT ── -->
|
|
684
851
|
<input
|
|
685
852
|
v-if="pendingCol.filterType === 'text'"
|
|
853
|
+
ref="pendingValueInputRef"
|
|
686
854
|
v-model="pendingValue"
|
|
687
855
|
type="text"
|
|
688
|
-
autofocus
|
|
689
856
|
@keydown.enter.stop="applyPendingFilter"
|
|
690
857
|
@keydown.escape.stop="closeFilterMenu"
|
|
691
|
-
placeholder="
|
|
692
|
-
class="w-full rounded-lg border border-
|
|
858
|
+
placeholder="Valor..."
|
|
859
|
+
class="w-full rounded-lg border border-dropdown-line bg-card text-foreground py-1.5 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/60 focus:border-primary"
|
|
693
860
|
/>
|
|
694
861
|
|
|
695
|
-
<!--
|
|
696
|
-
<div v-else-if="pendingCol.filterType === 'select'" class="space-y-0.5">
|
|
862
|
+
<!-- ── SELECT ── -->
|
|
863
|
+
<div v-else-if="pendingCol.filterType === 'select'" class="space-y-0.5 max-h-52 overflow-y-auto -mx-1">
|
|
697
864
|
<button
|
|
698
865
|
v-for="opt in pendingCol.filterOptions"
|
|
699
866
|
:key="opt.value"
|
|
700
867
|
type="button"
|
|
701
868
|
@click.stop="pendingValue = opt.value; applyPendingFilter()"
|
|
702
869
|
:class="[
|
|
703
|
-
'w-full flex items-center gap-2 px-2.5 py-
|
|
870
|
+
'w-full flex items-center gap-2 px-2.5 py-2 text-sm rounded-lg transition-colors text-left',
|
|
704
871
|
pendingValue === opt.value
|
|
705
|
-
? 'bg-
|
|
872
|
+
? 'bg-primary/10 text-primary'
|
|
706
873
|
: 'hover:bg-muted-hover text-foreground'
|
|
707
874
|
]"
|
|
708
875
|
>
|
|
709
876
|
<span class="flex-1">{{ opt.label }}</span>
|
|
710
|
-
<IconCheck v-if="pendingValue === opt.value" class="size-3.5 shrink-0 text-
|
|
877
|
+
<IconCheck v-if="pendingValue === opt.value" class="size-3.5 shrink-0 text-primary/70" />
|
|
711
878
|
</button>
|
|
712
879
|
</div>
|
|
713
880
|
|
|
714
|
-
<!--
|
|
715
|
-
<
|
|
716
|
-
<div class="flex gap-1">
|
|
717
|
-
<button
|
|
718
|
-
v-for="op in dateOps"
|
|
719
|
-
:key="op.value"
|
|
720
|
-
type="button"
|
|
721
|
-
@click.stop="pendingDateOp = op.value"
|
|
722
|
-
:class="[
|
|
723
|
-
'flex-1 py-1 text-xs rounded-lg border transition-colors',
|
|
724
|
-
pendingDateOp === op.value
|
|
725
|
-
? 'border-indigo-400 bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-300'
|
|
726
|
-
: 'border-card-line text-muted-foreground hover:bg-muted-hover'
|
|
727
|
-
]"
|
|
728
|
-
>
|
|
729
|
-
{{ op.label }}
|
|
730
|
-
</button>
|
|
731
|
-
</div>
|
|
881
|
+
<!-- ── DATERANGE ── -->
|
|
882
|
+
<template v-else-if="pendingCol.filterType === 'daterange'">
|
|
732
883
|
<template v-if="pendingDateOp === 'between'">
|
|
733
|
-
<input type="date" v-model="pendingValue.from" class="w-full rounded-lg border border-
|
|
734
|
-
<input type="date" v-model="pendingValue.to"
|
|
884
|
+
<input type="date" v-model="pendingValue.from" class="w-full rounded-lg border border-dropdown-line bg-card text-foreground py-1.5 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/60" />
|
|
885
|
+
<input type="date" v-model="pendingValue.to" class="w-full rounded-lg border border-dropdown-line bg-card text-foreground py-1.5 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/60" />
|
|
735
886
|
</template>
|
|
736
|
-
<input v-else type="date" v-model="pendingValue.singleDate" class="w-full rounded-lg border border-
|
|
737
|
-
</
|
|
887
|
+
<input v-else type="date" v-model="pendingValue.singleDate" class="w-full rounded-lg border border-dropdown-line bg-card text-foreground py-1.5 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/60" />
|
|
888
|
+
</template>
|
|
738
889
|
|
|
890
|
+
<!-- Apply (not for select — auto-applies on click) -->
|
|
739
891
|
<button
|
|
740
892
|
v-if="pendingCol.filterType !== 'select'"
|
|
741
893
|
type="button"
|
|
742
894
|
@click.stop="applyPendingFilter"
|
|
743
|
-
class="w-full py-1.5 text-sm font-medium
|
|
895
|
+
class="w-full py-1.5 text-sm font-medium rounded-lg bg-primary text-white hover:bg-primary/90 active:bg-primary/80 transition-colors"
|
|
744
896
|
>
|
|
745
897
|
Aplicar
|
|
746
898
|
</button>
|
|
899
|
+
|
|
747
900
|
</div>
|
|
748
901
|
</template>
|
|
749
902
|
|
package/components/Table.vue
CHANGED
|
@@ -10,28 +10,27 @@ import {
|
|
|
10
10
|
|
|
11
11
|
const props = defineProps({
|
|
12
12
|
endpoint: { type: String, required: true },
|
|
13
|
-
columns: { type: Array, required: true },
|
|
13
|
+
columns: { type: Array, required: true },
|
|
14
14
|
params: { type: Object, default: () => ({}) },
|
|
15
15
|
checkable: { type: Boolean, default: false },
|
|
16
16
|
search: { type: String, default: '' },
|
|
17
17
|
name: { type: String, required: true },
|
|
18
18
|
cached: { type: Boolean, default: false },
|
|
19
19
|
showReloadButton: { type: Boolean, default: true },
|
|
20
|
-
viewMode: { type: String, default: 'table' },
|
|
20
|
+
viewMode: { type: String, default: 'table' },
|
|
21
21
|
gridClass: { type: String, default: 'grid grid-cols-2 lg:grid-cols-3 gap-4' },
|
|
22
22
|
clickRowToOpen: { type: Boolean, default: false },
|
|
23
23
|
previewRowId: { type: [String, Number], default: null },
|
|
24
24
|
previewMode: { type: Boolean, default: false },
|
|
25
|
+
showPerPage: { type: Boolean, default: false },
|
|
25
26
|
})
|
|
26
27
|
|
|
27
28
|
const emit = defineEmits(['update:search', 'row-click', 'loaded', 'page-change', 'per-page-change'])
|
|
28
29
|
const instance = getCurrentInstance()
|
|
29
30
|
|
|
30
|
-
// ─── API / toast ─────────────────────────────────────────────────────────────
|
|
31
31
|
const api = useApi()
|
|
32
32
|
const toast = useToast()
|
|
33
33
|
|
|
34
|
-
// ─── Local data ───────────────────────────────────────────────────────────────
|
|
35
34
|
const tableData = ref([])
|
|
36
35
|
const rowCount = ref(0)
|
|
37
36
|
const loading = ref(false)
|
|
@@ -46,7 +45,6 @@ const skeletonRows = computed(() => {
|
|
|
46
45
|
})
|
|
47
46
|
const isGridView = computed(() => props.viewMode === 'grid')
|
|
48
47
|
|
|
49
|
-
// ─── TanStack state ───────────────────────────────────────────────────────────
|
|
50
48
|
const pagination = ref({ pageIndex: 0, pageSize: 10 })
|
|
51
49
|
const sorting = ref([])
|
|
52
50
|
const columnFilters = ref([])
|
|
@@ -61,7 +59,6 @@ const makeUpdater = (stateRef) => (updater) => {
|
|
|
61
59
|
stateRef.value = typeof updater === 'function' ? updater(stateRef.value) : updater
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
// ─── Column definitions ───────────────────────────────────────────────────────
|
|
65
62
|
const buildColumnDefs = () => {
|
|
66
63
|
const defs = []
|
|
67
64
|
if (props.checkable) {
|
|
@@ -97,7 +94,6 @@ const columnDefs = buildColumnDefs()
|
|
|
97
94
|
|
|
98
95
|
const hasFilterableColumns = computed(() => props.columns.some(c => c.filterable))
|
|
99
96
|
|
|
100
|
-
// ─── TanStack table instance ──────────────────────────────────────────────────
|
|
101
97
|
const table = useVueTable({
|
|
102
98
|
get data() { return tableData.value },
|
|
103
99
|
get rowCount() { return rowCount.value },
|
|
@@ -131,7 +127,6 @@ const table = useVueTable({
|
|
|
131
127
|
enableRowSelection: true,
|
|
132
128
|
})
|
|
133
129
|
|
|
134
|
-
// ─── Fetch ────────────────────────────────────────────────────────────────────
|
|
135
130
|
const buildRequestParams = () => {
|
|
136
131
|
const { sort, ...otherParams } = props.params
|
|
137
132
|
return {
|
|
@@ -170,14 +165,12 @@ const fetchData = async () => {
|
|
|
170
165
|
}
|
|
171
166
|
}
|
|
172
167
|
|
|
173
|
-
// ─── Scheduled fetch (deduplicates concurrent state changes) ─────────────────
|
|
174
168
|
let fetchTimeout = null
|
|
175
169
|
const scheduleFetch = (delay = 0) => {
|
|
176
170
|
if (fetchTimeout) clearTimeout(fetchTimeout)
|
|
177
171
|
fetchTimeout = setTimeout(() => fetchData(), delay)
|
|
178
172
|
}
|
|
179
173
|
|
|
180
|
-
// ─── Cache ────────────────────────────────────────────────────────────────────
|
|
181
174
|
const cacheKey = computed(() => {
|
|
182
175
|
if (!props.cached || !props.name) return null
|
|
183
176
|
const base = `full_table_${props.name}`
|
|
@@ -223,7 +216,6 @@ const clearCache = () => {
|
|
|
223
216
|
if (cacheKey.value) sessionStorage.removeItem(cacheKey.value)
|
|
224
217
|
}
|
|
225
218
|
|
|
226
|
-
// ─── Restore guard (prevents watchers from triggering fetch during restore) ───
|
|
227
219
|
const isRestoring = ref(false)
|
|
228
220
|
|
|
229
221
|
const loadFromCacheOnMount = async () => {
|
|
@@ -252,7 +244,6 @@ const loadFromCacheOnMount = async () => {
|
|
|
252
244
|
return true
|
|
253
245
|
}
|
|
254
246
|
|
|
255
|
-
// ─── Watchers ─────────────────────────────────────────────────────────────────
|
|
256
247
|
watch(tableData, (newData) => {
|
|
257
248
|
if (newData.length > 0 && tableBodyRef.value) {
|
|
258
249
|
const firstDataRow = Array.from(tableBodyRef.value.children).find(el => el.dataset.rowType === 'data')
|
|
@@ -287,7 +278,6 @@ watch(() => props.params, () => {
|
|
|
287
278
|
scheduleFetch(0)
|
|
288
279
|
}, { deep: true })
|
|
289
280
|
|
|
290
|
-
// ─── Lifecycle ────────────────────────────────────────────────────────────────
|
|
291
281
|
const initColumnOrder = () => {
|
|
292
282
|
const ids = props.checkable ? ['select'] : []
|
|
293
283
|
for (const col of props.columns) ids.push(col.key)
|
|
@@ -309,15 +299,12 @@ onBeforeUnmount(() => {
|
|
|
309
299
|
if (props.cached && tableData.value.length > 0) saveToCache()
|
|
310
300
|
})
|
|
311
301
|
|
|
312
|
-
// ─── Column settings panel ────────────────────────────────────────────────────
|
|
313
302
|
const setColumnOrder = (order) => { columnOrder.value = order }
|
|
314
303
|
|
|
315
|
-
// ─── Header drag reorder ──────────────────────────────────────────────────────
|
|
316
304
|
let draggedHeaderId = null
|
|
317
305
|
const dragOverHeaderId = ref(null)
|
|
318
306
|
const resizeHoverId = ref(null)
|
|
319
307
|
|
|
320
|
-
// ─── Column auto-size on double click ─────────────────────────────────────────
|
|
321
308
|
const _canvas = typeof document !== 'undefined' ? document.createElement('canvas') : null
|
|
322
309
|
const _ctx = _canvas?.getContext('2d')
|
|
323
310
|
|
|
@@ -356,7 +343,6 @@ const onHeaderDrop = (colId) => {
|
|
|
356
343
|
if (from < 0 || to < 0) return
|
|
357
344
|
order.splice(from, 1)
|
|
358
345
|
order.splice(to, 0, draggedHeaderId)
|
|
359
|
-
// keep 'select' pinned first
|
|
360
346
|
const selIdx = order.indexOf('select')
|
|
361
347
|
if (selIdx > 0) { order.splice(selIdx, 1); order.unshift('select') }
|
|
362
348
|
columnOrder.value = order
|
|
@@ -364,7 +350,6 @@ const onHeaderDrop = (colId) => {
|
|
|
364
350
|
dragOverHeaderId.value = null
|
|
365
351
|
}
|
|
366
352
|
|
|
367
|
-
// ─── Row selection ────────────────────────────────────────────────────────────
|
|
368
353
|
const getSelectedRows = () => {
|
|
369
354
|
const selected = table.getSelectedRowModel().rows.map(r => r.original)
|
|
370
355
|
return table.getIsAllRowsSelected()
|
|
@@ -372,7 +357,6 @@ const getSelectedRows = () => {
|
|
|
372
357
|
: { meta: { all: false }, rows: selected }
|
|
373
358
|
}
|
|
374
359
|
|
|
375
|
-
// ─── Export ───────────────────────────────────────────────────────────────────
|
|
376
360
|
const exportTable = async (format, exportAllPages, exportFilteredRows, selectedIds = null) => {
|
|
377
361
|
const { download } = useDownload()
|
|
378
362
|
const id = crypto.randomUUID()
|
|
@@ -417,7 +401,6 @@ const exportTable = async (format, exportAllPages, exportFilteredRows, selectedI
|
|
|
417
401
|
}
|
|
418
402
|
}
|
|
419
403
|
|
|
420
|
-
// ─── Per-page ─────────────────────────────────────────────────────────────────
|
|
421
404
|
const handlePerPageChange = (val) => {
|
|
422
405
|
if (val === 'custom') { isCustomPerPage.value = true; return }
|
|
423
406
|
table.setPageSize(parseInt(val))
|
|
@@ -428,7 +411,6 @@ const resetPerPage = () => {
|
|
|
428
411
|
if (![10, 25, 50, 100].includes(pagination.value.pageSize)) table.setPageSize(10)
|
|
429
412
|
}
|
|
430
413
|
|
|
431
|
-
// ─── Row click ────────────────────────────────────────────────────────────────
|
|
432
414
|
const hasRowClickListener = computed(() => !!instance?.vnode?.props?.onRowClick)
|
|
433
415
|
const isRowClickEnabled = computed(() => props.clickRowToOpen || props.previewMode || hasRowClickListener.value)
|
|
434
416
|
|
|
@@ -457,7 +439,6 @@ const handleRowKeydown = (row, e) => {
|
|
|
457
439
|
emit('row-click', row.original, e)
|
|
458
440
|
}
|
|
459
441
|
|
|
460
|
-
// ─── Expose ───────────────────────────────────────────────────────────────────
|
|
461
442
|
const reloadTable = () => {
|
|
462
443
|
clearCache()
|
|
463
444
|
isDataFromCache.value = false
|
|
@@ -494,12 +475,9 @@ defineExpose({
|
|
|
494
475
|
:style="{ width: col.getSize() + 'px' }"
|
|
495
476
|
>
|
|
496
477
|
</colgroup>
|
|
497
|
-
<thead class="relative z-20 bg-
|
|
478
|
+
<thead class="relative z-20 bg-background">
|
|
498
479
|
<template v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
|
499
|
-
|
|
500
|
-
<tr
|
|
501
|
-
class="divide-x divide-card-line"
|
|
502
|
-
>
|
|
480
|
+
<tr>
|
|
503
481
|
<th
|
|
504
482
|
v-for="header in headerGroup.headers"
|
|
505
483
|
:key="header.id"
|
|
@@ -512,22 +490,20 @@ defineExpose({
|
|
|
512
490
|
class="relative overflow-hidden"
|
|
513
491
|
:class="[
|
|
514
492
|
header.id === 'select' ? 'text-center' : '',
|
|
515
|
-
dragOverHeaderId === header.id ? 'bg-
|
|
493
|
+
dragOverHeaderId === header.id ? 'bg-primary/5 dark:bg-primary/10' : '',
|
|
516
494
|
header.column.getCanSort() ? 'cursor-pointer select-none' : '',
|
|
517
495
|
]"
|
|
518
496
|
@click="header.column.getCanSort() && header.column.toggleSorting()"
|
|
519
497
|
>
|
|
520
|
-
<!-- Select all checkbox -->
|
|
521
498
|
<template v-if="header.id === 'select'">
|
|
522
499
|
<input
|
|
523
500
|
type="checkbox"
|
|
524
501
|
:checked="table.getIsAllRowsSelected()"
|
|
525
502
|
:indeterminate="table.getIsSomeRowsSelected()"
|
|
526
503
|
@change="table.getToggleAllRowsSelectedHandler()($event)"
|
|
527
|
-
class="mx-2 shrink-0 border-card-line rounded-sm text-
|
|
504
|
+
class="mx-2 shrink-0 border-card-line rounded-sm text-primary focus:ring-0 focus:ring-offset-0 dark:bg-card"
|
|
528
505
|
/>
|
|
529
506
|
</template>
|
|
530
|
-
<!-- Regular column header -->
|
|
531
507
|
<template v-else>
|
|
532
508
|
<div class="px-4 py-3 flex items-center gap-x-1 text-xs font-medium text-muted-foreground w-full">
|
|
533
509
|
{{ header.column.columnDef.meta?.label ?? header.id }}
|
|
@@ -537,7 +513,6 @@ defineExpose({
|
|
|
537
513
|
<IconChevronUp v-else class="size-4" />
|
|
538
514
|
</span>
|
|
539
515
|
</div>
|
|
540
|
-
<!-- Resize handle -->
|
|
541
516
|
<div
|
|
542
517
|
v-if="header.column.getCanResize()"
|
|
543
518
|
class="absolute right-0 top-0 h-full w-3 cursor-col-resize group/rz flex items-center justify-center select-none touch-none"
|
|
@@ -552,18 +527,17 @@ defineExpose({
|
|
|
552
527
|
<div
|
|
553
528
|
class="h-4 w-px transition-all"
|
|
554
529
|
:class="header.column.getIsResizing()
|
|
555
|
-
? 'bg-
|
|
556
|
-
: 'bg-surface-1 group-hover/rz:bg-
|
|
530
|
+
? 'bg-primary/60 !w-0.5'
|
|
531
|
+
: 'bg-surface-1 group-hover/rz:bg-primary/40 dark:group-hover/rz:bg-primary group-hover/rz:w-0.5'"
|
|
557
532
|
/>
|
|
558
533
|
</div>
|
|
559
534
|
</template>
|
|
560
535
|
</th>
|
|
561
536
|
</tr>
|
|
562
537
|
|
|
563
|
-
<!-- Column filter row -->
|
|
564
538
|
<tr
|
|
565
539
|
v-if="hasFilterableColumns"
|
|
566
|
-
class="
|
|
540
|
+
class="border-b border-card-line bg-muted/50"
|
|
567
541
|
>
|
|
568
542
|
<th
|
|
569
543
|
v-for="header in headerGroup.headers"
|
|
@@ -575,7 +549,7 @@ defineExpose({
|
|
|
575
549
|
:value="header.column.getFilterValue() ?? ''"
|
|
576
550
|
@input="(e) => header.column.setFilterValue(e.target.value || undefined)"
|
|
577
551
|
:placeholder="`Filtrar ${header.column.columnDef.meta?.label ?? ''}...`"
|
|
578
|
-
class="w-full bg-card border border-card-line rounded-lg text-xs text-muted-foreground-1 px-2.5 py-1 focus:ring-2 focus:ring-
|
|
552
|
+
class="w-full bg-card border border-card-line rounded-lg text-xs text-muted-foreground-1 px-2.5 py-1 focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all"
|
|
579
553
|
/>
|
|
580
554
|
</th>
|
|
581
555
|
</tr>
|
|
@@ -588,7 +562,7 @@ defineExpose({
|
|
|
588
562
|
v-if="loading"
|
|
589
563
|
v-for="(_, i) in skeletonRows"
|
|
590
564
|
:key="'sk-' + i"
|
|
591
|
-
class="animate-pulse
|
|
565
|
+
class="animate-pulse bg-background"
|
|
592
566
|
>
|
|
593
567
|
<td
|
|
594
568
|
v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
|
|
@@ -601,12 +575,12 @@ defineExpose({
|
|
|
601
575
|
</td>
|
|
602
576
|
</tr>
|
|
603
577
|
|
|
604
|
-
<!-- Loading filler rows
|
|
578
|
+
<!-- Loading filler rows -->
|
|
605
579
|
<tr
|
|
606
580
|
v-if="loading && skeletonRows.length < pagination.pageSize"
|
|
607
581
|
v-for="i in (pagination.pageSize - skeletonRows.length)"
|
|
608
582
|
:key="'lf-' + i"
|
|
609
|
-
class="
|
|
583
|
+
class="bg-background"
|
|
610
584
|
>
|
|
611
585
|
<td
|
|
612
586
|
v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
|
|
@@ -615,12 +589,12 @@ defineExpose({
|
|
|
615
589
|
/>
|
|
616
590
|
</tr>
|
|
617
591
|
|
|
618
|
-
<!-- Empty filler rows
|
|
592
|
+
<!-- Empty filler rows -->
|
|
619
593
|
<tr
|
|
620
594
|
v-if="!loading && tableData.length === 0"
|
|
621
595
|
v-for="i in pagination.pageSize"
|
|
622
596
|
:key="'esk-' + i"
|
|
623
|
-
class="
|
|
597
|
+
class="bg-background"
|
|
624
598
|
>
|
|
625
599
|
<td
|
|
626
600
|
v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
|
|
@@ -638,11 +612,11 @@ defineExpose({
|
|
|
638
612
|
@click="(e) => handleRowClick(row, e)"
|
|
639
613
|
@keydown="(e) => handleRowKeydown(row, e)"
|
|
640
614
|
:tabindex="isRowClickEnabled ? 0 : undefined"
|
|
641
|
-
class="
|
|
615
|
+
class="bg-background hover:bg-layer-hover transition-colors"
|
|
642
616
|
:class="{
|
|
643
617
|
'cursor-pointer': isRowClickEnabled,
|
|
644
|
-
'bg-
|
|
645
|
-
'!bg-
|
|
618
|
+
'bg-primary/5 dark:bg-primary/10 hover:bg-primary/10': row.getIsSelected(),
|
|
619
|
+
'!bg-primary/10 dark:!bg-primary/15 ring-1 ring-inset ring-primary/30 dark:ring-primary/40': previewRowId && row.original.id === previewRowId,
|
|
646
620
|
}"
|
|
647
621
|
>
|
|
648
622
|
<td
|
|
@@ -656,7 +630,6 @@ defineExpose({
|
|
|
656
630
|
cell.column.id !== 'select' ? cell.column.columnDef.meta?.class ?? '' : '',
|
|
657
631
|
]"
|
|
658
632
|
>
|
|
659
|
-
<!-- Select checkbox -->
|
|
660
633
|
<template v-if="cell.column.id === 'select'">
|
|
661
634
|
<div @click.stop>
|
|
662
635
|
<input
|
|
@@ -668,7 +641,6 @@ defineExpose({
|
|
|
668
641
|
/>
|
|
669
642
|
</div>
|
|
670
643
|
</template>
|
|
671
|
-
<!-- Data cell with slot -->
|
|
672
644
|
<template v-else>
|
|
673
645
|
<slot :name="cell.column.id" :row="row.original" :value="cell.getValue()">
|
|
674
646
|
{{ cell.getValue() }}
|
|
@@ -677,12 +649,12 @@ defineExpose({
|
|
|
677
649
|
</td>
|
|
678
650
|
</tr>
|
|
679
651
|
|
|
680
|
-
<!-- Filler rows
|
|
652
|
+
<!-- Filler rows -->
|
|
681
653
|
<tr
|
|
682
654
|
v-if="!loading && tableData.length > 0 && tableData.length < pagination.pageSize"
|
|
683
655
|
v-for="i in (pagination.pageSize - tableData.length)"
|
|
684
656
|
:key="'fill-' + i"
|
|
685
|
-
class="
|
|
657
|
+
class="bg-background"
|
|
686
658
|
>
|
|
687
659
|
<td
|
|
688
660
|
v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
|
|
@@ -693,10 +665,9 @@ defineExpose({
|
|
|
693
665
|
</tbody>
|
|
694
666
|
</table>
|
|
695
667
|
|
|
696
|
-
<!-- Empty state overlays -->
|
|
697
668
|
<div
|
|
698
669
|
v-if="!loading && tableData.length === 0 && !search && !columnFilters.length"
|
|
699
|
-
class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-
|
|
670
|
+
class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-background/60 rounded-xl"
|
|
700
671
|
>
|
|
701
672
|
<slot name="empty">
|
|
702
673
|
<p class="text-muted-foreground text-lg font-medium italic">No hay registros</p>
|
|
@@ -705,7 +676,7 @@ defineExpose({
|
|
|
705
676
|
|
|
706
677
|
<div
|
|
707
678
|
v-if="!loading && tableData.length === 0 && (search || columnFilters.length)"
|
|
708
|
-
class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-
|
|
679
|
+
class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-background/60 rounded-xl"
|
|
709
680
|
>
|
|
710
681
|
<slot name="empty-search">
|
|
711
682
|
<p class="text-muted-foreground text-lg font-medium italic">No hay registros en la búsqueda</p>
|
|
@@ -741,7 +712,7 @@ defineExpose({
|
|
|
741
712
|
:toggle-row="() => row.toggleSelected()"
|
|
742
713
|
>
|
|
743
714
|
<div class="bg-card rounded-lg border border-card-line p-4 hover:shadow-md transition-shadow relative"
|
|
744
|
-
:class="{ 'ring-2 ring-
|
|
715
|
+
:class="{ 'ring-2 ring-primary/60': row.getIsSelected() }">
|
|
745
716
|
<div v-if="checkable" class="absolute top-2 left-2 z-10">
|
|
746
717
|
<input type="checkbox" :checked="row.getIsSelected()" @change="row.toggleSelected()"
|
|
747
718
|
class="rounded border-card-line dark:bg-card" />
|
|
@@ -768,11 +739,9 @@ defineExpose({
|
|
|
768
739
|
</div>
|
|
769
740
|
</div>
|
|
770
741
|
|
|
771
|
-
<!-- Pagination
|
|
742
|
+
<!-- Pagination bar -->
|
|
772
743
|
<div ref="paginationBarRef" class="flex flex-col sm:flex-row items-center justify-between gap-y-4 sm:gap-y-0 px-4 py-3 border-t border-card-line">
|
|
773
|
-
<!-- Left: reload, total, cache, columns button -->
|
|
774
744
|
<div class="flex items-center gap-x-4 flex-wrap gap-y-2">
|
|
775
|
-
<!-- Reload button -->
|
|
776
745
|
<div v-if="showReloadButton" class="flex items-center gap-x-2">
|
|
777
746
|
<IconReload
|
|
778
747
|
v-if="!loading"
|
|
@@ -787,10 +756,8 @@ defineExpose({
|
|
|
787
756
|
</div>
|
|
788
757
|
</div>
|
|
789
758
|
|
|
790
|
-
<!-- Total records -->
|
|
791
759
|
<p class="text-sm text-foreground font-medium">{{ rowCount }} registros</p>
|
|
792
760
|
|
|
793
|
-
<!-- Cache badge -->
|
|
794
761
|
<div v-if="isDataFromCache && cached" class="group relative flex items-center">
|
|
795
762
|
<div class="flex items-center gap-x-1.5 py-1 px-2.5 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 rounded-lg cursor-help hover:bg-emerald-500/20 transition-colors">
|
|
796
763
|
<IconBolt class="size-3.5 fill-current" />
|
|
@@ -804,13 +771,10 @@ defineExpose({
|
|
|
804
771
|
<div class="absolute top-full left-4 -mt-1 border-4 border-transparent border-t-slate-900"></div>
|
|
805
772
|
</div>
|
|
806
773
|
</div>
|
|
807
|
-
|
|
808
774
|
</div>
|
|
809
775
|
|
|
810
|
-
<!-- Right: per-page + pagination -->
|
|
811
776
|
<div class="flex items-center gap-x-8">
|
|
812
|
-
|
|
813
|
-
<div class="flex items-center gap-x-2">
|
|
777
|
+
<div v-if="showPerPage" class="flex items-center gap-x-2">
|
|
814
778
|
<label class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest">Filas:</label>
|
|
815
779
|
<select
|
|
816
780
|
v-if="!isCustomPerPage"
|
|
@@ -830,13 +794,12 @@ defineExpose({
|
|
|
830
794
|
:value="pagination.pageSize"
|
|
831
795
|
@change="(e) => table.setPageSize(parseInt(e.target.value) || 10)"
|
|
832
796
|
min="1" max="500"
|
|
833
|
-
class="w-14 bg-surface border-none text-[11px] font-bold text-muted-foreground-1 rounded-lg focus:ring-2 focus:ring-
|
|
797
|
+
class="w-14 bg-surface border-none text-[11px] font-bold text-muted-foreground-1 rounded-lg focus:ring-2 focus:ring-primary/20 py-1 px-2"
|
|
834
798
|
/>
|
|
835
|
-
<button @click="resetPerPage" class="text-[10px] text-
|
|
799
|
+
<button @click="resetPerPage" class="text-[10px] text-primary font-bold hover:underline">Volver</button>
|
|
836
800
|
</div>
|
|
837
801
|
</div>
|
|
838
802
|
|
|
839
|
-
<!-- Pagination nav -->
|
|
840
803
|
<nav class="flex justify-end items-center gap-x-1" aria-label="Pagination">
|
|
841
804
|
<button
|
|
842
805
|
type="button"
|
|
@@ -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
|