@edc-motor/ui 0.4.31 → 0.4.33
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/_chip.scss +11 -0
- package/scss/components/_index.scss +1 -0
- package/scss/components/_number-input.scss +48 -0
- 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.33",
|
|
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;
|
|
@@ -20,4 +20,15 @@
|
|
|
20
20
|
&.is-info { color: $info; border-color: color-mix(in srgb, $info 45%, transparent); }
|
|
21
21
|
&.is-missing { color: $warning; border-color: color-mix(in srgb, $warning 45%, transparent); }
|
|
22
22
|
&.is-failed { color: $danger; border-color: color-mix(in srgb, $danger 45%, transparent); }
|
|
23
|
+
|
|
24
|
+
// Chip de IDENTIDAD teñido por dato (p. ej. la facción de un juego): el
|
|
25
|
+
// color llega por --chip-tint (style en línea) y pasa a ser el FONDO; el
|
|
26
|
+
// texto elige blanco o negro por luminosidad real (truco lch, como los
|
|
27
|
+
// botones de bloque) — legible con cualquier color y en ambos temas, que
|
|
28
|
+
// era el problema del texto teñido sobre el fondo del chip de contorno.
|
|
29
|
+
&.is-tinted {
|
|
30
|
+
background: var(--chip-tint);
|
|
31
|
+
border-color: transparent;
|
|
32
|
+
@include contrast-text(var(--chip-tint));
|
|
33
|
+
}
|
|
23
34
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|