synthesisui 0.16.80 → 0.16.82
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/commands/import.js +230 -11
- package/dist/doctor/architecture.js +134 -0
- package/dist/doctor/call-sites.js +171 -0
- package/dist/doctor/component-gate.js +235 -0
- package/dist/doctor/transcribe.js +116 -17
- package/dist/doctor/variant-read.js +584 -0
- package/dist/progress.js +71 -0
- package/dist/skill-import.js +94 -1
- package/package.json +1 -1
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE VARIANT STYLES, READ OUT OF THE SOURCE - deterministically, no model.
|
|
3
|
+
*
|
|
4
|
+
* Every imported component arrived with `variants[axis][option] = {}`: the axes came
|
|
5
|
+
* from the TYPE, which says an axis EXISTS and nothing about what it does. So a real
|
|
6
|
+
* `Button` with nine variants and six sizes previewed as one look, and you were seeing
|
|
7
|
+
* a third of the component (dono, 01/08).
|
|
8
|
+
*
|
|
9
|
+
* The styling was sitting in the file the whole time, structured:
|
|
10
|
+
*
|
|
11
|
+
* const buttonVariants = cva("inline-flex rounded-[var(--radius-md)] …", {
|
|
12
|
+
* variants: {
|
|
13
|
+
* variant: { ocean: "bg-ocean-950 text-white hover:bg-ocean-900 dark:…" },
|
|
14
|
+
* size: { sm: "h-10 px-3 [font-size:var(--text-body-s)]" },
|
|
15
|
+
* },
|
|
16
|
+
* defaultVariants: { variant: "vibrant-purple", size: "md" },
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* Nine real colours, six real heights, and their own hover per variant. An object
|
|
20
|
+
* literal, so reading it needs no model and no guess - which is the whole rule this
|
|
21
|
+
* codebase runs on: deterministic first, a model only for what genuinely has no form.
|
|
22
|
+
*
|
|
23
|
+
* WHAT IT READS
|
|
24
|
+
* cva(base, { variants, defaultVariants, compoundVariants })
|
|
25
|
+
* tv({ base, variants, defaultVariants }) - tailwind-variants
|
|
26
|
+
* cn("always", cond && "sometimes") - where the condition names a
|
|
27
|
+
* prop and a value
|
|
28
|
+
*
|
|
29
|
+
* SOURCE ORDER IS PRESERVED, and that is not tidiness. Two classes touching one
|
|
30
|
+
* property resolve in their app by position in the string; a recipe resolves by
|
|
31
|
+
* position in `layers`. They agree only if the reader inserts in the order it read.
|
|
32
|
+
*/
|
|
33
|
+
import { parseClass, transcribe } from "./transcribe.js";
|
|
34
|
+
/**
|
|
35
|
+
* A Tailwind modifier, mapped to the condition it IS.
|
|
36
|
+
*
|
|
37
|
+
* `group-hover:` is the whole reason `within` exists: it means "when the thing around
|
|
38
|
+
* me is hovered", which is a different claim from `hover:` and had no expression at
|
|
39
|
+
* all before. `peer-*` is the sibling version of the same idea and reads the same way
|
|
40
|
+
* from a recipe's point of view.
|
|
41
|
+
*/
|
|
42
|
+
const MODIFIER_STATE = {
|
|
43
|
+
hover: "hover",
|
|
44
|
+
focus: "focus",
|
|
45
|
+
"focus-visible": "focusVisible",
|
|
46
|
+
"focus-within": "focus",
|
|
47
|
+
active: "active",
|
|
48
|
+
disabled: "disabled",
|
|
49
|
+
checked: "checked",
|
|
50
|
+
indeterminate: "checked",
|
|
51
|
+
selected: "selected",
|
|
52
|
+
open: "open",
|
|
53
|
+
invalid: "invalid",
|
|
54
|
+
required: "required",
|
|
55
|
+
"read-only": "readOnly",
|
|
56
|
+
placeholder: "placeholder",
|
|
57
|
+
even: "even",
|
|
58
|
+
odd: "odd",
|
|
59
|
+
first: "first",
|
|
60
|
+
last: "last",
|
|
61
|
+
empty: "empty",
|
|
62
|
+
"aria-checked": "checked",
|
|
63
|
+
"aria-selected": "selected",
|
|
64
|
+
"aria-expanded": "open",
|
|
65
|
+
"aria-invalid": "invalid",
|
|
66
|
+
"aria-busy": "loading",
|
|
67
|
+
};
|
|
68
|
+
/** `group-hover`, `group-focus`, `peer-checked` - the state of something ELSE. */
|
|
69
|
+
const RELATIONAL = /^(?:group|peer)-(.+)$/;
|
|
70
|
+
/** `data-[state=open]:`, `data-[checked]:` - how @base-ui and Radix spell a state. */
|
|
71
|
+
const DATA_MODIFIER = /^data-\[(?:state=)?([a-z-]+)\]$/;
|
|
72
|
+
/** A breakpoint is a bare name we cannot read as a state. Their scale names them, so
|
|
73
|
+
* the caller decides whether the name exists rather than this guessing a width. */
|
|
74
|
+
const BREAKPOINT = /^(?:xs|sm|md|lg|xl|2xl|3xl|tablet|desktop|wide|mobile)$/;
|
|
75
|
+
/** What a modifier chain means, as a condition. Order inside the chain does not
|
|
76
|
+
* matter to CSS, so this folds rather than sequences. */
|
|
77
|
+
export function conditionOf(modifiers) {
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const raw of modifiers) {
|
|
80
|
+
if (raw === "dark") {
|
|
81
|
+
// The dark half has its own home in the transcription and no slot inside a
|
|
82
|
+
// layer. Saying so beats writing the light value into a dark rule.
|
|
83
|
+
out.lost = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const relational = RELATIONAL.exec(raw);
|
|
87
|
+
if (relational) {
|
|
88
|
+
const state = MODIFIER_STATE[relational[1]];
|
|
89
|
+
if (state)
|
|
90
|
+
out.within = state;
|
|
91
|
+
else
|
|
92
|
+
out.lost = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const data = DATA_MODIFIER.exec(raw);
|
|
96
|
+
if (data) {
|
|
97
|
+
const state = MODIFIER_STATE[data[1]];
|
|
98
|
+
if (state)
|
|
99
|
+
out.state = state;
|
|
100
|
+
else
|
|
101
|
+
out.lost = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const state = MODIFIER_STATE[raw];
|
|
105
|
+
if (state) {
|
|
106
|
+
out.state = state;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (BREAKPOINT.test(raw)) {
|
|
110
|
+
out.at = raw;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
out.lost = true;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
/** A stable key for a condition, so classes sharing one land in one layer. */
|
|
118
|
+
function keyOf(c, variant) {
|
|
119
|
+
const v = Object.entries(variant ?? {})
|
|
120
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
121
|
+
.map(([k, o]) => `${k}=${o}`)
|
|
122
|
+
.join("&");
|
|
123
|
+
return `${v}|${c.state ?? ""}|${c.within ?? ""}|${c.at ?? ""}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Split a class list into the base and the conditional layers it contains.
|
|
127
|
+
*
|
|
128
|
+
* `variant` travels through untouched, so the ghost button's own hover comes out as
|
|
129
|
+
* `{ variant: { variant: "ghost" }, state: "hover" }` - one selector, which is exactly
|
|
130
|
+
* what one hover per element could never say.
|
|
131
|
+
*/
|
|
132
|
+
export function splitByCondition(classes, variant) {
|
|
133
|
+
const base = [];
|
|
134
|
+
const unslotted = [];
|
|
135
|
+
// A Map keeps insertion order, which IS the cascade order we promise to preserve.
|
|
136
|
+
const layers = new Map();
|
|
137
|
+
for (const cls of classes) {
|
|
138
|
+
const { modifiers } = parseClass(cls);
|
|
139
|
+
if (modifiers.length === 0) {
|
|
140
|
+
if (variant) {
|
|
141
|
+
const key = keyOf({}, variant);
|
|
142
|
+
const at = layers.get(key) ?? { when: { variant }, classes: [] };
|
|
143
|
+
at.classes.push(cls);
|
|
144
|
+
layers.set(key, at);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
base.push(cls);
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const cond = conditionOf(modifiers);
|
|
152
|
+
if (cond.lost) {
|
|
153
|
+
unslotted.push(cls);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const key = keyOf(cond, variant);
|
|
157
|
+
const layer = layers.get(key) ??
|
|
158
|
+
{
|
|
159
|
+
...(variant || cond.state || cond.within
|
|
160
|
+
? {
|
|
161
|
+
when: {
|
|
162
|
+
...(variant ? { variant } : {}),
|
|
163
|
+
...(cond.state ? { state: cond.state } : {}),
|
|
164
|
+
...(cond.within ? { within: cond.within } : {}),
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
: {}),
|
|
168
|
+
...(cond.at ? { at: cond.at } : {}),
|
|
169
|
+
classes: [],
|
|
170
|
+
};
|
|
171
|
+
layer.classes.push(cls);
|
|
172
|
+
layers.set(key, layer);
|
|
173
|
+
}
|
|
174
|
+
return { base, layers: [...layers.values()], unslotted };
|
|
175
|
+
}
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Reading the literal
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
/** From an opening delimiter, the index just past its match. Depth-counted, and
|
|
180
|
+
* string-aware so a brace inside a class list cannot end the object early. */
|
|
181
|
+
function endOf(source, open) {
|
|
182
|
+
const pairs = { "(": ")", "{": "}", "[": "]" };
|
|
183
|
+
const close = pairs[source[open]];
|
|
184
|
+
if (!close)
|
|
185
|
+
return -1;
|
|
186
|
+
let depth = 0;
|
|
187
|
+
let quote = null;
|
|
188
|
+
for (let i = open; i < source.length; i++) {
|
|
189
|
+
const ch = source[i];
|
|
190
|
+
if (quote) {
|
|
191
|
+
if (ch === "\\")
|
|
192
|
+
i += 1;
|
|
193
|
+
else if (ch === quote)
|
|
194
|
+
quote = null;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
198
|
+
quote = ch;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (ch === source[open])
|
|
202
|
+
depth += 1;
|
|
203
|
+
else if (ch === close) {
|
|
204
|
+
depth -= 1;
|
|
205
|
+
if (depth === 0)
|
|
206
|
+
return i + 1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return -1;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Every string literal in a fragment, concatenated.
|
|
213
|
+
*
|
|
214
|
+
* A real `cva` base is two strings joined by `+` across a line break, and a template
|
|
215
|
+
* literal with no interpolation is the same thing spelled differently. Anything with
|
|
216
|
+
* a `${}` in it is a runtime value and is left alone rather than half-read.
|
|
217
|
+
*/
|
|
218
|
+
export function literalClasses(fragment) {
|
|
219
|
+
const out = [];
|
|
220
|
+
const re = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|`([^`]*)`/g;
|
|
221
|
+
for (const m of fragment.matchAll(re)) {
|
|
222
|
+
const raw = m[1] ?? m[2] ?? m[3] ?? "";
|
|
223
|
+
if (m[3] != null && raw.includes("${"))
|
|
224
|
+
continue;
|
|
225
|
+
out.push(...raw.split(/\s+/).filter(Boolean));
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Line and block comments removed, quotes respected.
|
|
231
|
+
*
|
|
232
|
+
* A real `cva` annotates its sizes - `sm: "h-10 px-3", // 32px` - and without this the
|
|
233
|
+
* comment became part of the NEXT key: the axis came back with an option literally
|
|
234
|
+
* called `"// 24px\n sm"` (probe, 01/08).
|
|
235
|
+
*/
|
|
236
|
+
export function stripComments(body) {
|
|
237
|
+
let out = "";
|
|
238
|
+
let quote = null;
|
|
239
|
+
for (let i = 0; i < body.length; i++) {
|
|
240
|
+
const ch = body[i];
|
|
241
|
+
if (quote) {
|
|
242
|
+
out += ch;
|
|
243
|
+
if (ch === "\\") {
|
|
244
|
+
out += body[i + 1] ?? "";
|
|
245
|
+
i += 1;
|
|
246
|
+
}
|
|
247
|
+
else if (ch === quote) {
|
|
248
|
+
quote = null;
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
253
|
+
quote = ch;
|
|
254
|
+
out += ch;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (ch === "/" && body[i + 1] === "/") {
|
|
258
|
+
const nl = body.indexOf("\n", i);
|
|
259
|
+
if (nl === -1)
|
|
260
|
+
break;
|
|
261
|
+
i = nl - 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (ch === "/" && body[i + 1] === "*") {
|
|
265
|
+
const close = body.indexOf("*/", i + 2);
|
|
266
|
+
if (close === -1)
|
|
267
|
+
break;
|
|
268
|
+
i = close + 1;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
out += ch;
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
/** `key: <value>` at the top level of an object body, value returned verbatim. */
|
|
276
|
+
function topLevelEntries(raw) {
|
|
277
|
+
const body = stripComments(raw);
|
|
278
|
+
const out = [];
|
|
279
|
+
let i = 0;
|
|
280
|
+
let quote = null;
|
|
281
|
+
let depth = 0;
|
|
282
|
+
let keyStart = 0;
|
|
283
|
+
let key = null;
|
|
284
|
+
let valueStart = 0;
|
|
285
|
+
for (; i < body.length; i++) {
|
|
286
|
+
const ch = body[i];
|
|
287
|
+
if (quote) {
|
|
288
|
+
if (ch === "\\")
|
|
289
|
+
i += 1;
|
|
290
|
+
else if (ch === quote)
|
|
291
|
+
quote = null;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
295
|
+
quote = ch;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if ("([{".includes(ch)) {
|
|
299
|
+
depth += 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (")]}".includes(ch)) {
|
|
303
|
+
depth -= 1;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (depth !== 0)
|
|
307
|
+
continue;
|
|
308
|
+
if (ch === ":" && key === null) {
|
|
309
|
+
key = body
|
|
310
|
+
.slice(keyStart, i)
|
|
311
|
+
.trim()
|
|
312
|
+
.replace(/^["']|["']$/g, "");
|
|
313
|
+
valueStart = i + 1;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (ch === "," && key !== null) {
|
|
317
|
+
out.push({ key, value: body.slice(valueStart, i) });
|
|
318
|
+
key = null;
|
|
319
|
+
keyStart = i + 1;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (key !== null)
|
|
323
|
+
out.push({ key, value: body.slice(valueStart) });
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
/** The first `{` at the top level of an argument list - the options object. */
|
|
327
|
+
function topLevelBrace(args) {
|
|
328
|
+
let quote = null;
|
|
329
|
+
let depth = 0;
|
|
330
|
+
for (let i = 0; i < args.length; i++) {
|
|
331
|
+
const ch = args[i];
|
|
332
|
+
if (quote) {
|
|
333
|
+
if (ch === "\\")
|
|
334
|
+
i += 1;
|
|
335
|
+
else if (ch === quote)
|
|
336
|
+
quote = null;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
340
|
+
quote = ch;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (ch === "{" && depth === 0)
|
|
344
|
+
return i;
|
|
345
|
+
if ("([".includes(ch))
|
|
346
|
+
depth += 1;
|
|
347
|
+
else if (")]".includes(ch))
|
|
348
|
+
depth -= 1;
|
|
349
|
+
}
|
|
350
|
+
return -1;
|
|
351
|
+
}
|
|
352
|
+
/** The raw value of `name:` in an object body, or null. */
|
|
353
|
+
function valueAt(body, name) {
|
|
354
|
+
for (const entry of topLevelEntries(body)) {
|
|
355
|
+
if (entry.key === name)
|
|
356
|
+
return entry.value;
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
/** The body of `name: { … }` inside an object body, or null. */
|
|
361
|
+
function objectAt(body, name) {
|
|
362
|
+
for (const entry of topLevelEntries(body)) {
|
|
363
|
+
if (entry.key !== name)
|
|
364
|
+
continue;
|
|
365
|
+
const open = entry.value.indexOf("{");
|
|
366
|
+
if (open === -1)
|
|
367
|
+
return null;
|
|
368
|
+
const end = endOf(entry.value, open);
|
|
369
|
+
return end === -1 ? null : entry.value.slice(open + 1, end - 1);
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
/** `const NAME = cva(` / `= tv(` - the assignment, so a call site can find it. */
|
|
374
|
+
const VARIANT_CALL = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(cva|tv|cn)\s*\(/g;
|
|
375
|
+
/**
|
|
376
|
+
* Every variant definition in a file, in source order.
|
|
377
|
+
*
|
|
378
|
+
* A file usually has one; a compound component has one per part, which is why the
|
|
379
|
+
* symbol travels - `cardVariants` and `cardHeaderVariants` are two different things
|
|
380
|
+
* and only their names say which is which.
|
|
381
|
+
*/
|
|
382
|
+
export function readVariants(source) {
|
|
383
|
+
const out = [];
|
|
384
|
+
VARIANT_CALL.lastIndex = 0;
|
|
385
|
+
for (const m of source.matchAll(VARIANT_CALL)) {
|
|
386
|
+
const symbol = m[1];
|
|
387
|
+
const kind = m[2];
|
|
388
|
+
const open = (m.index ?? 0) + m[0].length - 1;
|
|
389
|
+
const end = endOf(source, open);
|
|
390
|
+
if (end === -1)
|
|
391
|
+
continue;
|
|
392
|
+
const args = source.slice(open + 1, end - 1);
|
|
393
|
+
/**
|
|
394
|
+
* TWO LEVELS, AND CONFLATING THEM READ NOTHING.
|
|
395
|
+
*
|
|
396
|
+
* `cva("base classes", { variants: … })` is an argument list whose SECOND item is
|
|
397
|
+
* the options object. Reading `variants` off the argument list finds nothing,
|
|
398
|
+
* because at that level `variants:` sits one brace deep - the first version came
|
|
399
|
+
* back with zero axes from a file declaring fifteen (probe, 01/08).
|
|
400
|
+
*/
|
|
401
|
+
const optionsOpen = topLevelBrace(args);
|
|
402
|
+
const optionsEnd = optionsOpen === -1 ? -1 : endOf(args, optionsOpen);
|
|
403
|
+
const options = optionsOpen === -1 || optionsEnd === -1
|
|
404
|
+
? ""
|
|
405
|
+
: args.slice(optionsOpen + 1, optionsEnd - 1);
|
|
406
|
+
// `tv({ base: … })` names its base; `cva(base, { … })` puts it first.
|
|
407
|
+
const leadingBase = kind === "tv" || optionsOpen === -1 ? "" : args.slice(0, optionsOpen);
|
|
408
|
+
const namedBase = valueAt(options, "base");
|
|
409
|
+
const read = {
|
|
410
|
+
symbol,
|
|
411
|
+
base: literalClasses(leadingBase || namedBase || ""),
|
|
412
|
+
variants: {},
|
|
413
|
+
defaults: {},
|
|
414
|
+
layers: [],
|
|
415
|
+
unslotted: [],
|
|
416
|
+
};
|
|
417
|
+
// The base's own modifiers are layers too: a `cva` base carries
|
|
418
|
+
// `disabled:opacity-50` and `focus-visible:ring-2` before any variant exists.
|
|
419
|
+
const split = splitByCondition(read.base);
|
|
420
|
+
read.base = split.base;
|
|
421
|
+
read.layers.push(...split.layers);
|
|
422
|
+
read.unslotted.push(...split.unslotted);
|
|
423
|
+
const variantsBody = objectAt(options, "variants");
|
|
424
|
+
if (variantsBody) {
|
|
425
|
+
for (const axis of topLevelEntries(variantsBody)) {
|
|
426
|
+
const openBrace = axis.value.indexOf("{");
|
|
427
|
+
if (openBrace === -1)
|
|
428
|
+
continue;
|
|
429
|
+
const axisEnd = endOf(axis.value, openBrace);
|
|
430
|
+
if (axisEnd === -1)
|
|
431
|
+
continue;
|
|
432
|
+
const axisBody = axis.value.slice(openBrace + 1, axisEnd - 1);
|
|
433
|
+
for (const option of topLevelEntries(axisBody)) {
|
|
434
|
+
const classes = literalClasses(option.value);
|
|
435
|
+
if (classes.length === 0)
|
|
436
|
+
continue;
|
|
437
|
+
read.variants[axis.key] ??= {};
|
|
438
|
+
read.variants[axis.key][option.key] = classes;
|
|
439
|
+
const per = splitByCondition(classes, { [axis.key]: option.key });
|
|
440
|
+
read.layers.push(...per.layers);
|
|
441
|
+
read.unslotted.push(...per.unslotted);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const defaultsBody = objectAt(options, "defaultVariants");
|
|
446
|
+
if (defaultsBody) {
|
|
447
|
+
for (const entry of topLevelEntries(defaultsBody)) {
|
|
448
|
+
const value = literalClasses(entry.value)[0];
|
|
449
|
+
if (value)
|
|
450
|
+
read.defaults[entry.key] = value;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* COMPOUND VARIANTS ARE THE CASE `layers` WAS SHAPED FOR.
|
|
455
|
+
*
|
|
456
|
+
* `{ variant: "ghost", size: "sm", class: "px-2" }` is two axes holding at once,
|
|
457
|
+
* which a flat `variants` map cannot express at all - it has one key per axis. It
|
|
458
|
+
* comes out as one layer with two conditions and one selector.
|
|
459
|
+
*/
|
|
460
|
+
const compoundIdx = options.search(/compoundVariants\s*:\s*\[/);
|
|
461
|
+
if (compoundIdx !== -1) {
|
|
462
|
+
const arrOpen = options.indexOf("[", compoundIdx);
|
|
463
|
+
const arrEnd = endOf(options, arrOpen);
|
|
464
|
+
if (arrEnd !== -1) {
|
|
465
|
+
const arr = options.slice(arrOpen + 1, arrEnd - 1);
|
|
466
|
+
let cursor = 0;
|
|
467
|
+
while (cursor < arr.length) {
|
|
468
|
+
const objOpen = arr.indexOf("{", cursor);
|
|
469
|
+
if (objOpen === -1)
|
|
470
|
+
break;
|
|
471
|
+
const objEnd = endOf(arr, objOpen);
|
|
472
|
+
if (objEnd === -1)
|
|
473
|
+
break;
|
|
474
|
+
const body = arr.slice(objOpen + 1, objEnd - 1);
|
|
475
|
+
const variant = {};
|
|
476
|
+
let classes = [];
|
|
477
|
+
for (const entry of topLevelEntries(body)) {
|
|
478
|
+
if (entry.key === "class" || entry.key === "className") {
|
|
479
|
+
classes = literalClasses(entry.value);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const value = literalClasses(entry.value)[0];
|
|
483
|
+
if (value)
|
|
484
|
+
variant[entry.key] = value;
|
|
485
|
+
}
|
|
486
|
+
if (classes.length > 0 && Object.keys(variant).length > 0) {
|
|
487
|
+
const per = splitByCondition(classes, variant);
|
|
488
|
+
read.layers.push(...per.layers);
|
|
489
|
+
read.unslotted.push(...per.unslotted);
|
|
490
|
+
}
|
|
491
|
+
cursor = objEnd;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
out.push(read);
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* A CONDITIONAL CLASS EXPRESSION - the other half of how real components style.
|
|
501
|
+
*
|
|
502
|
+
* `cn("base", variant === "solid" && "bg-primary", disabled && "opacity-50")`. The
|
|
503
|
+
* literal is right there beside the condition, and when the condition names a prop
|
|
504
|
+
* and a value it names a variant. When it names only a prop (`disabled && …`) it reads
|
|
505
|
+
* as that prop being truthy, which for a boolean IS the variant.
|
|
506
|
+
*
|
|
507
|
+
* Read separately from `cva` because a file can have both, and because this shape is
|
|
508
|
+
* what a codebase that never adopted a variant library uses everywhere.
|
|
509
|
+
*/
|
|
510
|
+
const INLINE_COND = /([A-Za-z_$][\w$.]*)\s*(?:===|==)\s*["']([^"']+)["']\s*&&\s*(["'`])((?:(?!\3).)*)\3|([A-Za-z_$][\w$]*)\s*&&\s*(["'`])((?:(?!\6).)*)\6/g;
|
|
511
|
+
export function readInlineConditions(source) {
|
|
512
|
+
const layers = [];
|
|
513
|
+
INLINE_COND.lastIndex = 0;
|
|
514
|
+
for (const m of source.matchAll(INLINE_COND)) {
|
|
515
|
+
const prop = (m[1] ?? m[5] ?? "").split(".").pop() ?? "";
|
|
516
|
+
const option = m[2] ?? "true";
|
|
517
|
+
const raw = m[4] ?? m[7] ?? "";
|
|
518
|
+
if (!prop || raw.includes("${"))
|
|
519
|
+
continue;
|
|
520
|
+
const classes = raw.split(/\s+/).filter(Boolean);
|
|
521
|
+
if (classes.length === 0)
|
|
522
|
+
continue;
|
|
523
|
+
const per = splitByCondition(classes, { [prop]: option });
|
|
524
|
+
layers.push(...per.layers);
|
|
525
|
+
}
|
|
526
|
+
return layers;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* The whole reading of one file's variant styling, transcribed - ready for a recipe.
|
|
530
|
+
*
|
|
531
|
+
* `declared` is their own custom properties, so `bg-ocean-950` comes out as a ref to
|
|
532
|
+
* the token they declared rather than as a hex we looked up.
|
|
533
|
+
*/
|
|
534
|
+
export function transcribeVariants(source, declared) {
|
|
535
|
+
const reads = readVariants(source);
|
|
536
|
+
const inline = readInlineConditions(source);
|
|
537
|
+
const axes = {};
|
|
538
|
+
const defaults = {};
|
|
539
|
+
const allLayers = [];
|
|
540
|
+
const baseClasses = [];
|
|
541
|
+
const unslotted = [];
|
|
542
|
+
for (const read of reads) {
|
|
543
|
+
baseClasses.push(...read.base);
|
|
544
|
+
allLayers.push(...read.layers);
|
|
545
|
+
unslotted.push(...read.unslotted);
|
|
546
|
+
Object.assign(defaults, read.defaults);
|
|
547
|
+
for (const [axis, options] of Object.entries(read.variants)) {
|
|
548
|
+
const names = Object.keys(options);
|
|
549
|
+
// Two options is what makes a closed set somebody chose from - the same rule
|
|
550
|
+
// the contract writer and the doctor already follow.
|
|
551
|
+
if (names.length >= 2)
|
|
552
|
+
axes[axis] = names;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
allLayers.push(...inline);
|
|
556
|
+
const layers = allLayers
|
|
557
|
+
.map((layer) => {
|
|
558
|
+
/**
|
|
559
|
+
* THE MODIFIER COMES OFF BEFORE THE TRANSCRIPTION.
|
|
560
|
+
*
|
|
561
|
+
* The condition is already carried by the layer, so `hover:bg-ocean-900` has to
|
|
562
|
+
* arrive as `bg-ocean-900`. Left on, `transcribe` correctly filed it under
|
|
563
|
+
* `states.hover` - and this reads `base`, so every hover in a nine-variant
|
|
564
|
+
* button vanished (probe, 01/08).
|
|
565
|
+
*/
|
|
566
|
+
const bare = layer.classes.map((cls) => parseClass(cls).utility);
|
|
567
|
+
const t = transcribe(bare, declared);
|
|
568
|
+
const style = { ...t.base, ...t.dark };
|
|
569
|
+
return {
|
|
570
|
+
...(layer.when ? { when: layer.when } : {}),
|
|
571
|
+
...(layer.at ? { at: layer.at } : {}),
|
|
572
|
+
style,
|
|
573
|
+
};
|
|
574
|
+
})
|
|
575
|
+
.filter((layer) => Object.keys(layer.style).length > 0);
|
|
576
|
+
return {
|
|
577
|
+
reads,
|
|
578
|
+
axes,
|
|
579
|
+
defaults,
|
|
580
|
+
layers,
|
|
581
|
+
base: transcribe(baseClasses, declared),
|
|
582
|
+
unslotted,
|
|
583
|
+
};
|
|
584
|
+
}
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT IS HAPPENING RIGHT NOW, and how much is left.
|
|
3
|
+
*
|
|
4
|
+
* "While the design system import is running it would be good to have an animated
|
|
5
|
+
* progress and underneath the current component and a (10 of 1235)" (dono, 01/08).
|
|
6
|
+
*
|
|
7
|
+
* A census over a real monorepo reads 2797 files and the terminal said nothing for
|
|
8
|
+
* most of it. Twenty silent minutes is indistinguishable from twenty broken ones, and
|
|
9
|
+
* that is the difference this makes: not speed, but the ability to wait.
|
|
10
|
+
*
|
|
11
|
+
* TWO AUDIENCES, ONE API. A person at a terminal gets a bar that redraws in place; the
|
|
12
|
+
* skill runs the CLI with no TTY and pipes the output, so it gets a plain milestone
|
|
13
|
+
* line every so often. Neither has to know about the other, and the non-TTY path never
|
|
14
|
+
* writes a carriage return into a transcript.
|
|
15
|
+
*/
|
|
16
|
+
const BAR_WIDTH = 24;
|
|
17
|
+
/** Every this-many items, the non-TTY path prints one line. Enough to see movement in
|
|
18
|
+
* a piped log without turning it into the log. */
|
|
19
|
+
const MILESTONE_EVERY = 25;
|
|
20
|
+
/** A bar that reads as a bar at any width. Two glyphs, no colour: this line is
|
|
21
|
+
* overwritten dozens of times a second and anything fancier flickers. */
|
|
22
|
+
function bar(done, total) {
|
|
23
|
+
const ratio = total > 0 ? Math.min(1, done / total) : 0;
|
|
24
|
+
const filled = Math.round(ratio * BAR_WIDTH);
|
|
25
|
+
return `${"█".repeat(filled)}${"░".repeat(BAR_WIDTH - filled)}`;
|
|
26
|
+
}
|
|
27
|
+
/** Truncated from the LEFT, because the end of a path is the part that identifies it. */
|
|
28
|
+
function tail(label, width) {
|
|
29
|
+
if (label.length <= width)
|
|
30
|
+
return label;
|
|
31
|
+
return `…${label.slice(-(width - 1))}`;
|
|
32
|
+
}
|
|
33
|
+
export function startProgress(title, opts) {
|
|
34
|
+
const write = opts?.write ?? ((s) => process.stdout.write(s));
|
|
35
|
+
const tty = opts?.isTty ?? Boolean(process.stdout.isTTY);
|
|
36
|
+
let total = opts?.total ?? 0;
|
|
37
|
+
let done = 0;
|
|
38
|
+
let last = "";
|
|
39
|
+
const paint = () => {
|
|
40
|
+
const count = total > 0 ? `${done} of ${total}` : String(done);
|
|
41
|
+
const line = ` ${bar(done, total)} ${count} ${tail(last, 40)}`;
|
|
42
|
+
// Pad to the previous width so a shorter label cannot leave the tail of a
|
|
43
|
+
// longer one behind on the line.
|
|
44
|
+
write(`\r${line.padEnd(80)}`);
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
step(label) {
|
|
48
|
+
done += 1;
|
|
49
|
+
last = label;
|
|
50
|
+
if (tty) {
|
|
51
|
+
paint();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// NO CARRIAGE RETURNS IN A TRANSCRIPT. A milestone line every so often, plus
|
|
55
|
+
// the first one so the reader knows the phase started at all.
|
|
56
|
+
if (done === 1 || done % MILESTONE_EVERY === 0) {
|
|
57
|
+
const count = total > 0 ? `${done} of ${total}` : `${done}`;
|
|
58
|
+
write(` ${title}: ${count} - ${label}\n`);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
total(n) {
|
|
62
|
+
total = n;
|
|
63
|
+
},
|
|
64
|
+
done(summary) {
|
|
65
|
+
if (tty)
|
|
66
|
+
write(`\r${" ".repeat(80)}\r`);
|
|
67
|
+
if (summary)
|
|
68
|
+
write(` ${summary}\n`);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|