@innertia-solutions/nuxt-theme-spark 0.1.141 → 0.1.143

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,29 +1,45 @@
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
+ import Table from './index.vue'
3
4
 
4
5
  const props = defineProps({
5
- table: { type: Object, default: null },
6
- endpoint: { type: String, default: '' },
7
- columns: { type: Array, required: true },
8
- name: { type: String, default: '' },
9
- params: { type: Object, default: () => ({}) },
10
- checkable: { type: Boolean, default: false },
11
- cached: { type: Boolean, default: false },
12
- showReloadButton: { type: Boolean, default: true },
13
- clickRowToOpen: { type: Boolean, default: false },
14
- searchPlaceholder: { type: String, default: 'Buscar...' },
15
- showSearch: { type: Boolean, default: true },
16
- showFilters: { type: Boolean, default: true },
17
- showExport: { type: Boolean, default: true },
18
- filters: { type: Array, default: () => [] },
19
- splitRatio: { type: Number, default: 60 },
20
- autoClosePreview: { type: Boolean, default: true },
6
+ table: { type: Object, default: null },
7
+ endpoint: { type: String, default: '' },
8
+ columns: { type: Array, required: true },
9
+ name: { type: String, default: '' },
10
+ params: { type: Object, default: () => ({}) },
11
+ checkable: { type: Boolean, default: false },
12
+ cached: { type: Boolean, default: false },
13
+ showReloadButton: { type: Boolean, default: true },
14
+ clickRowToOpen: { type: Boolean, default: false },
15
+ searchPlaceholder: { type: String, default: 'Buscar...' },
16
+ showSearch: { type: Boolean, default: true },
17
+ showFilters: { type: Boolean, default: true },
18
+ showExport: { type: Boolean, default: true },
19
+ filters: { type: Array, default: () => [] },
20
+ splitRatio: { type: Number, default: 60 },
21
+ autoClosePreview: { type: Boolean, default: true },
22
+ previewHref: { type: [String, Function], default: null }, // url fija o (row) => url
23
+ previewDeletable: { type: Boolean, default: false },
24
+ defaultPinnedColumns: { type: Object, default: null }, // { left?: string[], right?: string[] }
25
+ persistPreferences: { type: Boolean, default: true }, // persist column prefs in backend
21
26
  })
22
27
 
23
28
  const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
24
29
  const resolvedName = computed(() => props.table?.name ?? props.name)
25
30
 
26
- const emit = defineEmits(['row-click', 'loaded'])
31
+ // ─── Table preferences (column pinning, visibility, order) ───────────────────
32
+ const tablePrefName = computed(() => resolvedName.value || 'default')
33
+ const { preferences: tablePrefs, load: loadPrefs, save: savePrefs } = useTablePreferences(tablePrefName.value)
34
+
35
+ // Resolved initial pinned columns: merge defaultPinnedColumns with saved preferences
36
+ const resolvedPinnedColumns = computed(() => {
37
+ const saved = tablePrefs.value.pinning
38
+ if (saved) return saved
39
+ return props.defaultPinnedColumns ?? null
40
+ })
41
+
42
+ const emit = defineEmits(['row-click', 'loaded', 'preview-delete'])
27
43
  const slots = useSlots()
