@eduardoalvarez/arrecife 0.5.1 → 0.7.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 (78) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +868 -467
  3. package/dist/brand/index.cjs +112 -95
  4. package/dist/brand/index.d.cts +40 -39
  5. package/dist/brand/index.d.ts +40 -39
  6. package/dist/brand/index.js +5 -4
  7. package/dist/catalog-D13txprv.d.cts +78 -0
  8. package/dist/catalog-D13txprv.d.ts +78 -0
  9. package/dist/chart/index.cjs +100 -83
  10. package/dist/chart/index.d.cts +66 -66
  11. package/dist/chart/index.d.ts +66 -66
  12. package/dist/chart/index.js +14 -12
  13. package/dist/chunk-2WPWEIMD.js +27 -0
  14. package/dist/chunk-45HVCTB7.js +70 -0
  15. package/dist/{chunk-ZEOQKRQ7.js → chunk-727HCBD4.js} +1 -1
  16. package/dist/chunk-CKRSQPTX.js +36 -0
  17. package/dist/chunk-E6KFUSKB.js +144 -0
  18. package/dist/chunk-GCRII2KQ.js +86 -0
  19. package/dist/{chunk-YZ2SDOVZ.js → chunk-JN3IS5OS.js} +30 -30
  20. package/dist/chunk-ODBFN44D.js +45 -0
  21. package/dist/chunk-OMKSESQB.js +300 -0
  22. package/dist/{chunk-VPT32GPG.js → chunk-TA7TLWW4.js} +2 -2
  23. package/dist/chunk-WGNIRIN7.js +42 -0
  24. package/dist/doctor.mjs +166 -0
  25. package/dist/form/index.cjs +109 -92
  26. package/dist/form/index.d.cts +43 -42
  27. package/dist/form/index.d.ts +43 -42
  28. package/dist/form/index.js +25 -23
  29. package/dist/icons/index.cjs +149 -0
  30. package/dist/icons/index.d.cts +94 -0
  31. package/dist/icons/index.d.ts +94 -0
  32. package/dist/icons/index.js +28 -0
  33. package/dist/index-DlAO2JZs.d.cts +47 -0
  34. package/dist/index-DlAO2JZs.d.ts +47 -0
  35. package/dist/index.cjs +1292 -983
  36. package/dist/index.d.cts +927 -806
  37. package/dist/index.d.ts +927 -806
  38. package/dist/index.js +809 -778
  39. package/dist/{label-DuTvJGxD.d.ts → label-MgHFKnFy.d.cts} +3 -3
  40. package/dist/{label-DuTvJGxD.d.cts → label-MgHFKnFy.d.ts} +3 -3
  41. package/dist/og/index.cjs +133 -132
  42. package/dist/og/index.d.cts +93 -89
  43. package/dist/og/index.d.ts +93 -89
  44. package/dist/og/index.js +106 -106
  45. package/dist/shiki/index.cjs +28 -30
  46. package/dist/shiki/index.d.cts +4 -4
  47. package/dist/shiki/index.d.ts +4 -4
  48. package/dist/shiki/index.js +12 -12
  49. package/dist/social/index.cjs +67 -0
  50. package/dist/social/index.d.cts +2 -0
  51. package/dist/social/index.d.ts +2 -0
  52. package/dist/social/index.js +2 -0
  53. package/dist/theme/index.cjs +97 -0
  54. package/dist/theme/index.d.cts +144 -0
  55. package/dist/theme/index.d.ts +144 -0
  56. package/dist/theme/index.js +2 -0
  57. package/dist/tokens/index.cjs +159 -88
  58. package/dist/tokens/index.d.cts +277 -165
  59. package/dist/tokens/index.d.ts +277 -165
  60. package/dist/tokens/index.js +2 -2
  61. package/dist/tokens/theme.css +165 -100
  62. package/dist/variants/index.cjs +195 -0
  63. package/dist/variants/index.d.cts +195 -0
  64. package/dist/variants/index.d.ts +195 -0
  65. package/dist/variants/index.js +3 -0
  66. package/llms.txt +1145 -746
  67. package/package.json +42 -11
  68. package/dist/catalogo-Du5ID-Hi.d.cts +0 -77
  69. package/dist/catalogo-Du5ID-Hi.d.ts +0 -77
  70. package/dist/chunk-E3OMP2DL.js +0 -36
  71. package/dist/chunk-KPZNNMV5.js +0 -83
  72. package/dist/chunk-NHS7ETKJ.js +0 -27
  73. package/dist/chunk-TSPJOM6K.js +0 -229
  74. package/dist/chunk-UOWIDFCB.js +0 -81
  75. package/dist/tema/index.cjs +0 -94
  76. package/dist/tema/index.d.cts +0 -110
  77. package/dist/tema/index.d.ts +0 -110
  78. package/dist/tema/index.js +0 -2
package/llms.txt CHANGED
@@ -1,262 +1,566 @@
1
1
  # @eduardoalvarez/arrecife
2
2
 
3
- > GENERADO por `scripts/build-llms.mjs`. No lo edites a mano: la prosa está en
4
- > `docs/llms.plantilla.md` y el inventario sale de los tipos en cada build.
5
- > `pnpm check:llms` falla si este archivo y el código dejan de decir lo mismo.
3
+ > GENERATED by `scripts/build-llms.mjs`. Do not edit by hand: the prose lives in
4
+ > `docs/llms.template.md` and the inventory comes out of the types on every
5
+ > build. `pnpm check:llms` fails if this file and the code stop saying the same
6
+ > thing.
6
7
 
7
- Librería de componentes de la identidad visual de Eduardo Álvarez. React 19,
8
- TypeScript, Tailwind v4, shadcn/ui sobre Radix.
8
+ The component library of Eduardo Álvarez's visual identity. React 19,
9
+ TypeScript, Tailwind v4, shadcn/ui on top of Radix.
9
10
 
10
- Este documento es para un agente que escribe código en un proyecto que consume la
11
- librería. Si estás trabajando **dentro** del repo de Arrecife, el documento es
12
- `AGENTS.md`, no este.
11
+ This document is for an agent writing code in a project that consumes the
12
+ library. If you are working **inside** the Arrecife repo, the document is
13
+ `AGENTS.md`, not this one.
13
14
 
14
- ## Lo primero: no reimplementes lo que ya está aquí
15
+ ## First things first: do not reimplement what is already here
15
16
 
16
- Antes de escribir una tarjeta, un botón, una cabecera o un pie, busca en el
17
- inventario de más abajo. La librería existe porque cinco proyectos escribían las
18
- mismas piezas cada uno por su cuenta y se desincronizaban. Un componente nuevo
19
- escrito a mano en el proyecto consumidor reintroduce exactamente ese problema.
17
+ Before writing a card, a button, a header or a footer, look through the inventory
18
+ below. The library exists because five projects were each writing the same pieces
19
+ on their own and drifting apart. A new component hand-written in the consuming
20
+ project reintroduces exactly that problem.
20
21
 
21
- Tampoco escribas colores, tamaños ni espaciados a mano. Todo valor del sistema
22
- tiene un token y una utilidad de Tailwind; un `#hex` o un `p-[13px]` en el
23
- proyecto consumidor es la señal de que se eligió el camino equivocado.
22
+ Do not hand-write colors, sizes or spacing either. Every value in the system has
23
+ a token and a Tailwind utility; a `#hex` or a `p-[13px]` in the consuming project
24
+ is the sign that the wrong path was taken.
24
25
 
25
- ## Instalación
26
+ ## Installation
26
27
 
27
28
  ```bash
28
29
  pnpm add @eduardoalvarez/arrecife
29
30
  ```
30
31
 
31
- Requisitos, y no son opcionales:
32
+ Requirements, and they are not optional:
32
33
 
33
34
  | | |
34
35
  | --- | --- |
35
- | React | `^19.0.0` y `react-dom` `^19.0.0`, como peer dependencies |
36
- | Tailwind | v4. **No hay preset de v3**: la salida es `@theme`, que v3 no entiende |
37
- | Node | `>=22.18.0` para las subrutas que corren en build (`./og`, `./tokens`) |
36
+ | React | `^19.0.0` and `react-dom` `^19.0.0`, as peer dependencies |
37
+ | Tailwind | v4. **There is no v3 preset**: the output is `@theme`, which v3 does not understand |
38
+ | Node | `>=22.18.0` for the subpaths that run at build time (`./og`, `./tokens`) |
38
39
 
39
- Radix, `clsx`, `tailwind-merge`, `class-variance-authority`, `date-fns` y
40
- `react-day-picker` vienen como dependencias de la librería. No hace falta
41
- instalarlos ni declararlos.
40
+ Radix, `clsx`, `tailwind-merge`, `class-variance-authority`, `date-fns` and
41
+ `react-day-picker` come as dependencies of the library. You do not need to
42
+ install or declare them.
42
43
 
43
- **No lleva `lucide-react` ni ninguna librería de iconos.** Los glifos que los
44
- componentes necesitan van inline, heredan `currentColor` y miden 1em.
44
+ **It ships no icon set.** The glyphs the components
45
+ need are inline, inherit `currentColor` and measure 1em.
45
46
 
46
- ## Configuración de Tailwind
47
+ ## Tailwind configuration
47
48
 
48
- Dos líneas, en este orden, en la hoja de estilos de entrada del proyecto:
49
+ Two lines, in this order, in the project's entry stylesheet:
49
50
 
50
51
  ```css
51
52
  @import "tailwindcss";
52
53
  @import "@eduardoalvarez/arrecife/tokens/theme.css";
53
54
  ```
54
55
 
55
- Sin la segunda, los componentes se montan sin ningún estilo del sistema: las
56
- clases que usan (`bg-surface-raised`, `text-h1`, `rounded-card`) no existen en un
57
- Tailwind pelado.
56
+ Without the second one, the components mount with none of the system's styles:
57
+ the classes they use (`bg-surface-raised`, `text-h1`, `rounded-card`) do not
58
+ exist in a bare Tailwind.
58
59
 
59
- Tailwind tiene que escanear la librería para no purgar esas clases. Si el
60
- proyecto declara `@source`, incluye el paquete:
60
+ Tailwind has to scan the library so it does not purge those classes. If the
61
+ project declares `@source`, include the package:
61
62
 
62
63
  ```css
63
64
  @source "../node_modules/@eduardoalvarez/arrecife/dist";
64
65
  ```
65
66
 
66
- ### Modo claro y modo oscuro
67
+ ### Light mode and dark mode
67
68
 
68
- **El modo oscuro es el primario y es el default.** Un proyecto oscuro no declara
69
- nada. Un proyecto en modo claro declara el atributo en `<html>`:
69
+ **Dark mode is primary and it is the default.** A dark project declares nothing.
70
+ A project in light mode declares the attribute on `<html>`:
70
71
 
71
72
  ```html
72
73
  <html data-theme="light">
73
74
  ```
74
75
 
75
- No hay clase `dark:`. La variante disponible es `light:`, para los casos del modo
76
- claro invertido, y casi nunca hace falta: los tokens ya cambian solos.
76
+ There is no `dark:` class. The variant available is `light:`, for the cases of
77
+ inverted light mode, and it is almost never needed: the tokens already switch on
78
+ their own.
77
79
 
78
- ### Fuentes
80
+ ### Fonts
79
81
 
80
- La librería declara las familias **por nombre** y no las carga. El proyecto carga
81
- Bricolage Grotesque (`font-display`), Geist (`font-sans`) y JetBrains Mono
82
- (`font-mono`) como prefiera —`next/font`, `@fontsource`, un `<link>`—. Si no las
83
- carga, el navegador cae al fallback y la tipografía se ve mal.
82
+ The library declares the families **by name** and does not load them. The project
83
+ loads Bricolage Grotesque (`font-display`), Geist (`font-sans`) and JetBrains
84
+ Mono (`font-mono`) however it prefers — `next/font`, `@fontsource`, a `<link>`.
85
+ If it does not load them, the browser falls back and the typography looks wrong.
84
86
 
85
- El nombre tiene que coincidir **exactamente** con el que declaran los tokens, y
86
- esto ya ha fallado en dos proyectos:
87
+ The name has to match **exactly** the one the tokens declare, and this has
88
+ already failed in two projects:
87
89
 
88
- | Utilidad | Nombre que pide el token |
90
+ | Utility | Name the token asks for |
89
91
  | --- | --- |
90
92
  | `font-display` | `"Bricolage Grotesque"` |
91
93
  | `font-sans` | `"Geist"` |
92
94
  | `font-mono` | `"JetBrains Mono"` |
93
95
 
94
- Varios paquetes de fuentes las publican como `"Bricolage Grotesque Variable"` o
95
- `"Geist Variable"`. Registrar el `@font-face` con ese nombre NO carga lo que los
96
- tokens piden: la familia cae al sistema en silencio, sin error en consola. El
97
- `font-family` del `@font-face` es un alias que elige el proyecto, así que se
98
- escribe con el nombre de la tabla.
96
+ Several font packages publish them as `"Bricolage Grotesque Variable"` or
97
+ `"Geist Variable"`. Registering the `@font-face` under that name does NOT load
98
+ what the tokens ask for: the family falls back to the system silently, with no
99
+ console error. The `@font-face`'s `font-family` is an alias the project chooses,
100
+ so write it with the name from the table.
99
101
 
100
- ## Qué importar de dónde
102
+ ## What to import from where
101
103
 
102
- La elección importa, y por dos motivos distintos. Cuatro subrutas **no arrastran
103
- React**, así que pueden consumirse desde un worker, un `astro.config.mjs`, un
104
- script de build o un generador con Satori. Otras dos piden una **dependencia de
105
- pares opcional** que solo instala quien las use.
104
+ The choice matters, for two different reasons. Four subpaths **do not drag React
105
+ in**, so they can be consumed from a worker, an `astro.config.mjs`, a build
106
+ script or a Satori generator. Another two ask for an **optional peer dependency**
107
+ that only whoever uses them installs.
106
108
 
107
- | Subruta | Arrastra React | Pide además | Para qué |
109
+ | Subpath | Drags React | Also requires | What for |
108
110
  | --- | --- | --- | --- |
109
- | `@eduardoalvarez/arrecife` | sí | — | Componentes, primitivos, marca, `cn`. Reexporta tokens y tema |
110
- | `@eduardoalvarez/arrecife/tokens` | **no** | — | El objeto `tokens` en JS. Satori, Astro, scripts |
111
- | `@eduardoalvarez/arrecife/tokens/theme.css` | — | — | El `@theme` de Tailwind v4 |
112
- | `@eduardoalvarez/arrecife/tema` | **no** | — | `scriptTema` para el `<head>`, y leer o cambiar el modo |
113
- | `@eduardoalvarez/arrecife/og` | **no** | — | Las plantillas de Open Graph para Satori |
114
- | `@eduardoalvarez/arrecife/shiki` | **no** | — | El tema de resaltado de sintaxis |
115
- | `@eduardoalvarez/arrecife/brand` | sí | — | Logo, isotipo y mascota como componentes |
116
- | `@eduardoalvarez/arrecife/form` | sí | `react-hook-form` | La capa de formulario: etiquetas, errores y `aria-*` |
117
- | `@eduardoalvarez/arrecife/chart` | sí | `recharts` | El chasis de las gráficas y la paleta de series |
118
- | `@eduardoalvarez/arrecife/assets/*` | — | — | Los PNG de la marca |
119
-
120
- Importar la raíz desde un script de build para sacar un token es el error que las
121
- subrutas existen para evitar: arrastra React entero a un worker que no lo monta.
122
-
123
- `./form` y `./chart` están fuera de la raíz por el motivo simétrico: si colgaran
124
- del índice principal, los proyectos que no dibujan gráficas ni usan React Hook
125
- Form tendrían que instalar esas dependencias igualmente para que su bundler
126
- resolviera un import que nunca ejecutan.
111
+ | `@eduardoalvarez/arrecife` | yes | — | Components, primitives, brand, `cn`. Re-exports tokens and theme |
112
+ | `@eduardoalvarez/arrecife/tokens` | **no** | — | The `tokens` object in JS. Satori, Astro, scripts |
113
+ | `@eduardoalvarez/arrecife/tokens/theme.css` | — | — | Tailwind v4's `@theme` |
114
+ | `@eduardoalvarez/arrecife/theme` | **no** | — | `themeScript` for the `<head>`, and reading or changing the mode |
115
+ | `@eduardoalvarez/arrecife/variants` | **no** | — | The class vocabulary: `buttonVariants`, `badgeVariants`, `CARD_SURFACE` |
116
+ | `@eduardoalvarez/arrecife/og` | **no** | — | The Open Graph templates for Satori |
117
+ | `@eduardoalvarez/arrecife/shiki` | **no** | — | The syntax highlighting theme |
118
+ | `@eduardoalvarez/arrecife/brand` | yes | — | Logo, isotype and mascot as components |
119
+ | `@eduardoalvarez/arrecife/social` | yes, on the server only | — | The nine social icons, loose. No `"use client"` |
120
+ | `@eduardoalvarez/arrecife/icons` | yes | `@phosphor-icons/react` | `Icon`, which draws a Phosphor icon at the system's size, and at the weight its role asks for |
121
+ | `@eduardoalvarez/arrecife/form` | yes | `react-hook-form` | The form layer: labels, errors and `aria-*` |
122
+ | `@eduardoalvarez/arrecife/chart` | yes | `recharts` | The chart chassis and the series palette |
123
+ | `@eduardoalvarez/arrecife/assets/*` | — | — | The brand PNGs |
124
+
125
+ Importing the root from a build script to get one token is the mistake the
126
+ subpaths exist to prevent: it drags all of React into a worker that never mounts
127
+ it.
128
+
129
+ `./form`, `./chart` and `./icons` sit outside the root for the symmetric reason:
130
+ if they hung off the main index, the projects that draw no charts, use no React
131
+ Hook Form and need no icons would have to install those dependencies anyway so
132
+ their bundler could resolve an import they never execute. Two of the five consume
133
+ zero icons.
134
+
135
+ ### Next, Server Components and `"use client"`
136
+
137
+ The root, `./brand`, `./form` and `./chart` ship `"use client"` in the published
138
+ `dist/`, and they are the only four. They render React and their Radix primitives call `createContext` at
139
+ module scope, so without the directive a Next project with the App Router cannot
140
+ import them at all: it fails at build time with
141
+ `TypeError: (0 , r.createContext) is not a function`.
142
+
143
+ You do not add anything: importing `Button` from a Server Component works, and
144
+ the boundary is already where it belongs. What you should NOT do is wrap the
145
+ import in an adapter of your own marked `"use client"` — that was the workaround
146
+ before 0.6.0 and it pulled 272 KB of client chunk in for components that never
147
+ needed it.
148
+
149
+ The five portable subpaths do NOT carry the directive, and that is the half that
150
+ matters in a Server Component: `./tokens`, `./theme`, `./variants`, `./og` and
151
+ `./shiki` stay on the server. Neither does `./social`, which is a third case: it
152
+ renders React — it is nine `<svg>` — so it can never be portable, but it holds no
153
+ state and nothing about it needs a client boundary. It is the only way to put a
154
+ social icon in a Server Component, and § «The social icons come from `./social`»
155
+ below says why the grouped form cannot do it. If all you need are classes — for a `<div>`, an
156
+ `<a>` or an Astro island you do not want to hydrate — import them from
157
+ `./variants` and nothing crosses to the client:
158
+
159
+ ```tsx
160
+ // A Server Component, or an .astro frontmatter. No React reaches the browser.
161
+ import { buttonVariants, CARD_SURFACE } from '@eduardoalvarez/arrecife/variants';
162
+
163
+ <a className={buttonVariants({ variant: 'tertiary' })} href="/cursos">./ver_cursos →</a>
164
+ <div className={CARD_SURFACE}>…</div>
165
+ ```
166
+
167
+ In Astro and in plain Vite the directive is inert — a string literal at the top
168
+ of a module. Rollup may warn `Module level directives cause errors when bundled`
169
+ and nothing else happens: one `dist` serves the Next projects and the Astro ones.
127
170
 
128
171
  ```ts
129
- // Bien, en un generador de OG o en astro.config.mjs
172
+ // Good, in an OG generator or in astro.config.mjs
130
173
  import { tokens } from '@eduardoalvarez/arrecife/tokens';
131
174
  import { arrecife } from '@eduardoalvarez/arrecife/shiki';
132
175
 
133
- // Bien, en el <head> de un Astro que no monta React
134
- import { scriptTema } from '@eduardoalvarez/arrecife/tema';
176
+ // Good, in the <head> of an Astro that mounts no React
177
+ import { themeScript } from '@eduardoalvarez/arrecife/theme';
135
178
 
136
- // Mal: monta React donde no hace falta
179
+ // Bad: mounts React where it is not needed
137
180
  import { tokens } from '@eduardoalvarez/arrecife';
138
181
  ```
139
182
 
140
- ### El tema, y el parpadeo de la primera pintura
183
+ ### The theme, and the first-paint flash
141
184
 
142
- `ThemeToggle` es el botón; lo difícil está en `./tema`. Sin `scriptTema` inline en
143
- el `<head>`, la primera pintura sale con el modo por defecto y el elegido entra
144
- un frame después: en un sitio oscuro que el usuario dejó en claro, eso es un
145
- fogonazo blanco en cada carga.
185
+ `ThemeToggle` is the button; the hard part is in `./theme`. Without `themeScript`
186
+ inline in the `<head>`, the first paint comes out in the default mode and the
187
+ chosen one arrives a frame later: on a dark site the user left in light mode,
188
+ that is a white flash on every load.
146
189
 
147
190
  ```astro
148
191
  ---
149
- import { scriptTema } from '@eduardoalvarez/arrecife/tema';
192
+ import { themeScript } from '@eduardoalvarez/arrecife/theme';
150
193
  ---
151
194
  <head>
152
- <script is:inline set:html={scriptTema} />
195
+ <!-- The site follows the reader's OS, falling back to dark. -->
196
+ <script is:inline set:html={themeScript()} />
197
+
198
+ <!-- Or: this site IS dark, and the OS is not consulted. -->
199
+ <script is:inline set:html={themeScript({ base: 'dark' })} />
153
200
  </head>
154
201
  ```
155
202
 
