synthesisui 0.4.8 → 0.4.10

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("");
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/guide.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { nextFontSnippet } from "./fonts.js";
1
2
  const kebab = (v) => v.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
2
3
  const list = (items) => items.length ? items.map((i) => `\`${i}\``).join(", ") : "_(none)_";
3
4
  const dataAttrs = (variants) => Object.entries(variants).map(([axis, opts]) => `data-${kebab(axis)}="${Object.keys(opts).join("|")}"`);
@@ -67,6 +68,7 @@ export function buildGuide(payload) {
67
68
  .map((n) => `family=${n.replace(/ /g, "+")}:wght@400;500;600;700`)
68
69
  .join("&")}&display=swap`
69
70
  : null;
71
+ const nextFonts = nextFontSnippet(foundations.typography.families, slug);
70
72
  const fontsSection = fontsHref
71
73
  ? `
72
74
  ## Fonts
@@ -74,15 +76,29 @@ export function buildGuide(payload) {
74
76
  This system's type relies on ${list(fontFamilies)} - **the DS ships token names, not the
75
77
  fonts themselves.** If you don't load them they fall back to a generic family and the system loses
76
78
  its typographic identity. Load them once (any one approach):
77
-
78
- - **Google Fonts** - drop in your \`<head>\` (or root layout):
79
+ ${nextFonts
80
+ ? `
81
+ - **Next.js (recommended)** - \`next/font\` self-hosts the families (preloaded, size-adjusted
82
+ fallbacks - no font flash on refresh). Three small blocks:
83
+ \`\`\`ts
84
+ ${nextFonts.fontsFile.map((l) => ` ${l}`).join("\n")}
85
+ \`\`\`
86
+ \`\`\`tsx
87
+ ${nextFonts.layout.map((l) => ` ${l}`).join("\n")}
88
+ \`\`\`
89
+ \`\`\`css
90
+ ${nextFonts.css.map((l) => ` ${l}`).join("\n")}
91
+ \`\`\``
92
+ : ""}
93
+ - **Anywhere else** - Google Fonts \`<link>\` in the \`<head>\` (works everywhere, may flash on
94
+ cold loads):
79
95
  \`\`\`html
80
96
  <link rel="preconnect" href="https://fonts.googleapis.com" />
81
97
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
82
98
  <link rel="stylesheet" href="${fontsHref}" />
83
99
  \`\`\`
84
- - **Next.js** (\`next/font/google\`), **Fontsource**, or self-hosted \`@font-face\` work too - just
85
- register the families above. If a family isn't on Google Fonts, self-host it.
100
+ - **Fontsource** or self-hosted \`@font-face\` work too - just register the families above.
101
+ If a family isn't on Google Fonts, self-host it.
86
102
 
87
103
  ---
88
104
  `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Traz design systems do SynthesisUI para qualquer projeto (materializa em _local/ds/).",
5
5
  "type": "module",
6
6
  "bin": {