@eduardoalvarez/arrecife 0.6.0 → 0.8.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 (56) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +288 -49
  3. package/dist/brand/index.cjs +2 -2
  4. package/dist/brand/index.js +3 -3
  5. package/dist/chart/index.cjs +129 -2
  6. package/dist/chart/index.d.cts +114 -4
  7. package/dist/chart/index.d.ts +114 -4
  8. package/dist/chart/index.js +129 -5
  9. package/dist/{chunk-PMN7NR3G.js → chunk-5A5GH2PF.js} +1 -1
  10. package/dist/{chunk-DKCN7BAL.js → chunk-6IGD5REB.js} +1 -1
  11. package/dist/{chunk-XKYHTOUJ.js → chunk-FAAGZG7A.js} +1 -1
  12. package/dist/{chunk-O4TAH7YJ.js → chunk-FGFNK72B.js} +29 -5
  13. package/dist/chunk-HOADZ6GS.js +72 -0
  14. package/dist/chunk-LXRGQKMG.js +145 -0
  15. package/dist/{chunk-6O3KWB6P.js → chunk-MPZBF2TZ.js} +2 -2
  16. package/dist/{chunk-JMOOFZ3B.js → chunk-TRPBID2W.js} +1 -1
  17. package/dist/{chunk-25YNFCIF.js → chunk-XXDATT3A.js} +6 -3
  18. package/dist/doctor.mjs +248 -0
  19. package/dist/form/index.cjs +2 -2
  20. package/dist/form/index.d.cts +1 -1
  21. package/dist/form/index.d.ts +1 -1
  22. package/dist/form/index.js +4 -4
  23. package/dist/icons/index.cjs +149 -0
  24. package/dist/icons/index.d.cts +94 -0
  25. package/dist/icons/index.d.ts +94 -0
  26. package/dist/icons/index.js +28 -0
  27. package/dist/index-BbRplw_B.d.cts +58 -0
  28. package/dist/index-BbRplw_B.d.ts +58 -0
  29. package/dist/index.cjs +425 -251
  30. package/dist/index.d.cts +303 -104
  31. package/dist/index.d.ts +303 -104
  32. package/dist/index.js +283 -285
  33. package/dist/{label-MgHFKnFy.d.ts → label-DJ4HuD-R.d.cts} +3 -2
  34. package/dist/{label-MgHFKnFy.d.cts → label-DJ4HuD-R.d.ts} +3 -2
  35. package/dist/og/index.cjs +3 -2
  36. package/dist/og/index.js +1 -1
  37. package/dist/shiki/index.js +1 -1
  38. package/dist/social/data.cjs +161 -0
  39. package/dist/social/data.d.cts +161 -0
  40. package/dist/social/data.d.ts +161 -0
  41. package/dist/social/data.js +2 -0
  42. package/dist/social/index.cjs +153 -0
  43. package/dist/social/index.d.cts +2 -0
  44. package/dist/social/index.d.ts +2 -0
  45. package/dist/social/index.js +3 -0
  46. package/dist/tokens/index.cjs +29 -5
  47. package/dist/tokens/index.d.cts +37 -10
  48. package/dist/tokens/index.d.ts +37 -10
  49. package/dist/tokens/index.js +2 -2
  50. package/dist/tokens/theme.css +79 -22
  51. package/dist/variants/index.cjs +6 -3
  52. package/dist/variants/index.d.cts +6 -3
  53. package/dist/variants/index.d.ts +6 -3
  54. package/dist/variants/index.js +1 -1
  55. package/llms.txt +530 -80
  56. package/package.json +29 -2
package/llms.txt CHANGED
@@ -41,7 +41,7 @@ Radix, `clsx`, `tailwind-merge`, `class-variance-authority`, `date-fns` and
41
41
  `react-day-picker` come as dependencies of the library. You do not need to
42
42
  install or declare them.
43
43
 
44
- **It ships no `lucide-react` and no icon library.** The glyphs the components
44
+ **It ships no icon set.** The glyphs the components
45
45
  need are inline, inherit `currentColor` and measure 1em.
46
46
 
47
47
  ## Tailwind configuration
@@ -116,23 +116,27 @@ that only whoever uses them installs.
116
116
  | `@eduardoalvarez/arrecife/og` | **no** | — | The Open Graph templates for Satori |
117
117
  | `@eduardoalvarez/arrecife/shiki` | **no** | — | The syntax highlighting theme |
118
118
  | `@eduardoalvarez/arrecife/brand` | yes | — | Logo, isotype and mascot as components |
119
+ | `@eduardoalvarez/arrecife/social` | yes, on the server only | — | The ten social icons, loose. No `"use client"` |
120
+ | `@eduardoalvarez/arrecife/social/data` | **no** | — | The same ten as shapes, plus `socialSvg`. For a template that mounts no React |
121
+ | `@eduardoalvarez/arrecife/icons` | yes | `@phosphor-icons/react` | `Icon`, which draws a Phosphor icon at the system's size, and at the weight its role asks for |
119
122
  | `@eduardoalvarez/arrecife/form` | yes | `react-hook-form` | The form layer: labels, errors and `aria-*` |
120
- | `@eduardoalvarez/arrecife/chart` | yes | `recharts` | The chart chassis and the series palette |
123
+ | `@eduardoalvarez/arrecife/chart` | yes | `recharts` | The chart chassis, the series palette and the three chart types |
121
124
  | `@eduardoalvarez/arrecife/assets/*` | — | — | The brand PNGs |
122
125
 
123
126
  Importing the root from a build script to get one token is the mistake the
124
127
  subpaths exist to prevent: it drags all of React into a worker that never mounts
125
128
  it.
126
129
 
127
- `./form` and `./chart` sit outside the root for the symmetric reason: if they
128
- hung off the main index, the projects that draw no charts and use no React Hook
129
- Form would have to install those dependencies anyway so their bundler could
130
- resolve an import they never execute.
130
+ `./form`, `./chart` and `./icons` sit outside the root for the symmetric reason:
131
+ if they hung off the main index, the projects that draw no charts, use no React
132
+ Hook Form and need no icons would have to install those dependencies anyway so
133
+ their bundler could resolve an import they never execute. Two of the five consume
134
+ zero icons.
131
135
 
132
136
  ### Next, Server Components and `"use client"`
133
137
 
134
138
  The root, `./brand`, `./form` and `./chart` ship `"use client"` in the published
135
- `dist/`. They render React and their Radix primitives call `createContext` at
139
+ `dist/`, and they are the only four. They render React and their Radix primitives call `createContext` at
136
140
  module scope, so without the directive a Next project with the App Router cannot
137
141
  import them at all: it fails at build time with
138
142
  `TypeError: (0 , r.createContext) is not a function`.
@@ -143,9 +147,13 @@ import in an adapter of your own marked `"use client"` — that was the workarou
143
147
  before 0.6.0 and it pulled 272 KB of client chunk in for components that never
144
148
  needed it.
145
149
 
146
- The five portable subpaths do NOT carry the directive, and that is the half that
147
- matters in a Server Component: `./tokens`, `./theme`, `./variants`, `./og` and
148
- `./shiki` stay on the server. If all you need are classes — for a `<div>`, an
150
+ The six portable subpaths do NOT carry the directive, and that is the half that
151
+ matters in a Server Component: `./tokens`, `./theme`, `./variants`, `./social/data`,
152
+ `./og` and `./shiki` stay on the server. Neither does `./social`, which is a third
153
+ case: it renders React — it is ten `<svg>` — so it can never be portable, but it
154
+ holds no state and nothing about it needs a client boundary. It is the only way to put a
155
+ social icon in a Server Component, and § «The social icons come from `./social`»
156
+ below says why the grouped form cannot do it. If all you need are classes — for a `<div>`, an
149
157
  `<a>` or an Astro island you do not want to hydrate — import them from
150
158
  `./variants` and nothing crosses to the client:
151
159
 
@@ -202,21 +210,324 @@ It has to go INLINE. A `<script src>`, even a synchronous one, gets downloaded,
202
210
  and the flash comes back. The script re-attaches on `astro:after-swap` because
203
211
  view transitions replace the whole `<html>`.
204
212
 
