@innertia-solutions/innertia-nuxt 0.1.10 → 0.1.12
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/Auth/LoginForm.vue +1 -1
- package/components/DataPreview.vue +323 -0
- package/components/DataTable.vue +12 -12
- package/components/Forms/DatePicker.vue +64 -39
- package/components/Forms/Input.vue +8 -2
- package/components/Forms/Select.vue +56 -20
- package/components/Forms/SelectServer.vue +39 -34
- package/components/MobileBlocker.vue +5 -5
- package/components/MobileLoginPicker.vue +13 -13
- package/components/Organization/Picker.vue +3 -3
- package/components/PermissionsTree.vue +4 -4
- package/components/Table/Database.vue +5 -5
- package/components/Table/DownloadDropdown.vue +8 -8
- package/components/Table/Enterprise.vue +9 -9
- package/components/Table/FilterDropdown.vue +3 -3
- package/components/Table/Grid.vue +4 -4
- package/components/Table/Kanban.vue +3 -3
- package/components/Table/List.vue +6 -6
- package/components/Table/Standard.vue +69 -324
- package/components/Table/index.vue +13 -13
- package/components/TableExportable.vue +6 -6
- package/components/TableFilter.vue +4 -4
- package/components/Tree/Node.vue +187 -84
- package/components/Tree/Standard.vue +345 -57
- package/composables/useAuth.js +1 -1
- package/composables/useContext.js +53 -5
- package/composables/useMobileGuard.ts +21 -21
- package/composables/useOrganization.ts +3 -3
- package/css/tokens.css +90 -0
- 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/package.json +1 -1
- package/theme.css +2 -0
- package/composables/useApp.ts +0 -46
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@innertia-solutions/innertia-nuxt` are documented here.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### BREAKING CHANGES
|
|
8
|
+
|
|
9
|
+
#### apps → contexts rename
|
|
10
|
+
|
|
11
|
+
Aligns frontend with innertia-laravel which already renamed the concept.
|
|
12
|
+
|
|
13
|
+
**Config (`nuxt.config.ts` in your product):**
|
|
14
|
+
```diff
|
|
15
|
+
- innertia.apps: { backoffice: { ... }, technician: { ... } }
|
|
16
|
+
+ innertia.contexts: { backoffice: { ... }, technician: { ... } }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Types:**
|
|
20
|
+
```diff
|
|
21
|
+
- import type { AppDefinition } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
22
|
+
+ import type { ContextDefinition } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
23
|
+
|
|
24
|
+
- import type { AppMobileMode } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
25
|
+
+ import type { ContextMobileMode } from '@innertia-solutions/innertia-nuxt/app.config'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Composables:**
|
|
29
|
+
```diff
|
|
30
|
+
- const { current, all, accessible, canAccess } = useApp()
|
|
31
|
+
+ const { current, all, accessible, canAccess } = useContext()
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`useApp()` has been removed. Use `useContext()` for all context/app logic. `useContext()` now also exposes: `contexts` (dict), `currentContext`, `availableContexts`, `switchContext()`, `confirmSwitch()`, `hasAccessToContext()`.
|
|
35
|
+
|
|
36
|
+
**Login body field:**
|
|
37
|
+
|
|
38
|
+
`performLogin()` now sends `context` instead of `app` in the POST body — matching what innertia-laravel now expects.
|
package/app.config.ts
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
//
|
|
3
3
|
// appConfig: {
|
|
4
4
|
// innertia: {
|
|
5
|
-
//
|
|
5
|
+
// contexts: { backoffice: {...}, technician: {...} },
|
|
6
6
|
// mobile: { breakpoint: 1024 },
|
|
7
7
|
// },
|
|
8
8
|
// }
|
|
9
9
|
//
|
|
10
|
-
// Si el producto no declara `
|
|
10
|
+
// Si el producto no declara `contexts`, el feature de contextos queda inactivo
|
|
11
11
|
// (middleware no redirige, picker no se muestra).
|
|
12
12
|
export default defineAppConfig({
|
|
13
13
|
innertia: {
|
|
@@ -121,11 +121,11 @@ export default defineAppConfig({
|
|
|
121
121
|
},
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
|
-
* Declaración de
|
|
125
|
-
* Cada
|
|
124
|
+
* Declaración de contextos del producto.
|
|
125
|
+
* Cada contexto define un prefijo de URL que mapea a un contexto del backend
|
|
126
126
|
* (matching con `availableContexts` que devuelve `auth/me`).
|
|
127
127
|
*/
|
|
128
|
-
|
|
128
|
+
contexts: {} as Record<string, ContextDefinition>,
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* Política mobile global. Se aplica si una `app` no tiene `mobile.mode` propio.
|
|
@@ -144,7 +144,7 @@ export default defineAppConfig({
|
|
|
144
144
|
|
|
145
145
|
export type InnertiaMode = 'saas' | 'app'
|
|
146
146
|
|
|
147
|
-
export type
|
|
147
|
+
export type ContextMobileMode = 'allow' | 'block' | 'redirect'
|
|
148
148
|
|
|
149
149
|
/** Color: nombre de paleta Tailwind o scale custom 50→950. */
|
|
150
150
|
export type ColorScale = Partial<Record<50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950, string>>
|
|
@@ -175,8 +175,8 @@ export interface MenuItem {
|
|
|
175
175
|
pattern?: string
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
export interface
|
|
179
|
-
/** Prefijo de URL — la ruta debe empezar con esto para considerarse "dentro" del
|
|
178
|
+
export interface ContextDefinition {
|
|
179
|
+
/** Prefijo de URL — la ruta debe empezar con esto para considerarse "dentro" del contexto. */
|
|
180
180
|
path: string
|
|
181
181
|
/** Clave del contexto que matchea con `availableContexts` del backend. */
|
|
182
182
|
context: string
|
|
@@ -186,13 +186,13 @@ export interface AppDefinition {
|
|
|
186
186
|
description?: string
|
|
187
187
|
/** Nombre del icono de @tabler/icons-vue (sin "Icon"). */
|
|
188
188
|
icon?: string
|
|
189
|
-
/** Ruta de login del
|
|
189
|
+
/** Ruta de login del contexto. */
|
|
190
190
|
loginPath: string
|
|
191
191
|
/** Ruta home (post-login). */
|
|
192
192
|
home: string
|
|
193
193
|
/** Política de uso en mobile. */
|
|
194
194
|
mobile: {
|
|
195
|
-
mode:
|
|
195
|
+
mode: ContextMobileMode
|
|
196
196
|
/** Solo si mode === 'redirect' — adónde mandar desde mobile. */
|
|
197
197
|
redirectTo?: string
|
|
198
198
|
}
|
|
@@ -99,7 +99,7 @@ async function handleOauth(provider: OAuthProvider) {
|
|
|
99
99
|
|
|
100
100
|
// ── Cross-links a otros contextos mobile-friendly (opcional) ───────────────
|
|
101
101
|
const { isMobile } = useMobileGuard()
|
|
102
|
-
const { all } =
|
|
102
|
+
const { all } = useContext()
|
|
103
103
|
const otherMobileApps = computed(() => {
|
|
104
104
|
if (!props.showCrossLinks) return []
|
|
105
105
|
return all.value.filter((a: any) => a.path !== `/${props.context}` && a.mobile?.mode === 'allow')
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* <DataPreview> — Panel de preview overlay compartido por Table.Standard y Tree.Standard.
|
|
4
|
+
*
|
|
5
|
+
* Encapsula:
|
|
6
|
+
* - Panel absoluto a la derecha del container padre (position:relative)
|
|
7
|
+
* - Resize por drag del handle vertical en el borde izquierdo
|
|
8
|
+
* - Header con título + acciones + botones (abrir, eliminar, minimizar, cerrar)
|
|
9
|
+
* - Tabs inferior cuando se pasa `historyEndpoint` (Datos / Bitácora)
|
|
10
|
+
* - Cache de la fila previewada en sessionStorage cuando `cached`
|
|
11
|
+
* - Chip flotante (Teleport to body) compartido vía `useDockedPreviews` Pinia store
|
|
12
|
+
*
|
|
13
|
+
* El parent es dueño de `row` (v-model) — DataPreview lo seteoa a null al cerrar.
|
|
14
|
+
* El parent también maneja el click fuera + ESC porque conoce su containerRef.
|
|
15
|
+
*
|
|
16
|
+
* Slots:
|
|
17
|
+
* - header ({ row, close }) — título del panel
|
|
18
|
+
* - actions ({ row, close }) — botones extra al lado de los built-in
|
|
19
|
+
* - default ({ row, close }) — body del preview
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
IconExternalLink, IconTrash, IconMinus, IconX, IconMaximize,
|
|
24
|
+
} from '@tabler/icons-vue'
|
|
25
|
+
|
|
26
|
+
const props = defineProps({
|
|
27
|
+
/** Activa el preview (típicamente derivado de !!slots.preview en el parent). */
|
|
28
|
+
enabled: { type: Boolean, default: false },
|
|
29
|
+
/** Nombre único para cache + dock id. */
|
|
30
|
+
name: { type: String, default: 'default' },
|
|
31
|
+
/** Si true, persiste la fila previewada en sessionStorage entre navegaciones. */
|
|
32
|
+
cached: { type: Boolean, default: false },
|
|
33
|
+
/** URL fija o (row) => url para botón "abrir externa". */
|
|
34
|
+
previewHref: { type: [String, Function], default: null },
|
|
35
|
+
/** Muestra botón eliminar (emite @delete). */
|
|
36
|
+
previewDeletable: { type: Boolean, default: false },
|
|
37
|
+
/** Porcentaje inicial de la tabla (0-100). */
|
|
38
|
+
splitRatio: { type: Number, default: 55 },
|
|
39
|
+
/** Endpoint del historial. Cuando se pasa, muestra tabs Datos/Bitácora. */
|
|
40
|
+
historyEndpoint: { type: String, default: null },
|
|
41
|
+
/** Container ref (HTMLElement) para medir el width al hacer resize. */
|
|
42
|
+
containerRef: { type: Object, default: null },
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const emit = defineEmits(['delete'])
|
|
46
|
+
|
|
47
|
+
// v-model:row → parent es dueño de la fila activa.
|
|
48
|
+
const row = defineModel('row', { default: null })
|
|
49
|
+
|
|
50
|
+
const previewFromCache = ref(false)
|
|
51
|
+
const currentRatio = ref(props.splitRatio)
|
|
52
|
+
const panelRef = ref(null)
|
|
53
|
+
const previewTab = ref('datos')
|
|
54
|
+
|
|
55
|
+
const cacheKey = computed(() => `data-preview-${props.name}`)
|
|
56
|
+
|
|
57
|
+
const resolvedPreviewHref = computed(() => {
|
|
58
|
+
if (!props.previewHref || !row.value) return null
|
|
59
|
+
return typeof props.previewHref === 'function'
|
|
60
|
+
? props.previewHref(row.value)
|
|
61
|
+
: props.previewHref
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const hasHistory = computed(() => !!props.historyEndpoint)
|
|
65
|
+
|
|
66
|
+
// Resetear tab al cambiar de fila + reinit tooltips de Preline
|
|
67
|
+
watch(row, async (r) => {
|
|
68
|
+
previewTab.value = 'datos'
|
|
69
|
+
if (r) {
|
|
70
|
+
await nextTick()
|
|
71
|
+
if (typeof window !== 'undefined') window.HSStaticMethods?.autoInit?.(['Tooltip'])
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Persistir fila en sessionStorage
|
|
76
|
+
watch(row, (r) => {
|
|
77
|
+
if (!props.cached) return
|
|
78
|
+
if (r) sessionStorage.setItem(cacheKey.value, JSON.stringify(r))
|
|
79
|
+
else sessionStorage.removeItem(cacheKey.value)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const close = () => { row.value = null }
|
|
83
|
+
|
|
84
|
+
const startResize = (e) => {
|
|
85
|
+
e.preventDefault()
|
|
86
|
+
const onMove = (ev) => {
|
|
87
|
+
const el = props.containerRef
|
|
88
|
+
if (!el) return
|
|
89
|
+
const rect = el.getBoundingClientRect()
|
|
90
|
+
const ratio = ((ev.clientX - rect.left) / rect.width) * 100
|
|
91
|
+
currentRatio.value = Math.min(80, Math.max(25, ratio))
|
|
92
|
+
}
|
|
93
|
+
const onUp = () => {
|
|
94
|
+
window.removeEventListener('mousemove', onMove)
|
|
95
|
+
window.removeEventListener('mouseup', onUp)
|
|
96
|
+
}
|
|
97
|
+
window.addEventListener('mousemove', onMove)
|
|
98
|
+
window.addEventListener('mouseup', onUp)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Dock (chip flotante) ─────────────────────────────────────────────────────
|
|
102
|
+
const {
|
|
103
|
+
docked,
|
|
104
|
+
dock, undock: undockItem,
|
|
105
|
+
activeDockId, activeDockRect,
|
|
106
|
+
collapseDock,
|
|
107
|
+
} = useDockedPreviews()
|
|
108
|
+
const route = useRoute()
|
|
109
|
+
|
|
110
|
+
const minimize = () => {
|
|
111
|
+
if (!row.value) return
|
|
112
|
+
const label = row.value.name ?? row.value.title ?? row.value.email ?? String(row.value.id)
|
|
113
|
+
const subtitle = row.value.email ?? row.value.description ?? null
|
|
114
|
+
dock({
|
|
115
|
+
id: `${props.name}-${row.value.id}`,
|
|
116
|
+
label,
|
|
117
|
+
subtitle,
|
|
118
|
+
row: { ...row.value },
|
|
119
|
+
tableName: props.name,
|
|
120
|
+
route: route.path,
|
|
121
|
+
})
|
|
122
|
+
close()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const floatingItem = computed(() =>
|
|
126
|
+
activeDockId.value
|
|
127
|
+
? docked.value.find(d => d.id === activeDockId.value && d.tableName === props.name) ?? null
|
|
128
|
+
: null
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
const floatingPanelStyle = computed(() => {
|
|
132
|
+
const rect = activeDockRect.value
|
|
133
|
+
const panelW = 384
|
|
134
|
+
const bottom = 52
|
|
135
|
+
if (!rect || typeof window === 'undefined') return { bottom: bottom + 'px', right: '16px' }
|
|
136
|
+
const tabCenter = rect.left + rect.width / 2
|
|
137
|
+
let right = window.innerWidth - tabCenter - panelW / 2
|
|
138
|
+
right = Math.max(8, Math.min(right, window.innerWidth - panelW - 8))
|
|
139
|
+
return { bottom: bottom + 'px', right: right + 'px' }
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const expandToFull = (item) => {
|
|
143
|
+
row.value = item.row
|
|
144
|
+
undockItem(item.id)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Restore from cache on mount
|
|
148
|
+
onMounted(async () => {
|
|
149
|
+
if (props.cached && props.enabled) {
|
|
150
|
+
try {
|
|
151
|
+
const raw = sessionStorage.getItem(cacheKey.value)
|
|
152
|
+
if (raw) {
|
|
153
|
+
previewFromCache.value = true
|
|
154
|
+
row.value = JSON.parse(raw)
|
|
155
|
+
await nextTick()
|
|
156
|
+
previewFromCache.value = false
|
|
157
|
+
}
|
|
158
|
+
} catch {}
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
defineExpose({ panelRef })
|
|
163
|
+
</script>
|
|
164
|
+
|
|
165
|
+
<template>
|
|
166
|
+
<!-- Panel overlay (absolute right, dentro del container del parent) -->
|
|
167
|
+
<Transition
|
|
168
|
+
:enter-active-class="previewFromCache ? '' : 'transition ease-out duration-200'"
|
|
169
|
+
:enter-from-class="previewFromCache ? '' : 'opacity-0 translate-x-6'"
|
|
170
|
+
:enter-to-class="previewFromCache ? '' : 'opacity-100 translate-x-0'"
|
|
171
|
+
leave-active-class="transition ease-in duration-150"
|
|
172
|
+
leave-from-class="opacity-100 translate-x-0"
|
|
173
|
+
leave-to-class="opacity-0 translate-x-6"
|
|
174
|
+
>
|
|
175
|
+
<div
|
|
176
|
+
v-if="row && enabled"
|
|
177
|
+
ref="panelRef"
|
|
178
|
+
class="absolute top-0 right-0 bottom-0 z-30 flex flex-col bg-card border border-card-line rounded-card shadow-xl overflow-hidden"
|
|
179
|
+
:style="{ width: (100 - currentRatio) + '%' }"
|
|
180
|
+
>
|
|
181
|
+
<!-- Resize handle -->
|
|
182
|
+
<div
|
|
183
|
+
class="absolute left-1 top-1/2 -translate-y-1/2 h-12 w-1 cursor-col-resize rounded-full bg-border hover:bg-primary/50 transition-colors z-10"
|
|
184
|
+
@mousedown="startResize"
|
|
185
|
+
/>
|
|
186
|
+
|
|
187
|
+
<!-- Header -->
|
|
188
|
+
<div class="shrink-0 flex items-center gap-3 px-5 py-4 border-b border-card-line">
|
|
189
|
+
<div class="flex-1 min-w-0">
|
|
190
|
+
<slot name="header" :row="row" :close="close" />
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div class="flex items-center gap-0.5 shrink-0">
|
|
194
|
+
<slot name="actions" :row="row" :close="close" />
|
|
195
|
+
|
|
196
|
+
<!-- Abrir externa -->
|
|
197
|
+
<div v-if="resolvedPreviewHref" class="hs-tooltip [--placement:top] inline-block">
|
|
198
|
+
<NuxtLink
|
|
199
|
+
:to="resolvedPreviewHref"
|
|
200
|
+
class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-control text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
201
|
+
>
|
|
202
|
+
<IconExternalLink class="size-3.5" />
|
|
203
|
+
</NuxtLink>
|
|
204
|
+
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-popover shadow-2xs" role="tooltip">Abrir</span>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<!-- Eliminar -->
|
|
208
|
+
<div v-if="previewDeletable" class="hs-tooltip [--placement:top] inline-block">
|
|
209
|
+
<button
|
|
210
|
+
type="button"
|
|
211
|
+
class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-control text-muted-foreground hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
|
212
|
+
@click.stop="emit('delete', row)"
|
|
213
|
+
>
|
|
214
|
+
<IconTrash class="size-3.5" />
|
|
215
|
+
</button>
|
|
216
|
+
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-popover shadow-2xs" role="tooltip">Eliminar</span>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- Minimizar -->
|
|
220
|
+
<div class="hs-tooltip [--placement:top] inline-block">
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-control text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
224
|
+
@click.stop="minimize"
|
|
225
|
+
>
|
|
226
|
+
<IconMinus class="size-3.5" />
|
|
227
|
+
</button>
|
|
228
|
+
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-popover shadow-2xs" role="tooltip">Minimizar</span>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<!-- Cerrar -->
|
|
232
|
+
<div class="hs-tooltip [--placement:top] inline-block">
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-control text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
|
|
236
|
+
@click.stop="close"
|
|
237
|
+
>
|
|
238
|
+
<IconX class="size-3.5" />
|
|
239
|
+
</button>
|
|
240
|
+
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-popover shadow-2xs" role="tooltip">Cerrar</span>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- Body -->
|
|
246
|
+
<div class="flex-1 overflow-y-auto min-h-0">
|
|
247
|
+
<slot v-if="previewTab === 'datos'" :row="row" :close="close" />
|
|
248
|
+
<TablePreviewTimeline
|
|
249
|
+
v-else-if="previewTab === 'bitacora' && historyEndpoint"
|
|
250
|
+
:endpoint="historyEndpoint"
|
|
251
|
+
/>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<!-- Tabs (cuando hay historyEndpoint) -->
|
|
255
|
+
<div v-if="hasHistory" class="shrink-0 flex border-t border-card-line">
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
@click="previewTab = 'datos'"
|
|
259
|
+
:class="[
|
|
260
|
+
'flex-1 py-2.5 text-xs font-semibold transition-colors border-r border-card-line border-t-2 -mt-px',
|
|
261
|
+
previewTab === 'datos'
|
|
262
|
+
? 'border-t-card text-foreground'
|
|
263
|
+
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
264
|
+
]"
|
|
265
|
+
>
|
|
266
|
+
Datos
|
|
267
|
+
</button>
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
@click="previewTab = 'bitacora'"
|
|
271
|
+
:class="[
|
|
272
|
+
'flex-1 py-2.5 text-xs font-semibold transition-colors border-t-2 -mt-px',
|
|
273
|
+
previewTab === 'bitacora'
|
|
274
|
+
? 'border-t-card text-foreground'
|
|
275
|
+
: 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
276
|
+
]"
|
|
277
|
+
>
|
|
278
|
+
Bitácora
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</Transition>
|
|
283
|
+
|
|
284
|
+
<!-- Floating chip (dock minimizado) — siempre teleportado a body -->
|
|
285
|
+
<Teleport to="body">
|
|
286
|
+
<Transition
|
|
287
|
+
enter-active-class="transition ease-out duration-200"
|
|
288
|
+
enter-from-class="opacity-0 translate-y-4"
|
|
289
|
+
enter-to-class="opacity-100 translate-y-0"
|
|
290
|
+
leave-active-class="transition ease-in duration-150"
|
|
291
|
+
leave-from-class="opacity-100 translate-y-0"
|
|
292
|
+
leave-to-class="opacity-0 translate-y-4"
|
|
293
|
+
>
|
|
294
|
+
<div
|
|
295
|
+
v-if="floatingItem"
|
|
296
|
+
class="fixed z-[60] w-96 flex flex-col bg-card border border-card-line rounded-t-card shadow-2xl overflow-hidden"
|
|
297
|
+
:style="{ ...floatingPanelStyle, maxHeight: 'min(480px, calc(100vh - 60px))' }"
|
|
298
|
+
>
|
|
299
|
+
<div class="flex items-center gap-2 px-3 py-2.5 border-b border-card-line shrink-0 bg-surface select-none">
|
|
300
|
+
<span class="size-6 rounded-avatar bg-primary flex items-center justify-center text-[10px] font-bold text-primary-foreground shrink-0">
|
|
301
|
+
{{ (floatingItem.label?.[0] ?? '?').toUpperCase() }}
|
|
302
|
+
</span>
|
|
303
|
+
<div class="flex-1 min-w-0">
|
|
304
|
+
<p class="text-sm font-semibold text-foreground truncate leading-tight">{{ floatingItem.label }}</p>
|
|
305
|
+
<p v-if="floatingItem.subtitle" class="text-xs text-muted-foreground truncate">{{ floatingItem.subtitle }}</p>
|
|
306
|
+
</div>
|
|
307
|
+
<button type="button" title="Expandir" class="inline-flex items-center justify-center size-6 rounded-control text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors" @click.stop="expandToFull(floatingItem)">
|
|
308
|
+
<IconMaximize class="size-3.5" />
|
|
309
|
+
</button>
|
|
310
|
+
<button type="button" title="Minimizar" class="inline-flex items-center justify-center size-6 rounded-control text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors" @click.stop="collapseDock()">
|
|
311
|
+
<IconMinus class="size-3.5" />
|
|
312
|
+
</button>
|
|
313
|
+
<button type="button" title="Cerrar" class="inline-flex items-center justify-center size-6 rounded-control text-muted-foreground hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors" @click.stop="undockItem(floatingItem.id)">
|
|
314
|
+
<IconX class="size-3.5" />
|
|
315
|
+
</button>
|
|
316
|
+
</div>
|
|
317
|
+
<div class="flex-1 overflow-y-auto min-h-0">
|
|
318
|
+
<slot :row="floatingItem.row" :close="() => undockItem(floatingItem.id)" />
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
</Transition>
|
|
322
|
+
</Teleport>
|
|
323
|
+
</template>
|
package/components/DataTable.vue
CHANGED
|
@@ -436,7 +436,7 @@ const handleRowKeydown = (row, event) => {
|
|
|
436
436
|
type="checkbox"
|
|
437
437
|
:checked="isAllVisibleSelected"
|
|
438
438
|
@change="toggleSelectAll"
|
|
439
|
-
class="mx-2 shrink-0 border-gray-300 rounded-
|
|
439
|
+
class="mx-2 shrink-0 border-gray-300 rounded-control text-blue-900 focus:ring-blue-900 dark:bg-card border-card-line"
|
|
440
440
|
/>
|
|
441
441
|
</th>
|
|
442
442
|
<th
|
|
@@ -538,7 +538,7 @@ const handleRowKeydown = (row, event) => {
|
|
|
538
538
|
<!-- Empty overlays -->
|
|
539
539
|
<div
|
|
540
540
|
v-if="!loading && data.length === 0 && !search"
|
|
541
|
-
class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-card/60 transition-all rounded-
|
|
541
|
+
class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-card/60 transition-all rounded-card"
|
|
542
542
|
>
|
|
543
543
|
<slot name="empty">
|
|
544
544
|
<p class="text-muted-foreground text-lg font-medium italic">No hay registros</p>
|
|
@@ -547,7 +547,7 @@ const handleRowKeydown = (row, event) => {
|
|
|
547
547
|
|
|
548
548
|
<div
|
|
549
549
|
v-if="!loading && data.length === 0 && search"
|
|
550
|
-
class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-card/60 transition-all rounded-
|
|
550
|
+
class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-card/60 transition-all rounded-card"
|
|
551
551
|
>
|
|
552
552
|
<slot name="empty-search">
|
|
553
553
|
<p class="text-muted-foreground text-lg font-medium italic">No hay registros en la búsqueda</p>
|
|
@@ -561,7 +561,7 @@ const handleRowKeydown = (row, event) => {
|
|
|
561
561
|
<div v-if="loading" :class="gridClass">
|
|
562
562
|
<div v-for="(_, index) in skeletonRows" :key="'grid-skeleton-' + index" class="animate-pulse">
|
|
563
563
|
<slot name="grid-skeleton">
|
|
564
|
-
<div class="bg-card rounded-
|
|
564
|
+
<div class="bg-card rounded-card border border-card-line p-4">
|
|
565
565
|
<div class="space-y-3">
|
|
566
566
|
<div class="h-4 bg-surface-1 rounded w-3/4"></div>
|
|
567
567
|
<div class="h-4 bg-surface-1 rounded w-1/2"></div>
|
|
@@ -583,7 +583,7 @@ const handleRowKeydown = (row, event) => {
|
|
|
583
583
|
:checkable="checkable"
|
|
584
584
|
:toggleRow="() => toggleRow(row)"
|
|
585
585
|
>
|
|
586
|
-
<div class="bg-card rounded-
|
|
586
|
+
<div class="bg-card rounded-card border border-card-line p-4 hover:shadow-md transition-shadow relative">
|
|
587
587
|
<div v-if="checkable" class="absolute top-2 left-2 z-10">
|
|
588
588
|
<input
|
|
589
589
|
type="checkbox"
|
|
@@ -636,11 +636,11 @@ const handleRowKeydown = (row, event) => {
|
|
|
636
636
|
<p class="text-sm text-foreground font-medium">{{ meta.total }} registros</p>
|
|
637
637
|
|
|
638
638
|
<div v-if="isDataFromCache && cached" class="group relative flex items-center">
|
|
639
|
-
<div class="flex items-center gap-x-1.5 py-1 px-2.5 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 rounded-
|
|
639
|
+
<div class="flex items-center gap-x-1.5 py-1 px-2.5 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 rounded-badge cursor-help transition-colors hover:bg-emerald-500/20">
|
|
640
640
|
<IconBolt class="size-3.5 fill-current" />
|
|
641
641
|
<span class="text-[10px] font-bold uppercase tracking-wider">Instant</span>
|
|
642
642
|
</div>
|
|
643
|
-
<div class="absolute bottom-full mb-2 left-0 hidden group-hover:block w-48 p-2.5 bg-slate-900 text-white text-[11px] leading-relaxed rounded-
|
|
643
|
+
<div class="absolute bottom-full mb-2 left-0 hidden group-hover:block w-48 p-2.5 bg-slate-900 text-white text-[11px] leading-relaxed rounded-popover shadow-2xl z-50 animate-in fade-in zoom-in duration-200">
|
|
644
644
|
<div class="font-bold mb-1 flex items-center gap-x-1.5 text-emerald-400">
|
|
645
645
|
<IconBolt class="size-3" />
|
|
646
646
|
Datos en Caché
|
|
@@ -659,7 +659,7 @@ const handleRowKeydown = (row, event) => {
|
|
|
659
659
|
v-if="!isCustomPerPage"
|
|
660
660
|
:value="perPage"
|
|
661
661
|
@change="(e) => handlePerPageChange(e.target.value)"
|
|
662
|
-
class="bg-surface border-none text-[11px] font-bold text-muted-foreground-1 rounded-
|
|
662
|
+
class="bg-surface border-none text-[11px] font-bold text-muted-foreground-1 rounded-control focus:ring-0 cursor-pointer py-1 pl-2 pr-8"
|
|
663
663
|
>
|
|
664
664
|
<option :value="10">10</option>
|
|
665
665
|
<option :value="25">25</option>
|
|
@@ -673,7 +673,7 @@ const handleRowKeydown = (row, event) => {
|
|
|
673
673
|
v-model.number="perPage"
|
|
674
674
|
min="1"
|
|
675
675
|
max="500"
|
|
676
|
-
class="w-14 bg-surface border-none text-[11px] font-bold text-muted-foreground-1 rounded-
|
|
676
|
+
class="w-14 bg-surface border-none text-[11px] font-bold text-muted-foreground-1 rounded-control focus:ring-2 focus:ring-indigo-500/20 py-1 px-2"
|
|
677
677
|
@blur="perPage = perPage || 10"
|
|
678
678
|
/>
|
|
679
679
|
<button @click="resetPerPage" class="text-[10px] text-indigo-500 font-bold hover:underline">Volver</button>
|
|
@@ -683,7 +683,7 @@ const handleRowKeydown = (row, event) => {
|
|
|
683
683
|
<nav class="flex justify-end items-center gap-x-1" aria-label="Pagination">
|
|
684
684
|
<button
|
|
685
685
|
type="button"
|
|
686
|
-
class="size-8 flex items-center justify-center rounded-
|
|
686
|
+
class="size-8 flex items-center justify-center rounded-control text-foreground hover:bg-muted-hover disabled:opacity-30"
|
|
687
687
|
:disabled="page <= 1"
|
|
688
688
|
@click="goToPreviousPage"
|
|
689
689
|
>
|
|
@@ -692,13 +692,13 @@ const handleRowKeydown = (row, event) => {
|
|
|
692
692
|
</svg>
|
|
693
693
|
</button>
|
|
694
694
|
<div class="flex items-center gap-x-1 mx-2">
|
|
695
|
-
<span class="size-8 flex items-center justify-center text-xs font-bold rounded-
|
|
695
|
+
<span class="size-8 flex items-center justify-center text-xs font-bold rounded-control bg-surface text-foreground">{{ meta.current_page }}</span>
|
|
696
696
|
<span class="text-[10px] font-bold text-muted-foreground uppercase mx-1">de</span>
|
|
697
697
|
<span class="text-[10px] font-bold text-muted-foreground">{{ meta.last_page }}</span>
|
|
698
698
|
</div>
|
|
699
699
|
<button
|
|
700
700
|
type="button"
|
|
701
|
-
class="size-8 flex items-center justify-center rounded-
|
|
701
|
+
class="size-8 flex items-center justify-center rounded-control text-foreground hover:bg-muted-hover disabled:opacity-30"
|
|
702
702
|
:disabled="page >= meta.last_page"
|
|
703
703
|
@click="goToNextPage"
|
|
704
704
|
>
|