156
- Tiene que ir INLINE. Un `<script src>`, aunque sea síncrono, se descarga, y el
157
- parpadeo vuelve. El script reengancha en `astro:after-swap` porque las
158
- transiciones de vista reemplazan el `<html>` entero.
203
+ `base` is not «the fallback», it is «this site IS this mode». A stored choice
204
+ still wins over it, so the toggle keeps working — it sets what happens when
205
+ nobody has chosen yet. Use it when the project has already decided its mode and
206
+ does not want the OS overruling that.
207
+
208
+ It has to go INLINE. A `<script src>`, even a synchronous one, gets downloaded,
209
+ and the flash comes back. The script re-attaches on `astro:after-swap` because
210
+ view transitions replace the whole `<html>`.
211
+
212
+ ### `npx arrecife` — run it once after installing
213
+
214
+ Two things break with **no error at all**, and the command catches both.
215
+
216
+ **Tailwind purges everything the components emit** unless the stylesheet has
217
+ `@source "<path>/node_modules/@eduardoalvarez/arrecife/dist"`. It does not scan
218
+ `node_modules`. There is no console error and no undefined class: the card mounts
219
+ with no padding, no radius and no border. The path is relative to the SHEET, not
220
+ to the project root, and the command computes it.
221
+
222
+ **A `--color-*` of yours silently replaces ours.** A project coming from shadcn
223
+ has `@theme inline { --color-accent: var(--accent); }` — shadcn's `--accent` is
224
+ the hover surface, `#17303E`, and ours is the brand turquoise, `#35D6C0`. That
225
+ one line repainted 88 classes inside the library's own components grey. Five
226
+ names collide in total; four agree on the value and are harmless, and the command
227
+ tells them apart.
228
+
229
+ Do not silence it by removing the `@import`: the fix is the `@source` line, or
230
+ renaming your own token.
231
+
232
+ ### `SidebarNav` groups, and the icon replaces the prompt
233
+
234
+ ```tsx
235
+ <SidebarNav aria-label="Administración" brand={<>…</>} version="v0.6.0" branch="main">
236
+ <SidebarItem href="/admin" icon={<Icon as={SquaresFour} />} active>Resumen</SidebarItem>
237
+
238
+ <SidebarGroup label="Ventas">
239
+ <SidebarItem href="/admin/ventas" icon={<Icon as={CreditCard} />}>Ventas</SidebarItem>
240
+ <SidebarItem href="/admin/cupones" icon={<Icon as={Ticket} />}>Cupones</SidebarItem>
241
+ </SidebarGroup>
242
+ </SidebarNav>
243
+ ```
244
+
245
+ Past about eight items a flat sidebar stops being readable. Each `SidebarGroup`
246
+ is a nested list named by its label, so a screen reader says «lista Ventas, 3
247
+ elementos» instead of one list of eleven. The label is a paragraph and **not** a
248
+ heading on purpose: a sidebar is navigation, and a heading here would land in the
249
+ page's own outline.
250
+
251
+ **`icon` replaces the `▸`, it does not join it.** Do not pass a glyph and expect
252
+ the prompt as well. A sidebar with no icons keeps the prompt on every item, which
253
+ is what a four-section blog admin wants.
254
+
255
+ **`brand` does not replace `title`.** `title` is the eyebrow and also the `nav`'s
256
+ accessible name when it is a string; a logo is not an accessible name, so pass
257
+ `aria-label` when you use `brand`. See `docs/decisions.md` § 32.
258
+
259
+ **It collapses to a rail, and the toggle is CONTROLLED:**
260
+
261
+ ```tsx
262
+ const [collapsed, setCollapsed] = useState(false);
263
+
264
+ <SidebarNav
265
+ collapsed={collapsed}
266
+ onCollapsedChange={setCollapsed}
267
+ brand={<Wordmark />}
268
+ mark={<Isotype className="h-6" />}
269
+ user={<Avatar … />}
270
+ >
271
+ ```
272
+
273
+ There is no uncontrolled mode: this state is almost always persisted, and an
274
+ internal one would fight the cookie you already keep. `onCollapsedChange` is also
275
+ what makes the toggle appear — `collapsed` on its own is a rail with no way out,
276
+ which is a layout and not an accident.
159
277
 
160
- ### Los iconos de redes van agrupados
278
+ Collapsed, the widths become `w-sidebar-rail` (56) and `w-sidebar` (256), and the
279
+ component owns them only when it can collapse. It does **not** transition, on
280
+ purpose. `brand` is hidden and `mark` takes its place, because a wordmark does
281
+ not fit in a rail. Every label stays in the accessibility tree as `sr-only`, so
282
+ do not «simplify» by dropping the children of a collapsed item. See
283
+ `docs/decisions.md` § 34.
284
+
285
+ ### `Nav` is two slots and one height
286
+
287
+ Almost everything an app shell wants from a site bar is already a slot:
161
288
 
162
289
  ```tsx
163
- // ❌ no existe
290
+ <Nav
291
+ size="compact" // 56px, for a shell with a sidebar
292
+ brand={<a href="/"><Logo /><span><span className="text-accent">~/</span>cursos</span></a>}
293
+ actions={session ? <UserMenu /> : <Button size="sm" asChild><Link href="/login">Entrar</Link></Button>}
294
+ >
295
+ <NavItem href="/cursos" active>cursos</NavItem>
296
+ </Nav>
297
+ ```
298
+
299
+ `brand` and `actions` are `ReactNode`, so a wordmark, a user menu, a theme toggle
300
+ or a search box go in without the library knowing anything about sessions.
301
+ **Session state does not get a prop** — it is project infrastructure, and the
302
+ library takes none.
303
+
304
+ `size="compact"` is 56px instead of 64, for a bar that shares the screen with a
305
+ sidebar. It is a prop and not a class because the height lives on `Nav`'s inner
306
+ container: `className` reaches the `<header>` and stops there, so passing `h-14`
307
+ does nothing.
308
+
309
+ **One `Nav` per page.** It renders the site's `banner` landmark, and two banners
310
+ on one page is an accessibility failure — which is also why `PageHeader` goes
311
+ inside `<main>` and is not a landmark. See `docs/decisions.md` § 30.
312
+
313
+ ### Icons are yours, the way they are drawn is not
314
+
315
+ The library ships no icon set and `lib/glyphs.tsx` is not exported: it is the
316
+ minimum set the primitives need and it does not grow. What the library does ship
317
+ is the drawing.
318
+
319
+ ```tsx
320
+ import { GraduationCap, Trash } from '@phosphor-icons/react';
321
+ import { Icon } from '@eduardoalvarez/arrecife/icons';
322
+
323
+ // 1em, and `tone="action"` by default — `regular`, the stroke the document names
324
+ <Icon as={GraduationCap} />
325
+
326
+ // Inside a control with no text, the name goes on the CONTROL
327
+ <Button size="icon-sm" variant="secondary" aria-label="Borrar la fila">
328
+ <Icon as={Trash} />
329
+ </Button>
330
+
331
+ // Alone and meaning something on its own, it gets a name
332
+ <Icon as={Trophy} label="Curso completado" />
333
+ ```
334
+
335
+ **Do not size them by hand.** `size-4`, `size-3.5`, `size-6` scattered through a
336
+ codebase is what this replaces: at 1em the icon takes the size of the text it
337
+ sits in — 13px beside `text-label`, 15px beside `text-ui` — and nobody picks a
338
+ number.
339
+
340
+ **The weight is not yours to pick either, but it is not one value.** `tone` names
341
+ what the icon is doing and the weight follows from it. There are three and there
342
+ is no fourth:
343
+
344
+ | `tone` | Weight | What it is |
345
+ | --- | --- | --- |
346
+ | `action` · the default | `regular` | An icon that is a control or names one. It is the system's line: 16 on a 256 grid = 0.0625em, against the document's 1.6 on a 24 grid = 0.0667em. Six per cent apart, which is no pixel on any screen |
347
+ | `current` | `fill` | The one of a set you are on — the sidebar item carrying `aria-current` |
348
+ | `quiet` | `light` | Furniture: a marker in a metadata row, not a control and not a state |
349
+
350
+ ```tsx
351
+ <SidebarItem href="/cursos" active icon={<Icon as={GraduationCap} tone="current" />}>
352
+ cursos
353
+ </SidebarItem>
354
+ ```
355
+
356
+ `current` is the one that earns the axis. An active item already paints itself
357
+ biolume, and colour on its own is the channel WCAG 1.4.1 says may not carry
358
+ meaning alone; the fill is the second channel, and it is the one that survives a
359
+ forced-colours mode. `weight` is deliberately **not** a prop: Phosphor ships six
360
+ and this system reads three, because `thin`, `bold` and `duotone` have no role
361
+ behind them here.
362
+
363
+ `@phosphor-icons/react` is an **optional** peer dependency. If your project uses
364
+ no icons you install nothing; two of the five do exactly that.
365
+
366
+ **An icon is not illustration, and the two never substitute for each other.**
367
+ Tiburoncín — the faces, the poses, the fin — is the mascot; it comes from
368
+ `./brand`, and the manual says where a face may appear: empty states,
369
+ confirmations, errors, course progress, celebration, and nowhere else. An icon is
370
+ functional vocabulary and goes wherever a control needs a label it cannot spell.
371
+ Do not put an icon where the system asks for a face, and do not put a face where
372
+ a control wants an icon.
373
+
374
+ **In Next, import from `@phosphor-icons/react/ssr` inside a Server Component.**
375
+ Phosphor's default build reads `IconContext` through `useContext`, and a hook in
376
+ a Server Component throws. It ships no `"use client"` to stop you, so the failure
377
+ arrives at render rather than at build. The `/ssr` entry is the same icons
378
+ without the context read, and `Icon` works with either.
379
+
380
+ See `docs/decisions.md` § 29 and § 35.
381
+
382
+ ### `Stat`'s delta says direction, not judgement
383
+
384
+ ```tsx
385
+ <Stat
386
+ label="alumnos"
387
+ value="1.284"
388
+ delta={{ value: '+12 esta semana', direction: 'up' }}
389
+ />
390
+ ```
391
+
392
+ `direction` picks the arrow and **never the colour**. «+12 alumnos» and «+12
393
+ errores» point the same way and mean opposite things, so whether a number is good
394
+ news is `tone`'s job and yours: `neutral` for a datum, `alert` when the number IS
395
+ the problem, `achievement` when it is the reward. `alert` and `achievement` paint
396
+ the same sand on purpose — the API is the meaning, the colour is the
397
+ implementation. See `docs/decisions.md` § 28.
398
+
399
+ `delta.value` arrives already formatted, like `value`: the library imposes no
400
+ locale and computes no percentage. `spark` is a `ReactNode` and the library ships
401
+ no sparkline — pass your own, exactly like `icon`.
402
+
403
+ **Do not colour the number.** A neutral `Stat` renders its value in primary ink,
404
+ and biolume goes on the icon badge and the sparkline instead: three accents in
405
+ one card and the figure stops being the loudest thing in it. `alert` and
406
+ `achievement` DO paint the number sand, which is how «this number is not just a
407
+ number» is said. See `docs/decisions.md` § 31.
408
+
409
+ **`icon` is a badge in the corner opposite the title**, in a circle tinted at
410
+ 10 % of the tone. You pass the glyph; the circle, the tint and the size are the
411
+ component's.
412
+
413
+ ### The two shapes of `EmptyState`
414
+
415
+ ```tsx
416
+ // The empty state IS the screen: a search with no results, a 404, a section
417
+ // with nothing in it yet. It carries the face, and `expression` is mandatory.
418
+ <EmptyState expression="waiting" title="Sin resultados" description="…" />
419
+
420
+ // The hole INSIDE something else: a table page, a dashboard widget. No face, no
421
+ // surface, no border — the table or the card already draws the region.
422
+ <EmptyState variant="inline" title="No hay lecciones en esta página" />
423
+
424
+ // ❌ does not compile: the props are a union, and `inline` has no face
425
+ <EmptyState variant="inline" expression="waiting" title="…" />
426
+ ```
427
+
428
+ `inline` takes an optional `icon` — a `ReactNode` the project passes and sizes,
429
+ at 1em and in `currentColor`, like `Stat`'s. The library ships no icons.
430
+
431
+ Do not reach for `page` inside a table because the face is «nicer»: an admin
432
+ screen with a dozen empty regions gets a dozen mascots, which is what made every
433
+ consuming project write its own empty state instead of using this one.
434
+
435
+ ### The social icons come from `./social`
436
+
437
+ ```tsx
438
+ // ❌ does not exist: the root publishes them grouped, not loose
164
439
  import { GitHub } from '@eduardoalvarez/arrecife';
165
440
 
166
- // ✅
441
+ // ✅ the normal form
442
+ import { GitHub } from '@eduardoalvarez/arrecife/social';
443
+
444
+ // ✅ for iterating the catalogue
167
445
  import { social } from '@eduardoalvarez/arrecife';
168
446
  <social.GitHub />
169
447
  ```
170
448
 
171
- Los ocho: `GitHub`, `LinkedIn`, `X`, `Instagram`, `Discord`, `YouTube`, `Rss`,
172
- `Correo`. Van bajo namespace porque uno se llama `X` y sueltos colisiona.
449
+ All nine: `GitHub`, `LinkedIn`, `X`, `Instagram`, `Discord`, `YouTube`, `Rss`,
450
+ `Email`, `Newsletter`. `Newsletter` is the bell: a way to follow, like `Rss`,
451
+ named for what it means.
173
452
 
174
- Los glifos internos —`Close`, `ChevronDown`, `Sol`— **no se exportan** y no van a
175
- exportarse: son el juego mínimo de los primitivos. Un componente que necesita un
176
- icono lo recibe por prop (`Stat` tiene `icon`, cada `Red` de `Footer` tiene el
177
- suyo). No pidas que se publiquen: pasa el tuyo.
453
+ **In a Server Component the subpath is mandatory, not preferred.** The root
454
+ carries `"use client"`, and a client reference crosses the boundary per EXPORT —
455
+ the properties of a plain object are not exports, so `social.LinkedIn` is
456
+ `undefined` on the server and `undefined` as an element type kills the build at
457
+ prerender. `./social` carries no directive: it renders on the server and ships no
458
+ client JS. Use `social` only when mapping a list of names onto icons.
459
+
460
+ The root keeps the group because one of them is called `X`, and loose at the root
461
+ it collides. In the subpath, alias it: `import { X as XIcon }`.
462
+
463
+ The internal glyphs — `Close`, `ChevronDown`, `Sun` — are **not exported** and
464
+ are not going to be: they are the primitives' minimum set. A component that needs
465
+ an icon receives it as a prop (`Stat` has `icon`, each `SocialLink` in `Footer`
466
+ has its own). Do not ask for them to be published: pass your own.
178
467
 
179
468
  ## Tokens
180
469
 
181
- La fuente es un objeto de TypeScript y la salida CSS se genera de él, así que el
182
- mismo valor está disponible en los dos sitios y no pueden discrepar.
470
+ The source is a TypeScript object and the CSS output is generated from it, so the
471
+ same value is available in both places and they cannot disagree.
183
472
 
184
- | Token | Custom property | Utilidad de Tailwind |
473
+ | Token | Custom property | Tailwind utility |
185
474
  | --- | --- | --- |
186
- | `colors[modo].surfaceRaised` | `--color-surface-raised` | `bg-surface-raised` |
475
+ | `colors[mode].surfaceRaised` | `--color-surface-raised` | `bg-surface-raised` |
187
476
  | `brand.hull` | `--color-brand-hull` | `bg-brand-hull` |
188
- | `typeScale.h1` | `--text-h1` | `text-h1` (arrastra line-height, weight y tracking) |
477
+ | `typeScale.h1` | `--text-h1` | `text-h1` (brings line-height, weight and tracking) |
189
478
  | `fonts.display` | `--font-display` | `font-display` |
190
479
  | `radius.card` | `--radius-card` | `rounded-card` |
191
480
  | `spacing.stepLg` | `--spacing-step-lg` | `p-step-lg`, `gap-step-lg`, `mb-step-lg` |
192
481
  | `spacing.section` | `--spacing-section` | `py-section`, `mb-section` |
193
482
  | `control.md` | `--spacing-control-md` | `px-control-md` |
194
483
  | `control.icon` | `--spacing-control-icon` | `size-control-icon` (42×42) |
195
- | `gradient[modo].hero` | `--gradient-hero` | `degradado-hero` |
196
- | `size.nav` | `--spacing-nav` | `h-nav` |
484
+ | `gradient[mode].hero` | `--gradient-hero` | `gradient-hero` |
485
+ | `size.nav` / `size.navCompact` | `--spacing-nav` / `--spacing-nav-compact` | `h-nav` / `h-nav-compact` |
197
486
  | `size.content` / `size.wide` | `--container-content` / `--container-wide` | `max-w-content` / `max-w-wide` |
198
487
  | `limits.measure` | `--container-measure` | `max-w-measure` |
199
488
  | `shadow.standard` | `--shadow-standard` | `shadow-standard` |
200
489
  | `motion` | `--duration-standard`, `--ease-standard` | `duration-standard`, `ease-standard` |
201
490
 
202
- `transition-standard` es la única transición del sistema y solo puede animar
203
- color y borde: así está escrita la utilidad.
204
-
205
- **Los escalones de espaciado llevan `step` en el nombre y no es opcional.**
206
- `p-md` no es una clase de Arrecife: en un proyecto con Tailwind v4 cae en la
207
- escala numérica y no hace nada visible. El ritmo de página es `p-step-md`,
208
- `gap-step-sm`, `py-step-xl`. Llevan prefijo porque `xs, sm, md, lg, xl` son los
209
- nombres de la escala `--container-*` de Tailwind, y un `--spacing-md` propio se
210
- comía `max-w-md` en todo el proyecto sin avisar de nada. `max-w-*`, `w-*` y `h-*`
211
- son de Tailwind y se usan tal cual. Guía de migración desde la 0.2.0:
212
- <https://github.com/Proskynete/arrecife/blob/main/docs/migracion-0.3.md>.
213
-
214
- ## Reglas del sistema que el código consumidor no debe romper
215
-
216
- Son decisiones de identidad, ya medidas. Romperlas produce código que compila y
217
- se ve mal, o que falla la auditoría de accesibilidad del proyecto.
218
-
219
- 1. **Cero hex literales.** Todo color sale de un token o de su custom property.
220
- 2. **`Button variant="conversion"` va una sola vez por pantalla.** No se fuerza en
221
- runtime; dos en la misma página son un error de diseño.
222
- 3. **No hay variante de peligro en `Button`.** El error del sistema vive en los
223
- avisos y en la validación de campo, no en un botón rojo.
224
- 4. **`secondary` nunca se rellena el fondo.** Es borde y texto.
225
- 5. **Sin animaciones de entrada.** Modales, menús, tooltips y toasts aparecen
226
- donde van a quedarse. La única excepción es el spinner de `Button loading`.
227
- 6. **La semántica y la escala son independientes.** Un `h2` que debe verse
228
- pequeño es `<Text as="h2" variant="h3">`, nunca un `h3` que miente sobre la
229
- jerarquía.
230
- 7. **`textMuted` no va nunca sobre `surfaceRaised`**: da 4.07 en oscuro. Sobre
231
- superficie elevada —menús, tabs activos— el token es `textSecondary`.
232
- 8. **Un fondo teñido con un color semántico lleva texto de token de texto**, no
233
- del color semántico. El color se queda en el borde y en el glifo. Poner
234
- `accent` sobre su propio tinte al 8 % da 4.12 y no llega a AA.
235
- 9. **`Progress` exige `label`.** Una barra sin nombre accesible no dice de qué es.
236
- 10. **`Button size="icon"` exige `aria-label`.** No lleva texto.
237
- 11. **Las caras de la mascota solo aparecen** en estados vacíos, confirmaciones,
238
- errores, progreso de curso y celebración. Nunca en hero, precios, servicios,
239
- contacto ni CV.
240
- 12. **La aleta no es un parámetro libre**: `espuma` sobre fondo oscuro, `color`
241
- sobre fondo claro. Los componentes ya la eligen por el fondo.
242
-
243
- ## Lo que la librería NO hace, a propósito
244
-
245
- Estas son las confusiones que más veces se cometen al consumirla.
246
-
247
- - **No trae Shiki.** Publica el *tema*, no el resaltador. `CodeBlock` recibe el
248
- código **ya resaltado** por la herramienta del proyecto.
249
- - **No formatea fechas.** `ArticleCard`, `TalkCard` y compañía reciben la fecha ya
250
- formateada por el proyecto: la librería no impone locale. `dateTime` es aparte,
251
- en ISO, para el atributo del `<time>`.
252
- - **`NewsletterForm` no hace el POST.** Es presentacional: recibe `state` y emite
253
- `onSubmitEmail`. La llamada la hace el proyecto con su proveedor.
254
- - **No trae enrutador.** Los componentes con enlaces aceptan `asChild` para
255
- envolver el `Link` del framework.
256
- - **No carga fuentes.** Las declara por nombre.
257
- - **No hay preset de Tailwind v3.**
258
-
259
- ## Patrones de uso
491
+ `transition-standard` is the system's only transition and it can only animate
492
+ color and border: that is how the utility is written.
493
+
494
+ **The spacing steps carry `step` in the name and it is not optional.** `p-md` is
495
+ not an Arrecife class: in a Tailwind v4 project it lands on the numeric scale and
496
+ does nothing visible. Page rhythm is `p-step-md`, `gap-step-sm`, `py-step-xl`.
497
+ They carry a prefix because `xs, sm, md, lg, xl` are the names of Tailwind's
498
+ `--container-*` scale, and a `--spacing-md` of our own was swallowing `max-w-md`
499
+ across the whole project with nothing warning about it. `max-w-*`, `w-*` and
500
+ `h-*` belong to Tailwind and are used as they are. Migration guide from 0.2.0:
501
+ <https://github.com/Proskynete/arrecife/blob/main/docs/migration-0.3.md>.
502
+
503
+ ## System rules the consuming code must not break
504
+
505
+ These are identity decisions, already measured. Breaking them produces code that
506
+ compiles and looks wrong, or that fails the project's accessibility audit.
507
+
508
+ 1. **Zero literal hexes.** Every color comes from a token or its custom property.
509
+ 2. **`Button variant="conversion"` appears once per screen.** It is not enforced
510
+ at runtime; two on the same page are a design error.
511
+ 3. **`Button variant="destructive"` is for the irreversible only.** Never for
512
+ «cancel» on a form, and not inside an `AlertDialog` — there the confirm button
513
+ stays `primary`, because the title, the focus on cancel and the no-click-outside
514
+ already carry the weight. See `docs/decisions.md` § 21.
515
+ 4. **`secondary` is never filled.** It is border and text.
516
+ 5. **No entrance animations.** Modals, menus, tooltips and toasts appear where
517
+ they will stay. The only exception is the `Button loading` spinner.
518
+ 6. **Semantics and scale are independent.** An `h2` that has to look small is
519
+ `<Text as="h2" variant="h3">`, never an `h3` that lies about the hierarchy.
520
+ 7. **`textMuted` never goes over `surfaceRaised`**: it gives 4.07 in dark. Over a
521
+ raised surface — menus, active tabs — the token is `textSecondary`.
522
+ 8. **A background tinted with a semantic color carries text from a text token**,
523
+ not from the semantic color. The color stays on the border and on the glyph.
524
+ Putting `accent` over its own tint at 8 % gives 4.12 and does not reach AA.
525
+ 9. **`Progress` requires `label`.** A bar with no accessible name does not say
526
+ what it is about.
527
+ 10. **`Button size="icon"` and `size="icon-sm"` require `aria-label`.** They carry
528
+ no text. `icon` is 42×42 and is a page action; `icon-sm` is 32×32 and is for a
529
+ dense table row.
530
+ 11. **The mascot's faces only appear** in empty states, confirmations, errors,
531
+ course progress and celebration. Never in a hero, pricing, services, contact
532
+ or the CV.
533
+ **And not in every empty state either**: `EmptyState variant="inline"` is the
534
+ hole inside a table page or a dashboard widget, and it carries no face — the
535
+ type does not accept one. `page`, the default, is the one that IS the screen,
536
+ and there `expression` stays mandatory. A dozen mascots on one admin screen is
537
+ not the humour contract. See `docs/decisions.md` § 27.
538
+ 12. **The fin is not a free parameter**: `foam` on a dark background, `color` on a
539
+ light one. The components already choose it from the background.
540
+
541
+ ## What the library does NOT do, on purpose
542
+
543
+ These are the confusions people run into most often when consuming it.
544
+
545
+ - **It does not ship Shiki.** It publishes the *theme*, not the highlighter.
546
+ `CodeBlock` receives the code **already highlighted** by the project's tool.
547
+ - **It does not format dates.** `ArticleCard`, `TalkCard` and company receive the
548
+ date already formatted by the project: the library imposes no locale.
549
+ `dateTime` is separate, in ISO, for the `<time>` attribute.
550
+ - **`NewsletterForm` does not do the POST.** It is presentational: it takes
551
+ `state` and emits `onSubmitEmail`. The call is made by the project with its own
552
+ provider.
553
+ - **It ships no router.** The components with links accept `asChild` to wrap the
554
+ framework's `Link`.
555
+ - **It ships no `data-testid`.** A composed part your test suite has to reach is
556
+ reached with a slot: `ArticleCard`'s `tagAsChild`, `Breadcrumb`'s and
557
+ `TableOfContents`'s `linkAsChild`. They hand you the element and its
558
+ attributes and keep the classes. Do NOT select by structure or by a style
559
+ class — a style class is not a contract and it changes when the style does.
560
+ - **It does not load fonts.** It declares them by name.
561
+ - **There is no Tailwind v3 preset.**
562
+
563
+ ## Usage patterns
260
564
 
