@edc-motor/ui 0.5.7 → 0.5.9

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.7",
3
+ "version": "0.5.9",
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",
@@ -377,18 +377,25 @@
377
377
  }
378
378
  }
379
379
 
380
- // Cita: sin adorno de borde, en GRANDE ($fs-32, el mismo tamaño que un
381
- // título de bloque) y del color de acento.
380
+ // Cita: sin adorno de borde, en grande ($fs-28, el mismo cuerpo que un
381
+ // título de bloque; antes $fs-32 — un ~10% menos) y del color de acento,
382
+ // con interlineado propio (1.25: las citas de varias líneas respiraban
383
+ // de más con el line-height del cuerpo). En estrecho baja a $fs-24.
382
384
  &--quote .block__quote {
383
385
  margin: 0;
384
386
  // Fuente "especial" de la configuración de la web (doc 10); las fuentes
385
387
  // decorativas piden un cuerpo mayor.
386
388
  font-family: var(--font-special, inherit);
387
- font-size: $fs-32;
389
+ font-size: $fs-28;
390
+ line-height: 1.25;
388
391
  font-style: italic;
389
392
  font-weight: $k-fw-semibold;
390
393
  color: $accent-500;
391
394
 
395
+ @container content (max-width: #{$bp-sm - 1px}) {
396
+ font-size: $fs-24;
397
+ }
398
+
392
399
  // La cita llega como richtext y .rich-content fija su propio cuerpo
393
400
  // ($fs-16): aquí debe heredar el tamaño grande de la cita.
394
401
  .rich-content { font-size: inherit; }
@@ -6,11 +6,16 @@
6
6
  // evita que compita con el contenido.
7
7
  .page-background {
8
8
  position: fixed;
9
- top: 0;
9
+ // El TOP de la imagen queda justo debajo del header de la página: el
10
+ // LAYOUT (cascarón) define --page-background-top con la altura real de su
11
+ // site-header (si es estable por CSS, en scss; si no, medida con JS).
12
+ // Sin definirla vale 0px: comportamiento de siempre (toda la ventana).
13
+ top: var(--page-background-top, 0px);
10
14
  left: 0;
11
15
  width: 100%;
12
- height: 100vh; // fallback
13
- height: 100lvh; // altura SIN la barra del navegador móvil
16
+ height: calc(100vh - var(--page-background-top, 0px)); // fallback
17
+ // altura SIN la barra del navegador móvil
18
+ height: calc(100lvh - var(--page-background-top, 0px));
14
19
  background-size: cover;
15
20
  background-position: top;
16
21
  background-repeat: no-repeat;
@@ -52,6 +52,21 @@
52
52
 
53
53
  &--selected { box-shadow: 0 0 0 2px $surface, 0 0 0 4px var(--swatch-color); }
54
54
 
55
+ // Preset DINÁMICO del tema (token:* → var(--token), resuelto con el
56
+ // tema ACTUAL): borde siempre visible (su color puede confundirse con
57
+ // la superficie del formulario) y check en color de texto del tema (el
58
+ // blanco fijo desaparecía sobre las superficies claras). El anillo de
59
+ // selección en color de texto, como el custom: con el swatch del color
60
+ // del fondo, el anillo del propio color no se veía.
61
+ &--dynamic {
62
+ border-color: $border-strong;
63
+ color: $text-1;
64
+ }
65
+
66
+ &--dynamic#{&}--selected {
67
+ box-shadow: 0 0 0 2px $surface, 0 0 0 4px $text-1;
68
+ }
69
+
55
70
  &--custom { overflow: hidden; }
56
71
 
57
72
  &--custom-idle {
@@ -29,10 +29,11 @@
29
29
  min-width: 0;
30
30
  justify-content: center;
31
31
 
32
- // Estrecho: las tabs con icono se apilan (icono + etiqueta pequeña)
32
+ // Estrecho: las tabs con icono se quedan SOLO con el icono el texto
33
+ // desaparece (la etiqueta, visually hidden más abajo, sigue en el DOM
34
+ // accesible y el botón lleva title; antes se apilaban icono + etiqueta
35
+ // pequeña).
33
36
  &--has-icon {
34
- flex-direction: column;
35
- gap: 2px;
36
37
  padding: $space-1;
37
38
  }
38
39
 
@@ -42,7 +43,6 @@
42
43
  padding: $space-2;
43
44
 
44
45
  &--has-icon {
45
- flex-direction: row;
46
46
  gap: $space-1;
47
47
  padding: $space-2;
48
48
  }
@@ -78,17 +78,24 @@
78
78
  text-overflow: ellipsis;
79
79
  white-space: nowrap;
80
80
 
81
+ // En estrecho, la etiqueta de una tab CON icono desaparece VISUALMENTE
82
+ // (patrón visually-hidden, no display:none: sigue en el árbol de
83
+ // accesibilidad y da nombre al botón; el title del botón cubre el
84
+ // hover). De $bp-sm para arriba vuelve al flujo, en línea con el icono.
81
85
  .tabs__tab--has-icon & {
82
- font-size: 10px;
83
- line-height: 1;
86
+ position: absolute;
87
+ width: 1px;
88
+ height: 1px;
89
+ margin: -1px;
90
+ padding: 0;
91
+ clip-path: inset(50%);
84
92
 
85
93
  @container content (min-width: #{$bp-sm}) {
86
- font-size: $fs-12;
87
- line-height: inherit;
88
- }
89
-
90
- @container content (min-width: #{$bp-md}) {
91
- font-size: $fs-14;
94
+ position: static;
95
+ width: auto;
96
+ height: auto;
97
+ margin: 0;
98
+ clip-path: none;
92
99
  }
93
100
  }
94
101
  }
@@ -2,10 +2,12 @@
2
2
  import { computed, type CSSProperties } from 'vue'
3
3
 
4
4
  // Envoltorio común de todos los bloques públicos: aplica los campos comunes
5
- // (align, width, background) que el motor añade a cada tipo. El color de
6
- // fondo NO se aplica opaco: es un tinte semitransparente (--block-tint,
5
+ // (align, width, background) que el motor añade a cada tipo. Un hex de la
6
+ // paleta NO se aplica opaco: es un tinte semitransparente (--block-tint,
7
7
  // distinto por tema, patrón CDL) para que la imagen de fondo de la página se
8
- // vea a través. La anchura del CONTENIDO (full/wide/narrow) va por clase.
8
+ // vea a través. Los presets DINÁMICOS (`token:*`) van opacos: un token de
9
+ // superficie ya ES un color de fondo pensado para contrastar con la página,
10
+ // y al 15% resultaba inapreciable. La anchura del CONTENIDO va por clase.
9
11
  const props = defineProps<{ settings: Record<string, unknown> }>()
10
12
 
11
13
  const width = computed(() => `block--w-${(props.settings.width as string) || 'wide'}`)
@@ -19,16 +21,33 @@ const headingAlign = (key: 'title_align' | 'subtitle_align', prefix: string) =>
19
21
  return value && value !== 'inherit' ? `block--${prefix}-${value}` : ''
20
22
  }
