@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 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:
@@ -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(', ') || '(none close)'}? Use search_components.`);
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(', ') || '(none close)'}?`);
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':