@grampro/kit 2.7.2 → 2.7.3

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 (75) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +233 -209
  2. package/.gbs/skills/gbs-components/references/data-grid.md +9 -0
  3. package/.gbs/skills/gbs-components/references/forms.md +12 -0
  4. package/.gbs/skills/gbs-components/references/install.md +14 -3
  5. package/CHANGELOG.md +963 -861
  6. package/README.md +11 -3
  7. package/index.cjs +108 -123
  8. package/package.json +1 -1
  9. package/source/components/accordion/passport.json +1 -1
  10. package/source/components/alert/passport.json +1 -1
  11. package/source/components/avatar/passport.json +1 -1
  12. package/source/components/badge/passport.json +1 -1
  13. package/source/components/breadcrumb/passport.json +1 -1
  14. package/source/components/button/passport.json +1 -1
  15. package/source/components/card/passport.json +1 -1
  16. package/source/components/checkbox/passport.json +2 -2
  17. package/source/components/checkbox/passport.manual.json +1 -1
  18. package/source/components/checkbox/react/Checkbox.tsx +14 -0
  19. package/source/components/combobox/passport.json +3 -2
  20. package/source/components/combobox/passport.manual.json +1 -1
  21. package/source/components/combobox/react/MultiSelect.tsx +13 -0
  22. package/source/components/combobox/react/Select.tsx +13 -0
  23. package/source/components/combobox/react/surface.ts +70 -0
  24. package/source/components/data-grid/passport.json +6 -6
  25. package/source/components/data-grid/react/DataGrid.tsx +353 -351
  26. package/source/components/date-picker/passport.json +3 -2
  27. package/source/components/date-picker/passport.manual.json +1 -1
  28. package/source/components/date-picker/react/DatePicker.tsx +16 -2
  29. package/source/components/date-picker/react/DateRangePicker.tsx +16 -2
  30. package/source/components/date-picker/react/surface.ts +68 -0
  31. package/source/components/dialog/passport.json +1 -1
  32. package/source/components/file-uploader/passport.json +1 -1
  33. package/source/components/input/passport.json +2 -2
  34. package/source/components/input/passport.manual.json +1 -1
  35. package/source/components/input/react/Input.tsx +21 -0
  36. package/source/components/menu/passport.json +1 -1
  37. package/source/components/modal/passport.json +1 -1
  38. package/source/components/number-input/passport.json +2 -2
  39. package/source/components/number-input/passport.manual.json +1 -1
  40. package/source/components/number-input/react/NumberInput.tsx +30 -0
  41. package/source/components/popover/passport.json +1 -1
  42. package/source/components/progress/passport.json +1 -1
  43. package/source/components/radio-group/passport.json +2 -2
  44. package/source/components/radio-group/passport.manual.json +1 -1
  45. package/source/components/radio-group/react/RadioGroup.tsx +33 -0
  46. package/source/components/shared/core/agent/field-executors.ts +173 -137
  47. package/source/components/shared/core/agent/index.ts +21 -0
  48. package/source/components/shared/core/agent/surface.ts +437 -0
  49. package/source/components/shared/core/agent/webmcp.ts +332 -328
  50. package/source/components/shared/core/agent/write.ts +62 -58
  51. package/source/components/shared/index.ts +7 -0
  52. package/source/components/shared/react/AgentSurface.tsx +145 -0
  53. package/source/components/shared/react/agentSurfaceContext.ts +92 -0
  54. package/source/components/shared/react/useFieldAgent.ts +62 -88
  55. package/source/components/shared/version.json +4 -4
  56. package/source/components/shared/version.ts +6 -6
  57. package/source/components/skeleton/passport.json +1 -1
  58. package/source/components/spinner/passport.json +1 -1
  59. package/source/components/switch/passport.json +2 -2
  60. package/source/components/switch/passport.manual.json +1 -1
  61. package/source/components/switch/react/Switch.tsx +16 -0
  62. package/source/components/tabs/passport.json +1 -1
  63. package/source/components/textarea/passport.json +2 -2
  64. package/source/components/textarea/passport.manual.json +1 -1
  65. package/source/components/textarea/react/Textarea.tsx +16 -0
  66. package/source/components/toaster/passport.json +1 -1
  67. package/source/components/tooltip/passport.json +1 -1
  68. package/tools/doctor/baseline.cjs +5 -4
  69. package/tools/mcp/server.cjs +13 -1
  70. package/tools/passport/extract.cjs +20 -65
  71. package/tools/passport/hash.cjs +18 -1
  72. package/tools/passport/index.cjs +4 -2
  73. package/tools/passport/typescript.cjs +220 -0
  74. package/tools/prompt.cjs +205 -0
  75. package/tools/ui.cjs +67 -0
