@innertia-solutions/nuxt-theme-spark 0.1.140 → 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
  })
@@ -83,22 +103,54 @@ const activeFilterList = computed(() =>
83
103
 
84
104
  const activeFilterCount = computed(() => activeFilterList.value.length)
85
105
 
106
+ // Columns NOT yet filtered — what appears in the picker (already-active columns are hidden)
107
+ const availableFilterColumns = computed(() =>
108
+ filtersConfig.value.filter(col => {
109
+ const v = activeFilters.value[col.key]
110
+ if (!v) return true
111
+ if (col.filterType === 'daterange') return !v.from && !v.to
112
+ return !v.value
113
+ })
114
+ )
115
+
116
+ // Convert activeFilters to enriched [{field, operator, value}] for the backend DataTable
117
+ const enrichedFilters = computed(() => {
118
+ const result = []
119
+ for (const col of filtersConfig.value) {
120
+ const v = activeFilters.value[col.key]
121
+ if (!v) continue
122
+ if (col.filterType === 'daterange') {
123
+ if (!v.from && !v.to) continue
124
+ if (v.from) result.push({ field: col.key, operator: 'after', value: v.from })
125
+ if (v.to) result.push({ field: col.key, operator: 'before', value: v.to })
126
+ } else {
127
+ if (!v.value) continue
128
+ result.push({ field: col.key, operator: v.operator ?? 'contains', value: v.value })
129
+ }
130
+ }
131
+ return result
132
+ })
133
+
86
134
  const mergedParams = computed(() => ({
87
135
  ...props.params,
88
- ...activeFilters.value,
136
+ ...(enrichedFilters.value.length ? { filters: enrichedFilters.value } : {}),
89
137
  }))
90
138
 
91
139
  const removeFilter = (key) => {
92
140
  const u = { ...activeFilters.value }; delete u[key]; activeFilters.value = u
93
141
  }
94
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
+
95
148
  const openFilterMenu = async () => {
96
149
  filterMenuStep.value = 'columns'
97
150
  pendingCol.value = null
98
151
  showFilterPanel.value = true
99
152
  await nextTick()
100
- const rect = filterAddBtnRef.value?.getBoundingClientRect()
101
- if (rect) filterMenuStyle.value = { top: rect.bottom + 4 + 'px', left: rect.left + 'px' }
153
+ updateFilterMenuPosition()
102
154
  }
103
155
 
104
156
  const toggleFilterMenu = async () => {
@@ -121,7 +173,8 @@ const selectFilterColumn = (col) => {
121
173
  else if (existing?.to) { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: existing.to, from: '', to: '' } }
122
174
  else { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: '', from: '', to: '' } }
123
175
  } else {
124
- pendingValue.value = existing ?? ''
176
+ pendingOperator.value = existing?.operator ?? (col.filterType === 'select' ? 'is' : 'contains')
177
+ pendingValue.value = existing?.value ?? ''
125
178
  }
126
179
  filterMenuStep.value = 'value'
127
180
  }
@@ -135,9 +188,9 @@ const applyPendingFilter = () => {
135
188
  else if (pendingDateOp.value === 'after') v = { from: pendingValue.value.singleDate }
136
189
  else v = { to: pendingValue.value.singleDate }
137
190
  } else {
138
- v = pendingValue.value
191
+ v = { value: pendingValue.value, operator: pendingOperator.value }
139
192
  }
140
- activeFilters.value = { ...activeFilters.value, [col.key]: v || null }
193
+ activeFilters.value = { ...activeFilters.value, [col.key]: v }
141
194
  closeFilterMenu()
142
195
  }
143
196
 
@@ -156,8 +209,30 @@ const onFilterMenuOutsideClick = (e) => {
156
209
  }
157
210
  }
158
211
  watch(showFilterPanel, v => {
159
- if (v) document.addEventListener('mousedown', onFilterMenuOutsideClick)
160
- 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
161
236
  })
162
237
 
163
238
  // ─── Preview panel ─────────────────────────────────────────────────────────────
@@ -182,12 +257,19 @@ const resolvedHistoryEndpoint = computed(() => {
182
257
  return `history/${tableMeta.value.entity_type}/${previewRow.value.id}`
183
258
  })
184
259
 
185
- 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
+ })
186
267
 
