@edc-motor/admin-kit 0.4.10 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edc-motor/admin-kit",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
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.10",
36
+ "@edc-motor/ui": "^0.4.12",
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:
@@ -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="isMobile && (sidebarMobileOpen = false)">
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')