@grampro/kit 2.6.0 → 2.6.1

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/package.json +1 -1
  3. package/source/components/accordion/passport.json +1 -1
  4. package/source/components/alert/passport.json +1 -1
  5. package/source/components/avatar/passport.json +1 -1
  6. package/source/components/badge/passport.json +1 -1
  7. package/source/components/breadcrumb/passport.json +1 -1
  8. package/source/components/button/passport.json +1 -1
  9. package/source/components/card/passport.json +1 -1
  10. package/source/components/checkbox/passport.json +1 -1
  11. package/source/components/combobox/passport.json +1 -1
  12. package/source/components/data-grid/passport.json +1 -1
  13. package/source/components/date-picker/passport.json +1 -1
  14. package/source/components/dialog/passport.json +1 -1
  15. package/source/components/file-uploader/passport.json +1 -1
  16. package/source/components/input/passport.json +1 -1
  17. package/source/components/menu/passport.json +1 -1
  18. package/source/components/modal/passport.json +1 -1
  19. package/source/components/number-input/passport.json +1 -1
  20. package/source/components/popover/passport.json +1 -1
  21. package/source/components/progress/passport.json +1 -1
  22. package/source/components/radio-group/passport.json +1 -1
  23. package/source/components/skeleton/passport.json +1 -1
  24. package/source/components/spinner/passport.json +1 -1
  25. package/source/components/switch/passport.json +1 -1
  26. package/source/components/tabs/passport.json +1 -1
  27. package/source/components/textarea/passport.json +1 -1
  28. package/source/components/toaster/passport.json +1 -1
  29. package/source/components/tooltip/passport.json +1 -1
  30. package/tools/mcp/figma.cjs +131 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.6.1
4
+
5
+ ### Changed
6
+
7
+ **A screen costs about half what it did to read.** Measured on a real
8
+ purchase-orders design: 42.9 KB to 23.9 KB, roughly 13,300 tokens to
9
+ 7,400.
10
+
11
+ Eight table rows exported as eight sibling subtrees of identical shape
12
+ whose text differed — 48% of that screen was the same row said seven
13
+ more times. The first is now given in full, because the agent builds from
14
+ its shape, and the rest become `repeatedText`: one array of strings per
15
+ row, which is closer to what a DataGrid's `data` prop wants than the
16
+ subtrees were.
17
+
18
+ Runs are matched on structure rather than appearance. Keyed on style, the
19
+ rows never matched at all: a striped table alternates its background, so
20
+ every consecutive pair looked like a different shape and nothing
21
+ collapsed. Where the fills do vary across a run they come back as
22
+ `repeatedFills`, which is how the striping and the per-status badge
23
+ colours stay legible.
24
+
25
+ Only runs of three or more collapse, and only where there is depth to
26
+ save: two of a thing is a pair, not a list, and a row of labels has
27
+ nothing inside it worth folding.
28
+
29
+ ### Fixed
30
+
31
+ **`depth` is honoured on the plugin and file routes.** The tool has always
32
+ offered it — "Lower it if the payload is large" — and only ever applied
33
+ it to a design fetched from a URL. A caller who read the schema and
34
+ lowered it to cut a large payload got the large payload back. The same
35
+ screen at `depth: 3` is now 3.5 KB, and what was dropped says so rather
36
+ than going quiet.
37
+
38
+ **Selecting several frames exports several screens.** It used to produce
39
+ one pseudo-frame called "3 selected nodes" holding all of them, which
40
+ `design` then wrote as a single file under that name — a folder whose
41
+ whole point is one file per screen, with one file for three. The screens
42
+ were in there, but nothing said where one ended and the next began.
43
+
44
+ A selection of several frames now produces the same shape the whole-file
45
+ export does, so the split, the index and `figma_frame` treat them
46
+ identically. One frame selected is still one screen.
47
+
48
+ This is also the scoping the all-screens command lacks: a file with forty
49
+ frames in it need not be exported entire to build two of them. Select the
50
+ screens you want and export those.
51
+
52
+
3
53
  ## 2.6.0
4
54
 
5
55
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grampro/kit",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "description": "GramproKit \u2014 an agent-native UI runtime for React. Copy-in components you own, each publishing a contract that software can read.",
5
5
  "type": "module",
6
6
  "files": [
@@ -10,7 +10,7 @@
10
10
  ]