21
23
 
24
+ // El color de fondo puede ser un hex fijo o un preset DINÁMICO del tema
25
+ // serializado como `token:<nombre>` (p. ej. token:surface): se resuelve a
26
+ // su custom property (var(--surface)) y se aplica OPACO, sin el mix del
27
+ // tinte — un token de superficie ya es un fondo de nivel de tema que sigue
28
+ // al claro/oscuro; mezclado al 15% con transparente era invisible sobre la
29
+ // página. El nombre se sanea a [a-z0-9-] (el value viene del admin, pero un
30
+ // token roto no debe inyectar CSS).
31
+ const TOKEN_PREFIX = 'token:'
32
+ function isToken(value: string): boolean {
33
+ return value.startsWith(TOKEN_PREFIX)
34
+ }
35
+ function resolveToken(value: string): string {
36
+ return `var(--${value.slice(TOKEN_PREFIX.length).replace(/[^a-z0-9-]/gi, '')})`
37
+ }
38
+
22
39
  // La alineación va por CLASE (block--align-*, _blocks.scss), no por style en
23
40
  // línea: un style inline no se puede pisar desde CSS (bloqueaba el cambio a
24
41
  // izquierda del justificado en estrecho, DC-03 ampliado).
25
42
  const style = computed<CSSProperties>(() => {
26
43
  const background = props.settings.background as string | undefined
27
- return {
28
- '--block-bg': background
29
- ? `color-mix(in srgb, ${background} var(--block-tint, 15%), transparent)`
30
- : 'transparent',
44
+ let bg = 'transparent'
45
+ if (background) {
46
+ bg = isToken(background)
47
+ ? resolveToken(background)
48
+ : `color-mix(in srgb, ${background} var(--block-tint, 15%), transparent)`
31
49
  }
50
+ return { '--block-bg': bg }
32
51
  })
33
52
  </script>
34
53
 
@@ -1,7 +1,9 @@
1
1
  <script setup lang="ts">
2
- // Imagen de fondo de la página (patrón CDL): capa FIJA a toda la ventana,
3
- // detrás del contenido, atenuada según el tema (--page-bg-opacity) y con un
4
- // desaturado suave para que el texto siga siendo legible. Decorativa.
2
+ // Imagen de fondo de la página (patrón CDL): capa FIJA detrás del
3
+ // contenido desde justo debajo del header (--page-background-top, la
4
+ // define el layout del cascarón; 0 si no existe) hasta abajo —, atenuada
5
+ // según el tema (--page-bg-opacity) y con un desaturado suave para que el
6
+ // texto siga siendo legible. Decorativa.
5
7
  defineProps<{ image?: string | null }>()
6
8
  </script>
7
9
 
@@ -1,6 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import type { Component } from 'vue'
3
3
 
4
+ // Una tab CON icono lleva además title con su etiqueta: en estrecho el
5
+ // texto desaparece y queda solo el icono (la etiqueta sigue en el DOM
6
+ // accesible, visually hidden — ver _tabs.scss).
4
7
  interface Tab {
5
8
  key: string
6
9
  label: string
@@ -20,6 +23,7 @@ defineEmits<{ 'update:modelValue': [string] }>()
20
23
  type="button"
21
24
  class="tabs__tab"
22
25
  :class="{ 'tabs__tab--active': modelValue === tab.key, 'tabs__tab--has-icon': !!tab.icon }"
26
+ :title="tab.icon ? tab.label : undefined"
23
27
  @click="$emit('update:modelValue', tab.key)"
24
28
  >
25
29
  <component :is="tab.icon" v-if="tab.icon" class="tabs__icon" :size="16" />
@@ -2,16 +2,29 @@
2
2
  import { computed, ref, watch } from 'vue'
3
3
  import { Check, Pipette } from '@lucide/vue'
4
4
 
5
+ /** Preset DINÁMICO del tema: value `token:<nombre>` (p. ej. token:surface),
6
+ * que se resuelve a la custom property `var(--<nombre>)` del tema ACTIVO
7
+ * (los tokens del tema son CSS vars también en el admin). */
8
+ export interface ColorPreset {
9
+ value: string
10
+ label: string
11
+ }
12
+
5
13
  // Selector de color (portado/adaptado de kontuan). A diferencia de kontuan
6
14
  // (que guarda la clave de la paleta), aquí emite el HEX directamente, para
7
- // campos de color guardados como cadena hexadecimal.
15
+ // campos de color guardados como cadena hexadecimal. Además de la paleta
16
+ // fija puede ofrecer presets DINÁMICOS (`presets`, valores token:* — el
17
+ // fondo de bloque del CRM los usa): se pintan resueltos con el tema actual,
18
+ // con title descriptivo, y se seleccionan/deseleccionan igual que un hex.
8
19
  const props = withDefaults(
9
20
  defineProps<{
10
21
  modelValue: string | null
11
22
  label?: string
12
23
  allowCustom?: boolean
24
+ /** Presets dinámicos del tema, delante de la paleta (vacío = como siempre). */
25
+ presets?: ColorPreset[]
13
26
  }>(),
14
- { allowCustom: true },
27
+ { allowCustom: true, presets: () => [] },
15
28
  )
16
29
 
17
30
  const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
@@ -31,15 +44,32 @@ const PALETTE = [
31
44
  { name: 'Gris', hex: '#64748B' },
32
45
  ]
33
46
 
47
+ const TOKEN_PREFIX = 'token:'
48
+
49
+ /** CSS del swatch de un preset: token:x => var(--x); un hex pasa tal cual. */
50
+ function swatchColor(value: string): string {
51
+ if (!value.startsWith(TOKEN_PREFIX)) return value
52
+ return `var(--${value.slice(TOKEN_PREFIX.length).replace(/[^a-z0-9-]/gi, '')})`
53
+ }
54
+
34
55
  const norm = (v: string | null) => (v ?? '').toLowerCase()
35
- const isPreset = computed(() => PALETTE.some((c) => norm(c.hex) === norm(props.modelValue)))
36
- const isCustom = computed(() => !!props.modelValue && !isPreset.value)
56
+ const isPreset = computed(
57
+ () =>
58
+ PALETTE.some((c) => norm(c.hex) === norm(props.modelValue)) ||
59
+ props.presets.some((p) => norm(p.value) === norm(props.modelValue)),
60
+ )
61
+ // Un token desconocido tampoco es "custom": el input color solo entiende hex.
62
+ const isCustom = computed(
63
+ () => !!props.modelValue && !isPreset.value && !props.modelValue.startsWith(TOKEN_PREFIX),
64
+ )
37
65
 
38
66
  const customHex = ref(isCustom.value ? (props.modelValue as string) : '#FF7A00')
39
67
  watch(
40
68
  () => props.modelValue,
41
69
  (v) => {
42
- if (v && !PALETTE.some((c) => norm(c.hex) === norm(v))) customHex.value = v
70
+ if (v && !v.startsWith(TOKEN_PREFIX) && !PALETTE.some((c) => norm(c.hex) === norm(v))) {
71
+ customHex.value = v
72
+ }
43
73
  },
44
74
  )
45
75
 
@@ -54,6 +84,26 @@ function onCustomInput(event: Event) {
54
84
  <div class="palette-color-picker">
55
85
  <label v-if="label" class="palette-color-picker__label">{{ label }}</label>
56
86
  <div class="palette-color-picker__grid">
87
+ <!-- Presets dinámicos del tema (token:*), resueltos con el tema actual -->
88
+ <button
89
+ v-for="preset in presets"
90
+ :key="preset.value"
91
+ type="button"
92
+ class="palette-color-picker__swatch palette-color-picker__swatch--dynamic"
93
+ :class="{
94
+ 'palette-color-picker__swatch--selected': norm(modelValue) === norm(preset.value),
95
+ }"
96
+ :style="{ '--swatch-color': swatchColor(preset.value) }"
97
+ :title="preset.label"
98
+ @click="emit('update:modelValue', preset.value)"
99
+ >
100
+ <Check
101
+ v-if="norm(modelValue) === norm(preset.value)"
102
+ class="palette-color-picker__swatch-check"
103
+ :size="14"
104
+ />
105
+ </button>
106
+
57
107
  <button
58
108
  v-for="opt in PALETTE"
59
109
  :key="opt.hex"
package/src/index.ts CHANGED
@@ -12,7 +12,10 @@ export { default as BaseSelect, type SelectOption } from './components/BaseSelec
12
12
  export { default as SearchSelect, type SearchSelectOption } from './components/SearchSelect.vue'
13
13
  export { default as BaseCheckbox } from './components/BaseCheckbox.vue'
14
14
  export { default as NumericInput } from './components/NumericInput.vue'
15
- export { default as PaletteColorPicker } from './components/PaletteColorPicker.vue'
15
+ export {
16
+ default as PaletteColorPicker,
17
+ type ColorPreset,
18
+ } from './components/PaletteColorPicker.vue'
16
19
  export { default as TranslatableInput } from './components/TranslatableInput.vue'
17
20
  export { default as TranslatableImage } from './components/TranslatableImage.vue'
18
21
  // Locale global de un formulario: EditModal lo provee solo (selector compacto