@edc-motor/ui 0.5.3 → 0.5.5

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.3",
3
+ "version": "0.5.5",
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",
@@ -26,7 +26,11 @@
26
26
  bottom: 0;
27
27
  z-index: 40; // por debajo de la cabecera fija del cascarón (50)
28
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);
29
+ // OJO: 100% y no 100vw el ancho porcentual de un fixed resuelve contra
30
+ // el viewport REAL (sin la barra de scroll del navegador); con 100vw la
31
+ // scrollbar, cuando aparece, robaba su ancho y el asa/cierre quedaba
32
+ // fuera de pantalla en el tamaño más estrecho.
33
+ width: min(var(--app-right-sidebar-width), 100% - 36px);
30
34
  transform: translateX(100%);
31
35
  transition: transform 0.25s ease;
32
36
 
@@ -37,6 +37,10 @@
37
37
  }
38
38
 
39
39
  // Números de página clicables; el actual, marcado con aria-current.
40
+ // Los que NO son el actual van en caja como prev/next (fondo semiopaco +
41
+ // borde visibles ya en reposo); al hover, acento sutil: tinte de acento
42
+ // al 12% de fondo y el número en color de acento. El actual, como
43
+ // siempre: relleno de acento con texto de contraste.
40
44
  &__number {
41
45
  display: inline-flex;
42
46
  align-items: center;
@@ -44,17 +48,19 @@
44
48
  min-width: 32px;
45
49
  height: 32px;
46
50
  padding: 0 $space-1;
47
- background: none;
48
- border: 1px solid transparent;
51
+ background: color-mix(in srgb, $surface 65%, transparent);
52
+ border: 1px solid $border;
49
53
  border-radius: $radius-sm;
50
54
  color: $text-2;
51
55
  font-size: $fs-13;
56
+ // Más peso sin tocar el tamaño de la caja (32px se quedan como están).
57
+ font-weight: $k-fw-semibold;
52
58
  font-variant-numeric: tabular-nums;
53
- transition: color 0.15s ease, border-color 0.15s ease;
59
+ transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
54
60
 
55
61
  &:hover {
56
- color: $text-1;
57
- border-color: $border;
62
+ color: $accent-500;
63
+ background: color-mix(in srgb, $accent-500 12%, transparent);
58
64
  }
59
65
 
60
66
  &[aria-current="page"] {
@@ -17,6 +17,15 @@
17
17
  padding-block: 4px;
18
18
  }
19
19
 
20
+ // Variante compacta (prop compactTrigger): alto FIJO del select simple y
21
+ // resumen en una línea (el .base-select__value de arriba ya elipsa); las
22
+ // elegidas se pintan fuera (chips del consumidor), no dentro del trigger.
23
+ &--compact .base-select__trigger {
24
+ height: $input-height;
25
+ min-height: 0;
26
+ padding-block: 0;
27
+ }
28
+
20
29
  &__tags {
21
30
  display: flex;
22
31
  flex-wrap: wrap;
@@ -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,13 @@ 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 = props.overlayAlways || window.innerWidth < OVERLAY_BREAKPOINT
48
63
  if (!overlay.value) mobileOpen.value = false
49
64
  }
50
65
 
66
+ // Si el consumidor cambia el modo en caliente, el estado se recalcula.
67
+ watch(() => props.overlayAlways, checkOverlay)
68
+
51
69
  // En el drawer, Escape cierra (el click fuera lo cubre el telón).
52
70
  function onKeydown(e: KeyboardEvent) {
53
71
  if (e.key === 'Escape' && overlay.value && mobileOpen.value) closeMobile()
@@ -13,6 +13,11 @@ import type { SelectOption } from './BaseSelect.vue'
13
13
  // BaseSelect). El trigger pinta las elegidas como etiquetas con aspa: se
14
14
  // quitan desde ahí sin abrir el panel (también con Backspace); así no
15
15
  // hace falta ningún texto "N seleccionadas" que traducir.
16
+ // Con `compactTrigger` (opt-in) el trigger NO crece: mantiene el alto fijo
17
+ // del select simple y, en vez de etiquetas, pinta un resumen en una línea
18
+ // (las elegidas separadas por comas, con elipsis); quitar una elegida
19
+ // queda para fuera (el consumidor pinta sus propios chips) o para el
20
+ // propio panel/Backspace. Sin la prop, todo sigue como siempre.
16
21
  const props = withDefaults(
17
22
  defineProps<{
18
23
  modelValue?: (string | number)[]
@@ -20,12 +25,17 @@ const props = withDefaults(
20
25
  options: SelectOption[]
21
26
  /** Texto en reposo (sin nada marcado), p. ej. "Todas". */
22
27
  placeholder?: string
28
+ /**
29
+ * Trigger compacto de alto fijo: resumen en una línea (labels con
30
+ * comas y elipsis) en lugar de etiquetas con aspa dentro del trigger.
31
+ */
32
+ compactTrigger?: boolean
23
33
  error?: string
24
34
  hint?: string
25
35
  disabled?: boolean
26
36
  id?: string
27
37
  }>(),
28
- { modelValue: () => [], disabled: false },
38
+ { modelValue: () => [], disabled: false, compactTrigger: false },
29
39
  )
30
40
 
31
41
  const emit = defineEmits<{ 'update:modelValue': [value: string[]] }>()
@@ -44,6 +54,9 @@ const selectedValues = computed(() => new Set(props.modelValue.map((v) => String
44
54
  const isSelected = (option: SelectOption) => selectedValues.value.has(String(option.value))
45
55
  const selectedOptions = computed(() => props.options.filter((o) => isSelected(o)))
46
56
 
57
+ /** Resumen de una línea del trigger compacto: labels separadas por comas. */
58
+ const summary = computed(() => selectedOptions.value.map((o) => o.label).join(', '))
59
+
47
60
  /** Quita una elegida desde su etiqueta del trigger, sin tocar el panel. */
48
61
  function removeOption(option: SelectOption) {
49
62
  if (props.disabled) return
@@ -166,7 +179,7 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onOutside))
166
179
  <div
167
180
  ref="root"
168
181
  class="form-field__select-wrapper base-select multi-select"
169
- :class="{ 'is-open': open }"
182
+ :class="{ 'is-open': open, 'multi-select--compact': compactTrigger }"
170
183
  >
171
184
  <button
172
185
  :id="selectId"
@@ -180,9 +193,13 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onOutside))
180
193
  @click="toggle"
181
194
  @keydown="onKeydown"
182
195
  >
196
+ <!-- Trigger compacto (opt-in): resumen en una línea, sin etiquetas -->
197
+ <span v-if="compactTrigger && selectedOptions.length" class="base-select__value">
198
+ {{ summary }}
199
+ </span>
183
200
  <!-- Elegidas como etiquetas con aspa (el aspa NO abre el panel);
184
201
  span con role=button porque un button no puede anidar otro -->
185
- <span v-if="selectedOptions.length" class="multi-select__tags">
202
+ <span v-else-if="selectedOptions.length" class="multi-select__tags">
186
203
  <span v-for="option in selectedOptions" :key="option.value" class="multi-select__tag">
187
204
  {{ option.label }}
188
205
  <span