261
565
  ```tsx
262
566
  import { Text, Button, Badge, cn } from '@eduardoalvarez/arrecife';
@@ -264,12 +568,12 @@ import type { TextProps } from '@eduardoalvarez/arrecife';
264
568
 
265
569
  <Text variant="eyebrow" tone="muted">charlas</Text>
266
570
  <Text as="h2" variant="h1">Escalar con criterio</Text>
267
- <Text variant="body">Se corta solo a 68ch.</Text>
268
- <Text variant="ui" measure={false}>Sin corte, para una celda estrecha.</Text>
571
+ <Text variant="body">Clamps itself to 68ch.</Text>
572
+ <Text variant="ui" measure={false}>No clamp, for a narrow cell.</Text>
269
573
  ```
270
574
 
271
- `asChild` renderiza el hijo en vez del elemento propio. Es como se envuelve el
272
- enlace del framework sin perder los estilos:
575
+ `asChild` renders the child instead of the component's own element. It is how the
576
+ framework's link gets wrapped without losing the styles:
273
577
 
274
578
  ```tsx
275
579
  <Button asChild>
@@ -277,23 +581,23 @@ enlace del framework sin perder los estilos:
277
581
  </Button>
278
582
  ```
279
583
 
280
- `cn` es `clsx` + `tailwind-merge`. Se usa para componer `className` sin que dos
281
- utilidades del mismo grupo peleen.
584
+ `cn` is `clsx` + `tailwind-merge`. It is used to compose `className` without two
585
+ utilities from the same group fighting each other.
282
586
 
283
- Open Graph, sin React:
587
+ Open Graph, without React:
284
588
 
285
589
  ```ts
286
590
  import satori from 'satori';
287
- import { plantillaArticulo, OG } from '@eduardoalvarez/arrecife/og';
591
+ import { articleTemplate, OG } from '@eduardoalvarez/arrecife/og';
288
592
 
289
- const svg = await satori(plantillaArticulo({ title, date, readingMinutes }), {
593
+ const svg = await satori(articleTemplate({ title, date, readingMinutes }), {
290
594
  width: OG.width, // 1200
291
595
  height: OG.height, // 630
292
596
  fonts: [...],
293
597
  });
294
598
  ```
295
599
 
296
- Resaltado de sintaxis, desde la configuración del sitio:
600
+ Syntax highlighting, from the site's configuration:
297
601
 
298
602
  ```ts
299
603
  import { arrecife } from '@eduardoalvarez/arrecife/shiki';
@@ -303,1253 +607,1348 @@ export default defineConfig({
303
607
  });
304
608
  ```
305
609
 
306
- # Inventario
610
+ # Inventory
307
611
 
308
- Lo que sigue sale del compilador de TypeScript en cada build. Solo se listan los
309
- props **declarados por la librería**: los heredados de un elemento HTML o de una
310
- primitiva de Radix se resumen en la línea `Extiende`, y son los de siempre.
612
+ What follows comes out of the TypeScript compiler on every build. Only the props
613
+ **declared by the library** are listed: the ones inherited from an HTML element
614
+ or from a Radix primitive are summarised in the `Extends` line, and they are the
615
+ usual ones.
311
616
 
312
- ## Primitivos
617
+ ## Primitives
313
618
 
314
- Se importan de `@eduardoalvarez/arrecife`. 110 exportaciones.
619
+ Imported from `@eduardoalvarez/arrecife`. 110 exports.
315
620
 
316
621
  ### Accordion, AccordionItem, AccordionTrigger, AccordionContent
317
622
 
318
- Fuente: `src/primitives/accordion.tsx`
623
+ Source: `src/primitives/accordion.tsx`
319
624
 
320
625
  **Accordion**
321
- El plegable. Lo pedían dos proyectos: el FAQ del portafolio y el temario de cursos, que es literalmente una lista de secciones que se abren.
626
+ The disclosure. Two projects asked for it: the portfolio FAQ and the course syllabus, which is literally a list of sections that open.
322
627
 
323
- - Extiende: `ComponentPropsWithoutRef<typeof AccordionPrimitive.Root>`
324
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
628
+ - Extends: `ComponentPropsWithoutRef<typeof AccordionPrimitive.Root>`
629
+ - No own props: it passes through those of the element or primitive it wraps.
325
630
 
326
631
  **AccordionItem**
327
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
632
+ - No own props: it passes through those of the element or primitive it wraps.
328
633
 
329
634
  **AccordionTrigger**
330
- El disparador es el encabezado, así que va DENTRO de un `<h3>`: Radix envuelve el botón en `AccordionPrimitive.Header`, que renderiza el elemento que se le pida. Sin eso, un lector de pantalla ve una lista de botones sueltos y pierde la estructura de la página, que es justo lo que un FAQ necesita conservar.
635
+ The trigger IS the heading, so it goes INSIDE an `<h3>`: Radix wraps the button in `AccordionPrimitive.Header`, which renders whichever element you ask of it. Without that, a screen reader sees a list of loose buttons and loses the page structure, which is precisely what a FAQ needs to keep.
331
636
 
332
- - Extiende: `ComponentPropsWithoutRef< typeof AccordionPrimitive.Trigger >`
637
+ - Extends: `ComponentPropsWithoutRef< typeof AccordionPrimitive.Trigger >`
333
638
 
334
- | prop | tipo | req. | defecto | qué hace |
639
+ | prop | type | req. | default | what it does |
335
640
  | --- | --- | --- | --- | --- |
336
- | `headingLevel` | `4 \| 2 \| 3` | | `3` | Nivel del encabezado que envuelve al disparador. |
641
+ | `headingLevel` | `4 \| 2 \| 3` | | `3` | The level of the heading wrapping the trigger. |
337
642
 
338
643
  **AccordionContent**
339
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
644
+ - No own props: it passes through those of the element or primitive it wraps.
340
645
 
341
646
  ### AlertDialogOverlay, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogCancel, AlertDialogAction, AlertDialog, AlertDialogTrigger
342
647
 
343
- Fuente: `src/primitives/alert-dialog.tsx`
648
+ Source: `src/primitives/alert-dialog.tsx`
344
649
 
345
650
  **AlertDialogOverlay**
346
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
651
+ - No own props: it passes through those of the element or primitive it wraps.
347
652
 
348
653
  **AlertDialogContent**
349
- Sin entrada animada, igual que `Dialog`: aparece donde va a quedarse.
654
+ No entrance animation, same as `Dialog`: it appears where it will stay.
350
655
 
351
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
656
+ - No own props: it passes through those of the element or primitive it wraps.
352
657
 
353
658
  **AlertDialogHeader**
354
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
659
+ - No own props: it passes through those of the element or primitive it wraps.
355
660
 
356
661
  **AlertDialogFooter**
357
- Cancelar a la IZQUIERDA de confirmar en escritorio y ABAJO en móvil, que es lo que da `flex-col-reverse`: el orden del DOM pone cancelar primero —es donde va el foco— y en columna el dedo lo encuentra donde toca sin cambiar la tabulación.
662
+ Cancel to the LEFT of confirm on desktop and BELOW it on mobile, which is what `flex-col-reverse` gives: the DOM order puts cancel first — that is where focus goes — and in a column the thumb finds it where it should be without changing the tab order.
358
663
 
359
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
664
+ - No own props: it passes through those of the element or primitive it wraps.
360
665
 
361
666
  **AlertDialogTitle**
362
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
667
+ - No own props: it passes through those of the element or primitive it wraps.
363
668
 
364
669
  **AlertDialogDescription**
365
- Lo que se pierde, dicho entero. Es lo que el rol `alertdialog` hace que se anuncie de entrada, así que aquí no va «esta acción no se puede deshacer» suelto: va qué se borra y qué se lleva por delante.
670
+ What is lost, spelled out. The `alertdialog` role makes this get announced up front, so «this action cannot be undone» does not go here on its own: what goes here is what gets deleted and what it takes down with it.
366
671
 
367
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
672
+ - No own props: it passes through those of the element or primitive it wraps.
368
673
 
369
674
  **AlertDialogCancel**
370
- El que se lleva el foco al abrir.
675
+ The one that takes focus on open.
371
676
 
372
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
677
+ - No own props: it passes through those of the element or primitive it wraps.
373
678
 
374
679
  **AlertDialogAction**
375
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
680
+ - No own props: it passes through those of the element or primitive it wraps.
376
681
 
377
682
  **AlertDialog**
378
- La confirmación destructiva. NO es un `Dialog` con otro texto, y por eso está en su propio archivo y sobre su propia primitiva de Radix.
683
+ The destructive confirmation. It is NOT a `Dialog` with different text, which is why it lives in its own file and on its own Radix primitive.
379
684
 
380
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
685
+ - No own props: it passes through those of the element or primitive it wraps.
381
686
 
382
687
  **AlertDialogTrigger**
383
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
688
+ - No own props: it passes through those of the element or primitive it wraps.
384
689
 
385
690
  ### Alert
386
691
 
387
- Fuente: `src/primitives/alert.tsx`
692
+ Source: `src/primitives/alert.tsx`
388
693
 
389
- El aviso lleva el color en el fondo, no solo en el borde.
694
+ - Extends: `Omit<ComponentPropsWithoutRef<'div'>, 'title'> & VariantProps<typeof alert>`
390
695
 
391
- - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'title'> & VariantProps<typeof alert>`
392
-
393
- | prop | tipo | req. | defecto | qué hace |
696
+ | prop | type | req. | default | what it does |
394
697
  | --- | --- | --- | --- | --- |
395
- | `enfasis` | `"sutil" \| "fuerte"` | | `sutil` | |
396
- | `icon` | `ReactNode` | | | Sustituye el glifo mono de la variante. Nunca un emoji: si necesitas otra cosa, es un SVG de `glyphs`. |
698
+ | `emphasis` | `"subtle" \| "strong"` | | | |
699
+ | `icon` | `ReactNode` | | | Replaces the variant's mono glyph. Never an emoji: if you need something else, it is an SVG from `glyphs`. |
397
700
  | `title` | `ReactNode` | | | |
398
- | `variant` | `"accent" \| "success" \| "warning" \| "error"` | | `accent` | |
701
+ | `variant` | `"accent" \| "success" \| "warning" \| "error"` | | | |
399
702
 
400
703
  ### Avatar, AvatarImage, AvatarFallback, AvatarUpload
401
704
 
402
- Fuente: `src/primitives/avatar.tsx`
705
+ Source: `src/primitives/avatar.tsx`
403
706
 
404
707
  **Avatar**
405
- Uno solo para todo: la foto del autor y la de cualquier persona del sistema. No hay un `brand/Avatar` aparte — una foto de perfil con la piel de la marca es exactamente esto con un `src` distinto.
708
+ One for everything: the author's photo and anyone else's in the system. There is no separate `brand/Avatar` — a profile photo wearing the brand's skin is exactly this with a different `src`.
406
709
 
407
- - Extiende: `ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & VariantProps<typeof avatar>`
710
+ - Extends: `ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & VariantProps<typeof avatar>`
408
711
 
409
- | prop | tipo | req. | defecto | qué hace |
712
+ | prop | type | req. | default | what it does |
410
713
  | --- | --- | --- | --- | --- |
411
- | `size` | `"sm" \| "md" \| "lg" \| "xl"` | | `md` | |
714
+ | `size` | `"sm" \| "md" \| "lg" \| "xl"` | | | |
412
715
 
413
716
  **AvatarImage**
414
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
717
+ - No own props: it passes through those of the element or primitive it wraps.
415
718
 
416
719
  **AvatarFallback**
417
- Iniciales mientras la imagen carga, o cuando no hay imagen.
720
+ Initials while the image loads, or when there is no image.
418
721
 
419
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
722
+ - No own props: it passes through those of the element or primitive it wraps.
420
723
 
421
724
  **AvatarUpload**
422
- El avatar que se puede cambiar. `Avatar` muestra; este además deja elegir.
725
+ The avatar you can change. `Avatar` displays; this one also lets you pick.
423
726
 
424
- - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'onSelect' \| 'children'> & VariantProps<typeof avatar>`
727
+ - Extends: `Omit<ComponentPropsWithoutRef<'div'>, 'onSelect' \| 'children'> & VariantProps<typeof avatar>`
425
728
 
426
- | prop | tipo | req. | defecto | qué hace |
729
+ | prop | type | req. | default | what it does |
427
730
  | --- | --- | --- | --- | --- |
428
- | `accept` | `string` | | `image/*` | Qué acepta el diálogo del sistema. |
731
+ | `accept` | `string` | | `image/*` | What the system dialog accepts. |
429
732
  | `disabled` | `boolean \| undefined` | | `false` | |
430
- | `fallback` | `ReactNode` | | | Iniciales mientras no hay imagen. |
431
- | `label` | `string` | | `Cambiar la foto` | Nombre accesible del control. Es lo único que lo nombra: no hay texto visible. |
432
- | `onSelectFile` | `(archivo: File) => void` | | | Se dispara con el archivo elegido. La subida la hace el proyecto. |
733
+ | `fallback` | `ReactNode` | | | Initials while there is no image. |
734
+ | `label` | `string` | | `Cambiar la foto` | The control's accessible name. It is the only thing naming it: there is no visible text. |
735
+ | `onSelectFile` | `(file: File) => void` | | | Fires with the chosen file. The upload is the project's job. |
433
736
  | `size` | `"sm" \| "md" \| "lg" \| "xl"` | | | |
434
- | `src` | `string` | | | La imagen actual, ya subida. La previsualización local la gana mientras dure. |
737
+ | `src` | `string` | | | The current image, already uploaded. The local preview beats it while it lasts. |
435
738
 
436
739
  ### Badge, CategoryBadge, MetricBadge
437
740
 
438
- Fuente: `src/primitives/badge.tsx`
741
+ Source: `src/primitives/badge.tsx`
439
742
 
440
743
  **Badge**
441
- Semáforo. Cuadrada r6, sans 12.5/500 y fondo al 8 % del semántico — la receta del aviso en tamaño de palabra: un estado es un aviso de una sola palabra.
744
+ Three badge families, three components. Why there are three shapes and not one is in `variants/badge.ts`, next to the classes that make them.
442
745
 
443
- - Extiende: `ComponentPropsWithoutRef<'span'> & VariantProps<typeof badge>`
746
+ - Extends: `ComponentPropsWithoutRef<'span'> & VariantProps<typeof badge>`
444
747
 
445
- | prop | tipo | req. | defecto | qué hace |
748
+ | prop | type | req. | default | what it does |
446
749
  | --- | --- | --- | --- | --- |
447
- | `variant` | `"accent" \| "success" \| "warning" \| "error" \| "neutral" \| "warm"` | | `neutral` | |
750
+ | `variant` | `"neutral" \| "accent" \| "warm" \| "success" \| "warning" \| "error"` | | | |
448
751
 
449
752
  **CategoryBadge**
450
- Un slug, en arena. Sin transformar: los slugs ya vienen en minúscula y forzarla sería el mismo error que forzaba el `uppercase`.
753
+ - Extends: `ComponentPropsWithoutRef<'span'>`
451
754
 
452
- - Extiende: `ComponentPropsWithoutRef<'span'>`
453
-
454
- | prop | tipo | req. | defecto | qué hace |
755
+ | prop | type | req. | default | what it does |
455
756
  | --- | --- | --- | --- | --- |
456
- | `active` | `boolean \| undefined` | | `false` | Filtro seleccionado: arena sólido con tinta encima. |
757
+ | `active` | `boolean \| undefined` | | `false` | Selected filter: solid sand with ink on top. |
457
758
 
458
759
  **MetricBadge**
459
- - Extiende: `ComponentPropsWithoutRef<'span'>`
760
+ - Extends: `ComponentPropsWithoutRef<'span'>`
460
761
 
461
- | prop | tipo | req. | defecto | qué hace |
762
+ | prop | type | req. | default | what it does |
462
763
  | --- | --- | --- | --- | --- |
463
- | `boxed` | `boolean \| undefined` | | `false` | Añade el aro de hairline. Por defecto la métrica va sin caja. |
764
+ | `boxed` | `boolean \| undefined` | | `false` | Adds the hairline ring. By default a metric carries no box. |
464
765
 
465
766
  ### Button
466
767
 
467
- Fuente: `src/primitives/button.tsx`
468
-
469
- Las CUATRO variantes del sistema, y solo esas cuatro.
768
+ Source: `src/primitives/button.tsx`
470
769
 
471
- - Extiende: `ComponentPropsWithoutRef<'button'> & VariantProps<typeof button>`
770
+ - Extends: `ComponentPropsWithoutRef<'button'> & VariantProps<typeof button>`
472
771
 
473
- | prop | tipo | req. | defecto | qué hace |
772
+ | prop | type | req. | default | what it does |
474
773
  | --- | --- | --- | --- | --- |
475
- | `asChild` | `boolean` | | `false` | Renderiza el hijo en vez de un `<button>`, para envolver un enlace. |
476
- | `icon` | `ReactNode` | | | Glifo SVG antes del texto. Se oculta mientras carga. |
477
- | `loading` | `boolean` | | `false` | Deshabilita y anuncia `aria-busy`. Incompatible con `asChild`. |
478
- | `size` | `"sm" \| "md" \| "lg" \| "icon"` | | `md` | |
479
- | `variant` | `"primary" \| "conversion" \| "secondary" \| "tertiary"` | | `primary` | |
774
+ | `asChild` | `boolean` | | `false` | Renders the child instead of a `<button>`, to wrap a link. |
775
+ | `icon` | `ReactNode` | | | SVG glyph before the text. Hidden while loading. |
776
+ | `loading` | `boolean` | | `false` | Disables and announces `aria-busy`. Incompatible with `asChild`. |
777
+ | `size` | `"sm" \| "md" \| "lg" \| "icon" \| "icon-sm"` | | | |
778
+ | `variant` | `"primary" \| "conversion" \| "secondary" \| "tertiary" \| "destructive" \| "destructiveOutline"` | | | |
480
779
 
481
780
  ### Calendar
482
781
 
483
- Fuente: `src/primitives/calendar.tsx`
782
+ Source: `src/primitives/calendar.tsx`
484
783
 
485
- Calendario mensual navegable, sobre `react-day-picker`.
784
+ A navigable month calendar, on top of `react-day-picker`.
486
785
 
487
- - Extiende: `ComponentProps<typeof DayPicker>`
786
+ - Extends: `ComponentProps<typeof DayPicker>`
488
787
 
489
- | prop | tipo | req. | defecto | qué hace |
788
+ | prop | type | req. | default | what it does |
490
789
  | --- | --- | --- | --- | --- |
491
- | `fullWidth` | `boolean \| undefined` | | `false` | Estira el calendario hasta ocupar todo el ancho de su contenedor, con las celdas repartiéndoselo a partes iguales. |
790
+ | `fullWidth` | `boolean \| undefined` | | `false` | Stretches the calendar to fill its container's whole width, with the cells splitting it evenly. |
492
791
 
493
792
  ### Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter
494
793
 
495
- Fuente: `src/primitives/card.tsx`
794
+ Source: `src/primitives/card.tsx`
496
795
 
497
796
  **Card**
498
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
797
+ - No own props: it passes through those of the element or primitive it wraps.
499
798
 
500
799
  **CardHeader**
501
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
800
+ - No own props: it passes through those of the element or primitive it wraps.
502
801
 
503
802
  **CardTitle**
504
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
803
+ - No own props: it passes through those of the element or primitive it wraps.
505
804
 
506
805
  **CardDescription**
507
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
806
+ - No own props: it passes through those of the element or primitive it wraps.
508
807
 
509
808
  **CardContent**
510
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
809
+ - No own props: it passes through those of the element or primitive it wraps.
511
810
 
512
811
  **CardFooter**
513
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
812
+ - No own props: it passes through those of the element or primitive it wraps.
514
813
 
515
814
  ### Checkbox
516
815
 
517
- Fuente: `src/primitives/checkbox.tsx`
816
+ Source: `src/primitives/checkbox.tsx`
518
817
 
519
- - Extiende: `ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>`
520
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
818
+ - Extends: `ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>`
819
+ - No own props: it passes through those of the element or primitive it wraps.
521
820
 
522
821
  ### Code
523
822
 
524
- Fuente: `src/primitives/code.tsx`
823
+ Source: `src/primitives/code.tsx`
525
824
 
526
- Código en línea, dentro de prosa.
825
+ Inline code, inside prose.
527
826
 
528
- - Extiende: `ComponentPropsWithoutRef<'code'>`
529
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
827
+ - Extends: `ComponentPropsWithoutRef<'code'>`
828
+ - No own props: it passes through those of the element or primitive it wraps.
530
829
 
531
830
  ### DateField
532
831
 
533
- Fuente: `src/primitives/date-field.tsx`
832
+ Source: `src/primitives/date-field.tsx`
534
833
 
535
- Un campo de fecha sobre el control nativo, no sobre un calendario propio.
834
+ A date field on the native control, not on a calendar of our own.
536
835
 
537
- - Extiende: `Omit<ComponentPropsWithoutRef<'input'>, 'type'>`
836
+ - Extends: `Omit<ComponentPropsWithoutRef<'input'>, 'type'>`
538
837
 
539
- | prop | tipo | req. | defecto | qué hace |
838
+ | prop | type | req. | default | what it does |
540
839
  | --- | --- | --- | --- | --- |
541
840
  | `invalid` | `boolean \| undefined` | | `false` | |
542
- | `withTime` | `boolean \| undefined` | | `false` | Añade la hora al campo. Es el `datetime-local` nativo. |
841
+ | `withTime` | `boolean \| undefined` | | `false` | Adds the time to the field. It is the native `datetime-local`. |
543
842
 
544
843
  ### DialogOverlay, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, Dialog, DialogTrigger, DialogClose
545
844
 
546
- Fuente: `src/primitives/dialog.tsx`
845
+ Source: `src/primitives/dialog.tsx`
547
846
 
548
847
  **DialogOverlay**
549
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
848
+ - No own props: it passes through those of the element or primitive it wraps.
550
849
 
551
850
  **DialogContent**
552
- Sin entrada animada: no hay escala ni desplazamiento en el sistema.
851
+ No entrance animation: there is no scale or displacement in the system.
553
852
 
554
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
853
+ - No own props: it passes through those of the element or primitive it wraps.
555
854
 
556
855
  **DialogHeader**
557
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
856
+ - No own props: it passes through those of the element or primitive it wraps.
558
857
 
559
858
  **DialogFooter**