11
11
  },
12
12
  "source": {
13
- "libraryVersion": "2.6.0",
13
+ "libraryVersion": "2.6.1",
14
14
  "files": [
15
15
  "core/index.ts",
16
16
  "core/open.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.0",
11
+ "libraryVersion": "2.6.1",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/live.ts",
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.6.0",
12
+ "libraryVersion": "2.6.1",
13
13
  "files": [
14
14
  "core/identity.ts",
15
15
  "core/index.ts",
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.6.0",
12
+ "libraryVersion": "2.6.1",
13
13
  "files": [
14
14
  "core/count.ts",
15
15
  "core/index.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.0",
11
+ "libraryVersion": "2.6.1",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/trail.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.0",
11
+ "libraryVersion": "2.6.1",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/state.ts",
@@ -12,7 +12,7 @@
12
12
  ]
13
13
  },
14
14
  "source": {
15
- "libraryVersion": "2.6.0",
15
+ "libraryVersion": "2.6.1",
16
16
  "files": [
17
17
  "core/index.ts",
18
18
  "core/trend.ts",
@@ -10,7 +10,7 @@
10
10
  ]
11
11
  },
12
12
  "source": {
13
- "libraryVersion": "2.6.0",
13
+ "libraryVersion": "2.6.1",
14
14
  "files": [
15
15
  "core/group.ts",
16
16
  "core/index.ts",
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.6.0",
12
+ "libraryVersion": "2.6.1",
13
13
  "files": [
14
14
  "core/filter.ts",
15
15
  "core/index.ts",
@@ -14,7 +14,7 @@
14
14
  ]
15
15
  },
16
16
  "source": {
17
- "libraryVersion": "2.6.0",
17
+ "libraryVersion": "2.6.1",
18
18
  "files": [
19
19
  "agent/coerce.ts",
20
20
  "agent/contract.ts",
@@ -10,7 +10,7 @@
10
10
  ]
11
11
  },
12
12
  "source": {
13
- "libraryVersion": "2.6.0",
13
+ "libraryVersion": "2.6.1",
14
14
  "files": [
15
15
  "core/calendar.ts",
16
16
  "core/format.ts",
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.6.0",
12
+ "libraryVersion": "2.6.1",
13
13
  "files": [
14
14
  "core/api.ts",
15
15
  "core/dialog.ts",
@@ -13,7 +13,7 @@
13
13
  ]
14
14
  },
15
15
  "source": {
16
- "libraryVersion": "2.6.0",
16
+ "libraryVersion": "2.6.1",
17
17
  "files": [
18
18
  "core/format.ts",
19
19
  "core/index.ts",
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.6.0",
12
+ "libraryVersion": "2.6.1",
13
13
  "files": [
14
14
  "core/count.ts",
15
15
  "core/index.ts",
@@ -16,7 +16,7 @@
16
16
  ]
17
17
  },
18
18
  "source": {
19
- "libraryVersion": "2.6.0",
19
+ "libraryVersion": "2.6.1",
20
20
  "files": [
21
21
  "core/index.ts",
22
22
  "core/navigation.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.0",
11
+ "libraryVersion": "2.6.1",
12
12
  "files": [
13
13
  "core/dismiss.ts",
14
14
  "core/index.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.0",
11
+ "libraryVersion": "2.6.1",
12
12
  "files": [
13
13
  "core/format.ts",
14
14
  "core/index.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.0",
11
+ "libraryVersion": "2.6.1",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/types.ts",
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.6.0",
12
+ "libraryVersion": "2.6.1",
13
13
  "files": [
14
14
  "core/index.ts",
15
15
  "core/progress.ts",
@@ -10,7 +10,7 @@
10
10
  ]
11
11
  },
12
12
  "source": {
13
- "libraryVersion": "2.6.0",
13
+ "libraryVersion": "2.6.1",
14
14
  "files": [
15
15
  "core/group.ts",
16
16
  "core/index.ts",
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.6.0",
12
+ "libraryVersion": "2.6.1",
13
13
  "files": [
14
14
  "core/index.ts",
15
15
  "core/lines.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.0",
11
+ "libraryVersion": "2.6.1",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/types.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.0",
11
+ "libraryVersion": "2.6.1",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/toggle.ts",
@@ -12,7 +12,7 @@
12
12
  ]
13
13
  },
14
14
  "source": {
15
- "libraryVersion": "2.6.0",
15
+ "libraryVersion": "2.6.1",
16
16
  "files": [
17
17
  "core/index.ts",
18
18
  "core/navigation.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.0",
11
+ "libraryVersion": "2.6.1",
12
12
  "files": [
13
13
  "core/count.ts",
14
14
  "core/index.ts",
@@ -11,7 +11,7 @@
11
11
  ]
12
12
  },
13
13
  "source": {
14
- "libraryVersion": "2.6.0",
14
+ "libraryVersion": "2.6.1",
15
15
  "files": [
16
16
  "core/api.ts",
17
17
  "core/index.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.0",
11
+ "libraryVersion": "2.6.1",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/types.ts",
@@ -265,6 +265,123 @@ function repeatedNames(node, counts) {
265
265
 
266
266
  // ------------------------------------------------------------------- tool
267
267
 
268
+ /*
269
+ * Repetition is the point, and sending it in full is the waste.
270
+ *
271
+ * A table of eight rows exports as eight siblings of identical shape whose
272
+ * text differs. The agent needs the shape once and the values eight times;
273
+ * on a real screen the other seven copies were 48% of the payload.
274
+ */
275
+
276
+ /**
277
+ * A node's structure: names, types, nesting and layout.
278
+ *
279
+ * Style is deliberately not part of it. Table rows are zebra-striped, so
280
+ * consecutive rows differ by a background colour and nothing else — keying
281
+ * on style meant every pair looked distinct and no run was ever found. The
282
+ * variation is reported instead of being treated as a different shape.
283
+ */
284
+ function shapeKey(node) {
285
+ return JSON.stringify([
286
+ node.name,
287
+ node.type,
288
+ node.layout,
289
+ (node.children || []).map(shapeKey),
290
+ ]);
291
+ }
292
+
293
+ /** The fills down a subtree, so a collapsed run can say how they alternate. */
294
+ function fillsOf(node, into = []) {
295
+ if (node.style && node.style.fill) into.push(node.style.fill);
296
+ for (const child of node.children || []) fillsOf(child, into);
297
+ return into;
298
+ }
299
+
300
+ /** Every string in a subtree, in order: what one row of a table actually says. */
301
+ function textsOf(node, into = []) {
302
+ if (node.text) into.push(node.text);
303
+ for (const child of node.children || []) textsOf(child, into);
304
+ return into;
305
+ }
306
+
307
+ /** How many levels deep a subtree goes. */
308
+ const depthOf = (node) =>
309
+ 1 + Math.max(0, ...(node.children || []).map(depthOf));
310
+
311
+ /**
312
+ * Collapse runs of same-shaped siblings, and trim to `limit` levels.
313
+ *
314
+ * Only runs of three or more are collapsed: two of a thing is a pair, not a
315
+ * list, and the saving would not pay for the indirection. A run that is only
316
+ * one level deep is left alone too — there is nothing to save in a row of
317
+ * labels, and collapsing it would cost the agent the order they appear in.
318
+ */
319
+ function condenseTree(nodes, limit, level = 1) {
320
+ const out = [];
321
+ let i = 0;
322
+
323
+ while (i < nodes.length) {
324
+ const node = nodes[i];
325
+ const key = shapeKey(node);
326
+ let run = 1;
327
+ while (i + run < nodes.length && shapeKey(nodes[i + run]) === key) run += 1;
328
+
329
+ const worthIt = run >= 3 && depthOf(node) > 1;
330
+ const trimmed = trim(node, limit, level);
331
+
332
+ if (!worthIt) {
333
+ for (let n = 0; n < run; n += 1) out.push(trim(nodes[i + n], limit, level));
334
+ i += run;
335
+ continue;
336
+ }
337
+
338
+ /*
339
+ * The first in full, then the rest as what they say. `repeats` is the
340
+ * count including this one, so an agent reading only this node knows how
341
+ * many rows to render.
342
+ */
343
+ const rest = [];
344
+ for (let n = 1; n < run; n += 1) rest.push(textsOf(nodes[i + n]));
345
+
346
+ /*
347
+ * Fills are reported only where they actually differ across the run.
348
+ * Where they do it is nearly always zebra striping, and two colours
349
+ * said once is the whole of it.
350
+ */
351
+ const fills = [];
352
+ for (let n = 0; n < run; n += 1) fills.push(fillsOf(nodes[i + n]).join(","));
353
+ const varies = new Set(fills).size > 1;
354
+
355
+ out.push({
356
+ ...trimmed,
357
+ repeats: run,
358
+ repeatsNote:
359
+ `${run} siblings share this structure. The first is given in full; ` +
360
+ "`repeatedText` holds what each of the others says, in the same order " +
361
+ "as the text in this one." +
362
+ (varies ? " `repeatedFills` gives each one's colours, which alternate." : ""),
363
+ repeatedText: rest,
364
+ ...(varies ? { repeatedFills: fills.map((f) => f.split(",")) } : {}),
365
+ });
366
+ i += run;
367
+ }
368
+
369
+ return out;
370
+ }
371
+
372
+ /** Drop everything below `limit` levels, saying what was dropped. */
373
+ function trim(node, limit, level) {
374
+ if (!node.children || node.children.length === 0) return node;
375
+ if (level >= limit) {
376
+ const inside = node.children.reduce(
377
+ (n, child) => n + 1 + (child.children ? child.children.length : 0),
378
+ 0,
379
+ );
380
+ const { children, ...rest } = node;
381
+ return { ...rest, truncated: `${inside}+ nodes below this, not shown` };
382
+ }
383
+ return { ...node, children: condenseTree(node.children, limit, level + 1) };
384
+ }
268
385
  /*
269
386
  * A selection exported by the plugin, which has already done the condensing
270
387
  * inside Figma. The work left is the part that needs no file access: giving
@@ -273,7 +390,7 @@ function repeatedNames(node, counts) {
273
390
  * This is the path for anyone who would rather not hold an API token, and
274
391
  * for files the token cannot reach.
275
392
  */
276
- function fromPluginExport(design) {
393
+ function fromPluginExport(design, depth) {
277
394
  let parsed;
278
395
  try {
279
396
  parsed = typeof design === "string" ? JSON.parse(design) : design;
@@ -311,6 +428,14 @@ function fromPluginExport(design) {
311
428
  };
312
429
  for (const node of parsed.nodes) findInstances(node);
313
430
 
431
+ /*
432
+ * `depth` was offered on this tool and only ever honoured for a URL. A
433
+ * caller who read the schema and lowered it to cut a large payload got
434
+ * the large payload back.
435
+ */
436
+ const limit = Math.min(Number(depth) || MAX_DEPTH, MAX_DEPTH);
437
+ const tree = condenseTree(parsed.nodes, limit);
438
+
314
439
  return {
315
440
  source: "figma plugin export",
316
441
  frame: parsed.frame,
@@ -322,7 +447,7 @@ function fromPluginExport(design) {
322
447
  .map(([name, n]) => ({ name, times: n })),
323
448
  builtFromLibrary: instances.length > 0,
324
449
  componentsNamed: [...new Set(instances)],
325
- tree: parsed.nodes,
450
+ tree,
326
451
  /*
327
452
  * The same closing instruction the URL route gives. Without it an agent
328
453
  * arriving by the plugin — now the route the documentation leads with —
@@ -349,7 +474,7 @@ function fromPluginExport(design) {
349
474
  * has already condensed it; there is nothing further to drop that the
350
475
  * screen does not need.
351
476
  */
352
- function fromFile(file) {
477
+ function fromFile(file, depth) {
353
478
  let resolved;
354
479
  try {
355
480
  resolved = path.resolve(file);
@@ -371,14 +496,14 @@ function fromFile(file) {
371
496
  }
372
497
  // A byte-order mark survives a round trip through some editors and is not
373
498
  // valid JSON, so it goes before parsing rather than after a confusing throw.
374
- return fromPluginExport(body.replace(/^\uFEFF/, ""));
499
+ return fromPluginExport(body.replace(/^\uFEFF/, ""), depth);
375
500
  }
376
501
 
377
502
  async function figmaFrame({ url, design, designFile, frame, depth }) {
378
503
  // Both export routes need no token, so they are offered before one is
379
504
  // demanded. The file comes first: it is the one that scales.
380
- if (designFile) return fromFile(designFile);
381
- if (design) return fromPluginExport(design);
505
+ if (designFile) return fromFile(designFile, depth);
506
+ if (design) return fromPluginExport(design, depth);
382
507
 
383
508
  const token = figmaToken();
384
509
  if (!token) {