@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
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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: {
|
|
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
|
|
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
|
|
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 &&
|
|
316
|
-
const firstDataRow = Array.from(
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
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 (
|
|
396
|
-
|
|
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
|
-
<
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
|
861
|
-
|
|
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
|
-
|