@studiolxd/brand 49.17.0 → 49.19.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,155 @@ 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.19.0] — 2026-10-04
11
+
12
+ > **Minor.** Primera versión con **Brand nativo**: tokens, fuentes y componentes para SwiftUI (iOS 17 y macOS 14) y
13
+ > Jetpack Compose (Android), fuera del paquete npm. En React, opciones de fila en `List`/`ListItem`
14
+ > (`showSeparators`, `secondary`, `trailing`, `leading`), `NumberInput` sin valor y el aviso neutro de `Toast` en
15
+ > oscuro. Todo opcional: sin las props nuevas el HTML no cambia.
16
+
17
+ ### React — huecos de la primera app nativa (minor)
18
+
19
+ Todo opcional: sin las props nuevas el HTML es idéntico al anterior (pruebas con `toMatchInlineSnapshot` generadas
20
+ contra el código de antes, en `InputField.test.tsx`, `NumberInputField.test.tsx`, `PasswordField.test.tsx` y
21
+ `List.test.tsx`).
22
+
23
+ - **`ListItem` `leading`** (`ReactNode`): accesorio al principio de la fila (icono, avatar), pareja de `trailing`.
24
+ Clase nueva `list__item-leading`; story «Filas con accesorio inicial».
25
+ - **`NumberInput` / `NumberInputField` sin valor**: `value` y `defaultValue` admiten `null` (campo vacío), y la prop
26
+ nueva `onEmpty` hace que vaciar el texto sea un valor (sin ella, el campo se comporta como siempre y recupera el
27
+ último número al salir). Desde vacío, − y + cuentan como si el campo valiera 0 y se ajustan a `min`/`max`.
28
+ Story «Sin valor».
29
+ - **Foco desde fuera**: el `ref` ya llegaba al `<input>` en `InputField` (también en búsqueda), `NumberInputField` y
30
+ `PasswordField` (y al botón en `SelectField`); ahora lo fija una prueba y lo documenta `InputField.mdx`.
31
+ - **Documentación de `Button`**: la nota de `destructive` decía que valía para `primary` y `ghost` y mencionaba una
32
+ variante `accent` que ya no existe; en el código solo `outline` y `text` tienen estilo destructivo. Corregida la
33
+ nota (y `destructiveIntent` → `destructive`); el comportamiento no cambia.
34
+
35
+ ### Nativo — huecos de la primera app nativa (minor)
36
+
37
+ - **Generador de Kotlin**: los tamaños de letra de componente cuyo nombre no termina en `font-size`
38
+ (`empty-state.title-font-size-sm`, `description-font-size-sm`) salían como `Dp`; ahora todo token con `font-size`
39
+ como segmento es `TextUnit` (`sp`). `BrandEmptyState` deja de convertir a mano. Grupo `password-field` en
40
+ `NATIVE_COMPONENT_GROUPS`.
41
+ - **`PasswordField` nativo** (`BrandPasswordField`, SwiftUI y Compose): campo con ojo de mostrar u ocultar, etiqueta
42
+ oculta por defecto como en React, textos accesibles como parámetros. Ficha, capturas y parejas. **Compose: `BrandInputField`
43
+ pierde el ojo** (`passwordToggle`, `showPasswordLabel`, `hidePasswordLabel`; breaking menor en Compose): quien lo quiera usa `BrandPasswordField`.
44
+ - **Foco desde fuera** en `InputField`, `NumberInputField` y `PasswordField`: SwiftUI `.brandFocused($focus)` /
45
+ `.brandFocused($focus, equals:)` (un `FocusState` de la app aplicado al texto, no a los botones); Compose
46
+ `focusRequester: FocusRequester?`.
47
+ - **`NumberInputField` sin valor**: `Binding<Double?>` (SwiftUI; `Binding<Double>` sigue compilando) y `value: Double?`
48
+ (Compose; **cambio de firma**: `onValueChange` recibe `Double?`). Vacío = `nil`/`null`, con `placeholder` y
49
+ «Sin valor» (`emptyValueLabel`) para lectores de pantalla; desde vacío − y + parten de 0 y respetan `min`/`max`.
50
+ - **`BrandListItem`**: `leading` en SwiftUI (ya existía en Compose) y `action` (fila pulsable entera) en SwiftUI, pareja de
51
+ `onClick` de Compose, anotado en la ficha como diferencia deliberada.
52
+
53
+ ### Nativo — Jetpack Compose con los mismos componentes que SwiftUI (minor)
54
+
55
+ - **Tokens de componente en Kotlin**: `pnpm build:tokens` genera `BrandComponentTokens.kt` (mismos grupos y reglas que
56
+ `BrandComponentTokens.swift`; los colores y las medidas con par `surface-dark-*` salen como `BrandSchemeValue`, que se
57
+ resuelve con `.current`). El Swift generado no cambia.
58
+ - **Componentes de Compose** (`native/android/brand`): `BrandButton`, `BrandCloseButton`, `BrandHeading`/`BrandParagraph`/
59
+ `BrandText`, `BrandIcon` (77 iconos; `pnpm build:native-icons` genera también `BrandIconData.kt`), `BrandInputField`,
60
+ `BrandNumberInputField`, `BrandSelectField`, `BrandSwitcherField`, `BrandToggleGroup`, `BrandThemeSwitcher`,
61
+ `BrandList`/`BrandListItem` (`showSeparators`, `secondary`, `trailing`), `BrandTag`, `BrandEmptyState`, `BrandSkeleton`,
62
+ `BrandSheet`, `BrandConfirmDialog` y `ToastCenter`/`ToastHost`, con estados, escala de fuente, TalkBack, 48 dp, claro y
63
+ oscuro y «quitar animaciones». Cumplen las fichas de paridad (`ParityTest`), con capturas Paparazzi en claro y oscuro,
64
+ `@Preview` y parejas React ↔ Compose en `native/android/Comparisons/`. Sin dependencias nuevas en producción.
65
+ - Fichas de paridad: líneas «Android: …» en `differences`. `native/README.md` documenta los componentes de Compose.
66
+
67
+ ### React — `List`/`ListItem` (minor) y `Toast` (patch)
68
+
69
+ - **`List` `showSeparators`** (default `false`): línea entre filas con los tokens `separator.*`
70
+ (clase nueva `list--separated`). **`ListItem` `secondary` y `trailing`** (`ReactNode`): línea
71
+ menor atenuada y accesorio al final de la fila. Sin las props nuevas el HTML es exactamente el
72
+ de antes; la estructura de fila (`list__item-row`, `list__item-main`, `list__item-secondary`,
73
+ `list__item-trailing`) solo aparece al usarlas. Tokens nuevos en `text.list.*`
74
+ (`item-gap`, `secondary-*`, `separator-*`, con pares `surface-dark-*`). Prueba que fija el HTML
75
+ anterior en `List.test.tsx`.
76
+ - **Fix `Toast` neutro en oscuro**: el neutro (y `info`/`loading`) llevaba `.surface-dark` en la
77
+ raíz, que volteaba su relleno a blanco incluso sobre página clara y dejaba el aspa en blanco
78
+ sobre blanco. Ahora, como el `Alert`, la raíz solo la lleva `success`/`error`, y el interior
79
+ (contenido y aspa) lleva `.surface-invert` (neutro) o `.surface-light` (`warning`).
80
+
81
+ ### Nativo
82
+
83
+ - `BrandList(showsSeparators:)` pasa a **`showSeparators:`** (mismo nombre que en React; lo nativo
84
+ aún no se había publicado). `BrandListItem` `secondary`/`trailing` y los separadores leen ahora
85
+ los tokens `text.list.*`. Fichas `List.json`/`ListItem.json` actualizadas: ya no son «solo nativo».
86
+
87
+ > Infraestructura nativa (iOS, macOS y Android). **No cambia nada del paquete npm**: lo
88
+ > nativo vive en `native/`, `Package.swift` y `jitpack.yml`, fuera de `package.json#files`.
89
+
90
+ ### Nativo — nuevo (`native/`)
91
+
92
+ - **Tokens nativos con Style Dictionary**: dos plataformas nuevas en `sd.config.mjs`
93
+ (`swift`, `kotlin`) con formatos propios en `sd.formats.mjs`. `pnpm build:tokens`
94
+ genera `native/apple/…/Tokens/BrandTokens.swift` y
95
+ `native/android/…/tokens/BrandTokens.kt`: colores y roles claro/oscuro (`Color` dinámico
96
+ en Swift; `BrandColorRoles.light|dark` + `CompositionLocal` en Compose), espaciado,
97
+ radios, anchos de borde, tallas, opacidad, tipografía, sombras y motion. Los tokens de
98
+ componente quedan para cuando se porte cada componente.
99
+ - **Paquete SwiftPM** `StudiolxdBrand` (`Package.swift` en la raíz; iOS 17, macOS 14,
100
+ Swift 6): `registerFonts()`, `BrandTextStyle` y `Font.brand(_:)`.
101
+ - **Módulo Gradle** `com.github.studiolxd:brand` (`native/android`, minSdk 26, publicable
102
+ por JitPack con `jitpack.yml`): `BrandTheme`, `BrandFontFamily`, `BrandTypography`.
103
+ - **Fuentes**: `pnpm build:native-fonts` descomprime los woff2 variables (Google Sans Flex,
104
+ Google Sans Code, Libre Bodoni; cara `latin`) a TTF para ambas plataformas.
105
+ - **Paridad con React**: fichas por componente (`native/parity/`), validador
106
+ (`pnpm native:parity`, con TypeScript y `ajv`) y pruebas de paridad en Swift y Kotlin.
107
+ - **Capturas**: swift-snapshot-testing (Apple) y Paparazzi (Android), con una captura de
108
+ ejemplo de los roles de color en claro y oscuro.
109
+ - **`release:check`** comprueba que `npm pack` no incluye nada nativo, que los tokens
110
+ nativos están al día y la paridad; con `--with-native` compila y prueba Swift (macOS e
111
+ iOS) y Gradle.
112
+ - Documentación en `native/README.md` y en `CLAUDE.md` § «Nativo (iOS y Android)».
113
+
114
+ ### Nativo — componentes de SwiftUI (iOS 17 y macOS 14)
115
+
116
+ > Los primeros componentes nativos, los que pide Homenize. Mismos nombres de props y casos que React, solo tokens,
117
+ > claro y oscuro con colores dinámicos, tipo dinámico, VoiceOver, zona táctil de 44 pt, puntero y foco de teclado en
118
+ > macOS y «reducir movimiento». Detalle y ejemplos en `native/README.md` § «Componentes de SwiftUI». Compose recibirá los
119
+ > mismos componentes en la tarea siguiente.
120
+
121
+ - **15 componentes** portados, cada uno con su ficha de paridad (`native/parity/components/`), capturas en claro y
122
+ oscuro (iOS y macOS), `#Preview` con todas las variantes y la pareja con su story de Storybook
123
+ (`native/apple/Comparisons/<Componente>/`): `Button` (como `ButtonStyle` + `BrandButton`), `Heading`/`Paragraph`/`Text`,
124
+ `Icon`, `InputField`, `NumberInputField`, `SelectField`, `SwitcherField`, `ToggleGroup`, `ThemeSwitcher`, `List` +
125
+ `ListItem`, `Tag`, `EmptyState`, `Skeleton`, `Sheet` (`.sheet` nativo), `ConfirmDialog` y `Toast`/`Toaster`
126
+ (`ToastCenter` + `toastHost`).
127
+ - **Tokens de componente para Swift**: `pnpm build:tokens` genera también `BrandComponentTokens.swift` (un enum por
128
+ grupo de `NATIVE_COMPONENT_GROUPS`, con los `surface-dark-*` como `Color` dinámico, también los heredados por
129
+ referencia). Una medida con par oscuro sale como `BrandSchemeValue`. El CSS puro (`solid`, `vh`, `min()`…) se omite.
130
+ - **`BrandIcon`**: los mismos trazos que el `Icon` de React (77 iconos, retícula de 24), generados desde `Icon.tsx` con
131
+ `pnpm build:native-icons` (incluido en `build:all`).
132
+ - **Paridad**: las fichas admiten `differences` (lo que se queda a propósito) y `reactOnlyValues` (`block: boolean |
133
+ 'mobile'`), y `native:parity` entiende uniones numéricas (`Heading level: 1 | … | 6`).
134
+ - **Ayudantes**: `brandFont`/`brandLinedFont` (tipo dinámico e interlineado de CSS), `brandHitTarget` (44 pt),
135
+ `BrandControlSize` y `.brandControlSize(_:)`, `assertBrandSnapshots` (capturas de macOS en sRGB),
136
+ `native/apple/scripts/capture-story.mjs` y `pair-comparison.sh` (parejas React ↔ SwiftUI a la misma escala).
137
+ - Los ejemplos del README compilan (`ReadmeExamples.swift`).
138
+
139
+ ## [49.18.0] — 2026-10-03
140
+
141
+ > **Minor.** `RecoveryCodes` deja pasar el aviso de portapapeles roto, y `react-hook-form`
142
+ > pasa a peer opcional.
143
+
144
+ ### `RecoveryCodes` — `labels.error`
145
+
146
+ - Nueva clave **opcional** `labels.error`: el aviso del botón de copiar cuando el
147
+ portapapeles falla (contexto no seguro, permiso denegado). Hasta ahora salía solo de
148
+ `copy.error` del `BrandMessagesProvider`, así que una app que pasara las props sueltas,
149
+ sin proveedor, lanzaba al fallar la copia (visto en keycloakify-starter al pasar a npm).
150
+ Sin `labels.error` el comportamiento no cambia.
151
+
152
+ ### `react-hook-form`, peer opcional
153
+
154
+ - Solo lo importa `@studiolxd/brand/form-field`. Como `react-email`, pasa a
155
+ `peerDependenciesMeta.optional`: una app que no use ese subpath deja de instalarlo
156
+ (pnpm con `auto-install-peers` lo traía igualmente). Quien use `form-field` lo sigue
157
+ necesitando en sus `dependencies`.
158
+
10
159
  ## [49.17.0] — 2026-10-03