28
44
  const forwardedSlots = computed(() => {
29
45
  const excluded = new Set(['toolbar', 'preview'])
@@ -33,6 +49,7 @@ const forwardedSlots = computed(() => {
33
49
  const search = ref('')
34
50
  const activeFilters = ref({})
35
51
  const tableRef = ref(null)
52
+ const prefsLoaded = ref(false)
36
53
 
37
54
  // ─── Filter config ─────────────────────────────────────────────────────────────
38
55
  const filtersConfig = computed(() =>
@@ -51,6 +68,20 @@ const filterMenuRef = ref(null)
51
68
  const filterAddBtnRef = ref(null)
52
69
  const filterMenuStyle = ref({})
53
70
 
71
+ const pendingOperator = ref('contains')
72
+ const pendingValueInputRef = ref(null)
73
+
74
+ const textOps = [
75
+ { value: 'contains', label: 'contiene' },
76
+ { value: 'starts_with', label: 'empieza con' },
77
+ { value: 'equals', label: 'es igual a' },
78
+ ]
79
+
80
+ const selectOps = [
81
+ { value: 'is', label: 'es' },
82
+ { value: 'is_not', label: 'no es' },
83
+ ]
84
+
54
85
  const dateOps = [
55
86
  { value: 'before', label: 'antes de' },
56
87
  { value: 'after', label: 'después de' },
@@ -61,8 +92,9 @@ const activeFilterList = computed(() =>
61
92
  filtersConfig.value
62
93
  .filter(col => {
63
94
  const v = activeFilters.value[col.key]
95
+ if (!v) return false
64
96
  if (col.filterType === 'daterange') return v?.from || v?.to
65
- return v !== null && v !== undefined && v !== ''
97
+ return v?.value !== null && v?.value !== undefined && v?.value !== ''
66
98
  })
67
99
  .map(col => {
68
100
  const v = activeFilters.value[col.key]
@@ -72,10 +104,13 @@ const activeFilterList = computed(() =>
72
104
  else if (v.from) { displayOp = 'después de'; displayVal = v.from }
73
105
  else { displayOp = 'antes de'; displayVal = v.to }
74
106
  } else if (col.filterType === 'select') {
75
- displayOp = 'es'
76
- displayVal = col.filterOptions?.find(o => o.value === v)?.label ?? v
107
+ const op = selectOps.find(o => o.value === v.operator) ?? selectOps[0]
108
+ displayOp = op.label
109
+ displayVal = col.filterOptions?.find(o => o.value === v.value)?.label ?? v.value
77
110
  } else {
78
- displayOp = 'contiene'; displayVal = v
111
+ const op = textOps.find(o => o.value === v.operator) ?? textOps[0]
112
+ displayOp = op.label
113
+ displayVal = v.value
79
114
  }
80
115
  return { key: col.key, label: col.label, displayOp, displayVal, col }
81
116
  })
@@ -87,8 +122,9 @@ const activeFilterCount = computed(() => activeFilterList.value.length)
87
122
  const availableFilterColumns = computed(() =>
88
123
  filtersConfig.value.filter(col => {
89
124
  const v = activeFilters.value[col.key]
90
- if (col.filterType === 'daterange') return !v?.from && !v?.to
91
- return v === null || v === undefined || v === ''
125
+ if (!v) return true
126
+ if (col.filterType === 'daterange') return !v.from && !v.to
127
+ return !v.value
92
128
  })
93
129
  )
94
130
 
@@ -97,15 +133,14 @@ const enrichedFilters = computed(() => {
97
133
  const result = []
98
134
  for (const col of filtersConfig.value) {
99
135
  const v = activeFilters.value[col.key]
100
- if (v === null || v === undefined || v === '') continue
136
+ if (!v) continue
101
137
  if (col.filterType === 'daterange') {
102
- if (!v?.from && !v?.to) continue
138
+ if (!v.from && !v.to) continue
103
139
  if (v.from) result.push({ field: col.key, operator: 'after', value: v.from })
104
140
  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
141
  } else {
108
- result.push({ field: col.key, operator: 'contains', value: v })
142
+ if (!v.value) continue
143
+ result.push({ field: col.key, operator: v.operator ?? 'contains', value: v.value })
109
144
  }
110
145
  }
111
146
  return result
@@ -120,13 +155,17 @@ const removeFilter = (key) => {
120
155
  const u = { ...activeFilters.value }; delete u[key]; activeFilters.value = u
121
156
  }
122
157
 
158
+ const updateFilterMenuPosition = () => {
159
+ const rect = filterAddBtnRef.value?.getBoundingClientRect()
160
+ if (rect) filterMenuStyle.value = { top: rect.bottom + 4 + 'px', left: rect.left + 'px' }
161
+ }
162
+
123
163
  const openFilterMenu = async () => {
124
164
  filterMenuStep.value = 'columns'
125
165
  pendingCol.value = null
126
166
  showFilterPanel.value = true
127
167
  await nextTick()
128
- const rect = filterAddBtnRef.value?.getBoundingClientRect()
129
- if (rect) filterMenuStyle.value = { top: rect.bottom + 4 + 'px', left: rect.left + 'px' }
168
+ updateFilterMenuPosition()
130
169
  }
131
170
 
132
171
  const toggleFilterMenu = async () => {
@@ -149,7 +188,8 @@ const selectFilterColumn = (col) => {
149
188
  else if (existing?.to) { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: existing.to, from: '', to: '' } }
150
189
  else { pendingDateOp.value = 'before'; pendingValue.value = { singleDate: '', from: '', to: '' } }
151
190
  } else {
152
- pendingValue.value = existing ?? ''
191
+ pendingOperator.value = existing?.operator ?? (col.filterType === 'select' ? 'is' : 'contains')
192
+ pendingValue.value = existing?.value ?? ''
153
193
  }
154
194
  filterMenuStep.value = 'value'
155
195
  }
@@ -163,9 +203,9 @@ const applyPendingFilter = () => {
163
203
  else if (pendingDateOp.value === 'after') v = { from: pendingValue.value.singleDate }
164
204
  else v = { to: pendingValue.value.singleDate }
165
205
  } else {
166
- v = pendingValue.value
206
+ v = { value: pendingValue.value, operator: pendingOperator.value }
167
207
  }
168
- activeFilters.value = { ...activeFilters.value, [col.key]: v || null }
208
+ activeFilters.value = { ...activeFilters.value, [col.key]: v }
169
209
  closeFilterMenu()
170
210
  }
171
211
 
@@ -184,8 +224,30 @@ const onFilterMenuOutsideClick = (e) => {
184
224
  }
185
225
  }
186
226
  watch(showFilterPanel, v => {
187
- if (v) document.addEventListener('mousedown', onFilterMenuOutsideClick)
188
- else document.removeEventListener('mousedown', onFilterMenuOutsideClick)
227
+ if (v) {
228
+ document.addEventListener('mousedown', onFilterMenuOutsideClick)
229
+ window.addEventListener('scroll', updateFilterMenuPosition, true)
230
+ window.addEventListener('resize', updateFilterMenuPosition)
231
+ } else {
232
+ document.removeEventListener('mousedown', onFilterMenuOutsideClick)
233
+ window.removeEventListener('scroll', updateFilterMenuPosition, true)
234
+ window.removeEventListener('resize', updateFilterMenuPosition)
235
+ }
236
+ })
237
+
238
+ watch(filterMenuStep, async (step) => {
239
+ if (step === 'value' && pendingCol.value?.filterType === 'text') {
240
+ await nextTick()
241
+ pendingValueInputRef.value?.focus()
242
+ }
243
+ })
244
+
245
+ // ─── Preview href helper ───────────────────────────────────────────────────────
246
+ const resolvedPreviewHref = computed(() => {
247
+ if (!props.previewHref || !previewRow.value) return null
248
+ return typeof props.previewHref === 'function'
249
+ ? props.previewHref(previewRow.value)
250
+ : props.previewHref
189
251
  })
190
252
 
191
253
  // ─── Preview panel ─────────────────────────────────────────────────────────────
@@ -210,12 +272,19 @@ const resolvedHistoryEndpoint = computed(() => {
210
272
  return `history/${tableMeta.value.entity_type}/${previewRow.value.id}`
211
273
  })
212
274
 
213
- watch(previewRow, () => { previewTab.value = 'datos' })
275
+ watch(previewRow, async (row) => {
276
+ previewTab.value = 'datos'
277
+ if (row) {
278
+ await nextTick()
279
+ if (typeof window !== 'undefined') window.HSStaticMethods?.autoInit?.(['Tooltip'])
280
+ }
281
+ })
214
282
 
215
283
  const handleRowClick = (row) => {
216
284
  if (previewEnabled.value) {
285
+ if (previewRow.value?.id === row.id) return // ya está abierto, no pestañear
217
286
  collapseDock()
218
- previewRow.value = previewRow.value?.id === row.id ? null : row
287
+ previewRow.value = row
219
288
  } else {
220
289
  emit('row-click', row)
221
290
  }
@@ -348,6 +417,24 @@ onMounted(async () => {
348
417
  }
349
418
  } catch {}
350
419
  }
420
+
421
+ // Load column preferences from backend
422
+ if (props.persistPreferences && resolvedName.value) {
423
+ await loadPrefs()
424
+ // Apply saved visibility
425
+ if (tablePrefs.value.visibility && tableRef.value?.table) {
426
+ tableRef.value.table.setColumnVisibility(tablePrefs.value.visibility)
427
+ }
428
+ // Apply saved column order
429
+ if (tablePrefs.value.order?.length && tableRef.value?.setColumnOrder) {
430
+ tableRef.value.setColumnOrder(tablePrefs.value.order)
431
+ }
432
+ // Apply saved pinning
433
+ if (tablePrefs.value.pinning && tableRef.value?.table) {
434
+ tableRef.value.table.setColumnPinning(tablePrefs.value.pinning)
435
+ }
436
+ }
437
+ prefsLoaded.value = true
351
438
  })
352
439
  onBeforeUnmount(() => {
353
440
  window.removeEventListener('keydown', onEsc)
@@ -368,24 +455,105 @@ const orderedColumns = computed(() => {
368
455
  })
369
456
 
370
457
  let draggedKey = null
371
- const dragOverKey = ref(null)
458
+ let draggedFromSection = null // 'left' | 'center' | 'right'
459
+ const dragOverKey = ref(null)
460
+ const dragOverSection = ref(null)
461
+
462
+ // ─── Columns grouped by pinning section ───────────────────────────────────────
463
+ const columnsBySection = computed(() => {
464
+ // reactive dependency on pinning state
465
+ const _pin = tableRef.value?.columnPinning?.value
466
+ const cols = orderedColumns.value
467
+ if (!tableRef.value?.table) return { left: [], center: cols, right: [] }
468
+
469
+ const left = [], center = [], right = []
470
+ for (const col of cols) {
471
+ const pinned = tableRef.value.table.getColumn(col.key)?.getIsPinned()
472
+ if (pinned === 'left') left.push(col)
473
+ else if (pinned === 'right') right.push(col)
474
+ else center.push(col)
475
+ }
476
+ return { left, center, right }
477
+ })
372
478
 
373
- const onDragStart = (key) => { draggedKey = key }
374
- const onDragOver = (e, key) => { e.preventDefault(); dragOverKey.value = key }
375
- const onDragLeave = () => { dragOverKey.value = null }
376
- const onDrop = (key) => {
377
- if (!draggedKey || draggedKey === key) return
378
- const ids = tableRef.value?.table.getAllLeafColumns().map(c => c.id) ?? []
379
- const from = ids.indexOf(draggedKey)
380
- const to = ids.indexOf(key)
381
- if (from < 0 || to < 0) return
382
- ids.splice(from, 1)
383
- ids.splice(to, 0, draggedKey)
384
- const selIdx = ids.indexOf('select')
385
- if (selIdx > 0) { ids.splice(selIdx, 1); ids.unshift('select') }
386
- tableRef.value?.setColumnOrder(ids)
479
+ const resetColDrag = () => {
387
480
  draggedKey = null
481
+ draggedFromSection = null
388
482
  dragOverKey.value = null
483
+ dragOverSection.value = null
484
+ }
485
+
486
+ const onDragStart = (key, section) => { draggedKey = key; draggedFromSection = section }
487
+ const onDragLeave = () => { dragOverKey.value = null }
488
+
489
+ // Auto-pin anchor columns when any column enters/leaves a pinned section:
490
+ // Left section → checkbox (select) is always pinned left
491
+ // Right section → actions column is always pinned right
492
+ // Called AFTER pinColumn(draggedKey) so columnsBySection reflects the new state.
493
+ const enforceAnchorPins = (targetSection) => {
494
+ const t = tableRef.value
495
+ if (!t) return
496
+ const from = draggedFromSection // still valid before resetColDrag()
497
+
498
+ // ─── Left anchor: select checkbox ────────────────────────────────────────────
499
+ // Order (select always first) is enforced by a watch in Table/index.vue
500
+ if (props.checkable && t.table?.getColumn('select')) {
501
+ if (targetSection === 'left') {
502
+ t.pinColumn('select', 'left')
503
+ } else if (from === 'left' && columnsBySection.value.left.length === 0) {
504
+ t.pinColumn('select', false)
505
+ }
506
+ }
507
+
508
+ // ─── Right anchor: actions ────────────────────────────────────────────────────
509
+ // 'actions' has label:'' so it's excluded from orderedColumns/columnsBySection.
510
+ // We only auto-pin it if it exists in the columns definition.
511
+ const hasActions = props.columns.some(c => c.key === 'actions')
512
+ if (hasActions) {
513
+ if (targetSection === 'right') {
514
+ // Something was pinned right → force-pin actions too
515
+ t.pinColumn('actions', 'right')
516
+ } else if (from === 'right') {
517
+ // Something left the right section → unpin actions if no more right columns
518
+ if (columnsBySection.value.right.length === 0) t.pinColumn('actions', false)
519
+ }
520
+ }
521
+ }
522
+
523
+ // Drop on a specific column row (handles both reorder + section change)
524
+ const onDrop = (targetKey, targetSection) => {
525
+ if (!draggedKey) { resetColDrag(); return }
526
+
527
+ if (draggedFromSection !== targetSection) {
528
+ // Change pinning
529
+ const pinVal = targetSection === 'left' ? 'left' : targetSection === 'right' ? 'right' : false
530
+ tableRef.value?.pinColumn(draggedKey, pinVal)
531
+ enforceAnchorPins(targetSection)
532
+ persistCurrentPrefs()
533
+ } else if (draggedKey !== targetKey) {
534
+ // Reorder within section
535
+ const ids = tableRef.value?.table.getAllLeafColumns().map(c => c.id) ?? []
536
+ const from = ids.indexOf(draggedKey)
537
+ const to = ids.indexOf(targetKey)
538
+ if (from >= 0 && to >= 0) {
539
+ ids.splice(from, 1)
540
+ ids.splice(to, 0, draggedKey)
541
+ const selIdx = ids.indexOf('select')
542
+ if (selIdx > 0) { ids.splice(selIdx, 1); ids.unshift('select') }
543
+ tableRef.value?.setColumnOrder(ids)
544
+ }
545
+ }
546
+ resetColDrag()
547
+ }
548
+
549
+ // Drop on the section zone itself (empty area) — only changes pinning
550
+ const onDropSection = (targetSection) => {
551
+ if (!draggedKey || draggedFromSection === targetSection) { resetColDrag(); return }
552
+ const pinVal = targetSection === 'left' ? 'left' : targetSection === 'right' ? 'right' : false
553
+ tableRef.value?.pinColumn(draggedKey, pinVal)
554
+ enforceAnchorPins(targetSection)
555
+ persistCurrentPrefs()
556
+ resetColDrag()
389
557
  }
390
558
 
391
559
  const onColumnPanelOutsideClick = (e) => {
@@ -397,29 +565,74 @@ const onColumnPanelOutsideClick = (e) => {
397
565
  }
398
566
  }
399
567
 
568
+ const updateColumnPanelPosition = () => {
569
+ const rect = columnButtonRef.value?.getBoundingClientRect()
570
+ if (rect) columnPanelStyle.value = { top: rect.bottom + 6 + 'px', right: window.innerWidth - rect.right + 'px' }
571
+ }
572
+
400
573
  watch(showColumnPanel, async (v) => {
401
574
  if (v) {
402
575
  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
- }
576
+ updateColumnPanelPosition()
410
577
  document.addEventListener('mousedown', onColumnPanelOutsideClick)
578
+ window.addEventListener('scroll', updateColumnPanelPosition, true)
579
+ window.addEventListener('resize', updateColumnPanelPosition)
411
580
  } else {
412
581
  document.removeEventListener('mousedown', onColumnPanelOutsideClick)
582
+ window.removeEventListener('scroll', updateColumnPanelPosition, true)
583
+ window.removeEventListener('resize', updateColumnPanelPosition)
413
584
  }
414
585
  })
415
586
 
587
+ // ─── Persist column preferences when they change ─────────────────────────────
588
+ const persistCurrentPrefs = () => {
589
+ if (!props.persistPreferences || !resolvedName.value || !prefsLoaded.value || !tableRef.value) return
590
+ const tanTable = tableRef.value.table
591
+ if (!tanTable) return
592
+
593
+ const visibility = Object.fromEntries(
594
+ tanTable.getAllLeafColumns()
595
+ .filter(c => c.id !== 'select')
596
+ .map(c => [c.id, c.getIsVisible()])
597
+ )
598
+ const order = tanTable.getAllLeafColumns().map(c => c.id).filter(id => id !== 'select')
599
+ const rawPinning = tableRef.value.columnPinning?.value ?? tanTable.getState().columnPinning
600
+ const pinning = rawPinning
601
+ ? { left: rawPinning.left ?? [], right: rawPinning.right ?? [] }
602
+ : { left: [], right: [] }
603
+
604
+ savePrefs({ visibility, order, pinning })
605
+ }
606
+
607
+ // Watch column pinning changes via tableRef
608
+ watch(
609
+ () => tableRef.value?.columnPinning?.value,
610
+ () => { if (prefsLoaded.value) persistCurrentPrefs() },
611
+ { deep: true }
612
+ )
613
+
614
+ // Watch column visibility changes
615
+ watch(
616
+ () => tableRef.value?.table?.getState()?.columnVisibility,
617
+ () => { if (prefsLoaded.value) persistCurrentPrefs() },
618
+ { deep: true }
619
+ )
620
+
621
+ // Watch column order changes
622
+ watch(
623
+ () => tableRef.value?.table?.getState()?.columnOrder,
624
+ () => { if (prefsLoaded.value) persistCurrentPrefs() },
625
+ { deep: true }
626
+ )
627
+
416
628
  // ─── Expose ───────────────────────────────────────────────────────────────────
417
629
  const getSelectedRows = () => tableRef.value?.getSelectedRows()
418
630
  const reload = () => tableRef.value?.reload()
419
631
  const clearCache = () => tableRef.value?.clearCache()
420
632
  const exportTable = (format, allPages, filteredRows) => tableRef.value?.exportTable(format, allPages, filteredRows)
633
+ const pinColumn = (key, position) => tableRef.value?.pinColumn(key, position)
421
634
 
422
- defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, closePreview })
635
+ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, closePreview, pinColumn })
423
636
  </script>
424
637
 
425
638
  <template>
@@ -441,7 +654,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
441
654
  :class="[
442
655
  'inline-flex items-center gap-1.5 py-1.5 px-3 text-sm font-medium rounded-lg border transition-colors',
443
656
  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'
657
+ ? 'border-primary/40 bg-primary/10 text-primary'
445
658
  : 'border-card-line bg-card text-muted-foreground-1 hover:bg-muted-hover'
446
659
  ]"
447
660
  >
@@ -463,7 +676,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
463
676
  :class="[
464
677
  'p-1.5 inline-flex items-center justify-center rounded-lg border transition-colors',
465
678
  showColumnPanel
466
- ? 'border-indigo-300 bg-indigo-50 text-indigo-600 dark:bg-indigo-900/20 dark:border-indigo-700 dark:text-indigo-300'
679
+ ? 'border-primary/40 bg-primary/10 text-primary'
467
680
  : 'border-transparent text-muted-foreground hover:border-card-line hover:bg-muted-hover hover:text-foreground'
468
681
  ]"
