@innertia-solutions/nuxt-theme-spark 0.1.138 → 0.1.139
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/components/Admin/Page.vue +5 -5
- package/components/App/Button.vue +4 -4
- package/components/App/PreviewDock.vue +64 -0
- package/components/Table/Standard.vue +135 -6
- package/nuxt.config.ts +4 -0
- package/package.json +3 -1
- package/plugins/dockedPreviewsSync.client.js +17 -0
- package/shared/composables/useDockedPreviews.js +56 -0
- package/shared/stores/dockedPreviews.js +34 -0
|
@@ -25,10 +25,13 @@ const iconColorClass = computed(() => ({
|
|
|
25
25
|
</script>
|
|
26
26
|
|
|
27
27
|
<template>
|
|
28
|
-
<div class="space-y-
|
|
28
|
+
<div class="relative space-y-2">
|
|
29
29
|
|
|
30
30
|
<!-- Page header card -->
|
|
31
31
|
<div v-if="title" class="sticky top-0 z-20 -mx-3 -mt-3 px-3 pt-3 bg-background-1">
|
|
32
|
+
<div v-if="$slots.breadcrumb" class="flex items-center gap-x-1 px-1 pt-2 pb-0.5">
|
|
33
|
+
<slot name="breadcrumb" />
|
|
34
|
+
</div>
|
|
32
35
|
<div class="flex items-center justify-between bg-card border border-card-line rounded-2xl shadow-sm px-4 py-3">
|
|
33
36
|
<div class="flex items-center gap-x-4 min-w-0">
|
|
34
37
|
<div v-if="iconComponent" class="shrink-0 size-10 rounded-xl flex items-center justify-center border border-current/15" :class="iconColorClass">
|
|
@@ -42,9 +45,6 @@ const iconColorClass = computed(() => ({
|
|
|
42
45
|
<p class="text-sm text-muted-foreground">{{ description }}</p>
|
|
43
46
|
</template>
|
|
44
47
|
</div>
|
|
45
|
-
<div v-if="$slots.breadcrumb" class="flex items-center gap-x-1 mt-0.5">
|
|
46
|
-
<slot name="breadcrumb" />
|
|
47
|
-
</div>
|
|
48
48
|
</div>
|
|
49
49
|
</div>
|
|
50
50
|
<div v-if="$slots.actions" class="flex items-center gap-x-2 shrink-0 ms-4">
|
|
@@ -59,7 +59,7 @@ const iconColorClass = computed(() => ({
|
|
|
59
59
|
</div>
|
|
60
60
|
|
|
61
61
|
<!-- Page content -->
|
|
62
|
-
<slot
|
|
62
|
+
<div class="relative"><slot /></div>
|
|
63
63
|
|
|
64
64
|
</div>
|
|
65
65
|
</template>
|
|
@@ -22,22 +22,22 @@ const iconSizeClasses = computed(() => ({ xs:"size-2", sm:"size-3", md:"size-4",
|
|
|
22
22
|
|
|
23
23
|
const severityClasses = computed(() => {
|
|
24
24
|
if (props.variant === "dropdown") {
|
|
25
|
-
const d = { primary:"text-
|
|
25
|
+
const d = { primary: "text-primary hover:bg-primary/10", secondary:"text-foreground hover:bg-muted-hover", success:"text-emerald-600 hover:bg-emerald-50 dark:text-emerald-400 dark:hover:bg-emerald-900/20", danger:"text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20", warning:"text-yellow-600 hover:bg-yellow-50 dark:text-yellow-400 dark:hover:bg-yellow-900/20", info:"text-cyan-600 hover:bg-cyan-50 dark:text-cyan-400 dark:hover:bg-cyan-900/20" }
|
|
26
26
|
return d[props.severity] || d.secondary
|
|
27
27
|
}
|
|
28
28
|
const base = "rounded-lg border transition-colors"
|
|
29
|
-
const v = { primary:"border-
|
|
29
|
+
const v = { primary: "border-primary bg-primary/10 text-primary hover:bg-primary/20 dark:bg-primary/15 dark:hover:bg-primary/25", secondary:"border-slate-300 bg-slate-50 text-slate-700 hover:bg-muted-hover dark:border-card-line dark:bg-card dark:text-muted-foreground-1 dark:hover:bg-muted-hover", success:"border-emerald-600 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-500 dark:bg-emerald-900/20 dark:text-emerald-300 dark:hover:bg-emerald-900/35", danger:"border-red-600 bg-red-50 text-red-700 hover:bg-red-100 dark:border-red-500 dark:bg-red-900/20 dark:text-red-300 dark:hover:bg-red-900/35", warning:"border-yellow-600 bg-yellow-50 text-yellow-700 hover:bg-yellow-100 dark:border-yellow-500 dark:bg-yellow-900/20 dark:text-yellow-300 dark:hover:bg-yellow-900/35", info:"border-cyan-600 bg-cyan-50 text-cyan-700 hover:bg-cyan-100 dark:border-cyan-500 dark:bg-cyan-900/20 dark:text-cyan-300 dark:hover:bg-cyan-900/35" }
|
|
30
30
|
return `${base} ${v[props.severity] || v.primary}`
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
const buttonClasses = computed(() => {
|
|
34
34
|
if (props.variant === "dropdown") {
|
|
35
35
|
const dis = props.type === "button" ? "disabled:opacity-50 disabled:pointer-events-none" : isDisabled.value ? "opacity-50 pointer-events-none" : ""
|
|
36
|
-
return `w-full flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm transition-colors text-left ${severityClasses.value} ${dis} focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-
|
|
36
|
+
return `w-full flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm transition-colors text-left ${severityClasses.value} ${dis} focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary/40 dark:focus:ring-offset-gray-800 ${props.class}`
|
|
37
37
|
}
|
|
38
38
|
const dis = props.type === "button" ? "disabled:opacity-50 disabled:pointer-events-none" : isDisabled.value ? "opacity-50 pointer-events-none" : ""
|
|
39
39
|
const cursor = props.type === "link" ? "cursor-pointer" : ""
|
|
40
|
-
return `${sizeClasses.value} ${severityClasses.value} ${dis} focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-
|
|
40
|
+
return `${sizeClasses.value} ${severityClasses.value} ${dis} focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-primary/40 dark:focus:ring-offset-gray-800 ${cursor} inline-flex justify-center items-center gap-x-2 whitespace-nowrap ${props.class}`
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
const displayText = computed(() => props.loading ? props.loadingText : props.text)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { IconX } from '@tabler/icons-vue'
|
|
3
|
+
|
|
4
|
+
const { docked, undock, expandDock, activeDockId } = useDockedPreviews()
|
|
5
|
+
const router = useRouter()
|
|
6
|
+
const route = useRoute()
|
|
7
|
+
|
|
8
|
+
async function open(item, event) {
|
|
9
|
+
// Misma ruta → la tabla está montada, mostrar float encima del tab
|
|
10
|
+
if (route.path === item.route) {
|
|
11
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
12
|
+
expandDock(item.id, rect)
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
// Ruta diferente → navegar y restaurar como preview completo
|
|
16
|
+
await router.push(item.route)
|
|
17
|
+
await nextTick()
|
|
18
|
+
useNuxtApp().hooks.callHook('preview:restore', item)
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<Transition
|
|
24
|
+
enter-active-class="transition ease-out duration-200"
|
|
25
|
+
enter-from-class="opacity-0 translate-y-4"
|
|
26
|
+
enter-to-class="opacity-100 translate-y-0"
|
|
27
|
+
leave-active-class="transition ease-in duration-150"
|
|
28
|
+
leave-from-class="opacity-100 translate-y-0"
|
|
29
|
+
leave-to-class="opacity-0 translate-y-4"
|
|
30
|
+
>
|
|
31
|
+
<div
|
|
32
|
+
v-if="docked.length"
|
|
33
|
+
class="fixed bottom-0 left-0 right-0 z-50 flex items-center gap-2 px-4 py-2 bg-card/95 backdrop-blur-md border-t border-card-line shadow-lg"
|
|
34
|
+
>
|
|
35
|
+
<span class="text-xs text-muted-foreground shrink-0 mr-1">Minimizados</span>
|
|
36
|
+
|
|
37
|
+
<div class="flex items-center gap-2 flex-1 overflow-x-auto">
|
|
38
|
+
<button
|
|
39
|
+
v-for="item in docked"
|
|
40
|
+
:key="item.id"
|
|
41
|
+
type="button"
|
|
42
|
+
class="group inline-flex items-center gap-2 rounded-lg border px-3 py-1.5 text-sm transition-all shrink-0"
|
|
43
|
+
:class="activeDockId === item.id
|
|
44
|
+
? 'border-primary/50 bg-primary/10 text-primary shadow-sm'
|
|
45
|
+
: 'border-card-line bg-surface hover:bg-muted-hover text-foreground'"
|
|
46
|
+
@click="open(item, $event)"
|
|
47
|
+
>
|
|
48
|
+
<span class="size-5 rounded-full bg-primary flex items-center justify-center text-[10px] font-bold text-primary-foreground shrink-0">
|
|
49
|
+
{{ (item.label?.[0] ?? '?').toUpperCase() }}
|
|
50
|
+
</span>
|
|
51
|
+
<span class="font-medium max-w-32 truncate">{{ item.label }}</span>
|
|
52
|
+
<span v-if="item.subtitle" class="text-muted-foreground text-xs max-w-28 truncate hidden sm:inline">{{ item.subtitle }}</span>
|
|
53
|
+
|
|
54
|
+
<span
|
|
55
|
+
class="size-4 inline-flex items-center justify-center rounded hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 transition-colors ml-0.5"
|
|
56
|
+
@click.stop="undock(item.id)"
|
|
57
|
+
>
|
|
58
|
+
<IconX class="size-3" />
|
|
59
|
+
</span>
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</Transition>
|
|
64
|
+
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { IconSearch, IconAdjustmentsHorizontal, IconLayoutColumns, IconGripVertical } from '@tabler/icons-vue'
|
|
2
|
+
import { IconSearch, IconAdjustmentsHorizontal, IconLayoutColumns, IconGripVertical, IconMinus, IconMaximize, IconX } from '@tabler/icons-vue'
|
|
3
3
|
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
table: { type: Object, default: null },
|
|
@@ -17,6 +17,7 @@ const props = defineProps({
|
|
|
17
17
|
showExport: { type: Boolean, default: true },
|
|
18
18
|
filters: { type: Array, default: () => [] },
|
|
19
19
|
splitRatio: { type: Number, default: 60 },
|
|
20
|
+
autoClosePreview: { type: Boolean, default: true },
|
|
20
21
|
})
|
|
21
22
|
|
|
22
23
|
const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
|
|
@@ -61,6 +62,7 @@ const paginationHeight = ref(0)
|
|
|
61
62
|
const previewCacheKey = computed(() => `table-preview-${resolvedName.value}`)
|
|
62
63
|
|
|
63
64
|
const previewFromCache = ref(false)
|
|
65
|
+
const previewPanelRef = ref(null)
|
|
64
66
|
const closePreview = () => { previewRow.value = null }
|
|
65
67
|
|
|
66
68
|
const previewTab = ref('datos')
|
|
@@ -76,6 +78,7 @@ watch(previewRow, () => { previewTab.value = 'datos' })
|
|
|
76
78
|
|
|
77
79
|
const handleRowClick = (row) => {
|
|
78
80
|
if (previewEnabled.value) {
|
|
81
|
+
collapseDock()
|
|
79
82
|
previewRow.value = previewRow.value?.id === row.id ? null : row
|
|
80
83
|
} else {
|
|
81
84
|
emit('row-click', row)
|
|
@@ -129,10 +132,74 @@ const startResize = (e) => {
|
|
|
129
132
|
window.addEventListener('mouseup', onUp)
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
const onEsc = (e) => { if (e.key === 'Escape'
|
|
135
|
+
const onEsc = (e) => { if (e.key === 'Escape') { if (previewRow.value) closePreview(); else collapseDock() } }
|
|
136
|
+
|
|
137
|
+
// ─── Auto-close preview on outside click ──────────────────────────────────────
|
|
138
|
+
const onDocMousedown = (e) => {
|
|
139
|
+
if (props.autoClosePreview && previewRow.value && previewPanelRef.value && !previewPanelRef.value.contains(e.target)) {
|
|
140
|
+
closePreview()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── Dock (minimizar preview) ──────────────────────────────────────────────────
|
|
145
|
+
const {
|
|
146
|
+
docked,
|
|
147
|
+
dock, undock: undockItem, isActive,
|
|
148
|
+
activeDockId, activeDockRect,
|
|
149
|
+
expandDock, collapseDock,
|
|
150
|
+
} = useDockedPreviews()
|
|
151
|
+
const route = useRoute()
|
|
152
|
+
|
|
153
|
+
function minimizePreview() {
|
|
154
|
+
if (!previewRow.value) return
|
|
155
|
+
const label = previewRow.value.name ?? previewRow.value.title ?? previewRow.value.email ?? String(previewRow.value.id)
|
|
156
|
+
const subtitle = previewRow.value.email ?? previewRow.value.description ?? null
|
|
157
|
+
dock({
|
|
158
|
+
id: `${resolvedName.value}-${previewRow.value.id}`,
|
|
159
|
+
label,
|
|
160
|
+
subtitle,
|
|
161
|
+
row: { ...previewRow.value },
|
|
162
|
+
tableName: resolvedName.value,
|
|
163
|
+
route: route.path,
|
|
164
|
+
})
|
|
165
|
+
closePreview()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Item que debe mostrarse como mini-preview flotante (pertenece a esta tabla)
|
|
169
|
+
const floatingItem = computed(() =>
|
|
170
|
+
activeDockId.value
|
|
171
|
+
? docked.value.find(d => d.id === activeDockId.value && d.tableName === resolvedName.value) ?? null
|
|
172
|
+
: null
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
// Posición del panel flotante: centrado sobre el tab que lo abrió
|
|
176
|
+
const floatingPanelStyle = computed(() => {
|
|
177
|
+
const rect = activeDockRect.value
|
|
178
|
+
const panelW = 384
|
|
179
|
+
const bottom = 52
|
|
180
|
+
if (!rect || typeof window === 'undefined') return { bottom: bottom + 'px', right: '16px' }
|
|
181
|
+
const tabCenter = rect.left + rect.width / 2
|
|
182
|
+
let right = window.innerWidth - tabCenter - panelW / 2
|
|
183
|
+
right = Math.max(8, Math.min(right, window.innerWidth - panelW - 8))
|
|
184
|
+
return { bottom: bottom + 'px', right: right + 'px' }
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
function expandToFull(item) {
|
|
188
|
+
previewRow.value = item.row
|
|
189
|
+
undockItem(item.id)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Escuchar evento de restauración (fallback cuando la tabla no estaba montada)
|
|
193
|
+
onMounted(() => {
|
|
194
|
+
useNuxtApp().hooks.hook('preview:restore', (item) => {
|
|
195
|
+
if (item.tableName === resolvedName.value) previewRow.value = item.row
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
133
199
|
onMounted(async () => {
|
|
134
200
|
previewEnabled.value = !!slots.preview
|
|
135
201
|
window.addEventListener('keydown', onEsc)
|
|
202
|
+
document.addEventListener('mousedown', onDocMousedown)
|
|
136
203
|
// Restore preview from session cache — mark as from-cache to skip enter animation
|
|
137
204
|
if (props.cached && previewEnabled.value) {
|
|
138
205
|
try {
|
|
@@ -148,6 +215,7 @@ onMounted(async () => {
|
|
|
148
215
|
})
|
|
149
216
|
onBeforeUnmount(() => {
|
|
150
217
|
window.removeEventListener('keydown', onEsc)
|
|
218
|
+
document.removeEventListener('mousedown', onDocMousedown)
|
|
151
219
|
paginationObserver?.disconnect()
|
|
152
220
|
})
|
|
153
221
|
|
|
@@ -224,7 +292,7 @@ const reload = () => tableRef.value?.reload()
|
|
|
224
292
|
const clearCache = () => tableRef.value?.clearCache()
|
|
225
293
|
const exportTable = (format, allPages, filteredRows) => tableRef.value?.exportTable(format, allPages, filteredRows)
|
|
226
294
|
|
|
227
|
-
defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef })
|
|
295
|
+
defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, closePreview })
|
|
228
296
|
</script>
|
|
229
297
|
|
|
230
298
|
<template>
|
|
@@ -332,6 +400,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef })
|
|
|
332
400
|
>
|
|
333
401
|
<div
|
|
334
402
|
v-if="previewRow && previewEnabled"
|
|
403
|
+
ref="previewPanelRef"
|
|
335
404
|
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)]"
|
|
336
405
|
:style="{ width: (100 - currentRatio) + '%', bottom: paginationHeight + 'px' }"
|
|
337
406
|
>
|
|
@@ -343,9 +412,29 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef })
|
|
|
343
412
|
<!-- Preview -->
|
|
344
413
|
<div class="flex flex-col flex-1 overflow-hidden">
|
|
345
414
|
|
|
346
|
-
<!--
|
|
347
|
-
<div
|
|
348
|
-
<
|
|
415
|
+
<!-- Barra de acciones del preview -->
|
|
416
|
+
<div class="shrink-0 flex items-center justify-between gap-2 px-3 py-2 border-b border-card-line">
|
|
417
|
+
<div class="flex-1 min-w-0">
|
|
418
|
+
<slot name="preview-header" :row="previewRow" :close="closePreview" />
|
|
419
|
+
</div>
|
|
420
|
+
<div class="flex items-center gap-1 shrink-0">
|
|
421
|
+
<button
|
|
422
|
+
type="button"
|
|
423
|
+
class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
424
|
+
title="Minimizar"
|
|
425
|
+
@click.stop="minimizePreview"
|
|
426
|
+
>
|
|
427
|
+
<IconMinus class="size-3.5" />
|
|
428
|
+
</button>
|
|
429
|
+
<button
|
|
430
|
+
type="button"
|
|
431
|
+
class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
432
|
+
title="Cerrar"
|
|
433
|
+
@click.stop="closePreview"
|
|
434
|
+
>
|
|
435
|
+
<IconX class="size-3.5" />
|
|
436
|
+
</button>
|
|
437
|
+
</div>
|
|
349
438
|
</div>
|
|
350
439
|
|
|
351
440
|
<!-- Scrollable content -->
|
|
@@ -395,6 +484,46 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef })
|
|
|
395
484
|
</div>
|
|
396
485
|
</div>
|
|
397
486
|
|
|
487
|
+
<!-- ── Floating mini-preview (dock expand, estilo Gmail) ── -->
|
|
488
|
+
<Teleport to="body">
|
|
489
|
+
<Transition
|
|
490
|
+
enter-active-class="transition ease-out duration-200"
|
|
491
|
+
enter-from-class="opacity-0 translate-y-4"
|
|
492
|
+
enter-to-class="opacity-100 translate-y-0"
|
|
493
|
+
leave-active-class="transition ease-in duration-150"
|
|
494
|
+
leave-from-class="opacity-100 translate-y-0"
|
|
495
|
+
leave-to-class="opacity-0 translate-y-4"
|
|
496
|
+
>
|
|
497
|
+
<div
|
|
498
|
+
v-if="floatingItem"
|
|
499
|
+
class="fixed z-[60] w-96 flex flex-col bg-card border border-card-line rounded-t-xl shadow-2xl overflow-hidden"
|
|
500
|
+
:style="{ ...floatingPanelStyle, maxHeight: 'min(480px, calc(100vh - 60px))' }"
|
|
501
|
+
>
|
|
502
|
+
<div class="flex items-center gap-2 px-3 py-2.5 border-b border-card-line shrink-0 bg-surface select-none">
|
|
503
|
+
<span class="size-6 rounded-full bg-primary flex items-center justify-center text-[10px] font-bold text-primary-foreground shrink-0">
|
|
504
|
+
{{ (floatingItem.label?.[0] ?? '?').toUpperCase() }}
|
|
505
|
+
</span>
|
|
506
|
+
<div class="flex-1 min-w-0">
|
|
507
|
+
<p class="text-sm font-semibold text-foreground truncate leading-tight">{{ floatingItem.label }}</p>
|
|
508
|
+
<p v-if="floatingItem.subtitle" class="text-xs text-muted-foreground truncate">{{ floatingItem.subtitle }}</p>
|
|
509
|
+
</div>
|
|
510
|
+
<button type="button" title="Expandir" class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors" @click.stop="expandToFull(floatingItem)">
|
|
511
|
+
<IconMaximize class="size-3.5" />
|
|
512
|
+
</button>
|
|
513
|
+
<button type="button" title="Minimizar" class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors" @click.stop="collapseDock()">
|
|
514
|
+
<IconMinus class="size-3.5" />
|
|
515
|
+
</button>
|
|
516
|
+
<button type="button" title="Cerrar" class="inline-flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors" @click.stop="undockItem(floatingItem.id)">
|
|
517
|
+
<IconX class="size-3.5" />
|
|
518
|
+
</button>
|
|
519
|
+
</div>
|
|
520
|
+
<div class="flex-1 overflow-y-auto min-h-0">
|
|
521
|
+
<slot name="preview" :row="floatingItem.row" :close="() => undockItem(floatingItem.id)" />
|
|
522
|
+
</div>
|
|
523
|
+
</div>
|
|
524
|
+
</Transition>
|
|
525
|
+
</Teleport>
|
|
526
|
+
|
|
398
527
|
<!-- Column panel — teleported to body to escape overflow-hidden -->
|
|
399
528
|
<Teleport to="body">
|
|
400
529
|
<Transition
|
package/nuxt.config.ts
CHANGED
|
@@ -2,6 +2,10 @@ import tailwindcss from '@tailwindcss/vite'
|
|
|
2
2
|
|
|
3
3
|
export default defineNuxtConfig({
|
|
4
4
|
extends: ['@innertia-solutions/nuxt-core'],
|
|
5
|
+
modules: [
|
|
6
|
+
'@pinia/nuxt',
|
|
7
|
+
'pinia-plugin-persistedstate/nuxt', // required for dockedPreviews store persistence
|
|
8
|
+
],
|
|
5
9
|
css: ['@innertia-solutions/nuxt-theme-spark/spark.css'],
|
|
6
10
|
components: [
|
|
7
11
|
{ path: './components', pathPrefix: true, prefix: '' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@innertia-solutions/nuxt-theme-spark",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.139",
|
|
4
4
|
"description": "Innertia Solutions — Spark theme: backoffice, landing and mobile components and layouts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nuxt",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@innertia-solutions/nuxt-core": "^0.1.4",
|
|
32
|
+
"@pinia/nuxt": "^0.11.3",
|
|
33
|
+
"pinia-plugin-persistedstate": "^4.7.1",
|
|
32
34
|
"@tabler/icons-vue": "^3.44.0",
|
|
33
35
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
|
34
36
|
"@tailwindcss/forms": "^0.5.10",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sincroniza el store de previews minimizados entre pestañas del navegador.
|
|
3
|
+
* Escucha los eventos `storage` que dispara localStorage cuando otra pestaña escribe.
|
|
4
|
+
*/
|
|
5
|
+
export default defineNuxtPlugin(() => {
|
|
6
|
+
const store = useDockedPreviewsStore()
|
|
7
|
+
|
|
8
|
+
window.addEventListener('storage', (event) => {
|
|
9
|
+
if (event.key !== 'docked-previews' || !event.newValue) return
|
|
10
|
+
try {
|
|
11
|
+
const persisted = JSON.parse(event.newValue)
|
|
12
|
+
store.hydrate(persisted.items ?? [])
|
|
13
|
+
} catch {
|
|
14
|
+
// JSON inválido — ignorar
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable para manejar los previews minimizados.
|
|
3
|
+
*
|
|
4
|
+
* - `docked` / `dock` / `undock` / `isActive` → Pinia store (persiste en localStorage, sync entre tabs)
|
|
5
|
+
* - `activeDockId` / `activeDockRect` / expand / collapse → estado de UI efímero (no persistido)
|
|
6
|
+
*/
|
|
7
|
+
export function useDockedPreviews() {
|
|
8
|
+
const store = useDockedPreviewsStore()
|
|
9
|
+
|
|
10
|
+
// ─── UI state (no persiste) ───────────────────────────────────────────────────
|
|
11
|
+
const activeDockId = useState('docked-active-id', () => null)
|
|
12
|
+
const activeDockRect = useState('docked-active-rect', () => null)
|
|
13
|
+
|
|
14
|
+
// ─── Acceso reactivo a los items persistidos ──────────────────────────────────
|
|
15
|
+
const docked = computed(() => store.items)
|
|
16
|
+
|
|
17
|
+
// ─── Dock / undock ────────────────────────────────────────────────────────────
|
|
18
|
+
const dock = (payload) => store.add(payload)
|
|
19
|
+
|
|
20
|
+
const undock = (id) => {
|
|
21
|
+
store.remove(id)
|
|
22
|
+
if (activeDockId.value === id) {
|
|
23
|
+
activeDockId.value = null
|
|
24
|
+
activeDockRect.value = null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const isActive = (id) => !!store.items.find(d => d.id === id)
|
|
29
|
+
|
|
30
|
+
// ─── Panel flotante ───────────────────────────────────────────────────────────
|
|
31
|
+
const expandDock = (id, rect = null) => {
|
|
32
|
+
if (activeDockId.value === id) {
|
|
33
|
+
activeDockId.value = null
|
|
34
|
+
activeDockRect.value = null
|
|
35
|
+
} else {
|
|
36
|
+
activeDockId.value = id
|
|
37
|
+
activeDockRect.value = rect ? { left: rect.left, width: rect.width } : null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const collapseDock = () => {
|
|
42
|
+
activeDockId.value = null
|
|
43
|
+
activeDockRect.value = null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
docked,
|
|
48
|
+
activeDockId,
|
|
49
|
+
activeDockRect,
|
|
50
|
+
dock,
|
|
51
|
+
undock,
|
|
52
|
+
isActive,
|
|
53
|
+
expandDock,
|
|
54
|
+
collapseDock,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
|
|
3
|
+
// Storage SSR-safe: null en servidor, localStorage en cliente
|
|
4
|
+
const clientStorage = typeof window !== 'undefined' ? window.localStorage : null
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Store persistido en localStorage para los previews minimizados.
|
|
8
|
+
* Sobrevive recargas y se sincroniza entre pestañas (via plugin dockedPreviewsSync).
|
|
9
|
+
*/
|
|
10
|
+
export const useDockedPreviewsStore = defineStore('docked-previews', {
|
|
11
|
+
state: () => ({
|
|
12
|
+
items: [],
|
|
13
|
+
}),
|
|
14
|
+
|
|
15
|
+
actions: {
|
|
16
|
+
add({ id, label, subtitle, row, tableName, route }) {
|
|
17
|
+
if (this.items.find(d => d.id === id)) return
|
|
18
|
+
this.items.push({ id, label, subtitle: subtitle ?? null, row, tableName, route })
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
remove(id) {
|
|
22
|
+
this.items = this.items.filter(d => d.id !== id)
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
/** Sincroniza el estado desde otra pestaña (llamado por el plugin de storage). */
|
|
26
|
+
hydrate(items) {
|
|
27
|
+
this.items = items
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
persist: {
|
|
32
|
+
storage: clientStorage,
|
|
33
|
+
},
|
|
34
|
+
})
|