synthesisui 0.16.14 → 0.16.16

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
@@ -118,6 +118,61 @@ If it reports a value with no token, do NOT invent one. Say which value and what
118
118
  you would call it, and let a person decide. An agent that quiets the report by
119
119
  adding a token is growing the system unsupervised, which is worse than the
120
120
  drift because it looks solved.`;
121
+ /**
122
+ * TWO THINGS AN AGENT GOT WRONG ON 28/07, BOTH CHEAP TO SAY OUT LOUD.
123
+ *
124
+ * It started a dev server to look at its work and left it running. The owner
125
+ * found it by trying to start his own and getting "Another next dev server is
126
+ * already running", with a PID to kill. Nothing in any contract had said to
127
+ * clean up, and a build proves the same thing without holding a port.
128
+ *
129
+ * And it offered to rebuild the create-next-app landing page by hand, on
130
+ * `ds-button` and `ds-card`, when a command already resets it in two seconds.
131
+ * It had found `clean` while exploring the CLI, so this is not a discovery
132
+ * problem - it is that nothing told it the command answers that situation.
133
+ *
134
+ * `--force` needs a person because it deletes. The dry run does not, so the
135
+ * agent is pointed at the harmless half and told to ask for the other.
136
+ */
137
+ /**
138
+ * THE ONE THING MOST LIKELY TO ALREADY BE IN THE PROJECT.
139
+ *
140
+ * Every system compiles a `shadcn.css` mapping shadcn's whole variable contract
141
+ * onto its own tokens. It has been generated into every project since it was
142
+ * built and nothing ever mentioned it, so the feature that answers "why not just
143
+ * use shadcn" was sitting unimported in the repo of the person who asked.
144
+ *
145
+ * Told to the AGENT and not only to the person, because the agent is who reaches
146
+ * for a shadcn block. Left to guess, it installs `dashboard-01` and gets a
147
+ * sidebar on shadcn's defaults next to components wearing the system, which
148
+ * reads as a broken product rather than a missing import.
149
+ *
150
+ * Conditional on `components.json`: a project without shadcn must not carry a
151
+ * paragraph about it.
152
+ */
153
+ const SHADCN = (slug) => `
154
+
155
+ **This project has shadcn, and this system already speaks its language.** A
156
+ generated bridge maps shadcn's whole variable contract - colour, charts, radius,
157
+ the sidebar family - onto this system's tokens, in both schemes:
158
+
159
+ @import "_synthesisui/ds/${slug}/shadcn.css";
160
+
161
+ after the tokens.css import, in the same stylesheet. With that line in place,
162
+ shadcn components wear this system and need no edits from you. Without it they
163
+ stay on shadcn's defaults, which looks like the design system failing rather than
164
+ one import missing - so check for it before you add a shadcn component, and add
165
+ it if it is not there.`;
166
+ const VERIFY = `
167
+
168
+ **Verify with a build, not a running server.** If you do start a dev server, stop
169
+ it before you finish - one left running holds a port the next person needs.
170
+
171
+ If \`create-next-app\` boilerplate is in your way (the default page, the README,
172
+ the SVGs in \`public/\`), do not rebuild those files by hand. Run
173
+ \`npx synthesisui@latest clean\` to see what it would remove: it only touches files
174
+ nobody has edited. Ask the person before running it with \`--force\`, because that
175
+ one deletes.`;
121
176
  const SELF_CHECK_MANUAL = `
122
177
 
123
178
  **After writing or editing any UI file, check it before moving on:**
@@ -130,7 +185,7 @@ a component checked while you still have its context is a two-line fix; the same
130
185
  component found in a repo-wide audit next week is an archaeology exercise.${REFUSAL}
131
186
 
132
187
  Better than remembering: \`npx synthesisui@latest connect\` installs the check as
133
- an editor hook, so it runs on its own and this paragraph stops being your job.`;
188
+ an editor hook, so it runs on its own and this paragraph stops being your job.${VERIFY}`;
134
189
  const SELF_CHECK_HOOKED = `
135
190
 
136
191
  **The check runs by itself.** A hook reports on every file you write, naming the
