@studiolxd/brand 49.3.0 → 49.4.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,93 @@ 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.4.0] — 2026-09-17
11
+
12
+ > **Minor.** Cuatro huecos que dejó al descubierto la campaña de crudos de bricks: un bocadillo
13
+ > sobre un control apagado, la ranura de acciones de una tarjeta-enlace, dónde aterriza lo que
14
+ > calcula un motor de arrastre, y el HTML que no viene de React.
15
+
16
+ ### `Tooltip disabledTrigger` — el motivo de un control deshabilitado
17
+
18
+ Un control apagado es el que más necesita explicarse, y es justo el que no puede: un
19
+ `button[disabled]` no recibe eventos de puntero ni foco, así que no dispara nada. Con
20
+ `disabledTrigger`, el bocadillo se dispara desde un envoltorio focusable
21
+ (`span.tooltip__trigger`, `tabIndex={0}`) que pone el propio componente, y el CSS apaga los
22
+ eventos de puntero del hijo deshabilitado para que el hover **sobre el botón** llegue al
23
+ envoltorio. Eso segundo es lo que falta siempre que esto se monta a mano —el `<span tabIndex={0}>`
24
+ casero abre con el teclado pero no con el ratón—, y era el caso de `design-matrix-studio` en
25
+ bricks. El control sigue deshabilitado de verdad; lo único que cambia es quién dispara.
26
+
27
+ Se descartó la otra forma, un `Button disabledReason`: ataría el átomo más usado del sistema a
28
+ `Tooltip` y a su proveedor, lo obligaría a ser componente de cliente en apps que hoy lo pintan
29
+ desde el servidor, y no serviría para un `Select` o un `Switcher` apagados.
30
+
31
+ ### `Card linkOverlay` y `CardAction isolate` — la tarjeta que navega y lleva acciones
32
+
33
+ Envolver una tarjeta con menú en el `<a>` del router mete un `<button>` dentro de un `<a>`: no es
34
+ HTML válido, y el lector de pantalla lee un enlace cuyo nombre se traga el título, el estado, el
35
+ pie y la etiqueta del menú. La forma del sistema es `Card linkOverlay`: la tarjeta es contenedora,
36
+ el enlace es el del **título** y estira su área de pulsación a todo el bloque con una capa vacía;
37
+ la ranura de acciones sube por encima y se pulsa sola.
38
+
39
+ Y para lo que ya existe —una tarjeta envuelta en un enlace, una fila pulsable—, `CardAction`
40
+ **aísla su ranura por defecto** (`isolate`): detiene la propagación y, **solo cuando cuelga de un
41
+ enlace y solo para lo que se pulsa dentro de ella**, cancela la acción por defecto. Las dos cosas
42
+ hacen falta: detener la propagación no cancela la navegación, y acotar el `preventDefault` es lo
43
+ que evita romper un `type="submit"` de una tarjeta contenedora o el ítem de un `Menu`, que se
44
+ pinta en un portal fuera del enlace.
45
+
46
+ ### `Sortable` — dónde aterriza lo que calcula un motor de arrastre
47
+
48
+ El sistema no trae motor de arrastre y no va a traerlo. Lo que faltaba era dónde deja el motor lo
49
+ que calcula en cada fotograma, porque la respuesta de manual —`style={{ transform: CSS.Transform
50
+ .toString(transform) }}`— es justo lo que el sistema no puede emitir: una app con `style-src
51
+ 'self'` descarta el atributo `style` sin dejar ni una violación en consola.
52
+
53
+ `Sortable` recibe el desplazamiento como **dato** (`transform`, con la forma que devuelven dnd-kit
54
+ y compañía, para pasársela tal cual), lo escribe por el **CSSOM** en `--sortable-x`/`-y` y las dos
55
+ escalas, y publica el estado como atributo (`data-dragging`). El `transform` y la atenuación del
56
+ original (`--opacity-disabled`, sin token nuevo: el original queda fuera de juego) viven en la
57
+ hoja del sistema. Reenvía `ref`, que es lo que el motor necesita para registrar el nodo, y anula
58
+ la transición con `prefers-reduced-motion` —la dicta el motor en una cadena suya, así que no pasa
59
+ por los tokens de duración—.
60
+
61
+ ### `Prose html` — el HTML que no viene de React
62
+
63
+ `Prose` existía para vestir la semántica cruda de un documento, pero solo aceptaba `children`, así
64
+ que el HTML de un parser de `.docx`/`.pdf` acababa en un `dangerouslySetInnerHTML` sobre un
65
+ `<div>` desnudo —nueve sitios en una sola pantalla de bricks—, fuera de la escala, del ritmo y de
66
+ la medida de lectura. Ahora entra por `html`, **excluyente con `children` por tipo**. El DS **no
67
+ sanea**: la cadena llega ya limpia del consumidor, y por eso la prop es `string` y no un
68
+ `ReactNode`. Se descartó una pieza aparte (`RawHtml`) dentro de `Prose`: obligaría a escribir dos
69
+ componentes para una sola cosa y dejaría el HTML suelto sin vestir cuando se olvidara el de fuera.
70
+
71
+ ### Además
72
+
73
+ - Norma en Foundations › Opacidad: el original de un arrastre usa `--opacity-disabled`, que es el
74
+ mismo estado de «fuera de juego» del rol — no es un valor nuevo.
75
+ - `src/index.ts`, `scripts/entry-points.mjs` y `package.json#exports` registran `Sortable`
76
+ (componente de cliente).
77
+
78
+ ## [49.3.1] — 2026-09-17
79
+
80
+ > **Patch.** La v49.0.1 estrechó a `:where(.link)` el `inline-size: fit-content` que evita
81
+ > que un enlace suelto se estire dentro de un `Stack` — necesario para no encoger el `<a>`
82
+ > que es pieza de otro componente (`menu__item`, `card`…). Pero dejó fuera el `<a>` de un
83
+ > router de Server Component sin `class="link"` (el `Link` de next-intl, p. ej., que en RSC
84
+ > no admite `render` sobre el `Link` de brand sin romper el prerender): el «← Volver» sobre
85
+ > un `PageIntro` seguía necesitando un `Inline` envolvente para no estirar su subrayado por
86
+ > toda la página.
87
+
88
+ ### El arreglo
89
+
90
+ Nueva regla en `Link.css`, `:where(.stack > a:not([class]))`, aparte de `:where(.link)`:
91
+ solo alcanza al `<a>` que es hijo DIRECTO de un `Stack` y no lleva ninguna clase. El `<a>`
92
+ que es pieza de otro componente trae siempre su propia clase BEM (barrido en la v49.0.1),
93
+ así que queda fuera sin tocarlo. Story de contrato nueva (`tags: ['!dev']`) en
94
+ `Atoms/Stack` — «enlace suelto sin clase, hijo directo de un Stack, no se estira» — con el
95
+ caso del «← Volver» sobre un `PageIntro` dentro de un `Stack align="stretch"`.
96
+
10
97
  ## [49.3.0] — 2026-09-17
