@edc-motor/ui 0.5.7 → 0.5.8
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 +1 -1
- package/scss/components/_blocks.scss +10 -3
- package/scss/components/_page-background.scss +8 -3
- package/scss/components/_palette-color-picker.scss +15 -0
- package/scss/components/_tabs.scss +19 -12
- package/src/blocks/BlockShell.vue +12 -1
- package/src/blocks/PageBackground.vue +5 -3
- package/src/components/BaseTabs.vue +4 -0
- package/src/components/PaletteColorPicker.vue +55 -5
- package/src/index.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edc-motor/ui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
}
|
|
@@ -19,6 +19,17 @@ const headingAlign = (key: 'title_align' | 'subtitle_align', prefix: string) =>
|
|
|
19
19
|
return value && value !== 'inherit' ? `block--${prefix}-${value}` : ''
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// El color de fondo puede ser un hex fijo o un preset DINÁMICO del tema
|
|
23
|
+
// serializado como `token:<nombre>` (p. ej. token:surface): se resuelve a
|
|
24
|
+
// su custom property (var(--surface)) — color-mix admite vars — y así el
|
|
25
|
+
// tinte sigue al tema claro/oscuro. El nombre se sanea a [a-z0-9-] (el
|
|
26
|
+
// value viene del admin, pero un token roto no debe inyectar CSS).
|
|
27
|
+
const TOKEN_PREFIX = 'token:'
|
|
28
|
+
function resolveColor(value: string): string {
|
|
29
|
+
if (!value.startsWith(TOKEN_PREFIX)) return value
|
|
30
|
+
return `var(--${value.slice(TOKEN_PREFIX.length).replace(/[^a-z0-9-]/gi, '')})`
|
|
31
|
+
}
|
|
32
|
+
|
|
22
33
|
// La alineación va por CLASE (block--align-*, _blocks.scss), no por style en
|
|
23
34
|
// línea: un style inline no se puede pisar desde CSS (bloqueaba el cambio a
|
|
24
35
|
// izquierda del justificado en estrecho, DC-03 ampliado).
|
|
@@ -26,7 +37,7 @@ const style = computed<CSSProperties>(() => {
|
|
|
26
37
|
const background = props.settings.background as string | undefined
|
|
27
38
|
return {
|
|
28
39
|
'--block-bg': background
|
|
29
|
-
? `color-mix(in srgb, ${background} var(--block-tint, 15%), transparent)`
|
|
40
|
+
? `color-mix(in srgb, ${resolveColor(background)} var(--block-tint, 15%), transparent)`
|
|
30
41
|
: 'transparent',
|
|
31
42
|
}
|
|
32
43
|
})
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// Imagen de fondo de la página (patrón CDL): capa FIJA
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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(
|
|
36
|
-
|
|
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)))
|
|
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 {
|
|
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
|