560
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
859
+ - No own props: it passes through those of the element or primitive it wraps.
561
860
 
562
861
  **DialogTitle**
563
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
862
+ - No own props: it passes through those of the element or primitive it wraps.
564
863
 
565
864
  **DialogDescription**
566
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
865
+ - No own props: it passes through those of the element or primitive it wraps.
567
866
 
568
867
  **Dialog**
569
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
868
+ - No own props: it passes through those of the element or primitive it wraps.
570
869
 
571
870
  **DialogTrigger**
572
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
871
+ - No own props: it passes through those of the element or primitive it wraps.
573
872
 
574
873
  **DialogClose**
575
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
874
+ - No own props: it passes through those of the element or primitive it wraps.
576
875
 
577
876
  ### DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenu, DropdownMenuTrigger, DropdownMenuGroup, DropdownMenuRadioGroup, DropdownMenuSub
578
877
 
579
- Fuente: `src/primitives/dropdown-menu.tsx`
878
+ Source: `src/primitives/dropdown-menu.tsx`
580
879
 
581
880
  **DropdownMenuContent**
582
- Sin animación de entrada: el menú aparece, no se despliega.
881
+ No entrance animation: the menu appears, it does not unfold.
583
882
 
584
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
883
+ - No own props: it passes through those of the element or primitive it wraps.
585
884
 
586
885
  **DropdownMenuItem**
587
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
886
+ - No own props: it passes through those of the element or primitive it wraps.
588
887
 
589
888
  **DropdownMenuCheckboxItem**
590
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
889
+ - No own props: it passes through those of the element or primitive it wraps.
591
890
 
592
891
  **DropdownMenuRadioItem**
593
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
892
+ - No own props: it passes through those of the element or primitive it wraps.
594
893
 
595
894
  **DropdownMenuLabel**
596
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
895
+ - No own props: it passes through those of the element or primitive it wraps.
597
896
 
598
897
  **DropdownMenuSeparator**
599
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
898
+ - No own props: it passes through those of the element or primitive it wraps.
600
899
 
601
900
  **DropdownMenuSubTrigger**
602
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
901
+ - No own props: it passes through those of the element or primitive it wraps.
603
902
 
604
903
  **DropdownMenuSubContent**
605
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
904
+ - No own props: it passes through those of the element or primitive it wraps.
606
905
 
607
906
  **DropdownMenu**
608
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
907
+ - No own props: it passes through those of the element or primitive it wraps.
609
908
 
610
909
  **DropdownMenuTrigger**
611
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
910
+ - No own props: it passes through those of the element or primitive it wraps.
612
911
 
613
912
  **DropdownMenuGroup**
614
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
913
+ - No own props: it passes through those of the element or primitive it wraps.
615
914
 
616
915
  **DropdownMenuRadioGroup**
617
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
916
+ - No own props: it passes through those of the element or primitive it wraps.
618
917
 
619
918
  **DropdownMenuSub**
620
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
919
+ - No own props: it passes through those of the element or primitive it wraps.
621
920
 
622
921
  ### Input
623
922
 
624
- Fuente: `src/primitives/input.tsx`
923
+ Source: `src/primitives/input.tsx`
625
924
 
626
- - Extiende: `ComponentPropsWithoutRef<'input'>`
925
+ - Extends: `ComponentPropsWithoutRef<'input'>`
627
926
 
628
- | prop | tipo | req. | defecto | qué hace |
927
+ | prop | type | req. | default | what it does |
629
928
  | --- | --- | --- | --- | --- |
630
- | `invalid` | `boolean` | | `false` | Marca el control como inválido y tiñe el borde. |
929
+ | `invalid` | `boolean` | | `false` | Marks the control as invalid and tints the border. |
631
930
 
632
931
  ### Label
633
932
 
634
- Fuente: `src/primitives/label.tsx`
933
+ Source: `src/primitives/label.tsx`
635
934
 
636
- La escala `label`: 13px, que es el mínimo absoluto en pantalla del sistema.
935
+ The `label` scale: 13px, which is the system's absolute minimum on screen.
637
936
 
638
- - Extiende: `ComponentPropsWithoutRef<typeof LabelPrimitive.Root>`
639
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
937
+ - Extends: `ComponentPropsWithoutRef<typeof LabelPrimitive.Root>`
938
+ - No own props: it passes through those of the element or primitive it wraps.
640
939
 
641
940
  ### Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationPrevious, PaginationNext, PaginationEllipsis
642
941
 
643
- Fuente: `src/primitives/pagination.tsx`
942
+ Source: `src/primitives/pagination.tsx`
644
943
 
645
944
  **Pagination**
646
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
945
+ - No own props: it passes through those of the element or primitive it wraps.
647
946
 
648
947
  **PaginationContent**
649
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
948
+ - No own props: it passes through those of the element or primitive it wraps.
650
949
 
651
950
  **PaginationItem**
652
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
951
+ - No own props: it passes through those of the element or primitive it wraps.
653
952
 
654
953
  **PaginationLink**
655
- - Extiende: `ComponentPropsWithoutRef<'a'>`
954
+ - Extends: `ComponentPropsWithoutRef<'a'>`
656
955
 
657
- | prop | tipo | req. | defecto | qué hace |
956
+ | prop | type | req. | default | what it does |
658
957
  | --- | --- | --- | --- | --- |
659
958
  | `isActive` | `boolean` | | `false` | |
660
959
 
661
960
  **PaginationPrevious**
662
961
 
663
- | prop | tipo | req. | defecto | qué hace |
962
+ | prop | type | req. | default | what it does |
664
963
  | --- | --- | --- | --- | --- |
665
964
  | `isActive` | `boolean` | | | |
666
965
 
667
966
  **PaginationNext**
668
967
 
669
- | prop | tipo | req. | defecto | qué hace |
968
+ | prop | type | req. | default | what it does |
670
969
  | --- | --- | --- | --- | --- |
671
970
  | `isActive` | `boolean` | | | |
672
971
 
673
972
  **PaginationEllipsis**
674
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
973
+ - No own props: it passes through those of the element or primitive it wraps.
675
974
 
676
975
  ### PopoverContent, Popover, PopoverTrigger, PopoverAnchor
677
976
 
678
- Fuente: `src/primitives/popover.tsx`
977
+ Source: `src/primitives/popover.tsx`
679
978
 
680
979
  **PopoverContent**
681
- Sin animación de entrada: aparece donde va a quedarse, como el resto.
980
+ No entrance animation: it appears where it will stay, like the rest.
682
981
 
683
- - Extiende: `ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & Etiquetado`
684
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
982
+ - Extends: `ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & Labelled`
983
+ - No own props: it passes through those of the element or primitive it wraps.
685
984
 
686
985
  **Popover**
687
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
986
+ - No own props: it passes through those of the element or primitive it wraps.
688
987
 
689
988
  **PopoverTrigger**
690
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
989
+ - No own props: it passes through those of the element or primitive it wraps.
691
990
 
692
991
  **PopoverAnchor**
693
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
992
+ - No own props: it passes through those of the element or primitive it wraps.
694
993
 
695
994
  ### Progress
696
995
 
697
- Fuente: `src/primitives/progress.tsx`
996
+ Source: `src/primitives/progress.tsx`
698
997
 
699
- El ancho del indicador cambia, no se anima: el sistema no anima escala ni desplazamiento. `transition-standard` solo cubre color y borde, así que el salto de ancho es inmediato aunque la clase esté puesta.
998
+ The indicator's width changes, it is not animated: the system animates neither scale nor displacement. `transition-standard` only covers color and border, so the width jump is immediate even with the class in place.
700
999
 
701
- - Extiende: `ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>`
1000
+ - Extends: `ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>`
702
1001
 
703
- | prop | tipo | req. | defecto | qué hace |
1002
+ | prop | type | req. | default | what it does |
704
1003
  | --- | --- | --- | --- | --- |
705
- | `label` | `string` | sí | | Nombre accesible de la barra. Es obligatorio a propósito: una barra de progreso sin nombre no dice de qué es el progreso, y ninguna otra parte del componente puede deducirlo. |
706
- | `tone` | `"accent" \| "warm"` | | `accent` | Arena en vez de bioluz, para progreso de curso. |
1004
+ | `label` | `string` | yes | | The bar's accessible name. It is mandatory on purpose: a progress bar with no name does not say what the progress is about, and no other part of the component can deduce it. |
1005
+ | `tone` | `"accent" \| "warm"` | | `accent` | Sand instead of biolume, for course progress. |
707
1006
 
708
1007
  ### RadioGroup, RadioGroupItem
709
1008
 
710
- Fuente: `src/primitives/radio-group.tsx`
1009
+ Source: `src/primitives/radio-group.tsx`
711
1010
 
712
1011
  **RadioGroup**
713
- - Extiende: `ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>`
714
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1012
+ - Extends: `ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>`
1013
+ - No own props: it passes through those of the element or primitive it wraps.
715
1014
 
716
1015
  **RadioGroupItem**
717
- - Extiende: `ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>`
718
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1016
+ - Extends: `ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>`
1017
+ - No own props: it passes through those of the element or primitive it wraps.
719
1018
 
720
1019
  ### SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, Select, SelectGroup, SelectValue
721
1020
 
722
- Fuente: `src/primitives/select.tsx`
1021
+ Source: `src/primitives/select.tsx`
723
1022
 
724
1023
  **SelectTrigger**
725
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1024
+ - No own props: it passes through those of the element or primitive it wraps.
726
1025
 
727
1026
  **SelectContent**
728
- Sin animación de entrada: el menú aparece, no se despliega.
1027
+ No entrance animation: the menu appears, it does not unfold.
729
1028
 
730
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1029
+ - No own props: it passes through those of the element or primitive it wraps.
731
1030
 
732
1031
  **SelectLabel**
733
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1032
+ - No own props: it passes through those of the element or primitive it wraps.
734
1033
 
735
1034
  **SelectItem**
736
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1035
+ - No own props: it passes through those of the element or primitive it wraps.
737
1036
 
738
1037
  **SelectSeparator**
739
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1038
+ - No own props: it passes through those of the element or primitive it wraps.
740
1039
 
741
1040
  **Select**
742
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1041
+ - No own props: it passes through those of the element or primitive it wraps.
743
1042
 
744
1043
  **SelectGroup**
745
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1044
+ - No own props: it passes through those of the element or primitive it wraps.
746
1045
 
747
1046
  **SelectValue**
748
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1047
+ - No own props: it passes through those of the element or primitive it wraps.
749
1048
 
750
1049
  ### Separator
751
1050
 
752
- Fuente: `src/primitives/separator.tsx`
1051
+ Source: `src/primitives/separator.tsx`
753
1052
 
754
- `hairline`, no `border`: una división entre contenidos es sutil por definición. Para delimitar un control existe `border`, que es otro token.
1053
+ `hairline`, not `border`: a division between pieces of content is subtle by definition. To delimit a control there is `border`, which is another token.
755
1054
 
756
- - Extiende: `ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>`
757
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1055
+ - Extends: `ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>`
1056
+ - No own props: it passes through those of the element or primitive it wraps.
758
1057
 
759
1058
  ### SheetContent, SheetHeader, SheetBody, SheetFooter, SheetTitle, SheetDescription, Sheet, SheetTrigger, SheetClose
760
1059
 
761
- Fuente: `src/primitives/sheet.tsx`
1060
+ Source: `src/primitives/sheet.tsx`
762
1061
 
763
1062
  **SheetContent**
764
- La segunda y última excepción a «nada de desplazamiento», aprobada a sabiendas: un panel que entra desde un borde se desliza por definición, y quieto sería un modal descentrado.
1063
+ The second and last exception to «no displacement», approved knowingly: a panel entering from an edge slides by definition, and held still it would be an off-centre modal.
765
1064
 
766
- - Extiende: `ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & VariantProps<typeof panel>`
1065
+ - Extends: `ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & VariantProps<typeof panel>`
767
1066
 
768
- | prop | tipo | req. | defecto | qué hace |
1067
+ | prop | type | req. | default | what it does |
769
1068
  | --- | --- | --- | --- | --- |
770
1069
  | `side` | `"right" \| "left" \| "top" \| "bottom"` | | `right` | |
771
1070
 
772
1071
  **SheetHeader**
773
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1072
+ - No own props: it passes through those of the element or primitive it wraps.
774
1073
 
775
1074
  **SheetBody**
776
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1075
+ - No own props: it passes through those of the element or primitive it wraps.
777
1076
 
778
1077
  **SheetFooter**
779
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1078
+ - No own props: it passes through those of the element or primitive it wraps.
780
1079
 
781
1080
  **SheetTitle**
782
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1081
+ - No own props: it passes through those of the element or primitive it wraps.
783
1082
 
784
1083
  **SheetDescription**
785
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1084
+ - No own props: it passes through those of the element or primitive it wraps.
786
1085
 
787
1086
  **Sheet**
788
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1087
+ - No own props: it passes through those of the element or primitive it wraps.
789
1088
 
790
1089
  **SheetTrigger**
791
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1090
+ - No own props: it passes through those of the element or primitive it wraps.
792
1091
 
793
1092
  **SheetClose**
794
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1093
+ - No own props: it passes through those of the element or primitive it wraps.
795
1094
 
796
1095
  ### Skeleton
797
1096
 
798
- Fuente: `src/primitives/skeleton.tsx`
1097
+ Source: `src/primitives/skeleton.tsx`
799
1098
 
800
- Barrido de 1.4s lineal, del documento.
1099
+ A 1.4s linear sweep, from the document.
801
1100
 
802
- - Extiende: `ComponentPropsWithoutRef<'div'>`
1101
+ - Extends: `ComponentPropsWithoutRef<'div'>`
803
1102
 
804
- | prop | tipo | req. | defecto | qué hace |
1103
+ | prop | type | req. | default | what it does |
805
1104
  | --- | --- | --- | --- | --- |
806
- | `still` | `boolean \| undefined` | | `false` | Apaga el barrido. Para listas largas, donde muchas a la vez marean. |
1105
+ | `still` | `boolean \| undefined` | | `false` | Turns the sweep off. For long lists, where many at once are dizzying. |
807
1106
 
808
1107
  ### Switch
809
1108
 
810
- Fuente: `src/primitives/switch.tsx`
1109
+ Source: `src/primitives/switch.tsx`
811
1110
 
812
- La perilla cambia de posición, pero no se anima al hacerlo: la posición es el estado, no una transición. Lo único que transiciona es el color de la vía.
1111
+ The knob changes position, but is not animated while doing so: the position IS the state, not a transition. The only thing that transitions is the track's color.
813
1112
 
814
- - Extiende: `ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>`
815
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1113
+ - Extends: `ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>`
1114
+ - No own props: it passes through those of the element or primitive it wraps.
816
1115
 
817
1116
  ### Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption
818
1117
 
819
- Fuente: `src/primitives/table.tsx`
1118
+ Source: `src/primitives/table.tsx`
820
1119
 
821
1120
  **Table**
822
- El contenedor scrollea en horizontal: la página nunca lo hace.
1121
+ The container scrolls horizontally: the page never does.
823
1122
 
824
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1123
+ - No own props: it passes through those of the element or primitive it wraps.
825
1124
 
826
1125
  **TableHeader**
827
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1126
+ - No own props: it passes through those of the element or primitive it wraps.
828
1127
 
829
1128
  **TableBody**
830
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1129
+ - No own props: it passes through those of the element or primitive it wraps.
831
1130
 
832
1131
  **TableFooter**
833
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1132
+ - No own props: it passes through those of the element or primitive it wraps.
834
1133
 
835
1134
  **TableRow**
836
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1135
+ - No own props: it passes through those of the element or primitive it wraps.
837
1136
 
838
1137
  **TableHead**
839
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1138
+ - No own props: it passes through those of the element or primitive it wraps.
840
1139
 
841
1140
  **TableCell**
842
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1141
+ - No own props: it passes through those of the element or primitive it wraps.
843
1142
 
844
1143
  **TableCaption**
845
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1144
+ - No own props: it passes through those of the element or primitive it wraps.
846
1145
 
847
1146
  ### Tabs, TabsList, TabsTrigger, TabsContent
848
1147
 
849
- Fuente: `src/primitives/tabs.tsx`
1148
+ Source: `src/primitives/tabs.tsx`
850
1149
 
851
1150
  **Tabs**
852
- - Extiende: `ComponentPropsWithoutRef<typeof TabsPrimitive.Root>`
853
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1151
+ - Extends: `ComponentPropsWithoutRef<typeof TabsPrimitive.Root>`
1152
+ - No own props: it passes through those of the element or primitive it wraps.
854
1153
 
855
1154
  **TabsList**
856
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1155
+ - No own props: it passes through those of the element or primitive it wraps.
857
1156
 
858
1157
  **TabsTrigger**
859
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1158
+ - No own props: it passes through those of the element or primitive it wraps.
860
1159
 
861
1160
  **TabsContent**
862
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1161
+ - No own props: it passes through those of the element or primitive it wraps.
863
1162
 
864
1163
  ### Textarea
865
1164
 
866
- Fuente: `src/primitives/textarea.tsx`
1165
+ Source: `src/primitives/textarea.tsx`
867
1166
 
868
- - Extiende: `ComponentPropsWithoutRef<'textarea'>`
1167
+ - Extends: `ComponentPropsWithoutRef<'textarea'>`
869
1168
 
870
- | prop | tipo | req. | defecto | qué hace |
1169
+ | prop | type | req. | default | what it does |
871
1170
  | --- | --- | --- | --- | --- |
872
1171
  | `invalid` | `boolean` | | `false` | |
873
1172
 
874
1173
  ### Toaster
875
1174
 
876
- Fuente: `src/primitives/toaster.tsx`
1175
+ Source: `src/primitives/toaster.tsx`
877
1176
 
878
- Va UNA vez, lo más arriba posible del árbol. Dos `Toaster` montados pintan cada aviso dos veces: la lista es del módulo, no de la instancia.
1177
+ Mount it ONCE, as high in the tree as possible. Two mounted `Toaster`s paint every notice twice: the list belongs to the module, not to the instance.
879
1178
 
880
- - Extiende: `{ /** Cuánto dura un aviso que no dice lo contrario. */ duration?: number; /** * Nombre del landmark que Radix crea para la región de avisos. Se traduce * porque lo lee un lector de pantalla, y el default de Radix está en inglés. */ label?: string; }`
1179
+ - Extends: `{ /** How long a notice lasts when it does not say otherwise. */ duration?: number; /** * The name of the landmark Radix creates for the notices region. It is * translated because a screen reader reads it, and Radix's default is in * English. */ label?: string; }`
881
1180
 
882
- | prop | tipo | req. | defecto | qué hace |
1181
+ | prop | type | req. | default | what it does |
883
1182
  | --- | --- | --- | --- | --- |
884
- | `duration` | `number` | | `5000` | Cuánto dura un aviso que no dice lo contrario. |
885
- | `label` | `string` | | `Avisos` | Nombre del landmark que Radix crea para la región de avisos. Se traduce porque lo lee un lector de pantalla, y el default de Radix está en inglés. |
1183
+ | `duration` | `number` | | `5000` | How long a notice lasts when it does not say otherwise. |
1184
+ | `label` | `string` | | `Avisos` | The name of the landmark Radix creates for the notices region. It is translated because a screen reader reads it, and Radix's default is in English. |
886
1185
 
887
1186
  ### TooltipContent, TooltipProvider, Tooltip, TooltipTrigger
888
1187
 
889
- Fuente: `src/primitives/tooltip.tsx`
1188
+ Source: `src/primitives/tooltip.tsx`
890
1189
 
891
1190
  **TooltipContent**
892
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1191
+ - No own props: it passes through those of the element or primitive it wraps.
893
1192
 
894
1193
  **TooltipProvider**
895
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1194
+ - No own props: it passes through those of the element or primitive it wraps.
896
1195
 
897
1196
  **Tooltip**
898
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1197
+ - No own props: it passes through those of the element or primitive it wraps.
899
1198
 
900
1199
  **TooltipTrigger**
901
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1200
+ - No own props: it passes through those of the element or primitive it wraps.
902
1201
 
903
1202
  ### Text
904
1203
 
905
- Fuente: `src/primitives/typography.tsx`
1204
+ Source: `src/primitives/typography.tsx`
906
1205
 
907
- - Extiende: `Omit<ComponentPropsWithoutRef<'p'>, 'color'> & VariantProps<typeof texto>`
1206
+ - Extends: `Omit<ComponentPropsWithoutRef<'p'>, 'color'> & VariantProps<typeof text>`
908
1207
 
909
- | prop | tipo | req. | defecto | qué hace |
1208
+ | prop | type | req. | default | what it does |
910
1209
  | --- | --- | --- | --- | --- |
911
- | `as` | `"h2" \| "h3" \| "li" \| "p" \| "span" \| "caption" \| "dd" \| "dt" \| "em" \| "figcaption" \| "h1" \| "h4" \| "legend" \| "strong"` | | | Etiqueta HTML. Por defecto, la que corresponde a la escala. |
912
- | `asChild` | `boolean` | | `false` | Renderiza el hijo en vez de crear un elemento, para envolver un enlace. |
913
- | `measure` | `boolean` | | | Corta la línea a 68ch. Activo por defecto en `body`, que es la única escala pensada para leerse en párrafos largos. |
914
- | `tone` | `"accent" \| "success" \| "warning" \| "error" \| "warm" \| "primary" \| "secondary" \| "muted"` | | `primary` | |
915
- | `variant` | `"display" \| "h2" \| "h3" \| "label" \| "body" \| "h1" \| "meta" \| "stat" \| "lead" \| "ui" \| "tag" \| "chip" \| "eyebrow"` | | `body` | |
1210
+ | `as` | `"h1" \| "h2" \| "h3" \| "strong" \| "li" \| "p" \| "span" \| "caption" \| "dd" \| "dt" \| "em" \| "figcaption" \| "h4" \| "legend"` | | | HTML tag. Defaults to whichever one matches the scale. |
1211
+ | `asChild` | `boolean` | | `false` | Renders the child instead of creating an element, to wrap a link. |
1212
+ | `measure` | `boolean` | | | Clamps the line to 68ch. On by default for `body`, the only scale meant to be read in long paragraphs. |
1213
+ | `tone` | `"primary" \| "secondary" \| "accent" \| "warm" \| "success" \| "warning" \| "error" \| "muted"` | | | |
1214
+ | `variant` | `"display" \| "stat" \| "h1" \| "h2" \| "h3" \| "body" \| "lead" \| "ui" \| "label" \| "tag" \| "chip" \| "meta" \| "eyebrow"` | | | |
916
1215
 
917
- ## Componentes
1216
+ ## Components
918
1217
 
919
- Se importan de `@eduardoalvarez/arrecife`. 24 exportaciones.
1218
+ Imported from `@eduardoalvarez/arrecife`. 25 exports.
920
1219
 
921
1220
  ### ArticleCard
922
1221
 
923
- Fuente: `src/components/article-card/index.tsx`
1222
+ Source: `src/components/article-card/index.tsx`
924
1223
 
925
- La línea de metadatos va en `meta` y no en `eyebrow`: `18 ago 2026 · 8 min de lectura` es un dato, no un antetítulo, y en versalitas no era ninguna de las dos cosas.
1224
+ The metadata line uses `meta` and not `eyebrow`: `18 ago 2026 · 8 min de lectura` is a datum, not an overline, and in small caps it was neither.
926
1225
 
927
- - Extiende: `Omit<TarjetaProps, 'children' \| 'title'>`
1226
+ - Extends: `Omit<CardShellProps, 'children' \| 'title'>`
928
1227
 
