@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edc-motor/ui",
3
- "version": "0.4.30",
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í), subtítulo $fs-24 (entre el título y
43
- // la base), título $fs-32 (la cabecera sube a 40/28). Los títulos nunca
44
- // se justifican.
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-32; width: 100%; }
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-24; white-space: pre-line; width: 100%; }
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%;
@@ -21,6 +21,7 @@
21
21
  @use "translatable-input";
22
22
  @use "form-locale-switch";
23
23
  @use "image-upload";
24
+ @use "number-input";
24
25
  @use "font-upload";
25
26
  @use "toast";
26
27
  @use "toast-container";
@@ -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: h2 32 (el mismo tamaño que un título de bloque) ·
38
- // h3 24 · h4 22 (no hay token entre 20 y 24) · h5 20 · h6 18.
39
- h2 { font-size: $fs-32; }
40
- h3 { font-size: $fs-24; }
41
- h4 { font-size: 22px; }
42
- h5 { font-size: $fs-20; }
43
- h6 { font-size: $fs-18; }
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>
@@ -22,7 +22,7 @@ const props = withDefaults(
22
22
  nameDescLabel?: string
23
23
  }>(),
24
24
  {
25
- modelValue: 'latest',
25
+ modelValue: 'name',
26
26
  latestLabel: 'Más recientes primero',
27
27
  oldestLabel: 'Más antiguos primero',
28
28
  nameLabel: 'Alfabético (A-Z)',
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'
@@ -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