@grampro/kit 2.6.2 → 2.7.0

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 (94) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +5 -4
  2. package/.gbs/skills/gbs-components/references/install.md +17 -14
  3. package/CHANGELOG.md +201 -18
  4. package/README.md +24 -6
  5. package/index.cjs +59 -59
  6. package/package.json +2 -2
  7. package/schema/passport-overlay-v1.schema.json +67 -0
  8. package/source/components/accordion/passport.json +45 -8
  9. package/source/components/accordion/passport.manual.json +46 -0
  10. package/source/components/alert/passport.json +41 -8
  11. package/source/components/alert/passport.manual.json +42 -0
  12. package/source/components/avatar/passport.json +40 -7
  13. package/source/components/avatar/passport.manual.json +43 -0
  14. package/source/components/badge/passport.json +43 -7
  15. package/source/components/badge/passport.manual.json +46 -0
  16. package/source/components/breadcrumb/passport.json +36 -7
  17. package/source/components/breadcrumb/passport.manual.json +39 -0
  18. package/source/components/button/passport.json +1 -1
  19. package/source/components/button/passport.manual.json +1 -1
  20. package/source/components/card/passport.json +45 -8
  21. package/source/components/card/passport.manual.json +47 -0
  22. package/source/components/checkbox/core/agent.ts +33 -0
  23. package/source/components/checkbox/core/index.ts +2 -0
  24. package/source/components/checkbox/passport.json +83 -10
  25. package/source/components/checkbox/passport.manual.json +82 -0
  26. package/source/components/combobox/core/agent.ts +38 -0
  27. package/source/components/combobox/core/index.ts +2 -0
  28. package/source/components/combobox/passport.json +103 -11
  29. package/source/components/combobox/passport.manual.json +101 -0
  30. package/source/components/combobox/react/props.ts +14 -0
  31. package/source/components/combobox/react/useCombobox.ts +7 -1
  32. package/source/components/data-grid/export/xlsx.ts +9 -2
  33. package/source/components/data-grid/passport.json +2 -2
  34. package/source/components/data-grid/passport.manual.json +1 -1
  35. package/source/components/date-picker/core/agent.ts +42 -0
  36. package/source/components/date-picker/core/index.ts +2 -0
  37. package/source/components/date-picker/passport.json +102 -11
  38. package/source/components/date-picker/passport.manual.json +100 -0
  39. package/source/components/date-picker/react/DatePicker.tsx +6 -0
  40. package/source/components/date-picker/react/DateRangePicker.tsx +4 -0
  41. package/source/components/date-picker/react/props.ts +17 -0
  42. package/source/components/dialog/passport.json +54 -9
  43. package/source/components/dialog/passport.manual.json +54 -0
  44. package/source/components/file-uploader/passport.json +65 -10
  45. package/source/components/file-uploader/passport.manual.json +64 -0
  46. package/source/components/input/core/agent.ts +36 -0
  47. package/source/components/input/core/index.ts +2 -0
  48. package/source/components/input/passport.json +89 -12
  49. package/source/components/input/passport.manual.json +86 -0
  50. package/source/components/menu/passport.json +64 -9
  51. package/source/components/menu/passport.manual.json +64 -0
  52. package/source/components/modal/passport.json +52 -9
  53. package/source/components/modal/passport.manual.json +52 -0
  54. package/source/components/number-input/core/agent.ts +38 -0
  55. package/source/components/number-input/core/index.ts +2 -0
  56. package/source/components/number-input/passport.json +92 -11
  57. package/source/components/number-input/passport.manual.json +90 -0
  58. package/source/components/popover/passport.json +47 -8
  59. package/source/components/popover/passport.manual.json +48 -0
  60. package/source/components/progress/passport.json +37 -8
  61. package/source/components/progress/passport.manual.json +39 -0
  62. package/source/components/radio-group/core/agent.ts +34 -0
  63. package/source/components/radio-group/core/index.ts +2 -0
  64. package/source/components/radio-group/passport.json +77 -10
  65. package/source/components/radio-group/passport.manual.json +76 -0
  66. package/source/components/shared/core/agent/field-agent.ts +101 -0
  67. package/source/components/shared/core/agent/field-executors.ts +137 -0
  68. package/source/components/shared/core/agent/field.ts +649 -0
  69. package/source/components/shared/core/agent/index.ts +30 -0
  70. package/source/components/shared/core/agent/write.ts +58 -0
  71. package/source/components/shared/react/useFieldAgent.ts +88 -0
  72. package/source/components/skeleton/passport.json +37 -6
  73. package/source/components/skeleton/passport.manual.json +39 -0
  74. package/source/components/spinner/passport.json +34 -7
  75. package/source/components/spinner/passport.manual.json +37 -0
  76. package/source/components/switch/core/agent.ts +31 -0
  77. package/source/components/switch/core/index.ts +2 -0
  78. package/source/components/switch/passport.json +68 -10
  79. package/source/components/switch/passport.manual.json +67 -0
  80. package/source/components/tabs/passport.json +50 -9
  81. package/source/components/tabs/passport.manual.json +50 -0
  82. package/source/components/textarea/core/agent.ts +34 -0
  83. package/source/components/textarea/core/index.ts +2 -0
  84. package/source/components/textarea/passport.json +83 -10
  85. package/source/components/textarea/passport.manual.json +82 -0
  86. package/source/components/toaster/passport.json +41 -8
  87. package/source/components/toaster/passport.manual.json +42 -0
  88. package/source/components/tooltip/passport.json +39 -6
  89. package/source/components/tooltip/passport.manual.json +41 -0
  90. package/tools/mcp/README.md +9 -1
  91. package/tools/mcp/design.cjs +6 -1
  92. package/tools/mcp/figma.cjs +49 -9
  93. package/tools/mcp/install.cjs +6 -1
  94. package/tools/mcp/server.cjs +14 -2
