@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.
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { IconSearch, IconAdjustmentsHorizontal, IconLayoutColumns, IconGripVertical, IconMinus, IconMaximize, IconX } from '@tabler/icons-vue'
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 = ref('')
33
+ const search = ref('')
34
34
  const activeFilters = ref({})
35
- const showFilterPanel = ref(false)
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
- const activeFilterCount = computed(() =>
47
- Object.values(activeFilters.value).filter(v => v !== null && v !== undefined && v !== '').length
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
- <!-- Card único -->
302
- <div class="bg-card border border-card-line rounded-2xl shadow-sm overflow-hidden">
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
- <div v-if="showFilters && hasFilterableColumns" class="relative">
311
- <button
312
- type="button"
313
- @click="showFilterPanel = !showFilterPanel"
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="showColumnPanel = !showColumnPanel"
412
+ @click="toggleFilterMenu"
351
413
  :class="[
352
- 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
353
- showColumnPanel
354
- ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
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
- <IconLayoutColumns class="size-4" />
359
- Columnas
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
- <!-- Contenido: tabla siempre full width + preview overlay -->
366
- <div class="relative overflow-hidden">
367
-
368
- <!-- Tabla -->
369
- <Table
370
- ref="tableRef"
371
- :endpoint="resolvedEndpoint"
372
- :columns="columns"
373
- :name="resolvedName"
374
- :params="mergedParams"
375
- :search="search"
376
- :checkable="checkable"
377
- :cached="cached"
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"
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
- <template v-for="(_, name) in forwardedSlots" #[name]="slotProps">
388
- <slot :name="name" v-bind="slotProps ?? {}" />
389
- </template>
390
- </Table>
391
-
392
- <!-- Preview panel overlay — slides in from right, tapa la tabla -->
393
- <Transition
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
- <div
402
- v-if="previewRow && previewEnabled"
403
- ref="previewPanelRef"
404
- 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)]"
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
- <!-- Barra de acciones del preview -->
416
- <div class="shrink-0 flex items-center justify-between gap-2 px-3 py-2 border-b border-card-line">
417
- <div class="flex-1 min-w-0">
418
- <slot name="preview-header" :row="previewRow" :close="closePreview" />
419
- </div>
420
- <div class="flex items-center gap-1 shrink-0">
421
- <button
422
- type="button"
423
- class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
424
- title="Minimizar"
425
- @click.stop="minimizePreview"
426
- >
427
- <IconMinus class="size-3.5" />
428
- </button>
429
- <button
430
- type="button"
431
- class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
432
- title="Cerrar"
433
- @click.stop="closePreview"
434
- >
435
- <IconX class="size-3.5" />
436
- </button>
437
- </div>
438
- </div>
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
- <!-- Scrollable content -->
441
- <div class="flex-1 overflow-y-auto min-h-0">
442
- <slot v-if="previewTab === 'datos'" name="preview" :row="previewRow" :close="closePreview" />
443
- <Table.PreviewTimeline
444
- v-else-if="previewTab === 'bitacora' && resolvedHistoryEndpoint"
445
- :endpoint="resolvedHistoryEndpoint"
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
- @click="previewTab = 'datos'"
454
- :class="[
455
- 'flex-1 py-2.5 text-xs font-semibold transition-colors border-r border-card-line border-t-2 -mt-px',
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
- Datos
536
+ <IconMinus class="size-3.5" />
462
537
  </button>
463
538
  <button
464
539
  type="button"
465
- @click="resolvedHistoryEndpoint && (previewTab = 'bitacora')"
466
- :disabled="!resolvedHistoryEndpoint"
467
- :class="[
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
- Bitácora
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
- </Transition>
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innertia-solutions/nuxt-theme-spark",
3
- "version": "0.1.139",
3
+ "version": "0.1.140",
4
4
  "description": "Innertia Solutions — Spark theme: backoffice, landing and mobile components and layouts",
5
5
  "keywords": [
6
6
  "nuxt",