187
268
  const handleRowClick = (row) => {
188
269
  if (previewEnabled.value) {
270
+ if (previewRow.value?.id === row.id) return // ya está abierto, no pestañear
189
271
  collapseDock()
190
- previewRow.value = previewRow.value?.id === row.id ? null : row
272
+ previewRow.value = row
191
273
  } else {
192
274
  emit('row-click', row)
193
275
  }
@@ -369,19 +451,22 @@ const onColumnPanelOutsideClick = (e) => {
369
451
  }
370
452
  }
371
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
+
372
459
  watch(showColumnPanel, async (v) => {
373
460
  if (v) {
374
461
  await nextTick()
375
- const rect = columnButtonRef.value?.getBoundingClientRect()
376
- if (rect) {
377
- columnPanelStyle.value = {
378
- top: rect.bottom + 6 + 'px',
379
- right: window.innerWidth - rect.right + 'px',
380
- }
381
- }
462
+ updateColumnPanelPosition()
382
463
  document.addEventListener('mousedown', onColumnPanelOutsideClick)
464
+ window.addEventListener('scroll', updateColumnPanelPosition, true)
465
+ window.addEventListener('resize', updateColumnPanelPosition)
383
466
  } else {
384
467
  document.removeEventListener('mousedown', onColumnPanelOutsideClick)
468
+ window.removeEventListener('scroll', updateColumnPanelPosition, true)
469
+ window.removeEventListener('resize', updateColumnPanelPosition)
385
470
  }
386
471
  })
387
472
 
@@ -413,7 +498,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
413
498
  :class="[
414
499
  'inline-flex items-center gap-1.5 py-1.5 px-3 text-sm font-medium rounded-lg border transition-colors',
415
500
  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'
501
+ ? 'border-primary/40 bg-primary/10 text-primary'
417
502
  : 'border-card-line bg-card text-muted-foreground-1 hover:bg-muted-hover'
418
503
  ]"
419
504
  >
@@ -425,23 +510,25 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
425
510
  <!-- Slot for custom toolbar buttons -->
426
511
  <slot name="toolbar" />
427
512
 
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>
513
+ <!-- Secondary actions: pushed to the right, icon-only style -->
514
+ <div class="ml-auto flex items-center gap-1">
515
+ <button
516
+ ref="columnButtonRef"
517
+ type="button"
518
+ @click="showColumnPanel = !showColumnPanel"
519
+ :title="'Columnas'"
520
+ :class="[
521
+ 'p-1.5 inline-flex items-center justify-center rounded-lg border transition-colors',
522
+ showColumnPanel
523
+ ? 'border-primary/40 bg-primary/10 text-primary'
524
+ : 'border-transparent text-muted-foreground hover:border-card-line hover:bg-muted-hover hover:text-foreground'
525
+ ]"
526
+ >
527
+ <IconLayoutColumns class="size-4" />
528
+ </button>
443
529
 
444
- <TableExportable v-if="showExport" :table-ref="tableRef" :name="resolvedName" :columns="columns" />
530
+ <TableExportable v-if="showExport" :table-ref="tableRef" :name="resolvedName" :columns="columns" />
531
+ </div>
445
532
  </div>
446
533
 
447
534
  <!-- Filter chips row (shown when filters active) -->
@@ -456,7 +543,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
456
543
  <button
457
544
  type="button"
458
545
  @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"
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"
460
547
  >
461
548
  {{ chip.displayVal }}
462
549
  <IconChevronDown class="size-3 opacity-60" />
@@ -471,10 +558,8 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
471
558
  </div>
472
559
  </div>
473
560
 
474
- <!-- Table + preview overlay inside a minimal border box -->
475
- <div class="relative overflow-hidden rounded-xl border border-card-line">
476
-
477
- <!-- Tabla -->
561
+ <!-- Tabla -->
562
+ <div class="overflow-hidden border-t border-b border-card-line">
478
563
  <Table
479
564
  ref="tableRef"
480
565
  :endpoint="resolvedEndpoint"
@@ -497,100 +582,134 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
497
582
  <slot :name="name" v-bind="slotProps ?? {}" />
498
583
  </template>
499
584
  </Table>
585
+ </div>
500
586
 
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"
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) + '%' }"
509
601
  >
