@edc-motor/admin-kit 0.4.10 → 0.4.11
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/package.json +2 -2
- package/scss/components/_admin-layout.scss +82 -0
- package/scss/components/_entity-card.scss +15 -0
- package/src/crud/EntityCard.vue +7 -0
- package/src/index.ts +3 -0
- package/src/layout/AdminLayout.vue +17 -2
- package/src/layout/NavGroup.vue +75 -0
- package/src/layout/keys.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edc-motor/admin-kit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.11",
|
|
4
4
|
"description": "EdC Motor — kit del panel de administración: layout, gestores CRUD, imágenes, PDF, páginas, configuración, usuarios y copias (paquete fuente: lo compila el consumidor con Vite)",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@edc-motor/ui": "^0.4.
|
|
36
|
+
"@edc-motor/ui": "^0.4.11",
|
|
37
37
|
"axios": "^1.7.0",
|
|
38
38
|
"vue-draggable-plus": "^0.6.1"
|
|
39
39
|
},
|
|
@@ -145,6 +145,72 @@
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// Grupo plegable del menú (NavGroup): cabecera-botón con el mismo cuerpo que
|
|
149
|
+
// un nav-item (más el chevron) y panel de hijos con sangría. La animación va
|
|
150
|
+
// por grid-template-rows (0fr → 1fr): discreta y sin medir alturas en JS.
|
|
151
|
+
.nav-group {
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-direction: column;
|
|
154
|
+
|
|
155
|
+
&__toggle {
|
|
156
|
+
width: 100%;
|
|
157
|
+
background: none;
|
|
158
|
+
border: none;
|
|
159
|
+
font-family: inherit;
|
|
160
|
+
text-align: left;
|
|
161
|
+
cursor: pointer;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
&__chevron {
|
|
165
|
+
margin-left: auto;
|
|
166
|
+
color: $text-3;
|
|
167
|
+
transition: transform 0.2s ease;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
&--open > .nav-group__toggle .nav-group__chevron {
|
|
171
|
+
transform: rotate(90deg);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Activo (la ruta actual es de un hijo): la cabecera toma el acento; solo
|
|
175
|
+
// plegado toma también el fondo (desplegado ya se resalta el propio hijo).
|
|
176
|
+
&--active > .nav-group__toggle {
|
|
177
|
+
color: $accent-600;
|
|
178
|
+
}
|
|
179
|
+
&--active:not(.nav-group--open) > .nav-group__toggle {
|
|
180
|
+
background: $accent-100;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
&__panel {
|
|
184
|
+
display: grid;
|
|
185
|
+
grid-template-rows: 0fr;
|
|
186
|
+
transition: grid-template-rows 0.2s ease;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
&--open > .nav-group__panel {
|
|
190
|
+
grid-template-rows: 1fr;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
&__children {
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-direction: column;
|
|
196
|
+
gap: 2px;
|
|
197
|
+
min-height: 0;
|
|
198
|
+
overflow: hidden;
|
|
199
|
+
// Plegado, fuera del tab-order; al plegar espera a que acabe la animación.
|
|
200
|
+
visibility: hidden;
|
|
201
|
+
transition: visibility 0s 0.2s;
|
|
202
|
+
|
|
203
|
+
.nav-item {
|
|
204
|
+
padding-left: $space-6 + $space-2;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
&--open > .nav-group__panel > .nav-group__children {
|
|
209
|
+
visibility: visible;
|
|
210
|
+
transition: visibility 0s;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
148
214
|
.sidebar-user {
|
|
149
215
|
display: flex;
|
|
150
216
|
align-items: center;
|
|
@@ -266,6 +332,22 @@
|
|
|
266
332
|
.nav-label {
|
|
267
333
|
display: none;
|
|
268
334
|
}
|
|
335
|
+
|
|
336
|
+
// Carril de iconos: los grupos muestran siempre a sus hijos (lo fuerza
|
|
337
|
+
// NavGroup vía inject) y su cabecera queda inerte, sin chevron ni sangría.
|
|
338
|
+
.nav-group__chevron {
|
|
339
|
+
display: none;
|
|
340
|
+
}
|
|
341
|
+
.nav-group__toggle {
|
|
342
|
+
cursor: default;
|
|
343
|
+
|
|
344
|
+
&:hover {
|
|
345
|
+
background: transparent;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
.nav-group__children .nav-item {
|
|
349
|
+
padding-left: $space-3;
|
|
350
|
+
}
|
|
269
351
|
}
|
|
270
352
|
|
|
271
353
|
// ---- Modo móvil (< bp-md): el sidebar se vuelve drawer ----
|
|
@@ -20,6 +20,21 @@
|
|
|
20
20
|
&--muted { opacity: 0.65; }
|
|
21
21
|
&.is-active { border-color: $accent-500; }
|
|
22
22
|
|
|
23
|
+
// Borde teñido con el color de la entidad (prop accentColor, p. ej. su
|
|
24
|
+
// facción). El color viene de los datos del juego, así que en reposo nunca
|
|
25
|
+
// va puro: mezclado con el borde del tema queda sutil tanto en claro como
|
|
26
|
+
// en oscuro; gana presencia al hover y es pleno en la tarjeta seleccionada.
|
|
27
|
+
&--accented {
|
|
28
|
+
border-color: color-mix(in srgb, var(--entity-card-accent) 45%, $border);
|
|
29
|
+
|
|
30
|
+
&.entity-card--clickable:hover {
|
|
31
|
+
border-color: color-mix(in srgb, var(--entity-card-accent) 75%, $border-strong);
|
|
32
|
+
}
|
|
33
|
+
&.is-active {
|
|
34
|
+
border-color: var(--entity-card-accent);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
// Franja superior opcional (emblema/imagen). Cuadrada: con el grid de los
|
|
24
39
|
// index a 4 columnas la tarjeta es más estrecha y la imagen queda contenida
|
|
25
40
|
// (más bien pequeña) y centrada, nunca deformada ni recortada. Sin fondo:
|
package/src/crud/EntityCard.vue
CHANGED
|
@@ -18,6 +18,11 @@ withDefaults(
|
|
|
18
18
|
editable?: boolean
|
|
19
19
|
/** Texto accesible del botón de editar (agnóstico de i18n, DC-29). */
|
|
20
20
|
editLabel?: string
|
|
21
|
+
/**
|
|
22
|
+
* Color de acento de la entidad (p. ej. su facción, de los datos del
|
|
23
|
+
* juego): tiñe el borde de la tarjeta. Sin la prop, borde del tema.
|
|
24
|
+
*/
|
|
25
|
+
accentColor?: string
|
|
21
26
|
}>(),
|
|
22
27
|
{ editLabel: 'Editar' },
|
|
23
28
|
)
|
|
@@ -39,8 +44,10 @@ defineSlots<{
|
|
|
39
44
|
:class="{
|
|
40
45
|
'entity-card--clickable': clickable,
|
|
41
46
|
'entity-card--muted': muted,
|
|
47
|
+
'entity-card--accented': !!accentColor,
|
|
42
48
|
'is-active': active,
|
|
43
49
|
}"
|
|
50
|
+
:style="accentColor ? { '--entity-card-accent': accentColor } : undefined"
|
|
44
51
|
@click="clickable ? $emit('view') : undefined"
|
|
45
52
|
>
|
|
46
53
|
<div v-if="$slots.media" class="entity-card__media"><slot name="media" /></div>
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// @edc-motor/admin-kit — layout del panel + scaffolding CRUD (sobre @edc-motor/ui).
|
|
2
2
|
|
|
3
3
|
export { default as AdminLayout } from './layout/AdminLayout.vue'
|
|
4
|
+
// Grupo plegable para el slot #nav del AdminLayout: mezcla nav-item sueltos
|
|
5
|
+
// y grupos (icono + etiqueta + chevron) con plegado persistido en localStorage.
|
|
6
|
+
export { default as NavGroup } from './layout/NavGroup.vue'
|
|
4
7
|
// Panel derecho contextual (patrón kontuan): AdminLayout lo monta; cada vista
|
|
5
8
|
// lo activa con useRightSidebar() y Teleport a #right-sidebar-target.
|
|
6
9
|
export { default as RightSidebar } from './layout/RightSidebar.vue'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
|
2
|
+
import { ref, computed, onMounted, onUnmounted, provide, watch } from 'vue'
|
|
3
3
|
import { useRoute, type RouteLocationRaw } from 'vue-router'
|
|
4
4
|
import { ChevronLeft, ChevronRight, Menu, X } from '@lucide/vue'
|
|
5
5
|
import {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '@edc-motor/ui'
|
|
12
12
|
import RightSidebar from './RightSidebar.vue'
|
|
13
13
|
import { useRightSidebar } from '../composables/useRightSidebar'
|
|
14
|
+
import { SIDEBAR_RAIL } from './keys'
|
|
14
15
|
|
|
15
16
|
// Layout del panel — portado del AppLayout de kontuan (DC-28): sidebar
|
|
16
17
|
// colapsable en escritorio, drawer a pantalla completa en móvil, preferencias
|
|
@@ -84,6 +85,20 @@ function toggleSidebar() {
|
|
|
84
85
|
sidebarCollapsed.value = !sidebarCollapsed.value
|
|
85
86
|
localStorage.setItem('edc_admin_collapsed', sidebarCollapsed.value ? '1' : '0')
|
|
86
87
|
}
|
|
88
|
+
|
|
89
|
+
// Estado "carril de iconos" para los NavGroup del slot #nav (ver keys.ts).
|
|
90
|
+
provide(
|
|
91
|
+
SIDEBAR_RAIL,
|
|
92
|
+
computed(() => sidebarCollapsed.value && !isMobile.value),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
// En móvil, tocar un ENLACE del menú cierra el drawer; los toggles de grupo
|
|
96
|
+
// (NavGroup) no navegan y deben dejarlo abierto.
|
|
97
|
+
function onNavClick(event: MouseEvent) {
|
|
98
|
+
if (isMobile.value && (event.target as HTMLElement).closest('a')) {
|
|
99
|
+
sidebarMobileOpen.value = false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
87
102
|
</script>
|
|
88
103
|
|
|
89
104
|
<template>
|
|
@@ -131,7 +146,7 @@ function toggleSidebar() {
|
|
|
131
146
|
</div>
|
|
132
147
|
<hr v-if="!sidebarCollapsed || isMobile" class="sidebar-divider" />
|
|
133
148
|
|
|
134
|
-
<div class="sidebar-items" @click="
|
|
149
|
+
<div class="sidebar-items" @click="onNavClick">
|
|
135
150
|
<slot name="nav" />
|
|
136
151
|
</div>
|
|
137
152
|
</nav>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Grupo plegable del menú lateral: cabecera-botón (icono + etiqueta + chevron)
|
|
3
|
+
// que despliega/pliega sus hijos (los nav-item van en el slot por defecto,
|
|
4
|
+
// igual que sueltos en el #nav del AdminLayout). El estado se persiste en
|
|
5
|
+
// localStorage por clave de grupo (por defecto plegado) y, con la ruta actual
|
|
6
|
+
// en un hijo (prop `active`), el grupo se resalta y se auto-despliega.
|
|
7
|
+
import { computed, inject, ref, useId, watch } from 'vue'
|
|
8
|
+
import { ChevronRight } from '@lucide/vue'
|
|
9
|
+
import { SIDEBAR_RAIL } from './keys'
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
/** Etiqueta del grupo (ya traducida por la app). */
|
|
13
|
+
label: string
|
|
14
|
+
/** Clave del plegado en localStorage (`edc_admin_nav_<clave>`). */
|
|
15
|
+
storageKey: string
|
|
16
|
+
/**
|
|
17
|
+
* La ruta actual es de un hijo: resalta la cabecera y auto-despliega. La
|
|
18
|
+
* app lo calcula igual que el `active` de sus nav-item (p. ej. meta.nav).
|
|
19
|
+
*/
|
|
20
|
+
active?: boolean
|
|
21
|
+
}>()
|
|
22
|
+
|
|
23
|
+
defineSlots<{ icon?: () => unknown; default?: () => unknown }>()
|
|
24
|
+
|
|
25
|
+
const STORAGE_PREFIX = 'edc_admin_nav_'
|
|
26
|
+
|
|
27
|
+
// Por defecto plegado; si el usuario lo dejó abierto, se respeta.
|
|
28
|
+
const open = ref(localStorage.getItem(STORAGE_PREFIX + props.storageKey) === '1')
|
|
29
|
+
|
|
30
|
+
// Auto-despliegue al entrar en una ruta hija (sin persistir: solo los toggles
|
|
31
|
+
// del usuario guardan preferencia).
|
|
32
|
+
watch(
|
|
33
|
+
() => props.active,
|
|
34
|
+
(active) => {
|
|
35
|
+
if (active) open.value = true
|
|
36
|
+
},
|
|
37
|
+
{ immediate: true },
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// En el carril de iconos (sidebar colapsado en escritorio) los hijos se
|
|
41
|
+
// muestran siempre y el toggle no hace nada: plegar sin etiquetas ocultaría
|
|
42
|
+
// rutas. AdminLayout provee el estado; fuera de él, nunca es carril.
|
|
43
|
+
const rail = inject(
|
|
44
|
+
SIDEBAR_RAIL,
|
|
45
|
+
computed(() => false),
|
|
46
|
+
)
|
|
47
|
+
const expanded = computed(() => rail.value || open.value)
|
|
48
|
+
|
|
49
|
+
const panelId = useId()
|
|
50
|
+
|
|
51
|
+
function toggle() {
|
|
52
|
+
if (rail.value) return
|
|
53
|
+
open.value = !open.value
|
|
54
|
+
localStorage.setItem(STORAGE_PREFIX + props.storageKey, open.value ? '1' : '0')
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<div class="nav-group" :class="{ 'nav-group--open': expanded, 'nav-group--active': active }">
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
class="nav-item nav-group__toggle"
|
|
63
|
+
:aria-expanded="expanded"
|
|
64
|
+
:aria-controls="panelId"
|
|
65
|
+
@click="toggle"
|
|
66
|
+
>
|
|
67
|
+
<slot name="icon" />
|
|
68
|
+
<span class="nav-label">{{ label }}</span>
|
|
69
|
+
<ChevronRight class="nav-group__chevron" :size="16" aria-hidden="true" />
|
|
70
|
+
</button>
|
|
71
|
+
<div :id="panelId" class="nav-group__panel">
|
|
72
|
+
<div class="nav-group__children"><slot /></div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey } from 'vue'
|
|
2
|
+
|
|
3
|
+
// Estado "carril de iconos" del sidebar (colapsado en escritorio, sin
|
|
4
|
+
// etiquetas). Lo provee AdminLayout y lo consume NavGroup: en el carril un
|
|
5
|
+
// grupo plegado dejaría a sus hijos inalcanzables, así que se muestran
|
|
6
|
+
// siempre y el toggle queda inerte.
|
|
7
|
+
export const SIDEBAR_RAIL: InjectionKey<ComputedRef<boolean>> = Symbol('edc-admin-sidebar-rail')
|