@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edc-motor/ui",
3
- "version": "0.4.31",
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
  }
@@ -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
+ }
@@ -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