469
682
  >
@@ -486,7 +699,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
486
699
  <button
487
700
  type="button"
488
701
  @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"
702
+ 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
703
  >
491
704
  {{ chip.displayVal }}
492
705
  <IconChevronDown class="size-3 opacity-60" />
@@ -501,10 +714,8 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
501
714
  </div>
502
715
  </div>
503
716
 
504
- <!-- Table + preview overlay inside a minimal border box -->
505
- <div class="relative overflow-hidden rounded-xl border border-card-line">
506
-
507
- <!-- Tabla -->
717
+ <!-- Tabla -->
718
+ <div class="overflow-hidden border-t border-b border-card-line">
508
719
  <Table
509
720
  ref="tableRef"
510
721
  :endpoint="resolvedEndpoint"
@@ -518,6 +729,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
518
729
  :click-row-to-open="clickRowToOpen"
519
730
  :preview-row-id="previewRow?.id ?? null"
520
731
  :preview-mode="!!previewEnabled"
732
+ :pinned-columns="resolvedPinnedColumns"
521
733
  @row-click="handleRowClick"
522
734
  @loaded="handleLoaded"
523
735
  @page-change="closePreview"
@@ -527,100 +739,134 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
527
739
  <slot :name="name" v-bind="slotProps ?? {}" />
