synthesisui 0.16.118 → 0.16.119

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.
@@ -310,13 +310,23 @@ sketch) {
310
310
  * and stopped at, and a plain `div` is nobody's element.
311
311
  */
312
312
  const measured = sketch?.[node.at];
313
+ const kind = measured?.from ? frontierKind(measured.from) : "own";
314
+ /**
315
+ * A MOTION ELEMENT IS THEIRS TO NAME TOO. `motion.div` is the element their panel
316
+ * opens with, and the props that make it open have no CSS to translate to - so the
317
+ * snippet emits their element and their props, verbatim. Headless for the
318
+ * behaviour, motion for the animation; an opaque library is still named and
319
+ * stopped at.
320
+ */
313
321
  if (measured?.from &&
314
322
  measured.tag &&
315
- /^[A-Z]/.test(measured.tag) &&
316
- frontierKind(measured.from) === "headless") {
323
+ /^[A-Za-z]/.test(measured.tag) &&
324
+ (kind === "headless" || kind === "motion")) {
317
325
  node_.from = measured.from;
318
326
  node_.refName = measured.tag;
319
327
  }
328
+ if (measured?.motionProps)
329
+ node_.motionProps = measured.motionProps;
320
330
  }
321
331
  // The NAME is what carries the styles, so it is claimed even when nothing
322
332
  // resolved: the tree triggers on presence, and structure is most of the