11
98
 
12
99
  > **Minor.** Norma de redacción cerrada para el estado vacío: `EmptyState.title` nunca
@@ -0,0 +1,66 @@
1
+ import './Sortable.css';
2
+ /** Elementos con los que puede montarse el envoltorio ordenable. */
3
+ export type SortableElement = 'div' | 'li';
4
+ /**
5
+ * Desplazamiento que la biblioteca de arrastre calcula en cada fotograma, en
6
+ * píxeles. Es la forma que devuelven los motores de arrastre al uso (dnd-kit
7
+ * entre ellos), así que se les pasa tal cual: el DS no depende de ninguno.
8
+ */
9
+ export interface SortableTransform {
10
+ x: number;
11
+ y: number;
12
+ scaleX?: number;
13
+ scaleY?: number;
14
+ }
15
+ export interface SortableProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'style'> {
16
+ /** Elemento del envoltorio. `div` por defecto; `li` dentro de una lista. */
17
+ as?: SortableElement;
18
+ /**
19
+ * Desplazamiento del elemento mientras se reordena la lista, tal y como lo
20
+ * devuelve el motor de arrastre. `null` (o ausente) es la posición de
21
+ * reposo. Se escribe por el CSSOM en `--sortable-x`/`--sortable-y` (y las
22
+ * dos escalas), que es lo que consume el `transform` de la hoja.
23
+ */
24
+ transform?: SortableTransform | null;
25
+ /**
26
+ * Transición que acompaña al desplazamiento, la que dicte el motor (`transform
27
+ * 200ms ease`). `null` o ausente: sin transición. Con
28
+ * `prefers-reduced-motion` la hoja la anula.
29
+ */
30
+ transition?: string | null;
31
+ /**
32
+ * El elemento es el que se está arrastrando ahora mismo. Pone
33
+ * `data-dragging` —de donde cuelga el CSS— y atenúa el original, que se
34
+ * queda como hueco mientras la copia viaja con el puntero.
35
+ */
36
+ dragging?: boolean;
37
+ }
38
+ /**
39
+ * Envoltorio de un elemento **ordenable por arrastre**.
40
+ *
41
+ * El sistema no trae motor de arrastre y no va a traerlo: quien lo pone es la
42
+ * aplicación (dnd-kit, en la suite). Lo que faltaba era dónde aterriza lo que
43
+ * ese motor calcula en tiempo de ejecución, porque la respuesta evidente —un
44
+ * `style` inline con el `transform`— es justo lo que el sistema no puede
45
+ * emitir: una app servida con `style-src 'self'` descarta el atributo `style`
46
+ * sin dejar ni una violación en consola, y el arrastre se queda quieto sin
47
+ * que nadie sepa por qué.
48
+ *
49
+ * `Sortable` es ese aterrizaje: recibe el desplazamiento como **dato**, lo
50
+ * escribe por el CSSOM (que la CSP sí permite) en las variables que consume su
51
+ * hoja, y publica el estado como atributo (`data-dragging`). El aspecto —qué
52
+ * `transform` se aplica, cuánto se atenúa el original— vive en el CSS del
53
+ * sistema, no en la app.
54
+ *
55
+ * Reenvía `ref`: es lo que necesita el motor para medir y registrar el nodo
56
+ * (`setNodeRef` de dnd-kit).
57
+ *
58
+ * ```tsx
59
+ * const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
60
+ *
61
+ * <Sortable ref={setNodeRef} transform={transform} transition={transition} dragging={isDragging}>
62
+ * <Card>…</Card>
63
+ * </Sortable>
64
+ * ```
65
+ */
66
+ export declare const Sortable: import("react").ForwardRefExoticComponent<SortableProps & import("react").RefAttributes<HTMLElement>>;
@@ -41,6 +41,23 @@ export interface TooltipProps extends Omit<React.HTMLAttributes<HTMLElement>, 'c
41
41
  * @default true