528
740
  </template>
529
741
  </Table>
742
+ </div>
530
743
 
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"
744
+ <!-- Preview panel overlay deslizante desde la derecha (cubre todo el componente) -->
745
+ <Transition
746
+ :enter-active-class="previewFromCache ? '' : 'transition ease-out duration-200'"
747
+ :enter-from-class="previewFromCache ? '' : 'opacity-0 translate-x-6'"
748
+ :enter-to-class="previewFromCache ? '' : 'opacity-100 translate-x-0'"
749
+ leave-active-class="transition ease-in duration-150"
750
+ leave-from-class="opacity-100 translate-x-0"
751
+ leave-to-class="opacity-0 translate-x-6"
752
+ >
753
+ <div
754
+ v-if="previewRow && previewEnabled"
755
+ ref="previewPanelRef"
756
+ 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"
757
+ :style="{ width: (100 - currentRatio) + '%' }"
539
758
  >
759
+ <!-- Resize handle — thin pill on left edge -->
540
760
  <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">
761
+ 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"
762
+ @mousedown="startResize"
763
+ />
764
+
765
+ <!-- Barra de acciones del preview -->
766
+ <div class="shrink-0 flex items-center gap-3 px-5 py-4 border-b border-card-line">
767
+ <!-- Título (reemplazable via slot) -->
768
+ <div class="flex-1 min-w-0">
769
+ <slot name="preview-header" :row="previewRow" :close="closePreview" />
770
+ </div>
553
771
 
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>
772
+ <!-- Botones de acción todos icon-only, mismo tamaño y estilo -->
773
+ <div class="flex items-center gap-0.5 shrink-0">
774
+
775
+ <!-- Acciones extra configurables desde el padre -->
776
+ <slot name="preview-actions" :row="previewRow" :close="closePreview" />
777
+
778
+ <!-- Abrir (configurable vía :preview-href) -->
779
+ <div v-if="resolvedPreviewHref" class="hs-tooltip [--placement:top] inline-block">
780
+ <NuxtLink
781
+ :to="resolvedPreviewHref"
782
+ 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"
783
+ >
784
+ <IconExternalLink class="size-3.5" />
785
+ </NuxtLink>
786
+ <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
787
  </div>
