@eduardoalvarez/arrecife 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +185 -20
  3. package/dist/brand/index.js +3 -3
  4. package/dist/chart/index.js +3 -3
  5. package/dist/{chunk-XKYHTOUJ.js → chunk-2WPWEIMD.js} +1 -1
  6. package/dist/chunk-45HVCTB7.js +70 -0
  7. package/dist/{chunk-DKCN7BAL.js → chunk-727HCBD4.js} +1 -1
  8. package/dist/{chunk-25YNFCIF.js → chunk-E6KFUSKB.js} +6 -3
  9. package/dist/{chunk-6O3KWB6P.js → chunk-JN3IS5OS.js} +2 -2
  10. package/dist/{chunk-O4TAH7YJ.js → chunk-OMKSESQB.js} +27 -3
  11. package/dist/{chunk-PMN7NR3G.js → chunk-TA7TLWW4.js} +1 -1
  12. package/dist/{chunk-JMOOFZ3B.js → chunk-WGNIRIN7.js} +1 -1
  13. package/dist/doctor.mjs +166 -0
  14. package/dist/form/index.js +4 -4
  15. package/dist/icons/index.cjs +149 -0
  16. package/dist/icons/index.d.cts +94 -0
  17. package/dist/icons/index.d.ts +94 -0
  18. package/dist/icons/index.js +28 -0
  19. package/dist/index-DlAO2JZs.d.cts +47 -0
  20. package/dist/index-DlAO2JZs.d.ts +47 -0
  21. package/dist/index.cjs +279 -103
  22. package/dist/index.d.cts +186 -57
  23. package/dist/index.d.ts +186 -57
  24. package/dist/index.js +262 -179
  25. package/dist/og/index.cjs +3 -2
  26. package/dist/og/index.js +1 -1
  27. package/dist/shiki/index.js +1 -1
  28. package/dist/social/index.cjs +67 -0
  29. package/dist/social/index.d.cts +2 -0
  30. package/dist/social/index.d.ts +2 -0
  31. package/dist/social/index.js +2 -0
  32. package/dist/tokens/index.cjs +27 -3
  33. package/dist/tokens/index.d.cts +33 -6
  34. package/dist/tokens/index.d.ts +33 -6
  35. package/dist/tokens/index.js +2 -2
  36. package/dist/tokens/theme.css +33 -3
  37. package/dist/variants/index.cjs +6 -3
  38. package/dist/variants/index.d.cts +4 -1
  39. package/dist/variants/index.d.ts +4 -1
  40. package/dist/variants/index.js +1 -1
  41. package/llms.txt +363 -30
  42. package/package.json +24 -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,6 +116,8 @@ 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 nine social icons, loose. No `"use client"` |
