@eduardoalvarez/arrecife 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +868 -467
  3. package/dist/brand/index.cjs +112 -95
  4. package/dist/brand/index.d.cts +40 -39
  5. package/dist/brand/index.d.ts +40 -39
  6. package/dist/brand/index.js +5 -4
  7. package/dist/catalog-D13txprv.d.cts +78 -0
  8. package/dist/catalog-D13txprv.d.ts +78 -0
  9. package/dist/chart/index.cjs +100 -83
  10. package/dist/chart/index.d.cts +66 -66
  11. package/dist/chart/index.d.ts +66 -66
  12. package/dist/chart/index.js +14 -12
  13. package/dist/chunk-2WPWEIMD.js +27 -0
  14. package/dist/chunk-45HVCTB7.js +70 -0
  15. package/dist/{chunk-ZEOQKRQ7.js → chunk-727HCBD4.js} +1 -1
  16. package/dist/chunk-CKRSQPTX.js +36 -0
  17. package/dist/chunk-E6KFUSKB.js +144 -0
  18. package/dist/chunk-GCRII2KQ.js +86 -0
  19. package/dist/{chunk-YZ2SDOVZ.js → chunk-JN3IS5OS.js} +30 -30
  20. package/dist/chunk-ODBFN44D.js +45 -0
  21. package/dist/chunk-OMKSESQB.js +300 -0
  22. package/dist/{chunk-VPT32GPG.js → chunk-TA7TLWW4.js} +2 -2
  23. package/dist/chunk-WGNIRIN7.js +42 -0
  24. package/dist/doctor.mjs +166 -0
  25. package/dist/form/index.cjs +109 -92
  26. package/dist/form/index.d.cts +43 -42
  27. package/dist/form/index.d.ts +43 -42
  28. package/dist/form/index.js +25 -23
  29. package/dist/icons/index.cjs +149 -0
  30. package/dist/icons/index.d.cts +94 -0
  31. package/dist/icons/index.d.ts +94 -0
  32. package/dist/icons/index.js +28 -0
  33. package/dist/index-DlAO2JZs.d.cts +47 -0
  34. package/dist/index-DlAO2JZs.d.ts +47 -0
  35. package/dist/index.cjs +1292 -983
  36. package/dist/index.d.cts +927 -806
  37. package/dist/index.d.ts +927 -806
  38. package/dist/index.js +809 -778
  39. package/dist/{label-DuTvJGxD.d.ts → label-MgHFKnFy.d.cts} +3 -3
  40. package/dist/{label-DuTvJGxD.d.cts → label-MgHFKnFy.d.ts} +3 -3
  41. package/dist/og/index.cjs +133 -132
  42. package/dist/og/index.d.cts +93 -89
  43. package/dist/og/index.d.ts +93 -89
  44. package/dist/og/index.js +106 -106
  45. package/dist/shiki/index.cjs +28 -30
  46. package/dist/shiki/index.d.cts +4 -4
  47. package/dist/shiki/index.d.ts +4 -4
  48. package/dist/shiki/index.js +12 -12
  49. package/dist/social/index.cjs +67 -0
  50. package/dist/social/index.d.cts +2 -0
  51. package/dist/social/index.d.ts +2 -0
  52. package/dist/social/index.js +2 -0
  53. package/dist/theme/index.cjs +97 -0
  54. package/dist/theme/index.d.cts +144 -0
  55. package/dist/theme/index.d.ts +144 -0
  56. package/dist/theme/index.js +2 -0
  57. package/dist/tokens/index.cjs +159 -88
  58. package/dist/tokens/index.d.cts +277 -165
  59. package/dist/tokens/index.d.ts +277 -165
  60. package/dist/tokens/index.js +2 -2
  61. package/dist/tokens/theme.css +165 -100
  62. package/dist/variants/index.cjs +195 -0
  63. package/dist/variants/index.d.cts +195 -0
  64. package/dist/variants/index.d.ts +195 -0
  65. package/dist/variants/index.js +3 -0
  66. package/llms.txt +1145 -746
  67. package/package.json +42 -11
  68. package/dist/catalogo-Du5ID-Hi.d.cts +0 -77
  69. package/dist/catalogo-Du5ID-Hi.d.ts +0 -77
  70. package/dist/chunk-E3OMP2DL.js +0 -36
  71. package/dist/chunk-KPZNNMV5.js +0 -83
  72. package/dist/chunk-NHS7ETKJ.js +0 -27
  73. package/dist/chunk-TSPJOM6K.js +0 -229
  74. package/dist/chunk-UOWIDFCB.js +0 -81
  75. package/dist/tema/index.cjs +0 -94
  76. package/dist/tema/index.d.cts +0 -110
  77. package/dist/tema/index.d.ts +0 -110
  78. package/dist/tema/index.js +0 -2
