@grampro/kit 2.6.0 → 2.6.2
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/.gbs/skills/gbs-components/SKILL.md +16 -0
- package/CHANGELOG.md +95 -0
- package/README.md +42 -0
- package/package.json +1 -1
- package/source/components/accordion/passport.json +1 -1
- package/source/components/alert/passport.json +1 -1
- package/source/components/avatar/passport.json +1 -1
- package/source/components/badge/passport.json +1 -1
- package/source/components/breadcrumb/passport.json +1 -1
- package/source/components/button/passport.json +1 -1
- package/source/components/card/passport.json +1 -1
- package/source/components/checkbox/passport.json +1 -1
- package/source/components/combobox/passport.json +1 -1
- package/source/components/data-grid/passport.json +1 -1
- package/source/components/date-picker/passport.json +1 -1
- package/source/components/dialog/passport.json +1 -1
- package/source/components/file-uploader/passport.json +1 -1
- package/source/components/input/passport.json +1 -1
- package/source/components/menu/passport.json +1 -1
- package/source/components/modal/passport.json +1 -1
- package/source/components/number-input/passport.json +1 -1
- package/source/components/popover/passport.json +1 -1
- package/source/components/progress/passport.json +1 -1
- package/source/components/radio-group/passport.json +1 -1
- package/source/components/skeleton/passport.json +1 -1
- package/source/components/spinner/passport.json +1 -1
- package/source/components/switch/passport.json +1 -1
- package/source/components/tabs/passport.json +1 -1
- package/source/components/textarea/passport.json +1 -1
- package/source/components/toaster/passport.json +1 -1
- package/source/components/tooltip/passport.json +1 -1
- package/tools/mcp/README.md +2 -1
- package/tools/mcp/figma.cjs +131 -6
|
@@ -6,6 +6,22 @@ autoAttach: ["src/**/*.tsx", "src/**/*.jsx", "app/**/*.tsx", "components/**/*.ts
|
|
|
6
6
|
|
|
7
7
|
# GBS components (2.0 beta)
|
|
8
8
|
|
|
9
|
+
## If the MCP server is connected
|
|
10
|
+
|
|
11
|
+
This file is a summary. The `grampro` MCP server answers from passports
|
|
12
|
+
generated from the component source, so where the two differ it is right
|
|
13
|
+
and this is stale. When its tools are available:
|
|
14
|
+
|
|
15
|
+
- **`get_component <Name>`** before writing a component — every prop with
|
|
16
|
+
its type, the values a union accepts, which props are required, handler
|
|
17
|
+
signatures. Do not write a prop you have not read there.
|
|
18
|
+
- **`validate_usage`** on the JSX before reporting back. It catches props
|
|
19
|
+
invented from memory, which is the one failure a passport cannot prevent.
|
|
20
|
+
- **`figma_frame`** to read a design, from a `designFile` path or a Figma
|
|
21
|
+
URL, when building from one.
|
|
22
|
+
|
|
23
|
+
Neither needs the other installed. Without the server this file is what
|
|
24
|
+
you have, and everything below still applies.
|
|
9
25
|
## Install before you import
|
|
10
26
|
|
|
11
27
|
Not an npm dependency — the CLI **copies source into the repo**:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,100 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.6.2
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
**The agent skill now knows the MCP server exists.** It described the
|
|
8
|
+
component library and never mentioned the server, so an agent holding both
|
|
9
|
+
had nothing to tell it which to trust. The skill is a summary; the server
|
|
10
|
+
answers from passports generated from the source, so where they differ the
|
|
11
|
+
server is right and the skill is stale — and it now says so, along with
|
|
12
|
+
calling `get_component` before writing a component and `validate_usage`
|
|
13
|
+
before reporting back.
|
|
14
|
+
|
|
15
|
+
The only previous mention of "MCP" in the skill was WebMCP, the DataGrid's
|
|
16
|
+
browser-agent surface, which is a different thing entirely.
|
|
17
|
+
|
|
18
|
+
**Neither needs the other, and the docs now say so.** The server carries
|
|
19
|
+
its own copy of the guidance inside the package, so `get_guidance` answers
|
|
20
|
+
in a project with no `.gbs/` folder at all. What they do together is the
|
|
21
|
+
useful part: the skill is always in an agent's context and the server is
|
|
22
|
+
not, so the skill is what tells an agent to ask and the server is what it
|
|
23
|
+
asks.
|
|
24
|
+
|
|
25
|
+
The two pages now read in the order they are used — Theming, Agent skill,
|
|
26
|
+
MCP server.
|
|
27
|
+
|
|
28
|
+
**The text LLMs read was teaching a deprecated command.** `llms.txt` has a
|
|
29
|
+
hand-written preamble, so it lagged behind the CLI: it told every model
|
|
30
|
+
that reached the site to run `npx @grampro/kit -a <Component>`, a spelling
|
|
31
|
+
the CLI itself marks "Deprecated. Use: add". It now says `add`, and names
|
|
32
|
+
the Figma plugin, the whole-design export and `designFile`, none of which
|
|
33
|
+
it mentioned.
|
|
34
|
+
|
|
35
|
+
**The plugin's install link was invisible to every LLM.** It is rendered by
|
|
36
|
+
a component, and the Markdown conversion passed that through verbatim — so
|
|
37
|
+
`llms-full.txt` and every `.md` page carried a dangling `<FigmaPlugin />`
|
|
38
|
+
tag where the URL should have been, and the plugin's address appeared
|
|
39
|
+
nowhere a model could read it. The conversion emits a Markdown link now,
|
|
40
|
+
and the URL lives in one place so the button and the converter cannot
|
|
41
|
+
disagree about it.
|
|
42
|
+
|
|
43
|
+
**Every command is listed in one place.** `list` and `passport` appeared
|
|
44
|
+
in no user-facing documentation at all, and the rest were scattered across
|
|
45
|
+
three pages, so there was nowhere to look to find out what the CLI does.
|
|
46
|
+
The README now carries the lot in a table.
|
|
47
|
+
|
|
48
|
+
## 2.6.1
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
**A screen costs about half what it did to read.** Measured on a real
|
|
53
|
+
purchase-orders design: 42.9 KB to 23.9 KB, roughly 13,300 tokens to
|
|
54
|
+
7,400.
|
|
55
|
+
|
|
56
|
+
Eight table rows exported as eight sibling subtrees of identical shape
|
|
57
|
+
whose text differed — 48% of that screen was the same row said seven
|
|
58
|
+
more times. The first is now given in full, because the agent builds from
|
|
59
|
+
its shape, and the rest become `repeatedText`: one array of strings per
|
|
60
|
+
row, which is closer to what a DataGrid's `data` prop wants than the
|
|
61
|
+
subtrees were.
|
|
62
|
+
|
|
63
|
+
Runs are matched on structure rather than appearance. Keyed on style, the
|
|
64
|
+
rows never matched at all: a striped table alternates its background, so
|
|
65
|
+
every consecutive pair looked like a different shape and nothing
|
|
66
|
+
collapsed. Where the fills do vary across a run they come back as
|
|
67
|
+
`repeatedFills`, which is how the striping and the per-status badge
|
|
68
|
+
colours stay legible.
|
|
69
|
+
|
|
70
|
+
Only runs of three or more collapse, and only where there is depth to
|
|
71
|
+
save: two of a thing is a pair, not a list, and a row of labels has
|
|
72
|
+
nothing inside it worth folding.
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
**`depth` is honoured on the plugin and file routes.** The tool has always
|
|
77
|
+
offered it — "Lower it if the payload is large" — and only ever applied
|
|
78
|
+
it to a design fetched from a URL. A caller who read the schema and
|
|
79
|
+
lowered it to cut a large payload got the large payload back. The same
|
|
80
|
+
screen at `depth: 3` is now 3.5 KB, and what was dropped says so rather
|
|
81
|
+
than going quiet.
|
|
82
|
+
|
|
83
|
+
**Selecting several frames exports several screens.** It used to produce
|
|
84
|
+
one pseudo-frame called "3 selected nodes" holding all of them, which
|
|
85
|
+
`design` then wrote as a single file under that name — a folder whose
|
|
86
|
+
whole point is one file per screen, with one file for three. The screens
|
|
87
|
+
were in there, but nothing said where one ended and the next began.
|
|
88
|
+
|
|
89
|
+
A selection of several frames now produces the same shape the whole-file
|
|
90
|
+
export does, so the split, the index and `figma_frame` treat them
|
|
91
|
+
identically. One frame selected is still one screen.
|
|
92
|
+
|
|
93
|
+
This is also the scoping the all-screens command lacks: a file with forty
|
|
94
|
+
frames in it need not be exported entire to build two of them. Select the
|
|
95
|
+
screens you want and export those.
|
|
96
|
+
|
|
97
|
+
|
|
3
98
|
## 2.6.0
|
|
4
99
|
|
|
5
100
|
### Added
|
package/README.md
CHANGED
|
@@ -80,6 +80,21 @@ Each component also has a **framework-free core** — `createDialogStore()`,
|
|
|
80
80
|
`createGridEngine()` — with no React imports, usable on a server. That part is
|
|
81
81
|
genuinely headless, and the component READMEs document it under "Headless use".
|
|
82
82
|
|
|
83
|
+
## Commands
|
|
84
|
+
|
|
85
|
+
| | |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `add <Component>...` | Copy components into the project. `--dest` chooses where; `--legacy` reaches the 1.x set. |
|
|
88
|
+
| `list` | What is available to add. |
|
|
89
|
+
| `skill` | Install the agent skill. `--for claude,codex,antigravity` narrows it. |
|
|
90
|
+
| `mcp` | Run the MCP server. Agents start this themselves; you rarely type it. |
|
|
91
|
+
| `mcp install` | Write the server's configuration for Claude Code, Codex and Cursor. |
|
|
92
|
+
| `secret set figma` | Store a Figma token in the OS credential store. Also `get`, `list`, `remove`. |
|
|
93
|
+
| `design <file>` | Split a whole-design Figma export into a folder of screens. |
|
|
94
|
+
| `passport --check` | Fail if a passport has drifted from its source. For CI, and after forking a component. |
|
|
95
|
+
|
|
96
|
+
`npx @grampro/kit <command>`, or `grampro-kit` once it is a dev dependency.
|
|
97
|
+
|
|
83
98
|
## Documentation
|
|
84
99
|
|
|
85
100
|
[kit.gramproindia.com](https://kit.gramproindia.com) — usage and props per
|
|
@@ -122,6 +137,33 @@ takes. Full walkthrough, including a prompt to paste:
|
|
|
122
137
|
[kit.gramproindia.com/mcp-server](https://kit.gramproindia.com/mcp-server).
|
|
123
138
|
Details in [`tools/mcp/README.md`](tools/mcp/README.md).
|
|
124
139
|
|
|
140
|
+
## Figma plugin
|
|
141
|
+
|
|
142
|
+
[**Grampro Kit on the Figma Community**](https://www.figma.com/community/plugin/1692828324805361551)
|
|
143
|
+
|
|
144
|
+
Two jobs. It generates the whole design system inside Figma — 53 variables
|
|
145
|
+
across light and dark, 13 text styles, 38 component sets — and it exports
|
|
146
|
+
screens as JSON a coding agent can build from.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# in Figma: Grampro Kit → Export all screens for an agent → Save .json
|
|
150
|
+
npx @grampro/kit design ./design.json
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
design/
|
|
155
|
+
purchase-orders.json
|
|
156
|
+
suppliers.json
|
|
157
|
+
index.json every screen, the shared theme, the regions that repeat
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The agent reads `index.json` to plan, then builds one screen at a time
|
|
161
|
+
against `figma_frame`, `get_component` and `validate_usage`. It needs no
|
|
162
|
+
Figma API token and makes no network requests of any kind.
|
|
163
|
+
|
|
164
|
+
Source is in [`figma-plugin/`](figma-plugin/); the published listing is
|
|
165
|
+
what to install.
|
|
166
|
+
|
|
125
167
|
## Agent skill
|
|
126
168
|
|
|
127
169
|
Install the skill once per project so a coding agent uses these components
|
package/package.json
CHANGED
package/tools/mcp/README.md
CHANGED
|
@@ -127,7 +127,8 @@ invention, and this catches it:
|
|
|
127
127
|
|
|
128
128
|
## A whole design at once
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
Install [Grampro Kit from the Figma Community](https://www.figma.com/community/plugin/1692828324805361551). Then
|
|
131
|
+
**Grampro Kit → Export all screens for an agent** walks every
|
|
131
132
|
page and exports each top-level frame, with one set of theme values read
|
|
132
133
|
across the whole design rather than guessed per frame. Save it, then:
|
|
133
134
|
|
package/tools/mcp/figma.cjs
CHANGED
|
@@ -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
|
|
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) {
|