578
788
 
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
- />
789
+ <!-- Eliminar (configurable vía :preview-deletable) -->
790
+ <div v-if="previewDeletable" class="hs-tooltip [--placement:top] inline-block">
791
+ <button
792
+ type="button"
793
+ 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"
794
+ @click.stop="emit('preview-delete', previewRow)"
795
+ >
796
+ <IconTrash class="size-3.5" />
797
+ </button>
798
+ <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
799
  </div>
587
800
 
588
- <!-- Tabs bottom -->
589
- <div v-if="hasHistory" class="shrink-0 flex border-t border-card-line">
801
+ <!-- Minimizar (siempre) -->
802
+ <div class="hs-tooltip [--placement:top] inline-block">
590
803
  <button
591
804
  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
- ]"
805
+ 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"
806
+ @click.stop="minimizePreview"
599
807
  >
600
- Datos
808
+ <IconMinus class="size-3.5" />
601
809
  </button>
810
+ <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>
811
+ </div>
812
+
813
+ <!-- Cerrar (siempre) -->
814
+ <div class="hs-tooltip [--placement:top] inline-block">
602
815
  <button
603
816
  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
- ]"
817
+ 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"
818
+ @click.stop="closePreview"
614
819
  >
615
- Bitácora
820
+ <IconX class="size-3.5" />
616
821
  </button>
822
+ <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
823
  </div>
618
824
 
619
825
  </div>
620
826
  </div>
621
- </Transition>
622
827
 
