synthesisui 0.4.0 → 0.4.2
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 +17 -0
- package/dist/commands/advise.js +38 -1
- package/dist/commands/generate.js +44 -5
- package/dist/commands/refit.js +36 -3
- package/dist/component-codegen.js +56 -0
- package/dist/fonts.js +40 -0
- package/dist/index.js +7 -5
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -2,6 +2,7 @@ import { 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 { resolveRegistry } from "../config.js";
|
|
5
|
+
import { customFontFamilies, googleFontsHref } from "../fonts.js";
|
|
5
6
|
import { buildGuide } from "../guide.js";
|
|
6
7
|
import { body as line, section, snippet } from "../output.js";
|
|
7
8
|
import { fetchDesignSystem } from "../registry.js";
|
|
@@ -129,6 +130,22 @@ export async function add(slug, opts) {
|
|
|
129
130
|
console.log(line(`2. Scope your app: add data-ds="${payload.slug}" to a ROOT element, e.g. app/layout.tsx:`));
|
|
130
131
|
console.log("");
|
|
131
132
|
console.log(snippet([`<body data-ds="${payload.slug}">{children}</body>`]));
|
|
133
|
+
// 3. Load the type - the DS ships token NAMES, not the fonts themselves.
|
|
134
|
+
// Without this, display/body silently fall back and the identity is lost.
|
|
135
|
+
const families = payload.document.foundations.typography.families;
|
|
136
|
+
const fontsHref = googleFontsHref(families);
|
|
137
|
+
if (fontsHref) {
|
|
138
|
+
console.log("");
|
|
139
|
+
console.log(line("3. Load the type - this system ships font NAMES, not the fonts. Add to your app's <head> (e.g. app/layout.tsx) so the families resolve (else they fall back and the look is lost):"));
|
|
140
|
+
console.log("");
|
|
141
|
+
console.log(snippet([
|
|
142
|
+
`<link rel="preconnect" href="https://fonts.googleapis.com" />`,
|
|
143
|
+
`<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />`,
|
|
144
|
+
`<link rel="stylesheet" href="${fontsHref}" />`,
|
|
145
|
+
]));
|
|
146
|
+
console.log("");
|
|
147
|
+
console.log(line(` Prefer next/font or self-hosting? Fine - just register these exact families: ${customFontFamilies(families).join(", ")}.`));
|
|
148
|
+
}
|
|
132
149
|
console.log(section("Next"));
|
|
133
150
|
console.log(line(`synthesisui component ${payload.slug} button bring a component in as YOUR code`));
|
|
134
151
|
console.log(line(`synthesisui template ${payload.slug} landing materialize a whole page`));
|
package/dist/commands/advise.js
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { resolveRegistry } from "../config.js";
|
|
2
4
|
import { postAdvisor } from "../registry.js";
|
|
3
5
|
import { buildRepoContext } from "../repo-context.js";
|
|
6
|
+
/**
|
|
7
|
+
* Component names of the design system(s) installed in this project (from each
|
|
8
|
+
* DS's design-system.json). Grounds the advisor in what the DS ACTUALLY has, so
|
|
9
|
+
* it prefers real blocks and flags missing ones (instead of a fixed vocabulary).
|
|
10
|
+
*/
|
|
11
|
+
async function installedBlocks(root) {
|
|
12
|
+
const dsRoot = join(root, "_synthesisui", "ds");
|
|
13
|
+
const blocks = new Set();
|
|
14
|
+
let slugs = [];
|
|
15
|
+
try {
|
|
16
|
+
const entries = await readdir(dsRoot, { withFileTypes: true });
|
|
17
|
+
slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
for (const slug of slugs) {
|
|
23
|
+
try {
|
|
24
|
+
const lockRaw = await readFile(join(dsRoot, slug, ".lock"), "utf8");
|
|
25
|
+
const version = JSON.parse(lockRaw).version ?? 1;
|
|
26
|
+
const docRaw = await readFile(join(dsRoot, slug, `v${version}`, "design-system.json"), "utf8");
|
|
27
|
+
const doc = JSON.parse(docRaw);
|
|
28
|
+
for (const name of Object.keys(doc.components ?? {}))
|
|
29
|
+
blocks.add(name);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// skip a DS we can't read - the advisor still works without its catalog
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return [...blocks].sort();
|
|
36
|
+
}
|
|
4
37
|
/**
|
|
5
38
|
* Asks the hosted advisor for engagement-pattern proposals, grounded in THIS
|
|
6
39
|
* project (the CLI gathers a compact repo summary) + the value proposition you
|
|
@@ -10,7 +43,11 @@ export async function advise(valueProp, opts) {
|
|
|
10
43
|
const base = resolveRegistry(opts.registry);
|
|
11
44
|
const root = opts.dir ?? process.cwd();
|
|
12
45
|
const repo = await buildRepoContext(root);
|
|
13
|
-
const
|
|
46
|
+
const blocks = await installedBlocks(root);
|
|
47
|
+
const catalog = blocks.length
|
|
48
|
+
? `\n\nDesign-system blocks available in this project (prefer these; anything else can be created with \`synthesisui generate\`): ${blocks.join(", ")}`
|
|
49
|
+
: "";
|
|
50
|
+
const context = `Value proposition: ${valueProp}\n\n${repo}${catalog}`;
|
|
14
51
|
console.log(`→ asking the advisor at ${base} …`);
|
|
15
52
|
const res = await postAdvisor(base, context);
|
|
16
53
|
if (res.proposals.length === 0) {
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { generateComponentFiles } from "../component-codegen.js";
|
|
4
|
+
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
4
5
|
import { postGenerate, RegistryError } from "../registry.js";
|
|
6
|
+
/** PascalCase para o hint de import (course-card → CourseCard). */
|
|
7
|
+
function pascalName(name) {
|
|
8
|
+
return name.replace(/(^|[-_])([a-z0-9])/g, (_, __, c) => c.toUpperCase());
|
|
9
|
+
}
|
|
10
|
+
/** Versão ativa do DS instalado (do `.lock`); 1 se ausente - só nomeia o header. */
|
|
11
|
+
async function readActiveVersion(root, slug) {
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readFile(join(root, "_synthesisui", "ds", slug, ".lock"), "utf8");
|
|
14
|
+
const lock = JSON.parse(raw);
|
|
15
|
+
return typeof lock.version === "number" ? lock.version : 1;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
5
21
|
/** Slugs materialized under `_synthesisui/ds/` in the project. */
|
|
6
22
|
async function installedSlugs(root) {
|
|
7
23
|
try {
|
|
@@ -45,9 +61,32 @@ export async function generate(description, opts) {
|
|
|
45
61
|
const tries = `${res.tries} ${res.tries === 1 ? "try" : "tries"}`;
|
|
46
62
|
console.log(`✓ ${res.name} generated (${res.model}, ${tries})`);
|
|
47
63
|
console.log(` → _synthesisui/ds/${slug}/generated/${res.name}.{json,css}`);
|
|
64
|
+
// Materialize YOUR component (.tsx) too - the SAME codegen `component` uses -
|
|
65
|
+
// so a generated component is as usable as a brought-in one, not just a
|
|
66
|
+
// recipe you have to wire by hand.
|
|
67
|
+
const config = await readProjectConfig(root);
|
|
68
|
+
let materialized = false;
|
|
69
|
+
if (config.target === "next") {
|
|
70
|
+
const version = await readActiveVersion(root, slug);
|
|
71
|
+
const compDir = join(root, config.componentsDir, res.name);
|
|
72
|
+
await mkdir(compDir, { recursive: true });
|
|
73
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, version, config.styles);
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
await writeFile(join(compDir, file.filename), file.code, "utf8");
|
|
76
|
+
}
|
|
77
|
+
materialized = true;
|
|
78
|
+
console.log(`✓ ${config.componentsDir}/${res.name}/ → ${files.map((f) => f.filename).join(", ")} (styles: ${config.styles})`);
|
|
79
|
+
}
|
|
48
80
|
console.log("");
|
|
49
81
|
console.log("Use it:");
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
82
|
+
if (materialized) {
|
|
83
|
+
const comp = pascalName(res.name);
|
|
84
|
+
console.log(` • import { ${comp} } from "@/${config.componentsDir}/${res.name}";`);
|
|
85
|
+
console.log(` • <${comp} /> - compose its parts + content (see the recipe)`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log(` • @import "_synthesisui/ds/${slug}/generated/${res.name}.css" in your CSS`);
|
|
89
|
+
console.log(` • <div data-ds="${slug}"><div class="ds-${res.name}">…</div></div>`);
|
|
90
|
+
}
|
|
91
|
+
console.log(` (${res.usage.inputTokens} in / ${res.usage.outputTokens} out tokens · AI action - additive; nothing else changed)`);
|
|
53
92
|
}
|
package/dist/commands/refit.js
CHANGED
|
@@ -1,21 +1,44 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
import { generateComponentFiles } from "../component-codegen.js";
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
5
|
import { body, section, snippet } from "../output.js";
|
|
6
6
|
import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
|
|
7
|
-
/** Slugs
|
|
7
|
+
/** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
|
|
8
|
+
* a folder holding only refit artifacts doesn't count). */
|
|
8
9
|
async function installedSlugs(root) {
|
|
9
10
|
try {
|
|
10
11
|
const entries = await readdir(join(root, "_synthesisui", "ds"), {
|
|
11
12
|
withFileTypes: true,
|
|
12
13
|
});
|
|
13
|
-
|
|
14
|
+
const slugs = [];
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
if (!entry.isDirectory())
|
|
17
|
+
continue;
|
|
18
|
+
try {
|
|
19
|
+
await access(join(root, "_synthesisui", "ds", entry.name, ".lock"));
|
|
20
|
+
slugs.push(entry.name);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// artifacts-only folder (e.g. a refit before `add`) - not installed
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return slugs;
|
|
14
27
|
}
|
|
15
28
|
catch {
|
|
16
29
|
return [];
|
|
17
30
|
}
|
|
18
31
|
}
|
|
32
|
+
/** True when the system is actually installed (tokens.css present). */
|
|
33
|
+
async function isInstalled(root, slug) {
|
|
34
|
+
try {
|
|
35
|
+
await access(join(root, "_synthesisui", "ds", slug, ".lock"));
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
19
42
|
/**
|
|
20
43
|
* Marco B5 - the REVERSE bridge, from the CLI: takes a component that lives in
|
|
21
44
|
* YOUR app (arbitrary React/CSS), re-expresses it in the design system's
|
|
@@ -116,6 +139,16 @@ export async function refit(file, opts) {
|
|
|
116
139
|
materialized = true;
|
|
117
140
|
console.log(`✓ ${config.componentsDir}/${res.name}/ → ${files.map((f) => f.filename).join(", ")} (styles: ${config.styles})`);
|
|
118
141
|
}
|
|
142
|
+
// The materialized component references this system's tokens - without the
|
|
143
|
+
// install (tokens.css + scope) it renders unstyled. Say so, concretely.
|
|
144
|
+
if (!(await isInstalled(root, slug))) {
|
|
145
|
+
console.log(section("Heads up - system not installed here yet"));
|
|
146
|
+
console.log(body(`The component references "${slug}" tokens that this project doesn't have yet.`));
|
|
147
|
+
console.log("");
|
|
148
|
+
console.log(snippet([`npx synthesisui add ${slug}`]));
|
|
149
|
+
console.log("");
|
|
150
|
+
console.log(body("(then follow its one-time setup: global @import + data-ds scope)"));
|
|
151
|
+
}
|
|
119
152
|
if (res.suggestedRule) {
|
|
120
153
|
console.log(section("Suggested rule"));
|
|
121
154
|
console.log(body("The AI inferred a reusable rule from this component:"));
|
|
@@ -136,7 +136,30 @@ const STATIC = {
|
|
|
136
136
|
width: { "100%": "w-full" },
|
|
137
137
|
height: { "100%": "h-full" },
|
|
138
138
|
textDecoration: { none: "no-underline", underline: "underline" },
|
|
139
|
+
overflow: {
|
|
140
|
+
hidden: "overflow-hidden",
|
|
141
|
+
auto: "overflow-auto",
|
|
142
|
+
scroll: "overflow-scroll",
|
|
143
|
+
visible: "overflow-visible",
|
|
144
|
+
clip: "overflow-clip",
|
|
145
|
+
},
|
|
146
|
+
position: {
|
|
147
|
+
relative: "relative",
|
|
148
|
+
absolute: "absolute",
|
|
149
|
+
fixed: "fixed",
|
|
150
|
+
sticky: "sticky",
|
|
151
|
+
static: "static",
|
|
152
|
+
},
|
|
139
153
|
};
|
|
154
|
+
/** Fixed rem → Tailwind numeric scale step (0.25rem base), or null if not a
|
|
155
|
+
* clean multiple. So `0.5rem` → `2` (h-2/w-2) instead of an arbitrary value. */
|
|
156
|
+
function remToTwScale(value) {
|
|
157
|
+
const m = value.trim().match(/^(\d*\.?\d+)rem$/);
|
|
158
|
+
if (!m)
|
|
159
|
+
return null;
|
|
160
|
+
const n = Number.parseFloat(m[1]) / 0.25;
|
|
161
|
+
return Number.isInteger(n) && n >= 0 && n <= 96 ? String(n) : null;
|
|
162
|
+
}
|
|
140
163
|
/** One declaration → Tailwind classes (pretty when mappable, arbitrary-property
|
|
141
164
|
* otherwise - never dropped). */
|
|
142
165
|
function declToTailwind(prop, value) {
|
|
@@ -224,6 +247,18 @@ function declToTailwind(prop, value) {
|
|
|
224
247
|
if (/^\{typography\.scale\./.test(value))
|
|
225
248
|
return [];
|
|
226
249
|
break;
|
|
250
|
+
case "height": {
|
|
251
|
+
const n = remToTwScale(value);
|
|
252
|
+
if (n)
|
|
253
|
+
return [`h-${n}`];
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case "width": {
|
|
257
|
+
const n = remToTwScale(value);
|
|
258
|
+
if (n)
|
|
259
|
+
return [`w-${n}`];
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
227
262
|
}
|
|
228
263
|
return [arbitrary(prop, value)];
|
|
229
264
|
}
|
|
@@ -257,6 +292,25 @@ function header(slug, name, version, mode) {
|
|
|
257
292
|
].join("\n");
|
|
258
293
|
}
|
|
259
294
|
const joinCls = (parts) => `[${parts.join(", ")}].filter(Boolean).join(" ")`;
|
|
295
|
+
/** JSDoc showing how to compose the component with its parts + content, so the
|
|
296
|
+
* materialized code doesn't read as "a bare shell renders nothing" (dogfood
|
|
297
|
+
* #5). Built from the recipe's parts. */
|
|
298
|
+
function compositionHint(comp, name, recipe) {
|
|
299
|
+
const partNames = Object.keys(recipe.parts ?? {});
|
|
300
|
+
if (partNames.length === 0) {
|
|
301
|
+
return `/** Wears the "${name}" recipe. Put your content inside: <${comp}>…</${comp}>. */`;
|
|
302
|
+
}
|
|
303
|
+
const inner = partNames
|
|
304
|
+
.map((p) => ` * <${comp}${pascal(p)}>…</${comp}${pascal(p)}>`)
|
|
305
|
+
.join("\n");
|
|
306
|
+
return `/**
|
|
307
|
+
* Compose it with its parts + your own content - a bare <${comp} /> is just the
|
|
308
|
+
* empty shell. For example:
|
|
309
|
+
* <${comp}>
|
|
310
|
+
${inner}
|
|
311
|
+
* </${comp}>
|
|
312
|
+
*/`;
|
|
313
|
+
}
|
|
260
314
|
function emitCssMode(slug, name, recipe, version) {
|
|
261
315
|
const { tag, attrs, voidEl } = elementFor(name, recipe);
|
|
262
316
|
const axes = axesOf(recipe.variants);
|
|
@@ -291,6 +345,7 @@ import type { ComponentPropsWithoutRef } from "react";
|
|
|
291
345
|
|
|
292
346
|
type ${comp}Props = ${propsType(axes, tag)};
|
|
293
347
|
|
|
348
|
+
${compositionHint(comp, name, recipe)}
|
|
294
349
|
export function ${comp}({ ${destructure} }: ${comp}Props) {
|
|
295
350
|
return (
|
|
296
351
|
${rootJsx}
|
|
@@ -335,6 +390,7 @@ ${[...variantConsts, ...booleanConsts].join("\n")}
|
|
|
335
390
|
|
|
336
391
|
type ${comp}Props = ${propsType(axes, tag)};
|
|
337
392
|
|
|
393
|
+
${compositionHint(comp, name, recipe)}
|
|
338
394
|
export function ${comp}({ ${destructure} }: ${comp}Props) {
|
|
339
395
|
return (
|
|
340
396
|
<${tag}${attrs}
|
package/dist/fonts.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Fallbacks genéricos do CSS - não são webfonts.
|
|
2
|
+
const GENERIC_FAMILIES = new Set([
|
|
3
|
+
"sans-serif",
|
|
4
|
+
"serif",
|
|
5
|
+
"monospace",
|
|
6
|
+
"system-ui",
|
|
7
|
+
"ui-sans-serif",
|
|
8
|
+
"ui-serif",
|
|
9
|
+
"ui-monospace",
|
|
10
|
+
"cursive",
|
|
11
|
+
"fantasy",
|
|
12
|
+
"inherit",
|
|
13
|
+
"initial",
|
|
14
|
+
]);
|
|
15
|
+
/** Famílias CUSTOM (display/body/mono), dedup e sem os genéricos. */
|
|
16
|
+
export function customFontFamilies(families) {
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
const names = [];
|
|
19
|
+
for (const family of [families.display, families.body, families.mono]) {
|
|
20
|
+
const name = family?.trim();
|
|
21
|
+
if (!name)
|
|
22
|
+
continue;
|
|
23
|
+
const key = name.toLowerCase();
|
|
24
|
+
if (GENERIC_FAMILIES.has(key) || seen.has(key))
|
|
25
|
+
continue;
|
|
26
|
+
seen.add(key);
|
|
27
|
+
names.push(name);
|
|
28
|
+
}
|
|
29
|
+
return names;
|
|
30
|
+
}
|
|
31
|
+
/** URL do Google Fonts CSS2 pras famílias do documento, ou null se só genéricos. */
|
|
32
|
+
export function googleFontsHref(families) {
|
|
33
|
+
const names = customFontFamilies(families);
|
|
34
|
+
if (names.length === 0)
|
|
35
|
+
return null;
|
|
36
|
+
const query = names
|
|
37
|
+
.map((name) => `family=${name.replace(/ /g, "+")}:wght@400;500;600;700`)
|
|
38
|
+
.join("&");
|
|
39
|
+
return `https://fonts.googleapis.com/css2?${query}&display=swap`;
|
|
40
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -13,18 +13,20 @@ import { use } from "./commands/use.js";
|
|
|
13
13
|
import { RegistryError } from "./registry.js";
|
|
14
14
|
const HELP = `synthesisui - bring SynthesisUI design systems into your project
|
|
15
15
|
|
|
16
|
-
Usage:
|
|
16
|
+
Usage - deterministic, FREE:
|
|
17
17
|
synthesisui login [options] connect the CLI to your account (device-flow)
|
|
18
18
|
synthesisui init [options] write _synthesisui/config.json (target, dirs); --ds to bring one in
|
|
19
19
|
synthesisui list [options] list the published design systems
|
|
20
20
|
synthesisui add <slug> [options] materialize a DS into _synthesisui/ds/<slug>/
|
|
21
|
+
synthesisui component <slug> <name> bring one EXISTING component in as YOUR <Pascal>.tsx
|
|
21
22
|
synthesisui template <slug> <name> materialize a whole page from a DS template
|
|
22
|
-
synthesisui component <slug> <name> bring one component in - artifacts + YOUR <Pascal>.tsx in componentsDir
|
|
23
23
|
synthesisui upgrade <slug> update an installed DS + regenerate your components + migration brief
|
|
24
|
-
synthesisui refit <file> [--ds <slug>] send an app component INTO your DS (token-only) and get it back as code
|
|
25
24
|
synthesisui use <slug> "<intent>" print a ready-to-paste agent prompt to build/modify on-system
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
|
|
26
|
+
Usage - AI, USES CREDITS (login required):
|
|
27
|
+
synthesisui generate "<desc>" AI-create a NEW component your DS doesn't have (token-only recipe)
|
|
28
|
+
synthesisui advise "<value prop>" AI engagement-pattern proposals for this project
|
|
29
|
+
synthesisui refit <file> [--ds <slug>] AI-adapt an app component INTO your DS, get it back as code
|
|
28
30
|
|
|
29
31
|
Options:
|
|
30
32
|
--registry <url> registry URL (or env SYNTHESISUI_REGISTRY_URL)
|