@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.
@@ -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, default: 60 },
20
- autoClosePreview: { type: Boolean, default: true },
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
- displayOp = 'es'
76
- displayVal = col.filterOptions?.find(o => o.value === v)?.label ?? v
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
- displayOp = 'contiene'; displayVal = v
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 (col.filterType === 'daterange') return !v?.from && !v?.to
91
- return v === null || v === undefined || v === ''
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 === null || v === undefined || v === '') continue
121
+ if (!v) continue
101
122
  if (col.filterType === 'daterange') {
102
- if (!v?.from && !v?.to) continue
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
- result.push({ field: col.key, operator: 'contains', value: v })
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
- const rect = filterAddBtnRef.value?.getBoundingClientRect()
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
- pendingValue.value = existing ?? ''
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 || null }
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) document.addEventListener('mousedown', onFilterMenuOutsideClick)
188
- else document.removeEventListener('mousedown', onFilterMenuOutsideClick)
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, () => { previewTab.value = 'datos' })
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 = previewRow.value?.id === row.id ? null : row
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
- const rect = columnButtonRef.value?.getBoundingClientRect()
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-indigo-300 bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:border-indigo-700 dark:text-indigo-300'
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-indigo-300 bg-indigo-50 text-indigo-600 dark:bg-indigo-900/20 dark:border-indigo-700 dark:text-indigo-300'
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-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"
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
- <!-- Table + preview overlay inside a minimal border box -->
505
- <div class="relative overflow-hidden rounded-xl border border-card-line">
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
- <!-- 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"
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
- 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">
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
- <!-- 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" />
558
- </div>
559
- <div class="flex items-center gap-1 shrink-0">
560
- <button
561
- type="button"
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"
565
- >
566
- <IconMinus class="size-3.5" />
567
- </button>
568
- <button
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
- <!-- 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
- />
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
- <!-- Tabs bottom -->
589
- <div v-if="hasHistory" class="shrink-0 flex border-t border-card-line">
644
+ <!-- Minimizar (siempre) -->
645
+ <div class="hs-tooltip [--placement:top] inline-block">
590
646
  <button
591
647
  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
- ]"
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
- Datos
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
- @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
- ]"
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
- Bitácora
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
- </div>
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 min-w-52 overflow-hidden"
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 (only non-filtered columns shown) -->
771
+ <!-- Step 1: column picker -->
683
772
  <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">
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-2 px-3 py-2 text-sm hover:bg-muted-hover transition-colors text-left text-foreground"
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 input -->
797
+ <!-- Step 2: value config -->
704
798
  <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">
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
- <!-- text -->
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="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"
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
- <!-- select -->
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-1.5 text-sm rounded-lg transition-colors text-left',
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-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-300'
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-indigo-500" />
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
- <!-- 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>
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-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" />
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-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>
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 text-center rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition-colors"
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
 
@@ -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 }, // [{ key, label, sortable?, filterable?, class? }]
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' }, // 'table' | 'grid'
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-card">
478
+ <thead class="relative z-20 bg-background">
498
479
  <template v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
499
- <!-- Main header row -->
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-indigo-50 dark:bg-indigo-900/20' : '',
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-blue-900 focus:ring-0 focus:ring-offset-0 dark:bg-card"
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-indigo-400 dark:bg-indigo-500 !w-0.5'
556
- : 'bg-surface-1 group-hover/rz:bg-indigo-300 dark:group-hover/rz:bg-indigo-600 group-hover/rz:w-0.5'"
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="divide-x divide-card-line border-b border-card-line bg-muted/50"
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-indigo-500/20 focus:border-indigo-400 dark:focus:border-indigo-500 outline-none transition-all"
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 divide-x divide-card-line bg-card"
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: pad to pageSize so table height doesn't change -->
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="divide-x divide-card-line bg-card"
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: maintain table height when no results -->
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="divide-x divide-card-line bg-card"
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="divide-x divide-card-line bg-card hover:bg-layer-hover transition-colors"
615
+ class="bg-background hover:bg-layer-hover transition-colors"
642
616
  :class="{
643
617
  'cursor-pointer': isRowClickEnabled,
644
- 'bg-indigo-50/40 dark:bg-indigo-900/10 hover:bg-indigo-50/60': row.getIsSelected(),
645
- '!bg-indigo-50 dark:!bg-indigo-900/20 ring-1 ring-inset ring-indigo-200 dark:ring-indigo-700': previewRowId && row.original.id === previewRowId,
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: pad table to full page height when data < perPage -->
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="divide-x divide-card-line bg-card"
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-card/60 rounded-xl"
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-card/60 rounded-xl"
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-indigo-400 dark:ring-indigo-600': row.getIsSelected() }">
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 & controls bar -->
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
- <!-- Per page selector -->
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-indigo-500/20 py-1 px-2"
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-indigo-500 font-bold hover:underline">Volver</button>
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 = computed(() => selectedColumns.value.length === props.columns.length)
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 = ref(null)
46
+ const panelRef = ref(null)
47
47
  const triggerRef = ref(null)
48
48
 
49
49
  const onOutsideClick = (e) => {
50
50
  if (
51
- panelRef.value && !panelRef.value.contains(e.target) &&
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
- 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
76
+ 'p-1.5 inline-flex items-center justify-center rounded-lg border transition-colors',
74
77
  isOpen
75
- ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
76
- : 'border-card-line bg-card text-muted-foreground-1 hover:bg-muted-hover'
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-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
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-blue-500 min-w-0"
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-blue-600 dark:text-blue-400 hover:underline">
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-blue-600 hover:bg-blue-700 text-white text-sm font-medium transition-colors inline-flex items-center justify-center gap-2"
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innertia-solutions/nuxt-theme-spark",
3
- "version": "0.1.141",
3
+ "version": "0.1.142",
4
4
  "description": "Innertia Solutions — Spark theme: backoffice, landing and mobile components and layouts",
5
5
  "keywords": [
6
6
  "nuxt",