@eduardoalvarez/arrecife 0.5.1 → 0.6.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 (66) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +706 -470
  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-25YNFCIF.js +141 -0
  14. package/dist/{chunk-YZ2SDOVZ.js → chunk-6O3KWB6P.js} +30 -30
  15. package/dist/chunk-CKRSQPTX.js +36 -0
  16. package/dist/{chunk-ZEOQKRQ7.js → chunk-DKCN7BAL.js} +1 -1
  17. package/dist/chunk-GCRII2KQ.js +86 -0
  18. package/dist/chunk-JMOOFZ3B.js +42 -0
  19. package/dist/chunk-O4TAH7YJ.js +276 -0
  20. package/dist/chunk-ODBFN44D.js +45 -0
  21. package/dist/{chunk-VPT32GPG.js → chunk-PMN7NR3G.js} +2 -2
  22. package/dist/chunk-XKYHTOUJ.js +27 -0
  23. package/dist/form/index.cjs +109 -92
  24. package/dist/form/index.d.cts +43 -42
  25. package/dist/form/index.d.ts +43 -42
  26. package/dist/form/index.js +25 -23
  27. package/dist/index.cjs +1047 -914
  28. package/dist/index.d.cts +769 -777
  29. package/dist/index.d.ts +769 -777
  30. package/dist/index.js +608 -660
  31. package/dist/{label-DuTvJGxD.d.ts → label-MgHFKnFy.d.cts} +3 -3
  32. package/dist/{label-DuTvJGxD.d.cts → label-MgHFKnFy.d.ts} +3 -3
  33. package/dist/og/index.cjs +130 -130
  34. package/dist/og/index.d.cts +93 -89
  35. package/dist/og/index.d.ts +93 -89
  36. package/dist/og/index.js +106 -106
  37. package/dist/shiki/index.cjs +28 -30
  38. package/dist/shiki/index.d.cts +4 -4
  39. package/dist/shiki/index.d.ts +4 -4
  40. package/dist/shiki/index.js +12 -12
  41. package/dist/theme/index.cjs +97 -0
  42. package/dist/theme/index.d.cts +144 -0
  43. package/dist/theme/index.d.ts +144 -0
  44. package/dist/theme/index.js +2 -0
  45. package/dist/tokens/index.cjs +133 -86
  46. package/dist/tokens/index.d.cts +246 -161
  47. package/dist/tokens/index.d.ts +246 -161
  48. package/dist/tokens/index.js +2 -2
  49. package/dist/tokens/theme.css +133 -98
  50. package/dist/variants/index.cjs +192 -0
  51. package/dist/variants/index.d.cts +192 -0
  52. package/dist/variants/index.d.ts +192 -0
  53. package/dist/variants/index.js +3 -0
  54. package/llms.txt +810 -744
  55. package/package.json +20 -11
  56. package/dist/catalogo-Du5ID-Hi.d.cts +0 -77
  57. package/dist/catalogo-Du5ID-Hi.d.ts +0 -77
  58. package/dist/chunk-E3OMP2DL.js +0 -36
  59. package/dist/chunk-KPZNNMV5.js +0 -83
  60. package/dist/chunk-NHS7ETKJ.js +0 -27
  61. package/dist/chunk-TSPJOM6K.js +0 -229
  62. package/dist/chunk-UOWIDFCB.js +0 -81
  63. package/dist/tema/index.cjs +0 -94
  64. package/dist/tema/index.d.cts +0 -110
  65. package/dist/tema/index.d.ts +0 -110
  66. package/dist/tema/index.js +0 -2
package/dist/index.d.cts CHANGED
@@ -1,10 +1,11 @@
1
- export { BrandToken, ColorMode, ColorToken, ControlToken, FontToken, GradientToken, RadiusToken, SeriesToken, SintaxisToken, SizeToken, SpacingToken, Tokens, TypeScaleToken, brand, colors, control, dark, fonts, gradient, light, limits, motion, naming, radius, series, shadow, sintaxis, size, spacing, tagline, tokens, typeScale } from './tokens/index.cjs';
2
- import { Tema } from './tema/index.cjs';
3
- export { TEMA_ATRIBUTO, TEMA_CLAVE, TEMA_EVENTO, alternarTema, aplicarTema, escucharTema, scriptTema, temaActual, temaGuardado, temaPreferido } from './tema/index.cjs';
1
+ export { BrandToken, ColorMode, ColorToken, ControlToken, FontToken, GradientToken, RadiusToken, SeriesToken, SizeToken, SpacingToken, SyntaxToken, Tokens, TypeScaleToken, brand, colors, control, dark, fonts, gradient, light, limits, motion, naming, radius, series, shadow, size, spacing, syntax, tagline, tokens, typeScale } from './tokens/index.cjs';
2
+ import { Theme } from './theme/index.cjs';
3
+ export { THEME_ATTRIBUTE, THEME_EVENT, THEME_KEY, ThemeOptions, applyTheme, currentTheme, preferredTheme, storedTheme, themeScript, toggleTheme, watchTheme } from './theme/index.cjs';
4
+ import { alertVariants as alert, avatarVariants as avatar, badgeVariants as badge, buttonVariants as button, textVariants as text } from './variants/index.cjs';
5
+ export { CARD, CARD_HOVER, CARD_SURFACE, categoryBadgeVariants, metricBadgeVariants } from './variants/index.cjs';
4
6
  import * as react from 'react';
5
7
  import { ComponentPropsWithoutRef, ReactNode, ComponentProps, RefObject, SVGProps } from 'react';
6
8
  import * as AccordionPrimitive from '@radix-ui/react-accordion';
7
- import * as class_variance_authority_types from 'class-variance-authority/types';
8
9
  import { VariantProps } from 'class-variance-authority';
9
10
  import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
10
11
  import * as AvatarPrimitive from '@radix-ui/react-avatar';
@@ -12,372 +13,234 @@ import { DayPicker } from 'react-day-picker';
12
13
  import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
13
14
  import * as DialogPrimitive from '@radix-ui/react-dialog';
14
15
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
15
- export { L as Label, a as LabelProps } from './label-DuTvJGxD.cjs';
16
+ export { L as Label, a as LabelProps } from './label-MgHFKnFy.cjs';
16
17
  import * as PopoverPrimitive from '@radix-ui/react-popover';
17
18
  import * as ProgressPrimitive from '@radix-ui/react-progress';
18
19
  import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
19
20
  import * as SelectPrimitive from '@radix-ui/react-select';
20
21
  import * as SeparatorPrimitive from '@radix-ui/react-separator';
22
+ import * as class_variance_authority_types from 'class-variance-authority/types';
21
23
  import * as SwitchPrimitive from '@radix-ui/react-switch';
22
24
  import * as TabsPrimitive from '@radix-ui/react-tabs';
23
25
  import * as ToastPrimitive from '@radix-ui/react-toast';
24
26
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
25
- import { C as Cara, P as Pose } from './catalogo-Du5ID-Hi.cjs';
26
- export { A as Aleta, F as Fondo, R as RUTA_ASSETS, a as aletas, c as caras, l as listaCaras, b as listaPoses, p as poses, u as usoDeCara } from './catalogo-Du5ID-Hi.cjs';
27
- export { CaraDeMascota, CaraDeMascotaProps, Isotipo, IsotipoProps, Logo, LogoProps, Mascota, MascotaProps } from './brand/index.cjs';
27
+ import { F as Face, P as Pose } from './catalog-D13txprv.cjs';
28
+ export { A as ASSETS_PATH, B as Background, a as Fin, f as faceList, b as faceUsage, c as faces, d as fins, p as poseList, e as poses } from './catalog-D13txprv.cjs';
29
+ export { Isotype, IsotypeProps, Logo, LogoProps, Mascot, MascotFace, MascotFaceProps, MascotProps } from './brand/index.cjs';
28
30
  import { ClassValue } from 'clsx';
29
31
  import '@radix-ui/react-label';
30
32
 
31
33
  /**
32
- * El plegable. Lo pedían dos proyectos: el FAQ del portafolio y el temario de
33
- * cursos, que es literalmente una lista de secciones que se abren.
34
+ * The disclosure. Two projects asked for it: the portfolio FAQ and the course
35
+ * syllabus, which is literally a list of sections that open.
34
36
  *
35
- * La altura SÍ se anima, y es la cuarta excepción declarada del sistema.
37
+ * The height IS animated, and it is the system's fourth declared exception.
36
38
  *
37
- * Merece explicarse, porque la regla general es la contraria y este componente
38
- * nació sin animar citándola. La diferencia es que aquí no APARECE nada: se
39
- * abre un hueco, y todo lo que hay debajo del acordeón se desplaza. Sin
40
- * transición ese desplazamiento es un salto, y quien acaba de pulsar pierde el
41
- * sitio en la página — que es justo el daño que la regla «nada de movimiento»
42
- * existe para evitar. Es la misma categoría que el panel lateral, la segunda
43
- * excepción, y no la de una animación de entrada.
39
+ * It deserves explaining, because the general rule says the opposite and this
40
+ * component was born unanimated citing it. The difference is that nothing
41
+ * APPEARS here: a gap opens, and everything below the accordion shifts. Without
42
+ * a transition that shift is a jump, and whoever just clicked loses their place
43
+ * on the page — which is exactly the harm the «no movement» rule exists to
44
+ * prevent. It is the same category as the side panel, the second exception, and
45
+ * not that of an entrance animation.
44
46
  *
45
- * Va detrás de `motion-safe`, dura `--duration-standard` y usa
46
- * `--ease-standard`, así que no estrena un tiempo ni una curva. Quien pidió
47
- * menos movimiento sigue viendo el panel aparecer donde va a quedarse.
47
+ * It sits behind `motion-safe`, lasts `--duration-standard` and uses
48
+ * `--ease-standard`, so it introduces neither a new timing nor a new curve.
49
+ * Whoever asked for less motion still sees the panel appear where it will stay.
48
50
  *
49
- * Ver `docs/decisiones.md` § 20.
51
+ * See `docs/decisions.md` § 20.
50
52
  *
51
- * El galón, en cambio, gira sin transición: `transition-standard` solo cubre
52
- * color y borde, así que `rotate` salta aunque la clase esté puesta. Es el mismo
53
- * trato que recibe el ancho de `Progress`.
53
+ * The chevron, by contrast, rotates with no transition: `transition-standard`
54
+ * only covers color and border, so `rotate` snaps even with the class in place.
55
+ * It is the same treatment `Progress` gives its width.
54
56
  *
55
- * La división entre items es `hairline`, no `border`: es una separación de
56
- * lectura, no el borde de un control.
57
+ * The divider between items is `hairline`, not `border`: it is a reading
58
+ * separation, not the border of a control.
57
59
  */
58
60
  type AccordionProps = ComponentPropsWithoutRef<typeof AccordionPrimitive.Root>;
59
61
  declare function Accordion({ className, ...props }: AccordionProps): react.JSX.Element;
60
62
  declare function AccordionItem({ className, ...props }: ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>): react.JSX.Element;
61
63
  /**
62
- * El disparador es el encabezado, así que va DENTRO de un `<h3>`: Radix envuelve
63
- * el botón en `AccordionPrimitive.Header`, que renderiza el elemento que se le
64
- * pida. Sin eso, un lector de pantalla ve una lista de botones sueltos y pierde
65
- * la estructura de la página, que es justo lo que un FAQ necesita conservar.
66
- *
67
- * `headingLevel` existe porque el nivel correcto depende de dónde se monte: en
68
- * una página de FAQ el bloque cuelga de un `<h2>` de sección, y en un temario
69
- * puede colgar de un `<h3>`. Fijarlo aquí sería adivinar.
64
+ * The trigger IS the heading, so it goes INSIDE an `<h3>`: Radix wraps the
65
+ * button in `AccordionPrimitive.Header`, which renders whichever element you ask
66
+ * of it. Without that, a screen reader sees a list of loose buttons and loses
67
+ * the page structure, which is precisely what a FAQ needs to keep.
68
+ *
69
+ * `headingLevel` exists because the correct level depends on where it is
70
+ * mounted: on a FAQ page the block hangs off a section `<h2>`, and in a syllabus
71
+ * it may hang off an `<h3>`. Pinning it here would be guessing.
70
72
  */
71
73
  type AccordionTriggerProps = ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & {
72
- /** Nivel del encabezado que envuelve al disparador. */
74
+ /** The level of the heading wrapping the trigger. */
73
75
  headingLevel?: 2 | 3 | 4;
74
76
  };
75
77
  declare function AccordionTrigger({ className, children, headingLevel, ...props }: AccordionTriggerProps): react.JSX.Element;
76
78
  declare function AccordionContent({ className, children, ...props }: ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>): react.JSX.Element;
77
79
 
78
- /**
79
- * El aviso lleva el color en el fondo, no solo en el borde.
80
- *
81
- * La receta del sistema es fondo al 8 % del color semántico y borde al 22 %.
82
- * Este archivo daba `bg-surface` a las cuatro variantes, así que el tono vivía
83
- * entero en un borde de 1px: cuatro avisos que se distinguían entre sí por una
84
- * línea.
85
- *
86
- * Los cuatro tonos empiezan en ACENTO, que es el informativo del sistema (✦).
87
- * No hay `neutral`: un aviso sin color es un párrafo.
88
- *
89
- * MEDIDO en los dos modos, porque el 8 % del documento está calculado sobre
90
- * abismo y había que comprobar que sobrevive sobre papel. Contraste del tinte
91
- * contra el fondo de página:
92
- *
93
- * 8 % oscuro 8 % claro
94
- * accent 1.149 1.106
95
- * success 1.116 1.121
96
- * warning 1.126 1.109
97
- * error 1.067 1.120
98
- *
99
- * El modo claro NO necesita una segunda tabla: aguanta igual o mejor que el
100
- * oscuro. El único punto flojo del sistema es `error` sobre abismo, 1.067, que
101
- * es el tinte más tenue de los ocho y se apoya entero en el borde al 22 %.
102
- *
103
- * Hay una SEGUNDA receta, a propósito: el aviso bajo el formulario de
104
- * newsletter va al 10 % con el borde sólido para leerse bajo el campo. Es
105
- * `enfasis="fuerte"`, y no se unifica con la sutil porque la diferencia está
106
- * documentada.
107
- *
108
- * TERCERA corrección de contraste, en la línea de las tres que ya tenía
109
- * `tokens.ts`. El título iba en el color semántico, y en modo claro eso no puede
110
- * pasar AA: los semánticos claros están calibrados para pasar JUSTO sobre papel
111
- * (4.54–4.88), así que sobre su propio tinte al 8 % caen a 4.11–4.40. No hay
112
- * alfa que lo arregle — el problema es poner el color encima de sí mismo.
113
- *
114
- * El tinte es una SUPERFICIE, así que el texto que lleva encima es un token de
115
- * texto: `textPrimary` da 14.6–14.9 sobre los cuatro tintes. El color semántico
116
- * se queda donde no es texto — el borde y el glifo —, que es lo único que el
117
- * documento pedía de él. El glifo es decorativo y va `aria-hidden`, así que le
118
- * aplica el umbral de 3:1 y no el de 4.5: su peor caso claro es 4.11.
119
- *
120
- * El radio: el documento dice 12, que no es ninguno de los cinco radios del
121
- * sistema. Usa el de tarjeta antes que estrenar un sexto — ver
122
- * `docs/decisiones.md`.
123
- */
124
- declare const alert: (props?: ({
125
- variant?: "error" | "accent" | "success" | "warning" | null | undefined;
126
- enfasis?: "sutil" | "fuerte" | null | undefined;
127
- } & class_variance_authority_types.ClassProp) | undefined) => string;
128
80
  type AlertProps = Omit<ComponentPropsWithoutRef<'div'>, 'title'> & VariantProps<typeof alert> & {
129
81
  title?: ReactNode;
130
82
  /**
131
- * Sustituye el glifo mono de la variante. Nunca un emoji: si necesitas otra
132
- * cosa, es un SVG de `glyphs`.
83
+ * Replaces the variant's mono glyph. Never an emoji: if you need something
84
+ * else, it is an SVG from `glyphs`.
133
85
  */
134
86
  icon?: ReactNode;
135
87
  };
136
- declare function Alert({ className, variant, enfasis, title, icon, children, ...props }: AlertProps): react.JSX.Element;
88
+ declare function Alert({ className, variant, emphasis, title, icon, children, ...props }: AlertProps): react.JSX.Element;
137
89
 
138
90
  /**
139
- * La confirmación destructiva. NO es un `Dialog` con otro texto, y por eso está
140
- * en su propio archivo y sobre su propia primitiva de Radix.
141
- *
142
- * Tres diferencias, y las tres importan en el momento en que alguien va a
143
- * borrar un artículo:
144
- *
145
- * 1. El rol es `alertdialog`, no `dialog`. Un lector de pantalla lo anuncia
146
- * con la descripción incluida, sin esperar a que se navegue hasta ella.
147
- * 2. El foco inicial va al CANCELAR, no al primer elemento. Quien pulsa Enter
148
- * por inercia no borra nada. Radix lo hace solo si el cancelar existe, y
149
- * por eso `AlertDialogCancel` no es opcional en la práctica.
150
- * 3. NO se cierra al pulsar fuera ni tiene aspa. Salir de una confirmación es
151
- * una decisión, no un descuido: hay que decir que no.
152
- *
153
- * El botón de confirmar NO es rojo. El sistema no tiene variante de peligro
154
- * —`Button` lo dice explícito— y el error vive en los avisos y en la validación
155
- * de campo, no en un botón. Lo que comunica la gravedad es el texto: «Borrar el
156
- * artículo», no «Aceptar».
91
+ * The destructive confirmation. It is NOT a `Dialog` with different text, which
92
+ * is why it lives in its own file and on its own Radix primitive.
93
+ *
94
+ * Three differences, and all three matter at the moment somebody is about to
95
+ * delete an article:
96
+ *
97
+ * 1. The role is `alertdialog`, not `dialog`. A screen reader announces it
98
+ * with the description included, without waiting for you to navigate to it.
99
+ * 2. Initial focus goes to CANCEL, not to the first element. Whoever hits
100
+ * Enter out of inertia deletes nothing. Radix does this on its own only if
101
+ * the cancel exists, which is why `AlertDialogCancel` is not optional in
102
+ * practice.
103
+ * 3. It does NOT close on outside click and has no X. Leaving a confirmation
104
+ * is a decision, not a slip: you have to say no.
105
+ *
106
+ * The confirm button is NOT red, and since 0.6.0 that is a choice and no longer
107
+ * the absence of an option: `Button` has `destructive`, and here it is still not
108
+ * used. Everything above already carries the gravity — a title that says what is
109
+ * about to happen, focus on cancel, no closing by clicking outside — and a red
110
+ * button on top of that is shouting. What communicates the gravity is the text:
111
+ * «Borrar el artículo», not «Aceptar».
112
+ *
113
+ * `destructive` is for the destructive button that has none of that around it:
114
+ * a table row, a toolbar. See `docs/decisions.md` § 21.
157
115
  */
