synthesisui 0.1.2 → 0.1.5
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 +25 -24
- package/dist/claude-md.js +13 -13
- package/dist/commands/add.js +57 -23
- package/dist/commands/list.js +4 -4
- package/dist/commands/login.js +13 -13
- package/dist/config.js +8 -8
- package/dist/guide.js +82 -42
- package/dist/index.js +26 -15
- package/dist/registry.js +15 -10
- package/dist/types.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,54 +1,55 @@
|
|
|
1
1
|
# synthesisui
|
|
2
2
|
|
|
3
|
-
CLI
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
CLI to bring design systems published on [SynthesisUI](https://www.synthesisui.com)
|
|
4
|
+
into any project. It materializes the system into `_synthesisui/ds/<slug>/` and injects
|
|
5
|
+
a managed block into the root `CLAUDE.md`, so Claude Code builds components following the
|
|
6
|
+
design system.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Usage
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Without installing anything:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npx synthesisui login #
|
|
14
|
-
npx synthesisui list #
|
|
15
|
-
npx synthesisui add <slug> #
|
|
13
|
+
npx synthesisui login # connect the CLI to your account (device-flow in the browser)
|
|
14
|
+
npx synthesisui list # list the available design systems
|
|
15
|
+
npx synthesisui add <slug> # bring a DS into _synthesisui/ds/<slug>/
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Or install globally:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
npm install -g synthesisui
|
|
22
22
|
synthesisui add halogen
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
###
|
|
25
|
+
### What `add` materializes
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
In `_synthesisui/ds/<slug>/`:
|
|
28
28
|
|
|
29
|
-
- `design-system.json` —
|
|
30
|
-
- `tokens.css` — CSS custom properties
|
|
31
|
-
- `
|
|
32
|
-
-
|
|
29
|
+
- `design-system.json` — the canonical source of truth of the design system
|
|
30
|
+
- `tokens.css` — CSS custom properties scoped by `data-ds`
|
|
31
|
+
- `theme.css` — optional Tailwind v4 `@theme` adapter (use `bg-primary`, `p-md`, … backed by the tokens)
|
|
32
|
+
- `GUIDE.md` — instructions for the agent (semantic roles, mood, recipes, how to add components)
|
|
33
|
+
- `.lock` — pinned slug + version (reproducible)
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
And it injects an idempotent `<!-- synthesisui:start/end -->` block into the root `CLAUDE.md`,
|
|
36
|
+
reflecting every installed DS.
|
|
36
37
|
|
|
37
|
-
##
|
|
38
|
+
## Authentication
|
|
38
39
|
|
|
39
|
-
`synthesisui login`
|
|
40
|
-
|
|
40
|
+
`synthesisui login` uses device-flow (RFC 8628): it opens the browser, you confirm a code,
|
|
41
|
+
and the token is saved to `~/.synthesisui/credentials.json` (per machine). Logout = delete that file.
|
|
41
42
|
|
|
42
43
|
## Registry
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
By default it points to `https://www.synthesisui.com`. Override it with:
|
|
45
46
|
|
|
46
47
|
```bash
|
|
47
48
|
synthesisui list --registry http://localhost:3000
|
|
48
|
-
#
|
|
49
|
+
# or
|
|
49
50
|
SYNTHESISUI_REGISTRY_URL=http://localhost:3000 synthesisui list
|
|
50
51
|
```
|
|
51
52
|
|
|
52
|
-
##
|
|
53
|
+
## License
|
|
53
54
|
|
|
54
55
|
MIT
|
package/dist/claude-md.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
const START = "<!-- synthesisui:start -->";
|
|
4
4
|
const END = "<!-- synthesisui:end -->";
|
|
5
|
-
/**
|
|
5
|
+
/** Reads the installed DSs from the .lock files in _synthesisui/ds/<slug>/. */
|
|
6
6
|
async function readInstalled(projectRoot) {
|
|
7
7
|
const dsDir = join(projectRoot, "_synthesisui", "ds");
|
|
8
8
|
let entries = [];
|
|
@@ -21,7 +21,7 @@ async function readInstalled(projectRoot) {
|
|
|
21
21
|
locks.push({ slug: lock.slug, name: lock.name, version: lock.version });
|
|
22
22
|
}
|
|
23
23
|
catch {
|
|
24
|
-
//
|
|
24
|
+
// folder without a valid .lock — ignore
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
return locks;
|
|
@@ -31,26 +31,26 @@ function renderRegion(installed) {
|
|
|
31
31
|
return `${START}\n${END}`;
|
|
32
32
|
}
|
|
33
33
|
const lines = installed
|
|
34
|
-
.map((ds) => `- **${ds.name}** (\`${ds.slug}\`, v${ds.version}) —
|
|
34
|
+
.map((ds) => `- **${ds.name}** (\`${ds.slug}\`, v${ds.version}) — guide: \`_synthesisui/ds/${ds.slug}/v${ds.version}/GUIDE.md\``)
|
|
35
35
|
.join("\n");
|
|
36
36
|
const body = `## Design Systems (via SynthesisUI)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
(\`var(--ds-color-semantic-*)\`, \`--ds-spacing-*\`, etc.),
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
This project uses design system(s) brought in by the \`synthesisui\` CLI. **When creating or editing
|
|
39
|
+
components, read the system's GUIDE.md and follow it:** use only semantic tokens
|
|
40
|
+
(\`var(--ds-color-semantic-*)\`, \`--ds-spacing-*\`, etc.), scope the UI with \`data-ds="<slug>"\`,
|
|
41
|
+
and reuse the \`.ds-*\` classes. Do not use raw values outside the system's scale. **To review a
|
|
42
|
+
component, create an isolated sample page (e.g. \`app/synthesisui-samples/<component>/\`) — do not
|
|
43
|
+
apply it to real production pages unless asked.**
|
|
44
44
|
|
|
45
45
|
${lines}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
_Block managed by the CLI — do not edit by hand; run \`synthesisui add <slug>\` to update._`;
|
|
48
48
|
return `${START}\n${body}\n${END}`;
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* Regenerates the managed block in the root CLAUDE.md reflecting every installed
|
|
52
|
+
* DS. Idempotent: replaces the text between the markers if present, otherwise
|
|
53
|
+
* creates the file / appends the block. Returns whether the file was created.
|
|
54
54
|
*/
|
|
55
55
|
export async function syncClaudeMd(projectRoot) {
|
|
56
56
|
const installed = await readInstalled(projectRoot);
|
package/dist/commands/add.js
CHANGED
|
@@ -1,28 +1,50 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1
|
+
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
5
|
import { buildGuide } from "../guide.js";
|
|
6
6
|
import { fetchDesignSystem } from "../registry.js";
|
|
7
|
+
async function readRootLock(path) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
7
15
|
/**
|
|
8
|
-
*
|
|
16
|
+
* Materializes a published DS into `_synthesisui/ds/<slug>/v<version>/`, points
|
|
17
|
+
* stable root re-exports (tokens.css/theme.css) and a `.lock` at it, and updates
|
|
18
|
+
* CLAUDE.md. Older version folders are kept for rollback/diff.
|
|
9
19
|
*/
|
|
10
20
|
export async function add(slug, opts) {
|
|
11
21
|
const base = resolveRegistry(opts.registry);
|
|
12
22
|
const projectRoot = opts.dir ?? process.cwd();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
const label = opts.version != null ? `${slug}@v${opts.version}` : slug;
|
|
24
|
+
console.log(`→ fetching "${label}" from ${base} …`);
|
|
25
|
+
const payload = await fetchDesignSystem(base, slug, opts.version);
|
|
26
|
+
const slugDir = join(projectRoot, "_synthesisui", "ds", payload.slug);
|
|
27
|
+
const versionDir = join(slugDir, `v${payload.version}`);
|
|
28
|
+
const rootLockPath = join(slugDir, ".lock");
|
|
29
|
+
// know what was active before, to report install vs update vs switch
|
|
30
|
+
const prev = await readRootLock(rootLockPath);
|
|
31
|
+
await mkdir(versionDir, { recursive: true });
|
|
32
|
+
// 1. server artifacts (tokens.css, theme.css, …) → pinned version folder
|
|
18
33
|
for (const [filename, content] of Object.entries(payload.artifacts)) {
|
|
19
|
-
await writeFile(join(
|
|
34
|
+
await writeFile(join(versionDir, filename), content, "utf8");
|
|
35
|
+
}
|
|
36
|
+
// 2. canonical source of truth
|
|
37
|
+
await writeFile(join(versionDir, "design-system.json"), `${JSON.stringify(payload.document, null, 2)}\n`, "utf8");
|
|
38
|
+
// 3. guide for the agent (generated client-side from the document)
|
|
39
|
+
await writeFile(join(versionDir, "GUIDE.md"), buildGuide(payload), "utf8");
|
|
40
|
+
// 4. stable root re-exports for each CSS artifact → always the active version,
|
|
41
|
+
// so the consumer's @import path never changes across updates
|
|
42
|
+
const cssArtifacts = Object.keys(payload.artifacts).filter((f) => f.endsWith(".css"));
|
|
43
|
+
for (const filename of cssArtifacts) {
|
|
44
|
+
await writeFile(join(slugDir, filename), `/* Active version (v${payload.version}). Managed by synthesisui — do not edit. */\n` +
|
|
45
|
+
`@import "./v${payload.version}/${filename}";\n`, "utf8");
|
|
20
46
|
}
|
|
21
|
-
//
|
|
22
|
-
await writeFile(join(targetDir, "design-system.json"), `${JSON.stringify(payload.document, null, 2)}\n`, "utf8");
|
|
23
|
-
// 3. guia para o agente (gerado client-side a partir do document)
|
|
24
|
-
await writeFile(join(targetDir, "GUIDE.md"), buildGuide(payload), "utf8");
|
|
25
|
-
// 4. lock reproduzível
|
|
47
|
+
// 5. root pointer
|
|
26
48
|
const lock = {
|
|
27
49
|
slug: payload.slug,
|
|
28
50
|
name: payload.name,
|
|
@@ -30,21 +52,33 @@ export async function add(slug, opts) {
|
|
|
30
52
|
registry: base,
|
|
31
53
|
fetchedAt: new Date().toISOString(),
|
|
32
54
|
};
|
|
33
|
-
await writeFile(
|
|
34
|
-
//
|
|
55
|
+
await writeFile(rootLockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
|
|
56
|
+
// 6. discovery by the agent
|
|
35
57
|
const claudeMd = await syncClaudeMd(projectRoot);
|
|
58
|
+
// outcome line
|
|
59
|
+
const v = payload.version;
|
|
60
|
+
if (!prev) {
|
|
61
|
+
console.log(`✓ ${payload.name} v${v} installed → _synthesisui/ds/${payload.slug}/`);
|
|
62
|
+
}
|
|
63
|
+
else if (prev.version === v) {
|
|
64
|
+
console.log(`✓ ${payload.name} v${v} already installed${opts.version == null ? " (latest)" : ""} — refreshed`);
|
|
65
|
+
}
|
|
66
|
+
else if (v > prev.version) {
|
|
67
|
+
console.log(`↑ ${payload.name} v${prev.version} → v${v} (kept v${prev.version}/ for rollback)`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.log(`↺ ${payload.name} active version set to v${v} (was v${prev.version})`);
|
|
71
|
+
}
|
|
36
72
|
const files = [
|
|
37
73
|
...Object.keys(payload.artifacts),
|
|
38
74
|
"design-system.json",
|
|
39
75
|
"GUIDE.md",
|
|
40
|
-
".lock",
|
|
41
76
|
];
|
|
42
|
-
console.log(
|
|
43
|
-
console.log(` ${
|
|
44
|
-
console.log(` CLAUDE.md ${claudeMd.created ? "criado" : "atualizado"} (${claudeMd.count} sistema(s) instalado(s))`);
|
|
77
|
+
console.log(` v${v}/: ${files.join(", ")}`);
|
|
78
|
+
console.log(` CLAUDE.md ${claudeMd.created ? "created" : "updated"} (${claudeMd.count} system(s) installed)`);
|
|
45
79
|
console.log("");
|
|
46
|
-
console.log("
|
|
47
|
-
console.log(` • @import "_synthesisui/ds/${payload.slug}/tokens.css"
|
|
48
|
-
console.log(` •
|
|
49
|
-
console.log(` •
|
|
80
|
+
console.log("Next steps:");
|
|
81
|
+
console.log(` • @import "_synthesisui/ds/${payload.slug}/tokens.css" in your global CSS (stable path)`);
|
|
82
|
+
console.log(` • scope your UI with data-ds="${payload.slug}"`);
|
|
83
|
+
console.log(` • details and rules in _synthesisui/ds/${payload.slug}/v${v}/GUIDE.md`);
|
|
50
84
|
}
|
package/dist/commands/list.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { resolveRegistry } from "../config.js";
|
|
2
2
|
import { fetchList } from "../registry.js";
|
|
3
|
-
/**
|
|
3
|
+
/** Lists the published design systems available in the registry. */
|
|
4
4
|
export async function list(opts) {
|
|
5
5
|
const base = resolveRegistry(opts.registry);
|
|
6
6
|
const systems = await fetchList(base);
|
|
7
7
|
if (systems.length === 0) {
|
|
8
|
-
console.log(`
|
|
8
|
+
console.log(`No design systems published at ${base}.`);
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
|
-
console.log(`
|
|
11
|
+
console.log(`Available design systems (${base}):\n`);
|
|
12
12
|
const width = Math.max(...systems.map((s) => s.slug.length));
|
|
13
13
|
for (const s of systems) {
|
|
14
14
|
console.log(` ${s.slug.padEnd(width)} ${s.name} (v${s.version})`);
|
|
15
15
|
}
|
|
16
|
-
console.log(`\
|
|
16
|
+
console.log(`\nBring one in with: synthesisui add <slug>`);
|
|
17
17
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -4,7 +4,7 @@ import { RegistryError } from "../registry.js";
|
|
|
4
4
|
const CLIENT_ID = "synthesisui-cli";
|
|
5
5
|
const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
6
6
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
7
|
-
/**
|
|
7
|
+
/** Tries to open the OS browser; silent if it fails. */
|
|
8
8
|
function openBrowser(url) {
|
|
9
9
|
const [cmd, args] = process.platform === "darwin"
|
|
10
10
|
? ["open", [url]]
|
|
@@ -20,10 +20,10 @@ function openBrowser(url) {
|
|
|
20
20
|
child.unref();
|
|
21
21
|
}
|
|
22
22
|
catch {
|
|
23
|
-
//
|
|
23
|
+
// no browser available — the user opens it manually
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
/** Device authorization (RFC 8628):
|
|
26
|
+
/** Device authorization (RFC 8628): opens the browser, waits for approval. */
|
|
27
27
|
export async function login(opts) {
|
|
28
28
|
const base = resolveRegistry(opts.registry);
|
|
29
29
|
const codeRes = await fetch(`${base}/api/auth/device/code`, {
|
|
@@ -32,20 +32,20 @@ export async function login(opts) {
|
|
|
32
32
|
body: JSON.stringify({ client_id: CLIENT_ID }),
|
|
33
33
|
}).catch(() => null);
|
|
34
34
|
if (!codeRes || !codeRes.ok) {
|
|
35
|
-
throw new RegistryError(`
|
|
35
|
+
throw new RegistryError(`Could not start login at ${base}` +
|
|
36
36
|
(codeRes
|
|
37
37
|
? ` (HTTP ${codeRes.status}).`
|
|
38
|
-
: ".
|
|
38
|
+
: ". Check the URL and your connection."));
|
|
39
39
|
}
|
|
40
40
|
const code = (await codeRes.json());
|
|
41
|
-
console.log("\
|
|
42
|
-
console.log(` 1.
|
|
43
|
-
console.log(` 2.
|
|
44
|
-
console.log("(
|
|
41
|
+
console.log("\nTo connect the CLI to your account:");
|
|
42
|
+
console.log(` 1. open: ${code.verification_uri}`);
|
|
43
|
+
console.log(` 2. confirm the code: ${code.user_code}\n`);
|
|
44
|
+
console.log("(trying to open the browser…)");
|
|
45
45
|
openBrowser(code.verification_uri_complete);
|
|
46
46
|
let interval = (code.interval || 5) * 1000;
|
|
47
47
|
const deadline = Date.now() + (code.expires_in || 900) * 1000;
|
|
48
|
-
process.stdout.write("
|
|
48
|
+
process.stdout.write("waiting for approval");
|
|
49
49
|
while (Date.now() < deadline) {
|
|
50
50
|
await sleep(interval);
|
|
51
51
|
process.stdout.write(".");
|
|
@@ -63,7 +63,7 @@ export async function login(opts) {
|
|
|
63
63
|
: {};
|
|
64
64
|
if (tokenRes?.ok && typeof data.access_token === "string") {
|
|
65
65
|
await writeToken(data.access_token, base);
|
|
66
|
-
console.log("\n✓ Login
|
|
66
|
+
console.log("\n✓ Login complete. Token saved to ~/.synthesisui/credentials.json");
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
const err = data.error;
|
|
@@ -73,7 +73,7 @@ export async function login(opts) {
|
|
|
73
73
|
interval += 5000;
|
|
74
74
|
continue;
|
|
75
75
|
}
|
|
76
|
-
throw new RegistryError(`\nLogin
|
|
76
|
+
throw new RegistryError(`\nLogin failed: ${data.error_description ?? err ?? tokenRes?.status ?? "unknown error"}`);
|
|
77
77
|
}
|
|
78
|
-
throw new RegistryError("\
|
|
78
|
+
throw new RegistryError("\nThe code expired before approval. Run `synthesisui login` again.");
|
|
79
79
|
}
|
package/dist/config.js
CHANGED
|
@@ -2,20 +2,20 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* `--registry <url>`
|
|
7
|
-
*
|
|
5
|
+
* Default registry (canonical production domain). Overridable via
|
|
6
|
+
* `--registry <url>` or `SYNTHESISUI_REGISTRY_URL` (e.g. http://localhost:3000
|
|
7
|
+
* in dev).
|
|
8
8
|
*/
|
|
9
9
|
export const DEFAULT_REGISTRY = "https://www.synthesisui.com";
|
|
10
10
|
export function resolveRegistry(flag) {
|
|
11
11
|
const base = flag || process.env.SYNTHESISUI_REGISTRY_URL || DEFAULT_REGISTRY;
|
|
12
|
-
return base.replace(/\/+$/, ""); //
|
|
12
|
+
return base.replace(/\/+$/, ""); // no trailing slash
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
14
|
+
/** Where the device-flow token lives — per machine, in the home dir. */
|
|
15
15
|
export const credentialsPath = join(homedir(), ".synthesisui", "credentials.json");
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* Reads the saved token, if any. Optional for now (open gate); the device-flow
|
|
18
|
+
* is what writes it. Sent as a Bearer header when present.
|
|
19
19
|
*/
|
|
20
20
|
export async function readToken() {
|
|
21
21
|
try {
|
|
@@ -27,7 +27,7 @@ export async function readToken() {
|
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
30
|
+
/** Persists the device-flow token (chmod 600, user-only dir). */
|
|
31
31
|
export async function writeToken(token, registry) {
|
|
32
32
|
await mkdir(dirname(credentialsPath), { recursive: true, mode: 0o700 });
|
|
33
33
|
const payload = { token, registry, savedAt: new Date().toISOString() };
|
package/dist/guide.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const kebab = (v) => v.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
2
|
-
const list = (items) => items.length ? items.map((i) => `\`${i}\``).join(", ") : "_(
|
|
2
|
+
const list = (items) => items.length ? items.map((i) => `\`${i}\``).join(", ") : "_(none)_";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Builds GUIDE.md — instructions *for the agent* on how to build components
|
|
5
|
+
* that follow the design system. This is the piece that makes "I create the
|
|
6
|
+
* components with claude-code" work: the tokens alone are not enough, the agent
|
|
7
|
+
* needs the rules and the real vocabulary (semantic token names and recipes).
|
|
8
8
|
*/
|
|
9
9
|
export function buildGuide(payload) {
|
|
10
10
|
const { document: doc, slug, name, version } = payload;
|
|
@@ -13,13 +13,14 @@ export function buildGuide(payload) {
|
|
|
13
13
|
const hasAlt = foundations.color.semanticAlt &&
|
|
14
14
|
Object.keys(foundations.color.semanticAlt).length > 0;
|
|
15
15
|
const altScheme = meta.scheme === "light" ? "dark" : "light";
|
|
16
|
+
const hasTailwind = "theme.css" in payload.artifacts;
|
|
16
17
|
const componentLines = Object.entries(components).map(([cname, recipe]) => {
|
|
17
18
|
const cls = `.ds-${kebab(cname)}`;
|
|
18
19
|
const axes = Object.entries(recipe.variants).map(([axis, opts]) => {
|
|
19
20
|
const options = Object.keys(opts);
|
|
20
21
|
return `\`data-${kebab(axis)}="${options.join("|")}"\``;
|
|
21
22
|
});
|
|
22
|
-
const variantsText = axes.length ? ` —
|
|
23
|
+
const variantsText = axes.length ? ` — variants: ${axes.join(", ")}` : "";
|
|
23
24
|
return `- **${cname}** (\`${cls}\`)${variantsText}\n ${recipe.description}`;
|
|
24
25
|
});
|
|
25
26
|
const artifactList = Object.keys(payload.artifacts)
|
|
@@ -27,81 +28,120 @@ export function buildGuide(payload) {
|
|
|
27
28
|
.join(", ");
|
|
28
29
|
return `# Design System: ${name}
|
|
29
30
|
|
|
30
|
-
>
|
|
31
|
-
>
|
|
31
|
+
> Generated by \`synthesisui add ${slug}\` (v${version}). **Do not edit by hand** —
|
|
32
|
+
> run \`synthesisui add ${slug}\` again to update.
|
|
32
33
|
|
|
33
34
|
${meta.tagline}
|
|
34
35
|
|
|
35
36
|
**Mood:** ${meta.mood.join(" · ")}
|
|
36
|
-
**
|
|
37
|
-
${meta.sourceUrl ? `**
|
|
37
|
+
**Default scheme:** ${meta.scheme}${hasAlt ? ` (supports a toggle to ${altScheme})` : ""}
|
|
38
|
+
${meta.sourceUrl ? `**Reinterpretation of:** ${meta.sourceUrl}` : "**Original system.**"}
|
|
38
39
|
|
|
39
40
|
${meta.narrative}
|
|
40
41
|
|
|
41
42
|
---
|
|
42
43
|
|
|
43
|
-
##
|
|
44
|
+
## How to apply
|
|
44
45
|
|
|
45
|
-
1.
|
|
46
|
+
1. Import the tokens once in your project's global CSS:
|
|
46
47
|
\`\`\`css
|
|
47
48
|
@import "./_synthesisui/ds/${slug}/tokens.css";
|
|
48
49
|
\`\`\`
|
|
49
|
-
(
|
|
50
|
+
(adjust the relative path to where your CSS lives.)
|
|
50
51
|
|
|
51
|
-
2.
|
|
52
|
+
2. Wrap the tree that should use the system with the scope attribute:
|
|
52
53
|
\`\`\`html
|
|
53
|
-
<div data-ds="${slug}">…
|
|
54
|
+
<div data-ds="${slug}">…your UI here…</div>
|
|
54
55
|
\`\`\`
|
|
55
|
-
|
|
56
|
+
All \`--ds-*\` custom properties and \`.ds-*\` classes only apply inside that scope.
|
|
56
57
|
${hasAlt
|
|
57
58
|
? `
|
|
58
|
-
3. Light/dark:
|
|
59
|
+
3. Light/dark: an ancestor with \`data-scheme="${altScheme}"\` switches the neutral roles to the opposite mode.
|
|
59
60
|
\`\`\`html
|
|
60
61
|
<div data-scheme="${altScheme}"><div data-ds="${slug}">…</div></div>
|
|
61
62
|
\`\`\`
|
|
63
|
+
`
|
|
64
|
+
: ""}${hasTailwind
|
|
65
|
+
? `
|
|
66
|
+
## Styling with Tailwind v4 (preferred in this project)
|
|
67
|
+
|
|
68
|
+
Import \`theme.css\` after \`tailwindcss\` and \`tokens.css\`:
|
|
69
|
+
\`\`\`css
|
|
70
|
+
@import "tailwindcss";
|
|
71
|
+
@import "./_synthesisui/ds/${slug}/tokens.css";
|
|
72
|
+
@import "./_synthesisui/ds/${slug}/theme.css";
|
|
73
|
+
\`\`\`
|
|
74
|
+
This maps the DS tokens onto Tailwind's theme, so inside \`[data-ds="${slug}"]\` you get utilities
|
|
75
|
+
backed by the design system: \`bg-*\`/\`text-*\`/\`border-*\` (semantic colors), \`p-*\`/\`m-*\`/\`gap-*\`
|
|
76
|
+
(spacing), \`rounded-*\`, \`shadow-*\`, \`font-*\`, \`ease-*\`.
|
|
77
|
+
|
|
78
|
+
**Prefer these utilities for layout and new composition** — they are this project's idiom and read
|
|
79
|
+
far better than inline \`style\`. Reach for inline \`var(--ds-*)\` only when no utility fits.
|
|
80
|
+
|
|
81
|
+
\`\`\`tsx
|
|
82
|
+
// ✅ preferred — Tailwind utilities backed by the DS
|
|
83
|
+
<main className="bg-canvas text-foreground p-2xl flex flex-col gap-md">
|
|
84
|
+
<button className="ds-button" data-intent="primary">Save</button>
|
|
85
|
+
</main>
|
|
86
|
+
|
|
87
|
+
// ❌ avoid — inline styles with raw var() when a utility exists
|
|
88
|
+
<main style={{ background: "var(--ds-color-semantic-canvas)", padding: "var(--ds-spacing-2xl)" }}>
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
---
|
|
62
92
|
`
|
|
63
93
|
: ""}
|
|
64
|
-
|
|
94
|
+
This is **v${version}**. The stable entrypoints at \`_synthesisui/ds/${slug}/\` (the
|
|
95
|
+
\`tokens.css\`/\`theme.css\` re-exports, plus \`.lock\`) always point at the active version — import
|
|
96
|
+
those, not the versioned ones. The pinned files for this version — ${artifactList},
|
|
97
|
+
\`design-system.json\` (canonical source of truth), \`GUIDE.md\` (this file) — live in
|
|
98
|
+
\`_synthesisui/ds/${slug}/v${version}/\`.
|
|
65
99
|
|
|
66
100
|
---
|
|
67
101
|
|
|
68
|
-
##
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
102
|
+
## Rules (follow them when creating components)
|
|
103
|
+
${hasTailwind
|
|
104
|
+
? `
|
|
105
|
+
- **Styling mechanism:** prefer Tailwind utilities backed by the DS (\`bg-primary\`, \`p-md\`,
|
|
106
|
+
\`font-display\`, …) for layout and new composition, and reuse the \`.ds-*\` recipes for components
|
|
107
|
+
the DS already covers. Use inline \`style\` with \`var(--ds-*)\` only as a last resort. The token
|
|
108
|
+
names below are the source vocabulary — every utility derives from them.`
|
|
109
|
+
: ""}
|
|
110
|
+
- **Always use semantic tokens**, never raw values nor primitives directly.
|
|
111
|
+
Color: \`var(--ds-color-semantic-<role>)\`${hasTailwind ? " (utility: `bg-<role>`/`text-<role>`)" : ""}. The roles are: ${list(semanticRoles)}.
|
|
112
|
+
- Primitives (\`--ds-color-<palette>-<step>\`) exist but should **not** be referenced directly —
|
|
113
|
+
they feed the semantic roles.
|
|
114
|
+
- Spacing → \`var(--ds-spacing-<key>)\`: ${list(Object.keys(foundations.spacing))}.
|
|
115
|
+
- Radius → \`var(--ds-radius-<key>)\`: ${list(Object.keys(foundations.radius))}.
|
|
116
|
+
- Shadow → \`var(--ds-shadow-<key>)\`: ${list(Object.keys(foundations.shadow))}.
|
|
117
|
+
- Typography: families \`--ds-typography-families-{display,body,mono}\` (${foundations.typography.families.display}, ${foundations.typography.families.body}, ${foundations.typography.families.mono});
|
|
118
|
+
scale \`--ds-typography-scale-<key>-font-size\` etc.: ${list(Object.keys(foundations.typography.scale))}.
|
|
119
|
+
- Motion: durations \`--ds-motion-durations-<key>\` (${list(Object.keys(motion.durations))}) and
|
|
80
120
|
easings \`--ds-motion-easings-<key>\` (${list(Object.keys(motion.easings))}).
|
|
81
|
-
-
|
|
82
|
-
|
|
121
|
+
- When **creating a new component** the DS does not cover yet: compose it from these semantic
|
|
122
|
+
tokens to inherit the system's identity; do not invent colors/measures outside the scale.
|
|
83
123
|
|
|
84
124
|
---
|
|
85
125
|
|
|
86
|
-
##
|
|
126
|
+
## Where to preview
|
|
87
127
|
|
|
88
|
-
**Preview
|
|
89
|
-
|
|
90
|
-
samples
|
|
91
|
-
(home, layout,
|
|
92
|
-
|
|
128
|
+
**Preview in isolation, never on a real page.** When creating or demoing a component, generate a
|
|
129
|
+
dedicated sample page — \`app/synthesisui-samples/<component>/\` in the Next.js App Router (or the
|
|
130
|
+
equivalent samples route/folder in the project's stack). **Do not** apply the component to real
|
|
131
|
+
production pages (home, layout, existing routes) unless explicitly asked. Samples let you review the
|
|
132
|
+
component in the context of the design system without touching the app.
|
|
93
133
|
|
|
94
134
|
---
|
|
95
135
|
|
|
96
|
-
##
|
|
136
|
+
## Ready-made components
|
|
97
137
|
|
|
98
|
-
|
|
99
|
-
|
|
138
|
+
Each recipe becomes a \`.ds-<name>\` class (inside the \`[data-ds="${slug}"]\` scope).
|
|
139
|
+
Variants are \`data-<axis>="<option>"\` attributes; states (hover/focus/active/disabled) ship in the CSS.
|
|
100
140
|
|
|
101
141
|
${componentLines.join("\n\n")}
|
|
102
142
|
|
|
103
143
|
---
|
|
104
144
|
|
|
105
|
-
|
|
145
|
+
_Full canonical source of truth (including values and keyframes) in \`design-system.json\`._
|
|
106
146
|
`;
|
|
107
147
|
}
|
package/dist/index.js
CHANGED
|
@@ -3,25 +3,27 @@ import { add } from "./commands/add.js";
|
|
|
3
3
|
import { list } from "./commands/list.js";
|
|
4
4
|
import { login } from "./commands/login.js";
|
|
5
5
|
import { RegistryError } from "./registry.js";
|
|
6
|
-
const HELP = `synthesisui —
|
|
6
|
+
const HELP = `synthesisui — bring SynthesisUI design systems into your project
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
synthesisui login [
|
|
10
|
-
synthesisui list [
|
|
11
|
-
synthesisui add <slug> [
|
|
8
|
+
Usage:
|
|
9
|
+
synthesisui login [options] connect the CLI to your account (device-flow)
|
|
10
|
+
synthesisui list [options] list the published design systems
|
|
11
|
+
synthesisui add <slug> [options] materialize a DS into _synthesisui/ds/<slug>/
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
--registry <url> URL
|
|
15
|
-
--dir <path>
|
|
16
|
-
|
|
13
|
+
Options:
|
|
14
|
+
--registry <url> registry URL (or env SYNTHESISUI_REGISTRY_URL)
|
|
15
|
+
--dir <path> consumer project root (default: current directory)
|
|
16
|
+
--version <n> install a specific version (default: latest)
|
|
17
|
+
-h, --help this help
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
Examples:
|
|
19
20
|
synthesisui login
|
|
20
21
|
synthesisui list
|
|
21
22
|
synthesisui add halogen
|
|
23
|
+
synthesisui add halogen --version 3
|
|
22
24
|
synthesisui add halogen --registry http://localhost:3737
|
|
23
25
|
`;
|
|
24
|
-
/**
|
|
26
|
+
/** Extracts simple `--flag value` pairs and the remaining positionals. */
|
|
25
27
|
function parseFlags(argv) {
|
|
26
28
|
const positionals = [];
|
|
27
29
|
const flags = {};
|
|
@@ -63,25 +65,34 @@ async function main() {
|
|
|
63
65
|
case "add": {
|
|
64
66
|
const slug = args[0];
|
|
65
67
|
if (!slug) {
|
|
66
|
-
console.error("
|
|
68
|
+
console.error("error: provide the slug — `synthesisui add <slug>`");
|
|
67
69
|
process.exitCode = 1;
|
|
68
70
|
return;
|
|
69
71
|
}
|
|
70
|
-
|
|
72
|
+
let version;
|
|
73
|
+
if (typeof flags.version === "string") {
|
|
74
|
+
version = Number.parseInt(flags.version.replace(/^v/i, ""), 10);
|
|
75
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
76
|
+
console.error(`error: invalid --version "${flags.version}" — use an integer ≥ 1`);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
await add(slug, { registry, dir, version });
|
|
71
82
|
break;
|
|
72
83
|
}
|
|
73
84
|
case "login":
|
|
74
85
|
await login({ registry });
|
|
75
86
|
break;
|
|
76
87
|
default:
|
|
77
|
-
console.error(`
|
|
88
|
+
console.error(`unknown command: "${command}"\n`);
|
|
78
89
|
console.log(HELP);
|
|
79
90
|
process.exitCode = 1;
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
main().catch((err) => {
|
|
83
94
|
if (err instanceof RegistryError) {
|
|
84
|
-
console.error(`
|
|
95
|
+
console.error(`error: ${err.message}`);
|
|
85
96
|
}
|
|
86
97
|
else {
|
|
87
98
|
console.error(err);
|
package/dist/registry.js
CHANGED
|
@@ -11,29 +11,34 @@ async function request(url) {
|
|
|
11
11
|
res = await fetch(url, { headers: await authHeaders() });
|
|
12
12
|
}
|
|
13
13
|
catch {
|
|
14
|
-
throw new RegistryError(`
|
|
15
|
-
`
|
|
14
|
+
throw new RegistryError(`Could not reach the registry at ${url}. ` +
|
|
15
|
+
`Check the URL (--registry / SYNTHESISUI_REGISTRY_URL) and your connection.`);
|
|
16
16
|
}
|
|
17
17
|
return res;
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
19
|
+
/** Lists the available published design systems. */
|
|
20
20
|
export async function fetchList(base) {
|
|
21
21
|
const res = await request(`${base}/api/registry/ds`);
|
|
22
22
|
if (!res.ok) {
|
|
23
|
-
throw new RegistryError(`Registry
|
|
23
|
+
throw new RegistryError(`Registry responded ${res.status} while listing.`);
|
|
24
24
|
}
|
|
25
25
|
const body = (await res.json());
|
|
26
26
|
return body.designSystems ?? [];
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Fetches a published DS already compiled (document + artifacts). Without
|
|
30
|
+
* `version` it returns the latest; with `version` it returns that one.
|
|
31
|
+
*/
|
|
32
|
+
export async function fetchDesignSystem(base, slug, version) {
|
|
33
|
+
const url = new URL(`${base}/api/registry/ds/${encodeURIComponent(slug)}`);
|
|
34
|
+
if (version != null)
|
|
35
|
+
url.searchParams.set("version", String(version));
|
|
36
|
+
const res = await request(url.toString());
|
|
31
37
|
if (res.status === 404) {
|
|
32
|
-
throw new RegistryError(`
|
|
33
|
-
`Rode \`synthesisui list\` para ver os disponíveis.`);
|
|
38
|
+
throw new RegistryError(`No design system published with slug "${slug}"${version != null ? ` at version v${version}` : ""}. Run \`synthesisui list\` to see what's available.`);
|
|
34
39
|
}
|
|
35
40
|
if (!res.ok) {
|
|
36
|
-
throw new RegistryError(`Registry
|
|
41
|
+
throw new RegistryError(`Registry responded ${res.status} while fetching "${slug}".`);
|
|
37
42
|
}
|
|
38
43
|
return (await res.json());
|
|
39
44
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* `@synthesisui-hub/ds-contracts` (
|
|
4
|
-
*
|
|
2
|
+
* Minimal mirror of the registry contract — the CLI is standalone and does NOT
|
|
3
|
+
* import `@synthesisui-hub/ds-contracts` (it only consumes the endpoint JSON).
|
|
4
|
+
* We type only what the CLI reads to generate GUIDE.md and the .lock.
|
|
5
5
|
*/
|
|
6
6
|
export {};
|