@@ -145,7 +145,7 @@ function layoutOf(node) {
145
145
  * Figma wraps nearly every string in its own frame, and passing that
146
146
  * structure through verbatim buries the design in scaffolding.
147
147
  */
148
- function condense(node, depth) {
148
+ function condense(node, depth, names = { components: {}, componentSets: {} }) {
149
149
  const out = { name: node.name, type: node.type };
150
150
 
151
151
  const layout = layoutOf(node);
@@ -158,8 +158,24 @@ function condense(node, depth) {
158
158
  }
159
159
  }
160
160
 
161
- // An instance is a lookup, when the design was built from a library.
162
- if (node.type === "INSTANCE" && node.componentId) out.instanceOf = node.componentId;
161
+ /*
162
+ * An instance is a lookup, when the design was built from a library.
163
+ *
164
+ * `components` and `componentSets` come back in the same response and
165
+ * name every id in it. Reporting the id alone left an agent holding
166
+ * `1:234`, which tells it nothing; the name is the whole point.
167
+ *
168
+ * The set's name is preferred where there is one. A variant is called
169
+ * `Size=md, Variant=primary` and its set is called `Button`, and Button
170
+ * is what the agent is going to write.
171
+ */
172
+ if (node.type === "INSTANCE" && node.componentId) {
173
+ const master = names.components[node.componentId];
174
+ const set = master && master.componentSetId && names.componentSets[master.componentSetId];
175
+ const resolved = (set && set.name) || (master && master.name);
176
+ if (resolved) out.component = resolved;
177
+ else out.instanceOf = node.componentId;
178
+ }
163
179
 
164
180
  const style = {};
165
181
  const fill = solidFill(node);
@@ -173,7 +189,7 @@ function condense(node, depth) {
173
189
 
174
190
  const kids = (node.children || []).filter((c) => c.visible !== false);
175
191
  if (kids.length && depth < MAX_DEPTH) {
176
- const children = kids.map((c) => condense(c, depth + 1));
192
+ const children = kids.map((c) => condense(c, depth + 1, names));
177
193
  // A frame that exists only to hold one text node is noise.
178
194
  if (children.length === 1 && children[0].text && !children[0].children && !out.text) {
179
195
  out.text = children[0].text;
@@ -395,7 +411,12 @@ function fromPluginExport(design, depth) {
395
411
  try {
396
412
  parsed = typeof design === "string" ? JSON.parse(design) : design;
397
413
  } catch (error) {
398
- return { error: `That is not JSON: ${error.message}` };
414
+ // The parse error carries a slice of the input; do not pass it on.
415
+ return {
416
+ error:
417
+ "That is not JSON. Expected the output of the Figma plugin's " +
418
+ "'Export selection for an agent', or a path to a file it saved.",
419
+ };
399
420
  }
400
421
  if (!parsed || !Array.isArray(parsed.nodes)) {
401
422
  return {
@@ -589,11 +610,29 @@ async function figmaFrame({ url, design, designFile, frame, depth }) {
589
610
  repeatedNames(target, names);
590
611
 
591
612
  const limit = Math.min(Number(depth) || MAX_DEPTH, MAX_DEPTH);
592
- const tree = condense(target, MAX_DEPTH - limit);
613
+ /*
614
+ * Figma puts the name maps beside the nodes for a /nodes request and at
615
+ * the top level for a whole file, so both shapes are gathered.
616
+ */
617
+ const library = { components: {}, componentSets: {} };
618
+ for (const source of [file, ...Object.values(file.nodes || {})]) {
619
+ if (!source) continue;
620
+ Object.assign(library.components, source.components || {});
621
+ Object.assign(library.componentSets, source.componentSets || {});
622
+ }
623
+
624
+ const tree = condense(target, MAX_DEPTH - limit, library);
593
625
 
626
+ /*
627
+ * Only resolved names are collected. An instance whose master is not in
628
+ * the response keeps its id, and listing the node's own name beside real
629
+ * component names would read as though it were one.
630
+ */
594
631
  const instances = [];
632
+ let unresolved = 0;
595
633
  const findInstances = (n) => {
596
- if (n.instanceOf) instances.push(n.name);
634
+ if (n.component) instances.push(n.component);
635
+ else if (n.instanceOf) unresolved += 1;
597
636
  for (const c of n.children || []) findInstances(c);
598
637
  };
599
638
  findInstances(tree);
@@ -609,10 +648,11 @@ async function figmaFrame({ url, design, designFile, frame, depth }) {
609
648
  .sort((a, b) => b[1] - a[1])
610
649
  .slice(0, 20)
611
650
  .map(([name, n]) => ({ name, times: n })),
612
- builtFromLibrary: instances.length > 0,
651
+ builtFromLibrary: instances.length > 0 || unresolved > 0,
613
652
  tree,
614
653
  howToUse: instances.length
615
- ? "This design uses component instances; `instanceOf` names the master for each."
654
+ ? `This design is built from components: ${[...new Set(instances)].slice(0, 12).join(", ")}`
655
+ + ". `component` names the master on each instance — use it rather than inferring."
616
656
  : "This design was drawn without the component library, so nothing names a component directly. Use the region names and structure to decide which catalogue component each part is, then call get_component before writing it.",
617
657
  };
618
658
  }
@@ -86,7 +86,12 @@ function installJson(file, cwd) {
86
86
  try {
87
87
  config = JSON.parse(existing);
88
88
  } catch (error) {
89
- return { file, status: "failed", detail: `${file} is not valid JSON (${error.message}). Left alone.` };
89
+ // Naming the file is the useful part; the parse error would quote it.
90
+ return {
91
+ file,
92
+ status: "failed",
93
+ detail: `${file} is not valid JSON, so it has been left alone.`,
94
+ };
90
95
  }
91
96
  }
92
97
 
@@ -161,6 +161,18 @@ function getComponent(name) {
161
161
  states: p.states || [],
162
162
  composition: p.composition || {},
163
163
  accessibility: p.accessibility || {},
164
+ /*
165
+ * The authored half of the passport. `safeMutations` is the one an agent
166
+ * acts on directly — it is the difference between "you may restyle this"
167
+ * and "you are about to answer a form on the user's behalf" — so leaving
168
+ * it out of the tool meant authoring it and then never telling anyone.
169
+ *
170
+ * `operations` is empty for every component that is not agent-operable
171
+ * yet, and an empty object is the honest answer to "what can you drive".
172
+ */
173
+ safeMutations: p.safeMutations || {},
174
+ intentDomains: p.intentDomains || [],
175
+ operations: p.operations || {},
164
176
  };
165
177
  }
166
178
 
@@ -371,7 +383,7 @@ const TOOLS = [
371
383
  {
372
384
  name: "figma_frame",
373
385
  description:
374
- "BETA. Read a Figma design: from a URL (needs FIGMA_TOKEN), from a file the kit's Figma plugin saved (`designFile`, no token, and the one to use for a whole screen), or from that export pasted in (`design`). Called with just a URL it lists the frames in the file; called with `frame` it returns that screen's structure, text, the repeated regions that indicate lists and tables, and the theme variables its colours and radii map onto.",
386
+ "BETA. Read a Figma design. Start with a URL (`url`, needs FIGMA_TOKEN): it is one step, and it resolves the names of any component instances in the frame. A file the kit's Figma plugin saved (`designFile`) needs no token and carries every screen at once, which is what to use for a whole product or when a token is unavailable; that export pasted in (`design`) works for a small frame. Called with just a URL it lists the frames in the file; called with `frame` it returns that screen's structure, text, the repeated regions that indicate lists and tables, and the theme variables its colours and radii map onto.",
375
387
  inputSchema: {
376
388
  type: "object",
377
389
  properties: {
@@ -384,7 +396,7 @@ const TOOLS = [
384
396
  designFile: {
385
397
  type: "string",
386
398
  description:
387
- "Path to a .json saved by the plugin's 'Save .json' button. Preferred over `design`: the file is read here, so the raw export never passes through the conversation.",
399
+ "Path to a .json saved by the plugin's 'Save .json' button, or to one screen of a folder `grampro-kit design` wrote. Preferred over `design`: the file is read here, so the raw export never passes through the conversation.",
388
400
  },
389
401
  frame: { type: "string", description: "Which frame to read, from the listing." },
390
402
  depth: { type: "number", description: "How deep to descend. Lower it if the payload is large." },