205
- ### The social icons are namespaced
213
+ ### `npx arrecife` — run it once after installing
214
+
215
+ Two things break with **no error at all**, and the command catches both.
216
+
217
+ **Tailwind purges everything the components emit** unless the stylesheet has
218
+ `@source "<path>/node_modules/@eduardoalvarez/arrecife/dist"`. It does not scan
219
+ `node_modules`. There is no console error and no undefined class: the card mounts
220
+ with no padding, no radius and no border. The path is relative to the SHEET, not
221
+ to the project root, and the command computes it.
222
+
223
+ **A `--color-*` of yours silently replaces ours.** A project coming from shadcn
224
+ has `@theme inline { --color-accent: var(--accent); }` — shadcn's `--accent` is
225
+ the hover surface, `#17303E`, and ours is the brand turquoise, `#35D6C0`. That
226
+ one line repainted 88 classes inside the library's own components grey. Five
227
+ names collide in total; four agree on the value and are harmless, and the command
228
+ tells them apart.
229
+
230
+ Do not silence it by removing the `@import`: the fix is the `@source` line, or
231
+ renaming your own token.
232
+
233
+ ### `Nav` is two slots and one height
234
+
235
+ Almost everything an app shell wants from a site bar is already a slot:
236
+
237
+ ```tsx
238
+ <Nav
239
+ size="compact" // 56px, for a shell with a sidebar
240
+ brand={<a href="/"><Logo /><span><span className="text-accent">~/</span>cursos</span></a>}
241
+ actions={session ? <UserMenu /> : <Button size="sm" asChild><Link href="/login">Entrar</Link></Button>}
242
+ >
243
+ <NavItem href="/cursos" active>cursos</NavItem>
244
+ </Nav>
245
+ ```
246
+
247
+ `brand` and `actions` are `ReactNode`, so a wordmark, a user menu, a theme toggle
248
+ or a search box go in without the library knowing anything about sessions.
249
+ **Session state does not get a prop** — it is project infrastructure, and the
250
+ library takes none.
251
+
252
+ `size="compact"` is 56px instead of 64, for a bar that shares the screen with a
253
+ sidebar. It is a prop and not a class because the height lives on `Nav`'s inner
254
+ container: `className` reaches the `<header>` and stops there, so passing `h-14`
255
+ does nothing.
256
+
257
+ **One `Nav` per page.** It renders the site's `banner` landmark, and two banners
258
+ on one page is an accessibility failure — which is also why `PageHeader` goes
259
+ inside `<main>` and is not a landmark. See `decisions/0.7.md` § 30.
260
+
261
+ ### Icons are yours, the way they are drawn is not
262
+
263
+ The library ships no icon set and `lib/glyphs.tsx` is not exported: it is the
264
+ minimum set the primitives need and it does not grow. What the library does ship
265
+ is the drawing.
266
+
267
+ ```tsx
268
+ import { GraduationCap, Trash } from '@phosphor-icons/react';
269
+ import { Icon } from '@eduardoalvarez/arrecife/icons';
270
+
271
+ // 1em, and `tone="action"` by default — `regular`, the stroke the document names
272
+ <Icon as={GraduationCap} />
273
+
274
+ // Inside a control with no text, the name goes on the CONTROL
275
+ <Button size="icon-sm" variant="secondary" aria-label="Borrar la fila">
276
+ <Icon as={Trash} />
277
+ </Button>
278
+
279
+ // Alone and meaning something on its own, it gets a name
280
+ <Icon as={Trophy} label="Curso completado" />
281
+ ```
282
+
283
+ **Do not size them by hand.** `size-4`, `size-3.5`, `size-6` scattered through a
284
+ codebase is what this replaces: at 1em the icon takes the size of the text it
285
+ sits in — 13px beside `text-label`, 15px beside `text-ui` — and nobody picks a
286
+ number.
287
+
288
+ **The weight is not yours to pick either, but it is not one value.** `tone` names
289
+ what the icon is doing and the weight follows from it. There are three and there
290
+ is no fourth:
291
+
292
+ | `tone` | Weight | What it is |
293
+ | --- | --- | --- |
294
+ | `action` · the default | `regular` | An icon that is a control or names one. It is the system's line: 16 on a 256 grid = 0.0625em, against the document's 1.6 on a 24 grid = 0.0667em. Six per cent apart, which is no pixel on any screen |
295
+ | `current` | `fill` | The one of a set you are on — the nav item carrying `aria-current` |
296
+ | `quiet` | `light` | Furniture: a marker in a metadata row, not a control and not a state |
297
+
298
+ ```tsx
299
+ <NavItem href="/cursos" active icon={<Icon as={GraduationCap} tone="current" />}>
300
+ cursos
301
+ </NavItem>
302
+ ```
303
+
304
+ `current` is the one that earns the axis. An active item already paints itself
305
+ biolume, and colour on its own is the channel WCAG 1.4.1 says may not carry
306
+ meaning alone; the fill is the second channel, and it is the one that survives a
307
+ forced-colours mode. `weight` is deliberately **not** a prop: Phosphor ships six
308
+ and this system reads three, because `thin`, `bold` and `duotone` have no role
309
+ behind them here.
310
+
311
+ `@phosphor-icons/react` is an **optional** peer dependency. If your project uses
312
+ no icons you install nothing; two of the five do exactly that.
313
+
314
+ **An icon is not illustration, and the two never substitute for each other.**
315
+ Tiburoncín — the faces, the poses, the fin — is the mascot; it comes from
316
+ `./brand`, and the manual says where a face may appear: empty states,
317
+ confirmations, errors, course progress, celebration, and nowhere else. An icon is
318
+ functional vocabulary and goes wherever a control needs a label it cannot spell.
319
+ Do not put an icon where the system asks for a face, and do not put a face where
320
+ a control wants an icon.
321
+
322
+ **In Next, import from `@phosphor-icons/react/ssr` inside a Server Component.**
323
+ Phosphor's default build reads `IconContext` through `useContext`, and a hook in
324
+ a Server Component throws. It ships no `"use client"` to stop you, so the failure
325
+ arrives at render rather than at build. The `/ssr` entry is the same icons
326
+ without the context read, and `Icon` works with either.
327
+
328
+ See `decisions/0.7.md` § 29 and § 35.
329
+
330
+ ### `Stat`'s delta says direction, not judgement
206
331
 