623
- </div>
828
+ <!-- Scrollable content -->
829
+ <div class="flex-1 overflow-y-auto min-h-0">
830
+ <slot v-if="previewTab === 'datos'" name="preview" :row="previewRow" :close="closePreview" />
831
+ <Table.PreviewTimeline
832
+ v-else-if="previewTab === 'bitacora' && resolvedHistoryEndpoint"
833
+ :endpoint="resolvedHistoryEndpoint"
834
+ />
835
+ </div>
836
+
837
+ <!-- Tabs — bottom -->
838
+ <div v-if="hasHistory" class="shrink-0 flex border-t border-card-line">
839
+ <button
840
+ type="button"
841
+ @click="previewTab = 'datos'"
842
+ :class="[
843
+ 'flex-1 py-2.5 text-xs font-semibold transition-colors border-r border-card-line border-t-2 -mt-px',
844
+ previewTab === 'datos'
845
+ ? 'border-t-card text-foreground'
846
+ : 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
847
+ ]"
848
+ >
849
+ Datos
850
+ </button>
851
+ <button
852
+ type="button"
853
+ @click="resolvedHistoryEndpoint && (previewTab = 'bitacora')"
854
+ :disabled="!resolvedHistoryEndpoint"
855
+ :class="[
856
+ 'flex-1 py-2.5 text-xs font-semibold transition-colors border-t-2 -mt-px',
857
+ !resolvedHistoryEndpoint
858
+ ? 'border-t-transparent text-muted-foreground/40 cursor-not-allowed'
859
+ : previewTab === 'bitacora'
860
+ ? 'border-t-card text-foreground'
861
+ : 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
862
+ ]"
863
+ >
864
+ Bitácora
865
+ </button>
866
+ </div>
867
+
868
+ </div>
869
+ </Transition>
624
870
 
625
871
  <!-- ── Floating mini-preview (dock expand, estilo Gmail) ── -->
626
872
  <Teleport to="body">
@@ -675,23 +921,28 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
675
921
  <div
676
922
  v-if="showFilterPanel"
677
923
  ref="filterMenuRef"
678
- class="fixed z-[60] bg-dropdown border border-dropdown-line rounded-xl shadow-2xl min-w-52 overflow-hidden"
924
+ class="fixed z-[60] bg-dropdown border border-dropdown-line rounded-xl shadow-2xl overflow-hidden"
679
925
  :style="filterMenuStyle"
680
926
  >
681
927
 
682
- <!-- Step 1: column picker (only non-filtered columns shown) -->
928
+ <!-- Step 1: column picker -->
683
929
  <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">
930
+ <p class="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest px-3 pt-3 pb-1.5">
931
+ Filtrar por
932
+ </p>
933
+ <div class="pb-2 min-w-48">
686
934
  <template v-if="availableFilterColumns.length">
687
935
  <button
688
936
  v-for="col in availableFilterColumns"
689
937
  :key="col.key"
690
938
  type="button"
691
939
  @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"
940
+ 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
941
  >
694
- {{ col.label }}
942
+ <span>{{ col.label }}</span>
943
+ <span class="text-[11px] text-muted-foreground-2 group-hover:text-muted-foreground transition-colors capitalize">
944
+ {{ col.filterType === 'daterange' ? 'fecha' : col.filterType === 'select' ? 'opción' : 'texto' }}
945
+ </span>
695
946
  </button>
696
947
  </template>
697
948
  <p v-else class="px-3 py-3 text-xs text-muted-foreground italic">
@@ -700,84 +951,109 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
700
951
  </div>
701
952
  </template>
702
953
 
703
- <!-- Step 2: value input -->
954
+ <!-- Step 2: value config -->
704
955
  <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">
956
+
957
+ <!-- Header: back + field name + operator selector -->
958
+ <div class="flex items-center gap-1.5 px-2 py-2 border-b border-dropdown-line">
706
959
  <button
707
960
  type="button"
708
961
  @click.stop="filterMenuStep = 'columns'"
709
- class="text-muted-foreground hover:text-foreground transition-colors"
962
+ 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
963
  >
711
964
  <IconChevronLeft class="size-4" />
712
965
  </button>
713
966
  <span class="text-sm font-medium text-foreground">{{ pendingCol.label }}</span>
967
+
968
+ <!-- Operator: native select for text (3 options) -->
969
+ <select
970
+ v-if="pendingCol.filterType === 'text'"
971
+ v-model="pendingOperator"
972
+ 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"
973
+ >
974
+ <option v-for="op in textOps" :key="op.value" :value="op.value">{{ op.label }}</option>
975
+ </select>
976
+
977
+ <!-- Operator: segmented toggle for select (es / no es) -->
978
+ <div v-else-if="pendingCol.filterType === 'select'" class="ml-auto flex rounded-md border border-dropdown-line overflow-hidden text-xs">
979
+ <button
980
+ v-for="op in selectOps"
981
+ :key="op.value"
982
+ type="button"
983
+ @click.stop="pendingOperator = op.value"
984
+ :class="[
985
+ 'px-2.5 py-1 transition-colors',
986
+ pendingOperator === op.value
987
+ ? 'bg-primary/10 text-primary'
988
+ : 'text-muted-foreground hover:bg-muted-hover'
989
+ ]"
990
+ >
991
+ {{ op.label }}
992
+ </button>
993
+ </div>
994
+
995
+ <!-- Operator: inline select for daterange -->
996
+ <select
997
+ v-else-if="pendingCol.filterType === 'daterange'"
998
+ v-model="pendingDateOp"
999
+ 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"
1000
+ >
1001
+ <option v-for="op in dateOps" :key="op.value" :value="op.value">{{ op.label }}</option>
1002
+ </select>
714
1003
  </div>
715
- <div class="p-3 space-y-2.5">
716
1004
 
717
- <!-- text -->
1005
+ <div class="p-3 min-w-56 space-y-2">
1006
+
1007
+ <!-- ── TEXT ── -->
718
1008
  <input
719
1009
  v-if="pendingCol.filterType === 'text'"
1010
+ ref="pendingValueInputRef"
720
1011
  v-model="pendingValue"
721
1012
  type="text"
722
- autofocus
723
1013
  @keydown.enter.stop="applyPendingFilter"
724
1014
  @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"
1015
+ placeholder="Valor..."
1016
+ 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
1017
  />
728
1018
 
729
- <!-- select -->
730
- <div v-else-if="pendingCol.filterType === 'select'" class="space-y-0.5">
1019
+ <!-- ── SELECT ── -->
1020
+ <div v-else-if="pendingCol.filterType === 'select'" class="space-y-0.5 max-h-52 overflow-y-auto -mx-1">
731
1021
  <button
