@innertia-solutions/innertia-nuxt 0.1.1
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/.github/workflows/auto-publish.yml +64 -0
- package/.github/workflows/release.yml +59 -0
- package/README.md +60 -0
- package/app.config.ts +70 -0
- package/components/Admin/Base.vue +144 -0
- package/components/Admin/Header.vue +32 -0
- package/components/Admin/Page.vue +65 -0
- package/components/Admin/PageHeader.vue +31 -0
- package/components/App/Button.vue +59 -0
- package/components/App/DevEnvironmentBar.vue +43 -0
- package/components/App/Dropdown.vue +286 -0
- package/components/App/EmptyState.vue +433 -0
- package/components/App/LoadingState.vue +40 -0
- package/components/App/PageLoadingSpinner.vue +118 -0
- package/components/App/PreviewDock.vue +64 -0
- package/components/App/SwitchColorTheme.vue +51 -0
- package/components/App/Tag.vue +193 -0
- package/components/DataTable.vue +713 -0
- package/components/Forms/DatePicker.vue +255 -0
- package/components/Forms/Input.vue +75 -0
- package/components/Forms/Select.vue +100 -0
- package/components/Forms/SelectServer.vue +726 -0
- package/components/Layout/Admin.vue +32 -0
- package/components/Layout/Auth.vue +29 -0
- package/components/Layout/SidebarWithAppColumn.vue +388 -0
- package/components/Layout/TopBar.vue +113 -0
- package/components/MobileBlocker.vue +85 -0
- package/components/MobileLoginPicker.vue +83 -0
- package/components/Modal/Base.vue +29 -0
- package/components/Modal/DeleteConfirm.vue +48 -0
- package/components/Modal.vue +103 -0
- package/components/Nav/Tabs.vue +55 -0
- package/components/PermissionsTree.vue +272 -0
- package/components/Table/Database.vue +183 -0
- package/components/Table/DownloadDropdown.vue +111 -0
- package/components/Table/Enterprise.vue +540 -0
- package/components/Table/FilterDropdown.vue +226 -0
- package/components/Table/Grid.vue +62 -0
- package/components/Table/Kanban.vue +188 -0
- package/components/Table/List.vue +128 -0
- package/components/Table/PreviewTimeline.vue +118 -0
- package/components/Table/Standard.vue +1217 -0
- package/components/Table/index.vue +974 -0
- package/components/TableExportable.vue +172 -0
- package/components/TableFilter.vue +93 -0
- package/components/Toast/Alert.vue +113 -0
- package/components/Toast/Container.vue +34 -0
- package/components/Toast/Notification.vue +45 -0
- package/components/Toast/Process.vue +88 -0
- package/composables/useApi.js +95 -0
- package/composables/useApp.ts +46 -0
- package/composables/useAuth.js +82 -0
- package/composables/useContext.js +44 -0
- package/composables/useDate.js +241 -0
- package/composables/useDevice.js +21 -0
- package/composables/useDockedPreviews.js +56 -0
- package/composables/useDownload.js +87 -0
- package/composables/useEntity.js +82 -0
- package/composables/useForm.js +119 -0
- package/composables/useInnertiaMode.ts +25 -0
- package/composables/useMobileGuard.ts +81 -0
- package/composables/useNotifications.js +22 -0
- package/composables/usePermissions.js +23 -0
- package/composables/useRealtime.js +123 -0
- package/composables/useRequestInterceptors.js +27 -0
- package/composables/useRoles.js +53 -0
- package/composables/useRutFormatter.js +39 -0
- package/composables/useTable.ts +94 -0
- package/composables/useTablePreferences.ts +33 -0
- package/composables/useTenant.js +27 -0
- package/composables/useTimeAgo.js +37 -0
- package/composables/useToast.js +69 -0
- package/composables/useUserRealtime.js +17 -0
- package/composables/useUsers.js +111 -0
- package/css/themes/autumn.css +401 -0
- package/css/themes/bubblegum.css +408 -0
- package/css/themes/cashmere.css +412 -0
- package/css/themes/harvest.css +416 -0
- package/css/themes/moon.css +140 -0
- package/css/themes/ocean.css +273 -0
- package/css/themes/olive.css +413 -0
- package/css/themes/retro.css +431 -0
- package/css/themes/theme.css +725 -0
- package/error.vue +78 -0
- package/middleware/01.detect-subdomain.global.ts +43 -0
- package/middleware/02.validate-tenant.global.ts +67 -0
- package/middleware/03.apps.global.ts +88 -0
- package/middleware/auth.ts +9 -0
- package/middleware/guest.ts +9 -0
- package/nuxt.config.ts +42 -0
- package/package.json +60 -0
- package/pages/tenant-error.vue +50 -0
- package/plugins/api-auth.ts +12 -0
- package/plugins/api-tenant.client.ts +21 -0
- package/plugins/appearance.ts +8 -0
- package/plugins/auth-init.ts +34 -0
- package/plugins/dark-state.client.ts +29 -0
- package/plugins/dockedPreviewsSync.client.js +17 -0
- package/plugins/preline.client.ts +68 -0
- package/plugins/theme.client.ts +7 -0
- package/plugins/vue-query.ts +29 -0
- package/public/init-theme.js +15 -0
- package/spark.css +721 -0
- package/stores/auth.js +130 -0
- package/stores/dockedPreviews.js +34 -0
- package/stores/notifications.js +24 -0
- package/stores/tenant.js +54 -0
- package/stores/toast.js +129 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { IconSearch, IconAdjustmentsHorizontal, IconLayoutColumns, IconGripVertical, IconDownload, IconBookmark, IconChevronDown, IconX } from '@tabler/icons-vue'
|
|
3
|
+
|
|
4
|
+
// ─── Props ────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
// DataTable wiring
|
|
8
|
+
table: { type: Object, default: null },
|
|
9
|
+
endpoint: { type: String, default: '' },
|
|
10
|
+
endpointParams: { type: Object, default: () => ({}) },
|
|
11
|
+
columns: { type: Array, required: true },
|
|
12
|
+
name: { type: String, default: '' },
|
|
13
|
+
params: { type: Object, default: () => ({}) },
|
|
14
|
+
cached: { type: Boolean, default: true },
|
|
15
|
+
showReloadButton: { type: Boolean, default: true },
|
|
16
|
+
clickRowToOpen: { type: Boolean, default: false },
|
|
17
|
+
// Toolbar
|
|
18
|
+
searchPlaceholder: { type: String, default: 'Buscar...' },
|
|
19
|
+
showSearch: { type: Boolean, default: true },
|
|
20
|
+
showFilters: { type: Boolean, default: true },
|
|
21
|
+
showColumns: { type: Boolean, default: true },
|
|
22
|
+
showExport: { type: Boolean, default: true },
|
|
23
|
+
showSaveView: { type: Boolean, default: false },
|
|
24
|
+
// Checkboxes
|
|
25
|
+
checkable: { type: Boolean, default: true },
|
|
26
|
+
// Preview panel tabs: [{ key: 'resumen', label: 'Resumen' }, ...]
|
|
27
|
+
previewTabs: { type: Array, default: () => [] },
|
|
28
|
+
// Split ratio
|
|
29
|
+
splitRatio: { type: Number, default: 55 },
|
|
30
|
+
// Filters as chips in the filter bar: [{ key, label, options: [{value,label}] }]
|
|
31
|
+
filterChips: { type: Array, default: () => [] },
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
|
|
35
|
+
const resolvedName = computed(() => props.table?.name ?? props.name)
|
|
36
|
+
|
|
37
|
+
// ─── Emits / Slots ────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const emit = defineEmits(['row-click', 'loaded', 'save-view'])
|
|
40
|
+
const slots = useSlots()
|
|
41
|
+
|
|
42
|
+
const excludedSlots = new Set(['toolbar', 'action', 'filter-bar', 'preview', 'preview-header'])
|
|
43
|
+
const forwardedSlots = computed(() =>
|
|
44
|
+
Object.fromEntries(Object.entries(slots).filter(([k]) => !excludedSlots.has(k)))
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
// ─── Search & filters ─────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const search = ref('')
|
|
50
|
+
const activeFilters = ref({})
|
|
51
|
+
|
|
52
|
+
// Filter chips state: { [key]: selectedValue }
|
|
53
|
+
const chipValues = reactive(
|
|
54
|
+
Object.fromEntries((props.filterChips ?? []).map(f => [f.key, '']))
|
|
55
|
+
)
|
|
56
|
+
const openChip = ref(null)
|
|
57
|
+
|
|
58
|
+
const clearChips = () => {
|
|
59
|
+
for (const key of Object.keys(chipValues)) chipValues[key] = ''
|
|
60
|
+
}
|
|
61
|
+
const hasActiveChips = computed(() =>
|
|
62
|
+
Object.values(chipValues).some(v => v !== '' && v !== null && v !== undefined)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
// Advanced filter panel
|
|
66
|
+
const showFilterPanel = ref(false)
|
|
67
|
+
const filterPanelRef = ref(null)
|
|
68
|
+
|
|
69
|
+
const filtersConfig = computed(() =>
|
|
70
|
+
props.columns.filter(c => c.filterType)
|
|
71
|
+
)
|
|
72
|
+
const activeFilterCount = computed(() =>
|
|
73
|
+
Object.values(activeFilters.value).filter(v => v !== null && v !== undefined && v !== '').length
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const mergedParams = computed(() => ({
|
|
77
|
+
...props.params,
|
|
78
|
+
...props.endpointParams,
|
|
79
|
+
...activeFilters.value,
|
|
80
|
+
...Object.fromEntries(Object.entries(chipValues).filter(([, v]) => v !== '')),
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
// ─── Preview panel ────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
const previewRow = ref(null)
|
|
86
|
+
const currentRatio = ref(props.splitRatio)
|
|
87
|
+
const containerRef = ref(null)
|
|
88
|
+
const previewEnabled = ref(false)
|
|
89
|
+
const paginationHeight = ref(0)
|
|
90
|
+
const previewFromCache = ref(false)
|
|
91
|
+
|
|
92
|
+
const previewCacheKey = computed(() => `table-enterprise-preview-${resolvedName.value}`)
|
|
93
|
+
|
|
94
|
+
// Active preview tab
|
|
95
|
+
const firstTabKey = computed(() =>
|
|
96
|
+
props.previewTabs?.length ? props.previewTabs[0].key : 'datos'
|
|
97
|
+
)
|
|
98
|
+
const previewTab = ref(firstTabKey.value)
|
|
99
|
+
watch(previewRow, () => { previewTab.value = firstTabKey.value })
|
|
100
|
+
|
|
101
|
+
const closePreview = () => { previewRow.value = null }
|
|
102
|
+
|
|
103
|
+
const tableRef = ref(null)
|
|
104
|
+
const tableMeta = ref(null)
|
|
105
|
+
|
|
106
|
+
const handleRowClick = (row) => {
|
|
107
|
+
if (previewEnabled.value) {
|
|
108
|
+
previewRow.value = previewRow.value?.id === row.id ? null : row
|
|
109
|
+
} else {
|
|
110
|
+
emit('row-click', row)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const handleLoaded = (res) => {
|
|
115
|
+
emit('loaded', res)
|
|
116
|
+
if (res?.meta) tableMeta.value = res.meta
|
|
117
|
+
if (previewRow.value && Array.isArray(res?.data)) {
|
|
118
|
+
const fresh = res.data.find(r => r.id === previewRow.value.id)
|
|
119
|
+
if (fresh) previewRow.value = fresh
|
|
120
|
+
else closePreview()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Persist preview in session cache
|
|
125
|
+
watch(previewRow, (row) => {
|
|
126
|
+
if (!props.cached) return
|
|
127
|
+
if (row) sessionStorage.setItem(previewCacheKey.value, JSON.stringify(row))
|
|
128
|
+
else sessionStorage.removeItem(previewCacheKey.value)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Track pagination height
|
|
132
|
+
let paginationObserver = null
|
|
133
|
+
watch(() => tableRef.value?.paginationBarRef, (el) => {
|
|
134
|
+
paginationObserver?.disconnect()
|
|
135
|
+
paginationObserver = null
|
|
136
|
+
if (!el) return
|
|
137
|
+
paginationHeight.value = el.offsetHeight
|
|
138
|
+
paginationObserver = new ResizeObserver(() => { paginationHeight.value = el.offsetHeight })
|
|
139
|
+
paginationObserver.observe(el)
|
|
140
|
+
}, { flush: 'post' })
|
|
141
|
+
|
|
142
|
+
// Resize handle
|
|
143
|
+
const startResize = (e) => {
|
|
144
|
+
e.preventDefault()
|
|
145
|
+
const onMove = (ev) => {
|
|
146
|
+
if (!containerRef.value) return
|
|
147
|
+
const rect = containerRef.value.getBoundingClientRect()
|
|
148
|
+
const ratio = ((ev.clientX - rect.left) / rect.width) * 100
|
|
149
|
+
currentRatio.value = Math.min(80, Math.max(25, ratio))
|
|
150
|
+
}
|
|
151
|
+
const onUp = () => {
|
|
152
|
+
window.removeEventListener('mousemove', onMove)
|
|
153
|
+
window.removeEventListener('mouseup', onUp)
|
|
154
|
+
}
|
|
155
|
+
window.addEventListener('mousemove', onMove)
|
|
156
|
+
window.addEventListener('mouseup', onUp)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Column panel ─────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
const showColumnPanel = ref(false)
|
|
162
|
+
const columnPanelRef = ref(null)
|
|
163
|
+
const columnButtonRef = ref(null)
|
|
164
|
+
const columnPanelStyle = ref({})
|
|
165
|
+
|
|
166
|
+
const orderedColumns = computed(() => {
|
|
167
|
+
if (!tableRef.value) return props.columns.filter(c => c.label)
|
|
168
|
+
const ids = tableRef.value.table.getAllLeafColumns().map(c => c.id).filter(id => id !== 'select')
|
|
169
|
+
return ids.map(id => props.columns.find(c => c.key === id)).filter(c => c?.label)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
let draggedKey = null
|
|
173
|
+
const dragOverKey = ref(null)
|
|
174
|
+
const onDragStart = (key) => { draggedKey = key }
|
|
175
|
+
const onDragOver = (e, key) => { e.preventDefault(); dragOverKey.value = key }
|
|
176
|
+
const onDragLeave = () => { dragOverKey.value = null }
|
|
177
|
+
const onDrop = (key) => {
|
|
178
|
+
if (!draggedKey || draggedKey === key) return
|
|
179
|
+
const ids = tableRef.value?.table.getAllLeafColumns().map(c => c.id) ?? []
|
|
180
|
+
const from = ids.indexOf(draggedKey)
|
|
181
|
+
const to = ids.indexOf(key)
|
|
182
|
+
if (from < 0 || to < 0) return
|
|
183
|
+
ids.splice(from, 1); ids.splice(to, 0, draggedKey)
|
|
184
|
+
const selIdx = ids.indexOf('select')
|
|
185
|
+
if (selIdx > 0) { ids.splice(selIdx, 1); ids.unshift('select') }
|
|
186
|
+
tableRef.value?.setColumnOrder(ids)
|
|
187
|
+
draggedKey = null; dragOverKey.value = null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Outside-click helpers ────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
const onColumnOutsideClick = (e) => {
|
|
193
|
+
if (
|
|
194
|
+
columnPanelRef.value && !columnPanelRef.value.contains(e.target) &&
|
|
195
|
+
columnButtonRef.value && !columnButtonRef.value.contains(e.target)
|
|
196
|
+
) showColumnPanel.value = false
|
|
197
|
+
}
|
|
198
|
+
const onFilterOutsideClick = (e) => {
|
|
199
|
+
if (filterPanelRef.value && !filterPanelRef.value.contains(e.target)) showFilterPanel.value = false
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
watch(showColumnPanel, async (v) => {
|
|
203
|
+
if (v) {
|
|
204
|
+
await nextTick()
|
|
205
|
+
const rect = columnButtonRef.value?.getBoundingClientRect()
|
|
206
|
+
if (rect) columnPanelStyle.value = { top: rect.bottom + 6 + 'px', right: window.innerWidth - rect.right + 'px' }
|
|
207
|
+
document.addEventListener('mousedown', onColumnOutsideClick)
|
|
208
|
+
} else {
|
|
209
|
+
document.removeEventListener('mousedown', onColumnOutsideClick)
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
watch(showFilterPanel, (v) => {
|
|
213
|
+
if (v) document.addEventListener('mousedown', onFilterOutsideClick)
|
|
214
|
+
else document.removeEventListener('mousedown', onFilterOutsideClick)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// ─── Lifecycle ────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
const onEsc = (e) => { if (e.key === 'Escape' && previewRow.value) closePreview() }
|
|
220
|
+
onMounted(async () => {
|
|
221
|
+
previewEnabled.value = !!(slots.preview)
|
|
222
|
+
window.addEventListener('keydown', onEsc)
|
|
223
|
+
if (props.cached && previewEnabled.value) {
|
|
224
|
+
try {
|
|
225
|
+
const raw = sessionStorage.getItem(previewCacheKey.value)
|
|
226
|
+
if (raw) {
|
|
227
|
+
previewFromCache.value = true
|
|
228
|
+
previewRow.value = JSON.parse(raw)
|
|
229
|
+
await nextTick()
|
|
230
|
+
previewFromCache.value = false
|
|
231
|
+
}
|
|
232
|
+
} catch {}
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
onBeforeUnmount(() => {
|
|
236
|
+
window.removeEventListener('keydown', onEsc)
|
|
237
|
+
paginationObserver?.disconnect()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// ─── Expose ───────────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
const getSelectedRows = () => tableRef.value?.getSelectedRows()
|
|
243
|
+
const reload = () => tableRef.value?.reload()
|
|
244
|
+
const clearCache = () => tableRef.value?.clearCache()
|
|
245
|
+
const exportTable = (format, allPages, filteredRows) => tableRef.value?.exportTable(format, allPages, filteredRows)
|
|
246
|
+
defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, closePreview })
|
|
247
|
+
</script>
|
|
248
|
+
|
|
249
|
+
<template>
|
|
250
|
+
<div class="relative" ref="containerRef">
|
|
251
|
+
<div class="bg-card border border-card-line rounded-2xl shadow-sm overflow-hidden">
|
|
252
|
+
|
|
253
|
+
<!-- ── Toolbar ──────────────────────────────────────────────────────── -->
|
|
254
|
+
<div class="flex flex-wrap items-center gap-2 px-4 py-3 border-b border-card-line">
|
|
255
|
+
|
|
256
|
+
<!-- Search -->
|
|
257
|
+
<div v-if="showSearch" class="flex-1 min-w-48">
|
|
258
|
+
<Forms.Input v-model="search" type="search" :placeholder="searchPlaceholder" :icon-left="IconSearch" size="sm" />
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<!-- Advanced filters button -->
|
|
262
|
+
<button
|
|
263
|
+
v-if="showFilters && filtersConfig.length > 0"
|
|
264
|
+
type="button"
|
|
265
|
+
@click="showFilterPanel = !showFilterPanel"
|
|
266
|
+
:class="[
|
|
267
|
+
'py-1.5 px-3 inline-flex items-center gap-1.5 text-sm font-medium rounded-lg border transition-colors',
|
|
268
|
+
showFilterPanel || activeFilterCount > 0
|
|
269
|
+
? 'border-primary/50 bg-primary/5 text-primary dark:bg-primary/10'
|
|
270
|
+
: 'border-card-line bg-card text-muted-foreground-1 hover:bg-muted-hover',
|
|
271
|
+
]"
|
|
272
|
+
>
|
|
273
|
+
<IconAdjustmentsHorizontal class="size-4" stroke="1.5" />
|
|
274
|
+
Filtros
|
|
275
|
+
<span v-if="activeFilterCount > 0" class="text-xs font-bold">({{ activeFilterCount }})</span>
|
|
276
|
+
<IconChevronDown class="size-3.5" stroke="2" />
|
|
277
|
+
</button>
|
|
278
|
+
|
|
279
|
+
<!-- Toolbar slot (custom filter dropdowns, etc.) -->
|
|
280
|
+
<slot name="toolbar" />
|
|
281
|
+
|
|
282
|
+
<!-- Spacer -->
|
|
283
|
+
<div class="flex-1" />
|
|
284
|
+
|
|
285
|
+
<!-- Save view -->
|
|
286
|
+
<button
|
|
287
|
+
v-if="showSaveView"
|
|
288
|
+
type="button"
|
|
289
|
+
class="py-1.5 px-3 inline-flex items-center gap-1.5 text-sm font-medium rounded-lg border border-card-line bg-card text-muted-foreground-1 hover:bg-muted-hover transition-colors"
|
|
290
|
+
@click="emit('save-view')"
|
|
291
|
+
>
|
|
292
|
+
<IconBookmark class="size-4" stroke="1.5" />
|
|
293
|
+
Guardar vista
|
|
294
|
+
</button>
|
|
295
|
+
|
|
296
|
+
<!-- Columns -->
|
|
297
|
+
<button
|
|
298
|
+
v-if="showColumns"
|
|
299
|
+
ref="columnButtonRef"
|
|
300
|
+
type="button"
|
|
301
|
+
@click="showColumnPanel = !showColumnPanel"
|
|
302
|
+
:class="[
|
|
303
|
+
'py-1.5 px-3 inline-flex items-center gap-1.5 text-sm font-medium rounded-lg border transition-colors',
|
|
304
|
+
showColumnPanel
|
|
305
|
+
? 'border-primary/50 bg-primary/5 text-primary dark:bg-primary/10'
|
|
306
|
+
: 'border-card-line bg-card text-muted-foreground-1 hover:bg-muted-hover',
|
|
307
|
+
]"
|
|
308
|
+
>
|
|
309
|
+
<IconLayoutColumns class="size-4" stroke="1.5" />
|
|
310
|
+
Columnas
|
|
311
|
+
</button>
|
|
312
|
+
|
|
313
|
+
<!-- Export -->
|
|
314
|
+
<TableExportable v-if="showExport" :table-ref="tableRef" :name="resolvedName" :columns="columns" />
|
|
315
|
+
|
|
316
|
+
<!-- Primary action slot -->
|
|
317
|
+
<slot name="action" />
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<!-- ── Filter bar (chips) ───────────────────────────────────────────── -->
|
|
321
|
+
<div
|
|
322
|
+
v-if="filterChips.length > 0 || $slots['filter-bar']"
|
|
323
|
+
class="flex flex-wrap items-center gap-2 px-4 py-2.5 border-b border-card-line bg-surface/40"
|
|
324
|
+
>
|
|
325
|
+
<!-- Built-in chips from filterChips prop -->
|
|
326
|
+
<template v-if="filterChips.length > 0">
|
|
327
|
+
<div
|
|
328
|
+
v-for="chip in filterChips"
|
|
329
|
+
:key="chip.key"
|
|
330
|
+
class="relative"
|
|
331
|
+
>
|
|
332
|
+
<button
|
|
333
|
+
type="button"
|
|
334
|
+
@click="openChip = openChip === chip.key ? null : chip.key"
|
|
335
|
+
:class="[
|
|
336
|
+
'inline-flex items-center gap-1 px-2.5 py-1 rounded-lg text-sm border transition-colors',
|
|
337
|
+
chipValues[chip.key]
|
|
338
|
+
? 'border-primary/50 bg-primary/5 text-primary font-medium dark:bg-primary/10'
|
|
339
|
+
: 'border-card-line bg-card text-muted-foreground hover:bg-muted-hover',
|
|
340
|
+
]"
|
|
341
|
+
>
|
|
342
|
+
<span class="text-muted-foreground">{{ chip.label }}:</span>
|
|
343
|
+
<span class="font-medium">
|
|
344
|
+
{{ chip.options?.find(o => o.value === chipValues[chip.key])?.label ?? 'Todos' }}
|
|
345
|
+
</span>
|
|
346
|
+
<IconChevronDown class="size-3" stroke="2" />
|
|
347
|
+
</button>
|
|
348
|
+
|
|
349
|
+
<!-- Chip dropdown -->
|
|
350
|
+
<Transition
|
|
351
|
+
enter-active-class="transition ease-out duration-100"
|
|
352
|
+
enter-from-class="opacity-0 scale-95"
|
|
353
|
+
enter-to-class="opacity-100 scale-100"
|
|
354
|
+
leave-active-class="transition ease-in duration-75"
|
|
355
|
+
leave-from-class="opacity-100 scale-100"
|
|
356
|
+
leave-to-class="opacity-0 scale-95"
|
|
357
|
+
>
|
|
358
|
+
<div
|
|
359
|
+
v-if="openChip === chip.key"
|
|
360
|
+
v-click-outside="() => openChip = null"
|
|
361
|
+
class="absolute top-full left-0 z-50 mt-1 bg-dropdown border border-dropdown-line rounded-xl shadow-xl py-1 min-w-40"
|
|
362
|
+
>
|
|
363
|
+
<button
|
|
364
|
+
type="button"
|
|
365
|
+
class="w-full text-left px-3 py-1.5 text-sm hover:bg-muted-hover transition-colors"
|
|
366
|
+
:class="chipValues[chip.key] === '' ? 'font-semibold text-foreground' : 'text-muted-foreground'"
|
|
367
|
+
@click="chipValues[chip.key] = ''; openChip = null"
|
|
368
|
+
>Todos</button>
|
|
369
|
+
<button
|
|
370
|
+
v-for="opt in chip.options"
|
|
371
|
+
:key="opt.value"
|
|
372
|
+
type="button"
|
|
373
|
+
class="w-full text-left px-3 py-1.5 text-sm hover:bg-muted-hover transition-colors"
|
|
374
|
+
:class="chipValues[chip.key] === opt.value ? 'font-semibold text-foreground' : 'text-muted-foreground'"
|
|
375
|
+
@click="chipValues[chip.key] = opt.value; openChip = null"
|
|
376
|
+
>{{ opt.label }}</button>
|
|
377
|
+
</div>
|
|
378
|
+
</Transition>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<button
|
|
382
|
+
v-if="hasActiveChips"
|
|
383
|
+
type="button"
|
|
384
|
+
class="inline-flex items-center gap-1 px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
385
|
+
@click="clearChips"
|
|
386
|
+
>
|
|
387
|
+
<IconX class="size-3" stroke="2" />
|
|
388
|
+
Limpiar filtros
|
|
389
|
+
</button>
|
|
390
|
+
</template>
|
|
391
|
+
|
|
392
|
+
<!-- Custom filter bar slot -->
|
|
393
|
+
<slot name="filter-bar" />
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<!-- Advanced filter panel -->
|
|
397
|
+
<Transition
|
|
398
|
+
enter-active-class="transition ease-out duration-150"
|
|
399
|
+
enter-from-class="opacity-0 translate-y-1 scale-95"
|
|
400
|
+
enter-to-class="opacity-100 translate-y-0 scale-100"
|
|
401
|
+
leave-active-class="transition ease-in duration-100"
|
|
402
|
+
leave-from-class="opacity-100 translate-y-0 scale-100"
|
|
403
|
+
leave-to-class="opacity-0 translate-y-1 scale-95"
|
|
404
|
+
>
|
|
405
|
+
<div
|
|
406
|
+
v-if="showFilterPanel"
|
|
407
|
+
ref="filterPanelRef"
|
|
408
|
+
class="absolute top-14 left-4 z-50 bg-dropdown border border-dropdown-line rounded-xl shadow-2xl p-3 min-w-64 max-h-96 overflow-y-auto"
|
|
409
|
+
>
|
|
410
|
+
<p class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest mb-3 px-1">Filtros avanzados</p>
|
|
411
|
+
<TableFilter v-model="activeFilters" :columns="filtersConfig" />
|
|
412
|
+
</div>
|
|
413
|
+
</Transition>
|
|
414
|
+
|
|
415
|
+
<!-- ── Table + Preview ──────────────────────────────────────────────── -->
|
|
416
|
+
<div class="relative overflow-hidden">
|
|
417
|
+
|
|
418
|
+
<Table
|
|
419
|
+
ref="tableRef"
|
|
420
|
+
:endpoint="resolvedEndpoint"
|
|
421
|
+
:columns="columns"
|
|
422
|
+
:name="resolvedName"
|
|
423
|
+
:params="mergedParams"
|
|
424
|
+
:search="search"
|
|
425
|
+
:checkable="checkable"
|
|
426
|
+
:cached="cached"
|
|
427
|
+
:show-reload-button="showReloadButton"
|
|
428
|
+
:click-row-to-open="clickRowToOpen"
|
|
429
|
+
:preview-row-id="previewRow?.id ?? null"
|
|
430
|
+
:preview-mode="!!previewEnabled"
|
|
431
|
+
@row-click="handleRowClick"
|
|
432
|
+
@loaded="handleLoaded"
|
|
433
|
+
@page-change="closePreview"
|
|
434
|
+
@per-page-change="closePreview"
|
|
435
|
+
>
|
|
436
|
+
<template v-for="(_, sname) in forwardedSlots" #[sname]="slotProps">
|
|
437
|
+
<slot :name="sname" v-bind="slotProps ?? {}" />
|
|
438
|
+
</template>
|
|
439
|
+
</Table>
|
|
440
|
+
|
|
441
|
+
<!-- Preview overlay -->
|
|
442
|
+
<Transition
|
|
443
|
+
:enter-active-class="previewFromCache ? '' : 'transition ease-out duration-200'"
|
|
444
|
+
:enter-from-class="previewFromCache ? '' : 'opacity-0 translate-x-6'"
|
|
445
|
+
:enter-to-class="previewFromCache ? '' : 'opacity-100 translate-x-0'"
|
|
446
|
+
leave-active-class="transition ease-in duration-150"
|
|
447
|
+
leave-from-class="opacity-100 translate-x-0"
|
|
448
|
+
leave-to-class="opacity-0 translate-x-6"
|
|
449
|
+
>
|
|
450
|
+
<div
|
|
451
|
+
v-if="previewRow && previewEnabled"
|
|
452
|
+
class="absolute top-0 right-0 z-30 flex bg-card border-l border-card-line shadow-[-4px_0_16px_rgba(0,0,0,0.06)]"
|
|
453
|
+
:style="{ width: (100 - currentRatio) + '%', bottom: paginationHeight + 'px' }"
|
|
454
|
+
>
|
|
455
|
+
<!-- Resize handle -->
|
|
456
|
+
<div
|
|
457
|
+
class="w-1 shrink-0 cursor-col-resize bg-surface hover:bg-primary/40 transition-colors"
|
|
458
|
+
@mousedown="startResize"
|
|
459
|
+
/>
|
|
460
|
+
|
|
461
|
+
<div class="flex flex-col flex-1 overflow-hidden">
|
|
462
|
+
|
|
463
|
+
<!-- Fixed header -->
|
|
464
|
+
<div v-if="$slots['preview-header']" class="shrink-0 border-b border-card-line">
|
|
465
|
+
<slot name="preview-header" :row="previewRow" :close="closePreview" />
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<!-- Tabs (top) — only if previewTabs provided -->
|
|
469
|
+
<div
|
|
470
|
+
v-if="previewTabs && previewTabs.length > 1"
|
|
471
|
+
class="shrink-0 flex border-b border-card-line overflow-x-auto"
|
|
472
|
+
>
|
|
473
|
+
<button
|
|
474
|
+
v-for="tab in previewTabs"
|
|
475
|
+
:key="tab.key"
|
|
476
|
+
type="button"
|
|
477
|
+
@click="previewTab = tab.key"
|
|
478
|
+
:class="[
|
|
479
|
+
'px-4 py-2.5 text-xs font-semibold whitespace-nowrap transition-colors border-b-2 -mb-px',
|
|
480
|
+
previewTab === tab.key
|
|
481
|
+
? 'border-primary text-primary'
|
|
482
|
+
: 'border-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover',
|
|
483
|
+
]"
|
|
484
|
+
>{{ tab.label }}</button>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
<!-- Scrollable preview content -->
|
|
488
|
+
<div class="flex-1 overflow-y-auto min-h-0">
|
|
489
|
+
<slot name="preview" :row="previewRow" :tab="previewTab" :close="closePreview" />
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
</Transition>
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
|
|
498
|
+
<!-- Column panel (teleported) -->
|
|
499
|
+
<Teleport to="body">
|
|
500
|
+
<Transition
|
|
501
|
+
enter-active-class="transition ease-out duration-150"
|
|
502
|
+
enter-from-class="opacity-0 translate-y-1 scale-95"
|
|
503
|
+
enter-to-class="opacity-100 translate-y-0 scale-100"
|
|
504
|
+
leave-active-class="transition ease-in duration-100"
|
|
505
|
+
leave-from-class="opacity-100 translate-y-0 scale-100"
|
|
506
|
+
leave-to-class="opacity-0 translate-y-1 scale-95"
|
|
507
|
+
>
|
|
508
|
+
<div
|
|
509
|
+
v-if="showColumnPanel"
|
|
510
|
+
ref="columnPanelRef"
|
|
511
|
+
class="fixed z-50 bg-dropdown border border-dropdown-line rounded-xl shadow-2xl p-3 min-w-56 max-h-80 overflow-y-auto"
|
|
512
|
+
:style="columnPanelStyle"
|
|
513
|
+
>
|
|
514
|
+
<p class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest mb-2 px-1">Columnas visibles</p>
|
|
515
|
+
<div
|
|
516
|
+
v-for="col in orderedColumns"
|
|
517
|
+
:key="col.key"
|
|
518
|
+
draggable="true"
|
|
519
|
+
@dragstart="onDragStart(col.key)"
|
|
520
|
+
@dragover="(e) => onDragOver(e, col.key)"
|
|
521
|
+
@dragleave="onDragLeave"
|
|
522
|
+
@drop="onDrop(col.key)"
|
|
523
|
+
class="flex items-center gap-2 py-1.5 px-2 rounded-lg select-none transition-colors"
|
|
524
|
+
:class="dragOverKey === col.key ? 'bg-primary/10 ring-1 ring-primary/30' : 'hover:bg-muted-hover cursor-grab'"
|
|
525
|
+
>
|
|
526
|
+
<IconGripVertical class="size-4 text-muted-foreground-2 shrink-0" />
|
|
527
|
+
<input
|
|
528
|
+
type="checkbox"
|
|
529
|
+
:checked="tableRef?.table.getColumn(col.key)?.getIsVisible() ?? true"
|
|
530
|
+
@change="tableRef?.table.getColumn(col.key)?.toggleVisibility()"
|
|
531
|
+
@click.stop
|
|
532
|
+
class="rounded border-card-line bg-surface shrink-0 cursor-pointer"
|
|
533
|
+
/>
|
|
534
|
+
<span class="text-sm text-foreground truncate">{{ col.label }}</span>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
</Transition>
|
|
538
|
+
</Teleport>
|
|
539
|
+
</div>
|
|
540
|
+
</template>
|