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 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
@@ -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
- "flex", "grid", "justify", "items", "self", "order", "z",
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"],`, ` variable: "--font-ds-${seen.get(name)}",`, `});`);
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\`/\`animation\` (e.g. \`transition: color var(--ds-motion-durations-fast) var(--ds-motion-easings-standard)\`)
369
- so timing stays on-brand. The DS ships timing tokens, **not** a runtime - for entrance/reveal/stagger
370
- pair them with a motion lib (e.g. \`motion\`/Framer) or CSS \`@keyframes\`.
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
- synthesisui doctor [paths…] [--verbose] audit for DRIFT: every design value written by
41
- hand, the token your system already has for it, and the
42
- laws your system carries for what you use
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.23",
3
+ "version": "0.16.26",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {