@edc-motor/ui 0.4.12 → 0.4.14
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/ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.14",
|
|
4
4
|
"description": "EdC Motor — componentes públicos Vue 3 + tokens SCSS para webs de juegos de mesa (paquete fuente: lo compila el consumidor con Vite)",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -2,28 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
// Barra lateral derecha contextual de la web pública (AppRightSidebar.vue):
|
|
4
4
|
// cada vista la registra con useAppRightSidebar() y App.vue la monta una vez
|
|
5
|
-
// dentro de .site-main
|
|
6
|
-
// --app-right-sidebar-top
|
|
7
|
-
//
|
|
5
|
+
// dentro de .site-main. SIEMPRE fija a la derecha, POR DEBAJO de la cabecera
|
|
6
|
+
// fija del cascarón: arranca en --app-right-sidebar-top (el cascarón la fija
|
|
7
|
+
// a la altura real de su cabecera por breakpoint; sin fijarla, el borde
|
|
8
|
+
// superior) y llega hasta abajo. Así ni scrollea con la página ni "se acaba"
|
|
9
|
+
// en el footer, y la cabecera queda siempre a la vista y usable. Cerrada
|
|
10
|
+
// queda fuera de pantalla (translateX) y solo asoma el asa.
|
|
11
|
+
//
|
|
12
|
+
// El cascarón decide el hueco: con `--docked` (desplegada en ancho) da
|
|
13
|
+
// padding-right a .site-content/.app-footer vía body:has() — la cabecera NO
|
|
14
|
+
// lo necesita: la barra ya no la tapa. En estrecho el componente añade telón
|
|
15
|
+
// (también bajo la cabecera) y la barra se superpone (drawer).
|
|
16
|
+
|
|
17
|
+
// Ancho compartido con el cascarón (el hueco del contenido lo usa).
|
|
18
|
+
:root {
|
|
19
|
+
--app-right-sidebar-width: 320px;
|
|
20
|
+
}
|
|
8
21
|
|
|
9
22
|
.app-right-sidebar {
|
|
10
|
-
position:
|
|
11
|
-
top:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
position: fixed;
|
|
24
|
+
top: var(--app-right-sidebar-top, 0px);
|
|
25
|
+
right: 0;
|
|
26
|
+
bottom: 0;
|
|
27
|
+
z-index: 40; // por debajo de la cabecera fija del cascarón (50)
|
|
28
|
+
// En viewports mínimos deja sitio al asa (el telón cubre el resto).
|
|
29
|
+
width: min(var(--app-right-sidebar-width), 100vw - 36px);
|
|
30
|
+
transform: translateX(100%);
|
|
31
|
+
transition: transform 0.25s ease;
|
|
32
|
+
|
|
33
|
+
&--open {
|
|
34
|
+
transform: translateX(0);
|
|
35
|
+
}
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
// El panel de verdad (el aside deja overflow visible para el asa).
|
|
38
|
+
&__panel {
|
|
39
|
+
height: 100%;
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
background: $surface;
|
|
43
|
+
border-left: 1px solid $border;
|
|
44
|
+
overflow: hidden;
|
|
27
45
|
}
|
|
28
46
|
|
|
29
47
|
&__header {
|
|
@@ -31,7 +49,7 @@
|
|
|
31
49
|
align-items: center;
|
|
32
50
|
gap: $space-2;
|
|
33
51
|
height: 48px;
|
|
34
|
-
padding: 0 $space-
|
|
52
|
+
padding: 0 $space-4;
|
|
35
53
|
border-bottom: 1px solid $border;
|
|
36
54
|
flex-shrink: 0;
|
|
37
55
|
}
|
|
@@ -46,67 +64,63 @@
|
|
|
46
64
|
white-space: nowrap;
|
|
47
65
|
}
|
|
48
66
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
&__body {
|
|
68
|
+
flex: 1;
|
|
69
|
+
overflow-y: auto;
|
|
70
|
+
padding: $space-4;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Asa de abrir/cerrar, anclada al costado izquierdo de la barra (como la
|
|
74
|
+
// de la RightSidebar del admin): al viajar CON la barra nunca se desancla
|
|
75
|
+
// de ella. Con la barra cerrada asoma pegada al borde del viewport. El top
|
|
76
|
+
// es RELATIVO al techo de la barra (que ya arranca bajo la cabecera): por
|
|
77
|
+
// defecto asoma justo bajo ella; el cascarón puede moverla con
|
|
78
|
+
// --app-right-sidebar-handle-top.
|
|
79
|
+
&__handle {
|
|
80
|
+
position: absolute;
|
|
81
|
+
top: var(--app-right-sidebar-handle-top, #{$space-4});
|
|
82
|
+
right: 100%;
|
|
83
|
+
display: flex;
|
|
52
84
|
align-items: center;
|
|
53
85
|
justify-content: center;
|
|
54
|
-
width:
|
|
55
|
-
height:
|
|
56
|
-
|
|
57
|
-
background:
|
|
58
|
-
|
|
86
|
+
width: 28px;
|
|
87
|
+
height: 48px;
|
|
88
|
+
padding: 0;
|
|
89
|
+
background: $surface;
|
|
90
|
+
border: 1px solid $accent-500;
|
|
91
|
+
border-right: none;
|
|
92
|
+
border-radius: $radius-md 0 0 $radius-md;
|
|
93
|
+
color: $accent-500;
|
|
59
94
|
cursor: pointer;
|
|
60
|
-
border-radius: $radius-sm;
|
|
61
95
|
transition: background-color 0.15s ease, color 0.15s ease;
|
|
62
96
|
|
|
63
97
|
&:hover {
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
background: $surface-2;
|
|
99
|
+
color: $accent-600;
|
|
66
100
|
}
|
|
67
101
|
}
|
|
68
102
|
|
|
69
|
-
&__body {
|
|
70
|
-
flex: 1;
|
|
71
|
-
overflow-y: auto;
|
|
72
|
-
padding: $space-4;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
103
|
// El contenido registrado (selects de filtros…) se apila a una columna.
|
|
76
104
|
#app-right-sidebar-target {
|
|
77
105
|
display: grid;
|
|
78
106
|
gap: $space-3;
|
|
79
107
|
}
|
|
80
108
|
|
|
81
|
-
// Modo drawer (por debajo del umbral del componente):
|
|
82
|
-
// contenido,
|
|
83
|
-
&--drawer {
|
|
84
|
-
|
|
85
|
-
top: var(--app-right-sidebar-top, 0px);
|
|
86
|
-
right: 0;
|
|
87
|
-
bottom: 0;
|
|
88
|
-
// El align-self: flex-start del modo columna aplica aquí como alineación
|
|
89
|
-
// de caja posicionada y encogería el alto al contenido en vez de
|
|
90
|
-
// estirarlo hasta bottom: 0.
|
|
91
|
-
align-self: auto;
|
|
92
|
-
height: auto;
|
|
93
|
-
padding-top: 0;
|
|
94
|
-
z-index: 60; // por encima de la cabecera fija (50)
|
|
95
|
-
width: min(320px, 100vw);
|
|
96
|
-
transform: translateX(100%);
|
|
97
|
-
|
|
98
|
-
&.app-right-sidebar--drawer-open {
|
|
99
|
-
transform: translateX(0);
|
|
100
|
-
}
|
|
109
|
+
// Modo drawer (por debajo del umbral del componente): superpuesta al
|
|
110
|
+
// contenido, con telón y cierre por asa, click fuera o Escape.
|
|
111
|
+
&--drawer.app-right-sidebar--open {
|
|
112
|
+
box-shadow: $shadow-lg;
|
|
101
113
|
}
|
|
102
114
|
}
|
|
103
115
|
|
|
116
|
+
// Telón del drawer: bajo la cabecera (que sigue visible y clicable) y bajo
|
|
117
|
+
// la barra (40).
|
|
104
118
|
.app-right-sidebar-overlay {
|
|
105
119
|
position: fixed;
|
|
106
120
|
top: var(--app-right-sidebar-top, 0px);
|
|
107
|
-
left: 0;
|
|
108
121
|
right: 0;
|
|
109
122
|
bottom: 0;
|
|
123
|
+
left: 0;
|
|
110
124
|
background: rgb(0 0 0 / 40%);
|
|
111
|
-
z-index:
|
|
125
|
+
z-index: 35;
|
|
112
126
|
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
@use "tokens" as *;
|
|
2
2
|
|
|
3
|
-
// Toggles de ordenación (SortToggles.vue):
|
|
4
|
-
//
|
|
3
|
+
// Toggles de ordenación (SortToggles.vue): dos botones SUELTOS estilo
|
|
4
|
+
// action-button (sin caja ni borde, como los del header del admin), a la
|
|
5
|
+
// altura táctil de los inputs (36px). El icono marca el sentido y el color
|
|
6
|
+
// lo refuerza: activo ASCENDENTE en tinte suave de acento; activo
|
|
7
|
+
// DESCENDENTE relleno de acento pleno (misma familia, otra intensidad —
|
|
8
|
+
// funciona igual en claro y oscuro y con cualquier acento de juego).
|
|
5
9
|
.sort-toggles {
|
|
6
10
|
display: inline-flex;
|
|
7
|
-
gap:
|
|
8
|
-
height: $input-height;
|
|
9
|
-
padding: 2px;
|
|
10
|
-
background: $input-bg;
|
|
11
|
-
border: 1px solid $input-border;
|
|
12
|
-
border-radius: $input-radius;
|
|
11
|
+
gap: $space-2;
|
|
13
12
|
|
|
14
13
|
&__btn {
|
|
15
14
|
display: inline-flex;
|
|
@@ -17,19 +16,36 @@
|
|
|
17
16
|
justify-content: center;
|
|
18
17
|
flex: 1 1 auto;
|
|
19
18
|
min-width: 40px;
|
|
19
|
+
height: $input-height;
|
|
20
20
|
padding: 0 $space-2;
|
|
21
21
|
border: none;
|
|
22
|
-
border-radius: $radius-
|
|
23
|
-
background:
|
|
22
|
+
border-radius: $radius-md;
|
|
23
|
+
background: none;
|
|
24
24
|
color: $text-2;
|
|
25
25
|
cursor: pointer;
|
|
26
26
|
transition: background-color 0.15s ease, color 0.15s ease;
|
|
27
27
|
|
|
28
|
-
&:hover {
|
|
28
|
+
&:hover {
|
|
29
|
+
color: $accent-500;
|
|
30
|
+
background: color-mix(in srgb, $accent-500 10%, transparent);
|
|
31
|
+
}
|
|
29
32
|
|
|
33
|
+
// Activo ascendente: acento sobre su tinte suave (el activo del tema).
|
|
30
34
|
&.is-active {
|
|
31
|
-
background: color-mix(in srgb, $accent-500 14%, transparent);
|
|
32
35
|
color: $accent-500;
|
|
36
|
+
background: color-mix(in srgb, $accent-500 14%, transparent);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Activo descendente: el mismo acento, relleno (texto en $surface,
|
|
40
|
+
// como el contador de la colección del cascarón).
|
|
41
|
+
&.is-active.is-desc {
|
|
42
|
+
color: $surface;
|
|
43
|
+
background: $accent-500;
|
|
44
|
+
|
|
45
|
+
&:hover {
|
|
46
|
+
color: $surface;
|
|
47
|
+
background: $accent-600;
|
|
48
|
+
}
|
|
33
49
|
}
|
|
34
50
|
}
|
|
35
51
|
}
|
|
@@ -1,34 +1,46 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onBeforeUnmount, onMounted } from 'vue'
|
|
3
|
-
import { X } from '@lucide/vue'
|
|
3
|
+
import { Funnel, X } from '@lucide/vue'
|
|
4
4
|
import { useAppRightSidebar } from '../composables/useAppRightSidebar'
|
|
5
5
|
|
|
6
6
|
// Barra lateral derecha contextual de la web pública (misma mecánica que la
|
|
7
7
|
// RightSidebar del admin-kit): cada vista la activa con
|
|
8
8
|
// useAppRightSidebar().useRegister(titulo) y teletransporta su contenido
|
|
9
|
-
// (típicamente sus selects de filtros) a #app-right-sidebar-target.
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// cabecera
|
|
9
|
+
// (típicamente sus selects de filtros) a #app-right-sidebar-target.
|
|
10
|
+
//
|
|
11
|
+
// A diferencia del admin (marco fijo, scrollea el main), aquí el footer va
|
|
12
|
+
// al final del documento: la barra es SIEMPRE fija (por debajo de la
|
|
13
|
+
// cabecera fija del cascarón → bottom 0) para no scrollear con la página ni
|
|
14
|
+
// "acabarse" al llegar al pie; el cascarón fija --app-right-sidebar-top a la
|
|
15
|
+
// altura real de su cabecera por breakpoint. En ANCHO, desplegada, el
|
|
16
|
+
// cascarón le hace hueco al contenido (padding-right de .site-content /
|
|
17
|
+
// .app-footer sobre la clase `--docked`; la cabecera no lo necesita: la
|
|
18
|
+
// barra no la tapa); por debajo de OVERLAY_BREAKPOINT es un drawer
|
|
19
|
+
// superpuesto con telón (también bajo la cabecera, que sigue usable), click
|
|
20
|
+
// fuera y Escape. Se abre y se cierra con el ASA anclada a la propia barra
|
|
21
|
+
// (Funnel cerrada / X abierta), visible solo si la vista registró
|
|
22
|
+
// contenido; el cascarón puede ajustar su altura con
|
|
23
|
+
// --app-right-sidebar-handle-top (relativa al techo de la barra).
|
|
24
|
+
// La monta App.vue una vez, dentro de .site-main.
|
|
16
25
|
// Agnóstica de i18n (DC-29): textos por prop, defaults en castellano.
|
|
17
26
|
|
|
18
27
|
const props = withDefaults(
|
|
19
28
|
defineProps<{
|
|
29
|
+
/** Label del asa con la barra cerrada. */
|
|
30
|
+
openLabel?: string
|
|
31
|
+
/** Label del asa con la barra abierta. */
|
|
20
32
|
closeLabel?: string
|
|
21
33
|
/** Título por defecto si la vista registra sin título. */
|
|
22
34
|
fallbackTitle?: string
|
|
23
35
|
}>(),
|
|
24
|
-
{ closeLabel: 'Cerrar el panel', fallbackTitle: 'Filtros' },
|
|
36
|
+
{ openLabel: 'Abrir el panel', closeLabel: 'Cerrar el panel', fallbackTitle: 'Filtros' },
|
|
25
37
|
)
|
|
26
38
|
|
|
27
39
|
const { hasContent, collapsed, mobileOpen, overlay, title, isOpen, toggle, closeMobile } =
|
|
28
40
|
useAppRightSidebar()
|
|
29
41
|
|
|
30
|
-
// Por debajo de este ancho la barra se superpone (drawer) en vez de
|
|
31
|
-
//
|
|
42
|
+
// Por debajo de este ancho la barra se superpone (drawer) en vez de hacer
|
|
43
|
+
// hueco: coincide con el corte móvil del header público (900px).
|
|
32
44
|
const OVERLAY_BREAKPOINT = 900
|
|
33
45
|
|
|
34
46
|
function checkOverlay() {
|
|
@@ -53,12 +65,12 @@ onBeforeUnmount(() => {
|
|
|
53
65
|
})
|
|
54
66
|
|
|
55
67
|
const panelTitle = computed(() => title.value || props.fallbackTitle)
|
|
68
|
+
const handleLabel = computed(() => (isOpen.value ? props.closeLabel : props.openLabel))
|
|
56
69
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
70
|
+
// Desplegada en ANCHO: el cascarón le hace hueco (body:has en su scss).
|
|
71
|
+
// hasContent va en la clase porque v-show (display: none) no saca al aside
|
|
72
|
+
// de los selectores :has.
|
|
73
|
+
const docked = computed(() => hasContent.value && !overlay.value && !collapsed.value)
|
|
62
74
|
</script>
|
|
63
75
|
|
|
64
76
|
<template>
|
|
@@ -72,26 +84,33 @@ function handleClose(e: MouseEvent) {
|
|
|
72
84
|
v-show="hasContent"
|
|
73
85
|
class="app-right-sidebar"
|
|
74
86
|
:class="{
|
|
75
|
-
'app-right-sidebar--
|
|
87
|
+
'app-right-sidebar--open': hasContent && isOpen,
|
|
88
|
+
'app-right-sidebar--docked': docked,
|
|
76
89
|
'app-right-sidebar--drawer': overlay,
|
|
77
|
-
'app-right-sidebar--drawer-open': overlay && mobileOpen,
|
|
78
90
|
}"
|
|
79
|
-
:inert="!isOpen"
|
|
80
91
|
>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
<!-- Asa anclada a la barra (viaja con ella al abrir/cerrar): fuera del
|
|
93
|
+
panel inerte para poder abrirla con la barra cerrada. -->
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
class="app-right-sidebar__handle"
|
|
97
|
+
:aria-label="handleLabel"
|
|
98
|
+
:title="handleLabel"
|
|
99
|
+
:aria-expanded="isOpen"
|
|
100
|
+
@click="toggle"
|
|
101
|
+
>
|
|
102
|
+
<X v-if="isOpen" :size="18" />
|
|
103
|
+
<Funnel v-else :size="18" />
|
|
104
|
+
</button>
|
|
105
|
+
|
|
106
|
+
<div class="app-right-sidebar__panel" :inert="!isOpen">
|
|
107
|
+
<div class="app-right-sidebar__header">
|
|
108
|
+
<span class="app-right-sidebar__title">{{ panelTitle }}</span>
|
|
109
|
+
</div>
|
|
92
110
|
|
|
93
|
-
|
|
94
|
-
|
|
111
|
+
<div class="app-right-sidebar__body">
|
|
112
|
+
<div id="app-right-sidebar-target" />
|
|
113
|
+
</div>
|
|
95
114
|
</div>
|
|
96
115
|
</aside>
|
|
97
116
|
</template>
|
|
@@ -5,7 +5,10 @@ import { ArrowDownAZ, ArrowDownZA, CalendarArrowDown, CalendarArrowUp } from '@l
|
|
|
5
5
|
// Par de toggles de ordenación de los index: fecha (latest ⇄ oldest) y
|
|
6
6
|
// alfabético (name ⇄ name_desc). Pulsar el inactivo lo activa en su primer
|
|
7
7
|
// estado; pulsar el activo invierte el sentido. Los valores casan con el
|
|
8
|
-
// ?sort del catálogo público del core.
|
|
8
|
+
// ?sort del catálogo público del core. Botones SUELTOS estilo action-button
|
|
9
|
+
// (scss): el activo se tiñe de acento y `is-desc` marca el sentido
|
|
10
|
+
// DESCENDENTE (latest = más recientes primero, name_desc = Z-A) para que el
|
|
11
|
+
// color lo refuerce además del icono.
|
|
9
12
|
// Agnóstico de i18n (DC-29): textos por prop, defaults en castellano.
|
|
10
13
|
|
|
11
14
|
export type SortValue = 'latest' | 'oldest' | 'name' | 'name_desc'
|
|
@@ -55,7 +58,7 @@ function toggleAlpha() {
|
|
|
55
58
|
<button
|
|
56
59
|
type="button"
|
|
57
60
|
class="sort-toggles__btn"
|
|
58
|
-
:class="{ 'is-active': dateActive }"
|
|
61
|
+
:class="{ 'is-active': dateActive, 'is-desc': modelValue === 'latest' }"
|
|
59
62
|
:aria-pressed="dateActive"
|
|
60
63
|
:aria-label="dateLabel"
|
|
61
64
|
:title="dateLabel"
|
|
@@ -67,7 +70,7 @@ function toggleAlpha() {
|
|
|
67
70
|
<button
|
|
68
71
|
type="button"
|
|
69
72
|
class="sort-toggles__btn"
|
|
70
|
-
:class="{ 'is-active': alphaActive }"
|
|
73
|
+
:class="{ 'is-active': alphaActive, 'is-desc': modelValue === 'name_desc' }"
|
|
71
74
|
:aria-pressed="alphaActive"
|
|
72
75
|
:aria-label="alphaLabel"
|
|
73
76
|
:title="alphaLabel"
|
|
@@ -7,12 +7,12 @@ import { computed, onBeforeUnmount, ref } from 'vue'
|
|
|
7
7
|
* Estado singleton a nivel de módulo (como useToast/useConfirm: los juegos
|
|
8
8
|
* consumen el paquete como fuente con `optimizeDeps.exclude` para que solo
|
|
9
9
|
* exista una copia):
|
|
10
|
-
* - escritorio:
|
|
11
|
-
* contenido; se
|
|
10
|
+
* - escritorio: barra fija a la derecha cuando la vista actual registra
|
|
11
|
+
* contenido (el cascarón le hace hueco); se pliega con `collapsed`.
|
|
12
12
|
* - estrecho: oculta; entra como drawer superpuesto cuando `mobileOpen`.
|
|
13
13
|
*
|
|
14
|
-
* El
|
|
15
|
-
* el modo; `overlay` lo mantiene
|
|
14
|
+
* El asa anclada a la propia barra llama a `toggle()`, que despliega/pliega
|
|
15
|
+
* según el modo; `overlay` lo mantiene AppRightSidebar con el ancho.
|
|
16
16
|
*
|
|
17
17
|
* La propiedad se rastrea con un token opaco para que el `unregister` de una
|
|
18
18
|
* vista saliente nunca borre el contenido cuando otra vista ya ha registrado
|
|
@@ -46,7 +46,7 @@ export function useAppRightSidebar() {
|
|
|
46
46
|
mobileOpen.value = !mobileOpen.value
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/** Abre/cierra según el modo: el
|
|
49
|
+
/** Abre/cierra según el modo: el asa de la barra no distingue anchos. */
|
|
50
50
|
function toggle() {
|
|
51
51
|
if (overlay.value) toggleMobile()
|
|
52
52
|
else toggleCollapsed()
|