@sakaniui/react 0.8.11 → 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 +6 -0
- package/CHANGELOG.md +16 -0
- package/bin/sakaniui-mcp.mjs +32 -6
- package/dist/icon-index.json +1 -1
- package/dist/manifest.json +434 -63
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -152,6 +152,12 @@ CSS of a component into your own file.
|
|
|
152
152
|
|
|
153
153
|
Format: **Use for** · **Why it is built this way** · **Not for / instead**.
|
|
154
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
|
+
|
|
155
161
|
### Actions
|
|
156
162
|
- **Button** — Use for any action that does something. *Why:* 5 variants × 3 sizes mirror Figma 1:1;
|
|
157
163
|
`loading` disables interaction so double-submits can't happen. *Not for* navigation → `Link`; icon-only →
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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
|
+
|
|
5
21
|
## 0.8.11
|
|
6
22
|
|
|
7
23
|
Found by asking Claude Code, with the published 0.8.10 MCP server, to add a delete button to a table row:
|
package/bin/sakaniui-mcp.mjs
CHANGED
|
@@ -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;
|
|
@@ -434,7 +460,7 @@ function checkCode(source) {
|
|
|
434
460
|
if (!c && !isUtility) {
|
|
435
461
|
if (TYPE_LIKE.test(n.imported)) continue; // most likely a type export
|
|
436
462
|
const near = closest(n.imported);
|
|
437
|
-
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.` });
|
|
438
464
|
}
|
|
439
465
|
if (c && inInstalled(c) === false) errors.push({ line: imp.line, message: `${c.name} was added in ${c.since}; the project has ${projectVersion}.` });
|
|
440
466
|
}
|
|
@@ -578,7 +604,7 @@ const tools = {
|
|
|
578
604
|
const terms = q.value.toLowerCase().split(/\s+/).filter(Boolean);
|
|
579
605
|
const hits = manifest.components
|
|
580
606
|
.filter((c) => !k.value || c.kind === k.value)
|
|
581
|
-
.map((c) => [score(c, terms), c])
|
|
607
|
+
.map((c) => [score(c, terms, q.value), c])
|
|
582
608
|
.filter(([s]) => s > 0)
|
|
583
609
|
.sort((a, b) => b[0] - a[0])
|
|
584
610
|
.slice(0, limit)
|
|
@@ -594,7 +620,7 @@ const tools = {
|
|
|
594
620
|
run(args) {
|
|
595
621
|
const n = strArg(args, 'name', { required: true }); if (n.err) return fail(n.err);
|
|
596
622
|
const c = byName.get(n.value.toLowerCase());
|
|
597
|
-
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, ' '));
|
|
598
624
|
const out = { ...c, note: 'Native HTML attributes (className, style, aria-*, event handlers) are also accepted on the root element.' };
|
|
599
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.';
|
|
600
626
|
if (c.recipes && c.recipes.length) out.recipeNote = 'See `recipes`: patterns the types alone do not teach, compiled against this release.';
|
|
@@ -660,7 +686,7 @@ const tools = {
|
|
|
660
686
|
if (typeof props !== 'object' || Array.isArray(props)) return fail('"props" must be an object of prop name -> value.');
|
|
661
687
|
if (Object.keys(props).length > 200) return fail('"props" has too many entries (max 200).');
|
|
662
688
|
const c = byName.get(n.value.toLowerCase());
|
|
663
|
-
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());
|
|
664
690
|
const known = new Map(c.props.map((p) => [p.name, p]));
|
|
665
691
|
const errors = [];
|
|
666
692
|
const warnings = [];
|
|
@@ -784,7 +810,7 @@ function handle(msg) {
|
|
|
784
810
|
protocolVersion: asked && PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0],
|
|
785
811
|
capabilities: { tools: { listChanged: false } },
|
|
786
812
|
serverInfo: { name: 'sakaniui', title: 'SakaniUI', version: refVersion },
|
|
787
|
-
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}` : ''}`,
|
|
788
814
|
} });
|
|
789
815
|
}
|
|
790
816
|
case 'ping':
|