@@ -138,7 +193,7 @@ token this project already has for anything hardcoded. You do not need to run
138
193
  \`doctor\` by hand, and you should not wait for a final pass.
139
194
 
140
195
  When it names something, fix it in that file before moving to the next one -
141
- you still have the context, so it is a two-line change.${REFUSAL}`;
196
+ you still have the context, so it is a two-line change.${REFUSAL}${VERIFY}`;
142
197
  /**
143
198
  * THE INTERFACE LANGUAGE, READ FROM THE PROJECT RATHER THAN ASKED FOR.
144
199
  *
@@ -251,9 +306,16 @@ element. Write every user-facing string in that language - labels, empty states,
251
306
  \`alt\`. A screen reader pronounces \`aria-label\` using \`lang\`, so a mixed-language interface is
252
307
  worse than an untranslated one. If the attribute is wrong, change it rather than writing against
253
308
  it.`;
309
+ // Named for the first installed system: the bridge is per-system, and a
310
+ // project with two of them has already chosen which one dresses the app.
311
+ const bridgeable = installed.find((d) => !d.adopted);
312
+ const shadcn = bridgeable &&
313
+ (await readFile(join(projectRoot, "components.json"), "utf8").then(() => true, () => false))
314
+ ? SHADCN(bridgeable.slug)
315
+ : "";
254
316
  const body = `## Design Systems (via SynthesisUI)
255
317
 
256
- This project uses design system(s) tracked by the \`synthesisui\` CLI. ${rule}${language}
318
+ This project uses design system(s) tracked by the \`synthesisui\` CLI. ${rule}${language}${shadcn}
257
319
 
258
320
  ${sections.join("\n")}
259
321
 
@@ -138,19 +138,51 @@ export async function add(slug, opts) {
138
138
  console.log(line(`1. Import the system in your GLOBAL stylesheet, e.g. ${appDir}/globals.css`));
139
139
  console.log(line(` (the path is relative to that file - hence the leading ${importPrefix}):`));
140
140
  console.log("");
141
- console.log(snippet(hasTheme
142
- ? [
143
- `@import "tailwindcss";`,
144
- `@import "${importPrefix}_synthesisui/ds/${payload.slug}/tokens.css";`,
145
- `@import "${importPrefix}_synthesisui/ds/${payload.slug}/theme.css"; /* Tailwind utilities on your tokens */`,
146
- ]
147
- : [
148
- `@import "${importPrefix}_synthesisui/ds/${payload.slug}/tokens.css";`,
149
- ]));
141
+ // THE BRIDGE ONLY EXISTS FOR PEOPLE WHO ALREADY HAVE SHADCN, so it only
142
+ // appears for them. Every system compiles a `shadcn.css` mapping shadcn's
143
+ // whole variable contract onto its own tokens, and until 28/07 nothing said
144
+ // so - not this output, not the managed block, not the GUIDE beyond a filename
145
+ // in a list. The person it mattered most to asked "I don't get the use case, I
146
+ // can just build a design system with shadcn", with the answer sitting
147
+ // unimported in his own repo. Naming it to everybody would be noise; naming it
148
+ // to whoever has `components.json` is the whole feature arriving.
149
+ const hasShadcn = await access(join(projectRoot, "components.json")).then(() => true, () => false);
150
+ console.log(snippet([
151
+ ...(hasTheme ? [`@import "tailwindcss";`] : []),
152
+ `@import "${importPrefix}_synthesisui/ds/${payload.slug}/tokens.css";`,
153
+ ...(hasTheme
154
+ ? [
155
+ `@import "${importPrefix}_synthesisui/ds/${payload.slug}/theme.css"; /* Tailwind utilities on your tokens */`,
156
+ ]
157
+ : []),
158
+ ...(hasShadcn
159
+ ? [
160
+ `@import "${importPrefix}_synthesisui/ds/${payload.slug}/shadcn.css"; /* your shadcn components, wearing this system */`,
161
+ ]
162
+ : []),
163
+ ]));
164
+ if (hasShadcn) {
165
+ console.log("");
166
+ console.log(line(` You have shadcn here. That third line maps its whole variable contract`));
167
+ console.log(line(` onto this system - colour, charts, radius and the sidebar - so its`));
168
+ console.log(line(` components stop using shadcn's defaults. Both schemes included.`));
169
+ }
150
170
  console.log("");
151
171
  console.log(line(`2. Scope your app: add data-ds="${payload.slug}" to a ROOT element, e.g. ${appDir}/layout.tsx:`));
152
172
  console.log("");
