@studiolxd/brand 49.24.1 → 49.26.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/dist/_shared/tokens.js +23 -0
  3. package/dist/_types/atoms/Button/Button.d.ts +14 -0
  4. package/dist/_types/atoms/FileUpload/FileUpload.d.ts +22 -0
  5. package/dist/_types/molecules/LoadingRegion/LoadingRegion.d.ts +53 -0
  6. package/dist/_types/molecules/LoadingState/LoadingState.d.ts +50 -0
  7. package/dist/brand.css +41 -1
  8. package/dist/button.css +1 -1
  9. package/dist/button.js +57 -45
  10. package/dist/file-upload.css +1 -1
  11. package/dist/file-upload.js +129 -105
  12. package/dist/loading-region.css +1 -0
  13. package/dist/loading-region.js +61 -0
  14. package/dist/loading-state.css +1 -0
  15. package/dist/loading-state.js +52 -0
  16. package/dist/toaster.css +1 -1
  17. package/dist/tokens.css +45 -1
  18. package/package.json +11 -1
  19. package/src/tokens/components/button.css +2 -0
  20. package/src/tokens/index.css +2 -0
  21. package/src/tokens/molecules/loading-region.css +16 -0
  22. package/src/tokens/molecules/loading-region.ts +4 -0
  23. package/src/tokens/molecules/loading-state.css +17 -0
  24. package/src/tokens/molecules/loading-state.ts +4 -0
  25. package/src/tokens/scss/_index.legacy.scss +2 -0
  26. package/src/tokens/scss/_index.scss +2 -0
  27. package/src/tokens/scss/components/_button.scss +2 -0
  28. package/src/tokens/scss/molecules/_loading-region.scss +13 -0
  29. package/src/tokens/scss/molecules/_loading-state.scss +14 -0
  30. package/src/tokens/surface-dark-derived.css +2 -0
  31. package/src/tokens/surface-invert.css +2 -0
  32. package/src/tokens/surface-light.css +2 -0
  33. package/src/tokens/surface-public.css +1 -0
  34. package/src/tokens/tokens.json +23 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,81 @@ 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