602
+ <!-- Resize handle — thin pill on left edge -->
510
603
  <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">
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>
523
614
 
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" />
528
- </div>
529
- <div class="flex items-center gap-1 shrink-0">
530
- <button
531
- type="button"
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"
535
- >
536
- <IconMinus class="size-3.5" />
537
- </button>
538
- <button
539
- type="button"
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"
543
- >
544
- <IconX class="size-3.5" />
545
- </button>
546
- </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>
547
630
  </div>
548
631
 
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
- />
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>
556
642
  </div>
557
643
 
558
- <!-- Tabs bottom -->
559
- <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">
560
646
  <button
561
647
  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
- ]"
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"
569
650
  >
570
- Datos
651
+ <IconMinus class="size-3.5" />
571
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">
572
658
  <button
573
659
  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
- ]"
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"
584
662
  >
585
- Bitácora
663
+ <IconX class="size-3.5" />
586
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>
587
666
  </div>
588
667
 
589
668
  </div>
590
669
  </div>
591
- </Transition>
592
670
 
593
- </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>
594
713
 
595
714
  <!-- ── Floating mini-preview (dock expand, estilo Gmail) ── -->
596
715
  <Teleport to="body">
@@ -645,105 +764,139 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
645
764
  <div
646
765
  v-if="showFilterPanel"
647
766
  ref="filterMenuRef"
648
- 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"
649
768
  :style="filterMenuStyle"
650
769
  >
651
770
 
652
771
  <!-- Step 1: column picker -->
653
772
  <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>
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">
777
+ <template v-if="availableFilterColumns.length">
778
+ <button
779
+ v-for="col in availableFilterColumns"
780
+ :key="col.key"
781
+ type="button"
782
+ @click.stop="selectFilterColumn(col)"
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"
784
+ >
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>
789
+ </button>
790
+ </template>
791
+ <p v-else class="px-3 py-3 text-xs text-muted-foreground italic">
792
+ Todos los filtros están configurados
793
+ </p>
666
794
  </div>
667
795
  </template>
668
796
 
669
- <!-- Step 2: value input -->
797
+ <!-- Step 2: value config -->
670
798
  <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">
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">
672
802
  <button
673
803
  type="button"
674
804
  @click.stop="filterMenuStep = 'columns'"
675
- 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"
676
806
  >
677
807
  <IconChevronLeft class="size-4" />
678
808
  </button>
679
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>
680
846
  </div>
681
- <div class="p-3 space-y-2.5">
682
847
 
683
- <!-- text -->
848
+ <div class="p-3 min-w-56 space-y-2">
849
+
850
+ <!-- ── TEXT ── -->
684
851
  <input
685
852
  v-if="pendingCol.filterType === 'text'"
853
+ ref="pendingValueInputRef"
686
854
  v-model="pendingValue"
687
855
  type="text"
688
- autofocus
689
856
  @keydown.enter.stop="applyPendingFilter"
690
857
  @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"
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"
693
860
  />
694
861
 
695
- <!-- select -->
696
- <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">
697
864
  <button
698
865
  v-for="opt in pendingCol.filterOptions"
699
866
  :key="opt.value"
700
867
  type="button"
701
868
  @click.stop="pendingValue = opt.value; applyPendingFilter()"
702
869
  :class="[
703
- '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',
704
871
  pendingValue === opt.value
705
- ? 'bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-300'
872
+ ? 'bg-primary/10 text-primary'
706
873
  : 'hover:bg-muted-hover text-foreground'
707
874
  ]"
708
875
  >
709
876
  <span class="flex-1">{{ opt.label }}</span>
710
- <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" />
711
878
  </button>
712
879
  </div>
713
880
 
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>
881
+ <!-- ── DATERANGE ── -->
882
+ <template v-else-if="pendingCol.filterType === 'daterange'">
732
883
  <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" />
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" />
735
886
  </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>
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>
738
889
 
890
+ <!-- Apply (not for select — auto-applies on click) -->
739
891
  <button
740
892
  v-if="pendingCol.filterType !== 'select'"
741
893
  type="button"
742
894
  @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"
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"
744
896
  >
745
897
  Aplicar
746
898
  </button>
899
+
747
900
  </div>
748
901
  </template>
749
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.140",
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",