@eduardoalvarez/arrecife 0.1.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 (44) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE +21 -0
  3. package/README.md +520 -0
  4. package/assets/brand/face-annoyed.png +0 -0
  5. package/assets/brand/face-confused.png +0 -0
  6. package/assets/brand/face-hearts.png +0 -0
  7. package/assets/brand/face-laughing.png +0 -0
  8. package/assets/brand/face-shades.png +0 -0
  9. package/assets/brand/face-waiting.png +0 -0
  10. package/assets/brand/face-wink.png +0 -0
  11. package/assets/brand/fin-foam.png +0 -0
  12. package/assets/brand/fin.png +0 -0
  13. package/assets/brand/pose-desk.png +0 -0
  14. package/assets/brand/pose-laptop-coffee.png +0 -0
  15. package/assets/brand/pose-peek.png +0 -0
  16. package/assets/brand/pose-surf.png +0 -0
  17. package/dist/brand/index.cjs +228 -0
  18. package/dist/brand/index.d.cts +63 -0
  19. package/dist/brand/index.d.ts +63 -0
  20. package/dist/brand/index.js +3 -0
  21. package/dist/catalogo-Du5ID-Hi.d.cts +77 -0
  22. package/dist/catalogo-Du5ID-Hi.d.ts +77 -0
  23. package/dist/chunk-22OXRTAL.js +22 -0
  24. package/dist/chunk-DHF63QNM.js +231 -0
  25. package/dist/chunk-E3OMP2DL.js +36 -0
  26. package/dist/chunk-ZL5X6XO7.js +102 -0
  27. package/dist/index.cjs +3191 -0
  28. package/dist/index.d.cts +1024 -0
  29. package/dist/index.d.ts +1024 -0
  30. package/dist/index.js +2624 -0
  31. package/dist/og/index.cjs +322 -0
  32. package/dist/og/index.d.cts +164 -0
  33. package/dist/og/index.d.ts +164 -0
  34. package/dist/og/index.js +242 -0
  35. package/dist/shiki/index.cjs +127 -0
  36. package/dist/shiki/index.d.cts +16 -0
  37. package/dist/shiki/index.d.ts +16 -0
  38. package/dist/shiki/index.js +95 -0
  39. package/dist/tokens/index.cjs +263 -0
  40. package/dist/tokens/index.d.cts +623 -0
  41. package/dist/tokens/index.d.ts +623 -0
  42. package/dist/tokens/index.js +2 -0
  43. package/dist/tokens/theme.css +266 -0
  44. package/package.json +128 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,55 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-08-27)
