@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
@@ -0,0 +1,74 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ table: Object,
4
+ loading: Boolean,
5
+ skeletonRows: Array,
6
+ checkable: Boolean,
7
+ gridClass: String,
8
+ search: String,
9
+ columnFilters: Array,
10
+ bordered: { type: Boolean, default: true },
11
+ })
12
+
13
+ const emit = defineEmits(['row-click'])
14
+ </script>
15
+
16
+ <template>
17
+ <div :class="['relative pb-4', bordered ? 'pt-4 px-4' : '']">
18
+ <div v-if="loading" :class="gridClass">
19
+ <div v-for="(_, i) in skeletonRows" :key="'gsk-' + i" class="animate-pulse">
20
+ <slot name="grid-skeleton">
21
+ <div class="bg-card rounded-card border border-card-line p-4">
22
+ <div class="space-y-3">
23
+ <div class="h-4 bg-surface-1 rounded w-3/4"></div>
24
+ <div class="h-4 bg-surface-1 rounded w-1/2"></div>
25
+ <div class="h-6 bg-surface-1 rounded w-1/4"></div>
26
+ </div>
27
+ </div>
28
+ </slot>
29
+ </div>
30
+ </div>
31
+
32
+ <div v-else-if="table.getRowModel().rows.length > 0" :class="gridClass">
33
+ <slot
34
+ name="grid-item"
35
+ v-for="row in table.getRowModel().rows"
36
+ :key="row.id"
37
+ :row="row.original"
38
+ :tanstack-row="row"
39
+ :is-selected="row.getIsSelected()"
40
+ :checkable="checkable"
41
+ :toggle-row="() => row.toggleSelected()"
42
+ :on-row-click="(e) => emit('row-click', row, e)"
43
+ >
44
+ <div
45
+ class="bg-card rounded-card border border-card-line p-4 hover:shadow-md transition-shadow relative cursor-pointer"
46
+ :class="{ 'ring-2 ring-indigo-400 dark:ring-indigo-600': row.getIsSelected() }"
47
+ @click="emit('row-click', row, $event)"
48
+ >
49
+ <div v-if="checkable" class="absolute top-2 left-2 z-10">
50
+ <input type="checkbox" :checked="row.getIsSelected()" @change="row.toggleSelected()"
51
+ class="rounded border-card-line dark:bg-card" @click.stop />
52
+ </div>
53
+ <div class="space-y-2" :class="{ 'pt-6': checkable }">
54
+ <div v-for="cell in row.getVisibleCells().filter(c => c.column.id !== 'select')" :key="cell.id" class="flex justify-between">
55
+ <span class="text-sm text-muted-foreground">{{ cell.column.columnDef.meta?.label ?? cell.column.id }}:</span>
56
+ <span class="text-sm text-foreground">
57
+ <slot :name="cell.column.id" :row="row.original" :value="cell.getValue()">{{ cell.getValue() }}</slot>
58
+ </span>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </slot>
63
+ </div>
64
+
65
+ <div v-else class="flex items-center justify-center py-12">
66
+ <slot v-if="!search && !columnFilters?.length" name="empty">
67
+ <p class="text-muted-foreground text-lg">No hay registros</p>
68
+ </slot>
69
+ <slot v-else name="empty-search">
70
+ <p class="text-muted-foreground text-lg">No hay registros en la búsqueda</p>
71
+ </slot>
72
+ </div>
73
+ </div>
74
+ </template>
@@ -0,0 +1,236 @@
1
+ <script setup>
2
+ import { IconLoader2, IconX } from '@tabler/icons-vue'
3
+
4
+ const props = defineProps({
5
+ table: Object,
6
+ loading: Boolean,
7
+ skeletonRows: Array,
8
+ lastRowHeight: Number,
9
+ checkable: Boolean,
10
+ editingCell: { type: Object, default: null }, // { rowId, colKey } | null
11
+ editingValue: { type: String, default: '' },
12
+ savingCell: { type: Object, default: null }, // { rowId, colKey } | null
13
+ cellError: { type: Object, default: null }, // { rowId, colKey, message } | null
14
+ })
15
+
16
+ const emit = defineEmits(['row-click', 'start-edit', 'save-edit', 'cancel-edit', 'editing-value-change'])
17
+
18
+ const isCellEditing = (rowId, colKey) =>
19
+ props.editingCell?.rowId === rowId && props.editingCell?.colKey === colKey
20
+
21
+ const isCellSaving = (rowId, colKey) =>
22
+ props.savingCell?.rowId === rowId && props.savingCell?.colKey === colKey
23
+
24
+ const isCellError = (rowId, colKey) =>
25
+ props.cellError?.rowId === rowId && props.cellError?.colKey === colKey
26
+
27
+ const getVisibleDataCols = (row) =>
28
+ row.getVisibleCells().filter(c => c.column.id !== 'select')
29
+
30
+ const isEditable = (col) => col.columnDef.meta?.editable === true
31
+
32
+ // Tab key: save current cell and move to next editable cell
33
+ const handleTabKey = async (e, row, col) => {
34
+ e.preventDefault()
35
+ emit('save-edit', row, col)
36
+
37
+ // Find next editable cell
38
+ await nextTick()
39
+ const rows = props.table.getRowModel().rows
40
+ const rowIdx = rows.findIndex(r => r.id === row.id)
41
+ const cols = getVisibleDataCols(row)
42
+ const colIdx = cols.findIndex(c => c.column.id === col.column.id)
43
+
44
+ // Try next cells in the same row
45
+ for (let ci = colIdx + 1; ci < cols.length; ci++) {
46
+ if (isEditable(cols[ci].column)) {
47
+ const el = document.querySelector(`[data-cell-editable][data-row-idx="${rowIdx}"][data-col-idx="${ci}"]`)
48
+ if (el) { el.focus(); return }
49
+ }
50
+ }
51
+
52
+ // Try first editable cell in next row
53
+ for (let ri = rowIdx + 1; ri < rows.length; ri++) {
54
+ const nextRowCols = getVisibleDataCols(rows[ri])
55
+ for (let ci = 0; ci < nextRowCols.length; ci++) {
56
+ if (isEditable(nextRowCols[ci].column)) {
57
+ const el = document.querySelector(`[data-cell-editable][data-row-idx="${ri}"][data-col-idx="${ci}"]`)
58
+ if (el) { el.focus(); return }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ </script>
64
+
65
+ <template>
66
+ <div class="overflow-x-auto">
67
+ <table
68
+ class="w-full divide-y divide-card-line"
69
+ :style="{ tableLayout: 'auto', minWidth: '100%' }"
70
+ >
71
+ <thead class="bg-surface">
72
+ <tr>
73
+ <th
74
+ v-if="checkable"
75
+ scope="col"
76
+ class="text-center py-1.5 px-2.5 border-b border-card-line"
77
+ style="width: 36px"
78
+ >
79
+ <input
80
+ type="checkbox"
81
+ :checked="table.getIsAllRowsSelected()"
82
+ :indeterminate="table.getIsSomeRowsSelected()"
83
+ @change="table.getToggleAllRowsSelectedHandler()($event)"
84
+ class="shrink-0 border-card-line rounded-control text-blue-900 focus:ring-0 focus:ring-offset-0 dark:bg-card"
85
+ />
86
+ </th>
87
+ <th
88
+ v-for="header in table.getHeaderGroups()[0]?.headers.filter(h => h.id !== 'select') ?? []"
89
+ :key="header.id"
90
+ scope="col"
91
+ class="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider py-1.5 px-2.5 border-b border-card-line bg-surface text-left whitespace-nowrap"
92
+ >
93
+ {{ header.column.columnDef.meta?.label ?? header.id }}
94
+ </th>
95
+ </tr>
96
+ </thead>
97
+
98
+ <tbody class="divide-y divide-card-line/50">
99
+ <!-- Loading skeleton -->
100
+ <tr
101
+ v-if="loading"
102
+ v-for="(_, i) in skeletonRows"
103
+ :key="'dbsk-' + i"
104
+ class="animate-pulse bg-card"
105
+ >
106
+ <td
107
+ v-if="checkable"
108
+ class="py-2 px-2.5"
109
+ >
110
+ <div class="w-3 h-3 bg-surface-1 rounded mx-auto"></div>
111
+ </td>
112
+ <td
113
+ v-for="header in table.getHeaderGroups()[0]?.headers.filter(h => h.id !== 'select') ?? []"
114
+ :key="'dbskc-' + header.id"
115
+ class="py-2 px-2.5"
116
+ >
117
+ <div class="h-3 w-[60%] rounded bg-surface-1"></div>
118
+ </td>
119
+ </tr>
120
+
121
+ <!-- Empty state rows -->
122
+ <tr
123
+ v-if="!loading && table.getRowModel().rows.length === 0"
124
+ v-for="i in (table.getState?.().pagination?.pageSize ?? 10)"
125
+ :key="'dbesk-' + i"
126
+ class="bg-card"
127
+ >
128
+ <td
129
+ v-if="checkable"
130
+ class="py-2"
131
+ />
132
+ <td
133
+ v-for="header in table.getHeaderGroups()[0]?.headers.filter(h => h.id !== 'select') ?? []"
134
+ :key="'dbeskc-' + header.id"
135
+ class="py-2"
136
+ />
137
+ </tr>
138
+
139
+ <!-- Data rows -->
140
+ <tr
141
+ v-else
142
+ v-for="(row, rowIdx) in table.getRowModel().rows"
143
+ :key="row.id"
144
+ class="hover:bg-layer-hover transition-colors border-b border-card-line/50"
145
+ :class="{
146
+ 'bg-indigo-50/40 dark:bg-indigo-900/10': row.getIsSelected(),
147
+ }"
148
+ @click="emit('row-click', row, $event)"
149
+ >
150
+ <!-- Checkbox cell -->
151
+ <td
152
+ v-if="checkable"
153
+ class="text-center py-1.5 px-2.5"
154
+ style="width: 36px"
155
+ @click.stop
156
+ >
157
+ <input
158
+ type="checkbox"
159
+ :checked="row.getIsSelected()"
160
+ :disabled="!row.getCanSelect()"
161
+ @change="row.getToggleSelectedHandler()($event)"
162
+ class="rounded border-card-line focus:ring-0 focus:ring-offset-0 dark:bg-card"
163
+ />
164
+ </td>
165
+
166
+ <!-- Data cells -->
167
+ <td
168
+ v-for="(cell, colIdx) in getVisibleDataCols(row)"
169
+ :key="cell.id"
170
+ class="py-2 px-2.5 text-xs text-foreground relative group/cell overflow-hidden"
171
+ :class="cell.column.columnDef.meta?.class ?? ''"
172
+ :data-cell-editable="isEditable(cell.column) || undefined"
173
+ :data-row-idx="rowIdx"
174
+ :data-col-idx="colIdx"
175
+ :tabindex="isEditable(cell.column) && !isCellEditing(row.id, cell.column.id) ? 0 : undefined"
176
+ @click.stop="isEditable(cell.column) && !isCellEditing(row.id, cell.column.id) && emit('start-edit', row, cell)"
177
+ @keydown.enter.prevent="isEditable(cell.column) && !isCellEditing(row.id, cell.column.id) && emit('start-edit', row, cell)"
178
+ >
179
+ <!-- Editing state — absolute overlay so it never changes the row height -->
180
+ <template v-if="isCellEditing(row.id, cell.column.id)">
181
+ <div class="absolute inset-0 z-10 ring-1 ring-inset ring-indigo-400 bg-card">
182
+ <input
183
+ :value="editingValue"
184
+ @input="emit('editing-value-change', $event.target.value)"
185
+ @keydown.enter.prevent="emit('save-edit', row, cell)"
186
+ @keydown.escape.prevent="emit('cancel-edit')"
187
+ @keydown.tab="handleTabKey($event, row, cell)"
188
+ @click.stop
189
+ autofocus
190
+ class="w-full h-full px-2.5 text-xs text-foreground bg-transparent outline-none"
191
+ />
192
+ </div>
193
+ </template>
194
+
195
+ <!-- Saving state -->
196
+ <template v-else-if="isCellSaving(row.id, cell.column.id)">
197
+ <div class="flex items-center gap-1.5 text-muted-foreground">
198
+ <IconLoader2 class="size-3 animate-spin shrink-0 text-indigo-400" />
199
+ <span class="truncate">{{ cell.getValue() }}</span>
200
+ </div>
201
+ </template>
202
+
203
+ <!-- Error state -->
204
+ <template v-else-if="isCellError(row.id, cell.column.id)">
205
+ <div class="flex items-center gap-1 text-red-500" :title="cellError?.message">
206
+ <IconX class="size-3 shrink-0" />
207
+ <span class="truncate text-foreground">{{ cell.getValue() }}</span>
208
+ </div>
209
+ </template>
210
+
211
+ <!-- Normal display -->
212
+ <template v-else>
213
+ <div class="flex items-center gap-1">
214
+ <span class="truncate flex-1">
215
+ <slot :name="cell.column.id" :row="row.original" :value="cell.getValue()">
216
+ {{ cell.getValue() }}
217
+ </slot>
218
+ </span>
219
+ <!-- Pencil indicator for editable cells -->
220
+ <svg
221
+ v-if="isEditable(cell.column)"
222
+ xmlns="http://www.w3.org/2000/svg"
223
+ viewBox="0 0 16 16"
224
+ fill="currentColor"
225
+ class="size-2.5 shrink-0 text-muted-foreground opacity-0 group-hover/cell:opacity-60 transition-opacity"
226
+ >
227
+ <path d="M13.488 2.513a1.75 1.75 0 0 0-2.475 0L3.76 9.768a1.75 1.75 0 0 0-.455.88l-.5 2.5a.75.75 0 0 0 .875.876l2.5-.5a1.75 1.75 0 0 0 .88-.455l7.255-7.254a1.75 1.75 0 0 0 0-2.474ZM12.72 7.54l-.72.72L10.5 6.76l.72-.72 1.5 1.5ZM3.963 9.716l6.037-6.037 1.5 1.5-6.037 6.037a.25.25 0 0 1-.126.065l-1.68.336.336-1.68a.25.25 0 0 1 .065-.126Z" />
228
+ </svg>
229
+ </div>
230
+ </template>
231
+ </td>
232
+ </tr>
233
+ </tbody>
234
+ </table>
235
+ </div>
236
+ </template>
@@ -0,0 +1,299 @@
1
+ <script setup>
2
+ import {
3
+ IconSelector,
4
+ IconChevronUp,
5
+ IconChevronDown,
6
+ IconPin,
7
+ } from '@tabler/icons-vue'
8
+
9
+ const props = defineProps({
10
+ table: Object,
11
+ loading: Boolean,
12
+ skeletonRows: Array,
13
+ lastRowHeight: Number,
14
+ checkable: Boolean,
15
+ previewRowId: { type: [String, Number], default: null },
16
+ isRowClickEnabled: Boolean,
17
+ search: String,
18
+ columnFilters: Array,
19
+ hasFilterableColumns: Boolean,
20
+ pagination: Object,
21
+ getPinnedStyles: Function,
22
+ pinnedRowStyle: Function,
23
+ onHeaderDrop: Function,
24
+ onAutoSizeColumn: Function,
25
+ })
26
+
27
+ const emit = defineEmits(['row-click', 'row-keydown'])
28
+
29
+ // Internal drag state — purely visual, owned by this view
30
+ let draggedHeaderId = null
31
+ const dragOverHeaderId = ref(null)
32
+ const resizeHoverId = ref(null)
33
+
34
+ const tableBodyEl = ref(null)
35
+ defineExpose({ tableBodyEl })
36
+
37
+ const onHeaderDragStart = (colId) => { draggedHeaderId = colId }
38
+ const onHeaderDragOver = (e, colId) => { e.preventDefault(); dragOverHeaderId.value = colId }
39
+ const onHeaderDragLeave = () => { dragOverHeaderId.value = null }
40
+ const onHeaderDropLocal = (colId) => {
41
+ if (!draggedHeaderId || draggedHeaderId === colId) { draggedHeaderId = null; dragOverHeaderId.value = null; return }
42
+ if (colId === 'select') { draggedHeaderId = null; dragOverHeaderId.value = null; return }
43
+ props.onHeaderDrop?.(draggedHeaderId, colId)
44
+ draggedHeaderId = null
45
+ dragOverHeaderId.value = null
46
+ }
47
+ </script>
48
+
49
+ <template>
50
+ <div class="overflow-x-auto relative">
51
+ <table
52
+ class="relative divide-y divide-card-line"
53
+ :style="{ tableLayout: 'fixed', width: table.getTotalSize() + 'px', minWidth: '100%' }"
54
+ >
55
+ <colgroup>
56
+ <col
57
+ v-for="col in [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns(), ...table.getRightVisibleLeafColumns()]"
58
+ :key="col.id"
59
+ :style="{ width: col.getSize() + 'px' }"
60
+ >
61
+ </colgroup>
62
+ <thead class="relative z-20 bg-card">
63
+ <template v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
64
+ <!-- Main header row -->
65
+ <tr class="bg-card">
66
+ <th
67
+ v-for="header in headerGroup.headers"
68
+ :key="header.id"
69
+ scope="col"
70
+ :draggable="header.id !== 'select' && resizeHoverId !== header.id && !header.column.getIsPinned()"
71
+ @dragstart="header.id !== 'select' && resizeHoverId !== header.id && !header.column.getIsPinned() && onHeaderDragStart(header.id)"
72
+ @dragover="header.id !== 'select' && onHeaderDragOver($event, header.id)"
73
+ @dragleave="onHeaderDragLeave"
74
+ @drop="header.id !== 'select' && onHeaderDropLocal(header.id)"
75
+ class="relative"
76
+ :class="[
77
+ header.id === 'select' ? 'text-center' : '',
78
+ dragOverHeaderId === header.id ? 'bg-indigo-50 dark:bg-indigo-900/20' : '',
79
+ header.column.getCanSort() ? 'cursor-pointer select-none' : '',
80
+ ]"
81
+ :style="getPinnedStyles(header.column, true)"
82
+ @click="header.column.getCanSort() && header.column.toggleSorting()"
83
+ >
84
+ <!-- Select all checkbox -->
85
+ <template v-if="header.id === 'select'">
86
+ <input
87
+ type="checkbox"
88
+ :checked="table.getIsAllRowsSelected()"
89
+ :indeterminate="table.getIsSomeRowsSelected()"
90
+ @change="table.getToggleAllRowsSelectedHandler()($event)"
91
+ class="mx-2 shrink-0 border-card-line rounded-control text-blue-900 focus:ring-0 focus:ring-offset-0 dark:bg-card"
92
+ />
93
+ </template>
94
+ <!-- Regular column header -->
95
+ <template v-else>
96
+ <div
97
+ class="px-4 py-3 flex items-center gap-x-1 text-xs font-medium w-full overflow-hidden"
98
+ :class="header.column.getIsPinned() ? 'text-foreground' : 'text-muted-foreground'"
99
+ >
100
+ <IconPin v-if="header.column.getIsPinned()" class="size-3 shrink-0 text-indigo-400 dark:text-indigo-500" />
101
+ <span class="truncate">{{ header.column.columnDef.meta?.label ?? header.id }}</span>
102
+ <span v-if="header.column.getCanSort()">
103
+ <IconSelector v-if="!header.column.getIsSorted()" class="size-4 opacity-40" />
104
+ <IconChevronDown v-else-if="header.column.getIsSorted() === 'desc'" class="size-4" />
105
+ <IconChevronUp v-else class="size-4" />
106
+ </span>
107
+ </div>
108
+ <!-- Resize handle -->
109
+ <div
110
+ v-if="header.column.getCanResize()"
111
+ class="absolute right-0 top-0 h-full w-3 cursor-col-resize group/rz flex items-center justify-center select-none touch-none"
112
+ @mouseenter="resizeHoverId = header.id"
113
+ @mouseleave="resizeHoverId = null"
114
+ @mousedown.stop="header.getResizeHandler()?.($event)"
115
+ @touchstart.passive.stop="header.getResizeHandler()?.($event)"
116
+ @dblclick.stop="onAutoSizeColumn?.(header)"
117
+ @dragstart.stop.prevent
118
+ @click.stop
119
+ >
120
+ <div
121
+ class="h-4 w-px transition-all"
122
+ :class="header.column.getIsResizing()
123
+ ? 'bg-indigo-400 dark:bg-indigo-500 !w-0.5'
124
+ : 'bg-surface-1 group-hover/rz:bg-indigo-300 dark:group-hover/rz:bg-indigo-600 group-hover/rz:w-0.5'"
125
+ />
126
+ </div>
127
+ </template>
128
+ </th>
129
+ </tr>
130
+
131
+ <!-- Column filter row -->
132
+ <tr
133
+ v-if="hasFilterableColumns"
134
+ class="border-b border-card-line bg-muted/50"
135
+ >
136
+ <th
137
+ v-for="header in headerGroup.headers"
138
+ :key="'f-' + header.id"
139
+ :class="[
140
+ header.id === 'select' ? 'w-12' : 'px-3 py-1.5',
141
+ ]"
142
+ :style="getPinnedStyles(header.column, true)"
143
+ >
144
+ <input
145
+ v-if="header.column.getCanFilter()"
146
+ :value="header.column.getFilterValue() ?? ''"
147
+ @input="(e) => header.column.setFilterValue(e.target.value || undefined)"
148
+ :placeholder="`Filtrar ${header.column.columnDef.meta?.label ?? ''}...`"
149
+ 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"
150
+ />
151
+ </th>
152
+ </tr>
153
+ </template>
154
+ </thead>
155
+
156
+ <tbody ref="tableBodyEl" class="divide-y divide-card-line">
157
+ <!-- Loading skeleton rows -->
158
+ <tr
159
+ v-if="loading"
160
+ v-for="(_, i) in skeletonRows"
161
+ :key="'sk-' + i"
162
+ class="animate-pulse bg-card"
163
+ >
164
+ <td
165
+ v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
166
+ :key="'skc-' + header.id"
167
+ :class="[
168
+ header.id === 'select' ? 'text-center w-12' : 'px-4 overflow-hidden',
169
+ ]"
170
+ :style="{ height: lastRowHeight + 'px', ...getPinnedStyles(header.column) }"
171
+ >
172
+ <div v-if="header.id === 'select'" class="w-4 h-4 bg-surface-1 rounded mx-auto"></div>
173
+ <div v-else class="h-4 w-[50%] rounded bg-surface-1"></div>
174
+ </td>
175
+ </tr>
176
+
177
+ <!-- Loading filler rows -->
178
+ <tr
179
+ v-if="loading && skeletonRows.length < pagination.pageSize"
180
+ v-for="i in (pagination.pageSize - skeletonRows.length)"
181
+ :key="'lf-' + i"
182
+ class="bg-card"
183
+ >
184
+ <td
185
+ v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
186
+ :key="'lfc-' + header.id"
187
+ :style="{ height: lastRowHeight + 'px', ...getPinnedStyles(header.column) }"
188
+ />
189
+ </tr>
190
+
191
+ <!-- Empty filler rows -->
192
+ <tr
193
+ v-if="!loading && table.getRowModel().rows.length === 0"
194
+ v-for="i in pagination.pageSize"
195
+ :key="'esk-' + i"
196
+ class="bg-card"
197
+ >
198
+ <td
199
+ v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
200
+ :key="'eskc-' + header.id"
201
+ :style="{ height: lastRowHeight + 'px', ...getPinnedStyles(header.column) }"
202
+ />
203
+ </tr>
204
+
205
+ <!-- Data rows -->
206
+ <tr
207
+ v-else
208
+ v-for="row in table.getRowModel().rows"
209
+ :key="row.id"
210
+ data-row-type="data"
211
+ @click="(e) => emit('row-click', row, e)"
212
+ @keydown="(e) => emit('row-keydown', row, e)"
213
+ :tabindex="isRowClickEnabled ? 0 : undefined"
214
+ class="bg-card hover:bg-layer-hover transition-colors"
215
+ :class="{
216
+ 'cursor-pointer': isRowClickEnabled,
217
+ 'bg-indigo-50/40 dark:bg-indigo-900/10 hover:bg-indigo-50/60': row.getIsSelected(),
218
+ '!bg-indigo-50 dark:!bg-indigo-900/20 ring-1 ring-inset ring-indigo-200 dark:ring-indigo-700': previewRowId && row.original.id === previewRowId,
219
+ }"
220
+ :style="pinnedRowStyle(row)"
221
+ >
222
+ <td
223
+ v-for="cell in row.getVisibleCells()"
224
+ :key="cell.id"
225
+ :data-col-id="cell.column.id"
226
+ :class="[
227
+ cell.column.id === 'select'
228
+ ? 'text-center w-12 overflow-hidden'
229
+ : 'px-4 py-3 text-sm text-muted-foreground-1 overflow-hidden',
230
+ cell.column.id !== 'select' ? cell.column.columnDef.meta?.class ?? '' : '',
231
+ ]"
232
+ :style="getPinnedStyles(cell.column)"
233
+ >
234
+ <!-- Select checkbox -->
235
+ <template v-if="cell.column.id === 'select'">
236
+ <div @click.stop>
237
+ <input
238
+ type="checkbox"
239
+ :checked="row.getIsSelected()"
240
+ :disabled="!row.getCanSelect()"
241
+ @change="row.getToggleSelectedHandler()($event)"
242
+ class="rounded border-card-line focus:ring-0 focus:ring-offset-0 dark:bg-card"
243
+ />
244
+ </div>
245
+ </template>
246
+ <!-- Data cell with slot -->
247
+ <template v-else>
248
+ <slot :name="cell.column.id" :row="row.original" :value="cell.getValue()">
249
+ {{ cell.getValue() }}
250
+ </slot>
251
+ </template>
252
+ </td>
253
+ </tr>
254
+
255
+ <!-- Filler rows: pad table to full page height -->
256
+ <tr
257
+ v-if="!loading && table.getRowModel().rows.length > 0 && table.getRowModel().rows.length < pagination.pageSize"
258
+ v-for="i in (pagination.pageSize - table.getRowModel().rows.length)"
259
+ :key="'fill-' + i"
260
+ class="bg-card"
261
+ >
262
+ <td
263
+ v-for="header in (table.getHeaderGroups()[0]?.headers ?? [])"
264
+ :key="'fillc-' + header.id"
265
+ :style="{ height: lastRowHeight + 'px', ...getPinnedStyles(header.column) }"
266
+ />
267
+ </tr>
268
+ </tbody>
269
+ </table>
270
+
271
+ <!-- Empty state overlays -->
272
+ <div
273
+ v-if="!loading && table.getRowModel().rows.length === 0 && !search && !columnFilters?.length"
274
+ class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-card/60 rounded-card"
275
+ >
276
+ <slot name="empty">
277
+ <p class="text-muted-foreground text-lg font-medium italic">No hay registros</p>
278
+ </slot>
279
+ </div>
280
+
281
+ <div
282
+ v-if="!loading && table.getRowModel().rows.length === 0 && (search || columnFilters?.length)"
283
+ class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-card/60 rounded-card"
284
+ >
285
+ <slot name="empty-search">
286
+ <p class="text-muted-foreground text-lg font-medium italic">No hay registros en la búsqueda</p>
287
+ </slot>
288
+ </div>
289
+ </div>
290
+ </template>
291
+
292
+ <style scoped>
293
+ tbody tr {
294
+ --row-bg: var(--card, #fff);
295
+ }
296
+ tbody tr:hover {
297
+ --row-bg: var(--layer-hover, #f8fafc);
298
+ }
299
+ </style>