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 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 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";
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
- --font-sans a catalogue name ("Inter", "DM Sans", ...)
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", [plan.name, "--template", "react-ts"], process.cwd());
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 ${deps.join(", ")} with ${pm}…`);
115
- if (!addDependencies(pm, deps, plan.dir)) {
116
- p.cancel(`${pm} failed to install run ${pc.bold(`${installCommand(pm)}`)} in ./${plan.name} yourself.`);
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 ${deps.join(", ")} to package.json.`);
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" is a dist-tag, which every manager
14
- * accepts and replaces with a real range on first install. */
15
- export declare function recordDependencies(dir: string, deps: string[]): void;
16
- export declare function dependenciesFor(plan: ProjectPlan): string[];
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" is a dist-tag, which every manager
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] = "latest";
121
+ pkg.dependencies[dep.name] = dep.spec;
99
122
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
100
123
  }
101
- export function dependenciesFor(plan) {
102
- /* create-next-app's --tailwind scaffold already carries tailwindcss v4;
103
- * on Vite the guide installs it alongside the library, plus the plugin. */
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
- ? ["@forte-ui/react", "tailwindcss", "@tailwindcss/vite"]
106
- : ["@forte-ui/react"];
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
  }
@@ -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`;
@@ -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
- export declare function viteAppTsx(name: string, tailwind: boolean): string;
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
- export function viteAppTsx(name, tailwind) {
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
- return `import { Button, Card } from "@forte-ui/react";
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
- return `import { Button } from "@forte-ui/react";
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>{children}</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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-forte-ui",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.6",
4
4
  "description": "Scaffold a new app wired up with forte-ui — Vite or Next.js, with or without Tailwind, themed from your answers.",
5
5
  "license": "MIT",
6
6
  "type": "module",