@edc-motor/ui 0.4.12 → 0.4.13

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.12",
3
+ "version": "0.4.13",
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 (flex row con .site-content). El cascarón fija
6
- // --app-right-sidebar-top a la altura de su cabecera fija; sin fijarla, la
7
- // barra arranca en el borde superior.
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: sticky;
11
- top: 0;
12
- align-self: flex-start;
13
- height: 100dvh;
14
- width: 320px;
15
- flex-shrink: 0;
16
- padding-top: var(--app-right-sidebar-top, 0px);
17
- background: $surface;
18
- border-left: 1px solid $border;
19
- display: flex;
20
- flex-direction: column;
21
- overflow: hidden;
22
- transition: width 0.2s ease, transform 0.25s ease;
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
- &--collapsed {
25
- width: 0;
26
- border-left: 0;
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-2 0 $space-4;
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
- // Cerrar como el action-button del modal: sin caja, acento al hover.
50
- &__close {
51
- display: inline-flex;
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: 32px;
55
- height: 32px;
56
- border: none;
57
- background: none;
58
- color: $text-2;
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
- color: $accent-500;
65
- background: color-mix(in srgb, $accent-500 12%, transparent);
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): superpuesto al
82
- // contenido, bajo la cabecera, con telón y cierre por click fuera/Escape.
83
- &--drawer {
84
- position: fixed;
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: 55;
125
+ z-index: 35;
112
126
  }
@@ -1,15 +1,14 @@
1
1
  @use "tokens" as *;
2
2
 
3
- // Toggles de ordenación (SortToggles.vue): grupo segmentado a la altura de
4
- // los inputs (36px); el activo se tiñe de acento y su icono marca el sentido.
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: 2px;
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-sm;
23
- background: transparent;
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 { color: $text-1; }
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. En ancho
10
- // es una columna pegajosa junto al contenido; por debajo de
11
- // OVERLAY_BREAKPOINT pasa a drawer superpuesto con cierre por click fuera y
12
- // Escape. Se abre/cierra con el botón (Funnel) del header, que llama a
13
- // useAppRightSidebar().toggle(). La monta App.vue una vez, dentro de
14
- // .site-main; el cascarón fija --app-right-sidebar-top a la altura de su
15
- // cabecera fija.
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 ocupar
31
- // columna: coincide con el corte móvil del header público (900px).
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
- function handleClose(e: MouseEvent) {
58
- // Evita dejar el foco dentro de un subárbol inert.
59
- ;(e.currentTarget as HTMLElement | null)?.blur()
60
- toggle()
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--collapsed': collapsed && !overlay,
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
- <div class="app-right-sidebar__header">
82
- <span class="app-right-sidebar__title">{{ panelTitle }}</span>
83
- <button
84
- type="button"
85
- class="app-right-sidebar__close"
86
- :aria-label="closeLabel"
87
- @click="handleClose"
88
- >
89
- <X :size="18" />
90
- </button>
91
- </div>
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
- <div class="app-right-sidebar__body">
94
- <div id="app-right-sidebar-target" />
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: columna junto al contenido cuando la vista actual registra
11
- * contenido; se puede plegar con `collapsed`.
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 botón del header (Funnel) llama a `toggle()`, que despliega/pliega según
15
- * el modo; `overlay` lo mantiene el componente AppRightSidebar con el ancho.
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 botón del header no distingue anchos. */
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()