42
42
  */
43
43
  describe?: boolean;
44
+ /**
45
+ * El disparador está **deshabilitado** y el bocadillo es justo lo que
46
+ * explica por qué. Un `button[disabled]` no sirve de disparador: el
47
+ * navegador no le manda eventos de puntero ni lo deja recibir foco, así que
48
+ * el bocadillo no se abre ni con el ratón ni con el teclado — y quien no
49
+ * puede pulsar es precisamente quien necesita leer el motivo.
50
+ *
51
+ * Con `disabledTrigger` el bocadillo se dispara desde un envoltorio
52
+ * focusable (`span.tooltip__trigger`, `tabIndex={0}`) que pone el propio
53
+ * componente: es él quien recibe hover, foco y el `aria-describedby`,
54
+ * mientras el control de dentro sigue deshabilitado de verdad. El CSS
55
+ * apaga los eventos de puntero del hijo deshabilitado para que el hover
56
+ * sobre el botón llegue al envoltorio en vez de perderse.
57
+ *
58
+ * @default false
59
+ */
60
+ disabledTrigger?: boolean;
44
61
  /**
45
62
  * Nodo DOM donde montar el portal. Por defecto, el nodo de la superficie que
46
63
  * llegue por contexto —`SiteShell` publica el suyo, para que la capa herede
@@ -67,6 +67,23 @@ export interface CardProps extends Omit<React.ComponentPropsWithoutRef<'div'>, '
67
67
  color?: CardColor;
68
68
  /** Maqueta de la tarjeta. Default: `'default'`. */
69
69
  variant?: CardVariant;