+ ## [49.26.0] — 2026-10-05
11
+
12
+ > **Minor (49.26.0).** `LoadingState` puede pintar su texto en esperas largas con pasos. Sin cambios
13
+ > incompatibles: por defecto sigue mostrándose solo el girador.
14
+
15
+ ### `LoadingState` — `labelVisible`
16
+
17
+ - Prop nueva **`labelVisible`** (por defecto `false`): pinta el `label` bajo el girador, centrado, con el
18
+ estilo de la descripción de `EmptyState`. Para esperas largas con pasos («Procesando el documento:
19
+ paso 2 de 5…»); en esperas cortas no se usa.
20
+ - Es el mismo nodo que da el nombre accesible (no se duplica el anuncio). La caja lleva
21
+ `aria-live="polite"` y `aria-atomic="false"`: al cambiar el `label` se anuncia solo el texto nuevo.
22
+ - Sin `label` propio no pinta el texto genérico del catálogo: la espera sigue con el texto oculto (no es error).
23
+ - Tokens nuevos: `loading-state.label-font-family`, `label-font-size`, `sm-label-font-size`, `label-color`
24
+ (apuntan a los de la descripción de `EmptyState`; el par oscuro llega por el fichero derivado).
25
+
26
+ ### `FileUpload` / `FileUploadField` — estado de subida (`uploading`)
27
+
28
+ - Props nuevas: **`uploading`** (el girador va dentro de la zona; la zona no admite otro fichero —clic,
29
+ selector, soltar y quitar quedan inertes— pero conserva el foco: no es `disabled`; el control lleva
30
+ `aria-busy` y `aria-disabled`), **`uploadingLabel`** (texto de la espera; sin él, `spinner.label` del
31
+ catálogo, sin texto cableado nuevo) y **`uploadingLabelVisible`** (lo pinta en la zona, con el mismo criterio
32
+ que `LoadingState labelVisible`: sin `uploadingLabel` propio no pinta nada).
33
+ - La espera se anuncia con cortesía (`role="status"`, `aria-live="polite"`, `aria-atomic="false"`).
34
+ - Sin tokens nuevos. `FileUploadField` las hereda del átomo. Sin ficha de paridad nativa (el componente no la tiene).
35
+
36
+ ## [49.25.0] — 2026-10-05
37
+
38
+ > **Minor.** Las esperas pasan a ser piezas del sistema: las aplicaciones dejan de
39
+ > componerlas a mano (`LoadingRegion`/`SkeletonLines`/`SkeletonTable` de lmsmcp, `LoadingState`/
40
+ > `LoadingList`/`LoadingGrid` de creator, los botones con `Spinner` metido a mano). Criterio de la
41
+ > suite: esqueleto cuando se conoce la forma de lo que llega, girador cuando no o cuando se espera
42
+ > una acción, y la espera la anuncia el contenedor. Sin cambios incompatibles.
43
+
44
+ ### `LoadingState` — la espera de un bloque sin forma conocida (`@studiolxd/brand/loading-state`)
45
+
46
+ - Un girador centrado en los dos ejes, en una caja que reserva alto: nunca un `Spinner` suelto.
47
+ - **Solo se ve el girador.** El texto de la espera es el nombre accesible de la caja (`role="status"`,
48
+ `aria-busy`, `aria-labelledby` al texto oculto) y no se pinta. `label` lo cambia; sin él sale de
49
+ `spinner.label` del `BrandMessagesProvider` (sin espacio de textos nuevo). Fuera del proveedor hay
50
+ que pasar `label`: sin él ni catálogo, el error explícito de siempre.
51
+ - Tallas `md` (página o zona) y `sm` (diálogo, hoja, popover); `action` opcional (p. ej. «Cancelar»),
52
+ con el mismo `EmptyStateAction` que `EmptyState`.
53
+ - El alto: en una zona con alto definido la ocupa entera; en un flujo sin alto reserva el mínimo de
54
+ la talla; con **`fill`**, a página completa, toma el alto visible bajo la cabecera del `AppShell` y
55
+ centra sin provocar desplazamiento (para el `loading.tsx` de una ruta).
56
+
57
+ ### `LoadingRegion` y los esqueletos compuestos (`@studiolxd/brand/loading-region`)
58
+
59
+ - `LoadingRegion` envuelve esqueletos y anuncia la espera (`role="status"`, `aria-busy`, texto oculto
60
+ de `label` o `spinner.label`). `announce={false}` la deja solo a la vista, con `aria-hidden`, cuando
61
+ ya anuncia otra región viva.
62
+ - `SkeletonText` (`lines`, la última más corta), `SkeletonList` (`rows`), `SkeletonTable` (`rows`,
63
+ más la cabecera) y `SkeletonGrid` (`columns` 2|3|4, `rows`). Decorativos, sin atributo `style`: el
64
+ alto de cada barra sale de su token.
65
+
66
+ ### `Button loading`
67
+
68
+ - El girador entra delante del texto, que se queda; mide `1em`, como el icono que acompaña al
69
+ texto. Con `iconOnly` sustituye al icono y el botón no cambia de ancho.
70
+ - Inactivo y dicho: `aria-busy="true"` + `aria-disabled="true"`, **sin** `disabled` nativo (no
71
+ pierde el foco); el clic y el envío del formulario se cortan en el manejador. Con `href` quita el
72
+ `href`, como `disabled`. Con `render={<Link />}` corta el clic normal; el `href` del `Link` sigue
73
+ ahí (clic con modificador). No cambia dónde se puede usar `render` (solo desde cliente).
74
+ - Nativo: `loading` entra en `excluded` de la ficha de paridad de `Button` hasta que una app nativa
75
+ lo pida.
76
+
77
+ ### Tokens nuevos
78
+
79
+ - `loading-state.*`: `min-block-size`, `sm-min-block-size`, `gap`, `padding-block`, `padding-inline`,
80
+ `sm-padding-block`, `spinner-color` (en cascada desde `empty-state.*`).
81
+ - `loading-region.*`: `gap`, `text-gap`, `text-last-line-width`, `list-gap`, `list-row-height`,
82
+ `table-gap`, `table-header-height`, `table-row-height`, `grid-gap`, `grid-item-height`.
83
+ - `button.loading-gap`, `button.loading-spinner-size`.
84
+
10
85
  ## [49.24.1] — 2026-10-04