4
+
5
+
6
+ ### 🚀 Novedades
7
+
8
+ * **brand:** catálogo de Tiburoncín, Logo, Isotipo, Mascota y galería ([c0d616b](https://github.com/Proskynete/arrecife/commit/c0d616b0478005105e2fa6df474ee20eb1b14bb8))
9
+ * **ci:** comprobar la superficie publicada ([6c8e715](https://github.com/Proskynete/arrecife/commit/6c8e7154c734e8cc64f107d8ee3d3d878b77eb46))
10
+ * **components:** AudioPlayer migrado desde eduardoalvarez.dev ([b4c5345](https://github.com/Proskynete/arrecife/commit/b4c53459ad17bc97674f8a9108f9abf0f982407c))
11
+ * **components:** AuthorCard, Stat, TableOfContents y SidebarNav ([15c64c7](https://github.com/Proskynete/arrecife/commit/15c64c731e3da10356602799e7ea4f8bf508071f))
12
+ * **components:** EmptyState, Breadcrumb, Nav, Footer, Hero y NewsletterForm ([9ef81dd](https://github.com/Proskynete/arrecife/commit/9ef81dddd0a1368313a4f596ecff52a336c75f02))
13
+ * **components:** tarjetas, bloque de código, cita y cabecera de página ([6743361](https://github.com/Proskynete/arrecife/commit/6743361f349e46dbf7ab1e07942cf100c92b12fd))
14
+ * **og:** las cuatro plantillas con la retícula del documento ([68b9ebd](https://github.com/Proskynete/arrecife/commit/68b9ebdead8c69b788d29e5e5ab430389b600f0e))
15
+ * **primitives:** capa base sobre shadcn y Radix ([81855ee](https://github.com/Proskynete/arrecife/commit/81855eea62e36f4b07b0dc170dad577313eb35a5))
16
+ * **primitives:** Code en línea, skeleton con barrido y las escalas nuevas en Text ([9f742a3](https://github.com/Proskynete/arrecife/commit/9f742a30fae68ba149dc165030dffd59d96ec68c))
17
+ * **shiki:** el tema de resaltado generado desde tokens ([af5e795](https://github.com/Proskynete/arrecife/commit/af5e7956ca914376f265b80c225e24b21d8d653c))
18
+ * **tokens:** escalas nuevas, controles, degradados y paleta de sintaxis ([a17a508](https://github.com/Proskynete/arrecife/commit/a17a5081d4d112516e16f6961db502f5444a294c))
19
+ * **tokens:** fuente única, generador de Tailwind y guarda de pureza ([f6eb94b](https://github.com/Proskynete/arrecife/commit/f6eb94bb6ad47548fce9d5f39a47ef86130d9ae4))
20
+
21
+
22
+ ### 🐛 Correcciones
23
+
24
+ * **a11y:** cursor-pointer en todo lo que se pulsa ([70b0784](https://github.com/Proskynete/arrecife/commit/70b07849311024874e6127e617db71c3833de2b8))
25
+ * **alert:** tinte al 8%, glifos mono y la segunda receta ([b90affc](https://github.com/Proskynete/arrecife/commit/b90affc5106c802ef864056aaeeca29dd942cd23))
26
+ * **audio-player:** los tamaños del documento ([4e68362](https://github.com/Proskynete/arrecife/commit/4e6836247e3e811194522fdaa99d91c8a7a231d9))
27
+ * **badge:** separar categoría, estado y métrica ([1c0239a](https://github.com/Proskynete/arrecife/commit/1c0239a472f4703c0e5859db923f2b12467cf463))
28
+ * **button:** cuatro variantes, tamaños del documento y fuera el de peligro ([07d209f](https://github.com/Proskynete/arrecife/commit/07d209fa5b969982b9e2a642e0d6ed8bd75af822))
29
+ * **cards:** padding de 26, categoría en los tags y la superficie que no entra ([8f30fda](https://github.com/Proskynete/arrecife/commit/8f30fdad3109a836176980629204657af43a39bd))
30
+ * **ci:** la primera versión es 0.1.0 y feat sube la minor ([2ed3859](https://github.com/Proskynete/arrecife/commit/2ed385956dda5485ce23ff7e65818d175bc46607))
31
+ * **cn:** derivar de tokens las escalas que tailwind-merge debe conocer ([2d6c4e5](https://github.com/Proskynete/arrecife/commit/2d6c4e503346413d6d2789a768698ff0beeab836))
32
+ * **storybook:** .storybook entraba al tsconfig pero no se compilaba ([2885830](https://github.com/Proskynete/arrecife/commit/2885830b2fb0982cf657903cc20da998efba7273))
33
+ * **storybook:** el contenedor de docs también toma el fondo del token ([beeb434](https://github.com/Proskynete/arrecife/commit/beeb434631d49571554420c98150143811f1bd40))
34
+ * **storybook:** el wrapper interno de docs, que era el que pintaba blanco ([edda8c6](https://github.com/Proskynete/arrecife/commit/edda8c65ff05d1e3ca815d52d0be4cbcad91ad1f))
35
+ * **storybook:** forzar el fondo del contenedor de docs ([11fcfc7](https://github.com/Proskynete/arrecife/commit/11fcfc730f595926f04e69f171feb948b683d34a))
36
+ * **storybook:** tema de docs por la API y no a base de CSS a la contra ([86f80cf](https://github.com/Proskynete/arrecife/commit/86f80cfe7e26822a96169559824691f7164634da))
37
+ * **storybook:** tematizar el manager, que es lo que quedaba blanco ([d1643a9](https://github.com/Proskynete/arrecife/commit/d1643a9f3e648503a20fe9e016dcf3481d77c159))
38
+ * **tsconfig:** incluir .storybook por include y no por files ([ca8a8d6](https://github.com/Proskynete/arrecife/commit/ca8a8d6e26eba6e30e4bdd4019cc6673ef197542))
39
+
40
+
41
+ ### 📚 Documentación
42
+
43
+ * cómo se consume, qué se decidió y por qué ([0f9654f](https://github.com/Proskynete/arrecife/commit/0f9654ff8ae25ba9d9df0a2dbb422fc8abf05e55))
44
+ * incorporar al repo los documentos de identidad y las decisiones ([ca9273f](https://github.com/Proskynete/arrecife/commit/ca9273fa821d9314f54d8d821b4ee3dfeb9446df))
45
+ * **readme:** cómo publicar una versión ([df9702b](https://github.com/Proskynete/arrecife/commit/df9702b71ba22879afcfe5a5e5513abe59af38eb))
46
+ * **readme:** la fase 5, las subrutas nuevas y la tercera corrección de contraste ([871cee9](https://github.com/Proskynete/arrecife/commit/871cee92a8fef7ea33264b869140d45cd6f4d3b2))
47
+
48
+
49
+ ### 🚀 CI/CD
50
+
51
+ * no correr CI y lint dos veces por PR ([010ca3c](https://github.com/Proskynete/arrecife/commit/010ca3c249bcbaa3558ec30ff173fb053f56b56a))
52
+ * pipeline de publicación en npm y las comprobaciones de PR ([930da3d](https://github.com/Proskynete/arrecife/commit/930da3d91199eff38135e2a07456b02cd935c8a1))
53
+ * release-please y publicación de confianza con OIDC ([f005b8c](https://github.com/Proskynete/arrecife/commit/f005b8c3a0dfc0c42015140f52ebe4c37e8507e5))
54
+
55
+ ## Changelog
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eduardo Esteban Álvarez Castañeda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,520 @@
1
+ # Arrecife
2
+
3
+ Librería de componentes de la identidad visual de Eduardo Álvarez.
4
+ `@eduardoalvarez/arrecife` · React 19 · TypeScript · shadcn/ui · Storybook · tsup.
5
+
6
+ ## Los documentos de identidad
7
+
8
+ `docs/design-system.md` y `docs/manual-de-marca.md` son la extracción de los dos
9
+ canvas de Claude Design, en el repo para poder hacer `grep` y para versionarlos.
10
+ El canvas sigue siendo la fuente; esto es la copia consultable.
11
+
12
+ Está aquí por un motivo concreto: la paleta de resaltado vivía escrita a mano en
13
+ un proyecto con un `#E05252` que este README declara incorrecto desde hace
14
+ meses, y nadie lo vio porque el documento no era `grep`-able desde el código.
15
+
16
+ `docs/decisiones.md` es la otra mitad: los quince puntos donde el código y el
17
+ documento no dicen lo mismo, con la resolución y el motivo de cada uno.
18
+
19
+ ## La restricción que manda sobre todo lo demás
20
+
21
+ `src/tokens/` no importa nada: ni React, ni componentes, ni CSS de terceros. Es el
22
+ único subpaquete que pueden consumir los cinco proyectos, incluido un generador
23
+ de OG con Satori y un sitio Astro que no monta React. Si un token termina
24
+ dependiendo de un componente, la librería dejó de ser portable.
25
+
26
+ No es documentación: `pnpm check:tokens` lo verifica en cada build y ESLint lo
27
+ dice en el editor.
28
+
29
+ ## Salida de Tailwind
30
+
31
+ Una sola fuente, `src/tokens/tokens.ts`. Una salida generada,
32
+ `dist/tokens/theme.css`, con `@theme` para Tailwind v4. La genera
33
+ `scripts/build-tokens.mjs`; no se edita a mano y se regenera en cada build.
34
+
35
+ Decisión de la Fase 0: **solo v4**. El portfolio (`eduardoalvarez.dev`) migra de
36
+ Tailwind v3 a v4 antes de consumir Arrecife. Si esa migración se atrasa, volver a
37
+ publicar el preset de v3 es añadir un emisor más a `build-tokens.mjs` que lea el
38
+ mismo objeto `tokens`: la fuente no cambia.
39
+
40
+ ### Consumo
41
+
42
+ ```css
43
+ @import "tailwindcss";
44
+ @import "@eduardoalvarez/arrecife/tokens/theme.css";
45
+ ```
46
+
47
+ El modo oscuro es el primario y es el default. Un proyecto en modo claro declara
48
+ `data-theme="light"` en `<html>`; uno oscuro no necesita declarar nada.
49
+
50
+ Las familias tipográficas se declaran por nombre. Cada proyecto carga Bricolage
51
+ Grotesque, Geist y JetBrains Mono como prefiera: la librería no impone cómo.
52
+
53
+ ### Mapa de tokens a utilidades
54
+
55
+ | Token | Custom property | Utilidad |
56
+ | --- | --- | --- |
57
+ | `colors[modo].surfaceRaised` | `--color-surface-raised` | `bg-surface-raised` |
58
+ | `brand.hull` | `--color-brand-hull` | `bg-brand-hull` |
59
+ | `typeScale.h1` | `--text-h1` | `text-h1` (arrastra line-height, weight y tracking) |
60
+ | `fonts.display` | `--font-display` | `font-display` |
61
+ | `radius.card` | `--radius-card` | `rounded-card` |
62
+ | `spacing.lg` | `--spacing-lg` | `p-lg`, `gap-lg`, `mb-lg` |
63
+ | `control.md` | `--spacing-control-md` | `px-control-md` (padding de botón) |
64
+ | `control.icon` | `--spacing-control-icon` | `size-control-icon` (botón de icono 42×42) |
65
+ | `gradient[modo].hero` | `--gradient-hero` | `degradado-hero` (utilidad, sigue el modo) |
66
+ | `size.nav` | `--spacing-nav` | `h-nav` |
67
+ | `size.content` / `size.wide` | `--container-content` / `--container-wide` | `max-w-content` / `max-w-wide` |
68
+ | `limits.measure` | `--container-measure` | `max-w-measure` |
69
+ | `shadow.standard` | `--shadow-standard` | `shadow-standard` |
70
+ | `motion` | `--duration-standard`, `--ease-standard` | `duration-standard`, `ease-standard` |
71
+
72
+ El variante `light:` está disponible para los casos del modo claro invertido.
73
+
74
+ También se puede consumir el objeto en JS, sin CSS y sin React — es lo que usan
75
+ las plantillas de OG:
76
+
77
+ ```ts
78
+ import { tokens } from '@eduardoalvarez/arrecife/tokens';
79
+ ```
80
+
81
+ El tema de resaltado va en otra subruta por la misma razón — se consume desde
82
+ `astro.config.mjs`, no desde un componente:
83
+
84
+ ```ts
85
+ import { arrecife } from '@eduardoalvarez/arrecife/shiki';
86
+
87
+ export default defineConfig({
88
+ markdown: { syntaxHighlight: 'shiki', shikiConfig: { theme: arrecife } },
89
+ });
90
+ ```
91
+
92
+ **La librería no trae Shiki.** Los proyectos ya resaltan en build con su propia
93
+ herramienta; lo que les faltaba no era un resaltador, era el tema. `CodeBlock`
94
+ sigue recibiendo el código ya resaltado, que es para lo que está escrito.
95
+
96
+ Las plantillas de OG se publican en su propia subruta por la misma razón: un
97
+ generador corre en un worker o en un script de build y no debe arrastrar React ni
98
+ un solo componente.
99
+
100
+ ```ts
101
+ import satori from 'satori';
102
+ import { plantillaArticulo, OG } from '@eduardoalvarez/arrecife/og';
103
+
104
+ const svg = await satori(plantillaArticulo({ title, date, readingMinutes }), {
105
+ width: OG.width, // 1200
106
+ height: OG.height, // 630
107
+ fonts: [...],
108
+ });
109
+ ```
110
+
111
+ Son funciones puras que devuelven el árbol que Satori pinta, construido solo con
112
+ tokens. `dist/og/index.js` no menciona React en ninguna línea, y eso es
113
+ comprobable con un `grep`.
114
+
115
+ ## Cómo se usa desde un proyecto
116
+
117
+ ```tsx
118
+ import { Text, Button, Badge, cn } from '@eduardoalvarez/arrecife';
119
+ import type { TextProps } from '@eduardoalvarez/arrecife';
120
+
121
+ <Text variant="eyebrow" tone="muted">charlas</Text>
122
+ <Text as="h2" variant="h1">Escalar con criterio</Text>
123
+ <Text variant="body">Se corta solo a 68ch.</Text>
124
+ <Text variant="ui" measure={false}>Sin corte, para una celda estrecha.</Text>
125
+ ```
126
+
127
+ Cada componente publica su página de documentación en Storybook con la tabla de
128
+ props generada desde los tipos. Las de `Text`:
129
+
130
+ | prop | tipo | por defecto |
131
+ | --- | --- | --- |
132
+ | `variant` | `display · stat · h1 · h2 · h3 · body · lead · ui · label · tag · meta · chip · eyebrow` | `body` |
133
+ | `tone` | `primary · secondary · muted · accent · warm · success · warning · error` | `primary` |
134
+ | `as` | `h1 · h2 · h3 · h4 · p · span · strong · em · figcaption · caption · legend · dt · dd · li` | según `variant` |
135
+ | `measure` | `boolean` — corta a 68ch | `true` en `body` |
136
+ | `asChild` | `boolean` — renderiza el hijo, para envolver un enlace | `false` |
137
+
138
+ Comprobado empaquetando la librería con `pnpm pack` e instalándola en un proyecto
139
+ aparte: los tipos resuelven desde `dist/`, `./tokens` carga sin arrastrar React y
140
+ `./tokens/theme.css` se resuelve por subruta.
141
+
142
+ ## Scripts
143
+
144
+ | | |
145
+ | --- | --- |
146
+ | `pnpm build` | verifica la pureza de tokens, compila con tsup y genera `theme.css` |
147
+ | `pnpm typecheck` | `tsc --noEmit` |
148
+ | `pnpm lint` | ESLint, incluido el veto a hex literales fuera de `tokens.ts` |
149
+ | `pnpm check:tokens` | falla si `src/tokens/` importa algo de fuera |
150
+ | `pnpm test` | corre axe sobre las 161 stories, en los dos modos |
151
+ | `pnpm check:exports` | verifica que `dist/` tiene lo que `exports` promete |
152
+ | `pnpm storybook` | genera los tokens y levanta Storybook en el 6006 |
153
+
154
+ ## El contraste como test, no como panel
155
+
156
+ `pnpm test` monta cada story en un Chromium real y le pasa axe con
157
+ `a11y: { test: 'error' }`. Corre dos veces, una por modo: un color solo falla en
158
+ uno de los dos, así que pasar en oscuro no prueba nada sobre el claro.
159
+
160
+ Está comprobado que no es decorativo: devolver `textMuted` claro a su valor
161
+ anterior tira ocho stories con «insufficient color contrast of 4.24».
162
+
163
+ Hay una sola regla desactivada, en dos stories concretas y con el motivo escrito
164
+ al lado: `aria-hidden-focus` en `Select`/`DropdownMenu` abiertos. Radix marca
165
+ `aria-hidden` todo lo que queda fuera del portal y deja el disparador dentro
166
+ siendo focusable; el foco está atrapado por su `FocusScope`, así que no se puede
167
+ tabular hasta él. Es un desacuerdo conocido entre axe y Radix.
168
+
169
+ ## Correcciones de contraste
170
+
171
+ El documento de identidad medía todo contra `background`. Pero `surfaceRaised`
172
+ es el peor caso en los **dos** modos: en claro es más oscuro que el fondo de
173
+ página, en oscuro es más claro. Es donde viven menús y tabs activos.
174
+
175
+ | token | antes | ahora | motivo |
176
+ | --- | --- | --- | --- |
177
+ | `light.textMuted` | `#6B7480` | `#626A75` | 4.24 no llegaba a AA sobre papel |
178
+ | `light.warning` | `#9A6A12` | `#8D6111` | 4.23 no llegaba a AA sobre papel |
179
+ | `dark.error` | `#E05252` | `#E15757` | 4.35 sobre `surface`, que es donde va un error de formulario |
180
+
181
+ Los tres conservan tono y saturación exactos: solo cambia la luminosidad entre
182
+ uno y cuatro puntos. `accent` y `warm` claros no se tocan.
183
+
184
+ Token nuevo: `hairlineHover` — `#2C4D5D` en oscuro (el valor de la regla 6) y
185
+ `#D3C8B2` en claro, derivado de igualar el salto perceptual (ΔL\* 10.5) en vez
186
+ de la razón de contraste, que cerca del blanco se pasa de frenada.
187
+
188
+ `textMuted` no va nunca sobre `surfaceRaised`: en oscuro da 4.07. Los menús usan
189
+ `textSecondary`, que da 6.96.
190
+
191
+ ### La tercera corrección: el color semántico no es color de texto sobre su tinte
192
+
193
+ Salió al implementar la receta de avisos del documento — fondo al 8 % del color
194
+ semántico — y la tiró la suite en modo claro, en cinco stories.
195
+
196
+ Los semánticos claros están calibrados para pasar **justo** sobre papel. Teñir el
197
+ fondo con ellos los hunde por debajo de AA:
198
+
199
+ | tono | sobre papel | sobre su propio tinte 8 % | `textPrimary` sobre el tinte |
200
+ | --- | --- | --- | --- |
201
+ | `accent` | 4.55 | **4.12** | 14.82 |
202
+ | `warm` | 4.54 | **4.11** | 14.85 |
203
+ | `success` | 5.80 | 5.17 | 14.63 |
204
+ | `warning` | 4.88 | **4.40** | 14.78 |
205
+ | `error` | 4.87 | **4.35** | 14.64 |
206
+
207
+ No hay alfa que lo arregle: el problema es poner el color encima de sí mismo. La
208
+ resolución no toca la paleta — el tinte es una **superficie**, así que el texto
209
+ que lleva encima es un token de texto. El color semántico se queda donde no es
210
+ texto: el borde y el glifo.
211
+
212
+ El 8 %, por cierto, aguanta igual o mejor sobre papel que sobre abismo (1.106 vs.
213
+ 1.149 en acento). La sospecha de que el modo claro necesitaba una segunda tabla
214
+ iba al revés: el punto flojo del sistema es `error` sobre abismo, 1.067.
215
+
216
+ ## Publicar una versión
217
+
218
+ **No hay pasos manuales.** El tag, el CHANGELOG y el bump de versión los hace
219
+ release-please a partir de los commits convencionales que ya se escriben —y que
220
+ `lint-pr-title` ya obliga a escribir bien.
221
+
222
+ El ciclo, entero, está en `.github/workflows/release.yml`:
223
+
224
+ 1. Mergeas un PR a `main` con un título tipo `feat(badge): …`.
225
+ 2. release-please abre —o actualiza— un PR llamado `chore: versión X.Y.Z` con el
226
+ bump en `package.json` y la entrada nueva del `CHANGELOG.md`. Ese PR se queda
227
+ abierto y se va acumulando con cada merge, así que puedes juntar varios
228
+ cambios en una versión.
229
+ 3. Cuando lo mergeas, corta el tag, crea el release y dispara la publicación.
230
+ 4. Antes de subir nada, el workflow comprueba que el tag y `package.json`
231
+ coinciden, y corre lint, tipos, build, la verificación de `exports` y la
232
+ suite completa en los dos modos.
233
+
234
+ `feat:` sube la minor y `fix:` la patch. Mientras la versión sea `0.x`, un
235
+ cambio que rompe sube la **minor** y no la major: eso es lo que significa el
236
+ `0.` — que la API todavía se puede mover sin gastar la 1.0. Está en
237
+ `release-please-config.json`, y ahí también está `initial-version` con el `0.1.0`
238
+ de la primera versión.
239
+
240
+ Cuando la API se estabilice, se sube a `1.0.0` a mano una vez y a partir de ahí
241
+ un `BREAKING CHANGE:` sube la major como en cualquier paquete.
242
+
243
+ ### La publicación de confianza
244
+
245
+ El workflow publica con **OIDC**: GitHub emite un token que prueba «este build
246
+ salió de este repo y de este workflow», y npm lo cambia por permiso de
247
+ publicación. No hay ningún secreto de larga vida que robar, ni que rotar. De
248
+ paso genera la **procedencia**, que firma el paquete con un enlace verificable a
249
+ ese commit exacto.
250
+
251
+ Se configura una vez, en npmjs.com → el paquete → *Settings* → *Trusted
252
+ publisher*:
253
+
254
+ | Campo | Valor |
255
+ | --- | --- |
256
+ | Publisher | GitHub Actions |
257
+ | Organization or user | `Proskynete` |
258
+ | Repository | `arrecife` |
259
+ | Workflow filename | `release.yml` |
260
+ | Environment | `npm` |
261
+
262
+ Ese *Workflow filename* es la razón de que el release y la publicación vivan en
263
+ un solo archivo en vez de en un workflow reutilizable: npm casa el token contra
264
+ un nombre, y con `workflow_call` hay dos candidatos.
265
+
266
+ **El huevo y la gallina.** No se puede configurar un publicador de confianza en
267
+ un paquete que todavía no existe, así que la primera versión necesita token:
268
+
269
+ 1. Crea el entorno `npm` en la configuración del repo con el secreto
270
+ `NPM_TOKEN` (un token de tipo *automation*).
271
+ 2. Sube la versión y publica la primera vez. El workflow avisa en el log de que
272
+ está usando token.
273
+ 3. Configura la publicación de confianza con la tabla de arriba.
274
+ 4. **Borra el secreto `NPM_TOKEN`.** El paso que lo usa se salta solo cuando no
275
+ está, y OIDC toma el relevo sin tocar ni una línea del workflow.
276
+
277
+ Para probar sin gastar una versión: *Actions → Release y publicación → Run
278
+ workflow* con el ensayo activado. Hace todo menos publicar, no necesita token, y
279
+ el resumen del run lista qué archivos viajarían y cuánto pesa el tarball.
280
+
281
+ ## Estado
282
+
283
+ - **Fase 1** · andamiaje, tokens y Storybook con el switch de tema. Completa.
284
+ - **Fase 2** · `brand/`. Completa con los PNG que ya existían.
285
+ - **Fase 3** · los 18 primitivos sobre shadcn/Radix, más `Text` y ocho añadidos
286
+ después de medir el uso real en los cinco proyectos. Completa.
287
+ - **Fase 4** · `AudioPlayer`, migrado. Completa.
288
+ - **Fase 5** · completa. `ArticleCard`, `AuthorCard`, `TalkCard`, `CourseCard`,
289
+ `LinkRow`, `CodeBlock`, `Blockquote`, `PageHeader`, `EmptyState`, `Breadcrumb`,
290
+ `Nav`, `SidebarNav`, `TableOfContents`, `Stat`, `Footer`, `Hero`,
291
+ `NewsletterForm`, `og/` y `shiki/`.
292
+
293
+ El criterio para decidir qué entra sigue siendo el mismo: **codifica una regla
294
+ de identidad, tiene dos o más consumidores, y no arrastra infraestructura del
295
+ proyecto.**
296
+
297
+ ### `Hero` y `NewsletterForm` volvieron a entrar
298
+
299
+ Estaban fuera de la lista con un argumento escrito, y el argumento se revisó.
300
+
301
+ **`Hero`.** Se había descartado porque «el hero del portafolio y el de cursos son
302
+ el mismo esqueleto que una cabecera de sección, y `PageHeader` los cubre con una
303
+ prop de escala». Eso vale para el texto y solo para el texto. El hero del
304
+ documento tiene además degradado, radio de panel, texto acotado al 62 % del ancho
305
+ y la pose sangrando por la esquina inferior derecha — nada de lo cual cabe en una
306
+ prop de escala de `PageHeader`, y todo lo cual son reglas de identidad que si no
307
+ viven aquí se reimplementan cinco veces. Son dos piezas distintas: `PageHeader`
308
+ sigue siendo la cabecera de sección y va dentro de `<main>`; `Hero` es la portada
309
+ y va uno por sitio.
310
+
311
+ **`NewsletterForm`.** Se había descartado porque «la mitad de su código es un
312
+ `POST` a un endpoint que solo vive ahí: eso es infraestructura». Correcto, y por
313
+ eso el `POST` no está aquí. El componente es presentacional: recibe `state` y
314
+ emite `onSubmitEmail`, y el proyecto hace la llamada con su proveedor. Lo que sí
315
+ es identidad son los cuatro estados y, sobre todo, que el aviso vaya **debajo**
316
+ del formulario en vez de reemplazarlo — reemplazarlo es lo que rompe el caso real
317
+ de quien se suscribe con el correo equivocado.
318
+
319
+ `Nav`, `Footer`, `Breadcrumb` y `Hero` son composición de página y se pueden
320
+ discutir como piezas de librería. Entran igual: la estética CLI —el `./sección`
321
+ de la barra, el `~ / artículos / slug` de la ruta, la firma `$ cd ~/…` del pie—
322
+ es lo primero que se desincroniza cuando cinco proyectos la escriben cada uno por
323
+ su cuenta.
324
+
325
+ ### Decisiones de la Fase 3
326
+
327
+ - **Sin `lucide-react`.** Los ocho glifos que los primitivos necesitan están
328
+ inline en `src/lib/glyphs.tsx`, heredan `currentColor` y miden 1em. Una
329
+ librería de iconos como dependencia se la come cada uno de los cinco proyectos.
330
+ - **Sin animaciones de entrada.** Modales, menús, tooltips y toasts aparecen
331
+ donde van a quedarse. La perilla del `Switch` cambia de posición sin deslizarse.
332
+ La única transición del sistema es `transition-standard`, que solo puede animar
333
+ color y borde porque así está escrita la utilidad.
334
+ - **Una excepción, documentada:** el spinner de `Button loading` gira. Un botón
335
+ cargando sin movimiento es indistinguible de uno deshabilitado; es
336
+ realimentación de progreso, no de estado, y va envuelto en `motion-safe`.
337
+ - **`Progress` exige `label`.** Una barra sin nombre accesible no dice de qué es
338
+ el progreso, y ninguna otra parte del componente puede deducirlo.
339
+ - **Cero hex literales**, incluido `Button`. La regla 2 sale con
340
+ `light:bg-brand-hull`, porque el casco ya era un token.
341
+ - **`cursor-pointer` explícito** en todo lo que se pulsa. Tailwind v4 quitó del
342
+ preflight el `cursor: pointer` de `button`, así que un botón sin la clase se
343
+ queda con la flecha del sistema. Lo llevan `Button`, `Checkbox`,
344
+ `RadioGroupItem`, `Switch`, `TabsTrigger`, el disparador de `Select`, los
345
+ cerrar de `Dialog`/`Sheet`/`Toast`, `PaginationLink`, el casco de las tarjetas
346
+ pulsables y los enlaces de `Nav`, `Footer` y `Breadcrumb` — que renderizan
347
+ `<a>` sin `href` cuando se les enchufa el `Link` de un enrutador.
348
+
349
+ Dos excepciones deliberadas. `Label` apunta a un control pero no es el control.
350
+ Y los **ítems de menú** de `Select` y `DropdownMenu` se quedan en
351
+ `cursor-default`: un menú nativo no muestra la manito, y el highlight de la
352
+ fila ya dice que la fila responde.
353
+
354
+ ### La paleta de sintaxis
355
+
356
+ Del documento, literal: «keywords arena, strings bioluz, comments plancton,
357
+ identifiers espuma», sobre casco. Cuatro colores a propósito — funciones,
358
+ variables y tipos caen los tres en espuma, porque el sistema se comunica con
359
+ color y borde y no con ruido cromático. Los números y booleanos van con las
360
+ cadenas: el documento no los asigna, y agruparlos por «son literales» es más
361
+ coherente que estrenar un quinto color.
362
+
363
+ Medido sobre `brand.hull` #0B1524, todo AA:
364
+
365
+ | rol | token | contraste |
366
+ | --- | --- | --- |
367
+ | identificador | `textPrimary` | 16.42:1 |
368
+ | literal | `accent` | 10.05:1 |
369
+ | palabra clave | `warm` | 9.05:1 |
370
+ | comentario | `textMuted` | 5.43:1 |
371
+ | invalidez | `error` | 4.97:1 |
372
+
373
+ `brand.body` (#3E7CB1) no entra: el sistema lo restringe a relleno y aquí mide
374
+ 4.2:1.
375
+
376
+ Vivía escrita a mano en `eduardoalvarez.dev/src/settings/shiki-reef.ts`, y ahí
377
+ dentro se había quedado un `#E05252` — justo el hex que este README dice que está
378
+ mal. Es el caso de libro de por qué la paleta no puede vivir dentro de un
379
+ proyecto: el tema se genera desde `tokens.sintaxis` y el rojo sale corregido solo.
380
+
381
+ ### Temas anidables
382
+
383
+ `theme.css` emite un bloque por modo, no solo el claro. Así un subárbol puede
384
+ declarar el modo contrario al de la página y todo lo de dentro lo respeta.
385
+
386
+ Lo usa `CodeBlock`: `brand.hull` es «fondo de bloques de código», así que un
387
+ bloque es oscuro también en modo claro — y ahí `textPrimary` es casi negro. La
388
+ raíz del bloque declara `data-theme="dark"` y la tinta se resuelve sola. Es la
389
+ única isla de tema invertido del sistema, y es deliberada.
390
+
391
+ ### Las tarjetas y la regla 6
392
+
393
+ `ArticleCard`, `TalkCard`, `CourseCard` y `LinkRow` comparten un casco interno
394
+ que no se publica, para que la regla 6 viva en un solo sitio: el hover cambia el
395
+ borde de `hairline` a `hairlineHover` y tiñe el título de acento. Nada más.
396
+
397
+ `LinkRow` viene de `links/src/components/Card.astro`, que escalaba la tarjeta al
398
+ 102 %, subía el título un píxel y giraba y agrandaba el icono — cuatro
399
+ movimientos que el sistema no permite.
400
+
401
+ Ninguna tarjeta depende de un enrutador: por defecto renderizan un `<a href>`, y
402
+ `asChild` deja enchufar el `Link` de Next o de Astro.
403
+
404
+ ### `AudioPlayer` — qué cambió al migrarlo
405
+
406
+ La lógica no se reescribió. Los tres modos, el reproductor flotante, los saltos
407
+ de ±15s, el ciclo de velocidad 1 → 1.25 → 1.5 → 1.75 → 2 y el volumen con mute
408
+ son los del portafolio. Lo que cambió:
409
+
410
+ **Dos dependencias que un paquete no puede tener.** `Icon` del portafolio pasó a
411
+ `src/lib/glyphs.tsx` con los trazados idénticos; `trackEvent` pasó a la prop
412
+ `onFirstPlay`, que sigue disparándose una sola vez por carga.
413
+
414
+ **Un cambio de API.** `compact`/`banner` como dos booleanos pasaron a
415
+ `mode="full" | "compact" | "banner"`, que es el vocabulario con el que ya se
416
+ describían los tres modos. Hay que tocar las llamadas del portafolio en la Fase 6.
417
+
418
+ **Tres animaciones que el sistema no permite.** La onda ya no anima `scaleY` — las
419
+ barras siguen distinguiendo reproducción de pausa por opacidad. El flotante
420
+ aparece y desaparece en vez de deslizarse. La barra de progreso ya no interpola
421
+ el ancho, que además la hacía ir por detrás del audio. El giro del spinner de
422
+ carga se queda, con la misma justificación que en `Button`.
423
+
424
+ **Un fallo de contraste heredado.** El botón de velocidad ponía `textMuted` sobre
425
+ `surfaceRaised`: 4.07:1 en oscuro. Pasó a `textSecondary`. El original arrastra
426
+ ese fallo.
427
+
428
+ **Un `bug` latente.** Las piezas del reproductor viven a nivel de módulo, no
429
+ dentro del componente. Declaradas dentro, cambian de identidad en cada render y
430
+ React las remonta: con `timeupdate` disparando cuatro veces por segundo, el
431
+ arrastre de la barra perdía el pointer capture.
432
+
433
+ ### La marca
434
+
435
+ Las trece piezas de Tiburoncín estaban repartidas por los cinco proyectos, byte
436
+ a byte idénticas. Se consolidaron en `assets/brand/` y se publican en el
437
+ paquete; se sirven en `/brand`, que es la misma ruta que todos usan ya desde su
438
+ `public/`, así que el valor por defecto de `basePath` funciona sin configurar
439
+ nada.
440
+
441
+ ```tsx
442
+ import { Logo, Mascota, CaraDeMascota, listaCaras } from '@eduardoalvarez/arrecife/brand';
443
+ ```
444
+
445
+ | | |
446
+ | --- | --- |
447
+ | aletas | `fin.png` (dos azules) y `fin-foam.png` (silueta espuma) |
448
+ | caras | annoyed · confused · hearts · laughing · shades · waiting · wink |
449
+ | poses | desk · laptop-coffee · peek · surf |
450
+
451
+ Los nombres son un tipo: una cara que no existe no compila, y el autocompletado
452
+ ofrece las que hay. Añadir una es soltar el PNG y añadir una línea al catálogo.
453
+
454
+ **Regla 1 como API.** `sobre="oscuro"` usa la silueta a una tinta y
455
+ `sobre="claro"` la de dos azules. No es una nota en una guía: es una prop. El
456
+ análisis de píxeles lo confirma — el 94 % de `fin-foam.png` es `#EDF4F3`, o sea
457
+ el token espuma.
458
+
459
+ **Regla 5 como API.** El wordmark sale de `naming.wordmark` y siempre dice
460
+ «Eduardo Álvarez». No hay ninguna prop que permita cambiar ese texto, y
461
+ Tiburoncín no aparece escrito dentro del logo.
462
+
463
+ **Regla 4 como API.** Las caras van solo en estados vacíos, confirmaciones,
464
+ errores, progreso de curso y celebración. La regla vive en qué componentes
465
+ aceptan una cara, no en la documentación.
466
+
467
+ El formato es un detalle de implementación: cuando lleguen los SVG, se
468
+ reemplazan los archivos y no cambia una línea de código.
469
+
470
+ ### Los ocho que se añadieron después
471
+
472
+ No estaban en la lista original. Entraron midiendo en cuántos de los cinco
473
+ proyectos se usa cada uno, con el mismo criterio que sacó a `Hero` y
474
+ `NewsletterSection`.
475
+
476
+ | | archivos que lo usan | por qué |
477
+ | --- | --- | --- |
478
+ | `Card` | 34, en 4 proyectos | Es la única definición de qué es una superficie de tarjeta. Las cuatro tarjetas con dominio reutilizan sus clases. |
479
+ | `Label` | 21, en 2 | Había siete controles de formulario y ninguna etiqueta. |
480
+ | `Avatar` | 19, en 3 | Uno solo para todo: no hay un `brand/Avatar` aparte, porque una foto de perfil es esto con otro `src`. |
481
+ | `Sheet` | 6, en 3 | Es `Dialog` con variante de lado. |
482
+ | `Separator` | 8, en 2 | `hairline` era un token sin componente. |
483
+ | `Popover` | 5, en 3 | Base de cualquier selector desplegable. |
484
+ | `DateField` | — | El control nativo, sin dependencias, para elegir fecha en un formulario. |
485
+ | `Calendar` | 6, en 3 | Calendario mensual navegable, para el planificador de contenido. `fullWidth` lo estira al ancho del contenedor. |
486
+
487
+ No hay `DatePicker`: son `Popover` más `Calendar` y son cinco líneas. Un tercer
488
+ componente que solo pega dos que ya existen es superficie de API que mantener sin
489
+ ganar nada.
490
+
491
+ `Popover` exige `aria-label` o `aria-labelledby` en el tipo. Radix le pone
492
+ `role="dialog"` al contenido, y un diálogo sin nombre accesible no le dice nada a
493
+ un lector de pantalla: ahora no se puede olvidar porque no compila.
494
+
495
+ ### La segunda excepción de movimiento
496
+
497
+ `Sheet` se desliza. Es la segunda y última excepción a «nada de desplazamiento»,
498
+ aprobada a sabiendas: un panel que entra desde un borde quieto sería un modal
499
+ descentrado. Dura `--duration-standard` con `--ease-standard` —lo mismo y con la
500
+ misma curva que cualquier cambio de color— así que no introduce un tiempo nuevo,
501
+ y va detrás de `motion-safe`.
502
+
503
+ `Calendar` **no** anima el cambio de mes: `animate` de react-day-picker se queda
504
+ en su valor por defecto, que es apagado.
505
+
506
+ ### `Text` — la escala como API
507
+
508
+ `Text` no estaba en la lista original y se añadió después, porque sin él la
509
+ escala solo existía como clases sueltas y nada impedía poner `text-display` en un
510
+ párrafo. Tres reglas del sistema viven dentro del componente:
511
+
512
+ | regla | cómo se aplica |
513
+ | --- | --- |
514
+ | display solo para titulares, nunca cuerpo | la familia va atada a la escala; no existe una prop `font` |
515
+ | el peso y el tracking son de la escala | vienen del token `--text-*` y no se exponen |
516
+ | medida máxima de cuerpo 68ch | `body` la aplica solo; `measure={false}` la quita |
517
+
518
+ `as` y `variant` son independientes a propósito: un encabezado de segundo nivel
519
+ que tiene que verse más pequeño es `<Text as="h2" variant="h3">`, no un `h3` que
520
+ miente sobre la jerarquía de la página.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file