@sakaniui/react 0.8.10 → 0.8.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -1
- package/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/bin/sakaniui-mcp.mjs +97 -1
- package/dist/icon-index.json +1 -1
- package/dist/manifest.json +1756 -67
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -19,6 +19,11 @@ Editors can query the same data through the bundled MCP server
|
|
|
19
19
|
(`npx -y @sakaniui/mcp`): `search_components`, `get_component`, `search_icons`,
|
|
20
20
|
`check_code`, `check_usage`, `get_tokens`, `get_guide`, `check_compatibility`, `get_changelog`.
|
|
21
21
|
|
|
22
|
+
**Object props have a shape.** A prop such as `columns`, `items`, `options`, `plans` or `data` takes an array
|
|
23
|
+
of objects with exactly the fields listed under the prop's `shape` (required ones must be present, and
|
|
24
|
+
`keyof T` means "a field of your own data type"). `check_code` verifies the field names of inline literals.
|
|
25
|
+
Components with a pattern the types do not teach carry `recipes`: code that compiles.
|
|
26
|
+
|
|
22
27
|
**Start from the example, check before you answer.** Every component has `example`: its imports and the
|
|
23
28
|
element with each required prop filled in, type-checked against the release. Start from it instead of
|
|
24
29
|
writing the element from memory. Never guess an icon name: find it with `search_icons` (icons are
|
|
@@ -206,7 +211,11 @@ Format: **Use for** · **Why it is built this way** · **Not for / instead**.
|
|
|
206
211
|
### Content and data
|
|
207
212
|
- **Card** — a container surface for a unit of content, optionally with actions. Not a layout wrapper for
|
|
208
213
|
everything.
|
|
209
|
-
- **Table** — dense, comparable records.
|
|
214
|
+
- **Table** — dense, comparable records. `Table<T>` is generic: every column's `key` must be a field of
|
|
215
|
+
your row type `T` (`keyof T & string`), so a column that is not data (a delete button, a menu) still
|
|
216
|
+
needs a key: give the row type an optional `never` field (`actions?: never`) and render the control in
|
|
217
|
+
`render`. Inventing a key that is not on the row type does not compile (see `recipes` in
|
|
218
|
+
`get_component("Table")` for code that does). **StatCard** — one headline metric with trend. **ListItem** — a
|
|
210
219
|
row in a list. **Charts** — pick by question: trend over time → Line/Area; compare categories → Bar;
|
|
211
220
|
part of whole → Pie/Donut (few slices only); progress to a target → Radial; compare several measures →
|
|
212
221
|
Radar; drop-off → Funnel; intensity across two dimensions → Heatmap.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@sakaniui/react` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.8.11
|
|
6
|
+
|
|
7
|
+
Found by asking Claude Code, with the published 0.8.10 MCP server, to add a delete button to a table row:
|
|
8
|
+
every tool call was correct and `IconButton` was used properly, but it put the button in a new `Table`
|
|
9
|
+
column with `key: 'actions'`, which does not compile (`key` must be a field of the row type). The
|
|
10
|
+
contract only said `columns: TableColumn<T>[]`.
|
|
11
|
+
|
|
12
|
+
- **Object props now have a `shape`** in `manifest.json`: the fields of `TableColumn<T>`, `BreadcrumbItem`,
|
|
13
|
+
`PricingPlan`, `SelectOption`, chart data and 58 more (63 props), read from the shipped type
|
|
14
|
+
definitions, generics kept as declared (`key: keyof T & string`).
|
|
15
|
+
- **`recipes`**: code for patterns the types do not teach, compiled on every build. First one: a `Table`
|
|
16
|
+
column that is not data (a delete button, a menu). The build also checks the opposite, that a column
|
|
17
|
+
key which is not on the row type really does not compile.
|
|
18
|
+
- **MCP `check_code` checks inline object literals** against the shape: an unknown field (`title` for
|
|
19
|
+
`label`, `pct` for `percent`, with a suggestion), a missing required field (with the item index), and,
|
|
20
|
+
for a `Table`, a column `key` that is not a field of the `rows` written next to it. Spreads, identifiers
|
|
21
|
+
and anything it cannot read are left alone (0 errors on 685 real elements in three apps).
|
|
22
|
+
- `AGENTS.md` explains the generic `Table` rule and the new `shape` and `recipes`.
|
|
23
|
+
|
|
5
24
|
## 0.8.10
|
|
6
25
|
|
|
7
26
|
Readiness release: everything below was found by testing the package the way a new user, an editor
|
package/README.md
CHANGED
|
@@ -142,7 +142,7 @@ or `claude mcp add sakaniui -- cmd /c npx -y @sakaniui/mcp`). Needs Node.js 18 o
|
|
|
142
142
|
The server is its own tiny package, [`@sakaniui/mcp`](https://www.npmjs.com/package/@sakaniui/mcp) (about 1 MB, starts in a couple of seconds; it does not install the component library). The copy bundled in `@sakaniui/react` (`npx -y -p @sakaniui/react sakaniui-mcp`) still works but downloads far more.
|
|
143
143
|
|
|
144
144
|
Tools: `search_components`, `get_component` (with a type-checked `example`), `search_icons`,
|
|
145
|
-
`check_code` (checks a whole snippet: imports, required props, values, icon names), `check_usage`,
|
|
145
|
+
`check_code` (checks a whole snippet: imports, required props, values, icon names, and the field names of object props such as table columns), `check_usage`,
|
|
146
146
|
`list_components`, `get_tokens`, `get_guide`, `check_compatibility` and `get_changelog`.
|
|
147
147
|
|
|
148
148
|
- **Read-only.** It reads five fixed files from a `@sakaniui/react` install (package.json, the
|
package/bin/sakaniui-mcp.mjs
CHANGED
|
@@ -81,7 +81,7 @@ function unknownProp(c, name) {
|
|
|
81
81
|
}
|
|
82
82
|
// close spellings, plus the usual synonyms (leftIcon -> leadingIcon, onChange vs onValueChange is left to the list)
|
|
83
83
|
const canon = (x) => x.toLowerCase().replace(/left|start|before|prefix/g, 'leading').replace(/right|end|after|suffix/g, 'trailing').replace(/colou?r|tone|kind|type/g, 'variant');
|
|
84
|
-
const near = c.props.map((p) => p.name).filter((p) =>
|
|
84
|
+
const near = c.props.map((p) => p.name).filter((p) => similar(p, name) || canon(p) === canon(name));
|
|
85
85
|
return { level: 'error', message: `"${name}" is not a ${c.name} prop${near.length ? ` (did you mean ${near.map((x) => `"${x}"`).join(', ')}?)` : ''}. ${c.name} accepts: ${c.props.map((p) => p.name).join(', ') || 'no props of its own'}.` };
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -170,6 +170,16 @@ function distance(a, b) {
|
|
|
170
170
|
return d[a.length][b.length];
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/** `pct` for `percent`, `desc` for `description`: same first letter, every letter in order. */
|
|
174
|
+
function isAbbreviation(short, full) {
|
|
175
|
+
const a = short.toLowerCase(), b = full.toLowerCase();
|
|
176
|
+
if (a.length < 2 || a.length >= b.length || a[0] !== b[0]) return false;
|
|
177
|
+
let i = 0;
|
|
178
|
+
for (const ch of b) if (ch === a[i]) i++;
|
|
179
|
+
return i === a.length;
|
|
180
|
+
}
|
|
181
|
+
const similar = (a, b) => distance(a.toLowerCase(), b.toLowerCase()) <= 2 || isAbbreviation(a, b) || isAbbreviation(b, a);
|
|
182
|
+
|
|
173
183
|
function closest(name) {
|
|
174
184
|
const n = name.toLowerCase();
|
|
175
185
|
return manifest.components
|
|
@@ -454,6 +464,7 @@ function checkCode(source) {
|
|
|
454
464
|
errors.push(at(`${a.name}=${JSON.stringify(a.value)} is not allowed. Use one of: ${p.values.map((x) => JSON.stringify(x)).join(', ')}.`));
|
|
455
465
|
}
|
|
456
466
|
{ const tooNew = propTooNew(c, p, a.kind === 'string' ? a.value : undefined); if (tooNew) errors.push(at(tooNew)); }
|
|
467
|
+
for (const m of checkShape(c, p, a, new Map(el.attrs.map((x) => [x.name, x])))) errors.push(at(m));
|
|
457
468
|
if (p.type === 'IconComponent') {
|
|
458
469
|
if (a.kind === 'string') errors.push(at(`"${a.name}" takes an icon component, not a string: ${a.name}={IconName}.`));
|
|
459
470
|
else if (a.kind === 'element' || a.kind === 'identifier') {
|
|
@@ -470,6 +481,89 @@ function checkCode(source) {
|
|
|
470
481
|
return out;
|
|
471
482
|
}
|
|
472
483
|
|
|
484
|
+
/* ---------- literal shapes: field names inside inline object literals ---------- */
|
|
485
|
+
/** Splits `text` at top-level commas (ignores commas inside brackets, braces, parentheses and strings). */
|
|
486
|
+
function splitTopLevel(text) {
|
|
487
|
+
const parts = [];
|
|
488
|
+
let depth = 0, start = 0, quote = null;
|
|
489
|
+
for (let i = 0; i < text.length && parts.length < 2000; i++) {
|
|
490
|
+
const ch = text[i];
|
|
491
|
+
if (quote) { if (ch === '\\') i++; else if (ch === quote) quote = null; continue; }
|
|
492
|
+
if (ch === '"' || ch === "'" || ch === '`') { quote = ch; continue; }
|
|
493
|
+
if (ch === '{' || ch === '[' || ch === '(') depth++;
|
|
494
|
+
else if (ch === '}' || ch === ']' || ch === ')') depth--;
|
|
495
|
+
else if (ch === ',' && depth === 0) { parts.push(text.slice(start, i)); start = i + 1; }
|
|
496
|
+
}
|
|
497
|
+
parts.push(text.slice(start));
|
|
498
|
+
return parts.map((x) => x.trim()).filter(Boolean);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** The keys written in an object literal `{ a: 1, 'b': 2, c, ...d }`. spread: it also contains `...x`. */
|
|
502
|
+
function objectKeys(literal) {
|
|
503
|
+
const inner = literal.trim().slice(1, -1);
|
|
504
|
+
const keys = []; let spread = false;
|
|
505
|
+
for (const entry of splitTopLevel(inner)) {
|
|
506
|
+
if (entry.startsWith('...')) { spread = true; continue; }
|
|
507
|
+
const m = /^(?:'([^']+)'|"([^"]+)"|([A-Za-z_$][\w$]*))\s*(?::|\(|$)/.exec(entry);
|
|
508
|
+
if (m) keys.push(m[1] || m[2] || m[3]);
|
|
509
|
+
else spread = true; // computed key or something we cannot read: do not judge this object
|
|
510
|
+
}
|
|
511
|
+
return { keys, spread };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** The object literals in an inline `[ {...}, {...} ]` (or a single `{...}`). Null when it is not an inline literal. */
|
|
515
|
+
function inlineObjects(raw, collection) {
|
|
516
|
+
const t = raw.trim();
|
|
517
|
+
if (collection === 'array') {
|
|
518
|
+
if (!(t.startsWith('[') && t.endsWith(']'))) return null;
|
|
519
|
+
const items = splitTopLevel(t.slice(1, -1));
|
|
520
|
+
if (items.some((x) => !x.startsWith('{') || !x.endsWith('}'))) return items.filter((x) => x.startsWith('{') && x.endsWith('}')).length ? items.filter((x) => x.startsWith('{') && x.endsWith('}')) : null;
|
|
521
|
+
return items;
|
|
522
|
+
}
|
|
523
|
+
return t.startsWith('{') && t.endsWith('}') ? [t] : null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Checks inline object literals against the prop's `shape`. Returns messages. */
|
|
527
|
+
function checkShape(c, prop, attr, siblings) {
|
|
528
|
+
const sh = prop.shape;
|
|
529
|
+
if (!sh || attr.kind !== 'expression' || !attr.raw) return [];
|
|
530
|
+
const objs = inlineObjects(attr.raw, sh.collection);
|
|
531
|
+
if (!objs || !objs.length) return [];
|
|
532
|
+
const out = [];
|
|
533
|
+
const known = new Map(sh.fields.map((f) => [f.name, f]));
|
|
534
|
+
const list = sh.fields.map((f) => f.name + (f.required ? '' : '?')).join(', ');
|
|
535
|
+
objs.forEach((lit, idx) => {
|
|
536
|
+
const { keys, spread } = objectKeys(lit);
|
|
537
|
+
const where = sh.collection === 'array' ? `${prop.name}[${idx}]` : prop.name;
|
|
538
|
+
for (const k of keys) {
|
|
539
|
+
if (!known.has(k)) {
|
|
540
|
+
const near = sh.fields.map((f) => f.name).filter((f) => similar(f, k));
|
|
541
|
+
out.push(`${where}: "${k}" is not a field of ${sh.type}${near.length ? ` (did you mean "${near[0]}"?)` : ''}. Fields: ${list}.`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (!spread) for (const f of sh.fields) if (f.required && !keys.includes(f.name)) out.push(`${where}: missing required field "${f.name}" of ${sh.type}. Fields: ${list}.`);
|
|
545
|
+
});
|
|
546
|
+
// `key: keyof T`: the key must be a field of the rows given next to it (T[] sibling), when both are written inline
|
|
547
|
+
const keyField = sh.fields.find((f) => /^keyof T\b/.test(f.type));
|
|
548
|
+
if (keyField && sh.typeParameters && sh.typeParameters[0] === 'T') {
|
|
549
|
+
const rowsProp = c.props.find((p) => p.type === 'T[]');
|
|
550
|
+
const rowsAttr = rowsProp && siblings.get(rowsProp.name);
|
|
551
|
+
const rowObjs = rowsAttr && rowsAttr.kind === 'expression' ? inlineObjects(rowsAttr.raw || '', 'array') : null;
|
|
552
|
+
if (rowObjs && rowObjs.length) {
|
|
553
|
+
const rowKeys = new Set();
|
|
554
|
+
let rowsOpen = false;
|
|
555
|
+
for (const r of rowObjs) { const o = objectKeys(r); o.keys.forEach((k) => rowKeys.add(k)); if (o.spread) rowsOpen = true; }
|
|
556
|
+
if (!rowsOpen) {
|
|
557
|
+
for (const lit of objs) {
|
|
558
|
+
const kv = new RegExp(`(?:^|[{,\\s])${keyField.name}\\s*:\\s*['"]([^'"]+)['"]`).exec(lit);
|
|
559
|
+
if (kv && !rowKeys.has(kv[1])) out.push(`${prop.name}: ${keyField.name} "${kv[1]}" is not a field of the ${rowsProp.name} (${[...rowKeys].join(', ')}). It must be a key of the row type; for a column that is not data, add an optional field to the row type (see the recipe in get_component ${c.name}).`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return out;
|
|
565
|
+
}
|
|
566
|
+
|
|
473
567
|
const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
474
568
|
|
|
475
569
|
/* ---------- tools ---------- */
|
|
@@ -502,6 +596,8 @@ const tools = {
|
|
|
502
596
|
const c = byName.get(n.value.toLowerCase());
|
|
503
597
|
if (!c) return fail(`No component named "${n.value}". Did you mean: ${closest(n.value).join(', ') || '(none close)'}? Use search_components.`);
|
|
504
598
|
const out = { ...c, note: 'Native HTML attributes (className, style, aria-*, event handlers) are also accepted on the root element.' };
|
|
599
|
+
if (c.props.some((p) => p.shape)) out.shapeNote = 'A prop with a `shape` takes an object (or an array of objects) with exactly those fields; `required` fields must be present. In a generic shape, `keyof T & string` means the value must be a field of your own data type.';
|
|
600
|
+
if (c.recipes && c.recipes.length) out.recipeNote = 'See `recipes`: patterns the types alone do not teach, compiled against this release.';
|
|
505
601
|
if (c.example) out.exampleNote = 'Start from `example`: it is type-checked against this release. Replace the placeholder text and values; keep the prop names and shapes.';
|
|
506
602
|
if (c.props.some((p) => p.type === 'IconComponent')) out.iconNote = 'Icon props take the component itself (icon={IconTrash}), not an element. The example uses IconStar as a placeholder: find the real icon with search_icons.';
|
|
507
603
|
if (inInstalled(c) === false) out.availableInInstalled = false;
|