70
+ /**
71
+ * Modo contenedor: el enlace del **título** cubre toda la tarjeta. La
72
+ * tarjeta sigue siendo un `<div>` con contenido interactivo dentro —un menú
73
+ * en `CardAction`, un botón en el pie—, y quien navega es el `<a>` del
74
+ * título, que estira su área de pulsación a todo el bloque con una capa
75
+ * vacía (`::after`).
76
+ *
77
+ * Es la forma correcta de una «tarjeta que es enlace **y** lleva acciones»:
78
+ * meter un `<button>` dentro de un `<a>` —envolviendo la tarjeta en el
79
+ * `Link` del router— no es HTML válido, y un lector de pantalla anuncia un
80
+ * enlace cuyo nombre se come el título, el estado, el pie y la etiqueta del
81
+ * menú de una sentada. Con `linkOverlay` cada control conserva su papel: un
82
+ * enlace con el nombre del título, y un botón de menú aparte.
83
+ *
84
+ * La ranura de acciones queda por encima de la capa, así que se pulsa sola.
85
+ */
86
+ linkOverlay?: boolean;
70
87
  /**
71
88
  * Modo contenedor: la tarjeta entera es la opción de un grupo. Dentro va
72
89
  * un `RadioField` (o `CheckboxField`) del DS, que la tarjeta extiende a todo
@@ -136,8 +153,38 @@ export interface CardDescriptionProps extends Omit<ParagraphProps, 'children'> {
136
153
  * hereda el cuerpo de la superficie en la que viva la tarjeta.
137
154
  */
138
155
  export declare const CardDescription: import("react").ForwardRefExoticComponent<CardDescriptionProps & import("react").RefAttributes<HTMLParagraphElement>>;
139
- /** Acción alineada al extremo de la cabecera (menú, botón…). */
140
- export declare const CardAction: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
156
+ export interface CardActionProps extends CardPartProps {
157
+ /**
158
+ * La ranura **aísla sus eventos** de la tarjeta que la contiene, que es lo
159
+ * que se quiere siempre que la tarjeta sea a la vez un enlace (`Card href`,
160
+ * `Card render`, o un `<Link>` del router envolviéndola): pulsar el menú de
161
+ * la cabecera abre el menú, no navega.
162
+ *
163
+ * Hace dos cosas, y las dos hacen falta:
164
+ *
165
+ * - `stopPropagation`, para que el clic no llegue a un manejador que la
166
+ * tarjeta o la fila tengan puesto más arriba;
167
+ * - `preventDefault` **solo cuando la ranura cuelga de un enlace** —se
168
+ * comprueba en el DOM, buscando un `a[href]` por encima del propio nodo—
169
+ * y **solo para lo que se pulsa dentro de ella**. Sin esto no basta:
170
+ * detener la propagación no cancela la acción por defecto del navegador,
171
+ * que sigue el enlace igual. Y acotarlo evita romper lo que sí depende de
172
+ * su acción por defecto: un `type="submit"` dentro de una tarjeta
173
+ * contenedora, o el ítem de un `Menu` que se pinta en un portal fuera del
174
+ * enlace.
175
+ *
176
+ * `false` para la tarjeta que quiere lo contrario: que pulsar la acción
177
+ * cuente también como pulsar la tarjeta.
178
+ *
179
+ * @default true
180
+ */
181
+ isolate?: boolean;
182
+ }
183
+ /**
184
+ * Acción alineada al extremo de la cabecera (menú, botón…). Por defecto aísla
185
+ * sus eventos de la tarjeta-enlace que la contiene: ver `isolate`.
186
+ */
187
+ export declare const CardAction: import("react").ForwardRefExoticComponent<CardActionProps & import("react").RefAttributes<HTMLDivElement>>;
141
188
  /** Cuerpo de la tarjeta. */
142
189
  export declare const CardContent: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
