synthesisui 0.4.4 → 0.4.6

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/claude-md.js CHANGED
@@ -26,23 +26,83 @@ async function readInstalled(projectRoot) {
26
26
  }
27
27
  return locks;
28
28
  }
29
- function renderRegion(installed) {
29
+ /** First sentence of a description, capped - the manifest must stay lean. */
30
+ function summarize(desc) {
31
+ if (typeof desc !== "string" || !desc.trim())
32
+ return "";
33
+ const first = desc.trim().split(/(?<=\.)\s/)[0] ?? desc.trim();
34
+ return first.length > 90 ? `${first.slice(0, 87)}…` : first;
35
+ }
36
+ /** One manifest line per recipe: name, what it is, and its variant axes. */
37
+ function catalogLines(recipes) {
38
+ return Object.entries(recipes)
39
+ .sort(([a], [b]) => a.localeCompare(b))
40
+ .map(([name, recipe]) => {
41
+ const r = recipe;
42
+ const axes = Object.entries(r.variants ?? {})
43
+ .map(([axis, options]) => {
44
+ const keys = Object.keys(options);
45
+ return keys.length <= 4 ? `${axis}: ${keys.join("|")}` : axis;
46
+ })
47
+ .join("; ");
48
+ const desc = summarize(r.description);
49
+ return {
50
+ name,
51
+ line: ` - \`ds-${name}\`${desc ? ` - ${desc}` : ""}${axes ? ` [${axes}]` : ""}`,
52
+ };
53
+ });
54
+ }
55
+ /**
56
+ * The COMPONENT MANIFEST for one installed system, read from its versioned
57
+ * design-system.json. This is what lets the app's agent know what it already
58
+ * HAS - "use these before creating new ones" - instead of re-inventing
59
+ * buttons. Returns null when the document isn't readable (older installs).
60
+ */
61
+ async function readManifest(projectRoot, ds) {
62
+ try {
63
+ const raw = await readFile(join(projectRoot, "_synthesisui", "ds", ds.slug, `v${ds.version}`, "design-system.json"), "utf8");
64
+ const doc = JSON.parse(raw);
65
+ const components = catalogLines(doc.components ?? {});
66
+ const blocks = catalogLines(doc.blocks ?? {});
67
+ if (components.length === 0 && blocks.length === 0)
68
+ return null;
69
+ const lines = [];
70
+ if (components.length > 0) {
71
+ lines.push(` Components (${components.length}) - USE these before creating new ones:`);
72
+ lines.push(...components.map((c) => c.line));
73
+ }
74
+ if (blocks.length > 0) {
75
+ lines.push(` Engagement blocks (${blocks.length}):`);
76
+ lines.push(...blocks.map((c) => c.line));
77
+ }
78
+ return lines.join("\n");
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ async function renderRegion(projectRoot, installed) {
30
85
  if (installed.length === 0) {
31
86
  return `${START}\n${END}`;
32
87
  }
33
- const lines = installed
34
- .map((ds) => `- **${ds.name}** (\`${ds.slug}\`, v${ds.version}) - guide: \`_synthesisui/ds/${ds.slug}/v${ds.version}/GUIDE.md\``)
35
- .join("\n");
88
+ const sections = [];
89
+ for (const ds of installed) {
90
+ const head = `- **${ds.name}** (\`${ds.slug}\`, v${ds.version}) - guide: \`_synthesisui/ds/${ds.slug}/v${ds.version}/GUIDE.md\``;
91
+ const manifest = await readManifest(projectRoot, ds);
92
+ sections.push(manifest ? `${head}\n${manifest}` : head);
93
+ }
36
94
  const body = `## Design Systems (via SynthesisUI)
37
95
 
38
96
  This project uses design system(s) brought in by the \`synthesisui\` CLI. **When creating or editing
39
97
  components, read the system's GUIDE.md and follow it:** use only semantic tokens
40
98
  (\`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
99
+ and reuse the \`.ds-*\` classes. Do not use raw values outside the system's scale. **Before
100
+ creating any UI element, check the component manifest below - if it exists, use or extend
101
+ it (\`synthesisui component <slug> <name>\` materializes it as your code).** To review a
42
102
  component, create an isolated sample page (e.g. \`app/synthesisui-samples/<component>/\`) - do not
43
- apply it to real production pages unless asked.**
103
+ apply it to real production pages unless asked.
44
104
 
45
- ${lines}
105
+ ${sections.join("\n")}
46
106
 
47
107
  _Block managed by the CLI - do not edit by hand; run \`synthesisui add <slug>\` to update._`;
48
108
  return `${START}\n${body}\n${END}`;
@@ -54,7 +114,7 @@ _Block managed by the CLI - do not edit by hand; run \`synthesisui add <slug>\`
54
114
  */
55
115
  export async function syncClaudeMd(projectRoot) {
56
116
  const installed = await readInstalled(projectRoot);
57
- const region = renderRegion(installed);
117
+ const region = await renderRegion(projectRoot, installed);
58
118
  const path = join(projectRoot, "CLAUDE.md");
59
119
  let existing = null;
60
120
  try {
@@ -2,6 +2,7 @@ import { readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { generateComponentFiles } from "../component-codegen.js";
4
4
  import { readProjectConfig, resolveRegistry } from "../config.js";
5
+ import { diffLocalDocuments, localChangelogMarkdown, } from "../document-diff.js";
5
6
  import { body, section, snippet } from "../output.js";
6
7
  import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
7
8
  import { add } from "./add.js";
@@ -85,11 +86,29 @@ export async function upgrade(slug, opts) {
85
86
  }
86
87
  }
87
88
  }
88
- // 3. deterministic changelog → UPGRADE.md (the agent's migration brief)
89
- const log = await fetchChangelog(base, slug, installed, latest.version);
89
+ // 3. deterministic changelog → UPGRADE.md (the agent's migration brief).
90
+ // Diffed against the LOCALLY installed snapshot: personal systems mutate
91
+ // their working draft in place under a version number, so the server's
92
+ // vN may have moved since this app installed it ("draft drift") - a
93
+ // server-side history diff can be empty while this app's files differ.
94
+ // Fallback to the server changelog if the local snapshot is unreadable.
95
+ let markdown;
96
+ let breaking;
97
+ try {
98
+ const beforeDoc = JSON.parse(await readFile(join(slugDir, `v${installed}`, "design-system.json"), "utf8"));
99
+ const afterDoc = JSON.parse(await readFile(join(slugDir, `v${latest.version}`, "design-system.json"), "utf8"));
100
+ const local = diffLocalDocuments(beforeDoc, afterDoc);
101
+ markdown = localChangelogMarkdown(slug, installed, latest.version, local);
102
+ breaking = local.breaking;
103
+ }
104
+ catch {
105
+ const log = await fetchChangelog(base, slug, installed, latest.version);
106
+ markdown = log.markdown;
107
+ breaking = log.changelog.breaking;
108
+ }
90
109
  const upgradePath = join(slugDir, "UPGRADE.md");
91
110
  const brief = [
92
- log.markdown,
111
+ markdown,
93
112
  "",
94
113
  "---",
95
114
  "",
@@ -110,10 +129,10 @@ export async function upgrade(slug, opts) {
110
129
  await writeFile(upgradePath, brief, "utf8");
111
130
  // ── report ──
112
131
  console.log(section(`Upgraded ${slug}: v${installed} → v${latest.version}`));
113
- if (log.changelog.breaking.length > 0) {
114
- console.log(body(`Breaking changes (${log.changelog.breaking.length}):`));
132
+ if (breaking.length > 0) {
133
+ console.log(body(`Breaking changes (${breaking.length}):`));
115
134
  console.log("");
116
- console.log(snippet(log.changelog.breaking.map((item) => `- ${item}`)));
135
+ console.log(snippet(breaking.map((item) => `- ${item}`)));
117
136
  }
118
137
  else {
119
138
  console.log(body("No breaking changes detected."));
@@ -0,0 +1,174 @@
1
+ /**
2
+ * LOCAL document diff for `upgrade` - computed against the design-system.json
3
+ * snapshots on disk, not against the server's version history.
4
+ *
5
+ * Why local: personal design systems mutate their WORKING DRAFT in place under
6
+ * a fixed version number; only publishing freezes it. So the server's "v3" may
7
+ * have moved since this app installed it ("draft drift"), and a server-side
8
+ * v3→v4 changelog can be legitimately empty while the app's files differ. The
9
+ * app's truth is what it has installed - so that's what we diff.
10
+ */
11
+ /** Flatten a token subtree into `prefix.path → value` leaves. */
12
+ function flattenTokens(prefix, value, out) {
13
+ if (value == null)
14
+ return;
15
+ if (typeof value === "string" || typeof value === "number") {
16
+ out.set(prefix, String(value));
17
+ return;
18
+ }
19
+ if (Array.isArray(value)) {
20
+ value.forEach((v, i) => flattenTokens(`${prefix}.${i}`, v, out));
21
+ return;
22
+ }
23
+ if (typeof value === "object") {
24
+ for (const [k, v] of Object.entries(value)) {
25
+ flattenTokens(prefix ? `${prefix}.${k}` : k, v, out);
26
+ }
27
+ }
28
+ }
29
+ /** Key-order-insensitive equality (JSON docs round-trip with unstable order). */
30
+ function deepEqual(a, b) {
31
+ if (a === b)
32
+ return true;
33
+ if (typeof a !== typeof b)
34
+ return false;
35
+ if (Array.isArray(a) || Array.isArray(b)) {
36
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
37
+ return false;
38
+ return a.every((v, i) => deepEqual(v, b[i]));
39
+ }
40
+ if (a && b && typeof a === "object") {
41
+ const ka = Object.keys(a);
42
+ const kb = Object.keys(b);
43
+ if (ka.length !== kb.length)
44
+ return false;
45
+ return ka.every((k) => deepEqual(a[k], b[k]));
46
+ }
47
+ return false;
48
+ }
49
+ /** Variant options that existed and disappeared (breaking for the consumer). */
50
+ function removedVariantOptions(before, after) {
51
+ const out = [];
52
+ const prevVariants = (before.variants ?? {});
53
+ const nextVariants = (after.variants ?? {});
54
+ for (const [axis, options] of Object.entries(prevVariants)) {
55
+ const nextAxis = nextVariants[axis];
56
+ if (!nextAxis) {
57
+ out.push(axis);
58
+ continue;
59
+ }
60
+ for (const option of Object.keys(options)) {
61
+ if (!(option in nextAxis))
62
+ out.push(`${axis}="${option}"`);
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+ function diffRecipeMaps(before, after, label, breaking) {
68
+ const out = [];
69
+ const names = new Set([...Object.keys(before), ...Object.keys(after)]);
70
+ for (const name of [...names].sort()) {
71
+ const prev = before[name];
72
+ const next = after[name];
73
+ if (!prev) {
74
+ out.push({ name, kind: "added" });
75
+ continue;
76
+ }
77
+ if (!next) {
78
+ out.push({ name, kind: "removed" });
79
+ breaking.push(`${label} "${name}" was removed`);
80
+ continue;
81
+ }
82
+ if (deepEqual(prev, next))
83
+ continue;
84
+ out.push({ name, kind: "changed" });
85
+ for (const gone of removedVariantOptions(prev, next)) {
86
+ breaking.push(`${label} "${name}" no longer supports ${gone}`);
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+ /** Diff two installed design-system.json documents (token + recipe level). */
92
+ export function diffLocalDocuments(before, after) {
93
+ const breaking = [];
94
+ const prevTokens = new Map();
95
+ const nextTokens = new Map();
96
+ flattenTokens("", before.foundations, prevTokens);
97
+ flattenTokens("motion", before.motion, prevTokens);
98
+ flattenTokens("", after.foundations, nextTokens);
99
+ flattenTokens("motion", after.motion, nextTokens);
100
+ const tokens = [];
101
+ const paths = new Set([...prevTokens.keys(), ...nextTokens.keys()]);
102
+ for (const path of [...paths].sort()) {
103
+ const prev = prevTokens.get(path);
104
+ const next = nextTokens.get(path);
105
+ if (prev === next)
106
+ continue;
107
+ if (prev === undefined)
108
+ tokens.push({ path, after: next, kind: "added" });
109
+ else if (next === undefined) {
110
+ tokens.push({ path, before: prev, kind: "removed" });
111
+ breaking.push(`token "${path}" was removed`);
112
+ }
113
+ else
114
+ tokens.push({ path, before: prev, after: next, kind: "changed" });
115
+ }
116
+ const components = diffRecipeMaps((before.components ?? {}), (after.components ?? {}), "component", breaking);
117
+ const blocks = diffRecipeMaps((before.blocks ?? {}), (after.blocks ?? {}), "block", breaking);
118
+ return {
119
+ tokens,
120
+ components,
121
+ blocks,
122
+ breaking,
123
+ isEmpty: tokens.length === 0 && components.length === 0 && blocks.length === 0,
124
+ };
125
+ }
126
+ const CAP_TOKENS = 40;
127
+ /** The migration brief body, mirroring the server changelog's format. */
128
+ export function localChangelogMarkdown(slug, from, to, log) {
129
+ const lines = [
130
+ `# ${slug} - v${from} → v${to}`,
131
+ "",
132
+ `_Diffed against this app's installed v${from} snapshot (the app's truth) -`,
133
+ "personal systems can evolve in place under a version, so a server-side",
134
+ "history diff may miss what changed HERE._",
135
+ "",
136
+ ];
137
+ if (log.isEmpty) {
138
+ lines.push("No visual-contract changes for this app.", "");
139
+ return lines.join("\n");
140
+ }
141
+ if (log.breaking.length > 0) {
142
+ lines.push("## Breaking", "");
143
+ for (const item of log.breaking)
144
+ lines.push(`- ${item}`);
145
+ lines.push("");
146
+ }
147
+ if (log.tokens.length > 0) {
148
+ lines.push(`## Tokens (${log.tokens.length} change(s))`, "");
149
+ for (const t of log.tokens.slice(0, CAP_TOKENS)) {
150
+ const val = t.kind === "removed"
151
+ ? `removed (was \`${t.before}\`)`
152
+ : t.kind === "added"
153
+ ? `added: \`${t.after}\``
154
+ : `\`${t.before}\` → \`${t.after}\``;
155
+ lines.push(`- ${t.path}: ${val}`);
156
+ }
157
+ if (log.tokens.length > CAP_TOKENS) {
158
+ lines.push(`- …and ${log.tokens.length - CAP_TOKENS} more`);
159
+ }
160
+ lines.push("");
161
+ }
162
+ for (const [title, entries] of [
163
+ ["Components", log.components],
164
+ ["Blocks", log.blocks],
165
+ ]) {
166
+ if (entries.length === 0)
167
+ continue;
168
+ lines.push(`## ${title}`, "");
169
+ for (const entry of entries)
170
+ lines.push(`- \`ds-${entry.name}\` - ${entry.kind}`);
171
+ lines.push("");
172
+ }
173
+ return lines.join("\n");
174
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Traz design systems do SynthesisUI para qualquer projeto (materializa em _local/ds/).",
5
5
  "type": "module",
6
6
  "bin": {