11
160
 
12
161
  > **Minor.** Componente nuevo: `EmbedFrame`, el marco embebido del sistema, y las tres
package/README.md CHANGED
@@ -11,10 +11,10 @@ y el catálogo de Storybook es su fuente de verdad.
11
11
  pnpm add @studiolxd/brand
12
12
  ```
13
13
 
14
- El paquete declara como peer dependencies `react` y `react-dom` (>=18) y
15
- `react-hook-form` (>=7), del que dependen el `Form` y todos los campos que se
16
- atan a él. `react-email` (>=6) es un peer **opcional**: solo hace falta si se
17
- consume el subpath `@studiolxd/brand/email`.
14
+ El paquete declara como peer dependencies `react` y `react-dom` (>=18). Hay
15
+ dos peers **opcionales**, que solo hacen falta si se consume el subpath que
16
+ los importa: `react-hook-form` (>=7) para `@studiolxd/brand/form-field`, y
17
+ `react-email` (>=6) para `@studiolxd/brand/email`.
18
18
 
19
19
  ## Uso
20
20
 
@@ -1905,6 +1905,13 @@ var e = {
1905
1905
  "--text-list-letter-spacing": "0em",
1906
1906
  "--text-list-gap": "0.5rem",
1907
1907
  "--text-list-padding-inline-start": "2rem",
1908
+ "--text-list-item-gap": "0.75rem",
1909
+ "--text-list-secondary-font-size": "0.875rem",
1910
+ "--text-list-secondary-line-height": "1.65",
1911
+ "--text-list-secondary-color": "#4a4a4a",
1912
+ "--text-list-separator-color": "#111e30",
1913
+ "--text-list-separator-thickness": "1px",
1914
+ "--text-list-separator-spacing": "0.5rem",
1908
1915
  "--textarea-border-radius": "0px",
1909
1916
  "--textarea-border-width": "1px",
1910
1917
  "--textarea-border-style": "solid",
@@ -8,10 +8,30 @@ export interface ListItemProps extends React.ComponentPropsWithoutRef<'li'> {
8
8
  */
9
9
  as?: React.ElementType;
10
10
  children?: React.ReactNode;
11
+ /**
12
+ * Accesorio al principio de la fila: un icono, un avatar. Pareja de
13
+ * `trailing`; no se encoge ni parte línea y se centra con el bloque de texto.
14
+ */
15
+ leading?: React.ReactNode;
16
+ /**
17
+ * Línea menor bajo el contenido, atenuada y un peldaño por debajo del cuerpo
18
+ * (`text.paragraph.small`): la descripción de una fila de datos o de ajustes.
19
+ */
20
+ secondary?: React.ReactNode;
21
+ /**
22
+ * Accesorio al final de la fila, alineado a la derecha: un icono, un valor,
23
+ * un interruptor. No se encoge ni parte línea.
24
+ */
25
+ trailing?: React.ReactNode;
11
26
  }
12
27
  export interface ListProps extends React.ComponentPropsWithoutRef<'ul'> {
13
28
  /** Tipo de lista: con viñetas, numerada o sin decoración. */
14
29
  type?: ListType;
30
+ /**
31
+ * Dibuja una línea entre filas con los tokens `separator.*`, con su aire a
32
+ * cada lado en lugar del `text.list.gap`. Default `false`: sin línea.
33
+ */
34
+ showSeparators?: boolean;
15
35
  children: React.ReactNode;
16
36
  }
17
37
  /**
@@ -11,8 +11,13 @@ export interface NumberInputMessages {
11
11
  increment: string;
12
12
  }
13
13
  export interface NumberInputProps extends Omit<ComponentPropsWithoutRef<'input'>, 'size' | 'type' | 'value' | 'defaultValue' | 'onChange'> {
14
- value?: number;
15
- defaultValue?: number;
14
+ /**
15
+ * Valor controlado. `null` es «sin valor»: el campo se muestra vacío (y el
16
+ * `placeholder` se ve). Sigue siendo controlado; `undefined` es no controlado.
17
+ */
18
+ value?: number | null;
19
+ /** Valor inicial no controlado (default `0`). `null` arranca vacío. */
20
+ defaultValue?: number | null;
16
21
  min?: number;
17
22
  max?: number;
18
23
  step?: number;
@@ -40,6 +45,13 @@ export interface NumberInputProps extends Omit<ComponentPropsWithoutRef<'input'>
40
45
  */
41
46
  incrementLabel?: string;
42
47
  onChange?: (value: number) => void;
48
+ /**
49
+ * Se llama cuando quien teclea deja el campo vacío. Sin ella el campo se
50
+ * comporta como siempre (vaciar no emite nada y al salir recupera el último
51
+ * número); con ella, vaciar **es** un valor: el campo pasa a «sin valor» y
52
+ * esta función avisa de ello.
53
+ */
54
+ onEmpty?: () => void;
43
55
  onBlur?: React.FocusEventHandler<HTMLInputElement>;
44
56
  onFocus?: React.FocusEventHandler<HTMLInputElement>;
45
57
  }
@@ -11,8 +11,9 @@ export interface NumberInputFieldProps extends Omit<ComponentPropsWithoutRef<'in
11
11
  * es la primera de la lista, la etiqueta se oculta sola.
12
12
  */
13
13
  labelHidden?: boolean;
14
- value?: number;
15
- defaultValue?: number;
14
+ /** `null` es «sin valor»: el campo se muestra vacío. Ver `NumberInput`. */
15
+ value?: number | null;
16
+ defaultValue?: number | null;
16
17
  min?: number;
17
18
  max?: number;
18
19
  step?: number;
@@ -27,6 +28,8 @@ export interface NumberInputFieldProps extends Omit<ComponentPropsWithoutRef<'in
27
28
  size?: 'sm' | 'md' | 'lg';
28
29
  /** Recibe el valor ya normalizado, no el evento. */
29
30
  onChange?: (value: number) => void;
31
+ /** Quien teclea dejó el campo vacío; con ella, vaciar es un valor (`null`). Ver `NumberInput`. */
32
+ onEmpty?: () => void;
30
33
  /** Se añade DESPUÉS de las clases propias (el consumidor añade, no sustituye). */
31
34
  className?: string;
32
35
  }
@@ -7,6 +7,12 @@ export interface RecoveryCodesLabels {
7
7
  copy: string;
8
8
  /** Acuse del botón de copiar, mientras dura. */
9
9
  copied: string;
10
+ /**
11
+ * Aviso del botón de copiar si el portapapeles falla (contexto no seguro,
12
+ * permiso denegado). Opcional: sin él sale de `copy.error` del
13
+ * `BrandMessagesProvider`, y sin ninguno de los dos el componente lanza.
14
+ */
15
+ error?: string;
10
16
  }
11
17
  export interface RecoveryCodesProps {
12
18
  /** Los códigos, en el orden en que se numeran. */