package/CHANGELOG.md CHANGED
@@ -1,861 +1,963 @@
1
- # Changelog
2
-
3
- ## 2.7.2
4
-
5
- ### Fixed
6
-
7
- **`add` overwrote components you had edited, and said it had succeeded.**
8
- Components are copied into your project so that you can change them, but
9
- running `add` on one that was already installed replaced the whole folder
10
- unconditionally. It printed "installed successfully" and the edits were gone.
11
- The same happened through the MCP server: `install_component` runs `add`, so an
12
- agent asked to "install Button" in a project with a customised Button
13
- discarded the customisation.
14
-
15
- Now every install records what it wrote — a hash of each file and a copy of
16
- the component's contract, in `.install-manifest.json` beside the component —
17
- and `add` checks that record first. An untouched component is updated. An
18
- edited one is left in place: `add` lists the files that changed, prints how to
19
- see more, and exits 1, while the other components in the same command still
20
- install. Only `--force` replaces an edited component. `shared/` has worked this
21
- way since it was introduced; components never did.
22
-
23
- Components installed before this release have no record. An untouched one is
24
- recognised from its passport's `sourceHash` — recomputed over your copy with
25
- the labels the kit hashed it with, which a test holds to the shipped value for
26
- all 27 — and is updated normally. One that differs from this version cannot be
27
- told apart from an older untouched copy, so it is kept, not guessed at.
28
-
29
- **`install_component` told an agent nothing when it was refused.** The
30
- installer explains a refusal on stderr, and the tool only returned stdout — so
31
- the agent got "Install failed" followed by stylesheet instructions. It now
32
- returns what was installed, what was kept and why, and advice not to force it.
33
-
34
- **`passport` crashed in any project on TypeScript 7.** TypeScript 7 is a
35
- native rewrite whose npm package does not expose the compiler API passports
36
- are generated with, and it is what `npm i -D typescript` installs today. The
37
- command failed with `Cannot read properties of undefined (reading
38
- 'ReactJSX')`. It now checks for the API rather than for the package, falls
39
- back to a TypeScript 6 installed under an alias, and otherwise says exactly
40
- that, with the one-line fix.
41
-
42
- **`validate_usage` invented props from inside expressions.** Attribute names
43
- were matched anywhere in a tag, so `title={ok ? first : second}` reported
44
- `first` and `second` as unknown props. The tag scanner now reads names only at
45
- the top level, outside braces and quotes. It is shared with `doctor`, and stays
46
- linear on hostile input.
47
-
48
- ### Added
49
-
50
- **`npx @grampro/kit doctor`** — what you changed in your copies, what the new
51
- version changes, and which call sites in your code an update would break.
52
-
53
- It compares three versions of each component: the one you installed, your copy
54
- now, and the one the CLI carries. It compares them as contracts — props,
55
- accepted values, handlers, exports, agent operations — because a text diff of
56
- `Button.tsx` shows which lines moved, not that `variant` stopped accepting
57
- `"ghost"`. Edits from both sides to the same value set are merged rather than
58
- called a conflict; a conflict is an incompatible pair, such as you removing a
59
- prop that upstream changed. Then it scans your source and names the call sites
60
- that pass something the replacement would not accept.
61
-
62
- It never writes. `--json` gives the same report to agents and CI. The agent
63
- skill now tells agents to run it, and never to retry `add` with `--force` on
64
- someone else's edits. A new docs page, *Updating components*, covers all of it.
65
-
66
- ## 2.7.1
67
-
68
- ### Security
69
-
70
- **get_guidance could read any .md file on disk.** It appended `.md` to a
71
- caller-supplied `topic` and joined it onto the references folder with no
72
- traversal guard, so `topic = "../../../../CHANGELOG"` returned the repo
73
- CHANGELOG and the same trick reached any `.md` file the server process could
74
- open. Found by a VAPT pass driving the tool with traversal payloads. The
75
- server runs with your own authority, so it is a confused-deputy hole rather
76
- than privilege escalation — but a tool that advertises nine fixed topics
77
- should not become a file-exfiltration primitive for a prompt-injected agent.
78
- It now refuses any `topic` that is not one of the shipped guidance names, with
79
- a resolved-prefix check behind the allowlist. A regression test drives the
80
- same payloads.
81
-
82
- **Affected: 2.4.4 through 2.7.0** — every release that has shipped the MCP
83
- server. Each was checked by running the same payload against its published
84
- tarball. 2.4.3 and earlier have no MCP server and are not affected. A project
85
- that installed the package locally needs `npm i -D @grampro/kit@latest`. One
86
- configured with `npx -y @grampro/kit mcp` may keep running a cached copy; check
87
- with `npx -y @grampro/kit --version`, and if it is not 2.7.1, run
88
- `npx -y @grampro/kit@2.7.1 mcp` once to replace it.
89
-
90
- ## 2.7.0
91
-
92
- ### Added
93
-
94
- **A design read from a URL now names its components.** An instance built
95
- from a library is the most valuable thing a design can contain — it says
96
- "this is a Button" instead of leaving an agent to infer it from a layer
97
- name and a fill. The URL route recorded `instanceOf: "1:234"`, an opaque
98
- id nothing could be done with, and discarded the maps that name it, which
99
- Figma sends in the same response.
100
-
101
- It resolves them now, preferring the component set's name over the
102
- variant's: a variant is called `Size=md, Variant=primary` and its set is
103
- called `Button`, and Button is what the agent is going to write. An
104
- instance whose master is not in the response keeps its id rather than
105
- guessing.
106
-
107
- **Every component now says what it is for.** A passport had two halves: the
108
- derived one, generated from the TypeScript, and the authored one — `purpose`,
109
- `intentDomains`, `accessibility`, `composition`, `safeMutations`. The generator
110
- emits empty shells for the authored half because none of it is in the types,
111
- and only Button and DataGrid had ever been filled in. The other 25 are written
112
- now.
113
-
114
- `safeMutations` is the one an agent acts on: which props it may change without
115
- altering behaviour, and which it may not, with the reason. An agent restyling a
116
- FileUploader should be free to change `size` and `preview` and nowhere near
117
- `endpoint`, `headers` or `transport` — that component is the one where a wrong
118
- prop sends someone's data somewhere. `get_component` now returns all of it;
119
- until this release it served `purpose` and dropped the rest, so the authored
120
- half reached nobody.
121
-
122
- Where no value in the schema's `intentDomains` enum honestly describes a
123
- component, the list is left empty rather than filled for symmetry. Nine of them
124
- are presentational, and a wrong routing hint is worse than none.
125
-
126
- **A field runtime, so nine components are agent-operable instead of one.**
127
- `setValue`, `clear`, `focus`, and `open`/`close` for the two that have
128
- something to disclose, on Input, Textarea, NumberInput, Checkbox, Switch,
129
- RadioGroup, Combobox and DatePicker.
130
-
131
- The grid earned a runtime of its own because its operations are a language:
132
- filter, sort, pin, paginate, export. A field has one verb, so the verb lives in
133
- `shared/core/agent` once rather than eight times, and each component supplies
134
- only a contract built from its own props. The same five layers rule on it —
135
- schema, reference, coercion, policy, plausibility — so a write to a read-only
136
- field is refused by name, an option that does not exist comes back with the
137
- nearest one that does, and a number outside `min`/`max` is told which bound it
138
- crossed rather than being silently clamped.
139
-
140
- The open question was whether the executor abstraction generalised. A grid is
141
- driven through an imperative API and a field is not, and the architecture notes
142
- had flagged that as unproven. It holds: Combobox and DatePicker are driven
143
- through their handles, the other six through the element their ref already
144
- returns, and nothing above the executor knows which. `setValue` joins
145
- `ComboboxHandle` and `DatePickerHandle` as a public method; the six that expose
146
- the DOM element needed no API change at all, because the technique
147
- `Input.clear()` has always used — the prototype setter plus a real event — is
148
- the same write with a value in it.
149
-
150
- `useFieldAgent` binds a contract to a ref. No component grew a prop for this.
151
-
152
- **The authored passports had no schema of their own.** A `passport.manual.json`
153
- pointed at `passport-v1.schema.json`, which describes the *merged* passport —
154
- so an editor validating one reported all eight generated fields as missing
155
- (`passportVersion`, `identity`, `source`, `props`, `events`, `slots`, `states`,
156
- `coverage`), and `additionalProperties: false` made errors of the four keys only
157
- an overlay has: `component`, `propNotes`, `notImplemented`, `$operationsNote`.
158
-
159
- It was wrong on the two files that had it before and merely quiet; at
160
- twenty-seven it is the thing that teaches people to ignore squiggles.
161
- `schema/passport-overlay-v1.schema.json` describes what an overlay may
162
- actually contain, referencing the merged schema for the fields they share so
163
- the two cannot drift. A test holds every overlay to it, which also catches a
164
- mistyped key — previously a silent no-op, since the merge only reads the names
165
- it knows.
166
-
167
- ### Security
168
-
169
- **A failed JSON parse quoted the file back.** V8 puts the first ten or so
170
- characters of its input into the error it throws, so pointing `designFile` at
171
- something that was not JSON returned a slice of it:
172
-
173
- That is not JSON: Unexpected token 's', "sk-proj-RE"... is not valid JSON
174
-
175
- Ten bytes is not an extraction, but it is enough to confirm that a path
176
- exists and what kind of file sits there, and an agent can be pointed at paths
177
- in a loop. The server runs with your own authority and the agent could read
178
- the file directly, so this was hygiene rather than a hole — and it cost
179
- nothing to close. The three parse sites in `figma.cjs`, `design.cjs` and
180
- `install.cjs` now name the file and say what was expected, without quoting
181
- any of it.
182
-
183
- **The package no longer installs a build toolchain.**
184
- `@grampro/headless-helpers` declared `rollup`, its plugins and `typescript`
185
- as runtime dependencies, so installing the kit pulled a compiler and a
186
- bundler into `node_modules` and carried their advisories — an arbitrary file
187
- write in Rollup and two ReDoS in picomatch, none of which any of this code
188
- runs. Requiring `^1.0.22`, which moved them to devDependencies, removes 84
189
- packages from a clean install and leaves the package at zero advisories.
190
-
191
- **Mermaid rendered with sanitisation off.** The diagram component ran with
192
- `securityLevel: "loose"`, which permits HTML in labels and click handlers,
193
- and its output goes through `dangerouslySetInnerHTML`. Nothing was
194
- exploitable: the only diagram text comes from our own MDX at build time, and
195
- in fact no page renders one. But the two together are a trap for whoever
196
- first renders a diagram from anything else, so it is `"strict"` now.
197
-
198
- ### Changed
199
-
200
- **The docs lead with the URL route.** Pasting a Figma link is one step;
201
- the plugin route is five, and every one is a place to stop. The reason it
202
- was necessary for designs built from a component library is now gone, so
203
- the page says plainly: use the URL for a screen, and the plugin when you
204
- want a whole product in one pass, or have no token, or are rate limited.
205
-
206
- **`figma_frame` describes the URL route first.** Its own description still
207
- said `designFile` was "the one to use for a whole screen", which was true
208
- only while the URL route could not resolve component names. An agent reads
209
- that description before it picks a route, so it was steering towards the long
210
- way round. It now leads with the URL and keeps the file export for what the
211
- file export is actually better at: a whole product in one pass, or no usable
212
- token. The `llms.txt` preamble said nothing about the URL route at all, and
213
- now says it is the shortest path from a design to a screen.
214
-
215
- **The components are out of beta.** Only the MCP server still carries the
216
- label, and it carries it in both places that matter: the docs page and the
217
- `figma_frame` tool description an agent reads.
218
-
219
- The one that was doing damage was in the agent skill, which is always in an
220
- agent's context. It said **"Always pass `--beta`"** and explained that without
221
- it you get the 1.x set. That had inverted: `--beta` became a no-op that prints
222
- a deprecation notice, 2.x is what `add` installs, and `--legacy` is what
223
- reaches 1.x. An agent following the skill was passing a dead flag and being
224
- told the opposite of what the CLI does. The flag table now documents
225
- `--legacy` instead, and says plainly that the 1.x set is unmaintained.
226
-
227
- Also gone from the CLI's own output ("Redesigned beta components" when a name
228
- does not match), the `llms.txt` preamble, which told every model that read the
229
- site that the components were beta and their APIs might change, and the README
230
- and architecture notes. `--beta` is still accepted, because scripts use it.
231
-
232
- ### Fixed
233
-
234
- **Headings on the translated pages had meaningless ids.** The slugger kept
235
- only `[a-z0-9\s-]`, which leaves a Malayalam heading with nothing but its
236
- spaces — so every heading fell through to `section` or to the `-` its spaces
237
- became, and then took its id from the duplicate counter:
238
-
239
- id="-" id="--1" id="--2" id="-structured-data" id="section-2"
240
-
241
- That counter is positional, so adding a heading renumbered every anchor below
242
- it: no link to one could be relied on, and four prop-table links on
243
- `ml/breadcrumb` and `ml/button` pointed at the English anchors and had never
244
- resolved. The class keeps letters, digits and combining marks in any script
245
- now, so those headings slug to their own text. `\p{M}` matters as much as
246
- `\p{L}` here: Malayalam vowel signs are combining marks, and dropping them
247
- mangles the text rather than emptying it. Every ASCII heading slugs to the
248
- same string as before, so no existing anchor moved.
249
-
250
- `pnpm check:anchors` in `docs-site` now checks every in-site fragment against
251
- the headings its target page actually offers, because a dead anchor does not
252
- 404 — the page loads and ignores it.
253
-
254
- ## 2.6.3
255
-
256
- ### Fixed
257
-
258
- **The text LLMs read was teaching a deprecated command.** `llms.txt` has a
259
- hand-written preamble, so it lagged behind the CLI: it told every model
260
- that reached the site to run `npx @grampro/kit -a <Component>`, a spelling
261
- the CLI itself marks "Deprecated. Use: add". It now says `add`, and names
262
- the Figma plugin, the whole-design export and `designFile`, none of which
263
- it mentioned.
264
-
265
- **The plugin's install link was invisible to every LLM.** It is rendered by
266
- a component, and the Markdown conversion passed that through verbatim — so
267
- `llms-full.txt` and every `.md` page carried a dangling `<FigmaPlugin />`
268
- tag where the URL should have been, and the plugin's address appeared
269
- nowhere a model could read it. The conversion emits a Markdown link now,
270
- and the URL lives in one place so the button and the converter cannot
271
- disagree about it.
272
-
273
- **The Figma plugin's listing is in review**, which Figma is quoting at a
274
- few weeks. Every place that links it now says so and gives the interim
275
- path: clone the repository and import `figma-plugin/manifest.json` through
276
- **Plugins → Development → Import plugin from manifest...** in the desktop
277
- app. The build is committed, so there is nothing to compile.
278
-
279
- **Six links in the docs went nowhere.** `datagrid.mdx` and
280
- `fileuploader.mdx` pointed at `#column-options`, `#grid-api-ref`,
281
- `#server-side-pagination`, `#upload-protocol`, `#custom-transport` and
282
- `#server-implementation` as same-page anchors. Those sections moved to
283
- child pages when the docs were split and the links were never updated, so
284
- they had been doing nothing since. They point at the child pages now.
285
-
286
- ### Changed
287
-
288
- **Every command is listed in one place.** `list` and `passport` appeared
289
- in no user-facing documentation at all, and the rest were scattered across
290
- three pages, so there was nowhere to look to find out what the CLI does.
291
- The README now carries the lot in a table.
292
-
293
- ## 2.6.2
294
-
295
- ### Changed
296
-
297
- **The agent skill now knows the MCP server exists.** It described the
298
- component library and never mentioned the server, so an agent holding both
299
- had nothing to tell it which to trust. The skill is a summary; the server
300
- answers from passports generated from the source, so where they differ the
301
- server is right and the skill is stale — and it now says so, along with
302
- calling `get_component` before writing a component and `validate_usage`
303
- before reporting back.
304
-
305
- The only previous mention of "MCP" in the skill was WebMCP, the DataGrid's
306
- browser-agent surface, which is a different thing entirely.
307
-
308
- **Neither needs the other, and the docs now say so.** The server carries
309
- its own copy of the guidance inside the package, so `get_guidance` answers
310
- in a project with no `.gbs/` folder at all. What they do together is the
311
- useful part: the skill is always in an agent's context and the server is
312
- not, so the skill is what tells an agent to ask and the server is what it
313
- asks.
314
-
315
- The two pages now read in the order they are used — Theming, Agent skill,
316
- MCP server.
317
-
318
- ## 2.6.1
319
-
320
- ### Changed
321
-
322
- **A screen costs about half what it did to read.** Measured on a real
323
- purchase-orders design: 42.9 KB to 23.9 KB, roughly 13,300 tokens to
324
- 7,400.
325
-
326
- Eight table rows exported as eight sibling subtrees of identical shape
327
- whose text differed — 48% of that screen was the same row said seven
328
- more times. The first is now given in full, because the agent builds from
329
- its shape, and the rest become `repeatedText`: one array of strings per
330
- row, which is closer to what a DataGrid's `data` prop wants than the
331
- subtrees were.
332
-
333
- Runs are matched on structure rather than appearance. Keyed on style, the
334
- rows never matched at all: a striped table alternates its background, so
335
- every consecutive pair looked like a different shape and nothing
336
- collapsed. Where the fills do vary across a run they come back as
337
- `repeatedFills`, which is how the striping and the per-status badge
338
- colours stay legible.
339
-
340
- Only runs of three or more collapse, and only where there is depth to
341
- save: two of a thing is a pair, not a list, and a row of labels has
342
- nothing inside it worth folding.
343
-
344
- ### Fixed
345
-
346
- **`depth` is honoured on the plugin and file routes.** The tool has always
347
- offered it — "Lower it if the payload is large" — and only ever applied
348
- it to a design fetched from a URL. A caller who read the schema and
349
- lowered it to cut a large payload got the large payload back. The same
350
- screen at `depth: 3` is now 3.5 KB, and what was dropped says so rather
351
- than going quiet.
352
-
353
- **Selecting several frames exports several screens.** It used to produce
354
- one pseudo-frame called "3 selected nodes" holding all of them, which
355
- `design` then wrote as a single file under that name — a folder whose
356
- whole point is one file per screen, with one file for three. The screens
357
- were in there, but nothing said where one ended and the next began.
358
-
359
- A selection of several frames now produces the same shape the whole-file
360
- export does, so the split, the index and `figma_frame` treat them
361
- identically. One frame selected is still one screen.
362
-
363
- This is also the scoping the all-screens command lacks: a file with forty
364
- frames in it need not be exported entire to build two of them. Select the
365
- screens you want and export those.
366
-
367
-
368
- ## 2.6.0
369
-
370
- ### Added
371
-
372
- **A whole Figma design, exported once and built screen by screen.**
373
-
374
- In Figma, **Grampro Kit → Export all screens for an agent** walks every page
375
- and takes each top-level frame. Then:
376
-
377
- ```bash
378
- npx @grampro/kit design ./design.json
379
- ```
380
-
381
- ```text
382
- Wrote 3 screen(s) to design/
383
- purchase-orders.json Purchase orders (Product) — 22 nodes
384
- suppliers.json Suppliers (Product) — 16 nodes
385
- settings.json Settings (Admin) — 12 nodes
386
- index.json the plan: every screen, shared theme
387
- ```
388
-
389
- The accuracy is in the theme rather than the convenience. Derived one
390
- frame at a time it disagrees with itself — a screen that is mostly white
391
- names a different background from one that is mostly grey — and the agent
392
- then themes each page differently. Read across every screen, once, it is
393
- well founded.
394
-
395
- `index.json` is the plan: every screen with its size, node count and the
396
- regions whose names repeat, plus that shared theme. It is the cheap place
397
- to be wrong. A design drawn without the component library names nothing
398
- directly, so every component is inferred, and a guess that `Action` is a
399
- Card rather than a Button costs one sentence there and five screens of
400
- rework later.
401
-
402
- **`figma_frame` reads a file.** Pass `designFile` with a path instead of
403
- `design` with the JSON. A chat input caps at around fifty thousand
404
- characters and a whole screen passes that; and a pasted export sits in the
405
- conversation twice, once as the argument and again inside the tool call,
406
- where a file appears only in the answer.
407
-
408
- **Every prop the library declares now carries a description.** 117 did
409
- not, and the passports are what an agent reads instead of the source, so
410
- an undescribed prop is one it guesses at. `DataGrid`'s `columns` and
411
- `data` had none at all. See 2.5.0's notes — this is that work, reaching
412
- npm for the first time.
413
-
414
- ### Changed
415
-
416
- **The Figma export costs a third of what it did.** The copy button takes
417
- minified JSON: `JSON.parse` cannot tell the difference, and indentation
418
- was most of the payload. Three fields that said nothing went with it —
419
- `radius: 0` on every square frame, `padding: [0,0,0,0]` on every frame
420
- without padding, and `align`/`justify` restating Figma's own defaults —
421
- along with the child frame Figma wraps around every icon, whose entire
422
- content was the word "Vector" when the frame's name (`search`, `plus`)
423
- was the whole signal.
424
-
425
- **It also says more about what it exports.** Node size and stroke colour
426
- are now included, because most designs are not built from the kit's Figma
427
- library and an agent has to infer every component from what it can see. A
428
- 36px pill with a label is a button; the same shape at 200px is a card. A
429
- white box with a border is a secondary button; without one it is a
430
- surface. Neither was visible before.
431
-
432
- ### Fixed
433
-
434
- **The design system generator laid out over itself.** Variant grids were
435
- placed by column count without measuring whether the row fitted, so 14 of
436
- 38 component sets overflowed the card holding them — Alert reached 2444px
437
- inside a 1320px section.
438
-
439
- **Text that was told a width kept its own.** Figma creates a text node
440
- auto-width, and setting it to fill is quietly ignored, so 23 layers across
441
- the file measured themselves from their own characters. NumberInput's
442
- value layer, created empty and filled in later by a component property,
443
- wrapped one glyph per line.
444
-
445
- **Alert's coloured edge did not match its card**, being sized before the
446
- card had any content and never clipped to its rounded corners. Toast drew
447
- the same edge with the same fault.
448
-
449
- **The uploader thumbnails and the dialog's intent badge rendered at full
450
- saturation**, which also hid the icon drawn on them in the same colour.
451
-
452
- **The plugin's name displayed as mojibake** in Figma's menu, and its
453
- build was never committed despite its README saying so, so a clone could
454
- not run it.
455
-
456
- ## 2.5.0
457
-
458
- ### Added
459
-
460
- **Every prop this library declares now carries a description.**
461
-
462
- 117 did not, and the passports are what an agent reads instead of the
463
- source, so an undescribed prop is one it has to guess at. The worst of them
464
- were not obscure: `DataGrid`'s `columns` and `data` had no description at
465
- all, and `label`, `error`, `description` and `onChange` were bare on
466
- Checkbox, Switch and RadioGroup.
467
-
468
- Where reading the code turned up something a type cannot say, that is in
469
- the description now:
470
-
471
- - `onCellEdit` fires only when the value actually changed, and returning a
472
- promise makes the edit optimistic - the cell reverts and shows the
473
- rejection's message if it fails, so reject with the reason you want read.
474
- - `CheckboxGroup`'s values accumulate in the order they were checked, not
475
- the order the options are declared, so sort before comparing selections.
476
- - `Checkbox.onCheckedChange` never fires with `"indeterminate"`.
477
- - `FileUploader` treats name, size and last-modified time as a file's
478
- identity when rejecting duplicates.
479
- - `NumberInput`'s `min` and `max` apply on blur rather than per keystroke,
480
- unless `clampBehavior` says otherwise.
481
- - `DatePicker.onChange` always hands back a `Date` at local midnight, even
482
- though `value` accepts a string or a number.
483
- **The Figma token belongs to your operating system now, not to a file in
484
- your repository.**
485
-
486
- ```bash
487
- npx @grampro/kit secret set figma
488
- ```
489
-
490
- It prompts with the echo off and stores the token where the platform keeps
491
- credentials: the Data Protection API on Windows, the keychain on macOS, the
492
- Secret Service on Linux. Anywhere else it falls back to a 0600 file in your
493
- config directory and says so rather than implying otherwise. The value is
494
- never passed as an argument, because arguments land in shell history and in
495
- every process listing on the machine; pipe it instead if you need to script
496
- it.
497
-
498
- `secret list` shows what is stored, masked, and names the backend. `secret
499
- remove figma` deletes it.
500
-
501
- The old `.env` route still works, so upgrading breaks nothing, but reading
502
- from one now prints a note telling you to move it. A credential sitting in a
503
- working tree is one `git add -A` away from being published, which is the
504
- whole reason for the change.
505
-
506
- **`mcp install` writes the configuration for you.**
507
-
508
- ```bash
509
- npx @grampro/kit mcp install # every agent it knows
510
- npx @grampro/kit mcp install --for codex
511
- ```
512
-
513
- Claude Code, Codex and Cursor each take the same stdio server and differ
514
- only in where the configuration lives and what format it is in. This writes
515
- `.mcp.json`, `.codex/config.toml` and `.cursor/mcp.json` respectively,
516
- merging into whatever is already there and leaving other servers alone.
517
-
518
- It is worth preferring over editing those files by hand, which is how this
519
- goes wrong: PowerShell's `Out-File -Encoding utf8` adds a byte-order mark,
520
- and every JSON parser then rejects the file with an error naming the first
521
- character and explaining nothing. This writes UTF-8 without a BOM and with
522
- LF endings, and strips a BOM from a file that already has one.
523
-
524
- It writes to the project root — the nearest folder above you with a `.git`
525
- or a `package.json` — and prints which one it chose. An agent reads these
526
- files from the folder it has open, so running the command from a
527
- subdirectory would have written somewhere nothing ever looks while still
528
- reporting success.
529
-
530
- It also now says what has to happen next, because writing the file is not
531
- the same as loading the server. A `.mcp.json` server is project-scoped and
532
- Claude Code asks before trusting one, so it sits at `Pending approval` until
533
- you agree; and the configuration is read when a session starts, so an editor
534
- window that was already open sees nothing until it reloads. Installing at
535
- user scope — `claude mcp add -s user grampro -- npx -y @grampro/kit mcp` —
536
- skips both, at the cost of not being shareable through a commit.
537
-
538
- ### Changed
539
-
540
- **Reading a Figma design no longer downloads the whole file.**
541
-
542
- Figma charges for file reads by the size of the response, and the frame
543
- listing was fetching the entire document — 710 KB on the file this was
544
- tested against — to answer a question about one screen. Two of those in a
545
- row is what earns a 429, and the lockout is long: a `Retry-After` of 395274
546
- was observed on a single token.
547
-
548
- The listing now uses `depth=1`, which returns the document's outline rather
549
- than its contents, and the chosen frame is fetched by id. A URL that already
550
- carries a `node-id` — what Figma copies when you right-click a frame — goes
551
- straight there in one call. Responses are still held for five minutes, so
552
- the usual list-then-read flow costs one outline and one frame.
553
-
554
- If a token is already rate limited, the Figma plugin export route still
555
- works; it reads the scene graph from inside Figma and never calls the API.
556
-
557
- **The MCP server starts in about the time an empty Node process takes.**
558
-
559
- An agent spawns the server once per session, and it was loading the CLI's
560
- own dependencies first — `fs-extra` and `yargs`, roughly 70ms, and of no use
561
- to a JSON-RPC loop. The `mcp` command is now routed before they load.
562
-
563
- The passports are also indexed on the first question rather than re-read
564
- from disk for each one.
565
-
566
- ### Fixed
567
-
568
- **`Select`'s `closeOnSelect` was documented backwards, in both directions.**
569
- Its description read "Keep the list open after choosing. Default false."
570
- while the code defaults it to `true` and closing is what the prop does. An
571
- agent reading that passport would have had both the behaviour and the
572
- default inverted - the failure mode that makes a wrong description worse
573
- than a missing one. MultiSelect's was already right.
574
-
575
- **Description coverage was measured against a denominator nobody could
576
- move.** It counted every prop, including the HTML and ARIA attributes that
577
- arrive through `extends ButtonHTMLAttributes` - 390 of the 507 undescribed
578
- props were those. So Button sat at 31% with its entire own API documented
579
- bar one prop, and eleven components stayed under the 40% threshold with
580
- nothing left to write. A gate that cannot be satisfied by doing the work it
581
- asks for gets ignored.
582
-
583
- It now measures the props the library authors, excluding inherited
584
- attributes and the handful whose meaning is library-wide (`className`,
585
- `style`, `ref` and friends) rather than restating them 27 times. On that
586
- measure the library is at 87% and the weakest component at 75%, so the
587
- threshold moves from 40 to 75 - a floor rather than an aspiration.
588
- **The rate-limit message no longer promises that waiting a minute will
589
- help.** It said to try again shortly, which is wrong for a cost-based limit
590
- whose window runs to tens of minutes or longer. It now passes Figma's own
591
- `Retry-After` through as given, without asserting a unit — the value has
592
- been seen in a range where seconds and milliseconds differ by three orders
593
- of magnitude — and points at the plugin route, which cannot be rate limited.
594
-
595
- ## 2.4.4
596
-
597
- ### Added
598
-
599
- **An MCP server, so an agent can ask about a component instead of recalling
600
- it.**
601
-
602
- ```bash
603
- npx @grampro/kit mcp
604
- ```
605
-
606
- Seven tools: `list_components`, `get_component`, `get_example`,
607
- `get_guidance`, `figma_frame`, `install_component` and `validate_usage`. The
608
- catalogue is 280 KB of passports and a page needs two or three components,
609
- which is the argument for answering on demand rather than pasting a document
610
- into the prompt.
611
-
612
- `validate_usage` is the one that earns its place. A page written from
613
- passports alone compiled on the first attempt apart from a single prop the
614
- model had remembered from elsewhere — every type, union value and handler
615
- signature the passports carried came out right. So the remaining failure
616
- mode is invention, and that is what this checks.
617
-
618
- No dependencies: MCP is newline-delimited JSON-RPC over stdio, and a server
619
- shipped inside a library every consumer installs should not drag an SDK
620
- along with it.
621
-
622
- Marked beta — the tools work and are tested, but their names and shapes may
623
- change.
624
-
625
- ## 2.4.3
626
-
627
- ### Fixed
628
-
629
- **`passport --check` reported every component as stale on a machine whose
630
- line endings differed from the one that generated the passport.**
631
-
632
- A passport records a hash of the source it was derived from, and that hash
633
- was taken over the file bytes exactly as they sat on disk. A checkout on
634
- Windows holds CRLF and one on Linux holds LF, so the same unchanged file
635
- produced two different hashes, and the check announced that all 27
636
- components had drifted when nothing had changed at all.
637
-
638
- It now normalises line endings before hashing, so the hash describes the
639
- content rather than the checkout. If you have been seeing
640
-
641
- ```
642
- passport stale - source changed since the passport was generated
643
- ```
644
-
645
- on components you never touched, this is why, and it stops.
646
-
647
- Running `npm run passport` once after upgrading rewrites the hashes in your
648
- own copies.
649
-
650
- ## 2.4.2
651
-
652
- ### Fixed
653
-
654
- **Your own class now beats a component's variant.**
655
-
656
- Restyling a variant used to fail silently. A rule like
657
-
658
- ```css
659
- .btn-brand { --bt-fill: #7c3aed; }
660
- ```
661
-
662
- worked on a plain button and was ignored the moment anyone wrote
663
- `variant="outline"`, because the shipped rule named two things
664
- (`.bt-root[data-variant="outline"]`) where yours named one, and the browser
665
- prefers the more specific selector. No error, nothing in devtools -- the
666
- colour simply did not apply.
667
-
668
- Selectors that carry an option you chose -- `variant`, `size`, `orientation`,
669
- `appearance`, `placement` and the rest -- are now written so they add no
670
- weight of their own. One class from you is enough, on any variant, in any
671
- setup:
672
-
673
- ```tsx
674
- <Button variant="outline" className="btn-brand">Save</Button>
675
- ```
676
-
677
- **State deliberately still wins.** Disabled, invalid, selected, loading and
678
- the other runtime states keep their weight, because a brand colour has no
679
- business painting over a disabled control. To restyle a state, name the
680
- state: `.btn-brand[data-disabled] { ... }`.
681
-
682
- 215 selectors across 26 stylesheets. Verified by capturing the computed
683
- styles of 3,716 elements across every demo in light and dark before and
684
- after: the only differences were opacity on the shimmer and spinner, which
685
- differ between two runs of the same build because they are mid-animation.
686
- Nothing else moved.
687
-
688
- Nothing in your code needs to change. If you had already worked around this
689
- with a more specific selector, that still wins.
690
-
691
- ## 2.4.1
692
-
693
- ### Fixed
694
-
695
- **The stylesheet import printed after an install is now one you can paste.**
696
-
697
- 2.4.0 could put the components under `src/`, and printed the path to the
698
- barrel from the project root -- which is correct from nowhere in particular.
699
- In a Next.js app it read:
700
-
701
- ```css
702
- @import "./src/component-lib/gbs.css" layer(components);
703
- ```
704
-
705
- while the file to edit was `src/app/globals.css`, two directories away. You
706
- had to work out the `../` yourself.
707
-
708
- The installer now finds your global stylesheet in the usual places
709
- (`src/app/globals.css`, `app/globals.css`, `src/styles/globals.css`,
710
- `src/index.css` and friends), names it, and writes the path from there:
711
-
712
- ```
713
- Add this to src/app/globals.css:
714
-
715
- @import "tailwindcss";
716
- @import "../component-lib/gbs.css" layer(components);
717
- ```
718
-
719
- Where no stylesheet is found it falls back to the path from the project root,
720
- as before. The header inside the generated `gbs.css` follows the same rule.
721
-
722
- Nothing about the installed components changed; this is the installer's
723
- output only.
724
-
725
- ## 2.4.0
726
-
727
- The first release published as **`@grampro/kit`**. `gbs-add-block` keeps
728
- working for now and installs the same components, but new work happens here.
729
-
730
- ### The package has a new name
731
-
732
- ```bash
733
- npx @grampro/kit add DataGrid
734
- ```
735
-
736
- `npx gbs-add-block` still resolves, and the `gbs-add-block` binary is still
737
- installed, so existing scripts keep running. Both will stay for a few
738
- releases before the old name is deprecated.
739
-
740
- ### Commands read as commands
741
-
742
- `add`, `list`, `skill` and `passport` are verbs now:
743
-
744
- ```bash
745
- npx @grampro/kit add Button DataGrid
746
- npx @grampro/kit list
747
- npx @grampro/kit skill --for claude
748
- npx @grampro/kit passport --check
749
- ```
750
-
751
- The old flag spellings (`-a Button`, `-skill`, `-passport`) still work and
752
- print the new spelling once. `--legacy` reaches the 1.x component set, which
753
- is where `--beta` went: it used to opt *in* to the current components, and
754
- there is nothing left to opt into.
755
-
756
- ### You choose where components land
757
-
758
- The destination was fixed at `component-lib/` in the directory you ran the
759
- command from, which put it beside `src/` rather than inside it for most
760
- projects. Now:
761
-
762
- - `--dest <path>` puts them wherever you want.
763
- - Failing that, an install already in the project wins, so a second `add`
764
- lands beside the first. That is how a custom `--dest` is remembered; no
765
- config file is written into your project.
766
- - Failing that, a project with a `src/` folder gets `src/component-lib/`,
767
- and everything else keeps `component-lib/` at the root.
768
-
769
- Nothing moves on its own: if you already have `component-lib/`, that is
770
- still where components go.
771
-
772
- The stylesheet import printed after an install now names the path the files
773
- actually landed at, relative to your project root.
774
-
775
- ### Fixed
776
-
777
- - `add Button --dest app/ui` tried to install a component called `app/ui`.
778
- A flag's value was being read as a component name; the same was true of
779
- `--for`.
780
-
781
- ### Internal
782
-
783
- The library source moved from `source/beta-components` to
784
- `source/components`, and the 1.x set from `source/components` to
785
- `source/legacy-components`. This changes nothing for installed components —
786
- the files you receive and their import paths are identical — but every
787
- `passport.json` has a new `sourceHash`, because that hash covers
788
- repo-relative paths.
789
-
790
- ## 2.3.0
791
-
792
- Published as a minor, but **it contains one breaking change to styling**. Read
793
- the upgrade note below before updating.
794
-
795
- ### Breaking: component CSS is no longer in a cascade layer
796
-
797
- Component rules used to live in `@layer gbs`, and 2.1.0 asked Tailwind v4
798
- projects to add `@layer gbs, utilities;` to their global CSS. Both are gone.
799
-
800
- A cascade layer loses to *any* unlayered stylesheet regardless of specificity,
801
- and Tailwind v3's preflight and Bootstrap's Reboot are both unlayered. Measured
802
- on 2.2.0: under Tailwind v3 the components rendered with `border: 0` and
803
- `padding-left: 0`; under Bootstrap every button was square. No host-side
804
- declaration could fix either, because nothing layered can outrank unlayered CSS.
805
-
806
- Component CSS now ships unlayered and wins on ordinary specificity, so Tailwind
807
- v3, Bootstrap and framework-free projects work with no setup at all.
808
-
809
- **What to change:**
810
-
811
- - Delete any `@layer gbs, utilities;` (or similar) line you added for 2.1.0.
812
- - Import the generated barrel instead of individual stylesheets:
813
- `@import "./component-lib/gbs.css";`
814
- - **Tailwind v4 only** — import it after the framework and into its components
815
- layer, or utilities stop overriding component rules:
816
- ```css
817
- @import "tailwindcss";
818
- @import "./component-lib/gbs.css" layer(components);
819
- ```
820
-
821
- The installer detects which line you need and prints it.
822
-
823
- ### Added
824
-
825
- - **`<DataGrid ai />`** — a natural-language input above the grid, driven by an
826
- adapter the application supplies through `<GramproAIProvider>`. No model is
827
- bundled, downloaded or named, and with no provider the prop renders nothing.
828
- - **WebMCP** — `registerGridTool(agent)` exposes one tool, `operate_grid`, to a
829
- browser agent. Its schema is generated from the live grid. Needs no model.
830
- - **Generated stylesheet barrel** — the CLI writes `component-lib/gbs.css` on
831
- every install, so the import list stays correct as you add components.
832
- - **Thin scrollbars** across every scrolling surface, themeable from `:root`
833
- via `--gbs-scrollbar-width`, `--gbs-scrollbar-thumb`,
834
- `--gbs-scrollbar-thumb-hover` and `--gbs-scrollbar-track`. Add
835
- `class="gbs-scroll"` to your own scroll areas to match. Scroll containers the
836
- kit does not own are left alone.
837
-
838
- ### Security
839
-
840
- - Pinned `dompurify` to `^3.4.16` via `overrides`, clearing two DOM XSS
841
- advisories reachable through `@grampro/headless-helpers → jspdf`. The 2.0 beta
842
- components never used that chain. `npm audit` is now clean.
843
- - Removed the `path` dependency. `require("path")` resolves to Node's built-in,
844
- so the npm package was inert — a deprecated userland shim sitting in the
845
- dependency tree for no reason.
846
-
847
- ### Documentation
848
-
849
- - Documented what leaves the page with a remote AI adapter: the runtime contract
850
- carries real values from low-cardinality columns. Mark personal columns
851
- `semantics: { email: { pii: true } }` — a PII column is denied *and* never
852
- summarised — or pass `stats: false`. Note that `policy.denyFilter` governs what
853
- may be *done* with a column, not whether it may be *described*.
854
- - Corrected the agent skill's install instructions, which still told agents to
855
- import each stylesheet individually with no `layer()`.
856
-
857
- ### Packaging
858
-
859
- - Test files are no longer published (35 files, 233 KB). They were filtered out
860
- at install time anyway.
861
- - Added `repository`, `homepage` and `bugs` metadata.
1
+ # Changelog
2
+
3
+ ## 2.7.3
4
+
5
+ ### Added
6
+
7
+ **`<AgentSurface>`: a page that describes itself to a browser agent.** Wrap a
8
+ page in it and every form field and data grid inside registers itself — what
9
+ it holds, what it accepts, which operations it supports — from its own props,
10
+ as it is rendered now. In a browser with WebMCP one tool, `operate_page`, is
11
+ registered: `describe` returns that live manifest, `operate` sends intents to
12
+ one component by id. No tool definitions are written and none can drift from
13
+ the UI, because each description is the component's own.
14
+
15
+ ```tsx
16
+ <AgentSurface>
17
+ <Input label="Name" maxLength={80} />
18
+ <Select label="Team" options={teams} />
19
+ <DatePicker label="Due date" min="2026-01-01" max="2026-12-31" />
20
+ <DataGrid aria-label="Customers" data={rows} columns={columns} />
21
+ </AgentSurface>
22
+ ```
23
+
24
+ Every action goes through the targeted component's own validator, with the
25
+ guarantees a single component already had: no repair, no retry, confirmation
26
+ before anything irreversible, refusals named by the layer that made them. The
27
+ surface adds addressing, not a second way in. Password, hidden and file inputs
28
+ are never offered. `useAgentSurface()` hands the same surface to an
29
+ application that runs its own agent loop, so it works without WebMCP too.
30
+
31
+ Input, Textarea, NumberInput, Checkbox, Switch, RadioGroup, Select,
32
+ MultiSelect, DatePicker, DateRangePicker and DataGrid join. A component outside
33
+ a surface pays for one `useContext`; the surface itself is about 7 KB gzip, in
34
+ an application that renders it. The browser test drives the real components
35
+ through the tool and checks the page's own state, not the DOM.
36
+
37
+ ### Fixed
38
+
39
+ **An agent's write to a Checkbox or Switch reported success and changed
40
+ nothing.** The field runtime set `checked` and dispatched `change`, but React
41
+ hears a checkbox through `click`, so a controlled box flipped in the DOM while
42
+ the application's state — and `onCheckedChange` — never moved. Checkable
43
+ fields are now clicked when they are not already in the asked state, which is
44
+ what a person does. Found by the new browser test; unit tests could not see it.
45
+
46
+ **A NumberInput written by an agent depended on the field's locale.** The
47
+ number was written into the element as text, so `1.5` became `"1.5"`, which a
48
+ `de-DE` field reads as fifteen. Inside a surface a NumberInput is now written
49
+ as a number through its own commit path, with its `min`, `max` and snapping.
50
+
51
+ **A RadioGroup could not be driven as a group.** Its ref is not a group
52
+ element, so a field agent could reach at most one radio. Inside a surface it
53
+ is one choice among its options.
54
+
55
+ **`shared/` could stay behind after an update.** The installer compared
56
+ versions only, and 2.7.0 changed `shared/` without changing its version, so a
57
+ project that already had shared 1.3.0 kept the old files. It now compares the
58
+ files as well, and `shared/` is 1.4.0.
59
+
60
+ **`passport` on TypeScript 7 needed a manual step.** 2.7.2 fell back to a
61
+ TypeScript 6 installed under an alias, and asked for one otherwise.
62
+ It now works on any TypeScript, or none, with no setup. The project's
63
+ TypeScript is used when it has the compiler API; otherwise the exact version
64
+ the kit is built with (6.0.3) is fetched once and cached per user. The fetch
65
+ goes through the user's own npm, installs one pinned version with install
66
+ scripts disabled, is verified before use, and is moved into the cache only
67
+ once complete; nothing in the project changes. `GRAMPRO_KIT_OFFLINE=1` turns
68
+ it off, and then `doctor` falls back to file-level changes and says why.
69
+ Bundling TypeScript instead would have added 24 MB to every `npx` run, and a
70
+ second, simpler extractor would have disagreed with the first.
71
+
72
+ **`passport --check` failed for every component in a project, edited or not.**
73
+ The source hash was labelled with paths as they sit in the project
74
+ (`src/component-lib/…`) rather than as they sit in the kit, and each passport
75
+ was stamped with the project's own version, so no regenerated passport ever
76
+ matched the one a component shipped with. Both now use the kit's terms — the
77
+ labels through one shared function, the version from the install record — so
78
+ an untouched component passes, an edited one is reported, and the hint names
79
+ `npx @grampro/kit passport` rather than this repository's npm script.
80
+
81
+ ### Changed
82
+
83
+ **The interactive picker fits any terminal, and no longer jumps.** It cleared
84
+ the screen on every key and printed all 27 components, so it needed a
85
+ full-screen terminal and flickered as you moved. It now redraws only its own
86
+ lines, shows a scrolling window that fits the terminal, and clips every line to
87
+ its width. Type to search — by name, group or purpose — space to select,
88
+ ctrl+a for everything shown, enter to install, esc or ctrl+c to cancel, which
89
+ the old one ignored. Each row shows the component's group and whether it is
90
+ already installed; the line below shows what the component is for, from its
91
+ passport. `npx @grampro/kit add` with no names now opens it in a terminal, as
92
+ `-i` does. The install log is in colour when it goes to a terminal, and plain
93
+ when piped or when `NO_COLOR` is set. The MCP server's `install_component`,
94
+ which reads that log, runs it with colour off and strips any escape codes, so a
95
+ host that sets `FORCE_COLOR` cannot hide a refused install from the agent.
96
+
97
+ **The skill, `get_guidance` and llms.txt know about `<AgentSurface>`**: the
98
+ forms and data-grid references say when to use it, and the install reference
99
+ says an agent must always name components rather than open the picker.
100
+
101
+ **A DataGrid builds its agent only when something will use it** — the `ai`
102
+ prop, as before, or now an `<AgentSurface>` around it. A grid outside a
103
+ surface and without `ai` is unchanged.
104
+
105
+ ## 2.7.2
106
+
107
+ ### Fixed
108
+
109
+ **`add` overwrote components you had edited, and said it had succeeded.**
110
+ Components are copied into your project so that you can change them, but
111
+ running `add` on one that was already installed replaced the whole folder
112
+ unconditionally. It printed "installed successfully" and the edits were gone.
113
+ The same happened through the MCP server: `install_component` runs `add`, so an
114
+ agent asked to "install Button" in a project with a customised Button
115
+ discarded the customisation.
116
+
117
+ Now every install records what it wrote — a hash of each file and a copy of
118
+ the component's contract, in `.install-manifest.json` beside the component —
119
+ and `add` checks that record first. An untouched component is updated. An
120
+ edited one is left in place: `add` lists the files that changed, prints how to
121
+ see more, and exits 1, while the other components in the same command still
122
+ install. Only `--force` replaces an edited component. `shared/` has worked this
123
+ way since it was introduced; components never did.
124
+
125
+ Components installed before this release have no record. An untouched one is
126
+ recognised from its passport's `sourceHash` — recomputed over your copy with
127
+ the labels the kit hashed it with, which a test holds to the shipped value for
128
+ all 27 — and is updated normally. One that differs from this version cannot be
129
+ told apart from an older untouched copy, so it is kept, not guessed at.
130
+
131
+ **`install_component` told an agent nothing when it was refused.** The
132
+ installer explains a refusal on stderr, and the tool only returned stdout — so
133
+ the agent got "Install failed" followed by stylesheet instructions. It now
134
+ returns what was installed, what was kept and why, and advice not to force it.
135
+
136
+ **`passport` crashed in any project on TypeScript 7.** TypeScript 7 is a
137
+ native rewrite whose npm package does not expose the compiler API passports
138
+ are generated with, and it is what `npm i -D typescript` installs today. The
139
+ command failed with `Cannot read properties of undefined (reading
140
+ 'ReactJSX')`. It now checks for the API rather than for the package, falls
141
+ back to a TypeScript 6 installed under an alias, and otherwise says exactly
142
+ that, with the one-line fix.
143
+
144
+ **`validate_usage` invented props from inside expressions.** Attribute names
145
+ were matched anywhere in a tag, so `title={ok ? first : second}` reported
146
+ `first` and `second` as unknown props. The tag scanner now reads names only at
147
+ the top level, outside braces and quotes. It is shared with `doctor`, and stays
148
+ linear on hostile input.
149
+
150
+ ### Added
151
+
152
+ **`npx @grampro/kit doctor`** — what you changed in your copies, what the new
153
+ version changes, and which call sites in your code an update would break.
154
+
155
+ It compares three versions of each component: the one you installed, your copy
156
+ now, and the one the CLI carries. It compares them as contracts — props,
157
+ accepted values, handlers, exports, agent operations — because a text diff of
158
+ `Button.tsx` shows which lines moved, not that `variant` stopped accepting
159
+ `"ghost"`. Edits from both sides to the same value set are merged rather than
160
+ called a conflict; a conflict is an incompatible pair, such as you removing a
161
+ prop that upstream changed. Then it scans your source and names the call sites
162
+ that pass something the replacement would not accept.
163
+
164
+ It never writes. `--json` gives the same report to agents and CI. The agent
165
+ skill now tells agents to run it, and never to retry `add` with `--force` on
166
+ someone else's edits. A new docs page, *Updating components*, covers all of it.
167
+
168
+ ## 2.7.1
169
+
170
+ ### Security
171
+
172
+ **get_guidance could read any .md file on disk.** It appended `.md` to a
173
+ caller-supplied `topic` and joined it onto the references folder with no
174
+ traversal guard, so `topic = "../../../../CHANGELOG"` returned the repo
175
+ CHANGELOG and the same trick reached any `.md` file the server process could
176
+ open. Found by a VAPT pass driving the tool with traversal payloads. The
177
+ server runs with your own authority, so it is a confused-deputy hole rather
178
+ than privilege escalation — but a tool that advertises nine fixed topics
179
+ should not become a file-exfiltration primitive for a prompt-injected agent.
180
+ It now refuses any `topic` that is not one of the shipped guidance names, with
181
+ a resolved-prefix check behind the allowlist. A regression test drives the
182
+ same payloads.
183
+
184
+ **Affected: 2.4.4 through 2.7.0** — every release that has shipped the MCP
185
+ server. Each was checked by running the same payload against its published
186
+ tarball. 2.4.3 and earlier have no MCP server and are not affected. A project
187
+ that installed the package locally needs `npm i -D @grampro/kit@latest`. One
188
+ configured with `npx -y @grampro/kit mcp` may keep running a cached copy; check
189
+ with `npx -y @grampro/kit --version`, and if it is not 2.7.1, run
190
+ `npx -y @grampro/kit@2.7.1 mcp` once to replace it.
191
+
192
+ ## 2.7.0
193
+
194
+ ### Added
195
+
196
+ **A design read from a URL now names its components.** An instance built
197
+ from a library is the most valuable thing a design can contain — it says
198
+ "this is a Button" instead of leaving an agent to infer it from a layer
199
+ name and a fill. The URL route recorded `instanceOf: "1:234"`, an opaque
200
+ id nothing could be done with, and discarded the maps that name it, which
201
+ Figma sends in the same response.
202
+
203
+ It resolves them now, preferring the component set's name over the
204
+ variant's: a variant is called `Size=md, Variant=primary` and its set is
205
+ called `Button`, and Button is what the agent is going to write. An
206
+ instance whose master is not in the response keeps its id rather than
207
+ guessing.
208
+
209
+ **Every component now says what it is for.** A passport had two halves: the
210
+ derived one, generated from the TypeScript, and the authored one — `purpose`,
211
+ `intentDomains`, `accessibility`, `composition`, `safeMutations`. The generator
212
+ emits empty shells for the authored half because none of it is in the types,
213
+ and only Button and DataGrid had ever been filled in. The other 25 are written
214
+ now.
215
+
216
+ `safeMutations` is the one an agent acts on: which props it may change without
217
+ altering behaviour, and which it may not, with the reason. An agent restyling a
218
+ FileUploader should be free to change `size` and `preview` and nowhere near
219
+ `endpoint`, `headers` or `transport` — that component is the one where a wrong
220
+ prop sends someone's data somewhere. `get_component` now returns all of it;
221
+ until this release it served `purpose` and dropped the rest, so the authored
222
+ half reached nobody.
223
+
224
+ Where no value in the schema's `intentDomains` enum honestly describes a
225
+ component, the list is left empty rather than filled for symmetry. Nine of them
226
+ are presentational, and a wrong routing hint is worse than none.
227
+
228
+ **A field runtime, so nine components are agent-operable instead of one.**
229
+ `setValue`, `clear`, `focus`, and `open`/`close` for the two that have
230
+ something to disclose, on Input, Textarea, NumberInput, Checkbox, Switch,
231
+ RadioGroup, Combobox and DatePicker.
232
+
233
+ The grid earned a runtime of its own because its operations are a language:
234
+ filter, sort, pin, paginate, export. A field has one verb, so the verb lives in
235
+ `shared/core/agent` once rather than eight times, and each component supplies
236
+ only a contract built from its own props. The same five layers rule on it —
237
+ schema, reference, coercion, policy, plausibility — so a write to a read-only
238
+ field is refused by name, an option that does not exist comes back with the
239
+ nearest one that does, and a number outside `min`/`max` is told which bound it
240
+ crossed rather than being silently clamped.
241
+
242
+ The open question was whether the executor abstraction generalised. A grid is
243
+ driven through an imperative API and a field is not, and the architecture notes
244
+ had flagged that as unproven. It holds: Combobox and DatePicker are driven
245
+ through their handles, the other six through the element their ref already
246
+ returns, and nothing above the executor knows which. `setValue` joins
247
+ `ComboboxHandle` and `DatePickerHandle` as a public method; the six that expose
248
+ the DOM element needed no API change at all, because the technique
249
+ `Input.clear()` has always used — the prototype setter plus a real event — is
250
+ the same write with a value in it.
251
+
252
+ `useFieldAgent` binds a contract to a ref. No component grew a prop for this.
253
+
254
+ **The authored passports had no schema of their own.** A `passport.manual.json`
255
+ pointed at `passport-v1.schema.json`, which describes the *merged* passport —
256
+ so an editor validating one reported all eight generated fields as missing
257
+ (`passportVersion`, `identity`, `source`, `props`, `events`, `slots`, `states`,
258
+ `coverage`), and `additionalProperties: false` made errors of the four keys only
259
+ an overlay has: `component`, `propNotes`, `notImplemented`, `$operationsNote`.
260
+
261
+ It was wrong on the two files that had it before and merely quiet; at
262
+ twenty-seven it is the thing that teaches people to ignore squiggles.
263
+ `schema/passport-overlay-v1.schema.json` describes what an overlay may
264
+ actually contain, referencing the merged schema for the fields they share so
265
+ the two cannot drift. A test holds every overlay to it, which also catches a
266
+ mistyped key — previously a silent no-op, since the merge only reads the names
267
+ it knows.
268
+
269
+ ### Security
270
+
271
+ **A failed JSON parse quoted the file back.** V8 puts the first ten or so
272
+ characters of its input into the error it throws, so pointing `designFile` at
273
+ something that was not JSON returned a slice of it:
274
+
275
+ That is not JSON: Unexpected token 's', "sk-proj-RE"... is not valid JSON
276
+
277
+ Ten bytes is not an extraction, but it is enough to confirm that a path
278
+ exists and what kind of file sits there, and an agent can be pointed at paths
279
+ in a loop. The server runs with your own authority and the agent could read
280
+ the file directly, so this was hygiene rather than a hole — and it cost
281
+ nothing to close. The three parse sites in `figma.cjs`, `design.cjs` and
282
+ `install.cjs` now name the file and say what was expected, without quoting
283
+ any of it.
284
+
285
+ **The package no longer installs a build toolchain.**
286
+ `@grampro/headless-helpers` declared `rollup`, its plugins and `typescript`
287
+ as runtime dependencies, so installing the kit pulled a compiler and a
288
+ bundler into `node_modules` and carried their advisories — an arbitrary file
289
+ write in Rollup and two ReDoS in picomatch, none of which any of this code
290
+ runs. Requiring `^1.0.22`, which moved them to devDependencies, removes 84
291
+ packages from a clean install and leaves the package at zero advisories.
292
+
293
+ **Mermaid rendered with sanitisation off.** The diagram component ran with
294
+ `securityLevel: "loose"`, which permits HTML in labels and click handlers,
295
+ and its output goes through `dangerouslySetInnerHTML`. Nothing was
296
+ exploitable: the only diagram text comes from our own MDX at build time, and
297
+ in fact no page renders one. But the two together are a trap for whoever
298
+ first renders a diagram from anything else, so it is `"strict"` now.
299
+
300
+ ### Changed
301
+
302
+ **The docs lead with the URL route.** Pasting a Figma link is one step;
303
+ the plugin route is five, and every one is a place to stop. The reason it
304
+ was necessary for designs built from a component library is now gone, so
305
+ the page says plainly: use the URL for a screen, and the plugin when you
306
+ want a whole product in one pass, or have no token, or are rate limited.
307
+
308
+ **`figma_frame` describes the URL route first.** Its own description still
309
+ said `designFile` was "the one to use for a whole screen", which was true
310
+ only while the URL route could not resolve component names. An agent reads
311
+ that description before it picks a route, so it was steering towards the long
312
+ way round. It now leads with the URL and keeps the file export for what the
313
+ file export is actually better at: a whole product in one pass, or no usable
314
+ token. The `llms.txt` preamble said nothing about the URL route at all, and
315
+ now says it is the shortest path from a design to a screen.
316
+
317
+ **The components are out of beta.** Only the MCP server still carries the
318
+ label, and it carries it in both places that matter: the docs page and the
319
+ `figma_frame` tool description an agent reads.
320
+
321
+ The one that was doing damage was in the agent skill, which is always in an
322
+ agent's context. It said **"Always pass `--beta`"** and explained that without
323
+ it you get the 1.x set. That had inverted: `--beta` became a no-op that prints
324
+ a deprecation notice, 2.x is what `add` installs, and `--legacy` is what
325
+ reaches 1.x. An agent following the skill was passing a dead flag and being
326
+ told the opposite of what the CLI does. The flag table now documents
327
+ `--legacy` instead, and says plainly that the 1.x set is unmaintained.
328
+
329
+ Also gone from the CLI's own output ("Redesigned beta components" when a name
330
+ does not match), the `llms.txt` preamble, which told every model that read the
331
+ site that the components were beta and their APIs might change, and the README
332
+ and architecture notes. `--beta` is still accepted, because scripts use it.
333
+
334
+ ### Fixed
335
+
336
+ **Headings on the translated pages had meaningless ids.** The slugger kept
337
+ only `[a-z0-9\s-]`, which leaves a Malayalam heading with nothing but its
338
+ spaces — so every heading fell through to `section` or to the `-` its spaces
339
+ became, and then took its id from the duplicate counter:
340
+
341
+ id="-" id="--1" id="--2" id="-structured-data" id="section-2"
342
+
343
+ That counter is positional, so adding a heading renumbered every anchor below
344
+ it: no link to one could be relied on, and four prop-table links on
345
+ `ml/breadcrumb` and `ml/button` pointed at the English anchors and had never
346
+ resolved. The class keeps letters, digits and combining marks in any script
347
+ now, so those headings slug to their own text. `\p{M}` matters as much as
348
+ `\p{L}` here: Malayalam vowel signs are combining marks, and dropping them
349
+ mangles the text rather than emptying it. Every ASCII heading slugs to the
350
+ same string as before, so no existing anchor moved.
351
+
352
+ `pnpm check:anchors` in `docs-site` now checks every in-site fragment against
353
+ the headings its target page actually offers, because a dead anchor does not
354
+ 404 — the page loads and ignores it.
355
+
356
+ ## 2.6.3
357
+
358
+ ### Fixed
359
+
360
+ **The text LLMs read was teaching a deprecated command.** `llms.txt` has a
361
+ hand-written preamble, so it lagged behind the CLI: it told every model
362
+ that reached the site to run `npx @grampro/kit -a <Component>`, a spelling
363
+ the CLI itself marks "Deprecated. Use: add". It now says `add`, and names
364
+ the Figma plugin, the whole-design export and `designFile`, none of which
365
+ it mentioned.
366
+
367
+ **The plugin's install link was invisible to every LLM.** It is rendered by
368
+ a component, and the Markdown conversion passed that through verbatim — so
369
+ `llms-full.txt` and every `.md` page carried a dangling `<FigmaPlugin />`
370
+ tag where the URL should have been, and the plugin's address appeared
371
+ nowhere a model could read it. The conversion emits a Markdown link now,
372
+ and the URL lives in one place so the button and the converter cannot
373
+ disagree about it.
374
+
375
+ **The Figma plugin's listing is in review**, which Figma is quoting at a
376
+ few weeks. Every place that links it now says so and gives the interim
377
+ path: clone the repository and import `figma-plugin/manifest.json` through
378
+ **Plugins → Development → Import plugin from manifest...** in the desktop
379
+ app. The build is committed, so there is nothing to compile.
380
+
381
+ **Six links in the docs went nowhere.** `datagrid.mdx` and
382
+ `fileuploader.mdx` pointed at `#column-options`, `#grid-api-ref`,
383
+ `#server-side-pagination`, `#upload-protocol`, `#custom-transport` and
384
+ `#server-implementation` as same-page anchors. Those sections moved to
385
+ child pages when the docs were split and the links were never updated, so
386
+ they had been doing nothing since. They point at the child pages now.
387
+
388
+ ### Changed
389
+
390
+ **Every command is listed in one place.** `list` and `passport` appeared
391
+ in no user-facing documentation at all, and the rest were scattered across
392
+ three pages, so there was nowhere to look to find out what the CLI does.
393
+ The README now carries the lot in a table.
394
+
395
+ ## 2.6.2
396
+
397
+ ### Changed
398
+
399
+ **The agent skill now knows the MCP server exists.** It described the
400
+ component library and never mentioned the server, so an agent holding both
401
+ had nothing to tell it which to trust. The skill is a summary; the server
402
+ answers from passports generated from the source, so where they differ the
403
+ server is right and the skill is stale — and it now says so, along with
404
+ calling `get_component` before writing a component and `validate_usage`
405
+ before reporting back.
406
+
407
+ The only previous mention of "MCP" in the skill was WebMCP, the DataGrid's
408
+ browser-agent surface, which is a different thing entirely.
409
+
410
+ **Neither needs the other, and the docs now say so.** The server carries
411
+ its own copy of the guidance inside the package, so `get_guidance` answers
412
+ in a project with no `.gbs/` folder at all. What they do together is the
413
+ useful part: the skill is always in an agent's context and the server is
414
+ not, so the skill is what tells an agent to ask and the server is what it
415
+ asks.
416
+
417
+ The two pages now read in the order they are used — Theming, Agent skill,
418
+ MCP server.
419
+
420
+ ## 2.6.1
421
+
422
+ ### Changed
423
+
424
+ **A screen costs about half what it did to read.** Measured on a real
425
+ purchase-orders design: 42.9 KB to 23.9 KB, roughly 13,300 tokens to
426
+ 7,400.
427
+
428
+ Eight table rows exported as eight sibling subtrees of identical shape
429
+ whose text differed — 48% of that screen was the same row said seven
430
+ more times. The first is now given in full, because the agent builds from
431
+ its shape, and the rest become `repeatedText`: one array of strings per
432
+ row, which is closer to what a DataGrid's `data` prop wants than the
433
+ subtrees were.
434
+
435
+ Runs are matched on structure rather than appearance. Keyed on style, the
436
+ rows never matched at all: a striped table alternates its background, so
437
+ every consecutive pair looked like a different shape and nothing
438
+ collapsed. Where the fills do vary across a run they come back as
439
+ `repeatedFills`, which is how the striping and the per-status badge
440
+ colours stay legible.
441
+
442
+ Only runs of three or more collapse, and only where there is depth to
443
+ save: two of a thing is a pair, not a list, and a row of labels has
444
+ nothing inside it worth folding.
445
+
446
+ ### Fixed
447
+
448
+ **`depth` is honoured on the plugin and file routes.** The tool has always
449
+ offered it — "Lower it if the payload is large" — and only ever applied
450
+ it to a design fetched from a URL. A caller who read the schema and
451
+ lowered it to cut a large payload got the large payload back. The same
452
+ screen at `depth: 3` is now 3.5 KB, and what was dropped says so rather
453
+ than going quiet.
454
+
455
+ **Selecting several frames exports several screens.** It used to produce
456
+ one pseudo-frame called "3 selected nodes" holding all of them, which
457
+ `design` then wrote as a single file under that name — a folder whose
458
+ whole point is one file per screen, with one file for three. The screens
459
+ were in there, but nothing said where one ended and the next began.
460
+
461
+ A selection of several frames now produces the same shape the whole-file
462
+ export does, so the split, the index and `figma_frame` treat them
463
+ identically. One frame selected is still one screen.
464
+
465
+ This is also the scoping the all-screens command lacks: a file with forty
466
+ frames in it need not be exported entire to build two of them. Select the
467
+ screens you want and export those.
468
+
469
+
470
+ ## 2.6.0
471
+
472
+ ### Added
473
+
474
+ **A whole Figma design, exported once and built screen by screen.**
475
+
476
+ In Figma, **Grampro Kit → Export all screens for an agent** walks every page
477
+ and takes each top-level frame. Then:
478
+
479
+ ```bash
480
+ npx @grampro/kit design ./design.json
481
+ ```
482
+
483
+ ```text
484
+ Wrote 3 screen(s) to design/
485
+ purchase-orders.json Purchase orders (Product) — 22 nodes
486
+ suppliers.json Suppliers (Product) — 16 nodes
487
+ settings.json Settings (Admin) — 12 nodes
488
+ index.json the plan: every screen, shared theme
489
+ ```
490
+
491
+ The accuracy is in the theme rather than the convenience. Derived one
492
+ frame at a time it disagrees with itself — a screen that is mostly white
493
+ names a different background from one that is mostly grey — and the agent
494
+ then themes each page differently. Read across every screen, once, it is
495
+ well founded.
496
+
497
+ `index.json` is the plan: every screen with its size, node count and the
498
+ regions whose names repeat, plus that shared theme. It is the cheap place
499
+ to be wrong. A design drawn without the component library names nothing
500
+ directly, so every component is inferred, and a guess that `Action` is a
501
+ Card rather than a Button costs one sentence there and five screens of
502
+ rework later.
503
+
504
+ **`figma_frame` reads a file.** Pass `designFile` with a path instead of
505
+ `design` with the JSON. A chat input caps at around fifty thousand
506
+ characters and a whole screen passes that; and a pasted export sits in the
507
+ conversation twice, once as the argument and again inside the tool call,
508
+ where a file appears only in the answer.
509
+
510
+ **Every prop the library declares now carries a description.** 117 did
511
+ not, and the passports are what an agent reads instead of the source, so
512
+ an undescribed prop is one it guesses at. `DataGrid`'s `columns` and
513
+ `data` had none at all. See 2.5.0's notes — this is that work, reaching
514
+ npm for the first time.
515
+
516
+ ### Changed
517
+
518
+ **The Figma export costs a third of what it did.** The copy button takes
519
+ minified JSON: `JSON.parse` cannot tell the difference, and indentation
520
+ was most of the payload. Three fields that said nothing went with it —
521
+ `radius: 0` on every square frame, `padding: [0,0,0,0]` on every frame
522
+ without padding, and `align`/`justify` restating Figma's own defaults —
523
+ along with the child frame Figma wraps around every icon, whose entire
524
+ content was the word "Vector" when the frame's name (`search`, `plus`)
525
+ was the whole signal.
526
+
527
+ **It also says more about what it exports.** Node size and stroke colour
528
+ are now included, because most designs are not built from the kit's Figma
529
+ library and an agent has to infer every component from what it can see. A
530
+ 36px pill with a label is a button; the same shape at 200px is a card. A
531
+ white box with a border is a secondary button; without one it is a
532
+ surface. Neither was visible before.
533
+
534
+ ### Fixed
535
+
536
+ **The design system generator laid out over itself.** Variant grids were
537
+ placed by column count without measuring whether the row fitted, so 14 of
538
+ 38 component sets overflowed the card holding them — Alert reached 2444px
539
+ inside a 1320px section.
540
+
541
+ **Text that was told a width kept its own.** Figma creates a text node
542
+ auto-width, and setting it to fill is quietly ignored, so 23 layers across
543
+ the file measured themselves from their own characters. NumberInput's
544
+ value layer, created empty and filled in later by a component property,
545
+ wrapped one glyph per line.
546
+
547
+ **Alert's coloured edge did not match its card**, being sized before the
548
+ card had any content and never clipped to its rounded corners. Toast drew
549
+ the same edge with the same fault.
550
+
551
+ **The uploader thumbnails and the dialog's intent badge rendered at full
552
+ saturation**, which also hid the icon drawn on them in the same colour.
553
+
554
+ **The plugin's name displayed as mojibake** in Figma's menu, and its
555
+ build was never committed despite its README saying so, so a clone could
556
+ not run it.
557
+
558
+ ## 2.5.0
559
+
560
+ ### Added
561
+
562
+ **Every prop this library declares now carries a description.**
563
+
564
+ 117 did not, and the passports are what an agent reads instead of the
565
+ source, so an undescribed prop is one it has to guess at. The worst of them
566
+ were not obscure: `DataGrid`'s `columns` and `data` had no description at
567
+ all, and `label`, `error`, `description` and `onChange` were bare on
568
+ Checkbox, Switch and RadioGroup.
569
+
570
+ Where reading the code turned up something a type cannot say, that is in
571
+ the description now:
572
+
573
+ - `onCellEdit` fires only when the value actually changed, and returning a
574
+ promise makes the edit optimistic - the cell reverts and shows the
575
+ rejection's message if it fails, so reject with the reason you want read.
576
+ - `CheckboxGroup`'s values accumulate in the order they were checked, not
577
+ the order the options are declared, so sort before comparing selections.
578
+ - `Checkbox.onCheckedChange` never fires with `"indeterminate"`.
579
+ - `FileUploader` treats name, size and last-modified time as a file's
580
+ identity when rejecting duplicates.
581
+ - `NumberInput`'s `min` and `max` apply on blur rather than per keystroke,
582
+ unless `clampBehavior` says otherwise.
583
+ - `DatePicker.onChange` always hands back a `Date` at local midnight, even
584
+ though `value` accepts a string or a number.
585
+ **The Figma token belongs to your operating system now, not to a file in
586
+ your repository.**
587
+
588
+ ```bash
589
+ npx @grampro/kit secret set figma
590
+ ```
591
+
592
+ It prompts with the echo off and stores the token where the platform keeps
593
+ credentials: the Data Protection API on Windows, the keychain on macOS, the
594
+ Secret Service on Linux. Anywhere else it falls back to a 0600 file in your
595
+ config directory and says so rather than implying otherwise. The value is
596
+ never passed as an argument, because arguments land in shell history and in
597
+ every process listing on the machine; pipe it instead if you need to script
598
+ it.
599
+
600
+ `secret list` shows what is stored, masked, and names the backend. `secret
601
+ remove figma` deletes it.
602
+
603
+ The old `.env` route still works, so upgrading breaks nothing, but reading
604
+ from one now prints a note telling you to move it. A credential sitting in a
605
+ working tree is one `git add -A` away from being published, which is the
606
+ whole reason for the change.
607
+
608
+ **`mcp install` writes the configuration for you.**
609
+
610
+ ```bash
611
+ npx @grampro/kit mcp install # every agent it knows
612
+ npx @grampro/kit mcp install --for codex
613
+ ```
614
+
615
+ Claude Code, Codex and Cursor each take the same stdio server and differ
616
+ only in where the configuration lives and what format it is in. This writes
617
+ `.mcp.json`, `.codex/config.toml` and `.cursor/mcp.json` respectively,
618
+ merging into whatever is already there and leaving other servers alone.
619
+
620
+ It is worth preferring over editing those files by hand, which is how this
621
+ goes wrong: PowerShell's `Out-File -Encoding utf8` adds a byte-order mark,
622
+ and every JSON parser then rejects the file with an error naming the first
623
+ character and explaining nothing. This writes UTF-8 without a BOM and with
624
+ LF endings, and strips a BOM from a file that already has one.
625
+
626
+ It writes to the project root — the nearest folder above you with a `.git`
627
+ or a `package.json` — and prints which one it chose. An agent reads these
628
+ files from the folder it has open, so running the command from a
629
+ subdirectory would have written somewhere nothing ever looks while still
630
+ reporting success.
631
+
632
+ It also now says what has to happen next, because writing the file is not
633
+ the same as loading the server. A `.mcp.json` server is project-scoped and
634
+ Claude Code asks before trusting one, so it sits at `Pending approval` until
635
+ you agree; and the configuration is read when a session starts, so an editor
636
+ window that was already open sees nothing until it reloads. Installing at
637
+ user scope — `claude mcp add -s user grampro -- npx -y @grampro/kit mcp` —
638
+ skips both, at the cost of not being shareable through a commit.
639
+
640
+ ### Changed
641
+
642
+ **Reading a Figma design no longer downloads the whole file.**
643
+
644
+ Figma charges for file reads by the size of the response, and the frame
645
+ listing was fetching the entire document — 710 KB on the file this was
646
+ tested against — to answer a question about one screen. Two of those in a
647
+ row is what earns a 429, and the lockout is long: a `Retry-After` of 395274
648
+ was observed on a single token.
649
+
650
+ The listing now uses `depth=1`, which returns the document's outline rather
651
+ than its contents, and the chosen frame is fetched by id. A URL that already
652
+ carries a `node-id` — what Figma copies when you right-click a frame — goes
653
+ straight there in one call. Responses are still held for five minutes, so
654
+ the usual list-then-read flow costs one outline and one frame.
655
+
656
+ If a token is already rate limited, the Figma plugin export route still
657
+ works; it reads the scene graph from inside Figma and never calls the API.
658
+
659
+ **The MCP server starts in about the time an empty Node process takes.**
660
+
661
+ An agent spawns the server once per session, and it was loading the CLI's
662
+ own dependencies first — `fs-extra` and `yargs`, roughly 70ms, and of no use
663
+ to a JSON-RPC loop. The `mcp` command is now routed before they load.
664
+
665
+ The passports are also indexed on the first question rather than re-read
666
+ from disk for each one.
667
+
668
+ ### Fixed
669
+
670
+ **`Select`'s `closeOnSelect` was documented backwards, in both directions.**
671
+ Its description read "Keep the list open after choosing. Default false."
672
+ while the code defaults it to `true` and closing is what the prop does. An
673
+ agent reading that passport would have had both the behaviour and the
674
+ default inverted - the failure mode that makes a wrong description worse
675
+ than a missing one. MultiSelect's was already right.
676
+
677
+ **Description coverage was measured against a denominator nobody could
678
+ move.** It counted every prop, including the HTML and ARIA attributes that
679
+ arrive through `extends ButtonHTMLAttributes` - 390 of the 507 undescribed
680
+ props were those. So Button sat at 31% with its entire own API documented
681
+ bar one prop, and eleven components stayed under the 40% threshold with
682
+ nothing left to write. A gate that cannot be satisfied by doing the work it
683
+ asks for gets ignored.
684
+
685
+ It now measures the props the library authors, excluding inherited
686
+ attributes and the handful whose meaning is library-wide (`className`,
687
+ `style`, `ref` and friends) rather than restating them 27 times. On that
688
+ measure the library is at 87% and the weakest component at 75%, so the
689
+ threshold moves from 40 to 75 - a floor rather than an aspiration.
690
+ **The rate-limit message no longer promises that waiting a minute will
691
+ help.** It said to try again shortly, which is wrong for a cost-based limit
692
+ whose window runs to tens of minutes or longer. It now passes Figma's own
693
+ `Retry-After` through as given, without asserting a unit — the value has
694
+ been seen in a range where seconds and milliseconds differ by three orders
695
+ of magnitude — and points at the plugin route, which cannot be rate limited.
696
+
697
+ ## 2.4.4
698
+
699
+ ### Added
700
+
701
+ **An MCP server, so an agent can ask about a component instead of recalling
702
+ it.**
703
+
704
+ ```bash
705
+ npx @grampro/kit mcp
706
+ ```
707
+
708
+ Seven tools: `list_components`, `get_component`, `get_example`,
709
+ `get_guidance`, `figma_frame`, `install_component` and `validate_usage`. The
710
+ catalogue is 280 KB of passports and a page needs two or three components,
711
+ which is the argument for answering on demand rather than pasting a document
712
+ into the prompt.
713
+
714
+ `validate_usage` is the one that earns its place. A page written from
715
+ passports alone compiled on the first attempt apart from a single prop the
716
+ model had remembered from elsewhere — every type, union value and handler
717
+ signature the passports carried came out right. So the remaining failure
718
+ mode is invention, and that is what this checks.
719
+
720
+ No dependencies: MCP is newline-delimited JSON-RPC over stdio, and a server
721
+ shipped inside a library every consumer installs should not drag an SDK
722
+ along with it.
723
+
724
+ Marked beta — the tools work and are tested, but their names and shapes may
725
+ change.
726
+
727
+ ## 2.4.3
728
+
729
+ ### Fixed
730
+
731
+ **`passport --check` reported every component as stale on a machine whose
732
+ line endings differed from the one that generated the passport.**
733
+
734
+ A passport records a hash of the source it was derived from, and that hash
735
+ was taken over the file bytes exactly as they sat on disk. A checkout on
736
+ Windows holds CRLF and one on Linux holds LF, so the same unchanged file
737
+ produced two different hashes, and the check announced that all 27
738
+ components had drifted when nothing had changed at all.
739
+
740
+ It now normalises line endings before hashing, so the hash describes the
741
+ content rather than the checkout. If you have been seeing
742
+
743
+ ```
744
+ passport stale - source changed since the passport was generated
745
+ ```
746
+
747
+ on components you never touched, this is why, and it stops.
748
+
749
+ Running `npm run passport` once after upgrading rewrites the hashes in your
750
+ own copies.
751
+
752
+ ## 2.4.2
753
+
754
+ ### Fixed
755
+
756
+ **Your own class now beats a component's variant.**
757
+
758
+ Restyling a variant used to fail silently. A rule like
759
+
760
+ ```css
761
+ .btn-brand { --bt-fill: #7c3aed; }
762
+ ```
763
+
764
+ worked on a plain button and was ignored the moment anyone wrote
765
+ `variant="outline"`, because the shipped rule named two things
766
+ (`.bt-root[data-variant="outline"]`) where yours named one, and the browser
767
+ prefers the more specific selector. No error, nothing in devtools -- the
768
+ colour simply did not apply.
769
+
770
+ Selectors that carry an option you chose -- `variant`, `size`, `orientation`,
771
+ `appearance`, `placement` and the rest -- are now written so they add no
772
+ weight of their own. One class from you is enough, on any variant, in any
773
+ setup:
774
+
775
+ ```tsx
776
+ <Button variant="outline" className="btn-brand">Save</Button>
777
+ ```
778
+
779
+ **State deliberately still wins.** Disabled, invalid, selected, loading and
780
+ the other runtime states keep their weight, because a brand colour has no
781
+ business painting over a disabled control. To restyle a state, name the
782
+ state: `.btn-brand[data-disabled] { ... }`.
783
+
784
+ 215 selectors across 26 stylesheets. Verified by capturing the computed
785
+ styles of 3,716 elements across every demo in light and dark before and
786
+ after: the only differences were opacity on the shimmer and spinner, which
787
+ differ between two runs of the same build because they are mid-animation.
788
+ Nothing else moved.
789
+
790
+ Nothing in your code needs to change. If you had already worked around this
791
+ with a more specific selector, that still wins.
792
+
793
+ ## 2.4.1
794
+
795
+ ### Fixed
796
+
797
+ **The stylesheet import printed after an install is now one you can paste.**
798
+
799
+ 2.4.0 could put the components under `src/`, and printed the path to the
800
+ barrel from the project root -- which is correct from nowhere in particular.
801
+ In a Next.js app it read:
802
+
803
+ ```css
804
+ @import "./src/component-lib/gbs.css" layer(components);
805
+ ```
806
+
807
+ while the file to edit was `src/app/globals.css`, two directories away. You
808
+ had to work out the `../` yourself.
809
+
810
+ The installer now finds your global stylesheet in the usual places
811
+ (`src/app/globals.css`, `app/globals.css`, `src/styles/globals.css`,
812
+ `src/index.css` and friends), names it, and writes the path from there:
813
+
814
+ ```
815
+ Add this to src/app/globals.css:
816
+
817
+ @import "tailwindcss";
818
+ @import "../component-lib/gbs.css" layer(components);
819
+ ```
820
+
821
+ Where no stylesheet is found it falls back to the path from the project root,
822
+ as before. The header inside the generated `gbs.css` follows the same rule.
823
+
824
+ Nothing about the installed components changed; this is the installer's
825
+ output only.
826
+
827
+ ## 2.4.0
828
+
829
+ The first release published as **`@grampro/kit`**. `gbs-add-block` keeps
830
+ working for now and installs the same components, but new work happens here.
831
+
832
+ ### The package has a new name
833
+
834
+ ```bash
835
+ npx @grampro/kit add DataGrid
836
+ ```
837
+
838
+ `npx gbs-add-block` still resolves, and the `gbs-add-block` binary is still
839
+ installed, so existing scripts keep running. Both will stay for a few
840
+ releases before the old name is deprecated.
841
+
842
+ ### Commands read as commands
843
+
844
+ `add`, `list`, `skill` and `passport` are verbs now:
845
+
846
+ ```bash
847
+ npx @grampro/kit add Button DataGrid
848
+ npx @grampro/kit list
849
+ npx @grampro/kit skill --for claude
850
+ npx @grampro/kit passport --check
851
+ ```
852
+
853
+ The old flag spellings (`-a Button`, `-skill`, `-passport`) still work and
854
+ print the new spelling once. `--legacy` reaches the 1.x component set, which
855
+ is where `--beta` went: it used to opt *in* to the current components, and
856
+ there is nothing left to opt into.
857
+
858
+ ### You choose where components land
859
+
860
+ The destination was fixed at `component-lib/` in the directory you ran the
861
+ command from, which put it beside `src/` rather than inside it for most
862
+ projects. Now:
863
+
864
+ - `--dest <path>` puts them wherever you want.
865
+ - Failing that, an install already in the project wins, so a second `add`
866
+ lands beside the first. That is how a custom `--dest` is remembered; no
867
+ config file is written into your project.
868
+ - Failing that, a project with a `src/` folder gets `src/component-lib/`,
869
+ and everything else keeps `component-lib/` at the root.
870
+
871
+ Nothing moves on its own: if you already have `component-lib/`, that is
872
+ still where components go.
873
+
874
+ The stylesheet import printed after an install now names the path the files
875
+ actually landed at, relative to your project root.
876
+
877
+ ### Fixed
878
+
879
+ - `add Button --dest app/ui` tried to install a component called `app/ui`.
880
+ A flag's value was being read as a component name; the same was true of
881
+ `--for`.
882
+
883
+ ### Internal
884
+
885
+ The library source moved from `source/beta-components` to
886
+ `source/components`, and the 1.x set from `source/components` to
887
+ `source/legacy-components`. This changes nothing for installed components —
888
+ the files you receive and their import paths are identical — but every
889
+ `passport.json` has a new `sourceHash`, because that hash covers
890
+ repo-relative paths.
891
+
892
+ ## 2.3.0
893
+
894
+ Published as a minor, but **it contains one breaking change to styling**. Read
895
+ the upgrade note below before updating.
896
+
897
+ ### Breaking: component CSS is no longer in a cascade layer
898
+
899
+ Component rules used to live in `@layer gbs`, and 2.1.0 asked Tailwind v4
900
+ projects to add `@layer gbs, utilities;` to their global CSS. Both are gone.
901
+
902
+ A cascade layer loses to *any* unlayered stylesheet regardless of specificity,
903
+ and Tailwind v3's preflight and Bootstrap's Reboot are both unlayered. Measured
904
+ on 2.2.0: under Tailwind v3 the components rendered with `border: 0` and
905
+ `padding-left: 0`; under Bootstrap every button was square. No host-side
906
+ declaration could fix either, because nothing layered can outrank unlayered CSS.
907
+
908
+ Component CSS now ships unlayered and wins on ordinary specificity, so Tailwind
909
+ v3, Bootstrap and framework-free projects work with no setup at all.
910
+
911
+ **What to change:**
912
+
913
+ - Delete any `@layer gbs, utilities;` (or similar) line you added for 2.1.0.
914
+ - Import the generated barrel instead of individual stylesheets:
915
+ `@import "./component-lib/gbs.css";`
916
+ - **Tailwind v4 only** — import it after the framework and into its components
917
+ layer, or utilities stop overriding component rules:
918
+ ```css
919
+ @import "tailwindcss";
920
+ @import "./component-lib/gbs.css" layer(components);
921
+ ```
922
+
923
+ The installer detects which line you need and prints it.
924
+
925
+ ### Added
926
+
927
+ - **`<DataGrid ai />`** — a natural-language input above the grid, driven by an
928
+ adapter the application supplies through `<GramproAIProvider>`. No model is
929
+ bundled, downloaded or named, and with no provider the prop renders nothing.
930
+ - **WebMCP** — `registerGridTool(agent)` exposes one tool, `operate_grid`, to a
931
+ browser agent. Its schema is generated from the live grid. Needs no model.
932
+ - **Generated stylesheet barrel** — the CLI writes `component-lib/gbs.css` on
933
+ every install, so the import list stays correct as you add components.
934
+ - **Thin scrollbars** across every scrolling surface, themeable from `:root`
935
+ via `--gbs-scrollbar-width`, `--gbs-scrollbar-thumb`,
936
+ `--gbs-scrollbar-thumb-hover` and `--gbs-scrollbar-track`. Add
937
+ `class="gbs-scroll"` to your own scroll areas to match. Scroll containers the
938
+ kit does not own are left alone.
939
+
940
+ ### Security
941
+
942
+ - Pinned `dompurify` to `^3.4.16` via `overrides`, clearing two DOM XSS
943
+ advisories reachable through `@grampro/headless-helpers → jspdf`. The 2.0 beta
944
+ components never used that chain. `npm audit` is now clean.
945
+ - Removed the `path` dependency. `require("path")` resolves to Node's built-in,
946
+ so the npm package was inert — a deprecated userland shim sitting in the
947
+ dependency tree for no reason.
948
+
949
+ ### Documentation
950
+
951
+ - Documented what leaves the page with a remote AI adapter: the runtime contract
952
+ carries real values from low-cardinality columns. Mark personal columns
953
+ `semantics: { email: { pii: true } }` — a PII column is denied *and* never
954
+ summarised — or pass `stats: false`. Note that `policy.denyFilter` governs what
955
+ may be *done* with a column, not whether it may be *described*.
956
+ - Corrected the agent skill's install instructions, which still told agents to
957
+ import each stylesheet individually with no `layer()`.
958
+
959
+ ### Packaging
960
+
961
+ - Test files are no longer published (35 files, 233 KB). They were filtered out
962
+ at install time anyway.
963
+ - Added `repository`, `homepage` and `bugs` metadata.