207
332
  ```tsx
208
- // ❌ does not exist
333
+ <Stat
334
+ label="alumnos"
335
+ value="1.284"
336
+ delta={{ value: '+12 esta semana', direction: 'up' }}
337
+ />
338
+ ```
339
+
340
+ `direction` picks the arrow and **never the colour**. «+12 alumnos» and «+12
341
+ errores» point the same way and mean opposite things, so whether a number is good
342
+ news is `tone`'s job and yours: `neutral` for a datum, `alert` when the number IS
343
+ the problem, `achievement` when it is the reward. `alert` and `achievement` paint
344
+ the same sand on purpose — the API is the meaning, the colour is the
345
+ implementation. See `decisions/0.7.md` § 28.
346
+
347
+ `delta.value` arrives already formatted, like `value`: the library imposes no
348
+ locale and computes no percentage. `spark` is a `ReactNode` and the library ships
349
+ no sparkline — pass your own, exactly like `icon`.
350
+
351
+ **Do not colour the number.** A neutral `Stat` renders its value in primary ink,
352
+ and biolume goes on the icon badge and the sparkline instead: three accents in
353
+ one card and the figure stops being the loudest thing in it. `alert` and
354
+ `achievement` DO paint the number sand, which is how «this number is not just a
355
+ number» is said. See `decisions/0.7.md` § 31.
356
+
357
+ **`icon` is a badge in the corner opposite the title**, in a circle tinted at
358
+ 10 % of the tone. You pass the glyph; the circle, the tint and the size are the
359
+ component's.
360
+
361
+ ### The two shapes of `EmptyState`
362
+
363
+ ```tsx
364
+ // The empty state IS the screen: a search with no results, a 404, a section
365
+ // with nothing in it yet. It carries the face, and `expression` is mandatory.
366
+ <EmptyState expression="waiting" title="Sin resultados" description="…" />
367
+
368
+ // The hole INSIDE something else: a table page, a dashboard widget. No face, no
369
+ // surface, no border — the table or the card already draws the region.
370
+ <EmptyState variant="inline" title="No hay lecciones en esta página" />
371
+
372
+ // ❌ does not compile: the props are a union, and `inline` has no face
373
+ <EmptyState variant="inline" expression="waiting" title="…" />
374
+ ```
375
+
376
+ `inline` takes an optional `icon` — a `ReactNode` the project passes and sizes,
377
+ at 1em and in `currentColor`, like `Stat`'s. The library ships no icons.
378
+
379
+ Do not reach for `page` inside a table because the face is «nicer»: an admin
380
+ screen with a dozen empty regions gets a dozen mascots, which is what made every
381
+ consuming project write its own empty state instead of using this one.
382
+
383
+ ### The two shapes of `Footer`
384
+
385
+ ```tsx
386
+ // The default. Stacked rows, signature level with the first one.
387
+ <Footer brand={<Logo />} social={SOCIAL} />
388
+
389
+ // `full`. Brand and description on the left, link columns on the right,
390
+ // signature closing the piece behind a hairline.
391
+ <Footer
392
+ variant="full"
393
+ brand={<Logo />}
394
+ description="Cursos para construir con IA, directos y al grano."
395
+ columns={[
396
+ { title: 'Aprendizaje', links: [{ label: 'Cursos', href: '/cursos' }] },
397
+ { title: 'Legal', links: [{ label: 'Términos', href: '/terminos' }] },
398
+ ]}
399
+ social={SOCIAL}
400
+ action={<Button variant="tertiary" size="sm">Reportar un problema</Button>}
401
+ linkAsChild={({ href, children }) => <Link href={href}>{children}</Link>}
402
+ />
403
+ ```
404
+
405
+ **Passing no `variant` is the default shape, and it is not a fallback**: two of
406
+ the three sites that draw a footer want exactly that, and it has not changed.
407
+
408
+ `columns`, `description`, `action` and `linkAsChild` exist ONLY on `full`. The
409
+ props are a discriminated union, like `EmptyState`'s, so the default form cannot
410
+ be handed one — if `tsc` rejects a `columns` you passed, you meant to pass
411
+ `variant="full"` as well.
412
+
413
+ Reach for `full` whenever the footer has links at all. **There is no other
414
+ place to put them**: `Footer` takes no `children`, so a row of loose text links
415
+ does not compile. That row existed until 0.8.0 and no project ever passed it —
416
+ and columns are also the only way to say «this block changes with who is
417
+ looking», an admin block against an account block, because they are data you
418
+ build and not markup the library walks.
419
+
420
+ The `./` in front of each column link is the component's, like `NavItem`'s, and
421
+ it is `aria-hidden`. The column titles render as `<h3>`. Pass `linkAsChild` to
422
+ plug in the router's `Link`; without it the columns are plain `<a>` and every
423
+ navigation costs a page load.
424
+
425
+ ### `Table` brings its own surface
426
+
427
+ ```tsx
428
+ // ✅ this is the whole thing
429
+ <Table>…</Table>
430
+
431
+ // ❌ two borders. The radius, the border and the clip are the component's
432
+ <div className="rounded-lg border border-border bg-card overflow-hidden">
433
+ <Table>…</Table>
434
+ </div>
435
+ ```
436
+
437
+ `Table` draws `rounded-card`, `border-hairline` and the clip on the same
438
+ container that scrolls it horizontally. Do not wrap it in a surface of your own,
439
+ and do not add `overflow-hidden`: `TableRow`'s hover tint is already clipped by
440
+ the corners, which is what the wrapper used to be for.
441
+
442
+ `className` reaches the `<table>`, not the container, so a `rounded-none` from
443
+ the call site does nothing.
444
+
445
+ ### The three chart types, and the names they took
446
+
447
+ ```tsx
448
+ import { AreaChart, BarChart, LineChart } from '@eduardoalvarez/arrecife/chart';
449
+
450
+ <AreaChart
451
+ label="Registros nuevos por día"
452
+ summary="Sube de 24 a 52 con una caída en mayo."
453
+ height={200}
454
+ data={data}
455
+ series={[{ key: 'count', label: 'Registros' }]}
456
+ xKey="day"
457
+ />
458
+ ```
459
+
460
+ `data`, `series` and `xKey`, plus the mandatory `label`. The gradient, the grid,
461
+ the axes and the tooltip belong to the component — that composition is the system
462
+ drawing, and writing it at the call site is how it drifts.
463
+
464
+ **These are NOT Recharts' components of the same name.** Import ours; do not
465
+ import both in one file. Recharts' `Area`, `Line` and `XAxis` are still not
466
+ re-exported, because they are unchanged and wrapping them buys nothing.
467
+
468
+ `BarChart`'s `orientation` is named for what you see: `horizontal` lays the bars
469
+ down for a ranking. Recharts calls that same thing `layout="vertical"` — if you
470
+ are porting code, the value flips.
471
+
472
+ `stacked` on `AreaChart` and `BarChart` adds the series up. Without it areas
473
+ overlap, which is honest and rarely what you want with more than one series: to
474
+ COMPARE rather than add up, the type is `LineChart`.
475
+
476
+ Anything that is not a series over a category axis has no type and is not missing
477
+ one. A doughnut is `ChartContainer` plus Recharts' `Pie` with `SERIES_COLORS`.
478
+
479
+ ### The social icons come from `./social`
480
+
481
+ ```tsx
482
+ // ❌ does not exist: the root publishes them grouped, not loose
209
483
  import { GitHub } from '@eduardoalvarez/arrecife';
210
484
 
211
- // ✅
485
+ // ✅ the normal form
486
+ import { GitHub } from '@eduardoalvarez/arrecife/social';
487
+
488
+ // ✅ for iterating the catalogue
212
489
  import { social } from '@eduardoalvarez/arrecife';
213
490
  <social.GitHub />
214
491
  ```
215
492
 
216
- All nine: `GitHub`, `LinkedIn`, `X`, `Instagram`, `Discord`, `YouTube`, `Rss`,
217
- `Email`, `Newsletter`. They live under a namespace because one of them is called
218
- `X`, and loose it collides. `Newsletter` is the bell: a way to follow, like
219
- `Rss`, named for what it means.
493
+ All ten: `GitHub`, `LinkedIn`, `X`, `Instagram`, `Discord`, `YouTube`, `Rss`,
494
+ `Email`, `Newsletter`, `Website`. `Newsletter` is the bell: a way to follow, like
495
+ `Rss`, named for what it means. `Website` is «my other site» — the personal
496
+ domain in a footer full of networks — and it is what replaces borrowing a globe
497
+ from an icon set, which brings its own stroke weight and its own margins.
498
+
499
+ Six are brands and go SOLID, four are functional and use a 1.6 stroke. A row that
500
+ mixes the two pens is the normal case, not a defect: a brand is somebody else's
501
+ silhouette and cannot be outlined, and a symbol the system draws itself has no
502
+ owner to be faithful to.
503
+
504
+ **In a Server Component the subpath is mandatory, not preferred.** The root
505
+ carries `"use client"`, and a client reference crosses the boundary per EXPORT —
506
+ the properties of a plain object are not exports, so `social.LinkedIn` is
507
+ `undefined` on the server and `undefined` as an element type kills the build at
508
+ prerender. `./social` carries no directive: it renders on the server and ships no
509
+ client JS. Use `social` only when mapping a list of names onto icons.
510
+
511
+ The root keeps the group because one of them is called `X`, and loose at the root
512
+ it collides. In the subpath, alias it: `import { X as XIcon }`.
513
+
514
+ **If you mount no React, the shapes are published too.** `./social/data` imports
515
+ nothing, so an `.astro` that ships no framework JavaScript can draw the same
516
+ glyph instead of pasting the `<path>` into the project:
517
+
518
+ ```astro
519
+ ---
520
+ import { socialSvg } from '@eduardoalvarez/arrecife/social/data';
521
+ ---
522
+ <Fragment set:html={socialSvg('GitHub', { class: 'size-[19px]' })} />
523
+ ```
524
+
525
+ `socialGlyphs` is the catalogue keyed by name, `socialNames` is the ten names in
526
+ order, and every glyph is exported on its own — `gitHubGlyph`, `websiteGlyph` —
527
+ if you want the shapes rather than the markup. The React components are drawn
528
+ from that same file, so the two renderings cannot disagree. Reaching for
529
+ `socialGlyphs` or `socialSvg` names all ten, which is the price of iterating a
530
+ catalogue; `import { LinkedIn }` still costs one shape.
220
531
 
221
532
  The internal glyphs — `Close`, `ChevronDown`, `Sun` — are **not exported** and
222
533
  are not going to be: they are the primitives' minimum set. A component that needs
@@ -240,7 +551,7 @@ same value is available in both places and they cannot disagree.
240
551
  | `control.md` | `--spacing-control-md` | `px-control-md` |
241
552
  | `control.icon` | `--spacing-control-icon` | `size-control-icon` (42×42) |
