@sakaniui/react 0.8.10 → 0.8.12
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 +16 -1
- package/CHANGELOG.md +35 -0
- package/README.md +1 -1
- package/bin/sakaniui-mcp.mjs +129 -7
- package/dist/icon-index.json +1 -1
- package/dist/manifest.json +2187 -127
- 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
|
|
@@ -147,6 +152,12 @@ CSS of a component into your own file.
|
|
|
147
152
|
|
|
148
153
|
Format: **Use for** · **Why it is built this way** · **Not for / instead**.
|
|
149
154
|
|
|
155
|
+
**Looking for a component by another name?** SakaniUI uses its own names. A *dialog* or *alert dialog* is
|
|
156
|
+
`Modal`; a *snackbar* is `Toast`; a *dropdown menu* is `Menu`; a *navbar* or *app bar* is `TopBar`; a
|
|
157
|
+
*date picker* is `Calendar`; a *loader* is `Spinner`; a *toggle* is `Switch`; a *banner* is `Announcement`;
|
|
158
|
+
*autocomplete* is `Combobox`. There is no `Dialog`, `AlertDialog`, `Snackbar` or `Navbar`. Each component
|
|
159
|
+
lists its other names under `aliases` in `manifest.json`, and `search_components` understands them.
|
|
160
|
+
|
|
150
161
|
### Actions
|
|
151
162
|
- **Button** — Use for any action that does something. *Why:* 5 variants × 3 sizes mirror Figma 1:1;
|
|
152
163
|
`loading` disables interaction so double-submits can't happen. *Not for* navigation → `Link`; icon-only →
|
|
@@ -206,7 +217,11 @@ Format: **Use for** · **Why it is built this way** · **Not for / instead**.
|
|
|
206
217
|
### Content and data
|
|
207
218
|
- **Card** — a container surface for a unit of content, optionally with actions. Not a layout wrapper for
|
|
208
219
|
everything.
|
|
209
|
-
- **Table** — dense, comparable records.
|
|
220
|
+
- **Table** — dense, comparable records. `Table<T>` is generic: every column's `key` must be a field of
|
|
221
|
+
your row type `T` (`keyof T & string`), so a column that is not data (a delete button, a menu) still
|
|
222
|
+
needs a key: give the row type an optional `never` field (`actions?: never`) and render the control in
|
|
223
|
+
`render`. Inventing a key that is not on the row type does not compile (see `recipes` in
|
|
224
|
+
`get_component("Table")` for code that does). **StatCard** — one headline metric with trend. **ListItem** — a
|
|
210
225
|
row in a list. **Charts** — pick by question: trend over time → Line/Area; compare categories → Bar;
|
|
211
226
|
part of whole → Pie/Donut (few slices only); progress to a target → Radial; compare several measures →
|
|
212
227
|
Radar; drop-off → Funnel; intensity across two dimensions → Heatmap.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@sakaniui/react` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.8.12
|
|
6
|
+
|
|
7
|
+
Found while testing 0.8.11 in Claude Code: after writing correct code, the agent offered to wrap the
|
|
8
|
+
delete button in "SakaniUI's Dialog/AlertDialog". Neither exists; the component is `Modal`. It named it
|
|
9
|
+
from memory without looking it up.
|
|
10
|
+
|
|
11
|
+
- **`aliases` in `manifest.json`**: the other names people use for a component, 250+ across 61 components
|
|
12
|
+
(dialog, alert dialog -> `Modal`; snackbar -> `Toast`; navbar, app bar -> `TopBar`; date picker ->
|
|
13
|
+
`Calendar`; loader -> `Spinner`; toggle -> `Switch`; autocomplete -> `Combobox`; dropzone ->
|
|
14
|
+
`FileUpload`; pricing table -> `PricingTableBlock`; ...). Hand-written; the build checks every one
|
|
15
|
+
points at a real component and never shadows a real name.
|
|
16
|
+
- **MCP:** `search_components` ranks by alias ("dialog" finds `Modal` first; a plain exact name such as
|
|
17
|
+
"button" always wins). `get_component`, `check_usage` and `check_code` now say what the name is here:
|
|
18
|
+
*"Dialog is not exported by @sakaniui/react. In SakaniUI that is `Modal` (Confirmation dialog)."*
|
|
19
|
+
- `AGENTS.md` lists the most common renamings.
|
|
20
|
+
|
|
21
|
+
## 0.8.11
|
|
22
|
+
|
|
23
|
+
Found by asking Claude Code, with the published 0.8.10 MCP server, to add a delete button to a table row:
|
|
24
|
+
every tool call was correct and `IconButton` was used properly, but it put the button in a new `Table`
|
|
25
|
+
column with `key: 'actions'`, which does not compile (`key` must be a field of the row type). The
|
|
26
|
+
contract only said `columns: TableColumn<T>[]`.
|
|
27
|
+
|
|
28
|
+
- **Object props now have a `shape`** in `manifest.json`: the fields of `TableColumn<T>`, `BreadcrumbItem`,
|
|
29
|
+
`PricingPlan`, `SelectOption`, chart data and 58 more (63 props), read from the shipped type
|
|
30
|
+
definitions, generics kept as declared (`key: keyof T & string`).
|
|
31
|
+
- **`recipes`**: code for patterns the types do not teach, compiled on every build. First one: a `Table`
|
|
32
|
+
column that is not data (a delete button, a menu). The build also checks the opposite, that a column
|
|
33
|
+
key which is not on the row type really does not compile.
|
|
34
|
+
- **MCP `check_code` checks inline object literals** against the shape: an unknown field (`title` for
|
|
35
|
+
`label`, `pct` for `percent`, with a suggestion), a missing required field (with the item index), and,
|
|
36
|
+
for a `Table`, a column `key` that is not a field of the `rows` written next to it. Spreads, identifiers
|
|
37
|
+
and anything it cannot read are left alone (0 errors on 685 real elements in three apps).
|
|
38
|
+
- `AGENTS.md` explains the generic `Table` rule and the new `shape` and `recipes`.
|
|
39
|
+
|
|
5
40
|
## 0.8.10
|
|
6
41
|
|
|
7
42
|
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
|
|
|
@@ -121,6 +121,22 @@ function propTooNew(c, p, value, installed = projectVersion) {
|
|
|
121
121
|
|
|
122
122
|
/* ---------- helpers ---------- */
|
|
123
123
|
const byName = new Map(manifest.components.map((c) => [c.name.toLowerCase(), c]));
|
|
124
|
+
// Common names for components ("Dialog" -> Modal). Only used when a name is not found, and to rank search results.
|
|
125
|
+
const norm = (x) => String(x).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
126
|
+
const aliasIndex = new Map(); // normalized alias -> components
|
|
127
|
+
for (const c of manifest.components) for (const a of c.aliases || []) { const k = norm(a); if (!aliasIndex.has(k)) aliasIndex.set(k, []); aliasIndex.get(k).push(c); }
|
|
128
|
+
/** The real component(s) for a name that does not exist: "Dialog" -> [Modal]. */
|
|
129
|
+
function byAlias(name) {
|
|
130
|
+
const k = norm(name);
|
|
131
|
+
const exact = aliasIndex.get(k) || aliasIndex.get(k.replace(/s$/, '')) || [];
|
|
132
|
+
return [...new Set(exact)].slice(0, 4);
|
|
133
|
+
}
|
|
134
|
+
const aliasHint = (name) => {
|
|
135
|
+
const hits = byAlias(name);
|
|
136
|
+
if (!hits.length) return '';
|
|
137
|
+
return ` ${hits.length === 1 ? `In SakaniUI that is \`${hits[0].name}\` (${hits[0].summary.replace(/\.$/, '')})` : `In SakaniUI that could be ${hits.map((h) => `\`${h.name}\``).join(' or ')}`}.`;
|
|
138
|
+
};
|
|
139
|
+
|
|
124
140
|
const brief = (c) => {
|
|
125
141
|
const out = { name: c.name, kind: c.kind, category: c.category, import: c.import, since: c.since, summary: c.summary, requiredProps: c.contract.requiredProps };
|
|
126
142
|
if (inInstalled(c) === false) out.availableInInstalled = false;
|
|
@@ -150,9 +166,19 @@ const verArg = (args, key) => {
|
|
|
150
166
|
};
|
|
151
167
|
const firstError = (...rs) => rs.find((r) => r.err);
|
|
152
168
|
|
|
153
|
-
function score(c, terms) {
|
|
169
|
+
function score(c, terms, phrase = '') {
|
|
154
170
|
const hay = `${c.name} ${c.category ?? ''} ${c.summary} ${c.props.map((p) => p.name).join(' ')}`.toLowerCase();
|
|
155
171
|
let s = 0;
|
|
172
|
+
// an alias is the name people use for this component: "dialog" finds Modal, "snackbar" finds Toast
|
|
173
|
+
const p = norm(phrase);
|
|
174
|
+
let aliasWords = 0;
|
|
175
|
+
for (const a of c.aliases || []) {
|
|
176
|
+
const na = norm(a);
|
|
177
|
+
if (na === p || na + 's' === p || na === p + 's') s += 60;
|
|
178
|
+
else if (na.length >= 4 && p.includes(na)) s += 25;
|
|
179
|
+
else if (!aliasWords && terms.length && a.toLowerCase().split(/[^a-z0-9]+/).some((w) => w.length >= 3 && terms.includes(w))) { s += 6; aliasWords = 1; } // loosely related: counts once
|
|
180
|
+
}
|
|
181
|
+
if (p && norm(c.name) === p) s += 100; // the real name always wins
|
|
156
182
|
for (const t of terms) {
|
|
157
183
|
if (c.name.toLowerCase() === t) s += 20;
|
|
158
184
|
else if (c.name.toLowerCase().includes(t)) s += 8;
|
|
@@ -170,6 +196,16 @@ function distance(a, b) {
|
|
|
170
196
|
return d[a.length][b.length];
|
|
171
197
|
}
|
|
172
198
|
|
|
199
|
+
/** `pct` for `percent`, `desc` for `description`: same first letter, every letter in order. */
|
|
200
|
+
function isAbbreviation(short, full) {
|
|
201
|
+
const a = short.toLowerCase(), b = full.toLowerCase();
|
|
202
|
+
if (a.length < 2 || a.length >= b.length || a[0] !== b[0]) return false;
|
|
203
|
+
let i = 0;
|
|
204
|
+
for (const ch of b) if (ch === a[i]) i++;
|
|
205
|
+
return i === a.length;
|
|
206
|
+
}
|
|
207
|
+
const similar = (a, b) => distance(a.toLowerCase(), b.toLowerCase()) <= 2 || isAbbreviation(a, b) || isAbbreviation(b, a);
|
|
208
|
+
|
|
173
209
|
function closest(name) {
|
|
174
210
|
const n = name.toLowerCase();
|
|
175
211
|
return manifest.components
|
|
@@ -424,7 +460,7 @@ function checkCode(source) {
|
|
|
424
460
|
if (!c && !isUtility) {
|
|
425
461
|
if (TYPE_LIKE.test(n.imported)) continue; // most likely a type export
|
|
426
462
|
const near = closest(n.imported);
|
|
427
|
-
errors.push({ line: imp.line, message: `${n.imported} is not exported by @sakaniui/react.${near.length ? ` Did you mean: ${near.join(', ')}?` : ''} Use search_components.` });
|
|
463
|
+
errors.push({ line: imp.line, message: `${n.imported} is not exported by @sakaniui/react.${aliasHint(n.imported)}${near.length ? ` Did you mean: ${near.join(', ')}?` : ''} Use search_components.` });
|
|
428
464
|
}
|
|
429
465
|
if (c && inInstalled(c) === false) errors.push({ line: imp.line, message: `${c.name} was added in ${c.since}; the project has ${projectVersion}.` });
|
|
430
466
|
}
|
|
@@ -454,6 +490,7 @@ function checkCode(source) {
|
|
|
454
490
|
errors.push(at(`${a.name}=${JSON.stringify(a.value)} is not allowed. Use one of: ${p.values.map((x) => JSON.stringify(x)).join(', ')}.`));
|
|
455
491
|
}
|
|
456
492
|
{ const tooNew = propTooNew(c, p, a.kind === 'string' ? a.value : undefined); if (tooNew) errors.push(at(tooNew)); }
|
|
493
|
+
for (const m of checkShape(c, p, a, new Map(el.attrs.map((x) => [x.name, x])))) errors.push(at(m));
|
|
457
494
|
if (p.type === 'IconComponent') {
|
|
458
495
|
if (a.kind === 'string') errors.push(at(`"${a.name}" takes an icon component, not a string: ${a.name}={IconName}.`));
|
|
459
496
|
else if (a.kind === 'element' || a.kind === 'identifier') {
|
|
@@ -470,6 +507,89 @@ function checkCode(source) {
|
|
|
470
507
|
return out;
|
|
471
508
|
}
|
|
472
509
|
|
|
510
|
+
/* ---------- literal shapes: field names inside inline object literals ---------- */
|
|
511
|
+
/** Splits `text` at top-level commas (ignores commas inside brackets, braces, parentheses and strings). */
|
|
512
|
+
function splitTopLevel(text) {
|
|
513
|
+
const parts = [];
|
|
514
|
+
let depth = 0, start = 0, quote = null;
|
|
515
|
+
for (let i = 0; i < text.length && parts.length < 2000; i++) {
|
|
516
|
+
const ch = text[i];
|
|
517
|
+
if (quote) { if (ch === '\\') i++; else if (ch === quote) quote = null; continue; }
|
|
518
|
+
if (ch === '"' || ch === "'" || ch === '`') { quote = ch; continue; }
|
|
519
|
+
if (ch === '{' || ch === '[' || ch === '(') depth++;
|
|
520
|
+
else if (ch === '}' || ch === ']' || ch === ')') depth--;
|
|
521
|
+
else if (ch === ',' && depth === 0) { parts.push(text.slice(start, i)); start = i + 1; }
|
|
522
|
+
}
|
|
523
|
+
parts.push(text.slice(start));
|
|
524
|
+
return parts.map((x) => x.trim()).filter(Boolean);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** The keys written in an object literal `{ a: 1, 'b': 2, c, ...d }`. spread: it also contains `...x`. */
|
|
528
|
+
function objectKeys(literal) {
|
|
529
|
+
const inner = literal.trim().slice(1, -1);
|
|
530
|
+
const keys = []; let spread = false;
|
|
531
|
+
for (const entry of splitTopLevel(inner)) {
|
|
532
|
+
if (entry.startsWith('...')) { spread = true; continue; }
|
|
533
|
+
const m = /^(?:'([^']+)'|"([^"]+)"|([A-Za-z_$][\w$]*))\s*(?::|\(|$)/.exec(entry);
|
|
534
|
+
if (m) keys.push(m[1] || m[2] || m[3]);
|
|
535
|
+
else spread = true; // computed key or something we cannot read: do not judge this object
|
|
536
|
+
}
|
|
537
|
+
return { keys, spread };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** The object literals in an inline `[ {...}, {...} ]` (or a single `{...}`). Null when it is not an inline literal. */
|
|
541
|
+
function inlineObjects(raw, collection) {
|
|
542
|
+
const t = raw.trim();
|
|
543
|
+
if (collection === 'array') {
|
|
544
|
+
if (!(t.startsWith('[') && t.endsWith(']'))) return null;
|
|
545
|
+
const items = splitTopLevel(t.slice(1, -1));
|
|
546
|
+
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;
|
|
547
|
+
return items;
|
|
548
|
+
}
|
|
549
|
+
return t.startsWith('{') && t.endsWith('}') ? [t] : null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Checks inline object literals against the prop's `shape`. Returns messages. */
|
|
553
|
+
function checkShape(c, prop, attr, siblings) {
|
|
554
|
+
const sh = prop.shape;
|
|
555
|
+
if (!sh || attr.kind !== 'expression' || !attr.raw) return [];
|
|
556
|
+
const objs = inlineObjects(attr.raw, sh.collection);
|
|
557
|
+
if (!objs || !objs.length) return [];
|
|
558
|
+
const out = [];
|
|
559
|
+
const known = new Map(sh.fields.map((f) => [f.name, f]));
|
|
560
|
+
const list = sh.fields.map((f) => f.name + (f.required ? '' : '?')).join(', ');
|
|
561
|
+
objs.forEach((lit, idx) => {
|
|
562
|
+
const { keys, spread } = objectKeys(lit);
|
|
563
|
+
const where = sh.collection === 'array' ? `${prop.name}[${idx}]` : prop.name;
|
|
564
|
+
for (const k of keys) {
|
|
565
|
+
if (!known.has(k)) {
|
|
566
|
+
const near = sh.fields.map((f) => f.name).filter((f) => similar(f, k));
|
|
567
|
+
out.push(`${where}: "${k}" is not a field of ${sh.type}${near.length ? ` (did you mean "${near[0]}"?)` : ''}. Fields: ${list}.`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
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}.`);
|
|
571
|
+
});
|
|
572
|
+
// `key: keyof T`: the key must be a field of the rows given next to it (T[] sibling), when both are written inline
|
|
573
|
+
const keyField = sh.fields.find((f) => /^keyof T\b/.test(f.type));
|
|
574
|
+
if (keyField && sh.typeParameters && sh.typeParameters[0] === 'T') {
|
|
575
|
+
const rowsProp = c.props.find((p) => p.type === 'T[]');
|
|
576
|
+
const rowsAttr = rowsProp && siblings.get(rowsProp.name);
|
|
577
|
+
const rowObjs = rowsAttr && rowsAttr.kind === 'expression' ? inlineObjects(rowsAttr.raw || '', 'array') : null;
|
|
578
|
+
if (rowObjs && rowObjs.length) {
|
|
579
|
+
const rowKeys = new Set();
|
|
580
|
+
let rowsOpen = false;
|
|
581
|
+
for (const r of rowObjs) { const o = objectKeys(r); o.keys.forEach((k) => rowKeys.add(k)); if (o.spread) rowsOpen = true; }
|
|
582
|
+
if (!rowsOpen) {
|
|
583
|
+
for (const lit of objs) {
|
|
584
|
+
const kv = new RegExp(`(?:^|[{,\\s])${keyField.name}\\s*:\\s*['"]([^'"]+)['"]`).exec(lit);
|
|
585
|
+
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}).`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return out;
|
|
591
|
+
}
|
|
592
|
+
|
|
473
593
|
const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
474
594
|
|
|
475
595
|
/* ---------- tools ---------- */
|
|
@@ -484,7 +604,7 @@ const tools = {
|
|
|
484
604
|
const terms = q.value.toLowerCase().split(/\s+/).filter(Boolean);
|
|
485
605
|
const hits = manifest.components
|
|
486
606
|
.filter((c) => !k.value || c.kind === k.value)
|
|
487
|
-
.map((c) => [score(c, terms), c])
|
|
607
|
+
.map((c) => [score(c, terms, q.value), c])
|
|
488
608
|
.filter(([s]) => s > 0)
|
|
489
609
|
.sort((a, b) => b[0] - a[0])
|
|
490
610
|
.slice(0, limit)
|
|
@@ -500,8 +620,10 @@ const tools = {
|
|
|
500
620
|
run(args) {
|
|
501
621
|
const n = strArg(args, 'name', { required: true }); if (n.err) return fail(n.err);
|
|
502
622
|
const c = byName.get(n.value.toLowerCase());
|
|
503
|
-
if (!c) return fail(`No component named "${n.value}". Did you mean: ${closest(n.value).join(', ')
|
|
623
|
+
if (!c) return fail(`No component named "${n.value}".${aliasHint(n.value)} ${closest(n.value).length ? `Did you mean: ${closest(n.value).join(', ')}? ` : ''}Use search_components.`.replace(/\s+/g, ' '));
|
|
504
624
|
const out = { ...c, note: 'Native HTML attributes (className, style, aria-*, event handlers) are also accepted on the root element.' };
|
|
625
|
+
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.';
|
|
626
|
+
if (c.recipes && c.recipes.length) out.recipeNote = 'See `recipes`: patterns the types alone do not teach, compiled against this release.';
|
|
505
627
|
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
628
|
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
629
|
if (inInstalled(c) === false) out.availableInInstalled = false;
|
|
@@ -564,7 +686,7 @@ const tools = {
|
|
|
564
686
|
if (typeof props !== 'object' || Array.isArray(props)) return fail('"props" must be an object of prop name -> value.');
|
|
565
687
|
if (Object.keys(props).length > 200) return fail('"props" has too many entries (max 200).');
|
|
566
688
|
const c = byName.get(n.value.toLowerCase());
|
|
567
|
-
if (!c) return fail(`No component named "${n.value}". Did you mean: ${closest(n.value).join(', ')
|
|
689
|
+
if (!c) return fail(`No component named "${n.value}".${aliasHint(n.value)} ${closest(n.value).length ? `Did you mean: ${closest(n.value).join(', ')}?` : ''}`.replace(/\s+/g, ' ').trim());
|
|
568
690
|
const known = new Map(c.props.map((p) => [p.name, p]));
|
|
569
691
|
const errors = [];
|
|
570
692
|
const warnings = [];
|
|
@@ -688,7 +810,7 @@ function handle(msg) {
|
|
|
688
810
|
protocolVersion: asked && PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0],
|
|
689
811
|
capabilities: { tools: { listChanged: false } },
|
|
690
812
|
serverInfo: { name: 'sakaniui', title: 'SakaniUI', version: refVersion },
|
|
691
|
-
instructions: `SakaniUI ${refVersion}${projectVersion ? ` (project has ${projectVersion})` : ''}. Read-only reference. Look components up with search_components / get_component and start from their \`example\`; find icon names with search_icons (never guess them); run check_code on every snippet before you answer; style anything beyond props with the design tokens from get_tokens (CSS variables), not hard-coded colours or utility classes the project may not have; read get_guide when nothing fits.${note ? ` ${note}` : ''}`,
|
|
813
|
+
instructions: `SakaniUI ${refVersion}${projectVersion ? ` (project has ${projectVersion})` : ''}. Read-only reference. Look components up with search_components / get_component and start from their \`example\`; find icon names with search_icons (never guess them); search by the name you would normally use (dialog, snackbar, navbar, date picker) and it maps to the real component; run check_code on every snippet before you answer; style anything beyond props with the design tokens from get_tokens (CSS variables), not hard-coded colours or utility classes the project may not have; read get_guide when nothing fits.${note ? ` ${note}` : ''}`,
|
|
692
814
|
} });
|
|
693
815
|
}
|
|
694
816
|
case 'ping':
|