synthesisui 0.16.25 → 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/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/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