929
- | prop | tipo | req. | defecto | qué hace |
1228
+ | prop | type | req. | default | what it does |
930
1229
  | --- | --- | --- | --- | --- |
931
- | `asChild` | `boolean \| undefined` | | | Renderiza el hijo en vez de un `<a>`. Es como se enchufa el `Link` de Next o de Astro sin que la librería dependa de ningún enrutador. |
932
- | `date` | `ReactNode` | | | Fecha ya formateada por el proyecto: la librería no impone locale. |
933
- | `dateTime` | `string` | | | Valor de `datetime` del `<time>`, en ISO. |
934
- | `excerpt` | `ReactNode` | | | Entradilla. Se corta a dos líneas para que la rejilla no se desalinee. |
935
- | `headingLevel` | `2 \| 3` | | `3` | Nivel del titular. `h3` por defecto: una tarjeta suelta en una rejilla no gana el nivel que su posición no le da. |
1230
+ | `asChild` | `boolean \| undefined` | | | Renders the child instead of an `<a>`. It is how Next's or Astro's `Link` plugs in without the library depending on any router. |
1231
+ | `date` | `ReactNode` | | | Date already formatted by the project: the library imposes no locale. |
1232
+ | `dateTime` | `string` | | | The `<time>` element's `datetime` value, in ISO. |
1233
+ | `excerpt` | `ReactNode` | | | Standfirst. Clamped to two lines so the grid does not fall out of line. |
1234
+ | `headingLevel` | `2 \| 3` | | `3` | The headline level. `h3` by default: a lone card in a grid does not earn a level its position does not give it. |
936
1235
  | `readingMinutes` | `number` | | | |
1236
+ | `tagAsChild` | `(props: { tag: string; children: ReactNode; }) => ReactNode` | | | Renders each tag through the child, so an E2E suite can reach it. |
937
1237
  | `tags` | `readonly string[]` | | | |
938
- | `title` | `ReactNode` | sí | | |
1238
+ | `title` | `ReactNode` | yes | | |
939
1239
 
940
1240
  ### AudioPlayer
941
1241
 
942
- Fuente: `src/components/audio-player/index.tsx`
1242
+ Source: `src/components/audio-player/index.tsx`
943
1243
 
944
- - Extiende: `{ src: string; title?: string; /** * `full` para páginas de podcast, `compact` para barras laterales y `banner` * para artículos con narración. `compact` y `banner` traen además el * reproductor flotante cuando el estático sale de vista. */ mode?: AudioPlayerMode \| undefined; /** * Se llama una sola vez por carga, la primera vez que el audio arranca. * Aquí es donde el proyecto engancha su analítica; la librería no la trae. */ onFirstPlay?: ((title?: string) => void) \| undefined; }`
1244
+ - Extends: `{ src: string; title?: string; /** * `full` for podcast pages, `compact` for sidebars and `banner` for articles * with narration. `compact` and `banner` also bring the floating player when * the static one leaves the viewport. */ mode?: AudioPlayerMode \| undefined; /** * Called once per load, the first time the audio starts. * This is where the project hooks up its analytics; the library ships none. */ onFirstPlay?: ((title?: string) => void) \| undefined; }`
945
1245
 
946
- | prop | tipo | req. | defecto | qué hace |
1246
+ | prop | type | req. | default | what it does |
947
1247
  | --- | --- | --- | --- | --- |
948
- | `mode` | `"banner" \| "full" \| "compact"` | | `full` | `full` para páginas de podcast, `compact` para barras laterales y `banner` para artículos con narración. `compact` y `banner` traen además el reproductor flotante cuando el estático sale de vista. |
949
- | `onFirstPlay` | `(title?: string \| undefined) => void` | | | Se llama una sola vez por carga, la primera vez que el audio arranca. Aquí es donde el proyecto engancha su analítica; la librería no la trae. |
950
- | `src` | `string` | sí | | |
1248
+ | `mode` | `"banner" \| "full" \| "compact"` | | `full` | `full` for podcast pages, `compact` for sidebars and `banner` for articles with narration. `compact` and `banner` also bring the floating player when the static one leaves the viewport. |
1249
+ | `onFirstPlay` | `(title?: string \| undefined) => void` | | | Called once per load, the first time the audio starts. This is where the project hooks up its analytics; the library ships none. |
1250
+ | `src` | `string` | yes | | |
951
1251
  | `title` | `string` | | | |
952
1252
 
953
1253
  ### AuthorCard
954
1254
 
955
- Fuente: `src/components/author-card/index.tsx`
1255
+ Source: `src/components/author-card/index.tsx`
956
1256
 
957
- La firma al pie del artículo: avatar 52px, nombre 15/500 y el rol en mono muted. Tres datos, ni uno más.
1257
+ The byline at the foot of the article: 52px avatar, 15/500 name and the role in muted mono. Three data points, not one more.
958
1258
 
959
- - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'role'>`
1259
+ - Extends: `Omit<ComponentPropsWithoutRef<'div'>, 'role'>`
960
1260
 
961
- | prop | tipo | req. | defecto | qué hace |
1261
+ | prop | type | req. | default | what it does |
962
1262
  | --- | --- | --- | --- | --- |
963
- | `action` | `ReactNode` | | | Enlaces o botón de contacto. |
964
- | `bio` | `ReactNode` | | | Una o dos frases. Se corta a 68ch sola. |
965
- | `name` | `string` | sí | | |
966
- | `role` | `ReactNode` | | | El rol. Va en mono: es un dato, no una frase. |
967
- | `src` | `string` | | | URL del avatar. Sin ella se muestran las iniciales. |
1263
+ | `action` | `ReactNode` | | | Links or a contact button. |
1264
+ | `bio` | `ReactNode` | | | One or two sentences. It clamps itself to 68ch. |
1265
+ | `name` | `string` | yes | | |
1266
+ | `role` | `ReactNode` | | | The role. It goes in mono: it is a datum, not a sentence. |
1267
+ | `src` | `string` | | | The avatar's URL. Without it the initials are shown. |
968
1268
 
969
1269
  ### Blockquote
970
1270
 
971
- Fuente: `src/components/blockquote/index.tsx`
1271
+ Source: `src/components/blockquote/index.tsx`
972
1272
 
973
- La barra lateral es `accent`, que es el color interactivo, porque una cita es la voz de otro entrando en el texto. No lleva comillas decorativas: los glifos del sistema son SVG y una comilla de adorno no aporta nada que el borde y la sangría no digan ya.
1273
+ The side bar is `accent`, the interactive color, because a quotation is somebody else's voice entering the text. It carries no decorative quote marks: the system's glyphs are SVG, and an ornamental quote adds nothing the border and the indent do not already say.
974
1274
 
975
- - Extiende: `Omit<ComponentPropsWithoutRef<'blockquote'>, 'cite'>`
1275
+ - Extends: `Omit<ComponentPropsWithoutRef<'blockquote'>, 'cite'>`
976
1276
 
977
- | prop | tipo | req. | defecto | qué hace |
1277
+ | prop | type | req. | default | what it does |
978
1278
  | --- | --- | --- | --- | --- |
979
- | `author` | `ReactNode` | | | Quién lo dijo. Se marca como `<cite>`. |
980
- | `source` | `ReactNode` | | | Dónde lo dijo: charla, artículo, conversación. |
1279
+ | `author` | `ReactNode` | | | Who said it. Marked up as `<cite>`. |
1280
+ | `source` | `ReactNode` | | | Where they said it: a talk, an article, a conversation. |
981
1281
 
982
1282
  ### Breadcrumb
983
1283
 
984
- Fuente: `src/components/breadcrumb/index.tsx`
1284
+ Source: `src/components/breadcrumb/index.tsx`
985
1285
 
986
- - Extiende: `Omit<ComponentPropsWithoutRef<'nav'>, 'children'>`
1286
+ - Extends: `Omit<ComponentPropsWithoutRef<'nav'>, 'children'>`
987
1287
 
988
- | prop | tipo | req. | defecto | qué hace |
1288
+ | prop | type | req. | default | what it does |
989
1289
  | --- | --- | --- | --- | --- |
990
- | `homeHref` | `string` | | `/` | Destino del `~`. Por defecto, la raíz del sitio. |
991
- | `homeLabel` | `string` | | `Inicio` | Etiqueta accesible del `~`, que si no se lee como una tilde suelta. |
992
- | `items` | `readonly Migaja[]` | sí | | |
993
- | `linkAsChild` | `(props: { href: string; children: ReactNode; }) => ReactNode` | | | Renderiza los enlaces con el hijo, para enchufar el `Link` de Next o Astro. Recibe cada `href` en el `props` del Slot. |
1290
+ | `homeHref` | `string` | | `/` | Where the `~` goes. The site root by default. |
1291
+ | `homeLabel` | `string` | | `Inicio` | Accessible label for the `~`, which otherwise reads as a stray tilde. |
1292
+ | `items` | `readonly Crumb[]` | yes | | |
1293
+ | `linkAsChild` | `(props: { href: string; children: ReactNode; }) => ReactNode` | | | Renders the links through the child, to plug in Next's or Astro's `Link`. It receives each `href` in the Slot's `props`. |
994
1294
 
995
1295
  ### CodeBlock
996
1296
 
997
- Fuente: `src/components/code-block/index.tsx`
1297
+ Source: `src/components/code-block/index.tsx`
998
1298
 
999
- `brand.hull` es «casco · contorno y fondo de bloques de código», así que un bloque de código es oscuro también en modo claro. Por eso la raíz declara `data-theme="dark"`: todo lo de dentro — tinta, hairline, acento — pasa a la paleta oscura sin importar el tema de la página. Es la única isla de tema invertido del sistema, y es deliberada.
1299
+ `brand.hull` is «hull · outline and the background of code blocks», so a code block is dark in light mode too. That is why the root declares `data-theme="dark"`: everything inside — ink, hairline, accent — switches to the dark palette regardless of the page's theme. It is the system's only island of inverted theme, and it is deliberate.
1000
1300
 
1001
- - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'children'>`
1301
+ - Extends: `Omit<ComponentPropsWithoutRef<'div'>, 'children'>`
1002
1302
 
1003
- | prop | tipo | req. | defecto | qué hace |
1303
+ | prop | type | req. | default | what it does |
1004
1304
  | --- | --- | --- | --- | --- |
1005
- | `children` | `ReactNode` | sí | | El código ya resaltado, o texto plano. |
1006
- | `copyText` | `string` | | | Texto que se copia al portapapeles. Sin esto, no se muestra el botón. |
1007
- | `language` | `string` | | | Etiqueta del lenguaje. Se muestra en la barra superior. |
1305
+ | `children` | `ReactNode` | yes | | The already-highlighted code, or flat text. |
1306
+ | `copyText` | `string` | | | The text copied to the clipboard. Without it, the button is not shown. |
1307
+ | `language` | `string` | | | The language label. Shown in the top bar. |
1008
1308
 
1009
1309
  ### CourseCard
1010
1310
 
1011
- Fuente: `src/components/course-card/index.tsx`
1311
+ Source: `src/components/course-card/index.tsx`
1012
1312
 
1013
- - Extiende: `Omit<TarjetaProps, 'children' \| 'title'>`
1313
+ - Extends: `Omit<CardShellProps, 'children' \| 'title'>`
1014
1314
 
1015
- | prop | tipo | req. | defecto | qué hace |
1315
+ | prop | type | req. | default | what it does |
1016
1316
  | --- | --- | --- | --- | --- |
1017
- | `asChild` | `boolean \| undefined` | | | Renderiza el hijo en vez de un `<a>`. Es como se enchufa el `Link` de Next o de Astro sin que la librería dependa de ningún enrutador. |
1018
- | `meta` | `readonly ReactNode[]` | | | Nivel, duración, número de lecciones: lo que el proyecto quiera listar. |
1019
- | `progress` | `number` | | | Porcentaje cursado. Solo tiene sentido para quien ya está inscrito; cuando se pasa, la barra va en arena, que es el color del progreso de curso. |
1020
- | `status` | `ReactNode` | | | Etiqueta de estado: «próximamente», «gratis», «nuevo». |
1317
+ | `asChild` | `boolean \| undefined` | | | Renders the child instead of an `<a>`. It is how Next's or Astro's `Link` plugs in without the library depending on any router. |
1318
+ | `meta` | `readonly ReactNode[]` | | | Level, duration, number of lessons: whatever the project wants to list. |
1319
+ | `progress` | `number` | | | Percentage completed. It only makes sense for someone already enrolled; when passed, the bar goes in sand, which is the color of course progress. |
1320
+ | `status` | `ReactNode` | | | Status label: «próximamente», «gratis», «nuevo». |
1021
1321
  | `summary` | `ReactNode` | | | |
1022
- | `title` | `ReactNode` | sí | | |
1322
+ | `title` | `ReactNode` | yes | | |
1023
1323
 
1024
1324
  ### EmptyState
1025
1325
 
1026
- Fuente: `src/components/empty-state/index.tsx`
1326
+ Source: `src/components/empty-state/index.tsx`
1027
1327
 
1028
- La regla más importante de la mascota, por fin como código.
1328
+ - Extends: `EmptyStateBase & ( \| { /** `page`, the default: the empty state IS the screen or the section, and it carries the face. */ variant?: 'page' \| undefined; /** * The face. Mandatory on `page` and impossible on `inline` — the props * are a union, so the generated table cannot show a per-variant «req.» * and the sentence has to carry it. Without it, `page` is a centred * paragraph. */ expression: Face; /** Where the brand PNGs are served from. */ basePath?: string \| undefined; icon?: never; } \| { /** `inline`: the hole inside a table or a widget. No face, and no way to pass one. */ variant: 'inline'; /** * A glyph above the line. It measures 1em and inherits `currentColor`, * like `Stat`'s: the project passes its own and sizes it, because the * system has no icon library and is not getting one. */ icon?: ReactNode; expression?: never; basePath?: never; } )`
1029
1329
 
1030
- - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'title'>`
1031
-
1032
- | prop | tipo | req. | defecto | qué hace |
1330
+ | prop | type | req. | default | what it does |
1033
1331
  | --- | --- | --- | --- | --- |
1034
- | `action` | `ReactNode` | | | La acción que saca del estado vacío. Normalmente un botón terciario. |
1035
- | `basePath` | `string` | | | Dónde se sirven los PNG de la marca. |
1036
- | `description` | `ReactNode` | | | Una línea explicando qué falta o qué hacer. |
1037
- | `expresion` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | sí | | La cara. Obligatoria: sin ella esto es un párrafo centrado. |
1038
- | `title` | `ReactNode` | sí | | |
1332
+ | `action` | `ReactNode` | | | The action that gets you out of the empty state. Usually a tertiary button. |
1333
+ | `basePath` | `string` | | | Where the brand PNGs are served from. |
1334
+ | `description` | `ReactNode` | | | One line explaining what is missing or what to do. |
1335
+ | `expression` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | | | The face. Mandatory on `page` and impossible on `inline` — the props are a union, so the generated table cannot show a per-variant «req.» and the sentence has to carry it. Without it, `page` is a centred paragraph. |
1336
+ | `icon` | `ReactNode` | | | A glyph above the line. It measures 1em and inherits `currentColor`, like `Stat`'s: the project passes its own and sizes it, because the system has no icon library and is not getting one. |
1337
+ | `title` | `ReactNode` | yes | | |
1338
+ | `variant` | `"inline" \| "page"` | | | `page`, the default: the empty state IS the screen or the section, and it carries the face. `inline`: the hole inside a table or a widget. No face, and no way to pass one. |
1039
1339
 
1040
1340
  ### EventCalendar
1041
1341
 
1042
- Fuente: `src/components/event-calendar/index.tsx`
1342
+ Source: `src/components/event-calendar/index.tsx`
1043
1343
 
1044
- - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'onSelect' \| 'children'>`
1344
+ - Extends: `Omit<ComponentPropsWithoutRef<'div'>, 'onSelect' \| 'children'>`
1045
1345
 
1046
- | prop | tipo | req. | defecto | qué hace |
1346
+ | prop | type | req. | default | what it does |
1047
1347
  | --- | --- | --- | --- | --- |
1048
- | `emptyMessage` | `ReactNode` | | `Nada en este día.` | Texto del panel cuando el día elegido no tiene nada. |
1049
- | `events` | `readonly EventoCalendario[]` | sí | | |
1050
- | `formatDay` | `(dia: Date) => string` | | `(dia) => format(dia, "EEEE d de MMMM", { locale: es })` | Encabezado del panel. Por defecto, `es` de date-fns, como `Calendar`. |
1051
- | `formatTime` | `(fecha: Date) => string` | | `(fecha) => format(fecha, HH:mm, { locale: es })` | |
1052
- | `onCreateEvent` | `(evento: Omit<EventoCalendario, "id">) => void` | | | Sin él, la agenda es de solo lectura y no se pinta el formulario. |
1348
+ | `emptyMessage` | `ReactNode` | | `Nada en este día.` | The panel's text when the chosen day has nothing on it. |
1349
+ | `events` | `readonly CalendarEvent[]` | yes | | |
1350
+ | `formatDay` | `(day: Date) => string` | | `(day) => format(day, "EEEE d de MMMM", { locale: es })` | The panel's heading. Defaults to date-fns' `es`, like `Calendar`. |
1351
+ | `formatTime` | `(date: Date) => string` | | `(date) => format(date, HH:mm, { locale: es })` | |
1352
+ | `onCreateEvent` | `(event: Omit<CalendarEvent, "id">) => void` | | | Without it, the schedule is read-only and the form is not painted. |
1053
1353
  | `onDeleteEvent` | `(id: string) => void` | | | |
1054
- | `onSelectDay` | `(dia: Date) => void` | | | |
1055
- | `onUpdateEvent` | `(evento: EventoCalendario) => void` | | | |
1056
- | `selected` | `Date` | | | Día seleccionado, si el proyecto lo controla. Sin él, empieza en hoy. |
1354
+ | `onSelectDay` | `(day: Date) => void` | | | |
1355
+ | `onUpdateEvent` | `(event: CalendarEvent) => void` | | | |
1356
+ | `selected` | `Date` | | | Selected day, if the project controls it. Without it, it starts on today. |
1057
1357
 
1058
1358
  ### Footer, FooterLink
1059
1359
 
1060
- Fuente: `src/components/footer/index.tsx`
1360
+ Source: `src/components/footer/index.tsx`
1061
1361
 
1062
1362
  **Footer**
1063
- - Extiende: `ComponentPropsWithoutRef<'footer'>`
1363
+ - Extends: `ComponentPropsWithoutRef<'footer'>`
1064
1364
 
1065
- | prop | tipo | req. | defecto | qué hace |
1365
+ | prop | type | req. | default | what it does |
1066
1366
  | --- | --- | --- | --- | --- |
1067
- | `brand` | `ReactNode` | | | La fila de marca: la aleta y el wordmark, arriba del todo. |
1068
- | `social` | `readonly Red[]` | | | |
1069
- | `year` | `number` | | `new Date().getFullYear()` | Año de la firma. |
1367
+ | `brand` | `ReactNode` | | | The brand row: the fin and the wordmark, at the very top. |
1368
+ | `social` | `readonly SocialLink[]` | | | |
1369
+ | `year` | `number` | | `new Date().getFullYear()` | The signature's year. |
1070
1370
 
1071
1371
  **FooterLink**
1072
- - Extiende: `ComponentPropsWithoutRef<'a'>`
1372
+ - Extends: `ComponentPropsWithoutRef<'a'>`
1073
1373
 
1074
- | prop | tipo | req. | defecto | qué hace |
1374
+ | prop | type | req. | default | what it does |
1075
1375
  | --- | --- | --- | --- | --- |
1076
1376
  | `asChild` | `boolean \| undefined` | | `false` | |
1077
1377
 
1078
1378
  ### Hero
1079
1379
 
1080
- Fuente: `src/components/hero/index.tsx`
1380
+ Source: `src/components/hero/index.tsx`
1081
1381
 
1082
- UNO por sitio. Es la única pieza del sistema que se gasta como el botón de conversión, y por la misma razón: si hay dos, no hay ninguno.
1382
+ ONE per site. It is the only piece in the system that is spent like the conversion button, and for the same reason: if there are two, there are none.
1083
1383
 
1084
- - Extiende: `Omit<ComponentPropsWithoutRef<'section'>, 'title'>`
1384
+ - Extends: `Omit<ComponentPropsWithoutRef<'section'>, 'title'>`
1085
1385
 
1086
- | prop | tipo | req. | defecto | qué hace |
1386
+ | prop | type | req. | default | what it does |
1087
1387
  | --- | --- | --- | --- | --- |
1088
- | `action` | `ReactNode` | | | Los botones. Aquí va el único `conversion` de la pantalla. |
1388
+ | `action` | `ReactNode` | | | The buttons. The screen's only `conversion` goes here. |
1089
1389
  | `basePath` | `string` | | | |
1090
1390
  | `description` | `ReactNode` | | | |
1091
- | `eyebrow` | `ReactNode` | | | Mono, versalitas, en acento. |
1092
- | `pose` | `"desk" \| "laptop-coffee" \| "peek" \| "surf"` | | | La pose de Tiburoncín. Sin ella el hero es un panel con texto. |
1093
- | `title` | `ReactNode` | sí | | |
1094
- | `variant` | `"cabecera" \| "centrado"` | | `cabecera` | `cabecera` sangra la pose por la esquina; `centrado` la pone arriba y centra el texto, para una página que es solo esto. |
1391
+ | `eyebrow` | `ReactNode` | | | Mono, small caps, in accent. |
1392
+ | `pose` | `"desk" \| "laptop-coffee" \| "peek" \| "surf"` | | | Tiburoncín's pose. Without it the hero is a panel with text. |
1393
+ | `title` | `ReactNode` | yes | | |
1394
+ | `variant` | `"header" \| "centered"` | | `header` | `header` bleeds the pose off the corner; `centered` puts it on top and centres the text, for a page that is only this. |
1095
1395
 
1096
1396
  ### LinkRow
1097
1397
 
1098
- Fuente: `src/components/link-row/index.tsx`
1398
+ Source: `src/components/link-row/index.tsx`
1099
1399
 
1100
- Migrado desde `links/src/components/Card.astro`. El original escalaba la tarjeta al 102 %, subía el título un píxel y giraba y agrandaba el icono en hover — cuatro movimientos que el sistema no permite. Aquí el hover cambia el borde y el color del icono, y nada más.
1400
+ Migrated from `links/src/components/Card.astro`. The original scaled the card to 102 %, lifted the title by a pixel and rotated and enlarged the icon on hover — four movements the system does not allow. Here the hover changes the border and the icon's color, and nothing else.
1101
1401
 
1102
- - Extiende: `Omit<TarjetaProps, 'children'>`
1402
+ - Extends: `Omit<CardShellProps, 'children'>`
1103
1403
 
1104
- | prop | tipo | req. | defecto | qué hace |
1404
+ | prop | type | req. | default | what it does |
1105
1405
  | --- | --- | --- | --- | --- |
1106
- | `asChild` | `boolean \| undefined` | | | Renderiza el hijo en vez de un `<a>`. Es como se enchufa el `Link` de Next o de Astro sin que la librería dependa de ningún enrutador. |
1406
+ | `asChild` | `boolean \| undefined` | | | Renders the child instead of an `<a>`. It is how Next's or Astro's `Link` plugs in without the library depending on any router. |
1107
1407
  | `description` | `ReactNode` | | | |
