@grampro/kit 2.4.4 → 2.6.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.
- package/CHANGELOG.md +257 -0
- package/README.md +25 -5
- package/index.cjs +70 -4
- package/package.json +1 -1
- package/source/components/accordion/passport.json +11 -7
- package/source/components/accordion/react/Accordion.tsx +5 -0
- package/source/components/alert/passport.json +3 -3
- package/source/components/avatar/passport.json +8 -6
- package/source/components/avatar/react/Avatar.tsx +4 -0
- package/source/components/badge/passport.json +8 -6
- package/source/components/badge/react/Badge.tsx +1 -0
- package/source/components/badge/react/Tag.tsx +1 -0
- package/source/components/breadcrumb/passport.json +8 -6
- package/source/components/breadcrumb/react/Breadcrumb.tsx +1 -0
- package/source/components/button/passport.json +8 -6
- package/source/components/button/react/Button.tsx +1 -0
- package/source/components/card/passport.json +17 -9
- package/source/components/card/react/Card.tsx +5 -0
- package/source/components/card/react/Stat.tsx +5 -0
- package/source/components/checkbox/passport.json +32 -14
- package/source/components/checkbox/react/Checkbox.tsx +17 -0
- package/source/components/checkbox/react/CheckboxGroup.tsx +6 -0
- package/source/components/combobox/passport.json +36 -16
- package/source/components/combobox/react/Select.tsx +6 -1
- package/source/components/combobox/react/props.ts +23 -0
- package/source/components/data-grid/core/types.ts +53 -0
- package/source/components/data-grid/passport.json +82 -30
- package/source/components/data-grid/react/AskGrid.tsx +10 -0
- package/source/components/data-grid/react/DataGrid.tsx +16 -0
- package/source/components/date-picker/passport.json +30 -14
- package/source/components/date-picker/react/DatePicker.tsx +5 -0
- package/source/components/date-picker/react/props.ts +13 -1
- package/source/components/dialog/core/types.ts +14 -0
- package/source/components/dialog/passport.json +26 -12
- package/source/components/dialog/react/Dialog.tsx +4 -0
- package/source/components/file-uploader/passport.json +44 -18
- package/source/components/file-uploader/react/FileUploader.tsx +39 -0
- package/source/components/input/passport.json +14 -8
- package/source/components/input/react/Input.tsx +3 -0
- package/source/components/menu/passport.json +29 -13
- package/source/components/menu/react/Menu.tsx +7 -0
- package/source/components/menu/react/items.tsx +4 -0
- package/source/components/modal/passport.json +14 -8
- package/source/components/modal/react/Modal.tsx +6 -0
- package/source/components/number-input/passport.json +23 -11
- package/source/components/number-input/react/NumberInput.tsx +13 -0
- package/source/components/popover/passport.json +17 -9
- package/source/components/popover/react/Popover.tsx +7 -0
- package/source/components/progress/passport.json +3 -3
- package/source/components/radio-group/passport.json +32 -14
- package/source/components/radio-group/react/Radio.tsx +17 -0
- package/source/components/radio-group/react/RadioGroup.tsx +13 -0
- package/source/components/skeleton/passport.json +8 -6
- package/source/components/skeleton/react/Skeleton.tsx +5 -0
- package/source/components/spinner/passport.json +3 -3
- package/source/components/switch/passport.json +23 -11
- package/source/components/switch/react/Switch.tsx +14 -0
- package/source/components/tabs/passport.json +11 -7
- package/source/components/tabs/react/Tabs.tsx +2 -0
- package/source/components/textarea/passport.json +14 -8
- package/source/components/textarea/react/Textarea.tsx +3 -0
- package/source/components/toaster/passport.json +8 -6
- package/source/components/toaster/react/Toaster.tsx +4 -0
- package/source/components/tooltip/passport.json +3 -3
- package/tools/mcp/README.md +163 -36
- package/tools/mcp/design.cjs +154 -0
- package/tools/mcp/figma.cjs +136 -29
- package/tools/mcp/install.cjs +270 -0
- package/tools/mcp/secret-cli.cjs +174 -0
- package/tools/mcp/secret.cjs +289 -0
- package/tools/mcp/server.cjs +60 -16
- package/tools/passport/cli.cjs +8 -1
- package/tools/passport/extract.cjs +7 -3
- package/tools/passport/merge.cjs +24 -4
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
]
|
|
13
13
|
},
|
|
14
14
|
"source": {
|
|
15
|
-
"libraryVersion": "2.
|
|
15
|
+
"libraryVersion": "2.6.0",
|
|
16
16
|
"files": [
|
|
17
17
|
"core/index.ts",
|
|
18
18
|
"core/navigation.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"react/Tabs.tsx",
|
|
22
22
|
"react/context.ts"
|
|
23
23
|
],
|
|
24
|
-
"sourceHash": "sha256:
|
|
24
|
+
"sourceHash": "sha256:3d8e29857c59101e547f234c45735db68cc87ee143cc87d841372ec612e2c663"
|
|
25
25
|
},
|
|
26
26
|
"props": [
|
|
27
27
|
{
|
|
@@ -124,7 +124,9 @@
|
|
|
124
124
|
"type": "other",
|
|
125
125
|
"typeText": "ReactNode",
|
|
126
126
|
"required": false,
|
|
127
|
-
"origin": "derived"
|
|
127
|
+
"origin": "derived",
|
|
128
|
+
"description": "An icon before the tab's text.",
|
|
129
|
+
"descriptionOrigin": "jsdoc"
|
|
128
130
|
},
|
|
129
131
|
{
|
|
130
132
|
"name": "id",
|
|
@@ -155,7 +157,9 @@
|
|
|
155
157
|
"type": "function",
|
|
156
158
|
"signature": "((value: string) => void) | undefined",
|
|
157
159
|
"required": false,
|
|
158
|
-
"origin": "derived"
|
|
160
|
+
"origin": "derived",
|
|
161
|
+
"description": "The newly selected tab. Not fired when the same tab is clicked again.",
|
|
162
|
+
"descriptionOrigin": "jsdoc"
|
|
159
163
|
},
|
|
160
164
|
{
|
|
161
165
|
"name": "orientation",
|
|
@@ -305,9 +309,9 @@
|
|
|
305
309
|
"safeMutations": {},
|
|
306
310
|
"examples": [],
|
|
307
311
|
"coverage": {
|
|
308
|
-
"propsTotal":
|
|
309
|
-
"propsDescribed":
|
|
310
|
-
"describedPct":
|
|
312
|
+
"propsTotal": 11,
|
|
313
|
+
"propsDescribed": 9,
|
|
314
|
+
"describedPct": 82,
|
|
311
315
|
"authoredSections": [
|
|
312
316
|
"accessibility"
|
|
313
317
|
]
|
|
@@ -33,6 +33,7 @@ export interface TabsProps extends Omit<
|
|
|
33
33
|
value?: string;
|
|
34
34
|
/** Selected tab at first. Without either, the first enabled tab is selected. */
|
|
35
35
|
defaultValue?: string;
|
|
36
|
+
/** The newly selected tab. Not fired when the same tab is clicked again. */
|
|
36
37
|
onValueChange?(value: string): void;
|
|
37
38
|
/** Default `horizontal`. */
|
|
38
39
|
orientation?: TabsOrientation;
|
|
@@ -221,6 +222,7 @@ export interface TabProps extends Omit<
|
|
|
221
222
|
> {
|
|
222
223
|
/** Identifies the tab and its panel. */
|
|
223
224
|
value: string;
|
|
225
|
+
/** An icon before the tab's text. */
|
|
224
226
|
icon?: ReactNode;
|
|
225
227
|
/** A small count or label after the text, e.g. unread items. */
|
|
226
228
|
badge?: ReactNode;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
]
|
|
9
9
|
},
|
|
10
10
|
"source": {
|
|
11
|
-
"libraryVersion": "2.
|
|
11
|
+
"libraryVersion": "2.6.0",
|
|
12
12
|
"files": [
|
|
13
13
|
"core/count.ts",
|
|
14
14
|
"core/index.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"react/locale.ts",
|
|
20
20
|
"react/props.ts"
|
|
21
21
|
],
|
|
22
|
-
"sourceHash": "sha256:
|
|
22
|
+
"sourceHash": "sha256:a586004558fc7c4059756daa252c36028f3f9b1f4cf55fcccb9b5af2ce1e21e9"
|
|
23
23
|
},
|
|
24
24
|
"props": [
|
|
25
25
|
{
|
|
@@ -93,7 +93,9 @@
|
|
|
93
93
|
"type": "other",
|
|
94
94
|
"typeText": "ReactNode",
|
|
95
95
|
"required": false,
|
|
96
|
-
"origin": "derived"
|
|
96
|
+
"origin": "derived",
|
|
97
|
+
"description": "A hint under the label, linked as the field's description.",
|
|
98
|
+
"descriptionOrigin": "jsdoc"
|
|
97
99
|
},
|
|
98
100
|
{
|
|
99
101
|
"name": "disabled",
|
|
@@ -137,7 +139,9 @@
|
|
|
137
139
|
"type": "other",
|
|
138
140
|
"typeText": "ReactNode",
|
|
139
141
|
"required": false,
|
|
140
|
-
"origin": "derived"
|
|
142
|
+
"origin": "derived",
|
|
143
|
+
"description": "The field's name, shown above it and linked to it.",
|
|
144
|
+
"descriptionOrigin": "jsdoc"
|
|
141
145
|
},
|
|
142
146
|
{
|
|
143
147
|
"name": "localeText",
|
|
@@ -267,7 +271,9 @@
|
|
|
267
271
|
"typeText": "FieldSize | undefined",
|
|
268
272
|
"required": false,
|
|
269
273
|
"default": "md",
|
|
270
|
-
"origin": "derived"
|
|
274
|
+
"origin": "derived",
|
|
275
|
+
"description": "Default `md`.",
|
|
276
|
+
"descriptionOrigin": "jsdoc"
|
|
271
277
|
},
|
|
272
278
|
{
|
|
273
279
|
"name": "style",
|
|
@@ -372,9 +378,9 @@
|
|
|
372
378
|
"safeMutations": {},
|
|
373
379
|
"examples": [],
|
|
374
380
|
"coverage": {
|
|
375
|
-
"propsTotal":
|
|
376
|
-
"propsDescribed":
|
|
377
|
-
"describedPct":
|
|
381
|
+
"propsTotal": 12,
|
|
382
|
+
"propsDescribed": 10,
|
|
383
|
+
"describedPct": 83,
|
|
378
384
|
"authoredSections": [
|
|
379
385
|
"accessibility"
|
|
380
386
|
]
|
|
@@ -28,10 +28,13 @@ export interface TextareaProps
|
|
|
28
28
|
defaultValue?: string;
|
|
29
29
|
/** The new text on every change. The native `onChange` fires as well. */
|
|
30
30
|
onValueChange?(value: string): void;
|
|
31
|
+
/** The field's name, shown above it and linked to it. */
|
|
31
32
|
label?: ReactNode;
|
|
33
|
+
/** A hint under the label, linked as the field's description. */
|
|
32
34
|
description?: ReactNode;
|
|
33
35
|
/** Message below the field; also marks it invalid. */
|
|
34
36
|
error?: ReactNode;
|
|
37
|
+
/** Default `md`. */
|
|
35
38
|
size?: FieldSize;
|
|
36
39
|
/** Grow and shrink with the text, between `minRows` and `maxRows`. Default false. */
|
|
37
40
|
autoResize?: boolean;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
]
|
|
12
12
|
},
|
|
13
13
|
"source": {
|
|
14
|
-
"libraryVersion": "2.
|
|
14
|
+
"libraryVersion": "2.6.0",
|
|
15
15
|
"files": [
|
|
16
16
|
"core/api.ts",
|
|
17
17
|
"core/index.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"react/props.ts",
|
|
27
27
|
"react/useToasts.ts"
|
|
28
28
|
],
|
|
29
|
-
"sourceHash": "sha256:
|
|
29
|
+
"sourceHash": "sha256:976553d5d606c12b441caba76badad73812eea6dde400b51eb3ee17d05db7871"
|
|
30
30
|
},
|
|
31
31
|
"props": [
|
|
32
32
|
{
|
|
@@ -63,7 +63,9 @@
|
|
|
63
63
|
],
|
|
64
64
|
"typeText": "'ltr' | 'rtl' | 'auto' | undefined",
|
|
65
65
|
"required": false,
|
|
66
|
-
"origin": "derived"
|
|
66
|
+
"origin": "derived",
|
|
67
|
+
"description": "Writing direction, which decides which corner `position` means.\nDefault `auto`, taken from the document.",
|
|
68
|
+
"descriptionOrigin": "jsdoc"
|
|
67
69
|
},
|
|
68
70
|
{
|
|
69
71
|
"name": "duration",
|
|
@@ -211,9 +213,9 @@
|
|
|
211
213
|
"safeMutations": {},
|
|
212
214
|
"examples": [],
|
|
213
215
|
"coverage": {
|
|
214
|
-
"propsTotal":
|
|
215
|
-
"propsDescribed":
|
|
216
|
-
"describedPct":
|
|
216
|
+
"propsTotal": 8,
|
|
217
|
+
"propsDescribed": 8,
|
|
218
|
+
"describedPct": 100,
|
|
217
219
|
"authoredSections": [
|
|
218
220
|
"accessibility"
|
|
219
221
|
]
|
|
@@ -36,6 +36,10 @@ export interface ToasterProps {
|
|
|
36
36
|
icons?: Partial<Record<ToastType, ReactNode>>;
|
|
37
37
|
/** A store from `createToastStore()`. Defaults to the one behind `toast()`. */
|
|
38
38
|
store?: ToastStore;
|
|
39
|
+
/**
|
|
40
|
+
* Writing direction, which decides which corner `position` means.
|
|
41
|
+
* Default `auto`, taken from the document.
|
|
42
|
+
*/
|
|
39
43
|
dir?: "ltr" | "rtl" | "auto";
|
|
40
44
|
className?: string;
|
|
41
45
|
classNames?: Partial<Record<ToasterSlot, string>>;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
]
|
|
9
9
|
},
|
|
10
10
|
"source": {
|
|
11
|
-
"libraryVersion": "2.
|
|
11
|
+
"libraryVersion": "2.6.0",
|
|
12
12
|
"files": [
|
|
13
13
|
"core/index.ts",
|
|
14
14
|
"core/types.ts",
|
|
@@ -160,9 +160,9 @@
|
|
|
160
160
|
"safeMutations": {},
|
|
161
161
|
"examples": [],
|
|
162
162
|
"coverage": {
|
|
163
|
-
"propsTotal":
|
|
163
|
+
"propsTotal": 9,
|
|
164
164
|
"propsDescribed": 7,
|
|
165
|
-
"describedPct":
|
|
165
|
+
"describedPct": 78,
|
|
166
166
|
"authoredSections": [
|
|
167
167
|
"accessibility"
|
|
168
168
|
]
|
package/tools/mcp/README.md
CHANGED
|
@@ -4,31 +4,90 @@ Serves the component catalogue to a coding agent, so it writes against the
|
|
|
4
4
|
real contract instead of recalling one.
|
|
5
5
|
|
|
6
6
|
> **Beta.** The tools work and are tested, but their names and shapes may
|
|
7
|
-
> change before this settles.
|
|
8
|
-
|
|
7
|
+
> change before this settles.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @grampro/kit mcp install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That writes the configuration for every agent it knows about — Claude Code,
|
|
16
|
+
Codex and Cursor — and tells you which files it touched. Restrict it with
|
|
17
|
+
`--for codex` (or `claude`, `cursor`, or a comma-separated list).
|
|
18
|
+
|
|
19
|
+
It is worth using rather than editing the files by hand: it writes UTF-8
|
|
20
|
+
without a byte-order mark and with LF endings, which is the one thing that
|
|
21
|
+
reliably goes wrong. PowerShell's `Out-File -Encoding utf8` adds a BOM, and
|
|
22
|
+
every JSON parser then rejects the file with an error that names the first
|
|
23
|
+
character and explains nothing.
|
|
24
|
+
|
|
25
|
+
Running it again is safe. It merges into whatever is already there, leaves
|
|
26
|
+
other servers alone, and says `unchanged` when there is nothing to do.
|
|
27
|
+
|
|
28
|
+
It writes to the project root — the nearest folder up from you with a `.git`
|
|
29
|
+
or a `package.json` — and prints which one it chose, because an agent reads
|
|
30
|
+
these files from the folder it has open and writing to a subdirectory is the
|
|
31
|
+
one way this can quietly do nothing.
|
|
32
|
+
|
|
33
|
+
### If the server does not appear
|
|
34
|
+
|
|
35
|
+
The file being written is not the same as the server being loaded, and in
|
|
36
|
+
Claude Code two things stand between them.
|
|
37
|
+
|
|
38
|
+
**It needs approving.** A `.mcp.json` server is project-scoped, and Claude
|
|
39
|
+
Code asks before trusting one — until you say yes it sits at `Pending
|
|
40
|
+
approval`, and nothing asks twice. `claude mcp reset-project-choices` brings
|
|
41
|
+
the prompt back if it was dismissed.
|
|
42
|
+
|
|
43
|
+
**It needs a fresh session.** The configuration is read when a session
|
|
44
|
+
starts, so an editor window that was already open sees nothing. In the VS
|
|
45
|
+
Code extension, run *Developer: Reload Window*.
|
|
46
|
+
|
|
47
|
+
`/mcp` lists the servers and their state, which is the quickest way to tell
|
|
48
|
+
"not configured" from "configured, not approved".
|
|
49
|
+
|
|
50
|
+
To skip both, install it for yourself rather than for the project:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
claude mcp add -s user grampro -- npx -y @grampro/kit mcp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
User scope lives in `~/.claude.json`, loads in every project, and needs no
|
|
57
|
+
approval because it is yours rather than the repository's. Project scope is
|
|
58
|
+
the better choice when you want the rest of the team to get it from a commit.
|
|
59
|
+
|
|
60
|
+
### What it writes
|
|
61
|
+
|
|
62
|
+
| agent | file | format |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| Claude Code | `.mcp.json` | `mcpServers.grampro` |
|
|
65
|
+
| Codex | `.codex/config.toml` | `[mcp_servers.grampro]` |
|
|
66
|
+
| Cursor | `.cursor/mcp.json` | `mcpServers.grampro` |
|
|
67
|
+
|
|
68
|
+
The Codex config is project-scoped. For every project instead, put the same
|
|
69
|
+
`[mcp_servers.grampro]` block in `~/.codex/config.toml`.
|
|
70
|
+
|
|
71
|
+
A project that already has the package installed gets a direct `node`
|
|
72
|
+
invocation; anywhere else the config uses `npx -y @grampro/kit mcp`, which
|
|
73
|
+
works without an install but re-resolves the package on every start.
|
|
74
|
+
|
|
75
|
+
### By hand, if you prefer
|
|
9
76
|
|
|
10
77
|
```bash
|
|
11
78
|
claude mcp add grampro -- npx -y @grampro/kit mcp
|
|
12
79
|
```
|
|
13
80
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
"mcpServers": {
|
|
21
|
-
"grampro": {
|
|
22
|
-
"command": "node",
|
|
23
|
-
"args": ["node_modules/@grampro/kit/index.cjs", "mcp"]
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
81
|
+
```toml
|
|
82
|
+
# ~/.codex/config.toml
|
|
83
|
+
[mcp_servers.grampro]
|
|
84
|
+
command = "npx"
|
|
85
|
+
args = ["-y", "@grampro/kit", "mcp"]
|
|
27
86
|
```
|
|
28
87
|
|
|
29
88
|
MCP servers are not invoked by name. There is no `/grampro`; the model calls
|
|
30
|
-
the tools itself when they are relevant.
|
|
31
|
-
connected.
|
|
89
|
+
the tools itself when they are relevant. In Claude Code, `/mcp` shows whether
|
|
90
|
+
the server connected.
|
|
32
91
|
|
|
33
92
|
## Why
|
|
34
93
|
|
|
@@ -49,7 +108,7 @@ document pasted into the prompt.
|
|
|
49
108
|
| `get_component` | Props with types and allowed values, which are required, handler signatures, slots, states, composition rules |
|
|
50
109
|
| `get_example` | Working usage from the component's README |
|
|
51
110
|
| `get_guidance` | Authored notes on forms, overlays, the grid, pickers, styling, accessibility |
|
|
52
|
-
| `figma_frame` | A design's structure, text, repeated regions and theme variables |
|
|
111
|
+
| `figma_frame` | A design's structure, text, repeated regions and theme variables, from a `designFile` path, a pasted `design`, or a Figma `url` |
|
|
53
112
|
| `install_component` | Copies components into the project being worked in |
|
|
54
113
|
| `validate_usage` | Whether the JSX an agent wrote matches the passports |
|
|
55
114
|
|
|
@@ -66,18 +125,55 @@ invention, and this catches it:
|
|
|
66
125
|
dismissible — not a prop of this component. Did you mean: onDismiss
|
|
67
126
|
```
|
|
68
127
|
|
|
128
|
+
## A whole design at once
|
|
129
|
+
|
|
130
|
+
In Figma, **Grampro Kit → Export all screens for an agent** walks every
|
|
131
|
+
page and exports each top-level frame, with one set of theme values read
|
|
132
|
+
across the whole design rather than guessed per frame. Save it, then:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx @grampro/kit design ./design.json
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
Wrote 3 screen(s) to design/
|
|
140
|
+
purchase-orders.json Purchase orders (Product) — 22 nodes
|
|
141
|
+
suppliers.json Suppliers (Product) — 16 nodes
|
|
142
|
+
settings.json Settings (Admin) — 12 nodes
|
|
143
|
+
index.json the plan: every screen, shared theme
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
A browser cannot write a directory and a plugin is an iframe, so the
|
|
147
|
+
plugin hands over one file and the CLI does the filesystem work. Each
|
|
148
|
+
screen is written in the shape a single-frame export has, so `figma_frame`
|
|
149
|
+
reads one with `designFile` and no special case.
|
|
150
|
+
|
|
151
|
+
`index.json` is the plan: every screen with its size, node count and
|
|
152
|
+
repeated regions, plus the shared theme. Have an agent map regions to
|
|
153
|
+
components from that and correct it before any code exists — a guess that
|
|
154
|
+
`Action` is a Card rather than a Button costs one sentence there and five
|
|
155
|
+
screens of rework later. Then build one screen at a time, validating each.
|
|
156
|
+
|
|
69
157
|
## From a Figma design
|
|
70
158
|
|
|
71
|
-
|
|
159
|
+
Three routes, and only the first needs credentials.
|
|
72
160
|
|
|
73
161
|
**By URL.** Pass a `figma.com/design/...` link. Called with just the URL it
|
|
74
162
|
lists the frames in the file; called again with `frame` it returns that
|
|
75
|
-
screen.
|
|
163
|
+
screen. A link that already carries a `node-id` — which is what Figma copies
|
|
164
|
+
when you right-click a frame — skips straight to that frame in one call.
|
|
165
|
+
Needs a token.
|
|
76
166
|
|
|
77
167
|
**By plugin export.** In Figma, run **Grampro Kit → Export selection for an
|
|
78
168
|
agent**, then pass the JSON as `design`. The plugin reads the scene graph
|
|
79
|
-
directly, so this works on any file you can open
|
|
80
|
-
|
|
169
|
+
directly, so this works on any file you can open, asks for no token, and
|
|
170
|
+
never touches the REST API — which also means it cannot be rate limited.
|
|
171
|
+
|
|
172
|
+
**By file.** Press **Save .json** in that panel and pass the path as
|
|
173
|
+
`designFile`. Prefer this for anything but a small frame: a chat input
|
|
174
|
+
caps at around fifty thousand characters, and a pasted export sits in the
|
|
175
|
+
conversation twice — once as the argument, again inside the tool call —
|
|
176
|
+
where a file appears only in the answer.
|
|
81
177
|
|
|
82
178
|
Either way the answer is the same shape: the structure, the text, the
|
|
83
179
|
regions whose names repeat — eight `Status cell` nodes are a column — and
|
|
@@ -94,21 +190,51 @@ is the body text, and a near-black navy scores high on saturation, so both
|
|
|
94
190
|
"most common" and "most saturated" pick the wrong thing. Lightness and
|
|
95
191
|
saturation together find the accent.
|
|
96
192
|
|
|
193
|
+
### Rate limits
|
|
194
|
+
|
|
195
|
+
Figma's limits on file reads are cost-based, and the cost is the size of the
|
|
196
|
+
response. Asking for a whole file to answer a question about one frame is
|
|
197
|
+
what earns a 429, and the lockout is long — a `Retry-After` of 395274 has
|
|
198
|
+
been observed on a single token.
|
|
199
|
+
|
|
200
|
+
So nothing here fetches a whole file. Listing the frames uses `depth=1`,
|
|
201
|
+
which returns the document's outline rather than its contents, and the frame
|
|
202
|
+
itself is then fetched by id. A URL carrying a `node-id` skips the listing
|
|
203
|
+
altogether. Responses are held for five minutes, so the natural two-call
|
|
204
|
+
flow — list, then read — costs one listing and one frame.
|
|
205
|
+
|
|
206
|
+
If a token is already locked out, the plugin export route still works.
|
|
207
|
+
|
|
97
208
|
## Credentials
|
|
98
209
|
|
|
99
|
-
|
|
210
|
+
The URL route needs a Figma personal access token. Store it where the
|
|
211
|
+
operating system keeps credentials:
|
|
100
212
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
213
|
+
```bash
|
|
214
|
+
npx @grampro/kit secret set figma
|
|
215
|
+
```
|
|
104
216
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
217
|
+
It prompts, with the echo off, and never takes the value as an argument —
|
|
218
|
+
arguments land in shell history and in every process listing on the machine.
|
|
219
|
+
Piping works too, for a script: `echo "$TOKEN" | npx @grampro/kit secret set figma`.
|
|
108
220
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
221
|
+
| platform | where it goes |
|
|
222
|
+
| --- | --- |
|
|
223
|
+
| Windows | Data Protection API, tied to your Windows account |
|
|
224
|
+
| macOS | the keychain |
|
|
225
|
+
| Linux | Secret Service (GNOME Keyring, KWallet) |
|
|
226
|
+
| anything else | a 0600 file in your config directory, and it says so |
|
|
227
|
+
|
|
228
|
+
`secret list` shows what is stored, masked, and names the backend in use.
|
|
229
|
+
`secret remove figma` deletes it. Create the token at Figma → Settings →
|
|
230
|
+
Security → Personal access tokens, with `File content: read` and nothing
|
|
231
|
+
else.
|
|
232
|
+
|
|
233
|
+
The token is read from `FIGMA_TOKEN` in the environment first, then the
|
|
234
|
+
store, then a `.env` — the last only so that an upgrade does not break a
|
|
235
|
+
project set up the old way. Reading from a `.env` prints a note saying to
|
|
236
|
+
move it. A credential in a file inside a working tree is one `git add -A`
|
|
237
|
+
away from being published, which is why this moved.
|
|
112
238
|
|
|
113
239
|
## Suggested use
|
|
114
240
|
|
|
@@ -123,10 +249,11 @@ No dependencies. MCP is newline-delimited JSON-RPC over stdio, which is a
|
|
|
123
249
|
few hundred lines, and a server shipped inside a library that every consumer
|
|
124
250
|
installs should not drag an SDK along with it.
|
|
125
251
|
|
|
252
|
+
Start-up is about the cost of an empty Node process. An agent spawns the
|
|
253
|
+
server once per session, and `index.cjs` routes `mcp` before loading the
|
|
254
|
+
CLI's own dependencies, which are no use to a JSON-RPC loop and cost around
|
|
255
|
+
70ms. The passports are indexed on the first question and not re-read after.
|
|
256
|
+
|
|
126
257
|
The catalogue it serves is the one in this package, not the components
|
|
127
258
|
copied into your project — so an agent can see a component before you
|
|
128
259
|
install it, and `install_component` puts it in place.
|
|
129
|
-
|
|
130
|
-
A Figma file is fetched once and held for five minutes. The natural flow is
|
|
131
|
-
two calls, list then read, and refetching a large file in between is what
|
|
132
|
-
earns a rate limit.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `grampro-kit design <file>` — a whole-file Figma export, split into a folder.
|
|
3
|
+
*
|
|
4
|
+
* The plugin downloads one file because a browser cannot write a directory.
|
|
5
|
+
* Splitting it is filesystem work, which belongs in the CLI rather than in a
|
|
6
|
+
* plugin iframe, and it is what makes the result usable: an agent reads the
|
|
7
|
+
* index to plan, then opens one screen at a time, instead of loading every
|
|
8
|
+
* screen to build any of them.
|
|
9
|
+
*
|
|
10
|
+
* Each screen is written in the same shape a single-frame export has, so
|
|
11
|
+
* `figma_frame` reads one with no special case.
|
|
12
|
+
*/
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const { themeFrom } = require("./figma.cjs");
|
|
18
|
+
|
|
19
|
+
/** A file name from a screen name: lowercase, dashes, nothing exotic. */
|
|
20
|
+
const slug = (name) =>
|
|
21
|
+
String(name)
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
24
|
+
.replace(/^-|-$/g, "") || "screen";
|
|
25
|
+
|
|
26
|
+
/** Names a designer repeated are the ones that mean something. */
|
|
27
|
+
function countNames(node, counts) {
|
|
28
|
+
counts.set(node.name, (counts.get(node.name) || 0) + 1);
|
|
29
|
+
for (const child of node.children || []) countNames(child, counts);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const repeatedIn = (nodes) => {
|
|
33
|
+
const counts = new Map();
|
|
34
|
+
for (const node of nodes) countNames(node, counts);
|
|
35
|
+
return [...counts.entries()]
|
|
36
|
+
.filter(([, n]) => n > 2)
|
|
37
|
+
.sort((a, b) => b[1] - a[1])
|
|
38
|
+
.slice(0, 12)
|
|
39
|
+
.map(([name, times]) => ({ name, times }));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Component instances found anywhere in a screen. */
|
|
43
|
+
function componentsIn(nodes) {
|
|
44
|
+
const found = new Set();
|
|
45
|
+
const visit = (node) => {
|
|
46
|
+
if (node.component) found.add(node.component);
|
|
47
|
+
for (const child of node.children || []) visit(child);
|
|
48
|
+
};
|
|
49
|
+
for (const node of nodes) visit(node);
|
|
50
|
+
return [...found];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const countNodes = (nodes) =>
|
|
54
|
+
nodes.reduce((n, node) => n + 1 + countNodes(node.children || []), 0);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Split an export into `dest`.
|
|
58
|
+
*
|
|
59
|
+
* Returns what it wrote rather than printing, so the CLI owns the words and
|
|
60
|
+
* this stays testable.
|
|
61
|
+
*/
|
|
62
|
+
function splitDesign(file, dest) {
|
|
63
|
+
const source = path.resolve(file);
|
|
64
|
+
if (!fs.existsSync(source)) return { error: `No file at ${source}.` };
|
|
65
|
+
|
|
66
|
+
let design;
|
|
67
|
+
try {
|
|
68
|
+
design = JSON.parse(fs.readFileSync(source, "utf8").replace(/^/, ""));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return { error: `${source} is not JSON: ${error.message}` };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* A single-frame export is accepted too, as one screen. Someone who ran the
|
|
75
|
+
* wrong menu item should get a folder, not a lecture.
|
|
76
|
+
*/
|
|
77
|
+
const screens = Array.isArray(design.screens)
|
|
78
|
+
? design.screens
|
|
79
|
+
: Array.isArray(design.nodes)
|
|
80
|
+
? [{ name: design.frame || "screen", page: "", nodes: design.nodes }]
|
|
81
|
+
: null;
|
|
82
|
+
|
|
83
|
+
if (!screens) {
|
|
84
|
+
return {
|
|
85
|
+
error:
|
|
86
|
+
"Expected the output of the Figma plugin's 'Export all screens for an agent' " +
|
|
87
|
+
"(or 'Export selection'). This file has neither `screens` nor `nodes`.",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (screens.length === 0) return { error: "That export contains no screens." };
|
|
91
|
+
|
|
92
|
+
const out = path.resolve(dest);
|
|
93
|
+
fs.mkdirSync(out, { recursive: true });
|
|
94
|
+
|
|
95
|
+
const tokens = design.tokens || {};
|
|
96
|
+
const colours = new Map((tokens.colours || []).map((c) => [c.value, c.uses]));
|
|
97
|
+
const radii = new Map((tokens.radii || []).map((r) => [r.value, r.uses]));
|
|
98
|
+
const fontSizes = new Map((tokens.fontSizes || []).map((f) => [f.value, f.uses]));
|
|
99
|
+
|
|
100
|
+
const taken = new Set();
|
|
101
|
+
const written = [];
|
|
102
|
+
|
|
103
|
+
for (const screen of screens) {
|
|
104
|
+
// Two screens may share a name across pages; the second gets a suffix
|
|
105
|
+
// rather than quietly overwriting the first.
|
|
106
|
+
let name = slug(screen.name);
|
|
107
|
+
if (taken.has(name)) {
|
|
108
|
+
let n = 2;
|
|
109
|
+
while (taken.has(`${name}-${n}`)) n += 1;
|
|
110
|
+
name = `${name}-${n}`;
|
|
111
|
+
}
|
|
112
|
+
taken.add(name);
|
|
113
|
+
|
|
114
|
+
const fileName = `${name}.json`;
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
path.join(out, fileName),
|
|
117
|
+
`${JSON.stringify({ frame: screen.name, nodes: screen.nodes, tokens }, null, 2)}\n`,
|
|
118
|
+
"utf8",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
written.push({
|
|
122
|
+
name: screen.name,
|
|
123
|
+
page: screen.page || undefined,
|
|
124
|
+
file: fileName,
|
|
125
|
+
size: screen.size,
|
|
126
|
+
nodes: countNodes(screen.nodes),
|
|
127
|
+
repeatedRegions: repeatedIn(screen.nodes),
|
|
128
|
+
componentsNamed: componentsIn(screen.nodes),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* The index is what an agent reads first: every screen, how big it is, the
|
|
134
|
+
* regions whose names repeat, and one theme for all of them. Enough to
|
|
135
|
+
* propose a component for each region before any code is written, which is
|
|
136
|
+
* where a wrong guess is cheap to correct.
|
|
137
|
+
*/
|
|
138
|
+
const index = {
|
|
139
|
+
screens: written,
|
|
140
|
+
theme: themeFrom(colours, radii, fontSizes),
|
|
141
|
+
builtFromLibrary: written.some((s) => s.componentsNamed.length > 0),
|
|
142
|
+
howToUse:
|
|
143
|
+
"Read this index first and say which kit component each repeated region maps to, " +
|
|
144
|
+
"so the mapping can be corrected before any screen is built. Then take one screen " +
|
|
145
|
+
"at a time: figma_frame with designFile set to its file, get_component for each " +
|
|
146
|
+
"component before writing it, install_component for what is missing, and " +
|
|
147
|
+
"validate_usage on the JSX before moving to the next screen.",
|
|
148
|
+
};
|
|
149
|
+
fs.writeFileSync(path.join(out, "index.json"), `${JSON.stringify(index, null, 2)}\n`, "utf8");
|
|
150
|
+
|
|
151
|
+
return { dest: out, index, written };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { splitDesign, slug };
|