@edc-motor/ui 0.4.30 → 0.4.32
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/_forms.scss +3 -1
- package/scss/components/_blocks.scss +8 -8
- package/scss/components/_index.scss +1 -0
- package/scss/components/_number-input.scss +48 -0
- package/scss/components/_rich-content.scss +8 -7
- package/src/components/NumberInput.vue +82 -0
- package/src/components/SortToggles.vue +1 -1
- package/src/index.ts +1 -0
- package/src/lib/createApi.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edc-motor/ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.32",
|
|
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",
|
package/scss/_forms.scss
CHANGED
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
.field { display: flex; flex-direction: column; gap: $space-1; }
|
|
10
10
|
.field__label, .field label { font-size: $fs-13; color: $text-2; }
|
|
11
11
|
|
|
12
|
+
// El numérico compacto del NumberInput queda FUERA: esta regla genérica le
|
|
13
|
+
// ganaba por especificidad y le imponía el 100%/36px de un campo de línea.
|
|
12
14
|
input[type="text"],
|
|
13
15
|
input[type="email"],
|
|
14
16
|
input[type="password"],
|
|
15
17
|
input[type="search"],
|
|
16
|
-
input[type="number"],
|
|
18
|
+
input[type="number"]:not(.number-input__field),
|
|
17
19
|
select,
|
|
18
20
|
textarea {
|
|
19
21
|
font: inherit;
|
|
@@ -39,14 +39,17 @@
|
|
|
39
39
|
&--quote.block--w-narrow .block__inner { max-width: min(780px, 90%); }
|
|
40
40
|
|
|
41
41
|
// Tipografía de bloque: texto BASE $fs-16 (el mismo cuerpo que el wysiwyg
|
|
42
|
-
// por defecto — sin override aquí)
|
|
43
|
-
//
|
|
44
|
-
//
|
|
42
|
+
// por defecto — sin override aquí); título y subtítulo calcan la cabecera
|
|
43
|
+
// clásica de índices y fichas de CDL: título $fs-28 compacto (line-height
|
|
44
|
+
// 1.1, el h1 de base) y subtítulo atenuado AL TAMAÑO del cuerpo — la
|
|
45
|
+
// jerarquía la da el color, no otro escalón de tamaño. Vale para TODOS
|
|
46
|
+
// los bloques (la cabecera ya no sube a 40/28). Los títulos nunca se
|
|
47
|
+
// justifican.
|
|
45
48
|
// width 100%: en grids con justify-items start (el cuerpo del CTA) el
|
|
46
49
|
// elemento encoge a su contenido y el text-align no se vería.
|
|
47
|
-
&__title { margin: 0; font-size: $fs-
|
|
50
|
+
&__title { margin: 0; font-size: $fs-28; line-height: 1.1; width: 100%; }
|
|
48
51
|
// pre-line: el subtítulo es un textarea — sus saltos de línea cuentan.
|
|
49
|
-
&__subtitle { margin: 0; color: $text-2; font-size: $fs-
|
|
52
|
+
&__subtitle { margin: 0; color: $text-2; font-size: $fs-16; white-space: pre-line; width: 100%; }
|
|
50
53
|
&__text { color: $text-1; font-size: $fs-16; }
|
|
51
54
|
|
|
52
55
|
// Alineación del bloque (campo común `align`): por CLASE (BlockShell ya
|
|
@@ -118,9 +121,6 @@
|
|
|
118
121
|
.edc-button { justify-self: start; text-decoration: none; }
|
|
119
122
|
}
|
|
120
123
|
|
|
121
|
-
&--header .block__title { font-size: $fs-40; }
|
|
122
|
-
&--header .block__subtitle { font-size: $fs-28; }
|
|
123
|
-
|
|
124
124
|
&__media-layout { display: grid; gap: $space-4; }
|
|
125
125
|
&__image {
|
|
126
126
|
max-width: 100%;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
@use "tokens" as *;
|
|
2
|
+
|
|
3
|
+
// Input numérico compacto con steppers −/+ (NumberInput.vue): la estética
|
|
4
|
+
// del contador de copias del editor de mazos de CDL, generalizada.
|
|
5
|
+
.number-input {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: $space-1;
|
|
9
|
+
|
|
10
|
+
&__step {
|
|
11
|
+
display: inline-flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
width: 1.6rem;
|
|
15
|
+
height: 1.6rem;
|
|
16
|
+
padding: 0;
|
|
17
|
+
border: 1px solid $border;
|
|
18
|
+
border-radius: $radius-sm;
|
|
19
|
+
background: $surface-2;
|
|
20
|
+
color: $text-2;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
|
|
23
|
+
&:hover:not(:disabled) { color: $text-1; border-color: $text-3; }
|
|
24
|
+
&:disabled { opacity: $disabled-opacity; cursor: not-allowed; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&__field {
|
|
28
|
+
width: 2.6rem;
|
|
29
|
+
padding: 0.1rem 0.2rem;
|
|
30
|
+
text-align: center;
|
|
31
|
+
font-weight: $k-fw-bold;
|
|
32
|
+
font-size: $fs-13;
|
|
33
|
+
color: $text-1;
|
|
34
|
+
border: 1px solid $border;
|
|
35
|
+
border-radius: $radius-sm;
|
|
36
|
+
background: $surface;
|
|
37
|
+
appearance: textfield;
|
|
38
|
+
-moz-appearance: textfield;
|
|
39
|
+
|
|
40
|
+
&::-webkit-outer-spin-button,
|
|
41
|
+
&::-webkit-inner-spin-button {
|
|
42
|
+
appearance: none;
|
|
43
|
+
margin: 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&--invalid &__field { color: $danger; border-color: $danger; }
|
|
48
|
+
}
|
|
@@ -34,13 +34,14 @@
|
|
|
34
34
|
> :first-child { margin-block-start: 0; }
|
|
35
35
|
> :last-child { margin-block-end: 0; }
|
|
36
36
|
|
|
37
|
-
// Escala del wysiwyg
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
// Escala del wysiwyg, POR DEBAJO del título de bloque ($fs-28 desde que
|
|
38
|
+
// los bloques calcan la cabecera clásica): h2 24 · h3 20 · h4 18 · h5 16
|
|
39
|
+
// (como el cuerpo, se distingue por el peso) · h6 14.
|
|
40
|
+
h2 { font-size: $fs-24; }
|
|
41
|
+
h3 { font-size: $fs-20; }
|
|
42
|
+
h4 { font-size: $fs-18; }
|
|
43
|
+
h5 { font-size: $fs-16; }
|
|
44
|
+
h6 { font-size: $fs-14; }
|
|
44
45
|
|
|
45
46
|
ul,
|
|
46
47
|
ol {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { Minus, Plus } from '@lucide/vue'
|
|
4
|
+
|
|
5
|
+
// Input numérico compacto con steppers −/+ (nacido como el contador de
|
|
6
|
+
// copias del editor de mazos de CDL): input centrado sin flechas nativas,
|
|
7
|
+
// botones que respetan min/max y clamp al teclear. `invalid` pinta el borde
|
|
8
|
+
// en danger (p. ej. copias por encima del límite del modo).
|
|
9
|
+
const props = withDefaults(
|
|
10
|
+
defineProps<{
|
|
11
|
+
modelValue: number
|
|
12
|
+
min?: number
|
|
13
|
+
max?: number
|
|
14
|
+
step?: number
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
invalid?: boolean
|
|
17
|
+
/** aria-label del input (los botones llevan los suyos). */
|
|
18
|
+
label?: string
|
|
19
|
+
decreaseLabel?: string
|
|
20
|
+
increaseLabel?: string
|
|
21
|
+
}>(),
|
|
22
|
+
{ step: 1, disabled: false, invalid: false },
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits<{ 'update:modelValue': [number] }>()
|
|
26
|
+
|
|
27
|
+
function clamp(value: number): number {
|
|
28
|
+
if (Number.isNaN(value)) value = props.min ?? 0
|
|
29
|
+
if (props.min !== undefined) value = Math.max(props.min, value)
|
|
30
|
+
if (props.max !== undefined) value = Math.min(props.max, value)
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const atMin = computed(() => props.min !== undefined && props.modelValue <= props.min)
|
|
35
|
+
const atMax = computed(() => props.max !== undefined && props.modelValue >= props.max)
|
|
36
|
+
|
|
37
|
+
function stepBy(delta: number) {
|
|
38
|
+
emit('update:modelValue', clamp(props.modelValue + delta * props.step))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function onChange(event: Event) {
|
|
42
|
+
const input = event.target as HTMLInputElement
|
|
43
|
+
const value = clamp(Number(input.value))
|
|
44
|
+
// El clamp puede dejar el mismo modelValue (sin re-render): se repinta a mano.
|
|
45
|
+
input.value = String(value)
|
|
46
|
+
emit('update:modelValue', value)
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<span class="number-input" :class="{ 'number-input--invalid': invalid }">
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
class="number-input__step"
|
|
55
|
+
:disabled="disabled || atMin"
|
|
56
|
+
:aria-label="decreaseLabel"
|
|
57
|
+
@click="stepBy(-1)"
|
|
58
|
+
>
|
|
59
|
+
<Minus :size="14" />
|
|
60
|
+
</button>
|
|
61
|
+
<input
|
|
62
|
+
class="number-input__field"
|
|
63
|
+
type="number"
|
|
64
|
+
:value="modelValue"
|
|
65
|
+
:min="min"
|
|
66
|
+
:max="max"
|
|
67
|
+
:step="step"
|
|
68
|
+
:disabled="disabled"
|
|
69
|
+
:aria-label="label"
|
|
70
|
+
@change="onChange"
|
|
71
|
+
/>
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
class="number-input__step"
|
|
75
|
+
:disabled="disabled || atMax"
|
|
76
|
+
:aria-label="increaseLabel"
|
|
77
|
+
@click="stepBy(1)"
|
|
78
|
+
>
|
|
79
|
+
<Plus :size="14" />
|
|
80
|
+
</button>
|
|
81
|
+
</span>
|
|
82
|
+
</template>
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export {
|
|
|
31
31
|
export const RichTextInput = defineAsyncComponent(() => import('./components/RichTextInput.vue'))
|
|
32
32
|
export type { RichIcon, RichTextLabels } from './components/RichTextInput.vue'
|
|
33
33
|
export { default as ImageUpload } from './components/ImageUpload.vue'
|
|
34
|
+
export { default as NumberInput } from './components/NumberInput.vue'
|
|
34
35
|
export { default as FontUpload } from './components/FontUpload.vue'
|
|
35
36
|
export { default as BaseModal } from './components/BaseModal.vue'
|
|
36
37
|
export { default as EditModal } from './components/EditModal.vue'
|
package/src/lib/createApi.ts
CHANGED
|
@@ -7,6 +7,13 @@ export interface CreateApiOptions {
|
|
|
7
7
|
tokenKey: string
|
|
8
8
|
/** Se invoca cuando la API responde 401 (token inválido/expirado). */
|
|
9
9
|
onUnauthorized?: () => void
|
|
10
|
+
/**
|
|
11
|
+
* Locale ACTIVO de la interfaz (getter, se evalúa en cada petición): viaja
|
|
12
|
+
* como `?locale=` para que el SetLocale del servidor busque y ordene por el
|
|
13
|
+
* idioma que el usuario está viendo, no por el Accept-Language del
|
|
14
|
+
* navegador. Una petición con `locale` propio en params no se pisa.
|
|
15
|
+
*/
|
|
16
|
+
locale?: () => string | null | undefined
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
/**
|
|
@@ -24,6 +31,10 @@ export function createApi(options: CreateApiOptions): AxiosInstance {
|
|
|
24
31
|
if (token) {
|
|
25
32
|
config.headers.Authorization = `Bearer ${token}`
|
|
26
33
|
}
|
|
34
|
+
const locale = options.locale?.()
|
|
35
|
+
if (locale && !(config.params && 'locale' in config.params)) {
|
|
36
|
+
config.params = { ...config.params, locale }
|
|
37
|
+
}
|
|
27
38
|
return config
|
|
28
39
|
})
|
|
29
40
|
|