@studiolxd/brand 42.0.0 → 44.0.0

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/CHANGELOG.md CHANGED
@@ -7,6 +7,119 @@ El paquete sigue [semver](https://semver.org/lang/es/): **patch** para bug fixes
7
7
  regeneración de `dist`, **minor** para componentes/props/variantes/tokens nuevos, **major**
8
8
  para breaking changes.
9
9
 
10
+ ## [44.0.0] — 2026-09-15
11
+
12
+ > **Major.** Quinta familia al proveedor de textos: las subidas. `FileUpload`,
13
+ > `AvatarUpload` e `ImageCropDialog` dejan de traer castellano puesto, el peso de un
14
+ > archivo lo escribe el `locale`, y una lista de formatos lleva la conjunción de su idioma.
15
+
16
+ ### Las subidas leen del proveedor
17
+
18
+ Tres espacios nuevos en `BrandMessages`: **`fileUpload`**, **`imageCropDialog`** y
19
+ **`avatarUpload`**, veintidós claves. `CalendarPlanner` y `CalendarRoster` no estrenan
20
+ espacio: sus dos flechas leen `calendar.previousMonth` y `calendar.nextMonth`, que es
21
+ literalmente el mismo texto que el del `Calendar`. Mismo orden —**prop → proveedor →
22
+ error**— y misma naturaleza: incompatible **en tiempo de compilación**.
23
+
24
+ Un cambio incompatible más, de props: `AvatarUpload.cropTitle` pasa a **obligatorio**. El
25
+ diálogo no sabe qué se recorta —«Recorta tu foto», «Recorta tu logo»—, y eso es contenido de
26
+ la pantalla, no del sistema.
27
+
28
+ ### «2,5 MB» tiene dos mitades
29
+
30
+ `formatBytes` hacía `toFixed(1) + ' KB'`: punto decimal inglés cableado, en todos los
31
+ idiomas. Ahora el peso lo escribe `Intl.NumberFormat` con `style: 'unit'` en el `locale`
32
+ del componente, y al catálogo va solo la plantilla que lo envuelve («máx. {size}»). Con el
33
+ mismo catálogo inglés, `es-ES` escribe «max. 2,5 MB» y `en-US` «max. 2.5 MB». Es la regla
34
+ de la v43 aplicada a otra cosa: *si cambia al cambiar de idioma es cromo; si cambia al
35
+ cambiar de país es formato.*
36
+
37
+ Efecto de asumir CLDR: las unidades son las suyas —«kB» y no «KB»— y por debajo de un
38
+ kilobyte un `en-US` escribe «500 byte». La alternativa era volver a decidir a mano la
39
+ colocación del sufijo, que es justo lo que la regla prohíbe.
40
+
41
+ ### Dos textos que no se pueden traducir por separado
42
+
43
+ `AvatarUpload` tiene un botón con texto visible («Subir») y un nombre accesible («Subir el
44
+ logo»), y WCAG 2.5.3 exige que el segundo **contenga** al primero. Traducirlos en dos
45
+ sitios distintos es dejar que una traducción los despareje sin que falle nada; concatenarlos
46
+ en el componente es castellano disfrazado, porque el orden es del idioma —«Subir el logo»
47
+ lleva el verbo delante, «das Logo hochladen» lo lleva detrás—. Así que van los dos al
48
+ catálogo, `avatarUpload.button` y `avatarUpload.buttonFor(subject)`, con la regla escrita
49
+ al lado; la pantalla solo aporta el sujeto. El aviso de consola en desarrollo sigue
50
+ vigilando la contención.
51
+
52
+ ### Una lista lleva la conjunción de su idioma
53
+
54
+ Los formatos admitidos se unían con `join(', ')` y no ponían conjunción en ningún idioma:
55
+ «JPEG, PNG, WEBP». Ahora es `Intl.ListFormat` con `type: 'disjunction'`: «JPEG, PNG o
56
+ WEBP», «JPEG, PNG, or WEBP», «JPEG, PNG oder WEBP».
57
+
58
+ ### Lo que se queda como prop
59
+
60
+ `ImageCropDialog.title`, `cancelLabel` y `confirmLabel` eran ya obligatorias y sin default,
61
+ y son el modelo de toda la ola: el diálogo no sabe qué se recorta ni qué pasa al confirmar.
62
+ En `AvatarUpload`, en cambio, `cropCancel` y `cropConfirm` sí salen del catálogo, y no es
63
+ incoherencia: esa subida sí sabe lo que confirma —se guarda un recorte—, y vale igual en
64
+ todas las pantallas.
65
+
66
+ Quedan dos reenvíos puros a una pieza sin migrar: `ImageCropDialog.closeLabel` y
67
+ `AvatarUpload.cropCloseLabel` van al `Modal`, que aún trae su «Cerrar». Es la deuda que
68
+ esta ola deja apuntada, y es por donde sigue la siguiente.
69
+
70
+ ## [43.0.0] — 2026-09-15
71
+
72
+ > **Major.** Cuarta familia al proveedor de textos: fecha y hora. `Calendar`, `DatePicker`
73
+ > y `TimeSelect` dejan de traer castellano puesto, y con ellos se cierran los reenvíos que
74
+ > las olas anteriores dejaron inventariados.
75
+
76
+ ### Fecha y hora leen del proveedor
77
+
78
+ Tres espacios nuevos en `BrandMessages`: **`calendar`** (previousMonth, nextMonth,
79
+ previousYears, nextYears, yearGrid), **`datePicker`** (openCalendar, invalid, calendar y
80
+ las letras de la máscara) y **`timeSelect`** (hours, minutes y sus máscaras). Los campos
81
+ que los envuelven —`DatePickerField`, `DateTimeField`, `TimeField`— no pierden props: sus
82
+ reenvíos quedan como anulación puntual, pero ya no hay que enhebrar nada para traducir,
83
+ porque el proveedor llega por contexto a la pieza que pinta el texto. Se ha retirado
84
+ `SPANISH_MASK_LETTERS`, el último juego de letras castellanas vivo en el código.
85
+
86
+ Mismo orden que en las tres olas anteriores —**prop → proveedor → error**— y misma
87
+ naturaleza: incompatible **en tiempo de compilación**. El texto que no se pinta no se
88
+ exige: un `Calendar` sin navegación no pide las flechas, un `DatePicker` con fecha válida no
89
+ pide el aviso, un `TimeSelect` con hora no pide las máscaras.
90
+
91
+ ### Una máscara tiene dos mitades
92
+
93
+ `dd/mm/aaaa` no es una cosa, son dos decididas en sitios distintos. **El orden y el
94
+ separador son formato**: salen del `locale` vía `Intl.DateTimeFormat`, y una aplicación en
95
+ inglés que enseñe fechas españolas las quiere en orden español. **Las letras son idioma**:
96
+ `aaaa` es «año», `yyyy` es *year*, `jjjj` es *Jahr* — la abreviatura de una palabra, y se
97
+ traduce. Por eso el catálogo guarda las tres letras sueltas y nunca la máscara montada: la
98
+ arma el componente poniendo las letras en el orden del `locale`.
99
+
100
+ Lo mismo con `HH/MM`: parecen notación técnica, pero son la inicial de una palabra, y en
101
+ alemán son `SS/MM`. Lo que no se traduce es el dibujo —dos cifras, la hora antes que el
102
+ minuto, dos puntos en medio—.
103
+
104
+ La regla, en una frase, escrita en `Foundations › Internacionalización`: **si cambia al
105
+ cambiar de idioma es cromo; si cambia al cambiar de país es formato.**
106
+
107
+ ### Lo que se queda como prop
108
+
109
+ `label`, `helperText` y `errorMessage` de los tres campos; `gridLabel` del `Calendar`, que
110
+ nombra a ESE calendario («Fecha de alta») y sin el cual la rejilla toma el título del mes,
111
+ que ya viene del `locale`; `calendarLabel` cuando lo pone un campo, porque es su etiqueta;
112
+ y el `placeholder` del `DatePicker` cuando dice algo del campo («Desde»), que sustituye a la
113
+ máscara entera.
114
+
115
+ ### Pendiente, anotado
116
+
117
+ `CalendarPlanner` y `CalendarRoster` siguen con «Mes anterior» y «Mes siguiente» cableados:
118
+ es el mismo texto que `Calendar` y pueden leer de `calendar.*` tal cual. Y `TimeSelect` es
119
+ de 24 horas por construcción; el reloj de 12 con AM/PM es cosa del `locale`, no del idioma,
120
+ y hoy una aplicación en inglés americano ve las horas en formato militar. No es un texto,
121
+ y queda por decidir.
122
+
10
123
  ## [42.0.0] — 2026-09-15
11
124
 
12
125
  > **Major.** Tercera familia al proveedor de textos: los átomos y moléculas de formulario
@@ -1,16 +1,13 @@
1
1
  import '../datepicker.css';
2
- import { Icon as e } from "../icon.js";
3
- import { Input as t } from "../input.js";
4
- import { Popover as n } from "../popover.js";
5
- import { Calendar as r } from "../calendar.js";
6
- import { forwardRef as i, useCallback as a, useId as o, useMemo as s, useRef as c, useState as l } from "react";
7
- import { jsx as u, jsxs as d } from "react/jsx-runtime";
2
+ import { n as e } from "./brandmessagescontext.js";
3
+ import { Icon as t } from "../icon.js";
4
+ import { Input as n } from "../input.js";
5
+ import { Popover as r } from "../popover.js";
6
+ import { Calendar as i } from "../calendar.js";
7
+ import { forwardRef as a, useCallback as o, useId as s, useMemo as c, useRef as l, useState as u } from "react";
8
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
8
9
  //#region src/stories/molecules/DatePicker/dateMask.ts
9
- var f = {
10
- day: "dd",
11
- month: "mm",
12
- year: "aaaa"
13
- }, p = /[‎‏؜]/g, m = new Date(2026, 8, 25);
10
+ var p = /[‎‏؜]/g, m = new Date(2026, 8, 25);
14
11
  function h(e, t) {
15
12
  return String(e).padStart(t, "0");
16
13
  }
@@ -60,110 +57,110 @@ function g(e) {
60
57
  function _(e) {
61
58
  return `${String(e.getFullYear()).padStart(4, "0")}-${String(e.getMonth() + 1).padStart(2, "0")}-${String(e.getDate()).padStart(2, "0")}`;
62
59
  }
63
- var v = i(function({ value: i, onChange: p, placeholder: m, maskLetters: h = f, invalidMessage: v = "Escribe una fecha completa, con el día, el mes y el año.", openCalendarLabel: y = "Abrir calendario", minDate: b, maxDate: ee, disabledDates: x, size: S = "md", disabled: C, readOnly: w, error: T = !1, locale: E = "es-ES", id: D, name: O, describedBy: k, "aria-describedby": te, "aria-label": ne, calendarLabel: A = "Calendario", previousMonthLabel: re, nextMonthLabel: j, previousYearsLabel: M, nextYearsLabel: N, yearGridLabel: P, gridLabel: F, onBlur: I, className: L }, R) {
64
- let [z, B] = l(!1), V = s(() => g(E), [E]), H = i instanceof Date ? V.format(i) : "", [U, W] = l(H), [G, K] = l(H);
65
- H !== G && (K(H), W(H));
66
- let q = c(null), J = a((e) => {
67
- q.current = e, typeof R == "function" ? R(e) : R && (R.current = e);
68
- }, [R]), Y = U.trim(), X = Y ? V.parse(U) : null, Z = Y !== "" && !X, ie = T || Z, Q = `${o()}-date-picker-invalid`, ae = [k ?? te, Z ? Q : void 0].filter(Boolean).join(" ") || void 0, oe = a((e) => {
69
- (w || C) && e || B(e);
70
- }, [C, w]), $ = a((e) => {
60
+ var v = a(function({ value: a, onChange: p, placeholder: m, maskLetters: h, invalidMessage: v, openCalendarLabel: y, minDate: b, maxDate: x, disabledDates: S, size: C = "md", disabled: w, readOnly: T, error: E = !1, locale: D = "es-ES", id: O, name: k, describedBy: ee, "aria-describedby": te, "aria-label": ne, calendarLabel: re, previousMonthLabel: ie, nextMonthLabel: ae, previousYearsLabel: A, nextYearsLabel: j, yearGridLabel: M, gridLabel: N, onBlur: P, className: F }, I) {
61
+ let L = e("datePicker"), [R, z] = u(!1), B = c(() => g(D), [D]), V = a instanceof Date ? B.format(a) : "", [H, U] = u(V), [W, G] = u(V);
62
+ V !== W && (G(V), U(V));
63
+ let K = l(null), q = o((e) => {
64
+ K.current = e, typeof I == "function" ? I(e) : I && (I.current = e);
65
+ }, [I]), J = H.trim(), Y = J ? B.parse(H) : null, X = J !== "" && !Y, oe = E || X, Z = `${s()}-date-picker-invalid`, se = [ee ?? te, X ? Z : void 0].filter(Boolean).join(" ") || void 0, ce = o((e) => {
66
+ (T || w) && e || z(e);
67
+ }, [w, T]), le = o((e) => {
71
68
  let t = e.target.value;
72
- if (W(t), t.trim() === "") {
69
+ if (U(t), t.trim() === "") {
73
70
  p?.(null);
74
71
  return;
75
72
  }
76
- let n = V.parse(t);
73
+ let n = B.parse(t);
77
74
  n && p?.(n);
78
- }, [V, p]), se = a((e) => {
79
- if (e.key === "ArrowDown" && !w && !C) {
80
- e.preventDefault(), B(!0);
75
+ }, [B, p]), ue = o((e) => {
76
+ if (e.key === "ArrowDown" && !T && !w) {
77
+ e.preventDefault(), z(!0);
81
78
  return;
82
79
  }
83
- e.key === "Escape" && z && (e.preventDefault(), B(!1));
80
+ e.key === "Escape" && R && (e.preventDefault(), z(!1));
84
81
  }, [
85
- C,
86
- z,
87
- w
88
- ]), ce = a((e) => {
89
- W(V.format(e)), p?.(e), B(!1), requestAnimationFrame(() => q.current?.focus());
90
- }, [V, p]), le = /* @__PURE__ */ u("button", {
82
+ w,
83
+ R,
84
+ T
85
+ ]), Q = o((e) => {
86
+ U(B.format(e)), p?.(e), z(!1), requestAnimationFrame(() => K.current?.focus());
87
+ }, [B, p]), de = /* @__PURE__ */ d("button", {
91
88
  type: "button",
92
89
  className: "date-picker__button",
93
- "aria-label": y,
90
+ "aria-label": L("openCalendar", y),
94
91
  "aria-haspopup": "dialog",
95
- "aria-expanded": z,
96
- disabled: C,
97
- tabIndex: w ? -1 : void 0,
98
- children: /* @__PURE__ */ u(e, {
92
+ "aria-expanded": R,
93
+ disabled: w,
94
+ tabIndex: T ? -1 : void 0,
95
+ children: /* @__PURE__ */ d(t, {
99
96
  name: "calendar",
100
97
  size: "sm",
101
98
  className: "date-picker__glyph"
102
99
  })
103
- });
104
- return /* @__PURE__ */ d("div", {
100
+ }), $ = L("calendar", re);
101
+ return /* @__PURE__ */ f("div", {
105
102
  className: [
106
103
  "date-picker",
107
- S === "md" ? "" : `date-picker--${S}`,
108
- L ?? ""
104
+ C === "md" ? "" : `date-picker--${C}`,
105
+ F ?? ""
109
106
  ].filter(Boolean).join(" "),
110
107
  children: [
111
- O && /* @__PURE__ */ u("input", {
108
+ k && /* @__PURE__ */ d("input", {
112
109
  type: "hidden",
113
- name: O,
114
- value: i instanceof Date ? _(i) : ""
110
+ name: k,
111
+ value: a instanceof Date ? _(a) : ""
115
112
  }),
116
- /* @__PURE__ */ d("div", {
113
+ /* @__PURE__ */ f("div", {
117
114
  className: "date-picker__control",
118
- children: [/* @__PURE__ */ u(t, {
119
- ref: J,
120
- id: D,
115
+ children: [/* @__PURE__ */ d(n, {
116
+ ref: q,
117
+ id: O,
121
118
  className: "date-picker__input",
122
119
  type: "text",
123
120
  inputMode: "numeric",
124
121
  autoComplete: "off",
125
- size: S,
126
- error: ie,
127
- value: U,
128
- placeholder: m ?? V.mask(h),
129
- disabled: C,
130
- readOnly: w,
122
+ size: C,
123
+ error: oe,
124
+ value: H,
125
+ placeholder: m ?? B.mask(L("maskLetters", h)),
126
+ disabled: w,
127
+ readOnly: T,
131
128
  "aria-label": ne,
132
- "aria-describedby": ae,
133
- onChange: $,
134
- onKeyDown: se,
135
- onBlur: I
136
- }), /* @__PURE__ */ u(n, {
137
- trigger: le,
138
- label: A,
139
- open: z,
140
- onOpenChange: oe,
129
+ "aria-describedby": se,
130
+ onChange: le,
131
+ onKeyDown: ue,
132
+ onBlur: P
133
+ }), /* @__PURE__ */ d(r, {
134
+ trigger: de,
135
+ label: $,
136
+ open: R,
137
+ onOpenChange: ce,
141
138
  side: "bottom",
142
139
  align: "end",
143
140
  sideOffset: -1,
144
141
  className: "date-picker__popover",
145
- children: /* @__PURE__ */ u(r, {
146
- value: X ?? i ?? null,
147
- onChange: ce,
148
- gridLabel: F ?? A,
149
- previousMonthLabel: re,
150
- nextMonthLabel: j,
151
- previousYearsLabel: M,
152
- nextYearsLabel: N,
153
- yearGridLabel: P,
142
+ children: /* @__PURE__ */ d(i, {
143
+ value: Y ?? a ?? null,
144
+ onChange: Q,
145
+ gridLabel: N ?? $,
146
+ previousMonthLabel: ie,
147
+ nextMonthLabel: ae,
148
+ previousYearsLabel: A,
149
+ nextYearsLabel: j,
150
+ yearGridLabel: M,
154
151
  minDate: b,
155
- maxDate: ee,
156
- disabledDates: x,
157
- locale: E,
158
- size: S
152
+ maxDate: x,
153
+ disabledDates: S,
154
+ locale: D,
155
+ size: C
159
156
  })
160
157
  })]
161
158
  }),
162
- Z && /* @__PURE__ */ u("span", {
163
- id: Q,
159
+ X && /* @__PURE__ */ d("span", {
160
+ id: Z,
164
161
  className: "date-picker__message",
165
162
  role: "alert",
166
- children: v
163
+ children: L("invalid", v)
167
164
  })
168
165
  ]
169
166
  });