242
553
  | `gradient[mode].hero` | `--gradient-hero` | `gradient-hero` |
243
- | `size.nav` | `--spacing-nav` | `h-nav` |
554
+ | `size.nav` / `size.navCompact` | `--spacing-nav` / `--spacing-nav-compact` | `h-nav` / `h-nav-compact` |
244
555
  | `size.content` / `size.wide` | `--container-content` / `--container-wide` | `max-w-content` / `max-w-wide` |
245
556
  | `limits.measure` | `--container-measure` | `max-w-measure` |
246
557
  | `shadow.standard` | `--shadow-standard` | `shadow-standard` |
@@ -256,7 +567,7 @@ They carry a prefix because `xs, sm, md, lg, xl` are the names of Tailwind's
256
567
  `--container-*` scale, and a `--spacing-md` of our own was swallowing `max-w-md`
257
568
  across the whole project with nothing warning about it. `max-w-*`, `w-*` and
258
569
  `h-*` belong to Tailwind and are used as they are. Migration guide from 0.2.0:
259
- <https://github.com/Proskynete/arrecife/blob/main/docs/migration-0.3.md>.
570
+ <https://github.com/Proskynete/arrecife/blob/main/docs/runbooks/migration-0.3.md>.
260
571
 
261
572
  ## System rules the consuming code must not break
262
573
 
@@ -269,10 +580,16 @@ compiles and looks wrong, or that fails the project's accessibility audit.
269
580
  3. **`Button variant="destructive"` is for the irreversible only.** Never for
270
581
  «cancel» on a form, and not inside an `AlertDialog` — there the confirm button
271
582
  stays `primary`, because the title, the focus on cancel and the no-click-outside
272
- already carry the weight. See `docs/decisions.md` § 21.
583
+ already carry the weight. See `decisions/0.6.md` § 21.
273
584
  4. **`secondary` is never filled.** It is border and text.
274
585
  5. **No entrance animations.** Modals, menus, tooltips and toasts appear where
275
- they will stay. The only exception is the `Button loading` spinner.
586
+ they will stay. There are five declared exceptions, all behind `motion-safe`
587
+ and all with a reason written down: the `Button loading` spinner, `Sheet`'s
588
+ side panel, `Skeleton`'s shimmer, `Accordion`'s height and the `pulse-accent`
589
+ halo the footer signature ends in. All five are the same criterion — feedback
590
+ about progress or about spatial continuity — and a sixth lands on it or it
591
+ does not exist. The `caret` blink 0.6.0 shipped is removed in 0.8.0; it was
592
+ the only member of the second criterion § 23 opened for it.
276
593
  6. **Semantics and scale are independent.** An `h2` that has to look small is
277
594
  `<Text as="h2" variant="h3">`, never an `h3` that lies about the hierarchy.
278
595
  7. **`textMuted` never goes over `surfaceRaised`**: it gives 4.07 in dark. Over a
@@ -288,6 +605,11 @@ compiles and looks wrong, or that fails the project's accessibility audit.
288
605
  11. **The mascot's faces only appear** in empty states, confirmations, errors,
289
606
  course progress and celebration. Never in a hero, pricing, services, contact
290
607
  or the CV.
608
+ **And not in every empty state either**: `EmptyState variant="inline"` is the
609
+ hole inside a table page or a dashboard widget, and it carries no face — the
610
+ type does not accept one. `page`, the default, is the one that IS the screen,
611
+ and there `expression` stays mandatory. A dozen mascots on one admin screen is
612
+ not the humour contract. See `decisions/0.7.md` § 27.
291
613
  12. **The fin is not a free parameter**: `foam` on a dark background, `color` on a
292
614
  light one. The components already choose it from the background.
293
615
 
@@ -586,7 +908,7 @@ Source: `src/primitives/date-field.tsx`
586
908
 
587
909
  A date field on the native control, not on a calendar of our own.
588
910
 
589
- - Extends: `Omit<ComponentPropsWithoutRef<'input'>, 'type'>`
911
+ - Extends: `Omit<ComponentProps<'input'>, 'type'>`
590
912
 
591
913
  | prop | type | req. | default | what it does |
592
914
  | --- | --- | --- | --- | --- |
@@ -675,7 +997,9 @@ No entrance animation: the menu appears, it does not unfold.
675
997
 
676
998
  Source: `src/primitives/input.tsx`
677
999
 
678
- - Extends: `ComponentPropsWithoutRef<'input'>`
1000
+ `ComponentProps` and not `ComponentPropsWithoutRef`, and the difference is a bug and not a preference.
1001
+
1002
+ - Extends: `ComponentProps<'input'>`
679
1003
 
680
1004
  | prop | type | req. | default | what it does |
681
1005
  | --- | --- | --- | --- | --- |
@@ -687,7 +1011,7 @@ Source: `src/primitives/label.tsx`
687
1011
 
688
1012
  The `label` scale: 13px, which is the system's absolute minimum on screen.
689
1013
 
690
- - Extends: `ComponentPropsWithoutRef<typeof LabelPrimitive.Root>`
1014
+ - Extends: `ComponentProps<typeof LabelPrimitive.Root>`
691
1015
  - No own props: it passes through those of the element or primitive it wraps.
692
1016
 
693
1017
  ### Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationPrevious, PaginationNext, PaginationEllipsis
@@ -871,7 +1195,7 @@ The knob changes position, but is not animated while doing so: the position IS t
871
1195
  Source: `src/primitives/table.tsx`
872
1196
 
873
1197
  **Table**
874
- The container scrolls horizontally: the page never does.
1198
+ The table, and the surface it sits on. The two are one piece.
875
1199
 
876
1200
  - No own props: it passes through those of the element or primitive it wraps.
877
1201
 
@@ -894,6 +1218,8 @@ The container scrolls horizontally: the page never does.
894
1218
  - No own props: it passes through those of the element or primitive it wraps.
895
1219
 
896
1220
  **TableCaption**
1221
+ The caption, at the bottom and INSIDE the surface.
1222
+
897
1223
  - No own props: it passes through those of the element or primitive it wraps.
898
1224
 
899
1225
  ### Tabs, TabsList, TabsTrigger, TabsContent
@@ -917,7 +1243,9 @@ Source: `src/primitives/tabs.tsx`
917
1243
 
918
1244
  Source: `src/primitives/textarea.tsx`
919
1245
 
920
- - Extends: `ComponentPropsWithoutRef<'textarea'>`
1246
+ `ComponentProps` carries `ref`, which React 19 passes as a prop. See `InputProps`.
1247
+
1248
+ - Extends: `ComponentProps<'textarea'>`
921
1249
 
922
1250
  | prop | type | req. | default | what it does |
923
1251
  | --- | --- | --- | --- | --- |
@@ -968,7 +1296,7 @@ Source: `src/primitives/typography.tsx`
968
1296
 
969
1297
  ## Components
970
1298
 
971
- Imported from `@eduardoalvarez/arrecife`. 24 exports.
1299
+ Imported from `@eduardoalvarez/arrecife`. 21 exports.
972
1300
 
973
1301
  ### ArticleCard
974
1302
 
@@ -976,7 +1304,7 @@ Source: `src/components/article-card/index.tsx`
976
1304
 
977
1305
  The metadata line uses `meta` and not `eyebrow`: `18 ago 2026 · 8 min de lectura` is a datum, not an overline, and in small caps it was neither.
978
1306
 
979
- - Extends: `Omit<CardShellProps, 'children' \| 'title'>`
1307
+ - Extends: `Omit<CardShellProps, "children" \| "title">`
980
1308
 
981
1309
  | prop | type | req. | default | what it does |
982
1310
  | --- | --- | --- | --- | --- |
@@ -1078,17 +1406,17 @@ Source: `src/components/course-card/index.tsx`
1078
1406
 
1079
1407
  Source: `src/components/empty-state/index.tsx`
1080
1408
 
1081
- The mascot's most important rule, finally as code.
1082
-
1083
- - Extends: `Omit<ComponentPropsWithoutRef<'div'>, 'title'>`
1409
+ - Extends: `EmptyStateBase & ( \| { /** `page`, the default: the empty state IS the screen or the section, and it carries the face. */ variant?: 'page' \| undefined; /** * The face. Mandatory on `page` and impossible on `inline` — the props * are a union, so the generated table cannot show a per-variant «req.» * and the sentence has to carry it. Without it, `page` is a centred * paragraph. */ expression: Face; /** Where the brand PNGs are served from. */ basePath?: string \| undefined; icon?: never; } \| { /** `inline`: the hole inside a table or a widget. No face, and no way to pass one. */ variant: 'inline'; /** * A glyph above the line. It measures 1em and inherits `currentColor`, * like `Stat`'s: the project passes its own and sizes it, because the * system has no icon library and is not getting one. */ icon?: ReactNode; expression?: never; basePath?: never; } )`
1084
1410
 
