@eduardoalvarez/arrecife 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +868 -467
  3. package/dist/brand/index.cjs +112 -95
  4. package/dist/brand/index.d.cts +40 -39
  5. package/dist/brand/index.d.ts +40 -39
  6. package/dist/brand/index.js +5 -4
  7. package/dist/catalog-D13txprv.d.cts +78 -0
  8. package/dist/catalog-D13txprv.d.ts +78 -0
  9. package/dist/chart/index.cjs +100 -83
  10. package/dist/chart/index.d.cts +66 -66
  11. package/dist/chart/index.d.ts +66 -66
  12. package/dist/chart/index.js +14 -12
  13. package/dist/chunk-2WPWEIMD.js +27 -0
  14. package/dist/chunk-45HVCTB7.js +70 -0
  15. package/dist/{chunk-ZEOQKRQ7.js → chunk-727HCBD4.js} +1 -1
  16. package/dist/chunk-CKRSQPTX.js +36 -0
  17. package/dist/chunk-E6KFUSKB.js +144 -0
  18. package/dist/chunk-GCRII2KQ.js +86 -0
  19. package/dist/{chunk-YZ2SDOVZ.js → chunk-JN3IS5OS.js} +30 -30
  20. package/dist/chunk-ODBFN44D.js +45 -0
  21. package/dist/chunk-OMKSESQB.js +300 -0
  22. package/dist/{chunk-VPT32GPG.js → chunk-TA7TLWW4.js} +2 -2
  23. package/dist/chunk-WGNIRIN7.js +42 -0
  24. package/dist/doctor.mjs +166 -0
  25. package/dist/form/index.cjs +109 -92
  26. package/dist/form/index.d.cts +43 -42
  27. package/dist/form/index.d.ts +43 -42
  28. package/dist/form/index.js +25 -23
  29. package/dist/icons/index.cjs +149 -0
  30. package/dist/icons/index.d.cts +94 -0
  31. package/dist/icons/index.d.ts +94 -0
  32. package/dist/icons/index.js +28 -0
  33. package/dist/index-DlAO2JZs.d.cts +47 -0
  34. package/dist/index-DlAO2JZs.d.ts +47 -0
  35. package/dist/index.cjs +1292 -983
  36. package/dist/index.d.cts +927 -806
  37. package/dist/index.d.ts +927 -806
  38. package/dist/index.js +809 -778
  39. package/dist/{label-DuTvJGxD.d.ts → label-MgHFKnFy.d.cts} +3 -3
  40. package/dist/{label-DuTvJGxD.d.cts → label-MgHFKnFy.d.ts} +3 -3
  41. package/dist/og/index.cjs +133 -132
  42. package/dist/og/index.d.cts +93 -89
  43. package/dist/og/index.d.ts +93 -89
  44. package/dist/og/index.js +106 -106
  45. package/dist/shiki/index.cjs +28 -30
  46. package/dist/shiki/index.d.cts +4 -4
  47. package/dist/shiki/index.d.ts +4 -4
  48. package/dist/shiki/index.js +12 -12
  49. package/dist/social/index.cjs +67 -0
  50. package/dist/social/index.d.cts +2 -0
  51. package/dist/social/index.d.ts +2 -0
  52. package/dist/social/index.js +2 -0
  53. package/dist/theme/index.cjs +97 -0
  54. package/dist/theme/index.d.cts +144 -0
  55. package/dist/theme/index.d.ts +144 -0
  56. package/dist/theme/index.js +2 -0
  57. package/dist/tokens/index.cjs +159 -88
  58. package/dist/tokens/index.d.cts +277 -165
  59. package/dist/tokens/index.d.ts +277 -165
  60. package/dist/tokens/index.js +2 -2
  61. package/dist/tokens/theme.css +165 -100
  62. package/dist/variants/index.cjs +195 -0
  63. package/dist/variants/index.d.cts +195 -0
  64. package/dist/variants/index.d.ts +195 -0
  65. package/dist/variants/index.js +3 -0
  66. package/llms.txt +1145 -746
  67. package/package.json +42 -11
  68. package/dist/catalogo-Du5ID-Hi.d.cts +0 -77
  69. package/dist/catalogo-Du5ID-Hi.d.ts +0 -77
  70. package/dist/chunk-E3OMP2DL.js +0 -36
  71. package/dist/chunk-KPZNNMV5.js +0 -83
  72. package/dist/chunk-NHS7ETKJ.js +0 -27
  73. package/dist/chunk-TSPJOM6K.js +0 -229
  74. package/dist/chunk-UOWIDFCB.js +0 -81
  75. package/dist/tema/index.cjs +0 -94
  76. package/dist/tema/index.d.cts +0 -110
  77. package/dist/tema/index.d.ts +0 -110
  78. package/dist/tema/index.js +0 -2
package/dist/index.d.ts 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.js';
2
- import { Tema } from './tema/index.js';
3
- export { TEMA_ATRIBUTO, TEMA_CLAVE, TEMA_EVENTO, alternarTema, aplicarTema, escucharTema, scriptTema, temaActual, temaGuardado, temaPreferido } from './tema/index.js';
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.js';
2
+ import { Theme } from './theme/index.js';
3
+ export { THEME_ATTRIBUTE, THEME_EVENT, THEME_KEY, ThemeOptions, applyTheme, currentTheme, preferredTheme, storedTheme, themeScript, toggleTheme, watchTheme } from './theme/index.js';
4
+ import { alertVariants as alert, avatarVariants as avatar, badgeVariants as badge, buttonVariants as button, textVariants as text } from './variants/index.js';
5
+ export { CARD, CARD_HOVER, CARD_SURFACE, categoryBadgeVariants, metricBadgeVariants } from './variants/index.js';
4
6
  import * as react from 'react';
5
- import { ComponentPropsWithoutRef, ReactNode, ComponentProps, RefObject, SVGProps } from 'react';
7
+ import { ComponentPropsWithoutRef, ReactNode, ComponentProps, RefObject } 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,235 @@ 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.js';
16
+ export { L as Label, a as LabelProps } from './label-MgHFKnFy.js';
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.js';
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.js';
27
- export { CaraDeMascota, CaraDeMascotaProps, Isotipo, IsotipoProps, Logo, LogoProps, Mascota, MascotaProps } from './brand/index.js';
27
+ import { F as Face, P as Pose } from './catalog-D13txprv.js';
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.js';
29
+ export { Isotype, IsotypeProps, Logo, LogoProps, Mascot, MascotFace, MascotFaceProps, MascotProps } from './brand/index.js';
28
30
  import { ClassValue } from 'clsx';
31
+ export { i as social } from './index-DlAO2JZs.js';
29
32
  import '@radix-ui/react-label';
30
33
 
31
34
  /**
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.
35
+ * The disclosure. Two projects asked for it: the portfolio FAQ and the course
36
+ * syllabus, which is literally a list of sections that open.
34
37
  *
35
- * La altura SÍ se anima, y es la cuarta excepción declarada del sistema.
38
+ * The height IS animated, and it is the system's fourth declared exception.
36
39
  *
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.
40
+ * It deserves explaining, because the general rule says the opposite and this
41
+ * component was born unanimated citing it. The difference is that nothing
42
+ * APPEARS here: a gap opens, and everything below the accordion shifts. Without
43
+ * a transition that shift is a jump, and whoever just clicked loses their place
44
+ * on the page — which is exactly the harm the «no movement» rule exists to
45
+ * prevent. It is the same category as the side panel, the second exception, and
46
+ * not that of an entrance animation.
44
47
  *
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.
48
+ * It sits behind `motion-safe`, lasts `--duration-standard` and uses
49
+ * `--ease-standard`, so it introduces neither a new timing nor a new curve.
50
+ * Whoever asked for less motion still sees the panel appear where it will stay.
48
51
  *
49
- * Ver `docs/decisiones.md` § 20.
52
+ * See `docs/decisions.md` § 20.
50
53
  *
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`.
54
+ * The chevron, by contrast, rotates with no transition: `transition-standard`
55
+ * only covers color and border, so `rotate` snaps even with the class in place.
56
+ * It is the same treatment `Progress` gives its width.
54
57
  *
55
- * La división entre items es `hairline`, no `border`: es una separación de
56
- * lectura, no el borde de un control.
58
+ * The divider between items is `hairline`, not `border`: it is a reading
59
+ * separation, not the border of a control.
57
60
  */
58
61
  type AccordionProps = ComponentPropsWithoutRef<typeof AccordionPrimitive.Root>;
59
62
  declare function Accordion({ className, ...props }: AccordionProps): react.JSX.Element;
60
63
  declare function AccordionItem({ className, ...props }: ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>): react.JSX.Element;
61
64
  /**
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.
65
+ * The trigger IS the heading, so it goes INSIDE an `<h3>`: Radix wraps the
66
+ * button in `AccordionPrimitive.Header`, which renders whichever element you ask
67
+ * of it. Without that, a screen reader sees a list of loose buttons and loses
68
+ * the page structure, which is precisely what a FAQ needs to keep.
69
+ *
70
+ * `headingLevel` exists because the correct level depends on where it is
71
+ * mounted: on a FAQ page the block hangs off a section `<h2>`, and in a syllabus
72
+ * it may hang off an `<h3>`. Pinning it here would be guessing.
70
73
  */
71
74
  type AccordionTriggerProps = ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & {
72
- /** Nivel del encabezado que envuelve al disparador. */
75
+ /** The level of the heading wrapping the trigger. */
73
76
  headingLevel?: 2 | 3 | 4;
74
77
  };
75
78
  declare function AccordionTrigger({ className, children, headingLevel, ...props }: AccordionTriggerProps): react.JSX.Element;
76
79
  declare function AccordionContent({ className, children, ...props }: ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>): react.JSX.Element;
77
80
 
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
81
  type AlertProps = Omit<ComponentPropsWithoutRef<'div'>, 'title'> & VariantProps<typeof alert> & {
129
82
  title?: ReactNode;
130
83
  /**
131
- * Sustituye el glifo mono de la variante. Nunca un emoji: si necesitas otra
132
- * cosa, es un SVG de `glyphs`.
84
+ * Replaces the variant's mono glyph. Never an emoji: if you need something
85
+ * else, it is an SVG from `glyphs`.
133
86
  */
134
87
  icon?: ReactNode;
135
88
  };
136
- declare function Alert({ className, variant, enfasis, title, icon, children, ...props }: AlertProps): react.JSX.Element;
89
+ declare function Alert({ className, variant, emphasis, title, icon, children, ...props }: AlertProps): react.JSX.Element;
137
90
 
138
91
  /**
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».
92
+ * The destructive confirmation. It is NOT a `Dialog` with different text, which
93
+ * is why it lives in its own file and on its own Radix primitive.
94
+ *
95
+ * Three differences, and all three matter at the moment somebody is about to
96
+ * delete an article:
97
+ *
98
+ * 1. The role is `alertdialog`, not `dialog`. A screen reader announces it
99
+ * with the description included, without waiting for you to navigate to it.
100
+ * 2. Initial focus goes to CANCEL, not to the first element. Whoever hits
101
+ * Enter out of inertia deletes nothing. Radix does this on its own only if
102
+ * the cancel exists, which is why `AlertDialogCancel` is not optional in
103
+ * practice.
104
+ * 3. It does NOT close on outside click and has no X. Leaving a confirmation
105
+ * is a decision, not a slip: you have to say no.
106
+ *
107
+ * The confirm button is NOT red, and since 0.6.0 that is a choice and no longer
108
+ * the absence of an option: `Button` has `destructive`, and here it is still not
109
+ * used. Everything above already carries the gravity — a title that says what is
110
+ * about to happen, focus on cancel, no closing by clicking outside — and a red
111
+ * button on top of that is shouting. What communicates the gravity is the text:
112
+ * «Borrar el artículo», not «Aceptar».
113
+ *
114
+ * `destructive` is for the destructive button that has none of that around it:
115
+ * a table row, a toolbar. See `docs/decisions.md` § 21.
157
116
  */
158
117
  declare const AlertDialog: react.FC<AlertDialogPrimitive.AlertDialogProps>;
159
118
  declare const AlertDialogTrigger: react.ForwardRefExoticComponent<AlertDialogPrimitive.AlertDialogTriggerProps & react.RefAttributes<HTMLButtonElement>>;
160
119
  declare function AlertDialogOverlay({ className, ...props }: ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>): react.JSX.Element;
161
- /** Sin entrada animada, igual que `Dialog`: aparece donde va a quedarse. */
120
+ /** No entrance animation, same as `Dialog`: it appears where it will stay. */
162
121
  declare function AlertDialogContent({ className, children, ...props }: ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>): react.JSX.Element;
163
122
  declare function AlertDialogHeader({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
164
123
  /**
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.
124
+ * Cancel to the LEFT of confirm on desktop and BELOW it on mobile, which is what
125
+ * `flex-col-reverse` gives: the DOM order puts cancel first — that is where focus
126
+ * goes — and in a column the thumb finds it where it should be without changing
127
+ * the tab order.
169
128
  */
170
129
  declare function AlertDialogFooter({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
171
130
  declare function AlertDialogTitle({ className, ...props }: ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>): react.JSX.Element;
172
131
  /**
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.
132
+ * What is lost, spelled out. The `alertdialog` role makes this get announced up
133
+ * front, so «this action cannot be undone» does not go here on its own: what
134
+ * goes here is what gets deleted and what it takes down with it.
176
135
  */
177
136
  declare function AlertDialogDescription({ className, ...props }: ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>): react.JSX.Element;
178
- /** El que se lleva el foco al abrir. */
137
+ /** The one that takes focus on open. */
179
138
  declare function AlertDialogCancel({ className, ...props }: ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>): react.JSX.Element;
180
139
  declare function AlertDialogAction({ className, ...props }: ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>): react.JSX.Element;
181
140
 
182
- declare const avatar: (props?: ({
183
- size?: "sm" | "md" | "lg" | "xl" | null | undefined;
184
- } & class_variance_authority_types.ClassProp) | undefined) => string;
185
141
  type AvatarProps = ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & VariantProps<typeof avatar>;
186
142
  /**
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.
143
+ * One for everything: the author's photo and anyone else's in the system. There
144
+ * is no separate `brand/Avatar` — a profile photo wearing the brand's skin is
145
+ * exactly this with a different `src`.
190
146
  */
191
147
  declare function Avatar({ className, size, ...props }: AvatarProps): react.JSX.Element;
192
148
  declare function AvatarImage({ className, ...props }: ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>): react.JSX.Element;
193
- /** Iniciales mientras la imagen carga, o cuando no hay imagen. */
149
+ /** Initials while the image loads, or when there is no image. */
194
150
  declare function AvatarFallback({ className, ...props }: ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>): react.JSX.Element;
195
151
  type AvatarUploadProps = Omit<ComponentPropsWithoutRef<'div'>, 'onSelect' | 'children'> & VariantProps<typeof avatar> & {
196
- /** La imagen actual, ya subida. La previsualización local la gana mientras dure. */
152
+ /** The current image, already uploaded. The local preview beats it while it lasts. */
197
153
  src?: string | undefined;
198
- /** Iniciales mientras no hay imagen. */
154
+ /** Initials while there is no image. */
199
155
  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. */
156
+ /** Fires with the chosen file. The upload is the project's job. */
157
+ onSelectFile?: ((file: File) => void) | undefined;
158
+ /** What the system dialog accepts. */
203
159
  accept?: string;
204
- /** Nombre accesible del control. Es lo único que lo nombra: no hay texto visible. */
160
+ /** The control's accessible name. It is the only thing naming it: there is no visible text. */
205
161
  label?: string;
206
162
  disabled?: boolean | undefined;
207
163
  };
208
164
  /**
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.
165
+ * The avatar you can change. `Avatar` displays; this one also lets you pick.
166
+ *
167
+ * It is presentational, like `NewsletterForm`: it emits `onSelectFile` with the
168
+ * `File` and its job ends there. The upload does not belong here — every project
169
+ * has its own storage and its own endpoint — and a component doing the `POST`
170
+ * would be infrastructure, which is the third entry criterion and the one most
171
+ * often skipped.
172
+ *
173
+ * The preview is LOCAL and does not wait for the upload to finish. It is the
174
+ * difference between a control that responds and one that looks broken: seconds
175
+ * can pass between picking the file and the server returning the URL, and with
176
+ * no preview the avatar keeps the old photo as though nothing had happened. The
177
+ * `objectURL` is revoked on change and on unmount; not revoking it is a memory
178
+ * leak that does not show its face until the tenth photo.
179
+ *
180
+ * The control is a `<label>` with a hidden `<input type="file">` inside, not a
181
+ * `<button>` firing a synthetic click. The real input brings the system dialog,
182
+ * the browser's drag-and-drop and keyboard focus; the fake button has to be
183
+ * rebuilt from scratch and something is always missing.
227
184
  */
228
185
  declare function AvatarUpload({ src, fallback, onSelectFile, accept, label, size, disabled, className, ...props }: AvatarUploadProps): react.JSX.Element;
229
186
 
230
187
  /**
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.
245
- */
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.
188
+ * Three badge families, three components. Why there are three shapes and not one
189
+ * is in `variants/badge.ts`, next to the classes that make them.
261
190
  */
262
- declare const badge: (props?: ({
263
- variant?: "error" | "accent" | "success" | "warning" | "neutral" | "warm" | null | undefined;
264
- } & class_variance_authority_types.ClassProp) | undefined) => string;
265
191
  type BadgeProps = ComponentPropsWithoutRef<'span'> & VariantProps<typeof badge>;
266
192
  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
193
  type CategoryBadgeProps = ComponentPropsWithoutRef<'span'> & {
282
- /** Filtro seleccionado: arena sólido con tinta encima. */
194
+ /** Selected filter: solid sand with ink on top. */
283
195
  active?: boolean | undefined;
284
196
  };
285
197
  declare function CategoryBadge({ className, active, ...props }: CategoryBadgeProps): react.JSX.Element;
286
198
  type MetricBadgeProps = ComponentPropsWithoutRef<'span'> & {
287
- /** Añade el aro de hairline. Por defecto la métrica va sin caja. */
199
+ /** Adds the hairline ring. By default a metric carries no box. */
288
200
  boxed?: boolean | undefined;
289
201
  };
290
202
  declare function MetricBadge({ className, boxed, ...props }: MetricBadgeProps): react.JSX.Element;
291
203
 
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
204
  type ButtonProps = ComponentPropsWithoutRef<'button'> & VariantProps<typeof button> & {
321
- /** Renderiza el hijo en vez de un `<button>`, para envolver un enlace. */
205
+ /** Renders the child instead of a `<button>`, to wrap a link. */
322
206
  asChild?: boolean;
323
- /** Deshabilita y anuncia `aria-busy`. Incompatible con `asChild`. */
207
+ /** Disables and announces `aria-busy`. Incompatible with `asChild`. */
324
208
  loading?: boolean;
325
- /** Glifo SVG antes del texto. Se oculta mientras carga. */
209
+ /** SVG glyph before the text. Hidden while loading. */
326
210
  icon?: ReactNode;
327
211
  };
328
212
  declare function Button({ className, variant, size, asChild, loading, icon, children, disabled, ...props }: ButtonProps): react.JSX.Element;
329
213
 
330
214
  type CalendarProps = ComponentProps<typeof DayPicker> & {
331
215
  /**
332
- * Estira el calendario hasta ocupar todo el ancho de su contenedor, con las
333
- * celdas repartiéndoselo a partes iguales.
216
+ * Stretches the calendar to fill its container's whole width, with the cells
217
+ * splitting it evenly.
334
218
  *
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.
219
+ * Off, the calendar measures whatever its cells measure — 36px each — and that
220
+ * is what you want inside a `Popover`, where stretching it would leave a huge
221
+ * bubble. On, it is a planner's month view, which fills the page.
338
222
  *
339
- * Con varios meses, cada uno se lleva una fracción igual del ancho.
223
+ * With several months, each takes an equal fraction of the width.
340
224
  */
341
225
  fullWidth?: boolean | undefined;
342
226
  };
343
227
  /**
344
- * Calendario mensual navegable, sobre `react-day-picker`.
228
+ * A navigable month calendar, on top of `react-day-picker`.
345
229
  *
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.
230
+ * It is the library's only heavy dependency and it was let in knowingly: the
231
+ * content planner's calendar cannot be solved with the native control. For
232
+ * picking a date inside a form there is `DateField`, which drags nothing along.
350
233
  *
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.
234
+ * `animate` stays off — that is its default — so the month change does not
235
+ * slide. Days are marked with color and border, like everything else.
353
236
  *
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.
237
+ * The language defaults to Spanish because all five projects are; it is changed
238
+ * by passing another date-fns `locale`.
356
239
  *
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.
240
+ * It does not ship the library's `style.css`: every class comes from here, so
241
+ * the consumer need not import third-party CSS or fight its specificity.
360
242
  */
361
243
  declare function Calendar({ className, classNames, showOutsideDays, fullWidth, ...props }: CalendarProps): react.JSX.Element;
362
244
 
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
245
  declare function Card({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
382
246
  declare function CardHeader({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
383
247
  declare function CardTitle({ className, ...props }: ComponentPropsWithoutRef<'h3'>): react.JSX.Element;
@@ -389,41 +253,40 @@ type CheckboxProps = ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>;
389
253
  declare function Checkbox({ className, ...props }: CheckboxProps): react.JSX.Element;
390
254
 
391
255
  /**
392
- * Código en línea, dentro de prosa.
256
+ * Inline code, inside prose.
393
257
  *
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.
258
+ * It exists because it did not: every consumer wrote `<code className="font-mono">`
259
+ * by hand, and the brand story did it four times in a single paragraph. A bare
260
+ * `<code>` inherits the paragraph's size, so inside `body` (18px) you saw an
261
+ * 18px mono that the document has nowhere.
398
262
  *
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.
263
+ * It is not `CodeBlock`. The block is an island of dark theme over hull with a
264
+ * bar and a copy button; this is one word inside a sentence, which is why it
265
+ * stays on the page surface instead of inverting the theme.
402
266
  */
403
267
  type CodeProps = ComponentPropsWithoutRef<'code'>;
404
268
  declare function Code({ className, ...props }: CodeProps): react.JSX.Element;
405
269
 
406
270
  type DateFieldProps = Omit<ComponentPropsWithoutRef<'input'>, 'type'> & {
407
271
  invalid?: boolean | undefined;
408
- /** Añade la hora al campo. Es el `datetime-local` nativo. */
272
+ /** Adds the time to the field. It is the native `datetime-local`. */
409
273
  withTime?: boolean | undefined;
410
274
  };
411
275
  /**
412
- * Un campo de fecha sobre el control nativo, no sobre un calendario propio.
276
+ * A date field on the native control, not on a calendar of our own.
413
277
  *
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.
278
+ * It is a deliberate decision: `react-day-picker` would have been the library's
279
+ * first heavy dependency, with its own CSS and its own month-change animation —
280
+ * which the system does not allow. The native control brings the OS keyboard,
281
+ * the format matching the user's language and screen-reader support for free,
282
+ * which is more than a bespoke calendar gives without work.
420
283
  *
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.
284
+ * It covers picking a date inside a form. A navigable month calendar is a
285
+ * different thing and lives in the project that needs it.
423
286
  *
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.
287
+ * The native picker icon is tinted through `color-scheme`, which is the only
288
+ * thing the browser lets you control: it is tied to the active mode so no little
289
+ * white square shows up over abyss.
427
290
  */
428
291
  declare function DateField({ className, invalid, withTime, ...props }: DateFieldProps): react.JSX.Element;
429
292
 
@@ -431,7 +294,7 @@ declare const Dialog: react.FC<DialogPrimitive.DialogProps>;
431
294
  declare const DialogTrigger: react.ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & react.RefAttributes<HTMLButtonElement>>;
432
295
  declare const DialogClose: react.ForwardRefExoticComponent<DialogPrimitive.DialogCloseProps & react.RefAttributes<HTMLButtonElement>>;
433
296
  declare function DialogOverlay({ className, ...props }: ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>): react.JSX.Element;
434
- /** Sin entrada animada: no hay escala ni desplazamiento en el sistema. */
297
+ /** No entrance animation: there is no scale or displacement in the system. */
435
298
  declare function DialogContent({ className, children, ...props }: ComponentPropsWithoutRef<typeof DialogPrimitive.Content>): react.JSX.Element;
436
299
  declare function DialogHeader({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
437
300
  declare function DialogFooter({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
@@ -443,7 +306,7 @@ declare const DropdownMenuTrigger: react.ForwardRefExoticComponent<DropdownMenuP
443
306
  declare const DropdownMenuGroup: react.ForwardRefExoticComponent<DropdownMenuPrimitive.DropdownMenuGroupProps & react.RefAttributes<HTMLDivElement>>;
444
307
  declare const DropdownMenuRadioGroup: react.ForwardRefExoticComponent<DropdownMenuPrimitive.DropdownMenuRadioGroupProps & react.RefAttributes<HTMLDivElement>>;
445
308
  declare const DropdownMenuSub: react.FC<DropdownMenuPrimitive.DropdownMenuSubProps>;
446
- /** Sin animación de entrada: el menú aparece, no se despliega. */
309
+ /** No entrance animation: the menu appears, it does not unfold. */
447
310
  declare function DropdownMenuContent({ className, sideOffset, ...props }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>): react.JSX.Element;
448
311
  declare function DropdownMenuItem({ className, ...props }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>): react.JSX.Element;
449
312
  declare function DropdownMenuCheckboxItem({ className, children, ...props }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>): react.JSX.Element;
@@ -454,7 +317,7 @@ declare function DropdownMenuSubTrigger({ className, children, ...props }: Compo
454
317
  declare function DropdownMenuSubContent({ className, ...props }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>): react.JSX.Element;
455
318
 
456
319
  type InputProps = ComponentPropsWithoutRef<'input'> & {
457
- /** Marca el control como inválido y tiñe el borde. */
320
+ /** Marks the control as invalid and tints the border. */
458
321
  invalid?: boolean;
459
322
  };
460
323
  declare function Input({ className, invalid, ...props }: InputProps): react.JSX.Element;
@@ -474,36 +337,37 @@ declare const Popover: react.FC<PopoverPrimitive.PopoverProps>;
474
337
  declare const PopoverTrigger: react.ForwardRefExoticComponent<PopoverPrimitive.PopoverTriggerProps & react.RefAttributes<HTMLButtonElement>>;
475
338
  declare const PopoverAnchor: react.ForwardRefExoticComponent<PopoverPrimitive.PopoverAnchorProps & react.RefAttributes<HTMLDivElement>>;
476
339
  /**
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.
340
+ * Radix puts `role="dialog"` on the content, and a dialog with no accessible
341
+ * name says nothing to someone navigating with a screen reader. That is why the
342
+ * type demands one of the two: `aria-label` with the text, or `aria-labelledby`
343
+ * pointing at the title already visible on screen. It cannot be forgotten
344
+ * because it does not compile.
481
345
  */
482
- type Etiquetado = {
346
+ type Labelled = {
483
347
  'aria-label': string;
484
348
  'aria-labelledby'?: never;
485
349
  } | {
486
350
  'aria-labelledby': string;
487
351
  'aria-label'?: never;
488
352
  };
489
- type PopoverContentProps = ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & Etiquetado;
490
- /** Sin animación de entrada: aparece donde va a quedarse, como el resto. */
353
+ type PopoverContentProps = ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & Labelled;
354
+ /** No entrance animation: it appears where it will stay, like the rest. */
491
355
  declare function PopoverContent({ className, align, sideOffset, ...props }: PopoverContentProps): react.JSX.Element;
492
356
 
493
357
  type ProgressProps = ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
494
358
  /**
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.
359
+ * The bar's accessible name. It is mandatory on purpose: a progress bar with
360
+ * no name does not say what the progress is about, and no other part of the
361
+ * component can deduce it.
498
362
  */
499
363
  label: string;
500
- /** Arena en vez de bioluz, para progreso de curso. */
364
+ /** Sand instead of biolume, for course progress. */
501
365
  tone?: 'accent' | 'warm';
502
366
  };
503
367
  /**
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.
368
+ * The indicator's width changes, it is not animated: the system animates neither
369
+ * scale nor displacement. `transition-standard` only covers color and border, so
370
+ * the width jump is immediate even with the class in place.
507
371
  */
508
372
  declare function Progress({ className, value, label, tone, ...props }: ProgressProps): react.JSX.Element;
509
373
 
@@ -516,7 +380,7 @@ declare const Select: react.FC<SelectPrimitive.SelectProps>;
516
380
  declare const SelectGroup: react.ForwardRefExoticComponent<SelectPrimitive.SelectGroupProps & react.RefAttributes<HTMLDivElement>>;
517
381
  declare const SelectValue: react.ForwardRefExoticComponent<SelectPrimitive.SelectValueProps & react.RefAttributes<HTMLSpanElement>>;
518
382
  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. */
383
+ /** No entrance animation: the menu appears, it does not unfold. */
520
384
  declare function SelectContent({ className, children, position, ...props }: ComponentPropsWithoutRef<typeof SelectPrimitive.Content>): react.JSX.Element;
521
385
  declare function SelectLabel({ className, ...props }: ComponentPropsWithoutRef<typeof SelectPrimitive.Label>): react.JSX.Element;
522
386
  declare function SelectItem({ className, children, ...props }: ComponentPropsWithoutRef<typeof SelectPrimitive.Item>): react.JSX.Element;
@@ -524,8 +388,8 @@ declare function SelectSeparator({ className, ...props }: ComponentPropsWithoutR
524
388
 
525
389
  type SeparatorProps = ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>;
526
390
  /**
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.
391
+ * `hairline`, not `border`: a division between pieces of content is subtle by
392
+ * definition. To delimit a control there is `border`, which is another token.
529
393
  */
530
394
  declare function Separator({ className, orientation, decorative, ...props }: SeparatorProps): react.JSX.Element;
531
395
 
@@ -533,14 +397,14 @@ declare const Sheet: react.FC<DialogPrimitive.DialogProps>;
533
397
  declare const SheetTrigger: react.ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & react.RefAttributes<HTMLButtonElement>>;
534
398
  declare const SheetClose: react.ForwardRefExoticComponent<DialogPrimitive.DialogCloseProps & react.RefAttributes<HTMLButtonElement>>;
535
399
  /**
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.
400
+ * The second and last exception to «no displacement», approved knowingly: a
401
+ * panel entering from an edge slides by definition, and held still it would be
402
+ * an off-centre modal.
403
+ *
404
+ * It lasts `--duration-standard` with `--ease-standard`, that is, the same time
405
+ * and the same curve as any color change in the system, so it introduces no new
406
+ * timing. It sits behind `motion-safe`: whoever asked for less motion sees it
407
+ * appear without sliding.
544
408
  */
545
409
  declare const panel: (props?: ({
546
410
  side?: "bottom" | "left" | "right" | "top" | null | undefined;
@@ -554,35 +418,36 @@ declare function SheetTitle({ className, ...props }: ComponentPropsWithoutRef<ty
554
418
  declare function SheetDescription({ className, ...props }: ComponentPropsWithoutRef<typeof DialogPrimitive.Description>): react.JSX.Element;
555
419
 
556
420
  /**
557
- * Barrido de 1.4s lineal, del documento.
421
+ * A 1.4s linear sweep, from the document.
558
422
  *
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».
423
+ * It is the THIRD and last exception to «the system does not animate», alongside
424
+ * the button spinner and the side panel. All three are feedback about PROGRESS
425
+ * and not about state, which is the criterion: a block that is still and a block
426
+ * that will never load look exactly the same, and the skeleton exists to say
427
+ * «this is on its way», not «this is empty».
564
428
  *
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.
429
+ * It sits behind `motion-safe`, so it switches itself off for anyone who asked
430
+ * for less motion — and what is left is the block on `surfaceRaised`, which
431
+ * still communicates the shape of what is coming.
568
432
  *
569
- * `still` lo apaga a mano, para las tablas largas: veinte filas barriendo a la
570
- * vez es un estroboscopio, no una carga.
433
+ * `still` turns it off by hand, for long tables: twenty rows sweeping at once is
434
+ * a strobe, not a load.
571
435
  */
572
436
  type SkeletonProps = ComponentPropsWithoutRef<'div'> & {
573
- /** Apaga el barrido. Para listas largas, donde muchas a la vez marean. */
437
+ /** Turns the sweep off. For long lists, where many at once are dizzying. */
574
438
  still?: boolean | undefined;
575
439
  };
576
440
  declare function Skeleton({ className, still, ...props }: SkeletonProps): react.JSX.Element;
577
441
 
578
442
  type SwitchProps = ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>;
579
443
  /**
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.
444
+ * The knob changes position, but is not animated while doing so: the position IS
445
+ * the state, not a transition. The only thing that transitions is the track's
446
+ * color.
582
447
  */
583
448
  declare function Switch({ className, ...props }: SwitchProps): react.JSX.Element;
584
449
 
585
- /** El contenedor scrollea en horizontal: la página nunca lo hace. */
450
+ /** The container scrolls horizontally: the page never does. */
586
451
  declare function Table({ className, ...props }: ComponentPropsWithoutRef<'table'>): react.JSX.Element;
587
452
  declare function TableHeader({ className, ...props }: ComponentPropsWithoutRef<'thead'>): react.JSX.Element;
588
453
  declare function TableBody({ className, ...props }: ComponentPropsWithoutRef<'tbody'>): react.JSX.Element;
@@ -605,69 +470,72 @@ declare function Textarea({ className, invalid, ...props }: TextareaProps): reac
605
470
 
606
471
  declare const ToastAction: react.ForwardRefExoticComponent<ToastPrimitive.ToastActionProps & react.RefAttributes<HTMLButtonElement>>;
607
472
  declare const toast$1: (props?: ({
608
- variant?: "error" | "success" | "neutral" | null | undefined;
473
+ variant?: "error" | "neutral" | "success" | null | undefined;
609
474
  } & class_variance_authority_types.ClassProp) | undefined) => string;
610
475
  type ToastProps = ComponentPropsWithoutRef<typeof ToastPrimitive.Root> & VariantProps<typeof toast$1>;
611
476
 
612
477
  /**
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.
478
+ * The imperative face of Radix's `Toast`: `toast('Guardado')` from anywhere,
479
+ * without threading the notice through props down to the component that fires
480
+ * it.
481
+ *
482
+ * It exists because two projects were pulling in `sonner` for this. `Toast`
483
+ * covers the same role and has a different shape: Radix is declarative with a
484
+ * provider, and to show a notice from a `fetch`'s `catch` you have to lift state
485
+ * up to where the provider lives. That is exactly what `sonner` avoids, and it
486
+ * is a real need, not an API preference.
487
+ *
488
+ * The alternative was for the two projects to adapt. It was ruled out: the
489
+ * notice is fired by the data layer, which has no component nearby to hang a
490
+ * `useState` on — nor should it.
491
+ *
492
+ * What was NOT copied from `sonner` is the whole catalogue. There is no
493
+ * `toast.promise`, no `toast.custom`, no configurable positions, no stacking
494
+ * with perspective: they are four variations on the same thing and each one is
495
+ * public surface that has to be maintained. What is here are the three shapes
496
+ * the projects actually use — neutral, success, error — plus `dismiss`, and
497
+ * nothing else.
498
+ *
499
+ * State lives in a module, not in a context, because the whole point is being
500
+ * callable from outside the tree. `Toaster` subscribes with
501
+ * `useSyncExternalStore`, which is React 19's way of reading external state
502
+ * without effects or cascading renders.
636
503
  */
637
504
  type ToastVariant = NonNullable<ToastProps['variant']>;
638
505
  type ToastOptions = {
639
- /** La primera línea, en negrita. Sin ella el aviso es una sola frase. */
506
+ /** The first line, in bold. Without it the notice is a single sentence. */
640
507
  title?: ReactNode;
641
508
  description?: ReactNode;
642
509
  variant?: ToastVariant;
643
- /** Milisegundos en pantalla. `Infinity` lo deja hasta que se cierre a mano. */
510
+ /** Milliseconds on screen. `Infinity` leaves it until it is closed by hand. */
644
511
  duration?: number;
645
- /** Un `ToastAction`, si el aviso ofrece deshacer. */
512
+ /** A `ToastAction`, if the notice offers an undo. */
646
513
  action?: ReactNode;
647
514
  };
648
- type Lanzador = {
649
- (mensaje: ReactNode, opciones?: ToastOptions): string;
650
- success: (mensaje: ReactNode, opciones?: ToastOptions) => string;
651
- error: (mensaje: ReactNode, opciones?: ToastOptions) => string;
515
+ type Trigger = {
516
+ (message: ReactNode, options?: ToastOptions): string;
517
+ success: (message: ReactNode, options?: ToastOptions) => string;
518
+ error: (message: ReactNode, options?: ToastOptions) => string;
652
519
  dismiss: (id?: string) => void;
653
520
  };
654
521
  /**
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.
522
+ * Fires a notice. It returns its id, which is what you keep in order to close it
523
+ * by hand — the «guardando…» case that gets replaced when the request finishes.
657
524
  */
658
- declare const toast: Lanzador;
525
+ declare const toast: Trigger;
659
526
  type ToasterProps = {
660
- /** Cuánto dura un aviso que no dice lo contrario. */
527
+ /** How long a notice lasts when it does not say otherwise. */
661
528
  duration?: number;
662
529
  /**
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.
530
+ * The name of the landmark Radix creates for the notices region. It is
531
+ * translated because a screen reader reads it, and Radix's default is in
532
+ * English.
665
533
  */
666
534
  label?: string;
667
535
  };
668
536
  /**
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.
537
+ * Mount it ONCE, as high in the tree as possible. Two mounted `Toaster`s paint
538
+ * every notice twice: the list belongs to the module, not to the instance.
671
539
  */
672
540
  declare function Toaster({ duration, label }: ToasterProps): react.JSX.Element;
673
541
 
@@ -676,126 +544,148 @@ declare const Tooltip: react.FC<TooltipPrimitive.TooltipProps>;
676
544
  declare const TooltipTrigger: react.ForwardRefExoticComponent<TooltipPrimitive.TooltipTriggerProps & react.RefAttributes<HTMLButtonElement>>;
677
545
  declare function TooltipContent({ className, sideOffset, ...props }: ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>): react.JSX.Element;
678
546
 
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. */
547
+ /** Allowed tags. The list is short on purpose: this is not a styled `div`. */
548
+ type Label = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'span' | 'strong' | 'em' | 'figcaption' | 'caption' | 'legend' | 'dt' | 'dd' | 'li';
549
+ type TextProps = Omit<ComponentPropsWithoutRef<'p'>, 'color'> & VariantProps<typeof text> & {
550
+ /** HTML tag. Defaults to whichever one matches the scale. */
551
+ as?: Label;
552
+ /** Renders the child instead of creating an element, to wrap a link. */
689
553
  asChild?: boolean;
690
554
  /**
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.
555
+ * Clamps the line to 68ch. On by default for `body`, the only scale meant
556
+ * to be read in long paragraphs.
693
557
  */
694
558
  measure?: boolean;
695
559
  };
696
560
  declare function Text({ className, variant, tone, as, asChild, measure, children, ...props }: TextProps): react.JSX.Element;
697
561
 
698
- type TarjetaProps = ComponentPropsWithoutRef<'a'> & {
562
+ /**
563
+ * The shared shell of the cards. The class list lives in `variants/card.ts`,
564
+ * which brings no React, so `links` can read it instead of copying it.
565
+ */
566
+ type CardShellProps = ComponentPropsWithoutRef<'a'> & {
699
567
  /**
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.
568
+ * Renders the child instead of an `<a>`. It is how Next's or Astro's `Link`
569
+ * plugs in without the library depending on any router.
702
570
  */
703
571
  asChild?: boolean | undefined;
704
572
  children: ReactNode;
705
573
  };
706
574
 
707
- type ArticleCardProps = Omit<TarjetaProps, 'children' | 'title'> & {
575
+ type ArticleCardProps = Omit<CardShellProps, 'children' | 'title'> & {
708
576
  title: ReactNode;
709
- /** Entradilla. Se corta a dos líneas para que la rejilla no se desalinee. */
577
+ /** Standfirst. Clamped to two lines so the grid does not fall out of line. */
710
578
  excerpt?: ReactNode;
711
- /** Fecha ya formateada por el proyecto: la librería no impone locale. */
579
+ /** Date already formatted by the project: the library imposes no locale. */
712
580
  date?: ReactNode;
713
- /** Valor de `datetime` del `<time>`, en ISO. */
581
+ /** The `<time>` element's `datetime` value, in ISO. */
714
582
  dateTime?: string | undefined;
715
583
  readingMinutes?: number | undefined;
716
584
  tags?: readonly string[] | undefined;
717
585
  /**
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.
586
+ * The headline level. `h3` by default: a lone card in a grid does not earn a
587
+ * level its position does not give it.
720
588
  *
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.
589
+ * Restricted to two values on purpose. The listing page — where the cards ARE
590
+ * the section's main heading — needs `h2`, and that intent was lost with a
591
+ * constant; opening it up to `h4` or `h5`, on the other hand, is an invitation
592
+ * to skip levels, which is the failure the constant was preventing.
725
593
  */
726
594
  headingLevel?: 2 | 3;
595
+ /**
596
+ * Renders each tag through the child, so an E2E suite can reach it.
597
+ *
598
+ * The tags are the one composed part a project could not get at: they arrive
599
+ * as strings and the component turns them into badges, so a test had to select
600
+ * them by structure — `article > div > span` — or by a style class. The second
601
+ * one already broke the blog's suite once, because a style class is not a
602
+ * contract: it changes when the style changes.
603
+ *
604
+ * The library keeps the classes and the rule — a tag is a category pill, and
605
+ * that does not become negotiable — and hands over only the element and its
606
+ * attributes:
607
+ *
608
+ * tagAsChild={({ tag }) => <span data-testid={`tag-${tag}`}>{tag}</span>}
609
+ *
610
+ * It is the shape `linkAsChild` already has in `Breadcrumb` and
611
+ * `TableOfContents`, and it is deliberately the same: one idiom for «the
612
+ * project supplies the element, the library supplies the styling».
613
+ */
614
+ tagAsChild?: ((props: {
615
+ tag: string;
616
+ children: ReactNode;
617
+ }) => ReactNode) | undefined;
727
618
  };
728
619
  /**
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.
620
+ * The metadata line uses `meta` and not `eyebrow`: `18 ago 2026 · 8 min de
621
+ * lectura` is a datum, not an overline, and in small caps it was neither.
732
622
  *
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`.
623
+ * The tags are the CATEGORY family — a lowercase sand pill — not the status one.
624
+ * A slug is something you read as `engineering-culture`.
735
625
  */
736
- declare function ArticleCard({ title, excerpt, date, dateTime, readingMinutes, tags, headingLevel, className, ...props }: ArticleCardProps): react.JSX.Element;
626
+ declare function ArticleCard({ title, excerpt, date, dateTime, readingMinutes, tags, headingLevel, tagAsChild, className, ...props }: ArticleCardProps): react.JSX.Element;
737
627
 
738
628
  /**
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.
629
+ * The byline at the foot of the article: 52px avatar, 15/500 name and the role
630
+ * in muted mono. Three data points, not one more.
741
631
  *
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.
632
+ * It does NOT take a mascot face, and that is not an oversight: it is the same
633
+ * rule as `PageHeader`. A face here would be humour in the exact place where the
634
+ * reader is deciding whether the author knows what they are talking about —
635
+ * precisely what the manual's contract excludes.
746
636
  *
747
- * El avatar del manual es la cabeza dentro de un círculo azul tiburón sólido,
748
- * nunca la cara expresiva suelta.
637
+ * The manual's avatar is the head inside a solid shark-blue circle, never the
638
+ * expressive face on its own.
749
639
  */
750
640
  type AuthorCardProps = Omit<ComponentPropsWithoutRef<'div'>, 'role'> & {
751
641
  name: string;
752
- /** El rol. Va en mono: es un dato, no una frase. */
642
+ /** The role. It goes in mono: it is a datum, not a sentence. */
753
643
  role?: ReactNode;
754
- /** URL del avatar. Sin ella se muestran las iniciales. */
644
+ /** The avatar's URL. Without it the initials are shown. */
755
645
  src?: string | undefined;
756
- /** Una o dos frases. Se corta a 68ch sola. */
646
+ /** One or two sentences. It clamps itself to 68ch. */
757
647
  bio?: ReactNode;
758
- /** Enlaces o botón de contacto. */
648
+ /** Links or a contact button. */
759
649
  action?: ReactNode;
760
650
  };
761
651
  declare function AuthorCard({ name, role, src, bio, action, className, ...props }: AuthorCardProps): react.JSX.Element;
762
652
 
763
653
  /**
764
- * Migrado desde `eduardoalvarez.dev/src/components/audio-player/index.tsx`.
654
+ * Migrated from `eduardoalvarez.dev/src/components/audio-player/index.tsx`.
765
655
  *
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:
656
+ * The logic was not rewritten: the three modes, the floating player, the ±15s
657
+ * skips, the 1 → 1.25 → 1.5 → 1.75 → 2 speed cycle and the volume with mute are
658
+ * the same. What changed is the skin and the two dependencies a package cannot
659
+ * have:
770
660
  *
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.
661
+ * - the portfolio's `Icon` → the glyphs now live in `src/lib/glyphs.tsx`, with
662
+ * the same paths.
663
+ * - analytics' `trackEvent` → the `onFirstPlay` prop, which the consumer wires
664
+ * to whatever they use. It still fires exactly once per load.
775
665
  *
776
- * Y tres cosas que el sistema no permite:
666
+ * And three things the system does not allow:
777
667
  *
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.
668
+ * - The waveform no longer animates `scaleY`. The bars are still there and still
669
+ * tell playback from pause by opacity, but they do not scale.
670
+ * - The floating player appears and disappears instead of sliding.
671
+ * - The progress bar no longer interpolates its width.
782
672
  *
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`.
673
+ * The loading spinner's spin stays, with the same justification as in `Button`:
674
+ * it is feedback about progress, not about state, and it sits in `motion-safe`.
785
675
  */
786
676
  type AudioPlayerMode = 'full' | 'compact' | 'banner';
787
677
  type AudioPlayerProps = {
788
678
  src: string;
789
679
  title?: string;
790
680
  /**
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.
681
+ * `full` for podcast pages, `compact` for sidebars and `banner` for articles
682
+ * with narration. `compact` and `banner` also bring the floating player when
683
+ * the static one leaves the viewport.
794
684
  */
795
685
  mode?: AudioPlayerMode | undefined;
796
686
  /**
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.
687
+ * Called once per load, the first time the audio starts.
688
+ * This is where the project hooks up its analytics; the library ships none.
799
689
  */
800
690
  onFirstPlay?: ((title?: string) => void) | undefined;
801
691
  };
@@ -803,44 +693,44 @@ declare function AudioPlayer({ src, title, mode, onFirstPlay }: AudioPlayerProps
803
693
 
804
694
  type BlockquoteProps = Omit<ComponentPropsWithoutRef<'blockquote'>, 'cite'> & {
805
695
  children: ReactNode;
806
- /** Quién lo dijo. Se marca como `<cite>`. */
696
+ /** Who said it. Marked up as `<cite>`. */
807
697
  author?: ReactNode;
808
- /** Dónde lo dijo: charla, artículo, conversación. */
698
+ /** Where they said it: a talk, an article, a conversation. */
809
699
  source?: ReactNode;
810
700
  };
811
701
  /**
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.
702
+ * The side bar is `accent`, the interactive color, because a quotation is
703
+ * somebody else's voice entering the text. It carries no decorative quote marks:
704
+ * the system's glyphs are SVG, and an ornamental quote adds nothing the border
705
+ * and the indent do not already say.
816
706
  */
817
707
  declare function Blockquote({ children, author, source, className, ...props }: BlockquoteProps): react.JSX.Element;
818
708
 
819
709
  /**
820
- * La ruta como una ruta: `~ / artículos / cómo-escalar-un-equipo`.
710
+ * The path as a path: `~ / artículos / cómo-escalar-un-equipo`.
821
711
  *
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.
712
+ * The `~` is neither decoration nor a house icon: it is the filesystem's home,
713
+ * which is why the breadcrumb is mono and not sans. The separators use `border`
714
+ * (#22414F), the faintest token that still reads as a line.
825
715
  *
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.
716
+ * The last crumb is the current page, so it is not a link and carries
717
+ * `aria-current="page"`. That is the difference between an accessible breadcrumb
718
+ * and four consecutive links, one of which goes nowhere.
829
719
  */
830
- type Migaja = {
720
+ type Crumb = {
831
721
  label: ReactNode;
832
- /** Sin `href`, el tramo es texto. El último nunca debería llevarlo. */
722
+ /** With no `href`, the crumb is text. The last one should never carry it. */
833
723
  href?: string | undefined;
834
724
  };
835
725
  type BreadcrumbProps = Omit<ComponentPropsWithoutRef<'nav'>, 'children'> & {
836
- items: readonly Migaja[];
837
- /** Destino del `~`. Por defecto, la raíz del sitio. */
726
+ items: readonly Crumb[];
727
+ /** Where the `~` goes. The site root by default. */
838
728
  homeHref?: string;
839
- /** Etiqueta accesible del `~`, que si no se lee como una tilde suelta. */
729
+ /** Accessible label for the `~`, which otherwise reads as a stray tilde. */
840
730
  homeLabel?: string;
841
731
  /**
842
- * Renderiza los enlaces con el hijo, para enchufar el `Link` de Next o Astro.
843
- * Recibe cada `href` en el `props` del Slot.
732
+ * Renders the links through the child, to plug in Next's or Astro's `Link`.
733
+ * It receives each `href` in the Slot's `props`.
844
734
  */
845
735
  linkAsChild?: ((props: {
846
736
  href: string;
@@ -850,148 +740,176 @@ type BreadcrumbProps = Omit<ComponentPropsWithoutRef<'nav'>, 'children'> & {
850
740
  declare function Breadcrumb({ items, homeHref, homeLabel, linkAsChild, className, ...props }: BreadcrumbProps): react.JSX.Element;
851
741
 
852
742
  /**
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.
743
+ * `brand.hull` is «hull · outline and the background of code blocks», so a code
744
+ * block is dark in light mode too. That is why the root declares
745
+ * `data-theme="dark"`: everything inside — ink, hairline, accent — switches to
746
+ * the dark palette regardless of the page's theme. It is the system's only
747
+ * island of inverted theme, and it is deliberate.
858
748
  */
859
749
  type CodeBlockProps = Omit<ComponentPropsWithoutRef<'div'>, 'children'> & {
860
- /** El código ya resaltado, o texto plano. */
750
+ /** The already-highlighted code, or flat text. */
861
751
  children: ReactNode;
862
- /** Etiqueta del lenguaje. Se muestra en la barra superior. */
752
+ /** The language label. Shown in the top bar. */
863
753
  language?: string | undefined;
864
- /** Texto que se copia al portapapeles. Sin esto, no se muestra el botón. */
754
+ /** The text copied to the clipboard. Without it, the button is not shown. */
865
755
  copyText?: string | undefined;
866
756
  };
867
757
  declare function CodeBlock({ children, language, copyText, className, ...props }: CodeBlockProps): react.JSX.Element;
868
758
 
869
- type CourseCardProps = Omit<TarjetaProps, 'children' | 'title'> & {
759
+ type CourseCardProps = Omit<CardShellProps, 'children' | 'title'> & {
870
760
  title: ReactNode;
871
761
  summary?: ReactNode;
872
- /** Nivel, duración, número de lecciones: lo que el proyecto quiera listar. */
762
+ /** Level, duration, number of lessons: whatever the project wants to list. */
873
763
  meta?: readonly ReactNode[] | undefined;
874
- /** Etiqueta de estado: «próximamente», «gratis», «nuevo». */
764
+ /** Status label: «próximamente», «gratis», «nuevo». */
875
765
  status?: ReactNode;
876
766
  /**
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.
767
+ * Percentage completed. It only makes sense for someone already enrolled;
768
+ * when passed, the bar goes in sand, which is the color of course progress.
879
769
  */
880
770
  progress?: number | undefined;
881
771
  };
882
772
  declare function CourseCard({ title, summary, meta, status, progress, className, ...props }: CourseCardProps): react.JSX.Element;
883
773
 
884
774
  /**
885
- * La regla más importante de la mascota, por fin como código.
886
- *
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.
891
- *
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.
775
+ * The mascot's most important rule, finally as code.
776
+ *
777
+ * `catalog.ts` and `page-header/index.tsx` both cited it — «that is why
778
+ * `EmptyState` takes a face and `PageHeader` does not» — and the component did
779
+ * not exist, so the rule lived in a comment about a ghost component. Once the
780
+ * brand story started repeating the sentence, it was also a published promise.
781
+ *
782
+ * `page` keeps that rule intact and `expression` stays MANDATORY there: an empty
783
+ * state that IS the screen without a face is half the component. It is the only
784
+ * place, alongside the 404, the server error, course progress, celebration, the
785
+ * toast and the newsletter's «sin spam», where a face may appear.
786
+ *
787
+ * `inline` is the other situation, and it is a different one rather than a
788
+ * smaller one: the hole inside a table page or a dashboard widget, competing with
789
+ * a dozen elements around it. An admin panel has twenty of those on one screen,
790
+ * and twenty faces is not the humour contract, it is a zoo. The variant carries
791
+ * no face, and the type does not let one through. See `docs/decisions.md` § 27.
896
792
  */
897
- type EmptyStateProps = Omit<ComponentPropsWithoutRef<'div'>, 'title'> & {
898
- /** La cara. Obligatoria: sin ella esto es un párrafo centrado. */
899
- expresion: Cara;
793
+ type EmptyStateBase = Omit<ComponentPropsWithoutRef<'div'>, 'title'> & {
900
794
  title: ReactNode;
901
- /** Una línea explicando qué falta o qué hacer. */
795
+ /** One line explaining what is missing or what to do. */
902
796
  description?: ReactNode;
903
- /** La acción que saca del estado vacío. Normalmente un botón terciario. */
797
+ /** The action that gets you out of the empty state. Usually a tertiary button. */
904
798
  action?: ReactNode;
905
- /** Dónde se sirven los PNG de la marca. */
906
- basePath?: string | undefined;
907
799
  };
908
- declare function EmptyState({ expresion, title, description, action, basePath, className, ...props }: EmptyStateProps): react.JSX.Element;
800
+ type EmptyStateProps = EmptyStateBase & ({
801
+ /** `page`, the default: the empty state IS the screen or the section, and it carries the face. */
802
+ variant?: 'page' | undefined;
803
+ /**
804
+ * The face. Mandatory on `page` and impossible on `inline` — the props
805
+ * are a union, so the generated table cannot show a per-variant «req.»
806
+ * and the sentence has to carry it. Without it, `page` is a centred
807
+ * paragraph.
808
+ */
809
+ expression: Face;
810
+ /** Where the brand PNGs are served from. */
811
+ basePath?: string | undefined;
812
+ icon?: never;
813
+ } | {
814
+ /** `inline`: the hole inside a table or a widget. No face, and no way to pass one. */
815
+ variant: 'inline';
816
+ /**
817
+ * A glyph above the line. It measures 1em and inherits `currentColor`,
818
+ * like `Stat`'s: the project passes its own and sizes it, because the
819
+ * system has no icon library and is not getting one.
820
+ */
821
+ icon?: ReactNode;
822
+ expression?: never;
823
+ basePath?: never;
824
+ });
825
+ declare function EmptyState(props: EmptyStateProps): react.JSX.Element;
909
826
 
910
827
  /**
911
- * El calendario con eventos: verlos, crearlos, editarlos y borrarlos.
828
+ * The calendar with events: seeing them, creating, editing and deleting them.
912
829
  *
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.
830
+ * `Calendar` is the primitive and remains a grid of dates: it serves to PICK a
831
+ * day and knows nothing about content. This is the schedule, which is why it
832
+ * lives in `components/` and not next to that one — it has state, it has a form
833
+ * and it encodes what a day with things in it looks like.
917
834
  *
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.
835
+ * It is presentational, like the rest of the library: it takes `events` and
836
+ * emits `onCreateEvent`, `onUpdateEvent` and `onDeleteEvent`. It stores nothing,
837
+ * calls no API and generates no ids — the id is set by whoever persists,
838
+ * because they are the one who knows whether it comes from a database or a file.
922
839
  *
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.
840
+ * TWO COLUMNS and not a popover over the day. The popover is what everyone does
841
+ * and it hides the content behind a click: with the list beside it, a month with
842
+ * fifteen events reads at a glance and the selected day does not cover the grid.
843
+ * On a narrow screen they stack.
927
844
  *
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.
845
+ * Deleting goes through `AlertDialog` and not through a direct button. It is
846
+ * precisely the case it exists for: a destructive action with no undo.
930
847
  */
931
- type EventoCalendario = {
932
- /** Lo pone el proyecto. La librería nunca lo inventa. */
848
+ type CalendarEvent = {
849
+ /** Set by the project. The library never invents it. */
933
850
  id: string;
934
- /** Cuándo empieza, con hora. */
851
+ /** When it starts, with a time. */
935
852
  start: Date;
936
853
  title: string;
937
- /** `warm` cuando el evento es el problema, igual que en `Stat`. */
854
+ /** `warm` when the event is the problem, same as in `Stat`. */
938
855
  tone?: 'accent' | 'warm' | undefined;
939
856
  };
940
857
  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;
858
+ events: readonly CalendarEvent[];
859
+ /** Without it, the schedule is read-only and the form is not painted. */
860
+ onCreateEvent?: ((event: Omit<CalendarEvent, 'id'>) => void) | undefined;
861
+ onUpdateEvent?: ((event: CalendarEvent) => void) | undefined;
945
862
  onDeleteEvent?: ((id: string) => void) | undefined;
946
- /** Día seleccionado, si el proyecto lo controla. Sin él, empieza en hoy. */
863
+ /** Selected day, if the project controls it. Without it, it starts on today. */
947
864
  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. */
865
+ onSelectDay?: ((day: Date) => void) | undefined;
866
+ /** The panel's heading. Defaults to date-fns' `es`, like `Calendar`. */
867
+ formatDay?: ((day: Date) => string) | undefined;
868
+ formatTime?: ((date: Date) => string) | undefined;
869
+ /** The panel's text when the chosen day has nothing on it. */
953
870
  emptyMessage?: ReactNode;
954
871
  };
955
872
  declare function EventCalendar({ events, onCreateEvent, onUpdateEvent, onDeleteEvent, selected, onSelectDay, formatDay, formatTime, emptyMessage, className, ...props }: EventCalendarProps): react.JSX.Element;
956
873
 
957
874
  /**
958
- * El pie, y la firma CLI del sitio: `$ cd ~/eduardoalvarez.dev/2026`.
875
+ * The footer, and the site's CLI signature: `$ cd ~/eduardoalvarez.dev/2026`.
959
876
  *
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.
877
+ * The domain comes from `naming.domain` and not from a hand-written string, for
878
+ * the same reason as the wordmark: if it changes, it changes in all five
879
+ * projects at once.
963
880
  *
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.
881
+ * The social links are icons with NO visible text, so `aria-label` is not an
882
+ * improvement: it is the only thing that makes them legible. Which is why it is
883
+ * mandatory in the type and not an optional prop that gets forgotten.
967
884
  *
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.
885
+ * The signature sits top right, level with the FIRST row that exists, not at the
886
+ * end of the block. That is a composition decision and not a styling one: the
887
+ * footer can carry brand, links and social icons, and hanging the signature off
888
+ * one specific row sinks it the moment that row stops being the first.
972
889
  */
973
- type Red = {
974
- /** Lo que reemplaza al texto visible. Obligatorio. */
890
+ type SocialLink = {
891
+ /** What replaces the visible text. Mandatory. */
975
892
  label: string;
976
893
  href: string;
977
894
  /**
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.
895
+ * The glyph, at 19px. Brands are SOLID (`fill`) and functional icons use a 1.6
896
+ * stroke. Never an emoji.
980
897
  */
981
898
  icon: ReactNode;
982
899
  };
983
900
  type FooterProps = ComponentPropsWithoutRef<'footer'> & {
984
- social?: readonly Red[];
985
- /** Año de la firma. */
901
+ social?: readonly SocialLink[];
902
+ /** The signature's year. */
986
903
  year?: number;
987
- /** Enlaces de texto: aviso legal, RSS, mapa del sitio. */
904
+ /** Text links: legal notice, RSS, sitemap. */
988
905
  children?: ReactNode;
989
906
  /**
990
- * La fila de marca: la aleta y el wordmark, arriba del todo.
907
+ * The brand row: the fin and the wordmark, at the very top.
991
908
  *
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.
909
+ * It exists because without it that row ended up inside `children` with a
910
+ * `w-full` so it would take a line of its own. It worked and it was a patch:
911
+ * the brand is not one more text link, and a slot of its own says so in the
912
+ * type.
995
913
  */
996
914
  brand?: ReactNode;
997
915
  };
@@ -1002,413 +920,638 @@ type FooterLinkProps = ComponentPropsWithoutRef<'a'> & {
1002
920
  declare function FooterLink({ asChild, className, ...props }: FooterLinkProps): react.JSX.Element;
1003
921
 
1004
922
  /**
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.
923
+ * ONE per site. It is the only piece in the system that is spent like the
924
+ * conversion button, and for the same reason: if there are two, there are none.
925
+ *
926
+ * Gradient, panel radius, text at 62 % of the width and the pose bleeding off
927
+ * the bottom-right corner. That is the `header` variant, which is the default.
928
+ *
929
+ * On mobile there is no edge to bleed off, so the pose drops into the flow,
930
+ * below the buttons. It is not a `hidden` on small screens: the pose is 40 % of
931
+ * the hero's personality.
932
+ *
933
+ * The other variant is `centered`, and it exists because the rule above has a
934
+ * case where it does not apply. «Never centred» was written against the hero of
935
+ * a page with more content below it: there, a centred mascot under the headline
936
+ * is a cover illustration, not a header. But a links page is centred end to end
937
+ * and the mascot is the protagonist, not the flourish. That project skipped
938
+ * `Hero` entirely over this, which is worse: a declared rule with a name can be
939
+ * argued with; a copy of the gradient in another repo just drifts. The pose goes
940
+ * ABOVE the headline, not below it, so it still does not read as the
941
+ * illustration closing a block of text.
942
+ *
943
+ * The gradient comes from `--gradient-hero`, so it follows the mode. There is no
944
+ * hand-written angle in any project.
1027
945
  */
1028
946
  type HeroProps = Omit<ComponentPropsWithoutRef<'section'>, 'title'> & {
1029
947
  title: ReactNode;
1030
- /** Mono, versalitas, en acento. */
948
+ /** Mono, small caps, in accent. */
1031
949
  eyebrow?: ReactNode;
1032
950
  description?: ReactNode;
1033
- /** Los botones. Aquí va el único `conversion` de la pantalla. */
951
+ /** The buttons. The screen's only `conversion` goes here. */
1034
952
  action?: ReactNode;
1035
- /** La pose de Tiburoncín. Sin ella el hero es un panel con texto. */
953
+ /** Tiburoncín's pose. Without it the hero is a panel with text. */
1036
954
  pose?: Pose | undefined;
1037
955
  basePath?: string | undefined;
1038
956
  /**
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.
957
+ * `header` bleeds the pose off the corner; `centered` puts it on top and
958
+ * centres the text, for a page that is only this.
1041
959
  */
1042
- variant?: 'cabecera' | 'centrado';
960
+ variant?: 'header' | 'centered';
1043
961
  };
1044
962
  declare function Hero({ title, eyebrow, description, action, pose, basePath, variant, className, ...props }: HeroProps): react.JSX.Element;
1045
963
 
1046
- type LinkRowProps = Omit<TarjetaProps, 'children'> & {
964
+ type LinkRowProps = Omit<CardShellProps, 'children'> & {
1047
965
  name: ReactNode;
1048
966
  description?: ReactNode;
1049
- /** Glifo SVG del destino. Nunca un emoji. */
967
+ /** The target's SVG glyph. Never an emoji. */
1050
968
  icon?: ReactNode;
1051
- /** Marca el enlace como externo: añade la flecha y el `rel` seguro. */
969
+ /** Marks the link as external: adds the arrow and the safe `rel`. */
1052
970
  external?: boolean | undefined;
1053
971
  };
1054
972
  /**
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.
973
+ * Migrated from `links/src/components/Card.astro`. The original scaled the card
974
+ * to 102 %, lifted the title by a pixel and rotated and enlarged the icon on
975
+ * hover — four movements the system does not allow. Here the hover changes the
976
+ * border and the icon's color, and nothing else.
1059
977
  */
1060
978
  declare function LinkRow({ name, description, icon, external, className, ...props }: LinkRowProps): react.JSX.Element;
1061
979
 
1062
980
  /**
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.
981
+ * The site bar: 64px, abyss at 86 % and a 14px blur behind it — 56 when it
982
+ * shares the screen with a sidebar.
983
+ *
984
+ * It is page composition and not a primitive, but it lives in the library for a
985
+ * concrete reason: the CLI aesthetic of the items — mono, `./section` format —
986
+ * is the first thing that drifts when five projects each rewrite it on their
987
+ * own.
988
+ *
989
+ * It renders a `<header>` hanging directly off the `body`, so it IS the site's
990
+ * `banner` landmark. That is why `PageHeader` goes inside `<main>` and is not a
991
+ * landmark: two banners on one page is an accessibility failure.
992
+ *
993
+ * The items go on the RIGHT, next to the actions, not straight after the brand.
994
+ * With the brand on the left and the items right behind it, the navigation block
995
+ * floats in the middle of the bar and the eye has to cross the gap twice: once
996
+ * to read the brand and once to come back and find the section. Grouped on the
997
+ * right, brand and navigation are two anchors instead of three.
998
+ *
999
+ * `brand` and `actions` are `ReactNode` slots and they are the answer to almost
1000
+ * everything an app shell asks for: a `~/cursos` wordmark goes in `brand`, and a
1001
+ * user menu or a «Entrar» button goes in `actions`. Session state does not get a
1002
+ * prop of its own — it is project infrastructure, which is the third clause of
1003
+ * the criterion that decides what enters this library. See `docs/decisions.md`
1004
+ * § 30.
1079
1005
  */
1080
1006
  type NavProps = ComponentPropsWithoutRef<'header'> & {
1081
- /** El logo, a la izquierda. */
1007
+ /** The logo, on the left. */
1082
1008
  brand?: ReactNode;
1083
- /** Los items de sección. Van a la derecha, pegados a `actions`. */
1009
+ /** The section items. They go on the right, next to `actions`. */
1084
1010
  children?: ReactNode;
1085
- /** Acciones a la derecha: conversión, cambio de tema, buscar. */
1011
+ /** Actions on the right: conversion, theme switch, search. */
1086
1012
  actions?: ReactNode;
1013
+ /**
1014
+ * `compact` is 56px instead of 64, for a bar that shares the screen with a
1015
+ * sidebar: at 64 the two compete for the same corner and together they eat the
1016
+ * top of the content area.
1017
+ *
1018
+ * It is a prop and not a `className` because the height lives on the inner
1019
+ * container, which never sees one — `className` reaches the `<header>` and
1020
+ * stops there. Passing `h-14` from outside did nothing, silently, which is the
1021
+ * kind of failure this repo writes checks for.
1022
+ */
1023
+ size?: 'default' | 'compact' | undefined;
1087
1024
  };
1088
- declare function Nav({ brand, children, actions, className, ...props }: NavProps): react.JSX.Element;
1025
+ declare function Nav({ brand, children, actions, size, className, ...props }: NavProps): react.JSX.Element;
1089
1026
  type NavItemProps = ComponentPropsWithoutRef<'a'> & {
1090
- /** Sección actual: bioluz con subrayado de 1px. */
1027
+ /** Current section: biolume with a 1px underline. */
1091
1028
  active?: boolean | undefined;
1092
- /** Renderiza el hijo en vez de un `<a>`, para el `Link` del enrutador. */
1029
+ /** Renders the child instead of an `<a>`, for the router's `Link`. */
1093
1030
  asChild?: boolean | undefined;
1094
1031
  };
1095
1032
  /**
1096
- * El `./` lo pone el componente, no quien lo usa.
1033
+ * The `./` is put there by the component, not by whoever uses it.
1097
1034
  *
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
1035
+ * It is the same decision as the tertiary button: the format is part of the
1036
+ * piece, not a convention to be remembered across five projects. It is
1037
+ * `aria-hidden`, so a screen reader announces «artículos» and not «dot slash
1101
1038
  * artículos».
1102
1039
  *
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`.
1040
+ * The current section goes in BRACKETS as well as in biolume and underlined.
1041
+ * That is not decoration: the underline and the color are the same signal —
1042
+ * «this stands out» — and in a six-item mono bar at 13px that signal reads worse
1043
+ * than it looks in a mockup. Brackets are how a terminal marks the active path,
1044
+ * so they say «you are here» without relying on the color being told apart. They
1045
+ * are `aria-hidden`, because whoever is listening already has `aria-current`.
1109
1046
  */
1110
1047
  declare function NavItem({ active, asChild, className, children, ...props }: NavItemProps): react.JSX.Element;
1111
1048
 
1112
1049
  /**
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.
1050
+ * Four states, and the notice goes UNDER the form.
1051
+ *
1052
+ * Replacing the form with the success message is what almost everyone does and
1053
+ * it is what breaks the real case: somebody subscribes with the wrong email and
1054
+ * then has nowhere to type it again. The field stays.
1055
+ *
1056
+ * The component is presentational: it takes `state` and emits `onSubmitEmail`.
1057
+ * The network call is the project's job, because each has its own provider and
1058
+ * the library is not going to choose one for them.
1059
+ *
1060
+ * The notice uses the system's SECOND recipe — `emphasis="strong"`, background
1061
+ * at 10 % and a solid border — because it sits directly under a field that
1062
+ * already has a border: with the subtle recipe, the two lines read as a single
1063
+ * box.
1064
+ *
1065
+ * It is one of the places the mascot may appear: the «sin spam».
1066
+ *
1067
+ * The name field is OPTIONAL and off by default. It is not a styling prop: one
1068
+ * project's endpoint validates name and email and answers 400 if the first is
1069
+ * missing, so a one-field form there is not a poorer form — it is one that sends
1070
+ * something the server rejects. The name travels as the SECOND argument of
1071
+ * `onSubmitEmail`, so the calls that already exist — the ones only declaring
1072
+ * `(email)` — keep compiling.
1073
+ *
1074
+ * What the library does NOT do is validate the name. The project asking for it
1075
+ * bounds it between 2 and 50 characters and to letters and accents only; that
1076
+ * rule is theirs and the server's that actually checks it, and copying it here
1077
+ * would mean two sources drifting apart in silence. `nameInputProps` is there
1078
+ * for the project to put its own in.
1079
+ *
1080
+ * Four of the props are here because the blog had already built each one by
1081
+ * hand, and each workaround leaned on something nobody had promised: `aside`
1082
+ * replaces an absolutely positioned pose and a `md:pr-[330px]` measured off the
1083
+ * image; `resetOnSuccess` replaces finding the `<form>` with a `ref` on the
1084
+ * container; `onFieldChange` replaces an `onInput` on the `<section>` that
1085
+ * worked because the event bubbles; and `fieldErrors` replaces losing the second
1086
+ * message whenever two fields failed at once. A workaround that works by an
1087
+ * implementation detail is a bug with a delay.
1141
1088
  */
1142
- type NewsletterState = 'reposo' | 'enviando' | 'exito' | 'error';
1089
+ type NewsletterState = 'idle' | 'sending' | 'success' | 'error';
1143
1090
  type NewsletterFormProps = Omit<ComponentPropsWithoutRef<'section'>, 'title' | 'onSubmit'> & {
1144
1091
  title: ReactNode;
1145
1092
  description?: ReactNode;
1146
1093
  state?: NewsletterState;
1147
1094
  /**
1148
- * Se dispara con el correo ya leído del campo, y con el nombre si el campo
1149
- * está puesto.
1095
+ * Fires with the email already read from the field, and with the name if that
1096
+ * field is enabled.
1150
1097
  */
1151
1098
  onSubmitEmail?: ((email: string, name?: string) => void) | undefined;
1152
1099
  successMessage?: ReactNode;
1153
1100
  errorMessage?: ReactNode;
1154
- /** La letra pequeña. Es el «sin spam», y por eso admite cara. */
1101
+ /** The small print. It is the «sin spam», which is why it accepts a face. */
1155
1102
  disclaimer?: ReactNode;
1156
- expresion?: Cara | undefined;
1103
+ expression?: Face | undefined;
1157
1104
  basePath?: string | undefined;
1158
1105
  submitLabel?: string;
1159
1106
  placeholder?: string;
1160
1107
  fieldLabel?: string;
1161
- /** Añade el campo de nombre delante del correo. */
1108
+ /** Adds the name field ahead of the email one. */
1162
1109
  nameField?: boolean;
1163
1110
  nameLabel?: string;
1164
1111
  namePlaceholder?: string;
1165
1112
  /**
1166
- * Lo que el proyecto necesite colgar del campo de nombre: `minLength`,
1167
- * `maxLength`, `pattern`. La librería no impone ninguna de las tres.
1113
+ * Whatever the project needs to hang off the name field: `minLength`,
1114
+ * `maxLength`, `pattern`. The library imposes none of the three.
1168
1115
  */
1169
1116
  nameInputProps?: Omit<InputProps, 'id' | 'name' | 'disabled'> | undefined;
1117
+ /**
1118
+ * The illustration, as a second column inside the panel.
1119
+ *
1120
+ * It exists because the component builds its own children, so neither
1121
+ * `children` nor an extra `ReactNode` had anywhere to go: the blog ended up
1122
+ * positioning the desk pose absolutely over the panel and reserving room for
1123
+ * it with a hand-written `md:pr-[330px]`. That number depends on the image's
1124
+ * width and nothing keeps the two in step.
1125
+ *
1126
+ * It only becomes a column from `md` up. Below that it goes back into the
1127
+ * flow under the form, for the same reason `Hero`'s pose does: on a narrow
1128
+ * screen there is no second column to put it in.
1129
+ */
1130
+ aside?: ReactNode;
1131
+ /**
1132
+ * Empties the fields after a successful subscription. On by default.
1133
+ *
1134
+ * With the fields still full, the same email invites a second submission. The
1135
+ * blog worked around it by finding the `<form>` with a `ref` on the container
1136
+ * and calling `reset()`, because the component exposed no form — a trick that
1137
+ * works by an implementation detail and not by contract.
1138
+ */
1139
+ resetOnSuccess?: boolean;
1140
+ /**
1141
+ * Fires when either field changes. It is where the project clears its error.
1142
+ *
1143
+ * The blog was doing it by hanging an `onInput` off the `<section>` and
1144
+ * relying on the event bubbling up. That works, and it works by accident: it
1145
+ * depends on the spare props landing on the section, which is an
1146
+ * implementation detail and not something anybody promised.
1147
+ */
1148
+ onFieldChange?: ((field: 'name' | 'email', value: string) => void) | undefined;
1149
+ /**
1150
+ * A message under one specific field, instead of the single alert.
1151
+ *
1152
+ * With one bad field the general alert already names it, because the API
1153
+ * sends Zod's first message. With two bad at once only one of them is ever
1154
+ * seen. `fieldErrors` marks each field and puts its message underneath;
1155
+ * `errorMessage` still covers what belongs to the form as a whole — the 409,
1156
+ * the network failure — and both can show at the same time.
1157
+ */
1158
+ fieldErrors?: {
1159
+ name?: ReactNode;
1160
+ email?: ReactNode;
1161
+ } | undefined;
1170
1162
  };
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;
1163
+ 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
1164
 
1173
1165
  /**
1174
- * Una sola cabecera en dos escalas, no dos componentes.
1166
+ * One header at two scales, not two components.
1175
1167
  *
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.
1168
+ * The portfolio's hero and the courses one turned out to be the same skeleton —
1169
+ * eyebrow in accent, headline, clamped paragraph — at different sizes. Splitting
1170
+ * them into `Hero` and `PageHeader` would have duplicated the same rule in two
1171
+ * places and left the door open for them to drift apart over time.
1180
1172
  *
1181
- * `display` para portadas, `page` para cabeceras de sección.
1173
+ * `display` for covers, `page` for section headers.
1182
1174
  *
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.
1175
+ * It takes no mascot face, at either scale: faces go in empty states,
1176
+ * confirmations, errors, course progress and celebration.
1185
1177
  *
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.
1178
+ * It renders a `<header>`, and it goes INSIDE `<main>`. A `<header>` hanging
1179
+ * directly off `<body>` becomes a `banner` landmark, and then it competes with
1180
+ * the site header: two banners on one page is an accessibility failure. Inside
1181
+ * `<main>` it is not a landmark and it is the content's header, which is what
1182
+ * this component is.
1191
1183
  */
1192
- declare const cabecera: (props?: ({
1184
+ declare const header: (props?: ({
1193
1185
  size?: "display" | "page" | null | undefined;
1194
1186
  } & class_variance_authority_types.ClassProp) | undefined) => string;
1195
- type PageHeaderProps = Omit<ComponentPropsWithoutRef<'header'>, 'title'> & VariantProps<typeof cabecera> & {
1187
+ type PageHeaderProps = Omit<ComponentPropsWithoutRef<'header'>, 'title'> & VariantProps<typeof header> & {
1196
1188
  title: ReactNode;
1197
- /** Mono, versalitas, en acento. Es la sección a la que pertenece la página. */
1189
+ /** Mono, small caps, in accent. It is the section the page belongs to. */
1198
1190
  eyebrow?: ReactNode | undefined;
1199
1191
  description?: ReactNode | undefined;
1200
1192
  /**
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.
1193
+ * Slot for the calls to action. If a conversion button goes here, it is the
1194
+ * only one on the screen.
1203
1195
  */
1204
1196
  action?: ReactNode | undefined;
1205
- /** Nivel del titular. `h1` salvo que la página ya tenga uno. */
1197
+ /** The headline's level. `h1` unless the page already has one. */
1206
1198
  as?: 'h1' | 'h2' | undefined;
1207
1199
  };
1208
1200
  declare function PageHeader({ title, eyebrow, description, action, size, as, className, ...props }: PageHeaderProps): react.JSX.Element;
1209
1201
 
1210
1202
  /**
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.
1203
+ * How much you have read. It is NOT `Progress` under another name.
1204
+ *
1205
+ * `Progress` measures a task: there is a known total, somebody started it and it
1206
+ * is going to finish. This measures a POSITION in a document, which can be
1207
+ * travelled in both directions and of which there is nothing to complete. That
1208
+ * is why it carries neither `role="progressbar"` nor an accessible value: it is
1209
+ * `aria-hidden`.
1210
+ *
1211
+ * That last part is deliberate and it is the decision worth defending. A screen
1212
+ * reader already knows where it is in the document, and announcing «37 %» every
1213
+ * time it moves is noise, not information. The bar is visual orientation, and
1214
+ * what is purely visual is declared as such.
1215
+ *
1216
+ * The width is written directly, with no transition: `transition-standard` only
1217
+ * covers color and border, so the bar follows the scroll instead of chasing it.
1218
+ *
1219
+ * The measurement happens inside `requestAnimationFrame`. Reading `scrollTop` in
1220
+ * the scroll handler forces a synchronous reflow on every event, and in a long
1221
+ * article that shows up in the scroll itself — the opposite of what this piece
1222
+ * is for.
1230
1223
  */
1231
1224
  type ScrollingProgressBarProps = Omit<ComponentPropsWithoutRef<'div'>, 'children'> & {
1232
1225
  /**
1233
- * El elemento que se mide. Sin él, el documento entero.
1226
+ * The element being measured. Without it, the whole document.
1234
1227
  *
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.
1228
+ * It is passed when the bar should follow ONLY the article: if the page has a
1229
+ * tall header and a footer full of links, measuring the document hits 100 %
1230
+ * while two paragraphs are still left.
1238
1231
  */
1239
1232
  target?: RefObject<HTMLElement | null> | undefined;
1240
- /** Arena en vez de bioluz, para igualar el progreso de curso. */
1233
+ /** Sand instead of biolume, to match course progress. */
1241
1234
  tone?: 'accent' | 'warm';
1242
- /** Pega la barra al borde superior de la ventana. */
1235
+ /** Pins the bar to the top edge of the window. */
1243
1236
  sticky?: boolean;
1244
1237
  };
1245
1238
  declare function ScrollingProgressBar({ target, tone, sticky, className, ...props }: ScrollingProgressBarProps): react.JSX.Element;
1246
1239
 
1247
1240
  /**
1248
- * La barra lateral del admin del blog.
1241
+ * The blog admin's sidebar.
1249
1242
  *
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`.
1243
+ * The `▸` is put there by the component, same as `NavItem`'s `./` and the
1244
+ * breadcrumb's `~`: it is the same CLI aesthetic and the same decision — the
1245
+ * format is part of the piece, not a convention to be remembered. It is
1246
+ * `aria-hidden`.
1253
1247
  *
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.
1248
+ * An `icon` REPLACES it rather than joining it. Two marks before a label is one
1249
+ * more than the eye needs, and the `▸` exists to say «this is a place you can
1250
+ * go» — which is exactly what a section icon already says, and says better. A
1251
+ * sidebar with no icons keeps the prompt, which is the reading sites' case.
1252
+ *
1253
+ * The footer carries the version and the branch (`v5.0.1 · main`) in the `meta`
1254
+ * scale. It is not decoration: in an admin it is the first thing anyone asks
1255
+ * when something looks off.
1256
1256
  */
1257
1257
  type SidebarItemProps = ComponentPropsWithoutRef<'a'> & {
1258
1258
  active?: boolean | undefined;
1259
1259
  asChild?: boolean | undefined;
1260
- /** Contador a la derecha: borradores pendientes, media sin usar. */
1260
+ /** Counter on the right: pending drafts, unused media. */
1261
1261
  badge?: ReactNode;
1262
+ /**
1263
+ * The section's glyph, on the left. It REPLACES the `▸` rather than joining
1264
+ * it, and it inherits `currentColor`, so it follows the item's state without
1265
+ * being tinted separately.
1266
+ */
1267
+ icon?: ReactNode;
1268
+ };
1269
+ declare function SidebarItem({ active, asChild, badge, icon, className, children, ...props }: SidebarItemProps): react.JSX.Element;
1270
+ /**
1271
+ * A labelled block of items — «Contenido», «Alumnos», «Ventas».
1272
+ *
1273
+ * A panel's sidebar stops being readable somewhere around eight items, and the
1274
+ * fix is not a scrollbar: it is saying what the blocks are. The label is a `<p>`
1275
+ * and not a heading, because a heading here would land in the document outline
1276
+ * between the page's own — a sidebar is navigation, not content.
1277
+ *
1278
+ * The items go in a nested `<ul>` named by that label, which is what gives a
1279
+ * screen reader «lista Ventas, 3 elementos» instead of one list of eleven.
1280
+ */
1281
+ type SidebarGroupProps = Omit<ComponentPropsWithoutRef<'li'>, 'title'> & {
1282
+ /** The block's name. Sentence case, not a section title. */
1283
+ label: ReactNode;
1262
1284
  };
1263
- declare function SidebarItem({ active, asChild, badge, className, children, ...props }: SidebarItemProps): react.JSX.Element;
1285
+ declare function SidebarGroup({ label, children, className, ...props }: SidebarGroupProps): react.JSX.Element;
1264
1286
  type SidebarNavProps = ComponentPropsWithoutRef<'nav'> & {
1265
- /** Encabezado del panel. */
1287
+ /**
1288
+ * The panel's heading, as an eyebrow. It also names the `<nav>` when it is a
1289
+ * string, which is why a `brand` row does not replace it: a logo is not an
1290
+ * accessible name. Pass `aria-label` when neither fits.
1291
+ */
1266
1292
  title?: ReactNode;
1267
- /** Versión y rama, al pie. */
1293
+ /**
1294
+ * The row at the top: isotype and wordmark, `cursos · admin`. It is a slot and
1295
+ * not a `logo`/`name` pair because every panel spells its own name
1296
+ * differently, and the part that IS the system — the rhythm, the hairline
1297
+ * under it — is here.
1298
+ *
1299
+ * Collapsed, it is hidden: a wordmark does not fit in a rail and the component
1300
+ * cannot trim somebody else's markup. Pass the isotype on its own through
1301
+ * `mark` for the rail to keep something at the top.
1302
+ */
1303
+ brand?: ReactNode;
1304
+ /** What `brand` becomes in the rail. Usually the isotype with no wordmark. */
1305
+ mark?: ReactNode;
1306
+ /**
1307
+ * Who is signed in, at the bottom above the version. A slot, because an avatar
1308
+ * needs a session and a sign-out route and the library takes no project
1309
+ * infrastructure — the same reason `Nav`'s user menu goes in `actions`.
1310
+ */
1311
+ user?: ReactNode;
1312
+ /**
1313
+ * Turns the sidebar into a rail: icons only, and the widths become the
1314
+ * library's — `w-sidebar` and `w-sidebar-rail`. It is CONTROLLED and there is
1315
+ * no uncontrolled mode, because this state is almost always persisted in a
1316
+ * cookie or in `localStorage`, and an internal state would fight the one the
1317
+ * project already keeps.
1318
+ */
1319
+ collapsed?: boolean | undefined;
1320
+ /**
1321
+ * Called with what the state should become. With it, the toggle appears; with
1322
+ * `collapsed` alone the sidebar is a rail with no way out of it, which is a
1323
+ * legitimate layout and not an accident.
1324
+ */
1325
+ onCollapsedChange?: ((collapsed: boolean) => void) | undefined;
1326
+ /** The toggle's accessible name, in the two directions. */
1327
+ collapseLabel?: string;
1328
+ expandLabel?: string;
1329
+ /** Version and branch, at the bottom. */
1268
1330
  version?: ReactNode;
1269
1331
  branch?: ReactNode;
1270
1332
  };
1271
- declare function SidebarNav({ title, version, branch, children, className, ...props }: SidebarNavProps): react.JSX.Element;
1333
+ declare function SidebarNav({ title, brand, mark, user, collapsed, onCollapsedChange, collapseLabel, expandLabel, version, branch, children, className, ...props }: SidebarNavProps): react.JSX.Element;
1272
1334
 
1273
1335
  /**
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.
1336
+ * A large metric: the number in the `stat` scale and its name underneath.
1337
+ *
1338
+ * The document's rule is not one of style, it is one of semantics: «biolume for
1339
+ * the neutral and sand ONLY when the number is the problem». A 12 of
1340
+ * applications is a datum; a 0 of design systems is the problem the talk is
1341
+ * about. That is why `tone` is not an open palette — there are two values and
1342
+ * they mean different things.
1343
+ *
1344
+ * The reading order is title, the big number, and the standfirst below. The
1345
+ * number goes in the MIDDLE and not at the end on purpose: it is what people
1346
+ * came to read, and a two-line standfirst between the title and the figure
1347
+ * buries it. The top says what it is about, the middle says how much, and the
1348
+ * bottom holds the nuance only someone who stops will read.
1349
+ *
1350
+ * `delta` and `spark` sit with the number and not with the standfirst, because
1351
+ * both are about the number: how it moved and what shape the movement had. The
1352
+ * order survives — top what, middle how much, bottom the nuance.
1353
+ *
1354
+ * THE ICON IS A BADGE IN THE OPPOSITE CORNER, not a glyph before the title, and
1355
+ * the tone rides on it rather than on the number. In a panel of ten of these the
1356
+ * eyebrow is the same length in none of them, so an inline icon puts the only
1357
+ * coloured mark on a different x in every card; pinned to the corner it lands on
1358
+ * a grid. The circle is the tint pattern the system already has — `bg-accent/10`
1359
+ * as a surface and the colour on the GLYPH, per `docs/decisions.md` § 4b — and a
1360
+ * glyph clears the 3:1 graphical threshold where text would not clear 4.5.
1361
+ *
1362
+ * WHICH IS WHY A NEUTRAL NUMBER IS PRIMARY INK AND NOT BIOLUME. With a biolume
1363
+ * badge and a biolume sparkline, a biolume number is the third accent in a card
1364
+ * the size of a postcard, and the thing you came to read stops being the loudest
1365
+ * thing in it. `alert` and `achievement` DO still paint the number sand, so the
1366
+ * document's rule survives exactly where it matters: sand when the number is not
1367
+ * just a number. See `docs/decisions.md` § 31.
1286
1368
  */
1287
1369
  type StatProps = Omit<ComponentPropsWithoutRef<'div'>, 'title'> & {
1288
- /** El número, ya formateado. La librería no impone locale. */
1370
+ /** The number, already formatted. The library imposes no locale. */
1289
1371
  value: ReactNode;
1290
- /** Qué se está contando. Va en mono versalitas. */
1372
+ /** What is being counted. It goes in mono small caps. */
1291
1373
  label: ReactNode;
1292
- /** `alerta` solo cuando el número ES el problema. */
1293
- tone?: 'neutral' | 'alerta';
1294
- /** Con `progress`, la métrica se lee como avance y añade la barra. */
1374
+ /**
1375
+ * `alert` ONLY when the number is the problem, and `achievement` when it is
1376
+ * the opposite — the diplomas issued, the modules finished. The two paint the
1377
+ * same sand today and they are still two names: a system that names by meaning
1378
+ * cannot make «this is bad» the only way to say «this stands out». See
1379
+ * `docs/decisions.md` § 28.
1380
+ */
1381
+ tone?: 'neutral' | 'alert' | 'achievement';
1382
+ /** With `progress`, the metric reads as progress and adds the bar. */
1295
1383
  progress?: number | undefined;
1296
1384
  /**
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.
1385
+ * Glyph in a tinted circle, in the corner opposite the title. At 1em, and it
1386
+ * inherits `currentColor` from the badge, so it takes the tone without being
1387
+ * tinted separately.
1299
1388
  */
1300
1389
  icon?: ReactNode;
1301
1390
  /**
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.
1391
+ * The standfirst: the nuance the number alone does not give. «12 aplicaciones»
1392
+ * does not say whether that is a lot, and this is where that gets said.
1304
1393
  */
1305
1394
  description?: ReactNode;
1395
+ /** How the number moved since last time. */
1396
+ delta?: StatDelta | undefined;
1397
+ /**
1398
+ * The number's shape over time, under it. A `ReactNode` and not a data prop:
1399
+ * a sparkline needs a charting library, and this component lives in the barrel
1400
+ * that four projects install. The one project that draws them passes its own,
1401
+ * exactly like `icon`.
1402
+ */
1403
+ spark?: ReactNode;
1404
+ };
1405
+ type StatDelta = {
1406
+ /**
1407
+ * Already formatted — «+12 esta semana», «↑8 %». The library imposes no
1408
+ * locale, same as `value`.
1409
+ */
1410
+ value: ReactNode;
1411
+ /**
1412
+ * Which way it moved. It picks the GLYPH and never the colour, because a rise
1413
+ * is not automatically good: «+12 alumnos» and «+12 errores» point the same
1414
+ * way and mean opposite things. Whether the number matters is `tone`'s job,
1415
+ * and it is a decision the call site has already made.
1416
+ */
1417
+ direction: 'up' | 'down' | 'flat';
1306
1418
  };
1307
- declare function Stat({ value, label, tone, progress, icon, description, className, ...props }: StatProps): react.JSX.Element;
1419
+ declare function Stat({ value, label, tone, progress, icon, description, delta, spark, className, ...props }: StatProps): react.JSX.Element;
1308
1420
 
1309
- type TalkCardProps = Omit<TarjetaProps, 'children' | 'title'> & {
1421
+ type TalkContent = {
1310
1422
  title: ReactNode;
1311
- /** Dónde se dio: la conferencia, el meetup, el equipo. */
1423
+ /** Where it was given: the conference, the meetup, the team. */
1312
1424
  event: ReactNode;
1313
1425
  date?: ReactNode;
1314
1426
  dateTime?: string | undefined;
1315
1427
  location?: ReactNode;
1316
- /** Etiqueta corta de estado: «con vídeo», «próxima», «solo audio». */
1428
+ /** Short status label: «con vídeo», «próxima», «solo audio». */
1317
1429
  status?: ReactNode;
1318
1430
  /**
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.
1431
+ * What the talk was about. Clamped to two lines, same as `ArticleCard`'s
1432
+ * `excerpt`, so the grid does not fall out of line.
1321
1433
  */
1322
1434
  description?: ReactNode;
1323
1435
  };
1324
- declare function TalkCard({ title, event, date, dateTime, location, status, description, className, ...props }: TalkCardProps): react.JSX.Element;
1436
+ /**
1437
+ * A talk has more than one destination, and that is what shapes this type.
1438
+ *
1439
+ * Slides, repo, the recording, the event's own page: the listing used to show
1440
+ * them as loose links under each talk, and migrating to this component made them
1441
+ * DISAPPEAR — there was nowhere to put them, so the blog pointed the whole card
1442
+ * at the first one that existed and the rest were lost.
1443
+ *
1444
+ * So `resources` is a slot, and it forces a choice in the type: a card with
1445
+ * resources is NOT a link. An `<a>` inside an `<a>` is invalid HTML and the
1446
+ * browser un-nests it, so «the card links to the slides AND the slides link to
1447
+ * the slides» is not a thing that can render. With resources the card is an
1448
+ * `<article>` on the card surface and the links are the resources; without them
1449
+ * it stays the single-destination card it was.
1450
+ *
1451
+ * That is why this is a union and not one more optional prop: the combination
1452
+ * that cannot work does not compile.
1453
+ */
1454
+ type TalkCardProps = (Omit<CardShellProps, 'children' | 'title'> & TalkContent & {
1455
+ /** The card is the link. Do not pass resources with this. */
1456
+ resources?: never;
1457
+ }) | (Omit<ComponentPropsWithoutRef<'article'>, 'title'> & TalkContent & {
1458
+ /**
1459
+ * Slides, repo, recording. They are the links, so the card stops being
1460
+ * one.
1461
+ */
1462
+ resources: ReactNode;
1463
+ });
1464
+ declare function TalkCard({ title, event, date, dateTime, location, status, description, resources, className, ...props }: TalkCardProps): react.JSX.Element;
1325
1465
 
1326
1466
  /**
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.
1467
+ * The control that was missing. The library defined the whole theming system and
1468
+ * exposed nothing that changes it, so two projects were reimplementing it.
1469
+ *
1470
+ * The hard part is not the button: it is that the first paint does not flash and
1471
+ * that the choice survives navigation. That lives in
1472
+ * `@eduardoalvarez/arrecife/theme`, which does not import React — an Astro that
1473
+ * mounts none consumes it — and out of it comes `themeScript`, which goes inline
1474
+ * in the `<head>`. Without that script this button works and you still get the
1475
+ * flash on every load.
1476
+ *
1477
+ * BOTH icons are always rendered and CSS hides the spare one with the `light:`
1478
+ * variant. It is not an optimisation: it is what stops the server and the client
1479
+ * from disagreeing. The server does not know which theme the reader chose, so
1480
+ * whichever icon it picks in the HTML has a fifty-fifty chance of being wrong,
1481
+ * and correcting it on hydration is the flash all over again.
1482
+ *
1483
+ * The accessible name does NOT say which mode you are going to. That would be
1484
+ * more informative and would be a lie half the time, for the same reason as
1485
+ * above: the server's HTML fixes it before the theme is known. «Cambiar de
1486
+ * tema» is always true.
1345
1487
  */
1346
1488
  type ThemeToggleProps = Omit<ComponentPropsWithoutRef<'button'>, 'onClick'> & {
1347
- /** Nombre accesible. El botón no tiene texto visible, así que es lo único que lo nombra. */
1489
+ /** Accessible name. The button has no visible text, so it is the only thing naming it. */
1348
1490
  label?: string;
1349
- /** Se dispara con el tema que quedó puesto, por si el proyecto quiere anotarlo. */
1350
- onThemeChange?: ((tema: Tema) => void) | undefined;
1491
+ /** Fires with whichever theme ended up set, in case the project wants to record it. */
1492
+ onThemeChange?: ((theme: Theme) => void) | undefined;
1351
1493
  variant?: ButtonProps['variant'];
1352
1494
  size?: ButtonProps['size'];
1353
1495
  };
1354
1496
  declare function ThemeToggle({ label, onThemeChange, variant, size, className, ...props }: ThemeToggleProps): react.JSX.Element;
1355
1497
  /**
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.
1498
+ * The theme set right now, for a project that needs to branch in React — a
1499
+ * different logo per mode, an image with no light version.
1500
+ *
1501
+ * It is `useSyncExternalStore` and not a `useState` with an effect behind it
1502
+ * because the theme is exactly that: state living outside React, in an attribute
1503
+ * on `<html>` that can change without React finding out. Writing it as an effect
1504
+ * calling `setState` on mount is the pattern that triggers a cascading render
1505
+ * and that the `set-state-in-effect` rule rightly flags.
1506
+ *
1507
+ * `getServerSnapshot` returns `'dark'` because there is no `document` on the
1508
+ * server. The client's first render matches the server's and the real value
1509
+ * arrives afterwards, which is the same hydration mismatch `ThemeToggle` avoids
1510
+ * by rendering both icons.
1511
+ *
1512
+ * Hence the usage rule: if what branches is ONLY style, this is not needed and
1513
+ * the `light:` variant is better — it re-renders nothing. This is for when the
1514
+ * content changes.
1373
1515
  */
1374
- declare function useTema(): Tema;
1516
+ declare function useTheme(): Theme;
1375
1517
 
1376
1518
  /**
1377
- * «En esta página». El índice del artículo largo.
1519
+ * «En esta página». The long article's table of contents.
1378
1520
  *
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.
1521
+ * It is a `<nav>` with an accessible name of its own, not a loose list: on a
1522
+ * page that already has the site bar and the breadcrumb, a third group of links
1523
+ * with no name is indistinguishable from the other two for anyone navigating by
1524
+ * landmarks.
1382
1525
  *
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.
1526
+ * The active entry is marked with `aria-current`, not with color alone — the
1527
+ * section you are in cannot be communicated purely in biolume.
1385
1528
  *
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.
1529
+ * And that attribute is also THE HOOK: the active classes are applied with the
1530
+ * `aria-[current]:` variant, not with a ternary in the render. The difference is
1531
+ * the one between serving controlled only and also serving uncontrolled.
1389
1532
  *
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.
1533
+ * An Astro site solves scroll-spy with fifteen lines of script that set
1534
+ * `aria-current` on the visible link and remove it from the previous one. With
1535
+ * the state computed in the render, that script could do nothing: the table of
1536
+ * contents had to be hydrated as a React island on every article for something
1537
+ * that costs zero framework JavaScript. Now the CSS reacts to the attribute and
1538
+ * both ways of using it give the same result.
1396
1539
  *
1397
- * El gancho es la PRESENCIA del atributo, así que se quita para desmarcar; no
1398
- * se pone `aria-current="false"`.
1540
+ * The hook is the PRESENCE of the attribute, so it is removed to unmark; you do
1541
+ * not set `aria-current="false"`.
1399
1542
  */
1400
- type Entrada = {
1401
- /** El ancla, con `#`. */
1543
+ type TocEntry = {
1544
+ /** The anchor, with its `#`. */
1402
1545
  href: string;
1403
1546
  label: ReactNode;
1404
- /** Sangra la entrada. Solo dos niveles: h2 y h3. */
1547
+ /** Indents the entry. Only two levels: h2 and h3. */
1405
1548
  nested?: boolean | undefined;
1406
1549
  };
1407
1550
  type TableOfContentsProps = Omit<ComponentPropsWithoutRef<'nav'>, 'children'> & {
1408
- items: readonly Entrada[];
1409
- /** El título del bloque. */
1551
+ items: readonly TocEntry[];
1552
+ /** The block's title. */
1410
1553
  title?: ReactNode;
1411
- /** Ancla de la sección visible. */
1554
+ /** The anchor of the visible section. */
1412
1555
  activeHref?: string | undefined;
1413
1556
  linkAsChild?: ((props: {
1414
1557
  href: string;
@@ -1419,26 +1562,4 @@ declare function TableOfContents({ items, title, activeHref, linkAsChild, classN
1419
1562
 
1420
1563
  declare function cn(...inputs: ClassValue[]): string;
1421
1564
 
1422
- type IconProps = SVGProps<SVGSVGElement>;
1423
- declare const GitHub: (props: IconProps) => react.JSX.Element;
1424
- declare const LinkedIn: (props: IconProps) => react.JSX.Element;
1425
- declare const X: (props: IconProps) => react.JSX.Element;
1426
- declare const Instagram: (props: IconProps) => react.JSX.Element;
1427
- declare const Discord: (props: IconProps) => react.JSX.Element;
1428
- declare const YouTube: (props: IconProps) => react.JSX.Element;
1429
- declare const Rss: (props: IconProps) => react.JSX.Element;
1430
- declare const Correo: (props: IconProps) => react.JSX.Element;
1431
-
1432
- declare const social_Correo: typeof Correo;
1433
- declare const social_Discord: typeof Discord;
1434
- declare const social_GitHub: typeof GitHub;
1435
- declare const social_Instagram: typeof Instagram;
1436
- declare const social_LinkedIn: typeof LinkedIn;
1437
- declare const social_Rss: typeof Rss;
1438
- declare const social_X: typeof X;
1439
- declare const social_YouTube: typeof YouTube;
1440
- 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 };
1442
- }
1443
-
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 };
1565
+ 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, SidebarGroup, type SidebarGroupProps, SidebarItem, type SidebarItemProps, SidebarNav, type SidebarNavProps, Skeleton, type SkeletonProps, type SocialLink, Stat, type StatDelta, 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, text as textVariants, toast, useTheme };