synthesisui 0.16.113 → 0.16.116
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 +23 -0
- package/dist/commands/component.js +29 -0
- package/dist/commands/import.js +37 -0
- package/dist/doctor/public-api.js +119 -0
- package/dist/doctor/transcribe.js +304 -1
- package/package.json +1 -1
package/dist/anatomy-read.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* of the three outcomes this pipeline has paid for twice.
|
|
31
31
|
*/
|
|
32
32
|
import { transcribe, } from "./doctor/transcribe.js";
|
|
33
|
+
import { frontierKind } from "./frontier-kind.js";
|
|
33
34
|
/** The ten forms, closed. A renderer that accepts any tag draws anything. */
|
|
34
35
|
const FORMS = new Set([
|
|
35
36
|
"image",
|
|
@@ -295,6 +296,28 @@ sketch) {
|
|
|
295
296
|
if (typeof node.refName === "string" && node.refName.trim())
|
|
296
297
|
node_.refName = node.refName.trim();
|
|
297
298
|
}
|
|
299
|
+
else if (typeof node.at === "number") {
|
|
300
|
+
/**
|
|
301
|
+
* OR OFF THE SKETCH, WHICH IS THE CASE THAT ACTUALLY HAPPENS.
|
|
302
|
+
*
|
|
303
|
+
* The deriver stamps this on the read; an AUTHORED reading never does - it names
|
|
304
|
+
* `trigger` at index 1 and says nothing about `Collapsible.Trigger`, because it
|
|
305
|
+
* has no reason to. So the snippet kept emitting `<div>` on the very path the
|
|
306
|
+
* owner runs, and I read my own probe as proof of the opposite (dono, 04/08).
|
|
307
|
+
*
|
|
308
|
+
* The sketch measured the tag and the package, so this is the same lookup
|
|
309
|
+
* `when` already does one line up. Gated on HEADLESS: an opaque library is named
|
|
310
|
+
* and stopped at, and a plain `div` is nobody's element.
|
|
311
|
+
*/
|
|
312
|
+
const measured = sketch?.[node.at];
|
|
313
|
+
if (measured?.from &&
|
|
314
|
+
measured.tag &&
|
|
315
|
+
/^[A-Z]/.test(measured.tag) &&
|
|
316
|
+
frontierKind(measured.from) === "headless") {
|
|
317
|
+
node_.from = measured.from;
|
|
318
|
+
node_.refName = measured.tag;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
298
321
|
// The NAME is what carries the styles, so it is claimed even when nothing
|
|
299
322
|
// resolved: the tree triggers on presence, and structure is most of the
|
|
300
323
|
// value. A node with no name is pure structure and that is legitimate.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { emitCn, readVocabulary } from "../cn-codegen.js";
|
|
@@ -84,6 +85,34 @@ export async function component(slug, name, opts) {
|
|
|
84
85
|
.filter(Boolean)
|
|
85
86
|
.map((p) => p[0].toUpperCase() + p.slice(1))
|
|
86
87
|
.join("");
|
|
88
|
+
/**
|
|
89
|
+
* THEY ALREADY HAVE THIS COMPONENT, and the recipe says where.
|
|
90
|
+
*
|
|
91
|
+
* `findCollision` guards against shadowing by walking `packages` three levels down;
|
|
92
|
+
* their `CollapsibleCard` sits six levels below it, so the guard never reached it and
|
|
93
|
+
* `component` would write a second, worse copy of a component in their own
|
|
94
|
+
* `packages/ui`. The recipe carries the path now - repo-relative, so this is one
|
|
95
|
+
* `stat` rather than a walk - and the import line their own manifest declares.
|
|
96
|
+
*
|
|
97
|
+
* Only refuses when the file is actually HERE: the same system installed in another
|
|
98
|
+
* project has no such component, and there generating is exactly right.
|
|
99
|
+
*/
|
|
100
|
+
const theirs = res.recipe?.api;
|
|
101
|
+
const already = theirs?.file && existsSync(join(root, theirs.file)) ? theirs : null;
|
|
102
|
+
if (already && !opts.force) {
|
|
103
|
+
console.log(section("You already have this component"));
|
|
104
|
+
console.log(body(`\`${already.name}\` lives in ${already.file} - writing another one would leave you with two.`));
|
|
105
|
+
if (already.from) {
|
|
106
|
+
console.log("");
|
|
107
|
+
console.log(snippet([`import { ${already.name} } from "${already.from}";`]));
|
|
108
|
+
}
|
|
109
|
+
else if (already.exported === false) {
|
|
110
|
+
console.log(body(`It is not re-exported from that folder's entry, so import it by path.`));
|
|
111
|
+
}
|
|
112
|
+
console.log("");
|
|
113
|
+
console.log(body("The recipe and the compiled CSS are on disk either way - only the .tsx was not written. Pass --force to write it anyway."));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
87
116
|
const clash = await findCollision(root, config.componentsDir, res.name, pascalName);
|
|
88
117
|
if (!opts.force && (clash.exported || (clash.file && !clash.ours))) {
|
|
89
118
|
console.log(section("This name is already taken in your project"));
|
package/dist/commands/import.js
CHANGED
|
@@ -17,6 +17,7 @@ import { moduleImports, readModuleCss, readModuleUsage, transcribeModule, } from
|
|
|
17
17
|
import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
|
|
18
18
|
import { describeConvention, describeRemainder, detectConventions, } from "../doctor/idiom.js";
|
|
19
19
|
import { callSiteNode, placeLayers, } from "../doctor/place-layers.js";
|
|
20
|
+
import { publicApi, requiredProps } from "../doctor/public-api.js";
|
|
20
21
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
21
22
|
import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
|
|
22
23
|
import { describeSignals, emptySignals, finishSignals, readSignalsInto, } from "../doctor/signals.js";
|
|
@@ -1083,6 +1084,42 @@ export async function takeCensus(root, opts) {
|
|
|
1083
1084
|
for (const m of css.matchAll(/(--[a-zA-Z0-9_-]+)\s*:/g)) {
|
|
1084
1085
|
declaredNames.add(m[1]);
|
|
1085
1086
|
}
|
|
1087
|
+
/**
|
|
1088
|
+
* HOW SOMEBODY CALLS THE COMPONENT THEY ALREADY HAVE.
|
|
1089
|
+
*
|
|
1090
|
+
* The usage panel was handing people the markup of a component rebuilt out of our
|
|
1091
|
+
* compiled classes, for a component sitting in their own `packages/ui` with a name and
|
|
1092
|
+
* props - "você inventou diversas classes que não existem" (dono, 04/08), and the half
|
|
1093
|
+
* that lands is that those classes appear nowhere in his repo. He has
|
|
1094
|
+
* `<CollapsibleCard>`; the usage is its props.
|
|
1095
|
+
*
|
|
1096
|
+
* Measured, never inferred: the specifier is the name in THEIR manifest for the folder
|
|
1097
|
+
* being imported, and a component only carries it when the entry actually re-exports
|
|
1098
|
+
* the name - 35 of their 36, with `TypingIndicator` internal to `Message` and reported
|
|
1099
|
+
* as not exported. See `publicApi`.
|
|
1100
|
+
*/
|
|
1101
|
+
const api = await publicApi(root, scopeLabel);
|
|
1102
|
+
if (api) {
|
|
1103
|
+
for (const c of components) {
|
|
1104
|
+
const file = defined.find((d) => d.name === c.name)?.file;
|
|
1105
|
+
const source = file
|
|
1106
|
+
? sources.find((s) => s.file.endsWith(file))?.source
|
|
1107
|
+
: undefined;
|
|
1108
|
+
const required = source ? requiredProps(source, c.name) : [];
|
|
1109
|
+
c.api = {
|
|
1110
|
+
...(api.exports.has(c.name) ? { from: api.from } : { exported: false }),
|
|
1111
|
+
/**
|
|
1112
|
+
* REPO-RELATIVE, so the answer needs no second search. `defined[].file` is
|
|
1113
|
+
* relative to the scope; joined with the scope it is a path anybody can check
|
|
1114
|
+
* with one `stat` - which is how `component` knows they already have it without
|
|
1115
|
+
* walking a monorepo (their component sits six levels below `packages/`, and the
|
|
1116
|
+
* shallow walk that guards against shadowing never reached it).
|
|
1117
|
+
*/
|
|
1118
|
+
...(file ? { file: `${scopeLabel}/${file}` } : {}),
|
|
1119
|
+
...(required.length > 0 ? { required } : {}),
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1086
1123
|
const brokenRefs = findBrokenRefs(sources, declaredNames);
|
|
1087
1124
|
const conventions = detectConventions(sources);
|
|
1088
1125
|
const classStyle = detectClassStyle(sources);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HOW SOMEBODY CALLS THE COMPONENT THEY ALREADY HAVE.
|
|
3
|
+
*
|
|
4
|
+
* The usage panel was emitting the markup of a component rebuilt out of our compiled
|
|
5
|
+
* classes - `<div className="ds-collapsible-card-trigger">` and twenty lines like it. The
|
|
6
|
+
* owner's objection was exact (dono, 04/08): "você inventou diversas classes que não
|
|
7
|
+
* existem". They exist in our stylesheet and they exist NOWHERE in his repo, which is the
|
|
8
|
+
* half that matters. He has `<CollapsibleCard>`; the usage is its props.
|
|
9
|
+
*
|
|
10
|
+
* That question has a measured answer and we were not reading it:
|
|
11
|
+
*
|
|
12
|
+
* the package name their own `package.json` for the scope being imported
|
|
13
|
+
* is it exported follow `export * from` out of the entry - 35 of their 36
|
|
14
|
+
* which props the destructuring, already read (36 of 36)
|
|
15
|
+
* which are required their `interface XProps` - 23 of 38 files declare one
|
|
16
|
+
*
|
|
17
|
+
* Nothing here is inferred. A component the barrel does not reach is reported as not
|
|
18
|
+
* exported, which is a fact worth having - their `TypingIndicator` is internal to
|
|
19
|
+
* `Message`, and telling somebody to import it would be the invention.
|
|
20
|
+
*/
|
|
21
|
+
import { readFile } from "node:fs/promises";
|
|
22
|
+
import { dirname, join, normalize } from "node:path";
|
|
23
|
+
const NAMED = /export\s*\{([^}]*)\}\s*from\s*["']([^"']+)["']/g;
|
|
24
|
+
const DECLARED = /export\s+(?:const|function|class)\s+([A-Za-z_$][\w$]*)/g;
|
|
25
|
+
const STAR = /export\s+\*\s+from\s*["']([^"']+)["']/g;
|
|
26
|
+
async function readAt(base, rel) {
|
|
27
|
+
for (const candidate of [
|
|
28
|
+
rel,
|
|
29
|
+
`${rel}.ts`,
|
|
30
|
+
`${rel}.tsx`,
|
|
31
|
+
join(rel, "index.ts"),
|
|
32
|
+
join(rel, "index.tsx"),
|
|
33
|
+
]) {
|
|
34
|
+
const path = normalize(join(base, candidate));
|
|
35
|
+
const source = await readFile(path, "utf8").catch(() => null);
|
|
36
|
+
if (source != null)
|
|
37
|
+
return [path, source];
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Every name the entry exposes, following `export *` as far as it goes.
|
|
43
|
+
*
|
|
44
|
+
* Depth-capped and cycle-guarded: a barrel chain is somebody's folder layout, and
|
|
45
|
+
* `packages/ui` reaches its components through three of them.
|
|
46
|
+
*/
|
|
47
|
+
async function exportsOf(path, source, depth = 0, seen = new Set()) {
|
|
48
|
+
const out = new Set();
|
|
49
|
+
if (depth > 6 || seen.has(path))
|
|
50
|
+
return out;
|
|
51
|
+
seen.add(path);
|
|
52
|
+
NAMED.lastIndex = 0;
|
|
53
|
+
for (const m of source.matchAll(NAMED)) {
|
|
54
|
+
for (const part of m[1].split(",")) {
|
|
55
|
+
const name = part.split(" as ").pop()?.trim();
|
|
56
|
+
if (name && name !== "default")
|
|
57
|
+
out.add(name);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
DECLARED.lastIndex = 0;
|
|
61
|
+
for (const m of source.matchAll(DECLARED))
|
|
62
|
+
out.add(m[1]);
|
|
63
|
+
STAR.lastIndex = 0;
|
|
64
|
+
for (const m of source.matchAll(STAR)) {
|
|
65
|
+
const next = await readAt(dirname(path), m[1]);
|
|
66
|
+
if (!next)
|
|
67
|
+
continue;
|
|
68
|
+
for (const name of await exportsOf(next[0], next[1], depth + 1, seen))
|
|
69
|
+
out.add(name);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The public API of the folder being imported, or null when it has no manifest.
|
|
75
|
+
*
|
|
76
|
+
* `root` is the repository, `scope` the folder the census measured (`packages/ui`). A
|
|
77
|
+
* scope with no `package.json` of its own is not a package and there is no specifier to
|
|
78
|
+
* offer - an app's `src/components` is imported by path, which is theirs to know and not
|
|
79
|
+
* ours to guess.
|
|
80
|
+
*/
|
|
81
|
+
export async function publicApi(root, scope) {
|
|
82
|
+
if (!scope)
|
|
83
|
+
return null;
|
|
84
|
+
const manifest = await readFile(join(root, scope, "package.json"), "utf8")
|
|
85
|
+
.then((raw) => JSON.parse(raw))
|
|
86
|
+
.catch(() => null);
|
|
87
|
+
if (!manifest?.name)
|
|
88
|
+
return null;
|
|
89
|
+
const entry = manifest.main ?? manifest.module ?? "src/index.ts";
|
|
90
|
+
const found = await readAt(join(root, scope), entry);
|
|
91
|
+
if (!found)
|
|
92
|
+
return { from: manifest.name, exports: new Set() };
|
|
93
|
+
return {
|
|
94
|
+
from: manifest.name,
|
|
95
|
+
exports: await exportsOf(found[0], found[1]),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const PROPS_BLOCK = /(?:export\s+)?(?:interface|type)\s+(\w*Props)\s*(?:=\s*)?\{([\s\S]*?)\n\}/;
|
|
99
|
+
/**
|
|
100
|
+
* WHICH PROPS THEIR TYPE MARKS AS REQUIRED.
|
|
101
|
+
*
|
|
102
|
+
* The one thing that makes a call snippet copy-pasteable: `title: string` has to be
|
|
103
|
+
* filled and `subtitle?: string` does not. 23 of their 38 files declare a readable
|
|
104
|
+
* `interface XProps`; the rest extend a library's type or use generics, and there we say
|
|
105
|
+
* nothing rather than guessing which props somebody must pass.
|
|
106
|
+
*/
|
|
107
|
+
export function requiredProps(source, name) {
|
|
108
|
+
const scoped = new RegExp(`(?:export\\s+)?(?:interface|type)\\s+${name}Props\\s*(?:=\\s*)?\\{([\\s\\S]*?)\\n\\}`).exec(source);
|
|
109
|
+
const body = scoped?.[1] ?? PROPS_BLOCK.exec(source)?.[2];
|
|
110
|
+
if (!body)
|
|
111
|
+
return [];
|
|
112
|
+
const out = [];
|
|
113
|
+
for (const line of body.split("\n")) {
|
|
114
|
+
const m = /^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)(\?)?\s*:/.exec(line);
|
|
115
|
+
if (m && !m[2])
|
|
116
|
+
out.push(m[1]);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
@@ -90,7 +90,36 @@ const SPACING_PROPERTY = {
|
|
|
90
90
|
m: "margin",
|
|
91
91
|
mx: "marginInline",
|
|
92
92
|
my: "marginBlock",
|
|
93
|
+
/**
|
|
94
|
+
* THE FOUR SIDES, which had no entry - so `mt-1` read as nothing.
|
|
95
|
+
*
|
|
96
|
+
* On their card it is the 4px between the eyebrow and the title; across the library it
|
|
97
|
+
* is 20 utilities in 12 components. A margin is a spacing decision and the contract
|
|
98
|
+
* always held it; only the table was short (dono, 04/08).
|
|
99
|
+
*/
|
|
100
|
+
mt: "marginTop",
|
|
101
|
+
mr: "marginRight",
|
|
102
|
+
mb: "marginBottom",
|
|
103
|
+
ml: "marginLeft",
|
|
93
104
|
gap: "gap",
|
|
105
|
+
"gap-x": "columnGap",
|
|
106
|
+
"gap-y": "rowGap",
|
|
107
|
+
/**
|
|
108
|
+
* `space-y-1` sets a margin on the CHILDREN, which is not a declaration on the parent.
|
|
109
|
+
* As a row/column gap it is exact wherever the parent arranges - which is every one of
|
|
110
|
+
* their uses - and inert where it does not, so it never paints a wrong pixel.
|
|
111
|
+
*/
|
|
112
|
+
"space-y": "rowGap",
|
|
113
|
+
"space-x": "columnGap",
|
|
114
|
+
/** Position offsets - what makes `absolute inset-0` cover its parent. */
|
|
115
|
+
inset: "inset",
|
|
116
|
+
"inset-x": "insetInline",
|
|
117
|
+
"inset-y": "insetBlock",
|
|
118
|
+
top: "top",
|
|
119
|
+
right: "right",
|
|
120
|
+
bottom: "bottom",
|
|
121
|
+
left: "left",
|
|
122
|
+
basis: "flexBasis",
|
|
94
123
|
/**
|
|
95
124
|
* SIZE, which the first version had no entry for at all.
|
|
96
125
|
*
|
|
@@ -330,6 +359,119 @@ const TYPE_KEYWORD = {
|
|
|
330
359
|
italic: { property: "fontStyle", value: "italic" },
|
|
331
360
|
underline: { property: "textDecoration", value: "underline" },
|
|
332
361
|
};
|
|
362
|
+
/**
|
|
363
|
+
* THE DECLARATIONS THAT ARE A WHOLE WORD, and were being dropped as layout.
|
|
364
|
+
*
|
|
365
|
+
* The rule that keeps layout out of a recipe is right about `flex` and `gap` - identical
|
|
366
|
+
* in every design system, and a recipe full of them is a recipe about nothing. It was
|
|
367
|
+
* wrong about everything that came along for the ride. Swept across their 36 components,
|
|
368
|
+
* 564 of 1443 utilities produced no declaration at all, and the biggest groups were not
|
|
369
|
+
* arrangement but PLACE: 116 distribution utilities in 25 components, 69 position ones in
|
|
370
|
+
* 17 (dono, 04/08).
|
|
371
|
+
*
|
|
372
|
+
* The cost was visible on one card: the loading veil is `absolute inset-0 bg-white/50`,
|
|
373
|
+
* and without the position it stopped being a veil and became a block in the flow; the
|
|
374
|
+
* chevron's `justify-between` is what puts it at the far edge of the header.
|
|
375
|
+
*
|
|
376
|
+
* `flex` and `flex-col` stay OUT on purpose - they travel as the node's own form
|
|
377
|
+
* (`row`/`stack`), and declaring the same thing twice is how two sources of truth start
|
|
378
|
+
* disagreeing. What comes in here is the DIMENSION (`flex-1`, `shrink-0`) and the PLACE.
|
|
379
|
+
*/
|
|
380
|
+
const PLACE_KEYWORD = {
|
|
381
|
+
// Position: what makes an overlay an overlay.
|
|
382
|
+
relative: { property: "position", value: "relative" },
|
|
383
|
+
absolute: { property: "position", value: "absolute" },
|
|
384
|
+
fixed: { property: "position", value: "fixed" },
|
|
385
|
+
sticky: { property: "position", value: "sticky" },
|
|
386
|
+
static: { property: "position", value: "static" },
|
|
387
|
+
// Flex dimension. The direction is the node's form; how it grows is a decision.
|
|
388
|
+
"flex-1": { property: "flex", value: "1 1 0%" },
|
|
389
|
+
"flex-auto": { property: "flex", value: "1 1 auto" },
|
|
390
|
+
"flex-initial": { property: "flex", value: "0 1 auto" },
|
|
391
|
+
"flex-none": { property: "flex", value: "none" },
|
|
392
|
+
grow: { property: "flexGrow", value: "1" },
|
|
393
|
+
"grow-0": { property: "flexGrow", value: "0" },
|
|
394
|
+
"flex-grow": { property: "flexGrow", value: "1" },
|
|
395
|
+
"flex-grow-0": { property: "flexGrow", value: "0" },
|
|
396
|
+
shrink: { property: "flexShrink", value: "1" },
|
|
397
|
+
"shrink-0": { property: "flexShrink", value: "0" },
|
|
398
|
+
"flex-shrink": { property: "flexShrink", value: "1" },
|
|
399
|
+
"flex-shrink-0": { property: "flexShrink", value: "0" },
|
|
400
|
+
// Overflow: what makes a clip clip, and a scroller scroll.
|
|
401
|
+
"overflow-hidden": { property: "overflow", value: "hidden" },
|
|
402
|
+
"overflow-auto": { property: "overflow", value: "auto" },
|
|
403
|
+
"overflow-visible": { property: "overflow", value: "visible" },
|
|
404
|
+
"overflow-scroll": { property: "overflow", value: "scroll" },
|
|
405
|
+
"overflow-x-auto": { property: "overflowX", value: "auto" },
|
|
406
|
+
"overflow-y-auto": { property: "overflowY", value: "auto" },
|
|
407
|
+
"overflow-x-hidden": { property: "overflowX", value: "hidden" },
|
|
408
|
+
"overflow-y-hidden": { property: "overflowY", value: "hidden" },
|
|
409
|
+
"overflow-x-scroll": { property: "overflowX", value: "scroll" },
|
|
410
|
+
"overflow-y-scroll": { property: "overflowY", value: "scroll" },
|
|
411
|
+
// Display, when it is not the arrangement we already carry as a form.
|
|
412
|
+
"inline-flex": { property: "display", value: "inline-flex" },
|
|
413
|
+
"inline-block": { property: "display", value: "inline-block" },
|
|
414
|
+
inline: { property: "display", value: "inline" },
|
|
415
|
+
block: { property: "display", value: "block" },
|
|
416
|
+
hidden: { property: "display", value: "none" },
|
|
417
|
+
// Focus and the pointer: real decisions, and `outline-none` is 21 of theirs.
|
|
418
|
+
"outline-none": { property: "outline", value: "none" },
|
|
419
|
+
"cursor-pointer": { property: "cursor", value: "pointer" },
|
|
420
|
+
"cursor-default": { property: "cursor", value: "default" },
|
|
421
|
+
"cursor-not-allowed": { property: "cursor", value: "not-allowed" },
|
|
422
|
+
"cursor-text": { property: "cursor", value: "text" },
|
|
423
|
+
"cursor-grab": { property: "cursor", value: "grab" },
|
|
424
|
+
"cursor-wait": { property: "cursor", value: "wait" },
|
|
425
|
+
"pointer-events-none": { property: "pointerEvents", value: "none" },
|
|
426
|
+
"pointer-events-auto": { property: "pointerEvents", value: "auto" },
|
|
427
|
+
"select-none": { property: "userSelect", value: "none" },
|
|
428
|
+
"select-text": { property: "userSelect", value: "text" },
|
|
429
|
+
"select-all": { property: "userSelect", value: "all" },
|
|
430
|
+
// Text that has to fit, and numbers that have to line up.
|
|
431
|
+
"whitespace-nowrap": { property: "whiteSpace", value: "nowrap" },
|
|
432
|
+
"whitespace-pre": { property: "whiteSpace", value: "pre" },
|
|
433
|
+
"whitespace-pre-wrap": { property: "whiteSpace", value: "pre-wrap" },
|
|
434
|
+
"break-words": { property: "overflowWrap", value: "break-word" },
|
|
435
|
+
"break-all": { property: "wordBreak", value: "break-all" },
|
|
436
|
+
"tabular-nums": { property: "fontVariantNumeric", value: "tabular-nums" },
|
|
437
|
+
// How an image fills its frame.
|
|
438
|
+
"object-cover": { property: "objectFit", value: "cover" },
|
|
439
|
+
"object-contain": { property: "objectFit", value: "contain" },
|
|
440
|
+
"object-fill": { property: "objectFit", value: "fill" },
|
|
441
|
+
"object-none": { property: "objectFit", value: "none" },
|
|
442
|
+
"object-scale-down": { property: "objectFit", value: "scale-down" },
|
|
443
|
+
/**
|
|
444
|
+
* ONE UTILITY, THREE DECLARATIONS - and any one of them alone paints nothing, which is
|
|
445
|
+
* why this needed the `prop:value|…` form rather than the pair form.
|
|
446
|
+
*/
|
|
447
|
+
truncate: {
|
|
448
|
+
property: "overflow+textOverflow+whiteSpace",
|
|
449
|
+
value: "overflow:hidden|textOverflow:ellipsis|whiteSpace:nowrap",
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
/** `justify-between` → `space-between`, and the four that are not a straight copy. */
|
|
453
|
+
const ALIGNMENT = {
|
|
454
|
+
start: "flex-start",
|
|
455
|
+
end: "flex-end",
|
|
456
|
+
between: "space-between",
|
|
457
|
+
around: "space-around",
|
|
458
|
+
evenly: "space-evenly",
|
|
459
|
+
center: "center",
|
|
460
|
+
stretch: "stretch",
|
|
461
|
+
baseline: "baseline",
|
|
462
|
+
};
|
|
463
|
+
/** Which property each alignment prefix sets. */
|
|
464
|
+
const ALIGN_PROP = {
|
|
465
|
+
justify: "justifyContent",
|
|
466
|
+
items: "alignItems",
|
|
467
|
+
self: "alignSelf",
|
|
468
|
+
content: "alignContent",
|
|
469
|
+
"justify-items": "justifyItems",
|
|
470
|
+
"justify-self": "justifySelf",
|
|
471
|
+
"place-items": "placeItems",
|
|
472
|
+
"place-content": "placeContent",
|
|
473
|
+
"place-self": "placeSelf",
|
|
474
|
+
};
|
|
333
475
|
/** States we recognise. Anything else is skipped rather than invented. */
|
|
334
476
|
/**
|
|
335
477
|
* A MODIFIER'S STATE, IN THE CONTRACT'S SPELLING.
|
|
@@ -419,7 +561,22 @@ function withAlpha(d, alpha) {
|
|
|
419
561
|
return d;
|
|
420
562
|
return { ...d, value: `color-mix(in srgb, ${d.value} ${pct}%, transparent)` };
|
|
421
563
|
}
|
|
564
|
+
/** `w-1/2`, `basis-2/3` - a fraction is a width, and the `/` is not an alpha. */
|
|
565
|
+
const FRACTION = /^(w|h|basis|max-w|max-h|min-w|min-h|top|right|bottom|left|inset)-(\d+)\/(\d+)$/;
|
|
422
566
|
export function readUtility(utility, declared) {
|
|
567
|
+
/**
|
|
568
|
+
* BEFORE THE ALPHA SPLIT, because the two share a slash and only one of them is a
|
|
569
|
+
* colour. `w-1/2` was reaching the size reader as `w-1` and coming back `0.25rem` -
|
|
570
|
+
* silently wrong, which is worse than unread. Their `packages/ui` has none; two of
|
|
571
|
+
* their apps do (dono, 04/08).
|
|
572
|
+
*/
|
|
573
|
+
const fraction = FRACTION.exec(utility);
|
|
574
|
+
if (fraction) {
|
|
575
|
+
const property = SPACING_PROPERTY[fraction[1]];
|
|
576
|
+
const pct = (Number(fraction[2]) / Number(fraction[3])) * 100;
|
|
577
|
+
if (property)
|
|
578
|
+
return { property, value: `${Number(pct.toFixed(4))}%` };
|
|
579
|
+
}
|
|
423
580
|
const read = readUtilityCore(utility, declared);
|
|
424
581
|
return read ? withAlpha(read, utility.split("/")[1]) : read;
|
|
425
582
|
}
|
|
@@ -442,9 +599,36 @@ function readUtilityCore(utility, declared) {
|
|
|
442
599
|
const [core] = utility.split("/");
|
|
443
600
|
// A bare keyword carries a declaration with no scale behind it, so it is read
|
|
444
601
|
// before the dash split that everything else needs.
|
|
445
|
-
const keyword = TYPE_KEYWORD[core];
|
|
602
|
+
const keyword = TYPE_KEYWORD[core] ?? PLACE_KEYWORD[core];
|
|
446
603
|
if (keyword)
|
|
447
604
|
return keyword;
|
|
605
|
+
/**
|
|
606
|
+
* A BARE `rounded` IS TAILWIND'S OWN DEFAULT RADIUS, and it read as nothing - so six
|
|
607
|
+
* elements across their library arrived square. Their token wins if they declared one
|
|
608
|
+
* under that name, exactly as the scaled form already does.
|
|
609
|
+
*/
|
|
610
|
+
if (core === "rounded") {
|
|
611
|
+
return declared.has("--radius-DEFAULT")
|
|
612
|
+
? {
|
|
613
|
+
property: "borderRadius",
|
|
614
|
+
value: "{radius.DEFAULT}",
|
|
615
|
+
token: "--radius-DEFAULT",
|
|
616
|
+
}
|
|
617
|
+
: { property: "borderRadius", value: "0.25rem" };
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* ALIGNMENT - `justify-between`, `items-center`, `self-end`.
|
|
621
|
+
*
|
|
622
|
+
* 116 of these across 25 of their components, and they are not arrangement: they are
|
|
623
|
+
* WHERE the thing sits. The chevron on their card is at the far edge because of one.
|
|
624
|
+
*/
|
|
625
|
+
const align = /^(place-items|place-content|place-self|justify-items|justify-self|justify|items|self|content)-([a-z]+)$/.exec(core);
|
|
626
|
+
if (align) {
|
|
627
|
+
const property = ALIGN_PROP[align[1]];
|
|
628
|
+
const value = ALIGNMENT[align[2]];
|
|
629
|
+
if (property && value)
|
|
630
|
+
return { property, value };
|
|
631
|
+
}
|
|
448
632
|
/**
|
|
449
633
|
* BORDER WIDTH - `border`, `border-2`, `border-b`, `border-t-4`.
|
|
450
634
|
*
|
|
@@ -533,6 +717,18 @@ function readUtilityCore(utility, declared) {
|
|
|
533
717
|
return { property: "borderCollapse", value: rest };
|
|
534
718
|
}
|
|
535
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* `ring-1` IS A WIDTH, and it reached the colour reader first - which looked for a
|
|
722
|
+
* colour called `1`, found none, and returned null for all three of theirs. Its colour
|
|
723
|
+
* already reads as `outlineColor`, so the width belongs on the same family; a bare
|
|
724
|
+
* `ring` is 3px, Tailwind's own default.
|
|
725
|
+
*/
|
|
726
|
+
if (prefix === "ring" && (rest === "" || /^\d+$/.test(rest))) {
|
|
727
|
+
return { property: "outlineWidth", value: `${rest || 3}px` };
|
|
728
|
+
}
|
|
729
|
+
if (core === "ring") {
|
|
730
|
+
return { property: "outlineWidth", value: "3px" };
|
|
731
|
+
}
|
|
536
732
|
const colorProp = COLOR_PROPERTY[prefix];
|
|
537
733
|
if (colorProp) {
|
|
538
734
|
/**
|
|
@@ -677,6 +873,14 @@ function readUtilityCore(utility, declared) {
|
|
|
677
873
|
sizeRest = axis[2];
|
|
678
874
|
}
|
|
679
875
|
}
|
|
876
|
+
/** The same recombination for the axis-suffixed families: `space-y-1`, `inset-x-0`. */
|
|
877
|
+
if (prefix === "space" || prefix === "inset") {
|
|
878
|
+
const axis = /^([xy])-(.+)$/.exec(rest);
|
|
879
|
+
if (axis) {
|
|
880
|
+
sizePrefix = `${prefix}-${axis[1]}`;
|
|
881
|
+
sizeRest = axis[2];
|
|
882
|
+
}
|
|
883
|
+
}
|
|
680
884
|
const spaceProp = SPACING_PROPERTY[sizePrefix];
|
|
681
885
|
if (spaceProp) {
|
|
682
886
|
/** `gap-x-6` / `gap-y-2` - the two halves of gap, which one prefix cannot say. */
|
|
@@ -716,7 +920,87 @@ function readUtilityCore(utility, declared) {
|
|
|
716
920
|
if (MAX_WIDTH[sizeRest] && /^(max-w|max-h)$/.test(sizePrefix)) {
|
|
717
921
|
return { property: spaceProp, value: MAX_WIDTH[sizeRest] };
|
|
718
922
|
}
|
|
923
|
+
/**
|
|
924
|
+
* `top-full`, `inset-0`, `basis-auto` - the words an offset takes and a padding does
|
|
925
|
+
* not. Their Menu positions its popup with `top-full`, and a positioner that does not
|
|
926
|
+
* position is not the component.
|
|
927
|
+
*/
|
|
928
|
+
const OFFSET_WORD = {
|
|
929
|
+
full: "100%",
|
|
930
|
+
auto: "auto",
|
|
931
|
+
px: "1px",
|
|
932
|
+
};
|
|
933
|
+
if (OFFSET_WORD[sizeRest] &&
|
|
934
|
+
/^(inset|inset-x|inset-y|top|right|bottom|left|basis)$/.test(sizePrefix)) {
|
|
935
|
+
return { property: spaceProp, value: OFFSET_WORD[sizeRest] };
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* `z-50` - the stacking order, which is the difference between an overlay above the
|
|
940
|
+
* content and one behind it. Tailwind's own scale is the numbers themselves.
|
|
941
|
+
*/
|
|
942
|
+
if (prefix === "z" && (/^\d+$/.test(rest) || rest === "auto")) {
|
|
943
|
+
return { property: "zIndex", value: rest };
|
|
719
944
|
}
|
|
945
|
+
/**
|
|
946
|
+
* `backdrop-blur-[1px]`, `backdrop-blur-sm` - the frosted layer behind an overlay.
|
|
947
|
+
* Their loading veil is `bg-white/50 backdrop-blur-[1px]`, and without the blur the
|
|
948
|
+
* veil is a flat wash.
|
|
949
|
+
*/
|
|
950
|
+
const BLUR = {
|
|
951
|
+
none: "0",
|
|
952
|
+
sm: "4px",
|
|
953
|
+
"": "8px",
|
|
954
|
+
md: "12px",
|
|
955
|
+
lg: "16px",
|
|
956
|
+
xl: "24px",
|
|
957
|
+
};
|
|
958
|
+
const backdrop = /^backdrop-blur(?:-(.+))?$/.exec(core);
|
|
959
|
+
if (backdrop) {
|
|
960
|
+
const arg = backdrop[1] ?? "";
|
|
961
|
+
const arbitrary = /^\[(.+)\]$/.exec(arg);
|
|
962
|
+
const value = arbitrary ? arbitrary[1] : BLUR[arg];
|
|
963
|
+
if (value)
|
|
964
|
+
return { property: "backdropFilter", value: `blur(${value})` };
|
|
965
|
+
}
|
|
966
|
+
const blur = /^blur(?:-(.+))?$/.exec(core);
|
|
967
|
+
if (blur) {
|
|
968
|
+
const arg = blur[1] ?? "";
|
|
969
|
+
const arbitrary = /^\[(.+)\]$/.exec(arg);
|
|
970
|
+
const value = arbitrary ? arbitrary[1] : BLUR[arg];
|
|
971
|
+
if (value)
|
|
972
|
+
return { property: "filter", value: `blur(${value})` };
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* `rotate-180`, `scale-[0.98]` - a static transform, which is a state and not motion:
|
|
976
|
+
* their chevron points up when the card is open. Each one lands on `transform` whole,
|
|
977
|
+
* so two on the same element would overwrite - none of their 3 co-occur, and saying
|
|
978
|
+
* one of them is better than saying neither.
|
|
979
|
+
*/
|
|
980
|
+
const rotate = /^-?rotate-(.+)$/.exec(core);
|
|
981
|
+
if (rotate) {
|
|
982
|
+
const arbitrary = /^\[(.+)\]$/.exec(rotate[1]);
|
|
983
|
+
const deg = arbitrary ? arbitrary[1] : `${rotate[1]}deg`;
|
|
984
|
+
return {
|
|
985
|
+
property: "transform",
|
|
986
|
+
value: `rotate(${core.startsWith("-") ? `-${deg}` : deg})`,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
const scale = /^scale-(.+)$/.exec(core);
|
|
990
|
+
if (scale) {
|
|
991
|
+
const arbitrary = /^\[(.+)\]$/.exec(scale[1]);
|
|
992
|
+
const factor = arbitrary
|
|
993
|
+
? arbitrary[1]
|
|
994
|
+
: /^\d+$/.test(scale[1])
|
|
995
|
+
? String(Number(scale[1]) / 100)
|
|
996
|
+
: null;
|
|
997
|
+
if (factor)
|
|
998
|
+
return { property: "transform", value: `scale(${factor})` };
|
|
999
|
+
}
|
|
1000
|
+
/** `line-clamp-2` - how many lines survive before the ellipsis. */
|
|
1001
|
+
const clamp = /^line-clamp-(\d+)$/.exec(core);
|
|
1002
|
+
if (clamp)
|
|
1003
|
+
return { property: "WebkitLineClamp", value: clamp[1] };
|
|
720
1004
|
if (prefix === "rounded") {
|
|
721
1005
|
/**
|
|
722
1006
|
* `rounded-t-lg` - one side, two corners. The side names which half; the scale is
|
|
@@ -1239,6 +1523,25 @@ export function transcribe(classes, declared) {
|
|
|
1239
1523
|
// `rounded-t-lg` names two corners: the reader returns them joined with `+`, and
|
|
1240
1524
|
// the write splits - the same shape the `size` expansion above uses.
|
|
1241
1525
|
if (decl.property.includes("+")) {
|
|
1526
|
+
/**
|
|
1527
|
+
* AND WITH ITS OWN VALUE EACH, when one utility means three things.
|
|
1528
|
+
*
|
|
1529
|
+
* `truncate` is `overflow:hidden; text-overflow:ellipsis; white-space:nowrap` - any
|
|
1530
|
+
* one of the three alone paints nothing, so the pair form (same value, two
|
|
1531
|
+
* properties) could not carry it. `prop:value|prop:value` says both halves.
|
|
1532
|
+
*/
|
|
1533
|
+
if (decl.value.includes("|")) {
|
|
1534
|
+
for (const pair of decl.value.split("|")) {
|
|
1535
|
+
const [property, value] = pair.split(":");
|
|
1536
|
+
if (property && value && target[property] == null)
|
|
1537
|
+
target[property] = value;
|
|
1538
|
+
}
|
|
1539
|
+
if (decl.token)
|
|
1540
|
+
out.fromToken += 1;
|
|
1541
|
+
else
|
|
1542
|
+
out.fromLiteral += 1;
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1242
1545
|
for (const property of decl.property.split("+")) {
|
|
1243
1546
|
if (target[property] == null)
|
|
1244
1547
|
target[property] = decl.value;
|
package/package.json
CHANGED