120
+ | `@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
121
  | `@eduardoalvarez/arrecife/form` | yes | `react-hook-form` | The form layer: labels, errors and `aria-*` |
120
122
  | `@eduardoalvarez/arrecife/chart` | yes | `recharts` | The chart chassis and the series palette |
121
123
  | `@eduardoalvarez/arrecife/assets/*` | — | — | The brand PNGs |
@@ -124,15 +126,16 @@ Importing the root from a build script to get one token is the mistake the
124
126
  subpaths exist to prevent: it drags all of React into a worker that never mounts
125
127
  it.
126
128
 
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.
129
+ `./form`, `./chart` and `./icons` sit outside the root for the symmetric reason:
130
+ if they hung off the main index, the projects that draw no charts, use no React
131
+ Hook Form and need no icons would have to install those dependencies anyway so
132
+ their bundler could resolve an import they never execute. Two of the five consume
133
+ zero icons.
131
134
 
132
135
  ### Next, Server Components and `"use client"`
133
136
 
134
137
  The root, `./brand`, `./form` and `./chart` ship `"use client"` in the published
135
- `dist/`. They render React and their Radix primitives call `createContext` at
138
+ `dist/`, and they are the only four. They render React and their Radix primitives call `createContext` at
136
139
  module scope, so without the directive a Next project with the App Router cannot
137
140
  import them at all: it fails at build time with
138
141
  `TypeError: (0 , r.createContext) is not a function`.
@@ -145,7 +148,11 @@ needed it.
145
148
 
146
149
  The five portable subpaths do NOT carry the directive, and that is the half that
147
150
  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
151
+ `./shiki` stay on the server. Neither does `./social`, which is a third case: it
152
+ renders React — it is nine `<svg>` — so it can never be portable, but it holds no
153
+ state and nothing about it needs a client boundary. It is the only way to put a
154
+ social icon in a Server Component, and § «The social icons come from `./social`»
155
+ below says why the grouped form cannot do it. If all you need are classes — for a `<div>`, an
149
156
  `<a>` or an Astro island you do not want to hydrate — import them from
150
157
  `./variants` and nothing crosses to the client:
151
158
 
@@ -202,21 +209,256 @@ It has to go INLINE. A `<script src>`, even a synchronous one, gets downloaded,
202
209
  and the flash comes back. The script re-attaches on `astro:after-swap` because
203
210
  view transitions replace the whole `<html>`.
204
211
 
205
- ### The social icons are namespaced
212
+ ### `npx arrecife` — run it once after installing
213
+
214
+ Two things break with **no error at all**, and the command catches both.
215
+
216
+ **Tailwind purges everything the components emit** unless the stylesheet has
217
+ `@source "<path>/node_modules/@eduardoalvarez/arrecife/dist"`. It does not scan
218
+ `node_modules`. There is no console error and no undefined class: the card mounts
219
+ with no padding, no radius and no border. The path is relative to the SHEET, not
220
+ to the project root, and the command computes it.
221
+
222
+ **A `--color-*` of yours silently replaces ours.** A project coming from shadcn
223
+ has `@theme inline { --color-accent: var(--accent); }` — shadcn's `--accent` is
224
+ the hover surface, `#17303E`, and ours is the brand turquoise, `#35D6C0`. That
225
+ one line repainted 88 classes inside the library's own components grey. Five
226
+ names collide in total; four agree on the value and are harmless, and the command
227
+ tells them apart.
228
+
229
+ Do not silence it by removing the `@import`: the fix is the `@source` line, or
230
+ renaming your own token.
231
+
232
+ ### `SidebarNav` groups, and the icon replaces the prompt
233
+
234
+ ```tsx
235
+ <SidebarNav aria-label="Administración" brand={<>…</>} version="v0.6.0" branch="main">
236
+ <SidebarItem href="/admin" icon={<Icon as={SquaresFour} />} active>Resumen</SidebarItem>
237
+
238
+ <SidebarGroup label="Ventas">
239
+ <SidebarItem href="/admin/ventas" icon={<Icon as={CreditCard} />}>Ventas</SidebarItem>
240
+ <SidebarItem href="/admin/cupones" icon={<Icon as={Ticket} />}>Cupones</SidebarItem>
241
+ </SidebarGroup>
242
+ </SidebarNav>
243
+ ```
244
+
245
+ Past about eight items a flat sidebar stops being readable. Each `SidebarGroup`
246
+ is a nested list named by its label, so a screen reader says «lista Ventas, 3
247
+ elementos» instead of one list of eleven. The label is a paragraph and **not** a
248
+ heading on purpose: a sidebar is navigation, and a heading here would land in the
249
+ page's own outline.
250
+
251
+ **`icon` replaces the `▸`, it does not join it.** Do not pass a glyph and expect
252
+ the prompt as well. A sidebar with no icons keeps the prompt on every item, which
253
+ is what a four-section blog admin wants.
254
+
255
+ **`brand` does not replace `title`.** `title` is the eyebrow and also the `nav`'s
256
+ accessible name when it is a string; a logo is not an accessible name, so pass
257
+ `aria-label` when you use `brand`. See `docs/decisions.md` § 32.
258
+
259
+ **It collapses to a rail, and the toggle is CONTROLLED:**
260
+
261
+ ```tsx
262
+ const [collapsed, setCollapsed] = useState(false);
263
+
264
+ <SidebarNav
265
+ collapsed={collapsed}
266
+ onCollapsedChange={setCollapsed}
267
+ brand={<Wordmark />}
268
+ mark={<Isotype className="h-6" />}
269
+ user={<Avatar … />}
270
+ >
271
+ ```
272
+
273
+ There is no uncontrolled mode: this state is almost always persisted, and an
274
+ internal one would fight the cookie you already keep. `onCollapsedChange` is also
275
+ what makes the toggle appear — `collapsed` on its own is a rail with no way out,
276
+ which is a layout and not an accident.
277
+
278
+ Collapsed, the widths become `w-sidebar-rail` (56) and `w-sidebar` (256), and the
279
+ component owns them only when it can collapse. It does **not** transition, on
280
+ purpose. `brand` is hidden and `mark` takes its place, because a wordmark does
281
+ not fit in a rail. Every label stays in the accessibility tree as `sr-only`, so
282
+ do not «simplify» by dropping the children of a collapsed item. See
283
+ `docs/decisions.md` § 34.
284
+
285
+ ### `Nav` is two slots and one height
286
+
287
+ Almost everything an app shell wants from a site bar is already a slot:
288
+
289
+ ```tsx
290
+ <Nav
291
+ size="compact" // 56px, for a shell with a sidebar
292
+ brand={<a href="/"><Logo /><span><span className="text-accent">~/</span>cursos</span></a>}
293
+ actions={session ? <UserMenu /> : <Button size="sm" asChild><Link href="/login">Entrar</Link></Button>}
294
+ >
295
+ <NavItem href="/cursos" active>cursos</NavItem>
296
+ </Nav>
297
+ ```
298
+
299
+ `brand` and `actions` are `ReactNode`, so a wordmark, a user menu, a theme toggle
300
+ or a search box go in without the library knowing anything about sessions.
301
+ **Session state does not get a prop** — it is project infrastructure, and the
302
+ library takes none.
303
+
304
+ `size="compact"` is 56px instead of 64, for a bar that shares the screen with a
305
+ sidebar. It is a prop and not a class because the height lives on `Nav`'s inner
306
+ container: `className` reaches the `<header>` and stops there, so passing `h-14`
307
+ does nothing.
308
+
309
+ **One `Nav` per page.** It renders the site's `banner` landmark, and two banners
310
+ on one page is an accessibility failure — which is also why `PageHeader` goes
311
+ inside `<main>` and is not a landmark. See `docs/decisions.md` § 30.
312
+
313
+ ### Icons are yours, the way they are drawn is not
314
+
315
+ The library ships no icon set and `lib/glyphs.tsx` is not exported: it is the
316
+ minimum set the primitives need and it does not grow. What the library does ship
317
+ is the drawing.
318
+
319
+ ```tsx
320
+ import { GraduationCap, Trash } from '@phosphor-icons/react';
321
+ import { Icon } from '@eduardoalvarez/arrecife/icons';
322
+
323
+ // 1em, and `tone="action"` by default — `regular`, the stroke the document names
324
+ <Icon as={GraduationCap} />
325
+
326
+ // Inside a control with no text, the name goes on the CONTROL
327
+ <Button size="icon-sm" variant="secondary" aria-label="Borrar la fila">
328
+ <Icon as={Trash} />
329
+ </Button>
330
+
331
+ // Alone and meaning something on its own, it gets a name
332
+ <Icon as={Trophy} label="Curso completado" />
333
+ ```
334
+
335
+ **Do not size them by hand.** `size-4`, `size-3.5`, `size-6` scattered through a
336
+ codebase is what this replaces: at 1em the icon takes the size of the text it
337
+ sits in — 13px beside `text-label`, 15px beside `text-ui` — and nobody picks a
338
+ number.
339
+
340
+ **The weight is not yours to pick either, but it is not one value.** `tone` names
341
+ what the icon is doing and the weight follows from it. There are three and there
342
+ is no fourth:
343
+
344
+ | `tone` | Weight | What it is |
345
+ | --- | --- | --- |
346
+ | `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 |
347
+ | `current` | `fill` | The one of a set you are on — the sidebar item carrying `aria-current` |
348
+ | `quiet` | `light` | Furniture: a marker in a metadata row, not a control and not a state |
349
+
350
+ ```tsx
351
+ <SidebarItem href="/cursos" active icon={<Icon as={GraduationCap} tone="current" />}>
352
+ cursos
353
+ </SidebarItem>
354
+ ```
355
+
356
+ `current` is the one that earns the axis. An active item already paints itself
357
+ biolume, and colour on its own is the channel WCAG 1.4.1 says may not carry
358
+ meaning alone; the fill is the second channel, and it is the one that survives a
359
+ forced-colours mode. `weight` is deliberately **not** a prop: Phosphor ships six
360
+ and this system reads three, because `thin`, `bold` and `duotone` have no role
361
+ behind them here.
362
+
363
+ `@phosphor-icons/react` is an **optional** peer dependency. If your project uses
364
+ no icons you install nothing; two of the five do exactly that.
365
+
366
+ **An icon is not illustration, and the two never substitute for each other.**
367
+ Tiburoncín — the faces, the poses, the fin — is the mascot; it comes from
368
+ `./brand`, and the manual says where a face may appear: empty states,
369
+ confirmations, errors, course progress, celebration, and nowhere else. An icon is
370
+ functional vocabulary and goes wherever a control needs a label it cannot spell.
371
+ Do not put an icon where the system asks for a face, and do not put a face where
372
+ a control wants an icon.
373
+
374
+ **In Next, import from `@phosphor-icons/react/ssr` inside a Server Component.**
375
+ Phosphor's default build reads `IconContext` through `useContext`, and a hook in
376
+ a Server Component throws. It ships no `"use client"` to stop you, so the failure
377
+ arrives at render rather than at build. The `/ssr` entry is the same icons
378
+ without the context read, and `Icon` works with either.
379
+
380
+ See `docs/decisions.md` § 29 and § 35.
381
+
382
+ ### `Stat`'s delta says direction, not judgement
383
+
384
+ ```tsx
385
+ <Stat
386
+ label="alumnos"
387
+ value="1.284"
388
+ delta={{ value: '+12 esta semana', direction: 'up' }}
389
+ />
390
+ ```
391
+
392
+ `direction` picks the arrow and **never the colour**. «+12 alumnos» and «+12
393
+ errores» point the same way and mean opposite things, so whether a number is good
394
+ news is `tone`'s job and yours: `neutral` for a datum, `alert` when the number IS
395
+ the problem, `achievement` when it is the reward. `alert` and `achievement` paint
396
+ the same sand on purpose — the API is the meaning, the colour is the
397
+ implementation. See `docs/decisions.md` § 28.
398
+
399
+ `delta.value` arrives already formatted, like `value`: the library imposes no
400
+ locale and computes no percentage. `spark` is a `ReactNode` and the library ships
401
+ no sparkline — pass your own, exactly like `icon`.
402
+
403
+ **Do not colour the number.** A neutral `Stat` renders its value in primary ink,
404
+ and biolume goes on the icon badge and the sparkline instead: three accents in
405
+ one card and the figure stops being the loudest thing in it. `alert` and
406
+ `achievement` DO paint the number sand, which is how «this number is not just a
407
+ number» is said. See `docs/decisions.md` § 31.
408
+
409
+ **`icon` is a badge in the corner opposite the title**, in a circle tinted at
410
+ 10 % of the tone. You pass the glyph; the circle, the tint and the size are the
411
+ component's.
412
+
413
+ ### The two shapes of `EmptyState`
206
414
 
207
415
  ```tsx
208
- // ❌ does not exist
416
+ // The empty state IS the screen: a search with no results, a 404, a section
417
+ // with nothing in it yet. It carries the face, and `expression` is mandatory.
418
+ <EmptyState expression="waiting" title="Sin resultados" description="…" />
419
+
420
+ // The hole INSIDE something else: a table page, a dashboard widget. No face, no
421
+ // surface, no border — the table or the card already draws the region.
422
+ <EmptyState variant="inline" title="No hay lecciones en esta página" />
423
+
424
+ // ❌ does not compile: the props are a union, and `inline` has no face
425
+ <EmptyState variant="inline" expression="waiting" title="…" />
426
+ ```
427
+
428
+ `inline` takes an optional `icon` — a `ReactNode` the project passes and sizes,
429
+ at 1em and in `currentColor`, like `Stat`'s. The library ships no icons.
430
+
431
+ Do not reach for `page` inside a table because the face is «nicer»: an admin
432
+ screen with a dozen empty regions gets a dozen mascots, which is what made every
433
+ consuming project write its own empty state instead of using this one.
434
+
435
+ ### The social icons come from `./social`
436
+
437
+ ```tsx
438
+ // ❌ does not exist: the root publishes them grouped, not loose
209
439
  import { GitHub } from '@eduardoalvarez/arrecife';
210
440
 
211
- // ✅
441
+ // ✅ the normal form
442
+ import { GitHub } from '@eduardoalvarez/arrecife/social';
443
+
444
+ // ✅ for iterating the catalogue
212
445
  import { social } from '@eduardoalvarez/arrecife';
213
446
  <social.GitHub />
214
447
  ```
215
448
 
216
449
  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.
450
+ `Email`, `Newsletter`. `Newsletter` is the bell: a way to follow, like `Rss`,
451
+ named for what it means.
452
+
453
+ **In a Server Component the subpath is mandatory, not preferred.** The root
454
+ carries `"use client"`, and a client reference crosses the boundary per EXPORT —
455
+ the properties of a plain object are not exports, so `social.LinkedIn` is
456
+ `undefined` on the server and `undefined` as an element type kills the build at
457
+ prerender. `./social` carries no directive: it renders on the server and ships no
458
+ client JS. Use `social` only when mapping a list of names onto icons.
459
+
460
+ The root keeps the group because one of them is called `X`, and loose at the root
461
+ it collides. In the subpath, alias it: `import { X as XIcon }`.
220
462
 
221
463
  The internal glyphs — `Close`, `ChevronDown`, `Sun` — are **not exported** and
222
464
  are not going to be: they are the primitives' minimum set. A component that needs
@@ -240,7 +482,7 @@ same value is available in both places and they cannot disagree.
240
482
  | `control.md` | `--spacing-control-md` | `px-control-md` |
241
483
  | `control.icon` | `--spacing-control-icon` | `size-control-icon` (42×42) |
242
484
  | `gradient[mode].hero` | `--gradient-hero` | `gradient-hero` |
243
- | `size.nav` | `--spacing-nav` | `h-nav` |
485
+ | `size.nav` / `size.navCompact` | `--spacing-nav` / `--spacing-nav-compact` | `h-nav` / `h-nav-compact` |
244
486
  | `size.content` / `size.wide` | `--container-content` / `--container-wide` | `max-w-content` / `max-w-wide` |
245
487
  | `limits.measure` | `--container-measure` | `max-w-measure` |
246
488
  | `shadow.standard` | `--shadow-standard` | `shadow-standard` |
@@ -288,6 +530,11 @@ compiles and looks wrong, or that fails the project's accessibility audit.
288
530
  11. **The mascot's faces only appear** in empty states, confirmations, errors,
289
531
  course progress and celebration. Never in a hero, pricing, services, contact
290
532
  or the CV.
533
+ **And not in every empty state either**: `EmptyState variant="inline"` is the
534
+ hole inside a table page or a dashboard widget, and it carries no face — the
535
+ type does not accept one. `page`, the default, is the one that IS the screen,
536
+ and there `expression` stays mandatory. A dozen mascots on one admin screen is
537
+ not the humour contract. See `docs/decisions.md` § 27.
291
538
  12. **The fin is not a free parameter**: `foam` on a dark background, `color` on a
292
539
  light one. The components already choose it from the background.
293
540
 
@@ -968,7 +1215,7 @@ Source: `src/primitives/typography.tsx`
968
1215
 
969
1216
  ## Components
970
1217
 
971
- Imported from `@eduardoalvarez/arrecife`. 24 exports.
1218
+ Imported from `@eduardoalvarez/arrecife`. 25 exports.
972
1219
 
973
1220
  ### ArticleCard
974
1221
 
@@ -1078,17 +1325,17 @@ Source: `src/components/course-card/index.tsx`
1078
1325
 
1079
1326
  Source: `src/components/empty-state/index.tsx`
1080
1327
 
1081
- The mascot's most important rule, finally as code.
1082
-
1083
- - Extends: `Omit<ComponentPropsWithoutRef<'div'>, 'title'>`
1328
+ - 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
1329
 
1085
1330
  | prop | type | req. | default | what it does |
1086
1331
  | --- | --- | --- | --- | --- |
1087
1332
  | `action` | `ReactNode` | | | The action that gets you out of the empty state. Usually a tertiary button. |
1088
1333
  | `basePath` | `string` | | | Where the brand PNGs are served from. |
1089
1334
  | `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. |
1335
+ | `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. |
1336
+ | `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
1337
  | `title` | `ReactNode` | yes | | |
1338
+ | `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
1339
 
1093
1340
  ### EventCalendar
1094
1341
 
@@ -1167,7 +1414,7 @@ Migrated from `links/src/components/Card.astro`. The original scaled the card to
1167
1414
  Source: `src/components/nav/index.tsx`
1168
1415
 
1169
1416
  **Nav**
1170
- The site bar: 64px, abyss at 86 % and a 14px blur behind it.
1417
+ The site bar: 64px, abyss at 86 % and a 14px blur behind it — 56 when it shares the screen with a sidebar.
1171
1418
 
1172
1419
  - Extends: `ComponentPropsWithoutRef<'header'>`
1173
1420
 
@@ -1175,6 +1422,7 @@ The site bar: 64px, abyss at 86 % and a 14px blur behind it.
1175
1422
  | --- | --- | --- | --- | --- |
1176
1423
  | `actions` | `ReactNode` | | | Actions on the right: conversion, theme switch, search. |
1177
1424
  | `brand` | `ReactNode` | | | The logo, on the left. |
1425
+ | `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
1426
 
1179
1427
  **NavItem**
1180
1428
  The `./` is put there by the component, not by whoever uses it.
@@ -1198,10 +1446,10 @@ Source: `src/components/newsletter-form/index.tsx`
1198
1446
  | `basePath` | `string` | | | |
1199
1447
  | `description` | `ReactNode` | | | |
1200
1448
  | `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.` | |
1449
+ | `errorMessage` | `ReactNode` | | `No se pudo suscribir ese correo. Revísalo y vuelve a intentar.` | |
1202
1450
  | `expression` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | | | |
1203
1451
  | `fieldErrors` | `{ name?: ReactNode; email?: ReactNode; }` | | | A message under one specific field, instead of the single alert. |
1204
- | `fieldLabel` | `string` | | `Email electrónico` | |
1452
+ | `fieldLabel` | `string` | | `Correo electrónico` | |
1205
1453
  | `nameField` | `boolean` | | `false` | Adds the name field ahead of the email one. |
1206
1454
  | `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
1455
  | `nameLabel` | `string` | | `Nombre` | |
@@ -1212,7 +1460,7 @@ Source: `src/components/newsletter-form/index.tsx`
1212
1460
  | `resetOnSuccess` | `boolean` | | `true` | Empties the fields after a successful subscription. On by default. |
1213
1461
  | `state` | `"success" \| "error" \| "idle" \| "sending"` | | `idle` | |
1214
1462
  | `submitLabel` | `string` | | `Suscribirme` | |
1215
- | `successMessage` | `ReactNode` | | `Ya estás dentro. Te llega un email cada dos semanas, y nada más.` | |
1463
+ | `successMessage` | `ReactNode` | | `Ya estás dentro. Te llega un correo cada dos semanas, y nada más.` | |
1216
1464
  | `title` | `ReactNode` | yes | | |
1217
1465
 
1218
1466
  ### PageHeader
@@ -1246,7 +1494,7 @@ How much you have read. It is NOT `Progress` under another name.
1246
1494
  | `target` | `RefObject<HTMLElement \| null>` | | | The element being measured. Without it, the whole document. |
1247
1495
  | `tone` | `"accent" \| "warm"` | | `accent` | Sand instead of biolume, to match course progress. |
1248
1496
 
1249
- ### SidebarItem, SidebarNav
1497
+ ### SidebarItem, SidebarGroup, SidebarNav
1250
1498
 
1251
1499
  Source: `src/components/sidebar-nav/index.tsx`
1252
1500
 
@@ -1260,6 +1508,16 @@ The blog admin's sidebar.
1260
1508
  | `active` | `boolean \| undefined` | | `false` | |
1261
1509
  | `asChild` | `boolean \| undefined` | | `false` | |
1262
1510
  | `badge` | `ReactNode` | | | Counter on the right: pending drafts, unused media. |
1511
+ | `icon` | `ReactNode` | | | The section's glyph, on the left. It REPLACES the `▸` rather than joining it, and it inherits `currentColor`, so it follows the item's state without being tinted separately. |
1512
+
1513
+ **SidebarGroup**
1514
+ A labelled block of items — «Contenido», «Alumnos», «Ventas».
1515
+
1516
+ - Extends: `Omit<ComponentPropsWithoutRef<'li'>, 'title'>`
1517
+
1518
+ | prop | type | req. | default | what it does |
1519
+ | --- | --- | --- | --- | --- |
1520
+ | `label` | `ReactNode` | yes | | The block's name. Sentence case, not a section title. |
1263
1521
 
1264
1522
  **SidebarNav**
1265
1523
  - Extends: `ComponentPropsWithoutRef<'nav'>`
@@ -1267,6 +1525,13 @@ The blog admin's sidebar.
1267
1525
  | prop | type | req. | default | what it does |
1268
1526
  | --- | --- | --- | --- | --- |
1269
1527
  | `branch` | `ReactNode` | | | |
1528
+ | `brand` | `ReactNode` | | | The row at the top: isotype and wordmark, `cursos · admin`. It is a slot and not a `logo`/`name` pair because every panel spells its own name differently, and the part that IS the system — the rhythm, the hairline under it — is here. |
1529
+ | `collapsed` | `boolean \| undefined` | | `false` | Turns the sidebar into a rail: icons only, and the widths become the library's — `w-sidebar` and `w-sidebar-rail`. It is CONTROLLED and there is no uncontrolled mode, because this state is almost always persisted in a cookie or in `localStorage`, and an internal state would fight the one the project already keeps. |
1530
+ | `collapseLabel` | `string` | | `Plegar el panel` | The toggle's accessible name, in the two directions. |
1531
+ | `expandLabel` | `string` | | `Desplegar el panel` | |
1532
+ | `mark` | `ReactNode` | | | What `brand` becomes in the rail. Usually the isotype with no wordmark. |
1533
+ | `onCollapsedChange` | `(collapsed: boolean) => void` | | | Called with what the state should become. With it, the toggle appears; with `collapsed` alone the sidebar is a rail with no way out of it, which is a legitimate layout and not an accident. |
1534
+ | `user` | `ReactNode` | | | Who is signed in, at the bottom above the version. A slot, because an avatar needs a session and a sign-out route and the library takes no project infrastructure — the same reason `Nav`'s user menu goes in `actions`. |
1270
1535
  | `version` | `ReactNode` | | | Version and branch, at the bottom. |
1271
1536
 
1272
1537
  ### Stat
@@ -1279,11 +1544,13 @@ A large metric: the number in the `stat` scale and its name underneath.
1279
1544
 
1280
1545
  | prop | type | req. | default | what it does |
1281
1546
  | --- | --- | --- | --- | --- |
1547
+ | `delta` | `StatDelta` | | | How the number moved since last time. |
1282
1548
  | `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. |
1549
+ | `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
1550
  | `label` | `ReactNode` | yes | | What is being counted. It goes in mono small caps. |
1285
1551
  | `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. |
1552
+ | `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`. |
1553
+ | `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.md` § 28. |
1287
1554
  | `value` | `ReactNode` | yes | | The number, already formatted. The library imposes no locale. |
1288
1555
 
1289
1556
  ### TalkCard
@@ -1315,7 +1582,7 @@ The control that was missing. The library defined the whole theming system and e
1315
1582
 
1316
1583
  | prop | type | req. | default | what it does |
1317
1584
  | --- | --- | --- | --- | --- |
1318
- | `label` | `string` | | `Cambiar de theme` | Accessible name. The button has no visible text, so it is the only thing naming it. |
1585
+ | `label` | `string` | | `Cambiar de tema` | Accessible name. The button has no visible text, so it is the only thing naming it. |
1319
1586
  | `onThemeChange` | `(theme: Theme) => void` | | | Fires with whichever theme ended up set, in case the project wants to record it. |
1320
1587
  | `size` | `"sm" \| "md" \| "lg" \| "icon" \| "icon-sm"` | | `icon` | |
1321
1588
  | `variant` | `"primary" \| "conversion" \| "secondary" \| "tertiary" \| "destructive" \| "destructiveOutline"` | | `secondary` | |
@@ -1389,6 +1656,59 @@ Tiburoncín's head, with an expression.
1389
1656
  | `basePath` | `string` | | `ASSETS_PATH` | |
1390
1657
  | `expression` | `"annoyed" \| "confused" \| "hearts" \| "laughing" \| "shades" \| "waiting" \| "wink"` | yes | | |
1391
1658
 
1659
+ ## Social icons
1660
+
1661
+ Imported from `@eduardoalvarez/arrecife/social` · or grouped as `social` from the root. 9 exports.
1662
+
1663
+ ### GitHub, LinkedIn, X, Instagram, Discord, YouTube, Rss, Email, Newsletter
1664
+
1665
+ Source: `src/social/index.tsx`
1666
+
1667
+ **GitHub**
1668
+ - No own props: it passes through those of the element or primitive it wraps.
1669
+
1670
+ **LinkedIn**
1671
+ - No own props: it passes through those of the element or primitive it wraps.
1672
+
1673
+ **X**
1674
+ - No own props: it passes through those of the element or primitive it wraps.
1675
+
1676
+ **Instagram**
1677
+ - No own props: it passes through those of the element or primitive it wraps.
1678
+
1679
+ **Discord**
1680
+ - No own props: it passes through those of the element or primitive it wraps.
1681
+
1682
+ **YouTube**
1683
+ - No own props: it passes through those of the element or primitive it wraps.
1684
+
1685
+ **Rss**
1686
+ - No own props: it passes through those of the element or primitive it wraps.
1687
+
1688
+ **Email**
1689
+ - No own props: it passes through those of the element or primitive it wraps.
1690
+
1691
+ **Newsletter**
1692
+ 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.
1693
+
1694
+ - No own props: it passes through those of the element or primitive it wraps.
1695
+
1696
+ ## Icons
1697
+
1698
+ Imported from `@eduardoalvarez/arrecife/icons` · requires `@phosphor-icons/react`. 1 exports.
1699
+
1700
+ ### Icon
1701
+
1702
+ Source: `src/icons/index.tsx`
1703
+
1704
+ - Extends: `Omit<PhosphorIconProps, 'size' \| 'weight' \| 'ref'>`
1705
+
1706
+ | prop | type | req. | default | what it does |
1707
+ | --- | --- | --- | --- | --- |
1708
+ | `as` | `Icon` | yes | | The Phosphor icon itself, passed as a component: `<Icon as={Books} />`. |
1709
+ | `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. |
1710
+ | `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`. |
1711
+
1392
1712
  ## Forms
1393
1713
 
1394
1714
  Imported from `@eduardoalvarez/arrecife/form` · requires `react-hook-form`. 7 exports.
@@ -1521,7 +1841,7 @@ it: if the code does not mount React, that subpath is the one to import.
1521
1841
  | `radius` | `{ readonly chip: 6; readonly control: 10; readonly card: 14; readonly panel: 16; readonly pill: 999; }` | |
1522
1842
  | `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
1843
  | `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; }` | |
1844
+ | `size` | `{ readonly nav: 64; readonly navCompact: 56; readonly sidebar: 256; readonly sidebarRail: 56; readonly content: 760; readonly wide: 1180; }` | |
1525
1845
  | `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
1846
  | `syntax` | `{ background, identifier, literal, keyword, comment, invalid }` | The syntax highlighting palette. |
1527
1847
  | `tagline` | `{ long, short, en }` | |
@@ -1530,6 +1850,10 @@ it: if the code does not mount React, that subpath is the one to import.
1530
1850
 
1531
1851
  Types (13): `BrandToken`, `ColorMode`, `ColorToken`, `ControlToken`, `FontToken`, `GradientToken`, `RadiusToken`, `SeriesToken`, `SizeToken`, `SpacingToken`, `SyntaxToken`, `Tokens`, `TypeScaleToken`.
1532
1852
 
1853
+ ### `@eduardoalvarez/arrecife/social`
1854
+
1855
+ Types (1): `SocialIconProps`.
1856
+
1533
1857
  ### `@eduardoalvarez/arrecife/theme`
1534
1858
 
1535
1859
  | export | type | what it is |
@@ -1561,6 +1885,15 @@ Types (2): `Theme`, `ThemeOptions`.
1561
1885
 
1562
1886
  Types (8): `Background`, `Face`, `Fin`, `IsotypeProps`, `LogoProps`, `MascotFaceProps`, `MascotProps`, `Pose`.
1563
1887
 
1888
+ ### `@eduardoalvarez/arrecife/icons`
1889
+
1890
+ | export | type | what it is |
1891
+ | --- | --- | --- |
1892
+ | `ICON_WEIGHT` | `"light" \| "fill" \| "thin" \| "regular" \| "bold" \| "duotone"` | Phosphor's own name for the system's line. It is what `tone="action"` resolves to. |
1893
+ | `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. |
1894
+
1895
+ Types (2): `IconProps`, `IconTone`.
1896
+
1564
1897
  ### `@eduardoalvarez/arrecife/shiki`
1565
1898
 
1566
1899
  | export | type | what it is |
@@ -1602,11 +1935,11 @@ Types (6): `ArticleData`, `BaseData`, `CourseData`, `DefaultData`, `SatoriNode`,
1602
1935
  | export | type | what it is |
1603
1936
  | --- | --- | --- |
1604
1937
  | `cn` | `(...inputs: ClassValue[]): string` | |
1605
- | `social` | `typeof import("src/lib/social")` | |
1938
+ | `social` | `typeof import("src/social/index")` | |
1606
1939
  | `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
1940
  | `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
1941
 
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`.
1942
+ Types (62): `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`, `SidebarGroupProps`, `SidebarItemProps`, `SidebarNavProps`, `SkeletonProps`, `SocialLink`, `StatDelta`, `StatProps`, `SwitchProps`, `TableOfContentsProps`, `TabsProps`, `TalkCardProps`, `TextProps`, `TextareaProps`, `ThemeToggleProps`, `ToastOptions`, `ToastVariant`, `ToasterProps`, `TocEntry`.
1610
1943
 
1611
1944
  # Where to look if this is not enough
1612
1945
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eduardoalvarez/arrecife",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "The component library of Eduardo Álvarez’s visual identity",
5
5
  "license": "MIT",
6
6
  "author": "Eduardo Esteban Álvarez Castañeda <soy@eduardoalvarez.dev>",
@@ -49,6 +49,16 @@
49
49
  "import": "./dist/brand/index.js",
50
50
  "require": "./dist/brand/index.cjs"
51
51
  },
52
+ "./social": {
53
+ "types": "./dist/social/index.d.ts",
54
+ "import": "./dist/social/index.js",
55
+ "require": "./dist/social/index.cjs"
56
+ },
57
+ "./icons": {
58
+ "types": "./dist/icons/index.d.ts",
59
+ "import": "./dist/icons/index.js",
60
+ "require": "./dist/icons/index.cjs"
61
+ },
52
62
  "./og": {
53
63
  "types": "./dist/og/index.d.ts",
54
64
  "import": "./dist/og/index.js",
@@ -73,16 +83,23 @@
73
83
  "./assets/*": "./assets/*",
74
84
  "./package.json": "./package.json"
75
85
  },
86
+ "bin": {
87
+ "arrecife": "./dist/doctor.mjs"
88
+ },
76
89
  "main": "./dist/index.cjs",
77
90
  "module": "./dist/index.js",
78
91
  "types": "./dist/index.d.ts",
79
92
  "scripts": {
80
- "build": "pnpm check:tokens && pnpm check:namespace && tsup && node scripts/add-use-client.mjs && pnpm build:tokens && pnpm build:llms",
93
+ "build": "pnpm check:tokens && pnpm check:namespace && tsup && node scripts/add-use-client.mjs && pnpm build:tokens && pnpm build:doctor && pnpm build:llms",
81
94
  "build:tokens": "node scripts/build-tokens.mjs",
95
+ "build:doctor": "node scripts/build-doctor.mjs",
82
96
  "build:llms": "node scripts/build-llms.mjs",
83
97
  "check:tokens": "node scripts/check-tokens-purity.mjs",
84
98
  "check:namespace": "node scripts/check-tokens-namespace.mjs",
85
99
  "check:llms": "node scripts/build-llms.mjs --check",
100
+ "check:copy": "node scripts/check-copy-language.mjs",
101
+ "check:decisions": "node scripts/check-decisions.mjs",
102
+ "doctor": "node scripts/doctor.mjs",
86
103
  "check:exports": "node scripts/check-package-exports.mjs",
87
104
  "check:release": "node scripts/check-release-config.mjs",
88
105
  "typecheck": "tsc --noEmit",
@@ -97,12 +114,16 @@
97
114
  "test:light": "STORYBOOK_THEME=light vitest run --project storybook"
98
115
  },
99
116
  "peerDependencies": {
117
+ "@phosphor-icons/react": "^2.1.0",
100
118
  "react": "^19.0.0",
101
119
  "react-dom": "^19.0.0",
102
120
  "react-hook-form": "^7.0.0",
103
121
  "recharts": "^3.0.0"
104
122
  },
105
123
  "peerDependenciesMeta": {
124
+ "@phosphor-icons/react": {
125
+ "optional": true
126
+ },
106
127
  "react-hook-form": {
107
128
  "optional": true
108
129
  },
@@ -112,6 +133,7 @@
112
133
  },
113
134
  "devDependencies": {
114
135
  "@eslint/js": "^10.0.1",
136
+ "@phosphor-icons/react": "^2.1.10",
115
137
  "@storybook/addon-a11y": "^10.5.10",
116
138
  "@storybook/addon-docs": "^10.5.10",
117
139
  "@storybook/addon-themes": "^10.5.10",