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