732
1022
  v-for="opt in pendingCol.filterOptions"
733
1023
  :key="opt.value"
734
1024
  type="button"
735
1025
  @click.stop="pendingValue = opt.value; applyPendingFilter()"
736
1026
  :class="[
737
- 'w-full flex items-center gap-2 px-2.5 py-1.5 text-sm rounded-lg transition-colors text-left',
1027
+ 'w-full flex items-center gap-2 px-2.5 py-2 text-sm rounded-lg transition-colors text-left',
738
1028
  pendingValue === opt.value
739
- ? 'bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-300'
1029
+ ? 'bg-primary/10 text-primary'
740
1030
  : 'hover:bg-muted-hover text-foreground'
741
1031
  ]"
742
1032
  >
743
1033
  <span class="flex-1">{{ opt.label }}</span>
744
- <IconCheck v-if="pendingValue === opt.value" class="size-3.5 shrink-0 text-indigo-500" />
1034
+ <IconCheck v-if="pendingValue === opt.value" class="size-3.5 shrink-0 text-primary/70" />
745
1035
  </button>
746
1036
  </div>
747
1037
 
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>
1038
+ <!-- ── DATERANGE ── -->
1039
+ <template v-else-if="pendingCol.filterType === 'daterange'">
766
1040
  <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" />
1041
+ <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" />
1042
+ <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
1043
  </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>
1044
+ <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" />
1045
+ </template>
772
1046
 
1047
+ <!-- Apply (not for select — auto-applies on click) -->
773
1048
  <button
774
1049
  v-if="pendingCol.filterType !== 'select'"
775
1050
  type="button"
776
1051
  @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"
1052
+ 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
1053
  >
779
1054
  Aplicar
780
1055
  </button>
1056
+
781
1057
  </div>
782
1058
  </template>
783
1059
 
@@ -798,35 +1074,142 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
798
1074
  <div
799
1075
  v-if="showColumnPanel"
800
1076
  ref="columnPanelRef"
801
- class="fixed z-50 bg-dropdown border border-dropdown-line rounded-xl shadow-2xl p-3 min-w-56 max-h-80 overflow-y-auto"
1077
+ class="fixed z-50 bg-dropdown border border-dropdown-line rounded-xl shadow-2xl min-w-64 max-h-[480px] overflow-y-auto"
802
1078
  :style="columnPanelStyle"
803
1079
  >
804
- <p class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest mb-2 px-1">
805
- Columnas visibles
806
- </p>
807
- <div
808
- v-for="col in orderedColumns"
809
- :key="col.key"
810
- draggable="true"
811
- @dragstart="onDragStart(col.key)"
812
- @dragover="(e) => onDragOver(e, col.key)"
813
- @dragleave="onDragLeave"
814
- @drop="onDrop(col.key)"
815
- class="flex items-center gap-2 py-1.5 px-2 rounded-lg select-none transition-colors"
816
- :class="dragOverKey === col.key
817
- ? 'bg-blue-50 dark:bg-blue-900/20 ring-1 ring-blue-300 dark:ring-blue-700'
818
- : 'hover:bg-muted-hover cursor-grab'"
819
- >
820
- <IconGripVertical class="size-4 text-muted-foreground-2 shrink-0" />
821
- <input
822
- type="checkbox"
823
- :checked="tableRef?.table.getColumn(col.key)?.getIsVisible() ?? true"
824
- @change="tableRef?.table.getColumn(col.key)?.toggleVisibility()"
825
- @click.stop
826
- class="rounded border-card-line bg-surface shrink-0 cursor-pointer"
827
- />
828
- <span class="text-sm text-foreground truncate">{{ col.label }}</span>
1080
+
1081
+ <!-- ── Sección: Fija a la izquierda ── -->
1082
+ <div class="p-2 pb-1">
1083
+ <p class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest px-1 pb-1.5 flex items-center gap-1.5">
1084
+ <span class="size-1.5 rounded-full bg-indigo-400 inline-block"></span>
1085
+ Fija a la izquierda
1086
+ </p>
1087
+ <div
1088
+ class="rounded-lg min-h-[34px] transition-colors"
1089
+ :class="dragOverSection === 'left' && draggedKey && !columnsBySection.left.find(c => c.key === draggedKey)
1090
+ ? 'bg-indigo-50 dark:bg-indigo-900/20 ring-1 ring-indigo-300 dark:ring-indigo-700'
1091
+ : columnsBySection.left.length === 0 ? 'border border-dashed border-card-line' : ''"
1092
+ @dragover.prevent="dragOverSection = 'left'"
1093
+ @dragleave="dragOverSection = null"
1094
+ @drop.stop="onDropSection('left')"
1095
+ >
1096
+ <p v-if="columnsBySection.left.length === 0" class="flex items-center justify-center h-[34px] text-xs text-muted-foreground-2 italic select-none">
1097
+ Arrastra columnas aquí
1098
+ </p>
1099
+ <div
1100
+ v-for="col in columnsBySection.left"
1101
+ :key="col.key"
1102
+ draggable="true"
1103
+ @dragstart="onDragStart(col.key, 'left')"
1104
+ @dragover.prevent="dragOverSection = 'left'; dragOverKey = col.key"
1105
+ @dragleave="dragOverKey = null"
1106
+ @drop.stop="onDrop(col.key, 'left')"
1107
+ class="flex items-center gap-2 py-1.5 px-2 rounded-lg select-none cursor-grab transition-colors"
1108
+ :class="dragOverKey === col.key ? 'bg-indigo-50 dark:bg-indigo-900/20' : 'hover:bg-muted-hover'"
1109
+ >
1110
+ <IconGripVertical class="size-4 text-muted-foreground-2 shrink-0" />
1111
+ <input
1112
+ type="checkbox"
1113
+ :checked="tableRef?.table.getColumn(col.key)?.getIsVisible() ?? true"
1114
+ @change="tableRef?.table.getColumn(col.key)?.toggleVisibility(); persistCurrentPrefs()"
1115
+ @click.stop
1116
+ class="rounded border-card-line bg-surface shrink-0 cursor-pointer"
1117
+ />
1118
+ <span class="text-sm text-foreground truncate flex-1">{{ col.label }}</span>
1119
+ <span class="size-1.5 rounded-full bg-indigo-400 shrink-0 opacity-60" />
1120
+ </div>
1121
+ </div>
829
1122
  </div>