@@ -0,0 +1,300 @@
1
+ // src/tokens/tokens.ts
2
+ var dark = {
3
+ background: "#091319",
4
+ // abyss · page background
5
+ surface: "#10202B",
6
+ // trench · cards and panels
7
+ surfaceRaised: "#17303E",
8
+ // current · menus, active tabs
9
+ border: "#22414F",
10
+ // control borders
11
+ hairline: "#1E3441",
12
+ // subtle dividers
13
+ hairlineHover: "#2C4D5D",
14
+ // hairline on card hover
15
+ textPrimary: "#EDF4F3",
16
+ // foam 16.84:1
17
+ textSecondary: "#A7BCC4",
18
+ // haze 9.50:1
19
+ textMuted: "#71919C",
20
+ // plankton 5.57:1 never under 13px
21
+ accent: "#35D6C0",
22
+ // biolume 10.31:1 interactive
23
+ accentHover: "#5FE3D1",
24
+ accentOn: "#06171A",
25
+ // ink over biolume
26
+ warm: "#F2A65A",
27
+ // sand 9.28:1 human and conversion
28
+ warmHover: "#F7BB7D",
29
+ warmOn: "#2A1605",
30
+ // ink over sand
31
+ success: "#4FB477",
32
+ warning: "#E8A33D",
33
+ error: "#E15757",
34
+ // 4.51:1 over surface
35
+ /**
36
+ * The destructive fill. It is NOT `error`, and the difference is the role.
37
+ *
38
+ * `error` is a TEXT color: it has to read against a dark surface, so it sits
39
+ * mid-red. `danger` is a FILL: what has to read is the ink ON TOP of it, so it
40
+ * goes lighter. Same reasoning as `accent` and `accentOn` — one is measured
41
+ * against the page, the other against what it carries.
42
+ *
43
+ * Measured: ink 6.53:1 over the fill and 7.92:1 over the hover; the fill
44
+ * itself 6.71:1 over abyss, 5.94 over surface and 4.91 over surfaceRaised, so
45
+ * the outline variant reads as text on all three.
46
+ */
47
+ danger: "#F4736B",
48
+ dangerHover: "#F78D86",
49
+ dangerOn: "#2B0A08"
50
+ // ink over danger 6.53:1
51
+ };
52
+ var light = {
53
+ background: "#F6F2EA",
54
+ // paper
55
+ surface: "#FFFFFF",
56
+ surfaceRaised: "#EFE9DE",
57
+ border: "#E6DFD2",
58
+ hairline: "#EBE6DC",
59
+ hairlineHover: "#D3C8B2",
60
+ // hairline on card hover
61
+ textPrimary: "#0B1524",
62
+ // 16.40:1
63
+ textSecondary: "#3D4B58",
64
+ textMuted: "#626A75",
65
+ // 4.53:1 over surfaceRaised
66
+ accent: "#0D7C6F",
67
+ // dark biolume 4.55:1
68
+ accentHover: "#0C7466",
69
+ accentOn: "#FFFFFF",
70
+ warm: "#A65B27",
71
+ // dark sand 4.54:1
72
+ warmHover: "#96511F",
73
+ warmOn: "#FFF7EE",
74
+ success: "#0F6B52",
75
+ warning: "#8D6111",
76
+ // 4.51:1 over surfaceRaised
77
+ error: "#C0392B",
78
+ /**
79
+ * In light mode `danger` and `error` land on the same hex, and that is not an
80
+ * oversight: over paper a red dark enough to carry white ink is also the red
81
+ * that reads as text. In dark they part company because the fill has to be
82
+ * lighter than the text color, not darker.
83
+ *
84
+ * Measured: ink 5.11:1 over the fill and 6.61:1 over the hover; the fill
85
+ * itself 4.87:1 over paper, 5.44 over surface and 4.50 over surfaceRaised —
86
+ * the last one is exactly on the AA line, which is where every light semantic
87
+ * in this palette sits.
88
+ */
89
+ danger: "#C0392B",
90
+ dangerHover: "#A32F22",
91
+ dangerOn: "#FFF6F4"
92
+ // 5.11:1 over danger
93
+ };
94
+ var colors = { dark, light };
95
+ var brand = {
96
+ /** The mascot's body. 4.22:1 → FILL ONLY, NEVER text. */
97
+ body: "#3E7CB1",
98
+ /** Spot pattern. */
99
+ spots: "#C2D7E7",
100
+ /** Hull · outline, and the background of code blocks. */
101
+ hull: "#0B1524"
102
+ };
103
+ var fonts = {
104
+ /** Headlines and large numbers ONLY. Never body copy. */
105
+ display: '"Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif',
106
+ /** Body and interface. */
107
+ sans: '"Geist", ui-sans-serif, system-ui, sans-serif',
108
+ /** Code, paths, labels, metadata, the CLI signature. */
109
+ mono: '"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace'
110
+ };
111
+ var typeScale = {
112
+ display: { family: "display", size: 76, lineHeight: 0.96, weight: 800, tracking: "-0.035em" },
113
+ /** Large metrics. Numbers, not prose: line height 1 and no descenders. */
114
+ stat: { family: "display", size: 46, lineHeight: 1, weight: 800, tracking: "-0.035em" },
115
+ h1: { family: "display", size: 44, lineHeight: 1.05, weight: 700, tracking: "-0.03em" },
116
+ h2: { family: "display", size: 30, lineHeight: 1.1, weight: 600, tracking: "-0.02em" },
117
+ h3: { family: "display", size: 25, lineHeight: 1.15, weight: 600, tracking: "-0.02em" },
118
+ body: { family: "sans", size: 18, lineHeight: 1.75, weight: 400 },
119
+ /**
120
+ * 17px. The large button, and the deck on interior pages.
121
+ *
122
+ * It earns its own step because the identity document uses it TWICE, in two
123
+ * different pieces: «lg 15/30 r12» with 17px text, and «h1 44/700 · deck 17px
124
+ * · context paragraph 15px». One use would not have justified splitting the
125
+ * gap between `ui` (15) and `body` (18); two did.
126
+ */
127
+ lead: { family: "sans", size: 17, lineHeight: 1.5, weight: 400 },
128
+ ui: { family: "sans", size: 15, lineHeight: 1.6, weight: 400 },
129
+ label: { family: "sans", size: 13, lineHeight: 1.5, weight: 500 },
130
+ /**
131
+ * The status-badge scale: sans 12.5/500, squared off.
132
+ *
133
+ * That is half a pixel below `label`, and this time the half pixel matters: a
134
+ * status badge sits inside a table or next to a title, and at 13 it competed
135
+ * with the text it accompanies. The document specifies 12.5 and it shows.
136
+ */
137
+ tag: { family: "sans", size: 12.5, lineHeight: 1.4, weight: 500 },
138
+ /**
139
+ * The category and metric scale: mono 11.5.
140
+ *
141
+ * It breaks the floor set by `limits.minScreenPx`, deliberately. That floor
142
+ * protects TEXT — what you read in a sentence — and a one-word pill is not
143
+ * running text: it is a mark. Contrast is still measured and still passes AA
144
+ * (plankton 5.57:1 over abyss), which is the part that is not negotiable.
145
+ *
146
+ * At 13 the three badge families grew past the size of a small button and
147
+ * outweighed the title they accompany. See `docs/decisions.md`.
148
+ */
149
+ chip: { family: "mono", size: 11.5, lineHeight: 1.4, weight: 400 },
150
+ /**
151
+ * Mono with NO transform: dates, paths, versions, file names, the footer
152
+ * signature and metric labels. It is most of the mono in the system.
153
+ *
154
+ * It exists because `eyebrow` ships `uppercase` and that is not a preference
155
+ * you can switch off: it IS the eyebrow scale. An `18 Aug 2026` in small caps
156
+ * or a `pose-laptop-coffee.png` in all caps are wrong, and without this step
157
+ * the only way out was a `normal-case` at every call site.
158
+ *
159
+ * The document says 12.5. This is 13 for the same reason `textMuted` was
160
+ * corrected: `limits.minScreenPx` is 13 and plankton is «never under 13px».
161
+ * This scale is exactly where muted metadata gets written, so putting it at
162
+ * 12.5 would have made its most common use illegible by half a pixel.
163
+ */
164
+ meta: { family: "mono", size: 13, lineHeight: 1.6, weight: 400 },
165
+ eyebrow: { family: "mono", size: 12, tracking: "0.12em", transform: "uppercase" }
166
+ };
167
+ var limits = {
168
+ /** Absolute minimum on screen. */
169
+ minScreenPx: 13,
170
+ /** Absolute minimum in print. */
171
+ minPrintPt: 12,
172
+ /** Maximum body measure. */
173
+ measure: "68ch"
174
+ };
175
+ var radius = {
176
+ chip: 6,
177
+ control: 10,
178
+ card: 14,
179
+ panel: 16,
180
+ pill: 999
181
+ };
182
+ var control = {
183
+ sm: 14,
184
+ md: 22,
185
+ lg: 30,
186
+ /** Icon button: square, no text. */
187
+ icon: 42,
188
+ /**
189
+ * The dense icon button, 32×32.
190
+ *
191
+ * 42 is the right measure for a control you hit with a thumb, and the four
192
+ * reading sites are where that fits. `cursos` is the odd one out: it is the
193
+ * only admin app of the set, with three actions per table row, and at 42 the
194
+ * row grows with them — the original used 24 and 28.
195
+ *
196
+ * It is 32 and not 28 because 32 is `sm`'s height. A dense icon button lines
197
+ * up with a small text button, so a toolbar mixing the two stays on one
198
+ * baseline; 28 would have been a fifth height that matches nothing.
199
+ *
200
+ * It does not replace `icon`. A page's primary action stays at 42; this is for
201
+ * a row of a table. See `docs/decisions.md` § 22.
202
+ */
203
+ iconSm: 32
204
+ };
205
+ var spacing = {
206
+ stepXs: 8,
207
+ stepSm: 12,
208
+ stepMd: 16,
209
+ stepLg: 26,
210
+ stepXl: 40,
211
+ section: 96
212
+ };
213
+ var size = {
214
+ /** Navigation bar height. */
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,
240
+ /** Reading column width. */
241
+ content: 760,
242
+ /** Maximum page width. */
243
+ wide: 1180
244
+ };
245
+ var syntax = {
246
+ /** The hull. It is the block background in both modes. */
247
+ background: brand.hull,
248
+ /** Identifiers, functions, types, variables. */
249
+ identifier: dark.textPrimary,
250
+ /** Strings, numbers, booleans, null. */
251
+ literal: dark.accent,
252
+ /** Keywords, control flow, `import`, `this`. */
253
+ keyword: dark.warm,
254
+ /** Comments and punctuation. */
255
+ comment: dark.textMuted,
256
+ /** `markup.deleted`, `invalid`. */
257
+ invalid: dark.error
258
+ };
259
+ var series = {
260
+ dark: [dark.accent, dark.warm, brand.body, dark.textMuted],
261
+ light: [light.accent, light.warm, brand.body, light.textMuted]
262
+ };
263
+ var deep = "#0D2129";
264
+ var gradient = {
265
+ dark: {
266
+ hero: `linear-gradient(160deg, ${dark.background} 60%, ${deep} 100%)`,
267
+ section: `linear-gradient(150deg, ${dark.surface} 0%, ${deep} 100%)`,
268
+ /** The one used by the article OG template. 145°, from the document. */
269
+ og: `linear-gradient(145deg, ${dark.background} 55%, ${deep} 100%)`
270
+ },
271
+ light: {
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%)`
275
+ }
276
+ };
277
+ var shadow = {
278
+ standard: "0 1px 2px rgba(0, 0, 0, 0.35)"
279
+ };
280
+ var motion = {
281
+ duration: "150ms",
282
+ easing: "ease-out",
283
+ properties: "color, background-color, border-color, fill, stroke"
284
+ };
285
+ var tagline = {
286
+ /** Hero. */
287
+ long: "Ayudo a equipos de ingenier\xEDa a escalar con criterio",
288
+ /** Header, one line. */
289
+ short: "Ayudo a equipos a escalar con criterio",
290
+ /** LinkedIn. */
291
+ en: "Helping engineering teams scale with judgment"
292
+ };
293
+ var naming = {
294
+ wordmark: "Eduardo \xC1lvarez",
295
+ mascot: "Tiburonc\xEDn",
296
+ /** The domain, for the footer CLI signature: `$ cd ~/eduardoalvarez.dev/2026`. */
297
+ domain: "eduardoalvarez.dev"
298
+ };
299
+
300
+ export { brand, colors, control, dark, fonts, gradient, light, limits, motion, naming, radius, series, shadow, size, spacing, syntax, tagline, typeScale };
@@ -1,4 +1,4 @@
1
- import { naming, tagline, motion, shadow, series, sintaxis, gradient, size, spacing, control, radius, limits, typeScale, fonts, brand, colors } from './chunk-TSPJOM6K.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 = {
@@ -12,7 +12,7 @@ var tokens = {
12
12
  spacing,
13
13
  size,
14
14
  gradient,
15
- sintaxis,
15
+ syntax,
16
16
  series,
17
17
  shadow,
18
18
  motion,
@@ -0,0 +1,42 @@
1
+ import { text } from './chunk-ODBFN44D.js';
2
+ import { cn } from './chunk-2WPWEIMD.js';
3
+ import { Slot } from '@radix-ui/react-slot';
4
+ import 'class-variance-authority';
5
+ import { jsx } from 'react/jsx-runtime';
6
+
7
+ var LABEL = {
8
+ display: "h1",
9
+ stat: "p",
10
+ h1: "h1",
11
+ h2: "h2",
12
+ h3: "h3",
13
+ body: "p",
14
+ lead: "p",
15
+ ui: "p",
16
+ label: "span",
17
+ tag: "span",
18
+ meta: "p",
19
+ chip: "span",
20
+ eyebrow: "p"
21
+ };
22
+ function Text({
23
+ className,
24
+ variant,
25
+ tone,
26
+ as,
27
+ asChild = false,
28
+ measure,
29
+ children,
30
+ ...props
31
+ }) {
32
+ const scale = variant ?? "body";
33
+ const clamp = measure ?? scale === "body";
34
+ const classes = cn(text({ variant, tone }), clamp && "max-w-measure", className);
35
+ if (asChild) {
36
+ return /* @__PURE__ */ jsx(Slot, { className: classes, ...props, children });
37
+ }
38
+ const Label = as ?? LABEL[scale];
39
+ return /* @__PURE__ */ jsx(Label, { className: classes, ...props, children });
40
+ }
41
+
42
+ export { Text };
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `npx arrecife doctor` — the two failures a consuming project cannot see.
4
+ *
5
+ * Both were found by hand, in the same migration, and both cost hours because
6
+ * neither produces an error. This is the check the backlog asked for.
7
+ *
8
+ * ONE · THE MISSING `@source`. Tailwind only generates the classes it can see,
9
+ * and it does not look inside `node_modules`. Without the directive every class
10
+ * the components emit is purged — `p-step-lg`, `rounded-card`, `border-hairline`
11
+ * — and they render with no padding, no radius and no border. There is no
12
+ * console error and no undefined class: the component mounts, simply undressed.
13
+ * It is in the README and it was still forgotten, because until a project uses a
14
+ * component and not just a token it does not need the line.
15
+ *
16
+ * TWO · THE TOKEN NAMESPACE COLLIDING WITH shadcn's. A project coming from
17
+ * shadcn has `@theme inline { --color-accent: var(--accent); }`, and shadcn's
18
+ * `--accent` is the hover SURFACE, `#17303E`, while this library's is the brand
19
+ * turquoise, `#35D6C0`. The result was **88 classes inside the library's own
20
+ * components** painting grey — 28 `text-accent`, 26 `outline-accent`, 15
21
+ * `bg-accent`, 12 `border-accent`. Buttons, focus rings and badges came out the
22
+ * colour of a surface, and it looked as though the migration had done nothing.
23
+ *
24
+ * Five names collide in total. Four are harmless because the two sides happen to
25
+ * agree on the value, which is why the check reports the value on each side
26
+ * instead of just the name: a collision that agrees is worth knowing about and
27
+ * is not worth failing over.
28
+ *
29
+ * WHY A COMMAND AND NOT A README PARAGRAPH. Both of these WERE a README
30
+ * paragraph. The whole reason this library exists is that a rule which is only
31
+ * written down drifts, and a rule that fails silently drifts fastest.
32
+ */
33
+ import { readdir, readFile } from 'node:fs/promises';
34
+ import { dirname, join, relative, resolve } from 'node:path';
35
+ import { fileURLToPath } from 'node:url';
36
+
37
+ const PACKAGE = '@eduardoalvarez/arrecife';
38
+ const here = dirname(fileURLToPath(import.meta.url));
39
+ const cwd = process.cwd();
40
+
41
+ const SKIP = new Set(['node_modules', '.git', '.next', 'dist', 'build', 'out', '.astro', 'coverage']);
42
+
43
+ async function* css(dir) {
44
+ let items;
45
+ try {
46
+ items = await readdir(dir, { withFileTypes: true });
47
+ } catch {
48
+ return;
49
+ }
50
+ for (const item of items) {
51
+ if (item.name.startsWith('.') && item.name !== '.') continue;
52
+ const path = join(dir, item.name);
53
+ if (item.isDirectory()) {
54
+ if (SKIP.has(item.name)) continue;
55
+ yield* css(path);
56
+ } else if (item.name.endsWith('.css')) {
57
+ yield path;
58
+ }
59
+ }
60
+ }
61
+
62
+ /** `--name: value;` pairs inside every `@theme` block of a stylesheet. */
63
+ function themeProperties(source) {
64
+ const found = new Map();
65
+ // `@theme`, `@theme inline`, `@theme static` — the modifier does not matter
66
+ // for a name collision, only the name does.
67
+ const blocks = source.matchAll(/@theme[^{]*\{([\s\S]*?)\n\}/g);
68
+ for (const [, body] of blocks) {
69
+ for (const [, name, value] of body.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)) {
70
+ found.set(name, value.trim());
71
+ }
72
+ }
73
+ return found;
74
+ }
75
+
76
+ /** Our own published properties, read from the package rather than hardcoded. */
77
+ async function ours() {
78
+ for (const candidate of [
79
+ resolve(here, 'tokens/theme.css'), // published: dist/doctor.mjs beside dist/tokens/
80
+ resolve(here, '../dist/tokens/theme.css'), // in this repo, run from scripts/
81
+ ]) {
82
+ try {
83
+ return themeProperties(await readFile(candidate, 'utf8'));
84
+ } catch {
85
+ /* try the next one */
86
+ }
87
+ }
88
+ return new Map();
89
+ }
90
+
91
+ const problems = [];
92
+ const notes = [];
93
+
94
+ const theirs = await ours();
95
+ if (theirs.size === 0) {
96
+ console.error(`arrecife · could not read ${PACKAGE}'s own theme.css. Run \`pnpm build\` first.`);
97
+ process.exit(1);
98
+ }
99
+
100
+ let entries = 0;
101
+
102
+ for await (const file of css(cwd)) {
103
+ const source = await readFile(file, 'utf8');
104
+ const shown = relative(cwd, file);
105
+
106
+ // The entry sheet is the one that imports Tailwind. A partial that only holds
107
+ // an `@theme` block is checked for collisions all the same.
108
+ const isEntry = /@import\s+["']tailwindcss["']/.test(source);
109
+ const importsTokens = source.includes(`${PACKAGE}/tokens/theme.css`);
110
+ const hasSource = /@source\s+["'][^"']*@eduardoalvarez\/arrecife/.test(source);
111
+
112
+ if (isEntry) entries += 1;
113
+
114
+ if (isEntry && importsTokens && !hasSource) {
115
+ // The path is relative to the SHEET, not to the project root, which is the
116
+ // part that gets written wrong: a sheet in `src/styles/` goes up three.
117
+ const target = relative(dirname(file), join(cwd, 'node_modules', PACKAGE, 'dist'));
118
+ problems.push(
119
+ `${shown}\n` +
120
+ ` imports ${PACKAGE}/tokens/theme.css and has no @source.\n` +
121
+ ` Every class the components emit is being purged — silently. Add:\n\n` +
122
+ ` @source "${target.split('\\\\').join('/')}";\n`,
123
+ );
124
+ }
125
+
126
+ for (const [name, value] of themeProperties(source)) {
127
+ const mine = theirs.get(name);
128
+ if (mine === undefined) continue;
129
+
130
+ // A value that points at another property cannot be compared here, and it is
131
+ // exactly the shape the shadcn collision takes.
132
+ const indirect = value.includes('var(');
133
+ const same = !indirect && value.toLowerCase() === mine.toLowerCase();
134
+
135
+ if (same) {
136
+ notes.push(`${shown} redefines ${name}, with the same value (${value}). Harmless.`);
137
+ continue;
138
+ }
139
+
140
+ problems.push(
141
+ `${shown}\n` +
142
+ ` redefines ${name}, which ${PACKAGE} owns.\n` +
143
+ ` yours: ${value}${indirect ? ' ← points at another property, so it wins silently' : ''}\n` +
144
+ ` arrecife: ${mine}\n` +
145
+ ` Every class in the library that uses it takes YOUR value, including the\n` +
146
+ ` ones inside the library's own components.\n`,
147
+ );
148
+ }
149
+ }
150
+
151
+ if (entries === 0) {
152
+ console.log('arrecife · no stylesheet importing tailwindcss was found. Nothing to check.');
153
+ process.exit(0);
154
+ }
155
+
156
+ for (const note of notes) console.log(`arrecife · note · ${note}`);
157
+
158
+ if (problems.length > 0) {
159
+ console.error(`\narrecife · ${problems.length} thing(s) that fail without saying so:\n`);
160
+ for (const problem of problems) console.error(` ${problem}`);
161
+ process.exit(1);
162
+ }
163
+
164
+ console.log(
165
+ `arrecife · ${entries} stylesheet(s) checked · @source is in place and no token is redefined`,
166
+ );