synthesisui 0.16.23 → 0.16.26
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/claude-md.js +7 -0
- package/dist/cn-codegen.js +31 -1
- package/dist/fonts.js +11 -1
- package/dist/guide.js +47 -3
- package/dist/index.js +18 -3
- package/dist/motion.js +115 -0
- package/package.json +1 -1
package/dist/claude-md.js
CHANGED
|
@@ -290,6 +290,13 @@ against their own, so the last one written wins, which is what the call site rea
|
|
|
290
290
|
If an override is ignored, that component predates the resolver: regenerate it with
|
|
291
291
|
\`npx synthesisui@latest component <slug> <name>\` rather than reaching for \`!\`.
|
|
292
292
|
|
|
293
|
+
**Motion is selection, not improvisation.** The base is quiet: nothing moves until a person
|
|
294
|
+
asks. When they do, pick from the **Motion vocabulary** in GUIDE.md - \`animate-<name>\`
|
|
295
|
+
utilities already compiled on this system's clock - and never hand-roll \`@keyframes\` or raw
|
|
296
|
+
durations. A motion the vocabulary lacks is a gap like any other: file it (\`request_token\`
|
|
297
|
+
MCP tool, or \`npx synthesisui@latest request\`) and say what you chose from the vocabulary
|
|
298
|
+
instead.
|
|
299
|
+
|
|
293
300
|
Only write something new when nothing in the manifest covers the purpose - and when you do,
|
|
294
301
|
say which entry you considered and why it did not fit, then FILE it while the reasoning is
|
|
295
302
|
still yours: the \`request_component\` MCP tool, or \`npx synthesisui@latest request component
|
package/dist/cn-codegen.js
CHANGED
|
@@ -104,9 +104,26 @@ const PREFIXES = [
|
|
|
104
104
|
"gap-x", "gap-y", "gap",
|
|
105
105
|
"w", "h", "min-w", "min-h", "max-w", "max-h",
|
|
106
106
|
"leading", "tracking",
|
|
107
|
-
"
|
|
107
|
+
"justify-items", "justify-self", "justify",
|
|
108
|
+
"items", "self", "order", "z",
|
|
108
109
|
];
|
|
109
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Utilities that set \`display\`. One group, so \`hidden\` can evict \`flex\` -
|
|
113
|
+
* and kept OUT of the prefix walk, because \`flex\` also heads whole families
|
|
114
|
+
* that set OTHER properties. Read as a prefix, \`flex-row\` evicted the \`flex\`
|
|
115
|
+
* that makes the box a flex container, and every Row and Stack in the project
|
|
116
|
+
* stacked its children in silence. Found by an agent in the field (test03,
|
|
117
|
+
* 29/07), diagnosed against computed output, fixed locally, and FILED - this
|
|
118
|
+
* is its fix, ported to the source so it stops evaporating on regeneration.
|
|
119
|
+
*/
|
|
120
|
+
const DISPLAYS = new Set([
|
|
121
|
+
"flex", "grid", "block", "inline", "inline-flex", "inline-grid",
|
|
122
|
+
"inline-block", "hidden", "contents", "flow-root", "table", "list-item",
|
|
123
|
+
]);
|
|
124
|
+
const FLEX_DIRECTIONS = new Set(["row", "col", "row-reverse", "col-reverse"]);
|
|
125
|
+
const FLEX_WRAPS = new Set(["wrap", "nowrap", "wrap-reverse"]);
|
|
126
|
+
|
|
110
127
|
/**
|
|
111
128
|
* The property this class sets, or the class itself when we cannot tell.
|
|
112
129
|
*
|
|
@@ -124,10 +141,23 @@ function groupOf(cls: string): string {
|
|
|
124
141
|
const negative = base.startsWith("-");
|
|
125
142
|
if (negative) base = base.slice(1);
|
|
126
143
|
|
|
144
|
+
if (DISPLAYS.has(base)) return \`\${variants}display\`;
|
|
145
|
+
|
|
127
146
|
const dash = base.indexOf("-");
|
|
128
147
|
const head = dash === -1 ? base : base.slice(0, dash);
|
|
129
148
|
const rest = dash === -1 ? "" : base.slice(dash + 1);
|
|
130
149
|
|
|
150
|
+
if (head === "flex") {
|
|
151
|
+
if (FLEX_DIRECTIONS.has(rest)) return \`\${variants}flex-direction\`;
|
|
152
|
+
if (FLEX_WRAPS.has(rest)) return \`\${variants}flex-wrap\`;
|
|
153
|
+
return \`\${variants}flex\`; // flex-1 / flex-auto / flex-none / flex-initial
|
|
154
|
+
}
|
|
155
|
+
if (head === "grid") {
|
|
156
|
+
// grid-cols-* / grid-rows-* / grid-flow-* each own their property.
|
|
157
|
+
const sub = rest.indexOf("-") === -1 ? rest : rest.slice(0, rest.indexOf("-"));
|
|
158
|
+
return \`\${variants}grid-\${sub}\`;
|
|
159
|
+
}
|
|
160
|
+
|
|
131
161
|
// The two families a prefix cannot settle, answered by this system's names.
|
|
132
162
|
if (head === "text") {
|
|
133
163
|
if (TEXT_SIZES.has(rest)) return \`\${variants}font-size\`;
|
package/dist/fonts.js
CHANGED
|
@@ -62,7 +62,17 @@ export function nextFontSnippet(families, slug, appDir = "app") {
|
|
|
62
62
|
if (!seen.has(name)) {
|
|
63
63
|
seen.set(name, role);
|
|
64
64
|
importNames.push(importName(name));
|
|
65
|
-
consts.push(`export const ${seen.get(name)} = ${importName(name)}({`, ` subsets: ["latin"],`,
|
|
65
|
+
consts.push(`export const ${seen.get(name)} = ${importName(name)}({`, ` subsets: ["latin"],`,
|
|
66
|
+
// WEIGHTS ARE ALWAYS SPELLED OUT, because next/font only allows
|
|
67
|
+
// omitting them for VARIABLE fonts - and plenty of Google families
|
|
68
|
+
// ship as static cuts (IBM Plex Mono, for one). The generated file
|
|
69
|
+
// assumed variable and broke the consumer's first `next build`; an
|
|
70
|
+
// agent caught it in the field (test03, 29/07) and had to repair our
|
|
71
|
+
// output by hand. Explicit weights are valid for BOTH kinds, so
|
|
72
|
+
// emitting them always can never break - and these three are the
|
|
73
|
+
// steps every generated system actually uses (regular/medium/
|
|
74
|
+
// semibold), so no static cut is downloaded for nothing.
|
|
75
|
+
` weight: ["400", "500", "600"],`, ` variable: "--font-ds-${seen.get(name)}",`, `});`);
|
|
66
76
|
}
|
|
67
77
|
}
|
|
68
78
|
const fontsFile = [
|
package/dist/guide.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { nextFontSnippet } from "./fonts.js";
|
|
2
|
+
import { animationShorthand, classifyKeyframe, describeKeyframe, isFullRotation, } from "./motion.js";
|
|
2
3
|
const kebab = (v) => v.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
3
4
|
const list = (items) => items.length ? items.map((i) => `\`${i}\``).join(", ") : "_(none)_";
|
|
4
5
|
const dataAttrs = (variants) => Object.entries(variants).map(([axis, opts]) => `data-${kebab(axis)}="${Object.keys(opts).join("|")}"`);
|
|
@@ -136,6 +137,25 @@ ${depLines.join("\n")}
|
|
|
136
137
|
const hasTailwind = "theme.css" in payload.artifacts;
|
|
137
138
|
const hasParts = Object.values(components).some((r) => r.parts && Object.keys(r.parts).length > 0);
|
|
138
139
|
const componentLines = Object.entries(components).map(([cname, recipe]) => componentEntry(cname, recipe));
|
|
140
|
+
// Motion vocabulary: the system's keyframes, named and usable. Intent is
|
|
141
|
+
// classified from the frames (same logic that compiled the animate-*
|
|
142
|
+
// utilities into theme.css), so what this section PROMISES about a utility's
|
|
143
|
+
// clock is what the CSS actually does.
|
|
144
|
+
const INTENT_LINE = {
|
|
145
|
+
entrance: "entrance · runs once on mount",
|
|
146
|
+
exit: "exit · runs once, before removal",
|
|
147
|
+
loop: "ambient loop · infinite - decorative, use sparingly",
|
|
148
|
+
attention: "attention · one shot, only when asked",
|
|
149
|
+
};
|
|
150
|
+
const keyframeLines = Object.entries(motion.keyframes).map(([kname, frames]) => {
|
|
151
|
+
const kf = frames;
|
|
152
|
+
const intent = classifyKeyframe(kname, kf);
|
|
153
|
+
const usage = hasTailwind
|
|
154
|
+
? `\`animate-${kebab(kname)}\``
|
|
155
|
+
: `\`animation: ${animationShorthand(`ds-${slug}-${kebab(kname)}`, intent, isFullRotation(kf))}\``;
|
|
156
|
+
return `- ${usage} - ${INTENT_LINE[intent]} (${describeKeyframe(kf)})`;
|
|
157
|
+
});
|
|
158
|
+
const patternLines = Object.entries(motion.patterns ?? {}).map(([pname, p]) => `- **${pname}** (on ${p.trigger}): ${p.description}`);
|
|
139
159
|
// Engagement blocks (gamification library) - category apart from core components.
|
|
140
160
|
const blockEntries = Object.entries(doc.blocks ?? {});
|
|
141
161
|
const blockLines = blockEntries.map(([bname, recipe]) => componentEntry(bname, recipe));
|
|
@@ -365,11 +385,35 @@ ${hasTailwind
|
|
|
365
385
|
scale \`--ds-typography-scale-<key>-font-size\`${hasTailwind ? " (utility: `text-<key>`)" : ""}: ${list(Object.keys(foundations.typography.scale))}.
|
|
366
386
|
- Motion: durations \`--ds-motion-durations-<key>\` (${list(Object.keys(motion.durations))}) and
|
|
367
387
|
easings \`--ds-motion-easings-<key>\` (${list(Object.keys(motion.easings))}). Use them on
|
|
368
|
-
\`transition
|
|
369
|
-
so timing stays on-brand.
|
|
370
|
-
|
|
388
|
+
\`transition\` (e.g. \`transition: color var(--ds-motion-durations-fast) var(--ds-motion-easings-standard)\`)
|
|
389
|
+
so timing stays on-brand. For ANIMATION, this system ships a named vocabulary - see
|
|
390
|
+
**Motion vocabulary** below; never hand-roll \`@keyframes\` or raw durations.
|
|
371
391
|
- When **creating a new component** the DS does not cover yet: compose it from these semantic
|
|
372
392
|
tokens to inherit the system's identity; do not invent colors/measures outside the scale.
|
|
393
|
+
${keyframeLines.length > 0
|
|
394
|
+
? `
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Motion vocabulary
|
|
398
|
+
|
|
399
|
+
The base is QUIET: nothing moves until someone asks. When they do, animation is **selection
|
|
400
|
+
from this list**, not improvisation - these are the system's own animations, already compiled
|
|
401
|
+
into ${hasTailwind ? "`theme.css` as ready utilities" : `\`tokens.css\` as \`@keyframes ds-${slug}-<name>\``}, each on the clock its intent calls for:
|
|
402
|
+
|
|
403
|
+
${keyframeLines.join("\n")}
|
|
404
|
+
${patternLines.length > 0
|
|
405
|
+
? `
|
|
406
|
+
Interaction patterns the system pairs with them:
|
|
407
|
+
|
|
408
|
+
${patternLines.join("\n")}
|
|
409
|
+
`
|
|
410
|
+
: ""}
|
|
411
|
+
Rules: entrances run once, never on scroll-loop; ambient loops are decoration, one per view is
|
|
412
|
+
plenty; \`prefers-reduced-motion\` is already honored in the shipped CSS (a floor collapses all
|
|
413
|
+
animations) - don't undo it. If a request needs a motion this vocabulary lacks, do NOT hand-roll
|
|
414
|
+
\`@keyframes\` or raw durations: file it (\`request_token\` via MCP, or \`synthesisui request\`) and
|
|
415
|
+
tell the person what you chose from the vocabulary instead.`
|
|
416
|
+
: ""}
|
|
373
417
|
|
|
374
418
|
---
|
|
375
419
|
|
package/dist/index.js
CHANGED
|
@@ -37,9 +37,21 @@ Usage - deterministic, FREE:
|
|
|
37
37
|
synthesisui upgrade <slug> update an installed DS + regenerate your components + migration brief
|
|
38
38
|
synthesisui use <slug> "<intent>" print a ready-to-paste agent prompt to build/modify on-system
|
|
39
39
|
synthesisui clean [--force] strip create-next-app boilerplate (dry run without --force)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
|
|
41
|
+
Usage - governance (deterministic, FREE):
|
|
42
|
+
synthesisui adopt [--write] turn the design system you ALREADY have into a contract
|
|
43
|
+
your agent follows - without touching your CSS
|
|
44
|
+
synthesisui connect wire your agent: the check as an editor hook, the system
|
|
45
|
+
as MCP tools, and a contract that stops repeating itself
|
|
46
|
+
synthesisui doctor [paths…] [--verbose] audit for DRIFT: every design value written by hand, the
|
|
47
|
+
token your system already has for it, coherence, and the
|
|
48
|
+
record over time
|
|
49
|
+
synthesisui request [component|token] the queue of what your agent needed and the system
|
|
50
|
+
refused to invent (--done <id> closes one)
|
|
51
|
+
synthesisui sync send the local record - checks, fixes, open requests -
|
|
52
|
+
to your system's dashboard (by hand, never automatic)
|
|
53
|
+
synthesisui hook the check itself; installed by connect, run by your editor
|
|
54
|
+
synthesisui mcp the system as tools; installed by connect, run by your agent
|
|
43
55
|
|
|
44
56
|
Usage - AI, USES CREDITS (login required):
|
|
45
57
|
synthesisui generate "<desc>" AI-create a NEW component your DS doesn't have (token-only recipe)
|
|
@@ -73,12 +85,15 @@ Options:
|
|
|
73
85
|
-h, --help this help
|
|
74
86
|
|
|
75
87
|
Examples:
|
|
88
|
+
synthesisui adopt # you already have a system: start here
|
|
76
89
|
synthesisui login
|
|
77
90
|
synthesisui init --target next
|
|
78
91
|
synthesisui init --target next --ds halogen bootstrap + bring a system in
|
|
79
92
|
synthesisui doctor
|
|
80
93
|
synthesisui doctor apps/web packages/ui # scope the read in a monorepo
|
|
81
94
|
synthesisui doctor --strict
|
|
95
|
+
synthesisui connect # before opening your agent
|
|
96
|
+
synthesisui sync # after a session, or from CI
|
|
82
97
|
synthesisui list
|
|
83
98
|
synthesisui add halogen
|
|
84
99
|
synthesisui add halogen --version 3
|
package/dist/motion.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion intent, classified from the frames alone - the CLI copy.
|
|
3
|
+
*
|
|
4
|
+
* The web side (`apps/web/src/lib/ds/motion-vocabulary.ts`) uses this exact
|
|
5
|
+
* logic to decide the clock behind each `animate-<name>` utility it compiles
|
|
6
|
+
* into theme.css. The GUIDE this package writes has to DESCRIBE those same
|
|
7
|
+
* utilities, and the CLI is zero-dependency, so the logic exists twice - like
|
|
8
|
+
* the ledger's summarize. The web spec imports both copies and fails on drift.
|
|
9
|
+
*
|
|
10
|
+
* Intent from geometry:
|
|
11
|
+
* entrance starts off-identity, lands on identity → runs once, fills both
|
|
12
|
+
* exit the reverse → runs once
|
|
13
|
+
* loop round trip, full rotation, or travel with no identity at
|
|
14
|
+
* either end (marquee, shimmer) → ambient, infinite
|
|
15
|
+
* attention a round trip that is a gesture, not a mood (shake, tada)
|
|
16
|
+
* → one shot
|
|
17
|
+
*
|
|
18
|
+
* The one non-geometric input: a round trip is ambiguous (pulse loops, shake
|
|
19
|
+
* does not), and the tiebreak is the keyframe's NAME - authored, part of the
|
|
20
|
+
* contract, not a guess about it.
|
|
21
|
+
*/
|
|
22
|
+
const offsetOf = (key) => {
|
|
23
|
+
if (key === "from")
|
|
24
|
+
return 0;
|
|
25
|
+
if (key === "to")
|
|
26
|
+
return 100;
|
|
27
|
+
const n = Number.parseFloat(key);
|
|
28
|
+
return Number.isNaN(n) ? 0 : n;
|
|
29
|
+
};
|
|
30
|
+
const isIdentityValue = (prop, value) => {
|
|
31
|
+
const v = value.trim();
|
|
32
|
+
if (prop === "opacity")
|
|
33
|
+
return v === "1";
|
|
34
|
+
if (prop === "transform") {
|
|
35
|
+
if (v === "none")
|
|
36
|
+
return true;
|
|
37
|
+
return [...v.matchAll(/([a-zA-Z]+)\(([^)]*)\)/g)].every(([, fn, args]) => {
|
|
38
|
+
const nums = args.split(",").map((a) => Number.parseFloat(a));
|
|
39
|
+
if (fn.startsWith("translate"))
|
|
40
|
+
return nums.every((n) => n === 0);
|
|
41
|
+
if (fn.startsWith("scale"))
|
|
42
|
+
return nums.every((n) => n === 1);
|
|
43
|
+
if (fn.startsWith("rotate") || fn.startsWith("skew"))
|
|
44
|
+
return nums.every((n) => n === 0);
|
|
45
|
+
return false;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
const isIdentityFrame = (decls) => Object.entries(decls).every(([p, v]) => isIdentityValue(p, v));
|
|
51
|
+
const LOOP_NAMES = /pulse|spin|rotate|orbit|float|drift|marquee|breathe|shimmer|sheen|glow|ping|wave|bounce|ambient|scroll/i;
|
|
52
|
+
export function classifyKeyframe(name, frames) {
|
|
53
|
+
const ordered = Object.entries(frames).sort(([a], [b]) => offsetOf(a) - offsetOf(b));
|
|
54
|
+
if (ordered.length < 2)
|
|
55
|
+
return "attention";
|
|
56
|
+
const first = ordered[0][1];
|
|
57
|
+
const last = ordered[ordered.length - 1][1];
|
|
58
|
+
const fullTurn = /rotate\(\s*-?(360|720)deg\s*\)/.test(last.transform ?? "");
|
|
59
|
+
if (fullTurn)
|
|
60
|
+
return "loop";
|
|
61
|
+
const firstId = isIdentityFrame(first);
|
|
62
|
+
const lastId = isIdentityFrame(last);
|
|
63
|
+
if (JSON.stringify(first) === JSON.stringify(last)) {
|
|
64
|
+
// A round trip that never touches rest is a mood hovering around its own
|
|
65
|
+
// state (a flame at 0.7 opacity) → loop. One that departs FROM rest is
|
|
66
|
+
// ambiguous - pulse loops, shake does not - and only there the authored
|
|
67
|
+
// NAME breaks the tie.
|
|
68
|
+
if (!firstId)
|
|
69
|
+
return "loop";
|
|
70
|
+
return LOOP_NAMES.test(name) ? "loop" : "attention";
|
|
71
|
+
}
|
|
72
|
+
if (!firstId && lastId)
|
|
73
|
+
return "entrance";
|
|
74
|
+
if (firstId && !lastId) {
|
|
75
|
+
// Leaving is disappearing: an exit fades out. Rest → offset with the
|
|
76
|
+
// opacity intact is a conveyor starting its lap (marquee), not a goodbye.
|
|
77
|
+
const gone = Number.parseFloat(last.opacity ?? "") === 0;
|
|
78
|
+
return gone ? "exit" : "loop";
|
|
79
|
+
}
|
|
80
|
+
return "loop";
|
|
81
|
+
}
|
|
82
|
+
export const isFullRotation = (frames) => Object.values(frames).some((decls) => /rotate\(\s*-?(360|720)deg\s*\)/.test(decls.transform ?? ""));
|
|
83
|
+
export function animationShorthand(scopedName, intent, fullTurn) {
|
|
84
|
+
switch (intent) {
|
|
85
|
+
case "entrance":
|
|
86
|
+
case "exit":
|
|
87
|
+
return `${scopedName} var(--ds-motion-durations-base, 200ms) var(--ds-motion-easings-standard, ease) both`;
|
|
88
|
+
case "loop":
|
|
89
|
+
return fullTurn
|
|
90
|
+
? `${scopedName} var(--ds-motion-durations-ambient, 1s) linear infinite`
|
|
91
|
+
: `${scopedName} var(--ds-motion-durations-ambient, 2s) var(--ds-motion-easings-gentle, ease-in-out) infinite`;
|
|
92
|
+
case "attention":
|
|
93
|
+
return `${scopedName} var(--ds-motion-durations-slow, 400ms) var(--ds-motion-easings-standard, ease)`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function describeKeyframe(frames) {
|
|
97
|
+
const verbs = new Set();
|
|
98
|
+
for (const decls of Object.values(frames)) {
|
|
99
|
+
if ("opacity" in decls)
|
|
100
|
+
verbs.add("fades");
|
|
101
|
+
const t = decls.transform ?? "";
|
|
102
|
+
if (/translate/.test(t))
|
|
103
|
+
verbs.add("slides");
|
|
104
|
+
if (/scale/.test(t))
|
|
105
|
+
verbs.add("scales");
|
|
106
|
+
if (/rotate/.test(t))
|
|
107
|
+
verbs.add("rotates");
|
|
108
|
+
if (/skew/.test(t))
|
|
109
|
+
verbs.add("skews");
|
|
110
|
+
for (const prop of Object.keys(decls))
|
|
111
|
+
if (prop !== "opacity" && prop !== "transform")
|
|
112
|
+
verbs.add(`moves ${prop}`);
|
|
113
|
+
}
|
|
114
|
+
return [...verbs].join(", ") || "animates";
|
|
115
|
+
}
|
package/package.json
CHANGED