@innertia-solutions/nuxt-theme-spark 0.1.139 → 0.1.140
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 +398 -171
- 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,125 @@ 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
|
+
})
|
|
48
82
|
)
|
|
49
83
|
|
|
84
|
+
const activeFilterCount = computed(() => activeFilterList.value.length)
|
|
85
|
+
|
|
50
86
|
const mergedParams = computed(() => ({
|
|
51
87
|
...props.params,
|
|
52
88
|
...activeFilters.value,
|
|
53
89
|
}))
|
|
54
90
|
|
|
91
|
+
const removeFilter = (key) => {
|
|
92
|
+
const u = { ...activeFilters.value }; delete u[key]; activeFilters.value = u
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const openFilterMenu = async () => {
|
|
96
|
+
filterMenuStep.value = 'columns'
|
|
97
|
+
pendingCol.value = null
|
|
98
|
+
showFilterPanel.value = true
|
|
99
|
+
await nextTick()
|
|
100
|
+
const rect = filterAddBtnRef.value?.getBoundingClientRect()
|
|
101
|
+
if (rect) filterMenuStyle.value = { top: rect.bottom + 4 + 'px', left: rect.left + 'px' }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const toggleFilterMenu = async () => {
|
|
105
|
+
if (showFilterPanel.value) { closeFilterMenu() } else { await openFilterMenu() }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const closeFilterMenu = () => {
|
|
109
|
+
showFilterPanel.value = false
|
|
110
|
+
filterMenuStep.value = 'columns'
|
|
111
|
+
pendingCol.value = null
|
|
112
|
+
pendingValue.value = null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const selectFilterColumn = (col) => {
|
|
116
|
+
pendingCol.value = col
|
|
117
|
+
const existing = activeFilters.value[col.key]
|
|
118
|
+
if (col.filterType === 'daterange') {
|
|
119
|
+
if (existing?.from && existing?.to) { pendingDateOp.value = 'between'; pendingValue.value = { from: existing.from, to: existing.to, singleDate: '' } }
|
|
120
|
+
else if (existing?.from) { pendingDateOp.value = 'after'; pendingValue.value = { singleDate: existing.from, from: '', to: '' } }
|
|
121
|
+
else if (existing?.to) { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: existing.to, from: '', to: '' } }
|
|
122
|
+
else { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: '', from: '', to: '' } }
|
|
123
|
+
} else {
|
|
124
|
+
pendingValue.value = existing ?? ''
|
|
125
|
+
}
|
|
126
|
+
filterMenuStep.value = 'value'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const applyPendingFilter = () => {
|
|
130
|
+
if (!pendingCol.value) return
|
|
131
|
+
const col = pendingCol.value
|
|
132
|
+
let v
|
|
133
|
+
if (col.filterType === 'daterange') {
|
|
134
|
+
if (pendingDateOp.value === 'between') v = { from: pendingValue.value.from, to: pendingValue.value.to }
|
|
135
|
+
else if (pendingDateOp.value === 'after') v = { from: pendingValue.value.singleDate }
|
|
136
|
+
else v = { to: pendingValue.value.singleDate }
|
|
137
|
+
} else {
|
|
138
|
+
v = pendingValue.value
|
|
139
|
+
}
|
|
140
|
+
activeFilters.value = { ...activeFilters.value, [col.key]: v || null }
|
|
141
|
+
closeFilterMenu()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const openEditFilter = async (col) => {
|
|
145
|
+
selectFilterColumn(col)
|
|
146
|
+
showFilterPanel.value = true
|
|
147
|
+
await nextTick()
|
|
148
|
+
const rect = filterAddBtnRef.value?.getBoundingClientRect()
|
|
149
|
+
if (rect) filterMenuStyle.value = { top: rect.bottom + 4 + 'px', left: rect.left + 'px' }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const onFilterMenuOutsideClick = (e) => {
|
|
153
|
+
if (filterMenuRef.value && !filterMenuRef.value.contains(e.target) &&
|
|
154
|
+
filterAddBtnRef.value && !filterAddBtnRef.value.contains(e.target)) {
|
|
155
|
+
closeFilterMenu()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
watch(showFilterPanel, v => {
|
|
159
|
+
if (v) document.addEventListener('mousedown', onFilterMenuOutsideClick)
|
|
160
|
+
else document.removeEventListener('mousedown', onFilterMenuOutsideClick)
|
|
161
|
+
})
|
|
162
|
+
|
|
55
163
|
// ─── Preview panel ─────────────────────────────────────────────────────────────
|
|
56
164
|
const previewRow = ref(null)
|
|
57
165
|
const currentRatio = ref(props.splitRatio)
|
|
@@ -260,11 +368,6 @@ const onColumnPanelOutsideClick = (e) => {
|
|
|
260
368
|
showColumnPanel.value = false
|
|
261
369
|
}
|
|
262
370
|
}
|
|
263
|
-
const onFilterPanelOutsideClick = (e) => {
|
|
264
|
-
if (filterPanelRef.value && !filterPanelRef.value.contains(e.target)) {
|
|
265
|
-
showFilterPanel.value = false
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
371
|
|
|
269
372
|
watch(showColumnPanel, async (v) => {
|
|
270
373
|
if (v) {
|
|
@@ -281,10 +384,6 @@ watch(showColumnPanel, async (v) => {
|
|
|
281
384
|
document.removeEventListener('mousedown', onColumnPanelOutsideClick)
|
|
282
385
|
}
|
|
283
386
|
})
|
|
284
|
-
watch(showFilterPanel, (v) => {
|
|
285
|
-
if (v) document.addEventListener('mousedown', onFilterPanelOutsideClick)
|
|
286
|
-
else document.removeEventListener('mousedown', onFilterPanelOutsideClick)
|
|
287
|
-
})
|
|
288
387
|
|
|
289
388
|
// ─── Expose ───────────────────────────────────────────────────────────────────
|
|
290
389
|
const getSelectedRows = () => tableRef.value?.getSelectedRows()
|
|
@@ -298,190 +397,199 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
298
397
|
<template>
|
|
299
398
|
<div class="relative" ref="containerRef">
|
|
300
399
|
|
|
301
|
-
<!--
|
|
302
|
-
<div class="
|
|
303
|
-
|
|
304
|
-
<!-- Toolbar -->
|
|
305
|
-
<div class="flex flex-wrap items-center gap-3 px-4 py-3 border-b border-card-line">
|
|
306
|
-
<div v-if="showSearch" class="flex-1 min-w-48">
|
|
307
|
-
<Forms.Input v-model="search" type="search" :placeholder="searchPlaceholder" :icon-left="IconSearch" size="sm" />
|
|
308
|
-
</div>
|
|
400
|
+
<!-- Toolbar row (no card) -->
|
|
401
|
+
<div class="flex flex-wrap items-center gap-2 mb-2">
|
|
309
402
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
:class="[
|
|
315
|
-
'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
|
|
316
|
-
showFilterPanel || activeFilterCount > 0
|
|
317
|
-
? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
|
|
318
|
-
: 'border-card-line bg-card text-muted-foreground-1 hover:bg-muted-hover'
|
|
319
|
-
]"
|
|
320
|
-
>
|
|
321
|
-
<IconAdjustmentsHorizontal class="size-4" stroke="1.5" />
|
|
322
|
-
Filtros{{ activeFilterCount > 0 ? ` (${activeFilterCount})` : '' }}
|
|
323
|
-
</button>
|
|
324
|
-
|
|
325
|
-
<!-- Filter panel — anchored below button -->
|
|
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>
|
|
344
|
-
|
|
345
|
-
<slot name="toolbar" />
|
|
403
|
+
<!-- Search -->
|
|
404
|
+
<div v-if="showSearch" class="flex-1 min-w-48 max-w-xs">
|
|
405
|
+
<Forms.Input v-model="search" type="search" :placeholder="searchPlaceholder" :icon-left="IconSearch" size="sm" />
|
|
406
|
+
</div>
|
|
346
407
|
|
|
408
|
+
<!-- + Filtros button -->
|
|
409
|
+
<div v-if="showFilters && hasFilterableColumns" ref="filterAddBtnRef" class="relative">
|
|
347
410
|
<button
|
|
348
|
-
ref="columnButtonRef"
|
|
349
411
|
type="button"
|
|
350
|
-
@click="
|
|
412
|
+
@click="toggleFilterMenu"
|
|
351
413
|
:class="[
|
|
352
|
-
'
|
|
353
|
-
|
|
354
|
-
? 'border-
|
|
414
|
+
'inline-flex items-center gap-1.5 py-1.5 px-3 text-sm font-medium rounded-lg border transition-colors',
|
|
415
|
+
activeFilterList.length
|
|
416
|
+
? 'border-indigo-300 bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:border-indigo-700 dark:text-indigo-300'
|
|
355
417
|
: 'border-card-line bg-card text-muted-foreground-1 hover:bg-muted-hover'
|
|
356
418
|
]"
|
|
357
419
|
>
|
|
358
|
-
<
|
|
359
|
-
|
|
420
|
+
<IconPlus class="size-3.5" />
|
|
421
|
+
Filtros{{ activeFilterList.length ? ` (${activeFilterList.length})` : '' }}
|
|
360
422
|
</button>
|
|
361
|
-
|
|
362
|
-
<TableExportable v-if="showExport" :table-ref="tableRef" :name="resolvedName" :columns="columns" />
|
|
363
423
|
</div>
|
|
364
424
|
|
|
365
|
-
<!--
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
425
|
+
<!-- Slot for custom toolbar buttons -->
|
|
426
|
+
<slot name="toolbar" />
|
|
427
|
+
|
|
428
|
+
<!-- Columnas button -->
|
|
429
|
+
<button
|
|
430
|
+
ref="columnButtonRef"
|
|
431
|
+
type="button"
|
|
432
|
+
@click="showColumnPanel = !showColumnPanel"
|
|
433
|
+
:class="[
|
|
434
|
+
'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
|
|
435
|
+
showColumnPanel
|
|
436
|
+
? 'border-indigo-300 bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:border-indigo-700 dark:text-indigo-300'
|
|
437
|
+
: 'border-card-line bg-card text-muted-foreground-1 hover:bg-muted-hover'
|
|
438
|
+
]"
|
|
439
|
+
>
|
|
440
|
+
<IconLayoutColumns class="size-4" />
|
|
441
|
+
Columnas
|
|
442
|
+
</button>
|
|
443
|
+
|
|
444
|
+
<TableExportable v-if="showExport" :table-ref="tableRef" :name="resolvedName" :columns="columns" />
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<!-- Filter chips row (shown when filters active) -->
|
|
448
|
+
<div v-if="activeFilterList.length" class="flex flex-wrap items-center gap-1.5 mb-2">
|
|
449
|
+
<div
|
|
450
|
+
v-for="chip in activeFilterList"
|
|
451
|
+
:key="chip.key"
|
|
452
|
+
class="inline-flex items-center text-xs rounded-lg border border-card-line bg-card overflow-hidden"
|
|
453
|
+
>
|
|
454
|
+
<span class="px-2.5 py-1 text-foreground font-medium border-r border-card-line bg-surface">{{ chip.label }}</span>
|
|
455
|
+
<span class="px-2 py-1 text-muted-foreground">{{ chip.displayOp }}</span>
|
|
456
|
+
<button
|
|
457
|
+
type="button"
|
|
458
|
+
@click.stop="openEditFilter(chip.col)"
|
|
459
|
+
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
460
|
>
|
|
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"
|
|
461
|
+
{{ chip.displayVal }}
|
|
462
|
+
<IconChevronDown class="size-3 opacity-60" />
|
|
463
|
+
</button>
|
|
464
|
+
<button
|
|
465
|
+
type="button"
|
|
466
|
+
@click.stop="removeFilter(chip.key)"
|
|
467
|
+
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
468
|
>
|
|
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">
|
|
469
|
+
<IconX class="size-3" />
|
|
470
|
+
</button>
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
414
473
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
474
|
+
<!-- Table + preview overlay inside a minimal border box -->
|
|
475
|
+
<div class="relative overflow-hidden rounded-xl border border-card-line">
|
|
476
|
+
|
|
477
|
+
<!-- Tabla -->
|
|
478
|
+
<Table
|
|
479
|
+
ref="tableRef"
|
|
480
|
+
:endpoint="resolvedEndpoint"
|
|
481
|
+
:columns="columns"
|
|
482
|
+
:name="resolvedName"
|
|
483
|
+
:params="mergedParams"
|
|
484
|
+
:search="search"
|
|
485
|
+
:checkable="checkable"
|
|
486
|
+
:cached="cached"
|
|
487
|
+
:show-reload-button="showReloadButton"
|
|
488
|
+
:click-row-to-open="clickRowToOpen"
|
|
489
|
+
:preview-row-id="previewRow?.id ?? null"
|
|
490
|
+
:preview-mode="!!previewEnabled"
|
|
491
|
+
@row-click="handleRowClick"
|
|
492
|
+
@loaded="handleLoaded"
|
|
493
|
+
@page-change="closePreview"
|
|
494
|
+
@per-page-change="closePreview"
|
|
495
|
+
>
|
|
496
|
+
<template v-for="(_, name) in forwardedSlots" #[name]="slotProps">
|
|
497
|
+
<slot :name="name" v-bind="slotProps ?? {}" />
|
|
498
|
+
</template>
|
|
499
|
+
</Table>
|
|
439
500
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
501
|
+
<!-- Preview panel overlay — slides in from right, tapa la tabla -->
|
|
502
|
+
<Transition
|
|
503
|
+
:enter-active-class="previewFromCache ? '' : 'transition ease-out duration-200'"
|
|
504
|
+
:enter-from-class="previewFromCache ? '' : 'opacity-0 translate-x-6'"
|
|
505
|
+
:enter-to-class="previewFromCache ? '' : 'opacity-100 translate-x-0'"
|
|
506
|
+
leave-active-class="transition ease-in duration-150"
|
|
507
|
+
leave-from-class="opacity-100 translate-x-0"
|
|
508
|
+
leave-to-class="opacity-0 translate-x-6"
|
|
509
|
+
>
|
|
510
|
+
<div
|
|
511
|
+
v-if="previewRow && previewEnabled"
|
|
512
|
+
ref="previewPanelRef"
|
|
513
|
+
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)]"
|
|
514
|
+
:style="{ width: (100 - currentRatio) + '%', bottom: paginationHeight + 'px' }"
|
|
515
|
+
>
|
|
516
|
+
<!-- Resize handle -->
|
|
517
|
+
<div
|
|
518
|
+
class="w-1 shrink-0 cursor-col-resize bg-surface hover:bg-indigo-300 dark:hover:bg-indigo-600 transition-colors"
|
|
519
|
+
@mousedown="startResize"
|
|
520
|
+
/>
|
|
521
|
+
<!-- Preview -->
|
|
522
|
+
<div class="flex flex-col flex-1 overflow-hidden">
|
|
523
|
+
|
|
524
|
+
<!-- Barra de acciones del preview -->
|
|
525
|
+
<div class="shrink-0 flex items-center justify-between gap-2 px-3 py-2 border-b border-card-line">
|
|
526
|
+
<div class="flex-1 min-w-0">
|
|
527
|
+
<slot name="preview-header" :row="previewRow" :close="closePreview" />
|
|
447
528
|
</div>
|
|
448
|
-
|
|
449
|
-
<!-- Tabs — bottom -->
|
|
450
|
-
<div v-if="hasHistory" class="shrink-0 flex border-t border-card-line">
|
|
529
|
+
<div class="flex items-center gap-1 shrink-0">
|
|
451
530
|
<button
|
|
452
531
|
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
|
-
]"
|
|
532
|
+
class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
533
|
+
title="Minimizar"
|
|
534
|
+
@click.stop="minimizePreview"
|
|
460
535
|
>
|
|
461
|
-
|
|
536
|
+
<IconMinus class="size-3.5" />
|
|
462
537
|
</button>
|
|
463
538
|
<button
|
|
464
539
|
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
|
-
]"
|
|
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"
|
|
475
543
|
>
|
|
476
|
-
|
|
544
|
+
<IconX class="size-3.5" />
|
|
477
545
|
</button>
|
|
478
546
|
</div>
|
|
547
|
+
</div>
|
|
479
548
|
|
|
549
|
+
<!-- Scrollable content -->
|
|
550
|
+
<div class="flex-1 overflow-y-auto min-h-0">
|
|
551
|
+
<slot v-if="previewTab === 'datos'" name="preview" :row="previewRow" :close="closePreview" />
|
|
552
|
+
<Table.PreviewTimeline
|
|
553
|
+
v-else-if="previewTab === 'bitacora' && resolvedHistoryEndpoint"
|
|
554
|
+
:endpoint="resolvedHistoryEndpoint"
|
|
555
|
+
/>
|
|
480
556
|
</div>
|
|
557
|
+
|
|
558
|
+
<!-- Tabs — bottom -->
|
|
559
|
+
<div v-if="hasHistory" class="shrink-0 flex border-t border-card-line">
|
|
560
|
+
<button
|
|
561
|
+
type="button"
|
|
562
|
+
@click="previewTab = 'datos'"
|
|
563
|
+
:class="[
|
|
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
|
+
]"
|
|
569
|
+
>
|
|
570
|
+
Datos
|
|
571
|
+
</button>
|
|
572
|
+
<button
|
|
573
|
+
type="button"
|
|
574
|
+
@click="resolvedHistoryEndpoint && (previewTab = 'bitacora')"
|
|
575
|
+
:disabled="!resolvedHistoryEndpoint"
|
|
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
|
+
]"
|
|
584
|
+
>
|
|
585
|
+
Bitácora
|
|
586
|
+
</button>
|
|
587
|
+
</div>
|
|
588
|
+
|
|
481
589
|
</div>
|
|
482
|
-
</
|
|
590
|
+
</div>
|
|
591
|
+
</Transition>
|
|
483
592
|
|
|
484
|
-
</div>
|
|
485
593
|
</div>
|
|
486
594
|
|
|
487
595
|
<!-- ── Floating mini-preview (dock expand, estilo Gmail) ── -->
|
|
@@ -524,6 +632,125 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
524
632
|
</Transition>
|
|
525
633
|
</Teleport>
|
|
526
634
|
|
|
635
|
+
<!-- Filter menu — teleported to body -->
|
|
636
|
+
<Teleport to="body">
|
|
637
|
+
<Transition
|
|
638
|
+
enter-active-class="transition ease-out duration-150"
|
|
639
|
+
enter-from-class="opacity-0 translate-y-1 scale-95"
|
|
640
|
+
enter-to-class="opacity-100 translate-y-0 scale-100"
|
|
641
|
+
leave-active-class="transition ease-in duration-100"
|
|
642
|
+
leave-from-class="opacity-100 translate-y-0 scale-100"
|
|
643
|
+
leave-to-class="opacity-0 translate-y-1 scale-95"
|
|
644
|
+
>
|
|
645
|
+
<div
|
|
646
|
+
v-if="showFilterPanel"
|
|
647
|
+
ref="filterMenuRef"
|
|
648
|
+
class="fixed z-[60] bg-dropdown border border-dropdown-line rounded-xl shadow-2xl min-w-52 overflow-hidden"
|
|
649
|
+
:style="filterMenuStyle"
|
|
650
|
+
>
|
|
651
|
+
|
|
652
|
+
<!-- Step 1: column picker -->
|
|
653
|
+
<template v-if="filterMenuStep === 'columns'">
|
|
654
|
+
<p class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest px-3 pt-2.5 pb-1">Filtrar por</p>
|
|
655
|
+
<div class="pb-1.5">
|
|
656
|
+
<button
|
|
657
|
+
v-for="col in filtersConfig"
|
|
658
|
+
:key="col.key"
|
|
659
|
+
type="button"
|
|
660
|
+
@click.stop="selectFilterColumn(col)"
|
|
661
|
+
class="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted-hover transition-colors"
|
|
662
|
+
>
|
|
663
|
+
<span class="flex-1 text-left text-foreground">{{ col.label }}</span>
|
|
664
|
+
<span v-if="activeFilters[col.key]" class="text-[10px] font-semibold text-indigo-500 uppercase">activo</span>
|
|
665
|
+
</button>
|
|
666
|
+
</div>
|
|
667
|
+
</template>
|
|
668
|
+
|
|
669
|
+
<!-- Step 2: value input -->
|
|
670
|
+
<template v-else-if="filterMenuStep === 'value' && pendingCol">
|
|
671
|
+
<div class="flex items-center gap-2 px-3 py-2 border-b border-card-line bg-surface">
|
|
672
|
+
<button
|
|
673
|
+
type="button"
|
|
674
|
+
@click.stop="filterMenuStep = 'columns'"
|
|
675
|
+
class="text-muted-foreground hover:text-foreground transition-colors"
|
|
676
|
+
>
|
|
677
|
+
<IconChevronLeft class="size-4" />
|
|
678
|
+
</button>
|
|
679
|
+
<span class="text-sm font-medium text-foreground">{{ pendingCol.label }}</span>
|
|
680
|
+
</div>
|
|
681
|
+
<div class="p-3 space-y-2.5">
|
|
682
|
+
|
|
683
|
+
<!-- text -->
|
|
684
|
+
<input
|
|
685
|
+
v-if="pendingCol.filterType === 'text'"
|
|
686
|
+
v-model="pendingValue"
|
|
687
|
+
type="text"
|
|
688
|
+
autofocus
|
|
689
|
+
@keydown.enter.stop="applyPendingFilter"
|
|
690
|
+
@keydown.escape.stop="closeFilterMenu"
|
|
691
|
+
placeholder="Buscar..."
|
|
692
|
+
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"
|
|
693
|
+
/>
|
|
694
|
+
|
|
695
|
+
<!-- select -->
|
|
696
|
+
<div v-else-if="pendingCol.filterType === 'select'" class="space-y-0.5">
|
|
697
|
+
<button
|
|
698
|
+
v-for="opt in pendingCol.filterOptions"
|
|
699
|
+
:key="opt.value"
|
|
700
|
+
type="button"
|
|
701
|
+
@click.stop="pendingValue = opt.value; applyPendingFilter()"
|
|
702
|
+
:class="[
|
|
703
|
+
'w-full flex items-center gap-2 px-2.5 py-1.5 text-sm rounded-lg transition-colors text-left',
|
|
704
|
+
pendingValue === opt.value
|
|
705
|
+
? 'bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-300'
|
|
706
|
+
: 'hover:bg-muted-hover text-foreground'
|
|
707
|
+
]"
|
|
708
|
+
>
|
|
709
|
+
<span class="flex-1">{{ opt.label }}</span>
|
|
710
|
+
<IconCheck v-if="pendingValue === opt.value" class="size-3.5 shrink-0 text-indigo-500" />
|
|
711
|
+
</button>
|
|
712
|
+
</div>
|
|
713
|
+
|
|
714
|
+
<!-- daterange -->
|
|
715
|
+
<div v-else-if="pendingCol.filterType === 'daterange'" class="space-y-2">
|
|
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>
|
|
732
|
+
<template v-if="pendingDateOp === 'between'">
|
|
733
|
+
<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" />
|
|
734
|
+
<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" />
|
|
735
|
+
</template>
|
|
736
|
+
<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" />
|
|
737
|
+
</div>
|
|
738
|
+
|
|
739
|
+
<button
|
|
740
|
+
v-if="pendingCol.filterType !== 'select'"
|
|
741
|
+
type="button"
|
|
742
|
+
@click.stop="applyPendingFilter"
|
|
743
|
+
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"
|
|
744
|
+
>
|
|
745
|
+
Aplicar
|
|
746
|
+
</button>
|
|
747
|
+
</div>
|
|
748
|
+
</template>
|
|
749
|
+
|
|
750
|
+
</div>
|
|
751
|
+
</Transition>
|
|
752
|
+
</Teleport>
|
|
753
|
+
|
|
527
754
|
<!-- Column panel — teleported to body to escape overflow-hidden -->
|
|
528
755
|
<Teleport to="body">
|
|
529
756
|
<Transition
|