1085
1411
  | prop | type | req. | default | what it does |
1086
1412
  | --- | --- | --- | --- | --- |
1087
1413
  | `action` | `ReactNode` | | | The action that gets you out of the empty state. Usually a tertiary button. |
1088
1414
  | `basePath` | `string` | | | Where the brand PNGs are served from. |
1089
1415
  | `description` | `ReactNode` | | | One line explaining what is missing or what to do. |
1090
- | `expression` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | yes | | The face. Mandatory: without it this is a centred paragraph. |
1416
+ | `expression` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | | | The face. Mandatory on `page` and impossible on `inline` — the props are a union, so the generated table cannot show a per-variant «req.» and the sentence has to carry it. Without it, `page` is a centred paragraph. |
1417
+ | `icon` | `ReactNode` | | | A glyph above the line. It measures 1em and inherits `currentColor`, like `Stat`'s: the project passes its own and sizes it, because the system has no icon library and is not getting one. |
1091
1418
  | `title` | `ReactNode` | yes | | |
1419
+ | `variant` | `"inline" \| "page"` | | | `page`, the default: the empty state IS the screen or the section, and it carries the face. `inline`: the hole inside a table or a widget. No face, and no way to pass one. |
1092
1420
 
1093
1421
  ### EventCalendar
1094
1422
 
@@ -1108,26 +1436,24 @@ Source: `src/components/event-calendar/index.tsx`
1108
1436
  | `onUpdateEvent` | `(event: CalendarEvent) => void` | | | |
1109
1437
  | `selected` | `Date` | | | Selected day, if the project controls it. Without it, it starts on today. |
1110
1438
 
1111
- ### Footer, FooterLink
1439
+ ### Footer
1112
1440
 
1113
1441
  Source: `src/components/footer/index.tsx`
1114
1442
 
1115
- **Footer**
1116
- - Extends: `ComponentPropsWithoutRef<'footer'>`
1443
+ - Extends: `FooterBase & ( \| { /** The shape the library has always had: stacked rows and the signature at the top right. */ variant?: 'default' \| undefined; columns?: never; description?: never; action?: never; linkAsChild?: never; } \| { /** `full`: brand and description on the left, link columns on the right, signature closing it. */ variant: 'full'; /** The link columns. Mandatory: without them `full` is the default form with extra steps. */ columns: readonly FooterColumn[]; /** One line under the brand, saying what the site is. */ description?: ReactNode; /** An action under the row of icons — «Reportar un problema». Usually a tertiary button. */ action?: ReactNode; /** * Renders the column links through the child, to plug in the framework's * `Link`. It receives each `href` in the Slot's `props`. * * It is § 24's rule applied where it now bites: a column turns data into * markup, so without a slot the only way to reach one of those links * from a project is to select it by structure or by a style class, and * neither is a contract. `Breadcrumb` and `ArticleCard` have the same * signature on purpose. * * It is also what a client-side transition needs: `cursos` reached for * it the moment its columns stopped being `<a>` tags. */ linkAsChild?: ((props: { href: string; children: ReactNode }) => ReactNode) \| undefined; } )`
1117
1444
 
1118
1445
  | prop | type | req. | default | what it does |
1119
1446
  | --- | --- | --- | --- | --- |
1447
+ | `action` | `ReactNode` | | | An action under the row of icons — «Reportar un problema». Usually a tertiary button. |
1120
1448
  | `brand` | `ReactNode` | | | The brand row: the fin and the wordmark, at the very top. |
1449
+ | `columns` | `readonly FooterColumn[]` | | | The link columns. Mandatory: without them `full` is the default form with extra steps. |
1450
+ | `description` | `ReactNode` | | | One line under the brand, saying what the site is. |
1451
+ | `linkAsChild` | `(props: { href: string; children: ReactNode; }) => ReactNode` | | | Renders the column links through the child, to plug in the framework's `Link`. It receives each `href` in the Slot's `props`. |
1452
+ | `signatureHref` | `string` | | | Makes the domain inside the signature a link, keeping the `$`, the path and the prompt's mark as text. |
1121
1453
  | `social` | `readonly SocialLink[]` | | | |
1454
+ | `variant` | `"full" \| "default"` | | | The shape the library has always had: stacked rows and the signature at the top right. `full`: brand and description on the left, link columns on the right, signature closing it. |
1122
1455
  | `year` | `number` | | `new Date().getFullYear()` | The signature's year. |
1123
1456
 
1124
- **FooterLink**
1125
- - Extends: `ComponentPropsWithoutRef<'a'>`
1126
-
1127
- | prop | type | req. | default | what it does |
1128
- | --- | --- | --- | --- | --- |
1129
- | `asChild` | `boolean \| undefined` | | `false` | |
1130
-
1131
1457
  ### Hero
1132
1458
 
1133
1459
  Source: `src/components/hero/index.tsx`
@@ -1167,7 +1493,7 @@ Migrated from `links/src/components/Card.astro`. The original scaled the card to
1167
1493
  Source: `src/components/nav/index.tsx`
1168
1494
 
1169
1495
  **Nav**
1170
- The site bar: 64px, abyss at 86 % and a 14px blur behind it.
1496
+ The site bar: 64px, abyss at 86 % and a 14px blur behind it — 56 when it shares the screen with a sidebar.
1171
1497
 
1172
1498
  - Extends: `ComponentPropsWithoutRef<'header'>`
1173
1499
 
@@ -1175,6 +1501,7 @@ The site bar: 64px, abyss at 86 % and a 14px blur behind it.
1175
1501
  | --- | --- | --- | --- | --- |
1176
1502
  | `actions` | `ReactNode` | | | Actions on the right: conversion, theme switch, search. |
1177
1503
  | `brand` | `ReactNode` | | | The logo, on the left. |
1504
+ | `size` | `"compact" \| "default"` | | `default` | `compact` is 56px instead of 64, for a bar that shares the screen with a sidebar: at 64 the two compete for the same corner and together they eat the top of the content area. |
1178
1505
 
1179
1506
  **NavItem**
1180
1507
  The `./` is put there by the component, not by whoever uses it.
@@ -1198,10 +1525,10 @@ Source: `src/components/newsletter-form/index.tsx`
1198
1525
  | `basePath` | `string` | | | |
1199
1526
  | `description` | `ReactNode` | | | |
1200
1527
  | `disclaimer` | `ReactNode` | | | The small print. It is the «sin spam», which is why it accepts a face. |
1201
- | `errorMessage` | `ReactNode` | | `No se pudo suscribir ese email. Revísalo y vuelve a intentar.` | |
1528
+ | `errorMessage` | `ReactNode` | | `No se pudo suscribir ese correo. Revísalo y vuelve a intentar.` | |
1202
1529
  | `expression` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | | | |
1203
1530
  | `fieldErrors` | `{ name?: ReactNode; email?: ReactNode; }` | | | A message under one specific field, instead of the single alert. |
1204
- | `fieldLabel` | `string` | | `Email electrónico` | |
1531
+ | `fieldLabel` | `string` | | `Correo electrónico` | |
1205
1532
  | `nameField` | `boolean` | | `false` | Adds the name field ahead of the email one. |
1206
1533
  | `nameInputProps` | `Omit<InputProps, "id" \| "disabled" \| "name">` | | | Whatever the project needs to hang off the name field: `minLength`, `maxLength`, `pattern`. The library imposes none of the three. |
1207
1534
  | `nameLabel` | `string` | | `Nombre` | |
@@ -1212,7 +1539,7 @@ Source: `src/components/newsletter-form/index.tsx`
1212
1539
  | `resetOnSuccess` | `boolean` | | `true` | Empties the fields after a successful subscription. On by default. |
1213
1540
  | `state` | `"success" \| "error" \| "idle" \| "sending"` | | `idle` | |
1214
1541
  | `submitLabel` | `string` | | `Suscribirme` | |
1215
- | `successMessage` | `ReactNode` | | `Ya estás dentro. Te llega un email cada dos semanas, y nada más.` | |
1542
+ | `successMessage` | `ReactNode` | | `Ya estás dentro. Te llega un correo cada dos semanas, y nada más.` | |
1216
1543
  | `title` | `ReactNode` | yes | | |