143
190
  export interface CardFooterProps extends CardPartProps {
@@ -2,7 +2,7 @@ import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
2
  import './Prose.css';
3
3
  /** Elementos con los que puede montarse el contenedor de texto largo. */
4
4
  export type ProseElement = 'div' | 'article' | 'section' | 'main' | 'aside';
5
- export interface ProseProps extends ComponentPropsWithoutRef<'div'> {
5
+ interface ProseBaseProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'dangerouslySetInnerHTML'> {
6
6
  /**
7
7
  * Elemento del contenedor. `div` por defecto; `article` cuando el bloque es
8
8
  * el documento en sí (una página de documentación, un texto legal).
@@ -16,8 +16,32 @@ export interface ProseProps extends ComponentPropsWithoutRef<'div'> {
16
16
  * anchas o figuras a sangre.
17
17
  */
18
18
  measure?: boolean;
19
- children: ReactNode;
20
19
  }
20
+ export type ProseProps = (ProseBaseProps & {
21
+ /** Contenido en nodos de React. Excluyente con `html`. */
22
+ children: ReactNode;
23
+ html?: never;
24
+ }) | (ProseBaseProps & {
25
+ /**
26
+ * Cadena de **HTML ya saneado**, para el contenido que no viene de React: lo
27
+ * que devuelve un parser de `.docx`/`.pdf`, un campo de un CMS, un markdown ya
28
+ * compilado. Excluyente con `children`.
29
+ *
30
+ * Es la misma excepción que ya justifica a `Prose` —el contenido no trae
31
+ * clases porque no lo escribió nadie del sistema— llevada a su forma real: sin
32
+ * esta prop, cada aplicación acababa poniendo su propio
33
+ * `dangerouslySetInnerHTML` en un `<div>` desnudo, fuera de la hoja del
34
+ * sistema, y ese texto salía sin escala, sin ritmo y sin medida de lectura.
35
+ *
36
+ * **El sistema no sanea.** No lleva sanitizador ni va a llevarlo: la política
37
+ * de lo que se permite depende de la aplicación y de dónde venga el HTML. Lo
38
+ * que llega aquí tiene que venir ya limpio del consumidor (`sanitize-html`,
39
+ * DOMPurify, lo que use), y por eso la prop se declara `string` y no un
40
+ * `ReactNode`: es una frontera, y se cruza a conciencia.
41
+ */
42
+ html: string;
43
+ children?: never;
44
+ });
21
45
  /**
22
46
  * Contenedor de contenido largo: viste la semántica cruda que sale de un
23
47
  * markdown o de un CMS (`h2`–`h6`, `p`, `ul`/`ol`, `blockquote`, `pre`/`code`,
@@ -30,7 +54,11 @@ export interface ProseProps extends ComponentPropsWithoutRef<'div'> {
30
54
  * siendo de componentes (el único otro caso, mucho menor, es el `<mark>` del
31
55
  * extracto de `DocsSearch`).
32
56
  *
57
+ * El contenido entra de una de dos formas, **nunca de las dos**: `children`
58
+ * (nodos de React) o `html` (una cadena ya saneada). Ver la prop `html`.
59
+ *
33
60
  * Reenvía el resto de props del elemento (`data-*`, `aria-*`, `id`…) y
34
61
  * concatena `className` tras las clases propias.
35
62
  */
36
63
  export declare const Prose: import("react").ForwardRefExoticComponent<ProseProps & import("react").RefAttributes<HTMLDivElement>>;
64
+ export {};
package/dist/app-shell.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import './app-shell.css';
3
3
  import { n as e } from "./_shared/brandmessagescontext.js";
4
4
  import { SkipLink as t } from "./skip-link.js";
5
- import { TooltipProvider as n } from "./tooltip.js";
6
- import { t as r } from "./_shared/css-properties.js";
5
+ import { t as n } from "./_shared/css-properties.js";
6
+ import { TooltipProvider as r } from "./tooltip.js";
7
7
  import { n as i, t as a } from "./_shared/appshellcontext.js";
8
8
  import { useCallback as o, useEffect as s, useMemo as c, useState as l } from "react";
9
9
  import { jsx as u, jsxs as d } from "react/jsx-runtime";
@@ -49,10 +49,10 @@ function m({ header: i, sidebar: f, children: m, defaultSidebar: h = "open", sid
49
49
  j,
50
50
  M,
51
51
  S
52
- ]), F = r({ "--app-shell-sidebar-width": D ? `${D}px` : void 0 }), I = !S && T;
52
+ ]), F = n({ "--app-shell-sidebar-width": D ? `${D}px` : void 0 }), I = !S && T;
53
53
  return /* @__PURE__ */ u(a.Provider, {
54
54
  value: P,
55
- children: /* @__PURE__ */ d(n, { children: [/* @__PURE__ */ u(t, {
55
+ children: /* @__PURE__ */ d(r, { children: [/* @__PURE__ */ u(t, {
56
56
  href: "#main-content",
57
57
  children: x("skipToContent", b)
58
58
  }), /* @__PURE__ */ d("div", {