153
- console.log(snippet([`<body data-ds="${payload.slug}">{children}</body>`]));
173
+ console.log(snippet([
174
+ `<body data-ds="${payload.slug}" className="bg-canvas">{children}</body>`,
175
+ ]));
176
+ // THE SCOPE DELIBERATELY DOES NOT PAINT A BACKGROUND, and every project hits
177
+ // it. `[data-ds]` sets ink and body family only, because scopes NEST: a dark
178
+ // contrast section over a photograph carries its own nested `data-ds` so its
179
+ // tokens remap, and a background on every scope would paint over the picture.
180
+ // So the page background is the root element's job, and saying so here is the
181
+ // fix - an agent found this by reading rendered CSS on 28/07 and had to
182
+ // deduce it.
183
+ console.log(line(` \`bg-canvas\` is not optional: the scope sets text colour and type, not the page`));
184
+ console.log(line(` background. Scopes nest (a dark section carries its own), so painting every one`));
185
+ console.log(line(` would cover whatever is behind it.`));
154
186
  // 3. Load the type - the DS ships token NAMES, not the fonts themselves.
155
187
  // Next apps get fonts.ts MATERIALIZED (deterministic does, not teaches):
156
188
  // next/font = self-hosted + preloaded + adjusted fallback, no FOUT
@@ -287,15 +287,37 @@ async function readWiring(root, slug) {
287
287
  fontsWritten: false,
288
288
  /** …and the stylesheet actually maps it onto the system's type tokens. */
289
289
  fontsMapped: false,
290
+ /**
291
+ * THE FEATURE NOBODY KNEW WE SHIPPED.
292
+ *
293
+ * Every system compiles a `shadcn.css` mapping shadcn's whole variable
294
+ * contract onto its own tokens, so shadcn components wear the system instead
295
+ * of shadcn's defaults. It has been generated into every project since it
296
+ * was built, and until 28/07 nothing mentioned it: not `add`, not the
297
+ * managed block, not the GUIDE except as a filename in a list. The only
298
+ * documentation was a comment inside the file, which is the same as none.
299
+ *
300
+ * The person it mattered most to is the one who asked "I don't get the use
301
+ * case, I can just build a design system with shadcn" - and the answer was
302
+ * sitting unimported in his own repo.
303
+ *
304
+ * Reported only when the project HAS shadcn. Telling everybody about a
305
+ * bridge they will never use is how a report earns the skim.
306
+ */
307
+ hasShadcn: false,
308
+ bridged: false,
290
309
  };
291
310
  if (!slug)
292
311
  return w;
