kaizen-lib 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -12,7 +12,7 @@ styled-components con `moduleResolution: "bundler"`, así que lo transpilan ello
12
12
  |---|---|---|
13
13
  | `kaizen-lib/tokens` | Referencias `var(--kz-*)` tipadas | listo |
14
14
  | `kaizen-lib/tokens.css` | Valores por defecto, tema oscuro y claro | listo |
15
- | `kaizen-lib/ui` | Componentes (`IconButton`, `ReactionButton`, `Medidor`, `Relieve`) | en curso |
15
+ | `kaizen-lib/ui` | Componentes (`IconButton`, `ReactionButton`, `Medidor`, `Relieve`, `LangSelector`) | en curso |
16
16
 
17
17
  ## Instalación
18
18
 
@@ -171,6 +171,32 @@ intercepta el `as` de cualquier componente que envuelve, así que envolverlo as
171
171
  saltear toda su lógica. El relieve es neutro por defecto (negro/blanco) — un producto con
172
172
  marca lo tiñe redefiniendo esos tres tokens, igual que con cualquier otro.
173
173
 
174
+ ```tsx
175
+ import { LangSelector } from 'kaizen-lib/ui';
176
+
177
+ <LangSelector
178
+ actual={i18n.language}
179
+ onChange={(codigo) => i18n.changeLanguage(codigo)}
180
+ etiqueta={(siguiente) => `Cambiar idioma a ${siguiente.nombre}`}
181
+ opciones={[
182
+ { codigo: 'es', nombre: 'Español', icono: <ReactCountryFlag countryCode="AR" /> },
183
+ { codigo: 'en', nombre: 'English', icono: <ReactCountryFlag countryCode="US" /> },
184
+ ]}
185
+ />
186
+ ```
187
+
188
+ **`LangSelector`** es el botón de idioma de una topbar: cicla a la siguiente opción en
189
+ cada tap y muestra el ícono del idioma actual, con el relieve de `Relieve` ya incluido
190
+ (a diferencia de `IconButton`, acá no hace falta envolverlo aparte — todo consumidor lo
191
+ quería). No trae banderas — mismo criterio que `IconButton` no trae íconos —, así que
192
+ `icono` es cosa del producto; `react-country-flag` (ya usado en tuxon-front) es una
193
+ opción probada. `etiqueta` arma el nombre accesible a partir de la opción siguiente y la
194
+ actual — es obligatoria y sin default: la frase y el idioma son del producto (dos ya
195
+ difieren: uno anuncia el destino, "Cambiar idioma a English"; otro el estado, "Idioma:
196
+ English. Cambiar idioma"). `onChange` recibe el código de la opción siguiente; qué hacer
197
+ con eso (`i18n.changeLanguage`, `next-intl`, lo que sea) es del producto — la librería no
198
+ sabe qué mecanismo de traducción hay atrás.
199
+
174
200
  ## Desarrollo
175
201
 
176
202
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-lib",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Tokens y componentes compartidos entre los fronts de Kaizen.",
5
5
  "license": "MIT",
6
6
  "author": "Nicolás Sandobal",
@@ -0,0 +1,81 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { IconButton } from './IconButton';
5
+ import { Relieve } from './Relieve';
6
+ import type { IconButtonSize } from './shell';
7
+
8
+ /**
9
+ * Una opción del selector: el código de idioma, su nombre (para el nombre
10
+ * accesible) y el ícono que lo representa. La librería no trae banderas —
11
+ * mismo criterio que `IconButton` no trae íconos — porque una bandera bien
12
+ * hecha es un SVG por país, no algo que valga la pena embeber en un paquete
13
+ * sin build. `react-country-flag` (ya usado en tuxon-front) es una opción
14
+ * probada; cualquier ícono sirve.
15
+ */
16
+ export type OpcionDeIdioma<T extends string> = {
17
+ codigo: T;
18
+ /** Nombre del idioma, en ese idioma ("English", no "Inglés"). */
19
+ nombre: string;
20
+ icono: ReactNode;
21
+ };
22
+
23
+ type Props<T extends string> = {
24
+ opciones: OpcionDeIdioma<T>[];
25
+ actual: T;
26
+ onChange: (codigo: T) => void;
27
+ /**
28
+ * Arma el nombre accesible del botón, a partir de la opción actual y la
29
+ * siguiente (a la que se pasa al tocar). Obligatoria y sin default: qué
30
+ * frase decir y en qué idioma es del producto, la librería no lo fija.
31
+ * Dos productos ya difieren en esto mismo — uno anuncia el destino
32
+ * ("Cambiar idioma a English"), otro el estado ("Idioma: English. Cambiar
33
+ * idioma") — y los dos son legítimos.
34
+ */
35
+ etiqueta: (siguiente: OpcionDeIdioma<T>, actual: OpcionDeIdioma<T>) => string;
36
+ size?: IconButtonSize;
37
+ };
38
+
39
+ /**
40
+ * El botón de idioma de una topbar: cicla a la siguiente opción en cada tap
41
+ * y muestra el ícono del idioma actual. No es un `<select>` ni un menú — es
42
+ * el mismo patrón que ya tenían, cada uno por su cuenta, mixbol-front,
43
+ * nsandev-front, tutipoker-school-front y valle-verde antes de esto.
44
+ *
45
+ * Trae el relieve neumórfico (`Relieve`) incluido: a diferencia de
46
+ * `IconButton`, acá no hace falta envolverlo aparte, porque el 100% de los
47
+ * consumidores lo quería.
48
+ *
49
+ * `onChange` recibe el CÓDIGO de la opción siguiente — qué hacer con eso
50
+ * (`i18n.changeLanguage`, guardarlo en `localStorage`, lo que sea) es del
51
+ * producto. La librería no sabe qué mecanismo de traducción hay atrás.
52
+ *
53
+ * ```tsx
54
+ * <LangSelector
55
+ * actual={idiomaActual}
56
+ * onChange={(codigo) => i18n.changeLanguage(codigo)}
57
+ * etiqueta={(siguiente) => `Cambiar idioma a ${siguiente.nombre}`}
58
+ * opciones={[
59
+ * { codigo: 'es', nombre: 'Español', icono: <ReactCountryFlag countryCode="AR" /> },
60
+ * { codigo: 'en', nombre: 'English', icono: <ReactCountryFlag countryCode="US" /> },
61
+ * ]}
62
+ * />
63
+ * ```
64
+ */
65
+ export function LangSelector<T extends string>({ opciones, actual, onChange, etiqueta, size = 'lg' }: Props<T>) {
66
+ // Sin opciones no hay nada que ciclar. Defensivo: un selector de idioma
67
+ // vacío es un error de uso, no algo que la librería deba explicar.
68
+ if (opciones.length === 0) return null;
69
+
70
+ const indiceActual = opciones.findIndex((opcion) => opcion.codigo === actual);
71
+ const opcionActual = opciones[indiceActual < 0 ? 0 : indiceActual] as OpcionDeIdioma<T>;
72
+ const siguiente = opciones[(indiceActual + 1 + opciones.length) % opciones.length] as OpcionDeIdioma<T>;
73
+
74
+ return (
75
+ <Relieve>
76
+ <IconButton size={size} label={etiqueta(siguiente, opcionActual)} onClick={() => onChange(siguiente.codigo)}>
77
+ {opcionActual.icono}
78
+ </IconButton>
79
+ </Relieve>
80
+ );
81
+ }
package/src/ui/index.ts CHANGED
@@ -5,3 +5,5 @@ export type { MedidorSentido } from './Medidor';
5
5
  export { Relieve } from './Relieve';
6
6
  export { ReactionButton } from './ReactionButton';
7
7
  export type { ReactionTone } from './ReactionButton';
8
+ export { LangSelector } from './LangSelector';
9
+ export type { OpcionDeIdioma } from './LangSelector';