158
116
  declare const AlertDialog: react.FC<AlertDialogPrimitive.AlertDialogProps>;
159
117
  declare const AlertDialogTrigger: react.ForwardRefExoticComponent<AlertDialogPrimitive.AlertDialogTriggerProps & react.RefAttributes<HTMLButtonElement>>;
160
118
  declare function AlertDialogOverlay({ className, ...props }: ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>): react.JSX.Element;
161
- /** Sin entrada animada, igual que `Dialog`: aparece donde va a quedarse. */
119
+ /** No entrance animation, same as `Dialog`: it appears where it will stay. */
162
120
  declare function AlertDialogContent({ className, children, ...props }: ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>): react.JSX.Element;
163
121
  declare function AlertDialogHeader({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
164
122
  /**
165
- * Cancelar a la IZQUIERDA de confirmar en escritorio y ABAJO en móvil, que es lo
166
- * que da `flex-col-reverse`: el orden del DOM pone cancelar primero —es donde va
167
- * el foco— y en columna el dedo lo encuentra donde toca sin cambiar la
168
- * tabulación.
123
+ * Cancel to the LEFT of confirm on desktop and BELOW it on mobile, which is what
124
+ * `flex-col-reverse` gives: the DOM order puts cancel first — that is where focus
125
+ * goes — and in a column the thumb finds it where it should be without changing
126
+ * the tab order.
169
127
  */
170
128
  declare function AlertDialogFooter({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
171
129
  declare function AlertDialogTitle({ className, ...props }: ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>): react.JSX.Element;
172
130
  /**
173
- * Lo que se pierde, dicho entero. Es lo que el rol `alertdialog` hace que se
174
- * anuncie de entrada, así que aquí no va «esta acción no se puede deshacer»
175
- * suelto: va qué se borra y qué se lleva por delante.
131
+ * What is lost, spelled out. The `alertdialog` role makes this get announced up
132
+ * front, so «this action cannot be undone» does not go here on its own: what
133
+ * goes here is what gets deleted and what it takes down with it.
176
134
  */
177
135
  declare function AlertDialogDescription({ className, ...props }: ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>): react.JSX.Element;
178
- /** El que se lleva el foco al abrir. */
136
+ /** The one that takes focus on open. */
179
137
  declare function AlertDialogCancel({ className, ...props }: ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>): react.JSX.Element;
180
138
  declare function AlertDialogAction({ className, ...props }: ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>): react.JSX.Element;
181
139
 
182
- declare const avatar: (props?: ({
183
- size?: "sm" | "md" | "lg" | "xl" | null | undefined;
184
- } & class_variance_authority_types.ClassProp) | undefined) => string;
185
140
  type AvatarProps = ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & VariantProps<typeof avatar>;
186
141
  /**
187
- * Uno solo para todo: la foto del autor y la de cualquier persona del sistema.
188
- * No hay un `brand/Avatar` aparte — una foto de perfil con la piel de la marca
189
- * es exactamente esto con un `src` distinto.
142
+ * One for everything: the author's photo and anyone else's in the system. There
143
+ * is no separate `brand/Avatar` — a profile photo wearing the brand's skin is
144
+ * exactly this with a different `src`.
190
145
  */
191
146
  declare function Avatar({ className, size, ...props }: AvatarProps): react.JSX.Element;
192
147
  declare function AvatarImage({ className, ...props }: ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>): react.JSX.Element;
193
- /** Iniciales mientras la imagen carga, o cuando no hay imagen. */
148
+ /** Initials while the image loads, or when there is no image. */
194
149
  declare function AvatarFallback({ className, ...props }: ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>): react.JSX.Element;
195
150
  type AvatarUploadProps = Omit<ComponentPropsWithoutRef<'div'>, 'onSelect' | 'children'> & VariantProps<typeof avatar> & {
196
- /** La imagen actual, ya subida. La previsualización local la gana mientras dure. */
151
+ /** The current image, already uploaded. The local preview beats it while it lasts. */
197
152
  src?: string | undefined;
198
- /** Iniciales mientras no hay imagen. */
153
+ /** Initials while there is no image. */
199
154
  fallback?: ReactNode;
200
- /** Se dispara con el archivo elegido. La subida la hace el proyecto. */
201
- onSelectFile?: ((archivo: File) => void) | undefined;
202
- /** Qué acepta el diálogo del sistema. */
155
+ /** Fires with the chosen file. The upload is the project's job. */
156
+ onSelectFile?: ((file: File) => void) | undefined;
157
+ /** What the system dialog accepts. */
203
158
  accept?: string;
204
- /** Nombre accesible del control. Es lo único que lo nombra: no hay texto visible. */
159
+ /** The control's accessible name. It is the only thing naming it: there is no visible text. */
205
160
  label?: string;
206
161
  disabled?: boolean | undefined;
207
162
  };
208
163
  /**
209
- * El avatar que se puede cambiar. `Avatar` muestra; este además deja elegir.
210
- *
211
- * Es presentacional, como `NewsletterForm`: emite `onSelectFile` con el `File` y
212
- * ahí se acaba su trabajo. La subida no entra —cada proyecto tiene su
213
- * almacenamiento y su endpoint— y un componente que hiciera el `POST` sería
214
- * infraestructura, que es el tercer criterio de entrada y el que más se salta.
215
- *
216
- * La previsualización es LOCAL y no espera a que la subida termine. Es la
217
- * diferencia entre un control que responde y uno que parece roto: entre elegir
218
- * el archivo y que el servidor devuelva la URL pueden pasar segundos, y sin
219
- * previa el avatar se queda con la foto vieja como si no hubiera pasado nada.
220
- * El `objectURL` se revoca al cambiar y al desmontar; no revocarlo es una fuga
221
- * de memoria que no da la cara hasta la décima foto.
222
- *
223
- * El control es un `<label>` con un `<input type="file">` oculto dentro, no un
224
- * `<button>` que dispara un click sintético. El input real trae el diálogo del
225
- * sistema, el arrastrar-y-soltar del navegador y el foco por teclado; el botón
226
- * falso hay que reconstruirlo entero y siempre falta algo.
164
+ * The avatar you can change. `Avatar` displays; this one also lets you pick.
165
+ *
166
+ * It is presentational, like `NewsletterForm`: it emits `onSelectFile` with the
167
+ * `File` and its job ends there. The upload does not belong here — every project
168
+ * has its own storage and its own endpoint — and a component doing the `POST`
169
+ * would be infrastructure, which is the third entry criterion and the one most
170
+ * often skipped.
171
+ *
172
+ * The preview is LOCAL and does not wait for the upload to finish. It is the
173
+ * difference between a control that responds and one that looks broken: seconds
174
+ * can pass between picking the file and the server returning the URL, and with
175
+ * no preview the avatar keeps the old photo as though nothing had happened. The
176
+ * `objectURL` is revoked on change and on unmount; not revoking it is a memory
177
+ * leak that does not show its face until the tenth photo.
178
+ *
179
+ * The control is a `<label>` with a hidden `<input type="file">` inside, not a
180
+ * `<button>` firing a synthetic click. The real input brings the system dialog,
181
+ * the browser's drag-and-drop and keyboard focus; the fake button has to be
182
+ * rebuilt from scratch and something is always missing.
227
183
  */
228
184
  declare function AvatarUpload({ src, fallback, onSelectFile, accept, label, size, disabled, className, ...props }: AvatarUploadProps): react.JSX.Element;
229
185
 
230
186
  /**
231
- * TRES familias de etiqueta, no una.
232
- *
233
- * El sistema define tres formas distintas y este archivo las servía todas como
234
- * píldora mono en versalitas. Cada una tiene su forma porque cada una dice otra
235
- * cosa:
236
- *
237
- * categoría · píldora r999, mono 11.5, arena → un slug: `engineering-culture`
238
- * estado · cuadrada r6, sans 12.5/500, tinte → un semáforo: publicado, borrador
239
- * métrica · píldora, mono 11.5 muted → un dato: `8 min de lectura`
240
- *
241
- * NINGUNA va en versalitas. El `uppercase tracking-[0.12em]` que tenían las tres
242
- * venía de `text-eyebrow`, que es la escala del eyebrow y no la de las
243
- * etiquetas: convertía `engineering-culture` en `ENGINEERING-CULTURE` y
244
- * `pose-laptop-coffee.png` en un nombre de archivo que no existe.
187
+ * Three badge families, three components. Why there are three shapes and not one
188
+ * is in `variants/badge.ts`, next to the classes that make them.
245
189
  */
246
- /**
247
- * Semáforo. Cuadrada r6, sans 12.5/500 y fondo al 8 % del semántico — la receta
248
- * del aviso en tamaño de palabra: un estado es un aviso de una sola palabra.
249
- *
250
- * SIN borde. Lo llevó un tiempo, puesto para reforzar el tono, y pesaba: una
251
- * caja con borde al lado de un título se lee como un control y no como un dato.
252
- * El tinte solo es lo que pide el documento y es lo que se ve más limpio.
253
- *
254
- * El texto va en `textPrimary`, no en el color del tono. En modo claro los
255
- * semánticos están calibrados para pasar JUSTO sobre papel, así que sobre su
256
- * propio tinte caen a 4.10–4.40 y no pasan AA. El tinte es una superficie;
257
- * encima va un token de texto. Medido en `alert.tsx`.
258
- *
259
- * El tono nunca es el único portador del significado: la etiqueta dice
260
- * «Publicado» o «Borrador» con todas sus letras.
261
- */
262
- declare const badge: (props?: ({
263
- variant?: "error" | "accent" | "success" | "warning" | "neutral" | "warm" | null | undefined;
264
- } & class_variance_authority_types.ClassProp) | undefined) => string;
265
190
  type BadgeProps = ComponentPropsWithoutRef<'span'> & VariantProps<typeof badge>;
266
191
  declare function Badge({ className, variant, ...props }: BadgeProps): react.JSX.Element;
267
- /**
268
- * Un slug, en arena. Sin transformar: los slugs ya vienen en minúscula y
269
- * forzarla sería el mismo error que forzaba el `uppercase`.
270
- *
271
- * El borde del documento es `#4A3A25`, que es arena al 28 % sobre abismo: sale
272
- * de la paleta, así que no entra como token nuevo. En modo claro la misma regla
273
- * da arena oscura al 28 % sobre papel, que es lo que se quiere.
274
- *
275
- * La variante rellena NO es decorativa: es el único indicador de filtro activo
276
- * del listado de artículos. Por eso `active` es una prop y no un `className`.
277
- */
278
- declare const categoria: (props?: ({
279
- active?: boolean | null | undefined;
280
- } & class_variance_authority_types.ClassProp) | undefined) => string;
281
192
  type CategoryBadgeProps = ComponentPropsWithoutRef<'span'> & {
282
- /** Filtro seleccionado: arena sólido con tinta encima. */
193
+ /** Selected filter: solid sand with ink on top. */
283
194
  active?: boolean | undefined;
284
195
  };
285
196
  declare function CategoryBadge({ className, active, ...props }: CategoryBadgeProps): react.JSX.Element;
286
197
  type MetricBadgeProps = ComponentPropsWithoutRef<'span'> & {
287
- /** Añade el aro de hairline. Por defecto la métrica va sin caja. */
198
+ /** Adds the hairline ring. By default a metric carries no box. */
288
199
  boxed?: boolean | undefined;
289
200
  };
290
201
  declare function MetricBadge({ className, boxed, ...props }: MetricBadgeProps): react.JSX.Element;
291
202
 
292
- /**
293
- * Las CUATRO variantes del sistema, y solo esas cuatro.
294
- *
295
- * Regla de marca 2, como código y no como documentación: en modo claro el botón
296
- * primario no puede ser bioluz ni arena, así que pasa a casco sólido. No hay un
297
- * hex literal en ningún lado — `brand.hull` es un token, y el hover reusa
298
- * `textSecondary` en vez de inventar un `hullHover`.
299
- *
300
- * Regla de marca 3: `conversion` va UNA sola vez por pantalla. Está documentado
301
- * en la story y no se fuerza en runtime: dos botones de conversión en una misma
302
- * página son un problema de diseño, no un error que deba tirar el render.
303
- *
304
- * `secondary` NUNCA se rellena el fondo. Es borde y texto: en reposo, hairline
305
- * de hover y espuma; en hover, los dos pasan a bioluz. Un secundario relleno es
306
- * un primario mal teñido, y era lo que hacía este archivo.
307
- *
308
- * `tertiary` es la estética CLI del sistema: mono, formato `./acción →`, sin
309
- * caja. Aparece en cada tarjeta, así que no es un `ghost` genérico con otro
310
- * nombre — el formato del texto es parte de la variante.
311
- *
312
- * No hay variante de peligro. El error del sistema vive en los avisos y en la
313
- * validación de campo, no en un botón rojo. Si alguna vez hace falta uno de
314
- * verdad, entra primero en el documento y luego aquí.
315
- */
316
- declare const button: (props?: ({
317
- variant?: "primary" | "conversion" | "secondary" | "tertiary" | null | undefined;
318
- size?: "icon" | "sm" | "md" | "lg" | null | undefined;
319
- } & class_variance_authority_types.ClassProp) | undefined) => string;
320
203
  type ButtonProps = ComponentPropsWithoutRef<'button'> & VariantProps<typeof button> & {
321
- /** Renderiza el hijo en vez de un `<button>`, para envolver un enlace. */
204
+ /** Renders the child instead of a `<button>`, to wrap a link. */
322
205
  asChild?: boolean;
323
- /** Deshabilita y anuncia `aria-busy`. Incompatible con `asChild`. */
206
+ /** Disables and announces `aria-busy`. Incompatible with `asChild`. */
324
207
  loading?: boolean;
325
- /** Glifo SVG antes del texto. Se oculta mientras carga. */
208
+ /** SVG glyph before the text. Hidden while loading. */
326
209
  icon?: ReactNode;
327
210
  };
328
211
  declare function Button({ className, variant, size, asChild, loading, icon, children, disabled, ...props }: ButtonProps): react.JSX.Element;
329
212
 
330
213
  type CalendarProps = ComponentProps<typeof DayPicker> & {
331
214
  /**
332
- * Estira el calendario hasta ocupar todo el ancho de su contenedor, con las
333
- * celdas repartiéndoselo a partes iguales.
215
+ * Stretches the calendar to fill its container's whole width, with the cells
216
+ * splitting it evenly.
334
217
  *
335
- * Apagado, el calendario mide lo que miden sus celdas —36 px cada una— y es lo
336
- * que quieres dentro de un `Popover`, donde estirarlo dejaría un globo enorme.
337
- * Encendido, es la vista de mes de un planificador, que ocupa la página.
218
+ * Off, the calendar measures whatever its cells measure — 36px each — and that
219
+ * is what you want inside a `Popover`, where stretching it would leave a huge
220
+ * bubble. On, it is a planner's month view, which fills the page.
338
221
  *
339
- * Con varios meses, cada uno se lleva una fracción igual del ancho.
222
+ * With several months, each takes an equal fraction of the width.
340
223
  */
341
224
  fullWidth?: boolean | undefined;
342
225
  };
343
226
  /**
344
- * Calendario mensual navegable, sobre `react-day-picker`.
227
+ * A navigable month calendar, on top of `react-day-picker`.
345
228
  *
346
- * Es la única dependencia pesada de la librería y entró a sabiendas: el
347
- * calendario del planificador de contenido no se puede resolver con el control
348
- * nativo. Para elegir una fecha dentro de un formulario existe `DateField`, que
349
- * no arrastra nada.
229
+ * It is the library's only heavy dependency and it was let in knowingly: the
230
+ * content planner's calendar cannot be solved with the native control. For
231
+ * picking a date inside a form there is `DateField`, which drags nothing along.
350
232
  *
351
- * `animate` se queda apagado —es su valor por defecto— así que el cambio de mes
352
- * no se desliza. Los días se marcan con color y borde, como todo lo demás.
233
+ * `animate` stays off — that is its default — so the month change does not
234
+ * slide. Days are marked with color and border, like everything else.
353
235
  *
354
- * El idioma va en español por defecto porque los cinco proyectos lo están; se
355
- * cambia pasando otro `locale` de date-fns.
236
+ * The language defaults to Spanish because all five projects are; it is changed
237
+ * by passing another date-fns `locale`.
356
238
  *
357
- * No trae el `style.css` de la librería: todas las clases salen de aquí, así que
358
- * el consumidor no tiene que importar CSS de terceros ni pelearse con su
359
- * especificidad.
239
+ * It does not ship the library's `style.css`: every class comes from here, so
240
+ * the consumer need not import third-party CSS or fight its specificity.
360
241
  */
361
242
  declare function Calendar({ className, classNames, showOutsideDays, fullWidth, ...props }: CalendarProps): react.JSX.Element;
362
243
 
363
- /**
364
- * El contenedor de superficie del sistema, y la única definición de lo que es
365
- * una tarjeta: `surface`, borde `hairline`, radio de tarjeta.
366
- *
367
- * Las tarjetas con dominio — `ArticleCard`, `TalkCard`, `CourseCard`,
368
- * `LinkRow` — reutilizan estas mismas clases, así que si el radio o el borde
369
- * cambian, cambian en todas a la vez.
370
- *
371
- * El documento daba a la tarjeta un fondo propio, `#0B1620`, un cuarto nivel de
372
- * superficie entre abismo y fosa. No entra: no tiene par en modo claro, y una
373
- * superficie sin par es un token que miente en la mitad de los proyectos. La
374
- * tarjeta es `surface`, y el documento se corrige — ver `docs/decisiones.md`.
375
- *
376
- * El padding sí estaba mal: el documento pide 26 (`lg`) y aquí había 16 (`md`).
377
- */
378
- declare const SUPERFICIE_TARJETA = "rounded-card border-hairline bg-surface border";
379
- /** El hover de la regla 6: solo el borde. Se aplica donde la tarjeta es pulsable. */
380
- declare const HOVER_TARJETA = "transition-standard hover:border-hairline-hover";
381
244
  declare function Card({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
382
245
  declare function CardHeader({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
383
246
  declare function CardTitle({ className, ...props }: ComponentPropsWithoutRef<'h3'>): react.JSX.Element;
@@ -389,41 +252,40 @@ type CheckboxProps = ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>;
389
252
  declare function Checkbox({ className, ...props }: CheckboxProps): react.JSX.Element;
390
253
 
391
254
  /**
392
- * Código en línea, dentro de prosa.
255
+ * Inline code, inside prose.
393
256
  *
394
- * Existe porque no existía: cada consumidor escribía `<code className="font-mono">`
395
- * a mano, y la story de marca lo hacía cuatro veces en un solo párrafo. Un
396
- * `<code>` suelto hereda el tamaño del párrafo, así que dentro de `body` (18px)
397
- * se veía un mono de 18px que el documento no tiene en ninguna parte.
257
+ * It exists because it did not: every consumer wrote `<code className="font-mono">`
258
+ * by hand, and the brand story did it four times in a single paragraph. A bare
259
+ * `<code>` inherits the paragraph's size, so inside `body` (18px) you saw an
260
+ * 18px mono that the document has nowhere.
398
261
  *
399
- * No es `CodeBlock`. El bloque es una isla de tema oscuro sobre casco con barra
400
- * y botón de copiar; esto es una palabra dentro de una frase, y por eso se queda
401
- * en la superficie de la página en vez de invertir el tema.
262
+ * It is not `CodeBlock`. The block is an island of dark theme over hull with a
263
+ * bar and a copy button; this is one word inside a sentence, which is why it
264
+ * stays on the page surface instead of inverting the theme.
402
265
  */
403
266
  type CodeProps = ComponentPropsWithoutRef<'code'>;
404
267
  declare function Code({ className, ...props }: CodeProps): react.JSX.Element;
405
268
 
406
269
  type DateFieldProps = Omit<ComponentPropsWithoutRef<'input'>, 'type'> & {
407
270
  invalid?: boolean | undefined;
408
- /** Añade la hora al campo. Es el `datetime-local` nativo. */
271
+ /** Adds the time to the field. It is the native `datetime-local`. */
409
272
  withTime?: boolean | undefined;
410
273
  };
411
274
  /**
412
- * Un campo de fecha sobre el control nativo, no sobre un calendario propio.
275
+ * A date field on the native control, not on a calendar of our own.
413
276
  *
414
- * Es una decisión deliberada: `react-day-picker` habría sido la primera
415
- * dependencia pesada de la librería, con su propio CSS y su propia animación de
416
- * cambio de mes — que el sistema no permite. El control nativo trae gratis el
417
- * teclado del sistema operativo, el formato según el idioma del usuario y el
418
- * soporte de lector de pantalla, que es más de lo que un calendario a medida da
419
- * sin trabajo.
277
+ * It is a deliberate decision: `react-day-picker` would have been the library's
278
+ * first heavy dependency, with its own CSS and its own month-change animation —
279
+ * which the system does not allow. The native control brings the OS keyboard,
280
+ * the format matching the user's language and screen-reader support for free,
281
+ * which is more than a bespoke calendar gives without work.
420
282
  *
421
- * Cubre elegir una fecha dentro de un formulario. Un calendario mensual
422
- * navegable es otra cosa y vive en el proyecto que lo necesita.
283
+ * It covers picking a date inside a form. A navigable month calendar is a
284
+ * different thing and lives in the project that needs it.
423
285
  *
424
- * El icono nativo del selector se tiñe con `color-scheme`, que es lo único que
425
- * el navegador deja controlar: se ata al modo activo para que no aparezca un
426
- * cuadradito blanco sobre fondo abismo.
286
+ * The native picker icon is tinted through `color-scheme`, which is the only
287
+ * thing the browser lets you control: it is tied to the active mode so no little
288
+ * white square shows up over abyss.
427
289
  */
428
290
  declare function DateField({ className, invalid, withTime, ...props }: DateFieldProps): react.JSX.Element;
429
291
 
@@ -431,7 +293,7 @@ declare const Dialog: react.FC<DialogPrimitive.DialogProps>;
431
293
  declare const DialogTrigger: react.ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & react.RefAttributes<HTMLButtonElement>>;
432
294
  declare const DialogClose: react.ForwardRefExoticComponent<DialogPrimitive.DialogCloseProps & react.RefAttributes<HTMLButtonElement>>;
433
295
  declare function DialogOverlay({ className, ...props }: ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>): react.JSX.Element;
434
- /** Sin entrada animada: no hay escala ni desplazamiento en el sistema. */
296
+ /** No entrance animation: there is no scale or displacement in the system. */
435
297
  declare function DialogContent({ className, children, ...props }: ComponentPropsWithoutRef<typeof DialogPrimitive.Content>): react.JSX.Element;
436
298
  declare function DialogHeader({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
437
299
  declare function DialogFooter({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
@@ -443,7 +305,7 @@ declare const DropdownMenuTrigger: react.ForwardRefExoticComponent<DropdownMenuP
443
305
  declare const DropdownMenuGroup: react.ForwardRefExoticComponent<DropdownMenuPrimitive.DropdownMenuGroupProps & react.RefAttributes<HTMLDivElement>>;
444
306
  declare const DropdownMenuRadioGroup: react.ForwardRefExoticComponent<DropdownMenuPrimitive.DropdownMenuRadioGroupProps & react.RefAttributes<HTMLDivElement>>;
445
307
  declare const DropdownMenuSub: react.FC<DropdownMenuPrimitive.DropdownMenuSubProps>;
446
- /** Sin animación de entrada: el menú aparece, no se despliega. */
308
+ /** No entrance animation: the menu appears, it does not unfold. */
447
309
  declare function DropdownMenuContent({ className, sideOffset, ...props }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>): react.JSX.Element;
448
310
  declare function DropdownMenuItem({ className, ...props }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>): react.JSX.Element;
449
311
  declare function DropdownMenuCheckboxItem({ className, children, ...props }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>): react.JSX.Element;
@@ -454,7 +316,7 @@ declare function DropdownMenuSubTrigger({ className, children, ...props }: Compo
454
316
  declare function DropdownMenuSubContent({ className, ...props }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>): react.JSX.Element;
455
317
 
456
318
  type InputProps = ComponentPropsWithoutRef<'input'> & {
457
- /** Marca el control como inválido y tiñe el borde. */
319
+ /** Marks the control as invalid and tints the border. */
458
320
  invalid?: boolean;
459
321
  };
460
322
  declare function Input({ className, invalid, ...props }: InputProps): react.JSX.Element;
@@ -474,36 +336,37 @@ declare const Popover: react.FC<PopoverPrimitive.PopoverProps>;
474
336
  declare const PopoverTrigger: react.ForwardRefExoticComponent<PopoverPrimitive.PopoverTriggerProps & react.RefAttributes<HTMLButtonElement>>;
475
337
  declare const PopoverAnchor: react.ForwardRefExoticComponent<PopoverPrimitive.PopoverAnchorProps & react.RefAttributes<HTMLDivElement>>;
476
338
  /**
477
- * Radix le pone `role="dialog"` al contenido, y un diálogo sin nombre accesible
478
- * no le dice nada a quien navega con lector de pantalla. Por eso el tipo exige
479
- * uno de los dos: `aria-label` con el texto, o `aria-labelledby` apuntando al
480
- * título que ya se ve en pantalla. No se puede olvidar porque no compila.
339
+ * Radix puts `role="dialog"` on the content, and a dialog with no accessible
340
+ * name says nothing to someone navigating with a screen reader. That is why the
341
+ * type demands one of the two: `aria-label` with the text, or `aria-labelledby`
342
+ * pointing at the title already visible on screen. It cannot be forgotten
343
+ * because it does not compile.
481
344
  */
482
- type Etiquetado = {
345
+ type Labelled = {
483
346
  'aria-label': string;
484
347
  'aria-labelledby'?: never;
485
348
  } | {
486
349
  'aria-labelledby': string;
487
350
  'aria-label'?: never;
488
351
  };
489
- type PopoverContentProps = ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & Etiquetado;
490
- /** Sin animación de entrada: aparece donde va a quedarse, como el resto. */
352
+ type PopoverContentProps = ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & Labelled;
353
+ /** No entrance animation: it appears where it will stay, like the rest. */
491
354
  declare function PopoverContent({ className, align, sideOffset, ...props }: PopoverContentProps): react.JSX.Element;
492
355
 
493
356
  type ProgressProps = ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
494
357
  /**
495
- * Nombre accesible de la barra. Es obligatorio a propósito: una barra de
496
- * progreso sin nombre no dice de qué es el progreso, y ninguna otra parte del
497
- * componente puede deducirlo.
358
+ * The bar's accessible name. It is mandatory on purpose: a progress bar with
359
+ * no name does not say what the progress is about, and no other part of the
360
+ * component can deduce it.
498
361
  */
499
362
  label: string;
500
- /** Arena en vez de bioluz, para progreso de curso. */
363
+ /** Sand instead of biolume, for course progress. */
501
364
  tone?: 'accent' | 'warm';
502
365
  };
503
366
  /**
504
- * El ancho del indicador cambia, no se anima: el sistema no anima escala ni
505
- * desplazamiento. `transition-standard` solo cubre color y borde, así que el
506
- * salto de ancho es inmediato aunque la clase esté puesta.
367
+ * The indicator's width changes, it is not animated: the system animates neither
368
+ * scale nor displacement. `transition-standard` only covers color and border, so
369
+ * the width jump is immediate even with the class in place.
507
370
  */
508
371
  declare function Progress({ className, value, label, tone, ...props }: ProgressProps): react.JSX.Element;
509
372
 
@@ -516,7 +379,7 @@ declare const Select: react.FC<SelectPrimitive.SelectProps>;
516
379
  declare const SelectGroup: react.ForwardRefExoticComponent<SelectPrimitive.SelectGroupProps & react.RefAttributes<HTMLDivElement>>;
517
380
  declare const SelectValue: react.ForwardRefExoticComponent<SelectPrimitive.SelectValueProps & react.RefAttributes<HTMLSpanElement>>;
518
381
  declare function SelectTrigger({ className, children, ...props }: ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>): react.JSX.Element;
519
- /** Sin animación de entrada: el menú aparece, no se despliega. */
382
+ /** No entrance animation: the menu appears, it does not unfold. */
520
383
  declare function SelectContent({ className, children, position, ...props }: ComponentPropsWithoutRef<typeof SelectPrimitive.Content>): react.JSX.Element;
521
384
  declare function SelectLabel({ className, ...props }: ComponentPropsWithoutRef<typeof SelectPrimitive.Label>): react.JSX.Element;
522
385
  declare function SelectItem({ className, children, ...props }: ComponentPropsWithoutRef<typeof SelectPrimitive.Item>): react.JSX.Element;
@@ -524,8 +387,8 @@ declare function SelectSeparator({ className, ...props }: ComponentPropsWithoutR
524
387
 
525
388
  type SeparatorProps = ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>;
526
389
  /**
527
- * `hairline`, no `border`: una división entre contenidos es sutil por
528
- * definición. Para delimitar un control existe `border`, que es otro token.
390
+ * `hairline`, not `border`: a division between pieces of content is subtle by
391
+ * definition. To delimit a control there is `border`, which is another token.
529
392
  */
530
393
  declare function Separator({ className, orientation, decorative, ...props }: SeparatorProps): react.JSX.Element;
531
394
 
@@ -533,14 +396,14 @@ declare const Sheet: react.FC<DialogPrimitive.DialogProps>;
533
396
  declare const SheetTrigger: react.ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & react.RefAttributes<HTMLButtonElement>>;
534
397
  declare const SheetClose: react.ForwardRefExoticComponent<DialogPrimitive.DialogCloseProps & react.RefAttributes<HTMLButtonElement>>;
535
398
  /**
536
- * La segunda y última excepción a «nada de desplazamiento», aprobada a
537
- * sabiendas: un panel que entra desde un borde se desliza por definición, y
538
- * quieto sería un modal descentrado.
539
- *
540
- * Dura `--duration-standard` con `--ease-standard`, o sea lo mismo y con la
541
- * misma curva que cualquier cambio de color del sistema, así que no introduce
542
- * un tiempo nuevo. Va detrás de `motion-safe`: quien pidió menos movimiento lo
543
- * ve aparecer sin deslizarse.
399
+ * The second and last exception to «no displacement», approved knowingly: a
400
+ * panel entering from an edge slides by definition, and held still it would be
401
+ * an off-centre modal.
402
+ *
403
+ * It lasts `--duration-standard` with `--ease-standard`, that is, the same time
404
+ * and the same curve as any color change in the system, so it introduces no new
405
+ * timing. It sits behind `motion-safe`: whoever asked for less motion sees it
406
+ * appear without sliding.
544
407
  */
545
408
  declare const panel: (props?: ({
546
409
  side?: "bottom" | "left" | "right" | "top" | null | undefined;
@@ -554,35 +417,36 @@ declare function SheetTitle({ className, ...props }: ComponentPropsWithoutRef<ty
554
417
  declare function SheetDescription({ className, ...props }: ComponentPropsWithoutRef<typeof DialogPrimitive.Description>): react.JSX.Element;
555
418
 
556
419
  /**
557
- * Barrido de 1.4s lineal, del documento.
420
+ * A 1.4s linear sweep, from the document.
558
421
  *
559
- * Es la TERCERA y última excepción a «el sistema no anima», junto al spinner del
560
- * botón y el panel lateral. Las tres son realimentación de PROGRESO y no de
561
- * estado, que es el criterio: un bloque quieto y un bloque que nunca va a cargar
562
- * se ven exactamente igual, y el skeleton existe para decir «esto viene en
563
- * camino», no «esto está vacío».
422
+ * It is the THIRD and last exception to «the system does not animate», alongside
423
+ * the button spinner and the side panel. All three are feedback about PROGRESS
424
+ * and not about state, which is the criterion: a block that is still and a block
425
+ * that will never load look exactly the same, and the skeleton exists to say
426
+ * «this is on its way», not «this is empty».
564
427
  *
565
- * Va detrás de `motion-safe`, así que se apaga solo para quien pidió menos
566
- * movimiento — y ahí queda el bloque en `surfaceRaised`, que sigue comunicando
567
- * la forma de lo que va a llegar.
428
+ * It sits behind `motion-safe`, so it switches itself off for anyone who asked
429
+ * for less motion — and what is left is the block on `surfaceRaised`, which
430
+ * still communicates the shape of what is coming.
568
431
  *
569
- * `still` lo apaga a mano, para las tablas largas: veinte filas barriendo a la
570
- * vez es un estroboscopio, no una carga.
432
+ * `still` turns it off by hand, for long tables: twenty rows sweeping at once is
433
+ * a strobe, not a load.
571
434
  */
572
435
  type SkeletonProps = ComponentPropsWithoutRef<'div'> & {
573
- /** Apaga el barrido. Para listas largas, donde muchas a la vez marean. */
436
+ /** Turns the sweep off. For long lists, where many at once are dizzying. */
574
437
  still?: boolean | undefined;
575
438
  };
576
439
  declare function Skeleton({ className, still, ...props }: SkeletonProps): react.JSX.Element;
577
440
 
578
441
  type SwitchProps = ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>;
579
442
  /**
580
- * La perilla cambia de posición, pero no se anima al hacerlo: la posición es el
581
- * estado, no una transición. Lo único que transiciona es el color de la vía.
443
+ * The knob changes position, but is not animated while doing so: the position IS
444
+ * the state, not a transition. The only thing that transitions is the track's
445
+ * color.
582
446
  */
583
447
  declare function Switch({ className, ...props }: SwitchProps): react.JSX.Element;
584
448
 
585
- /** El contenedor scrollea en horizontal: la página nunca lo hace. */
449
+ /** The container scrolls horizontally: the page never does. */
586
450
  declare function Table({ className, ...props }: ComponentPropsWithoutRef<'table'>): react.JSX.Element;
587
451
  declare function TableHeader({ className, ...props }: ComponentPropsWithoutRef<'thead'>): react.JSX.Element;
588
452
  declare function TableBody({ className, ...props }: ComponentPropsWithoutRef<'tbody'>): react.JSX.Element;
@@ -605,69 +469,72 @@ declare function Textarea({ className, invalid, ...props }: TextareaProps): reac
605
469
 
606
470
  declare const ToastAction: react.ForwardRefExoticComponent<ToastPrimitive.ToastActionProps & react.RefAttributes<HTMLButtonElement>>;
607
471
  declare const toast$1: (props?: ({
608
- variant?: "error" | "success" | "neutral" | null | undefined;
472
+ variant?: "error" | "neutral" | "success" | null | undefined;
609
473
  } & class_variance_authority_types.ClassProp) | undefined) => string;
610
474
  type ToastProps = ComponentPropsWithoutRef<typeof ToastPrimitive.Root> & VariantProps<typeof toast$1>;
611
475
 
612
476
  /**
613
- * La cara imperativa del `Toast` de Radix: `toast('Guardado')` desde cualquier
614
- * sitio, sin pasar el aviso por props hasta el componente que lo dispara.
615
- *
616
- * Existe porque dos proyectos traían `sonner` para esto. `Toast` cubre el mismo
617
- * rol y tiene otra forma: Radix es declarativo con proveedor, y para mostrar un
618
- * aviso desde el `catch` de un `fetch` hay que subir estado hasta donde vive el
619
- * proveedor. Eso es exactamente lo que `sonner` evita, y es una necesidad real,
620
- * no una preferencia de API.
621
- *
622
- * La alternativa era que los dos proyectos se adaptaran. Se descartó: el aviso
623
- * lo dispara la capa de datos, que no tiene —ni debería tener— un componente
624
- * cerca al que subirle un `useState`.
625
- *
626
- * Lo que NO se copió de `sonner` es el catálogo entero. No hay `toast.promise`,
627
- * ni `toast.custom`, ni posiciones configurables, ni apilado con perspectiva:
628
- * son cuatro variantes de lo mismo y cada una es superficie pública que hay que
629
- * mantener. Están las tres formas que los proyectos usan de verdad —neutral,
630
- * éxito, error—, `dismiss` y nada más.
631
- *
632
- * El estado vive en un módulo, no en un contexto, porque el punto es que se
633
- * pueda llamar desde fuera del árbol. `Toaster` se suscribe con
634
- * `useSyncExternalStore`, que es la forma que React 19 tiene de leer un estado
635
- * externo sin efectos ni renders en cascada.
477
+ * The imperative face of Radix's `Toast`: `toast('Guardado')` from anywhere,
478
+ * without threading the notice through props down to the component that fires
479
+ * it.
480
+ *
481
+ * It exists because two projects were pulling in `sonner` for this. `Toast`
482
+ * covers the same role and has a different shape: Radix is declarative with a
483
+ * provider, and to show a notice from a `fetch`'s `catch` you have to lift state
484
+ * up to where the provider lives. That is exactly what `sonner` avoids, and it
485
+ * is a real need, not an API preference.
486
+ *
487
+ * The alternative was for the two projects to adapt. It was ruled out: the
488
+ * notice is fired by the data layer, which has no component nearby to hang a
489
+ * `useState` on — nor should it.
490
+ *
491
+ * What was NOT copied from `sonner` is the whole catalogue. There is no
492
+ * `toast.promise`, no `toast.custom`, no configurable positions, no stacking
493
+ * with perspective: they are four variations on the same thing and each one is
494
+ * public surface that has to be maintained. What is here are the three shapes
495
+ * the projects actually use — neutral, success, error — plus `dismiss`, and
496
+ * nothing else.
497
+ *
498
+ * State lives in a module, not in a context, because the whole point is being
499
+ * callable from outside the tree. `Toaster` subscribes with
500
+ * `useSyncExternalStore`, which is React 19's way of reading external state
501
+ * without effects or cascading renders.
636
502
  */
637
503
  type ToastVariant = NonNullable<ToastProps['variant']>;
638
504
  type ToastOptions = {
639
- /** La primera línea, en negrita. Sin ella el aviso es una sola frase. */
505
+ /** The first line, in bold. Without it the notice is a single sentence. */
640
506
  title?: ReactNode;
641
507
  description?: ReactNode;
642
508
  variant?: ToastVariant;
643
- /** Milisegundos en pantalla. `Infinity` lo deja hasta que se cierre a mano. */
509
+ /** Milliseconds on screen. `Infinity` leaves it until it is closed by hand. */
644
510
  duration?: number;
645
- /** Un `ToastAction`, si el aviso ofrece deshacer. */
511
+ /** A `ToastAction`, if the notice offers an undo. */
646
512
  action?: ReactNode;
647
513
  };
648
- type Lanzador = {
649
- (mensaje: ReactNode, opciones?: ToastOptions): string;
650
- success: (mensaje: ReactNode, opciones?: ToastOptions) => string;
651
- error: (mensaje: ReactNode, opciones?: ToastOptions) => string;
514
+ type Trigger = {
515
+ (message: ReactNode, options?: ToastOptions): string;
516
+ success: (message: ReactNode, options?: ToastOptions) => string;
517
+ error: (message: ReactNode, options?: ToastOptions) => string;
652
518
  dismiss: (id?: string) => void;
653
519
  };
654
520
  /**
655
- * Lanza un aviso. Devuelve su id, que es lo que hay que guardar para cerrarlo a
656
- * mano —el caso de «guardando…» que se reemplaza cuando termina la petición.
521
+ * Fires a notice. It returns its id, which is what you keep in order to close it
522
+ * by hand — the «guardando…» case that gets replaced when the request finishes.
657
523
  */
658
- declare const toast: Lanzador;
524
+ declare const toast: Trigger;
659
525
  type ToasterProps = {
660
- /** Cuánto dura un aviso que no dice lo contrario. */
526
+ /** How long a notice lasts when it does not say otherwise. */
661
527
  duration?: number;
662
528
  /**
663
- * Nombre del landmark que Radix crea para la región de avisos. Se traduce
664
- * porque lo lee un lector de pantalla, y el default de Radix está en inglés.
529
+ * The name of the landmark Radix creates for the notices region. It is
530
+ * translated because a screen reader reads it, and Radix's default is in
531
+ * English.
665
532
  */
666
533
  label?: string;
667
534
  };
668
535
  /**
669
- * Va UNA vez, lo más arriba posible del árbol. Dos `Toaster` montados pintan
670
- * cada aviso dos veces: la lista es del módulo, no de la instancia.
536
+ * Mount it ONCE, as high in the tree as possible. Two mounted `Toaster`s paint
537
+ * every notice twice: the list belongs to the module, not to the instance.
671
538
  */
672
539
  declare function Toaster({ duration, label }: ToasterProps): react.JSX.Element;
673
540
 
@@ -676,126 +543,148 @@ declare const Tooltip: react.FC<TooltipPrimitive.TooltipProps>;
676
543
  declare const TooltipTrigger: react.ForwardRefExoticComponent<TooltipPrimitive.TooltipTriggerProps & react.RefAttributes<HTMLButtonElement>>;
677
544
  declare function TooltipContent({ className, sideOffset, ...props }: ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>): react.JSX.Element;
678
545
 
679
- declare const texto: (props?: ({
680
- variant?: "display" | "h2" | "h3" | "label" | "body" | "h1" | "meta" | "stat" | "lead" | "ui" | "tag" | "chip" | "eyebrow" | null | undefined;
681
- tone?: "error" | "accent" | "success" | "warning" | "primary" | "secondary" | "warm" | "muted" | null | undefined;
682
- } & class_variance_authority_types.ClassProp) | undefined) => string;
683
- /** Etiquetas admitidas. La lista es corta a propósito: no es un `div` con estilo. */
684
- type Etiqueta = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'span' | 'strong' | 'em' | 'figcaption' | 'caption' | 'legend' | 'dt' | 'dd' | 'li';
685
- type TextProps = Omit<ComponentPropsWithoutRef<'p'>, 'color'> & VariantProps<typeof texto> & {
686
- /** Etiqueta HTML. Por defecto, la que corresponde a la escala. */
687
- as?: Etiqueta;
688
- /** Renderiza el hijo en vez de crear un elemento, para envolver un enlace. */
546
+ /** Allowed tags. The list is short on purpose: this is not a styled `div`. */
547
+ type Label = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'span' | 'strong' | 'em' | 'figcaption' | 'caption' | 'legend' | 'dt' | 'dd' | 'li';
548
+ type TextProps = Omit<ComponentPropsWithoutRef<'p'>, 'color'> & VariantProps<typeof text> & {
549
+ /** HTML tag. Defaults to whichever one matches the scale. */
550
+ as?: Label;
551
+ /** Renders the child instead of creating an element, to wrap a link. */
689
552
  asChild?: boolean;
690
553
  /**
691
- * Corta la línea a 68ch. Activo por defecto en `body`, que es la única
692
- * escala pensada para leerse en párrafos largos.
554
+ * Clamps the line to 68ch. On by default for `body`, the only scale meant
555
+ * to be read in long paragraphs.
693
556
  */
694
557
  measure?: boolean;
695
558
  };
696
559
  declare function Text({ className, variant, tone, as, asChild, measure, children, ...props }: TextProps): react.JSX.Element;
697
560
 
698
- type TarjetaProps = ComponentPropsWithoutRef<'a'> & {
561
+ /**
562
+ * The shared shell of the cards. The class list lives in `variants/card.ts`,
563
+ * which brings no React, so `links` can read it instead of copying it.
564
+ */
565
+ type CardShellProps = ComponentPropsWithoutRef<'a'> & {
699
566
  /**
700
- * Renderiza el hijo en vez de un `<a>`. Es como se enchufa el `Link` de Next
701
- * o de Astro sin que la librería dependa de ningún enrutador.
567
+ * Renders the child instead of an `<a>`. It is how Next's or Astro's `Link`
568
+ * plugs in without the library depending on any router.
702
569
  */
703
570
  asChild?: boolean | undefined;
704
571
  children: ReactNode;
705
572
  };
706
573
 
707
- type ArticleCardProps = Omit<TarjetaProps, 'children' | 'title'> & {
574
+ type ArticleCardProps = Omit<CardShellProps, 'children' | 'title'> & {
708
575
  title: ReactNode;
709
- /** Entradilla. Se corta a dos líneas para que la rejilla no se desalinee. */
576
+ /** Standfirst. Clamped to two lines so the grid does not fall out of line. */
710
577
  excerpt?: ReactNode;
711
- /** Fecha ya formateada por el proyecto: la librería no impone locale. */
578
+ /** Date already formatted by the project: the library imposes no locale. */
712
579
  date?: ReactNode;
713
- /** Valor de `datetime` del `<time>`, en ISO. */
580
+ /** The `<time>` element's `datetime` value, in ISO. */
714
581
  dateTime?: string | undefined;
715
582
  readingMinutes?: number | undefined;
716
583
  tags?: readonly string[] | undefined;
717
584
  /**
718
- * Nivel del titular. `h3` por defecto: una tarjeta suelta en una rejilla no
719
- * gana el nivel que su posición no le da.
585
+ * The headline level. `h3` by default: a lone card in a grid does not earn a
586
+ * level its position does not give it.
720
587
  *
721
- * Acotado a dos valores a propósito. La página de listado —donde las tarjetas
722
- * SÍ son el encabezado principal de la sección— necesita `h2`, y esa
723
- * intención se perdía con la constante; abrirlo hasta `h4` o `h5`, en cambio,
724
- * es invitar a saltarse niveles, que es el fallo que la constante evitaba.
588
+ * Restricted to two values on purpose. The listing page — where the cards ARE
589
+ * the section's main heading — needs `h2`, and that intent was lost with a
590
+ * constant; opening it up to `h4` or `h5`, on the other hand, is an invitation
591
+ * to skip levels, which is the failure the constant was preventing.
725
592
  */
726
593
  headingLevel?: 2 | 3;
594
+ /**
595
+ * Renders each tag through the child, so an E2E suite can reach it.
596
+ *
597
+ * The tags are the one composed part a project could not get at: they arrive
598
+ * as strings and the component turns them into badges, so a test had to select
599
+ * them by structure — `article > div > span` — or by a style class. The second
600
+ * one already broke the blog's suite once, because a style class is not a
601
+ * contract: it changes when the style changes.
602
+ *
603
+ * The library keeps the classes and the rule — a tag is a category pill, and
604
+ * that does not become negotiable — and hands over only the element and its
605
+ * attributes:
606
+ *
607
+ * tagAsChild={({ tag }) => <span data-testid={`tag-${tag}`}>{tag}</span>}
608
+ *
609
+ * It is the shape `linkAsChild` already has in `Breadcrumb` and
610
+ * `TableOfContents`, and it is deliberately the same: one idiom for «the
611
+ * project supplies the element, the library supplies the styling».
612
+ */
613
+ tagAsChild?: ((props: {
614
+ tag: string;
615
+ children: ReactNode;
616
+ }) => ReactNode) | undefined;
727
617
  };
728
618
  /**
729
- * La línea de metadatos va en `meta` y no en `eyebrow`: `18 ago 2026 · 8 min de
730
- * lectura` es un dato, no un antetítulo, y en versalitas no era ninguna de las
731
- * dos cosas.
619
+ * The metadata line uses `meta` and not `eyebrow`: `18 ago 2026 · 8 min de
620
+ * lectura` is a datum, not an overline, and in small caps it was neither.
732
621
  *
733
- * Los tags son la familia CATEGORÍA — píldora de arena en minúscula —, no la de
734
- * estado. Un slug es lo que se lee `engineering-culture`.
622
+ * The tags are the CATEGORY family — a lowercase sand pill — not the status one.
623
+ * A slug is something you read as `engineering-culture`.
735
624
  */
736
- declare function ArticleCard({ title, excerpt, date, dateTime, readingMinutes, tags, headingLevel, className, ...props }: ArticleCardProps): react.JSX.Element;
625
+ declare function ArticleCard({ title, excerpt, date, dateTime, readingMinutes, tags, headingLevel, tagAsChild, className, ...props }: ArticleCardProps): react.JSX.Element;
737
626
 
738
627
  /**
739
- * La firma al pie del artículo: avatar 52px, nombre 15/500 y el rol en mono
740
- * muted. Tres datos, ni uno más.
628
+ * The byline at the foot of the article: 52px avatar, 15/500 name and the role
629
+ * in muted mono. Three data points, not one more.
741
630
  *
742
- * NO recibe cara de mascota, y no es un descuido: es la misma regla que
743
- * `PageHeader`. Una cara aquí sería humor en el sitio donde el lector está
744
- * decidiendo si el autor sabe de lo que habla — justo lo que el contrato del
745
- * manual excluye.
631
+ * It does NOT take a mascot face, and that is not an oversight: it is the same
632
+ * rule as `PageHeader`. A face here would be humour in the exact place where the
633
+ * reader is deciding whether the author knows what they are talking about —
634
+ * precisely what the manual's contract excludes.
746
635
  *
747
- * El avatar del manual es la cabeza dentro de un círculo azul tiburón sólido,
748
- * nunca la cara expresiva suelta.
636
+ * The manual's avatar is the head inside a solid shark-blue circle, never the
637
+ * expressive face on its own.
749
638
  */
750
639
  type AuthorCardProps = Omit<ComponentPropsWithoutRef<'div'>, 'role'> & {
751
640
  name: string;
752
- /** El rol. Va en mono: es un dato, no una frase. */
641
+ /** The role. It goes in mono: it is a datum, not a sentence. */
753
642
  role?: ReactNode;
754
- /** URL del avatar. Sin ella se muestran las iniciales. */
643
+ /** The avatar's URL. Without it the initials are shown. */
755
644
  src?: string | undefined;
756
- /** Una o dos frases. Se corta a 68ch sola. */
645
+ /** One or two sentences. It clamps itself to 68ch. */
757
646
  bio?: ReactNode;
758
- /** Enlaces o botón de contacto. */
647
+ /** Links or a contact button. */
759
648
  action?: ReactNode;
760
649
  };
761
650
  declare function AuthorCard({ name, role, src, bio, action, className, ...props }: AuthorCardProps): react.JSX.Element;
762
651
 
763
652
  /**
764
- * Migrado desde `eduardoalvarez.dev/src/components/audio-player/index.tsx`.
653
+ * Migrated from `eduardoalvarez.dev/src/components/audio-player/index.tsx`.
765
654
  *
766
- * La lógica no se reescribió: los tres modos, el reproductor flotante, los
767
- * saltos de ±15s, el ciclo de velocidad 1 → 1.25 → 1.5 → 1.75 → 2 y el volumen
768
- * con mute son los mismos. Lo que cambió es la piel y las dos dependencias que
769
- * un paquete no puede tener:
655
+ * The logic was not rewritten: the three modes, the floating player, the ±15s
656
+ * skips, the 1 → 1.25 → 1.5 → 1.75 → 2 speed cycle and the volume with mute are
657
+ * the same. What changed is the skin and the two dependencies a package cannot
658
+ * have:
770
659
  *
771
- * - `Icon` del portafolio → los glifos viven ahora en `src/lib/glyphs.tsx`,
772
- * con los mismos trazados.
773
- * - `trackEvent` de analítica → la prop `onFirstPlay`, que el consumidor
774
- * conecta a lo que use. Sigue disparándose una sola vez por carga.
660
+ * - the portfolio's `Icon` → the glyphs now live in `src/lib/glyphs.tsx`, with
661
+ * the same paths.
662
+ * - analytics' `trackEvent` → the `onFirstPlay` prop, which the consumer wires
663
+ * to whatever they use. It still fires exactly once per load.
775
664
  *
776
- * Y tres cosas que el sistema no permite:
665
+ * And three things the system does not allow:
777
666
  *
778
- * - La onda ya no anima `scaleY`. Las barras siguen ahí y siguen distinguiendo
779
- * reproducción de pausa por opacidad, pero no escalan.
780
- * - El reproductor flotante aparece y desaparece en vez de deslizarse.
781
- * - La barra de progreso ya no interpola el ancho.
667
+ * - The waveform no longer animates `scaleY`. The bars are still there and still
668
+ * tell playback from pause by opacity, but they do not scale.
669
+ * - The floating player appears and disappears instead of sliding.
670
+ * - The progress bar no longer interpolates its width.
782
671
  *
783
- * El giro del spinner de carga se queda, con la misma justificación que en
784
- * `Button`: es realimentación de progreso, no de estado, y va en `motion-safe`.
672
+ * The loading spinner's spin stays, with the same justification as in `Button`:
673
+ * it is feedback about progress, not about state, and it sits in `motion-safe`.
785
674
  */
786
675
  type AudioPlayerMode = 'full' | 'compact' | 'banner';
787
676
  type AudioPlayerProps = {
788
677
  src: string;
789
678
  title?: string;
790
679
  /**
791
- * `full` para páginas de podcast, `compact` para barras laterales y `banner`
792
- * para artículos con narración. `compact` y `banner` traen además el
793
- * reproductor flotante cuando el estático sale de vista.
680
+ * `full` for podcast pages, `compact` for sidebars and `banner` for articles
681
+ * with narration. `compact` and `banner` also bring the floating player when
682
+ * the static one leaves the viewport.
794
683
  */
795
684
  mode?: AudioPlayerMode | undefined;
796
685
  /**
797
- * Se llama una sola vez por carga, la primera vez que el audio arranca.
798
- * Aquí es donde el proyecto engancha su analítica; la librería no la trae.
686
+ * Called once per load, the first time the audio starts.
687
+ * This is where the project hooks up its analytics; the library ships none.
799
688
  */
800
689
  onFirstPlay?: ((title?: string) => void) | undefined;
801
690
  };
@@ -803,44 +692,44 @@ declare function AudioPlayer({ src, title, mode, onFirstPlay }: AudioPlayerProps
803
692
 
804
693
  type BlockquoteProps = Omit<ComponentPropsWithoutRef<'blockquote'>, 'cite'> & {
805
694
  children: ReactNode;
806
- /** Quién lo dijo. Se marca como `<cite>`. */
695
+ /** Who said it. Marked up as `<cite>`. */
807
696
  author?: ReactNode;
808
- /** Dónde lo dijo: charla, artículo, conversación. */
697
+ /** Where they said it: a talk, an article, a conversation. */
809
698
  source?: ReactNode;
810
699
  };
811
700
  /**
812
- * La barra lateral es `accent`, que es el color interactivo, porque una cita es
813
- * la voz de otro entrando en el texto. No lleva comillas decorativas: los
814
- * glifos del sistema son SVG y una comilla de adorno no aporta nada que el
815
- * borde y la sangría no digan ya.
701
+ * The side bar is `accent`, the interactive color, because a quotation is
702
+ * somebody else's voice entering the text. It carries no decorative quote marks:
703
+ * the system's glyphs are SVG, and an ornamental quote adds nothing the border
704
+ * and the indent do not already say.
816
705
  */
817
706
  declare function Blockquote({ children, author, source, className, ...props }: BlockquoteProps): react.JSX.Element;
818
707
 
819
708
  /**
820
- * La ruta como una ruta: `~ / artículos / cómo-escalar-un-equipo`.
709
+ * The path as a path: `~ / artículos / cómo-escalar-un-equipo`.
821
710
  *
822
- * El `~` no es decoración ni un icono de casa: es el home del sistema de
823
- * archivos, y por eso el breadcrumb es mono y no sans. Los separadores van en
824
- * `border` (#22414F), que es el token más tenue que sigue leyéndose como línea.
711
+ * The `~` is neither decoration nor a house icon: it is the filesystem's home,
712
+ * which is why the breadcrumb is mono and not sans. The separators use `border`
713
+ * (#22414F), the faintest token that still reads as a line.
825
714
  *
826
- * El último tramo es la página actual, así que no es un enlace y lleva
827
- * `aria-current="page"`. Es la diferencia entre una miga de pan accesible y
828
- * cuatro enlaces seguidos, uno de los cuales no va a ninguna parte.
715
+ * The last crumb is the current page, so it is not a link and carries
716
+ * `aria-current="page"`. That is the difference between an accessible breadcrumb
717
+ * and four consecutive links, one of which goes nowhere.
829
718
  */
830
- type Migaja = {
719
+ type Crumb = {
831
720
  label: ReactNode;
832
- /** Sin `href`, el tramo es texto. El último nunca debería llevarlo. */
721
+ /** With no `href`, the crumb is text. The last one should never carry it. */
833
722
  href?: string | undefined;
834
723
  };
835
724
  type BreadcrumbProps = Omit<ComponentPropsWithoutRef<'nav'>, 'children'> & {
836
- items: readonly Migaja[];
837
- /** Destino del `~`. Por defecto, la raíz del sitio. */
725
+ items: readonly Crumb[];
726
+ /** Where the `~` goes. The site root by default. */
838
727
  homeHref?: string;
839
- /** Etiqueta accesible del `~`, que si no se lee como una tilde suelta. */
728
+ /** Accessible label for the `~`, which otherwise reads as a stray tilde. */
840
729
  homeLabel?: string;
841
730
  /**
842
- * Renderiza los enlaces con el hijo, para enchufar el `Link` de Next o Astro.
843
- * Recibe cada `href` en el `props` del Slot.
731
+ * Renders the links through the child, to plug in Next's or Astro's `Link`.
732
+ * It receives each `href` in the Slot's `props`.
844
733
  */
845
734
  linkAsChild?: ((props: {
846
735
  href: string;
@@ -850,148 +739,149 @@ type BreadcrumbProps = Omit<ComponentPropsWithoutRef<'nav'>, 'children'> & {
850
739
  declare function Breadcrumb({ items, homeHref, homeLabel, linkAsChild, className, ...props }: BreadcrumbProps): react.JSX.Element;
851
740
 
852
741
  /**
853
- * `brand.hull` es «casco · contorno y fondo de bloques de código», así que un
854
- * bloque de código es oscuro también en modo claro. Por eso la raíz declara
855
- * `data-theme="dark"`: todo lo de dentro — tinta, hairline, acento — pasa a la
856
- * paleta oscura sin importar el tema de la página. Es la única isla de tema
857
- * invertido del sistema, y es deliberada.
742
+ * `brand.hull` is «hull · outline and the background of code blocks», so a code
743
+ * block is dark in light mode too. That is why the root declares
744
+ * `data-theme="dark"`: everything inside — ink, hairline, accent — switches to
745
+ * the dark palette regardless of the page's theme. It is the system's only
746
+ * island of inverted theme, and it is deliberate.
858
747
  */
859
748
  type CodeBlockProps = Omit<ComponentPropsWithoutRef<'div'>, 'children'> & {
860
- /** El código ya resaltado, o texto plano. */
749
+ /** The already-highlighted code, or flat text. */
861
750
  children: ReactNode;
862
- /** Etiqueta del lenguaje. Se muestra en la barra superior. */
751
+ /** The language label. Shown in the top bar. */
863
752
  language?: string | undefined;
864
- /** Texto que se copia al portapapeles. Sin esto, no se muestra el botón. */
753
+ /** The text copied to the clipboard. Without it, the button is not shown. */
865
754
  copyText?: string | undefined;
866
755
  };
867
756
  declare function CodeBlock({ children, language, copyText, className, ...props }: CodeBlockProps): react.JSX.Element;
868
757
 
869
- type CourseCardProps = Omit<TarjetaProps, 'children' | 'title'> & {
758
+ type CourseCardProps = Omit<CardShellProps, 'children' | 'title'> & {
870
759
  title: ReactNode;
871
760
  summary?: ReactNode;
872
- /** Nivel, duración, número de lecciones: lo que el proyecto quiera listar. */
761
+ /** Level, duration, number of lessons: whatever the project wants to list. */
873
762
  meta?: readonly ReactNode[] | undefined;
874
- /** Etiqueta de estado: «próximamente», «gratis», «nuevo». */
763
+ /** Status label: «próximamente», «gratis», «nuevo». */
875
764
  status?: ReactNode;
876
765
  /**
877
- * Porcentaje cursado. Solo tiene sentido para quien ya está inscrito; cuando
878
- * se pasa, la barra va en arena, que es el color del progreso de curso.
766
+ * Percentage completed. It only makes sense for someone already enrolled;
767
+ * when passed, the bar goes in sand, which is the color of course progress.
879
768
  */
880
769
  progress?: number | undefined;
881
770
  };
882
771
  declare function CourseCard({ title, summary, meta, status, progress, className, ...props }: CourseCardProps): react.JSX.Element;
883
772
 
884
773
  /**
885
- * La regla más importante de la mascota, por fin como código.
774
+ * The mascot's most important rule, finally as code.
886
775
  *
887
- * `catalogo.ts` y `page-header/index.tsx` la citaban los dos —«por eso
888
- * `EmptyState` recibe una cara y `PageHeader` no»— y el componente no existía,
889
- * así que la regla vivía en un comentario sobre un componente fantasma. Desde
890
- * que la story de marca repite la frase, además, era una promesa publicada.
776
+ * `catalog.ts` and `page-header/index.tsx` both cited it — «that is why
777
+ * `EmptyState` takes a face and `PageHeader` does not» — and the component did
778
+ * not exist, so the rule lived in a comment about a ghost component. Once the
779
+ * brand story started repeating the sentence, it was also a published promise.
891
780
  *
892
- * `expresion` es OBLIGATORIA y no opcional: un estado vacío sin cara es la mitad
893
- * del componente. Es el único sitio, junto con el 404, el error de servidor, el
894
- * progreso de curso, la celebración, el toast y el «sin spam» del newsletter,
895
- * donde una cara puede aparecer.
781
+ * `expression` is MANDATORY and not optional: an empty state without a face is
782
+ * half the component. It is the only place, alongside the 404, the server error,
783
+ * course progress, celebration, the toast and the newsletter's «sin spam», where
784
+ * a face may appear.
896
785
  */
897
786
  type EmptyStateProps = Omit<ComponentPropsWithoutRef<'div'>, 'title'> & {
898
- /** La cara. Obligatoria: sin ella esto es un párrafo centrado. */
899
- expresion: Cara;
787
+ /** The face. Mandatory: without it this is a centred paragraph. */
788
+ expression: Face;
900
789
  title: ReactNode;
901
- /** Una línea explicando qué falta o qué hacer. */
790
+ /** One line explaining what is missing or what to do. */
902
791
  description?: ReactNode;
903
- /** La acción que saca del estado vacío. Normalmente un botón terciario. */
792
+ /** The action that gets you out of the empty state. Usually a tertiary button. */
904
793
  action?: ReactNode;
905
- /** Dónde se sirven los PNG de la marca. */
794
+ /** Where the brand PNGs are served from. */
906
795
  basePath?: string | undefined;
907
796
  };
908
- declare function EmptyState({ expresion, title, description, action, basePath, className, ...props }: EmptyStateProps): react.JSX.Element;
797
+ declare function EmptyState({ expression, title, description, action, basePath, className, ...props }: EmptyStateProps): react.JSX.Element;
909
798
 
910
799
  /**
911
- * El calendario con eventos: verlos, crearlos, editarlos y borrarlos.
800
+ * The calendar with events: seeing them, creating, editing and deleting them.
912
801
  *
913
- * `Calendar` es la primitiva y sigue siendo una rejilla de fechas: sirve para
914
- * ELEGIR un día y no sabe nada de contenido. Esto es la agenda, y por eso vive
915
- * en `components/` y no al lado de aquella — tiene estado, tiene formulario y
916
- * codifica cómo se ve un día con cosas dentro.
802
+ * `Calendar` is the primitive and remains a grid of dates: it serves to PICK a
803
+ * day and knows nothing about content. This is the schedule, which is why it
804
+ * lives in `components/` and not next to that one — it has state, it has a form
805
+ * and it encodes what a day with things in it looks like.
917
806
  *
918
- * Es presentacional, como el resto de la librería: recibe `events` y emite
919
- * `onCreateEvent`, `onUpdateEvent` y `onDeleteEvent`. No guarda nada, no llama a
920
- * ninguna API y no genera ids — el id lo pone quien persiste, porque es quien
921
- * sabe si viene de una base de datos o de un fichero.
807
+ * It is presentational, like the rest of the library: it takes `events` and
808
+ * emits `onCreateEvent`, `onUpdateEvent` and `onDeleteEvent`. It stores nothing,
809
+ * calls no API and generates no ids — the id is set by whoever persists,
810
+ * because they are the one who knows whether it comes from a database or a file.
922
811
  *
923
- * DOS COLUMNAS y no un popover sobre el día. El popover es lo que hace todo el
924
- * mundo y esconde el contenido detrás de un clic: con la lista al lado, un mes
925
- * con quince eventos se lee de un vistazo y el día seleccionado no tapa la
926
- * rejilla. En pantalla estrecha se apilan.
812
+ * TWO COLUMNS and not a popover over the day. The popover is what everyone does
813
+ * and it hides the content behind a click: with the list beside it, a month with
814
+ * fifteen events reads at a glance and the selected day does not cover the grid.
815
+ * On a narrow screen they stack.
927
816
  *
928
- * El borrado pasa por `AlertDialog` y no por un botón directo. Es exactamente el
929
- * caso para el que existe: una acción destructiva sin deshacer.
817
+ * Deleting goes through `AlertDialog` and not through a direct button. It is
818
+ * precisely the case it exists for: a destructive action with no undo.
930
819
  */
931
- type EventoCalendario = {
932
- /** Lo pone el proyecto. La librería nunca lo inventa. */
820
+ type CalendarEvent = {
821
+ /** Set by the project. The library never invents it. */
933
822
  id: string;
934
- /** Cuándo empieza, con hora. */
823
+ /** When it starts, with a time. */
935
824
  start: Date;
936
825
  title: string;
937
- /** `warm` cuando el evento es el problema, igual que en `Stat`. */
826
+ /** `warm` when the event is the problem, same as in `Stat`. */
938
827
  tone?: 'accent' | 'warm' | undefined;
939
828
  };
940
829
  type EventCalendarProps = Omit<ComponentPropsWithoutRef<'div'>, 'onSelect' | 'children'> & {
941
- events: readonly EventoCalendario[];
942
- /** Sin él, la agenda es de solo lectura y no se pinta el formulario. */
943
- onCreateEvent?: ((evento: Omit<EventoCalendario, 'id'>) => void) | undefined;
944
- onUpdateEvent?: ((evento: EventoCalendario) => void) | undefined;
830
+ events: readonly CalendarEvent[];
831
+ /** Without it, the schedule is read-only and the form is not painted. */
832
+ onCreateEvent?: ((event: Omit<CalendarEvent, 'id'>) => void) | undefined;
833
+ onUpdateEvent?: ((event: CalendarEvent) => void) | undefined;
945
834
  onDeleteEvent?: ((id: string) => void) | undefined;
946
- /** Día seleccionado, si el proyecto lo controla. Sin él, empieza en hoy. */
835
+ /** Selected day, if the project controls it. Without it, it starts on today. */
947
836
  selected?: Date | undefined;
948
- onSelectDay?: ((dia: Date) => void) | undefined;
949
- /** Encabezado del panel. Por defecto, `es` de date-fns, como `Calendar`. */
950
- formatDay?: ((dia: Date) => string) | undefined;
951
- formatTime?: ((fecha: Date) => string) | undefined;
952
- /** Texto del panel cuando el día elegido no tiene nada. */
837
+ onSelectDay?: ((day: Date) => void) | undefined;
838
+ /** The panel's heading. Defaults to date-fns' `es`, like `Calendar`. */
839
+ formatDay?: ((day: Date) => string) | undefined;
840
+ formatTime?: ((date: Date) => string) | undefined;
841
+ /** The panel's text when the chosen day has nothing on it. */
953
842
  emptyMessage?: ReactNode;
954
843
  };
955
844
  declare function EventCalendar({ events, onCreateEvent, onUpdateEvent, onDeleteEvent, selected, onSelectDay, formatDay, formatTime, emptyMessage, className, ...props }: EventCalendarProps): react.JSX.Element;
956
845
 
957
846
  /**
958
- * El pie, y la firma CLI del sitio: `$ cd ~/eduardoalvarez.dev/2026`.
847
+ * The footer, and the site's CLI signature: `$ cd ~/eduardoalvarez.dev/2026`.
959
848
  *
960
- * El dominio sale de `naming.domain` y no de una cadena escrita a mano, por la
961
- * misma razón que el wordmark: si cambia, cambia en los cinco proyectos a la
962
- * vez.
849
+ * The domain comes from `naming.domain` and not from a hand-written string, for
850
+ * the same reason as the wordmark: if it changes, it changes in all five
851
+ * projects at once.
963
852
  *
964
- * Los enlaces de redes son iconos SIN texto visible, así que `aria-label` no es
965
- * una mejora: es lo único que los hace legibles. Por eso es obligatorio en el
966
- * tipo y no una prop opcional que se olvide.
853
+ * The social links are icons with NO visible text, so `aria-label` is not an
854
+ * improvement: it is the only thing that makes them legible. Which is why it is
855
+ * mandatory in the type and not an optional prop that gets forgotten.
967
856
  *
968
- * La firma va arriba a la derecha, al nivel de la PRIMERA fila que exista, no
969
- * al final del bloque. Es una decisión de composición y no de estilo: el pie
970
- * puede llevar marca, enlaces y redes, y colgar la firma de una fila concreta
971
- * la hunde en cuanto esa fila deja de ser la primera.
857
+ * The signature sits top right, level with the FIRST row that exists, not at the
858
+ * end of the block. That is a composition decision and not a styling one: the
859
+ * footer can carry brand, links and social icons, and hanging the signature off
860
+ * one specific row sinks it the moment that row stops being the first.
972
861
  */
973
- type Red = {
974
- /** Lo que reemplaza al texto visible. Obligatorio. */
862
+ type SocialLink = {
863
+ /** What replaces the visible text. Mandatory. */
975
864
  label: string;
976
865
  href: string;
977
866
  /**
978
- * El glifo, a 19px. Las marcas van en SÓLIDO (`fill`) y los iconos
979
- * funcionales en trazo de 1.6. Nunca un emoji.
867
+ * The glyph, at 19px. Brands are SOLID (`fill`) and functional icons use a 1.6
868
+ * stroke. Never an emoji.
980
869
  */
981
870
  icon: ReactNode;
982
871
  };
983
872
  type FooterProps = ComponentPropsWithoutRef<'footer'> & {
984
- social?: readonly Red[];
985
- /** Año de la firma. */
873
+ social?: readonly SocialLink[];
874
+ /** The signature's year. */
986
875
  year?: number;
987
- /** Enlaces de texto: aviso legal, RSS, mapa del sitio. */
876
+ /** Text links: legal notice, RSS, sitemap. */
988
877
  children?: ReactNode;
989
878
  /**
990
- * La fila de marca: la aleta y el wordmark, arriba del todo.
879
+ * The brand row: the fin and the wordmark, at the very top.
991
880
  *
992
- * Existe porque sin ella acababa metida en `children` con un `w-full` para
993
- * que se llevara su propia línea. Funcionaba y era un apaño: la marca no es
994
- * un enlace de texto más, y una ranura propia lo dice en el tipo.
881
+ * It exists because without it that row ended up inside `children` with a
882
+ * `w-full` so it would take a line of its own. It worked and it was a patch:
883
+ * the brand is not one more text link, and a slot of its own says so in the
884
+ * type.
995
885
  */
996
886
  brand?: ReactNode;
997
887
  };
@@ -1002,413 +892,503 @@ type FooterLinkProps = ComponentPropsWithoutRef<'a'> & {
1002
892
  declare function FooterLink({ asChild, className, ...props }: FooterLinkProps): react.JSX.Element;
1003
893
 
1004
894
  /**
1005
- * UNO por sitio. Es la única pieza del sistema que se gasta como el botón de
1006
- * conversión, y por la misma razón: si hay dos, no hay ninguno.
1007
- *
1008
- * Degradado, radio de panel, texto al 62 % del ancho y la pose sangrando por el
1009
- * borde inferior derecho. Eso es la variante `cabecera`, que es el defecto.
1010
- *
1011
- * En móvil no hay borde por el que sangrar, así que la pose baja al flujo, bajo
1012
- * los botones. No es un `hidden` en pantalla pequeña: la pose es el 40 % de la
1013
- * personalidad del hero.
1014
- *
1015
- * La otra variante es `centrado`, y existe porque la regla de arriba tiene un
1016
- * caso donde no aplica. «Nunca centrada» se escribió contra el hero de una
1017
- * página con más contenido debajo: ahí una mascota centrada bajo el titular es
1018
- * una ilustración de portada, no una cabecera. Pero una página de enlaces es
1019
- * centrada de extremo a extremo y la mascota es el protagonista, no el remate.
1020
- * Ese proyecto se saltaba `Hero` entero por esto, que es peor: una regla
1021
- * declarada y con nombre se discute; una copia del degradado en otro repo se
1022
- * desincroniza. La pose va ARRIBA del titular, no debajo, para que siga sin
1023
- * leerse como la ilustración que cierra un bloque de texto.
1024
- *
1025
- * El degradado viene de `--gradient-hero`, así que sigue el modo. No hay ángulo
1026
- * escrito a mano en ningún proyecto.
895
+ * ONE per site. It is the only piece in the system that is spent like the
896
+ * conversion button, and for the same reason: if there are two, there are none.
897
+ *
898
+ * Gradient, panel radius, text at 62 % of the width and the pose bleeding off
899
+ * the bottom-right corner. That is the `header` variant, which is the default.
900
+ *
901
+ * On mobile there is no edge to bleed off, so the pose drops into the flow,
902
+ * below the buttons. It is not a `hidden` on small screens: the pose is 40 % of
903
+ * the hero's personality.
904
+ *
905
+ * The other variant is `centered`, and it exists because the rule above has a
906
+ * case where it does not apply. «Never centred» was written against the hero of
907
+ * a page with more content below it: there, a centred mascot under the headline
908
+ * is a cover illustration, not a header. But a links page is centred end to end
909
+ * and the mascot is the protagonist, not the flourish. That project skipped
910
+ * `Hero` entirely over this, which is worse: a declared rule with a name can be
911
+ * argued with; a copy of the gradient in another repo just drifts. The pose goes
912
+ * ABOVE the headline, not below it, so it still does not read as the
913
+ * illustration closing a block of text.
914
+ *
915
+ * The gradient comes from `--gradient-hero`, so it follows the mode. There is no
916
+ * hand-written angle in any project.
1027
917
  */
1028
918
  type HeroProps = Omit<ComponentPropsWithoutRef<'section'>, 'title'> & {
1029
919
  title: ReactNode;
1030
- /** Mono, versalitas, en acento. */
920
+ /** Mono, small caps, in accent. */
1031
921
  eyebrow?: ReactNode;
1032
922
  description?: ReactNode;
1033
- /** Los botones. Aquí va el único `conversion` de la pantalla. */
923
+ /** The buttons. The screen's only `conversion` goes here. */
1034
924
  action?: ReactNode;
1035
- /** La pose de Tiburoncín. Sin ella el hero es un panel con texto. */
925
+ /** Tiburoncín's pose. Without it the hero is a panel with text. */
1036
926
  pose?: Pose | undefined;
1037
927
  basePath?: string | undefined;
1038
928
  /**
1039
- * `cabecera` sangra la pose por la esquina; `centrado` la pone arriba y
1040
- * centra el texto, para una página que es solo esto.
929
+ * `header` bleeds the pose off the corner; `centered` puts it on top and
930
+ * centres the text, for a page that is only this.
1041
931
  */
1042
- variant?: 'cabecera' | 'centrado';
932
+ variant?: 'header' | 'centered';
1043
933
  };
1044
934
  declare function Hero({ title, eyebrow, description, action, pose, basePath, variant, className, ...props }: HeroProps): react.JSX.Element;
1045
935
 
1046
- type LinkRowProps = Omit<TarjetaProps, 'children'> & {
936
+ type LinkRowProps = Omit<CardShellProps, 'children'> & {
1047
937
  name: ReactNode;
1048
938
  description?: ReactNode;
1049
- /** Glifo SVG del destino. Nunca un emoji. */
939
+ /** The target's SVG glyph. Never an emoji. */
1050
940
  icon?: ReactNode;
1051
- /** Marca el enlace como externo: añade la flecha y el `rel` seguro. */
941
+ /** Marks the link as external: adds the arrow and the safe `rel`. */
1052
942
  external?: boolean | undefined;
1053
943
  };
1054
944
  /**
1055
- * Migrado desde `links/src/components/Card.astro`. El original escalaba la
1056
- * tarjeta al 102 %, subía el título un píxel y giraba y agrandaba el icono en
1057
- * hover — cuatro movimientos que el sistema no permite. Aquí el hover cambia el
1058
- * borde y el color del icono, y nada más.
945
+ * Migrated from `links/src/components/Card.astro`. The original scaled the card
946
+ * to 102 %, lifted the title by a pixel and rotated and enlarged the icon on
947
+ * hover — four movements the system does not allow. Here the hover changes the
948
+ * border and the icon's color, and nothing else.
1059
949
  */
1060
950
  declare function LinkRow({ name, description, icon, external, className, ...props }: LinkRowProps): react.JSX.Element;
1061
951
 
1062
952
  /**
1063
- * La barra del sitio: 64px, abismo al 86 % y desenfoque de 14px detrás.
1064
- *
1065
- * Es composición de página y no un primitivo, pero vive en la librería por una
1066
- * razón concreta: la estética CLI de los items —mono, formato `./sección`— es lo
1067
- * primero que se desincroniza cuando cinco proyectos la reescriben cada uno por
1068
- * su cuenta.
1069
- *
1070
- * Renderiza `<header>` colgando directamente del `body`, así que ES el landmark
1071
- * `banner` del sitio. Por eso `PageHeader` va dentro de `<main>` y no es
1072
- * landmark: dos banners en una página son un fallo de accesibilidad.
1073
- *
1074
- * Los items van a la DERECHA, pegados a las acciones, no a continuación de la
1075
- * marca. Con la marca a la izquierda y los items justo detrás, el bloque de
1076
- * navegación queda flotando en medio de la barra y el ojo tiene que cruzar el
1077
- * hueco dos veces: una para leer la marca y otra para volver a buscar la
1078
- * sección. Agrupados a la derecha, marca y navegación son dos anclas y no tres.
953
+ * The site bar: 64px, abyss at 86 % and a 14px blur behind it.
954
+ *
955
+ * It is page composition and not a primitive, but it lives in the library for a
956
+ * concrete reason: the CLI aesthetic of the items — mono, `./section` format —
957
+ * is the first thing that drifts when five projects each rewrite it on their
958
+ * own.
959
+ *
960
+ * It renders a `<header>` hanging directly off the `body`, so it IS the site's
961
+ * `banner` landmark. That is why `PageHeader` goes inside `<main>` and is not a
962
+ * landmark: two banners on one page is an accessibility failure.
963
+ *
964
+ * The items go on the RIGHT, next to the actions, not straight after the brand.
965
+ * With the brand on the left and the items right behind it, the navigation block
966
+ * floats in the middle of the bar and the eye has to cross the gap twice: once
967
+ * to read the brand and once to come back and find the section. Grouped on the
968
+ * right, brand and navigation are two anchors instead of three.
1079
969
  */
1080
970
  type NavProps = ComponentPropsWithoutRef<'header'> & {
1081
- /** El logo, a la izquierda. */
971
+ /** The logo, on the left. */
1082
972
  brand?: ReactNode;
1083
- /** Los items de sección. Van a la derecha, pegados a `actions`. */
973
+ /** The section items. They go on the right, next to `actions`. */
1084
974
  children?: ReactNode;
1085
- /** Acciones a la derecha: conversión, cambio de tema, buscar. */
975
+ /** Actions on the right: conversion, theme switch, search. */
1086
976
  actions?: ReactNode;
1087
977
  };
1088
978
  declare function Nav({ brand, children, actions, className, ...props }: NavProps): react.JSX.Element;
1089
979
  type NavItemProps = ComponentPropsWithoutRef<'a'> & {
1090
- /** Sección actual: bioluz con subrayado de 1px. */
980
+ /** Current section: biolume with a 1px underline. */
1091
981
  active?: boolean | undefined;
1092
- /** Renderiza el hijo en vez de un `<a>`, para el `Link` del enrutador. */
982
+ /** Renders the child instead of an `<a>`, for the router's `Link`. */
1093
983
  asChild?: boolean | undefined;
1094
984
  };
1095
985
  /**
1096
- * El `./` lo pone el componente, no quien lo usa.
986
+ * The `./` is put there by the component, not by whoever uses it.
1097
987
  *
1098
- * Es la misma decisión que el botón terciario: el formato es parte de la pieza,
1099
- * no una convención que haya que recordar en cinco proyectos. Va `aria-hidden`,
1100
- * así que un lector de pantalla anuncia «artículos» y no «punto barra
988
+ * It is the same decision as the tertiary button: the format is part of the
989
+ * piece, not a convention to be remembered across five projects. It is
990
+ * `aria-hidden`, so a screen reader announces «artículos» and not «dot slash
1101
991
  * artículos».
1102
992
  *
1103
- * La sección actual va entre CORCHETES además de en bioluz y subrayada. No es
1104
- * decoración: el subrayado y el color son la misma señal —«esto destaca»— y en
1105
- * una barra de seis items en mono, a 13px, esa señal se lee peor de lo que
1106
- * parece en una maqueta. Los corchetes son la forma en que una terminal marca la
1107
- * ruta activa, así que dicen «estás aquí» sin depender de que se distinga el
1108
- * color. Van `aria-hidden`, porque quien escucha ya tiene `aria-current`.
993
+ * The current section goes in BRACKETS as well as in biolume and underlined.
994
+ * That is not decoration: the underline and the color are the same signal —
995
+ * «this stands out» — and in a six-item mono bar at 13px that signal reads worse
996
+ * than it looks in a mockup. Brackets are how a terminal marks the active path,
997
+ * so they say «you are here» without relying on the color being told apart. They
998
+ * are `aria-hidden`, because whoever is listening already has `aria-current`.
1109
999
  */
1110
1000
  declare function NavItem({ active, asChild, className, children, ...props }: NavItemProps): react.JSX.Element;
1111
1001
 
1112
1002
  /**
1113
- * Cuatro estados, y el aviso va DEBAJO del formulario.
1114
- *
1115
- * Reemplazar el formulario por el mensaje de éxito es lo que hace casi todo el
1116
- * mundo y es lo que rompe el caso real: alguien se suscribe con el correo
1117
- * equivocado y ya no tiene dónde volver a escribirlo. El campo se queda.
1118
- *
1119
- * El componente es presentacional: recibe `state` y emite `onSubmitEmail`. La
1120
- * llamada de red la hace el proyecto, porque cada uno tiene su proveedor y la
1121
- * librería no va a elegirlo por ellos.
1122
- *
1123
- * El aviso usa la SEGUNDA receta del sistema —`enfasis="fuerte"`, fondo al 10 %
1124
- * y borde sólido— porque va pegado bajo un campo que ya tiene borde: con la
1125
- * receta sutil, las dos líneas se leen como una sola caja.
1126
- *
1127
- * Es uno de los sitios donde la mascota puede aparecer: el «sin spam».
1128
- *
1129
- * El campo de nombre es OPCIONAL y está apagado por defecto. No es una prop de
1130
- * estilo: el endpoint de uno de los proyectos valida nombre y correo y responde
1131
- * 400 si falta el primero, así que un formulario de un solo campo ahí no es un
1132
- * formulario más pobre — es uno que envía algo que el servidor rechaza. El
1133
- * nombre viaja como SEGUNDO argumento de `onSubmitEmail`, para que las llamadas
1134
- * que ya existen —las que solo declaran `(email)`— sigan compilando.
1135
- *
1136
- * Lo que la librería NO hace es validar el nombre. El proyecto que lo pide lo
1137
- * acota entre 2 y 50 caracteres y solo letras y acentos; esa regla es suya y del
1138
- * servidor que la comprueba de verdad, y copiarla aquí sería tener dos fuentes
1139
- * que se desincronizan en silencio. `nameInputProps` está para que el proyecto
1140
- * ponga la suya.
1003
+ * Four states, and the notice goes UNDER the form.
1004
+ *
1005
+ * Replacing the form with the success message is what almost everyone does and
1006
+ * it is what breaks the real case: somebody subscribes with the wrong email and
1007
+ * then has nowhere to type it again. The field stays.
1008
+ *
1009
+ * The component is presentational: it takes `state` and emits `onSubmitEmail`.
1010
+ * The network call is the project's job, because each has its own provider and
1011
+ * the library is not going to choose one for them.
1012
+ *
1013
+ * The notice uses the system's SECOND recipe — `emphasis="strong"`, background
1014
+ * at 10 % and a solid border — because it sits directly under a field that
1015
+ * already has a border: with the subtle recipe, the two lines read as a single
1016
+ * box.
1017
+ *
1018
+ * It is one of the places the mascot may appear: the «sin spam».
1019
+ *
1020
+ * The name field is OPTIONAL and off by default. It is not a styling prop: one
1021
+ * project's endpoint validates name and email and answers 400 if the first is
1022
+ * missing, so a one-field form there is not a poorer form — it is one that sends
1023
+ * something the server rejects. The name travels as the SECOND argument of
1024
+ * `onSubmitEmail`, so the calls that already exist — the ones only declaring
1025
+ * `(email)` — keep compiling.
1026
+ *
1027
+ * What the library does NOT do is validate the name. The project asking for it
1028
+ * bounds it between 2 and 50 characters and to letters and accents only; that
1029
+ * rule is theirs and the server's that actually checks it, and copying it here
1030
+ * would mean two sources drifting apart in silence. `nameInputProps` is there
1031
+ * for the project to put its own in.
1032
+ *
1033
+ * Four of the props are here because the blog had already built each one by
1034
+ * hand, and each workaround leaned on something nobody had promised: `aside`
1035
+ * replaces an absolutely positioned pose and a `md:pr-[330px]` measured off the
1036
+ * image; `resetOnSuccess` replaces finding the `<form>` with a `ref` on the
1037
+ * container; `onFieldChange` replaces an `onInput` on the `<section>` that
1038
+ * worked because the event bubbles; and `fieldErrors` replaces losing the second
1039
+ * message whenever two fields failed at once. A workaround that works by an
1040
+ * implementation detail is a bug with a delay.
1141
1041
  */
1142
- type NewsletterState = 'reposo' | 'enviando' | 'exito' | 'error';
1042
+ type NewsletterState = 'idle' | 'sending' | 'success' | 'error';
1143
1043
  type NewsletterFormProps = Omit<ComponentPropsWithoutRef<'section'>, 'title' | 'onSubmit'> & {
1144
1044
  title: ReactNode;
1145
1045
  description?: ReactNode;
1146
1046
  state?: NewsletterState;
1147
1047
  /**
1148
- * Se dispara con el correo ya leído del campo, y con el nombre si el campo
1149
- * está puesto.
1048
+ * Fires with the email already read from the field, and with the name if that
1049
+ * field is enabled.
1150
1050
  */
1151
1051
  onSubmitEmail?: ((email: string, name?: string) => void) | undefined;
1152
1052
  successMessage?: ReactNode;
1153
1053
  errorMessage?: ReactNode;
1154
- /** La letra pequeña. Es el «sin spam», y por eso admite cara. */
1054
+ /** The small print. It is the «sin spam», which is why it accepts a face. */
1155
1055
  disclaimer?: ReactNode;
1156
- expresion?: Cara | undefined;
1056
+ expression?: Face | undefined;
1157
1057
  basePath?: string | undefined;
1158
1058
  submitLabel?: string;
1159
1059
  placeholder?: string;
1160
1060
  fieldLabel?: string;
1161
- /** Añade el campo de nombre delante del correo. */
1061
+ /** Adds the name field ahead of the email one. */
1162
1062
  nameField?: boolean;
1163
1063
  nameLabel?: string;
1164
1064
  namePlaceholder?: string;
1165
1065
  /**
1166
- * Lo que el proyecto necesite colgar del campo de nombre: `minLength`,
1167
- * `maxLength`, `pattern`. La librería no impone ninguna de las tres.
1066
+ * Whatever the project needs to hang off the name field: `minLength`,
1067
+ * `maxLength`, `pattern`. The library imposes none of the three.
1168
1068
  */
1169
1069
  nameInputProps?: Omit<InputProps, 'id' | 'name' | 'disabled'> | undefined;
1070
+ /**
1071
+ * The illustration, as a second column inside the panel.
1072
+ *
1073
+ * It exists because the component builds its own children, so neither
1074
+ * `children` nor an extra `ReactNode` had anywhere to go: the blog ended up
1075
+ * positioning the desk pose absolutely over the panel and reserving room for
1076
+ * it with a hand-written `md:pr-[330px]`. That number depends on the image's
1077
+ * width and nothing keeps the two in step.
1078
+ *
1079
+ * It only becomes a column from `md` up. Below that it goes back into the
1080
+ * flow under the form, for the same reason `Hero`'s pose does: on a narrow
1081
+ * screen there is no second column to put it in.
1082
+ */
1083
+ aside?: ReactNode;
1084
+ /**
1085
+ * Empties the fields after a successful subscription. On by default.
1086
+ *
1087
+ * With the fields still full, the same email invites a second submission. The
1088
+ * blog worked around it by finding the `<form>` with a `ref` on the container
1089
+ * and calling `reset()`, because the component exposed no form — a trick that
1090
+ * works by an implementation detail and not by contract.
1091
+ */
1092
+ resetOnSuccess?: boolean;
1093
+ /**
1094
+ * Fires when either field changes. It is where the project clears its error.
1095
+ *
1096
+ * The blog was doing it by hanging an `onInput` off the `<section>` and
1097
+ * relying on the event bubbling up. That works, and it works by accident: it
1098
+ * depends on the spare props landing on the section, which is an
1099
+ * implementation detail and not something anybody promised.
1100
+ */
1101
+ onFieldChange?: ((field: 'name' | 'email', value: string) => void) | undefined;
1102
+ /**
1103
+ * A message under one specific field, instead of the single alert.
1104
+ *
1105
+ * With one bad field the general alert already names it, because the API
1106
+ * sends Zod's first message. With two bad at once only one of them is ever
1107
+ * seen. `fieldErrors` marks each field and puts its message underneath;
1108
+ * `errorMessage` still covers what belongs to the form as a whole — the 409,
1109
+ * the network failure — and both can show at the same time.
1110
+ */
1111
+ fieldErrors?: {
1112
+ name?: ReactNode;
1113
+ email?: ReactNode;
1114
+ } | undefined;
1170
1115
  };
1171
- declare function NewsletterForm({ title, description, state, onSubmitEmail, successMessage, errorMessage, disclaimer, expresion, basePath, submitLabel, placeholder, fieldLabel, nameField, nameLabel, namePlaceholder, nameInputProps, className, ...props }: NewsletterFormProps): react.JSX.Element;
1116
+ declare function NewsletterForm({ title, description, state, onSubmitEmail, successMessage, errorMessage, disclaimer, expression, basePath, submitLabel, placeholder, fieldLabel, nameField, nameLabel, namePlaceholder, nameInputProps, aside, resetOnSuccess, onFieldChange, fieldErrors, className, ...props }: NewsletterFormProps): react.JSX.Element;
1172
1117
 
1173
1118
  /**
1174
- * Una sola cabecera en dos escalas, no dos componentes.
1119
+ * One header at two scales, not two components.
1175
1120
  *
1176
- * El hero del portafolio y el de cursos resultaron ser el mismo esqueleto —
1177
- * eyebrow en acento, titular, párrafo acotado — con distinto tamaño. Separarlos
1178
- * en `Hero` y `PageHeader` habría duplicado la misma regla en dos sitios y
1179
- * habría dejado la puerta abierta a que se separaran con el tiempo.
1121
+ * The portfolio's hero and the courses one turned out to be the same skeleton —
1122
+ * eyebrow in accent, headline, clamped paragraph — at different sizes. Splitting
1123
+ * them into `Hero` and `PageHeader` would have duplicated the same rule in two
1124
+ * places and left the door open for them to drift apart over time.
1180
1125
  *
1181
- * `display` para portadas, `page` para cabeceras de sección.
1126
+ * `display` for covers, `page` for section headers.
1182
1127
  *
1183
- * No recibe cara de la mascota, ni en una escala ni en la otra: las caras van en
1184
- * estados vacíos, confirmaciones, errores, progreso de curso y celebración.
1128
+ * It takes no mascot face, at either scale: faces go in empty states,
1129
+ * confirmations, errors, course progress and celebration.
1185
1130
  *
1186
- * Renderiza un `<header>`, y va DENTRO de `<main>`. Un `<header>` que cuelga
1187
- * directamente del `<body>` se convierte en landmark `banner`, y entonces
1188
- * compite con la cabecera del sitio: dos banners en una página es un fallo de
1189
- * accesibilidad. Dentro de `<main>` no es landmark y sí es la cabecera del
1190
- * contenido, que es lo que este componente es.
1131
+ * It renders a `<header>`, and it goes INSIDE `<main>`. A `<header>` hanging
1132
+ * directly off `<body>` becomes a `banner` landmark, and then it competes with
1133
+ * the site header: two banners on one page is an accessibility failure. Inside
1134
+ * `<main>` it is not a landmark and it is the content's header, which is what
1135
+ * this component is.
1191
1136
  */
1192
- declare const cabecera: (props?: ({
1137
+ declare const header: (props?: ({
1193
1138
  size?: "display" | "page" | null | undefined;
1194
1139
  } & class_variance_authority_types.ClassProp) | undefined) => string;
1195
- type PageHeaderProps = Omit<ComponentPropsWithoutRef<'header'>, 'title'> & VariantProps<typeof cabecera> & {
1140
+ type PageHeaderProps = Omit<ComponentPropsWithoutRef<'header'>, 'title'> & VariantProps<typeof header> & {
1196
1141
  title: ReactNode;
1197
- /** Mono, versalitas, en acento. Es la sección a la que pertenece la página. */
1142
+ /** Mono, small caps, in accent. It is the section the page belongs to. */
1198
1143
  eyebrow?: ReactNode | undefined;
1199
1144
  description?: ReactNode | undefined;
1200
1145
  /**
1201
- * Ranura para las llamadas a la acción. Si aquí va un botón de conversión,
1202
- * es el único de la pantalla.
1146
+ * Slot for the calls to action. If a conversion button goes here, it is the
1147
+ * only one on the screen.
1203
1148
  */
1204
1149
  action?: ReactNode | undefined;
1205
- /** Nivel del titular. `h1` salvo que la página ya tenga uno. */
1150
+ /** The headline's level. `h1` unless the page already has one. */
1206
1151
  as?: 'h1' | 'h2' | undefined;
1207
1152
  };
1208
1153
  declare function PageHeader({ title, eyebrow, description, action, size, as, className, ...props }: PageHeaderProps): react.JSX.Element;
1209
1154
 
1210
1155
  /**
1211
- * Cuánto llevas leído. NO es `Progress` con otro nombre.
1212
- *
1213
- * `Progress` mide una tarea: hay un total conocido, alguien la empezó y va a
1214
- * terminar. Esto mide una POSICIÓN en un documento, que se puede recorrer en
1215
- * los dos sentidos y de la que no hay nada que completar. Por eso no lleva
1216
- * `role="progressbar"` ni valor accesible: va `aria-hidden`.
1217
- *
1218
- * Eso último es deliberado y es la decisión que hay que defender. Un lector de
1219
- * pantalla ya sabe dónde está en el documento y anunciarle «37 %» cada vez que
1220
- * se mueve es ruido, no información. La barra es orientación visual, y lo que
1221
- * es solo visual se declara como tal.
1222
- *
1223
- * El ancho se escribe directamente, sin transición: `transition-standard` solo
1224
- * cubre color y borde, así que la barra sigue al scroll en vez de perseguirlo.
1225
- *
1226
- * La medición va dentro de `requestAnimationFrame`. Leer `scrollTop` en el
1227
- * manejador de scroll fuerza un reflujo síncrono en cada evento, y en un
1228
- * artículo largo eso se nota en el propio scroll — el efecto contrario al que
1229
- * busca la pieza.
1156
+ * How much you have read. It is NOT `Progress` under another name.
1157
+ *
1158
+ * `Progress` measures a task: there is a known total, somebody started it and it
1159
+ * is going to finish. This measures a POSITION in a document, which can be
1160
+ * travelled in both directions and of which there is nothing to complete. That
1161
+ * is why it carries neither `role="progressbar"` nor an accessible value: it is
1162
+ * `aria-hidden`.
1163
+ *
1164
+ * That last part is deliberate and it is the decision worth defending. A screen
1165
+ * reader already knows where it is in the document, and announcing «37 %» every
1166
+ * time it moves is noise, not information. The bar is visual orientation, and
1167
+ * what is purely visual is declared as such.
1168
+ *
1169
+ * The width is written directly, with no transition: `transition-standard` only
1170
+ * covers color and border, so the bar follows the scroll instead of chasing it.
1171
+ *
1172
+ * The measurement happens inside `requestAnimationFrame`. Reading `scrollTop` in
1173
+ * the scroll handler forces a synchronous reflow on every event, and in a long
1174
+ * article that shows up in the scroll itself — the opposite of what this piece
1175
+ * is for.
1230
1176
  */
1231
1177
  type ScrollingProgressBarProps = Omit<ComponentPropsWithoutRef<'div'>, 'children'> & {
1232
1178
  /**
1233
- * El elemento que se mide. Sin él, el documento entero.
1179
+ * The element being measured. Without it, the whole document.
1234
1180
  *
1235
- * Se pasa cuando la barra debe seguir SOLO al artículo: si la página tiene
1236
- * una cabecera alta y un pie con enlaces, medir el documento marca el 100 %
1237
- * cuando todavía quedan dos párrafos.
1181
+ * It is passed when the bar should follow ONLY the article: if the page has a
1182
+ * tall header and a footer full of links, measuring the document hits 100 %
1183
+ * while two paragraphs are still left.
1238
1184
  */
1239
1185
  target?: RefObject<HTMLElement | null> | undefined;
1240
- /** Arena en vez de bioluz, para igualar el progreso de curso. */
1186
+ /** Sand instead of biolume, to match course progress. */
1241
1187
  tone?: 'accent' | 'warm';
1242
- /** Pega la barra al borde superior de la ventana. */
1188
+ /** Pins the bar to the top edge of the window. */
1243
1189
  sticky?: boolean;
1244
1190
  };
1245
1191
  declare function ScrollingProgressBar({ target, tone, sticky, className, ...props }: ScrollingProgressBarProps): react.JSX.Element;
1246
1192
 
1247
1193
  /**
1248
- * La barra lateral del admin del blog.
1194
+ * The blog admin's sidebar.
1249
1195
  *
1250
- * El `▸` lo pone el componente, igual que el `./` de `NavItem` y el `~` del
1251
- * breadcrumb: es la misma estética CLI y la misma decisión — el formato es parte
1252
- * de la pieza, no una convención que haya que recordar. Va `aria-hidden`.
1196
+ * The `▸` is put there by the component, same as `NavItem`'s `./` and the
1197
+ * breadcrumb's `~`: it is the same CLI aesthetic and the same decision — the
1198
+ * format is part of the piece, not a convention to be remembered. It is
1199
+ * `aria-hidden`.
1253
1200
  *
1254
- * El pie lleva la versión y la rama (`v5.0.1 · main`) en la escala `meta`. No es
1255
- * decoración: en un admin es lo primero que se pregunta cuando algo se ve raro.
1201
+ * The footer carries the version and the branch (`v5.0.1 · main`) in the `meta`
1202
+ * scale. It is not decoration: in an admin it is the first thing anyone asks
1203
+ * when something looks off.
1256
1204
  */
1257
1205
  type SidebarItemProps = ComponentPropsWithoutRef<'a'> & {
1258
1206
  active?: boolean | undefined;
1259
1207
  asChild?: boolean | undefined;
1260
- /** Contador a la derecha: borradores pendientes, media sin usar. */
1208
+ /** Counter on the right: pending drafts, unused media. */
1261
1209
  badge?: ReactNode;
1262
1210
  };
1263
1211
  declare function SidebarItem({ active, asChild, badge, className, children, ...props }: SidebarItemProps): react.JSX.Element;
1264
1212
  type SidebarNavProps = ComponentPropsWithoutRef<'nav'> & {
1265
- /** Encabezado del panel. */
1213
+ /** The panel's heading. */
1266
1214
  title?: ReactNode;
1267
- /** Versión y rama, al pie. */
1215
+ /** Version and branch, at the bottom. */
1268
1216
  version?: ReactNode;
1269
1217
  branch?: ReactNode;
1270
1218
  };
1271
1219
  declare function SidebarNav({ title, version, branch, children, className, ...props }: SidebarNavProps): react.JSX.Element;
1272
1220
 
1273
1221
  /**
1274
- * Una métrica grande: el número en la escala `stat` y su nombre debajo.
1275
- *
1276
- * La regla del documento no es de estilo, es de semántica: «bioluz para lo
1277
- * neutro y arena SOLO cuando el número es el problema». Un 12 de aplicaciones
1278
- * es un dato; un 0 de design systems es el problema del que trata la charla.
1279
- * Por eso `tone` no es una paleta abierta — son dos valores y significan cosas
1280
- * distintas.
1281
- *
1282
- * El orden de lectura es icono + título, el número grande, y la bajada debajo.
1283
- * El número va en MEDIO y no al final a propósito: es lo que se viene a leer, y
1284
- * una bajada de dos líneas entre el título y la cifra la entierra. Arriba queda
1285
- * de qué va, en medio cuánto, y abajo el matiz que solo lee quien se para.
1222
+ * A large metric: the number in the `stat` scale and its name underneath.
1223
+ *
1224
+ * The document's rule is not one of style, it is one of semantics: «biolume for
1225
+ * the neutral and sand ONLY when the number is the problem». A 12 of
1226
+ * applications is a datum; a 0 of design systems is the problem the talk is
1227
+ * about. That is why `tone` is not an open palette — there are two values and
1228
+ * they mean different things.
1229
+ *
1230
+ * The reading order is icon + title, the big number, and the standfirst below.
1231
+ * The number goes in the MIDDLE and not at the end on purpose: it is what people
1232
+ * came to read, and a two-line standfirst between the title and the figure
1233
+ * buries it. The top says what it is about, the middle says how much, and the
1234
+ * bottom holds the nuance only someone who stops will read.
1286
1235
  */
1287
1236
  type StatProps = Omit<ComponentPropsWithoutRef<'div'>, 'title'> & {
1288
- /** El número, ya formateado. La librería no impone locale. */
1237
+ /** The number, already formatted. The library imposes no locale. */
1289
1238
  value: ReactNode;
1290
- /** Qué se está contando. Va en mono versalitas. */
1239
+ /** What is being counted. It goes in mono small caps. */
1291
1240
  label: ReactNode;
1292
- /** `alerta` solo cuando el número ES el problema. */
1241
+ /** `alerta` only when the number IS the problem. */
1293
1242
  tone?: 'neutral' | 'alerta';
1294
- /** Con `progress`, la métrica se lee como avance y añade la barra. */
1243
+ /** With `progress`, the metric reads as progress and adds the bar. */
1295
1244
  progress?: number | undefined;
1296
1245
  /**
1297
- * Glifo al lado del título, a 1em. Hereda `currentColor`, así que sigue al
1298
- * tono del título y no hay que teñirlo aparte.
1246
+ * Glyph beside the title, at 1em. It inherits `currentColor`, so it follows
1247
+ * the title's tone and does not have to be tinted separately.
1299
1248
  */
1300
1249
  icon?: ReactNode;
1301
1250
  /**
1302
- * La bajada: el matiz que el número solo no da. «12 aplicaciones» no dice si
1303
- * son muchas, y aquí es donde se dice.
1251
+ * The standfirst: the nuance the number alone does not give. «12 aplicaciones»
1252
+ * does not say whether that is a lot, and this is where that gets said.
1304
1253
  */
1305
1254
  description?: ReactNode;
1306
1255
  };
1307
1256
  declare function Stat({ value, label, tone, progress, icon, description, className, ...props }: StatProps): react.JSX.Element;
1308
1257
 
1309
- type TalkCardProps = Omit<TarjetaProps, 'children' | 'title'> & {
1258
+ type TalkContent = {
1310
1259
  title: ReactNode;
1311
- /** Dónde se dio: la conferencia, el meetup, el equipo. */
1260
+ /** Where it was given: the conference, the meetup, the team. */
1312
1261
  event: ReactNode;
1313
1262
  date?: ReactNode;
1314
1263
  dateTime?: string | undefined;
1315
1264
  location?: ReactNode;
1316
- /** Etiqueta corta de estado: «con vídeo», «próxima», «solo audio». */
1265
+ /** Short status label: «con vídeo», «próxima», «solo audio». */
1317
1266
  status?: ReactNode;
1318
1267
  /**
1319
- * De qué iba la charla. Se corta a dos líneas, igual que el `excerpt` de
1320
- * `ArticleCard`, para que la rejilla no se desalinee.
1268
+ * What the talk was about. Clamped to two lines, same as `ArticleCard`'s
1269
+ * `excerpt`, so the grid does not fall out of line.
1321
1270
  */
1322
1271
  description?: ReactNode;
1323
1272
  };
1324
- declare function TalkCard({ title, event, date, dateTime, location, status, description, className, ...props }: TalkCardProps): react.JSX.Element;
1273
+ /**
1274
+ * A talk has more than one destination, and that is what shapes this type.
1275
+ *
1276
+ * Slides, repo, the recording, the event's own page: the listing used to show
1277
+ * them as loose links under each talk, and migrating to this component made them
1278
+ * DISAPPEAR — there was nowhere to put them, so the blog pointed the whole card
1279
+ * at the first one that existed and the rest were lost.
1280
+ *
1281
+ * So `resources` is a slot, and it forces a choice in the type: a card with
1282
+ * resources is NOT a link. An `<a>` inside an `<a>` is invalid HTML and the
1283
+ * browser un-nests it, so «the card links to the slides AND the slides link to
1284
+ * the slides» is not a thing that can render. With resources the card is an
1285
+ * `<article>` on the card surface and the links are the resources; without them
1286
+ * it stays the single-destination card it was.
1287
+ *
1288
+ * That is why this is a union and not one more optional prop: the combination
1289
+ * that cannot work does not compile.
1290
+ */
1291
+ type TalkCardProps = (Omit<CardShellProps, 'children' | 'title'> & TalkContent & {
1292
+ /** The card is the link. Do not pass resources with this. */
1293
+ resources?: never;
1294
+ }) | (Omit<ComponentPropsWithoutRef<'article'>, 'title'> & TalkContent & {
1295
+ /**
1296
+ * Slides, repo, recording. They are the links, so the card stops being
1297
+ * one.
1298
+ */
1299
+ resources: ReactNode;
1300
+ });
1301
+ declare function TalkCard({ title, event, date, dateTime, location, status, description, resources, className, ...props }: TalkCardProps): react.JSX.Element;
1325
1302
 
1326
1303
  /**
1327
- * El control que faltaba. La librería definía todo el sistema de temas y no
1328
- * exponía lo que lo cambia, así que dos proyectos lo reimplementaban.
1329
- *
1330
- * Lo difícil no es el botón: es que la primera pintura no parpadee y que la
1331
- * elección sobreviva a la navegación. Eso vive en `@eduardoalvarez/arrecife/tema`,
1332
- * que no importa React —lo consume un Astro que no monta ninguno— y de ahí sale
1333
- * `scriptTema`, que va inline en el `<head>`. Sin ese script, este botón
1334
- * funciona y aun así se ve el fogonazo en cada carga.
1335
- *
1336
- * Los DOS iconos se renderizan siempre y el que sobra lo esconde el CSS con la
1337
- * variante `light:`. No es una optimización: es lo que evita que el servidor y
1338
- * el cliente discrepen. El servidor no sabe qué tema eligió quien va a leer, así
1339
- * que cualquier icono que elija en el HTML tiene la mitad de probabilidades de
1340
- * ser el equivocado, y corregirlo al hidratar es el parpadeo otra vez.
1341
- *
1342
- * El nombre accesible NO dice a qué modo se va. Sería más informativo y sería
1343
- * una mentira la mitad del tiempo por lo mismo de arriba: el HTML del servidor
1344
- * lo fija antes de saber el tema. «Cambiar de tema» es cierto siempre.
1304
+ * The control that was missing. The library defined the whole theming system and
1305
+ * exposed nothing that changes it, so two projects were reimplementing it.
1306
+ *
1307
+ * The hard part is not the button: it is that the first paint does not flash and
1308
+ * that the choice survives navigation. That lives in
1309
+ * `@eduardoalvarez/arrecife/theme`, which does not import React — an Astro that
1310
+ * mounts none consumes it — and out of it comes `themeScript`, which goes inline
1311
+ * in the `<head>`. Without that script this button works and you still get the
1312
+ * flash on every load.
1313
+ *
1314
+ * BOTH icons are always rendered and CSS hides the spare one with the `light:`
1315
+ * variant. It is not an optimisation: it is what stops the server and the client
1316
+ * from disagreeing. The server does not know which theme the reader chose, so
1317
+ * whichever icon it picks in the HTML has a fifty-fifty chance of being wrong,
1318
+ * and correcting it on hydration is the flash all over again.
1319
+ *
1320
+ * The accessible name does NOT say which mode you are going to. That would be
1321
+ * more informative and would be a lie half the time, for the same reason as
1322
+ * above: the server's HTML fixes it before the theme is known. «Cambiar de
1323
+ * tema» is always true.
1345
1324
  */
1346
1325
  type ThemeToggleProps = Omit<ComponentPropsWithoutRef<'button'>, 'onClick'> & {
1347
- /** Nombre accesible. El botón no tiene texto visible, así que es lo único que lo nombra. */
1326
+ /** Accessible name. The button has no visible text, so it is the only thing naming it. */
1348
1327
  label?: string;
1349
- /** Se dispara con el tema que quedó puesto, por si el proyecto quiere anotarlo. */
1350
- onThemeChange?: ((tema: Tema) => void) | undefined;
1328
+ /** Fires with whichever theme ended up set, in case the project wants to record it. */
1329
+ onThemeChange?: ((theme: Theme) => void) | undefined;
1351
1330
  variant?: ButtonProps['variant'];
1352
1331
  size?: ButtonProps['size'];
1353
1332
  };
1354
1333
  declare function ThemeToggle({ label, onThemeChange, variant, size, className, ...props }: ThemeToggleProps): react.JSX.Element;
1355
1334
  /**
1356
- * El tema puesto ahora mismo, para un proyecto que necesite ramificar en React
1357
- * —un logo distinto por modo, una imagen que no tiene versión clara—.
1358
- *
1359
- * Es `useSyncExternalStore` y no un `useState` con un efecto detrás porque el
1360
- * tema es exactamente eso: un estado que vive fuera de React, en un atributo del
1361
- * `<html>` que puede cambiar sin que React se entere. Escribirlo con un efecto
1362
- * que llama a `setState` en el montaje es el patrón que dispara un render en
1363
- * cascada y que la regla `set-state-in-effect` señala con razón.
1364
- *
1365
- * `getServerSnapshot` devuelve `'dark'` porque en el servidor no hay `document`.
1366
- * El primer render del cliente coincide con el del servidor y el valor real
1367
- * entra después, que es la misma discrepancia de hidratación que `ThemeToggle`
1368
- * evita renderizando los dos iconos.
1369
- *
1370
- * De ahí la regla de uso: si lo que ramifica es SOLO estilo, esto no hace falta
1371
- * y la variante `light:` es mejor — no re-renderiza nada. Esto es para cuando
1372
- * cambia el contenido.
1335
+ * The theme set right now, for a project that needs to branch in React — a
1336
+ * different logo per mode, an image with no light version.
1337
+ *
1338
+ * It is `useSyncExternalStore` and not a `useState` with an effect behind it
1339
+ * because the theme is exactly that: state living outside React, in an attribute
1340
+ * on `<html>` that can change without React finding out. Writing it as an effect
1341
+ * calling `setState` on mount is the pattern that triggers a cascading render
1342
+ * and that the `set-state-in-effect` rule rightly flags.
1343
+ *
1344
+ * `getServerSnapshot` returns `'dark'` because there is no `document` on the
1345
+ * server. The client's first render matches the server's and the real value
1346
+ * arrives afterwards, which is the same hydration mismatch `ThemeToggle` avoids
1347
+ * by rendering both icons.
1348
+ *
1349
+ * Hence the usage rule: if what branches is ONLY style, this is not needed and
1350
+ * the `light:` variant is better — it re-renders nothing. This is for when the
1351
+ * content changes.
1373
1352
  */
1374
- declare function useTema(): Tema;
1353
+ declare function useTheme(): Theme;
1375
1354
 
1376
1355
  /**
1377
- * «En esta página». El índice del artículo largo.
1356
+ * «En esta página». The long article's table of contents.
1378
1357
  *
1379
- * Es un `<nav>` con nombre accesible propio, no una lista suelta: en una página
1380
- * que ya tiene la barra del sitio y las migas, un tercer grupo de enlaces sin
1381
- * nombre es indistinguible de los otros dos para quien navega por landmarks.
1358
+ * It is a `<nav>` with an accessible name of its own, not a loose list: on a
1359
+ * page that already has the site bar and the breadcrumb, a third group of links
1360
+ * with no name is indistinguishable from the other two for anyone navigating by
1361
+ * landmarks.
1382
1362
  *
1383
- * El activo se marca con `aria-current`, no solo con color — la sección en la
1384
- * que estás no puede comunicarse únicamente con bioluz.
1363
+ * The active entry is marked with `aria-current`, not with color alone — the
1364
+ * section you are in cannot be communicated purely in biolume.
1385
1365
  *
1386
- * Y ese atributo es además EL GANCHO: las clases del activo se aplican con la
1387
- * variante `aria-[current]:`, no con un ternario en el render. La diferencia es
1388
- * la que hay entre servir solo controlado y servir también sin controlar.
1366
+ * And that attribute is also THE HOOK: the active classes are applied with the
1367
+ * `aria-[current]:` variant, not with a ternary in the render. The difference is
1368
+ * the one between serving controlled only and also serving uncontrolled.
1389
1369
  *
1390
- * Un sitio Astro resuelve el scroll-spy con quince líneas de script que ponen
1391
- * `aria-current` en el enlace visible y quitan el del anterior. Con el estado
1392
- * calculado en el render, ese script no podía hacer nada: había que hidratar el
1393
- * índice como isla de React en cada artículo para algo que cuesta cero
1394
- * JavaScript de framework. Ahora el CSS reacciona al atributo y las dos formas
1395
- * de usarlo dan el mismo resultado.
1370
+ * An Astro site solves scroll-spy with fifteen lines of script that set
1371
+ * `aria-current` on the visible link and remove it from the previous one. With
1372
+ * the state computed in the render, that script could do nothing: the table of
1373
+ * contents had to be hydrated as a React island on every article for something
1374
+ * that costs zero framework JavaScript. Now the CSS reacts to the attribute and
1375
+ * both ways of using it give the same result.
1396
1376
  *
1397
- * El gancho es la PRESENCIA del atributo, así que se quita para desmarcar; no
1398
- * se pone `aria-current="false"`.
1377
+ * The hook is the PRESENCE of the attribute, so it is removed to unmark; you do
1378
+ * not set `aria-current="false"`.
1399
1379
  */
1400
- type Entrada = {
1401
- /** El ancla, con `#`. */
1380
+ type TocEntry = {
1381
+ /** The anchor, with its `#`. */
1402
1382
  href: string;
1403
1383
  label: ReactNode;
1404
- /** Sangra la entrada. Solo dos niveles: h2 y h3. */
1384
+ /** Indents the entry. Only two levels: h2 and h3. */
1405
1385
  nested?: boolean | undefined;
1406
1386
  };
1407
1387
  type TableOfContentsProps = Omit<ComponentPropsWithoutRef<'nav'>, 'children'> & {
1408
- items: readonly Entrada[];
1409
- /** El título del bloque. */
1388
+ items: readonly TocEntry[];
1389
+ /** The block's title. */
1410
1390
  title?: ReactNode;
1411
- /** Ancla de la sección visible. */
1391
+ /** The anchor of the visible section. */
1412
1392
  activeHref?: string | undefined;
1413
1393
  linkAsChild?: ((props: {
1414
1394
  href: string;
@@ -1427,18 +1407,30 @@ declare const Instagram: (props: IconProps) => react.JSX.Element;
1427
1407
  declare const Discord: (props: IconProps) => react.JSX.Element;
1428
1408
  declare const YouTube: (props: IconProps) => react.JSX.Element;
1429
1409
  declare const Rss: (props: IconProps) => react.JSX.Element;
1430
- declare const Correo: (props: IconProps) => react.JSX.Element;
1410
+ declare const Email: (props: IconProps) => react.JSX.Element;
1411
+ /**
1412
+ * The newsletter. It plays the same role as `Rss` — a way to follow, not a
1413
+ * social network — which is why it belongs in this catalogue and does not open
1414
+ * the door to an icon library.
1415
+ *
1416
+ * It is named for what it means and not for what it draws, like everything else
1417
+ * in the system: it is a bell, and it is called `Newsletter`. `eduardoalvarez.dev`
1418
+ * had it drawn in the project, following the contract by hand so it would not
1419
+ * clash while it waited.
1420
+ */
1421
+ declare const Newsletter: (props: IconProps) => react.JSX.Element;
1431
1422
 
1432
- declare const social_Correo: typeof Correo;
1433
1423
  declare const social_Discord: typeof Discord;
1424
+ declare const social_Email: typeof Email;
1434
1425
  declare const social_GitHub: typeof GitHub;
1435
1426
  declare const social_Instagram: typeof Instagram;
1436
1427
  declare const social_LinkedIn: typeof LinkedIn;
1428
+ declare const social_Newsletter: typeof Newsletter;
1437
1429
  declare const social_Rss: typeof Rss;
1438
1430
  declare const social_X: typeof X;
1439
1431
  declare const social_YouTube: typeof YouTube;
1440
1432
  declare namespace social {
1441
- export { social_Correo as Correo, social_Discord as Discord, social_GitHub as GitHub, social_Instagram as Instagram, social_LinkedIn as LinkedIn, social_Rss as Rss, social_X as X, social_YouTube as YouTube };
1433
+ export { social_Discord as Discord, social_Email as Email, social_GitHub as GitHub, social_Instagram as Instagram, social_LinkedIn as LinkedIn, social_Newsletter as Newsletter, social_Rss as Rss, social_X as X, social_YouTube as YouTube };
1442
1434
  }
1443
1435
 
1444
- export { Accordion, AccordionContent, AccordionItem, type AccordionProps, AccordionTrigger, type AccordionTriggerProps, Alert, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogTitle, AlertDialogTrigger, type AlertProps, ArticleCard, type ArticleCardProps, AudioPlayer, type AudioPlayerMode, type AudioPlayerProps, AuthorCard, type AuthorCardProps, Avatar, AvatarFallback, AvatarImage, type AvatarProps, AvatarUpload, type AvatarUploadProps, Badge, type BadgeProps, Blockquote, type BlockquoteProps, Breadcrumb, type BreadcrumbProps, Button, type ButtonProps, Calendar, type CalendarProps, Cara, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CategoryBadge, type CategoryBadgeProps, Checkbox, type CheckboxProps, Code, CodeBlock, type CodeBlockProps, type CodeProps, CourseCard, type CourseCardProps, DateField, type DateFieldProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, type Entrada, EventCalendar, type EventCalendarProps, type EventoCalendario, Footer, FooterLink, type FooterLinkProps, type FooterProps, HOVER_TARJETA, Hero, type HeroProps, Input, type InputProps, LinkRow, type LinkRowProps, MetricBadge, type MetricBadgeProps, type Migaja, Nav, NavItem, type NavItemProps, type NavProps, NewsletterForm, type NewsletterFormProps, type NewsletterState, PageHeader, type PageHeaderProps, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, type PaginationLinkProps, PaginationNext, PaginationPrevious, Popover, PopoverAnchor, PopoverContent, type PopoverContentProps, PopoverTrigger, Pose, Progress, type ProgressProps, RadioGroup, RadioGroupItem, type RadioGroupItemProps, type RadioGroupProps, type Red, SUPERFICIE_TARJETA, ScrollingProgressBar, type ScrollingProgressBarProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeparatorProps, Sheet, SheetBody, SheetClose, SheetContent, type SheetContentProps, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, SidebarItem, type SidebarItemProps, SidebarNav, type SidebarNavProps, Skeleton, type SkeletonProps, Stat, type StatProps, Switch, type SwitchProps, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableOfContents, type TableOfContentsProps, TableRow, Tabs, TabsContent, TabsList, type TabsProps, TabsTrigger, TalkCard, type TalkCardProps, Tema, Text, type TextProps, Textarea, type TextareaProps, ThemeToggle, type ThemeToggleProps, ToastAction, type ToastOptions, type ToastVariant, Toaster, type ToasterProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, alert as alertVariants, avatar as avatarVariants, badge as badgeVariants, button as buttonVariants, categoria as categoryBadgeVariants, cn, social, texto as textVariants, toast, useTema };
1436
+ export { Accordion, AccordionContent, AccordionItem, type AccordionProps, AccordionTrigger, type AccordionTriggerProps, Alert, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogTitle, AlertDialogTrigger, type AlertProps, ArticleCard, type ArticleCardProps, AudioPlayer, type AudioPlayerMode, type AudioPlayerProps, AuthorCard, type AuthorCardProps, Avatar, AvatarFallback, AvatarImage, type AvatarProps, AvatarUpload, type AvatarUploadProps, Badge, type BadgeProps, Blockquote, type BlockquoteProps, Breadcrumb, type BreadcrumbProps, Button, type ButtonProps, Calendar, type CalendarEvent, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CategoryBadge, type CategoryBadgeProps, Checkbox, type CheckboxProps, Code, CodeBlock, type CodeBlockProps, type CodeProps, CourseCard, type CourseCardProps, type Crumb, DateField, type DateFieldProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, EventCalendar, type EventCalendarProps, Face, Footer, FooterLink, type FooterLinkProps, type FooterProps, Hero, type HeroProps, Input, type InputProps, LinkRow, type LinkRowProps, MetricBadge, type MetricBadgeProps, Nav, NavItem, type NavItemProps, type NavProps, NewsletterForm, type NewsletterFormProps, type NewsletterState, PageHeader, type PageHeaderProps, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, type PaginationLinkProps, PaginationNext, PaginationPrevious, Popover, PopoverAnchor, PopoverContent, type PopoverContentProps, PopoverTrigger, Pose, Progress, type ProgressProps, RadioGroup, RadioGroupItem, type RadioGroupItemProps, type RadioGroupProps, ScrollingProgressBar, type ScrollingProgressBarProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeparatorProps, Sheet, SheetBody, SheetClose, SheetContent, type SheetContentProps, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, SidebarItem, type SidebarItemProps, SidebarNav, type SidebarNavProps, Skeleton, type SkeletonProps, type SocialLink, Stat, type StatProps, Switch, type SwitchProps, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableOfContents, type TableOfContentsProps, TableRow, Tabs, TabsContent, TabsList, type TabsProps, TabsTrigger, TalkCard, type TalkCardProps, Text, type TextProps, Textarea, type TextareaProps, Theme, ThemeToggle, type ThemeToggleProps, ToastAction, type ToastOptions, type ToastVariant, Toaster, type ToasterProps, type TocEntry, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, alert as alertVariants, avatar as avatarVariants, badge as badgeVariants, button as buttonVariants, cn, social, text as textVariants, toast, useTheme };