312
+ w.hasShadcn = await readFile(join(root, "components.json"), "utf8").then(() => true, () => false);
293
313
  for await (const file of walk(root)) {
294
314
  const src = await readFile(file, "utf8").catch(() => "");
295
315
  if (!src)
296
316
  continue;
297
317
  if (src.includes(`_synthesisui/ds/${slug}/tokens.css`))
298
318
  w.imported = true;
319
+ if (src.includes(`_synthesisui/ds/${slug}/shadcn.css`))
320
+ w.bridged = true;
299
321
  if (src.includes(`data-ds="${slug}"`))
300
322
  w.scoped = true;
301
323
  // The requirement that stayed invisible. `init` writes a fonts file and
@@ -306,7 +328,9 @@ async function readWiring(root, slug) {
306
328
  w.fontsWritten = true;
307
329
  if (/--ds-typography-families-\w+\s*:\s*var\(\s*--font-ds-/.test(src))
308
330
  w.fontsMapped = true;
309
- if (w.imported && w.scoped && w.fontsMapped)
331
+ // The bridge joins the early exit, otherwise the walk can stop before the
332
+ // stylesheet that imports it and report a wired project as unbridged.
333
+ if (w.imported && w.scoped && w.fontsMapped && (!w.hasShadcn || w.bridged))
310
334
  break;
311
335
  }
312
336
  return w;
@@ -461,6 +485,37 @@ export async function doctor(opts) {
461
485
  }
462
486
  console.log(body("The exact snippets are in the output of `init`."));
463
487
  }
488
+ /**
489
+ * SHADCN IS THE MOST LIKELY THING ALREADY IN THE PROJECT, AND THE BRIDGE WAS
490
+ * INVISIBLE.
491
+ *
492
+ * Two states, and both are worth a line. Unimported is a real finding: the
493
+ * components a person spends all day looking at are on shadcn's defaults while
494
+ * everything around them wears the system, which reads as "this product does
495
+ * not work here" rather than "one import missing".
496
+ *
497
+ * Imported is worth saying too, and that is the unusual part. It is the only
498
+ * place we can answer "how do I know my shadcn is going through the system?"
499
+ * with something checked rather than claimed. A governance tool that cannot
500
+ * show its work is asking for trust it has not earned.
501
+ */
502
+ if (hasSystem && wiring.hasShadcn) {
503
+ console.log("");
504
+ if (wiring.bridged) {
505
+ console.log(body(`✓ shadcn is reading ${table.name ?? table.slug}, not its own`));
506
+ console.log(body(" defaults. Colour, charts, radius and the sidebar all resolve"));
507
+ console.log(body(" to this system's tokens, in both schemes."));
508
+ }
509
+ else {
510
+ console.log(body("This project has shadcn, and it is not wearing the"));
511
+ console.log(body("system yet - its components are still on shadcn's defaults."));
512
+ console.log("");
513
+ console.log(body(` @import "_synthesisui/ds/${table.slug}/shadcn.css";`));
514
+ console.log("");
515
+ console.log(body(" after the tokens.css import, in the same stylesheet. One line,"));
516
+ console.log(body(" and every shadcn component switches over."));
517
+ }
518
+ }
464
519
  if (hasSystem && measurable) {
465
520
  console.log("");
466
521
  console.log(body(`Token coverage ${meter(d.coverage)} ${String(d.coverage).padStart(3)}%`));
@@ -1,5 +1,5 @@
1
- import { readFile } from "node:fs/promises";
2
- import { relative, resolve } from "node:path";
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join, relative, resolve } from "node:path";
3
3
  import { diagnose, scanSource } from "../doctor/scan.js";
4
4
  import { loadSystem } from "./doctor.js";
5
5
  const pass = () => ({ continue: true });
@@ -13,6 +13,39 @@ const speak = (context) => ({
13
13
  /** Files where a design value can even appear. A hook that parses a README on
14
14
  * every edit is paying for nothing. */
15
15
  const UI_FILE = /\.(tsx|jsx|ts|js|css|scss|vue|svelte)$/i;
16
+ /** Written after the hook's first report of any kind, and never read for anything
17
+ * else. Lives in our own directory, so removing the system removes the memory
18
+ * too. It is committed with `_synthesisui/`, which means the greeting is
19
+ * per-project and not per-teammate - a wart worth the simplicity, since the
20
+ * second person to clone a wired repo is not the one wondering if it installed. */
21
+ const GREETED = "_synthesisui/.hook-greeted";
22
+ const markGreeted = (root) => writeFile(join(root, GREETED), "", "utf8").catch(() => { });
23
+ /** One sentence, once per project. Addressed to the agent because the hook has
24
+ * no way to reach the person, and the person is who needs to know. */
25
+ async function greet(root, rel) {
26
+ const marker = join(root, GREETED);
27
+ try {
28
+ await readFile(marker, "utf8");
29
+ return null;
30
+ }
31
+ catch {
32
+ // Not greeted yet. Write first: a failed write must not produce a hook that
33
+ // greets on every single edit for the rest of the project's life.
34
+ try {
35
+ await writeFile(marker, "", "utf8");
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ return [
42
+ `${rel} - checked, nothing to name. Every value in it has a token.`,
43
+ "",
44
+ "That check now runs on every file you write, by itself. Tell the person once",
45
+ "that it is live, so they know it installed, and then stop mentioning it - from",
46
+ "here it only speaks when something needs a name.",
47
+ ].join("\n");
48
+ }
16
49
  async function report(root, filePath) {
17
50
  const { table } = await loadSystem(root);
18
51
  if (table.byName.size === 0)
@@ -28,7 +61,11 @@ async function report(root, filePath) {
28
61
  // no token to move to, so the only honest advice is "ask a person" - and
29
62
  // saying that after every edit trains the reader to skip the block.
30
63
  if (named.length === 0 && phantoms.length === 0)
31
- return null;
64
+ return greet(root, rel);
65
+ // A report IS the evidence the greeting exists to provide, so it counts as the
66
+ // introduction. Otherwise a project whose first file had drift would get the
67
+ // "it is live" sentence afterwards, telling somebody who just watched it work.
68
+ await markGreeted(root);
32
69
  const lines = [`${rel} - checked against ${table.name ?? table.slug}.`];
33
70
  if (named.length > 0) {
34
71
  lines.push("", "Values written by hand that this system already has a name for:", ...named
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.14",
3
+ "version": "0.16.16",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {