create-forte-ui 1.0.0-alpha.4 → 1.0.0-alpha.6
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 +12 -1
- package/dist/args.js +18 -2
- package/dist/index.js +38 -8
- package/dist/overlay.d.ts +12 -4
- package/dist/overlay.js +39 -9
- package/dist/prompts.js +13 -2
- package/dist/scaffold.d.ts +7 -0
- package/dist/scaffold.js +9 -0
- package/dist/templates.d.ts +5 -1
- package/dist/templates.js +58 -8
- package/dist/theme.d.ts +6 -1
- package/dist/theme.js +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ npm create forte-ui@latest my-app
|
|
|
10
10
|
|
|
11
11
|
Four questions get you to a running app (name, framework, Tailwind, accent
|
|
12
12
|
color); everything else — secondary color, neutral tint, radius, density,
|
|
13
|
-
motion, fonts — hides behind one "customize further?" gate. Every prompt has a
|
|
13
|
+
motion, light/dark, fonts — hides behind one "customize further?" gate. Every prompt has a
|
|
14
14
|
flag twin, so the [Theme Studio](https://forte-ui.com/theme/) can hand you a
|
|
15
15
|
complete command line and `--yes` runs without a single question:
|
|
16
16
|
|
package/dist/args.d.ts
CHANGED
|
@@ -6,8 +6,19 @@ export type CliOptions = {
|
|
|
6
6
|
framework?: Framework;
|
|
7
7
|
tailwind?: boolean;
|
|
8
8
|
pm?: PackageManager;
|
|
9
|
+
/** Version spec for `@forte-ui/react` — exact ("1.0.0-alpha.4"), dist-tag
|
|
10
|
+
* ("alpha"), or range. Deliberately NOT validated here: the package
|
|
11
|
+
* manager understands every legal form (tags, ranges, `file:`…) and
|
|
12
|
+
* rejects a wrong one at install with a better error than any regex
|
|
13
|
+
* would give. Undefined = the registry's `latest`. Dev/CI knob only —
|
|
14
|
+
* no prompt, and the studio dialog knows nothing about it. */
|
|
15
|
+
library?: string;
|
|
9
16
|
yes: boolean;
|
|
10
17
|
install: boolean;
|
|
18
|
+
/** Install the forte-ui agent skill (skills.sh) into the project. On by
|
|
19
|
+
* default; `--no-skill` opts out, and `--no-install` skips it with
|
|
20
|
+
* everything else. */
|
|
21
|
+
skill: boolean;
|
|
11
22
|
help: boolean;
|
|
12
23
|
version: boolean;
|
|
13
24
|
/** Only the keys given as flags — presence is what suppresses the prompt. */
|
|
@@ -16,4 +27,4 @@ export type CliOptions = {
|
|
|
16
27
|
export declare class UsageError extends Error {
|
|
17
28
|
}
|
|
18
29
|
export declare function parseCliArgs(argv: string[]): CliOptions;
|
|
19
|
-
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 --no-install write files only; skip installing dependencies\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 grey) 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 --font-sans
|
|
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 grey) 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 catalogue name (\"Inter\", \"DM Sans\", ...)\n --font-mono a catalogue name (\"JetBrains Mono\", ...)\n\nDesign the theme visually instead: https://forte-ui.com/theme\n";
|
package/dist/args.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { parseArgs } from "node:util";
|
|
12
12
|
import { SANS_FONTS, MONO_FONTS } from "./fonts.js";
|
|
13
13
|
import { hexToOklch } from "./color.js";
|
|
14
|
-
import { RADIUS, DENSITY, MOTION } from "./theme.js";
|
|
14
|
+
import { RADIUS, DENSITY, MOTION, SCHEME } from "./theme.js";
|
|
15
15
|
import { PACKAGE_MANAGERS } from "./scaffold.js";
|
|
16
16
|
export class UsageError extends Error {
|
|
17
17
|
}
|
|
@@ -51,11 +51,14 @@ export function parseCliArgs(argv) {
|
|
|
51
51
|
radius: { type: "string" },
|
|
52
52
|
density: { type: "string" },
|
|
53
53
|
motion: { type: "string" },
|
|
54
|
+
scheme: { type: "string" },
|
|
54
55
|
"font-sans": { type: "string" },
|
|
55
56
|
"font-mono": { type: "string" },
|
|
57
|
+
library: { type: "string" },
|
|
56
58
|
pm: { type: "string" },
|
|
57
59
|
yes: { type: "boolean", short: "y" },
|
|
58
60
|
"no-install": { type: "boolean" },
|
|
61
|
+
"no-skill": { type: "boolean" },
|
|
59
62
|
help: { type: "boolean", short: "h" },
|
|
60
63
|
version: { type: "boolean", short: "v" },
|
|
61
64
|
},
|
|
@@ -92,6 +95,8 @@ export function parseCliArgs(argv) {
|
|
|
92
95
|
answers.motion =
|
|
93
96
|
values.motion.toLowerCase() === "system" ? "default" : oneOf("motion", values.motion, MOTION);
|
|
94
97
|
}
|
|
98
|
+
if (values.scheme !== undefined)
|
|
99
|
+
answers.scheme = oneOf("scheme", values.scheme, SCHEME);
|
|
95
100
|
if (values["font-sans"] !== undefined) {
|
|
96
101
|
answers.fontSans = fontByName("font-sans", values["font-sans"], SANS_FONTS);
|
|
97
102
|
}
|
|
@@ -103,8 +108,10 @@ export function parseCliArgs(argv) {
|
|
|
103
108
|
framework: values.framework === undefined ? undefined : oneOf("framework", values.framework, ["next", "vite"]),
|
|
104
109
|
tailwind: values.tailwind ? true : values["no-tailwind"] ? false : undefined,
|
|
105
110
|
pm: values.pm === undefined ? undefined : oneOf("pm", values.pm, PACKAGE_MANAGERS),
|
|
111
|
+
library: values.library,
|
|
106
112
|
yes: values.yes ?? false,
|
|
107
113
|
install: !(values["no-install"] ?? false),
|
|
114
|
+
skill: !(values["no-skill"] ?? false),
|
|
108
115
|
help: values.help ?? false,
|
|
109
116
|
version: values.version ?? false,
|
|
110
117
|
answers,
|
|
@@ -126,7 +133,13 @@ Project
|
|
|
126
133
|
--tailwind wire the Tailwind v4 bridge (default yes)
|
|
127
134
|
--no-tailwind plain CSS setup
|
|
128
135
|
--pm npm | pnpm | yarn | bun (default: whoever invoked us)
|
|
136
|
+
--library version spec for @forte-ui/react — exact
|
|
137
|
+
("1.0.0-alpha.4"), a dist-tag ("alpha"), or a range.
|
|
138
|
+
Default: latest.
|
|
129
139
|
--no-install write files only; skip installing dependencies
|
|
140
|
+
and the agent skill
|
|
141
|
+
--no-skill skip installing the forte-ui agent skill
|
|
142
|
+
(skills.sh — .agents/skills plus .claude/skills)
|
|
130
143
|
-y, --yes accept the defaults for everything not passed
|
|
131
144
|
|
|
132
145
|
Theme — every skipped value keeps the library default and writes NOTHING,
|
|
@@ -139,7 +152,10 @@ so the app keeps following the library when defaults are tuned.
|
|
|
139
152
|
--motion system (default) | reduce | full
|
|
140
153
|
"full" overrides the OS reduced-motion preference
|
|
141
154
|
for everyone — prefer leaving it unset.
|
|
142
|
-
--
|
|
155
|
+
--scheme system (default) | light | dark
|
|
156
|
+
"light" / "dark" pin one palette on <html> and
|
|
157
|
+
leave the theme toggle and its replay script out.
|
|
158
|
+
--font-sans a catalogue name ("Inter", "DM Sans", ...)
|
|
143
159
|
--font-mono a catalogue name ("JetBrains Mono", ...)
|
|
144
160
|
|
|
145
161
|
Design the theme visually instead: https://forte-ui.com/theme
|
package/dist/index.js
CHANGED
|
@@ -15,8 +15,8 @@ import pc from "picocolors";
|
|
|
15
15
|
import { parseCliArgs, UsageError, HELP } from "./args.js";
|
|
16
16
|
import { collectPlan } from "./prompts.js";
|
|
17
17
|
import { hexToOklch, validateSeed } from "./color.js";
|
|
18
|
-
import { applyOverlay, dependenciesFor, recordDependencies } from "./overlay.js";
|
|
19
|
-
import { detectPackageManager, runScaffolder, addDependencies, devCommand, installCommand, } from "./scaffold.js";
|
|
18
|
+
import { applyOverlay, dependenciesFor, recordDependencies, toAddSpecs, } from "./overlay.js";
|
|
19
|
+
import { detectPackageManager, runScaffolder, addDependencies, installSkill, devCommand, installCommand, } from "./scaffold.js";
|
|
20
20
|
function ownVersion() {
|
|
21
21
|
const pkgPath = path.join(fileURLToPath(import.meta.url), "../../package.json");
|
|
22
22
|
return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
|
|
@@ -41,7 +41,19 @@ function warnAboutSeeds(plan) {
|
|
|
41
41
|
}
|
|
42
42
|
function scaffold(plan, pm) {
|
|
43
43
|
if (plan.framework === "vite") {
|
|
44
|
-
return runScaffolder(pm, "create-vite@latest", [
|
|
44
|
+
return runScaffolder(pm, "create-vite@latest", [
|
|
45
|
+
plan.name,
|
|
46
|
+
"--template",
|
|
47
|
+
"react-ts",
|
|
48
|
+
/* create-vite 9 goes interactive whenever stdin is a TTY, template
|
|
49
|
+
* or not: it asks which linter to use and then "Install and start
|
|
50
|
+
* now?". A yes to that starts the dev server INSIDE our child
|
|
51
|
+
* process, so control never comes back and the overlay is never
|
|
52
|
+
* applied — the user ends up with a stock Vite app and no forte-ui.
|
|
53
|
+
* Non-interactive takes its defaults (Oxlint, no install), and the
|
|
54
|
+
* one install happens at the end with the library included. */
|
|
55
|
+
"--no-interactive",
|
|
56
|
+
], process.cwd());
|
|
45
57
|
}
|
|
46
58
|
return runScaffolder(pm, "create-next-app@latest", [
|
|
47
59
|
plan.name,
|
|
@@ -109,17 +121,35 @@ async function main() {
|
|
|
109
121
|
process.exit(1);
|
|
110
122
|
}
|
|
111
123
|
p.log.success(`Wired up forte-ui: ${written.join(", ")}`);
|
|
112
|
-
const deps = dependenciesFor(plan);
|
|
124
|
+
const deps = dependenciesFor(plan, opts.library);
|
|
125
|
+
const specs = toAddSpecs(deps);
|
|
113
126
|
if (opts.install) {
|
|
114
|
-
p.log.step(`Installing ${
|
|
115
|
-
if (!addDependencies(pm,
|
|
116
|
-
|
|
127
|
+
p.log.step(`Installing ${specs.join(", ")} with ${pm}…`);
|
|
128
|
+
if (!addDependencies(pm, specs, plan.dir)) {
|
|
129
|
+
/* A `--library` spec is validated only here, by the registry itself —
|
|
130
|
+
* so on failure say which spec died rather than suggesting a re-run
|
|
131
|
+
* that would resolve the exact same thing. */
|
|
132
|
+
p.cancel(opts.library
|
|
133
|
+
? `${pm} could not install @forte-ui/react@${opts.library} — see its output above. ` +
|
|
134
|
+
`Check that the --library spec exists on the registry.`
|
|
135
|
+
: `${pm} failed to install — run ${pc.bold(`${installCommand(pm)}`)} in ./${plan.name} yourself.`);
|
|
117
136
|
process.exit(1);
|
|
118
137
|
}
|
|
119
138
|
}
|
|
120
139
|
else {
|
|
121
140
|
recordDependencies(plan.dir, deps);
|
|
122
|
-
p.log.info(`Skipped install; added ${
|
|
141
|
+
p.log.info(`Skipped install; added ${specs.join(", ")} to package.json.`);
|
|
142
|
+
}
|
|
143
|
+
/* The agent skill, so AI tooling in the new project starts past the
|
|
144
|
+
* library's silent traps. Gated on `install` too: `--no-install` means
|
|
145
|
+
* "write files, run nothing". Failure is a warning, not an exit — the app
|
|
146
|
+
* is complete without it, and the guide's one-liner recovers it. */
|
|
147
|
+
if (opts.install && opts.skill) {
|
|
148
|
+
p.log.step("Installing the forte-ui agent skill…");
|
|
149
|
+
if (!installSkill(pm, plan.dir)) {
|
|
150
|
+
p.log.warn("Skill install failed — the app is unaffected. " +
|
|
151
|
+
`Run ${pc.bold("npx skills add arsen/forte-ui -s forte-ui")} in ./${plan.name} to retry.`);
|
|
152
|
+
}
|
|
123
153
|
}
|
|
124
154
|
const steps = [
|
|
125
155
|
`cd ${plan.name}`,
|
package/dist/overlay.d.ts
CHANGED
|
@@ -9,8 +9,16 @@ export type ProjectPlan = {
|
|
|
9
9
|
};
|
|
10
10
|
/** Returns the files it wrote (project-relative), for the summary. */
|
|
11
11
|
export declare function applyOverlay(plan: ProjectPlan): string[];
|
|
12
|
+
export type Dependency = {
|
|
13
|
+
name: string;
|
|
14
|
+
spec: string;
|
|
15
|
+
};
|
|
12
16
|
/** The `--no-install` fallback: record the dependencies so the user's own
|
|
13
|
-
* install resolves them. "latest"
|
|
14
|
-
* accepts and replaces with a real range on first install
|
|
15
|
-
|
|
16
|
-
export declare function
|
|
17
|
+
* install resolves them. The default spec is the "latest" dist-tag, which
|
|
18
|
+
* every manager accepts and replaces with a real range on first install —
|
|
19
|
+
* and so is anything `--library` put there instead, tag, range or exact. */
|
|
20
|
+
export declare function recordDependencies(dir: string, deps: Dependency[]): void;
|
|
21
|
+
/** `pm add` arguments: the bare name resolves `latest` exactly as before, so
|
|
22
|
+
* only a `--library` spec changes what gets asked of the registry. */
|
|
23
|
+
export declare function toAddSpecs(deps: Dependency[]): string[];
|
|
24
|
+
export declare function dependenciesFor(plan: ProjectPlan, library?: string): Dependency[];
|
package/dist/overlay.js
CHANGED
|
@@ -24,7 +24,7 @@ function applyVite({ name, dir, tailwind, answers }) {
|
|
|
24
24
|
written.push("src/index.css");
|
|
25
25
|
write(dir, "src/main.tsx", viteMainTsx(tailwind));
|
|
26
26
|
written.push("src/main.tsx");
|
|
27
|
-
write(dir, "src/App.tsx", viteAppTsx(name, tailwind));
|
|
27
|
+
write(dir, "src/App.tsx", viteAppTsx(name, tailwind, answers.scheme === "system"));
|
|
28
28
|
written.push("src/App.tsx");
|
|
29
29
|
/* The guide's warning covers App.css too: its rules are unlayered and the
|
|
30
30
|
* new App.tsx no longer imports it. Delete rather than empty, so nobody
|
|
@@ -47,6 +47,28 @@ function applyVite({ name, dir, tailwind, answers }) {
|
|
|
47
47
|
`the create-vite template may have changed; add the attribute(s) by hand.`);
|
|
48
48
|
}
|
|
49
49
|
html = anchored.replace(/<title>[^<]*<\/title>/, `<title>${name}</title>`);
|
|
50
|
+
/* Replay a stored light/dark choice before first paint — the guides'
|
|
51
|
+
* "Light and dark" step. A bundled component cannot run before the bundle,
|
|
52
|
+
* so on Vite the script lives in the document itself; the string must stay
|
|
53
|
+
* byte-identical to `themeInitScript` in @forte-ui/react, which is the
|
|
54
|
+
* other writer of the same localStorage key.
|
|
55
|
+
*
|
|
56
|
+
* Not when a scheme is pinned: `data-theme` is then static in the markup
|
|
57
|
+
* above, and the script would OVERRIDE it with any `forte-theme` record on
|
|
58
|
+
* the origin — which on localhost is shared by every project, so a toggle
|
|
59
|
+
* pressed in another app would flip this one's pinned palette. */
|
|
60
|
+
if (answers.scheme === "system") {
|
|
61
|
+
const themeReplay = " <!-- Replays a stored light/dark choice before first paint\n" +
|
|
62
|
+
" (themeInitScript from @forte-ui/react). -->\n" +
|
|
63
|
+
' <script>(function(){try{var t=localStorage.getItem("forte-theme");if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t)}catch(e){}})();</script>\n';
|
|
64
|
+
const replayed = html.replace(/[ \t]*<\/head>/, `${themeReplay} </head>`);
|
|
65
|
+
if (replayed === html) {
|
|
66
|
+
throw new Error("could not find '</head>' in index.html to add the theme replay script — " +
|
|
67
|
+
"the create-vite template may have changed; copy the snippet from the " +
|
|
68
|
+
"guide's 'Light and dark' step into <head> by hand.");
|
|
69
|
+
}
|
|
70
|
+
html = replayed;
|
|
71
|
+
}
|
|
50
72
|
/* Tailwind only: pin the cascade-layer order in the DOCUMENT, not the
|
|
51
73
|
* stylesheet. The bridge's own `@layer theme, base, forte, components,
|
|
52
74
|
* utilities;` statement is supposed to do this, but Vite's CSS pipeline
|
|
@@ -88,20 +110,28 @@ function applyNext({ name, dir, tailwind, answers }) {
|
|
|
88
110
|
return written;
|
|
89
111
|
}
|
|
90
112
|
/** The `--no-install` fallback: record the dependencies so the user's own
|
|
91
|
-
* install resolves them. "latest"
|
|
92
|
-
* accepts and replaces with a real range on first install
|
|
113
|
+
* install resolves them. The default spec is the "latest" dist-tag, which
|
|
114
|
+
* every manager accepts and replaces with a real range on first install —
|
|
115
|
+
* and so is anything `--library` put there instead, tag, range or exact. */
|
|
93
116
|
export function recordDependencies(dir, deps) {
|
|
94
117
|
const pkgPath = path.join(dir, "package.json");
|
|
95
118
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
96
119
|
pkg.dependencies ??= {};
|
|
97
120
|
for (const dep of deps)
|
|
98
|
-
pkg.dependencies[dep] =
|
|
121
|
+
pkg.dependencies[dep.name] = dep.spec;
|
|
99
122
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
100
123
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
124
|
+
/** `pm add` arguments: the bare name resolves `latest` exactly as before, so
|
|
125
|
+
* only a `--library` spec changes what gets asked of the registry. */
|
|
126
|
+
export function toAddSpecs(deps) {
|
|
127
|
+
return deps.map((d) => (d.spec === "latest" ? d.name : `${d.name}@${d.spec}`));
|
|
128
|
+
}
|
|
129
|
+
export function dependenciesFor(plan, library) {
|
|
130
|
+
/* `--library` steers ONLY the library. The Vite Tailwind extras stay on
|
|
131
|
+
* latest — the templates target current Tailwind v4 regardless of which
|
|
132
|
+
* forte-ui build is being tried out. */
|
|
133
|
+
const lib = { name: "@forte-ui/react", spec: library ?? "latest" };
|
|
104
134
|
return plan.framework === "vite" && plan.tailwind
|
|
105
|
-
? ["
|
|
106
|
-
: [
|
|
135
|
+
? [lib, { name: "tailwindcss", spec: "latest" }, { name: "@tailwindcss/vite", spec: "latest" }]
|
|
136
|
+
: [lib];
|
|
107
137
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -95,12 +95,12 @@ export async function collectPlan(opts) {
|
|
|
95
95
|
if (!opts.yes && !flagged.has("seed")) {
|
|
96
96
|
answers.seed = await hexPrompt("Accent color — your brand's hex, the whole palette derives from it")();
|
|
97
97
|
}
|
|
98
|
-
const advancedKeys = ["secondary", "tint", "radius", "density", "motion", "fontSans", "fontMono"];
|
|
98
|
+
const advancedKeys = ["secondary", "tint", "radius", "density", "motion", "scheme", "fontSans", "fontMono"];
|
|
99
99
|
const remaining = advancedKeys.filter((k) => !flagged.has(k));
|
|
100
100
|
const customize = !opts.yes &&
|
|
101
101
|
remaining.length > 0 &&
|
|
102
102
|
accept(await p.confirm({
|
|
103
|
-
message: "Customize further? (secondary, neutrals, radius, density, motion, fonts)",
|
|
103
|
+
message: "Customize further? (secondary, neutrals, radius, density, motion, light/dark, fonts)",
|
|
104
104
|
initialValue: false,
|
|
105
105
|
}));
|
|
106
106
|
if (customize) {
|
|
@@ -155,6 +155,17 @@ export async function collectPlan(opts) {
|
|
|
155
155
|
],
|
|
156
156
|
}));
|
|
157
157
|
}
|
|
158
|
+
if (remaining.includes("scheme")) {
|
|
159
|
+
answers.scheme = accept(await p.select({
|
|
160
|
+
message: "Light and dark",
|
|
161
|
+
initialValue: "system",
|
|
162
|
+
options: [
|
|
163
|
+
{ value: "system", label: "Both", hint: "follows the OS, with a toggle" },
|
|
164
|
+
{ value: "light", label: "Light only" },
|
|
165
|
+
{ value: "dark", label: "Dark only" },
|
|
166
|
+
],
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
158
169
|
if (remaining.includes("fontSans")) {
|
|
159
170
|
answers.fontSans = await fontPrompt("Sans font", SANS_FONTS);
|
|
160
171
|
}
|
package/dist/scaffold.d.ts
CHANGED
|
@@ -12,6 +12,13 @@ export declare function runScaffolder(pm: PackageManager, pkg: string, args: str
|
|
|
12
12
|
/** `pm add <deps>` in the project — resolves "latest" into a real caret range
|
|
13
13
|
* in package.json and performs the full install in one pass. */
|
|
14
14
|
export declare function addDependencies(pm: PackageManager, deps: string[], cwd: string): boolean;
|
|
15
|
+
/** Install the forte-ui agent skill into the project via skills.sh, the
|
|
16
|
+
* same command the AI-agents guide documents. `-s forte-ui` is load-bearing:
|
|
17
|
+
* the repository also carries internal skills (release-prep) that must not
|
|
18
|
+
* reach consumers. `-y` writes the universal `.agents/skills` copy plus the
|
|
19
|
+
* per-agent links — `.claude/skills` for Claude Code among them — without
|
|
20
|
+
* prompting. */
|
|
21
|
+
export declare function installSkill(pm: PackageManager, cwd: string): boolean;
|
|
15
22
|
/** The dev-server line for the outro, in the user's own manager. */
|
|
16
23
|
export declare function devCommand(pm: PackageManager): string;
|
|
17
24
|
export declare function installCommand(pm: PackageManager): string;
|
package/dist/scaffold.js
CHANGED
|
@@ -46,6 +46,15 @@ export function addDependencies(pm, deps, cwd) {
|
|
|
46
46
|
const sub = pm === "npm" ? "install" : "add";
|
|
47
47
|
return run(pm, [sub, ...deps], cwd);
|
|
48
48
|
}
|
|
49
|
+
/** Install the forte-ui agent skill into the project via skills.sh, the
|
|
50
|
+
* same command the AI-agents guide documents. `-s forte-ui` is load-bearing:
|
|
51
|
+
* the repository also carries internal skills (release-prep) that must not
|
|
52
|
+
* reach consumers. `-y` writes the universal `.agents/skills` copy plus the
|
|
53
|
+
* per-agent links — `.claude/skills` for Claude Code among them — without
|
|
54
|
+
* prompting. */
|
|
55
|
+
export function installSkill(pm, cwd) {
|
|
56
|
+
return runScaffolder(pm, "skills@latest", ["add", "arsen/forte-ui", "-s", "forte-ui", "-y"], cwd);
|
|
57
|
+
}
|
|
49
58
|
/** The dev-server line for the outro, in the user's own manager. */
|
|
50
59
|
export function devCommand(pm) {
|
|
51
60
|
return pm === "npm" ? "npm run dev" : `${pm} dev`;
|
package/dist/templates.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
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
|
-
|
|
4
|
+
/** `toggle` is false when the answers pinned a colour scheme: `data-theme`
|
|
5
|
+
* then sits statically on `<html>` and a toggle would be a button whose one
|
|
6
|
+
* job is to fight it. The import goes with the element, so the starter
|
|
7
|
+
* still compiles clean under `noUnusedLocals`. */
|
|
8
|
+
export declare function viteAppTsx(name: string, tailwind: boolean, toggle: boolean): string;
|
|
5
9
|
export declare const VITE_CONFIG_TW = "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport tailwindcss from \"@tailwindcss/vite\";\n\nexport default defineConfig({\n plugins: [react(), tailwindcss()],\n});\n";
|
|
6
10
|
export declare function nextGlobalsCss(a: ThemeAnswers, tailwind: boolean): string;
|
|
7
11
|
export declare function nextLayoutTsx(name: string, a: ThemeAnswers, tailwind: boolean): string;
|
package/dist/templates.js
CHANGED
|
@@ -18,6 +18,28 @@ const BODY_RULE = `body {
|
|
|
18
18
|
function joinBlocks(...blocks) {
|
|
19
19
|
return blocks.filter(Boolean).join("\n") + "\n";
|
|
20
20
|
}
|
|
21
|
+
/* The starter's one piece of chrome, and the working half of the guides'
|
|
22
|
+
* "Light and dark" step: the theme the questionnaire just seeded can be
|
|
23
|
+
* flipped without writing a line. Two spellings of the same corner, because
|
|
24
|
+
* the Tailwind paths have utilities and the plain ones do not.
|
|
25
|
+
*
|
|
26
|
+
* `fixed` rather than in flow, so the page still centres the component it is
|
|
27
|
+
* there to demonstrate — and the offsets are space tokens rather than `1rem`,
|
|
28
|
+
* because this file is the first thing a new app is copied out of and a
|
|
29
|
+
* hardcoded value here is the habit it teaches. */
|
|
30
|
+
const TOGGLE_TW = `<ThemeToggle className="fixed top-4 right-4" />`;
|
|
31
|
+
const TOGGLE_STYLE = `<ThemeToggle
|
|
32
|
+
style={{ position: "fixed", top: "var(--forte-space-4)", right: "var(--forte-space-4)" }}
|
|
33
|
+
/>`;
|
|
34
|
+
/** `TOGGLE_STYLE` re-indented to sit at `indent` spaces (its first line is
|
|
35
|
+
* placed by the caller, the continuation lines by this). */
|
|
36
|
+
function indented(block, indent) {
|
|
37
|
+
const pad = " ".repeat(indent);
|
|
38
|
+
return block
|
|
39
|
+
.split("\n")
|
|
40
|
+
.map((line, i) => (i === 0 ? line : pad + line))
|
|
41
|
+
.join("\n");
|
|
42
|
+
}
|
|
21
43
|
/* -------------------------------------------------------------------------
|
|
22
44
|
* Vite
|
|
23
45
|
* ---------------------------------------------------------------------- */
|
|
@@ -56,14 +78,19 @@ createRoot(document.getElementById("root")!).render(
|
|
|
56
78
|
);
|
|
57
79
|
`;
|
|
58
80
|
}
|
|
59
|
-
|
|
81
|
+
/** `toggle` is false when the answers pinned a colour scheme: `data-theme`
|
|
82
|
+
* then sits statically on `<html>` and a toggle would be a button whose one
|
|
83
|
+
* job is to fight it. The import goes with the element, so the starter
|
|
84
|
+
* still compiles clean under `noUnusedLocals`. */
|
|
85
|
+
export function viteAppTsx(name, tailwind, toggle) {
|
|
60
86
|
if (tailwind) {
|
|
61
|
-
|
|
87
|
+
const toggleLine = toggle ? ` ${TOGGLE_TW}\n` : "";
|
|
88
|
+
return `import { Button, Card${toggle ? ", ThemeToggle" : ""} } from "@forte-ui/react";
|
|
62
89
|
|
|
63
90
|
export default function App() {
|
|
64
91
|
return (
|
|
65
92
|
<main className="grid min-h-dvh place-items-center bg-background text-foreground">
|
|
66
|
-
<Card.Root variant="elevated" className="items-start gap-5">
|
|
93
|
+
${toggleLine} <Card.Root variant="elevated" className="items-start gap-5">
|
|
67
94
|
<h1 className="text-5 font-semibold">${name}</h1>
|
|
68
95
|
<p className="text-2 text-foreground-muted">
|
|
69
96
|
Utilities and components, one theme.
|
|
@@ -75,12 +102,13 @@ export default function App() {
|
|
|
75
102
|
}
|
|
76
103
|
`;
|
|
77
104
|
}
|
|
78
|
-
|
|
105
|
+
const toggleLine = toggle ? ` ${indented(TOGGLE_STYLE, 6)}\n` : "";
|
|
106
|
+
return `import { Button${toggle ? ", ThemeToggle" : ""} } from "@forte-ui/react";
|
|
79
107
|
|
|
80
108
|
export default function App() {
|
|
81
109
|
return (
|
|
82
110
|
<main style={{ display: "grid", placeItems: "center", minHeight: "100dvh" }}>
|
|
83
|
-
<Button>It works</Button>
|
|
111
|
+
${toggleLine} <Button>It works</Button>
|
|
84
112
|
</main>
|
|
85
113
|
);
|
|
86
114
|
}
|
|
@@ -113,8 +141,28 @@ export function nextLayoutTsx(name, a, tailwind) {
|
|
|
113
141
|
* globals.css imports it after the bridge (see the guide's ordering note),
|
|
114
142
|
* and a second import here would pin the `forte` layer before `base`. */
|
|
115
143
|
const themeImport = tailwind ? "" : `import "@forte-ui/react/theme.css";\n`;
|
|
144
|
+
/* ThemeScript + suppressHydrationWarning are the guides' "Light and dark"
|
|
145
|
+
* step: the script replays a stored light/dark choice onto <html> before
|
|
146
|
+
* first paint, and the suppression covers the attribute it legitimately
|
|
147
|
+
* adds. Harmless with nothing stored — the page just follows the OS. */
|
|
148
|
+
/* The toggle that writes what the script replays. It goes in the LAYOUT,
|
|
149
|
+
* not the page, because it is chrome: it then survives every route the app
|
|
150
|
+
* grows, and the page stays the one file a reader edits first. Being a flat
|
|
151
|
+
* export it needs no `"use client"` here — the boundary the compound
|
|
152
|
+
* components force on `page.tsx` does not reach the layout. */
|
|
153
|
+
/* A pinned scheme drops all three. The attribute is static, so there is
|
|
154
|
+
* nothing for the script to replay and nothing for the suppression to
|
|
155
|
+
* cover — and the script is actively wrong here, not merely idle: it
|
|
156
|
+
* replays whatever `forte-theme` record is on the origin, and on
|
|
157
|
+
* localhost every project shares one origin, so a toggle pressed in some
|
|
158
|
+
* OTHER app would flip this one's pinned palette on load. */
|
|
159
|
+
const pinned = a.scheme !== "system";
|
|
160
|
+
const toggle = tailwind ? TOGGLE_TW : indented(TOGGLE_STYLE, 8);
|
|
161
|
+
const libImport = pinned ? "" : `import { ThemeScript, ThemeToggle } from "@forte-ui/react";\n`;
|
|
162
|
+
const head = pinned ? "" : ` <head>\n <ThemeScript />\n </head>\n`;
|
|
163
|
+
const toggleLine = pinned ? "" : ` ${toggle}\n`;
|
|
116
164
|
return `import type { Metadata } from "next";
|
|
117
|
-
${font.importLine ? font.importLine + "\n" : ""}${themeImport}import "./globals.css";
|
|
165
|
+
${libImport}${font.importLine ? font.importLine + "\n" : ""}${themeImport}import "./globals.css";
|
|
118
166
|
|
|
119
167
|
${font.consts ? font.consts + "\n\n" : ""}export const metadata: Metadata = {
|
|
120
168
|
title: "${name}",
|
|
@@ -127,8 +175,10 @@ export default function RootLayout({
|
|
|
127
175
|
children: React.ReactNode;
|
|
128
176
|
}>) {
|
|
129
177
|
return (
|
|
130
|
-
<html lang="en"${htmlAttrs(a)}${font.htmlClassAttr}>
|
|
131
|
-
<body>
|
|
178
|
+
<html lang="en"${htmlAttrs(a)}${font.htmlClassAttr}${pinned ? "" : " suppressHydrationWarning"}>
|
|
179
|
+
${head} <body>
|
|
180
|
+
${toggleLine} {children}
|
|
181
|
+
</body>
|
|
132
182
|
</html>
|
|
133
183
|
);
|
|
134
184
|
}
|
package/dist/theme.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export declare const RADIUS: readonly ["none", "default", "soft", "pill"];
|
|
2
2
|
export declare const DENSITY: readonly ["compact", "default", "spacious"];
|
|
3
3
|
export declare const MOTION: readonly ["full", "default", "reduce"];
|
|
4
|
+
export declare const SCHEME: readonly ["system", "light", "dark"];
|
|
4
5
|
export type Radius = (typeof RADIUS)[number];
|
|
5
6
|
export type Density = (typeof DENSITY)[number];
|
|
6
7
|
export type Motion = (typeof MOTION)[number];
|
|
8
|
+
export type Scheme = (typeof SCHEME)[number];
|
|
7
9
|
export type ThemeAnswers = {
|
|
8
10
|
/** Accent seed hex, or null to keep the library default (write nothing). */
|
|
9
11
|
seed: string | null;
|
|
@@ -13,6 +15,9 @@ export type ThemeAnswers = {
|
|
|
13
15
|
radius: Radius;
|
|
14
16
|
density: Density;
|
|
15
17
|
motion: Motion;
|
|
18
|
+
/** "system" follows the OS and scaffolds the toggle; "light" / "dark" pin
|
|
19
|
+
* one palette on `<html>` and leave the toggle and its replay script out. */
|
|
20
|
+
scheme: Scheme;
|
|
16
21
|
/** Catalogue names. "System" writes nothing. */
|
|
17
22
|
fontSans: string;
|
|
18
23
|
fontMono: string;
|
|
@@ -33,7 +38,7 @@ export declare function rootBlock(a: ThemeAnswers, fontMode: FontMode): string;
|
|
|
33
38
|
export declare function fontImports(a: ThemeAnswers): string[];
|
|
34
39
|
/** ` data-forte-radius="pill" ...` — leading space included, "" when all
|
|
35
40
|
* defaults. Default modes stay UNSET so the app keeps following the OS
|
|
36
|
-
* (motion) and the library's own defaults. */
|
|
41
|
+
* (motion, colour scheme) and the library's own defaults. */
|
|
37
42
|
export declare function htmlAttrs(a: ThemeAnswers): string;
|
|
38
43
|
export type NextFontSetup = {
|
|
39
44
|
/** `import { Inter, JetBrains_Mono } from "next/font/google";` or "". */
|
package/dist/theme.js
CHANGED
|
@@ -14,6 +14,13 @@ import { hexToOklch, bestOnColor } from "./color.js";
|
|
|
14
14
|
export const RADIUS = ["none", "default", "soft", "pill"];
|
|
15
15
|
export const DENSITY = ["compact", "default", "spacious"];
|
|
16
16
|
export const MOTION = ["full", "default", "reduce"];
|
|
17
|
+
/* "system" is spelled out rather than being the "default" the other presets
|
|
18
|
+
* use, because it is also the flag value and the studio's strip label: the
|
|
19
|
+
* one mode that is not a mode. Pinning one writes `data-theme` on `<html>`,
|
|
20
|
+
* and that ATTRIBUTE is the whole mechanism — the stylesheet resolves every
|
|
21
|
+
* `light-dark()` through the `color-scheme` it sets, so a pinned app ships
|
|
22
|
+
* both palettes and simply never shows the other one. */
|
|
23
|
+
export const SCHEME = ["system", "light", "dark"];
|
|
17
24
|
export const DEFAULT_ANSWERS = {
|
|
18
25
|
seed: null,
|
|
19
26
|
secondary: null,
|
|
@@ -21,6 +28,7 @@ export const DEFAULT_ANSWERS = {
|
|
|
21
28
|
radius: "default",
|
|
22
29
|
density: "default",
|
|
23
30
|
motion: "default",
|
|
31
|
+
scheme: "system",
|
|
24
32
|
fontSans: "System",
|
|
25
33
|
fontMono: "System",
|
|
26
34
|
};
|
|
@@ -80,12 +88,13 @@ export function fontImports(a) {
|
|
|
80
88
|
}
|
|
81
89
|
/** ` data-forte-radius="pill" ...` — leading space included, "" when all
|
|
82
90
|
* defaults. Default modes stay UNSET so the app keeps following the OS
|
|
83
|
-
* (motion) and the library's own defaults. */
|
|
91
|
+
* (motion, colour scheme) and the library's own defaults. */
|
|
84
92
|
export function htmlAttrs(a) {
|
|
85
93
|
const attrs = [
|
|
86
94
|
a.radius !== "default" && `data-forte-radius="${a.radius}"`,
|
|
87
95
|
a.density !== "default" && `data-forte-density="${a.density}"`,
|
|
88
96
|
a.motion !== "default" && `data-forte-motion="${a.motion}"`,
|
|
97
|
+
a.scheme !== "system" && `data-theme="${a.scheme}"`,
|
|
89
98
|
].filter(Boolean);
|
|
90
99
|
return attrs.length ? " " + attrs.join(" ") : "";
|
|
91
100
|
}
|
package/package.json
CHANGED