synthesisui 0.16.77 → 0.16.79

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.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * TWO FACTS ABOUT THE PROJECT THE CODEGEN CANNOT GUESS.
3
+ *
4
+ * Both were being read by exactly one of the four commands that generate code, so
5
+ * the other three emitted something subtly wrong and nothing failed:
6
+ *
7
+ * the React major decides whether a component can take a `ref`
8
+ * the class convention decides whether its classes exist at all
9
+ *
10
+ * The second is the serious one. A compiled class attaches to THEIR markup, so an
11
+ * imported system keeps its own spelling - `.metric-card__title`, not
12
+ * `.ds-metric-card-title`. The codegen hardcoded ours, so `synthesisui component`
13
+ * handed somebody a React component wearing classes their own stylesheet never
14
+ * emits. It renders completely unstyled, and nothing anywhere reports a problem.
15
+ */
16
+ import { readdir, readFile } from "node:fs/promises";
17
+ import { join, relative } from "node:path";
18
+ import { DEFAULT_CONVENTION, } from "./component-codegen.js";
19
+ /**
20
+ * Which React the consumer is on, for the ref-carrying prop type.
21
+ *
22
+ * From 19 a `ref` is an ordinary prop; before it a function component needs
23
+ * `forwardRef`. Guessing high on an older project would emit a type that accepts a
24
+ * ref React then silently drops, so anything unreadable falls back to the
25
+ * ref-less type.
26
+ */
27
+ export async function reactMajorOf(root) {
28
+ const raw = await readFile(join(root, "package.json"), "utf8").catch(() => "");
29
+ if (!raw)
30
+ return null;
31
+ try {
32
+ const pkg = JSON.parse(raw);
33
+ const spec = pkg.dependencies?.react ?? pkg.devDependencies?.react;
34
+ const major = /(\d+)/.exec(spec ?? "")?.[1];
35
+ return major ? Number(major) : null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ export async function findCollision(root, componentsDir, name, pascal) {
42
+ const target = join(root, componentsDir, name, `${name}.tsx`);
43
+ const head = await readFile(target, "utf8")
44
+ .then((t) => t.slice(0, 300))
45
+ .catch(() => null);
46
+ const ours = head?.includes("Generated by SynthesisUI") ?? false;
47
+ /**
48
+ * Their own export of that name, searched where a component library lives.
49
+ *
50
+ * Deliberately shallow: `src`, `app`, `components`, `packages` and the
51
+ * configured folder, skipping anything generated. A deep walk of a monorepo to
52
+ * answer one question is not worth the seconds.
53
+ */
54
+ const pattern = new RegExp(`export\\s+(?:default\\s+)?(?:function|const|class)\\s+${pascal}\\b`);
55
+ const roots = [
56
+ componentsDir,
57
+ "src/components",
58
+ "components",
59
+ "src/ui",
60
+ "packages",
61
+ ];
62
+ let exported = null;
63
+ for (const rel of roots) {
64
+ if (exported)
65
+ break;
66
+ for await (const file of walkShallow(join(root, rel), 3)) {
67
+ if (!/\.(tsx|jsx|ts)$/.test(file))
68
+ continue;
69
+ if (file === target)
70
+ continue;
71
+ const source = await readFile(file, "utf8").catch(() => "");
72
+ if (source.includes("Generated by SynthesisUI"))
73
+ continue;
74
+ if (pattern.test(source)) {
75
+ exported = relative(root, file);
76
+ break;
77
+ }
78
+ }
79
+ }
80
+ return { file: head == null ? null : relative(root, target), exported, ours };
81
+ }
82
+ /** Files under `dir`, at most `depth` levels down, skipping the usual noise. */
83
+ async function* walkShallow(dir, depth) {
84
+ if (depth < 0)
85
+ return;
86
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
87
+ for (const entry of entries) {
88
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
89
+ continue;
90
+ const full = join(dir, entry.name);
91
+ if (entry.isDirectory())
92
+ yield* walkShallow(full, depth - 1);
93
+ else
94
+ yield full;
95
+ }
96
+ }
97
+ /** The pinned version of an installed system, from its `.lock`. */
98
+ async function pinnedVersion(dir) {
99
+ const raw = await readFile(join(dir, ".lock"), "utf8").catch(() => "");
100
+ if (!raw)
101
+ return null;
102
+ try {
103
+ const lock = JSON.parse(raw);
104
+ return typeof lock.version === "number" ? lock.version : null;
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ /**
111
+ * HOW THE INSTALLED SYSTEM SPELLS A CLASS, read off the document on disk.
112
+ *
113
+ * The document is the same one the compiled CSS came from, so this can never
114
+ * disagree with the stylesheet sitting next to it - which is the property that
115
+ * matters. A response field would be fresher, and a caller that has one should
116
+ * prefer it; this is what every other caller can rely on.
117
+ *
118
+ * Absent, unreadable or never declared all mean OURS, to the character, so no
119
+ * project that exists has to change.
120
+ */
121
+ export async function readInstalledConvention(root, slug) {
122
+ const dir = join(root, "_synthesisui", "ds", slug);
123
+ const version = await pinnedVersion(dir);
124
+ const raw = version
125
+ ? await readFile(join(dir, `v${version}`, "design-system.json"), "utf8").catch(() => "")
126
+ : "";
127
+ if (!raw)
128
+ return DEFAULT_CONVENTION;
129
+ try {
130
+ const doc = JSON.parse(raw);
131
+ const declared = doc.meta?.classNames;
132
+ if (!declared ||
133
+ typeof declared.prefix !== "string" ||
134
+ typeof declared.partSeparator !== "string") {
135
+ return DEFAULT_CONVENTION;
136
+ }
137
+ return {
138
+ prefix: declared.prefix,
139
+ partSeparator: declared.partSeparator,
140
+ };
141
+ }
142
+ catch {
143
+ return DEFAULT_CONVENTION;
144
+ }
145
+ }
@@ -133,6 +133,7 @@ Open the project and answer the questions below. Then add a \`reading\` object t
133
133
  "themes": { "default": "dark", "has": ["dark"] },
134
134
  "roles": { "canvas": "#050505", "foreground": "#f9fafb", "primary": "#4A90E2" },
135
135
  "fonts": { "display": "Inter", "body": "Inter" },
136
+ "typeRoles": { "base": "body-m", "display": "h1", "xs": "caption" },
136
137
  "concept": "one paragraph on what this product is",
137
138
  "rules": [
138
139
  {
@@ -149,14 +150,56 @@ Open the project and answer the questions below. Then add a \`reading\` object t
149
150
  "kind": "limit",
150
151
  "files": 1,
151
152
  "evidence": "one dashboard page; may just be how it happened"
153
+ },
154
+ {
155
+ "text": "TextEditor's editable region is <EditorContent> - never render children into it directly",
156
+ "applies": ["TextEditor"],
157
+ "kind": "implementation",
158
+ "fact": true,
159
+ "evidence": "read in TextEditor itself: useEditor() feeds a single <EditorContent>"
152
160
  }
153
161
  ],
154
162
  "components": {
155
163
  "MetricCard": {
156
- "parts": [
157
- { "name": "icon", "classes": "size-8 text-ocean-500" },
158
- { "name": "label", "classes": "text-xs uppercase text-lightgray-500" },
159
- { "name": "value", "classes": "text-2xl font-bold" }
164
+ "anatomy": [
165
+ { "as": "icon", "name": "icon", "classes": "size-8 text-ocean-500" },
166
+ {
167
+ "as": "stack",
168
+ "name": "body",
169
+ "classes": "flex flex-col gap-1",
170
+ "children": [
171
+ { "as": "text", "name": "label", "classes": "text-xs uppercase text-lightgray-500" },
172
+ { "as": "heading", "name": "value", "classes": "text-2xl font-bold" }
173
+ ]
174
+ }
175
+ ]
176
+ },
177
+ "ArticleCard": {
178
+ "root": "Card",
179
+ "anatomy": [
180
+ { "as": "image", "name": "cover", "classes": "aspect-video w-full rounded-t-lg" },
181
+ { "as": "heading", "name": "title", "classes": "text-lg font-semibold" },
182
+ { "as": "component", "ref": "TextEditor" },
183
+ {
184
+ "as": "row",
185
+ "name": "footer",
186
+ "classes": "flex items-center gap-2 border-t p-3",
187
+ "children": [
188
+ { "as": "button", "name": "publish", "classes": "btn btn-primary" },
189
+ { "as": "button", "name": "discard", "classes": "btn btn-ghost" }
190
+ ]
191
+ }
192
+ ]
193
+ },
194
+ "TextEditor": {
195
+ "anatomy": [
196
+ {
197
+ "as": "row",
198
+ "name": "toolbar",
199
+ "classes": "flex gap-1 border-b p-2",
200
+ "children": [{ "as": "component", "ref": "ToolbarButton" }]
201
+ },
202
+ { "as": "external", "from": "@tiptap/react" }
160
203
  ]
161
204
  }
162
205
  },
@@ -192,6 +235,26 @@ about which one is the page. Read a screen and see.
192
235
  **\`fonts\` and \`concept\`** - the voice, and one paragraph on what this product is. The concept
193
236
  feeds every recommendation downstream, so a real one beats a generic one by a wide margin.
194
237
 
238
+ **\`typeRoles\` - which of THEIR type steps plays each of our seven slots.**
239
+
240
+ Their scale arrives under their own names now - \`h1\`, \`body-m\`, \`caption\`, \`overline\` - read
241
+ straight off their \`--text-*\` declarations. What arithmetic cannot know is which of those steps
242
+ is *body copy* and which is *the display size*, and our own components ask for it by slot:
243
+
244
+ \`\`\`json
245
+ "typeRoles": {
246
+ "xs": "caption", "sm": "body-s", "base": "body-m", "lg": "body-l",
247
+ "xl": "h3", "2xl": "h2", "display": "h1"
248
+ }
249
+ \`\`\`
250
+
251
+ Leave it out and we pick by size, which is usually right and occasionally silly - a project whose
252
+ \`overline\` is tiny and whose \`caption\` is tinier gets them the wrong way round. One line from you
253
+ fixes it, and a person can correct it later in the studio either way.
254
+
255
+ **Do not rename their steps to match ours.** \`h1\` stays \`h1\`. The whole point is that editing
256
+ \`body-m\` in the studio moves the text in their app.
257
+
195
258
  **\`rules\` - how this company BUILDS, which is half of what they actually made.**
196
259
 
197
260
  A design system that arrives as tokens and recipes is only the vocabulary. The other half is the
@@ -229,12 +292,39 @@ Use it only when the rule genuinely depends on the environment. Most rules do no
229
292
  a Sidebar loose"* is true about their design regardless of framework, and pinning it to \`next\`
230
293
  would quietly drop it the day they add a second app.
231
294
 
232
- **Report \`files\` honestly - it decides whether the rule governs.** Three or more files is a
233
- habit and the rule arrives active; one file is a coincidence and it arrives as a candidate,
295
+ **Report \`files\` honestly - it decides whether an OBSERVED rule governs.** Three or more files
296
+ is a habit and the rule arrives active; one file is a coincidence and it arrives as a candidate,
234
297
  inactive, waiting for the person to promote it. You do not make that call; you report the
235
298
  evidence and a threshold makes it. So a pattern you saw once should say \`"files": 1\` even when
236
299
  you are confident - being wrong about a law is worse than being slow about one.
237
300
 
301
+ **\`fact: true\` - for a rule you read in the DEFINITION, where counting is the wrong question.**
302
+
303
+ Three of these were reported as \`files: 1\` and arrived inactive, which was correct arithmetic on
304
+ the wrong kind of claim (dono, 01/08):
305
+
306
+ \`\`\`
307
+ the editable region is <EditorContent>
308
+ toolbar actions go through editor.chain().focus()
309
+ extensions are configured at construction
310
+ \`\`\`
311
+
312
+ None of those is a coincidence waiting for a second sighting. They are how the component IS
313
+ built, read off its own source, and they are true the moment somebody wrote it. Counting how many
314
+ files agree would leave every construction law in the codebase inactive forever.
315
+
316
+ So the test is **where you read it**, not how sure you feel:
317
+
318
+ \`\`\`
319
+ fact: true you read it inside the component's own definition - its imports, its
320
+ JSX, how its state is wired. True by construction.
321
+ files: N you inferred it from how the component is USED across the project.
322
+ An observation, and the count is what makes it a habit.
323
+ \`\`\`
324
+
325
+ A rule can carry \`fact\` OR \`files\`, never both. If you find yourself wanting both, it is an
326
+ observation - use \`files\`.
327
+
238
328
  **Write \`evidence\` as what you actually saw.** "All three app shells wrap them; none renders
239
329
  them loose" lets somebody disagree with a fact. "Best practice" lets them disagree only with
240
330
  you.
@@ -250,40 +340,160 @@ Things worth looking for, none of them guessable from tokens:
250
340
  If you cannot say where a rule came from, do not send it. An invented law is worse than a
251
341
  missing one, because it will be obeyed.
252
342
 
253
- **\`components[Name].parts\` - what each component is MADE OF.** This is the field that decides
254
- whether a component previews as itself or as a grey box with a sentence in it, and only you can
255
- fill it.
343
+ **\`components[Name].anatomy\` - what each component is MADE OF, and in what shape.** This is the
344
+ field that decides whether a component previews as itself or as a grey box with a sentence in it,
345
+ and only you can fill it.
256
346
 
257
347
  The census reads the ROOT element's classes and stops there, on purpose: descending a fixed
258
- number of levels picks a layout wrapper as often as a semantic part. **You decide how far down a
259
- part lives**, because you read the component. Send a name and the classes on it; the CLI turns
260
- those classes into declarations and the platform turns declarations into roles.
348
+ number of levels picks a layout wrapper as often as a semantic part. **You read the component, so
349
+ you decide the shape.** Send a tree; the CLI turns the classes into declarations and the platform
350
+ turns declarations into roles.
351
+
352
+ ### The three frontiers
353
+
354
+ Every node you send is one of three things, and knowing which is the whole job:
355
+
356
+ \`\`\`
357
+ a PART an element of theirs it has styles, and we draw it
358
+ a COMPONENT a component of theirs it has a recipe of its own → named block + a RULE
359
+ an EXTERNAL a third-party library we do not have it and never will → block + rules
360
+ \`\`\`
361
+
362
+ **Depth is not a number, it is where the frontier sits.** Descend until you meet another
363
+ component or a library, stop there, and record the edge. That is why no parameter tells you how
364
+ deep to go: a \`Divider\` is one node deep and a dashboard shell is five, and both are complete.
261
365
 
262
- **The name is load-bearing, not a label.** The renderer infers a part's role from it:
366
+ **You decide the depth, and nothing downstream caps it.** The platform follows a
367
+ \`component\` edge into that component's own anatomy, and then into ITS edges, as far as the chain
368
+ goes - \`Chat → Message → TypingIndicator\` renders all three. So a frontier is not a dead end you
369
+ are apologising for; it is how the chain gets walked. Record the edge and stop, and the whole
370
+ depth appears anyway.
263
371
 
264
- - \`root\`, \`wrapper\`, \`container\`, \`base\`, \`content\` - the element itself, and **skipped**. Do not
265
- send these; their styles already sit on the component.
266
- - anything matching \`icon\`, \`dot\`, \`indicator\`, \`bar\`, \`avatar\`, \`media\`, \`swatch\` - renders as a
267
- bare span, sized and coloured by its own styles
268
- - anything matching \`button\`, \`action\`, \`cta\` - renders as a real button carrying the label
269
- - everything else - carries text
372
+ ### The ROOT is a frontier too
270
373
 
271
- So a part named \`div2\` previews as a text node reading "Div2". Name what it IS: \`label\`, \`value\`,
272
- \`delta\`, \`icon\`, \`action\`, \`title\`, \`meta\`.
374
+ \`\`\`json
375
+ "components": {
376
+ "ArticleCard": {
377
+ "root": "Card",
378
+ "anatomy": [ … ]
379
+ },
380
+ "Modal": {
381
+ "root": "BaseDialog.Root",
382
+ "anatomy": [ … ]
383
+ }
384
+ }
385
+ \`\`\`
386
+
387
+ **Say what the component RETURNS when it is not a plain tag.** Measured on a real library
388
+ (dono, 01/08): 14 of 23 components return one of their own - \`ArticleCard\` returns a \`<Card>\`,
389
+ \`Button\` and \`Text\` return a \`<Component>\` - and 17 of 23 have no style of their own at all,
390
+ because the surface belongs to the root.
391
+
392
+ Without \`root\`, an \`ArticleCard\` previewed as floating text: the background, the border, the
393
+ radius and the padding had nowhere to come from. With it, the card wears \`Card\`'s recipe as its
394
+ shell and its own parts inside.
395
+
396
+ - **their component** - the name, as their code spells it: \`"root": "Card"\`
397
+ - **a library** - the dotted namespace, verbatim: \`"root": "Radio.Root"\`,
398
+ \`"root": "BaseDialog.Root"\`, \`"root": "Popover.Root"\`
399
+ - **a plain tag** - leave it out. \`<div>\`, \`<td>\`, \`<button>\` are not frontiers.
400
+
401
+ ### The nine forms
402
+
403
+ \`as\` says what a node IS, and the renderer draws that. Nothing else is accepted:
404
+
405
+ \`\`\`
406
+ image a picture region: cover, thumbnail, media
407
+ heading the title line
408
+ text body copy, a label, a value
409
+ button an action
410
+ field an input somebody types into
411
+ icon a glyph, or a bare shape with no text
412
+ row arranges its children ACROSS
413
+ stack arranges its children DOWN
414
+ component their component → needs "ref": the name as their code spells it
415
+ external a library → needs "from": the package name
416
+ \`\`\`
417
+
418
+ Only \`row\` and \`stack\` take \`children\`. Everything else is a leaf.
419
+
420
+ **\`name\` is the part name**, and it is what carries the styles - flat, lowercase, kebab. Never
421
+ nested: \`"actions.generate"\` compiles to two CSS classes and is invalid, so name it
422
+ \`"generate-action"\`. Name what it IS - \`label\`, \`value\`, \`delta\`, \`cover\`, \`toolbar\` - because the
423
+ name becomes a class in their stylesheet and \`div2\` is a name somebody has to live with.
424
+
425
+ \`root\`, \`wrapper\`, \`container\`, \`base\` and \`content\` are the element itself: **do not send them as
426
+ nodes.** Their styles already sit on the component, and a node for them draws a box inside its own
427
+ box.
428
+
429
+ A node with no \`classes\` is fine - it still carries structure, which is most of the value. A
430
+ \`component\` or \`external\` node takes no \`name\` and no \`classes\`: the styles there are not theirs
431
+ to hold.
273
432
 
274
- Send them **in the order they appear**, because that is the order they render. Three or four
275
- named parts is a recognisable component; twelve is a transcription of their DOM, and nobody
276
- needs the layout divs.
433
+ ### A component of theirs is a NAMED BLOCK, not an expansion
277
434
 
278
- **Send parts for every component that has visible structure**, which is nearly all of them. A
435
+ When you hit \`<TextEditor>\` inside \`<ArticleCard>\`, send
436
+ \`{ "as": "component", "ref": "TextEditor" }\` and **stop**. Do not inline what TextEditor is made
437
+ of. It previews as a block carrying its name, and it becomes a **rule** - \`ArticleCard\` +
438
+ \`TextEditor\`, a relation, which is the one thing a flat list of prose could never say.
439
+
440
+ That edge is a **fact, not a habit**: you saw it in the definition, so it is true once and for
441
+ all, and it arrives active without waiting for a third sighting.
442
+
443
+ Two things that are NOT edges, and sending them as such would be wrong:
444
+
445
+ - \`motion.div\`, \`Dialog.Root\`, \`Radio.Item\` - a library's namespace, not a component of theirs
446
+ - an HTML element with a capital in a variable name
447
+
448
+ If the component came from a package, it is \`external\`, not \`component\`.
449
+
450
+ ### An external dependency does not render, and that is the answer
451
+
452
+ A \`TextEditor\` built on tiptap cannot be previewed: the editable region is tiptap's, and drawing
453
+ a fake one would be inventing. So it previews as a block naming the package, and the value moves
454
+ into the **rules** - which is where an agent will read it anyway.
455
+
456
+ A library is never one rule, it is a family, and you are reading the file so you can see all of
457
+ them. Send them as normal \`rules\` with \`applies: ["TextEditor"]\` and
458
+ \`kind: "implementation"\`:
459
+
460
+ \`\`\`
461
+ requires @tiptap/react
462
+ the editable region is <EditorContent> - never render children into it directly
463
+ toolbar buttons go through editor.chain().focus()
464
+ extensions are configured at construction, not toggled later
465
+ \`\`\`
466
+
467
+ **Name the package in the rule; leave the version to the evidence.** The CLI reads the version out
468
+ of their manifest and attaches it (\`"^2.1.0 in packages/ui"\`), so the rule does not go stale the
469
+ day they upgrade and the information is not lost either.
470
+
471
+ ### How much to send
472
+
473
+ Send an anatomy for **every component with visible structure**, which is nearly all of them. A
279
474
  \`Chat\` has a message list and a composer; a \`MetricCard\` has a label and a value; a
280
- \`CircularProgress\` has a track and a fill. Sending none is the right answer only for something
281
- genuinely undivided - a \`Divider\`, a \`Spacer\`.
475
+ \`CircularProgress\` has a track and a fill. Sending none is right only for something genuinely
476
+ undivided - a \`Divider\`, a \`Spacer\`.
477
+
478
+ **There is no node budget, and the one I gave you was wrong.** "Three to six nodes is a
479
+ recognisable component" cost a real \`ArticleCard\` most of itself: it has a carousel with arrows,
480
+ three image buttons, a source chip, a refresh, a rich body, a source line and a "Similar
481
+ published content" region with two selects and two buttons - about fourteen regions - and the
482
+ reading came back with six because the guidance said so (dono, 01/08).
483
+
484
+ **One node per region a person can point at.** If they can say "that part", it is a node. What
485
+ you still collapse is a wrapper whose only job is \`flex\` - fold it into the \`row\` it already is,
486
+ because that is not a region, it is plumbing.
487
+
488
+ The cost of sending none is not neutral. A component with no anatomy previews as a grey box with
489
+ a sentence in it, or - if its kind is \`indicator\` - as a small blank shape. A component with a
490
+ real tree previews as itself, and its spec view shows what the system understood. That is the
491
+ whole reason this field exists, and skipping it quietly is how a real library came back looking
492
+ empty (dono, 01/08).
282
493
 
283
- The cost of sending none is not neutral. A component with no parts previews as a grey box with a
284
- sentence in it, or - if its anatomy is \`indicator\` - as a small blank shape. A component with
285
- three named parts previews as itself. That difference is the whole reason this field exists, and
286
- skipping it quietly is how a real library came back looking empty (dono, 01/08).
494
+ The older flat form - \`"parts": [{ "name": "label", "classes": "..." }]\` - still works and still
495
+ styles correctly. It just carries no shape, so the preview lays the parts out in a row and the
496
+ spec view has no nesting to draw.
287
497
 
288
498
  ### 3. Walk them through the decisions, one at a time
289
499
 
@@ -411,10 +621,15 @@ proposal. Say all of this in your own words - do not just print the link:
411
621
 
412
622
  - the scheme it opens in, and whether a second one was built
413
623
  - their ramps under their own family names
414
- - their exclusive components as contracts - **axes declared, every style block empty.** Say
415
- this out loud, because a component list full of blank recipes looks like a failure until
416
- someone explains it was deliberate. The platform shows them as *Not written yet*, not as a
624
+ - their exclusive components as contracts - **the axes their types declare, the look transcribed
625
+ out of their own class names where it was readable, and the anatomy you sent.** Say what is
626
+ still empty out loud, because a half-written recipe looks like a failure until someone explains
627
+ which half was deliberate: nothing was invented, so a component whose classes named no token
628
+ arrives with structure and no colour. The platform shows that as *Not written yet*, not as a
417
629
  zero.
630
+ - **the shape of each component**, if you sent one: its parts nested as they nest in their code,
631
+ the components of theirs it composes, and the libraries it needs. Each edge is also a rule now,
632
+ which is what makes it survive being looked at once.
418
633
 
419
634
  **What is waiting in v2**, which only they can approve: near-duplicate colours collapsed,
420
635
  unnamed heavy hitters given a place. Point at the link the CLI printed and name the two or
@@ -444,8 +659,10 @@ The user should end up with a system that reads like theirs and not like ours:
444
659
  - **their names travel** - \`vivid-pink\`, \`darkgray-900\`, whatever they called it
445
660
  - **their default face** - a dark product opens dark
446
661
  - **no borrowed colour** - if it is in the system, it is in their code
447
- - **their exclusive components arrive as contracts** - axes declared, every style block empty,
448
- because a contract says what may vary and a recipe would be us redesigning their component
662
+ - **their exclusive components arrive as contracts** - the axes declared, the look transcribed and
663
+ never invented, and the anatomy read out of their own JSX
664
+ - **a component previews as itself** - an \`ArticleCard\` shows a cover, a title and a footer of
665
+ buttons, and says out loud that the editor in the middle is their \`TextEditor\` on tiptap
449
666
  - **the duplicates are named out loud** - three components that all read as a badge, a brand
450
667
  colour painted 275 times with no token
451
668
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.77",
3
+ "version": "0.16.79",
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": {