1217
1544
 
1218
1545
  ### PageHeader
@@ -1246,29 +1573,6 @@ How much you have read. It is NOT `Progress` under another name.
1246
1573
  | `target` | `RefObject<HTMLElement \| null>` | | | The element being measured. Without it, the whole document. |
1247
1574
  | `tone` | `"accent" \| "warm"` | | `accent` | Sand instead of biolume, to match course progress. |
1248
1575
 
1249
- ### SidebarItem, SidebarNav
1250
-
1251
- Source: `src/components/sidebar-nav/index.tsx`
1252
-
1253
- **SidebarItem**
1254
- The blog admin's sidebar.
1255
-
1256
- - Extends: `ComponentPropsWithoutRef<'a'>`
1257
-
1258
- | prop | type | req. | default | what it does |
1259
- | --- | --- | --- | --- | --- |
1260
- | `active` | `boolean \| undefined` | | `false` | |
1261
- | `asChild` | `boolean \| undefined` | | `false` | |
1262
- | `badge` | `ReactNode` | | | Counter on the right: pending drafts, unused media. |
1263
-
1264
- **SidebarNav**
1265
- - Extends: `ComponentPropsWithoutRef<'nav'>`
1266
-
1267
- | prop | type | req. | default | what it does |
1268
- | --- | --- | --- | --- | --- |
1269
- | `branch` | `ReactNode` | | | |
1270
- | `version` | `ReactNode` | | | Version and branch, at the bottom. |
1271
-
1272
1576
  ### Stat
1273
1577
 
1274
1578
  Source: `src/components/stat/index.tsx`
@@ -1279,11 +1583,13 @@ A large metric: the number in the `stat` scale and its name underneath.
1279
1583
 
1280
1584
  | prop | type | req. | default | what it does |
1281
1585
  | --- | --- | --- | --- | --- |
1586
+ | `delta` | `StatDelta` | | | How the number moved since last time. |
1282
1587
  | `description` | `ReactNode` | | | The standfirst: the nuance the number alone does not give. «12 aplicaciones» does not say whether that is a lot, and this is where that gets said. |
1283
- | `icon` | `ReactNode` | | | Glyph beside the title, at 1em. It inherits `currentColor`, so it follows the title's tone and does not have to be tinted separately. |
1588
+ | `icon` | `ReactNode` | | | Glyph in a tinted circle, in the corner opposite the title. At 1em, and it inherits `currentColor` from the badge, so it takes the tone without being tinted separately. |
1284
1589
  | `label` | `ReactNode` | yes | | What is being counted. It goes in mono small caps. |
1285
1590
  | `progress` | `number` | | | With `progress`, the metric reads as progress and adds the bar. |
1286
- | `tone` | `"neutral" \| "alerta"` | | `neutral` | `alerta` only when the number IS the problem. |
1591
+ | `spark` | `ReactNode` | | | The number's shape over time, under it. A `ReactNode` and not a data prop: a sparkline needs a charting library, and this component lives in the barrel that four projects install. The one project that draws them passes its own, exactly like `icon`. |
1592
+ | `tone` | `"neutral" \| "alert" \| "achievement"` | | `neutral` | `alert` ONLY when the number is the problem, and `achievement` when it is the opposite — the diplomas issued, the modules finished. The two paint the same sand today and they are still two names: a system that names by meaning cannot make «this is bad» the only way to say «this stands out». See `docs/decisions/0.7.md` § 28. |
1287
1593
  | `value` | `ReactNode` | yes | | The number, already formatted. The library imposes no locale. |
1288
1594
 
1289
1595
  ### TalkCard
@@ -1315,7 +1621,7 @@ The control that was missing. The library defined the whole theming system and e
1315
1621
 
1316
1622
  | prop | type | req. | default | what it does |
1317
1623
  | --- | --- | --- | --- | --- |
1318
- | `label` | `string` | | `Cambiar de theme` | Accessible name. The button has no visible text, so it is the only thing naming it. |
1624
+ | `label` | `string` | | `Cambiar de tema` | Accessible name. The button has no visible text, so it is the only thing naming it. |
1319
1625
  | `onThemeChange` | `(theme: Theme) => void` | | | Fires with whichever theme ended up set, in case the project wants to record it. |
1320
1626
  | `size` | `"sm" \| "md" \| "lg" \| "icon" \| "icon-sm"` | | `icon` | |
1321
1627
  | `variant` | `"primary" \| "conversion" \| "secondary" \| "tertiary" \| "destructive" \| "destructiveOutline"` | | `secondary` | |
@@ -1389,6 +1695,64 @@ Tiburoncín's head, with an expression.
1389
1695
  | `basePath` | `string` | | `ASSETS_PATH` | |
1390
1696
  | `expression` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | yes | | |
1391
1697
 
1698
+ ## Social icons
1699
+
1700
+ Imported from `@eduardoalvarez/arrecife/social` · or grouped as `social` from the root. 10 exports.
1701
+
1702
+ ### GitHub, LinkedIn, X, Instagram, Discord, YouTube, Rss, Email, Newsletter, Website
1703
+
1704
+ Source: `src/social/index.tsx`
1705
+
1706
+ **GitHub**
1707
+ - No own props: it passes through those of the element or primitive it wraps.
1708
+
1709
+ **LinkedIn**
1710
+ - No own props: it passes through those of the element or primitive it wraps.
1711
+
1712
+ **X**
1713
+ - No own props: it passes through those of the element or primitive it wraps.
1714
+
1715
+ **Instagram**
1716
+ - No own props: it passes through those of the element or primitive it wraps.
1717
+
1718
+ **Discord**
1719
+ - No own props: it passes through those of the element or primitive it wraps.
1720
+
1721
+ **YouTube**
1722
+ - No own props: it passes through those of the element or primitive it wraps.
1723
+
1724
+ **Rss**
1725
+ - No own props: it passes through those of the element or primitive it wraps.
1726
+
1727
+ **Email**
1728
+ - No own props: it passes through those of the element or primitive it wraps.
1729
+
1730
+ **Newsletter**
1731
+ The newsletter. It plays the same role as `Rss` — a way to follow, not a social network — which is why it belongs in this catalogue and does not open the door to an icon library.
1732
+
1733
+ - No own props: it passes through those of the element or primitive it wraps.
1734
+
1735
+ **Website**
1736
+ «My other site»: the personal domain in a footer full of social networks.
1737
+
1738
+ - No own props: it passes through those of the element or primitive it wraps.
1739
+
1740
+ ## Icons
1741
+
1742
+ Imported from `@eduardoalvarez/arrecife/icons` · requires `@phosphor-icons/react`. 1 exports.
1743
+
1744
+ ### Icon
1745
+
1746
+ Source: `src/icons/index.tsx`
1747
+
1748
+ - Extends: `Omit<PhosphorIconProps, 'size' \| 'weight' \| 'ref'>`
1749
+
1750
+ | prop | type | req. | default | what it does |
1751
+ | --- | --- | --- | --- | --- |
1752
+ | `as` | `Icon` | yes | | The Phosphor icon itself, passed as a component: `<Icon as={Books} />`. |
1753
+ | `label` | `string` | | | The accessible name. WITHOUT it the icon is decorative and gets `aria-hidden`, which is the right default: most icons sit beside their own label and announcing them twice is noise. |
1754
+ | `tone` | `"action" \| "current" \| "quiet"` | | `action` | WHAT THE ICON IS DOING, which is what picks the weight. Three values, and there is no fourth: `action` is the default and the system's line, `current` is the one of a set you are on, `quiet` is furniture that is not a control. `weight` is deliberately not a prop — see `TONE_WEIGHT`. |
1755
+
1392
1756
  ## Forms
1393
1757
 
1394
1758
  Imported from `@eduardoalvarez/arrecife/form` · requires `react-hook-form`. 7 exports.
@@ -1434,9 +1798,9 @@ The layer that ties the controls to a form with validation and messages.
1434
1798
 
1435
1799
  ## Charts
1436
1800
 
1437
- Imported from `@eduardoalvarez/arrecife/chart` · requires `recharts`. 5 exports.
1801
+ Imported from `@eduardoalvarez/arrecife/chart` · requires `recharts`. 8 exports.
1438
1802
 
1439
- ### ChartContainer, ChartTooltip, ChartLegend, ChartTooltipContent, ChartLegendContent
1803
+ ### ChartContainer, ChartTooltip, ChartLegend, ChartTooltipContent, ChartLegendContent, AreaChart, BarChart, LineChart
1440
1804
 
