create-forte-ui 1.0.0-alpha.4
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 +46 -0
- package/dist/args.d.ts +19 -0
- package/dist/args.js +146 -0
- package/dist/color.d.ts +49 -0
- package/dist/color.js +96 -0
- package/dist/fonts.d.ts +23 -0
- package/dist/fonts.js +83 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +135 -0
- package/dist/overlay.d.ts +16 -0
- package/dist/overlay.js +107 -0
- package/dist/prompts.d.ts +4 -0
- package/dist/prompts.js +166 -0
- package/dist/scaffold.d.ts +17 -0
- package/dist/scaffold.js +55 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +172 -0
- package/dist/theme.d.ts +46 -0
- package/dist/theme.js +124 -0
- package/package.json +45 -0
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/* The forte-ui overlay: what turns an upstream scaffold into the guides'
|
|
2
|
+
* finished walkthrough. Wholesale replacement, not merging — every file
|
|
3
|
+
* touched here was written by the scaffolder seconds ago, and the guides'
|
|
4
|
+
* "replace, don't merge" warnings exist because the scaffold CSS is unlayered
|
|
5
|
+
* author CSS that would beat everything in `@layer forte.*`.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { htmlAttrs } from "./theme.js";
|
|
10
|
+
import { viteIndexCss, viteMainTsx, viteAppTsx, VITE_CONFIG_TW, nextGlobalsCss, nextLayoutTsx, nextPageTsx, } from "./templates.js";
|
|
11
|
+
function write(dir, rel, content) {
|
|
12
|
+
fs.writeFileSync(path.join(dir, rel), content);
|
|
13
|
+
}
|
|
14
|
+
function remove(dir, rel) {
|
|
15
|
+
fs.rmSync(path.join(dir, rel), { force: true });
|
|
16
|
+
}
|
|
17
|
+
/** Returns the files it wrote (project-relative), for the summary. */
|
|
18
|
+
export function applyOverlay(plan) {
|
|
19
|
+
return plan.framework === "vite" ? applyVite(plan) : applyNext(plan);
|
|
20
|
+
}
|
|
21
|
+
function applyVite({ name, dir, tailwind, answers }) {
|
|
22
|
+
const written = [];
|
|
23
|
+
write(dir, "src/index.css", viteIndexCss(answers, tailwind));
|
|
24
|
+
written.push("src/index.css");
|
|
25
|
+
write(dir, "src/main.tsx", viteMainTsx(tailwind));
|
|
26
|
+
written.push("src/main.tsx");
|
|
27
|
+
write(dir, "src/App.tsx", viteAppTsx(name, tailwind));
|
|
28
|
+
written.push("src/App.tsx");
|
|
29
|
+
/* The guide's warning covers App.css too: its rules are unlayered and the
|
|
30
|
+
* new App.tsx no longer imports it. Delete rather than empty, so nobody
|
|
31
|
+
* re-imports a file that looks like it should exist. */
|
|
32
|
+
remove(dir, "src/App.css");
|
|
33
|
+
if (tailwind) {
|
|
34
|
+
write(dir, "vite.config.ts", VITE_CONFIG_TW);
|
|
35
|
+
written.push("vite.config.ts");
|
|
36
|
+
}
|
|
37
|
+
/* Vite's `<html>` lives in index.html. Anchored replacements rather than a
|
|
38
|
+
* rewrite — the rest of the file (favicon, root div, module script) is the
|
|
39
|
+
* scaffolder's to evolve. A missed anchor is a template change upstream:
|
|
40
|
+
* report it, never guess. */
|
|
41
|
+
const htmlPath = path.join(dir, "index.html");
|
|
42
|
+
let html = fs.readFileSync(htmlPath, "utf8");
|
|
43
|
+
const attrs = htmlAttrs(answers);
|
|
44
|
+
const anchored = html.replace(/<html lang="en">/, `<html lang="en"${attrs}>`);
|
|
45
|
+
if (attrs && anchored === html) {
|
|
46
|
+
throw new Error(`could not find '<html lang="en">' in index.html to add${attrs} — ` +
|
|
47
|
+
`the create-vite template may have changed; add the attribute(s) by hand.`);
|
|
48
|
+
}
|
|
49
|
+
html = anchored.replace(/<title>[^<]*<\/title>/, `<title>${name}</title>`);
|
|
50
|
+
/* Tailwind only: pin the cascade-layer order in the DOCUMENT, not the
|
|
51
|
+
* stylesheet. The bridge's own `@layer theme, base, forte, components,
|
|
52
|
+
* utilities;` statement is supposed to do this, but Vite's CSS pipeline
|
|
53
|
+
* (Tailwind's compiler re-slotting the statement, lightningcss merging
|
|
54
|
+
* statements under minification, chunk concatenation order) rewrites it,
|
|
55
|
+
* and the observed result is `base` first appearing AFTER `forte` — at
|
|
56
|
+
* which point Preflight's `button { background: transparent }` beats every
|
|
57
|
+
* component by layer order and buttons render as bare text. An inline
|
|
58
|
+
* <style> ahead of every stylesheet is untouchable by that pipeline, and
|
|
59
|
+
* layer order is fixed at first appearance, so nothing later can unpin it.
|
|
60
|
+
* Next.js needs none of this — its pipeline emits the statement in order. */
|
|
61
|
+
if (tailwind) {
|
|
62
|
+
const pin = " <!-- Pins the cascade-layer order before any stylesheet loads; the CSS\n" +
|
|
63
|
+
" bundler can reorder @layer statements, and first appearance wins. -->\n" +
|
|
64
|
+
" <style>@layer theme, base, forte, components, utilities;</style>\n";
|
|
65
|
+
const pinned = html.replace(/[ \t]*<\/head>/, `${pin} </head>`);
|
|
66
|
+
if (pinned === html) {
|
|
67
|
+
throw new Error("could not find '</head>' in index.html to pin the cascade-layer order — " +
|
|
68
|
+
"the create-vite template may have changed; add " +
|
|
69
|
+
"<style>@layer theme, base, forte, components, utilities;</style> to <head> by hand.");
|
|
70
|
+
}
|
|
71
|
+
html = pinned;
|
|
72
|
+
}
|
|
73
|
+
fs.writeFileSync(htmlPath, html);
|
|
74
|
+
written.push("index.html");
|
|
75
|
+
return written;
|
|
76
|
+
}
|
|
77
|
+
function applyNext({ name, dir, tailwind, answers }) {
|
|
78
|
+
const written = [];
|
|
79
|
+
write(dir, "app/globals.css", nextGlobalsCss(answers, tailwind));
|
|
80
|
+
written.push("app/globals.css");
|
|
81
|
+
write(dir, "app/layout.tsx", nextLayoutTsx(name, answers, tailwind));
|
|
82
|
+
written.push("app/layout.tsx");
|
|
83
|
+
write(dir, "app/page.tsx", nextPageTsx(name, tailwind));
|
|
84
|
+
written.push("app/page.tsx");
|
|
85
|
+
/* The plain scaffold ships a page.module.css the new page no longer
|
|
86
|
+
* imports; the Tailwind scaffold has none, so `force` covers both. */
|
|
87
|
+
remove(dir, "app/page.module.css");
|
|
88
|
+
return written;
|
|
89
|
+
}
|
|
90
|
+
/** 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. */
|
|
93
|
+
export function recordDependencies(dir, deps) {
|
|
94
|
+
const pkgPath = path.join(dir, "package.json");
|
|
95
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
96
|
+
pkg.dependencies ??= {};
|
|
97
|
+
for (const dep of deps)
|
|
98
|
+
pkg.dependencies[dep] = "latest";
|
|
99
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
100
|
+
}
|
|
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. */
|
|
104
|
+
return plan.framework === "vite" && plan.tailwind
|
|
105
|
+
? ["@forte-ui/react", "tailwindcss", "@tailwindcss/vite"]
|
|
106
|
+
: ["@forte-ui/react"];
|
|
107
|
+
}
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/* The questionnaire. Four questions get you to a running app; everything
|
|
2
|
+
* else hides behind one "customize further?" gate, because six theme prompts
|
|
3
|
+
* up front reads as a form, and the people who skipped the Theme Studio
|
|
4
|
+
* mostly want `dev` running. A flag answers its question before it is asked.
|
|
5
|
+
*/
|
|
6
|
+
import * as p from "@clack/prompts";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { SANS_FONTS, MONO_FONTS } from "./fonts.js";
|
|
10
|
+
import { hexToOklch } from "./color.js";
|
|
11
|
+
import { DEFAULT_ANSWERS } from "./theme.js";
|
|
12
|
+
import { UsageError } from "./args.js";
|
|
13
|
+
function accept(value) {
|
|
14
|
+
if (p.isCancel(value)) {
|
|
15
|
+
p.cancel("Cancelled — nothing was written.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
export function validateProjectName(name) {
|
|
21
|
+
if (!name)
|
|
22
|
+
return "A project name is required.";
|
|
23
|
+
if (name === "." || name.includes("/") || name.includes("\\")) {
|
|
24
|
+
return "Pass a new directory name — scaffolding into an existing directory is not supported.";
|
|
25
|
+
}
|
|
26
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
|
|
27
|
+
return "Use a lowercase npm-style name: letters, digits, dots, dashes.";
|
|
28
|
+
}
|
|
29
|
+
if (fs.existsSync(path.resolve(name)))
|
|
30
|
+
return `"${name}" already exists here.`;
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const hexPrompt = (message) => async () => {
|
|
34
|
+
const value = accept(await p.text({
|
|
35
|
+
message,
|
|
36
|
+
placeholder: "Enter to keep the library default",
|
|
37
|
+
validate: (v) => {
|
|
38
|
+
if (!v)
|
|
39
|
+
return undefined;
|
|
40
|
+
return hexToOklch(v.startsWith("#") ? v : `#${v}`) ? undefined : "Expected a hex colour like #6d43d4.";
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
if (!value)
|
|
44
|
+
return null;
|
|
45
|
+
return (value.startsWith("#") ? value : `#${value}`).toLowerCase();
|
|
46
|
+
};
|
|
47
|
+
async function fontPrompt(message, list) {
|
|
48
|
+
return accept(await p.select({
|
|
49
|
+
message,
|
|
50
|
+
initialValue: "System",
|
|
51
|
+
options: list.map((f) => ({
|
|
52
|
+
value: f.name,
|
|
53
|
+
label: f.name,
|
|
54
|
+
hint: f.stack === null ? "keep the library default" : undefined,
|
|
55
|
+
})),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
export async function collectPlan(opts) {
|
|
59
|
+
if (opts.yes && !opts.name) {
|
|
60
|
+
throw new UsageError("--yes needs a project name: create-forte-ui my-app --yes");
|
|
61
|
+
}
|
|
62
|
+
let name = opts.name;
|
|
63
|
+
if (name) {
|
|
64
|
+
const problem = validateProjectName(name);
|
|
65
|
+
if (problem)
|
|
66
|
+
throw new UsageError(problem);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
name = accept(await p.text({
|
|
70
|
+
message: "Project name",
|
|
71
|
+
placeholder: "my-app",
|
|
72
|
+
validate: (v) => validateProjectName(v ?? ""),
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
const framework = opts.framework ??
|
|
76
|
+
(opts.yes
|
|
77
|
+
? "next"
|
|
78
|
+
: accept(await p.select({
|
|
79
|
+
message: "Framework",
|
|
80
|
+
initialValue: "next",
|
|
81
|
+
options: [
|
|
82
|
+
{ value: "next", label: "Next.js", hint: "App Router" },
|
|
83
|
+
{ value: "vite", label: "Vite", hint: "react-ts template" },
|
|
84
|
+
],
|
|
85
|
+
})));
|
|
86
|
+
const tailwind = opts.tailwind ??
|
|
87
|
+
(opts.yes
|
|
88
|
+
? true
|
|
89
|
+
: accept(await p.confirm({
|
|
90
|
+
message: "Tailwind? (wires the token bridge — utilities follow your theme)",
|
|
91
|
+
initialValue: true,
|
|
92
|
+
})));
|
|
93
|
+
const answers = { ...DEFAULT_ANSWERS, ...opts.answers };
|
|
94
|
+
const flagged = new Set(Object.keys(opts.answers));
|
|
95
|
+
if (!opts.yes && !flagged.has("seed")) {
|
|
96
|
+
answers.seed = await hexPrompt("Accent color — your brand's hex, the whole palette derives from it")();
|
|
97
|
+
}
|
|
98
|
+
const advancedKeys = ["secondary", "tint", "radius", "density", "motion", "fontSans", "fontMono"];
|
|
99
|
+
const remaining = advancedKeys.filter((k) => !flagged.has(k));
|
|
100
|
+
const customize = !opts.yes &&
|
|
101
|
+
remaining.length > 0 &&
|
|
102
|
+
accept(await p.confirm({
|
|
103
|
+
message: "Customize further? (secondary, neutrals, radius, density, motion, fonts)",
|
|
104
|
+
initialValue: false,
|
|
105
|
+
}));
|
|
106
|
+
if (customize) {
|
|
107
|
+
if (remaining.includes("secondary")) {
|
|
108
|
+
answers.secondary = await hexPrompt("Secondary color — hex")();
|
|
109
|
+
}
|
|
110
|
+
if (remaining.includes("tint")) {
|
|
111
|
+
const tint = accept(await p.text({
|
|
112
|
+
message: "Neutral tint — 0 (pure grey) to 1 (full brand tint)",
|
|
113
|
+
placeholder: "1",
|
|
114
|
+
validate: (v) => {
|
|
115
|
+
if (!v)
|
|
116
|
+
return undefined;
|
|
117
|
+
const n = Number(v);
|
|
118
|
+
return Number.isFinite(n) && n >= 0 && n <= 1 ? undefined : "A number between 0 and 1.";
|
|
119
|
+
},
|
|
120
|
+
}));
|
|
121
|
+
if (tint)
|
|
122
|
+
answers.tint = Number(tint);
|
|
123
|
+
}
|
|
124
|
+
if (remaining.includes("radius")) {
|
|
125
|
+
answers.radius = accept(await p.select({
|
|
126
|
+
message: "Radius preset",
|
|
127
|
+
initialValue: "default",
|
|
128
|
+
options: [
|
|
129
|
+
{ value: "default", label: "Default" },
|
|
130
|
+
{ value: "none", label: "None", hint: "sharp corners everywhere" },
|
|
131
|
+
{ value: "soft", label: "Soft", hint: "one step rounder" },
|
|
132
|
+
{ value: "pill", label: "Pill", hint: "fully rounded controls" },
|
|
133
|
+
],
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
if (remaining.includes("density")) {
|
|
137
|
+
answers.density = accept(await p.select({
|
|
138
|
+
message: "Density preset",
|
|
139
|
+
initialValue: "default",
|
|
140
|
+
options: [
|
|
141
|
+
{ value: "default", label: "Comfortable", hint: "the default" },
|
|
142
|
+
{ value: "compact", label: "Compact" },
|
|
143
|
+
{ value: "spacious", label: "Spacious" },
|
|
144
|
+
],
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
if (remaining.includes("motion")) {
|
|
148
|
+
answers.motion = accept(await p.select({
|
|
149
|
+
message: "Motion",
|
|
150
|
+
initialValue: "default",
|
|
151
|
+
options: [
|
|
152
|
+
{ value: "default", label: "System", hint: "follows the OS reduced-motion setting" },
|
|
153
|
+
{ value: "reduce", label: "Reduce", hint: "geometry collapses, fades stay" },
|
|
154
|
+
{ value: "full", label: "Full", hint: "overrides the OS preference for everyone" },
|
|
155
|
+
],
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
if (remaining.includes("fontSans")) {
|
|
159
|
+
answers.fontSans = await fontPrompt("Sans font", SANS_FONTS);
|
|
160
|
+
}
|
|
161
|
+
if (remaining.includes("fontMono")) {
|
|
162
|
+
answers.fontMono = await fontPrompt("Mono font", MONO_FONTS);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { name, dir: path.resolve(name), framework, tailwind, answers };
|
|
166
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
|
|
2
|
+
export declare const PACKAGE_MANAGERS: readonly PackageManager[];
|
|
3
|
+
/** `npm_config_user_agent` is set by every package manager for its child
|
|
4
|
+
* processes ("pnpm/9.0.0 npm/? node/v20..."), so `pnpm create forte-ui`
|
|
5
|
+
* self-identifies. Direct `npx` invocation reports npm, which is right. */
|
|
6
|
+
export declare function detectPackageManager(): PackageManager;
|
|
7
|
+
/** Run a `create-*` package without installing it, through the invoking
|
|
8
|
+
* manager's own runner where it has one. Yarn Classic has no `dlx`, so it
|
|
9
|
+
* falls back to npx — npm is a Node guarantee, Berry users still get the
|
|
10
|
+
* right lockfile from the later install step. */
|
|
11
|
+
export declare function runScaffolder(pm: PackageManager, pkg: string, args: string[], cwd: string): boolean;
|
|
12
|
+
/** `pm add <deps>` in the project — resolves "latest" into a real caret range
|
|
13
|
+
* in package.json and performs the full install in one pass. */
|
|
14
|
+
export declare function addDependencies(pm: PackageManager, deps: string[], cwd: string): boolean;
|
|
15
|
+
/** The dev-server line for the outro, in the user's own manager. */
|
|
16
|
+
export declare function devCommand(pm: PackageManager): string;
|
|
17
|
+
export declare function installCommand(pm: PackageManager): string;
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* Process plumbing: which package manager invoked us, and how to run the
|
|
2
|
+
* upstream scaffolders through it. The framework templates are deliberately
|
|
3
|
+
* NOT ours — `create-vite` and `create-next-app` keep their own scaffolds
|
|
4
|
+
* current, and this CLI only owns the forte-ui overlay on top. That division
|
|
5
|
+
* is what keeps the tool from rotting the way frozen templates do.
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
export const PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
9
|
+
/** `npm_config_user_agent` is set by every package manager for its child
|
|
10
|
+
* processes ("pnpm/9.0.0 npm/? node/v20..."), so `pnpm create forte-ui`
|
|
11
|
+
* self-identifies. Direct `npx` invocation reports npm, which is right. */
|
|
12
|
+
export function detectPackageManager() {
|
|
13
|
+
const agent = process.env.npm_config_user_agent ?? "";
|
|
14
|
+
if (agent.startsWith("pnpm"))
|
|
15
|
+
return "pnpm";
|
|
16
|
+
if (agent.startsWith("yarn"))
|
|
17
|
+
return "yarn";
|
|
18
|
+
if (agent.startsWith("bun"))
|
|
19
|
+
return "bun";
|
|
20
|
+
return "npm";
|
|
21
|
+
}
|
|
22
|
+
/* Windows resolves `npx`/`pnpm` through .cmd shims, which spawnSync only
|
|
23
|
+
* finds with a shell. */
|
|
24
|
+
const needsShell = process.platform === "win32";
|
|
25
|
+
function run(cmd, args, cwd) {
|
|
26
|
+
const result = spawnSync(cmd, args, { stdio: "inherit", cwd, shell: needsShell });
|
|
27
|
+
return result.status === 0;
|
|
28
|
+
}
|
|
29
|
+
/** Run a `create-*` package without installing it, through the invoking
|
|
30
|
+
* manager's own runner where it has one. Yarn Classic has no `dlx`, so it
|
|
31
|
+
* falls back to npx — npm is a Node guarantee, Berry users still get the
|
|
32
|
+
* right lockfile from the later install step. */
|
|
33
|
+
export function runScaffolder(pm, pkg, args, cwd) {
|
|
34
|
+
switch (pm) {
|
|
35
|
+
case "pnpm":
|
|
36
|
+
return run("pnpm", ["dlx", pkg, ...args], cwd);
|
|
37
|
+
case "bun":
|
|
38
|
+
return run("bunx", [pkg, ...args], cwd);
|
|
39
|
+
default:
|
|
40
|
+
return run("npx", ["--yes", pkg, ...args], cwd);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** `pm add <deps>` in the project — resolves "latest" into a real caret range
|
|
44
|
+
* in package.json and performs the full install in one pass. */
|
|
45
|
+
export function addDependencies(pm, deps, cwd) {
|
|
46
|
+
const sub = pm === "npm" ? "install" : "add";
|
|
47
|
+
return run(pm, [sub, ...deps], cwd);
|
|
48
|
+
}
|
|
49
|
+
/** The dev-server line for the outro, in the user's own manager. */
|
|
50
|
+
export function devCommand(pm) {
|
|
51
|
+
return pm === "npm" ? "npm run dev" : `${pm} dev`;
|
|
52
|
+
}
|
|
53
|
+
export function installCommand(pm) {
|
|
54
|
+
return pm === "yarn" ? "yarn" : `${pm} install`;
|
|
55
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ThemeAnswers } from "./theme.js";
|
|
2
|
+
export declare function viteIndexCss(a: ThemeAnswers, tailwind: boolean): string;
|
|
3
|
+
export declare function viteMainTsx(tailwind: boolean): string;
|
|
4
|
+
export declare function viteAppTsx(name: string, tailwind: boolean): string;
|
|
5
|
+
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
|
+
export declare function nextGlobalsCss(a: ThemeAnswers, tailwind: boolean): string;
|
|
7
|
+
export declare function nextLayoutTsx(name: string, a: ThemeAnswers, tailwind: boolean): string;
|
|
8
|
+
export declare function nextPageTsx(name: string, tailwind: boolean): string;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/* The files the overlay writes, one builder per file. The content is the
|
|
2
|
+
* getting-started guides' snippets, parameterised — the guides are the spec,
|
|
3
|
+
* and keeping these byte-close to the published steps is what lets a reader
|
|
4
|
+
* diff a scaffolded app against the walkthrough and see only their own
|
|
5
|
+
* answers. When a guide step changes, change the builder with it.
|
|
6
|
+
*/
|
|
7
|
+
import { rootBlock, fontImports, htmlAttrs, nextFontSetup } from "./theme.js";
|
|
8
|
+
/* The body rule from the guides' "Replace the scaffold CSS" step. Present on
|
|
9
|
+
* the non-Tailwind paths only — the Tailwind starter page carries the same
|
|
10
|
+
* three declarations as utilities on `<main>`. */
|
|
11
|
+
const BODY_RULE = `body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
background: var(--forte-color-background);
|
|
14
|
+
color: var(--forte-color-foreground);
|
|
15
|
+
font-family: var(--forte-font-sans);
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
18
|
+
function joinBlocks(...blocks) {
|
|
19
|
+
return blocks.filter(Boolean).join("\n") + "\n";
|
|
20
|
+
}
|
|
21
|
+
/* -------------------------------------------------------------------------
|
|
22
|
+
* Vite
|
|
23
|
+
* ---------------------------------------------------------------------- */
|
|
24
|
+
export function viteIndexCss(a, tailwind) {
|
|
25
|
+
const imports = fontImports(a);
|
|
26
|
+
const fontBlock = imports.length
|
|
27
|
+
? `/* Or self-host these — any @font-face works. */\n${imports.join("\n")}\n`
|
|
28
|
+
: "";
|
|
29
|
+
if (tailwind) {
|
|
30
|
+
/* Ordering is load-bearing (see the guide): the bridge's first line pins
|
|
31
|
+
* the cascade-layer order, so it must precede `tailwindcss` and
|
|
32
|
+
* `theme.css`. The font imports carry no layer statements — and @import
|
|
33
|
+
* must precede every other statement — so they lead the file. */
|
|
34
|
+
return joinBlocks(fontBlock, `@import "@forte-ui/react/tailwind.css";
|
|
35
|
+
@import "tailwindcss";
|
|
36
|
+
@import "@forte-ui/react/theme.css";
|
|
37
|
+
`, rootBlock(a, "import"));
|
|
38
|
+
}
|
|
39
|
+
return joinBlocks(fontBlock, rootBlock(a, "import"), BODY_RULE);
|
|
40
|
+
}
|
|
41
|
+
export function viteMainTsx(tailwind) {
|
|
42
|
+
/* On the Tailwind path `theme.css` is imported from index.css AFTER the
|
|
43
|
+
* bridge — importing it here would pin the `forte` layer first and hand
|
|
44
|
+
* Preflight the win. Without Tailwind there is no ordering hazard and the
|
|
45
|
+
* guide imports it at the entry point. */
|
|
46
|
+
const themeImport = tailwind ? "" : `import "@forte-ui/react/theme.css";\n`;
|
|
47
|
+
return `import { StrictMode } from "react";
|
|
48
|
+
import { createRoot } from "react-dom/client";
|
|
49
|
+
${themeImport}import "./index.css";
|
|
50
|
+
import App from "./App.tsx";
|
|
51
|
+
|
|
52
|
+
createRoot(document.getElementById("root")!).render(
|
|
53
|
+
<StrictMode>
|
|
54
|
+
<App />
|
|
55
|
+
</StrictMode>,
|
|
56
|
+
);
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
export function viteAppTsx(name, tailwind) {
|
|
60
|
+
if (tailwind) {
|
|
61
|
+
return `import { Button, Card } from "@forte-ui/react";
|
|
62
|
+
|
|
63
|
+
export default function App() {
|
|
64
|
+
return (
|
|
65
|
+
<main className="grid min-h-dvh place-items-center bg-background text-foreground">
|
|
66
|
+
<Card.Root variant="elevated" className="items-start gap-5">
|
|
67
|
+
<h1 className="text-5 font-semibold">${name}</h1>
|
|
68
|
+
<p className="text-2 text-foreground-muted">
|
|
69
|
+
Utilities and components, one theme.
|
|
70
|
+
</p>
|
|
71
|
+
<Button>It works</Button>
|
|
72
|
+
</Card.Root>
|
|
73
|
+
</main>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
return `import { Button } from "@forte-ui/react";
|
|
79
|
+
|
|
80
|
+
export default function App() {
|
|
81
|
+
return (
|
|
82
|
+
<main style={{ display: "grid", placeItems: "center", minHeight: "100dvh" }}>
|
|
83
|
+
<Button>It works</Button>
|
|
84
|
+
</main>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
export const VITE_CONFIG_TW = `import { defineConfig } from "vite";
|
|
90
|
+
import react from "@vitejs/plugin-react";
|
|
91
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
92
|
+
|
|
93
|
+
export default defineConfig({
|
|
94
|
+
plugins: [react(), tailwindcss()],
|
|
95
|
+
});
|
|
96
|
+
`;
|
|
97
|
+
/* -------------------------------------------------------------------------
|
|
98
|
+
* Next.js
|
|
99
|
+
* ---------------------------------------------------------------------- */
|
|
100
|
+
export function nextGlobalsCss(a, tailwind) {
|
|
101
|
+
if (tailwind) {
|
|
102
|
+
return joinBlocks(`@import "@forte-ui/react/tailwind.css";
|
|
103
|
+
@import "tailwindcss";
|
|
104
|
+
@import "@forte-ui/react/theme.css";
|
|
105
|
+
`, rootBlock(a, "next-font"));
|
|
106
|
+
}
|
|
107
|
+
return joinBlocks(rootBlock(a, "next-font"), BODY_RULE);
|
|
108
|
+
}
|
|
109
|
+
export function nextLayoutTsx(name, a, tailwind) {
|
|
110
|
+
const font = nextFontSetup(a);
|
|
111
|
+
/* Without Tailwind, `theme.css` is imported here, above globals — safe
|
|
112
|
+
* because nothing else declares layers. With Tailwind it must NOT be:
|
|
113
|
+
* globals.css imports it after the bridge (see the guide's ordering note),
|
|
114
|
+
* and a second import here would pin the `forte` layer before `base`. */
|
|
115
|
+
const themeImport = tailwind ? "" : `import "@forte-ui/react/theme.css";\n`;
|
|
116
|
+
return `import type { Metadata } from "next";
|
|
117
|
+
${font.importLine ? font.importLine + "\n" : ""}${themeImport}import "./globals.css";
|
|
118
|
+
|
|
119
|
+
${font.consts ? font.consts + "\n\n" : ""}export const metadata: Metadata = {
|
|
120
|
+
title: "${name}",
|
|
121
|
+
description: "Scaffolded by create-forte-ui",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export default function RootLayout({
|
|
125
|
+
children,
|
|
126
|
+
}: Readonly<{
|
|
127
|
+
children: React.ReactNode;
|
|
128
|
+
}>) {
|
|
129
|
+
return (
|
|
130
|
+
<html lang="en"${htmlAttrs(a)}${font.htmlClassAttr}>
|
|
131
|
+
<body>{children}</body>
|
|
132
|
+
</html>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
export function nextPageTsx(name, tailwind) {
|
|
138
|
+
if (tailwind) {
|
|
139
|
+
/* "use client" because of Card.Root: a server component receives a
|
|
140
|
+
* client-reference proxy for the namespace, property access resolves to
|
|
141
|
+
* undefined, and prerendering throws "Element type is invalid". The
|
|
142
|
+
* flat-export page below needs no boundary. */
|
|
143
|
+
return `"use client";
|
|
144
|
+
|
|
145
|
+
import { Button, Card } from "@forte-ui/react";
|
|
146
|
+
|
|
147
|
+
export default function Home() {
|
|
148
|
+
return (
|
|
149
|
+
<main className="grid min-h-dvh place-items-center bg-background text-foreground">
|
|
150
|
+
<Card.Root variant="elevated" className="items-start gap-5">
|
|
151
|
+
<h1 className="text-5 font-semibold">${name}</h1>
|
|
152
|
+
<p className="text-2 text-foreground-muted">
|
|
153
|
+
Utilities and components, one theme.
|
|
154
|
+
</p>
|
|
155
|
+
<Button>It works</Button>
|
|
156
|
+
</Card.Root>
|
|
157
|
+
</main>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
return `import { Button } from "@forte-ui/react";
|
|
163
|
+
|
|
164
|
+
export default function Home() {
|
|
165
|
+
return (
|
|
166
|
+
<main style={{ display: "grid", placeItems: "center", minHeight: "100dvh" }}>
|
|
167
|
+
<Button>It works</Button>
|
|
168
|
+
</main>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
`;
|
|
172
|
+
}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export declare const RADIUS: readonly ["none", "default", "soft", "pill"];
|
|
2
|
+
export declare const DENSITY: readonly ["compact", "default", "spacious"];
|
|
3
|
+
export declare const MOTION: readonly ["full", "default", "reduce"];
|
|
4
|
+
export type Radius = (typeof RADIUS)[number];
|
|
5
|
+
export type Density = (typeof DENSITY)[number];
|
|
6
|
+
export type Motion = (typeof MOTION)[number];
|
|
7
|
+
export type ThemeAnswers = {
|
|
8
|
+
/** Accent seed hex, or null to keep the library default (write nothing). */
|
|
9
|
+
seed: string | null;
|
|
10
|
+
secondary: string | null;
|
|
11
|
+
/** 0 = pure grey neutrals, 1 = full brand tint. 1 is the shipped default. */
|
|
12
|
+
tint: number;
|
|
13
|
+
radius: Radius;
|
|
14
|
+
density: Density;
|
|
15
|
+
motion: Motion;
|
|
16
|
+
/** Catalogue names. "System" writes nothing. */
|
|
17
|
+
fontSans: string;
|
|
18
|
+
fontMono: string;
|
|
19
|
+
};
|
|
20
|
+
export declare const DEFAULT_ANSWERS: ThemeAnswers;
|
|
21
|
+
/** How the chosen fonts reach `--forte-font-sans` / `--forte-font-mono`:
|
|
22
|
+
* Vite loads the css2 stylesheet by `@import` and writes the stack verbatim;
|
|
23
|
+
* Next loads through `next/font/google` and points the token at the variable
|
|
24
|
+
* the loader defines. Same answer, framework-idiomatic serialisation. */
|
|
25
|
+
export type FontMode = "import" | "next-font";
|
|
26
|
+
/** The declarations for the `:root` block, one string per line, WITHOUT the
|
|
27
|
+
* surrounding braces. Empty array = every answer was a default = no block. */
|
|
28
|
+
export declare function rootDeclarations(a: ThemeAnswers, fontMode: FontMode): string[];
|
|
29
|
+
/** A complete `:root { ... }` block, or "" when nothing deviates. */
|
|
30
|
+
export declare function rootBlock(a: ThemeAnswers, fontMode: FontMode): string;
|
|
31
|
+
/** `@import url(...)` lines for the chosen Google fonts (Vite path). Font
|
|
32
|
+
* imports carry no layer statements, so they are safe ahead of the bridge. */
|
|
33
|
+
export declare function fontImports(a: ThemeAnswers): string[];
|
|
34
|
+
/** ` data-forte-radius="pill" ...` — leading space included, "" when all
|
|
35
|
+
* defaults. Default modes stay UNSET so the app keeps following the OS
|
|
36
|
+
* (motion) and the library's own defaults. */
|
|
37
|
+
export declare function htmlAttrs(a: ThemeAnswers): string;
|
|
38
|
+
export type NextFontSetup = {
|
|
39
|
+
/** `import { Inter, JetBrains_Mono } from "next/font/google";` or "". */
|
|
40
|
+
importLine: string;
|
|
41
|
+
/** The `const fontSans = Inter({...});` declarations, or "". */
|
|
42
|
+
consts: string;
|
|
43
|
+
/** ` className={...}` for the `<html>` element, or "". */
|
|
44
|
+
htmlClassAttr: string;
|
|
45
|
+
};
|
|
46
|
+
export declare function nextFontSetup(a: ThemeAnswers): NextFontSetup;
|