create-forte-ui 1.0.0-alpha.9 → 1.0.0-beta.2
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 +1 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +5 -5
- package/dist/color.d.ts +3 -3
- package/dist/color.js +4 -4
- package/dist/fonts.js +1 -1
- package/dist/overlay.js +7 -2
- package/dist/prompts.js +3 -3
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +12 -6
- package/dist/theme.d.ts +17 -4
- package/dist/theme.js +22 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ are tuned.
|
|
|
37
37
|
|
|
38
38
|
- The starter files live in `src/templates.ts`; when a guide step changes,
|
|
39
39
|
change the matching builder in the same commit.
|
|
40
|
-
- The font
|
|
40
|
+
- The font catalog (`src/fonts.ts`) and color maths (`src/color.ts`) are
|
|
41
41
|
the modules of record — the docs' Theme Studio re-exports them from this
|
|
42
42
|
package.
|
|
43
43
|
- `pnpm --filter create-forte-ui smoke` scaffolds and builds all four
|
package/dist/args.d.ts
CHANGED
|
@@ -27,4 +27,4 @@ export type CliOptions = {
|
|
|
27
27
|
export declare class UsageError extends Error {
|
|
28
28
|
}
|
|
29
29
|
export declare function parseCliArgs(argv: string[]): CliOptions;
|
|
30
|
-
export declare const HELP = "create-forte-ui \u2014 scaffold a new app wired up with forte-ui\n\nUsage\n pnpm create forte-ui [name] [flags]\n npm create forte-ui@latest [name] -- [flags]\n\nEvery flag has a prompt twin; a passed flag suppresses its prompt. With no\nflags you get the questionnaire, with --yes you get a Next.js + Tailwind app\non the library's default theme.\n\nProject\n [name] project directory (prompted if omitted)\n -f, --framework next | vite\n --tailwind wire the Tailwind v4 bridge (default yes)\n --no-tailwind plain CSS setup\n --pm npm | pnpm | yarn | bun (default: whoever invoked us)\n --library version spec for @forte-ui/react \u2014 exact\n (\"1.0.0-alpha.4\"), a dist-tag (\"alpha\"), or a range.\n Default: latest.\n --no-install write files only; skip installing dependencies\n and the agent skill\n --no-skill skip installing the forte-ui agent skill\n (skills.sh \u2014 .agents/skills plus .claude/skills)\n -y, --yes accept the defaults for everything not passed\n\nTheme \u2014 every skipped value keeps the library default and writes NOTHING,\nso the app keeps following the library when defaults are tuned.\n --seed, --accent accent seed, hex (\"#6d43d4\")\n --secondary secondary seed, hex\n --tint neutral tint, 0 (pure
|
|
30
|
+
export declare const HELP = "create-forte-ui \u2014 scaffold a new app wired up with forte-ui\n\nUsage\n pnpm create forte-ui [name] [flags]\n npm create forte-ui@latest [name] -- [flags]\n\nEvery flag has a prompt twin; a passed flag suppresses its prompt. With no\nflags you get the questionnaire, with --yes you get a Next.js + Tailwind app\non the library's default theme.\n\nProject\n [name] project directory (prompted if omitted)\n -f, --framework next | vite\n --tailwind wire the Tailwind v4 bridge (default yes)\n --no-tailwind plain CSS setup\n --pm npm | pnpm | yarn | bun (default: whoever invoked us)\n --library version spec for @forte-ui/react \u2014 exact\n (\"1.0.0-alpha.4\"), a dist-tag (\"alpha\"), or a range.\n Default: latest.\n --no-install write files only; skip installing dependencies\n and the agent skill\n --no-skill skip installing the forte-ui agent skill\n (skills.sh \u2014 .agents/skills plus .claude/skills)\n -y, --yes accept the defaults for everything not passed\n\nTheme \u2014 every skipped value keeps the library default and writes NOTHING,\nso the app keeps following the library when defaults are tuned.\n --seed, --accent accent seed, hex (\"#6d43d4\")\n --secondary secondary seed, hex\n --tint neutral tint, 0 (pure gray) to 1 (default)\n --radius none | soft | pill\n --density compact | spacious\n --motion system (default) | reduce | full\n \"full\" overrides the OS reduced-motion preference\n for everyone \u2014 prefer leaving it unset.\n --scheme system (default) | light | dark\n \"light\" / \"dark\" pin one palette on <html> and\n leave the theme toggle and its replay script out.\n --font-sans a catalog name (\"Inter\", \"DM Sans\", ...)\n --font-mono a catalog name (\"JetBrains Mono\", ...)\n\nDesign the theme visually instead: https://forte-ui.com/theme\n";
|
package/dist/args.js
CHANGED
|
@@ -18,7 +18,7 @@ export class UsageError extends Error {
|
|
|
18
18
|
function normalizeHex(flag, value) {
|
|
19
19
|
const hex = value.startsWith("#") ? value : `#${value}`;
|
|
20
20
|
if (!hexToOklch(hex)) {
|
|
21
|
-
throw new UsageError(`--${flag} expects a hex
|
|
21
|
+
throw new UsageError(`--${flag} expects a hex color like "#6d43d4", got "${value}"`);
|
|
22
22
|
}
|
|
23
23
|
return hex.toLowerCase();
|
|
24
24
|
}
|
|
@@ -32,7 +32,7 @@ function oneOf(flag, value, options) {
|
|
|
32
32
|
function fontByName(flag, value, list) {
|
|
33
33
|
const match = list.find((f) => f.name.toLowerCase() === value.toLowerCase());
|
|
34
34
|
if (!match) {
|
|
35
|
-
throw new UsageError(`--${flag}: "${value}" is not in the
|
|
35
|
+
throw new UsageError(`--${flag}: "${value}" is not in the catalog. Choices: ${list.map((f) => f.name).join(", ")}`);
|
|
36
36
|
}
|
|
37
37
|
return match.name;
|
|
38
38
|
}
|
|
@@ -146,7 +146,7 @@ Theme — every skipped value keeps the library default and writes NOTHING,
|
|
|
146
146
|
so the app keeps following the library when defaults are tuned.
|
|
147
147
|
--seed, --accent accent seed, hex ("#6d43d4")
|
|
148
148
|
--secondary secondary seed, hex
|
|
149
|
-
--tint neutral tint, 0 (pure
|
|
149
|
+
--tint neutral tint, 0 (pure gray) to 1 (default)
|
|
150
150
|
--radius none | soft | pill
|
|
151
151
|
--density compact | spacious
|
|
152
152
|
--motion system (default) | reduce | full
|
|
@@ -155,8 +155,8 @@ so the app keeps following the library when defaults are tuned.
|
|
|
155
155
|
--scheme system (default) | light | dark
|
|
156
156
|
"light" / "dark" pin one palette on <html> and
|
|
157
157
|
leave the theme toggle and its replay script out.
|
|
158
|
-
--font-sans a
|
|
159
|
-
--font-mono a
|
|
158
|
+
--font-sans a catalog name ("Inter", "DM Sans", ...)
|
|
159
|
+
--font-mono a catalog name ("JetBrains Mono", ...)
|
|
160
160
|
|
|
161
161
|
Design the theme visually instead: https://forte-ui.com/theme
|
|
162
162
|
`;
|
package/dist/color.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Color maths for the Theme Studio and the create-forte-ui CLI. This is the
|
|
3
3
|
* module of record — the docs' `lib/color.ts` re-exports it from here.
|
|
4
4
|
*
|
|
5
5
|
* Mirrors the model the library's CSS uses, so what the studio reports is what
|
|
6
6
|
* the browser will actually paint:
|
|
7
7
|
* - sRGB is naive-CLIPPED, not gamut-mapped, because that is what browsers do
|
|
8
|
-
* today. Reporting a gamut-mapped
|
|
8
|
+
* today. Reporting a gamut-mapped color here would understate how far an
|
|
9
9
|
* out-of-range seed drifts.
|
|
10
10
|
* - Contrast is WCAG 2.x relative luminance, the ratio the success criteria
|
|
11
11
|
* are actually written against.
|
|
@@ -25,7 +25,7 @@ export declare function oklchToHex(o: Oklch): string;
|
|
|
25
25
|
export declare function hexToOklch(hex: string): Oklch | null;
|
|
26
26
|
export declare function contrast(a: Rgb, b: Rgb): number;
|
|
27
27
|
/**
|
|
28
|
-
* The exact readable text
|
|
28
|
+
* The exact readable text color for a solid fill, chosen by measuring both
|
|
29
29
|
* candidates rather than by the CSS fallback's fitted lightness threshold.
|
|
30
30
|
* Emitting this as a literal is what makes the studio's output correct in
|
|
31
31
|
* every browser, including those without contrast-color().
|
package/dist/color.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Color maths for the Theme Studio and the create-forte-ui CLI. This is the
|
|
3
3
|
* module of record — the docs' `lib/color.ts` re-exports it from here.
|
|
4
4
|
*
|
|
5
5
|
* Mirrors the model the library's CSS uses, so what the studio reports is what
|
|
6
6
|
* the browser will actually paint:
|
|
7
7
|
* - sRGB is naive-CLIPPED, not gamut-mapped, because that is what browsers do
|
|
8
|
-
* today. Reporting a gamut-mapped
|
|
8
|
+
* today. Reporting a gamut-mapped color here would understate how far an
|
|
9
9
|
* out-of-range seed drifts.
|
|
10
10
|
* - Contrast is WCAG 2.x relative luminance, the ratio the success criteria
|
|
11
11
|
* are actually written against.
|
|
@@ -61,7 +61,7 @@ export function contrast(a, b) {
|
|
|
61
61
|
const WHITE = [1, 1, 1];
|
|
62
62
|
const BLACK = [0, 0, 0];
|
|
63
63
|
/**
|
|
64
|
-
* The exact readable text
|
|
64
|
+
* The exact readable text color for a solid fill, chosen by measuring both
|
|
65
65
|
* candidates rather than by the CSS fallback's fitted lightness threshold.
|
|
66
66
|
* Emitting this as a literal is what makes the studio's output correct in
|
|
67
67
|
* every browser, including those without contrast-color().
|
|
@@ -80,7 +80,7 @@ export function validateSeed(seed) {
|
|
|
80
80
|
if (outOfGamut) {
|
|
81
81
|
out.push({
|
|
82
82
|
level: "warn",
|
|
83
|
-
message: "Outside the sRGB gamut. Browsers clip rather than gamut-map, which shifts the painted lightness — the 9/10 hover step can visually collapse, and the
|
|
83
|
+
message: "Outside the sRGB gamut. Browsers clip rather than gamut-map, which shifts the painted lightness — the 9/10 hover step can visually collapse, and the color will differ between sRGB and P3 displays.",
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
if (seed.l < ENVELOPE.lMin) {
|
package/dist/fonts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* The font
|
|
1
|
+
/* The font catalog — the ten most-used Google Fonts of each kind, plus the
|
|
2
2
|
* library's own default stack as "System". This is the module of record: the
|
|
3
3
|
* docs' Theme Studio re-exports it from here, so the CLI's font prompt and
|
|
4
4
|
* the studio's pickers cannot drift apart. Everything a consumer of
|
package/dist/overlay.js
CHANGED
|
@@ -40,9 +40,14 @@ function applyVite({ name, dir, tailwind, answers }) {
|
|
|
40
40
|
* report it, never guess. */
|
|
41
41
|
const htmlPath = path.join(dir, "index.html");
|
|
42
42
|
let html = fs.readFileSync(htmlPath, "utf8");
|
|
43
|
-
|
|
43
|
+
/* `forte-reset` is unconditional — it switches on the reset stylesheet
|
|
44
|
+
* index.css imports, so the two must not drift. That makes the anchor
|
|
45
|
+
* always load-bearing here, unlike the theme attributes, which are empty on
|
|
46
|
+
* an all-defaults run: the guard below therefore checks the replacement
|
|
47
|
+
* outright rather than only when `attrs` is non-empty. */
|
|
48
|
+
const attrs = `${htmlAttrs(answers)} class="forte-reset"`;
|
|
44
49
|
const anchored = html.replace(/<html lang="en">/, `<html lang="en"${attrs}>`);
|
|
45
|
-
if (
|
|
50
|
+
if (anchored === html) {
|
|
46
51
|
throw new Error(`could not find '<html lang="en">' in index.html to add${attrs} — ` +
|
|
47
52
|
`the create-vite template may have changed; add the attribute(s) by hand.`);
|
|
48
53
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -12,7 +12,7 @@ import { DEFAULT_ANSWERS } from "./theme.js";
|
|
|
12
12
|
import { UsageError } from "./args.js";
|
|
13
13
|
function accept(value) {
|
|
14
14
|
if (p.isCancel(value)) {
|
|
15
|
-
p.cancel("
|
|
15
|
+
p.cancel("Canceled — nothing was written.");
|
|
16
16
|
process.exit(1);
|
|
17
17
|
}
|
|
18
18
|
return value;
|
|
@@ -37,7 +37,7 @@ const hexPrompt = (message) => async () => {
|
|
|
37
37
|
validate: (v) => {
|
|
38
38
|
if (!v)
|
|
39
39
|
return undefined;
|
|
40
|
-
return hexToOklch(v.startsWith("#") ? v : `#${v}`) ? undefined : "Expected a hex
|
|
40
|
+
return hexToOklch(v.startsWith("#") ? v : `#${v}`) ? undefined : "Expected a hex color like #6d43d4.";
|
|
41
41
|
},
|
|
42
42
|
}));
|
|
43
43
|
if (!value)
|
|
@@ -109,7 +109,7 @@ export async function collectPlan(opts) {
|
|
|
109
109
|
}
|
|
110
110
|
if (remaining.includes("tint")) {
|
|
111
111
|
const tint = accept(await p.text({
|
|
112
|
-
message: "Neutral tint — 0 (pure
|
|
112
|
+
message: "Neutral tint — 0 (pure gray) to 1 (full brand tint)",
|
|
113
113
|
placeholder: "1",
|
|
114
114
|
validate: (v) => {
|
|
115
115
|
if (!v)
|
package/dist/templates.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ThemeAnswers } from "./theme.js";
|
|
2
2
|
export declare function viteIndexCss(a: ThemeAnswers, tailwind: boolean): string;
|
|
3
3
|
export declare function viteMainTsx(tailwind: boolean): string;
|
|
4
|
-
/** `toggle` is false when the answers pinned a
|
|
4
|
+
/** `toggle` is false when the answers pinned a color scheme: `data-theme`
|
|
5
5
|
* then sits statically on `<html>` and a toggle would be a button whose one
|
|
6
6
|
* job is to fight it. The import goes with the element, so the starter
|
|
7
7
|
* still compiles clean under `noUnusedLocals`. */
|
package/dist/templates.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* diff a scaffolded app against the walkthrough and see only their own
|
|
5
5
|
* answers. When a guide step changes, change the builder with it.
|
|
6
6
|
*/
|
|
7
|
-
import { rootBlock, fontImports, htmlAttrs, nextFontSetup } from "./theme.js";
|
|
7
|
+
import { rootBlock, fontImports, htmlAttrs, htmlClassAttr, nextFontSetup, } from "./theme.js";
|
|
8
8
|
/* The body rule from the guides' "Replace the scaffold CSS" step. Present on
|
|
9
9
|
* the non-Tailwind paths only — the Tailwind starter page carries the same
|
|
10
10
|
* three declarations as utilities on `<main>`. */
|
|
@@ -23,7 +23,7 @@ function joinBlocks(...blocks) {
|
|
|
23
23
|
* flipped without writing a line. Two spellings of the same corner, because
|
|
24
24
|
* the Tailwind paths have utilities and the plain ones do not.
|
|
25
25
|
*
|
|
26
|
-
* `fixed` rather than in flow, so the page still
|
|
26
|
+
* `fixed` rather than in flow, so the page still centers the component it is
|
|
27
27
|
* there to demonstrate — and the offsets are space tokens rather than `1rem`,
|
|
28
28
|
* because this file is the first thing a new app is copied out of and a
|
|
29
29
|
* hardcoded value here is the habit it teaches. */
|
|
@@ -56,6 +56,7 @@ export function viteIndexCss(a, tailwind) {
|
|
|
56
56
|
return joinBlocks(fontBlock, `@import "@forte-ui/react/tailwind.css";
|
|
57
57
|
@import "tailwindcss";
|
|
58
58
|
@import "@forte-ui/react/theme.css";
|
|
59
|
+
@import "@forte-ui/react/styles/reset.css";
|
|
59
60
|
`, rootBlock(a, "import"));
|
|
60
61
|
}
|
|
61
62
|
return joinBlocks(fontBlock, rootBlock(a, "import"), BODY_RULE);
|
|
@@ -65,7 +66,9 @@ export function viteMainTsx(tailwind) {
|
|
|
65
66
|
* bridge — importing it here would pin the `forte` layer first and hand
|
|
66
67
|
* Preflight the win. Without Tailwind there is no ordering hazard and the
|
|
67
68
|
* guide imports it at the entry point. */
|
|
68
|
-
const themeImport = tailwind
|
|
69
|
+
const themeImport = tailwind
|
|
70
|
+
? ""
|
|
71
|
+
: `import "@forte-ui/react/theme.css";\nimport "@forte-ui/react/styles/reset.css";\n`;
|
|
69
72
|
return `import { StrictMode } from "react";
|
|
70
73
|
import { createRoot } from "react-dom/client";
|
|
71
74
|
${themeImport}import "./index.css";
|
|
@@ -78,7 +81,7 @@ createRoot(document.getElementById("root")!).render(
|
|
|
78
81
|
);
|
|
79
82
|
`;
|
|
80
83
|
}
|
|
81
|
-
/** `toggle` is false when the answers pinned a
|
|
84
|
+
/** `toggle` is false when the answers pinned a color scheme: `data-theme`
|
|
82
85
|
* then sits statically on `<html>` and a toggle would be a button whose one
|
|
83
86
|
* job is to fight it. The import goes with the element, so the starter
|
|
84
87
|
* still compiles clean under `noUnusedLocals`. */
|
|
@@ -130,6 +133,7 @@ export function nextGlobalsCss(a, tailwind) {
|
|
|
130
133
|
return joinBlocks(`@import "@forte-ui/react/tailwind.css";
|
|
131
134
|
@import "tailwindcss";
|
|
132
135
|
@import "@forte-ui/react/theme.css";
|
|
136
|
+
@import "@forte-ui/react/styles/reset.css";
|
|
133
137
|
`, rootBlock(a, "next-font"));
|
|
134
138
|
}
|
|
135
139
|
return joinBlocks(rootBlock(a, "next-font"), BODY_RULE);
|
|
@@ -140,7 +144,9 @@ export function nextLayoutTsx(name, a, tailwind) {
|
|
|
140
144
|
* because nothing else declares layers. With Tailwind it must NOT be:
|
|
141
145
|
* globals.css imports it after the bridge (see the guide's ordering note),
|
|
142
146
|
* and a second import here would pin the `forte` layer before `base`. */
|
|
143
|
-
const themeImport = tailwind
|
|
147
|
+
const themeImport = tailwind
|
|
148
|
+
? ""
|
|
149
|
+
: `import "@forte-ui/react/theme.css";\nimport "@forte-ui/react/styles/reset.css";\n`;
|
|
144
150
|
/* ThemeScript + suppressHydrationWarning are the guides' "Light and dark"
|
|
145
151
|
* step: the script replays a stored light/dark choice onto <html> before
|
|
146
152
|
* first paint, and the suppression covers the attribute it legitimately
|
|
@@ -175,7 +181,7 @@ export default function RootLayout({
|
|
|
175
181
|
children: React.ReactNode;
|
|
176
182
|
}>) {
|
|
177
183
|
return (
|
|
178
|
-
<html lang="en"${htmlAttrs(a)}${font.
|
|
184
|
+
<html lang="en"${htmlAttrs(a)}${htmlClassAttr(font.fontVars)}${pinned ? "" : " suppressHydrationWarning"}>
|
|
179
185
|
${head} <body>
|
|
180
186
|
${toggleLine} {children}
|
|
181
187
|
</body>
|
package/dist/theme.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type ThemeAnswers = {
|
|
|
10
10
|
/** Accent seed hex, or null to keep the library default (write nothing). */
|
|
11
11
|
seed: string | null;
|
|
12
12
|
secondary: string | null;
|
|
13
|
-
/** 0 = pure
|
|
13
|
+
/** 0 = pure gray neutrals, 1 = full brand tint. 1 is the shipped default. */
|
|
14
14
|
tint: number;
|
|
15
15
|
radius: Radius;
|
|
16
16
|
density: Density;
|
|
@@ -18,7 +18,7 @@ export type ThemeAnswers = {
|
|
|
18
18
|
/** "system" follows the OS and scaffolds the toggle; "light" / "dark" pin
|
|
19
19
|
* one palette on `<html>` and leave the toggle and its replay script out. */
|
|
20
20
|
scheme: Scheme;
|
|
21
|
-
/**
|
|
21
|
+
/** Catalog names. "System" writes nothing. */
|
|
22
22
|
fontSans: string;
|
|
23
23
|
fontMono: string;
|
|
24
24
|
};
|
|
@@ -38,7 +38,7 @@ export declare function rootBlock(a: ThemeAnswers, fontMode: FontMode): string;
|
|
|
38
38
|
export declare function fontImports(a: ThemeAnswers): string[];
|
|
39
39
|
/** ` data-forte-radius="pill" ...` — leading space included, "" when all
|
|
40
40
|
* defaults. Default modes stay UNSET so the app keeps following the OS
|
|
41
|
-
* (motion,
|
|
41
|
+
* (motion, color scheme) and the library's own defaults. */
|
|
42
42
|
export declare function htmlAttrs(a: ThemeAnswers): string;
|
|
43
43
|
export type NextFontSetup = {
|
|
44
44
|
/** `import { Inter, JetBrains_Mono } from "next/font/google";` or "". */
|
|
@@ -46,6 +46,19 @@ export type NextFontSetup = {
|
|
|
46
46
|
/** The `const fontSans = Inter({...});` declarations, or "". */
|
|
47
47
|
consts: string;
|
|
48
48
|
/** ` className={...}` for the `<html>` element, or "". */
|
|
49
|
-
|
|
49
|
+
/** The `next/font` CSS-variable idents, e.g. ["fontSans.variable"]. The
|
|
50
|
+
* `<html>` class list is composed from these plus `forte-reset`, which is
|
|
51
|
+
* why this is the parts rather than a finished attribute. */
|
|
52
|
+
fontVars: string[];
|
|
50
53
|
};
|
|
51
54
|
export declare function nextFontSetup(a: ThemeAnswers): NextFontSetup;
|
|
55
|
+
/** ` className="forte-reset"` — or a template literal when next/font variables
|
|
56
|
+
* have to ride along on the same element.
|
|
57
|
+
*
|
|
58
|
+
* `forte-reset` is unconditional: it is what switches on
|
|
59
|
+
* `@forte-ui/react/styles/reset.css`, which the starter imports. A scaffolded
|
|
60
|
+
* app is the one place the blanket reset is unambiguously right — there is no
|
|
61
|
+
* existing markup for it to change behavior under, and it becomes the
|
|
62
|
+
* baseline everything the author writes next is built against. The library
|
|
63
|
+
* itself stays scoped to `[data-forte]` for exactly the opposite reason. */
|
|
64
|
+
export declare function htmlClassAttr(fontVars: string[]): string;
|
package/dist/theme.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
*
|
|
3
3
|
* The record mirrors the Theme Studio's `ThemeConfig` — same keys, same value
|
|
4
4
|
* sets — so a studio-exported command line maps onto the CLI's flags with no
|
|
5
|
-
* translation. It differs in one deliberate way: the
|
|
6
|
-
* nullable. The studio always shows a concrete
|
|
5
|
+
* translation. It differs in one deliberate way: the color fields are
|
|
6
|
+
* nullable. The studio always shows a concrete color because its controls
|
|
7
7
|
* need something to display; the CLI's contract is "only serialise
|
|
8
8
|
* deviations", so an unanswered prompt stays `null` and writes NOTHING. A
|
|
9
9
|
* scaffolded default would be a frozen default — an app that restates
|
|
@@ -56,7 +56,7 @@ export function rootDeclarations(a, fontMode) {
|
|
|
56
56
|
if (mono.stack)
|
|
57
57
|
lines.push(`--forte-font-mono: var(--font-mono), ${MONO_FALLBACK};`);
|
|
58
58
|
}
|
|
59
|
-
/* Same emission as the studio's copied CSS: the on-
|
|
59
|
+
/* Same emission as the studio's copied CSS: the on-color is measured here
|
|
60
60
|
* rather than left to the CSS fallback's fitted lightness threshold, so it
|
|
61
61
|
* is exact in every browser — including those without contrast-color(). */
|
|
62
62
|
const on = a.seed ? bestOnColor(hexToOklch(a.seed)) : null;
|
|
@@ -88,7 +88,7 @@ export function fontImports(a) {
|
|
|
88
88
|
}
|
|
89
89
|
/** ` data-forte-radius="pill" ...` — leading space included, "" when all
|
|
90
90
|
* defaults. Default modes stay UNSET so the app keeps following the OS
|
|
91
|
-
* (motion,
|
|
91
|
+
* (motion, color scheme) and the library's own defaults. */
|
|
92
92
|
export function htmlAttrs(a) {
|
|
93
93
|
const attrs = [
|
|
94
94
|
a.radius !== "default" && `data-forte-radius="${a.radius}"`,
|
|
@@ -98,7 +98,7 @@ export function htmlAttrs(a) {
|
|
|
98
98
|
].filter(Boolean);
|
|
99
99
|
return attrs.length ? " " + attrs.join(" ") : "";
|
|
100
100
|
}
|
|
101
|
-
/** The axes string in the
|
|
101
|
+
/** The axes string in the catalog is the truth about what a family can
|
|
102
102
|
* serve: `400..700` is a variable font (no `weight` needed), `400;500;700`
|
|
103
103
|
* is static and next/font requires the explicit list. */
|
|
104
104
|
function weightsFromAxes(css) {
|
|
@@ -113,7 +113,7 @@ export function nextFontSetup(a) {
|
|
|
113
113
|
{ font: findFont(MONO_FONTS, a.fontMono), variable: "--font-mono", ident: "fontMono" },
|
|
114
114
|
].filter((p) => p.font.css);
|
|
115
115
|
if (picks.length === 0)
|
|
116
|
-
return { importLine: "", consts: "",
|
|
116
|
+
return { importLine: "", consts: "", fontVars: [] };
|
|
117
117
|
const names = picks.map((p) => p.font.name.replaceAll(" ", "_"));
|
|
118
118
|
const consts = picks
|
|
119
119
|
.map((p, i) => {
|
|
@@ -122,12 +122,24 @@ export function nextFontSetup(a) {
|
|
|
122
122
|
return `const ${p.ident} = ${names[i]}({ subsets: ["latin"], variable: "${p.variable}"${weightOpt} });`;
|
|
123
123
|
})
|
|
124
124
|
.join("\n");
|
|
125
|
-
const classExpr = picks.length === 1
|
|
126
|
-
? `{${picks[0].ident}.variable}`
|
|
127
|
-
: `{\`\${fontSans.variable} \${fontMono.variable}\`}`;
|
|
128
125
|
return {
|
|
129
126
|
importLine: `import { ${names.join(", ")} } from "next/font/google";`,
|
|
130
127
|
consts,
|
|
131
|
-
|
|
128
|
+
fontVars: picks.map((p) => `${p.ident}.variable`),
|
|
132
129
|
};
|
|
133
130
|
}
|
|
131
|
+
/** ` className="forte-reset"` — or a template literal when next/font variables
|
|
132
|
+
* have to ride along on the same element.
|
|
133
|
+
*
|
|
134
|
+
* `forte-reset` is unconditional: it is what switches on
|
|
135
|
+
* `@forte-ui/react/styles/reset.css`, which the starter imports. A scaffolded
|
|
136
|
+
* app is the one place the blanket reset is unambiguously right — there is no
|
|
137
|
+
* existing markup for it to change behavior under, and it becomes the
|
|
138
|
+
* baseline everything the author writes next is built against. The library
|
|
139
|
+
* itself stays scoped to `[data-forte]` for exactly the opposite reason. */
|
|
140
|
+
export function htmlClassAttr(fontVars) {
|
|
141
|
+
if (fontVars.length === 0)
|
|
142
|
+
return ` className="forte-reset"`;
|
|
143
|
+
const vars = fontVars.map((v) => `\${${v}}`).join(" ");
|
|
144
|
+
return ` className={\`forte-reset ${vars}\`}`;
|
|
145
|
+
}
|
package/package.json
CHANGED