@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.
- package/CHANGELOG.md +38 -0
- package/app.config.ts +10 -10
- package/components/Attention.vue +269 -0
- package/components/Auth/LoginForm.vue +1 -1
- package/components/Chart/index.vue +193 -0
- package/components/Container.vue +116 -0
- package/components/CopyButton.vue +98 -0
- package/components/Files/Card.vue +215 -0
- package/components/Files/Preview.vue +675 -0
- package/components/Files/Upload.vue +408 -0
- package/components/Files/UploadModal.vue +83 -0
- package/components/Forms/InputPin.vue +162 -0
- package/components/InfoToolbar.vue +54 -0
- package/components/Kanban/Standard.vue +588 -0
- package/components/Loader.vue +358 -0
- package/components/MobileBlocker.vue +5 -5
- package/components/MobileLoginPicker.vue +13 -13
- package/components/Modal/Base.vue +6 -0
- package/components/Modal/Confirm.vue +89 -0
- package/components/Modal/DeleteConfirm.vue +14 -31
- package/components/Modal/Form.vue +76 -0
- package/components/Modal/Host.vue +58 -0
- package/components/Modal/Prompt.vue +74 -0
- package/components/Modal.vue +119 -34
- package/components/Organization/Picker.vue +3 -3
- package/components/Table/Standard.vue +198 -25
- package/components/Table/index.vue +169 -369
- package/components/Table/views/ViewCards.vue +74 -0
- package/components/Table/views/ViewDatabase.vue +236 -0
- package/components/Table/views/ViewTable.vue +299 -0
- package/components/Toast/Alert.vue +16 -111
- package/components/Toast/Container.vue +4 -29
- package/components/Toast/Notification.vue +8 -43
- package/components/Toast/Process.vue +8 -86
- package/components/Toast/Stack.vue +59 -0
- package/components/Toast/Standard.vue +115 -0
- package/components/Tree/Standard.vue +49 -4
- package/components/Workflow/Board.vue +356 -0
- package/components/Workflow/Builder.vue +454 -0
- package/components/Workflow/Form.vue +526 -0
- package/components/Workflow/Viewer.vue +225 -0
- package/components/Workflow/edges/TransitionEdge.vue +84 -0
- package/components/Workflow/nodes/StepNode.vue +88 -0
- package/composables/useAuth.js +1 -1
- package/composables/useClipboard.js +79 -0
- package/composables/useConfetti.js +194 -0
- package/composables/useContext.js +53 -5
- package/composables/useFile.js +189 -0
- package/composables/useMobileGuard.ts +21 -21
- package/composables/useModal.js +71 -0
- package/composables/useOrganization.ts +3 -3
- package/composables/useToast.js +38 -65
- package/composables/useWorkflow/form.js +162 -0
- package/composables/useWorkflow/index.js +18 -0
- package/composables/useWorkflow/view.js +220 -0
- package/composables/useWorkflowLayout.js +162 -0
- package/css/tokens.css +10 -10
- package/docs/superpowers/plans/2026-05-27-nuxt-apps-to-contexts-rename.md +630 -0
- package/docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md +113 -0
- package/middleware/{03.apps.global.ts → 03.contexts.global.ts} +25 -25
- package/nuxt.config.ts +1 -1
- package/package.json +13 -8
- package/stores/modal.js +48 -0
- package/stores/toast.js +156 -117
- 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>
|