@eduardoalvarez/arrecife 0.1.0 → 0.2.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 (4) hide show
  1. package/CHANGELOG.md +65 -36
  2. package/README.md +43 -0
  3. package/llms.txt +1223 -0
  4. package/package.json +11 -6
package/llms.txt ADDED
@@ -0,0 +1,1223 @@
1
+ # @eduardoalvarez/arrecife
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.
6
+
7
+ Librería de componentes de la identidad visual de Eduardo Álvarez. React 19,
8
+ TypeScript, Tailwind v4, shadcn/ui sobre Radix.
9
+
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.
13
+
14
+ ## Lo primero: no reimplementes lo que ya está aquí
15
+
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.
20
+
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.
24
+
25
+ ## Instalación
26
+
27
+ ```bash
28
+ pnpm add @eduardoalvarez/arrecife
29
+ ```
30
+
31
+ Requisitos, y no son opcionales:
32
+
33
+ | | |
34
+ | --- | --- |
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`) |
38
+
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.
42
+
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.
45
+
46
+ ## Configuración de Tailwind
47
+
48
+ Dos líneas, en este orden, en la hoja de estilos de entrada del proyecto:
49
+
50
+ ```css
51
+ @import "tailwindcss";
52
+ @import "@eduardoalvarez/arrecife/tokens/theme.css";
53
+ ```
54
+
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.
58
+
59
+ Tailwind tiene que escanear la librería para no purgar esas clases. Si el
60
+ proyecto declara `@source`, incluye el paquete:
61
+
62
+ ```css
63
+ @source "../node_modules/@eduardoalvarez/arrecife/dist";
64
+ ```
65
+
66
+ ### Modo claro y modo oscuro
67
+
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>`:
70
+
71
+ ```html
72
+ <html data-theme="light">
73
+ ```
74
+
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.
77
+
78
+ ### Fuentes
79
+
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.
84
+
85
+ ## Qué importar de dónde
86
+
87
+ `exports` tiene cinco subrutas y la elección importa: tres de ellas **no
88
+ arrastran React**, y por eso pueden consumirse desde un worker, un
89
+ `astro.config.mjs`, un script de build o un generador con Satori.
90
+
91
+ | Subruta | Arrastra React | Para qué |
92
+ | --- | --- | --- |
93
+ | `@eduardoalvarez/arrecife` | sí | Componentes, primitivos, marca, `cn`. Reexporta tokens |
94
+ | `@eduardoalvarez/arrecife/tokens` | **no** | El objeto `tokens` en JS. Satori, Astro, scripts |
95
+ | `@eduardoalvarez/arrecife/tokens/theme.css` | — | El `@theme` de Tailwind v4 |
96
+ | `@eduardoalvarez/arrecife/og` | **no** | Las plantillas de Open Graph para Satori |
97
+ | `@eduardoalvarez/arrecife/shiki` | **no** | El tema de resaltado de sintaxis |
98
+ | `@eduardoalvarez/arrecife/brand` | sí | Logo, isotipo y mascota como componentes |
99
+ | `@eduardoalvarez/arrecife/assets/*` | — | Los PNG de la marca |
100
+
101
+ Importar la raíz desde un script de build para sacar un token es el error que las
102
+ subrutas existen para evitar: arrastra React entero a un worker que no lo monta.
103
+
104
+ ```ts
105
+ // Bien, en un generador de OG o en astro.config.mjs
106
+ import { tokens } from '@eduardoalvarez/arrecife/tokens';
107
+ import { arrecife } from '@eduardoalvarez/arrecife/shiki';
108
+
109
+ // Mal: monta React donde no hace falta
110
+ import { tokens } from '@eduardoalvarez/arrecife';
111
+ ```
112
+
113
+ ## Tokens
114
+
115
+ La fuente es un objeto de TypeScript y la salida CSS se genera de él, así que el
116
+ mismo valor está disponible en los dos sitios y no pueden discrepar.
117
+
118
+ | Token | Custom property | Utilidad de Tailwind |
119
+ | --- | --- | --- |
120
+ | `colors[modo].surfaceRaised` | `--color-surface-raised` | `bg-surface-raised` |
121
+ | `brand.hull` | `--color-brand-hull` | `bg-brand-hull` |
122
+ | `typeScale.h1` | `--text-h1` | `text-h1` (arrastra line-height, weight y tracking) |
123
+ | `fonts.display` | `--font-display` | `font-display` |
124
+ | `radius.card` | `--radius-card` | `rounded-card` |
125
+ | `spacing.lg` | `--spacing-lg` | `p-lg`, `gap-lg`, `mb-lg` |
126
+ | `control.md` | `--spacing-control-md` | `px-control-md` |
127
+ | `control.icon` | `--spacing-control-icon` | `size-control-icon` (42×42) |
128
+ | `gradient[modo].hero` | `--gradient-hero` | `degradado-hero` |
129
+ | `size.nav` | `--spacing-nav` | `h-nav` |
130
+ | `size.content` / `size.wide` | `--container-content` / `--container-wide` | `max-w-content` / `max-w-wide` |
131
+ | `limits.measure` | `--container-measure` | `max-w-measure` |
132
+ | `shadow.standard` | `--shadow-standard` | `shadow-standard` |
133
+ | `motion` | `--duration-standard`, `--ease-standard` | `duration-standard`, `ease-standard` |
134
+
135
+ `transition-standard` es la única transición del sistema y solo puede animar
136
+ color y borde: así está escrita la utilidad.
137
+
138
+ ## Reglas del sistema que el código consumidor no debe romper
139
+
140
+ Son decisiones de identidad, ya medidas. Romperlas produce código que compila y
141
+ se ve mal, o que falla la auditoría de accesibilidad del proyecto.
142
+
143
+ 1. **Cero hex literales.** Todo color sale de un token o de su custom property.
144
+ 2. **`Button variant="conversion"` va una sola vez por pantalla.** No se fuerza en
145
+ runtime; dos en la misma página son un error de diseño.
146
+ 3. **No hay variante de peligro en `Button`.** El error del sistema vive en los
147
+ avisos y en la validación de campo, no en un botón rojo.
148
+ 4. **`secondary` nunca se rellena el fondo.** Es borde y texto.
149
+ 5. **Sin animaciones de entrada.** Modales, menús, tooltips y toasts aparecen
150
+ donde van a quedarse. La única excepción es el spinner de `Button loading`.
151
+ 6. **La semántica y la escala son independientes.** Un `h2` que debe verse
152
+ pequeño es `<Text as="h2" variant="h3">`, nunca un `h3` que miente sobre la
153
+ jerarquía.
154
+ 7. **`textMuted` no va nunca sobre `surfaceRaised`**: da 4.07 en oscuro. Sobre
155
+ superficie elevada —menús, tabs activos— el token es `textSecondary`.
156
+ 8. **Un fondo teñido con un color semántico lleva texto de token de texto**, no
157
+ del color semántico. El color se queda en el borde y en el glifo. Poner
158
+ `accent` sobre su propio tinte al 8 % da 4.12 y no llega a AA.
159
+ 9. **`Progress` exige `label`.** Una barra sin nombre accesible no dice de qué es.
160
+ 10. **`Button size="icon"` exige `aria-label`.** No lleva texto.
161
+ 11. **Las caras de la mascota solo aparecen** en estados vacíos, confirmaciones,
162
+ errores, progreso de curso y celebración. Nunca en hero, precios, servicios,
163
+ contacto ni CV.
164
+ 12. **La aleta no es un parámetro libre**: `espuma` sobre fondo oscuro, `color`
165
+ sobre fondo claro. Los componentes ya la eligen por el fondo.
166
+
167
+ ## Lo que la librería NO hace, a propósito
168
+
169
+ Estas son las confusiones que más veces se cometen al consumirla.
170
+
171
+ - **No trae Shiki.** Publica el *tema*, no el resaltador. `CodeBlock` recibe el
172
+ código **ya resaltado** por la herramienta del proyecto.
173
+ - **No formatea fechas.** `ArticleCard`, `TalkCard` y compañía reciben la fecha ya
174
+ formateada por el proyecto: la librería no impone locale. `dateTime` es aparte,
175
+ en ISO, para el atributo del `<time>`.
176
+ - **`NewsletterForm` no hace el POST.** Es presentacional: recibe `state` y emite
177
+ `onSubmitEmail`. La llamada la hace el proyecto con su proveedor.
178
+ - **No trae enrutador.** Los componentes con enlaces aceptan `asChild` para
179
+ envolver el `Link` del framework.
180
+ - **No carga fuentes.** Las declara por nombre.
181
+ - **No hay preset de Tailwind v3.**
182
+
183
+ ## Patrones de uso
184
+
185
+ ```tsx
186
+ import { Text, Button, Badge, cn } from '@eduardoalvarez/arrecife';
187
+ import type { TextProps } from '@eduardoalvarez/arrecife';
188
+
189
+ <Text variant="eyebrow" tone="muted">charlas</Text>
190
+ <Text as="h2" variant="h1">Escalar con criterio</Text>
191
+ <Text variant="body">Se corta solo a 68ch.</Text>
192
+ <Text variant="ui" measure={false}>Sin corte, para una celda estrecha.</Text>
193
+ ```
194
+
195
+ `asChild` renderiza el hijo en vez del elemento propio. Es como se envuelve el
196
+ enlace del framework sin perder los estilos:
197
+
198
+ ```tsx
199
+ <Button asChild>
200
+ <Link href="/cursos">Ver los cursos</Link>
201
+ </Button>
202
+ ```
203
+
204
+ `cn` es `clsx` + `tailwind-merge`. Se usa para componer `className` sin que dos
205
+ utilidades del mismo grupo peleen.
206
+
207
+ Open Graph, sin React:
208
+
209
+ ```ts
210
+ import satori from 'satori';
211
+ import { plantillaArticulo, OG } from '@eduardoalvarez/arrecife/og';
212
+
213
+ const svg = await satori(plantillaArticulo({ title, date, readingMinutes }), {
214
+ width: OG.width, // 1200
215
+ height: OG.height, // 630
216
+ fonts: [...],
217
+ });
218
+ ```
219
+
220
+ Resaltado de sintaxis, desde la configuración del sitio:
221
+
222
+ ```ts
223
+ import { arrecife } from '@eduardoalvarez/arrecife/shiki';
224
+
225
+ export default defineConfig({
226
+ markdown: { syntaxHighlight: 'shiki', shikiConfig: { theme: arrecife } },
227
+ });
228
+ ```
229
+
230
+ # Inventario
231
+
232
+ Lo que sigue sale del compilador de TypeScript en cada build. Solo se listan los
233
+ props **declarados por la librería**: los heredados de un elemento HTML o de una
234
+ primitiva de Radix se resumen en la línea `Extiende`, y son los de siempre.
235
+
236
+ ## Primitivos
237
+
238
+ Se importan de `@eduardoalvarez/arrecife`. 100 exportaciones.
239
+
240
+ ### Alert
241
+
242
+ Fuente: `src/primitives/alert.tsx`
243
+
244
+ El aviso lleva el color en el fondo, no solo en el borde.
245
+
246
+ - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'title'> & VariantProps<typeof alert>`
247
+
248
+ | prop | tipo | req. | defecto | qué hace |
249
+ | --- | --- | --- | --- | --- |
250
+ | `enfasis` | `"sutil" \| "fuerte"` | | `sutil` | |
251
+ | `icon` | `ReactNode` | | | Sustituye el glifo mono de la variante. Nunca un emoji: si necesitas otra cosa, es un SVG de `glyphs`. |
252
+ | `title` | `ReactNode` | | | |
253
+ | `variant` | `"accent" \| "success" \| "warning" \| "error"` | | `accent` | |
254
+
255
+ ### Avatar, AvatarImage, AvatarFallback
256
+
257
+ Fuente: `src/primitives/avatar.tsx`
258
+
259
+ **Avatar**
260
+ 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.
261
+
262
+ - Extiende: `ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & VariantProps<typeof avatar>`
263
+
264
+ | prop | tipo | req. | defecto | qué hace |
265
+ | --- | --- | --- | --- | --- |
266
+ | `size` | `"sm" \| "md" \| "lg" \| "xl"` | | `md` | |
267
+
268
+ **AvatarImage**
269
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
270
+
271
+ **AvatarFallback**
272
+ Iniciales mientras la imagen carga, o cuando no hay imagen.
273
+
274
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
275
+
276
+ ### Badge, CategoryBadge, MetricBadge
277
+
278
+ Fuente: `src/primitives/badge.tsx`
279
+
280
+ **Badge**
281
+ 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.
282
+
283
+ - Extiende: `ComponentPropsWithoutRef<'span'> & VariantProps<typeof badge>`
284
+
285
+ | prop | tipo | req. | defecto | qué hace |
286
+ | --- | --- | --- | --- | --- |
287
+ | `variant` | `"accent" \| "success" \| "warning" \| "error" \| "neutral" \| "warm"` | | `neutral` | |
288
+
289
+ **CategoryBadge**
290
+ Un slug, en arena. Sin transformar: los slugs ya vienen en minúscula y forzarla sería el mismo error que forzaba el `uppercase`.
291
+
292
+ - Extiende: `ComponentPropsWithoutRef<'span'>`
293
+
294
+ | prop | tipo | req. | defecto | qué hace |
295
+ | --- | --- | --- | --- | --- |
296
+ | `active` | `boolean \| undefined` | | `false` | Filtro seleccionado: arena sólido con tinta encima. |
297
+
298
+ **MetricBadge**
299
+ - Extiende: `ComponentPropsWithoutRef<'span'>`
300
+
301
+ | prop | tipo | req. | defecto | qué hace |
302
+ | --- | --- | --- | --- | --- |
303
+ | `boxed` | `boolean \| undefined` | | `false` | Añade el aro de hairline. Por defecto la métrica va sin caja. |
304
+
305
+ ### Button
306
+
307
+ Fuente: `src/primitives/button.tsx`
308
+
309
+ Las CUATRO variantes del sistema, y solo esas cuatro.
310
+
311
+ - Extiende: `ComponentPropsWithoutRef<'button'> & VariantProps<typeof button>`
312
+
313
+ | prop | tipo | req. | defecto | qué hace |
314
+ | --- | --- | --- | --- | --- |
315
+ | `asChild` | `boolean` | | `false` | Renderiza el hijo en vez de un `<button>`, para envolver un enlace. |
316
+ | `icon` | `ReactNode` | | | Glifo SVG antes del texto. Se oculta mientras carga. |
317
+ | `loading` | `boolean` | | `false` | Deshabilita y anuncia `aria-busy`. Incompatible con `asChild`. |
318
+ | `size` | `"sm" \| "md" \| "lg" \| "icon"` | | `md` | |
319
+ | `variant` | `"primary" \| "conversion" \| "secondary" \| "tertiary"` | | `primary` | |
320
+
321
+ ### Calendar
322
+
323
+ Fuente: `src/primitives/calendar.tsx`
324
+
325
+ Calendario mensual navegable, sobre `react-day-picker`.
326
+
327
+ - Extiende: `ComponentProps<typeof DayPicker>`
328
+
329
+ | prop | tipo | req. | defecto | qué hace |
330
+ | --- | --- | --- | --- | --- |
331
+ | `fullWidth` | `boolean \| undefined` | | `false` | Estira el calendario hasta ocupar todo el ancho de su contenedor, con las celdas repartiéndoselo a partes iguales. |
332
+
333
+ ### Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter
334
+
335
+ Fuente: `src/primitives/card.tsx`
336
+
337
+ **Card**
338
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
339
+
340
+ **CardHeader**
341
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
342
+
343
+ **CardTitle**
344
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
345
+
346
+ **CardDescription**
347
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
348
+
349
+ **CardContent**
350
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
351
+
352
+ **CardFooter**
353
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
354
+
355
+ ### Checkbox
356
+
357
+ Fuente: `src/primitives/checkbox.tsx`
358
+
359
+ - Extiende: `ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>`
360
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
361
+
362
+ ### Code
363
+
364
+ Fuente: `src/primitives/code.tsx`
365
+
366
+ Código en línea, dentro de prosa.
367
+
368
+ - Extiende: `ComponentPropsWithoutRef<'code'>`
369
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
370
+
371
+ ### DateField
372
+
373
+ Fuente: `src/primitives/date-field.tsx`
374
+
375
+ Un campo de fecha sobre el control nativo, no sobre un calendario propio.
376
+
377
+ - Extiende: `Omit<ComponentPropsWithoutRef<'input'>, 'type'>`
378
+
379
+ | prop | tipo | req. | defecto | qué hace |
380
+ | --- | --- | --- | --- | --- |
381
+ | `invalid` | `boolean \| undefined` | | `false` | |
382
+ | `withTime` | `boolean \| undefined` | | `false` | Añade la hora al campo. Es el `datetime-local` nativo. |
383
+
384
+ ### DialogOverlay, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, Dialog, DialogTrigger, DialogClose
385
+
386
+ Fuente: `src/primitives/dialog.tsx`
387
+
388
+ **DialogOverlay**
389
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
390
+
391
+ **DialogContent**
392
+ Sin entrada animada: no hay escala ni desplazamiento en el sistema.
393
+
394
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
395
+
396
+ **DialogHeader**
397
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
398
+
399
+ **DialogFooter**
400
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
401
+
402
+ **DialogTitle**
403
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
404
+
405
+ **DialogDescription**
406
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
407
+
408
+ **Dialog**
409
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
410
+
411
+ **DialogTrigger**
412
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
413
+
414
+ **DialogClose**
415
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
416
+
417
+ ### DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenu, DropdownMenuTrigger, DropdownMenuGroup, DropdownMenuRadioGroup, DropdownMenuSub
418
+
419
+ Fuente: `src/primitives/dropdown-menu.tsx`
420
+
421
+ **DropdownMenuContent**
422
+ Sin animación de entrada: el menú aparece, no se despliega.
423
+
424
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
425
+
426
+ **DropdownMenuItem**
427
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
428
+
429
+ **DropdownMenuCheckboxItem**
430
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
431
+
432
+ **DropdownMenuRadioItem**
433
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
434
+
435
+ **DropdownMenuLabel**
436
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
437
+
438
+ **DropdownMenuSeparator**
439
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
440
+
441
+ **DropdownMenuSubTrigger**
442
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
443
+
444
+ **DropdownMenuSubContent**
445
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
446
+
447
+ **DropdownMenu**
448
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
449
+
450
+ **DropdownMenuTrigger**
451
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
452
+
453
+ **DropdownMenuGroup**
454
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
455
+
456
+ **DropdownMenuRadioGroup**
457
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
458
+
459
+ **DropdownMenuSub**
460
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
461
+
462
+ ### Input
463
+
464
+ Fuente: `src/primitives/input.tsx`
465
+
466
+ - Extiende: `ComponentPropsWithoutRef<'input'>`
467
+
468
+ | prop | tipo | req. | defecto | qué hace |
469
+ | --- | --- | --- | --- | --- |
470
+ | `invalid` | `boolean` | | `false` | Marca el control como inválido y tiñe el borde. |
471
+
472
+ ### Label
473
+
474
+ Fuente: `src/primitives/label.tsx`
475
+
476
+ La escala `label`: 13px, que es el mínimo absoluto en pantalla del sistema.
477
+
478
+ - Extiende: `ComponentPropsWithoutRef<typeof LabelPrimitive.Root>`
479
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
480
+
481
+ ### Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationPrevious, PaginationNext, PaginationEllipsis
482
+
483
+ Fuente: `src/primitives/pagination.tsx`
484
+
485
+ **Pagination**
486
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
487
+
488
+ **PaginationContent**
489
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
490
+
491
+ **PaginationItem**
492
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
493
+
494
+ **PaginationLink**
495
+ - Extiende: `ComponentPropsWithoutRef<'a'>`
496
+
497
+ | prop | tipo | req. | defecto | qué hace |
498
+ | --- | --- | --- | --- | --- |
499
+ | `isActive` | `boolean` | | `false` | |
500
+
501
+ **PaginationPrevious**
502
+
503
+ | prop | tipo | req. | defecto | qué hace |
504
+ | --- | --- | --- | --- | --- |
505
+ | `isActive` | `boolean` | | | |
506
+
507
+ **PaginationNext**
508
+
509
+ | prop | tipo | req. | defecto | qué hace |
510
+ | --- | --- | --- | --- | --- |
511
+ | `isActive` | `boolean` | | | |
512
+
513
+ **PaginationEllipsis**
514
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
515
+
516
+ ### PopoverContent, Popover, PopoverTrigger, PopoverAnchor
517
+
518
+ Fuente: `src/primitives/popover.tsx`
519
+
520
+ **PopoverContent**
521
+ Sin animación de entrada: aparece donde va a quedarse, como el resto.
522
+
523
+ - Extiende: `ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & Etiquetado`
524
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
525
+
526
+ **Popover**
527
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
528
+
529
+ **PopoverTrigger**
530
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
531
+
532
+ **PopoverAnchor**
533
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
534
+
535
+ ### Progress
536
+
537
+ Fuente: `src/primitives/progress.tsx`
538
+
539
+ 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.
540
+
541
+ - Extiende: `ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>`
542
+
543
+ | prop | tipo | req. | defecto | qué hace |
544
+ | --- | --- | --- | --- | --- |
545
+ | `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. |
546
+ | `tone` | `"accent" \| "warm"` | | `accent` | Arena en vez de bioluz, para progreso de curso. |
547
+
548
+ ### RadioGroup, RadioGroupItem
549
+
550
+ Fuente: `src/primitives/radio-group.tsx`
551
+
552
+ **RadioGroup**
553
+ - Extiende: `ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>`
554
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
555
+
556
+ **RadioGroupItem**
557
+ - Extiende: `ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>`
558
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
559
+
560
+ ### SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, Select, SelectGroup, SelectValue
561
+
562
+ Fuente: `src/primitives/select.tsx`
563
+
564
+ **SelectTrigger**
565
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
566
+
567
+ **SelectContent**
568
+ Sin animación de entrada: el menú aparece, no se despliega.
569
+
570
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
571
+
572
+ **SelectLabel**
573
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
574
+
575
+ **SelectItem**
576
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
577
+
578
+ **SelectSeparator**
579
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
580
+
581
+ **Select**
582
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
583
+
584
+ **SelectGroup**
585
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
586
+
587
+ **SelectValue**
588
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
589
+
590
+ ### Separator
591
+
592
+ Fuente: `src/primitives/separator.tsx`
593
+
594
+ `hairline`, no `border`: una división entre contenidos es sutil por definición. Para delimitar un control existe `border`, que es otro token.
595
+
596
+ - Extiende: `ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>`
597
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
598
+
599
+ ### SheetContent, SheetHeader, SheetBody, SheetFooter, SheetTitle, SheetDescription, Sheet, SheetTrigger, SheetClose
600
+
601
+ Fuente: `src/primitives/sheet.tsx`
602
+
603
+ **SheetContent**
604
+ 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.
605
+
606
+ - Extiende: `ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & VariantProps<typeof panel>`
607
+
608
+ | prop | tipo | req. | defecto | qué hace |
609
+ | --- | --- | --- | --- | --- |
610
+ | `side` | `"right" \| "left" \| "top" \| "bottom"` | | `right` | |
611
+
612
+ **SheetHeader**
613
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
614
+
615
+ **SheetBody**
616
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
617
+
618
+ **SheetFooter**
619
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
620
+
621
+ **SheetTitle**
622
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
623
+
624
+ **SheetDescription**
625
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
626
+
627
+ **Sheet**
628
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
629
+
630
+ **SheetTrigger**
631
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
632
+
633
+ **SheetClose**
634
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
635
+
636
+ ### Skeleton
637
+
638
+ Fuente: `src/primitives/skeleton.tsx`
639
+
640
+ Barrido de 1.4s lineal, del documento.
641
+
642
+ - Extiende: `ComponentPropsWithoutRef<'div'>`
643
+
644
+ | prop | tipo | req. | defecto | qué hace |
645
+ | --- | --- | --- | --- | --- |
646
+ | `still` | `boolean \| undefined` | | `false` | Apaga el barrido. Para listas largas, donde muchas a la vez marean. |
647
+
648
+ ### Switch
649
+
650
+ Fuente: `src/primitives/switch.tsx`
651
+
652
+ 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.
653
+
654
+ - Extiende: `ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>`
655
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
656
+
657
+ ### Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption
658
+
659
+ Fuente: `src/primitives/table.tsx`
660
+
661
+ **Table**
662
+ El contenedor scrollea en horizontal: la página nunca lo hace.
663
+
664
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
665
+
666
+ **TableHeader**
667
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
668
+
669
+ **TableBody**
670
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
671
+
672
+ **TableFooter**
673
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
674
+
675
+ **TableRow**
676
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
677
+
678
+ **TableHead**
679
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
680
+
681
+ **TableCell**
682
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
683
+
684
+ **TableCaption**
685
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
686
+
687
+ ### Tabs, TabsList, TabsTrigger, TabsContent
688
+
689
+ Fuente: `src/primitives/tabs.tsx`
690
+
691
+ **Tabs**
692
+ - Extiende: `ComponentPropsWithoutRef<typeof TabsPrimitive.Root>`
693
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
694
+
695
+ **TabsList**
696
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
697
+
698
+ **TabsTrigger**
699
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
700
+
701
+ **TabsContent**
702
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
703
+
704
+ ### Textarea
705
+
706
+ Fuente: `src/primitives/textarea.tsx`
707
+
708
+ - Extiende: `ComponentPropsWithoutRef<'textarea'>`
709
+
710
+ | prop | tipo | req. | defecto | qué hace |
711
+ | --- | --- | --- | --- | --- |
712
+ | `invalid` | `boolean` | | `false` | |
713
+
714
+ ### ToastViewport, Toast, ToastTitle, ToastDescription, ToastProvider, ToastAction
715
+
716
+ Fuente: `src/primitives/toast.tsx`
717
+
718
+ **ToastViewport**
719
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
720
+
721
+ **Toast**
722
+ Sin deslizamiento de entrada: el aviso aparece donde va a quedarse.
723
+
724
+ - Extiende: `ComponentPropsWithoutRef<typeof ToastPrimitive.Root> & VariantProps<typeof toast>`
725
+
726
+ | prop | tipo | req. | defecto | qué hace |
727
+ | --- | --- | --- | --- | --- |
728
+ | `variant` | `"success" \| "error" \| "neutral"` | | `neutral` | |
729
+
730
+ **ToastTitle**
731
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
732
+
733
+ **ToastDescription**
734
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
735
+
736
+ **ToastProvider**
737
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
738
+
739
+ **ToastAction**
740
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
741
+
742
+ ### TooltipContent, TooltipProvider, Tooltip, TooltipTrigger
743
+
744
+ Fuente: `src/primitives/tooltip.tsx`
745
+
746
+ **TooltipContent**
747
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
748
+
749
+ **TooltipProvider**
750
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
751
+
752
+ **Tooltip**
753
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
754
+
755
+ **TooltipTrigger**
756
+ - Sin props propios: pasa los del elemento o la primitiva que envuelve.
757
+
758
+ ### Text
759
+
760
+ Fuente: `src/primitives/typography.tsx`
761
+
762
+ - Extiende: `Omit<ComponentPropsWithoutRef<'p'>, 'color'> & VariantProps<typeof texto>`
763
+
764
+ | prop | tipo | req. | defecto | qué hace |
765
+ | --- | --- | --- | --- | --- |
766
+ | `as` | `"caption" \| "dd" \| "dt" \| "em" \| "figcaption" \| "h1" \| "h2" \| "h3" \| "h4" \| "legend" \| "li" \| "p" \| "span" \| "strong"` | | | Etiqueta HTML. Por defecto, la que corresponde a la escala. |
767
+ | `asChild` | `boolean` | | `false` | Renderiza el hijo en vez de crear un elemento, para envolver un enlace. |
768
+ | `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. |
769
+ | `tone` | `"accent" \| "success" \| "warning" \| "error" \| "warm" \| "primary" \| "secondary" \| "muted"` | | `primary` | |
770
+ | `variant` | `"display" \| "body" \| "h1" \| "h2" \| "h3" \| "label" \| "meta" \| "stat" \| "lead" \| "ui" \| "tag" \| "chip" \| "eyebrow"` | | `body` | |
771
+
772
+ ## Componentes
773
+
774
+ Se importan de `@eduardoalvarez/arrecife`. 21 exportaciones.
775
+
776
+ ### ArticleCard
777
+
778
+ Fuente: `src/components/article-card/index.tsx`
779
+
780
+ 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.
781
+
782
+ - Extiende: `Omit<TarjetaProps, 'children' \| 'title'>`
783
+
784
+ | prop | tipo | req. | defecto | qué hace |
785
+ | --- | --- | --- | --- | --- |
786
+ | `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. |
787
+ | `date` | `ReactNode` | | | Fecha ya formateada por el proyecto: la librería no impone locale. |
788
+ | `dateTime` | `string` | | | Valor de `datetime` del `<time>`, en ISO. |
789
+ | `excerpt` | `ReactNode` | | | Entradilla. Se corta a dos líneas para que la rejilla no se desalinee. |
790
+ | `readingMinutes` | `number` | | | |
791
+ | `tags` | `readonly string[]` | | | |
792
+ | `title` | `ReactNode` | sí | | |
793
+
794
+ ### AudioPlayer
795
+
796
+ Fuente: `src/components/audio-player/index.tsx`
797
+
798
+ - 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; }`
799
+
800
+ | prop | tipo | req. | defecto | qué hace |
801
+ | --- | --- | --- | --- | --- |
802
+ | `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. |
803
+ | `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. |
804
+ | `src` | `string` | sí | | |
805
+ | `title` | `string` | | | |
806
+
807
+ ### AuthorCard
808
+
809
+ Fuente: `src/components/author-card/index.tsx`
810
+
811
+ La firma al pie del artículo: avatar 52px, nombre 15/500 y el rol en mono muted. Tres datos, ni uno más.
812
+
813
+ - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'role'>`
814
+
815
+ | prop | tipo | req. | defecto | qué hace |
816
+ | --- | --- | --- | --- | --- |
817
+ | `action` | `ReactNode` | | | Enlaces o botón de contacto. |
818
+ | `bio` | `ReactNode` | | | Una o dos frases. Se corta a 68ch sola. |
819
+ | `name` | `string` | sí | | |
820
+ | `role` | `ReactNode` | | | El rol. Va en mono: es un dato, no una frase. |
821
+ | `src` | `string` | | | URL del avatar. Sin ella se muestran las iniciales. |
822
+
823
+ ### Blockquote
824
+
825
+ Fuente: `src/components/blockquote/index.tsx`
826
+
827
+ 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.
828
+
829
+ - Extiende: `Omit<ComponentPropsWithoutRef<'blockquote'>, 'cite'>`
830
+
831
+ | prop | tipo | req. | defecto | qué hace |
832
+ | --- | --- | --- | --- | --- |
833
+ | `author` | `ReactNode` | | | Quién lo dijo. Se marca como `<cite>`. |
834
+ | `source` | `ReactNode` | | | Dónde lo dijo: charla, artículo, conversación. |
835
+
836
+ ### Breadcrumb
837
+
838
+ Fuente: `src/components/breadcrumb/index.tsx`
839
+
840
+ - Extiende: `Omit<ComponentPropsWithoutRef<'nav'>, 'children'>`
841
+
842
+ | prop | tipo | req. | defecto | qué hace |
843
+ | --- | --- | --- | --- | --- |
844
+ | `homeHref` | `string` | | `/` | Destino del `~`. Por defecto, la raíz del sitio. |
845
+ | `homeLabel` | `string` | | `Inicio` | Etiqueta accesible del `~`, que si no se lee como una tilde suelta. |
846
+ | `items` | `readonly Migaja[]` | sí | | |
847
+ | `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. |
848
+
849
+ ### CodeBlock
850
+
851
+ Fuente: `src/components/code-block/index.tsx`
852
+
853
+ `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.
854
+
855
+ - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'children'>`
856
+
857
+ | prop | tipo | req. | defecto | qué hace |
858
+ | --- | --- | --- | --- | --- |
859
+ | `children` | `ReactNode` | sí | | El código ya resaltado, o texto plano. |
860
+ | `copyText` | `string` | | | Texto que se copia al portapapeles. Sin esto, no se muestra el botón. |
861
+ | `language` | `string` | | | Etiqueta del lenguaje. Se muestra en la barra superior. |
862
+
863
+ ### CourseCard
864
+
865
+ Fuente: `src/components/course-card/index.tsx`
866
+
867
+ - Extiende: `Omit<TarjetaProps, 'children' \| 'title'>`
868
+
869
+ | prop | tipo | req. | defecto | qué hace |
870
+ | --- | --- | --- | --- | --- |
871
+ | `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. |
872
+ | `meta` | `readonly ReactNode[]` | | | Nivel, duración, número de lecciones: lo que el proyecto quiera listar. |
873
+ | `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. |
874
+ | `status` | `ReactNode` | | | Etiqueta de estado: «próximamente», «gratis», «nuevo». |
875
+ | `summary` | `ReactNode` | | | |
876
+ | `title` | `ReactNode` | sí | | |
877
+
878
+ ### EmptyState
879
+
880
+ Fuente: `src/components/empty-state/index.tsx`
881
+
882
+ La regla más importante de la mascota, por fin como código.
883
+
884
+ - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'title'>`
885
+
886
+ | prop | tipo | req. | defecto | qué hace |
887
+ | --- | --- | --- | --- | --- |
888
+ | `action` | `ReactNode` | | | La acción que saca del estado vacío. Normalmente un botón terciario. |
889
+ | `basePath` | `string` | | | Dónde se sirven los PNG de la marca. |
890
+ | `description` | `ReactNode` | | | Una línea explicando qué falta o qué hacer. |
891
+ | `expresion` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | sí | | La cara. Obligatoria: sin ella esto es un párrafo centrado. |
892
+ | `title` | `ReactNode` | sí | | |
893
+
894
+ ### Footer, FooterLink
895
+
896
+ Fuente: `src/components/footer/index.tsx`
897
+
898
+ **Footer**
899
+ - Extiende: `ComponentPropsWithoutRef<'footer'>`
900
+
901
+ | prop | tipo | req. | defecto | qué hace |
902
+ | --- | --- | --- | --- | --- |
903
+ | `social` | `readonly Red[]` | | | |
904
+ | `year` | `number` | | `new Date().getFullYear()` | Año de la firma. |
905
+
906
+ **FooterLink**
907
+ - Extiende: `ComponentPropsWithoutRef<'a'>`
908
+
909
+ | prop | tipo | req. | defecto | qué hace |
910
+ | --- | --- | --- | --- | --- |
911
+ | `asChild` | `boolean \| undefined` | | `false` | |
912
+
913
+ ### Hero
914
+
915
+ Fuente: `src/components/hero/index.tsx`
916
+
917
+ 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.
918
+
919
+ - Extiende: `Omit<ComponentPropsWithoutRef<'section'>, 'title'>`
920
+
921
+ | prop | tipo | req. | defecto | qué hace |
922
+ | --- | --- | --- | --- | --- |
923
+ | `action` | `ReactNode` | | | Los botones. Aquí va el único `conversion` de la pantalla. |
924
+ | `basePath` | `string` | | | |
925
+ | `description` | `ReactNode` | | | |
926
+ | `eyebrow` | `ReactNode` | | | Mono, versalitas, en acento. |
927
+ | `pose` | `"desk" \| "laptop-coffee" \| "peek" \| "surf"` | | | La pose de Tiburoncín. Sin ella el hero es un panel con texto. |
928
+ | `title` | `ReactNode` | sí | | |
929
+
930
+ ### LinkRow
931
+
932
+ Fuente: `src/components/link-row/index.tsx`
933
+
934
+ 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.
935
+
936
+ - Extiende: `Omit<TarjetaProps, 'children'>`
937
+
938
+ | prop | tipo | req. | defecto | qué hace |
939
+ | --- | --- | --- | --- | --- |
940
+ | `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. |
941
+ | `description` | `ReactNode` | | | |
942
+ | `external` | `boolean \| undefined` | | `false` | Marca el enlace como externo: añade la flecha y el `rel` seguro. |
943
+ | `icon` | `ReactNode` | | | Glifo SVG del destino. Nunca un emoji. |
944
+ | `name` | `ReactNode` | sí | | |
945
+
946
+ ### Nav, NavItem
947
+
948
+ Fuente: `src/components/nav/index.tsx`
949
+
950
+ **Nav**
951
+ La barra del sitio: 64px, abismo al 86 % y desenfoque de 14px detrás.
952
+
953
+ - Extiende: `ComponentPropsWithoutRef<'header'>`
954
+
955
+ | prop | tipo | req. | defecto | qué hace |
956
+ | --- | --- | --- | --- | --- |
957
+ | `actions` | `ReactNode` | | | Acciones a la derecha: conversión, cambio de tema, buscar. |
958
+ | `brand` | `ReactNode` | | | El logo, a la izquierda. |
959
+
960
+ **NavItem**
961
+ El `./` lo pone el componente, no quien lo usa.
962
+
963
+ - Extiende: `ComponentPropsWithoutRef<'a'>`
964
+
965
+ | prop | tipo | req. | defecto | qué hace |
966
+ | --- | --- | --- | --- | --- |
967
+ | `active` | `boolean \| undefined` | | `false` | Sección actual: bioluz con subrayado de 1px. |
968
+ | `asChild` | `boolean \| undefined` | | `false` | Renderiza el hijo en vez de un `<a>`, para el `Link` del enrutador. |
969
+
970
+ ### NewsletterForm
971
+
972
+ Fuente: `src/components/newsletter-form/index.tsx`
973
+
974
+ - Extiende: `Omit<ComponentPropsWithoutRef<'section'>, 'title' \| 'onSubmit'>`
975
+
976
+ | prop | tipo | req. | defecto | qué hace |
977
+ | --- | --- | --- | --- | --- |
978
+ | `basePath` | `string` | | | |
979
+ | `description` | `ReactNode` | | | |
980
+ | `disclaimer` | `ReactNode` | | | La letra pequeña. Es el «sin spam», y por eso admite cara. |
981
+ | `errorMessage` | `ReactNode` | | `No se pudo suscribir ese correo. Revísalo y vuelve a intentar.` | |
982
+ | `expresion` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | | | |
983
+ | `fieldLabel` | `string` | | `Correo electrónico` | |
984
+ | `onSubmitEmail` | `(email: string) => void` | | | Se dispara con el correo ya leído del campo. |
985
+ | `placeholder` | `string` | | `tu@correo.dev` | |
986
+ | `state` | `"error" \| "reposo" \| "enviando" \| "exito"` | | `reposo` | |
987
+ | `submitLabel` | `string` | | `Suscribirme` | |
988
+ | `successMessage` | `ReactNode` | | `Ya estás dentro. Te llega un correo cada dos semanas, y nada más.` | |
989
+ | `title` | `ReactNode` | sí | | |
990
+
991
+ ### PageHeader
992
+
993
+ Fuente: `src/components/page-header/index.tsx`
994
+
995
+ Una sola cabecera en dos escalas, no dos componentes.
996
+
997
+ - Extiende: `Omit<ComponentPropsWithoutRef<'header'>, 'title'> & VariantProps<typeof cabecera>`
998
+
999
+ | prop | tipo | req. | defecto | qué hace |
1000
+ | --- | --- | --- | --- | --- |
1001
+ | `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. |
1002
+ | `as` | `"h1" \| "h2"` | | `h1` | Nivel del titular. `h1` salvo que la página ya tenga uno. |
1003
+ | `description` | `ReactNode` | | | |
1004
+ | `eyebrow` | `ReactNode` | | | Mono, versalitas, en acento. Es la sección a la que pertenece la página. |
1005
+ | `size` | `"display" \| "page"` | | `page` | |
1006
+ | `title` | `ReactNode` | sí | | |
1007
+
1008
+ ### SidebarItem, SidebarNav
1009
+
1010
+ Fuente: `src/components/sidebar-nav/index.tsx`
1011
+
1012
+ **SidebarItem**
1013
+ La barra lateral del admin del blog.
1014
+
1015
+ - Extiende: `ComponentPropsWithoutRef<'a'>`
1016
+
1017
+ | prop | tipo | req. | defecto | qué hace |
1018
+ | --- | --- | --- | --- | --- |
1019
+ | `active` | `boolean \| undefined` | | `false` | |
1020
+ | `asChild` | `boolean \| undefined` | | `false` | |
1021
+ | `badge` | `ReactNode` | | | Contador a la derecha: borradores pendientes, media sin usar. |
1022
+
1023
+ **SidebarNav**
1024
+ - Extiende: `ComponentPropsWithoutRef<'nav'>`
1025
+
1026
+ | prop | tipo | req. | defecto | qué hace |
1027
+ | --- | --- | --- | --- | --- |
1028
+ | `branch` | `ReactNode` | | | |
1029
+ | `version` | `ReactNode` | | | Versión y rama, al pie. |
1030
+
1031
+ ### Stat
1032
+
1033
+ Fuente: `src/components/stat/index.tsx`
1034
+
1035
+ Una métrica grande: el número en la escala `stat` y su nombre debajo.
1036
+
1037
+ - Extiende: `Omit<ComponentPropsWithoutRef<'div'>, 'title'>`
1038
+
1039
+ | prop | tipo | req. | defecto | qué hace |
1040
+ | --- | --- | --- | --- | --- |
1041
+ | `label` | `ReactNode` | sí | | Qué se está contando. Va en mono versalitas. |
1042
+ | `progress` | `number` | | | Con `progress`, la métrica se lee como avance y añade la barra. |
1043
+ | `tone` | `"neutral" \| "alerta"` | | `neutral` | `alerta` solo cuando el número ES el problema. |
1044
+ | `value` | `ReactNode` | sí | | El número, ya formateado. La librería no impone locale. |
1045
+
1046
+ ### TalkCard
1047
+
1048
+ Fuente: `src/components/talk-card/index.tsx`
1049
+
1050
+ - Extiende: `Omit<TarjetaProps, 'children' \| 'title'>`
1051
+
1052
+ | prop | tipo | req. | defecto | qué hace |
1053
+ | --- | --- | --- | --- | --- |
1054
+ | `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. |
1055
+ | `date` | `ReactNode` | | | |
1056
+ | `dateTime` | `string` | | | |
1057
+ | `event` | `ReactNode` | sí | | Dónde se dio: la conferencia, el meetup, el equipo. |
1058
+ | `location` | `ReactNode` | | | |
1059
+ | `status` | `ReactNode` | | | Etiqueta corta de estado: «con vídeo», «próxima», «solo audio». |
1060
+ | `title` | `ReactNode` | sí | | |
1061
+
1062
+ ### TableOfContents
1063
+
1064
+ Fuente: `src/components/toc/index.tsx`
1065
+
1066
+ - Extiende: `Omit<ComponentPropsWithoutRef<'nav'>, 'children'>`
1067
+
1068
+ | prop | tipo | req. | defecto | qué hace |
1069
+ | --- | --- | --- | --- | --- |
1070
+ | `activeHref` | `string` | | | Ancla de la sección visible. |
1071
+ | `items` | `readonly Entrada[]` | sí | | |
1072
+ | `linkAsChild` | `(props: { href: string; children: ReactNode; }) => ReactNode` | | | |
1073
+
1074
+ ## Marca
1075
+
1076
+ Se importan de `@eduardoalvarez/arrecife` o `@eduardoalvarez/arrecife/brand`. 4 exportaciones.
1077
+
1078
+ ### Isotipo
1079
+
1080
+ Fuente: `src/brand/isotipo.tsx`
1081
+
1082
+ - Extiende: `Omit<ComponentPropsWithoutRef<'img'>, 'src' \| 'alt'>`
1083
+
1084
+ | prop | tipo | req. | defecto | qué hace |
1085
+ | --- | --- | --- | --- | --- |
1086
+ | `alt` | `string` | | | Texto alternativo. Vacío cuando el isotipo acompaña a un texto que ya lo nombra. |
1087
+ | `basePath` | `string` | | `RUTA_ASSETS` | |
1088
+ | `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. |
1089
+
1090
+ ### Logo
1091
+
1092
+ Fuente: `src/brand/logo.tsx`
1093
+
1094
+ 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.
1095
+
1096
+ - Extiende: `Omit<ComponentPropsWithoutRef<'span'>, 'children'>`
1097
+
1098
+ | prop | tipo | req. | defecto | qué hace |
1099
+ | --- | --- | --- | --- | --- |
1100
+ | `basePath` | `string` | | `RUTA_ASSETS` | |
1101
+ | `sobre` | `"oscuro" \| "claro"` | | `oscuro` | |
1102
+ | `soloIsotipo` | `boolean \| undefined` | | `false` | Oculta el wordmark y deja solo la aleta, para barras muy estrechas. |
1103
+
1104
+ ### Mascota, CaraDeMascota
1105
+
1106
+ Fuente: `src/brand/mascota.tsx`
1107
+
1108
+ **Mascota**
1109
+ Tiburoncín de cuerpo entero.
1110
+
1111
+ - Extiende: `Base`
1112
+
1113
+ | prop | tipo | req. | defecto | qué hace |
1114
+ | --- | --- | --- | --- | --- |
1115
+ | `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. |
1116
+ | `basePath` | `string` | | `RUTA_ASSETS` | |
1117
+ | `pose` | `"desk" \| "laptop-coffee" \| "peek" \| "surf"` | sí | | |
1118
+
1119
+ **CaraDeMascota**
1120
+ La cabeza de Tiburoncín, con expresión.
1121
+
1122
+ - Extiende: `Base`
1123
+
1124
+ | prop | tipo | req. | defecto | qué hace |
1125
+ | --- | --- | --- | --- | --- |
1126
+ | `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. |
1127
+ | `basePath` | `string` | | `RUTA_ASSETS` | |
1128
+ | `expresion` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | sí | | |
1129
+
1130
+ ## Exportaciones que no son componentes
1131
+
1132
+ La raíz reexporta todo lo de `./tokens` y `./brand` por conveniencia. Cada uno
1133
+ aparece una sola vez, en la subruta más específica que lo publica: si el código
1134
+ no monta React, esa subruta es la que hay que importar.
1135
+
1136
+ ### `@eduardoalvarez/arrecife/tokens`
1137
+
1138
+ | export | tipo | qué es |
1139
+ | --- | --- | --- |
1140
+ | `brand` | `{ readonly body: "#3E7CB1"; readonly spots: "#C2D7E7"; readonly hull: "#0B1524"; }` | Marca — iguales en los dos modos. |
1141
+ | `colors` | `{ dark, light }` | |
1142
+ | `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`. |
1143
+ | `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. |
1144
+ | `fonts` | `{ display, sans, mono }` | |
1145
+ | `gradient` | `{ dark, light }` | |
1146
+ | `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. |
1147
+ | `limits` | `{ readonly minScreenPx: 13; readonly minPrintPt: 12; readonly measure: "68ch"; }` | Límites duros de legibilidad. |
1148
+ | `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. |
1149
+ | `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. |
1150
+ | `radius` | `{ readonly chip: 6; readonly control: 10; readonly card: 14; readonly panel: 16; readonly pill: 999; }` | |
1151
+ | `shadow` | `{ readonly standard: "0 1px 2px rgba(0, 0, 0, 0.35)"; }` | Un solo nivel. No hay escala de elevación. |
1152
+ | `sintaxis` | `{ fondo, identificador, literal, palabraClave, comentario, invalido }` | La paleta del resaltado de sintaxis. |
1153
+ | `size` | `{ readonly nav: 64; readonly content: 760; readonly wide: 1180; }` | |
1154
+ | `spacing` | `{ readonly xs: 8; readonly sm: 12; readonly md: 16; readonly lg: 26; readonly xl: 40; readonly section: 96; }` | |
1155
+ | `tagline` | `{ largo, corto, en }` | |
1156
+ | `tokens` | `{ colors, brand, fonts, typeScale, limits, radius, control, spacing, size, gradient, sintaxis, shadow, motion, tagline, naming }` | Todos los tokens en un solo objeto, para plantillas Satori y generadores. |
1157
+ | `typeScale` | `{ display, stat, h1, h2, h3, body, lead, ui, label, tag, chip, meta, eyebrow }` | |
1158
+
1159
+ Tipos (12): `BrandToken`, `ColorMode`, `ColorToken`, `ControlToken`, `FontToken`, `GradientToken`, `RadiusToken`, `SintaxisToken`, `SizeToken`, `SpacingToken`, `Tokens`, `TypeScaleToken`.
1160
+
1161
+ ### `@eduardoalvarez/arrecife/brand`
1162
+
1163
+ | export | tipo | qué es |
1164
+ | --- | --- | --- |
1165
+ | `aletas` | `{ readonly color: "fin.png"; readonly espuma: "fin-foam.png"; }` | La aleta, en sus dos variantes. |
1166
+ | `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. |
1167
+ | `listaCaras` | `readonly ("annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink")[]` | |
1168
+ | `listaPoses` | `readonly ("desk" \| "laptop-coffee" \| "peek" \| "surf")[]` | |
1169
+ | `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. |
1170
+ | `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. |
1171
+ | `usoDeCara` | `{ wink, waiting, laughing, shades, hearts, confused, annoyed }` | El uso asignado de cada cara, del inventario del manual. |
1172
+
1173
+ Tipos (8): `Aleta`, `Cara`, `CaraDeMascotaProps`, `Fondo`, `IsotipoProps`, `LogoProps`, `MascotaProps`, `Pose`.
1174
+
1175
+ ### `@eduardoalvarez/arrecife/shiki`
1176
+
1177
+ | export | tipo | qué es |
1178
+ | --- | --- | --- |
1179
+ | `arrecife` | `TemaShiki` | |
1180
+
1181
+ Tipos (1): `TemaShiki`.
1182
+
1183
+ ### `@eduardoalvarez/arrecife/og`
1184
+
1185
+ | export | tipo | qué es |
1186
+ | --- | --- | --- |
1187
+ | `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. |
1188
+ | `plantillaArticulo` | `(datos: DatosArticulo): NodoSatori` | Artículo · degradado 145° sobre abismo, categoría y lectura en arena. |
1189
+ | `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` | |
1190
+ | `plantillaCharla` | `(datos: DatosCharla): NodoSatori` | Charla · eyebrow en bioluz con evento y año, pose sangrando por la esquina. |
1191
+ | `plantillaCurso` | `(datos: DatosCurso): NodoSatori` | Curso · LA ÚNICA PLANTILLA EN CLARO. |
1192
+ | `plantillaDefecto` | `(datos?: DatosDefecto): NodoSatori` | Por defecto · la excepción declarada del documento. |
1193
+
1194
+ Tipos (6): `DatosArticulo`, `DatosBase`, `DatosCharla`, `DatosCurso`, `DatosDefecto`, `NodoSatori`.
1195
+
1196
+ ### `@eduardoalvarez/arrecife`
1197
+
1198
+ | export | tipo | qué es |
1199
+ | --- | --- | --- |
1200
+ | `alertVariants` | `(props?: (ConfigVariants<{ variant: { accent: string; success: string; warning: string; error: string; }; enfasis: { sutil: string; fuerte: string; }; }> & ClassProp) \| undefined): string` | |
1201
+ | `avatarVariants` | `(props?: (ConfigVariants<{ size: { sm: string; md: string; lg: string; xl: string; }; }> & ClassProp) \| undefined): string` | |
1202
+ | `badgeVariants` | `(props?: (ConfigVariants<{ variant: { neutral: string; accent: string; warm: string; success: string; warning: string; error: string; }; }> & ClassProp) \| undefined): string` | |
1203
+ | `buttonVariants` | `(props?: (ConfigVariants<{ variant: { primary: string[]; conversion: string; secondary: string[]; tertiary: string[]; }; size: { sm: string; md: string; lg: string; icon: string; }; }> & ClassProp) \| undefined): string` | |
1204
+ | `categoryBadgeVariants` | `(props?: (ConfigVariants<{ active: { false: string; true: string; }; }> & ClassProp) \| undefined): string` | |
1205
+ | `cn` | `(...inputs: ClassValue[]): string` | |
1206
+ | `HOVER_TARJETA` | `"transition-standard hover:border-hairline-hover"` | El hover de la regla 6: solo el borde. Se aplica donde la tarjeta es pulsable. |
1207
+ | `social` | `typeof import("src/lib/social")` | |
1208
+ | `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. |
1209
+ | `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` | |
1210
+
1211
+ Tipos (51): `AlertProps`, `ArticleCardProps`, `AudioPlayerMode`, `AudioPlayerProps`, `AuthorCardProps`, `AvatarProps`, `BadgeProps`, `BlockquoteProps`, `BreadcrumbProps`, `ButtonProps`, `CalendarProps`, `CategoryBadgeProps`, `CheckboxProps`, `CodeBlockProps`, `CodeProps`, `CourseCardProps`, `DateFieldProps`, `EmptyStateProps`, `Entrada`, `FooterLinkProps`, `FooterProps`, `HeroProps`, `InputProps`, `LabelProps`, `LinkRowProps`, `MetricBadgeProps`, `Migaja`, `NavItemProps`, `NavProps`, `NewsletterFormProps`, `NewsletterState`, `PageHeaderProps`, `PaginationLinkProps`, `PopoverContentProps`, `ProgressProps`, `RadioGroupItemProps`, `RadioGroupProps`, `Red`, `SeparatorProps`, `SheetContentProps`, `SidebarItemProps`, `SidebarNavProps`, `SkeletonProps`, `StatProps`, `SwitchProps`, `TableOfContentsProps`, `TabsProps`, `TalkCardProps`, `TextProps`, `TextareaProps`, `ToastProps`.
1212
+
1213
+ # Dónde mirar si esto no basta
1214
+
1215
+ - Storybook publica cada componente con sus stories y la tabla de props generada
1216
+ desde los tipos.
1217
+ - `README.md` del repo: el porqué de cada decisión, la tabla de correcciones de
1218
+ contraste y el ciclo de publicación.
1219
+ - `docs/design-system.md` y `docs/manual-de-marca.md`: los documentos de
1220
+ identidad, consultables con `grep`.
1221
+ - `docs/decisiones.md`: los quince puntos donde el código y el documento no
1222
+ decían lo mismo, con la resolución de cada uno.
1223
+ - `AGENTS.md`: para trabajar dentro del repo de la librería.