1441
1805
  Source: `src/chart/index.tsx`
1442
1806
 
@@ -1483,12 +1847,85 @@ The legend, with the tooltip's same square swatch and the `label` scale.
1483
1847
  | `className` | `string` | | | |
1484
1848
  | `payload` | `readonly ChartPayloadItem[]` | | | |
1485
1849
 
1850
+ **AreaChart**
1851
+ A series over time, with the fill fading out underneath it.
1852
+
1853
+ - Extends: `SeriesChartProps`
1854
+
1855
+ | prop | type | req. | default | what it does |
1856
+ | --- | --- | --- | --- | --- |
1857
+ | `data` | `readonly ChartDatum[]` | yes | | |
1858
+ | `formatter` | `(value: unknown) => ReactNode` | | | Formats the value in the tooltip. The library imposes no locale. |
1859
+ | `height` | `number` | | | Height in pixels. Recharts needs a concrete one to measure itself. |
1860
+ | `label` | `string` | yes | | What the chart shows, in one sentence. Mandatory, like `Progress`'s `label`: a bar `<svg>` with no accessible name is not «a chart without a label», it is an empty region. |
1861
+ | `legend` | `boolean \| undefined` | | | Shows the legend. It defaults to «only when there is more than one series»: a legend naming the one line already named by the chart's own heading is a row of pixels that says nothing. |
1862
+ | `series` | `readonly ChartSeries[]` | yes | | |
1863
+ | `stacked` | `boolean \| undefined` | | `false` | Adds the series up instead of overlaying them. |
1864
+ | `summary` | `ReactNode` | | | What the chart says, in words. It goes in a visually hidden `figcaption`. |
1865
+ | `xKey` | `string` | yes | | The key on the category axis: the day, the month, the course. |
1866
+
1867
+ **BarChart**
1868
+ Bars, upright or lying down.
1869
+
1870
+ - Extends: `SeriesChartProps`
1871
+
1872
+ | prop | type | req. | default | what it does |
1873
+ | --- | --- | --- | --- | --- |
1874
+ | `data` | `readonly ChartDatum[]` | yes | | |
1875
+ | `formatter` | `(value: unknown) => ReactNode` | | | Formats the value in the tooltip. The library imposes no locale. |
1876
+ | `height` | `number` | | | Height in pixels. Recharts needs a concrete one to measure itself. |
1877
+ | `label` | `string` | yes | | What the chart shows, in one sentence. Mandatory, like `Progress`'s `label`: a bar `<svg>` with no accessible name is not «a chart without a label», it is an empty region. |
1878
+ | `legend` | `boolean \| undefined` | | | Shows the legend. It defaults to «only when there is more than one series»: a legend naming the one line already named by the chart's own heading is a row of pixels that says nothing. |
1879
+ | `orientation` | `"horizontal" \| "vertical"` | | `vertical` | `vertical` grows the bars upwards, which is the default. `horizontal` is a ranking. |
1880
+ | `series` | `readonly ChartSeries[]` | yes | | |
1881
+ | `stacked` | `boolean \| undefined` | | `false` | Stacks the series instead of putting them side by side. |
1882
+ | `summary` | `ReactNode` | | | What the chart says, in words. It goes in a visually hidden `figcaption`. |
1883
+ | `xKey` | `string` | yes | | The key on the category axis: the day, the month, the course. |
1884
+
1885
+ **LineChart**
1886
+ Lines, for comparing series against each other.
1887
+
1888
+ - Extends: `SeriesChartProps`
1889
+
1890
+ | prop | type | req. | default | what it does |
1891
+ | --- | --- | --- | --- | --- |
1892
+ | `data` | `readonly ChartDatum[]` | yes | | |
1893
+ | `formatter` | `(value: unknown) => ReactNode` | | | Formats the value in the tooltip. The library imposes no locale. |
1894
+ | `height` | `number` | | | Height in pixels. Recharts needs a concrete one to measure itself. |
1895
+ | `label` | `string` | yes | | What the chart shows, in one sentence. Mandatory, like `Progress`'s `label`: a bar `<svg>` with no accessible name is not «a chart without a label», it is an empty region. |
1896
+ | `legend` | `boolean \| undefined` | | | Shows the legend. It defaults to «only when there is more than one series»: a legend naming the one line already named by the chart's own heading is a row of pixels that says nothing. |
1897
+ | `series` | `readonly ChartSeries[]` | yes | | |
1898
+ | `summary` | `ReactNode` | | | What the chart says, in words. It goes in a visually hidden `figcaption`. |
1899
+ | `xKey` | `string` | yes | | The key on the category axis: the day, the month, the course. |
1900
+
1486
1901
  ## Exports that are not components
1487
1902
 
1488
1903
  The root re-exports everything from `./tokens` and `./brand` for convenience.
1489
1904
  Each one appears exactly once, under the most specific subpath that publishes
1490
1905
  it: if the code does not mount React, that subpath is the one to import.
1491
1906
 
1907
+ ### `@eduardoalvarez/arrecife/social/data`
1908
+
1909
+ | export | type | what it is |
1910
+ | --- | --- | --- |
1911
+ | `discordGlyph` | `SocialGlyph` | Discord's mark. Brand, so it is a solid silhouette. |
1912
+ | `emailGlyph` | `SocialGlyph` | The envelope. Functional, so it is a 1.6 stroke. |
1913
+ | `gitHubGlyph` | `SocialGlyph` | GitHub's mark. Brand, so it is a solid silhouette. |
1914
+ | `instagramGlyph` | `SocialGlyph` | Instagram's mark. Brand, so it is a solid silhouette. |
1915
+ | `linkedInGlyph` | `SocialGlyph` | LinkedIn's mark. Brand, so it is a solid silhouette. |
1916
+ | `newsletterGlyph` | `SocialGlyph` | |
1917
+ | `rssGlyph` | `SocialGlyph` | The feed. Functional, so it is a 1.6 stroke — with one filled dot, because a ring that small reads as a smudge. |
1918
+ | `SOCIAL_STROKE_WIDTH` | `1.6` | The stroke width of a functional glyph, from the document. |
1919
+ | `SOCIAL_VIEW_BOX` | `"0 0 24 24"` | The grid every glyph is drawn on. |
1920
+ | `socialGlyphs` | `{ Discord, Email, GitHub, Instagram, LinkedIn, Newsletter, Rss, Website, X, YouTube }` | The whole catalogue, keyed by the name each glyph is exported under in `./social`. |
1921
+ | `socialNames` | `readonly ("Discord" \| "Email" \| "GitHub" \| "Instagram" \| "LinkedIn" \| "Newsletter" \| "Rss" \| "Website" \| "X" \| "YouTube")[]` | The ten names, in the order the catalogue declares them. |
1922
+ | `socialSvg` | `(name: "Discord" \| "Email" \| "GitHub" \| "Instagram" \| "LinkedIn" \| "Newsletter" \| "Rss" \| "Website" \| "X" \| "YouTube", extra?: Readonly<Record<string, string \| number>>): string` | One glyph as a complete `<svg>` string, for a template that cannot mount React. |
1923
+ | `websiteGlyph` | `SocialGlyph` | «My other site», and the reason it is here rather than borrowed. |
1924
+ | `xGlyph` | `SocialGlyph` | X's mark. Brand, so it is a solid silhouette. |
1925
+ | `youTubeGlyph` | `SocialGlyph` | YouTube's mark. Brand, so it is a solid silhouette. |
1926
+
1927
+ Types (3): `SocialGlyph`, `SocialGlyphShape`, `SocialName`.
1928
+
1492
1929
  ### `@eduardoalvarez/arrecife/variants`
1493
1930
 
1494
1931
  | export | type | what it is |
@@ -1521,7 +1958,7 @@ it: if the code does not mount React, that subpath is the one to import.
1521
1958
  | `radius` | `{ readonly chip: 6; readonly control: 10; readonly card: 14; readonly panel: 16; readonly pill: 999; }` | |
1522
1959
  | `series` | `{ readonly dark: readonly ["#35D6C0", "#F2A65A", "#3E7CB1", "#71919C"]; readonly light: readonly ["#0D7C6F", "#A65B27", "#3E7CB1", "#626A75"]; }` | The chart series palette. FOUR, for the same reason as the syntax palette: the system communicates with color and border, not with chromatic noise. |
