kaizen-lib 0.2.1 → 0.2.2

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,28 @@ 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
+ opciones={[
181
+ { codigo: 'es', nombre: 'Español', icono: <ReactCountryFlag countryCode="AR" /> },
182
+ { codigo: 'en', nombre: 'English', icono: <ReactCountryFlag countryCode="US" /> },
183
+ ]}
184
+ />
185
+ ```
186
+
187
+ **`LangSelector`** es el botón de idioma de una topbar: cicla a la siguiente opción en
188
+ cada tap y muestra el ícono del idioma actual, con el relieve de `Relieve` ya incluido
189
+ (a diferencia de `IconButton`, acá no hace falta envolverlo aparte — todo consumidor lo
190
+ quería). No trae banderas — mismo criterio que `IconButton` no trae íconos —, así que
191
+ `icono` es cosa del producto; `react-country-flag` (ya usado en tuxon-front) es una
192
+ opción probada. `onChange` recibe el código de la opción siguiente; qué hacer con eso
193
+ (`i18n.changeLanguage`, `next-intl`, lo que sea) es del producto — la librería no sabe
194
+ qué mecanismo de traducción hay atrás.
195
+
174
196
  ## Desarrollo
175
197
 
176
198
  ```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.2",
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,71 @@
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
+ size?: IconButtonSize;
28
+ };
29
+
30
+ /**
31
+ * El botón de idioma de una topbar: cicla a la siguiente opción en cada tap
32
+ * y muestra el ícono del idioma actual. No es un `<select>` ni un menú — es
33
+ * el mismo patrón que ya tenían, cada uno por su cuenta, mixbol-front,
34
+ * nsandev-front, tutipoker-school-front y valle-verde antes de esto.
35
+ *
36
+ * Trae el relieve neumórfico (`Relieve`) incluido: a diferencia de
37
+ * `IconButton`, acá no hace falta envolverlo aparte, porque el 100% de los
38
+ * consumidores lo quería.
39
+ *
40
+ * `onChange` recibe el CÓDIGO de la opción siguiente — qué hacer con eso
41
+ * (`i18n.changeLanguage`, guardarlo en `localStorage`, lo que sea) es del
42
+ * producto. La librería no sabe qué mecanismo de traducción hay atrás.
43
+ *
44
+ * ```tsx
45
+ * <LangSelector
46
+ * actual={idiomaActual}
47
+ * onChange={(codigo) => i18n.changeLanguage(codigo)}
48
+ * opciones={[
49
+ * { codigo: 'es', nombre: 'Español', icono: <ReactCountryFlag countryCode="AR" /> },
50
+ * { codigo: 'en', nombre: 'English', icono: <ReactCountryFlag countryCode="US" /> },
51
+ * ]}
52
+ * />
53
+ * ```
54
+ */
55
+ export function LangSelector<T extends string>({ opciones, actual, onChange, size = 'lg' }: Props<T>) {
56
+ // Sin opciones no hay nada que ciclar. Defensivo: un selector de idioma
57
+ // vacío es un error de uso, no algo que la librería deba explicar.
58
+ if (opciones.length === 0) return null;
59
+
60
+ const indiceActual = opciones.findIndex((opcion) => opcion.codigo === actual);
61
+ const opcionActual = opciones[indiceActual < 0 ? 0 : indiceActual] as OpcionDeIdioma<T>;
62
+ const siguiente = opciones[(indiceActual + 1 + opciones.length) % opciones.length] as OpcionDeIdioma<T>;
63
+
64
+ return (
65
+ <Relieve>
66
+ <IconButton size={size} label={`Cambiar idioma a ${siguiente.nombre}`} onClick={() => onChange(siguiente.codigo)}>
67
+ {opcionActual.icono}
68
+ </IconButton>
69
+ </Relieve>
70
+ );
71
+ }
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';