1108
- | `external` | `boolean \| undefined` | | `false` | Marca el enlace como externo: añade la flecha y el `rel` seguro. |
1109
- | `icon` | `ReactNode` | | | Glifo SVG del destino. Nunca un emoji. |
1110
- | `name` | `ReactNode` | sí | | |
1408
+ | `external` | `boolean \| undefined` | | `false` | Marks the link as external: adds the arrow and the safe `rel`. |
1409
+ | `icon` | `ReactNode` | | | The target's SVG glyph. Never an emoji. |
1410
+ | `name` | `ReactNode` | yes | | |
1111
1411
 
1112
1412
  ### Nav, NavItem
1113
1413
 
1114
- Fuente: `src/components/nav/index.tsx`
1414
+ Source: `src/components/nav/index.tsx`
1115
1415
 
1116
1416
  **Nav**
1117
- La barra del sitio: 64px, abismo al 86 % y desenfoque de 14px detrás.
1417
+ The site bar: 64px, abyss at 86 % and a 14px blur behind it — 56 when it shares the screen with a sidebar.
1118
1418
 
1119
- - Extiende: `ComponentPropsWithoutRef<'header'>`
1419
+ - Extends: `ComponentPropsWithoutRef<'header'>`
1120
1420
 
1121
- | prop | tipo | req. | defecto | qué hace |
1421
+ | prop | type | req. | default | what it does |
1122
1422
  | --- | --- | --- | --- | --- |
1123
- | `actions` | `ReactNode` | | | Acciones a la derecha: conversión, cambio de tema, buscar. |
1124
- | `brand` | `ReactNode` | | | El logo, a la izquierda. |
1423
+ | `actions` | `ReactNode` | | | Actions on the right: conversion, theme switch, search. |
1424
+ | `brand` | `ReactNode` | | | The logo, on the left. |
1425
+ | `size` | `"compact" \| "default"` | | `default` | `compact` is 56px instead of 64, for a bar that shares the screen with a sidebar: at 64 the two compete for the same corner and together they eat the top of the content area. |
1125
1426
 
1126
1427
  **NavItem**
1127
- El `./` lo pone el componente, no quien lo usa.
1428
+ The `./` is put there by the component, not by whoever uses it.
1128
1429
 
1129
- - Extiende: `ComponentPropsWithoutRef<'a'>`
1430
+ - Extends: `ComponentPropsWithoutRef<'a'>`
1130
1431
 
1131
- | prop | tipo | req. | defecto | qué hace |
1432
+ | prop | type | req. | default | what it does |
1132
1433
  | --- | --- | --- | --- | --- |
1133
- | `active` | `boolean \| undefined` | | `false` | Sección actual: bioluz con subrayado de 1px. |
1134
- | `asChild` | `boolean \| undefined` | | `false` | Renderiza el hijo en vez de un `<a>`, para el `Link` del enrutador. |
1434
+ | `active` | `boolean \| undefined` | | `false` | Current section: biolume with a 1px underline. |
1435
+ | `asChild` | `boolean \| undefined` | | `false` | Renders the child instead of an `<a>`, for the router's `Link`. |
1135
1436
 
1136
1437
  ### NewsletterForm
1137
1438
 
1138
- Fuente: `src/components/newsletter-form/index.tsx`
1439
+ Source: `src/components/newsletter-form/index.tsx`
1139
1440
 
1140
- - Extiende: `Omit<ComponentPropsWithoutRef<'section'>, 'title' \| 'onSubmit'>`
1441
+ - Extends: `Omit<ComponentPropsWithoutRef<'section'>, 'title' \| 'onSubmit'>`
1141
1442
 
1142
- | prop | tipo | req. | defecto | qué hace |
1443
+ | prop | type | req. | default | what it does |
1143
1444
  | --- | --- | --- | --- | --- |
1445
+ | `aside` | `ReactNode` | | | The illustration, as a second column inside the panel. |
1144
1446
  | `basePath` | `string` | | | |
1145
1447
  | `description` | `ReactNode` | | | |
1146
- | `disclaimer` | `ReactNode` | | | La letra pequeña. Es el «sin spam», y por eso admite cara. |
1448
+ | `disclaimer` | `ReactNode` | | | The small print. It is the «sin spam», which is why it accepts a face. |
1147
1449
  | `errorMessage` | `ReactNode` | | `No se pudo suscribir ese correo. Revísalo y vuelve a intentar.` | |
1148
- | `expresion` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | | | |
1450
+ | `expression` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | | | |
1451
+ | `fieldErrors` | `{ name?: ReactNode; email?: ReactNode; }` | | | A message under one specific field, instead of the single alert. |
1149
1452
  | `fieldLabel` | `string` | | `Correo electrónico` | |
1150
- | `nameField` | `boolean` | | `false` | Añade el campo de nombre delante del correo. |
1151
- | `nameInputProps` | `Omit<InputProps, "id" \| "disabled" \| "name">` | | | Lo que el proyecto necesite colgar del campo de nombre: `minLength`, `maxLength`, `pattern`. La librería no impone ninguna de las tres. |
1453
+ | `nameField` | `boolean` | | `false` | Adds the name field ahead of the email one. |
1454
+ | `nameInputProps` | `Omit<InputProps, "id" \| "disabled" \| "name">` | | | Whatever the project needs to hang off the name field: `minLength`, `maxLength`, `pattern`. The library imposes none of the three. |
1152
1455
  | `nameLabel` | `string` | | `Nombre` | |
1153
1456
  | `namePlaceholder` | `string` | | `Cómo te llamas` | |
1154
- | `onSubmitEmail` | `(email: string, name?: string \| undefined) => void` | | | Se dispara con el correo ya leído del campo, y con el nombre si el campo está puesto. |
1155
- | `placeholder` | `string` | | `tu@correo.dev` | |
1156
- | `state` | `"error" \| "reposo" \| "enviando" \| "exito"` | | `reposo` | |
1457
+ | `onFieldChange` | `(field: "email" \| "name", value: string) => void` | | | Fires when either field changes. It is where the project clears its error. |
1458
+ | `onSubmitEmail` | `(email: string, name?: string \| undefined) => void` | | | Fires with the email already read from the field, and with the name if that field is enabled. |
1459
+ | `placeholder` | `string` | | `tu@email.dev` | |
1460
+ | `resetOnSuccess` | `boolean` | | `true` | Empties the fields after a successful subscription. On by default. |
1461
+ | `state` | `"success" \| "error" \| "idle" \| "sending"` | | `idle` | |
1157
1462
  | `submitLabel` | `string` | | `Suscribirme` | |
1158
1463
  | `successMessage` | `ReactNode` | | `Ya estás dentro. Te llega un correo cada dos semanas, y nada más.` | |
1159
- | `title` | `ReactNode` | sí | | |
1464
+ | `title` | `ReactNode` | yes | | |
1160
1465
 
1161
1466
  ### PageHeader
1162
1467
 
1163
- Fuente: `src/components/page-header/index.tsx`
1468
+ Source: `src/components/page-header/index.tsx`
1164
1469
 
1165
- Una sola cabecera en dos escalas, no dos componentes.
1470
+ One header at two scales, not two components.
1166
1471
 
1167
- - Extiende: `Omit<ComponentPropsWithoutRef<'header'>, 'title'> & VariantProps<typeof cabecera>`
1472
+ - Extends: `Omit<ComponentPropsWithoutRef<'header'>, 'title'> & VariantProps<typeof header>`
1168
1473
 
1169
- | prop | tipo | req. | defecto | qué hace |
1474
+ | prop | type | req. | default | what it does |
1170
1475
  | --- | --- | --- | --- | --- |
1171
- | `action` | `ReactNode` | | | Ranura para las llamadas a la acción. Si aquí va un botón de conversión, es el único de la pantalla. |
1172
- | `as` | `"h2" \| "h1"` | | `h1` | Nivel del titular. `h1` salvo que la página ya tenga uno. |
1476
+ | `action` | `ReactNode` | | | Slot for the calls to action. If a conversion button goes here, it is the only one on the screen. |
1477
+ | `as` | `"h1" \| "h2"` | | `h1` | The headline's level. `h1` unless the page already has one. |
1173
1478
  | `description` | `ReactNode` | | | |
1174
- | `eyebrow` | `ReactNode` | | | Mono, versalitas, en acento. Es la sección a la que pertenece la página. |
1479
+ | `eyebrow` | `ReactNode` | | | Mono, small caps, in accent. It is the section the page belongs to. |
1175
1480
  | `size` | `"display" \| "page"` | | `page` | |
1176
- | `title` | `ReactNode` | sí | | |
1481
+ | `title` | `ReactNode` | yes | | |
1177
1482
 
1178
1483
  ### ScrollingProgressBar
1179
1484
 
1180
- Fuente: `src/components/scrolling-progress-bar/index.tsx`
1485
+ Source: `src/components/scrolling-progress-bar/index.tsx`
1181
1486
 
1182
- Cuánto llevas leído. NO es `Progress` con otro nombre.
1487
+ How much you have read. It is NOT `Progress` under another name.
1183
1488
 
1184
- - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'children'>`
1489
+ - Extends: `Omit<ComponentPropsWithoutRef<'div'>, 'children'>`
1185
1490
 
1186
- | prop | tipo | req. | defecto | qué hace |
1491
+ | prop | type | req. | default | what it does |
1187
1492
  | --- | --- | --- | --- | --- |
1188
- | `sticky` | `boolean` | | `true` | Pega la barra al borde superior de la ventana. |
1189
- | `target` | `RefObject<HTMLElement \| null>` | | | El elemento que se mide. Sin él, el documento entero. |
1190
- | `tone` | `"accent" \| "warm"` | | `accent` | Arena en vez de bioluz, para igualar el progreso de curso. |
1493
+ | `sticky` | `boolean` | | `true` | Pins the bar to the top edge of the window. |
1494
+ | `target` | `RefObject<HTMLElement \| null>` | | | The element being measured. Without it, the whole document. |
1495
+ | `tone` | `"accent" \| "warm"` | | `accent` | Sand instead of biolume, to match course progress. |
1191
1496
 
1192
- ### SidebarItem, SidebarNav
1497
+ ### SidebarItem, SidebarGroup, SidebarNav
1193
1498
 
1194
- Fuente: `src/components/sidebar-nav/index.tsx`
1499
+ Source: `src/components/sidebar-nav/index.tsx`
1195
1500
 
1196
1501
  **SidebarItem**
1197
- La barra lateral del admin del blog.
1502
+ The blog admin's sidebar.
1198
1503
 
1199
- - Extiende: `ComponentPropsWithoutRef<'a'>`
1504
+ - Extends: `ComponentPropsWithoutRef<'a'>`
1200
1505
 
1201
- | prop | tipo | req. | defecto | qué hace |
1506
+ | prop | type | req. | default | what it does |
1202
1507
  | --- | --- | --- | --- | --- |
1203
1508
  | `active` | `boolean \| undefined` | | `false` | |
1204
1509
  | `asChild` | `boolean \| undefined` | | `false` | |
1205
- | `badge` | `ReactNode` | | | Contador a la derecha: borradores pendientes, media sin usar. |
1510
+ | `badge` | `ReactNode` | | | Counter on the right: pending drafts, unused media. |
1511
+ | `icon` | `ReactNode` | | | The section's glyph, on the left. It REPLACES the `▸` rather than joining it, and it inherits `currentColor`, so it follows the item's state without being tinted separately. |
1512
+
1513
+ **SidebarGroup**
1514
+ A labelled block of items — «Contenido», «Alumnos», «Ventas».
1515
+
1516
+ - Extends: `Omit<ComponentPropsWithoutRef<'li'>, 'title'>`
1517
+
1518
+ | prop | type | req. | default | what it does |
1519
+ | --- | --- | --- | --- | --- |
1520
+ | `label` | `ReactNode` | yes | | The block's name. Sentence case, not a section title. |
1206
1521
 
1207
1522
  **SidebarNav**
1208
- - Extiende: `ComponentPropsWithoutRef<'nav'>`
1523
+ - Extends: `ComponentPropsWithoutRef<'nav'>`
1209
1524
 
1210
- | prop | tipo | req. | defecto | qué hace |
1525
+ | prop | type | req. | default | what it does |
1211
1526
  | --- | --- | --- | --- | --- |
1212
1527
  | `branch` | `ReactNode` | | | |
1213
- | `version` | `ReactNode` | | | Versión y rama, al pie. |
1528
+ | `brand` | `ReactNode` | | | The row at the top: isotype and wordmark, `cursos · admin`. It is a slot and not a `logo`/`name` pair because every panel spells its own name differently, and the part that IS the system — the rhythm, the hairline under it — is here. |
1529
+ | `collapsed` | `boolean \| undefined` | | `false` | Turns the sidebar into a rail: icons only, and the widths become the library's — `w-sidebar` and `w-sidebar-rail`. It is CONTROLLED and there is no uncontrolled mode, because this state is almost always persisted in a cookie or in `localStorage`, and an internal state would fight the one the project already keeps. |
1530
+ | `collapseLabel` | `string` | | `Plegar el panel` | The toggle's accessible name, in the two directions. |
1531
+ | `expandLabel` | `string` | | `Desplegar el panel` | |
1532
+ | `mark` | `ReactNode` | | | What `brand` becomes in the rail. Usually the isotype with no wordmark. |
1533
+ | `onCollapsedChange` | `(collapsed: boolean) => void` | | | Called with what the state should become. With it, the toggle appears; with `collapsed` alone the sidebar is a rail with no way out of it, which is a legitimate layout and not an accident. |
1534
+ | `user` | `ReactNode` | | | Who is signed in, at the bottom above the version. A slot, because an avatar needs a session and a sign-out route and the library takes no project infrastructure — the same reason `Nav`'s user menu goes in `actions`. |
1535
+ | `version` | `ReactNode` | | | Version and branch, at the bottom. |
1214
1536
 
1215
1537
  ### Stat
1216
1538
 
1217
- Fuente: `src/components/stat/index.tsx`
1539
+ Source: `src/components/stat/index.tsx`
1218
1540
 
1219
- Una métrica grande: el número en la escala `stat` y su nombre debajo.
1541
+ A large metric: the number in the `stat` scale and its name underneath.
1220
1542
 
1221
- - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'title'>`
1543
+ - Extends: `Omit<ComponentPropsWithoutRef<'div'>, 'title'>`
1222
1544
 
1223
- | prop | tipo | req. | defecto | qué hace |
1545
+ | prop | type | req. | default | what it does |
1224
1546
  | --- | --- | --- | --- | --- |
1225
- | `description` | `ReactNode` | | | La bajada: el matiz que el número solo no da. «12 aplicaciones» no dice si son muchas, y aquí es donde se dice. |
1226
- | `icon` | `ReactNode` | | | Glifo al lado del título, a 1em. Hereda `currentColor`, así que sigue al tono del título y no hay que teñirlo aparte. |
1227
- | `label` | `ReactNode` | sí | | Qué se está contando. Va en mono versalitas. |
1228
- | `progress` | `number` | | | Con `progress`, la métrica se lee como avance y añade la barra. |
1229
- | `tone` | `"neutral" \| "alerta"` | | `neutral` | `alerta` solo cuando el número ES el problema. |
1230
- | `value` | `ReactNode` | sí | | El número, ya formateado. La librería no impone locale. |
1547
+ | `delta` | `StatDelta` | | | How the number moved since last time. |
1548
+ | `description` | `ReactNode` | | | The standfirst: the nuance the number alone does not give. «12 aplicaciones» does not say whether that is a lot, and this is where that gets said. |
1549
+ | `icon` | `ReactNode` | | | Glyph in a tinted circle, in the corner opposite the title. At 1em, and it inherits `currentColor` from the badge, so it takes the tone without being tinted separately. |
1550
+ | `label` | `ReactNode` | yes | | What is being counted. It goes in mono small caps. |
1551
+ | `progress` | `number` | | | With `progress`, the metric reads as progress and adds the bar. |
1552
+ | `spark` | `ReactNode` | | | The number's shape over time, under it. A `ReactNode` and not a data prop: a sparkline needs a charting library, and this component lives in the barrel that four projects install. The one project that draws them passes its own, exactly like `icon`. |
1553
+ | `tone` | `"neutral" \| "alert" \| "achievement"` | | `neutral` | `alert` ONLY when the number is the problem, and `achievement` when it is the opposite — the diplomas issued, the modules finished. The two paint the same sand today and they are still two names: a system that names by meaning cannot make «this is bad» the only way to say «this stands out». See `docs/decisions.md` § 28. |
1554
+ | `value` | `ReactNode` | yes | | The number, already formatted. The library imposes no locale. |
1231
1555
 
1232
1556
  ### TalkCard
1233
1557
 
1234
- Fuente: `src/components/talk-card/index.tsx`
1558
+ Source: `src/components/talk-card/index.tsx`
1559
+
1560
+ A talk has more than one destination, and that is what shapes this type.
1235
1561
 
1236
- - Extiende: `Omit<TarjetaProps, 'children' \| 'title'>`
1562
+ - Extends: `\| (Omit<CardShellProps, 'children' \| 'title'> & TalkContent & { /** The card is the link. Do not pass resources with this. */ resources?: never; }) \| (Omit<ComponentPropsWithoutRef<'article'>, 'title'> & TalkContent & { /** * Slides, repo, recording. They are the links, so the card stops being * one. */ resources: ReactNode; })`
1237
1563
 
1238
- | prop | tipo | req. | defecto | qué hace |
1564
+ | prop | type | req. | default | what it does |
1239
1565
  | --- | --- | --- | --- | --- |
1240
- | `asChild` | `boolean \| undefined` | | | Renderiza el hijo en vez de un `<a>`. Es como se enchufa el `Link` de Next o de Astro sin que la librería dependa de ningún enrutador. |
1241
1566
  | `date` | `ReactNode` | | | |
1242
1567
  | `dateTime` | `string` | | | |
1243
- | `description` | `ReactNode` | | | De qué iba la charla. Se corta a dos líneas, igual que el `excerpt` de `ArticleCard`, para que la rejilla no se desalinee. |
1244
- | `event` | `ReactNode` | sí | | Dónde se dio: la conferencia, el meetup, el equipo. |
1568
+ | `description` | `ReactNode` | | | What the talk was about. Clamped to two lines, same as `ArticleCard`'s `excerpt`, so the grid does not fall out of line. |
1569
+ | `event` | `ReactNode` | yes | | Where it was given: the conference, the meetup, the team. |
1245
1570
  | `location` | `ReactNode` | | | |
1246
- | `status` | `ReactNode` | | | Etiqueta corta de estado: «con vídeo», «próxima», «solo audio». |
1247
- | `title` | `ReactNode` | sí | | |
1571
+ | `resources` | `ReactNode` | | | The card is the link. Do not pass resources with this. Slides, repo, recording. They are the links, so the card stops being one. |
1572
+ | `status` | `ReactNode` | | | Short status label: «con vídeo», «próxima», «solo audio». |
1573
+ | `title` | `ReactNode` | yes | | |
1248
1574
 
1249
1575
  ### ThemeToggle
1250
1576
 
1251
- Fuente: `src/components/theme-toggle/index.tsx`
1577
+ Source: `src/components/theme-toggle/index.tsx`
1252
1578
 
1253
- El control que faltaba. La librería definía todo el sistema de temas y no exponía lo que lo cambia, así que dos proyectos lo reimplementaban.
1579
+ The control that was missing. The library defined the whole theming system and exposed nothing that changes it, so two projects were reimplementing it.
1254
1580
 
1255
- - Extiende: `Omit<ComponentPropsWithoutRef<'button'>, 'onClick'>`
1581
+ - Extends: `Omit<ComponentPropsWithoutRef<'button'>, 'onClick'>`
1256
1582
 
1257
- | prop | tipo | req. | defecto | qué hace |
1583
+ | prop | type | req. | default | what it does |
1258
1584
  | --- | --- | --- | --- | --- |
1259
- | `label` | `string` | | `Cambiar de tema` | Nombre accesible. El botón no tiene texto visible, así que es lo único que lo nombra. |
1260
- | `onThemeChange` | `(tema: Tema) => void` | | | Se dispara con el tema que quedó puesto, por si el proyecto quiere anotarlo. |
1261
- | `size` | `"sm" \| "md" \| "lg" \| "icon"` | | `icon` | |
1262
- | `variant` | `"primary" \| "conversion" \| "secondary" \| "tertiary"` | | `secondary` | |
1585
+ | `label` | `string` | | `Cambiar de tema` | Accessible name. The button has no visible text, so it is the only thing naming it. |
1586
+ | `onThemeChange` | `(theme: Theme) => void` | | | Fires with whichever theme ended up set, in case the project wants to record it. |
1587
+ | `size` | `"sm" \| "md" \| "lg" \| "icon" \| "icon-sm"` | | `icon` | |
1588
+ | `variant` | `"primary" \| "conversion" \| "secondary" \| "tertiary" \| "destructive" \| "destructiveOutline"` | | `secondary` | |
1263
1589
 
1264
1590
  ### TableOfContents
1265
1591
 
1266
- Fuente: `src/components/toc/index.tsx`
1592
+ Source: `src/components/toc/index.tsx`
1267
1593
 
1268
- - Extiende: `Omit<ComponentPropsWithoutRef<'nav'>, 'children'>`
1594
+ - Extends: `Omit<ComponentPropsWithoutRef<'nav'>, 'children'>`
1269
1595
 
1270
- | prop | tipo | req. | defecto | qué hace |
1596
+ | prop | type | req. | default | what it does |
1271
1597
  | --- | --- | --- | --- | --- |
1272
- | `activeHref` | `string` | | | Ancla de la sección visible. |
1273
- | `items` | `readonly Entrada[]` | sí | | |
1598
+ | `activeHref` | `string` | | | The anchor of the visible section. |
1599
+ | `items` | `readonly TocEntry[]` | yes | | |
1274
1600
  | `linkAsChild` | `(props: { href: string; children: ReactNode; }) => ReactNode` | | | |
1275
1601
 
1276
- ## Marca
1602
+ ## Brand
1277
1603
 
1278
- Se importan de `@eduardoalvarez/arrecife` o `@eduardoalvarez/arrecife/brand`. 4 exportaciones.
1604
+ Imported from `@eduardoalvarez/arrecife` or `@eduardoalvarez/arrecife/brand`. 4 exports.
1279
1605
 
1280
- ### Isotipo
1606
+ ### Isotype
1281
1607
 
1282
- Fuente: `src/brand/isotipo.tsx`
1608
+ Source: `src/brand/isotype.tsx`
1283
1609
 
1284
- - Extiende: `Omit<ComponentPropsWithoutRef<'img'>, 'src' \| 'alt'>`
1610
+ - Extends: `Omit<ComponentPropsWithoutRef<'img'>, 'src' \| 'alt'>`
1285
1611
 
1286
- | prop | tipo | req. | defecto | qué hace |
1612
+ | prop | type | req. | default | what it does |
1287
1613
  | --- | --- | --- | --- | --- |
1288
- | `alt` | `string` | | | Texto alternativo. Vacío cuando el isotipo acompaña a un texto que ya lo nombra. |
1289
- | `basePath` | `string` | | `RUTA_ASSETS` | |
1290
- | `sobre` | `"oscuro" \| "claro"` | | `oscuro` | Sobre qué fondo se monta. Es obligatorio decidirlo, aunque tenga default: el cuerpo de la aleta es casi negro, así que la variante de dos azules desaparece sobre abismo. Al ser una prop, la regla deja de ser algo que recordar. |
1614
+ | `alt` | `string` | | | Alt text. Empty when the isotype accompanies text that already names it. |
1615
+ | `background` | `"dark" \| "light"` | | `dark` | Which background it sits on. Deciding is mandatory even though it has a default: the fin's body is nearly black, so the two-blue variant disappears over abyss. Being a prop, the rule stops being something to remember. |
1616
+ | `basePath` | `string` | | `ASSETS_PATH` | |
1291
1617
 