1523
1960
  | `shadow` | `{ readonly standard: "0 1px 2px rgba(0, 0, 0, 0.35)"; }` | A single level. There is no elevation scale. |
1524
- | `size` | `{ readonly nav: 64; readonly content: 760; readonly wide: 1180; }` | |
1961
+ | `size` | `{ readonly nav: 64; readonly navCompact: 56; readonly sidebar: 256; readonly sidebarRail: 56; readonly content: 760; readonly wide: 1180; }` | |
1525
1962
  | `spacing` | `{ readonly stepXs: 8; readonly stepSm: 12; readonly stepMd: 16; readonly stepLg: 26; readonly stepXl: 40; readonly section: 96; }` | Page rhythm. All five steps carry `step` in the name, and that is not decoration: it is the fix for a bug that never surfaced anywhere. |
1526
1963
  | `syntax` | `{ background, identifier, literal, keyword, comment, invalid }` | The syntax highlighting palette. |
1527
1964
  | `tagline` | `{ long, short, en }` | |
@@ -1530,6 +1967,10 @@ it: if the code does not mount React, that subpath is the one to import.
1530
1967
 
1531
1968
  Types (13): `BrandToken`, `ColorMode`, `ColorToken`, `ControlToken`, `FontToken`, `GradientToken`, `RadiusToken`, `SeriesToken`, `SizeToken`, `SpacingToken`, `SyntaxToken`, `Tokens`, `TypeScaleToken`.
1532
1969
 
1970
+ ### `@eduardoalvarez/arrecife/social`
1971
+
1972
+ Types (1): `SocialIconProps`.
1973
+
1533
1974
  ### `@eduardoalvarez/arrecife/theme`
1534
1975
 
1535
1976
  | export | type | what it is |
@@ -1561,6 +2002,15 @@ Types (2): `Theme`, `ThemeOptions`.
1561
2002
 
1562
2003
  Types (8): `Background`, `Face`, `Fin`, `IsotypeProps`, `LogoProps`, `MascotFaceProps`, `MascotProps`, `Pose`.
1563
2004
 
2005
+ ### `@eduardoalvarez/arrecife/icons`
2006
+
2007
+ | export | type | what it is |
2008
+ | --- | --- | --- |
2009
+ | `ICON_WEIGHT` | `"light" \| "fill" \| "thin" \| "regular" \| "bold" \| "duotone"` | Phosphor's own name for the system's line. It is what `tone="action"` resolves to. |
2010
+ | `TONE_WEIGHT` | `Record<IconTone, IconWeight>` | The three roles, and the weight each one is drawn at. This is the whole of the weight axis: Phosphor ships six and this system reads three, because the other three — `thin`, `bold`, `duotone` — have no role behind them here. |
2011
+
2012
+ Types (2): `IconProps`, `IconTone`.
2013
+
1564
2014
  ### `@eduardoalvarez/arrecife/shiki`
1565
2015
 
1566
2016
  | export | type | what it is |
@@ -1576,7 +2026,7 @@ Types (1): `ShikiTheme`.
1576
2026
  | `SERIES_COLORS` | `string[]` | All four, in order, to hand to a `Pie` with `Cell` in one go. |
1577
2027
  | `seriesColor` | `(index: number): string` | The color of series `index`, as a custom property. |
1578
2028
 
1579
- Types (4): `ChartContainerProps`, `ChartLegendContentProps`, `ChartPayloadItem`, `ChartTooltipContentProps`.
2029
+ Types (10): `AreaChartProps`, `BarChartProps`, `ChartContainerProps`, `ChartDatum`, `ChartLegendContentProps`, `ChartPayloadItem`, `ChartSeries`, `ChartTooltipContentProps`, `LineChartProps`, `SeriesChartProps`.
1580
2030
 
1581
2031
  ### `@eduardoalvarez/arrecife/form`
1582
2032
 
@@ -1602,11 +2052,11 @@ Types (6): `ArticleData`, `BaseData`, `CourseData`, `DefaultData`, `SatoriNode`,
1602
2052
  | export | type | what it is |
1603
2053
  | --- | --- | --- |
1604
2054
  | `cn` | `(...inputs: ClassValue[]): string` | |
1605
- | `social` | `typeof import("src/lib/social")` | |
2055
+ | `social` | `typeof import("src/social/index")` | |
1606
2056
  | `toast` | `(message: ReactNode, options?: ToastOptions \| undefined): string` | Fires a notice. It returns its id, which is what you keep in order to close it by hand — the «guardando…» case that gets replaced when the request finishes. |
1607
2057
  | `useTheme` | `(): Theme` | The theme set right now, for a project that needs to branch in React — a different logo per mode, an image with no light version. |
1608
2058
 
1609
- Types (60): `AccordionProps`, `AccordionTriggerProps`, `AlertProps`, `ArticleCardProps`, `AudioPlayerMode`, `AudioPlayerProps`, `AuthorCardProps`, `AvatarProps`, `AvatarUploadProps`, `BadgeProps`, `BlockquoteProps`, `BreadcrumbProps`, `ButtonProps`, `CalendarEvent`, `CalendarProps`, `CategoryBadgeProps`, `CheckboxProps`, `CodeBlockProps`, `CodeProps`, `CourseCardProps`, `Crumb`, `DateFieldProps`, `EmptyStateProps`, `EventCalendarProps`, `FooterLinkProps`, `FooterProps`, `HeroProps`, `InputProps`, `LabelProps`, `LinkRowProps`, `MetricBadgeProps`, `NavItemProps`, `NavProps`, `NewsletterFormProps`, `NewsletterState`, `PageHeaderProps`, `PaginationLinkProps`, `PopoverContentProps`, `ProgressProps`, `RadioGroupItemProps`, `RadioGroupProps`, `ScrollingProgressBarProps`, `SeparatorProps`, `SheetContentProps`, `SidebarItemProps`, `SidebarNavProps`, `SkeletonProps`, `SocialLink`, `StatProps`, `SwitchProps`, `TableOfContentsProps`, `TabsProps`, `TalkCardProps`, `TextProps`, `TextareaProps`, `ThemeToggleProps`, `ToastOptions`, `ToastVariant`, `ToasterProps`, `TocEntry`.
2059
+ Types (60): `AccordionProps`, `AccordionTriggerProps`, `AlertProps`, `ArticleCardProps`, `AudioPlayerMode`, `AudioPlayerProps`, `AuthorCardProps`, `AvatarProps`, `AvatarUploadProps`, `BadgeProps`, `BlockquoteProps`, `BreadcrumbProps`, `ButtonProps`, `CalendarEvent`, `CalendarProps`, `CategoryBadgeProps`, `CheckboxProps`, `CodeBlockProps`, `CodeProps`, `CourseCardProps`, `Crumb`, `DateFieldProps`, `EmptyStateProps`, `EventCalendarProps`, `FooterColumn`, `FooterColumnLink`, `FooterProps`, `HeroProps`, `InputProps`, `LabelProps`, `LinkRowProps`, `MetricBadgeProps`, `NavItemProps`, `NavProps`, `NewsletterFormProps`, `NewsletterState`, `PageHeaderProps`, `PaginationLinkProps`, `PopoverContentProps`, `ProgressProps`, `RadioGroupItemProps`, `RadioGroupProps`, `ScrollingProgressBarProps`, `SeparatorProps`, `SheetContentProps`, `SkeletonProps`, `SocialLink`, `StatDelta`, `StatProps`, `SwitchProps`, `TableOfContentsProps`, `TabsProps`, `TalkCardProps`, `TextProps`, `TextareaProps`, `ThemeToggleProps`, `ToastOptions`, `ToastVariant`, `ToasterProps`, `TocEntry`.
1610
2060
 
1611
2061
  # Where to look if this is not enough
1612
2062
 
@@ -1614,8 +2064,8 @@ Types (60): `AccordionProps`, `AccordionTriggerProps`, `AlertProps`, `ArticleCar
1614
2064
  generated from the types.
1615
2065
  - The repo's `README.md`: the reasoning behind each decision, the contrast
1616
2066
  correction table and the release cycle.
1617
- - `docs/design-system.md` and `docs/brand-manual.md`: the identity documents,
2067
+ - `architecture/design-system.md` and `architecture/brand-manual.md`: the identity documents,
1618
2068
  greppable.
1619
- - `docs/decisions.md`: the points where the code and the document did not say the
2069
+ - `decisions/`: the points where the code and the document did not say the
1620
2070
  same thing, each with its resolution.
1621
2071
  - `AGENTS.md`: for working inside the library's repo.