@@ -1205,7 +1205,7 @@ export async function takeCensus(root, opts) {
1205
1205
  * the name - 35 of their 36, with `TypingIndicator` internal to `Message` and reported
1206
1206
  * as not exported. See `publicApi`.
1207
1207
  */
1208
- const api = await publicApi(root, scopeLabel);
1208
+ const api = await publicApi(root);
1209
1209
  if (api) {
1210
1210
  for (const c of components) {
1211
1211
  const file = defined.find((d) => d.name === c.name)?.file;
@@ -1214,7 +1214,16 @@ export async function takeCensus(root, opts) {
1214
1214
  : undefined;
1215
1215
  const required = source ? requiredProps(source, c.name) : [];
1216
1216
  c.api = {
1217
- ...(api.exports.has(c.name) ? { from: api.from } : { exported: false }),
1217
+ /**
1218
+ * SILENCE WHEN THE ENTRY WAS NOT READ. An unreadable entry gives an empty export
1219
+ * set, and calling that "internal" would have marked every component internal at
1220
+ * once - see `PublicApi.read`.
1221
+ */
1222
+ ...(!api.read
1223
+ ? {}
1224
+ : api.exports.has(c.name)
1225
+ ? { from: api.from }
1226
+ : { exported: false }),
1218
1227
  /**
1219
1228
  * REPO-RELATIVE, so the answer needs no second search. `defined[].file` is
1220
1229
  * relative to the scope; joined with the scope it is a path anybody can check
@@ -1222,7 +1231,7 @@ export async function takeCensus(root, opts) {
1222
1231
  * walking a monorepo (their component sits six levels below `packages/`, and the
1223
1232
  * shallow walk that guards against shadowing never reached it).
1224
1233
  */
1225
- ...(file ? { file: `${scopeLabel}/${file}` } : {}),
1234
+ ...(file ? { file: scopeLabel ? `${scopeLabel}/${file}` : file } : {}),
1226
1235
  ...(required.length > 0 ? { required } : {}),
1227
1236
  };
1228
1237
  }
@@ -71,28 +71,32 @@ async function exportsOf(path, source, depth = 0, seen = new Set()) {
71
71
  return out;
72
72
  }
73
73
  /**
74
- * The public API of the folder being imported, or null when it has no manifest.
74
+ * The public API of the folder being read, or null when it has no manifest.
75
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
76
+ * `dir` is the folder the census measured - which is ALREADY `<repo>/packages/ui` on a
77
+ * scoped run. Joining the scope on again looked for `packages/ui/packages/ui/package.json`,
78
+ * found nothing and returned null, so the specifier and the required props were absent on
79
+ * all 36 of their components while the reading that needed them sat right there
80
+ * (dono, 04/08).
81
+ *
82
+ * A folder with no `package.json` of its own is not a package and there is no specifier to
78
83
  * offer - an app's `src/components` is imported by path, which is theirs to know and not
79
84
  * ours to guess.
80
85
  */
81
- export async function publicApi(root, scope) {
82
- if (!scope)
83
- return null;
84
- const manifest = await readFile(join(root, scope, "package.json"), "utf8")
86
+ export async function publicApi(dir) {
87
+ const manifest = await readFile(join(dir, "package.json"), "utf8")
85
88
  .then((raw) => JSON.parse(raw))
86
89
  .catch(() => null);
87
90
  if (!manifest?.name)
88
91
  return null;
89
92
  const entry = manifest.main ?? manifest.module ?? "src/index.ts";
90
- const found = await readAt(join(root, scope), entry);
93
+ const found = await readAt(dir, entry);
91
94
  if (!found)
92
- return { from: manifest.name, exports: new Set() };
95
+ return { from: manifest.name, exports: new Set(), read: false };
93
96
  return {
94
97
  from: manifest.name,
95
98
  exports: await exportsOf(found[0], found[1]),
99
+ read: true,
96
100
  };
97
101
  }
98
102
  const PROPS_BLOCK = /(?:export\s+)?(?:interface|type)\s+(\w*Props)\s*(?:=\s*)?\{([\s\S]*?)\n\}/;
@@ -243,6 +243,43 @@ export function guardAt(source, at) {
243
243
  return null;
244
244
  return negated ? `!(${cond})` : cond;
245
245
  }
246
+ /** The motion props a tag carries, as their own text. See `SketchNode.motionProps`. */
247
+ const MOTION_PROP = /\b(initial|animate|exit|transition|whileHover|whileTap|whileFocus|whileDrag|layoutId)=\{/g;
248
+ export function motionPropsOf(body) {
249
+ const parts = [];
250
+ MOTION_PROP.lastIndex = 0;
251
+ for (const m of body.matchAll(MOTION_PROP)) {
252
+ const open = (m.index ?? 0) + m[0].length - 1;
253
+ let depth = 0;
254
+ let end = -1;
255
+ for (let i = open; i < body.length; i++) {
256
+ if (body[i] === "{")
257
+ depth++;
258
+ else if (body[i] === "}") {
259
+ depth--;
260
+ if (depth === 0) {
261
+ end = i;
262
+ break;
263
+ }
264
+ }
265
+ }
266
+ if (end === -1)
267
+ continue;
268
+ const value = body
269
+ .slice(open, end + 1)
270
+ .replace(/\s+/g, " ")
271
+ .trim();
272
+ // A value that reaches outside the tag - a variable, a hook, a function - is theirs
273
+ // and copying its NAME would point the snippet at something it cannot see.
274
+ if (!/^\{\s*[[{"'\d]/.test(value))
275
+ continue;
276
+ parts.push(`${m[1]}=${value}`);
277
+ }
278
+ if (parts.length === 0)
279
+ return null;
280
+ const out = parts.join(" ");
281
+ return out.length > 400 ? null : out;
282
+ }
246
283
  export function sketchOf(source, name) {
247
284
  /**
248
285
  * READ THE IMPORTS BEFORE SLICING - they live at the top of the file, and the
@@ -296,11 +333,25 @@ export function sketchOf(source, name) {
296
333
  if (rootAt !== -1 && event.at === rootAt)
297
334
  node.root = true;
298
335
  // What a capitalised tag IS, from the file's own imports - see `SketchNode.from`.
299
- if (/^[A-Z]/.test(event.tag)) {
336
+ /**
337
+ * A DOT IS A NAMESPACE, and a namespace is an import - `motion.div` is lowercase and
338
+ * still comes from `motion/react`. Only capitalised tags were stamped, so the element
339
+ * their animated panel is built on had no package and the snippet emitted a plain
340
+ * `<div>` with their animation props on it (dono, 04/08).
341
+ */
342
+ if (/^[A-Z]/.test(event.tag) || event.tag.includes(".")) {
300
343
  const pkg = packageOfTag(event.tag, imports);
301
344
  if (pkg)
302
345
  node.from = pkg;
303
346
  }
347
+ /**
348
+ * THE MOTION PROPS, off the opening tag - see `SketchNode.motionProps`. Kept whole and
349
+ * short: a prop whose value is a function or a variable is theirs to write, and a copy
350
+ * of `animate={controls}` would name something the snippet cannot reach.
351
+ */
352
+ const motion = motionPropsOf(event.body);
353
+ if (motion)
354
+ node.motionProps = motion;
304
355
  // What has to be true for this element to exist at all - see `guardAt`.
305
356
  const guard = guardAt(source, event.at);
306
357
  if (guard) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.118",
3
+ "version": "0.16.119",
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": {