synthesisui 0.16.76 → 0.16.78

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,89 @@
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 { readFile } from "node:fs/promises";
17
+ import { join } 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
+ /** The pinned version of an installed system, from its `.lock`. */
42
+ async function pinnedVersion(dir) {
43
+ const raw = await readFile(join(dir, ".lock"), "utf8").catch(() => "");
44
+ if (!raw)
45
+ return null;
46
+ try {
47
+ const lock = JSON.parse(raw);
48
+ return typeof lock.version === "number" ? lock.version : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ /**
55
+ * HOW THE INSTALLED SYSTEM SPELLS A CLASS, read off the document on disk.
56
+ *
57
+ * The document is the same one the compiled CSS came from, so this can never
58
+ * disagree with the stylesheet sitting next to it - which is the property that
59
+ * matters. A response field would be fresher, and a caller that has one should
60
+ * prefer it; this is what every other caller can rely on.
61
+ *
62
+ * Absent, unreadable or never declared all mean OURS, to the character, so no
63
+ * project that exists has to change.
64
+ */
65
+ export async function readInstalledConvention(root, slug) {
66
+ const dir = join(root, "_synthesisui", "ds", slug);
67
+ const version = await pinnedVersion(dir);
68
+ const raw = version
69
+ ? await readFile(join(dir, `v${version}`, "design-system.json"), "utf8").catch(() => "")
70
+ : "";
71
+ if (!raw)
72
+ return DEFAULT_CONVENTION;
73
+ try {
74
+ const doc = JSON.parse(raw);
75
+ const declared = doc.meta?.classNames;
76
+ if (!declared ||
77
+ typeof declared.prefix !== "string" ||
78
+ typeof declared.partSeparator !== "string") {
79
+ return DEFAULT_CONVENTION;
80
+ }
81
+ return {
82
+ prefix: declared.prefix,
83
+ partSeparator: declared.partSeparator,
84
+ };
85
+ }
86
+ catch {
87
+ return DEFAULT_CONVENTION;
88
+ }
89
+ }
@@ -153,10 +153,39 @@ Open the project and answer the questions below. Then add a \`reading\` object t
153
153
  ],
154
154
  "components": {
155
155
  "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" }
156
+ "anatomy": [
157
+ { "as": "icon", "name": "icon", "classes": "size-8 text-ocean-500" },
158
+ {
159
+ "as": "stack",
160
+ "name": "body",
161
+ "classes": "flex flex-col gap-1",
162
+ "children": [
163
+ { "as": "text", "name": "label", "classes": "text-xs uppercase text-lightgray-500" },
164
+ { "as": "heading", "name": "value", "classes": "text-2xl font-bold" }
165
+ ]
166
+ }
167
+ ]
168
+ },
169
+ "ArticleCard": {
170
+ "anatomy": [
171
+ { "as": "image", "name": "cover", "classes": "aspect-video w-full rounded-t-lg" },
172
+ { "as": "heading", "name": "title", "classes": "text-lg font-semibold" },
173
+ { "as": "component", "ref": "TextEditor" },
174
+ {
175
+ "as": "row",
176
+ "name": "footer",
177
+ "classes": "flex items-center gap-2 border-t p-3",
178
+ "children": [
179
+ { "as": "button", "name": "publish", "classes": "btn btn-primary" },
180
+ { "as": "button", "name": "discard", "classes": "btn btn-ghost" }
181
+ ]
182
+ }
183
+ ]
184
+ },
185
+ "TextEditor": {
186
+ "anatomy": [
187
+ { "as": "row", "name": "toolbar", "classes": "flex gap-1 border-b p-2" },
188
+ { "as": "external", "from": "@tiptap/react" }
160
189
  ]
161
190
  }
162
191
  },
@@ -250,40 +279,119 @@ Things worth looking for, none of them guessable from tokens:
250
279
  If you cannot say where a rule came from, do not send it. An invented law is worse than a
251
280
  missing one, because it will be obeyed.
252
281
 
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.
282
+ **\`components[Name].anatomy\` - what each component is MADE OF, and in what shape.** This is the
283
+ field that decides whether a component previews as itself or as a grey box with a sentence in it,
284
+ and only you can fill it.
256
285
 
257
286
  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.
287
+ number of levels picks a layout wrapper as often as a semantic part. **You read the component, so
288
+ you decide the shape.** Send a tree; the CLI turns the classes into declarations and the platform
289
+ turns declarations into roles.
290
+
291
+ ### The three frontiers
292
+
293
+ Every node you send is one of three things, and knowing which is the whole job:
294
+
295
+ \`\`\`
296
+ a PART an element of theirs it has styles, and we draw it
297
+ a COMPONENT a component of theirs it has a recipe of its own → named block + a RULE
298
+ an EXTERNAL a third-party library we do not have it and never will → block + rules
299
+ \`\`\`
300
+
301
+ **Depth is not a number, it is where the frontier sits.** Descend until you meet another
302
+ component or a library, stop there, and record the edge. That is why no parameter tells you how
303
+ deep to go: a \`Divider\` is one node deep and a dashboard shell is five, and both are complete.
304
+
305
+ ### The nine forms
306
+
307
+ \`as\` says what a node IS, and the renderer draws that. Nothing else is accepted:
308
+
309
+ \`\`\`
310
+ image a picture region: cover, thumbnail, media
311
+ heading the title line
312
+ text body copy, a label, a value
313
+ button an action
314
+ field an input somebody types into
315
+ icon a glyph, or a bare shape with no text
316
+ row arranges its children ACROSS
317
+ stack arranges its children DOWN
318
+ component their component → needs "ref": the name as their code spells it
319
+ external a library → needs "from": the package name
320
+ \`\`\`
321
+
322
+ Only \`row\` and \`stack\` take \`children\`. Everything else is a leaf.
261
323
 
262
- **The name is load-bearing, not a label.** The renderer infers a part's role from it:
324
+ **\`name\` is the part name**, and it is what carries the styles - flat, lowercase, kebab. Never
325
+ nested: \`"actions.generate"\` compiles to two CSS classes and is invalid, so name it
326
+ \`"generate-action"\`. Name what it IS - \`label\`, \`value\`, \`delta\`, \`cover\`, \`toolbar\` - because the
327
+ name becomes a class in their stylesheet and \`div2\` is a name somebody has to live with.
263
328
 
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
329
+ \`root\`, \`wrapper\`, \`container\`, \`base\` and \`content\` are the element itself: **do not send them as
330
+ nodes.** Their styles already sit on the component, and a node for them draws a box inside its own
331
+ box.
270
332
 
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\`.
333
+ A node with no \`classes\` is fine - it still carries structure, which is most of the value. A
334
+ \`component\` or \`external\` node takes no \`name\` and no \`classes\`: the styles there are not theirs
335
+ to hold.
273
336
 
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.
337
+ ### A component of theirs is a NAMED BLOCK, not an expansion
277
338
 
278
- **Send parts for every component that has visible structure**, which is nearly all of them. A
339
+ When you hit \`<TextEditor>\` inside \`<ArticleCard>\`, send
340
+ \`{ "as": "component", "ref": "TextEditor" }\` and **stop**. Do not inline what TextEditor is made
341
+ of. It previews as a block carrying its name, and it becomes a **rule** - \`ArticleCard\` +
342
+ \`TextEditor\`, a relation, which is the one thing a flat list of prose could never say.
343
+
344
+ That edge is a **fact, not a habit**: you saw it in the definition, so it is true once and for
345
+ all, and it arrives active without waiting for a third sighting.
346
+
347
+ Two things that are NOT edges, and sending them as such would be wrong:
348
+
349
+ - \`motion.div\`, \`Dialog.Root\`, \`Radio.Item\` - a library's namespace, not a component of theirs
350
+ - an HTML element with a capital in a variable name
351
+
352
+ If the component came from a package, it is \`external\`, not \`component\`.
353
+
354
+ ### An external dependency does not render, and that is the answer
355
+
356
+ A \`TextEditor\` built on tiptap cannot be previewed: the editable region is tiptap's, and drawing
357
+ a fake one would be inventing. So it previews as a block naming the package, and the value moves
358
+ into the **rules** - which is where an agent will read it anyway.
359
+
360
+ A library is never one rule, it is a family, and you are reading the file so you can see all of
361
+ them. Send them as normal \`rules\` with \`applies: ["TextEditor"]\` and
362
+ \`kind: "implementation"\`:
363
+
364
+ \`\`\`
365
+ requires @tiptap/react
366
+ the editable region is <EditorContent> - never render children into it directly
367
+ toolbar buttons go through editor.chain().focus()
368
+ extensions are configured at construction, not toggled later
369
+ \`\`\`
370
+
371
+ **Name the package in the rule; leave the version to the evidence.** The CLI reads the version out
372
+ of their manifest and attaches it (\`"^2.1.0 in packages/ui"\`), so the rule does not go stale the
373
+ day they upgrade and the information is not lost either.
374
+
375
+ ### How much to send
376
+
377
+ Send an anatomy for **every component with visible structure**, which is nearly all of them. A
279
378
  \`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\`.
379
+ \`CircularProgress\` has a track and a fill. Sending none is right only for something genuinely
380
+ undivided - a \`Divider\`, a \`Spacer\`.
381
+
382
+ Three to six nodes is a recognisable component. Twelve is a transcription of their DOM, and
383
+ nobody needs the layout divs - collapse a wrapper whose only job is \`flex\` into the \`row\` it
384
+ already is.
385
+
386
+ The cost of sending none is not neutral. A component with no anatomy previews as a grey box with
387
+ a sentence in it, or - if its kind is \`indicator\` - as a small blank shape. A component with a
388
+ real tree previews as itself, and its spec view shows what the system understood. That is the
389
+ whole reason this field exists, and skipping it quietly is how a real library came back looking
390
+ empty (dono, 01/08).
282
391
 
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).
392
+ The older flat form - \`"parts": [{ "name": "label", "classes": "..." }]\` - still works and still
393
+ styles correctly. It just carries no shape, so the preview lays the parts out in a row and the
394
+ spec view has no nesting to draw.
287
395
 
288
396
  ### 3. Walk them through the decisions, one at a time
289
397
 
@@ -411,10 +519,15 @@ proposal. Say all of this in your own words - do not just print the link:
411
519
 
412
520
  - the scheme it opens in, and whether a second one was built
413
521
  - 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
522
+ - their exclusive components as contracts - **the axes their types declare, the look transcribed
523
+ out of their own class names where it was readable, and the anatomy you sent.** Say what is
524
+ still empty out loud, because a half-written recipe looks like a failure until someone explains
525
+ which half was deliberate: nothing was invented, so a component whose classes named no token
526
+ arrives with structure and no colour. The platform shows that as *Not written yet*, not as a
417
527
  zero.
528
+ - **the shape of each component**, if you sent one: its parts nested as they nest in their code,
529
+ the components of theirs it composes, and the libraries it needs. Each edge is also a rule now,
530
+ which is what makes it survive being looked at once.
418
531
 
419
532
  **What is waiting in v2**, which only they can approve: near-duplicate colours collapsed,
420
533
  unnamed heavy hitters given a place. Point at the link the CLI printed and name the two or
@@ -444,8 +557,10 @@ The user should end up with a system that reads like theirs and not like ours:
444
557
  - **their names travel** - \`vivid-pink\`, \`darkgray-900\`, whatever they called it
445
558
  - **their default face** - a dark product opens dark
446
559
  - **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
560
+ - **their exclusive components arrive as contracts** - the axes declared, the look transcribed and
561
+ never invented, and the anatomy read out of their own JSX
562
+ - **a component previews as itself** - an \`ArticleCard\` shows a cover, a title and a footer of
563
+ buttons, and says out loud that the editor in the middle is their \`TextEditor\` on tiptap
449
564
  - **the duplicates are named out loud** - three components that all read as a badge, a brand
450
565
  colour painted 275 times with no token
451
566
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.76",
3
+ "version": "0.16.78",
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": {