1292
1618
  ### Logo
1293
1619
 
1294
- Fuente: `src/brand/logo.tsx`
1620
+ Source: `src/brand/logo.tsx`
1295
1621
 
1296
- El wordmark sale de `naming.wordmark`, no de una cadena escrita a mano, y siempre dice «Eduardo Álvarez». La mascota se llama Tiburoncín y no aparece escrita dentro del logo: no hay ninguna prop que permita cambiar el texto.
1622
+ The wordmark comes from `naming.wordmark`, not from a hand-written string, and it always reads «Eduardo Álvarez». The mascot is called Tiburoncín and its name never appears inside the logo: there is no prop that changes the text.
1297
1623
 
1298
- - Extiende: `Omit<ComponentPropsWithoutRef<'span'>, 'children'>`
1624
+ - Extends: `Omit<ComponentPropsWithoutRef<'span'>, 'children'>`
1299
1625
 
1300
- | prop | tipo | req. | defecto | qué hace |
1626
+ | prop | type | req. | default | what it does |
1301
1627
  | --- | --- | --- | --- | --- |
1302
- | `basePath` | `string` | | `RUTA_ASSETS` | |
1303
- | `conLema` | `boolean \| undefined` | | `false` | Añade el lema bajo el wordmark, separado de la aleta por una divisoria. |
1304
- | `sobre` | `"oscuro" \| "claro"` | | `oscuro` | |
1305
- | `soloIsotipo` | `boolean \| undefined` | | `false` | Oculta el wordmark y deja solo la aleta, para barras muy estrechas. |
1628
+ | `background` | `"dark" \| "light"` | | `dark` | |
1629
+ | `basePath` | `string` | | `ASSETS_PATH` | |
1630
+ | `isotypeOnly` | `boolean \| undefined` | | `false` | Hides the wordmark and leaves only the fin, for very narrow bars. |
1631
+ | `withTagline` | `boolean \| undefined` | | `false` | Adds the tagline under the wordmark, separated from the fin by a divider. |
1306
1632
 
1307
- ### Mascota, CaraDeMascota
1633
+ ### Mascot, MascotFace
1308
1634
 
1309
- Fuente: `src/brand/mascota.tsx`
1635
+ Source: `src/brand/mascot.tsx`
1636
+
1637
+ **Mascot**
1638
+ Full-body Tiburoncín.
1639
+
1640
+ - Extends: `Base`
1641
+
1642
+ | prop | type | req. | default | what it does |
1643
+ | --- | --- | --- | --- | --- |
1644
+ | `alt` | `string` | | | Alt text. Empty by default: the mascot is illustration and the text beside it already says what there is to know. Fill it in only when the image carries information that is not written next to it. |
1645
+ | `basePath` | `string` | | `ASSETS_PATH` | |
1646
+ | `pose` | `"desk" \| "laptop-coffee" \| "peek" \| "surf"` | yes | | |
1310
1647
 
1311
- **Mascota**
1312
- Tiburoncín de cuerpo entero.
1648
+ **MascotFace**
1649
+ Tiburoncín's head, with an expression.
1313
1650
 
1314
- - Extiende: `Base`
1651
+ - Extends: `Base`
1315
1652
 
1316
- | prop | tipo | req. | defecto | qué hace |
1653
+ | prop | type | req. | default | what it does |
1317
1654
  | --- | --- | --- | --- | --- |
1318
- | `alt` | `string` | | | Texto alternativo. Vacío por defecto: la mascota es ilustración y el texto que la acompaña ya dice lo que hay que saber. Se rellena solo cuando la imagen aporta información que no está escrita al lado. |
1319
- | `basePath` | `string` | | `RUTA_ASSETS` | |
1320
- | `pose` | `"desk" \| "laptop-coffee" \| "peek" \| "surf"` | sí | | |
1655
+ | `alt` | `string` | | | Alt text. Empty by default: the mascot is illustration and the text beside it already says what there is to know. Fill it in only when the image carries information that is not written next to it. |
1656
+ | `basePath` | `string` | | `ASSETS_PATH` | |
1657
+ | `expression` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | yes | | |
1658
+
1659
+ ## Social icons
1660
+
1661
+ Imported from `@eduardoalvarez/arrecife/social` · or grouped as `social` from the root. 9 exports.
1662
+
1663
+ ### GitHub, LinkedIn, X, Instagram, Discord, YouTube, Rss, Email, Newsletter
1664
+
1665
+ Source: `src/social/index.tsx`
1666
+
1667
+ **GitHub**
1668
+ - No own props: it passes through those of the element or primitive it wraps.
1669
+
1670
+ **LinkedIn**
1671
+ - No own props: it passes through those of the element or primitive it wraps.
1672
+
1673
+ **X**
1674
+ - No own props: it passes through those of the element or primitive it wraps.
1321
1675
 
1322
- **CaraDeMascota**
1323
- La cabeza de Tiburoncín, con expresión.
1676
+ **Instagram**
1677
+ - No own props: it passes through those of the element or primitive it wraps.
1324
1678
 
1325
- - Extiende: `Base`
1679
+ **Discord**
1680
+ - No own props: it passes through those of the element or primitive it wraps.
1326
1681
 
1327
- | prop | tipo | req. | defecto | qué hace |
1682
+ **YouTube**
1683
+ - No own props: it passes through those of the element or primitive it wraps.
1684
+
1685
+ **Rss**
1686
+ - No own props: it passes through those of the element or primitive it wraps.
1687
+
1688
+ **Email**
1689
+ - No own props: it passes through those of the element or primitive it wraps.
1690
+
1691
+ **Newsletter**
1692
+ The newsletter. It plays the same role as `Rss` — a way to follow, not a social network — which is why it belongs in this catalogue and does not open the door to an icon library.
1693
+
1694
+ - No own props: it passes through those of the element or primitive it wraps.
1695
+
1696
+ ## Icons
1697
+
1698
+ Imported from `@eduardoalvarez/arrecife/icons` · requires `@phosphor-icons/react`. 1 exports.
1699
+
1700
+ ### Icon
1701
+
1702
+ Source: `src/icons/index.tsx`
1703
+
1704
+ - Extends: `Omit<PhosphorIconProps, 'size' \| 'weight' \| 'ref'>`
1705
+
1706
+ | prop | type | req. | default | what it does |
1328
1707
  | --- | --- | --- | --- | --- |
1329
- | `alt` | `string` | | | Texto alternativo. Vacío por defecto: la mascota es ilustración y el texto que la acompaña ya dice lo que hay que saber. Se rellena solo cuando la imagen aporta información que no está escrita al lado. |
1330
- | `basePath` | `string` | | `RUTA_ASSETS` | |
1331
- | `expresion` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | sí | | |
1708
+ | `as` | `Icon` | yes | | The Phosphor icon itself, passed as a component: `<Icon as={Books} />`. |
1709
+ | `label` | `string` | | | The accessible name. WITHOUT it the icon is decorative and gets `aria-hidden`, which is the right default: most icons sit beside their own label and announcing them twice is noise. |
1710
+ | `tone` | `"action" \| "current" \| "quiet"` | | `action` | WHAT THE ICON IS DOING, which is what picks the weight. Three values, and there is no fourth: `action` is the default and the system's line, `current` is the one of a set you are on, `quiet` is furniture that is not a control. `weight` is deliberately not a prop — see `TONE_WEIGHT`. |
1332
1711
 
1333
- ## Formularios
1712
+ ## Forms
1334
1713
 
1335
- Se importan de `@eduardoalvarez/arrecife/form` · pide `react-hook-form`. 7 exportaciones.
1714
+ Imported from `@eduardoalvarez/arrecife/form` · requires `react-hook-form`. 7 exports.
1336
1715
 
1337
1716
  ### FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage, Form
1338
1717
 
1339
- Fuente: `src/form/index.tsx`
1718
+ Source: `src/form/index.tsx`
1340
1719
 
1341
1720
  **FormField**
1342
- Un campo controlado. Envuelve el `Controller` de RHF y además publica el nombre en contexto, que es de donde lo leen la etiqueta y el mensaje sin que haya que repetirlo tres veces.
1721
+ A controlled field. It wraps RHF's `Controller` and also publishes the name into context, which is where the label and the message read it from without having to repeat it three times.
1343
1722
 
1344
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1723
+ - No own props: it passes through those of the element or primitive it wraps.
1345
1724
 
1346
1725
  **FormItem**
1347
- La caja del campo: etiqueta, control, ayuda y mensaje, en columna.
1726
+ The field's box: label, control, help and message, in a column.
1348
1727
 
1349
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1728
+ - No own props: it passes through those of the element or primitive it wraps.
1350
1729
 
1351
1730
  **FormLabel**
1352
- La etiqueta NO se tiñe de rojo cuando el campo falla.
1731
+ The label is NOT tinted red when the field fails.
1353
1732
 
1354
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1733
+ - No own props: it passes through those of the element or primitive it wraps.
1355
1734
 
1356
1735
  **FormControl**
1357
- Envuelve al control y le cablea los atributos: el `id` que la etiqueta apunta, el `aria-describedby` con la ayuda y el mensaje, y el `aria-invalid`.
1736
+ Wraps the control and wires its attributes: the `id` the label points at, the `aria-describedby` with the help and the message, and the `aria-invalid`.
1358
1737
 
1359
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1738
+ - No own props: it passes through those of the element or primitive it wraps.
1360
1739
 
1361
1740
  **FormDescription**
1362
- La ayuda del campo. Se anuncia siempre, haya error o no.
1741
+ The field's help text. It is always announced, error or not.
1363
1742
 
1364
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1743
+ - No own props: it passes through those of the element or primitive it wraps.
1365
1744
 
1366
1745
  **FormMessage**
1367
- El mensaje de validación. Sin error no renderiza nada: un hueco reservado para el fallo desplaza el resto del formulario cada vez que aparece.
1746
+ The validation message. With no error it renders nothing: a gap reserved for the failure shifts the rest of the form every time it appears.
1368
1747
 
1369
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1748
+ - No own props: it passes through those of the element or primitive it wraps.
1370
1749
 
1371
1750
  **Form**
1372
- La capa que ata los controles a un formulario con validación y mensajes.
1751
+ The layer that ties the controls to a form with validation and messages.
1373
1752
 
1374
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1753
+ - No own props: it passes through those of the element or primitive it wraps.
1375
1754
 
1376
- ## Gráficas
1755
+ ## Charts
1377
1756
 
1378
- Se importan de `@eduardoalvarez/arrecife/chart` · pide `recharts`. 5 exportaciones.
1757
+ Imported from `@eduardoalvarez/arrecife/chart` · requires `recharts`. 5 exports.
1379
1758
 
1380
1759
  ### ChartContainer, ChartTooltip, ChartLegend, ChartTooltipContent, ChartLegendContent
1381
1760
 
1382
- Fuente: `src/chart/index.tsx`
1761
+ Source: `src/chart/index.tsx`
1383
1762
 
1384
1763
  **ChartContainer**
1385
- Envuelve la gráfica en un `<figure>` con nombre accesible y le da a Recharts el alto concreto que necesita para medirse.
1764
+ Wraps the chart in a `<figure>` with an accessible name and gives Recharts the concrete height it needs to measure itself.
1386
1765
 
1387
- - Extiende: `Omit<ComponentPropsWithoutRef<'figure'>, 'title'>`
1766
+ - Extends: `Omit<ComponentPropsWithoutRef<'figure'>, 'title'>`
1388
1767
 
1389
- | prop | tipo | req. | defecto | qué hace |
1768
+ | prop | type | req. | default | what it does |
1390
1769
  | --- | --- | --- | --- | --- |
1391
- | `height` | `number` | | `320` | Alto en píxeles. Recharts necesita uno concreto para medir. |
1392
- | `label` | `string` | sí | | Qué muestra la gráfica, en una frase. Obligatorio, como el `label` de `Progress`: un `<svg>` de barras sin nombre accesible no es «una gráfica sin etiqueta», es una región vacía. |
1393
- | `summary` | `ReactNode` | | | Lo que la gráfica dice, en palabras. Va en un `figcaption` oculto visualmente. |
1770
+ | `height` | `number` | | `320` | Height in pixels. Recharts needs a concrete one to measure itself. |
1771
+ | `label` | `string` | yes | | What the chart shows, in one sentence. Mandatory, like `Progress`'s `label`: a bar `<svg>` with no accessible name is not «a chart without a label», it is an empty region. |
1772
+ | `summary` | `ReactNode` | | | What the chart says, in words. It goes in a visually hidden `figcaption`. |
1394
1773
 
1395
1774
  **ChartTooltip**
1396
- El `Tooltip` de Recharts con los defectos del sistema: sin animación y con el cursor teñido de `surfaceRaised`.
1775
+ Recharts' `Tooltip` with the system's defaults: no animation, and the cursor tinted `surfaceRaised`.
1397
1776
 
1398
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1777
+ - No own props: it passes through those of the element or primitive it wraps.
1399
1778
 
1400
1779
  **ChartLegend**
1401
- - Sin props propios: pasa los del elemento o la primitiva que envuelve.
1780
+ - No own props: it passes through those of the element or primitive it wraps.
1402
1781
 
1403
1782
  **ChartTooltipContent**
1404
- La caja del tooltip. Es una tarjeta del sistema —`surface`, borde de control, sombra estándar— y no la caja blanca de Recharts, que en modo oscuro es un rectángulo blanco encima de un panel oscuro.
1783
+ The tooltip's box. It is a system card — `surface`, control border, standard shadow — and not Recharts' white box, which in dark mode is a white rectangle on top of a dark panel.
1405
1784
 
1406
- - Extiende: `{ active?: boolean \| undefined; payload?: readonly ChartPayloadItem[] \| undefined; label?: ReactNode; /** Formatea el valor. Sin ella se imprime tal cual: la librería no impone locale. */ formatter?: ((valor: unknown, item: ChartPayloadItem) => ReactNode) \| undefined; /** Oculta el encabezado, para una gráfica de una sola categoría. */ hideLabel?: boolean; className?: string; }`
1785
+ - Extends: `{ active?: boolean \| undefined; payload?: readonly ChartPayloadItem[] \| undefined; label?: ReactNode; /** Formats the value. Without it, it is printed as is: the library imposes no locale. */ formatter?: ((value: unknown, item: ChartPayloadItem) => ReactNode) \| undefined; /** Hides the header, for a single-category chart. */ hideLabel?: boolean; className?: string; }`
1407
1786
 
1408
- | prop | tipo | req. | defecto | qué hace |
1787
+ | prop | type | req. | default | what it does |
1409
1788
  | --- | --- | --- | --- | --- |
1410
1789
  | `active` | `boolean \| undefined` | | | |
1411
1790
  | `className` | `string` | | | |
1412
- | `formatter` | `(valor: unknown, item: ChartPayloadItem) => ReactNode` | | | Formatea el valor. Sin ella se imprime tal cual: la librería no impone locale. |
1413
- | `hideLabel` | `boolean` | | `false` | Oculta el encabezado, para una gráfica de una sola categoría. |
1791
+ | `formatter` | `(value: unknown, item: ChartPayloadItem) => ReactNode` | | | Formats the value. Without it, it is printed as is: the library imposes no locale. |
1792
+ | `hideLabel` | `boolean` | | `false` | Hides the header, for a single-category chart. |
1414
1793
  | `label` | `ReactNode` | | | |
1415
1794
  | `payload` | `readonly ChartPayloadItem[]` | | | |
1416
1795
 
1417
1796
  **ChartLegendContent**
1418
- La leyenda con la misma marca cuadrada del tooltip y la escala `label`.
1797
+ The legend, with the tooltip's same square swatch and the `label` scale.
1419
1798
 
1420
- - Extiende: `{ payload?: readonly ChartPayloadItem[] \| undefined; className?: string; }`
1799
+ - Extends: `{ payload?: readonly ChartPayloadItem[] \| undefined; className?: string; }`
1421
1800
 
1422
- | prop | tipo | req. | defecto | qué hace |
1801
+ | prop | type | req. | default | what it does |
1423
1802
  | --- | --- | --- | --- | --- |
1424
1803
  | `className` | `string` | | | |
1425
1804
  | `payload` | `readonly ChartPayloadItem[]` | | | |
1426
1805
 
1427
- ## Exportaciones que no son componentes
1806
+ ## Exports that are not components
1428
1807
 
1429
- La raíz reexporta todo lo de `./tokens` y `./brand` por conveniencia. Cada uno
1430
- aparece una sola vez, en la subruta más específica que lo publica: si el código
1431
- no monta React, esa subruta es la que hay que importar.
1808
+ The root re-exports everything from `./tokens` and `./brand` for convenience.
1809
+ Each one appears exactly once, under the most specific subpath that publishes
1810
+ it: if the code does not mount React, that subpath is the one to import.
1811
+
1812
+ ### `@eduardoalvarez/arrecife/variants`
1813
+
1814
+ | export | type | what it is |
1815
+ | --- | --- | --- |
1816
+ | `alertVariants` | `(props?: (ConfigVariants<{ variant: { accent: string; success: string; warning: string; error: string; }; emphasis: { subtle: string; strong: string; }; }> & ClassProp) \| undefined): string` | |
1817
+ | `avatarVariants` | `(props?: (ConfigVariants<{ size: { sm: string; md: string; lg: string; xl: string; }; }> & ClassProp) \| undefined): string` | |
1818
+ | `badgeVariants` | `(props?: (ConfigVariants<{ variant: { neutral: string; accent: string; warm: string; success: string; warning: string; error: string; }; }> & ClassProp) \| undefined): string` | |
1819
+ | `buttonVariants` | `(props?: (ConfigVariants<{ variant: { primary: string[]; conversion: string; secondary: string[]; tertiary: string[]; destructive: string; destructiveOutline: string[]; }; size: { sm: string; md: string; lg: string; icon: string; 'icon-sm': string; }; }> & ClassProp) \| undefined): string` | |
1820
+ | `CARD` | `string[]` | |
1821
+ | `CARD_HOVER` | `"transition-standard hover:border-hairline-hover"` | |
1822
+ | `CARD_SURFACE` | `"rounded-card border-hairline bg-surface border"` | |
1823
+ | `categoryBadgeVariants` | `(props?: (ConfigVariants<{ active: { false: string; true: string; }; }> & ClassProp) \| undefined): string` | |
1824
+ | `metricBadgeVariants` | `string[]` | |
1825
+ | `textVariants` | `(props?: (ConfigVariants<{ variant: { display: string; stat: string; h1: string; h2: string; h3: string; body: string; lead: string; ui: string; label: string; tag: string; chip: string; meta: string; eyebrow: string; }; tone: { ...; }; }> & ClassProp) \| undefined): string` | |
1432
1826
 
1433
1827
  ### `@eduardoalvarez/arrecife/tokens`
1434
1828
 
1435
- | export | tipo | qué es |
1829
+ | export | type | what it is |
1436
1830
  | --- | --- | --- |
1437
- | `brand` | `{ readonly body: "#3E7CB1"; readonly spots: "#C2D7E7"; readonly hull: "#0B1524"; }` | Marca — iguales en los dos modos. |
1831
+ | `brand` | `{ readonly body: "#3E7CB1"; readonly spots: "#C2D7E7"; readonly hull: "#0B1524"; }` | Brand — identical in both modes. |
1438
1832
  | `colors` | `{ dark, light }` | |
1439
- | `control` | `{ readonly sm: 14; readonly md: 22; readonly lg: 30; readonly icon: 42; }` | Controles, del documento: `sm 8/14 · md 12/22 · lg 15/30 · icono 42×42`. |
1440
- | `dark` | `{ background, surface, surfaceRaised, border, hairline, hairlineHover, textPrimary, textSecondary, textMuted, accent, accentHover, accentOn, warm, warmHover, warmOn, success, warning, error }` | Modo oscuro (primario). Contrastes medidos sobre `background` #091319. |
1833
+ | `control` | `{ readonly sm: 14; readonly md: 22; readonly lg: 30; readonly icon: 42; readonly iconSm: 32; }` | Controls, from the document: `sm 8/14 · md 12/22 · lg 15/30 · icon 42×42`. |
1834
+ | `dark` | `{ background, surface, surfaceRaised, border, hairline, hairlineHover, textPrimary, textSecondary, textMuted, accent, accentHover, accentOn, warm, warmHover, warmOn, success, warning, error, danger, dangerHover, dangerOn }` | Dark mode (primary). Contrast measured against `background` #091319. |
1441
1835
  | `fonts` | `{ display, sans, mono }` | |
1442
1836
  | `gradient` | `{ dark, light }` | |
1443
- | `light` | `{ background, surface, surfaceRaised, border, hairline, hairlineHover, textPrimary, textSecondary, textMuted, accent, accentHover, accentOn, warm, warmHover, warmOn, success, warning, error }` | Modo claro. Contrastes medidos sobre `background` #F6F2EA. `background` es blanco CÁLIDO: nunca #FFF como fondo de página. |
1444
- | `limits` | `{ readonly minScreenPx: 13; readonly minPrintPt: 12; readonly measure: "68ch"; }` | Límites duros de legibilidad. |
1445
- | `motion` | `{ readonly duration: "150ms"; readonly easing: "ease-out"; readonly properties: "color, background-color, border-color, fill, stroke"; }` | 150ms ease-out — solo color y borde. El sistema no anima posición ni escala: los estados se comunican con borde y color, no con movimiento. |
1446
- | `naming` | `{ readonly wordmark: "Eduardo Álvarez"; readonly mascot: "Tiburoncín"; readonly domain: "eduardoalvarez.dev"; }` | El wordmark siempre dice «Eduardo Álvarez». La mascota se llama Tiburoncín y nunca aparece escrita dentro del logo. |
1837
+ | `light` | `{ background, surface, surfaceRaised, border, hairline, hairlineHover, textPrimary, textSecondary, textMuted, accent, accentHover, accentOn, warm, warmHover, warmOn, success, warning, error, danger, dangerHover, dangerOn }` | Light mode. Contrast measured against `background` #F6F2EA. `background` is WARM white: never #FFF as the page background. |
1838
+ | `limits` | `{ readonly minScreenPx: 13; readonly minPrintPt: 12; readonly measure: "68ch"; }` | Hard legibility limits. |
1839
+ | `motion` | `{ readonly duration: "150ms"; readonly easing: "ease-out"; readonly properties: "color, background-color, border-color, fill, stroke"; }` | 150ms ease-out — color and border only. The system animates neither position nor scale: states are communicated with border and color, not with movement. |
1840
+ | `naming` | `{ readonly wordmark: "Eduardo Álvarez"; readonly mascot: "Tiburoncín"; readonly domain: "eduardoalvarez.dev"; }` | The wordmark always reads «Eduardo Álvarez». The mascot is called Tiburoncín and its name never appears inside the logo. |
1447
1841
  | `radius` | `{ readonly chip: 6; readonly control: 10; readonly card: 14; readonly panel: 16; readonly pill: 999; }` | |
1448
- | `series` | `{ readonly dark: readonly ["#35D6C0", "#F2A65A", "#3E7CB1", "#71919C"]; readonly light: readonly ["#0D7C6F", "#A65B27", "#3E7CB1", "#626A75"]; }` | La paleta de series de las gráficas. CUATRO, por el mismo motivo que la de sintaxis: el sistema se comunica con color y borde, no con ruido cromático. |
1449
- | `shadow` | `{ readonly standard: "0 1px 2px rgba(0, 0, 0, 0.35)"; }` | Un solo nivel. No hay escala de elevación. |
1450
- | `sintaxis` | `{ fondo, identificador, literal, palabraClave, comentario, invalido }` | La paleta del resaltado de sintaxis. |
1451
- | `size` | `{ readonly nav: 64; readonly content: 760; readonly wide: 1180; }` | |
1452
- | `spacing` | `{ readonly stepXs: 8; readonly stepSm: 12; readonly stepMd: 16; readonly stepLg: 26; readonly stepXl: 40; readonly section: 96; }` | El ritmo de página. Los cinco escalones llevan `step` en el nombre, y no es decoración: es la corrección de un bug que no dio la cara en ningún sitio. |
1453
- | `tagline` | `{ largo, corto, en }` | |
1454
- | `tokens` | `{ colors, brand, fonts, typeScale, limits, radius, control, spacing, size, gradient, sintaxis, series, shadow, motion, tagline, naming }` | Todos los tokens en un solo objeto, para plantillas Satori y generadores. |
1842
+ | `series` | `{ readonly dark: readonly ["#35D6C0", "#F2A65A", "#3E7CB1", "#71919C"]; readonly light: readonly ["#0D7C6F", "#A65B27", "#3E7CB1", "#626A75"]; }` | The chart series palette. FOUR, for the same reason as the syntax palette: the system communicates with color and border, not with chromatic noise. |
1843
+ | `shadow` | `{ readonly standard: "0 1px 2px rgba(0, 0, 0, 0.35)"; }` | A single level. There is no elevation scale. |
1844
+ | `size` | `{ readonly nav: 64; readonly navCompact: 56; readonly sidebar: 256; readonly sidebarRail: 56; readonly content: 760; readonly wide: 1180; }` | |
1845
+ | `spacing` | `{ readonly stepXs: 8; readonly stepSm: 12; readonly stepMd: 16; readonly stepLg: 26; readonly stepXl: 40; readonly section: 96; }` | Page rhythm. All five steps carry `step` in the name, and that is not decoration: it is the fix for a bug that never surfaced anywhere. |
1846
+ | `syntax` | `{ background, identifier, literal, keyword, comment, invalid }` | The syntax highlighting palette. |
1847
+ | `tagline` | `{ long, short, en }` | |
1848
+ | `tokens` | `{ colors, brand, fonts, typeScale, limits, radius, control, spacing, size, gradient, syntax, series, shadow, motion, tagline, naming }` | Every token in a single object, for Satori templates and generators. |
1455
1849
  | `typeScale` | `{ display, stat, h1, h2, h3, body, lead, ui, label, tag, chip, meta, eyebrow }` | |
1456
1850
 
1457
- Tipos (13): `BrandToken`, `ColorMode`, `ColorToken`, `ControlToken`, `FontToken`, `GradientToken`, `RadiusToken`, `SeriesToken`, `SintaxisToken`, `SizeToken`, `SpacingToken`, `Tokens`, `TypeScaleToken`.
1851
+ Types (13): `BrandToken`, `ColorMode`, `ColorToken`, `ControlToken`, `FontToken`, `GradientToken`, `RadiusToken`, `SeriesToken`, `SizeToken`, `SpacingToken`, `SyntaxToken`, `Tokens`, `TypeScaleToken`.
1852
+
1853
+ ### `@eduardoalvarez/arrecife/social`
1854
+
1855
+ Types (1): `SocialIconProps`.
1856
+
1857
+ ### `@eduardoalvarez/arrecife/theme`
1858
+
1859
+ | export | type | what it is |
1860
+ | --- | --- | --- |
1861
+ | `applyTheme` | `(theme: Theme, persist?: boolean): void` | Sets the theme on `<html>` and persists it. |
1862
+ | `currentTheme` | `(): Theme` | The theme currently in place, read from the DOM. |
1863
+ | `preferredTheme` | `({ base }?: ThemeOptions): Theme` | The theme that applies: whatever was chosen, and with no choice, `base` if the site declared one, else whatever the system asks for. With no `prefers-color-scheme` declared, dark, which is primary. |
1864
+ | `storedTheme` | `(): Theme \| null` | The stored preference, if any. `null` means «nobody has chosen», which is not the same as «chose dark»: with no choice, the system decides. |
1865
+ | `THEME_ATTRIBUTE` | `"data-theme"` | The attribute the `[data-theme]` blocks in `theme.css` read. |
1866
+ | `THEME_EVENT` | `"arrecife:theme"` | The event emitted when the theme changes. |
1867
+ | `THEME_KEY` | `"arrecife-theme"` | The `localStorage` key. |
1868
+ | `themeScript` | `({ base }?: ThemeOptions): string` | The script that goes INLINE in the `<head>`, before any stylesheet. |
1869
+ | `toggleTheme` | `(): Theme` | Switches to the opposite one and returns whichever stuck. |
1870
+ | `watchTheme` | `(onChange: (theme: Theme) => void, options?: ThemeOptions): () => void` | Subscribes to theme changes and returns the function that cancels it. |
1871
+
1872
+ Types (2): `Theme`, `ThemeOptions`.
1458
1873
 
1459
1874
  ### `@eduardoalvarez/arrecife/brand`
1460
1875
 
1461
- | export | tipo | qué es |
1876
+ | export | type | what it is |
1462
1877
  | --- | --- | --- |
1463
- | `aletas` | `{ readonly color: "fin.png"; readonly espuma: "fin-foam.png"; }` | La aleta, en sus dos variantes. |
1464
- | `caras` | `{ annoyed, confused, hearts, laughing, shades, waiting, wink }` | Las caras. Solo se usan en estados vacíos, confirmaciones, errores, progreso de curso y celebración — nunca en hero, precios, servicios, contacto ni CV. Por eso `EmptyState` recibe una cara y `PageHeader` no. |
1465
- | `listaCaras` | `readonly ("annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink")[]` | |
1466
- | `listaPoses` | `readonly ("desk" \| "laptop-coffee" \| "peek" \| "surf")[]` | |
1467
- | `poses` | `{ readonly desk: "pose-desk.png"; readonly 'laptop-coffee': "pose-laptop-coffee.png"; readonly peek: "pose-peek.png"; readonly surf: "pose-surf.png"; }` | Poses de cuerpo entero. |
1468
- | `RUTA_ASSETS` | `"/brand"` | Dónde se sirven los PNG. Por defecto `/brand`, que es donde ya viven en los cinco proyectos (`public/brand/`), así que no hay nada que configurar. |
1469
- | `usoDeCara` | `{ wink, waiting, laughing, shades, hearts, confused, annoyed }` | El uso asignado de cada cara, del inventario del manual. |
1878
+ | `ASSETS_PATH` | `"/brand"` | Where the PNGs are served from. `/brand` by default, which is where they already live in all five projects (`public/brand/`), so there is nothing to configure. |
1879
+ | `faceList` | `readonly ("annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink")[]` | |
1880
+ | `faces` | `{ annoyed, confused, hearts, laughing, shades, waiting, wink }` | The faces. They are used only in empty states, confirmations, errors, course progress and celebration — never in a hero, pricing, services, contact or CV. That is why `EmptyState` takes a face and `PageHeader` does not. |
1881
+ | `faceUsage` | `{ wink, waiting, laughing, shades, hearts, confused, annoyed }` | The assigned use of each face, from the manual's inventory. |
1882
+ | `fins` | `{ readonly color: "fin.png"; readonly foam: "fin-foam.png"; }` | The fin, in its two variants. |
1883
+ | `poseList` | `readonly ("desk" \| "laptop-coffee" \| "peek" \| "surf")[]` | |
1884
+ | `poses` | `{ readonly desk: "pose-desk.png"; readonly 'laptop-coffee': "pose-laptop-coffee.png"; readonly peek: "pose-peek.png"; readonly surf: "pose-surf.png"; }` | Full-body poses. |
1470
1885
 
1471
- Tipos (8): `Aleta`, `Cara`, `CaraDeMascotaProps`, `Fondo`, `IsotipoProps`, `LogoProps`, `MascotaProps`, `Pose`.
1886
+ Types (8): `Background`, `Face`, `Fin`, `IsotypeProps`, `LogoProps`, `MascotFaceProps`, `MascotProps`, `Pose`.
1472
1887
 
1473
- ### `@eduardoalvarez/arrecife/shiki`
1888
+ ### `@eduardoalvarez/arrecife/icons`
1474
1889
 
1475
- | export | tipo | qué es |
1890
+ | export | type | what it is |
1476
1891
  | --- | --- | --- |
1477
- | `arrecife` | `TemaShiki` | |
1892
+ | `ICON_WEIGHT` | `"light" \| "fill" \| "thin" \| "regular" \| "bold" \| "duotone"` | Phosphor's own name for the system's line. It is what `tone="action"` resolves to. |
1893
+ | `TONE_WEIGHT` | `Record<IconTone, IconWeight>` | The three roles, and the weight each one is drawn at. This is the whole of the weight axis: Phosphor ships six and this system reads three, because the other three — `thin`, `bold`, `duotone` — have no role behind them here. |
1478
1894
 
1479
- Tipos (1): `TemaShiki`.
1895
+ Types (2): `IconProps`, `IconTone`.
1480
1896
 
1481
- ### `@eduardoalvarez/arrecife/chart`
1897
+ ### `@eduardoalvarez/arrecife/shiki`
1482
1898
 
1483
- | export | tipo | qué es |
1899
+ | export | type | what it is |
1484
1900
  | --- | --- | --- |
1485
- | `colorDeSerie` | `(indice: number): string` | El color de la serie `indice`, como custom property. |
1486
- | `COLORES_DE_SERIE` | `string[]` | Las cuatro, en orden, para pasárselas de golpe a un `Pie` con `Cell`. |
1901
+ | `arrecife` | `ShikiTheme` | |
1487
1902
 
1488
- Tipos (4): `ChartContainerProps`, `ChartLegendContentProps`, `ChartPayloadItem`, `ChartTooltipContentProps`.
1903
+ Types (1): `ShikiTheme`.
1489
1904
 
1490
- ### `@eduardoalvarez/arrecife/tema`
1905
+ ### `@eduardoalvarez/arrecife/chart`
1491
1906
 
1492
- | export | tipo | qué es |
1907
+ | export | type | what it is |
1493
1908
  | --- | --- | --- |
1494
- | `alternarTema` | `(): Tema` | Cambia al contrario y devuelve el que quedó. |
1495
- | `aplicarTema` | `(tema: Tema, persistir?: boolean): void` | Pone el tema en el `<html>` y lo persiste. |
1496
- | `escucharTema` | `(alCambiar: (tema: Tema) => void): () => void` | Se suscribe a los cambios de tema y devuelve la función que cancela. |
1497
- | `scriptTema` | `string` | El script que va INLINE en el `<head>`, antes de cualquier hoja de estilo. |
1498
- | `TEMA_ATRIBUTO` | `"data-theme"` | El atributo que leen los bloques `[data-theme]` de `theme.css`. |
1499
- | `TEMA_CLAVE` | `"arrecife-tema"` | La clave de `localStorage`. |
1500
- | `TEMA_EVENTO` | `"arrecife:tema"` | El evento que se emite cuando el tema cambia. |
1501
- | `temaActual` | `(): Tema` | El tema que hay puesto ahora mismo, leído del DOM. |
1502
- | `temaGuardado` | `(): Tema \| null` | La preferencia guardada, si la hay. `null` significa «nadie ha elegido», que no es lo mismo que «eligió oscuro»: sin elección manda el sistema. |
1503
- | `temaPreferido` | `(): Tema` | El tema que corresponde: lo elegido, y si no hay elección, lo que pida el sistema. Sin `prefers-color-scheme` declarado, oscuro, que es el primario. |
1504
-
1505
- Tipos (1): `Tema`.
1909
+ | `SERIES_COLORS` | `string[]` | All four, in order, to hand to a `Pie` with `Cell` in one go. |
1910
+ | `seriesColor` | `(index: number): string` | The color of series `index`, as a custom property. |
1911
+
1912
+ Types (4): `ChartContainerProps`, `ChartLegendContentProps`, `ChartPayloadItem`, `ChartTooltipContentProps`.
1506
1913
 
1507
1914
  ### `@eduardoalvarez/arrecife/form`
1508
1915
 
1509
- | export | tipo | qué es |
1916
+ | export | type | what it is |
1510
1917
  | --- | --- | --- |
1511
- | `useFormField` | `(): { invalid: boolean; isDirty: boolean; isTouched: boolean; isValidating: boolean; error?: FieldError; name: string; id: string; idDescripcion: string; idMensaje: string; }` | Lo que necesita cualquier pieza del campo: el nombre, los tres ids y el estado de validación. |
1918
+ | `useFormField` | `(): { invalid: boolean; isDirty: boolean; isTouched: boolean; isValidating: boolean; error?: FieldError; name: string; id: string; descriptionId: string; messageId: string; }` | What any piece of the field needs: the name, the three ids and the validation state. |
1512
1919
 
1513
1920
  ### `@eduardoalvarez/arrecife/og`
1514
1921
 
1515
- | export | tipo | qué es |
1922
+ | export | type | what it is |
1516
1923
  | --- | --- | --- |
1517
- | `OG` | `{ readonly width: 1200; readonly height: 630; readonly margen: 64; readonly mascota: 430; readonly aletaFirma: 34; readonly reservaMascota: 560; }` | El lienzo y la retícula. Son las medidas de producción. |
1518
- | `plantillaArticulo` | `(datos: DatosArticulo): NodoSatori` | Artículo · degradado 145° sobre abismo, categoría y lectura en arena. |
1519
- | `plantillaBase` | `(opciones: { modo: "oscuro" \| "claro"; fondo: string; eyebrow: { texto: string; color: string; }; title: string; bajada?: string \| undefined; firma: string; firmaColor: string; base: string; mascota?: NodoSatori \| null \| undefined; mascotaIzquierda?: boolean \| undefined; tinta: string; tintaSecundaria: string; }): NodoSatori` | |
1520
- | `plantillaCharla` | `(datos: DatosCharla): NodoSatori` | Charla · eyebrow en bioluz con evento y año, pose sangrando por la esquina. |
1521
- | `plantillaCurso` | `(datos: DatosCurso): NodoSatori` | Curso · LA ÚNICA PLANTILLA EN CLARO. |
1522
- | `plantillaDefecto` | `(datos?: DatosDefecto): NodoSatori` | Por defecto · la excepción declarada del documento. |
1924
+ | `articleTemplate` | `(data: ArticleData): SatoriNode` | Article · 145° gradient over abyss, category and reading time in sand. |
1925
+ | `courseTemplate` | `(data: CourseData): SatoriNode` | Course · THE ONLY LIGHT TEMPLATE. |
1926
+ | `defaultTemplate` | `(data?: DefaultData): SatoriNode` | Default · the document's declared exception. |
1927
+ | `OG` | `{ readonly width: 1200; readonly height: 630; readonly margin: 64; readonly mascot: 430; readonly signatureFin: 34; readonly mascotReserve: 560; }` | The canvas and the grid. These are the production measurements. |
1928
+ | `plantillaBase` | `(options: { mode: "dark" \| "light"; background: string; eyebrow: { text: string; color: string; }; title: string; bajada?: string \| undefined; signature: string; firmaColor: string; base: string; mascot?: SatoriNode \| ... 1 more ... \| undefined; mascotaIzquierda?: boolean \| undefined; tinta: string; tintaSecundaria: string; }): SatoriNode` | |
1929
+ | `talkTemplate` | `(data: TalkData): SatoriNode` | Talk · eyebrow in biolume with the event and year, pose bleeding off the corner. |
1523
1930
 
1524
- Tipos (6): `DatosArticulo`, `DatosBase`, `DatosCharla`, `DatosCurso`, `DatosDefecto`, `NodoSatori`.
1931
+ Types (6): `ArticleData`, `BaseData`, `CourseData`, `DefaultData`, `SatoriNode`, `TalkData`.
1525
1932
 
1526
1933
  ### `@eduardoalvarez/arrecife`
1527
1934
 
1528
- | export | tipo | qué es |
1935
+ | export | type | what it is |
1529
1936
  | --- | --- | --- |
1530
- | `alertVariants` | `(props?: (ConfigVariants<{ variant: { accent: string; success: string; warning: string; error: string; }; enfasis: { sutil: string; fuerte: string; }; }> & ClassProp) \| undefined): string` | |
1531
- | `avatarVariants` | `(props?: (ConfigVariants<{ size: { sm: string; md: string; lg: string; xl: string; }; }> & ClassProp) \| undefined): string` | |
1532
- | `badgeVariants` | `(props?: (ConfigVariants<{ variant: { neutral: string; accent: string; warm: string; success: string; warning: string; error: string; }; }> & ClassProp) \| undefined): string` | |
1533
- | `buttonVariants` | `(props?: (ConfigVariants<{ variant: { primary: string[]; conversion: string; secondary: string[]; tertiary: string[]; }; size: { sm: string; md: string; lg: string; icon: string; }; }> & ClassProp) \| undefined): string` | |
1534
- | `categoryBadgeVariants` | `(props?: (ConfigVariants<{ active: { false: string; true: string; }; }> & ClassProp) \| undefined): string` | |
1535
1937
  | `cn` | `(...inputs: ClassValue[]): string` | |
1536
- | `HOVER_TARJETA` | `"transition-standard hover:border-hairline-hover"` | El hover de la regla 6: solo el borde. Se aplica donde la tarjeta es pulsable. |
1537
- | `social` | `typeof import("src/lib/social")` | |
1538
- | `SUPERFICIE_TARJETA` | `"rounded-card border-hairline bg-surface border"` | El contenedor de superficie del sistema, y la única definición de lo que es una tarjeta: `surface`, borde `hairline`, radio de tarjeta. |
1539
- | `textVariants` | `(props?: (ConfigVariants<{ variant: { display: string; stat: string; h1: string; h2: string; h3: string; body: string; lead: string; ui: string; label: string; tag: string; chip: string; meta: string; eyebrow: string; }; tone: { ...; }; }> & ClassProp) \| undefined): string` | |
1540
- | `toast` | `(mensaje: ReactNode, opciones?: ToastOptions \| undefined): string` | Lanza un aviso. Devuelve su id, que es lo que hay que guardar para cerrarlo a mano —el caso de «guardando…» que se reemplaza cuando termina la petición. |
1541
- | `useTema` | `(): Tema` | El tema puesto ahora mismo, para un proyecto que necesite ramificar en React —un logo distinto por modo, una imagen que no tiene versión clara—. |
1542
-
1543
- Tipos (60): `AccordionProps`, `AccordionTriggerProps`, `AlertProps`, `ArticleCardProps`, `AudioPlayerMode`, `AudioPlayerProps`, `AuthorCardProps`, `AvatarProps`, `AvatarUploadProps`, `BadgeProps`, `BlockquoteProps`, `BreadcrumbProps`, `ButtonProps`, `CalendarProps`, `CategoryBadgeProps`, `CheckboxProps`, `CodeBlockProps`, `CodeProps`, `CourseCardProps`, `DateFieldProps`, `EmptyStateProps`, `Entrada`, `EventCalendarProps`, `EventoCalendario`, `FooterLinkProps`, `FooterProps`, `HeroProps`, `InputProps`, `LabelProps`, `LinkRowProps`, `MetricBadgeProps`, `Migaja`, `NavItemProps`, `NavProps`, `NewsletterFormProps`, `NewsletterState`, `PageHeaderProps`, `PaginationLinkProps`, `PopoverContentProps`, `ProgressProps`, `RadioGroupItemProps`, `RadioGroupProps`, `Red`, `ScrollingProgressBarProps`, `SeparatorProps`, `SheetContentProps`, `SidebarItemProps`, `SidebarNavProps`, `SkeletonProps`, `StatProps`, `SwitchProps`, `TableOfContentsProps`, `TabsProps`, `TalkCardProps`, `TextProps`, `TextareaProps`, `ThemeToggleProps`, `ToastOptions`, `ToastVariant`, `ToasterProps`.
1544
-
1545
- # Dónde mirar si esto no basta
1546
-
1547
- - Storybook publica cada componente con sus stories y la tabla de props generada
1548
- desde los tipos.
1549
- - `README.md` del repo: el porqué de cada decisión, la tabla de correcciones de
1550
- contraste y el ciclo de publicación.
1551
- - `docs/design-system.md` y `docs/manual-de-marca.md`: los documentos de
1552
- identidad, consultables con `grep`.
1553
- - `docs/decisiones.md`: los quince puntos donde el código y el documento no
1554
- decían lo mismo, con la resolución de cada uno.
1555
- - `AGENTS.md`: para trabajar dentro del repo de la librería.
1938
+ | `social` | `typeof import("src/social/index")` | |
1939
+ | `toast` | `(message: ReactNode, options?: ToastOptions \| undefined): string` | Fires a notice. It returns its id, which is what you keep in order to close it by hand — the «guardando…» case that gets replaced when the request finishes. |
1940
+ | `useTheme` | `(): Theme` | The theme set right now, for a project that needs to branch in React — a different logo per mode, an image with no light version. |
1941
+
1942
+ Types (62): `AccordionProps`, `AccordionTriggerProps`, `AlertProps`, `ArticleCardProps`, `AudioPlayerMode`, `AudioPlayerProps`, `AuthorCardProps`, `AvatarProps`, `AvatarUploadProps`, `BadgeProps`, `BlockquoteProps`, `BreadcrumbProps`, `ButtonProps`, `CalendarEvent`, `CalendarProps`, `CategoryBadgeProps`, `CheckboxProps`, `CodeBlockProps`, `CodeProps`, `CourseCardProps`, `Crumb`, `DateFieldProps`, `EmptyStateProps`, `EventCalendarProps`, `FooterLinkProps`, `FooterProps`, `HeroProps`, `InputProps`, `LabelProps`, `LinkRowProps`, `MetricBadgeProps`, `NavItemProps`, `NavProps`, `NewsletterFormProps`, `NewsletterState`, `PageHeaderProps`, `PaginationLinkProps`, `PopoverContentProps`, `ProgressProps`, `RadioGroupItemProps`, `RadioGroupProps`, `ScrollingProgressBarProps`, `SeparatorProps`, `SheetContentProps`, `SidebarGroupProps`, `SidebarItemProps`, `SidebarNavProps`, `SkeletonProps`, `SocialLink`, `StatDelta`, `StatProps`, `SwitchProps`, `TableOfContentsProps`, `TabsProps`, `TalkCardProps`, `TextProps`, `TextareaProps`, `ThemeToggleProps`, `ToastOptions`, `ToastVariant`, `ToasterProps`, `TocEntry`.
1943
+
1944
+ # Where to look if this is not enough
1945
+
1946
+ - Storybook publishes every component with its stories and the props table
1947
+ generated from the types.
1948
+ - The repo's `README.md`: the reasoning behind each decision, the contrast
1949
+ correction table and the release cycle.
1950
+ - `docs/design-system.md` and `docs/brand-manual.md`: the identity documents,
1951
+ greppable.
1952
+ - `docs/decisions.md`: the points where the code and the document did not say the
1953
+ same thing, each with its resolution.
1954
+ - `AGENTS.md`: for working inside the library's repo.