@innertia-solutions/nuxt-theme-spark 0.1.141 → 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 +272 -153
- 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
|
})
|
|
@@ -87,8 +107,9 @@ const activeFilterCount = computed(() => activeFilterList.value.length)
|
|
|
87
107
|
const availableFilterColumns = computed(() =>
|
|
88
108
|
filtersConfig.value.filter(col => {
|
|
89
109
|
const v = activeFilters.value[col.key]
|
|
90
|
-
if (
|
|
91
|
-
|
|
110
|
+
if (!v) return true
|
|
111
|
+
if (col.filterType === 'daterange') return !v.from && !v.to
|
|
112
|
+
return !v.value
|
|
92
113
|
})
|
|
93
114
|
)
|
|
94
115
|
|
|
@@ -97,15 +118,14 @@ const enrichedFilters = computed(() => {
|
|
|
97
118
|
const result = []
|
|
98
119
|
for (const col of filtersConfig.value) {
|
|
99
120
|
const v = activeFilters.value[col.key]
|
|
100
|
-
if (v
|
|
121
|
+
if (!v) continue
|
|
101
122
|
if (col.filterType === 'daterange') {
|
|
102
|
-
if (!v
|
|
123
|
+
if (!v.from && !v.to) continue
|
|
103
124
|
if (v.from) result.push({ field: col.key, operator: 'after', value: v.from })
|
|
104
125
|
if (v.to) result.push({ field: col.key, operator: 'before', value: v.to })
|
|
105
|
-
} else if (col.filterType === 'select') {
|
|
106
|
-
result.push({ field: col.key, operator: 'is', value: v })
|
|
107
126
|
} else {
|
|
108
|
-
|
|
127
|
+
if (!v.value) continue
|
|
128
|
+
result.push({ field: col.key, operator: v.operator ?? 'contains', value: v.value })
|
|
109
129
|
}
|
|
110
130
|
}
|
|
111
131
|
return result
|
|
@@ -120,13 +140,17 @@ const removeFilter = (key) => {
|
|
|
120
140
|
const u = { ...activeFilters.value }; delete u[key]; activeFilters.value = u
|
|
121
141
|
}
|
|
122
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
|
+
|
|
123
148
|
const openFilterMenu = async () => {
|
|
124
149
|
filterMenuStep.value = 'columns'
|
|
125
150
|
pendingCol.value = null
|
|
126
151
|
showFilterPanel.value = true
|
|
127
152
|
await nextTick()
|
|
128
|
-
|
|
129
|
-
if (rect) filterMenuStyle.value = { top: rect.bottom + 4 + 'px', left: rect.left + 'px' }
|
|
153
|
+
updateFilterMenuPosition()
|
|
130
154
|
}
|
|
131
155
|
|
|
132
156
|
const toggleFilterMenu = async () => {
|
|
@@ -149,7 +173,8 @@ const selectFilterColumn = (col) => {
|
|
|
149
173
|
else if (existing?.to) { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: existing.to, from: '', to: '' } }
|
|
150
174
|
else { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: '', from: '', to: '' } }
|
|
151
175
|
} else {
|
|
152
|
-
|
|
176
|
+
pendingOperator.value = existing?.operator ?? (col.filterType === 'select' ? 'is' : 'contains')
|
|
177
|
+
pendingValue.value = existing?.value ?? ''
|
|
153
178
|
}
|
|
154
179
|
filterMenuStep.value = 'value'
|
|
155
180
|
}
|
|
@@ -163,9 +188,9 @@ const applyPendingFilter = () => {
|
|
|
163
188
|
else if (pendingDateOp.value === 'after') v = { from: pendingValue.value.singleDate }
|
|
164
189
|
else v = { to: pendingValue.value.singleDate }
|
|
165
190
|
} else {
|
|
166
|
-
v = pendingValue.value
|
|
191
|
+
v = { value: pendingValue.value, operator: pendingOperator.value }
|
|
167
192
|
}
|
|
168
|
-
activeFilters.value = { ...activeFilters.value, [col.key]: v
|
|
193
|
+
activeFilters.value = { ...activeFilters.value, [col.key]: v }
|
|
169
194
|
closeFilterMenu()
|
|
170
195
|
}
|
|
171
196
|
|
|
@@ -184,8 +209,30 @@ const onFilterMenuOutsideClick = (e) => {
|
|
|
184
209
|
}
|
|
185
210
|
}
|
|
186
211
|
watch(showFilterPanel, v => {
|
|
187
|
-
if (v)
|
|
188
|
-
|
|
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
|
|
189
236
|
})
|
|
190
237
|
|
|
191
238
|
// ─── Preview panel ─────────────────────────────────────────────────────────────
|
|
@@ -210,12 +257,19 @@ const resolvedHistoryEndpoint = computed(() => {
|
|
|
210
257
|
return `history/${tableMeta.value.entity_type}/${previewRow.value.id}`
|
|
211
258
|
})
|
|
212
259
|
|
|
213
|
-
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
|
+
})
|
|
214
267
|
|
|
215
268
|
const handleRowClick = (row) => {
|
|
216
269
|
if (previewEnabled.value) {
|
|
270
|
+
if (previewRow.value?.id === row.id) return // ya está abierto, no pestañear
|
|
217
271
|
collapseDock()
|
|
218
|
-
previewRow.value =
|
|
272
|
+
previewRow.value = row
|
|
219
273
|
} else {
|
|
220
274
|
emit('row-click', row)
|
|
221
275
|
}
|
|
@@ -397,19 +451,22 @@ const onColumnPanelOutsideClick = (e) => {
|
|
|
397
451
|
}
|
|
398
452
|
}
|
|
399
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
|
+
|
|
400
459
|
watch(showColumnPanel, async (v) => {
|
|
401
460
|
if (v) {
|
|
402
461
|
await nextTick()
|
|
403
|
-
|
|
404
|
-
if (rect) {
|
|
405
|
-
columnPanelStyle.value = {
|
|
406
|
-
top: rect.bottom + 6 + 'px',
|
|
407
|
-
right: window.innerWidth - rect.right + 'px',
|
|
408
|
-
}
|
|
409
|
-
}
|
|
462
|
+
updateColumnPanelPosition()
|
|
410
463
|
document.addEventListener('mousedown', onColumnPanelOutsideClick)
|
|
464
|
+
window.addEventListener('scroll', updateColumnPanelPosition, true)
|
|
465
|
+
window.addEventListener('resize', updateColumnPanelPosition)
|
|
411
466
|
} else {
|
|
412
467
|
document.removeEventListener('mousedown', onColumnPanelOutsideClick)
|
|
468
|
+
window.removeEventListener('scroll', updateColumnPanelPosition, true)
|
|
469
|
+
window.removeEventListener('resize', updateColumnPanelPosition)
|
|
413
470
|
}
|
|
414
471
|
})
|
|
415
472
|
|
|
@@ -441,7 +498,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
441
498
|
:class="[
|
|
442
499
|
'inline-flex items-center gap-1.5 py-1.5 px-3 text-sm font-medium rounded-lg border transition-colors',
|
|
443
500
|
activeFilterList.length
|
|
444
|
-
? 'border-
|
|
501
|
+
? 'border-primary/40 bg-primary/10 text-primary'
|
|
445
502
|
: 'border-card-line bg-card text-muted-foreground-1 hover:bg-muted-hover'
|
|
446
503
|
]"
|
|
447
504
|
>
|
|
@@ -463,7 +520,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
463
520
|
:class="[
|
|
464
521
|
'p-1.5 inline-flex items-center justify-center rounded-lg border transition-colors',
|
|
465
522
|
showColumnPanel
|
|
466
|
-
? 'border-
|
|
523
|
+
? 'border-primary/40 bg-primary/10 text-primary'
|
|
467
524
|
: 'border-transparent text-muted-foreground hover:border-card-line hover:bg-muted-hover hover:text-foreground'
|
|
468
525
|
]"
|
|
469
526
|
>
|
|
@@ -486,7 +543,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
486
543
|
<button
|
|
487
544
|
type="button"
|
|
488
545
|
@click.stop="openEditFilter(chip.col)"
|
|
489
|
-
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"
|
|
490
547
|
>
|
|
491
548
|
{{ chip.displayVal }}
|
|
492
549
|
<IconChevronDown class="size-3 opacity-60" />
|
|
@@ -501,10 +558,8 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
501
558
|
</div>
|
|
502
559
|
</div>
|
|
503
560
|
|
|
504
|
-
<!--
|
|
505
|
-
<div class="
|
|
506
|
-
|
|
507
|
-
<!-- Tabla -->
|
|
561
|
+
<!-- Tabla -->
|
|
562
|
+
<div class="overflow-hidden border-t border-b border-card-line">
|
|
508
563
|
<Table
|
|
509
564
|
ref="tableRef"
|
|
510
565
|
:endpoint="resolvedEndpoint"
|
|
@@ -527,100 +582,134 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
527
582
|
<slot :name="name" v-bind="slotProps ?? {}" />
|
|
528
583
|
</template>
|
|
529
584
|
</Table>
|
|
585
|
+
</div>
|
|
530
586
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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) + '%' }"
|
|
539
601
|
>
|
|
602
|
+
<!-- Resize handle — thin pill on left edge -->
|
|
540
603
|
<div
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
<!-- Preview -->
|
|
552
|
-
<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>
|
|
553
614
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
type="button"
|
|
570
|
-
class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
571
|
-
title="Cerrar"
|
|
572
|
-
@click.stop="closePreview"
|
|
573
|
-
>
|
|
574
|
-
<IconX class="size-3.5" />
|
|
575
|
-
</button>
|
|
576
|
-
</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>
|
|
577
630
|
</div>
|
|
578
631
|
|
|
579
|
-
<!--
|
|
580
|
-
<div class="
|
|
581
|
-
<
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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>
|
|
586
642
|
</div>
|
|
587
643
|
|
|
588
|
-
<!--
|
|
589
|
-
<div
|
|
644
|
+
<!-- Minimizar (siempre) -->
|
|
645
|
+
<div class="hs-tooltip [--placement:top] inline-block">
|
|
590
646
|
<button
|
|
591
647
|
type="button"
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
'flex-1 py-2.5 text-xs font-semibold transition-colors border-r border-card-line border-t-2 -mt-px',
|
|
595
|
-
previewTab === 'datos'
|
|
596
|
-
? 'border-t-card text-foreground'
|
|
597
|
-
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
598
|
-
]"
|
|
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"
|
|
599
650
|
>
|
|
600
|
-
|
|
651
|
+
<IconMinus class="size-3.5" />
|
|
601
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">
|
|
602
658
|
<button
|
|
603
659
|
type="button"
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
:class="[
|
|
607
|
-
'flex-1 py-2.5 text-xs font-semibold transition-colors border-t-2 -mt-px',
|
|
608
|
-
!resolvedHistoryEndpoint
|
|
609
|
-
? 'border-t-transparent text-muted-foreground/40 cursor-not-allowed'
|
|
610
|
-
: previewTab === 'bitacora'
|
|
611
|
-
? 'border-t-card text-foreground'
|
|
612
|
-
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
613
|
-
]"
|
|
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"
|
|
614
662
|
>
|
|
615
|
-
|
|
663
|
+
<IconX class="size-3.5" />
|
|
616
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>
|
|
617
666
|
</div>
|
|
618
667
|
|
|
619
668
|
</div>
|
|
620
669
|
</div>
|
|
621
|
-
</Transition>
|
|
622
670
|
|
|
623
|
-
|
|
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>
|
|
624
713
|
|
|
625
714
|
<!-- ── Floating mini-preview (dock expand, estilo Gmail) ── -->
|
|
626
715
|
<Teleport to="body">
|
|
@@ -675,23 +764,28 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
675
764
|
<div
|
|
676
765
|
v-if="showFilterPanel"
|
|
677
766
|
ref="filterMenuRef"
|
|
678
|
-
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"
|
|
679
768
|
:style="filterMenuStyle"
|
|
680
769
|
>
|
|
681
770
|
|
|
682
|
-
<!-- Step 1: column picker
|
|
771
|
+
<!-- Step 1: column picker -->
|
|
683
772
|
<template v-if="filterMenuStep === 'columns'">
|
|
684
|
-
<p class="text-[10px] font-
|
|
685
|
-
|
|
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">
|
|
686
777
|
<template v-if="availableFilterColumns.length">
|
|
687
778
|
<button
|
|
688
779
|
v-for="col in availableFilterColumns"
|
|
689
780
|
:key="col.key"
|
|
690
781
|
type="button"
|
|
691
782
|
@click.stop="selectFilterColumn(col)"
|
|
692
|
-
class="w-full flex items-center gap-
|
|
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"
|
|
693
784
|
>
|
|
694
|
-
{{ col.label }}
|
|
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>
|
|
695
789
|
</button>
|
|
696
790
|
</template>
|
|
697
791
|
<p v-else class="px-3 py-3 text-xs text-muted-foreground italic">
|
|
@@ -700,84 +794,109 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
700
794
|
</div>
|
|
701
795
|
</template>
|
|
702
796
|
|
|
703
|
-
<!-- Step 2: value
|
|
797
|
+
<!-- Step 2: value config -->
|
|
704
798
|
<template v-else-if="filterMenuStep === 'value' && pendingCol">
|
|
705
|
-
|
|
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">
|
|
706
802
|
<button
|
|
707
803
|
type="button"
|
|
708
804
|
@click.stop="filterMenuStep = 'columns'"
|
|
709
|
-
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"
|
|
710
806
|
>
|
|
711
807
|
<IconChevronLeft class="size-4" />
|
|
712
808
|
</button>
|
|
713
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>
|
|
714
846
|
</div>
|
|
715
|
-
<div class="p-3 space-y-2.5">
|
|
716
847
|
|
|
717
|
-
|
|
848
|
+
<div class="p-3 min-w-56 space-y-2">
|
|
849
|
+
|
|
850
|
+
<!-- ── TEXT ── -->
|
|
718
851
|
<input
|
|
719
852
|
v-if="pendingCol.filterType === 'text'"
|
|
853
|
+
ref="pendingValueInputRef"
|
|
720
854
|
v-model="pendingValue"
|
|
721
855
|
type="text"
|
|
722
|
-
autofocus
|
|
723
856
|
@keydown.enter.stop="applyPendingFilter"
|
|
724
857
|
@keydown.escape.stop="closeFilterMenu"
|
|
725
|
-
placeholder="
|
|
726
|
-
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"
|
|
727
860
|
/>
|
|
728
861
|
|
|
729
|
-
<!--
|
|
730
|
-
<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">
|
|
731
864
|
<button
|
|
732
865
|
v-for="opt in pendingCol.filterOptions"
|
|
733
866
|
:key="opt.value"
|
|
734
867
|
type="button"
|
|
735
868
|
@click.stop="pendingValue = opt.value; applyPendingFilter()"
|
|
736
869
|
:class="[
|
|
737
|
-
'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',
|
|
738
871
|
pendingValue === opt.value
|
|
739
|
-
? 'bg-
|
|
872
|
+
? 'bg-primary/10 text-primary'
|
|
740
873
|
: 'hover:bg-muted-hover text-foreground'
|
|
741
874
|
]"
|
|
742
875
|
>
|
|
743
876
|
<span class="flex-1">{{ opt.label }}</span>
|
|
744
|
-
<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" />
|
|
745
878
|
</button>
|
|
746
879
|
</div>
|
|
747
880
|
|
|
748
|
-
<!--
|
|
749
|
-
<
|
|
750
|
-
<div class="flex gap-1">
|
|
751
|
-
<button
|
|
752
|
-
v-for="op in dateOps"
|
|
753
|
-
:key="op.value"
|
|
754
|
-
type="button"
|
|
755
|
-
@click.stop="pendingDateOp = op.value"
|
|
756
|
-
:class="[
|
|
757
|
-
'flex-1 py-1 text-xs rounded-lg border transition-colors',
|
|
758
|
-
pendingDateOp === op.value
|
|
759
|
-
? 'border-indigo-400 bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-300'
|
|
760
|
-
: 'border-card-line text-muted-foreground hover:bg-muted-hover'
|
|
761
|
-
]"
|
|
762
|
-
>
|
|
763
|
-
{{ op.label }}
|
|
764
|
-
</button>
|
|
765
|
-
</div>
|
|
881
|
+
<!-- ── DATERANGE ── -->
|
|
882
|
+
<template v-else-if="pendingCol.filterType === 'daterange'">
|
|
766
883
|
<template v-if="pendingDateOp === 'between'">
|
|
767
|
-
<input type="date" v-model="pendingValue.from" class="w-full rounded-lg border border-
|
|
768
|
-
<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" />
|
|
769
886
|
</template>
|
|
770
|
-
<input v-else type="date" v-model="pendingValue.singleDate" class="w-full rounded-lg border border-
|
|
771
|
-
</
|
|
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>
|
|
772
889
|
|
|
890
|
+
<!-- Apply (not for select — auto-applies on click) -->
|
|
773
891
|
<button
|
|
774
892
|
v-if="pendingCol.filterType !== 'select'"
|
|
775
893
|
type="button"
|
|
776
894
|
@click.stop="applyPendingFilter"
|
|
777
|
-
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"
|
|
778
896
|
>
|
|
779
897
|
Aplicar
|
|
780
898
|
</button>
|
|
899
|
+
|
|
781
900
|
</div>
|
|
782
901
|
</template>
|
|
783
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
|