1123
+
1124
+ <div class="mx-3 border-t border-dropdown-line" />
1125
+
1126
+ <!-- ── Sección: Columnas libres ── -->
1127
+ <div class="p-2 py-1">
1128
+ <p class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest px-1 pb-1.5 flex items-center gap-1.5">
1129
+ <span class="size-1.5 rounded-full bg-muted-foreground-2 inline-block"></span>
1130
+ Columnas
1131
+ </p>
1132
+ <div
1133
+ class="rounded-lg min-h-[34px] transition-colors"
1134
+ :class="dragOverSection === 'center' && draggedKey && !columnsBySection.center.find(c => c.key === draggedKey)
1135
+ ? 'bg-muted/60 ring-1 ring-border'
1136
+ : ''"
1137
+ @dragover.prevent="dragOverSection = 'center'"
1138
+ @dragleave="dragOverSection = null"
1139
+ @drop.stop="onDropSection('center')"
1140
+ >
1141
+ <p v-if="columnsBySection.center.length === 0" class="flex items-center justify-center h-[34px] text-xs text-muted-foreground-2 italic select-none">
1142
+ Sin columnas libres
1143
+ </p>
1144
+ <div
1145
+ v-for="col in columnsBySection.center"
1146
+ :key="col.key"
1147
+ draggable="true"
1148
+ @dragstart="onDragStart(col.key, 'center')"
1149
+ @dragover.prevent="dragOverSection = 'center'; dragOverKey = col.key"
1150
+ @dragleave="dragOverKey = null"
1151
+ @drop.stop="onDrop(col.key, 'center')"
1152
+ class="flex items-center gap-2 py-1.5 px-2 rounded-lg select-none cursor-grab transition-colors"
1153
+ :class="dragOverKey === col.key ? 'bg-blue-50 dark:bg-blue-900/20 ring-1 ring-blue-200 dark:ring-blue-700' : 'hover:bg-muted-hover'"
1154
+ >
1155
+ <IconGripVertical class="size-4 text-muted-foreground-2 shrink-0" />
1156
+ <input
1157
+ type="checkbox"
1158
+ :checked="tableRef?.table.getColumn(col.key)?.getIsVisible() ?? true"
1159
+ @change="tableRef?.table.getColumn(col.key)?.toggleVisibility(); persistCurrentPrefs()"
1160
+ @click.stop
1161
+ class="rounded border-card-line bg-surface shrink-0 cursor-pointer"
1162
+ />
1163
+ <span class="text-sm text-foreground truncate flex-1">{{ col.label }}</span>
1164
+ </div>
1165
+ </div>
1166
+ </div>
1167
+
1168
+ <div class="mx-3 border-t border-dropdown-line" />
1169
+
1170
+ <!-- ── Sección: Fija a la derecha ── -->
1171
+ <div class="p-2 pt-1">
1172
+ <p class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest px-1 pb-1.5 flex items-center gap-1.5">
1173
+ <span class="size-1.5 rounded-full bg-amber-400 inline-block"></span>
1174
+ Fija a la derecha
1175
+ </p>
1176
+ <div
1177
+ class="rounded-lg min-h-[34px] transition-colors"
1178
+ :class="dragOverSection === 'right' && draggedKey && !columnsBySection.right.find(c => c.key === draggedKey)
1179
+ ? 'bg-amber-50 dark:bg-amber-900/20 ring-1 ring-amber-300 dark:ring-amber-700'
1180
+ : columnsBySection.right.length === 0 ? 'border border-dashed border-card-line' : ''"
1181
+ @dragover.prevent="dragOverSection = 'right'"
1182
+ @dragleave="dragOverSection = null"
1183
+ @drop.stop="onDropSection('right')"
1184
+ >
1185
+ <p v-if="columnsBySection.right.length === 0" class="flex items-center justify-center h-[34px] text-xs text-muted-foreground-2 italic select-none">
1186
+ Arrastra columnas aquí
1187
+ </p>
1188
+ <div
1189
+ v-for="col in columnsBySection.right"
1190
+ :key="col.key"
1191
+ draggable="true"
1192
+ @dragstart="onDragStart(col.key, 'right')"
1193
+ @dragover.prevent="dragOverSection = 'right'; dragOverKey = col.key"
1194
+ @dragleave="dragOverKey = null"
1195
+ @drop.stop="onDrop(col.key, 'right')"
1196
+ class="flex items-center gap-2 py-1.5 px-2 rounded-lg select-none cursor-grab transition-colors"
1197
+ :class="dragOverKey === col.key ? 'bg-amber-50 dark:bg-amber-900/20' : 'hover:bg-muted-hover'"
1198
+ >
1199
+ <IconGripVertical class="size-4 text-muted-foreground-2 shrink-0" />
1200
+ <input
1201
+ type="checkbox"
1202
+ :checked="tableRef?.table.getColumn(col.key)?.getIsVisible() ?? true"
1203
+ @change="tableRef?.table.getColumn(col.key)?.toggleVisibility(); persistCurrentPrefs()"
1204
+ @click.stop
1205
+ class="rounded border-card-line bg-surface shrink-0 cursor-pointer"
1206
+ />
1207
+ <span class="text-sm text-foreground truncate flex-1">{{ col.label }}</span>
1208
+ <span class="size-1.5 rounded-full bg-amber-400 shrink-0 opacity-60" />
1209
+ </div>
1210
+ </div>
1211
+ </div>
1212
+
830
1213
  </div>
831
1214
  </Transition>
832
1215
  </Teleport>