@supertype.ai/foundations 0.1.35 → 0.1.36
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.
- package/README.md +42 -8
- package/bin/foundations.mjs +294 -58
- package/llms.txt +7 -5
- package/package.json +5 -2
- package/src/index.css +37 -0
package/README.md
CHANGED
|
@@ -55,7 +55,15 @@ yarn add @supertype.ai/foundations
|
|
|
55
55
|
# or: npm install @supertype.ai/foundations
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
Peers are React 19+, Next 15+, `next-view-transitions` 0.3+ and
|
|
58
|
+
Peers are **Tailwind 4+**, React 19+, Next 15+, `next-view-transitions` 0.3+ and
|
|
59
|
+
`@base-ui/react` 1.4+.
|
|
60
|
+
|
|
61
|
+
**Tailwind v4 is required, not preferred.** `tokens.css` declares
|
|
62
|
+
`@custom-variant` and `@theme inline`, and the `@source` line below is v4-only
|
|
63
|
+
syntax; on v3 they are parse errors. If you are still on v3, run
|
|
64
|
+
[`npx @tailwindcss/upgrade`](https://tailwindcss.com/docs/upgrade-guide) first —
|
|
65
|
+
`foundations init` will tell you so rather than writing a block your build
|
|
66
|
+
cannot parse.
|
|
59
67
|
|
|
60
68
|
<details>
|
|
61
69
|
<summary>Installing from a git tag instead</summary>
|
|
@@ -66,28 +74,52 @@ untagged git dependency resolves to a different commit on a fresh install.
|
|
|
66
74
|
|
|
67
75
|
```jsonc
|
|
68
76
|
// package.json
|
|
69
|
-
"@supertype.ai/foundations": "https://github.com/supertypeai/foundations.git#v0.1.
|
|
77
|
+
"@supertype.ai/foundations": "https://github.com/supertypeai/foundations.git#v0.1.36"
|
|
70
78
|
```
|
|
71
79
|
|
|
72
80
|
</details>
|
|
73
81
|
|
|
74
|
-
### 2. Import the CSS
|
|
82
|
+
### 2. Import the CSS
|
|
83
|
+
|
|
84
|
+
```css
|
|
85
|
+
/* app/globals.css */
|
|
86
|
+
@import "tailwindcss";
|
|
87
|
+
@import "@supertype.ai/foundations";
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
That one line carries `tokens.css`, `theme.css`, `type.css` and `prose.css` in
|
|
91
|
+
the order the cascade needs, and registers the package’s own `@source` so
|
|
92
|
+
Tailwind scans the components it ships. There is no path for you to work out and
|
|
93
|
+
no order for you to keep: Tailwind v4 resolves `@source` relative to the file
|
|
94
|
+
that declares it, so the package points at its own `dist/`, correctly, wherever
|
|
95
|
+
it happens to be installed.
|
|
96
|
+
|
|
97
|
+
Add `@import "@supertype.ai/foundations/shiki.css";` after it only if you render
|
|
98
|
+
code fences.
|
|
99
|
+
|
|
100
|
+
<details>
|
|
101
|
+
<summary>Importing the parts separately</summary>
|
|
102
|
+
|
|
103
|
+
The granular entry points are still exported and still supported, for the app
|
|
104
|
+
that paints every colour role itself and wants `tokens.css` without `theme.css`.
|
|
105
|
+
Taking them means owning the order and the scan path yourself:
|
|
75
106
|
|
|
76
107
|
```css
|
|
77
|
-
/* app/
|
|
108
|
+
/* app/globals.css */
|
|
78
109
|
@import "tailwindcss";
|
|
79
110
|
@import "@supertype.ai/foundations/tokens.css"; /* structural tokens + dark variant */
|
|
80
111
|
@import "@supertype.ai/foundations/theme.css"; /* the house palette */
|
|
81
112
|
@import "@supertype.ai/foundations/type.css"; /* the type ramp + font roles */
|
|
82
113
|
@import "@supertype.ai/foundations/prose.css"; /* inline-code rule */
|
|
83
|
-
@import "@supertype.ai/foundations/shiki.css"; /* only if you render code fences */
|
|
84
114
|
|
|
85
115
|
@source '../node_modules/@supertype.ai/foundations/dist/**/*.js';
|
|
86
116
|
```
|
|
87
117
|
|
|
88
|
-
**The `@source` line is required.** Tailwind does not scan
|
|
89
|
-
default, so without it the package’s classes get purged
|
|
90
|
-
components render without styles.
|
|
118
|
+
**The `@source` line is required in this form.** Tailwind does not scan
|
|
119
|
+
`node_modules` by default, so without it the package’s classes get purged
|
|
120
|
+
and the components render without styles. The path is relative to your CSS file,
|
|
121
|
+
so it changes with your layout — and in a workspace, where the package hoists to
|
|
122
|
+
the repo root, `../node_modules` is not where it lives.
|
|
91
123
|
|
|
92
124
|
**`theme.css` is required.** `tokens.css` names the colour roles, and
|
|
93
125
|
`theme.css` gives them values. Without it, the colour utilities cannot be
|
|
@@ -97,6 +129,8 @@ for marker highlights, and the `accordion-down` and `accordion-up` keyframes.
|
|
|
97
129
|
Skip it only if you declare every role yourself; `foundations doctor` fails if
|
|
98
130
|
neither path is true.
|
|
99
131
|
|
|
132
|
+
</details>
|
|
133
|
+
|
|
100
134
|
### 3. Bind the fonts
|
|
101
135
|
|
|
102
136
|
The package cannot load the typefaces for you. `next/font` runs in your app and
|
package/bin/foundations.mjs
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* that changes in the package changes here on the next release.
|
|
23
23
|
*/
|
|
24
24
|
import { existsSync, lstatSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
25
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
25
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
26
26
|
import { fileURLToPath } from "node:url";
|
|
27
27
|
|
|
28
28
|
const PKG_NAME = "@supertype.ai/foundations";
|
|
@@ -80,11 +80,19 @@ const readJson = (file) => {
|
|
|
80
80
|
}
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Directories that never hold an app's own source: dependencies, and the build
|
|
85
|
+
* output that mirrors it. Walking from the app root rather than a list of
|
|
86
|
+
* blessed directories means these have to be named, but naming what is not
|
|
87
|
+
* source is a much shorter and much more stable list than guessing what is.
|
|
88
|
+
*/
|
|
89
|
+
const NOT_SOURCE = new Set(["node_modules", "dist", "build", "out", "coverage", "public", "vendor", "target"]);
|
|
90
|
+
|
|
83
91
|
/** Files under `dir` with one of `exts`, depth-limited and blind to build output. */
|
|
84
92
|
const walk = (dir, exts, depth = 4, out = []) => {
|
|
85
93
|
if (depth < 0 || !existsSync(dir)) return out;
|
|
86
94
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
87
|
-
if (entry.name.startsWith(".") || entry.name
|
|
95
|
+
if (entry.name.startsWith(".") || NOT_SOURCE.has(entry.name)) continue;
|
|
88
96
|
const full = join(dir, entry.name);
|
|
89
97
|
if (entry.isDirectory()) walk(full, exts, depth - 1, out);
|
|
90
98
|
else if (exts.some((ext) => entry.name.endsWith(ext))) out.push(full);
|
|
@@ -120,15 +128,94 @@ const findAppRoot = (from) => {
|
|
|
120
128
|
}
|
|
121
129
|
};
|
|
122
130
|
|
|
131
|
+
/**
|
|
132
|
+
* `name` as Node would resolve it from `dir`: the first `node_modules` on the
|
|
133
|
+
* way up that actually holds it. A workspace hoists dependencies to the repo
|
|
134
|
+
* root, so the copy an app runs against is routinely nowhere near its own
|
|
135
|
+
* package.json, and assuming otherwise is how a check ends up reporting a
|
|
136
|
+
* missing peer that is installed and an `@source` path that points at nothing.
|
|
137
|
+
*/
|
|
138
|
+
const resolveDep = (dir, name) => {
|
|
139
|
+
const segments = name.split("/");
|
|
140
|
+
let at = resolve(dir);
|
|
141
|
+
for (;;) {
|
|
142
|
+
const candidate = join(at, "node_modules", ...segments);
|
|
143
|
+
if (existsSync(candidate)) return candidate;
|
|
144
|
+
const up = dirname(at);
|
|
145
|
+
if (up === at) return null;
|
|
146
|
+
at = up;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The installed Tailwind's major version, or null when it is not installed yet
|
|
152
|
+
* — which is the normal case for `npx … init` run before the first install, and
|
|
153
|
+
* not something to fail on.
|
|
154
|
+
*
|
|
155
|
+
* This is read from the installed package rather than inferred from which
|
|
156
|
+
* directives the app's CSS uses, because the two questions are genuinely
|
|
157
|
+
* separate: `@tailwind utilities;` is still legal in v4, and a v3 app being
|
|
158
|
+
* upgraded passes through states where its CSS and its lockfile disagree.
|
|
159
|
+
*/
|
|
160
|
+
const tailwindMajor = (appRoot) => {
|
|
161
|
+
const at = resolveDep(appRoot, "tailwindcss");
|
|
162
|
+
const meta = at ? readJson(join(at, "package.json")) : null;
|
|
163
|
+
return meta ? (versionParts(meta.version)?.[0] ?? null) : null;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The dialect a stylesheet is written in, for when the installed copy cannot
|
|
168
|
+
* answer — an app running `npx … init` before its first install. v4 imports the
|
|
169
|
+
* framework; v3 uses `@tailwind base`/`components`, which v4 removed.
|
|
170
|
+
* `@tailwind utilities` is deliberately not listed: it is still legal in v4, so
|
|
171
|
+
* it is not evidence of anything.
|
|
172
|
+
*/
|
|
173
|
+
const dialectOf = (css) => {
|
|
174
|
+
if (/@import\s+["']tailwindcss(?:\/[\w./-]+)?["']/.test(css)) return 4;
|
|
175
|
+
if (/@tailwind\s+(?:base|components)\b/.test(css)) return 3;
|
|
176
|
+
return null;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Which Tailwind this app is on, best evidence first: the installed copy, then
|
|
181
|
+
* the dialect of its entry stylesheet. Null when neither can say, which is a
|
|
182
|
+
* normal state and not a failure — a fresh app has no CSS and no install yet.
|
|
183
|
+
*/
|
|
184
|
+
const tailwindOf = (appRoot, cssFile) =>
|
|
185
|
+
tailwindMajor(appRoot) ?? (cssFile ? dialectOf(stripComments(read(cssFile) ?? "")) : null);
|
|
186
|
+
|
|
187
|
+
/** Everything the style layer does is v4 syntax, so this is the one way out. */
|
|
188
|
+
const TAILWIND_UPGRADE =
|
|
189
|
+
"Upgrade first: `npx @tailwindcss/upgrade` — https://tailwindcss.com/docs/upgrade-guide";
|
|
190
|
+
|
|
191
|
+
/** Why v4 is not negotiable, said the same way wherever it comes up. */
|
|
192
|
+
const TAILWIND_WHY =
|
|
193
|
+
"tokens.css declares @custom-variant and @theme inline, and the @source line is v4-only syntax.";
|
|
194
|
+
|
|
123
195
|
/* -------------------------------------------------- what the package wants */
|
|
124
196
|
|
|
125
197
|
/**
|
|
126
|
-
* The
|
|
127
|
-
*
|
|
128
|
-
* the
|
|
198
|
+
* The one import that carries the whole style layer. index.css registers the
|
|
199
|
+
* package's own `@source` and imports its own files in the one order the
|
|
200
|
+
* cascade accepts, so an app taking this single line cannot get the order, the
|
|
201
|
+
* path, or the completeness wrong — there is nothing left for it to get wrong.
|
|
202
|
+
*/
|
|
203
|
+
const BUNDLE = PKG_NAME;
|
|
204
|
+
|
|
205
|
+
/** Either spelling: the bare specifier, or the file named outright. */
|
|
206
|
+
const isBundle = (spec) => spec === PKG_NAME || spec === `${PKG_NAME}/index.css`;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The granular entry points, in the order they have to be imported: tokens,
|
|
210
|
+
* then theme, then type. `exports` is already in that order, which is also the
|
|
211
|
+
* order the README documented before the bundle existed.
|
|
212
|
+
*
|
|
213
|
+
* They remain exported, and remain supported, for the app that paints every
|
|
214
|
+
* colour role itself and wants tokens.css without theme.css. index.css is not
|
|
215
|
+
* one of them: it is the whole, not a part.
|
|
129
216
|
*/
|
|
130
217
|
const cssEntries = Object.keys(pkgJson.exports)
|
|
131
|
-
.filter((key) => key.endsWith(".css"))
|
|
218
|
+
.filter((key) => key.endsWith(".css") && key !== "./index.css")
|
|
132
219
|
.map((key) => `${PKG_NAME}/${key.slice(2)}`);
|
|
133
220
|
|
|
134
221
|
/** Only needed if the app renders code fences. */
|
|
@@ -164,11 +251,47 @@ const fontVars = (() => {
|
|
|
164
251
|
|
|
165
252
|
/* -------------------------------------------------------- consumer probing */
|
|
166
253
|
|
|
167
|
-
/**
|
|
254
|
+
/** What Tailwind is processed from. v4 drops the preprocessors; v3 apps use them. */
|
|
255
|
+
const STYLE_EXTS = [".css", ".pcss", ".postcss", ".scss"];
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* A file that starts a Tailwind cascade, in either dialect: v4 pulls the
|
|
259
|
+
* framework in with `@import` (including the layered form, where the three
|
|
260
|
+
* layers are imported separately), v3 with `@tailwind`. Both name the same
|
|
261
|
+
* file — the one the app's styles begin at — so finding it does not depend on
|
|
262
|
+
* which version is installed, and must not, or an app on the wrong version
|
|
263
|
+
* gets told it has no stylesheet at all.
|
|
264
|
+
*/
|
|
265
|
+
const TAILWIND_ENTRY =
|
|
266
|
+
/@import\s+["']tailwindcss(?:\/[\w./-]+)?["']|@tailwind\s+(?:base|components|utilities)\b/;
|
|
267
|
+
|
|
268
|
+
/** Conventional entry names, best first. Only ever used to break a tie. */
|
|
269
|
+
const ENTRY_NAMES = ["globals", "global", "index", "main", "app", "styles", "tailwind"];
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The CSS file that starts the cascade. Searched from the app root rather than
|
|
273
|
+
* a list of blessed directories, because `app/`, `src/` and `styles/` are three
|
|
274
|
+
* of the places it lives and not the only three.
|
|
275
|
+
*
|
|
276
|
+
* More than one file can import Tailwind — an embed, a Storybook preview, an
|
|
277
|
+
* email template. The real entry is the shallowest, and among equals the one
|
|
278
|
+
* named the way the scaffolds name it.
|
|
279
|
+
*/
|
|
168
280
|
const findCssEntry = (appRoot) => {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
281
|
+
const candidates = walk(appRoot, STYLE_EXTS, 6).filter((file) =>
|
|
282
|
+
TAILWIND_ENTRY.test(stripComments(read(file) ?? "")),
|
|
283
|
+
);
|
|
284
|
+
const rank = (file) => {
|
|
285
|
+
const rel = relative(appRoot, file);
|
|
286
|
+
const at = ENTRY_NAMES.indexOf(basename(rel).replace(/\.[^.]+$/, ""));
|
|
287
|
+
return [rel.split(sep).length, at === -1 ? ENTRY_NAMES.length : at, rel];
|
|
288
|
+
};
|
|
289
|
+
return (
|
|
290
|
+
candidates
|
|
291
|
+
.map((file) => [rank(file), file])
|
|
292
|
+
.sort(([a], [b]) => (a[0] - b[0]) || (a[1] - b[1]) || (a[2] < b[2] ? -1 : 1))
|
|
293
|
+
.map(([, file]) => file)[0] ?? null
|
|
294
|
+
);
|
|
172
295
|
};
|
|
173
296
|
|
|
174
297
|
const LAYOUTS = ["app/layout.tsx", "src/app/layout.tsx", "app/layout.jsx", "src/app/layout.jsx"];
|
|
@@ -213,6 +336,13 @@ const stripComments = (css) => {
|
|
|
213
336
|
/** `@import` targets in source order, so one parse gives presence and ordering. */
|
|
214
337
|
const importsOf = (css) => [...css.matchAll(/@import\s+["']([^"']+)["']/g)].map((m) => m[1]);
|
|
215
338
|
|
|
339
|
+
/**
|
|
340
|
+
* An import of the framework itself. v4's layered form names three files rather
|
|
341
|
+
* than one, and the block has to land after the last of them, so anywhere the
|
|
342
|
+
* code asks "is this Tailwind coming in here" has to accept both spellings.
|
|
343
|
+
*/
|
|
344
|
+
const isTailwindImport = (spec) => spec === "tailwindcss" || spec.startsWith("tailwindcss/");
|
|
345
|
+
|
|
216
346
|
const sourceDirectives = (css) => [...css.matchAll(/@source\s+["']([^"']+)["']/g)].map((m) => m[1]);
|
|
217
347
|
|
|
218
348
|
/** Consts assigned from a next/font call, so we can check how they are used. */
|
|
@@ -234,7 +364,11 @@ const fontConsts = (src) => {
|
|
|
234
364
|
|
|
235
365
|
/** The @source line for this app, relative to the CSS file wherever yarn put us. */
|
|
236
366
|
const sourceGlob = (appRoot, cssFile) => {
|
|
237
|
-
|
|
367
|
+
// Where it actually is, which in a workspace is the repo root rather than
|
|
368
|
+
// this app. The conventional path is the fallback for an app that has not
|
|
369
|
+
// installed the package yet, where there is nothing to resolve.
|
|
370
|
+
const installed =
|
|
371
|
+
resolveDep(appRoot, PKG_NAME) ?? join(appRoot, "node_modules", ...PKG_NAME.split("/"));
|
|
238
372
|
let path = relative(dirname(cssFile), join(installed, "dist"));
|
|
239
373
|
path = path.split(sep).join("/");
|
|
240
374
|
if (!path.startsWith(".")) path = `./${path}`;
|
|
@@ -255,7 +389,7 @@ const isGitSpec = (spec) =>
|
|
|
255
389
|
const checkInstall = (appRoot) => {
|
|
256
390
|
const out = [];
|
|
257
391
|
const appPkg = readJson(join(appRoot, "package.json"));
|
|
258
|
-
const installed =
|
|
392
|
+
const installed = resolveDep(appRoot, PKG_NAME);
|
|
259
393
|
|
|
260
394
|
const spec = appPkg?.dependencies?.[PKG_NAME] ?? appPkg?.devDependencies?.[PKG_NAME] ?? null;
|
|
261
395
|
|
|
@@ -298,7 +432,7 @@ const checkInstall = (appRoot) => {
|
|
|
298
432
|
}
|
|
299
433
|
}
|
|
300
434
|
|
|
301
|
-
if (
|
|
435
|
+
if (installed) {
|
|
302
436
|
if (lstatSync(installed).isSymbolicLink()) {
|
|
303
437
|
out.push(
|
|
304
438
|
finding(
|
|
@@ -328,7 +462,8 @@ const checkInstall = (appRoot) => {
|
|
|
328
462
|
|
|
329
463
|
for (const [peer, range] of Object.entries(pkgJson.peerDependencies ?? {})) {
|
|
330
464
|
const soft = peer === "@base-ui/react"; // Only Accordion and Tabs need it.
|
|
331
|
-
const
|
|
465
|
+
const at = resolveDep(appRoot, peer);
|
|
466
|
+
const meta = at ? readJson(join(at, "package.json")) : null;
|
|
332
467
|
if (!meta) {
|
|
333
468
|
out.push(
|
|
334
469
|
finding(
|
|
@@ -345,25 +480,80 @@ const checkInstall = (appRoot) => {
|
|
|
345
480
|
return out;
|
|
346
481
|
};
|
|
347
482
|
|
|
348
|
-
const checkStyles = (appRoot) => {
|
|
483
|
+
const checkStyles = (appRoot, major) => {
|
|
349
484
|
const out = [];
|
|
350
485
|
const cssFile = findCssEntry(appRoot);
|
|
351
486
|
if (!cssFile) {
|
|
352
487
|
out.push(
|
|
353
|
-
finding(
|
|
488
|
+
finding(
|
|
489
|
+
"error",
|
|
490
|
+
"no CSS entry importing Tailwind",
|
|
491
|
+
`looked for a ${STYLE_EXTS.join(", ")} file under ${appRoot}`,
|
|
492
|
+
"Create one (app/globals.css), then run `foundations init`.",
|
|
493
|
+
),
|
|
354
494
|
);
|
|
355
495
|
return out;
|
|
356
496
|
}
|
|
357
497
|
|
|
358
498
|
const css = stripComments(read(cssFile) ?? "");
|
|
359
499
|
const rel = relative(appRoot, cssFile);
|
|
500
|
+
|
|
501
|
+
// Every check below reads a v4 cascade. Running them against a v3 stylesheet
|
|
502
|
+
// reports four separate failures that are all one cause, and buries it.
|
|
503
|
+
if (major !== null && major < 4) {
|
|
504
|
+
out.push(
|
|
505
|
+
finding(
|
|
506
|
+
"error",
|
|
507
|
+
`${rel} is a Tailwind v${major} stylesheet, and the package needs v4`,
|
|
508
|
+
TAILWIND_WHY,
|
|
509
|
+
TAILWIND_UPGRADE,
|
|
510
|
+
),
|
|
511
|
+
);
|
|
512
|
+
return out;
|
|
513
|
+
}
|
|
360
514
|
const order = importsOf(css);
|
|
515
|
+
const tailwindAt = order.reduce((at, spec, i) => (isTailwindImport(spec) ? i : at), -1);
|
|
516
|
+
|
|
517
|
+
// Applies to either shape, so it is asked before they diverge.
|
|
518
|
+
if (/@custom-variant\s+dark/.test(css)) {
|
|
519
|
+
out.push(
|
|
520
|
+
finding(
|
|
521
|
+
"warn",
|
|
522
|
+
"this app declares its own dark variant",
|
|
523
|
+
rel,
|
|
524
|
+
"tokens.css already binds dark: to the .dark class. With two declarations the later one wins, and nothing tells you which.",
|
|
525
|
+
),
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// The bundle carries the @source line and fixes the order inside the package,
|
|
530
|
+
// so the only thing an app can still get wrong is putting it before Tailwind.
|
|
531
|
+
const bundleAt = order.findIndex(isBundle);
|
|
532
|
+
if (bundleAt !== -1) {
|
|
533
|
+
out.push(
|
|
534
|
+
bundleAt < tailwindAt
|
|
535
|
+
? finding(
|
|
536
|
+
"error",
|
|
537
|
+
`${BUNDLE} is imported before Tailwind`,
|
|
538
|
+
rel,
|
|
539
|
+
"It re-points variables Tailwind defines, so it has to come after.",
|
|
540
|
+
)
|
|
541
|
+
: finding("ok", "the style layer is complete", `${rel} → ${BUNDLE}`),
|
|
542
|
+
);
|
|
543
|
+
if (!order.some((spec) => OPTIONAL_CSS.has(spec))) {
|
|
544
|
+
out.push(
|
|
545
|
+
finding("info", `${[...OPTIONAL_CSS][0]} is not imported`, "only needed if you render code fences", "Add it when you add Shiki."),
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
|
|
361
551
|
const seen = new Map(order.map((spec, i) => [spec, i]));
|
|
362
552
|
|
|
363
553
|
const missing = [];
|
|
364
554
|
/** Set when theme.css is absent but the app declares every role itself. */
|
|
365
555
|
let selfPainted = false;
|
|
366
|
-
let last =
|
|
556
|
+
let last = tailwindAt;
|
|
367
557
|
for (const entry of cssEntries) {
|
|
368
558
|
const at = seen.get(entry);
|
|
369
559
|
if (at === undefined) {
|
|
@@ -423,17 +613,6 @@ const checkStyles = (appRoot) => {
|
|
|
423
613
|
out.push(finding("ok", "@source scans the package", ours));
|
|
424
614
|
}
|
|
425
615
|
|
|
426
|
-
if (/@custom-variant\s+dark/.test(css)) {
|
|
427
|
-
out.push(
|
|
428
|
-
finding(
|
|
429
|
-
"warn",
|
|
430
|
-
"this app declares its own dark variant",
|
|
431
|
-
rel,
|
|
432
|
-
"tokens.css already binds dark: to the .dark class. With two declarations the later one wins, and nothing tells you which.",
|
|
433
|
-
),
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
616
|
const required = missing.filter(
|
|
438
617
|
(entry) => !OPTIONAL_CSS.has(entry) && !(entry === PALETTE_CSS && selfPainted),
|
|
439
618
|
);
|
|
@@ -500,8 +679,16 @@ const expandImports = (file, { includePackage }, seen = new Set()) => {
|
|
|
500
679
|
if (seen.has(file)) return "";
|
|
501
680
|
seen.add(file);
|
|
502
681
|
return (read(file) ?? "").replace(/@import\s+["']([^"']+)["'];?/g, (_line, spec) => {
|
|
503
|
-
|
|
504
|
-
|
|
682
|
+
// The bundle is ours too, and it pulls the rest in relatively — so it has to
|
|
683
|
+
// be followed rather than read. Reading it would yield four @import lines
|
|
684
|
+
// and no palette, and every role would measure as unpainted.
|
|
685
|
+
const ours = isBundle(spec)
|
|
686
|
+
? "index.css"
|
|
687
|
+
: spec.startsWith(`${PKG_NAME}/`)
|
|
688
|
+
? spec.slice(PKG_NAME.length + 1)
|
|
689
|
+
: null;
|
|
690
|
+
if (ours !== null)
|
|
691
|
+
return includePackage ? expandImports(join(pkgRoot, "src", ours), { includePackage }, seen) : "";
|
|
505
692
|
if (spec.startsWith(".")) return expandImports(resolve(dirname(file), spec), { includePackage }, seen);
|
|
506
693
|
return "";
|
|
507
694
|
});
|
|
@@ -561,10 +748,14 @@ const serif = Average({ variable: "--font-average", weight: "400", subsets: ["la
|
|
|
561
748
|
<html className={\`\${sans.variable} \${mono.variable} \${serif.variable} font-sans\`}>`;
|
|
562
749
|
|
|
563
750
|
const doctor = async (appRoot) => {
|
|
751
|
+
// Asked once, because it decides what the rest of the report can mean.
|
|
752
|
+
const major = tailwindOf(appRoot, findCssEntry(appRoot));
|
|
564
753
|
const install = checkInstall(appRoot);
|
|
565
|
-
const styles = checkStyles(appRoot);
|
|
754
|
+
const styles = checkStyles(appRoot, major);
|
|
566
755
|
const fonts = checkFonts(appRoot);
|
|
567
|
-
|
|
756
|
+
// On v3 the cascade never assembles, so measuring the colours it did not
|
|
757
|
+
// produce would fill the report with failures that all have one cause.
|
|
758
|
+
const contrast = major !== null && major < 4 ? [] : await checkContrast(appRoot);
|
|
568
759
|
const all = [...install, ...styles, ...fonts, ...contrast];
|
|
569
760
|
|
|
570
761
|
console.log(`\n${bold(PKG_NAME)} ${dim(`doctor · ${appRoot}`)}`);
|
|
@@ -588,12 +779,27 @@ const doctor = async (appRoot) => {
|
|
|
588
779
|
|
|
589
780
|
const init = (appRoot, { dryRun }) => {
|
|
590
781
|
const cssFile = findCssEntry(appRoot);
|
|
782
|
+
const major = tailwindOf(appRoot, cssFile);
|
|
783
|
+
|
|
784
|
+
// Patching a v3 stylesheet would trade a working build for a pile of parse
|
|
785
|
+
// errors, so this stops at the diagnosis instead. Nothing is written.
|
|
786
|
+
if (major !== null && major < 4) {
|
|
787
|
+
console.log(`\n${paint(31, "✖")} this app is on ${bold(`Tailwind v${major}`)}, and the package needs v4.`);
|
|
788
|
+
if (cssFile) console.log(` ${dim(`${relative(appRoot, cssFile)} is the entry, and it is in the v3 dialect.`)}`);
|
|
789
|
+
console.log(` ${dim(TAILWIND_WHY)} ${dim("On v3 they are parse errors.")}`);
|
|
790
|
+
console.log(`\n ${dim("→")} ${TAILWIND_UPGRADE}`);
|
|
791
|
+
console.log(`\nThen run ${bold("foundations init")} again.\n`);
|
|
792
|
+
return 1;
|
|
793
|
+
}
|
|
794
|
+
|
|
591
795
|
if (!cssFile) {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
);
|
|
796
|
+
// Named where the scaffolds actually put it, so the instruction matches the
|
|
797
|
+
// file the reader is about to create.
|
|
798
|
+
const target = existsSync(join(appRoot, "src/app")) ? "src/app/globals.css" : "app/globals.css";
|
|
799
|
+
console.log(`\nNo CSS entry importing Tailwind under ${appRoot}.`);
|
|
800
|
+
console.log(dim(`Looked for a ${STYLE_EXTS.join(", ")} file that imports it.`));
|
|
801
|
+
console.log(`\nCreate ${bold(target)} with:\n`);
|
|
802
|
+
console.log(`@import "tailwindcss";\n@import "${BUNDLE}";`);
|
|
597
803
|
console.log();
|
|
598
804
|
return 1;
|
|
599
805
|
}
|
|
@@ -602,13 +808,52 @@ const init = (appRoot, { dryRun }) => {
|
|
|
602
808
|
const live = stripComments(before);
|
|
603
809
|
const rel = relative(appRoot, cssFile);
|
|
604
810
|
const lines = before.split("\n");
|
|
811
|
+
const present = new Set(importsOf(live));
|
|
605
812
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
813
|
+
/** After Tailwind itself: everything the package ships re-points its variables. */
|
|
814
|
+
const anchorIn = (from) =>
|
|
815
|
+
from.reduce((at, line, i) => (importsOf(stripComments(line)).some(isTailwindImport) ? i : at), -1);
|
|
816
|
+
|
|
817
|
+
if ([...present].some(isBundle)) {
|
|
818
|
+
console.log(`\n${rel} already imports the style layer.`);
|
|
819
|
+
} else if (!cssEntries.some((entry) => present.has(entry))) {
|
|
820
|
+
// Nothing of ours in the file yet, so it gets the single import. There is
|
|
821
|
+
// no order to arrange and no @source to compute: the package does both.
|
|
822
|
+
const line = `@import "${BUNDLE}";`;
|
|
823
|
+
lines.splice(anchorIn(lines) + 1, 0, line);
|
|
824
|
+
if (dryRun) {
|
|
825
|
+
console.log(`\n${bold(rel)} ${dim("(dry run — nothing written)")}`);
|
|
826
|
+
} else {
|
|
827
|
+
writeFileSync(cssFile, lines.join("\n"));
|
|
828
|
+
console.log(`\n${paint(32, "✔")} patched ${bold(rel)}`);
|
|
829
|
+
}
|
|
830
|
+
console.log(` ${paint(32, "+")} ${line}`);
|
|
831
|
+
} else {
|
|
832
|
+
legacy(appRoot, cssFile, { before, live, rel, lines, present, anchorIn, dryRun });
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
console.log(`\n${bold("Bind the fonts")} in your root layout. The package cannot load them for you:\n`);
|
|
836
|
+
console.log(fontSnippet());
|
|
837
|
+
console.log(`\n${bold("If you use a coding agent")}, point it at the API summary:\n`);
|
|
838
|
+
console.log(` ${dim("# CLAUDE.md, AGENTS.md, or your agent's equivalent")}`);
|
|
839
|
+
console.log(` @node_modules/${PKG_NAME}/llms.txt`);
|
|
840
|
+
console.log(`\nThen: ${bold("foundations doctor")}\n`);
|
|
841
|
+
return 0;
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* The old shape: the four entry points written out by the consumer, plus an
|
|
846
|
+
* `@source` line it had to path itself. Still supported, and still repaired —
|
|
847
|
+
* an app on it is not broken, and rewriting a file someone else wrote is not
|
|
848
|
+
* this command's call. `init` only offers the one-line form.
|
|
849
|
+
*
|
|
850
|
+
* Lift the package's own @import lines out, then lay them back down in the
|
|
851
|
+
* one order the cascade accepts. Lifting the whole LINE keeps a trailing
|
|
852
|
+
* comment attached to the import a consumer wrote it against, and makes a file
|
|
853
|
+
* that is merely out of order repairable rather than only diagnosable.
|
|
854
|
+
*/
|
|
855
|
+
const legacy = (appRoot, cssFile, { live, rel, lines, present, anchorIn, dryRun }) => {
|
|
610
856
|
const existing = new Map();
|
|
611
|
-
const present = new Set(importsOf(live));
|
|
612
857
|
const kept = lines.filter((line) => {
|
|
613
858
|
const [spec] = importsOf(stripComments(line));
|
|
614
859
|
// `present` keeps a commented-out import where it is instead of reviving it.
|
|
@@ -635,9 +880,7 @@ const init = (appRoot, { dryRun }) => {
|
|
|
635
880
|
if (!added.length && !reordered && !needsSource) {
|
|
636
881
|
console.log(`\n${rel} already imports the style layer, in order, and scans the package.`);
|
|
637
882
|
} else {
|
|
638
|
-
|
|
639
|
-
const anchor = kept.reduce((at, line, i) => (/@import\s+["']tailwindcss["']/.test(line) ? i : at), -1);
|
|
640
|
-
kept.splice(anchor + 1, 0, ...block);
|
|
883
|
+
kept.splice(anchorIn(kept) + 1, 0, ...block);
|
|
641
884
|
|
|
642
885
|
if (dryRun) {
|
|
643
886
|
console.log(`\n${bold(rel)} ${dim("(dry run — nothing written)")}`);
|
|
@@ -652,18 +895,9 @@ const init = (appRoot, { dryRun }) => {
|
|
|
652
895
|
if (reordered) console.log(` ${dim("reordered: later files re-point variables the earlier ones define")}`);
|
|
653
896
|
}
|
|
654
897
|
|
|
655
|
-
console.log(
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
// The package ships an llms.txt for whatever coding agent the app runs. It is
|
|
659
|
-
// only useful if the agent is pointed at it, and that is one line in a file
|
|
660
|
-
// this command should not edit on its own.
|
|
661
|
-
console.log(`\n${bold("If you use a coding agent")}, point it at the API summary:\n`);
|
|
662
|
-
console.log(` ${dim("# CLAUDE.md, AGENTS.md, or your agent's equivalent")}`);
|
|
663
|
-
console.log(` @node_modules/${PKG_NAME}/llms.txt`);
|
|
664
|
-
|
|
665
|
-
console.log(`\nThen: ${bold("foundations doctor")}\n`);
|
|
666
|
-
return 0;
|
|
898
|
+
console.log(
|
|
899
|
+
`\n${dim(`These ${cssEntries.filter((e) => !OPTIONAL_CSS.has(e)).length} lines and the @source can now be one: @import "${BUNDLE}";`)}`,
|
|
900
|
+
);
|
|
667
901
|
};
|
|
668
902
|
|
|
669
903
|
const usage = () => {
|
|
@@ -682,7 +916,9 @@ Options
|
|
|
682
916
|
/* ------------------------------------------------------------------- entry */
|
|
683
917
|
|
|
684
918
|
const args = process.argv.slice(2);
|
|
685
|
-
|
|
919
|
+
/** Flags that take a value, so the value is not mistaken for the command. */
|
|
920
|
+
const VALUED = new Set(["--cwd"]);
|
|
921
|
+
const command = args.find((a, i) => !a.startsWith("--") && !VALUED.has(args[i - 1])) ?? "help";
|
|
686
922
|
const flag = (name) => args.includes(`--${name}`);
|
|
687
923
|
const value = (name) => {
|
|
688
924
|
const at = args.indexOf(`--${name}`);
|
package/llms.txt
CHANGED
|
@@ -187,14 +187,16 @@ Code samples and commit messages stay conventional. This is about prose.
|
|
|
187
187
|
- `<TypographyMuted tone="default">`: a type error. Use `TypographyP`.
|
|
188
188
|
- Binding fonts with `font.className` instead of `font.variable`. The className
|
|
189
189
|
form sets `font-family` on the element and leaves the roles unresolved.
|
|
190
|
-
-
|
|
191
|
-
the
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
- Writing the style layer out by hand. `@import "@supertype.ai/foundations";` after
|
|
191
|
+
`@import "tailwindcss";` is the whole of it: it carries tokens, theme, type and
|
|
192
|
+
prose in order and registers the package's own `@source`. Split into the granular
|
|
193
|
+
imports, an app owns the order, the palette and the scan path — and a wrong
|
|
194
|
+
`@source` path purges every class the package ships, silently.
|
|
194
195
|
- Using `Accordion` for a static FAQ. `Disclosure` runs on the browser alone.
|
|
195
196
|
- Adding a second `@custom-variant dark`. `tokens.css` already binds it.
|
|
196
197
|
|
|
197
|
-
Run `npx foundations doctor` in the app to check the
|
|
198
|
+
Run `npx foundations doctor` in the app to check the font binding, the style layer
|
|
199
|
+
and the dark variant.
|
|
198
200
|
|
|
199
201
|
## Surfaces
|
|
200
202
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supertype.ai/foundations",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -38,9 +38,11 @@
|
|
|
38
38
|
},
|
|
39
39
|
"exports": {
|
|
40
40
|
".": {
|
|
41
|
+
"style": "./src/index.css",
|
|
41
42
|
"types": "./dist/index.d.ts",
|
|
42
43
|
"default": "./dist/index.js"
|
|
43
44
|
},
|
|
45
|
+
"./index.css": "./src/index.css",
|
|
44
46
|
"./blocks": {
|
|
45
47
|
"types": "./dist/blocks/index.d.ts",
|
|
46
48
|
"default": "./dist/blocks/index.js"
|
|
@@ -109,7 +111,8 @@
|
|
|
109
111
|
"@base-ui/react": ">=1.4",
|
|
110
112
|
"next": ">=15",
|
|
111
113
|
"next-view-transitions": ">=0.3",
|
|
112
|
-
"react": ">=19"
|
|
114
|
+
"react": ">=19",
|
|
115
|
+
"tailwindcss": ">=4"
|
|
113
116
|
},
|
|
114
117
|
"devDependencies": {
|
|
115
118
|
"@base-ui/react": "^1.4.1",
|
package/src/index.css
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* The whole style layer, in one import.
|
|
2
|
+
*
|
|
3
|
+
* An app used to write these four imports itself, in this order, plus an
|
|
4
|
+
* `@source` line pointing back into node_modules — and every part of that was
|
|
5
|
+
* something to get wrong. The order is a cascade, so a file out of place
|
|
6
|
+
* re-points variables an earlier one defines. The `@source` path is relative to
|
|
7
|
+
* the app's CSS file, so it moved with the app's layout and broke outright in a
|
|
8
|
+
* workspace, where the package hoists to the repo root: Tailwind then scanned
|
|
9
|
+
* nothing, purged every class this package ships, and rendered the components
|
|
10
|
+
* unstyled with no error anywhere.
|
|
11
|
+
*
|
|
12
|
+
* None of it was ever the app's business. Tailwind v4 resolves `@source`
|
|
13
|
+
* relative to the file that declares it, so the package can register its own
|
|
14
|
+
* content — and then the order, the path and the completeness are all decided
|
|
15
|
+
* here, where they are the same for everyone.
|
|
16
|
+
*
|
|
17
|
+
* @import "tailwindcss";
|
|
18
|
+
* @import "@supertype.ai/foundations";
|
|
19
|
+
*
|
|
20
|
+
* shiki.css is deliberately not here: an app that renders no code fences should
|
|
21
|
+
* not pay for it. Import it separately when you add Shiki.
|
|
22
|
+
*
|
|
23
|
+
* The granular entry points remain exported, for the one app in a hundred that
|
|
24
|
+
* paints every colour role itself and wants tokens.css without theme.css.
|
|
25
|
+
*/
|
|
26
|
+
@import "./tokens.css";
|
|
27
|
+
@import "./theme.css";
|
|
28
|
+
@import "./type.css";
|
|
29
|
+
@import "./prose.css";
|
|
30
|
+
|
|
31
|
+
/* Relative to this file, so it is right wherever the package is installed.
|
|
32
|
+
*
|
|
33
|
+
* Narrowed to `.js` rather than pointing at the directory: Tailwind reads every
|
|
34
|
+
* source file as plain text, so scanning all of dist/ hands it the prose in the
|
|
35
|
+
* .d.ts doc comments and it emits rules for words that happen to look like
|
|
36
|
+
* classes. `check-candidates.mjs` guards the same boundary from the other side. */
|
|
37
|
+
@source "../dist/**/*.js";
|