@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/CHANGELOG.md CHANGED
@@ -1,5 +1,74 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.0](https://github.com/Proskynete/arrecife/compare/v0.6.0...v0.7.0) (2026-09-04)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * **icons:** `Icon` no longer accepts `weight`. Pass `tone` instead — `action` is the old default, `current` is `fill` and `quiet` is `light`. The version it changes in has not been published, so nothing on npm carries the old shape.
9
+ * **components:** `Stat`'s `tone="alerta"` becomes `tone="alert"`. Nothing else changes — same colour, same rule, same default. Migration in docs/migration-0.7.md.
10
+
11
+ ### 🚀 Novedades
12
+
13
+ * **components:** EmptyState gains the shape that carries no face ([7292ec0](https://github.com/Proskynete/arrecife/commit/7292ec07cc3332442335325c49bbc0f3473267fc))
14
+ * **components:** Nav gains the one thing of the three it was missing ([1d2ec5b](https://github.com/Proskynete/arrecife/commit/1d2ec5b0a39d46bf17ee91f2a756c3fa7e8e1cd8))
15
+ * **components:** Stat gains delta, spark and the achievement tone ([0cf4348](https://github.com/Proskynete/arrecife/commit/0cf4348f4efd16c49ab54a5c205bb06c4f04ffd6))
16
+ * **components:** Stat's tone stops being called alerta ([43ffa43](https://github.com/Proskynete/arrecife/commit/43ffa431ac6f675ab27be2a8096e0acbad758711))
17
+ * **components:** the sidebar collapses to a rail, and takes who is signed in ([6fae58f](https://github.com/Proskynete/arrecife/commit/6fae58f3e6bc9cf975a78ca1aeab484421386f51))
18
+ * **components:** the sidebar gets blocks, and the icon replaces the prompt ([1d910a3](https://github.com/Proskynete/arrecife/commit/1d910a38d56000d955bc317fc4566fb7038864f0))
19
+ * **components:** the Stat card as it goes in the panel ([50e47f1](https://github.com/Proskynete/arrecife/commit/50e47f1b898afe207011d00f983af260e623a970))
20
+ * **icons:** the system adopts Phosphor, and it still ships no icons ([8a89c4e](https://github.com/Proskynete/arrecife/commit/8a89c4ef3e2292b8cb5d3c0a287fa59ceb635679))
21
+ * **icons:** the weight is an axis with three roles, and none of them is bold ([a8a9bed](https://github.com/Proskynete/arrecife/commit/a8a9bed9df6dea6c48b63dfd5e4af8f44368befa))
22
+ * npx arrecife, for the two failures that produce no error ([1b2160f](https://github.com/Proskynete/arrecife/commit/1b2160f378a53d433851b4c7995d164f3b93edd1))
23
+ * **social:** the nine icons get a subpath that crosses the RSC boundary ([bfa239f](https://github.com/Proskynete/arrecife/commit/bfa239f6c75c8cd99b7c9fa0bad155fdc68b7d98))
24
+ * **tokens:** a second bar height, for a shell that also has a sidebar ([2277afb](https://github.com/Proskynete/arrecife/commit/2277afb6bc2a2674d7722d69a989259634b74cbd))
25
+ * **tokens:** the sidebar's two widths, because a collapsible one needs both ([8b04bc0](https://github.com/Proskynete/arrecife/commit/8b04bc0dcc065d408b40d2076cb61d5bbe19dbc6))
26
+
27
+
28
+ ### 🐛 Correcciones
29
+
30
+ * **a11y:** the focus ring is one utility, at the offset the document gives ([66519b9](https://github.com/Proskynete/arrecife/commit/66519b9ab9834608aae7098b127d1789ad932579))
31
+ * **a11y:** the two accessible names the English sweep took with it ([dea2b8c](https://github.com/Proskynete/arrecife/commit/dea2b8cf17b5ee4a449d92c4805e89c528aa4021))
32
+ * **components:** the default copy goes back to Spanish ([1b0121e](https://github.com/Proskynete/arrecife/commit/1b0121e284b3a5fd244ba63704adf35de06937d5))
33
+ * **components:** the story taught a face map that `faceUsage` denies ([98c46ff](https://github.com/Proskynete/arrecife/commit/98c46ff337e90eb4acb6e90b38a00b54be4805e3))
34
+ * **components:** thirteen more strings the sweep corrupted, in the demo copy ([d6dd7fc](https://github.com/Proskynete/arrecife/commit/d6dd7fcc84a205dba562c8e1f1559328bac73e76))
35
+ * **tokens:** no light gradient ends on surfaceRaised, where accent is 4.21 ([802fdd0](https://github.com/Proskynete/arrecife/commit/802fdd09e6841ea29c384e5f8f99866c1b30bb81))
36
+
37
+
38
+ ### 🔧 Mantenimiento
39
+
40
+ * **components:** the AudioPlayer's internals finish the move to English ([db566d7](https://github.com/Proskynete/arrecife/commit/db566d7223c04db928e925ef0fb4edc462c240f8))
41
+
42
+
43
+ ### 📚 Documentación
44
+
45
+ * § 35 and § 36, the weight axis and the two actions nobody carried over ([dc72b1d](https://github.com/Proskynete/arrecife/commit/dc72b1d70fe5344b34473fb1a6ae8bb22dca29e6))
46
+ * § 37, the focus ring, and the one half of it that is an interpretation ([6084787](https://github.com/Proskynete/arrecife/commit/6084787068bbd5882574aae323d42581d7ccdbd7))
47
+ * § 9 is ratified and § 38 answers the number nobody looked up ([cd301fc](https://github.com/Proskynete/arrecife/commit/cd301fc8bcba06a4df3928de83fa8eee573cc261))
48
+ * an icon is not illustration, and the version that says so is 0.7.0 ([5d5a88c](https://github.com/Proskynete/arrecife/commit/5d5a88c162fe8ec747602cff7c09476650cab2b8))
49
+ * **components:** the delta says direction, and the document says so first ([ad39c6f](https://github.com/Proskynete/arrecife/commit/ad39c6f9776817f745cca82b1c93fab6287eec31))
50
+ * the ban on icon libraries is lifted, and the set was picked by measurement ([b313b64](https://github.com/Proskynete/arrecife/commit/b313b64b69e0db6a75001a888d95be7c71d5b211))
51
+ * the contrast rule for surfaceRaised covers gradients too ([e5e92d5](https://github.com/Proskynete/arrecife/commit/e5e92d5efc7a0610d06b09b6a757def4fb88dce8))
52
+ * the focus ring is a utility now, and AGENTS said to write it out by hand ([1943567](https://github.com/Proskynete/arrecife/commit/1943567070e30aed733b08c1ca66fd18287221ff))
53
+ * the neutral number stops being biolume, and where the tone went ([9f3fffb](https://github.com/Proskynete/arrecife/commit/9f3fffbd1f6e65f8797a140f6be71268a2f62d38))
54
+ * the rail gets in, and the entry says which premise expired ([86d7b04](https://github.com/Proskynete/arrecife/commit/86d7b044432120d2b29eb3295c1958075a6109c1))
55
+ * the second shape of the empty state, and where it bends the document ([612e7a8](https://github.com/Proskynete/arrecife/commit/612e7a8c78b49a03a6db8f787b90ae13bc60a6eb))
56
+ * the sidebar's blocks, and why the label is not a heading ([dc030f1](https://github.com/Proskynete/arrecife/commit/dc030f1ae707e4ee8596b4b6659716c3debd0fa0))
57
+ * the third tone, and the fourth breaking change ([abb6dfc](https://github.com/Proskynete/arrecife/commit/abb6dfce7cf93f2167dbff29c32c8d6b399c440c))
58
+ * the two forms of the social icons, and which one crosses the boundary ([9b90a82](https://github.com/Proskynete/arrecife/commit/9b90a829201de857462c80cf846dde46d628597b))
59
+ * the two silent failures get a command, and the README stops being the guard ([4b40bb6](https://github.com/Proskynete/arrecife/commit/4b40bb6359e79100544da3ba8cb0586dfc82bd03))
60
+ * the weight is three values, and the README and AGENTS said it was one ([05f3351](https://github.com/Proskynete/arrecife/commit/05f33513c4332ab4651b0a256ad8cbecfe810e34))
61
+ * this is two releases, not one — the guide splits back ([16fdf94](https://github.com/Proskynete/arrecife/commit/16fdf9494092f0b77f7397d891637d130407555b))
62
+ * two of the three things Nav was missing were already there ([f77b712](https://github.com/Proskynete/arrecife/commit/f77b7125d500122b26bf22f79f5983be483e3df4))
63
+
64
+
65
+ ### 🚀 CI/CD
66
+
67
+ * a check for the copy, because «nothing catches this» was the wrong answer ([74169c3](https://github.com/Proskynete/arrecife/commit/74169c3803f753a61800de130184ec0a2043d821))
68
+ * icons joins the scope list ([783acc4](https://github.com/Proskynete/arrecife/commit/783acc432cea7f947e3dbd8fc3590be9617c5102))
69
+ * social joins the scope list, by the rule that was already written ([d805a37](https://github.com/Proskynete/arrecife/commit/d805a37e0206ff3f6f71718d69d8db93e2e70f75))
70
+ * the actions decisions.md owes a canvas get printed, because § 22's was not ([c5dc751](https://github.com/Proskynete/arrecife/commit/c5dc751aa01be82c3429239afb6372fa8471acc6))
71
+
3
72
  ## [0.6.0](https://github.com/Proskynete/arrecife/compare/v0.5.1...v0.6.0) (2026-09-02)
4
73
 
5
74
 
package/README.md CHANGED
@@ -86,6 +86,62 @@ the sheet in `src/styles/` it goes up two levels and not one. The blog's E2E
86
86
  tests caught it, not the build, and until 0.3.0 this was only written in
87
87
  `llms.txt` — the file an agent reads and a person does not.
88
88
 
89
+ ### `npx arrecife` — the two things that fail without saying so
90
+
91
+ ```
92
+ npx arrecife
93
+ ```
94
+
95
+ It reads your stylesheets and checks the two failures that produce no error, both
96
+ of which cost real hours in the migration:
97
+
98
+ **The missing `@source`**, above. It also works out the path for you, counted
99
+ from the sheet and not from the project root, which is the part that gets written
100
+ wrong.
101
+
102
+ **A token of yours redefining one of ours.** A project coming from shadcn brings
103
+ `@theme inline { --color-accent: var(--accent); }`, and the two are not the same
104
+ colour: shadcn's `--accent` is the hover **surface**, `#17303E`, and this
105
+ library's is the brand turquoise, `#35D6C0`. The result was **88 classes inside
106
+ the library's own components** painting grey — 28 `text-accent`, 26 focus rings,
107
+ 15 `bg-accent`, 12 `border-accent`. Buttons, focus rings and badges came out the
108
+ colour of a surface and it looked as though the migration had done nothing. (The
109
+ twenty-six are one `focus-ring` utility now, which changes the count and not the
110
+ failure: it reads `var(--color-accent)` like everything else here.)
111
+
112
+ ```
113
+ arrecife · 2 thing(s) that fail without saying so:
114
+
115
+ src/styles/globals.css
116
+ imports @eduardoalvarez/arrecife/tokens/theme.css and has no @source.
117
+ Every class the components emit is being purged — silently. Add:
118
+
119
+ @source "../../node_modules/@eduardoalvarez/arrecife/dist";
120
+
121
+ src/styles/globals.css
122
+ redefines --color-accent, which @eduardoalvarez/arrecife owns.
123
+ yours: var(--accent) ← points at another property, so it wins silently
124
+ arrecife: #35D6C0
125
+ ```
126
+
127
+ Five names collide with shadcn's — `background`, `border`, `warm`, `warm-hover`
128
+ and `accent`. Four are harmless because both sides happen to agree on the value,
129
+ so the command reports the value on each side and only fails on the ones that
130
+ differ. A collision that agrees is worth knowing about and is not worth failing
131
+ over.
132
+
133
+ > **Coming from 0.6.0.** One break, and it is a find and replace the type checker
134
+ > points at: `Stat`'s `tone="alerta"` is `tone="alert"`.
135
+ >
136
+ > Everything else is additive, and most of it lets a project delete something it
137
+ > was maintaining by hand: `./social` and `./icons` are two new subpaths,
138
+ > `EmptyState` has a shape with no face, `Stat` covers the KPI cards, `SidebarNav`
139
+ > groups and collapses, and `npx arrecife` catches two failures that produce no
140
+ > error at all.
141
+ >
142
+ > Run `npx arrecife` first, then read
143
+ > [`docs/migration-0.7.md`](docs/migration-0.7.md).
144
+
89
145
  > **Coming from 0.5.x.** Two unrelated things landed in 0.6.0, and they ship
90
146
  > together because in `0.x` a breaking change bumps the minor.
91
147
  >
@@ -275,32 +331,42 @@ Verified by packing the library with `pnpm pack` and installing it in a separate
275
331
  project: the types resolve from `dist/`, `./tokens` loads without dragging React
276
332
  in and `./tokens/theme.css` resolves by subpath.
277
333
 
278
- ### The social icons are namespaced
279
-
280
- It is the first thing anyone consuming the library trips over, because the
281
- natural form does not work:
334
+ ### The social icons come from `./social`
282
335
 
283
336
  ```tsx
284
- // ❌ does not exist
337
+ // ❌ does not exist: the root publishes them grouped, not loose
285
338
  import { GitHub, LinkedIn } from '@eduardoalvarez/arrecife';
286
339
 
287
- // ✅
288
- import { social } from '@eduardoalvarez/arrecife';
340
+ // ✅ the normal form
341
+ import { GitHub, LinkedIn } from '@eduardoalvarez/arrecife/social';
289
342
 
343
+ // ✅ for iterating the catalogue
344
+ import { social } from '@eduardoalvarez/arrecife';
290
345
  <social.GitHub />
291
- <social.LinkedIn />
292
346
  ```
293
347
 
294
348
  All nine are `GitHub`, `LinkedIn`, `X`, `Instagram`, `Discord`, `YouTube`, `Rss`,
295
- `Email` and `Newsletter`. They live under a namespace for a concrete reason:
296
- **one of them is called `X`**.
349
+ `Email` and `Newsletter`.
350
+
351
+ **The two forms are not taste, and in Next they are not interchangeable.** The
352
+ root carries `"use client"`, and what crosses into a Server Component is a client
353
+ reference **per export** — the properties of a plain object are not exports. So
354
+ from a Server Component `social.LinkedIn` is `undefined`, and `undefined` as an
355
+ element type kills the build at prerender. `./social` carries no directive: the
356
+ icon renders on the server, ships no client JS, and pulls 5.6 KB instead of the
357
+ root's 116 KB. Reach for the subpath by default; reach for `social` when you are
358
+ mapping a list of link names onto icons.
359
+
360
+ The namespace stays because **one of them is called `X`**. An `export const X` at
361
+ the root of a component library collides with anything — a generic's type
362
+ variable, an `import { X }` from somewhere else — and the failure shows up far
363
+ from here. In the subpath you asked for icons, so the collision is yours to
364
+ resolve and it takes one word: `import { X as XIcon }`.
297
365
 
298
366
  `Newsletter` is the bell, and it is named for what it means and not for what it
299
367
  draws — same as everything else in the system. It plays `Rss`'s role: a way to
300
368
  follow, not a social network. That is what keeps it inside this catalogue and
301
- keeps the catalogue from turning into an icon library. An `export const X` at the root of a component library collides
302
- with anything — a generic's type variable, an `import { X }` from somewhere else
303
- — and the failure shows up far from here.
369
+ keeps the catalogue from turning into an icon library.
304
370
 
305
371
  **The internal glyphs are NOT exported.** `Close`, `ChevronDown`, `Copy`, `Sun`
306
372
  and company are the minimum set the primitives need and they stay inside.
@@ -309,6 +375,77 @@ decided not to have, and from there it grows on its own. A project that needs an
309
375
  icon passes its own: `Stat` receives `icon`, `Footer` receives each social link's
310
376
  `icon`.
311
377
 
378
+ ### The icons are yours, the way they are drawn is not
379
+
380
+ That last sentence used to end there, and «its own what, drawn how» had no
381
+ answer. The admin panel imports 89 distinct icons in 229 places — 77 of them
382
+ domain icons for a course admin, which no design system was going to ship — and
383
+ drew them at `size-4` twenty-six times, plus `size-3.5`, `size-3`, `size-6` and
384
+ `size-7`, with no rule behind any of them.
385
+
386
+ ```tsx
387
+ import { GraduationCap } from '@phosphor-icons/react';
388
+ import { Icon } from '@eduardoalvarez/arrecife/icons';
389
+
390
+ <Icon as={GraduationCap} />
391
+ ```
392
+
393
+ 1em, so the icon takes the size of the text it sits in and nobody picks a number.
394
+ Weight `regular` by default, and **that is the whole reason the set is
395
+ Phosphor**: it bakes the weight into the path instead of exposing a
396
+ `strokeWidth`, and its regular lands on the one stroke the identity document
397
+ names. Measured on the `Minus` path itself, whose regular form is a bar of radius
398
+ 8 on a 256 grid:
399
+
400
+ | | Line | As a fraction of the rendered size |
401
+ | --- | --- | --- |
402
+ | phosphor `regular` | 16 on a 256 grid | **0.0625em** |
403
+ | the document | 1.6 on a 24 grid | **0.0667em** |
404
+ | `lib/glyphs.tsx` | 1.75 on a 16 grid | 0.109em |
405
+
406
+ Six per cent apart, which is no pixel on any screen. Nothing had to be derived and
407
+ no number had to be invented. The `Icons/Icon` → `regular IS the document's
408
+ stroke` story alternates the bars so the claim can be checked instead of believed
409
+ — and it also shows the third row, because **`glyphs.tsx` is the outlier**: at
410
+ 0.109em it is three quarters heavier than both, it was never argued anywhere, and
411
+ aligning it would restyle every primitive in the library. That is a separate
412
+ change and `docs/decisions.md` § 29 says so.
413
+
414
+ **The weight is an axis with three values, and `tone` is how you name them.**
415
+ `weight` is not a prop: Phosphor ships six and this system reads three, because
416
+ `thin`, `bold` and `duotone` have no role behind them here.
417
+
418
+ | `tone` | Weight | What it is |
419
+ | --- | --- | --- |
420
+ | `action` · the default | `regular` | An icon that is a control or names one |
421
+ | `current` | `fill` | The one of a set you are on — the item carrying `aria-current` |
422
+ | `quiet` | `light` | Furniture: a marker in a metadata row, not a control and not a state |
423
+
424
+ `current` is the value that earns the axis, and the argument is not taste.
425
+ An active sidebar item already says so in biolume, and **colour on its own is the
426
+ one channel WCAG 1.4.1 says may not carry meaning** — the fill is the second
427
+ channel, and it is the one that survives a forced-colours mode where the biolume
428
+ does not. `quiet` is the opposite problem: in a metadata row the icon is not the
429
+ point of the line, and at `regular` it draws as heavy as the date beside it.
430
+ `docs/decisions.md` § 35 has the rest.
431
+
432
+ `@phosphor-icons/react` is an **optional** peer dependency on its own subpath, by
433
+ the same rule as `./form` and `./chart`: two of the five projects use no icons and
434
+ install nothing.
435
+
436
+ **An icon is not illustration.** Tiburoncín — the faces, the poses, the fin — is
437
+ the mascot, it comes from `./brand`, and the manual doses it by surface: a face
438
+ only in an empty state, a confirmation, an error, course progress or a
439
+ celebration. An icon is functional vocabulary and goes wherever a control needs a
440
+ label it cannot spell. Adopting a set changed nothing about the first, and
441
+ neither stands in for the other in either direction.
442
+
443
+ **In Next, import from `@phosphor-icons/react/ssr` inside a Server Component.**
444
+ Phosphor's default build reads `IconContext` through `useContext`, and a hook in a
445
+ Server Component throws — and it ships no `"use client"` to stop you, so the
446
+ failure arrives at render. The `/ssr` entry is the same icons without the context
447
+ read, and `Icon` works with either.
448
+
312
449
  ### `"use client"` is in the published `dist`
313
450
 
314
451
  The root, `./brand`, `./form` and `./chart` carry the directive. They render
@@ -325,13 +462,20 @@ Marking them client would be a lie with a cost: a Server Component importing
325
462
  `buttonVariants`, a function that returns a string, would pull a client boundary
326
463
  in with it.
327
464
 
465
+ `./social` is the third case, and it is why the check stopped looking only at the
466
+ portable ones. It renders React — it is nine `<svg>` — so it can never be
467
+ portable, and it holds no state, so it must not be a client entry either. Listed
468
+ in neither set, nothing would have noticed it being marked client by mistake, and
469
+ that mistake undoes the only reason the subpath exists. See `docs/decisions.md`
470
+ § 26.
471
+
328
472
  It is stamped by `scripts/add-use-client.mjs` after tsup, and not by tsup's
329
473
  `banner`. That was tried first: esbuild writes the directive and the bundling
330
474
  pass strips it back out with a `Module level directives cause errors when
331
475
  bundled` warning. The build stayed green and the published package was broken for
332
476
  Next — the worst way to fail, because the failure surfaces in somebody else's
333
477
  project. `check:exports` now verifies it in both directions: present on the four
334
- client entries, absent from the portable ones.
478
+ client entries, absent from every other subpath.
335
479
 
336
480
  It is inert outside Next. In Astro and in plain Vite it is a string literal at
337
481
  the top of a module; Rollup may warn and nothing else happens. One `dist` serves
@@ -354,9 +498,11 @@ five projects.
354
498
  In `cursos` it forced a `"use client"` on an adapter whose entire content was one
355
499
  call to CVA. In `links`, which depends on no React at all, it was not even an
356
500
  option: that project copied the class vocabulary by hand into `LinkRow.astro` and
357
- `Footer.astro`, and the copy had already drifted once — the hero gradient sat at
358
- `55%` and `#e9eeea` against the token's `60%` and `#EFE9DE`, and nothing compared
359
- them.
501
+ `Footer.astro`, and the copy had already drifted once: the hero gradient sat at
502
+ `55%` and `#e9eeea` against the token's `60%` and, at the time, `#EFE9DE`, and
503
+ nothing compared them. That light stop is `#FFFFFF` now — § 9 measured it — which
504
+ is the same lesson seen from the other end. A copied value goes stale the moment
505
+ the original moves, and only the original is ever right.
360
506
 
361
507
  The rule for what belongs in the subpath: if it returns classes, it goes there;
362
508
  if it returns markup, it stays in the component. `Button` renders a `<button>`,
@@ -442,6 +588,21 @@ contrast ratio, which overshoots near white.
442
588
  `textMuted` never goes over `surfaceRaised`: in dark it gives 4.07. Menus use
443
589
  `textSecondary`, which gives 6.96.
444
590
 
591
+ **And no gradient ends there either**, which is the same rule applied to a
592
+ surface that moves. The light `hero` and `section` blocks used to sweep from the
593
+ page down onto `surfaceRaised`, where light `accent` reads **4.21** and `warm`
594
+ **4.19** — both under 4.5, and both of them fine at 4.55 and 4.53 on the page
595
+ they started from. That makes a token's contrast a function of **where in the
596
+ panel the text happens to sit**, which no token can guarantee and the suite
597
+ cannot see: axe does not evaluate text over a gradient, so both modes passed it.
598
+ `Hero` puts an `accent` eyebrow directly on that gradient.
599
+
600
+ The light blocks now sweep between `background` and `surface` and never touch
601
+ `surfaceRaised`, so the darkest point of either one is the page itself — a token
602
+ that passes on the page passes at every point of the sweep. `docs/decisions.md`
603
+ § 9 has the measurements, including the two other things the first composition
604
+ got wrong.
605
+
445
606
  ### The third correction: a semantic color is not a text color over its own tint
446
607
 
447
608
  It came up while implementing the document's alert recipe — background at 8 % of
@@ -668,9 +829,13 @@ about as library pieces. They get in anyway: the CLI aesthetic — the bar's
668
829
 
669
830
  ### Phase 3 decisions
670
831
 
671
- - **No `lucide-react`.** The eight glyphs the primitives need are inline in
672
- `src/lib/glyphs.tsx`, inherit `currentColor` and measure 1em. An icon library
673
- as a dependency is something each of the five projects pays for.
832
+ - **It ships no icon set**, and that has not changed. The eight glyphs the
833
+ primitives need are inline in `src/lib/glyphs.tsx`, inherit `currentColor` and
834
+ measure 1em, and they are not exported. What DID change is that
835
+ `@phosphor-icons/react` is now an optional peer on `./icons`, so the set a
836
+ project chooses is drawn at the system's weight — see «The icons are yours»
837
+ above. Optional and on a subpath is the point: the two projects that use no
838
+ icons install nothing.
674
839
  - **No entrance animations.** Modals, menus, tooltips and toasts appear where
675
840
  they will stay. The `Switch` knob changes position without sliding. The
676
841
  system's only transition is `transition-standard`, which can only animate color
@@ -1,6 +1,6 @@
1
1
  'use client';
2
- export { Isotype, Logo, Mascot, MascotFace } from '../chunk-6O3KWB6P.js';
2
+ export { Isotype, Logo, Mascot, MascotFace } from '../chunk-JN3IS5OS.js';
3
+ import '../chunk-2WPWEIMD.js';
3
4
  export { ASSETS_PATH, faceList, faceUsage, faces, fins, poseList, poses } from '../chunk-CKRSQPTX.js';
4
- import '../chunk-XKYHTOUJ.js';
5
- import '../chunk-O4TAH7YJ.js';
5
+ import '../chunk-OMKSESQB.js';
6
6
  import '../chunk-MLKGABMK.js';
@@ -1,8 +1,8 @@
1
1
  'use client';
2
- import { Text } from '../chunk-JMOOFZ3B.js';
2
+ import { Text } from '../chunk-WGNIRIN7.js';
3
3
  import '../chunk-ODBFN44D.js';
4
- import { cn } from '../chunk-XKYHTOUJ.js';
5
- import '../chunk-O4TAH7YJ.js';
4
+ import { cn } from '../chunk-2WPWEIMD.js';
5
+ import '../chunk-OMKSESQB.js';
6
6
  import '../chunk-MLKGABMK.js';
7
7
  import { ResponsiveContainer, Tooltip, Legend } from 'recharts';
8
8
  import { jsxs, jsx } from 'react/jsx-runtime';
@@ -1,4 +1,4 @@
1
- import { spacing, control, radius, typeScale } from './chunk-O4TAH7YJ.js';
1
+ import { spacing, control, radius, typeScale } from './chunk-OMKSESQB.js';
2
2
  import { clsx } from 'clsx';
3
3
  import { extendTailwindMerge } from 'tailwind-merge';
4
4
 
@@ -0,0 +1,70 @@
1
+ import { __export } from './chunk-MLKGABMK.js';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+
4
+ // src/social/index.tsx
5
+ var social_exports = {};
6
+ __export(social_exports, {
7
+ Discord: () => Discord,
8
+ Email: () => Email,
9
+ GitHub: () => GitHub,
10
+ Instagram: () => Instagram,
11
+ LinkedIn: () => LinkedIn,
12
+ Newsletter: () => Newsletter,
13
+ Rss: () => Rss,
14
+ X: () => X,
15
+ YouTube: () => YouTube
16
+ });
17
+ function Brand({ children, ...props }) {
18
+ return /* @__PURE__ */ jsx(
19
+ "svg",
20
+ {
21
+ viewBox: "0 0 24 24",
22
+ width: "1em",
23
+ height: "1em",
24
+ fill: "currentColor",
25
+ "aria-hidden": "true",
26
+ focusable: "false",
27
+ ...props,
28
+ children
29
+ }
30
+ );
31
+ }
32
+ function Functional({ children, ...props }) {
33
+ return /* @__PURE__ */ jsx(
34
+ "svg",
35
+ {
36
+ viewBox: "0 0 24 24",
37
+ width: "1em",
38
+ height: "1em",
39
+ fill: "none",
40
+ stroke: "currentColor",
41
+ strokeWidth: 1.6,
42
+ strokeLinecap: "round",
43
+ strokeLinejoin: "round",
44
+ "aria-hidden": "true",
45
+ focusable: "false",
46
+ ...props,
47
+ children
48
+ }
49
+ );
50
+ }
51
+ var GitHub = (props) => /* @__PURE__ */ jsx(Brand, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M12 .5C5.73.5.5 5.73.5 12a11.5 11.5 0 0 0 7.86 10.91c.58.1.79-.25.79-.55 0-.27-.01-1.18-.02-2.14-3.2.7-3.88-1.36-3.88-1.36-.52-1.33-1.28-1.68-1.28-1.68-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.19 1.77 1.19 1.03 1.77 2.7 1.26 3.36.96.1-.75.4-1.26.73-1.55-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11 11 0 0 1 5.79 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.8 1.18 1.83 1.18 3.09 0 4.42-2.69 5.39-5.25 5.68.41.36.78 1.06.78 2.14 0 1.55-.01 2.79-.01 3.17 0 .3.2.66.79.55A11.5 11.5 0 0 0 23.5 12C23.5 5.73 18.27.5 12 .5Z" }) });
52
+ var LinkedIn = (props) => /* @__PURE__ */ jsx(Brand, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5ZM3 9h4v12H3V9Zm7 0h3.8v1.71h.05a4.17 4.17 0 0 1 3.75-2.06c4.01 0 4.75 2.64 4.75 6.07V21h-4v-5.4c0-1.29-.02-2.95-1.8-2.95-1.8 0-2.08 1.4-2.08 2.85V21h-3.97V9Z" }) });
53
+ var X = (props) => /* @__PURE__ */ jsx(Brand, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M17.53 3h3.06l-6.69 7.64L21.75 21h-6.16l-4.83-6.3L5.25 21H2.19l7.15-8.17L2.5 3h6.32l4.36 5.77L17.53 3Zm-1.07 16.17h1.7L7.62 4.74H5.8l10.66 14.43Z" }) });
54
+ var Instagram = (props) => /* @__PURE__ */ jsx(Brand, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M12 2.16c3.2 0 3.58.01 4.85.07 1.17.05 1.8.25 2.23.41.56.22.96.48 1.38.9.42.42.68.82.9 1.38.16.42.36 1.06.41 2.23.06 1.27.07 1.65.07 4.85s-.01 3.58-.07 4.85c-.05 1.17-.25 1.8-.41 2.23-.22.56-.48.96-.9 1.38-.42.42-.82.68-1.38.9-.42.16-1.06.36-2.23.41-1.27.06-1.65.07-4.85.07s-3.58-.01-4.85-.07c-1.17-.05-1.8-.25-2.23-.41-.56-.22-.96-.48-1.38-.9-.42-.42-.68-.82-.9-1.38-.16-.42-.36-1.06-.41-2.23C2.17 15.58 2.16 15.2 2.16 12s.01-3.58.07-4.85c.05-1.17.25-1.8.41-2.23.22-.56.48-.96.9-1.38.42-.42.82-.68 1.38-.9.42-.16 1.06-.36 2.23-.41C8.42 2.17 8.8 2.16 12 2.16Zm0 3.68a6.16 6.16 0 1 0 0 12.32 6.16 6.16 0 0 0 0-12.32Zm0 10.16a4 4 0 1 1 0-8 4 4 0 0 1 0 8Zm7.85-10.4a1.44 1.44 0 1 1-2.88 0 1.44 1.44 0 0 1 2.88 0Z" }) });
55
+ var Discord = (props) => /* @__PURE__ */ jsx(Brand, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M20.32 5.56A18.5 18.5 0 0 0 15.7 4.1a.07.07 0 0 0-.07.04c-.2.36-.42.82-.58 1.19a17 17 0 0 0-5.1 0c-.16-.38-.39-.83-.59-1.19a.07.07 0 0 0-.07-.04c-1.6.28-3.15.77-4.62 1.46a.06.06 0 0 0-.03.03C1.66 10 .9 14.28 1.28 18.5a.08.08 0 0 0 .03.05 18.6 18.6 0 0 0 5.6 2.84.07.07 0 0 0 .08-.03c.43-.59.81-1.21 1.14-1.86a.07.07 0 0 0-.04-.1c-.6-.23-1.18-.51-1.74-.83a.07.07 0 0 1-.01-.12l.35-.27a.07.07 0 0 1 .07-.01 13.3 13.3 0 0 0 11.3 0 .07.07 0 0 1 .08.01l.34.27c.04.04.04.1-.01.12-.55.33-1.13.6-1.74.83a.07.07 0 0 0-.04.1c.34.65.72 1.27 1.14 1.86a.07.07 0 0 0 .08.03 18.5 18.5 0 0 0 5.6-2.84.07.07 0 0 0 .04-.05c.45-4.88-.75-9.12-3.18-12.88a.06.06 0 0 0-.03-.03ZM8.4 15.93c-1.1 0-2.01-1.01-2.01-2.25 0-1.24.89-2.25 2.01-2.25 1.13 0 2.03 1.02 2.02 2.25 0 1.24-.9 2.25-2.02 2.25Zm7.22 0c-1.1 0-2.01-1.01-2.01-2.25 0-1.24.89-2.25 2.01-2.25 1.13 0 2.03 1.02 2.02 2.25 0 1.24-.89 2.25-2.02 2.25Z" }) });
56
+ var YouTube = (props) => /* @__PURE__ */ jsx(Brand, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M23.5 6.9a3 3 0 0 0-2.12-2.12C19.5 4.27 12 4.27 12 4.27s-7.5 0-9.38.51A3 3 0 0 0 .5 6.9 31.3 31.3 0 0 0 0 12a31.3 31.3 0 0 0 .5 5.1 3 3 0 0 0 2.12 2.12c1.88.51 9.38.51 9.38.51s7.5 0 9.38-.51a3 3 0 0 0 2.12-2.12A31.3 31.3 0 0 0 24 12a31.3 31.3 0 0 0-.5-5.1ZM9.6 15.6V8.4l6.24 3.6-6.24 3.6Z" }) });
57
+ var Rss = (props) => /* @__PURE__ */ jsxs(Functional, { ...props, children: [
58
+ /* @__PURE__ */ jsx("path", { d: "M4 11a9 9 0 0 1 9 9M4 4a16 16 0 0 1 16 16" }),
59
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "19", r: "1.6", fill: "currentColor", stroke: "none" })
60
+ ] });
61
+ var Email = (props) => /* @__PURE__ */ jsxs(Functional, { ...props, children: [
62
+ /* @__PURE__ */ jsx("rect", { x: "2.75", y: "4.75", width: "18.5", height: "14.5", rx: "2" }),
63
+ /* @__PURE__ */ jsx("path", { d: "m3.5 7.5 7.4 5.2a2 2 0 0 0 2.2 0l7.4-5.2" })
64
+ ] });
65
+ var Newsletter = (props) => /* @__PURE__ */ jsxs(Functional, { ...props, children: [
66
+ /* @__PURE__ */ jsx("path", { d: "M18 8.5a6 6 0 1 0-12 0c0 5-2 6.5-2 6.5h16s-2-1.5-2-6.5" }),
67
+ /* @__PURE__ */ jsx("path", { d: "M13.7 19a2 2 0 0 1-3.4 0" })
68
+ ] });
69
+
70
+ export { Discord, Email, GitHub, Instagram, LinkedIn, Newsletter, Rss, X, YouTube, social_exports };
@@ -1,4 +1,4 @@
1
- import { cn } from './chunk-XKYHTOUJ.js';
1
+ import { cn } from './chunk-2WPWEIMD.js';
2
2
  import * as LabelPrimitive from '@radix-ui/react-label';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
 
@@ -6,7 +6,7 @@ var button = cva(
6
6
  "inline-flex cursor-pointer items-center justify-center gap-step-xs whitespace-nowrap select-none",
7
7
  "rounded-control font-sans font-medium",
8
8
  "transition-standard",
9
- "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",
9
+ "focus-ring",
10
10
  "disabled:pointer-events-none disabled:opacity-50",
11
11
  "[&_svg]:shrink-0"
12
12
  ],
@@ -17,7 +17,10 @@ var button = cva(
17
17
  "bg-accent text-accent-on hover:bg-accent-hover",
18
18
  "light:bg-brand-hull light:text-accent-on light:hover:bg-text-secondary"
19
19
  ],
20
- conversion: "bg-warm text-warm-on hover:bg-warm-hover",
20
+ // The one control whose focus ring is not biolume. It is the system's
21
+ // only sand fill, and a biolume ring three pixels off a sand button puts
22
+ // both of the brand's accents in the same glance. See `decisions.md` § 37.
23
+ conversion: "bg-warm text-warm-on hover:bg-warm-hover focus-ring-warm",
21
24
  secondary: [
22
25
  "border-hairline-hover border bg-transparent text-text-primary",
23
26
  "hover:border-accent hover:text-accent"
@@ -135,7 +138,7 @@ var CARD = [
135
138
  "group block cursor-pointer",
136
139
  CARD_SURFACE,
137
140
  CARD_HOVER,
138
- "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
141
+ "focus-ring"
139
142
  ];
140
143
 
141
144
  export { CARD, CARD_HOVER, CARD_SURFACE, alert, avatar, badge, button, category, metric };
@@ -1,6 +1,6 @@
1
+ import { cn } from './chunk-2WPWEIMD.js';
1
2
  import { fins, ASSETS_PATH, poses, faces } from './chunk-CKRSQPTX.js';
2
- import { cn } from './chunk-XKYHTOUJ.js';
3
- import { naming, tagline } from './chunk-O4TAH7YJ.js';
3
+ import { naming, tagline } from './chunk-OMKSESQB.js';
4
4
  import { jsx, jsxs } from 'react/jsx-runtime';
5
5
 
6
6
  function Isotype({
@@ -213,6 +213,30 @@ var spacing = {
213
213
  var size = {
214
214
  /** Navigation bar height. */
215
215
  nav: 64,
216
+ /**
217
+ * The bar when it shares the screen with a sidebar.
218
+ *
219
+ * It is not «a smaller bar because it looks better». At 64 the header of an
220
+ * app shell competes with the rail beside it for the same corner, and the two
221
+ * together eat the top of the content area. 56 is what `cursos` had arrived at
222
+ * on its own, and it is the same reasoning as `control.iconSm`: the one admin
223
+ * app of the five is denser than the four reading sites. See
224
+ * `docs/decisions.md` § 30.
225
+ */
226
+ navCompact: 56,
227
+ /**
228
+ * The admin sidebar, and the rail it collapses to.
229
+ *
230
+ * They are tokens and not the consumer's `w-*` because a collapsible sidebar
231
+ * has to know BOTH: the layout beside it reserves one of the two, and two
232
+ * numbers written by hand in two files drift the first time one of them moves.
233
+ * A sidebar that never collapses is still just a box the layout sizes.
234
+ *
235
+ * 56 is the rail: `navCompact`'s height, so a panel's bar and its rail meet in
236
+ * a square corner instead of a step.
237
+ */
238
+ sidebar: 256,
239
+ sidebarRail: 56,
216
240
  /** Reading column width. */
217
241
  content: 760,
218
242
  /** Maximum page width. */
@@ -245,9 +269,9 @@ var gradient = {
245
269
  og: `linear-gradient(145deg, ${dark.background} 55%, ${deep} 100%)`
246
270
  },
247
271
  light: {
248
- hero: `linear-gradient(160deg, ${light.background} 60%, ${light.surfaceRaised} 100%)`,
249
- section: `linear-gradient(150deg, ${light.surface} 0%, ${light.surfaceRaised} 100%)`,
250
- og: `linear-gradient(145deg, ${light.background} 55%, ${light.surfaceRaised} 100%)`
272
+ hero: `linear-gradient(160deg, ${light.background} 60%, ${light.surface} 100%)`,
273
+ section: `linear-gradient(150deg, ${light.surface} 0%, ${light.background} 100%)`,
274
+ og: `linear-gradient(145deg, ${light.background} 55%, ${light.surface} 100%)`
251
275
  }
252
276
  };
253
277
  var shadow = {
@@ -1,4 +1,4 @@
1
- import { naming, tagline, motion, shadow, series, syntax, gradient, size, spacing, control, radius, limits, typeScale, fonts, brand, colors } from './chunk-O4TAH7YJ.js';
1
+ import { naming, tagline, motion, shadow, series, syntax, gradient, size, spacing, control, radius, limits, typeScale, fonts, brand, colors } from './chunk-OMKSESQB.js';
2
2
 
3
3
  // src/tokens/index.ts
4
4
  var tokens = {
@@ -1,5 +1,5 @@
1
1
  import { text } from './chunk-ODBFN44D.js';
2
- import { cn } from './chunk-XKYHTOUJ.js';
2
+ import { cn } from './chunk-2WPWEIMD.js';
3
3
  import { Slot } from '@radix-ui/react-slot';
4
4
  import 'class-variance-authority';
5
5
  import { jsx } from 'react/jsx-runtime';