11
86
 
12
87
  > **Patch.** `AppShell` y `ChatShell` ya no rompen la hidratación por debajo del punto de ruptura de
@@ -407,6 +407,8 @@ var e = {
407
407
  "--button-height": "2.5rem",
408
408
  "--button-sm-height": "2rem",
409
409
  "--button-lg-height": "3rem",
410
+ "--button-loading-gap": "0.5rem",
411
+ "--button-loading-spinner-size": "1em",
410
412
  "--button-padding-inline": "2rem",
411
413
  "--button-focus-ring-width": "2px",
412
414
  "--button-focus-ring-style": "solid",
@@ -1103,6 +1105,27 @@ var e = {
1103
1105
  "--link-accent-1-hover-color": "#baabff",
1104
1106
  "--link-accent-1-underline-width": "1px",
1105
1107
  "--link-accent-1-hover-underline-width": "0px",
1108
+ "--loading-region-gap": "1.5rem",
1109
+ "--loading-region-text-gap": "0.5rem",
1110
+ "--loading-region-text-last-line-width": "60%",
1111
+ "--loading-region-list-gap": "0.5rem",
1112
+ "--loading-region-list-row-height": "2.5rem",
1113
+ "--loading-region-table-gap": "0.5rem",
1114
+ "--loading-region-table-header-height": "2.5rem",
1115
+ "--loading-region-table-row-height": "2rem",
1116
+ "--loading-region-grid-gap": "1rem",
1117
+ "--loading-region-grid-item-height": "6rem",
1118
+ "--loading-state-min-block-size": "16rem",
1119
+ "--loading-state-sm-min-block-size": "8rem",
1120
+ "--loading-state-gap": "1rem",
1121
+ "--loading-state-padding-block": "3rem",
1122
+ "--loading-state-padding-inline": "1.5rem",
1123
+ "--loading-state-sm-padding-block": "1.5rem",
1124
+ "--loading-state-spinner-color": "#6b6b6b",
1125
+ "--loading-state-label-font-family": "\"Google Sans Flex\", system-ui, sans-serif",
1126
+ "--loading-state-label-font-size": "1rem",
1127
+ "--loading-state-sm-label-font-size": "0.875rem",
1128
+ "--loading-state-label-color": "#4a4a4a",
1106
1129
  "--logo-height-sm": "2rem",
1107
1130
  "--logo-height-md": "2.5rem",
1108
1131
  "--logo-height-lg": "3rem",
@@ -60,6 +60,20 @@ export interface ButtonBaseProps extends Omit<React.ComponentPropsWithoutRef<'bu
60
60
  */
61
61
  type?: 'button' | 'submit' | 'reset';
62
62
  onClick?: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
63
+ /**
64
+ * La acción está en marcha: el girador entra delante del texto (que se queda
65
+ * a la vista) y el botón queda inactivo y lo dice — `aria-busy="true"` y
66
+ * `aria-disabled="true"`. No usa `disabled` nativo a propósito: un botón que
67
+ * se deshabilita justo después del clic pierde el foco, y el lector de
68
+ * pantalla con él. El clic (y el envío del formulario, si es `submit`) se
69
+ * corta en el manejador.
70
+ *
71
+ * Con texto, el botón crece lo que ocupan el girador y su aire; con
72
+ * `iconOnly` el girador **sustituye** al icono y el botón no cambia de ancho.
73
+ * El resultado de la acción no lo anuncia el botón: lo anuncia quien lo
74
+ * enseña (el `Toast`, el error del campo).
75
+ */
76
+ loading?: boolean;
63
77
  /** Renders as <a> when provided */
64
78
  href?: string;
65
79
  /** Adds target="_blank" rel="noopener noreferrer" (solo con href) */
@@ -44,6 +44,28 @@ export interface FileUploadProps {
44
44
  defaultValue?: File[];
45
45
  onChange?: (files: File[]) => void;
46
46
  progress?: number;
47
+ /**
48
+ * La subida está en curso. El girador sustituye al icono **dentro de la
49
+ * zona**, que deja de admitir ficheros (clic, selector y soltar quedan
50
+ * inertes, también el aspa de quitar) pero **conserva el foco**: no es
51
+ * `disabled`, así que quien estaba en el campo no lo pierde. El control
52
+ * lleva `aria-busy` y `aria-disabled`, y la espera se anuncia con cortesía
53
+ * (`role="status"`). Es distinto de `progress`, que dibuja una barra con un
54
+ * porcentaje conocido: pueden convivir.
55
+ */
56
+ uploading?: boolean;
57
+ /**
58
+ * Texto de la subida en curso («Subiendo 2 de 5…»). **No se ve** salvo con
59
+ * `uploadingLabelVisible`: es lo que anuncia el lector de pantalla. **Sin
60
+ * default**: sin él, sale de `spinner.label` del `BrandMessagesProvider`.
61
+ */
62
+ uploadingLabel?: string;
63
+ /**
64
+ * Pinta `uploadingLabel` en la zona, bajo el girador, para subidas largas con
65
+ * progreso por pasos. **Sin `uploadingLabel` propio no pinta nada**: el
66
+ * texto genérico del catálogo no se ve nunca. Default `false`.
67
+ */
68
+ uploadingLabelVisible?: boolean;
47
69
  disabled?: boolean;
48
70
  error?: boolean;
49
71
  id?: string;
@@ -0,0 +1,53 @@
1
+ import './LoadingRegion.css';
2
+ export interface LoadingRegionProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'role'> {
3
+ /**
4
+ * Lo que se espera, con puntos suspensivos («Cargando bancos…»). Se anuncia,
5
+ * no se ve. **Sin default**: sin él, sale de `spinner.label` del
6
+ * `BrandMessagesProvider`. No se lee cuando `announce` es `false`.
7
+ */
8
+ label?: string;
9
+ /**
10
+ * `false` cuando la espera ya la anuncia una región viva que existe antes y
11
+ * después de cargar: la región queda solo a la vista (`aria-hidden`), para
12
+ * no anunciarla dos veces.
13
+ */
14
+ announce?: boolean;
15
+ /** Los esqueletos: `SkeletonText`, `SkeletonList`, `SkeletonTable`, `SkeletonGrid` o `Skeleton` sueltos. */
16
+ children: React.ReactNode;
17
+ }
18
+ /**
19
+ * La espera de un contenido cuya forma se conoce: reserva su sitio con
20
+ * esqueletos y la página no salta al llegar. El `Skeleton` es decorativo; quien
21
+ * anuncia es esta región, con `role="status"`, `aria-busy` y el texto de la
22
+ * espera oculto a la vista. Apila sus hijos con aire entre bloques.
23
+ */
24
+ export declare function LoadingRegion({ label, announce, children, className, ...rest }: LoadingRegionProps): import("react/jsx-runtime").JSX.Element;
25
+ export interface SkeletonTextProps {
26
+ /** Líneas del párrafo; la última sale más corta. */
27
+ lines?: number;
28
+ }
29
+ /** Un bloque de texto —un párrafo, una lista de definiciones— que aún no llegó. Decorativo. */
30
+ export declare function SkeletonText({ lines }: SkeletonTextProps): import("react/jsx-runtime").JSX.Element;
31
+ export interface SkeletonListProps {
32
+ /** Filas de la lista. */
33
+ rows?: number;
34
+ }
35
+ /** Una lista que aún no llegó: una barra por fila, del alto de un control. Decorativo. */
36
+ export declare function SkeletonList({ rows }: SkeletonListProps): import("react/jsx-runtime").JSX.Element;
37
+ export interface SkeletonTableProps {
38
+ /** Filas de cuerpo, sin contar la cabecera. */
39
+ rows?: number;
40
+ }
41
+ /**
42
+ * Una tabla que aún no llegó: la cabecera y sus filas. No calca el diseño —no
43
+ * hace falta—, solo ocupa un sitio parecido. Decorativo.
44
+ */
45
+ export declare function SkeletonTable({ rows }: SkeletonTableProps): import("react/jsx-runtime").JSX.Element;
46
+ export interface SkeletonGridProps {
47
+ /** Columnas de la rejilla. */
48
+ columns?: 2 | 3 | 4;
49
+ /** Filas de la rejilla: se pintan `columns × rows` celdas. */
50
+ rows?: number;
51
+ }
52
+ /** Una rejilla de tarjetas o miniaturas que aún no llegó. Decorativo. */
53
+ export declare function SkeletonGrid({ columns, rows }: SkeletonGridProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,50 @@
1
+ import type { EmptyStateAction } from '../EmptyState/EmptyState';
2
+ import './LoadingState.css';
3
+ export interface LoadingStateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'role' | 'children'> {
4
+ /**
5
+ * Lo que se espera, con puntos suspensivos («Cargando revisión…»: ver
6
+ * Foundations → Redacción). **No se ve**: es el nombre accesible de la
7
+ * espera y lo que anuncian los lectores de pantalla. **Sin default**: sin
8
+ * él, sale de `spinner.label` del `BrandMessagesProvider` — el mismo texto
9
+ * que el `Spinner`.
10
+ */
11
+ label?: string;
12
+ /**
13
+ * Pinta el `label` bajo el girador, centrado, con el texto atenuado de la
14
+ * descripción de `EmptyState`. Para esperas **largas con pasos** («Procesando
15
+ * el documento: paso 2 de 5…»), donde el texto informa de por dónde va. Es el
16
+ * mismo nodo que da el nombre accesible: no se anuncia dos veces, y cuando
17
+ * `label` cambia la región lo anuncia con cortesía (`polite`). **Sin `label`
18
+ * propio no pinta nada**: el texto genérico del catálogo («Cargando…») no se
19
+ * ve nunca. Por defecto `false`: en una espera corta el texto es ruido.
20
+ */
21
+ labelVisible?: boolean;
22
+ /**
23
+ * `md` para una página o una zona; `sm` para el cuerpo de un diálogo, una
24
+ * hoja, un popover o una barra lateral.
25
+ */
26
+ size?: 'sm' | 'md';
27
+ /**
28
+ * A página completa: la caja toma el alto visible bajo la cabecera del
29
+ * `AppShell` (la ventana menos el cromo del armazón y el relleno del
30
+ * contenido) y centra el girador en él, sin provocar desplazamiento. Para el
31
+ * `loading.tsx` de una ruta cuya forma no se conoce. Sin `fill`, la caja ya
32
+ * ocupa el alto de una zona que lo tenga definido y, si no, reserva el
33
+ * mínimo de su talla.
34
+ */
35
+ fill?: boolean;
36
+ /** Una salida mientras se espera (cancelar un proceso largo). */
37
+ action?: EmptyStateAction;
38
+ }
39
+ /**
40
+ * La espera de un bloque cuya forma no se conoce: un girador centrado en una
41
+ * caja que **reserva alto** y, si hace falta, una salida. A la vista solo está
42
+ * el girador; el texto de la espera es solo para los lectores de pantalla,
43
+ * salvo con `labelVisible` (esperas largas con pasos). Cuando la forma sí se conoce (tabla, lista, ficha), la respuesta es
44
+ * `LoadingRegion` con esqueletos.
45
+ *
46
+ * Anuncia: la caja es `role="status"` con `aria-busy`, y su nombre es el texto
47
+ * de la espera, oculto a la vista. El girador va decorativo, para no
48
+ * anunciarla dos veces.
49
+ */
50
+ export declare function LoadingState({ label, labelVisible, size, fill, action, className, ...rest }: LoadingStateProps): import("react/jsx-runtime").JSX.Element;