synthesisui 0.16.89 → 0.16.91
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/doctor/signals.js +71 -0
- package/dist/doctor/transcribe.js +215 -3
- package/dist/skill-import.js +18 -1
- package/package.json +1 -1
package/dist/doctor/signals.js
CHANGED
|
@@ -75,11 +75,19 @@ export function emptySignals() {
|
|
|
75
75
|
enableSystem: null,
|
|
76
76
|
prefersColorScheme: 0,
|
|
77
77
|
htmlCarries: [],
|
|
78
|
+
page: [],
|
|
78
79
|
},
|
|
79
80
|
libraries: [],
|
|
80
81
|
conflicts: [],
|
|
81
82
|
};
|
|
82
83
|
}
|
|
84
|
+
function addPagePaint(into, token, scheme, source) {
|
|
85
|
+
const found = into.theme.page.find((p) => p.token === token && p.scheme === scheme && p.source === source);
|
|
86
|
+
if (found)
|
|
87
|
+
found.count += 1;
|
|
88
|
+
else
|
|
89
|
+
into.theme.page.push({ token, scheme, source, count: 1 });
|
|
90
|
+
}
|
|
83
91
|
const ANY_IMPORT = /(?:from\s+["']([^"'.][^"']*)["']|require\(\s*["']([^"'.][^"']*)["']\s*\))/g;
|
|
84
92
|
/** The package a specifier belongs to: `@tiptap/react` and `motion/react` both keep the
|
|
85
93
|
* part a manifest would name. */
|
|
@@ -110,6 +118,13 @@ export function readSignalsInto(into, imports, file, source) {
|
|
|
110
118
|
const isStyle = /\.(css|scss|sass|less)$/i.test(file);
|
|
111
119
|
if (isStyle) {
|
|
112
120
|
into.theme.prefersColorScheme += (source.match(/prefers-color-scheme/g) ?? []).length;
|
|
121
|
+
/**
|
|
122
|
+
* `body { background: … }` in a global stylesheet is the page saying so itself -
|
|
123
|
+
* the strongest of the three page signals and the one a class sweep never finds.
|
|
124
|
+
*/
|
|
125
|
+
for (const m of source.matchAll(/(?:^|\})\s*(?:html|body|:root)[^{]*\{[^}]*?background(?:-color)?\s*:\s*([^;}]+)/g)) {
|
|
126
|
+
addPagePaint(into, m[1].trim(), "light", "global-css");
|
|
127
|
+
}
|
|
113
128
|
return;
|
|
114
129
|
}
|
|
115
130
|
if (!isCode)
|
|
@@ -152,6 +167,37 @@ export function readSignalsInto(into, imports, file, source) {
|
|
|
152
167
|
}
|
|
153
168
|
}
|
|
154
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* THE PAGE PAINTS, from the markup. A `min-h-screen` container with a background is
|
|
172
|
+
* the page - that idiom is universal, which is what makes this generalize across
|
|
173
|
+
* folder layouts. `dark:bg-x` files under dark; `bg-x` under light.
|
|
174
|
+
*/
|
|
175
|
+
for (const m of source.matchAll(/className=\{?["'`]([^"'`]+)["'`]/g)) {
|
|
176
|
+
const list = m[1].split(/\s+/);
|
|
177
|
+
const isViewport = list.some((c) => /^(?:min-h-screen|h-screen|min-h-dvh|h-dvh)$/.test(c));
|
|
178
|
+
if (isViewport) {
|
|
179
|
+
for (const c of list) {
|
|
180
|
+
const dark = /^dark:bg-([a-z][a-z0-9-]*)$/.exec(c);
|
|
181
|
+
const light = /^bg-([a-z][a-z0-9-]*)$/.exec(c);
|
|
182
|
+
if (dark)
|
|
183
|
+
addPagePaint(into, dark[1], "dark", "viewport-container");
|
|
184
|
+
else if (light &&
|
|
185
|
+
!/^(gradient|opacity|clip|cover|contain|center|fixed|local|scroll|none|auto|repeat|transparent|current|inherit)/.test(light[1])) {
|
|
186
|
+
addPagePaint(into, light[1], "light", "viewport-container");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const m of source.matchAll(/<(?:main|body)\b[^>]*className=\{?["'`]([^"'`]+)["'`]/g)) {
|
|
192
|
+
for (const c of m[1].split(/\s+/)) {
|
|
193
|
+
const dark = /^dark:bg-([a-z][a-z0-9-]*)$/.exec(c);
|
|
194
|
+
const light = /^bg-([a-z][a-z0-9-]*)$/.exec(c);
|
|
195
|
+
if (dark)
|
|
196
|
+
addPagePaint(into, dark[1], "dark", "main-or-body");
|
|
197
|
+
else if (light)
|
|
198
|
+
addPagePaint(into, light[1], "light", "main-or-body");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
155
201
|
ANY_IMPORT.lastIndex = 0;
|
|
156
202
|
const seen = new Set();
|
|
157
203
|
for (const m of source.matchAll(ANY_IMPORT)) {
|
|
@@ -267,6 +313,31 @@ export function describeSignals(s) {
|
|
|
267
313
|
if (t.htmlCarries.length > 0) {
|
|
268
314
|
out.push(`\`<html>\` carries ${t.htmlCarries.map((a) => `\`${a}\``).join(" ")} - which is the ancestor every dark rule of theirs matches, and therefore the one ours must match.`);
|
|
269
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* The page, said as evidence with the SOURCE named - "2 full-viewport containers"
|
|
318
|
+
* is a claim somebody can weigh against "1 main". Strongest first per scheme.
|
|
319
|
+
*/
|
|
320
|
+
const strength = {
|
|
321
|
+
"global-css": 3,
|
|
322
|
+
"main-or-body": 2,
|
|
323
|
+
"viewport-container": 1,
|
|
324
|
+
};
|
|
325
|
+
for (const scheme of ["dark", "light"]) {
|
|
326
|
+
const paints = t.page
|
|
327
|
+
.filter((p) => p.scheme === scheme)
|
|
328
|
+
.sort((a, b) => strength[b.source] - strength[a.source] || b.count - a.count);
|
|
329
|
+
if (paints.length === 0)
|
|
330
|
+
continue;
|
|
331
|
+
const label = {
|
|
332
|
+
"global-css": "body in global CSS",
|
|
333
|
+
"main-or-body": "on <main>/<body>",
|
|
334
|
+
"viewport-container": "full-viewport container",
|
|
335
|
+
};
|
|
336
|
+
out.push(`the ${scheme} page paints ${paints
|
|
337
|
+
.slice(0, 3)
|
|
338
|
+
.map((p) => `\`${p.token}\` (${p.count}x ${label[p.source]})`)
|
|
339
|
+
.join(", ")} - the canvas question, answered from the files rather than hunted.`);
|
|
340
|
+
}
|
|
270
341
|
for (const family of ["primitives", "motion", "icons", "charts", "editors"]) {
|
|
271
342
|
const found = s.libraries.filter((l) => l.family === family);
|
|
272
343
|
if (found.length === 0)
|
|
@@ -121,10 +121,114 @@ const RADIUS = {
|
|
|
121
121
|
/** Tailwind's own colour table, for the values a project uses without declaring.
|
|
122
122
|
* Only the ones that actually turn up: white, black, and the neutral ramp a
|
|
123
123
|
* real project reached for 106 times. */
|
|
124
|
+
/**
|
|
125
|
+
* THE SIXTY DROPS WERE MINE, NOT THEIRS. test14's not-expressed listed ~60 classes that
|
|
126
|
+
* "start like a design decision and read as nothing" - and enumerating them found that
|
|
127
|
+
* every family is real, valid Tailwind these tables never held: `w-full`, `text-center`,
|
|
128
|
+
* `leading-tight`, `tracking-wide`, `shadow-sm` (with the token DECLARED), `gap-x-6`,
|
|
129
|
+
* `rounded-t-lg`, `border-none` (dono, 01/08). The unreadable report did its job; this is
|
|
130
|
+
* the coverage it bought.
|
|
131
|
+
*/
|
|
132
|
+
/** Tailwind's spacing formula: N x 0.25rem, halves included - `size-4.5` is 1.125rem,
|
|
133
|
+
* and a table of steps is always one step short (test14). */
|
|
134
|
+
function numericSpacing(step) {
|
|
135
|
+
if (!/^\d+(\.\d+)?$/.test(step))
|
|
136
|
+
return null;
|
|
137
|
+
const n = Number(step);
|
|
138
|
+
return n === 0 ? "0" : `${n * 0.25}rem`;
|
|
139
|
+
}
|
|
140
|
+
const SIZE_KEYWORD = {
|
|
141
|
+
full: "100%",
|
|
142
|
+
auto: "auto",
|
|
143
|
+
min: "min-content",
|
|
144
|
+
max: "max-content",
|
|
145
|
+
fit: "fit-content",
|
|
146
|
+
px: "1px",
|
|
147
|
+
"0": "0",
|
|
148
|
+
};
|
|
149
|
+
/** Axis-dependent: `w-screen` is 100vw and `h-screen` is 100vh. */
|
|
150
|
+
const SCREEN = {
|
|
151
|
+
w: "100vw",
|
|
152
|
+
h: "100vh",
|
|
153
|
+
"min-w": "100vw",
|
|
154
|
+
"min-h": "100vh",
|
|
155
|
+
"max-w": "100vw",
|
|
156
|
+
"max-h": "100vh",
|
|
157
|
+
};
|
|
158
|
+
const TEXT_ALIGN = new Set([
|
|
159
|
+
"left",
|
|
160
|
+
"center",
|
|
161
|
+
"right",
|
|
162
|
+
"justify",
|
|
163
|
+
"start",
|
|
164
|
+
"end",
|
|
165
|
+
]);
|
|
166
|
+
const LEADING = {
|
|
167
|
+
none: "1",
|
|
168
|
+
tight: "1.25",
|
|
169
|
+
snug: "1.375",
|
|
170
|
+
normal: "1.5",
|
|
171
|
+
relaxed: "1.625",
|
|
172
|
+
loose: "2",
|
|
173
|
+
};
|
|
174
|
+
const TRACKING = {
|
|
175
|
+
tighter: "-0.05em",
|
|
176
|
+
tight: "-0.025em",
|
|
177
|
+
normal: "0",
|
|
178
|
+
wide: "0.025em",
|
|
179
|
+
wider: "0.05em",
|
|
180
|
+
widest: "0.1em",
|
|
181
|
+
};
|
|
182
|
+
/** Tailwind's own shadows, for when a project uses them undeclared. A declared
|
|
183
|
+
* `--shadow-x` always wins and travels as the ref. */
|
|
184
|
+
const TAILWIND_SHADOW = {
|
|
185
|
+
sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
|
186
|
+
md: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
|
|
187
|
+
lg: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
|
|
188
|
+
xl: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)",
|
|
189
|
+
"2xl": "0 25px 50px -12px rgb(0 0 0 / 0.25)",
|
|
190
|
+
inner: "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)",
|
|
191
|
+
none: "none",
|
|
192
|
+
};
|
|
193
|
+
const BORDER_STYLE = new Set([
|
|
194
|
+
"solid",
|
|
195
|
+
"dashed",
|
|
196
|
+
"dotted",
|
|
197
|
+
"double",
|
|
198
|
+
"hidden",
|
|
199
|
+
"none",
|
|
200
|
+
]);
|
|
201
|
+
/** `rounded-t-lg` → the two top corners: the side names half the corners each. */
|
|
202
|
+
const RADIUS_SIDE = {
|
|
203
|
+
t: ["borderTopLeftRadius", "borderTopRightRadius"],
|
|
204
|
+
b: ["borderBottomLeftRadius", "borderBottomRightRadius"],
|
|
205
|
+
l: ["borderTopLeftRadius", "borderBottomLeftRadius"],
|
|
206
|
+
r: ["borderTopRightRadius", "borderBottomRightRadius"],
|
|
207
|
+
tl: ["borderTopLeftRadius"],
|
|
208
|
+
tr: ["borderTopRightRadius"],
|
|
209
|
+
bl: ["borderBottomLeftRadius"],
|
|
210
|
+
br: ["borderBottomRightRadius"],
|
|
211
|
+
};
|
|
124
212
|
const TAILWIND_COLOR = {
|
|
125
213
|
white: "#ffffff",
|
|
126
214
|
black: "#000000",
|
|
127
215
|
transparent: "transparent",
|
|
216
|
+
// zinc and blue, because a real Select is styled entirely in them and every
|
|
217
|
+
// reference read as nothing (test14, 01/08). Tailwind's own values, verbatim.
|
|
218
|
+
"zinc-50": "#fafafa",
|
|
219
|
+
"zinc-100": "#f4f4f5",
|
|
220
|
+
"zinc-200": "#e4e4e7",
|
|
221
|
+
"zinc-300": "#d4d4d8",
|
|
222
|
+
"zinc-400": "#a1a1aa",
|
|
223
|
+
"zinc-500": "#71717a",
|
|
224
|
+
"zinc-600": "#52525b",
|
|
225
|
+
"zinc-700": "#3f3f46",
|
|
226
|
+
"zinc-800": "#27272a",
|
|
227
|
+
"zinc-900": "#18181b",
|
|
228
|
+
"zinc-950": "#09090b",
|
|
229
|
+
"blue-400": "#60a5fa",
|
|
230
|
+
"blue-500": "#3b82f6",
|
|
231
|
+
"blue-600": "#2563eb",
|
|
128
232
|
"neutral-50": "#fafafa",
|
|
129
233
|
"neutral-100": "#f5f5f5",
|
|
130
234
|
"neutral-200": "#e5e5e5",
|
|
@@ -340,6 +444,18 @@ export function readUtility(utility, declared) {
|
|
|
340
444
|
return null;
|
|
341
445
|
const prefix = core.slice(0, dash);
|
|
342
446
|
const rest = core.slice(dash + 1);
|
|
447
|
+
/**
|
|
448
|
+
* `border-none` is a style and `border-collapse` a table's own model - neither a
|
|
449
|
+
* width nor a colour. BEFORE the colour branch, because `border` is a colour prefix
|
|
450
|
+
* and that branch returns null for a name it does not know (test14).
|
|
451
|
+
*/
|
|
452
|
+
if (prefix === "border") {
|
|
453
|
+
if (BORDER_STYLE.has(rest))
|
|
454
|
+
return { property: "borderStyle", value: rest };
|
|
455
|
+
if (rest === "collapse" || rest === "separate") {
|
|
456
|
+
return { property: "borderCollapse", value: rest };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
343
459
|
const colorProp = COLOR_PROPERTY[prefix];
|
|
344
460
|
if (colorProp) {
|
|
345
461
|
/**
|
|
@@ -381,6 +497,10 @@ export function readUtility(utility, declared) {
|
|
|
381
497
|
return null;
|
|
382
498
|
}
|
|
383
499
|
if (prefix === "text") {
|
|
500
|
+
// `text-center` is an alignment, not a colour and not a size - and it read as
|
|
501
|
+
// nothing on a real table's cells (test14).
|
|
502
|
+
if (TEXT_ALIGN.has(rest))
|
|
503
|
+
return { property: "textAlign", value: rest };
|
|
384
504
|
/**
|
|
385
505
|
* A SIZE TRAVELS AS A LITERAL, and that is not a shortcut.
|
|
386
506
|
*
|
|
@@ -406,6 +526,10 @@ export function readUtility(utility, declared) {
|
|
|
406
526
|
if (prefix === "font") {
|
|
407
527
|
// A family (`font-sans`) and a weight (`font-bold`) share this prefix, and
|
|
408
528
|
// the weight table is what tells them apart.
|
|
529
|
+
// `font-regular` is this library's own spelling of 400 - non-standard Tailwind,
|
|
530
|
+
// real in their files, and dropped whole (test14).
|
|
531
|
+
if (rest === "regular")
|
|
532
|
+
return { property: "fontWeight", value: "400" };
|
|
409
533
|
if (FONT_WEIGHT[rest]) {
|
|
410
534
|
/**
|
|
411
535
|
* ALSO A LITERAL, for a reason worth writing down: a weight ref is a valid
|
|
@@ -439,11 +563,86 @@ export function readUtility(utility, declared) {
|
|
|
439
563
|
if (prefix === "opacity" && /^\d{1,3}$/.test(rest)) {
|
|
440
564
|
return { property: "opacity", value: String(Number(rest) / 100) };
|
|
441
565
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
566
|
+
/**
|
|
567
|
+
* `shadow-sm` with `--shadow-sm` DECLARED read as nothing - there was no shadow branch
|
|
568
|
+
* at all, and elevations on parts vanished across a whole real library (test14).
|
|
569
|
+
*/
|
|
570
|
+
if (prefix === "shadow") {
|
|
571
|
+
const own = `--shadow-${rest}`;
|
|
572
|
+
if (declared.has(own)) {
|
|
573
|
+
return { property: "boxShadow", value: `{shadow.${rest}}`, token: own };
|
|
574
|
+
}
|
|
575
|
+
if (TAILWIND_SHADOW[rest]) {
|
|
576
|
+
return { property: "boxShadow", value: TAILWIND_SHADOW[rest] };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// `leading-tight`, `tracking-wide` - the typography refinements, fixed scales.
|
|
580
|
+
if (prefix === "leading") {
|
|
581
|
+
if (LEADING[rest])
|
|
582
|
+
return { property: "lineHeight", value: LEADING[rest] };
|
|
583
|
+
if (/^\d+(\.\d+)?$/.test(rest)) {
|
|
584
|
+
return { property: "lineHeight", value: `${Number(rest) * 0.25}rem` };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (prefix === "tracking" && TRACKING[rest]) {
|
|
588
|
+
return { property: "letterSpacing", value: TRACKING[rest] };
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* `min-w-0` splits at the FIRST dash into prefix `min`, which no table holds - the
|
|
592
|
+
* `min-w`/`max-h` keys were dead on arrival. Recombine the axis before looking up.
|
|
593
|
+
*/
|
|
594
|
+
let sizePrefix = prefix;
|
|
595
|
+
let sizeRest = rest;
|
|
596
|
+
if (prefix === "min" || prefix === "max") {
|
|
597
|
+
const axis = /^(w|h)-(.+)$/.exec(rest);
|
|
598
|
+
if (axis) {
|
|
599
|
+
sizePrefix = `${prefix}-${axis[1]}`;
|
|
600
|
+
sizeRest = axis[2];
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const spaceProp = SPACING_PROPERTY[sizePrefix];
|
|
604
|
+
if (spaceProp) {
|
|
605
|
+
/** `gap-x-6` / `gap-y-2` - the two halves of gap, which one prefix cannot say. */
|
|
606
|
+
if (sizePrefix === "gap" && /^[xy]-/.test(sizeRest)) {
|
|
607
|
+
const axisProp = sizeRest.startsWith("x-") ? "columnGap" : "rowGap";
|
|
608
|
+
const step = sizeRest.slice(2);
|
|
609
|
+
const size = SPACING[step] ?? numericSpacing(step);
|
|
610
|
+
if (size)
|
|
611
|
+
return { property: axisProp, value: size };
|
|
612
|
+
}
|
|
613
|
+
const size = SPACING[sizeRest] ?? numericSpacing(sizeRest);
|
|
614
|
+
if (size)
|
|
615
|
+
return { property: spaceProp, value: size };
|
|
616
|
+
/** `w-full`, `h-screen`, `min-w-0` - the keyword sizes, axis-aware. */
|
|
617
|
+
if (sizeRest === "screen" && SCREEN[sizePrefix]) {
|
|
618
|
+
return { property: spaceProp, value: SCREEN[sizePrefix] };
|
|
619
|
+
}
|
|
620
|
+
if (SIZE_KEYWORD[sizeRest] &&
|
|
621
|
+
/^(w|h|min-w|min-h|max-w|max-h|size)$/.test(sizePrefix)) {
|
|
622
|
+
return { property: spaceProp, value: SIZE_KEYWORD[sizeRest] };
|
|
623
|
+
}
|
|
445
624
|
}
|
|
446
625
|
if (prefix === "rounded") {
|
|
626
|
+
/**
|
|
627
|
+
* `rounded-t-lg` - one side, two corners. The side names which half; the scale is
|
|
628
|
+
* the same one the whole-box read already resolves, their token first.
|
|
629
|
+
*/
|
|
630
|
+
const side = /^(tl|tr|bl|br|t|b|l|r)-(.+)$/.exec(rest);
|
|
631
|
+
if (side && RADIUS_SIDE[side[1]]) {
|
|
632
|
+
const own = `--radius-${side[2]}`;
|
|
633
|
+
const value = declared.has(own)
|
|
634
|
+
? `{radius.${side[2]}}`
|
|
635
|
+
: (RADIUS[side[2]] ?? null);
|
|
636
|
+
if (value) {
|
|
637
|
+
// One utility, two corners: returned as the first and the caller's write
|
|
638
|
+
// handles pairs the same way `size-N` is handled - see the transcribe loop.
|
|
639
|
+
return {
|
|
640
|
+
property: RADIUS_SIDE[side[1]].join("+"),
|
|
641
|
+
value,
|
|
642
|
+
...(declared.has(own) ? { token: own } : {}),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
447
646
|
const own = `--radius-${rest}`;
|
|
448
647
|
if (declared.has(own)) {
|
|
449
648
|
return {
|
|
@@ -828,6 +1027,19 @@ export function transcribe(classes, declared) {
|
|
|
828
1027
|
out.fromLiteral += 1;
|
|
829
1028
|
continue;
|
|
830
1029
|
}
|
|
1030
|
+
// `rounded-t-lg` names two corners: the reader returns them joined with `+`, and
|
|
1031
|
+
// the write splits - the same shape the `size` expansion above uses.
|
|
1032
|
+
if (decl.property.includes("+")) {
|
|
1033
|
+
for (const property of decl.property.split("+")) {
|
|
1034
|
+
if (target[property] == null)
|
|
1035
|
+
target[property] = decl.value;
|
|
1036
|
+
}
|
|
1037
|
+
if (decl.token)
|
|
1038
|
+
out.fromToken += 1;
|
|
1039
|
+
else
|
|
1040
|
+
out.fromLiteral += 1;
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
831
1043
|
if (target[decl.property] != null)
|
|
832
1044
|
continue;
|
|
833
1045
|
target[decl.property] = decl.value;
|
package/dist/skill-import.js
CHANGED
|
@@ -251,12 +251,29 @@ html carries lang="en" data-theme="dark" - which is the ancestor every dark rule
|
|
|
251
251
|
That is a real run. Your job is to CONFIRM OR CORRECT it, not to reproduce it - and a sweep
|
|
252
252
|
would bring back a different slice than the next one, which is the worse property of the two.
|
|
253
253
|
|
|
254
|
+
**Confirmation costs ZERO reads.** A real run spent 7 pattern searches and 39 file reads to
|
|
255
|
+
"confirm" a theme the census had already counted (test14, 01/08) - that is re-deriving, not
|
|
256
|
+
confirming. The rule: the census numbers are TRUE unless something you already read while
|
|
257
|
+
doing other work contradicts them. Only on a contradiction do you open a file, and only the
|
|
258
|
+
one file the evidence names. If nothing contradicts, write the census's answer down and move
|
|
259
|
+
on - it costs one sentence.
|
|
260
|
+
|
|
254
261
|
\`has: ["dark"]\` on a dark-only product is the right answer. It is better than filling a light
|
|
255
262
|
slot on their behalf, and it makes adding light a deliberate act they take later.
|
|
256
263
|
|
|
257
264
|
**\`roles\` - which of their values carries which meaning**, when their token names do not say.
|
|
258
265
|
A project naming its neutrals by temperature (\`--color-darkgray-900\`) has told you nothing
|
|
259
|
-
about which one is the page
|
|
266
|
+
about which one is the page - and the census now answers that too, under "the page paints":
|
|
267
|
+
|
|
268
|
+
\`\`\`
|
|
269
|
+
the dark page paints darkgray-500 (2x on <main>/<body>, 2x full-viewport container)
|
|
270
|
+
the light page paints #EDF3FA (1x body in global CSS), lightgray-200 (2x viewport)
|
|
271
|
+
\`\`\`
|
|
272
|
+
|
|
273
|
+
Three countable sources, strongest first: \`body { background }\` in global CSS, a background
|
|
274
|
+
on \`<main>\`/\`<body>\`, and a full-viewport container (\`min-h-screen\` + a background IS the
|
|
275
|
+
page). **Do not hunt for the canvas** - pick from this evidence, and name the source when you
|
|
276
|
+
report it. When the sources disagree, that disagreement is itself the finding to surface.
|
|
260
277
|
|
|
261
278
|
**\`fonts\` and \`concept\`** - the voice, and one paragraph on what this product is. The concept
|
|
262
279
|
feeds every recommendation downstream, so a real one beats a generic one by a wide margin.
|
package/package.json
CHANGED