@innertia-solutions/innertia-nuxt 0.1.11 → 0.1.13

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/app.config.ts +10 -10
  3. package/components/Attention.vue +269 -0
  4. package/components/Auth/LoginForm.vue +1 -1
  5. package/components/Chart/index.vue +193 -0
  6. package/components/Container.vue +116 -0
  7. package/components/CopyButton.vue +98 -0
  8. package/components/Files/Card.vue +215 -0
  9. package/components/Files/Preview.vue +675 -0
  10. package/components/Files/Upload.vue +408 -0
  11. package/components/Files/UploadModal.vue +83 -0
  12. package/components/Forms/InputPin.vue +162 -0
  13. package/components/InfoToolbar.vue +54 -0
  14. package/components/Kanban/Standard.vue +588 -0
  15. package/components/Loader.vue +358 -0
  16. package/components/MobileBlocker.vue +5 -5
  17. package/components/MobileLoginPicker.vue +13 -13
  18. package/components/Modal/Base.vue +6 -0
  19. package/components/Modal/Confirm.vue +89 -0
  20. package/components/Modal/DeleteConfirm.vue +14 -31
  21. package/components/Modal/Form.vue +76 -0
  22. package/components/Modal/Host.vue +58 -0
  23. package/components/Modal/Prompt.vue +74 -0
  24. package/components/Modal.vue +119 -34
  25. package/components/Organization/Picker.vue +3 -3
  26. package/components/Table/Standard.vue +198 -25
  27. package/components/Table/index.vue +169 -369
  28. package/components/Table/views/ViewCards.vue +74 -0
  29. package/components/Table/views/ViewDatabase.vue +236 -0
  30. package/components/Table/views/ViewTable.vue +299 -0
  31. package/components/Toast/Alert.vue +16 -111
  32. package/components/Toast/Container.vue +4 -29
  33. package/components/Toast/Notification.vue +8 -43
  34. package/components/Toast/Process.vue +8 -86
  35. package/components/Toast/Stack.vue +59 -0
  36. package/components/Toast/Standard.vue +115 -0
  37. package/components/Tree/Standard.vue +49 -4
  38. package/components/Workflow/Board.vue +356 -0
  39. package/components/Workflow/Builder.vue +454 -0
  40. package/components/Workflow/Form.vue +526 -0
  41. package/components/Workflow/Viewer.vue +225 -0
  42. package/components/Workflow/edges/TransitionEdge.vue +84 -0
  43. package/components/Workflow/nodes/StepNode.vue +88 -0
  44. package/composables/useAuth.js +1 -1
  45. package/composables/useClipboard.js +79 -0
  46. package/composables/useConfetti.js +194 -0
  47. package/composables/useContext.js +53 -5
  48. package/composables/useFile.js +189 -0
  49. package/composables/useMobileGuard.ts +21 -21
  50. package/composables/useModal.js +71 -0
  51. package/composables/useOrganization.ts +3 -3
  52. package/composables/useToast.js +38 -65
  53. package/composables/useWorkflow/form.js +162 -0
  54. package/composables/useWorkflow/index.js +18 -0
  55. package/composables/useWorkflow/view.js +220 -0
  56. package/composables/useWorkflowLayout.js +162 -0
  57. package/css/tokens.css +10 -10
  58. package/docs/superpowers/plans/2026-05-27-nuxt-apps-to-contexts-rename.md +630 -0
  59. package/docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md +113 -0
  60. package/middleware/{03.apps.global.ts → 03.contexts.global.ts} +25 -25
  61. package/nuxt.config.ts +1 -1
  62. package/package.json +13 -8
  63. package/stores/modal.js +48 -0
  64. package/stores/toast.js +156 -117
  65. package/composables/useApp.ts +0 -46
@@ -1,13 +1,12 @@
1
1
  <script setup>
2
2
  import { useVueTable, getCoreRowModel } from '@tanstack/vue-table'
3
3
  import {
4
- IconSelector,
5
- IconChevronUp,
6
- IconChevronDown,
7
4
  IconReload,
8
5
  IconBolt,
9
- IconPin,
10
6
  } from '@tabler/icons-vue'
7
+ import ViewTable from './views/ViewTable.vue'
8
+ import ViewCards from './views/ViewCards.vue'
9
+ import ViewDatabase from './views/ViewDatabase.vue'
11
10
 
