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.
- package/dist/anatomy-read.js +231 -0
- package/dist/commands/add.js +23 -1
- package/dist/commands/component.js +11 -23
- package/dist/commands/doctor.js +49 -1
- package/dist/commands/generate.js +5 -1
- package/dist/commands/import.js +71 -15
- package/dist/commands/refit.js +2 -1
- package/dist/commands/upgrade.js +6 -1
- package/dist/component-codegen.js +243 -25
- package/dist/doctor/dependencies.js +90 -0
- package/dist/doctor/transcribe.js +117 -0
- package/dist/project-facts.js +89 -0
- package/dist/skill-import.js +149 -34
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/skill-import.js
CHANGED
|
@@ -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
|
-
"
|
|
157
|
-
{ "name": "icon", "classes": "size-8 text-ocean-500" },
|
|
158
|
-
{
|
|
159
|
-
|
|
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].
|
|
254
|
-
whether a component previews as itself or as a grey box with a sentence in it,
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
272
|
-
\`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
281
|
-
|
|
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
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
415
|
-
|
|
416
|
-
|
|
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,
|
|
448
|
-
|
|
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