@edc-motor/ui 0.5.4 → 0.5.6

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.5.4",
3
+ "version": "0.5.6",
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",
@@ -87,6 +87,23 @@ $icon-chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000
87
87
  cursor: pointer;
88
88
  }
89
89
 
90
+ // MITIGACIÓN del «baile» al hover en el modal de bloque (DPI fraccionario):
91
+ // dentro de un modal, el realce de hover/focus es INSTANTÁNEO. La
92
+ // transición de border-color/box-shadow fuerza ~10 repintados seguidos con
93
+ // un área de invalidación mayor que el propio input (el box-shadow pinta
94
+ // fuera de su caja) y, con las filas del esquema en columnas de ancho
95
+ // fraccional (auto-fit 1fr) y un devicePixelRatio no entero (125%/150%),
96
+ // esa re-rasterización repetida puede hacer temblar los campos VECINOS.
97
+ // Un único repintado por cambio de estado elimina el tembleque sin tocar
98
+ // la geometría ni el aspecto final.
99
+ .modal & {
100
+ &__input,
101
+ &__textarea,
102
+ &__select {
103
+ transition: none;
104
+ }
105
+ }
106
+
90
107
  &--error &__input,
91
108
  &--error &__textarea,
92
109
  &--error &__select {
@@ -32,9 +32,23 @@
32
32
  background: var(--swatch-color);
33
33
  color: #fff;
34
34
  cursor: pointer;
35
- transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
35
+ transition: box-shadow 0.15s, border-color 0.15s;
36
36
 
37
- &:hover { transform: scale(1.06); }
37
+ // Realce al hover: SOLO halo (sombra), nunca transform — el
38
+ // scale(1.06) de antes rasterizaba el swatch (y sus 2px de borde) en
39
+ // fracciones de píxel durante la transición: al pasear el ratón por
40
+ // el formulario del modal de bloque la fila entera «bailaba» y los
41
+ // bordes finos vecinos parpadeaban (peor aún con zoom/DPI
42
+ // fraccionario). El halo se pinta fuera de la caja y no toca ni
43
+ // layout ni raster de los vecinos.
44
+ // (el seleccionado conserva su anillo: el halo solo aplica sin marcar)
45
+ &:hover:not(#{&}--selected) {
46
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--swatch-color) 40%, transparent);
47
+ }
48
+
49
+ &--custom-idle:hover:not(#{&}--selected) {
50
+ box-shadow: 0 0 0 3px color-mix(in srgb, #3b82f6 40%, transparent);
51
+ }
38
52
 
39
53
  &--selected { box-shadow: 0 0 0 2px $surface, 0 0 0 4px var(--swatch-color); }
40
54
 
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, onBeforeUnmount, onMounted } from 'vue'
2
+ import { computed, onBeforeUnmount, onMounted, watch } from 'vue'
3
3
  import { Funnel, X } from '@lucide/vue'
4
4
  import { useAppRightSidebar } from '../composables/useAppRightSidebar'
5
5
 
@@ -17,7 +17,11 @@ import { useAppRightSidebar } from '../composables/useAppRightSidebar'
17
17
  // .app-footer sobre la clase `--docked`; la cabecera no lo necesita: la
18
18
  // barra no la tapa); por debajo de OVERLAY_BREAKPOINT es un drawer
19
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
20
+ // fuera y Escape. Con `overlayAlways` el modo drawer vale para TODAS las
21
+ // anchuras: la barra se superpone siempre al contenido (nunca le roba ancho
22
+ // al main) y se abre/cierra igual que el drawer estrecho — para juegos que
23
+ // prefieren no hacer hueco ni en escritorio. Se abre y se cierra con el ASA
24
+ // anclada a la propia barra
21
25
  // (Funnel cerrada / X abierta), visible solo si la vista registró
22
26
  // contenido; el cascarón puede ajustar su altura con
23
27
  // --app-right-sidebar-handle-top (relativa al techo de la barra).
@@ -32,8 +36,19 @@ const props = withDefaults(
32
36
  closeLabel?: string
33
37
  /** Título por defecto si la vista registra sin título. */
34
38
  fallbackTitle?: string
39
+ /**
40
+ * Drawer superpuesto en TODAS las anchuras: nunca "atraca" (docked) ni
41
+ * le hace hueco el cascarón — siempre telón + superposición, como en
42
+ * estrecho. Por defecto, solo por debajo del corte de 900px.
43
+ */
44
+ overlayAlways?: boolean
35
45
  }>(),
36
- { openLabel: 'Abrir el panel', closeLabel: 'Cerrar el panel', fallbackTitle: 'Filtros' },
46
+ {
47
+ openLabel: 'Abrir el panel',
48
+ closeLabel: 'Cerrar el panel',
49
+ fallbackTitle: 'Filtros',
50
+ overlayAlways: false,
51
+ },
37
52
  )
38
53
 
39
54
  const { hasContent, collapsed, mobileOpen, overlay, title, isOpen, toggle, closeMobile } =
@@ -44,10 +59,20 @@ const { hasContent, collapsed, mobileOpen, overlay, title, isOpen, toggle, close
44
59
  const OVERLAY_BREAKPOINT = 900
45
60
 
46
61
  function checkOverlay() {
47
- overlay.value = window.innerWidth < OVERLAY_BREAKPOINT
62
+ overlay.value =
63
+ props.overlayAlways || (typeof window !== 'undefined' && window.innerWidth < OVERLAY_BREAKPOINT)
48
64
  if (!overlay.value) mobileOpen.value = false
49
65
  }
50
66
 
67
+ // EAGER, en el propio setup (no esperar a mounted): que no exista NI UN
68
+ // frame en el que overlayAlways pudiera pintarse como columna atracada
69
+ // (p. ej. si el singleton del composable se recrea en caliente con HMR y
70
+ // `overlay` vuelve a su valor inicial `false`).
71
+ checkOverlay()
72
+
73
+ // Si el consumidor cambia el modo en caliente, el estado se recalcula.
74
+ watch(() => props.overlayAlways, checkOverlay)
75
+
51
76
  // En el drawer, Escape cierra (el click fuera lo cubre el telón).
52
77
  function onKeydown(e: KeyboardEvent) {
53
78
  if (e.key === 'Escape' && overlay.value && mobileOpen.value) closeMobile()
@@ -69,8 +94,13 @@ const handleLabel = computed(() => (isOpen.value ? props.closeLabel : props.open
69
94
 
70
95
  // Desplegada en ANCHO: el cascarón le hace hueco (body:has en su scss).
71
96
  // 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)
97
+ // de los selectores :has. Con `overlayAlways`, `--docked` es imposible por
98
+ // construcción (aunque `overlay` aún no se hubiera recalculado): el
99
+ // cascarón no puede hacerle hueco JAMÁS y el drawer estrecho es la única
100
+ // versión que existe.
101
+ const docked = computed(
102
+ () => !props.overlayAlways && hasContent.value && !overlay.value && !collapsed.value,
103
+ )
74
104
  </script>
75
105
 
76
106
  <template>