12
11
  const props = defineProps({
13
12
  endpoint: { type: String, required: true },
@@ -18,12 +17,20 @@ const props = defineProps({
18
17
  name: { type: String, required: true },
19
18
  cached: { type: Boolean, default: false },
20
19
  showReloadButton: { type: Boolean, default: true },
21
- viewMode: { type: String, default: 'table' }, // 'table' | 'grid'
20
+ /** 'bottom' (default) | 'top' — posición de la barra de paginación + reload + total + instant. */
21
+ infoPosition: { type: String, default: 'bottom' },
22
+ /** Mostrar el selector "Filas: 10/25/50/100/Otro...". Default false. */
23
+ showPerPage: { type: Boolean, default: false },
24
+ /** 'table' | 'cards' | 'database' | 'grid' (legacy → maps to cards) */
25
+ viewMode: { type: String, default: 'table' },
22
26
  gridClass: { type: String, default: 'grid grid-cols-2 lg:grid-cols-3 gap-4' },
23
27
  clickRowToOpen: { type: Boolean, default: false },
24
28
  previewRowId: { type: [String, Number], default: null },
25
29
  previewMode: { type: Boolean, default: false },
26
30
  pinnedColumns: { type: Object, default: null }, // { left?: string[], right?: string[] }
31
+ /** Mutation function for database inline editing: async (row, colKey, value) => void */
32
+ updateMutation: { type: Function, default: null },
33
+ bordered: { type: Boolean, default: true },
27
34
  })
28
35
 
29
36
  const emit = defineEmits(['update:search', 'row-click', 'loaded', 'page-change', 'per-page-change'])
@@ -40,13 +47,54 @@ const loading = ref(false)
40
47
  const isDataFromCache = ref(false)
41
48
  const lastDataLength = ref(-1)
42
49
  const lastRowHeight = ref(48)
43
- const tableBodyRef = ref(null)
50
+ const viewTableRef = ref(null)
44
51
  const paginationBarRef = ref(null)
45
52
  const skeletonRows = computed(() => {
46
53
  const count = lastDataLength.value < 0 ? pagination.value.pageSize : lastDataLength.value
47
54
  return Array.from({ length: count })
48
55
  })
49
- const isGridView = computed(() => props.viewMode === 'grid')
56
+
57
+ // Backward compat: 'grid' maps to 'cards'
58
+ const currentView = computed(() => {
59
+ const v = props.viewMode
60
+ if (v === 'grid') return 'cards'
61
+ return v // 'table' | 'cards' | 'database'
62
+ })
63
+
64
+ // ─── Inline editing state (used by ViewDatabase) ──────────────────────────────
65
+ const editingCell = ref(null) // { rowId, colKey } | null
66
+ const editingValue = ref('')
67
+ const savingCell = ref(null) // { rowId, colKey } | null
68
+ const cellError = ref(null) // { rowId, colKey, message } | null
69
+
70
+ const startEdit = (row, col) => {
71
+ editingCell.value = { rowId: row.id, colKey: col.column.id }
72
+ editingValue.value = String(col.getValue() ?? '')
73
+ cellError.value = null
74
+ }
75
+
76
+ const cancelEdit = () => {
77
+ editingCell.value = null
78
+ editingValue.value = ''
79
+ cellError.value = null
80
+ }
81
+
82
+ const saveEdit = async (row, col) => {
83
+ if (!props.updateMutation || !editingCell.value) return
84
+ const { rowId, colKey } = editingCell.value
85
+ const value = editingValue.value
86
+ editingCell.value = null
87
+ editingValue.value = ''
88
+ savingCell.value = { rowId, colKey }
89
+ cellError.value = null
90
+ try {
91
+ await props.updateMutation(row.original, colKey, value)
92
+ } catch (e) {
93
+ cellError.value = { rowId, colKey, message: e?.message ?? 'Error al guardar' }
94
+ } finally {
95
+ savingCell.value = null
96
+ }
97
+ }
50
98
 
51
99
  // ─── TanStack state ───────────────────────────────────────────────────────────
52
100
  const pagination = ref({ pageIndex: 0, pageSize: 10 })
@@ -90,7 +138,13 @@ const buildColumnDefs = () => {
90
138
  size: col.size ?? 200,
91
139
  minSize: 60,
92
140
  maxSize: 800,
93
- meta: { class: col.class ?? '', label: col.label },
141
+ meta: {
142
+ class: col.class ?? '',
143
+ label: col.label,
144
+ editable: col.editable ?? false,
145
+ type: col.type ?? 'text',
146
+ options: col.options ?? [],
147
+ },
94
148
  })
95
149
  }
96
150
  return defs
