synthesisui 0.4.9 → 0.4.11
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/dist/commands/add.js +54 -15
- package/dist/commands/clean.js +3 -1
- package/dist/commands/template.js +4 -3
- package/dist/fonts.js +4 -4
- package/dist/index.js +10 -1
- package/dist/registry.js +3 -1
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { syncClaudeMd } from "../claude-md.js";
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
@@ -14,6 +14,19 @@ async function readRootLock(path) {
|
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
const exists = (path) => access(path).then(() => true, () => false);
|
|
18
|
+
/**
|
|
19
|
+
* Where the app's routes live: the configured pagesDir at the project root
|
|
20
|
+
* (`app/`) or nested under `src/` (`src/app/` - create-next-app's other
|
|
21
|
+
* layout). Null when neither exists (instructions-only mode).
|
|
22
|
+
*/
|
|
23
|
+
async function detectAppDir(root, pagesDir) {
|
|
24
|
+
if (await exists(join(root, pagesDir)))
|
|
25
|
+
return pagesDir;
|
|
26
|
+
if (await exists(join(root, "src", pagesDir)))
|
|
27
|
+
return `src/${pagesDir}`;
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
17
30
|
/**
|
|
18
31
|
* Materializes a published DS into `_synthesisui/ds/<slug>/v<version>/`, points
|
|
19
32
|
* stable root re-exports (tokens.css/theme.css) and a `.lock` at it, and updates
|
|
@@ -115,36 +128,62 @@ export async function add(slug, opts) {
|
|
|
115
128
|
return;
|
|
116
129
|
const hasTheme = cssArtifacts.includes("theme.css");
|
|
117
130
|
// ── DX: concrete paths + copy-pasteable snippets, with breathing room ──
|
|
131
|
+
// Where the app actually lives (app/ vs src/app/) drives every printed
|
|
132
|
+
// path: the @import depth, the layout example, and where fonts.ts lands.
|
|
133
|
+
const projectConfig = await readProjectConfig(projectRoot);
|
|
134
|
+
const appDir = (await detectAppDir(projectRoot, projectConfig.pagesDir)) ??
|
|
135
|
+
projectConfig.pagesDir;
|
|
136
|
+
const importPrefix = "../".repeat(appDir.split("/").length);
|
|
118
137
|
console.log(section("One-time setup (once per app)"));
|
|
119
|
-
console.log(line(
|
|
120
|
-
console.log(line(
|
|
138
|
+
console.log(line(`1. Import the system in your GLOBAL stylesheet, e.g. ${appDir}/globals.css`));
|
|
139
|
+
console.log(line(` (the path is relative to that file - hence the leading ${importPrefix}):`));
|
|
121
140
|
console.log("");
|
|
122
141
|
console.log(snippet(hasTheme
|
|
123
142
|
? [
|
|
124
143
|
`@import "tailwindcss";`,
|
|
125
|
-
`@import "
|
|
126
|
-
`@import "
|
|
144
|
+
`@import "${importPrefix}_synthesisui/ds/${payload.slug}/tokens.css";`,
|
|
145
|
+
`@import "${importPrefix}_synthesisui/ds/${payload.slug}/theme.css"; /* Tailwind utilities on your tokens */`,
|
|
127
146
|
]
|
|
128
|
-
: [
|
|
147
|
+
: [
|
|
148
|
+
`@import "${importPrefix}_synthesisui/ds/${payload.slug}/tokens.css";`,
|
|
149
|
+
]));
|
|
129
150
|
console.log("");
|
|
130
|
-
console.log(line(`2. Scope your app: add data-ds="${payload.slug}" to a ROOT element, e.g.
|
|
151
|
+
console.log(line(`2. Scope your app: add data-ds="${payload.slug}" to a ROOT element, e.g. ${appDir}/layout.tsx:`));
|
|
131
152
|
console.log("");
|
|
132
153
|
console.log(snippet([`<body data-ds="${payload.slug}">{children}</body>`]));
|
|
133
154
|
// 3. Load the type - the DS ships token NAMES, not the fonts themselves.
|
|
134
|
-
// Next apps get
|
|
135
|
-
//
|
|
136
|
-
// as the framework-agnostic path.
|
|
155
|
+
// Next apps get fonts.ts MATERIALIZED (deterministic does, not teaches):
|
|
156
|
+
// next/font = self-hosted + preloaded + adjusted fallback, no FOUT
|
|
157
|
+
// "blink". The Google Fonts <link> stays as the framework-agnostic path.
|
|
137
158
|
const families = payload.document.foundations.typography.families;
|
|
138
159
|
const fontsHref = googleFontsHref(families);
|
|
139
|
-
const projectConfig = await readProjectConfig(projectRoot);
|
|
140
160
|
const nextFonts = projectConfig.target === "next"
|
|
141
|
-
? nextFontSnippet(families, payload.slug)
|
|
161
|
+
? nextFontSnippet(families, payload.slug, appDir)
|
|
142
162
|
: null;
|
|
143
163
|
if (nextFonts) {
|
|
164
|
+
const fontsPath = join(projectRoot, ...appDir.split("/"), "fonts.ts");
|
|
165
|
+
let wroteFonts = false;
|
|
166
|
+
if (!(await exists(fontsPath)) &&
|
|
167
|
+
(await exists(join(projectRoot, ...appDir.split("/"))))) {
|
|
168
|
+
const header = [
|
|
169
|
+
`// Self-hosted type for the "${payload.slug}" design system (via next/font -`,
|
|
170
|
+
`// preloaded, no font flash). Generated by \`synthesisui add\`; edit freely.`,
|
|
171
|
+
];
|
|
172
|
+
await writeFile(fontsPath, `${[...header, ...nextFonts.fontsFile.slice(1)].join("\n")}\n`, "utf8");
|
|
173
|
+
wroteFonts = true;
|
|
174
|
+
}
|
|
144
175
|
console.log("");
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
176
|
+
if (wroteFonts) {
|
|
177
|
+
console.log(line(`3. ✓ wrote ${appDir}/fonts.ts - self-hosted type via next/font (preloaded, no font flash).`));
|
|
178
|
+
console.log(line(" Finish the wiring with two small edits:"));
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.log(line(`3. Load the type via next/font (${appDir}/fonts.ts already exists - left untouched; it should export:)`));
|
|
182
|
+
console.log("");
|
|
183
|
+
console.log(snippet(nextFonts.fontsFile));
|
|
184
|
+
console.log("");
|
|
185
|
+
console.log(line(" Then finish the wiring:"));
|
|
186
|
+
}
|
|
148
187
|
console.log("");
|
|
149
188
|
console.log(snippet(nextFonts.layout));
|
|
150
189
|
console.log("");
|
package/dist/commands/clean.js
CHANGED
|
@@ -90,7 +90,9 @@ export async function clean(opts) {
|
|
|
90
90
|
// 4. Default README.
|
|
91
91
|
const readmePath = join(root, "README.md");
|
|
92
92
|
const readmeSrc = await readIf(readmePath);
|
|
93
|
-
if (readmeSrc &&
|
|
93
|
+
if (readmeSrc &&
|
|
94
|
+
/create-next-app/.test(readmeSrc) &&
|
|
95
|
+
/Getting Started/.test(readmeSrc)) {
|
|
94
96
|
actions.push({
|
|
95
97
|
verb: "remove",
|
|
96
98
|
path: "README.md",
|
|
@@ -16,14 +16,15 @@ export async function template(slug, name, opts) {
|
|
|
16
16
|
const target = opts.target === "general" || opts.target === "next"
|
|
17
17
|
? opts.target
|
|
18
18
|
: config.target;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const asName = opts.as && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(opts.as) ? opts.as : undefined;
|
|
20
|
+
console.log(`→ generating "${name}"${asName ? ` as "${asName}"` : ""} from "${slug}" (${target}) …`);
|
|
21
|
+
const generated = await fetchTemplate(base, slug, name, target, opts.version, asName);
|
|
21
22
|
// --out targets the page (1st file); sibling files (e.g. the CSS) land in the
|
|
22
23
|
// same directory. Without --out, everything goes under templates/<name>/ -
|
|
23
24
|
// a loose landing.tsx at the app/ root read as a route without being one,
|
|
24
25
|
// and a second template turned the app dir into soup.
|
|
25
26
|
const [pageFile, ...siblings] = generated.files;
|
|
26
|
-
const defaultDir = join("templates", name);
|
|
27
|
+
const defaultDir = join("templates", asName ?? name);
|
|
27
28
|
const pageRel = opts.out ?? join(defaultDir, pageFile.filename);
|
|
28
29
|
const pageDir = dirname(join(root, pageRel));
|
|
29
30
|
await mkdir(pageDir, { recursive: true });
|
package/dist/fonts.js
CHANGED
|
@@ -46,7 +46,7 @@ export function googleFontsHref(families) {
|
|
|
46
46
|
* `<link href="fonts.googleapis.com...">` stays as the framework-agnostic
|
|
47
47
|
* fallback - it works everywhere but swaps visibly on cold loads.
|
|
48
48
|
*/
|
|
49
|
-
export function nextFontSnippet(families, slug) {
|
|
49
|
+
export function nextFontSnippet(families, slug, appDir = "app") {
|
|
50
50
|
const roles = ["display", "body", "mono"].filter((role) => {
|
|
51
51
|
const name = families[role]?.trim();
|
|
52
52
|
return name && !GENERIC_FAMILIES.has(name.toLowerCase());
|
|
@@ -66,7 +66,7 @@ export function nextFontSnippet(families, slug) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
const fontsFile = [
|
|
69
|
-
`//
|
|
69
|
+
`// ${appDir}/fonts.ts`,
|
|
70
70
|
`import { ${importNames.join(", ")} } from "next/font/google";`,
|
|
71
71
|
...consts,
|
|
72
72
|
];
|
|
@@ -75,12 +75,12 @@ export function nextFontSnippet(families, slug) {
|
|
|
75
75
|
return `--font-ds-${seen.get(name)}`;
|
|
76
76
|
};
|
|
77
77
|
const layout = [
|
|
78
|
-
`//
|
|
78
|
+
`// ${appDir}/layout.tsx`,
|
|
79
79
|
`import { ${[...new Set(roles.map((r) => seen.get(families[r].trim())))].join(", ")} } from "./fonts";`,
|
|
80
80
|
`<body data-ds="${slug}" className={\`${[...new Set(roles.map((r) => `\${${seen.get(families[r].trim())}.variable}`))].join(" ")}\`}>`,
|
|
81
81
|
];
|
|
82
82
|
const css = [
|
|
83
|
-
`/*
|
|
83
|
+
`/* ${appDir}/globals.css - AFTER the tokens.css import */`,
|
|
84
84
|
`[data-ds="${slug}"] {`,
|
|
85
85
|
...roles.map((role) => ` --ds-typography-families-${role}: var(${roleVar(role)});`),
|
|
86
86
|
`}`,
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ Usage - deterministic, FREE:
|
|
|
21
21
|
synthesisui add <slug> [options] materialize a DS into _synthesisui/ds/<slug>/
|
|
22
22
|
synthesisui component <slug> <name> bring one EXISTING component in as YOUR <Pascal>.tsx
|
|
23
23
|
synthesisui template <slug> <name> materialize a whole page from a DS template
|
|
24
|
+
(--as landing-home names the output - multi-page safe)
|
|
24
25
|
synthesisui upgrade <slug> update an installed DS + regenerate your components + migration brief
|
|
25
26
|
synthesisui use <slug> "<intent>" print a ready-to-paste agent prompt to build/modify on-system
|
|
26
27
|
synthesisui clean [--force] strip create-next-app boilerplate (dry run without --force)
|
|
@@ -178,7 +179,15 @@ async function main() {
|
|
|
178
179
|
}
|
|
179
180
|
const target = typeof flags.target === "string" ? flags.target : undefined;
|
|
180
181
|
const out = typeof flags.out === "string" ? flags.out : undefined;
|
|
181
|
-
|
|
182
|
+
const as_ = typeof flags.as === "string" ? flags.as : undefined;
|
|
183
|
+
await template(slug, name, {
|
|
184
|
+
registry,
|
|
185
|
+
dir,
|
|
186
|
+
out,
|
|
187
|
+
target,
|
|
188
|
+
version,
|
|
189
|
+
as: as_,
|
|
190
|
+
});
|
|
182
191
|
break;
|
|
183
192
|
}
|
|
184
193
|
case "component": {
|
package/dist/registry.js
CHANGED
|
@@ -46,12 +46,14 @@ export async function fetchDesignSystem(base, slug, version) {
|
|
|
46
46
|
* Fetches a whole page generated from a DS template (`?template=&target=`). The
|
|
47
47
|
* server codegens it from `document.layouts[<template>]`; the CLI just writes it.
|
|
48
48
|
*/
|
|
49
|
-
export async function fetchTemplate(base, slug, template, target, version) {
|
|
49
|
+
export async function fetchTemplate(base, slug, template, target, version, as_) {
|
|
50
50
|
const url = new URL(`${base}/api/registry/ds/${encodeURIComponent(slug)}`);
|
|
51
51
|
url.searchParams.set("template", template);
|
|
52
52
|
url.searchParams.set("target", target);
|
|
53
53
|
if (version != null)
|
|
54
54
|
url.searchParams.set("version", String(version));
|
|
55
|
+
if (as_)
|
|
56
|
+
url.searchParams.set("as", as_);
|
|
55
57
|
const res = await request(url.toString());
|
|
56
58
|
if (res.status === 404) {
|
|
57
59
|
const body = (await res.json().catch(() => ({})));
|