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.
@@ -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("1. Import the system in your GLOBAL stylesheet, e.g. app/globals.css"));
120
- console.log(line(" (the path is relative to that file - hence the leading ../):"));
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 "../_synthesisui/ds/${payload.slug}/tokens.css";`,
126
- `@import "../_synthesisui/ds/${payload.slug}/theme.css"; /* Tailwind utilities on your tokens */`,
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
- : [`@import "../_synthesisui/ds/${payload.slug}/tokens.css";`]));
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. app/layout.tsx:`));
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 the next/font recipe (self-hosted + preloaded + adjusted
135
- // fallback = no FOUT "blink" on refresh); the Google Fonts <link> stays
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
- console.log(line("3. Load the type via next/font (recommended: self-hosted, preloaded, no font flash on refresh). Three small blocks:"));
146
- console.log("");
147
- console.log(snippet(nextFonts.fontsFile));
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("");
@@ -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 && /create-next-app/.test(readmeSrc) && /Getting Started/.test(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
- console.log(`→ generating "${name}" from "${slug}" (${target}) …`);
20
- const generated = await fetchTemplate(base, slug, name, target, opts.version);
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
- `// app/fonts.ts`,
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
- `// app/layout.tsx`,
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
- `/* app/globals.css - AFTER the tokens.css import */`,
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
- await template(slug, name, { registry, dir, out, target, version });
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(() => ({})));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "Traz design systems do SynthesisUI para qualquer projeto (materializa em _local/ds/).",
5
5
  "type": "module",
6
6
  "bin": {