@@ -147,10 +201,6 @@ const getPinnedStyles = (column, isHeader = false) => {
147
201
  if (!pinned) return {}
148
202
  const z = isHeader ? 2 : 1
149
203
  const w = column.getSize() + 'px'
150
- // Headers always use the solid card background.
151
- // Body cells use --row-bg, which is set to solid colors only (normal + hover) via <style scoped>.
152
- // Selected/preview rows intentionally don't set --row-bg so sticky cells fall back to --card,
153
- // which prevents semi-transparent tints from bleeding through the sticky cell.
154
204
  const bg = isHeader ? 'var(--card, #fff)' : 'var(--row-bg, var(--card, #fff))'
155
205
  const base = {
156
206
  position: 'sticky',
@@ -160,14 +210,11 @@ const getPinnedStyles = (column, isHeader = false) => {
160
210
  minWidth: w,
161
211
  maxWidth: w,
162
212
  }
163
- // inset box-shadow: paints inside the cell so it can't be covered by adjacent cells
164
- // and always follows the visual sticky position (unlike border or outset box-shadow)
165
213
  if (pinned === 'left') return { ...base, left: column.getStart('left') + 'px', boxShadow: 'inset -1px 0 0 0 var(--card-line, #e5e7eb)' }
166
214
  if (pinned === 'right') return { ...base, right: column.getAfter('right') + 'px', boxShadow: 'inset 1px 0 0 0 var(--card-line, #e5e7eb)' }
167
215
  return {}
168
216
  }
169
217
 
170
-
171
218
  // Initialize pinning from prop if provided
172
219
  onMounted(() => {
173
220
  if (props.pinnedColumns) {
@@ -179,7 +226,6 @@ onMounted(() => {
179
226
  })
180
227
 
181
228
  // Keep 'select' always first in the left pinning array.
182
- // Fires synchronously so the colgroup/headers never render in wrong order.
183
229
  watch(() => columnPinning.value.left, (left) => {
184
230
  if (props.checkable && left.includes('select') && left[0] !== 'select') {
185
231
  columnPinning.value = {
@@ -228,7 +274,7 @@ const fetchData = async () => {
228
274
  }
229
275
  }
230
276
 
231
- // ─── Scheduled fetch (deduplicates concurrent state changes) ─────────────────
277
+ // ─── Scheduled fetch ──────────────────────────────────────────────────────────
232
278
  let fetchTimeout = null
233
279
  const scheduleFetch = (delay = 0) => {
234
280
  if (fetchTimeout) clearTimeout(fetchTimeout)
@@ -281,7 +327,7 @@ const clearCache = () => {
281
327
  if (cacheKey.value) sessionStorage.removeItem(cacheKey.value)
282
328
  }
283
329
 
284
- // ─── Restore guard (prevents watchers from triggering fetch during restore) ───
330
+ // ─── Restore guard ────────────────────────────────────────────────────────────
285
331
  const isRestoring = ref(false)
286
332
 
287
333
  const loadFromCacheOnMount = async () => {
@@ -312,8 +358,8 @@ const loadFromCacheOnMount = async () => {
312
358
 
313
359
  // ─── Watchers ─────────────────────────────────────────────────────────────────
314
360
  watch(tableData, (newData) => {
315
- if (newData.length > 0 && tableBodyRef.value) {
316
- const firstDataRow = Array.from(tableBodyRef.value.children).find(el => el.dataset.rowType === 'data')
361
+ if (newData.length > 0 && viewTableRef.value?.tableBodyEl) {
362
+ const firstDataRow = Array.from(viewTableRef.value.tableBodyEl.children).find(el => el.dataset.rowType === 'data')
317
363
  if (firstDataRow) {
318
364
  const h = firstDataRow.getBoundingClientRect().height
319
365
  if (h > 0) lastRowHeight.value = h
@@ -370,10 +416,20 @@ onBeforeUnmount(() => {
370
416
  // ─── Column settings panel ────────────────────────────────────────────────────
371
417
  const setColumnOrder = (order) => { columnOrder.value = order }
372
418
 
373
- // ─── Header drag reorder ──────────────────────────────────────────────────────
374
- let draggedHeaderId = null
375
- const dragOverHeaderId = ref(null)
376
- const resizeHoverId = ref(null)
419
+ // ─── Header drag reorder (handler called from ViewTable) ──────────────────────
420
+ const onHeaderDrop = (fromId, toId) => {
421
+ if (!fromId || fromId === toId) return
422
+ if (toId === 'select') return
423
+ const order = [...columnOrder.value]
424
+ const from = order.indexOf(fromId)
425
+ const to = order.indexOf(toId)
426
+ if (from < 0 || to < 0) return
427
+ order.splice(from, 1)
428
+ order.splice(to, 0, fromId)
429
+ const selIdx = order.indexOf('select')
430
+ if (selIdx > 0) { order.splice(selIdx, 1); order.unshift('select') }
431
+ columnOrder.value = order
432
+ }
377
433
 
378
434
  // ─── Column auto-size on double click ─────────────────────────────────────────
379
435
  const _canvas = typeof document !== 'undefined' ? document.createElement('canvas') : null
@@ -385,15 +441,16 @@ const measureText = (text, font) => {
385
441
  return _ctx.measureText(String(text ?? '')).width
386
442
  }
387
443
 
388
- const autoSizeColumn = (header) => {
444
+ const onAutoSizeColumn = (header) => {
389
445
  const colId = header.column.id
390
446
  const pad = 32
447
+ const tbody = viewTableRef.value?.tableBodyEl
391
448
 
392
449
  const label = header.column.columnDef.meta?.label ?? header.id
393
450
  let max = measureText(label, '500 12px ui-sans-serif,system-ui,sans-serif') + pad + 20
394
451
 
395
- if (tableBodyRef.value) {
396
- tableBodyRef.value.querySelectorAll(`td[data-col-id="${colId}"]`).forEach(td => {
452
+ if (tbody) {
453
+ tbody.querySelectorAll(`td[data-col-id="${colId}"]`).forEach(td => {
397
454
  const w = measureText(td.textContent?.trim(), '14px ui-sans-serif,system-ui,sans-serif') + pad
398
455
  if (w > max) max = w
399
456
  })
@@ -402,26 +459,6 @@ const autoSizeColumn = (header) => {
402
459
  table.setColumnSizing(prev => ({ ...prev, [colId]: Math.ceil(max) }))
403
460
  }
404
461
 
405
- const onHeaderDragStart = (colId) => { draggedHeaderId = colId }
406
- const onHeaderDragOver = (e, colId) => { e.preventDefault(); dragOverHeaderId.value = colId }
407
- const onHeaderDragLeave = () => { dragOverHeaderId.value = null }
408
- const onHeaderDrop = (colId) => {
409
- if (!draggedHeaderId || draggedHeaderId === colId) return
410
- if (colId === 'select') return
411
- const order = [...columnOrder.value]
412
- const from = order.indexOf(draggedHeaderId)
413
- const to = order.indexOf(colId)
414
- if (from < 0 || to < 0) return
415
- order.splice(from, 1)
416
- order.splice(to, 0, draggedHeaderId)
417
- // keep 'select' pinned first
418
- const selIdx = order.indexOf('select')
419
- if (selIdx > 0) { order.splice(selIdx, 1); order.unshift('select') }
420
- columnOrder.value = order
421
- draggedHeaderId = null
422
- dragOverHeaderId.value = null
423
- }
424
-
425
462
  // ─── Row selection ────────────────────────────────────────────────────────────
426
463
  const getSelectedRows = () => {
427
464
  const selected = table.getSelectedRowModel().rows.map(r => r.original)
@@ -515,18 +552,11 @@ const handleRowKeydown = (row, e) => {
515
552
  emit('row-click', row.original, e)
516
553
  }
517
554
 
518
- // Compute --row-bg for pinned (sticky) cells.
519
- // Non-pinned cells get background from Tailwind classes on <tr>.
520
- // Pinned cells inherit --row-bg which must be a solid opaque color (no transparency → no bleed-through).
521
- // color-mix() blends the Tailwind tint with the card color to produce an opaque equivalent.
522
- // For normal/hover rows the <style scoped> CSS rule handles it; selected/preview override via inline style.
523
555
  const pinnedRowStyle = (row) => {
524
556
  if (props.previewRowId && row.original.id === props.previewRowId) {
525
- // !bg-indigo-50 → solid indigo-50
526
557
  return { '--row-bg': 'color-mix(in srgb, #eef2ff 100%, var(--card, #fff))' }
527
558
  }
528
559
  if (row.getIsSelected()) {
529
- // bg-indigo-50/40 → 40% indigo-50 blended with card
530
560
  return { '--row-bg': 'color-mix(in srgb, #eef2ff 40%, var(--card, #fff))' }
531
561
  }
532
562
  return {}
@@ -539,6 +569,11 @@ const reloadTable = () => {
539
569
  fetchData()
540
570
  }
541
571
 
572
+ const setPageSize = (size) => {
573
+ pagination.value = { pageIndex: 0, pageSize: size }
574
+ table.setPageSize(size)
575
+ }
576
+
542
577
  defineExpose({
543
578
  getSelectedRows,
544
579
  loading,
@@ -547,318 +582,98 @@ defineExpose({
547
582
  clearCache,
548
583
  table,
549
584
  setColumnOrder,
585
+ setPageSize,
550
586
  isDataFromCache,
551
587
  cached: computed(() => props.cached),
552
588
  paginationBarRef,
553
589
  columnPinning,
554
590
  pinColumn,
591
+ editingCell,
592
+ cancelEdit,
555
593
  })
556
594
  </script>
557
595
 
558
596
  <template>
559
- <div class="relative">
597
+ <div class="relative flex flex-col">
560
598
 
561
599
  <!-- Table view -->
562
- <div v-if="!isGridView" class="overflow-x-auto relative">
563
- <table
564
- class="relative divide-y divide-card-line"
565
- :style="{ tableLayout: 'fixed', width: table.getTotalSize() + 'px', minWidth: '100%' }"
566
- >
567
- <colgroup>
568
- <!-- Must use pinning order (left|center|right) — same as getHeaderGroups() and row.getVisibleCells() -->
569
- <col
570
- v-for="col in [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns(), ...table.getRightVisibleLeafColumns()]"
571
- :key="col.id"
572
- :style="{ width: col.getSize() + 'px' }"
573
- >
574
- </colgroup>
575
- <thead class="relative z-20 bg-card">
576
- <template v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
577
- <!-- Main header row -->
578
- <tr class="bg-card">
579
- <th
580
- v-for="header in headerGroup.headers"
581
- :key="header.id"
582
- scope="col"
583
- :draggable="header.id !== 'select' && resizeHoverId !== header.id && !header.column.getIsPinned()"
584
- @dragstart="header.id !== 'select' && resizeHoverId !== header.id && !header.column.getIsPinned() && onHeaderDragStart(header.id)"
585
- @dragover="header.id !== 'select' && onHeaderDragOver($event, header.id)"
586
- @dragleave="onHeaderDragLeave"
587
- @drop="header.id !== 'select' && onHeaderDrop(header.id)"
588
- class="relative"
589
- :class="[
590
- header.id === 'select' ? 'text-center' : '',
591
- dragOverHeaderId === header.id ? 'bg-indigo-50 dark:bg-indigo-900/20' : '',
592
- header.column.getCanSort() ? 'cursor-pointer select-none' : '',
593
- ]"
594
- :style="getPinnedStyles(header.column, true)"
595
- @click="header.column.getCanSort() && header.column.toggleSorting()"
596
- >
597
- <!-- Select all checkbox -->
598
- <template v-if="header.id === 'select'">
599
- <input
600
- type="checkbox"
601
- :checked="table.getIsAllRowsSelected()"
602
- :indeterminate="table.getIsSomeRowsSelected()"
603
- @change="table.getToggleAllRowsSelectedHandler()($event)"
604
- class="mx-2 shrink-0 border-card-line rounded-control text-blue-900 focus:ring-0 focus:ring-offset-0 dark:bg-card"
605
- />
606
- </template>
607
- <!-- Regular column header -->
608
- <template v-else>
609
- <div
610
- class="px-4 py-3 flex items-center gap-x-1 text-xs font-medium w-full overflow-hidden"
611
- :class="header.column.getIsPinned() ? 'text-foreground' : 'text-muted-foreground'"
612
- >
613
- <IconPin v-if="header.column.getIsPinned()" class="size-3 shrink-0 text-indigo-400 dark:text-indigo-500" />
614
- <span class="truncate">{{ header.column.columnDef.meta?.label ?? header.id }}</span>
615
- <span v-if="header.column.getCanSort()">
616
- <IconSelector v-if="!header.column.getIsSorted()" class="size-4 opacity-40" />
617
- <IconChevronDown v-else-if="header.column.getIsSorted() === 'desc'" class="size-4" />
618
- <IconChevronUp v-else class="size-4" />
619
- </span>
620
- </div>
621
- <!-- Resize handle -->
622
- <div
623
- v-if="header.column.getCanResize()"
624
- class="absolute right-0 top-0 h-full w-3 cursor-col-resize group/rz flex items-center justify-center select-none touch-none"
625
- @mouseenter="resizeHoverId = header.id"
626
- @mouseleave="resizeHoverId = null"
627
- @mousedown.stop="header.getResizeHandler()?.($event)"
628
- @touchstart.passive.stop="header.getResizeHandler()?.($event)"
629
- @dblclick.stop="autoSizeColumn(header)"
630
- @dragstart.stop.prevent
631
- @click.stop
632
- >
633
- <div
634
- class="h-4 w-px transition-all"
635
- :class="header.column.getIsResizing()
636
- ? 'bg-indigo-400 dark:bg-indigo-500 !w-0.5'
637
- : 'bg-surface-1 group-hover/rz:bg-indigo-300 dark:group-hover/rz:bg-indigo-600 group-hover/rz:w-0.5'"
638
- />
639
- </div>
640
- </template>
641
- </th>
642
- </tr>
643
-
644
- <!-- Column filter row -->
645
- <tr
646
- v-if="hasFilterableColumns"
647
- class="border-b border-card-line bg-muted/50"
648
- >
649
- <th
650
- v-for="header in headerGroup.headers"
651
- :key="'f-' + header.id"
652
- :class="[
653
- header.id === 'select' ? 'w-12' : 'px-3 py-1.5',
654
- ]"
655
- :style="getPinnedStyles(header.column, true)"
656
- >
657
- <input
658
- v-if="header.column.getCanFilter()"
659
- :value="header.column.getFilterValue() ?? ''"
660
- @input="(e) => header.column.setFilterValue(e.target.value || undefined)"
661
- :placeholder="`Filtrar ${header.column.columnDef.meta?.label ?? ''}...`"
662
- class="w-full bg-card border border-card-line rounded-control 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"
663
- />
664
- </th>
665
- </tr>
666
- </template>
667
- </thead>
668
-
669
- <tbody ref="tableBodyRef" class="divide-y divide-card-line">
670
- <!-- Loading skeleton rows -->
671
- <tr
672
- v-if="loading"
673
- v-for="(_, i) in skeletonRows"
674
- :key="'sk-' + i"
675
- class="animate-pulse bg-card"
676
- >
677
- <td
678
- v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
679
- :key="'skc-' + header.id"
680
- :class="[
681
- header.id === 'select' ? 'text-center w-12' : 'px-4 overflow-hidden',
682
- ]"
683
- :style="{ height: lastRowHeight + 'px', ...getPinnedStyles(header.column) }"
684
- >
685
- <div v-if="header.id === 'select'" class="w-4 h-4 bg-surface-1 rounded mx-auto"></div>
686
- <div v-else class="h-4 w-[50%] rounded bg-surface-1"></div>
687
- </td>
688
- </tr>
689
-
690
- <!-- Loading filler rows: pad to pageSize so table height doesn't change -->
691
- <tr
692
- v-if="loading && skeletonRows.length < pagination.pageSize"
693
- v-for="i in (pagination.pageSize - skeletonRows.length)"
694
- :key="'lf-' + i"
695
- class="bg-card"
696
- >
697
- <td
698
- v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
699
- :key="'lfc-' + header.id"
700
- :style="{ height: lastRowHeight + 'px', ...getPinnedStyles(header.column) }"
701
- />
702
- </tr>
703
-
704
- <!-- Empty filler rows: maintain table height when no results -->
705
- <tr
706
- v-if="!loading && tableData.length === 0"
707
- v-for="i in pagination.pageSize"
708
- :key="'esk-' + i"
709
- class="bg-card"
710
- >
711
- <td
712
- v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
713
- :key="'eskc-' + header.id"
714
- :style="{ height: lastRowHeight + 'px', ...getPinnedStyles(header.column) }"
715
- />
716
- </tr>
717
-
718
- <!-- Data rows -->
719
- <tr
720
- v-else
721
- v-for="row in table.getRowModel().rows"
722
- :key="row.id"
723
- data-row-type="data"
724
- @click="(e) => handleRowClick(row, e)"
725
- @keydown="(e) => handleRowKeydown(row, e)"
726
- :tabindex="isRowClickEnabled ? 0 : undefined"
727
- class="bg-card hover:bg-layer-hover transition-colors"
728
- :class="{
729
- 'cursor-pointer': isRowClickEnabled,
730
- 'bg-indigo-50/40 dark:bg-indigo-900/10 hover:bg-indigo-50/60': row.getIsSelected(),
731
- '!bg-indigo-50 dark:!bg-indigo-900/20 ring-1 ring-inset ring-indigo-200 dark:ring-indigo-700': previewRowId && row.original.id === previewRowId,
732
- }"
733
- :style="pinnedRowStyle(row)"
734
- >
735
- <td
736
- v-for="cell in row.getVisibleCells()"
737
- :key="cell.id"
738
- :data-col-id="cell.column.id"
739
- :class="[
740
- cell.column.id === 'select'
741
- ? 'text-center w-12 overflow-hidden'
742
- : 'px-4 py-3 text-sm text-muted-foreground-1 overflow-hidden',
743
- cell.column.id !== 'select' ? cell.column.columnDef.meta?.class ?? '' : '',
744
- ]"
745
- :style="getPinnedStyles(cell.column)"
746
- >
747
- <!-- Select checkbox -->
748
- <template v-if="cell.column.id === 'select'">
749
- <div @click.stop>
750
- <input
751
- type="checkbox"
752
- :checked="row.getIsSelected()"
753
- :disabled="!row.getCanSelect()"
754
- @change="row.getToggleSelectedHandler()($event)"
755
- class="rounded border-card-line focus:ring-0 focus:ring-offset-0 dark:bg-card"
756
- />
757
- </div>
758
- </template>
759
- <!-- Data cell with slot -->
760
- <template v-else>
761
- <slot :name="cell.column.id" :row="row.original" :value="cell.getValue()">
762
- {{ cell.getValue() }}
763
- </slot>
764
- </template>
765
- </td>
766
- </tr>
767
-
768
- <!-- Filler rows: pad table to full page height when data < perPage -->
769
- <tr
770
- v-if="!loading && tableData.length > 0 && tableData.length < pagination.pageSize"
771
- v-for="i in (pagination.pageSize - tableData.length)"
772
- :key="'fill-' + i"
773
- class="bg-card"
774
- >
775
- <td
776
- v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
777
- :key="'fillc-' + header.id"
778
- :style="{ height: lastRowHeight + 'px', ...getPinnedStyles(header.column) }"
779
- />
780
- </tr>
781
- </tbody>
782
- </table>
783
-
784
- <!-- Empty state overlays -->
785
- <div
786
- v-if="!loading && tableData.length === 0 && !search && !columnFilters.length"
787
- class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-card/60 rounded-card"
788
- >
789
- <slot name="empty">
790
- <p class="text-muted-foreground text-lg font-medium italic">No hay registros</p>
791
- </slot>
792
- </div>
793
-
794
- <div
795
- v-if="!loading && tableData.length === 0 && (search || columnFilters.length)"
796
- class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-card/60 rounded-card"
797
- >
798
- <slot name="empty-search">
799
- <p class="text-muted-foreground text-lg font-medium italic">No hay registros en la búsqueda</p>
800
- </slot>
801
- </div>
802
- </div>
803
-
804
- <!-- Grid view -->
805
- <div v-else class="relative">
806
- <div v-if="loading" :class="gridClass">
807
- <div v-for="(_, i) in skeletonRows" :key="'gsk-' + i" class="animate-pulse">
808
- <slot name="grid-skeleton">
809
- <div class="bg-card rounded-card border border-card-line p-4">
810
- <div class="space-y-3">
811
- <div class="h-4 bg-surface-1 rounded w-3/4"></div>
812
- <div class="h-4 bg-surface-1 rounded w-1/2"></div>
813
- <div class="h-6 bg-surface-1 rounded w-1/4"></div>
814
- </div>
815
- </div>
816
- </slot>
817
- </div>
818
- </div>
819
-
820
- <div v-else-if="tableData.length > 0" :class="gridClass">
821
- <slot
822
- name="grid-item"
823
- v-for="row in table.getRowModel().rows"
824
- :key="row.id"
825
- :row="row.original"
826
- :tanstack-row="row"
827
- :is-selected="row.getIsSelected()"
828
- :checkable="checkable"
829
- :toggle-row="() => row.toggleSelected()"
830
- >
831
- <div class="bg-card rounded-card border border-card-line p-4 hover:shadow-md transition-shadow relative"
832
- :class="{ 'ring-2 ring-indigo-400 dark:ring-indigo-600': row.getIsSelected() }">
833
- <div v-if="checkable" class="absolute top-2 left-2 z-10">
834
- <input type="checkbox" :checked="row.getIsSelected()" @change="row.toggleSelected()"
835
- class="rounded border-card-line dark:bg-card" />
836
- </div>
837
- <div class="space-y-2" :class="{ 'pt-6': checkable }">
838
- <div v-for="cell in row.getVisibleCells().filter(c => c.column.id !== 'select')" :key="cell.id" class="flex justify-between">
839
- <span class="text-sm text-muted-foreground">{{ cell.column.columnDef.meta?.label ?? cell.column.id }}:</span>
840
- <span class="text-sm text-foreground">
841
- <slot :name="cell.column.id" :row="row.original" :value="cell.getValue()">{{ cell.getValue() }}</slot>
842
- </span>
843
- </div>
844
- </div>
845
- </div>
846
- </slot>
847
- </div>
848
-
849
- <div v-else class="flex items-center justify-center py-12">
850
- <slot v-if="!search && !columnFilters.length" name="empty">
851
- <p class="text-muted-foreground text-lg">No hay registros</p>
852
- </slot>
853
- <slot v-else name="empty-search">
854
- <p class="text-muted-foreground text-lg">No hay registros en la búsqueda</p>
855
- </slot>
856
- </div>
857
- </div>
600
+ <ViewTable
601
+ v-if="currentView === 'table'"
602
+ ref="viewTableRef"
603
+ :table="table"
604
+ :loading="loading"
605
+ :skeleton-rows="skeletonRows"
606
+ :last-row-height="lastRowHeight"
607
+ :checkable="checkable"
608
+ :preview-row-id="previewRowId"
609
+ :is-row-click-enabled="isRowClickEnabled"
610
+ :search="search"
611
+ :column-filters="columnFilters"
612
+ :has-filterable-columns="hasFilterableColumns"
613
+ :pagination="pagination"
614
+ :get-pinned-styles="getPinnedStyles"
615
+ :pinned-row-style="pinnedRowStyle"
616
+ :on-header-drop="onHeaderDrop"
617
+ :on-auto-size-column="onAutoSizeColumn"
618
+ @row-click="handleRowClick"
619
+ @row-keydown="handleRowKeydown"
620
+ >
621
+ <template v-for="(_, name) in $slots" #[name]="slotProps">
622
+ <slot :name="name" v-bind="slotProps ?? {}" />
623
+ </template>
624
+ </ViewTable>
625
+
626
+ <!-- Cards / grid view -->
627
+ <ViewCards
628
+ v-else-if="currentView === 'cards'"
629
+ :table="table"
630
+ :loading="loading"
631
+ :skeleton-rows="skeletonRows"
632
+ :checkable="checkable"
633
+ :grid-class="gridClass"
634
+ :search="search"
635
+ :column-filters="columnFilters"
636
+ :bordered="bordered"
637
+ @row-click="handleRowClick"
638
+ >
639
+ <template v-for="(_, name) in $slots" #[name]="slotProps">
640
+ <slot :name="name" v-bind="slotProps ?? {}" />
641
+ </template>
642
+ </ViewCards>
643
+
644
+ <!-- Database inline-edit view -->
645
+ <ViewDatabase
646
+ v-else-if="currentView === 'database'"
647
+ :table="table"
648
+ :loading="loading"
649
+ :skeleton-rows="skeletonRows"
650
+ :last-row-height="lastRowHeight"
651
+ :checkable="checkable"
652
+ :editing-cell="editingCell"
653
+ :editing-value="editingValue"
654
+ :saving-cell="savingCell"
655
+ :cell-error="cellError"
656
+ @row-click="handleRowClick"
657
+ @start-edit="startEdit"
658
+ @save-edit="saveEdit"
659
+ @cancel-edit="cancelEdit"
660
+ @editing-value-change="(v) => editingValue = v"
661
+ >
662
+ <template v-for="(_, name) in $slots" #[name]="slotProps">
663
+ <slot :name="name" v-bind="slotProps ?? {}" />
664
+ </template>
665
+ </ViewDatabase>
858
666
 
859
667
  <!-- Pagination & controls bar -->
860
- <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">
861
- <!-- Left: reload, total, cache, columns button -->
668
+ <div
669
+ v-if="infoPosition !== 'none'"
670
+ ref="paginationBarRef"
671
+ :class="[
672
+ 'flex flex-col sm:flex-row items-center justify-between gap-y-4 sm:gap-y-0 px-4 py-3',
673
+ infoPosition === 'top' ? 'order-first border-b border-card-line' : 'border-t border-card-line',
674
+ ]"
675
+ >
676
+ <!-- Left: reload, total, cache -->
862
677
  <div class="flex items-center gap-x-4 flex-wrap gap-y-2">
863
678
  <!-- Reload button -->
864
679
  <div v-if="showReloadButton" class="flex items-center gap-x-2">
@@ -892,13 +707,12 @@ defineExpose({
892
707
  <div class="absolute top-full left-4 -mt-1 border-4 border-transparent border-t-slate-900"></div>
893
708
  </div>
894
709
  </div>
895
-
896
710
  </div>
897
711
 
898
712
  <!-- Right: per-page + pagination -->
899
713
  <div class="flex items-center gap-x-8">
900
714
  <!-- Per page selector -->
901
- <div class="flex items-center gap-x-2">
715
+ <div v-if="showPerPage" class="flex items-center gap-x-2">
902
716
  <label class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest">Filas:</label>
903
717
  <select
904
718
  v-if="!isCustomPerPage"
@@ -958,17 +772,3 @@ defineExpose({
958
772
  </div>
959
773
  </div>
960
774
  </template>
961
-
962
- <style scoped>
963
- /* --row-bg drives the background of sticky (pinned) body cells.
964
- Only solid, opaque values here — semi-transparent tints for selected/preview
965
- rows intentionally do NOT override --row-bg, so pinned cells stay opaque
966
- and text from scrolling content can't bleed through. */
967
- tbody tr {
968
- --row-bg: var(--card, #fff);
969
- }
970
- tbody tr:hover {
971
- --row-bg: var(--layer-hover, #f8fafc);
972
- }
973
- </style>
974
-