@innertia-solutions/nuxt-theme-spark 0.1.139 → 0.1.141
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 +426 -165
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { IconSearch,
|
|
2
|
+
import { IconSearch, IconLayoutColumns, IconGripVertical, IconMinus, IconMaximize, IconX, IconPlus, IconChevronLeft, IconCheck, IconChevronDown } from '@tabler/icons-vue'
|
|
3
3
|
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
table: { type: Object, default: null },
|
|
@@ -30,11 +30,9 @@ const forwardedSlots = computed(() => {
|
|
|
30
30
|
return Object.fromEntries(Object.entries(slots).filter(([k]) => !excluded.has(k)))
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
const search
|
|
33
|
+
const search = ref('')
|
|
34
34
|
const activeFilters = ref({})
|
|
35
|
-
const
|
|
36
|
-
const filterPanelRef = ref(null)
|
|
37
|
-
const tableRef = ref(null)
|
|
35
|
+
const tableRef = ref(null)
|
|
38
36
|
|
|
39
37
|
// ─── Filter config ─────────────────────────────────────────────────────────────
|
|
40
38
|
const filtersConfig = computed(() =>
|
|
@@ -43,15 +41,153 @@ const filtersConfig = computed(() =>
|
|
|
43
41
|
|
|
44
42
|
const hasFilterableColumns = computed(() => filtersConfig.value.length > 0)
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
// ─── Notion-style filter ───────────────────────────────────────────────────────
|
|
45
|
+
const showFilterPanel = ref(false)
|
|
46
|
+
const filterMenuStep = ref('columns') // 'columns' | 'value'
|
|
47
|
+
const pendingCol = ref(null)
|
|
48
|
+
const pendingValue = ref(null) // string for text/select, { singleDate, from, to } for daterange
|
|
49
|
+
const pendingDateOp = ref('before') // 'before' | 'after' | 'between'
|
|
50
|
+
const filterMenuRef = ref(null)
|
|
51
|
+
const filterAddBtnRef = ref(null)
|
|
52
|
+
const filterMenuStyle = ref({})
|
|
53
|
+
|
|
54
|
+
const dateOps = [
|
|
55
|
+
{ value: 'before', label: 'antes de' },
|
|
56
|
+
{ value: 'after', label: 'después de' },
|
|
57
|
+
{ value: 'between', label: 'entre' },
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
const activeFilterList = computed(() =>
|
|
61
|
+
filtersConfig.value
|
|
62
|
+
.filter(col => {
|
|
63
|
+
const v = activeFilters.value[col.key]
|
|
64
|
+
if (col.filterType === 'daterange') return v?.from || v?.to
|
|
65
|
+
return v !== null && v !== undefined && v !== ''
|
|
66
|
+
})
|
|
67
|
+
.map(col => {
|
|
68
|
+
const v = activeFilters.value[col.key]
|
|
69
|
+
let displayOp = '', displayVal = ''
|
|
70
|
+
if (col.filterType === 'daterange') {
|
|
71
|
+
if (v.from && v.to) { displayOp = 'entre'; displayVal = `${v.from} y ${v.to}` }
|
|
72
|
+
else if (v.from) { displayOp = 'después de'; displayVal = v.from }
|
|
73
|
+
else { displayOp = 'antes de'; displayVal = v.to }
|
|
74
|
+
} else if (col.filterType === 'select') {
|
|
75
|
+
displayOp = 'es'
|
|
76
|
+
displayVal = col.filterOptions?.find(o => o.value === v)?.label ?? v
|
|
77
|
+
} else {
|
|
78
|
+
displayOp = 'contiene'; displayVal = v
|
|
79
|
+
}
|
|
80
|
+
return { key: col.key, label: col.label, displayOp, displayVal, col }
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const activeFilterCount = computed(() => activeFilterList.value.length)
|
|
85
|
+
|
|
86
|
+
// Columns NOT yet filtered — what appears in the picker (already-active columns are hidden)
|
|
87
|
+
const availableFilterColumns = computed(() =>
|
|
88
|
+
filtersConfig.value.filter(col => {
|
|
89
|
+
const v = activeFilters.value[col.key]
|
|
90
|
+
if (col.filterType === 'daterange') return !v?.from && !v?.to
|
|
91
|
+
return v === null || v === undefined || v === ''
|
|
92
|
+
})
|
|
48
93
|
)
|
|
49
94
|
|
|
95
|
+
// Convert activeFilters to enriched [{field, operator, value}] for the backend DataTable
|
|
96
|
+
const enrichedFilters = computed(() => {
|
|
97
|
+
const result = []
|
|
98
|
+
for (const col of filtersConfig.value) {
|
|
99
|
+
const v = activeFilters.value[col.key]
|
|
100
|
+
if (v === null || v === undefined || v === '') continue
|
|
101
|
+
if (col.filterType === 'daterange') {
|
|
102
|
+
if (!v?.from && !v?.to) continue
|
|
103
|
+
if (v.from) result.push({ field: col.key, operator: 'after', value: v.from })
|
|
104
|
+
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
|
+
} else {
|
|
108
|
+
result.push({ field: col.key, operator: 'contains', value: v })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return result
|
|
112
|
+
})
|
|
113
|
+
|
|
50
114
|
const mergedParams = computed(() => ({
|
|
51
115
|
...props.params,
|
|
52
|
-
...
|
|
116
|
+
...(enrichedFilters.value.length ? { filters: enrichedFilters.value } : {}),
|
|
53
117
|
}))
|
|
54
118
|
|
|
119
|
+
const removeFilter = (key) => {
|
|
120
|
+
const u = { ...activeFilters.value }; delete u[key]; activeFilters.value = u
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const openFilterMenu = async () => {
|
|
124
|
+
filterMenuStep.value = 'columns'
|
|
125
|
+
pendingCol.value = null
|
|
126
|
+
showFilterPanel.value = true
|
|
127
|
+
await nextTick()
|
|
128
|
+
const rect = filterAddBtnRef.value?.getBoundingClientRect()
|
|
129
|
+
if (rect) filterMenuStyle.value = { top: rect.bottom + 4 + 'px', left: rect.left + 'px' }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const toggleFilterMenu = async () => {
|
|
133
|
+
if (showFilterPanel.value) { closeFilterMenu() } else { await openFilterMenu() }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const closeFilterMenu = () => {
|
|
137
|
+
showFilterPanel.value = false
|
|
138
|
+
filterMenuStep.value = 'columns'
|
|
139
|
+
pendingCol.value = null
|
|
140
|
+
pendingValue.value = null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const selectFilterColumn = (col) => {
|
|
144
|
+
pendingCol.value = col
|
|
145
|
+
const existing = activeFilters.value[col.key]
|
|
146
|
+
if (col.filterType === 'daterange') {
|
|
147
|
+
if (existing?.from && existing?.to) { pendingDateOp.value = 'between'; pendingValue.value = { from: existing.from, to: existing.to, singleDate: '' } }
|
|
148
|
+
else if (existing?.from) { pendingDateOp.value = 'after'; pendingValue.value = { singleDate: existing.from, from: '', to: '' } }
|
|
149
|
+
else if (existing?.to) { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: existing.to, from: '', to: '' } }
|
|
150
|
+
else { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: '', from: '', to: '' } }
|
|
151
|
+
} else {
|
|
152
|
+
pendingValue.value = existing ?? ''
|
|
153
|
+
}
|
|
154
|
+
filterMenuStep.value = 'value'
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const applyPendingFilter = () => {
|
|
158
|
+
if (!pendingCol.value) return
|
|
159
|
+
const col = pendingCol.value
|
|
160
|
+
let v
|
|
161
|
+
if (col.filterType === 'daterange') {
|
|
162
|
+
if (pendingDateOp.value === 'between') v = { from: pendingValue.value.from, to: pendingValue.value.to }
|
|
163
|
+
else if (pendingDateOp.value === 'after') v = { from: pendingValue.value.singleDate }
|
|
164
|
+
else v = { to: pendingValue.value.singleDate }
|
|
165
|
+
} else {
|
|
166
|
+
v = pendingValue.value
|
|
167
|
+
}
|
|
168
|
+
activeFilters.value = { ...activeFilters.value, [col.key]: v || null }
|
|
169
|
+
closeFilterMenu()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const openEditFilter = async (col) => {
|
|
173
|
+
selectFilterColumn(col)
|
|
174
|
+
showFilterPanel.value = true
|
|
175
|
+
await nextTick()
|
|
176
|
+
const rect = filterAddBtnRef.value?.getBoundingClientRect()
|
|
177
|
+
if (rect) filterMenuStyle.value = { top: rect.bottom + 4 + 'px', left: rect.left + 'px' }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const onFilterMenuOutsideClick = (e) => {
|
|
181
|
+
if (filterMenuRef.value && !filterMenuRef.value.contains(e.target) &&
|
|
182
|
+
filterAddBtnRef.value && !filterAddBtnRef.value.contains(e.target)) {
|
|
183
|
+
closeFilterMenu()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
watch(showFilterPanel, v => {
|
|
187
|
+
if (v) document.addEventListener('mousedown', onFilterMenuOutsideClick)
|
|
188
|
+
else document.removeEventListener('mousedown', onFilterMenuOutsideClick)
|
|
189
|
+
})
|
|
190
|
+
|
|
55
191
|
// ─── Preview panel ─────────────────────────────────────────────────────────────
|
|
56
192
|
const previewRow = ref(null)
|
|
57
193
|
const currentRatio = ref(props.splitRatio)
|
|
@@ -260,11 +396,6 @@ const onColumnPanelOutsideClick = (e) => {
|
|
|
260
396
|
showColumnPanel.value = false
|
|
261
397
|
}
|
|
262
398
|
}
|
|
263
|
-
const onFilterPanelOutsideClick = (e) => {
|
|
264
|
-
if (filterPanelRef.value && !filterPanelRef.value.contains(e.target)) {
|
|
265
|
-
showFilterPanel.value = false
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
399
|
|
|
269
400
|
watch(showColumnPanel, async (v) => {
|
|
270
401
|
if (v) {
|
|
@@ -281,10 +412,6 @@ watch(showColumnPanel, async (v) => {
|
|
|
281
412
|
document.removeEventListener('mousedown', onColumnPanelOutsideClick)
|
|
282
413
|
}
|
|
283
414
|
})
|
|
284
|
-
watch(showFilterPanel, (v) => {
|
|
285
|
-
if (v) document.addEventListener('mousedown', onFilterPanelOutsideClick)
|
|
286
|
-
else document.removeEventListener('mousedown', onFilterPanelOutsideClick)
|
|
287
|
-
})
|
|
288
415
|
|
|
289
416
|
// ─── Expose ───────────────────────────────────────────────────────────────────
|
|
290
417
|
const getSelectedRows = () => tableRef.value?.getSelectedRows()
|
|
@@ -298,190 +425,201 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
298
425
|
<template>
|
|
299
426
|
<div class="relative" ref="containerRef">
|
|
300
427
|
|
|
301
|
-
<!--
|
|
302
|
-
<div class="
|
|
428
|
+
<!-- Toolbar row (no card) -->
|
|
429
|
+
<div class="flex flex-wrap items-center gap-2 mb-2">
|
|
303
430
|
|
|
304
|
-
<!--
|
|
305
|
-
<div class="flex
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
</div>
|
|
431
|
+
<!-- Search -->
|
|
432
|
+
<div v-if="showSearch" class="flex-1 min-w-48 max-w-xs">
|
|
433
|
+
<Forms.Input v-model="search" type="search" :placeholder="searchPlaceholder" :icon-left="IconSearch" size="sm" />
|
|
434
|
+
</div>
|
|
309
435
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
<Transition
|
|
327
|
-
enter-active-class="transition ease-out duration-150"
|
|
328
|
-
enter-from-class="opacity-0 translate-y-1 scale-95"
|
|
329
|
-
enter-to-class="opacity-100 translate-y-0 scale-100"
|
|
330
|
-
leave-active-class="transition ease-in duration-100"
|
|
331
|
-
leave-from-class="opacity-100 translate-y-0 scale-100"
|
|
332
|
-
leave-to-class="opacity-0 translate-y-1 scale-95"
|
|
333
|
-
>
|
|
334
|
-
<div
|
|
335
|
-
v-if="showFilterPanel"
|
|
336
|
-
ref="filterPanelRef"
|
|
337
|
-
class="absolute top-full left-0 z-50 mt-1.5 bg-dropdown border border-dropdown-line rounded-xl shadow-2xl p-3 min-w-56 max-h-96 overflow-y-auto"
|
|
338
|
-
>
|
|
339
|
-
<p class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest mb-3 px-1">Filtros</p>
|
|
340
|
-
<TableFilter v-model="activeFilters" :columns="filtersConfig" />
|
|
341
|
-
</div>
|
|
342
|
-
</Transition>
|
|
343
|
-
</div>
|
|
436
|
+
<!-- + Filtros button -->
|
|
437
|
+
<div v-if="showFilters && hasFilterableColumns" ref="filterAddBtnRef" class="relative">
|
|
438
|
+
<button
|
|
439
|
+
type="button"
|
|
440
|
+
@click="toggleFilterMenu"
|
|
441
|
+
:class="[
|
|
442
|
+
'inline-flex items-center gap-1.5 py-1.5 px-3 text-sm font-medium rounded-lg border transition-colors',
|
|
443
|
+
activeFilterList.length
|
|
444
|
+
? 'border-indigo-300 bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:border-indigo-700 dark:text-indigo-300'
|
|
445
|
+
: 'border-card-line bg-card text-muted-foreground-1 hover:bg-muted-hover'
|
|
446
|
+
]"
|
|
447
|
+
>
|
|
448
|
+
<IconPlus class="size-3.5" />
|
|
449
|
+
Filtros{{ activeFilterList.length ? ` (${activeFilterList.length})` : '' }}
|
|
450
|
+
</button>
|
|
451
|
+
</div>
|
|
344
452
|
|
|
345
|
-
|
|
453
|
+
<!-- Slot for custom toolbar buttons -->
|
|
454
|
+
<slot name="toolbar" />
|
|
346
455
|
|
|
456
|
+
<!-- Secondary actions: pushed to the right, icon-only style -->
|
|
457
|
+
<div class="ml-auto flex items-center gap-1">
|
|
347
458
|
<button
|
|
348
459
|
ref="columnButtonRef"
|
|
349
460
|
type="button"
|
|
350
461
|
@click="showColumnPanel = !showColumnPanel"
|
|
462
|
+
:title="'Columnas'"
|
|
351
463
|
:class="[
|
|
352
|
-
'
|
|
464
|
+
'p-1.5 inline-flex items-center justify-center rounded-lg border transition-colors',
|
|
353
465
|
showColumnPanel
|
|
354
|
-
? 'border-
|
|
355
|
-
: 'border-
|
|
466
|
+
? 'border-indigo-300 bg-indigo-50 text-indigo-600 dark:bg-indigo-900/20 dark:border-indigo-700 dark:text-indigo-300'
|
|
467
|
+
: 'border-transparent text-muted-foreground hover:border-card-line hover:bg-muted-hover hover:text-foreground'
|
|
356
468
|
]"
|
|
357
469
|
>
|
|
358
470
|
<IconLayoutColumns class="size-4" />
|
|
359
|
-
Columnas
|
|
360
471
|
</button>
|
|
361
472
|
|
|
362
473
|
<TableExportable v-if="showExport" :table-ref="tableRef" :name="resolvedName" :columns="columns" />
|
|
363
474
|
</div>
|
|
475
|
+
</div>
|
|
364
476
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
:show-reload-button="showReloadButton"
|
|
379
|
-
:click-row-to-open="clickRowToOpen"
|
|
380
|
-
:preview-row-id="previewRow?.id ?? null"
|
|
381
|
-
:preview-mode="!!previewEnabled"
|
|
382
|
-
@row-click="handleRowClick"
|
|
383
|
-
@loaded="handleLoaded"
|
|
384
|
-
@page-change="closePreview"
|
|
385
|
-
@per-page-change="closePreview"
|
|
477
|
+
<!-- Filter chips row (shown when filters active) -->
|
|
478
|
+
<div v-if="activeFilterList.length" class="flex flex-wrap items-center gap-1.5 mb-2">
|
|
479
|
+
<div
|
|
480
|
+
v-for="chip in activeFilterList"
|
|
481
|
+
:key="chip.key"
|
|
482
|
+
class="inline-flex items-center text-xs rounded-lg border border-card-line bg-card overflow-hidden"
|
|
483
|
+
>
|
|
484
|
+
<span class="px-2.5 py-1 text-foreground font-medium border-r border-card-line bg-surface">{{ chip.label }}</span>
|
|
485
|
+
<span class="px-2 py-1 text-muted-foreground">{{ chip.displayOp }}</span>
|
|
486
|
+
<button
|
|
487
|
+
type="button"
|
|
488
|
+
@click.stop="openEditFilter(chip.col)"
|
|
489
|
+
class="inline-flex items-center gap-1 px-2 py-1 text-indigo-600 dark:text-indigo-400 font-medium hover:bg-indigo-50 dark:hover:bg-indigo-900/20 transition-colors border-x border-card-line"
|
|
386
490
|
>
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
:enter-active-class="previewFromCache ? '' : 'transition ease-out duration-200'"
|
|
395
|
-
:enter-from-class="previewFromCache ? '' : 'opacity-0 translate-x-6'"
|
|
396
|
-
:enter-to-class="previewFromCache ? '' : 'opacity-100 translate-x-0'"
|
|
397
|
-
leave-active-class="transition ease-in duration-150"
|
|
398
|
-
leave-from-class="opacity-100 translate-x-0"
|
|
399
|
-
leave-to-class="opacity-0 translate-x-6"
|
|
491
|
+
{{ chip.displayVal }}
|
|
492
|
+
<IconChevronDown class="size-3 opacity-60" />
|
|
493
|
+
</button>
|
|
494
|
+
<button
|
|
495
|
+
type="button"
|
|
496
|
+
@click.stop="removeFilter(chip.key)"
|
|
497
|
+
class="px-1.5 py-1 text-muted-foreground hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
|
400
498
|
>
|
|
401
|
-
<
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
:style="{ width: (100 - currentRatio) + '%', bottom: paginationHeight + 'px' }"
|
|
406
|
-
>
|
|
407
|
-
<!-- Resize handle -->
|
|
408
|
-
<div
|
|
409
|
-
class="w-1 shrink-0 cursor-col-resize bg-surface hover:bg-indigo-300 dark:hover:bg-indigo-600 transition-colors"
|
|
410
|
-
@mousedown="startResize"
|
|
411
|
-
/>
|
|
412
|
-
<!-- Preview -->
|
|
413
|
-
<div class="flex flex-col flex-1 overflow-hidden">
|
|
499
|
+
<IconX class="size-3" />
|
|
500
|
+
</button>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
414
503
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
504
|
+
<!-- Table + preview overlay inside a minimal border box -->
|
|
505
|
+
<div class="relative overflow-hidden rounded-xl border border-card-line">
|
|
506
|
+
|
|
507
|
+
<!-- Tabla -->
|
|
508
|
+
<Table
|
|
509
|
+
ref="tableRef"
|
|
510
|
+
:endpoint="resolvedEndpoint"
|
|
511
|
+
:columns="columns"
|
|
512
|
+
:name="resolvedName"
|
|
513
|
+
:params="mergedParams"
|
|
514
|
+
:search="search"
|
|
515
|
+
:checkable="checkable"
|
|
516
|
+
:cached="cached"
|
|
517
|
+
:show-reload-button="showReloadButton"
|
|
518
|
+
:click-row-to-open="clickRowToOpen"
|
|
519
|
+
:preview-row-id="previewRow?.id ?? null"
|
|
520
|
+
:preview-mode="!!previewEnabled"
|
|
521
|
+
@row-click="handleRowClick"
|
|
522
|
+
@loaded="handleLoaded"
|
|
523
|
+
@page-change="closePreview"
|
|
524
|
+
@per-page-change="closePreview"
|
|
525
|
+
>
|
|
526
|
+
<template v-for="(_, name) in forwardedSlots" #[name]="slotProps">
|
|
527
|
+
<slot :name="name" v-bind="slotProps ?? {}" />
|
|
528
|
+
</template>
|
|
529
|
+
</Table>
|
|
439
530
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
531
|
+
<!-- Preview panel overlay — slides in from right, tapa la tabla -->
|
|
532
|
+
<Transition
|
|
533
|
+
:enter-active-class="previewFromCache ? '' : 'transition ease-out duration-200'"
|
|
534
|
+
:enter-from-class="previewFromCache ? '' : 'opacity-0 translate-x-6'"
|
|
535
|
+
:enter-to-class="previewFromCache ? '' : 'opacity-100 translate-x-0'"
|
|
536
|
+
leave-active-class="transition ease-in duration-150"
|
|
537
|
+
leave-from-class="opacity-100 translate-x-0"
|
|
538
|
+
leave-to-class="opacity-0 translate-x-6"
|
|
539
|
+
>
|
|
540
|
+
<div
|
|
541
|
+
v-if="previewRow && previewEnabled"
|
|
542
|
+
ref="previewPanelRef"
|
|
543
|
+
class="absolute top-0 right-0 z-30 flex bg-card border-l border-card-line shadow-[-4px_0_16px_rgba(0,0,0,0.06)]"
|
|
544
|
+
:style="{ width: (100 - currentRatio) + '%', bottom: paginationHeight + 'px' }"
|
|
545
|
+
>
|
|
546
|
+
<!-- Resize handle -->
|
|
547
|
+
<div
|
|
548
|
+
class="w-1 shrink-0 cursor-col-resize bg-surface hover:bg-indigo-300 dark:hover:bg-indigo-600 transition-colors"
|
|
549
|
+
@mousedown="startResize"
|
|
550
|
+
/>
|
|
551
|
+
<!-- Preview -->
|
|
552
|
+
<div class="flex flex-col flex-1 overflow-hidden">
|
|
553
|
+
|
|
554
|
+
<!-- Barra de acciones del preview -->
|
|
555
|
+
<div class="shrink-0 flex items-center justify-between gap-2 px-3 py-2 border-b border-card-line">
|
|
556
|
+
<div class="flex-1 min-w-0">
|
|
557
|
+
<slot name="preview-header" :row="previewRow" :close="closePreview" />
|
|
447
558
|
</div>
|
|
448
|
-
|
|
449
|
-
<!-- Tabs — bottom -->
|
|
450
|
-
<div v-if="hasHistory" class="shrink-0 flex border-t border-card-line">
|
|
559
|
+
<div class="flex items-center gap-1 shrink-0">
|
|
451
560
|
<button
|
|
452
561
|
type="button"
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
previewTab === 'datos'
|
|
457
|
-
? 'border-t-card text-foreground'
|
|
458
|
-
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
459
|
-
]"
|
|
562
|
+
class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
563
|
+
title="Minimizar"
|
|
564
|
+
@click.stop="minimizePreview"
|
|
460
565
|
>
|
|
461
|
-
|
|
566
|
+
<IconMinus class="size-3.5" />
|
|
462
567
|
</button>
|
|
463
568
|
<button
|
|
464
569
|
type="button"
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
'flex-1 py-2.5 text-xs font-semibold transition-colors border-t-2 -mt-px',
|
|
469
|
-
!resolvedHistoryEndpoint
|
|
470
|
-
? 'border-t-transparent text-muted-foreground/40 cursor-not-allowed'
|
|
471
|
-
: previewTab === 'bitacora'
|
|
472
|
-
? 'border-t-card text-foreground'
|
|
473
|
-
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
474
|
-
]"
|
|
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"
|
|
475
573
|
>
|
|
476
|
-
|
|
574
|
+
<IconX class="size-3.5" />
|
|
477
575
|
</button>
|
|
478
576
|
</div>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
<!-- Scrollable content -->
|
|
580
|
+
<div class="flex-1 overflow-y-auto min-h-0">
|
|
581
|
+
<slot v-if="previewTab === 'datos'" name="preview" :row="previewRow" :close="closePreview" />
|
|
582
|
+
<Table.PreviewTimeline
|
|
583
|
+
v-else-if="previewTab === 'bitacora' && resolvedHistoryEndpoint"
|
|
584
|
+
:endpoint="resolvedHistoryEndpoint"
|
|
585
|
+
/>
|
|
586
|
+
</div>
|
|
479
587
|
|
|
588
|
+
<!-- Tabs — bottom -->
|
|
589
|
+
<div v-if="hasHistory" class="shrink-0 flex border-t border-card-line">
|
|
590
|
+
<button
|
|
591
|
+
type="button"
|
|
592
|
+
@click="previewTab = 'datos'"
|
|
593
|
+
:class="[
|
|
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
|
+
]"
|
|
599
|
+
>
|
|
600
|
+
Datos
|
|
601
|
+
</button>
|
|
602
|
+
<button
|
|
603
|
+
type="button"
|
|
604
|
+
@click="resolvedHistoryEndpoint && (previewTab = 'bitacora')"
|
|
605
|
+
:disabled="!resolvedHistoryEndpoint"
|
|
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
|
+
]"
|
|
614
|
+
>
|
|
615
|
+
Bitácora
|
|
616
|
+
</button>
|
|
480
617
|
</div>
|
|
618
|
+
|
|
481
619
|
</div>
|
|
482
|
-
</
|
|
620
|
+
</div>
|
|
621
|
+
</Transition>
|
|
483
622
|
|
|
484
|
-
</div>
|
|
485
623
|
</div>
|
|
486
624
|
|
|
487
625
|
<!-- ── Floating mini-preview (dock expand, estilo Gmail) ── -->
|
|
@@ -524,6 +662,129 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
524
662
|
</Transition>
|
|
525
663
|
</Teleport>
|
|
526
664
|
|
|
665
|
+
<!-- Filter menu — teleported to body -->
|
|
666
|
+
<Teleport to="body">
|
|
667
|
+
<Transition
|
|
668
|
+
enter-active-class="transition ease-out duration-150"
|
|
669
|
+
enter-from-class="opacity-0 translate-y-1 scale-95"
|
|
670
|
+
enter-to-class="opacity-100 translate-y-0 scale-100"
|
|
671
|
+
leave-active-class="transition ease-in duration-100"
|
|
672
|
+
leave-from-class="opacity-100 translate-y-0 scale-100"
|
|
673
|
+
leave-to-class="opacity-0 translate-y-1 scale-95"
|
|
674
|
+
>
|
|
675
|
+
<div
|
|
676
|
+
v-if="showFilterPanel"
|
|
677
|
+
ref="filterMenuRef"
|
|
678
|
+
class="fixed z-[60] bg-dropdown border border-dropdown-line rounded-xl shadow-2xl min-w-52 overflow-hidden"
|
|
679
|
+
:style="filterMenuStyle"
|
|
680
|
+
>
|
|
681
|
+
|
|
682
|
+
<!-- Step 1: column picker (only non-filtered columns shown) -->
|
|
683
|
+
<template v-if="filterMenuStep === 'columns'">
|
|
684
|
+
<p class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest px-3 pt-2.5 pb-1">Filtrar por</p>
|
|
685
|
+
<div class="pb-1.5">
|
|
686
|
+
<template v-if="availableFilterColumns.length">
|
|
687
|
+
<button
|
|
688
|
+
v-for="col in availableFilterColumns"
|
|
689
|
+
:key="col.key"
|
|
690
|
+
type="button"
|
|
691
|
+
@click.stop="selectFilterColumn(col)"
|
|
692
|
+
class="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted-hover transition-colors text-left text-foreground"
|
|
693
|
+
>
|
|
694
|
+
{{ col.label }}
|
|
695
|
+
</button>
|
|
696
|
+
</template>
|
|
697
|
+
<p v-else class="px-3 py-3 text-xs text-muted-foreground italic">
|
|
698
|
+
Todos los filtros están configurados
|
|
699
|
+
</p>
|
|
700
|
+
</div>
|
|
701
|
+
</template>
|
|
702
|
+
|
|
703
|
+
<!-- Step 2: value input -->
|
|
704
|
+
<template v-else-if="filterMenuStep === 'value' && pendingCol">
|
|
705
|
+
<div class="flex items-center gap-2 px-3 py-2 border-b border-card-line bg-surface">
|
|
706
|
+
<button
|
|
707
|
+
type="button"
|
|
708
|
+
@click.stop="filterMenuStep = 'columns'"
|
|
709
|
+
class="text-muted-foreground hover:text-foreground transition-colors"
|
|
710
|
+
>
|
|
711
|
+
<IconChevronLeft class="size-4" />
|
|
712
|
+
</button>
|
|
713
|
+
<span class="text-sm font-medium text-foreground">{{ pendingCol.label }}</span>
|
|
714
|
+
</div>
|
|
715
|
+
<div class="p-3 space-y-2.5">
|
|
716
|
+
|
|
717
|
+
<!-- text -->
|
|
718
|
+
<input
|
|
719
|
+
v-if="pendingCol.filterType === 'text'"
|
|
720
|
+
v-model="pendingValue"
|
|
721
|
+
type="text"
|
|
722
|
+
autofocus
|
|
723
|
+
@keydown.enter.stop="applyPendingFilter"
|
|
724
|
+
@keydown.escape.stop="closeFilterMenu"
|
|
725
|
+
placeholder="Buscar..."
|
|
726
|
+
class="w-full rounded-lg border border-card-line bg-card text-foreground py-1.5 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
|
|
727
|
+
/>
|
|
728
|
+
|
|
729
|
+
<!-- select -->
|
|
730
|
+
<div v-else-if="pendingCol.filterType === 'select'" class="space-y-0.5">
|
|
731
|
+
<button
|
|
732
|
+
v-for="opt in pendingCol.filterOptions"
|
|
733
|
+
:key="opt.value"
|
|
734
|
+
type="button"
|
|
735
|
+
@click.stop="pendingValue = opt.value; applyPendingFilter()"
|
|
736
|
+
:class="[
|
|
737
|
+
'w-full flex items-center gap-2 px-2.5 py-1.5 text-sm rounded-lg transition-colors text-left',
|
|
738
|
+
pendingValue === opt.value
|
|
739
|
+
? 'bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-300'
|
|
740
|
+
: 'hover:bg-muted-hover text-foreground'
|
|
741
|
+
]"
|
|
742
|
+
>
|
|
743
|
+
<span class="flex-1">{{ opt.label }}</span>
|
|
744
|
+
<IconCheck v-if="pendingValue === opt.value" class="size-3.5 shrink-0 text-indigo-500" />
|
|
745
|
+
</button>
|
|
746
|
+
</div>
|
|
747
|
+
|
|
748
|
+
<!-- daterange -->
|
|
749
|
+
<div v-else-if="pendingCol.filterType === 'daterange'" class="space-y-2">
|
|
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>
|
|
766
|
+
<template v-if="pendingDateOp === 'between'">
|
|
767
|
+
<input type="date" v-model="pendingValue.from" class="w-full rounded-lg border border-card-line bg-card text-foreground py-1.5 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500" />
|
|
768
|
+
<input type="date" v-model="pendingValue.to" class="w-full rounded-lg border border-card-line bg-card text-foreground py-1.5 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500" />
|
|
769
|
+
</template>
|
|
770
|
+
<input v-else type="date" v-model="pendingValue.singleDate" class="w-full rounded-lg border border-card-line bg-card text-foreground py-1.5 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500" />
|
|
771
|
+
</div>
|
|
772
|
+
|
|
773
|
+
<button
|
|
774
|
+
v-if="pendingCol.filterType !== 'select'"
|
|
775
|
+
type="button"
|
|
776
|
+
@click.stop="applyPendingFilter"
|
|
777
|
+
class="w-full py-1.5 text-sm font-medium text-center rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition-colors"
|
|
778
|
+
>
|
|
779
|
+
Aplicar
|
|
780
|
+
</button>
|
|
781
|
+
</div>
|
|
782
|
+
</template>
|
|
783
|
+
|
|
784
|
+
</div>
|
|
785
|
+
</Transition>
|
|
786
|
+
</Teleport>
|
|
787
|
+
|
|
527
788
|
<!-- Column panel — teleported to body to escape overflow-hidden -->
|
|
528
789
|
<Teleport to="body">
|
|
529
790
|
<Transition
|