onsystem 0.8.0 → 0.9.0

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.
@@ -1,5 +1,5 @@
1
- import { Ct as isInside, Dt as defaultProjectConfig, F as TOKEN_CATEGORIES, G as DEFAULT_SEVERITY, H as CONFIG_FILES, J as LEGACY_CONFIG_FILES, Q as excludedBy, St as SCRIPT_FILE, Tt as relativePath, Y as RULE_IDS, _ as NAME, at as LIBRARIES, bt as truncate, ct as notReadYet, d as searchComponents, et as loadConfig, ft as detectProject, m as uncheckedNotice, n as DesignSystemHost, ot as LIBRARY_IDS, pt as findDesignSystemPackage, rt as formatPx, st as libraryTitle, u as buildSearchIndex, v as VERSION, vt as closest, xt as unique, yt as plural } from "./design-system-Dmcv7eHR.js";
2
- import { n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-CM1RyqZA.js";
1
+ import { $ as LEGACY_CONFIG_FILES, At as relativePath, Ct as closest, Dt as SCRIPT_FILE, Et as unique, Mt as defaultProjectConfig, Ot as isInside, Tt as truncate, X as DEFAULT_SEVERITY, _t as detectProject, at as loadConfig, ct as formatPx, d as descriptionOf, dt as LIBRARY_IDS, et as RULE_IDS, f as searchComponents, ft as libraryTitle, h as uncheckedNotice, n as DesignSystemHost, pt as notReadYet, q as CONFIG_FILES, rt as excludedBy, u as buildSearchIndex, ut as LIBRARIES, v as NAME, vt as findDesignSystemPackage, wt as plural, y as VERSION, z as TOKEN_CATEGORIES } from "./design-system-Bykei5W0.js";
2
+ import { n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-Dp1q8qZ8.js";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -232,19 +232,122 @@ function importStatement(component) {
232
232
  function variantSummary(component) {
233
233
  return component.variants.map((v) => `${v.name}: ${v.values.join(" | ")}${v.default ? ` (default ${v.default})` : ""}`);
234
234
  }
235
+ /** An icon: a component that renders an <svg>. */
236
+ function isIcon(component) {
237
+ return component.element === "svg";
238
+ }
239
+ /**
240
+ * Icons from one import, or from the modules of one folder, are listed by name on one line from
241
+ * this many on: a spinner or a logo is listed like the rest.
242
+ */
243
+ const ICON_LINE = 5;
244
+ /**
245
+ * What list_components shows of `items`, after `filter`. `packageOf`: at a workspace root, the
246
+ * package an item comes from, so two apps' `@/components/icons` are not taken for one.
247
+ */
248
+ function listComponents(items, component, filter = {}, packageOf = () => "") {
249
+ const query = filter.query?.trim().toLowerCase();
250
+ const matching = items.filter((item) => {
251
+ const c = component(item);
252
+ if (filter.kind && filter.kind === "icon" !== isIcon(c)) return false;
253
+ const texts = [
254
+ c.name,
255
+ ...c.aliases,
256
+ ...c.subcomponents,
257
+ descriptionOf(c) ?? ""
258
+ ];
259
+ return !query || texts.some((text) => text.toLowerCase().includes(query));
260
+ });
261
+ const icons = [];
262
+ if (!filter.kind) {
263
+ const group = (groups, item, from, folder) => {
264
+ const key = `${packageOf(item)}\0${from}`;
265
+ const known = groups.get(key) ?? {
266
+ from,
267
+ items: [],
268
+ ...folder ? { folder } : {}
269
+ };
270
+ known.items.push(item);
271
+ groups.set(key, known);
272
+ };
273
+ const byImport = /* @__PURE__ */ new Map();
274
+ for (const item of matching) {
275
+ const c = component(item);
276
+ if (isIcon(c)) group(byImport, item, c.library ?? c.importPath);
277
+ }
278
+ const byFolder = /* @__PURE__ */ new Map();
279
+ for (const found of byImport.values()) {
280
+ if (found.items.length >= ICON_LINE) {
281
+ icons.push(found);
282
+ continue;
283
+ }
284
+ for (const item of found.items) {
285
+ const c = component(item);
286
+ const folder = c.importPath.slice(0, Math.max(0, c.importPath.lastIndexOf("/")));
287
+ if (!c.library && folder) group(byFolder, item, folder, true);
288
+ }
289
+ }
290
+ icons.push(...[...byFolder.values()].filter((found) => found.items.length >= ICON_LINE));
291
+ }
292
+ const inLine = new Set(icons.flatMap((found) => found.items));
293
+ const rest = matching.filter((item) => !inLine.has(item));
294
+ const shown = rest.slice(0, filter.limit ?? rest.length);
295
+ return {
296
+ shown,
297
+ icons,
298
+ matched: matching.length,
299
+ omitted: rest.length - shown.length
300
+ };
301
+ }
302
+ /** `query "chart" and kind "icon"`, or '' without filters. */
303
+ function filterText(filter) {
304
+ return [filter.query?.trim() ? `query "${filter.query.trim()}"` : "", filter.kind ? `kind "${filter.kind}"` : ""].filter(Boolean).join(" and ");
305
+ }
306
+ /** The first line of a list: how many components there are, or how many match the filter. */
307
+ function listHead(roots, parts, listed, filter) {
308
+ const filtered = filterText(filter);
309
+ if (filtered) return `${listed.matched} of ${plural(roots, "component")} match ${filtered}`;
310
+ return `${plural(roots, "component")}${parts ? ` (+${plural(parts, "part")})` : ""}`;
311
+ }
312
+ /**
313
+ * `128 icons from "@dub/ui" (each renders <svg>; import { Name } from "@dub/ui"): Apple, Bolt`,
314
+ * or for a module per icon, `6 icons in "@/components/icons" (each renders <svg>; import { Name }
315
+ * from "@/components/icons/<module>"): CheckIcon (check), PlusIcon (plus)`.
316
+ */
317
+ function iconLine(group) {
318
+ const { from, items } = group;
319
+ const count = plural(items.length, "icon");
320
+ if (!group.folder) return `${count} from "${from}" (each renders <svg>; import { Name } from "${from}"): ${items.map((c) => c.name).join(", ")}`;
321
+ return `${count} in "${from}" (each renders <svg>; import { Name } from "${from}/<module>"): ${items.map((c) => `${c.name} (${c.importPath.slice(from.length + 1)})`).join(", ")}`;
322
+ }
323
+ /** What a list says at its end about the icons it named and the components `limit` left out. */
324
+ function listTail(listed) {
325
+ const lines = [];
326
+ if (listed.icons.length) lines.push("", "Pass kind \"icon\" to list the icons one by one.");
327
+ if (listed.omitted) lines.push("", `${listed.omitted.toLocaleString("en-US")} more not shown: pass a higher \`limit\`, or a \`query\`.`);
328
+ return lines;
329
+ }
235
330
  /** `project`: the folder of a workspace root's project the list is for. */
236
- function renderComponentList(ds, project) {
331
+ function renderComponentList(ds, project, filter = {}) {
237
332
  const roots = ds.roots();
238
- if (!roots.length) return `No components found${project ? ` in ${project}` : ""}. Check the "components" globs in the config (root: ${ds.root}).`;
333
+ const where = project ? ` in ${project}` : "";
334
+ if (!roots.length) return `No components found${where}. Check the "components" globs in the config (root: ${ds.root}).`;
335
+ const listed = listComponents(roots, (c) => c, filter);
336
+ if (!listed.matched) return `No components match ${filterText(filter)}${where} (${roots.length} in all). Call list_components without filters to see them.`;
239
337
  const parts = ds.components.length - roots.length;
240
- const lines = [`${plural(roots.length, "component")}${parts ? ` (+${plural(parts, "part")})` : ""}${project ? ` in ${project}` : ""}. Call get_component before using one.`, ""];
241
- const own = roots.filter((c) => !c.library);
338
+ const lines = [`${listHead(roots.length, parts, listed, filter)}${where}. Call get_component before using one.`, ""];
339
+ const own = listed.shown.filter((c) => !c.library);
242
340
  for (const component of own) lines.push(...listItem(component, registryNote(ds, component)));
243
- for (const library of new Set(roots.flatMap((c) => c.library ? [c.library] : []))) {
341
+ for (const library of new Set(listed.shown.flatMap((c) => c.library ? [c.library] : []))) {
244
342
  if (own.length) lines.push("");
245
343
  lines.push(`From ${library} (import { Name } from "${library}"):`, "");
246
- for (const component of roots.filter((c) => c.library === library)) lines.push(...listItem(component, "", false));
344
+ for (const component of listed.shown.filter((c) => c.library === library)) lines.push(...listItem(component, "", false));
345
+ }
346
+ for (const group of listed.icons) {
347
+ if (lines.at(-1) !== "") lines.push("");
348
+ lines.push(iconLine(group));
247
349
  }
350
+ lines.push(...listTail(listed));
248
351
  return lines.join("\n");
249
352
  }
250
353
  /** ` (from @signoff-ui, read-only)` for a component copied from a shadcn registry. */
@@ -254,7 +357,8 @@ function registryNote(ds, component) {
254
357
  }
255
358
  function listItem(component, note = "", withImport = true) {
256
359
  const element = component.element ? ` <${component.element}>` : "";
257
- const lines = [`${component.name}${element}${note} — ${firstSentence(component.description) || "No description."}`];
360
+ const description = firstSentence(descriptionOf(component));
361
+ const lines = [`${component.name}${element}${note}${description ? ` — ${description}` : ""}`];
258
362
  for (const variant of variantSummary(component)) lines.push(` ${variant}`);
259
363
  if (component.subcomponents.length) lines.push(` parts: ${component.subcomponents.join(", ")}`);
260
364
  if (withImport) lines.push(` ${importStatement(component)}`);
@@ -268,22 +372,44 @@ function listOf$1(items, max = 3) {
268
372
  }
269
373
  const PICK_PROJECT = "This is a monorepo root: pass `path`, the file you are editing, to get_component, get_tokens and check_ui so they answer for its project.";
270
374
  /** Every project's components at a workspace root, grouped by the package they come from. */
271
- function renderCatalogList(catalog) {
375
+ function renderCatalogList(catalog, filter = {}) {
272
376
  const roots = catalog.roots();
273
377
  if (!roots.length) return `No components found in any project of this monorepo (root: ${catalog.root}). Run \`onsystem inspect\` there to see what was looked at.`;
274
- const byPackage = /* @__PURE__ */ new Map();
275
- for (const entry of roots) byPackage.set(entry.package, [...byPackage.get(entry.package) ?? [], entry]);
378
+ const listed = listComponents(roots, (e) => e.component, filter, (e) => e.package);
379
+ if (!listed.matched) return `No components match ${filterText(filter)} in any project (${roots.length} in all). Call list_components without filters to see them.`;
380
+ const packages = new Set(roots.map((e) => e.package));
276
381
  const parts = catalog.entries.length - roots.length;
277
- const lines = [`${plural(roots.length, "component")}${parts ? ` (+${plural(parts, "part")})` : ""} from ${plural(byPackage.size, "package")}, used by ${plural(catalog.projects.length, "project")}. ${PICK_PROJECT}`];
278
- for (const [pkg, entries] of byPackage) {
279
- const projects = [...new Set(entries.flatMap((e) => e.projects))];
382
+ const lines = [`${listHead(roots.length, parts, listed, filter)} from ${plural(packages.size, "package")}, used by ${plural(catalog.projects.length, "project")}. ${PICK_PROJECT}`];
383
+ const byPackage = /* @__PURE__ */ new Map();
384
+ const section = (pkg) => {
385
+ const known = byPackage.get(pkg) ?? {
386
+ entries: [],
387
+ icons: []
388
+ };
389
+ byPackage.set(pkg, known);
390
+ return known;
391
+ };
392
+ for (const entry of listed.shown) section(entry.package).entries.push(entry);
393
+ for (const group of listed.icons) {
394
+ const [first] = group.items;
395
+ const items = group.items.map((e) => e.component);
396
+ if (first) section(first.package).icons.push(iconLine({
397
+ ...group,
398
+ items
399
+ }));
400
+ }
401
+ for (const [pkg, { entries, icons }] of byPackage) {
402
+ const projects = [...new Set(roots.filter((e) => e.package === pkg).flatMap((e) => e.projects))];
280
403
  lines.push("", `## ${pkg} (used by ${listOf$1(projects)})`);
281
404
  for (const entry of entries) {
282
- const others = entry.ambiguous ? catalog.roots().filter((e) => e !== entry && e.component.name === entry.component.name) : [];
405
+ const others = entry.ambiguous ? roots.filter((e) => e !== entry && e.component.name === entry.component.name) : [];
283
406
  const note = others.length ? ` (also in ${[...new Set(others.map((e) => e.package))].join(", ")})` : "";
284
407
  lines.push(...listItem(entry.component, note));
285
408
  }
409
+ if (entries.length && icons.length) lines.push("");
410
+ lines.push(...icons);
286
411
  }
412
+ lines.push(...listTail(listed));
287
413
  return lines.join("\n");
288
414
  }
289
415
  /** Several components under one name at a workspace root, and how to pick one. */
@@ -362,18 +488,26 @@ const REACT_ARIA_NOTABLE = [
362
488
  "isReadOnly",
363
489
  "autoFocus"
364
490
  ];
491
+ /**
492
+ * A prop's description longer than this is cut to its first sentence: a primitive library's
493
+ * JSDoc (Base UI's, Radix's) runs to paragraphs per prop, and the structured content has it all.
494
+ */
495
+ const PROP_LINE = 160;
365
496
  function renderProp(prop) {
366
497
  const optional = prop.required ? "" : "?";
367
498
  const fallback = prop.default !== void 0 ? ` = ${prop.default}` : "";
368
499
  const deprecated = prop.deprecated ? ` DEPRECATED${prop.deprecated === true ? "" : `: ${prop.deprecated}`}.` : "";
369
- const description = prop.description ? ` — ${prop.description.replace(/\s*\n\s*/g, " ")}` : "";
500
+ const text = prop.description?.replace(/\s*\n\s*/g, " ").trim();
501
+ const line = text && text.length > PROP_LINE ? firstSentence(text, PROP_LINE) : text;
502
+ const description = line ? ` — ${line}` : "";
370
503
  return `- ${prop.name}${optional}: ${prop.type}${fallback}${description}${deprecated}`;
371
504
  }
372
505
  /** `origin`: at a workspace root, the package the component comes from and who uses it. */
373
506
  function renderComponent(ds, component, origin) {
374
507
  const lines = [`# ${component.name}`];
375
508
  if (component.deprecated) lines.push(`DEPRECATED${component.deprecated === true ? "" : `: ${component.deprecated}`}`);
376
- if (component.description) lines.push(component.description);
509
+ const description = descriptionOf(component);
510
+ if (description) lines.push(description);
377
511
  lines.push("", importStatement(component));
378
512
  const facts = [
379
513
  component.element ? `Renders <${component.element}>` : void 0,
@@ -386,6 +520,13 @@ function renderComponent(ds, component, origin) {
386
520
  if (origin) lines.push(`From ${origin.package}, used by ${listOf$1(origin.projects)}.`);
387
521
  const registry = ds.registryOf(component);
388
522
  if (registry) lines.push(`Installed from the shadcn registry ${registry.registry} into ${registry.folder}: use it as it is. Its code is the registry's, so change it upstream, not here; \`npx shadcn add --overwrite\` replaces the file.`);
523
+ if (component.subcomponents.length) {
524
+ lines.push("", "## Parts");
525
+ for (const name of component.subcomponents) {
526
+ const part = ds.getComponent(name);
527
+ lines.push(`- ${name}${part?.description ? ` — ${firstSentence(part.description)}` : ""}`);
528
+ }
529
+ }
389
530
  lines.push("", "## Props");
390
531
  if (!component.props.length && !component.inherits.length) lines.push("No props.");
391
532
  for (const prop of component.props) lines.push(renderProp(prop));
@@ -411,13 +552,6 @@ function renderComponent(ds, component, origin) {
411
552
  lines.push(` when ${when}: ${compound.classes}`);
412
553
  }
413
554
  }
414
- if (component.subcomponents.length) {
415
- lines.push("", "## Parts");
416
- for (const name of component.subcomponents) {
417
- const part = ds.getComponent(name);
418
- lines.push(`- ${name}${part?.description ? ` — ${firstSentence(part.description)}` : ""}`);
419
- }
420
- }
421
555
  const tokens = ds.relatedTokens(component);
422
556
  if (tokens.length) {
423
557
  lines.push("", "## Design tokens used");
@@ -440,31 +574,63 @@ function renderComponent(ds, component, origin) {
440
574
  }
441
575
  return lines.join("\n");
442
576
  }
577
+ /** The first line of search results: best first, or, when none clearly matches, that none does. */
578
+ function searchHead(query, hits, where = "") {
579
+ return hits.every((hit) => hit.weak) ? `No component clearly matches "${query}"${where}. The nearest have only a word or two in common with it:` : `Components for "${query}"${where}, best first:`;
580
+ }
581
+ /** The last line of search results. */
582
+ function searchTail(hits) {
583
+ return hits.every((hit) => hit.weak) ? "If none of them fits, the design system may have no component for this: ask, or build it from the components it has (list_components). Call get_component before using one." : "Call get_component for props, variants and examples.";
584
+ }
585
+ /** `1. CardHeader (part of Card) — Top section of a card.` */
586
+ function searchItem(index, component, from = "") {
587
+ const parent = component.parent ? ` (part of ${component.parent})` : "";
588
+ const description = firstSentence(descriptionOf(component));
589
+ return `${index + 1}. ${component.name}${parent}${from}${description ? ` — ${description}` : ""}`;
590
+ }
443
591
  function renderSearch(query, hits) {
444
592
  if (!hits.length) return `No components match "${query}". Try other words, or list_components to browse.`;
445
- const lines = [`Components for "${query}", best first:`, ""];
593
+ const lines = [searchHead(query, hits), ""];
446
594
  hits.forEach((hit, i) => {
447
595
  const c = hit.component;
448
- const parent = c.parent ? ` (part of ${c.parent})` : "";
449
- lines.push(`${i + 1}. ${c.name}${parent} — ${firstSentence(c.description) || "No description."}`);
596
+ lines.push(searchItem(i, c));
450
597
  lines.push(` score ${hit.score} · matched: ${hit.matched.join(", ")} · ${importStatement(c)}`);
451
598
  });
452
- lines.push("", "Call get_component for props, variants and examples.");
599
+ lines.push("", searchTail(hits));
453
600
  return lines.join("\n");
454
601
  }
455
602
  /** Search hits at a workspace root, each with the package it comes from. */
456
603
  function renderCatalogSearch(query, hits) {
457
604
  if (!hits.length) return `No components match "${query}" in any project. Try other words, or list_components to browse.`;
458
- const lines = [`Components for "${query}" across the monorepo's projects, best first:`, ""];
605
+ const lines = [searchHead(query, hits, " across the monorepo's projects"), ""];
459
606
  hits.forEach((hit, i) => {
460
607
  const c = hit.component;
461
- const parent = c.parent ? ` (part of ${c.parent})` : "";
462
- lines.push(`${i + 1}. ${c.name}${parent} from ${hit.entry.package} — ${firstSentence(c.description) || "No description."}`);
608
+ lines.push(searchItem(i, c, ` from ${hit.entry.package}`));
463
609
  lines.push(` score ${hit.score} · matched: ${hit.matched.join(", ")} · used by ${listOf$1(hit.entry.projects)} · ${importStatement(c)}`);
464
610
  });
465
- lines.push("", `Call get_component for props, variants and examples. ${PICK_PROJECT}`);
611
+ lines.push("", `${searchTail(hits)} ${PICK_PROJECT}`);
466
612
  return lines.join("\n");
467
613
  }
614
+ /**
615
+ * Where a token is listed in get_tokens: under its category, or, for a color of a scoped
616
+ * family (`chart-*`, `sidebar-*`, the syntax colors one file uses), under that family, with
617
+ * what it is for. An agent looking for a green with no `--success` in the theme otherwise finds
618
+ * `chart-2` among the colors, takes its dark value, and check_ui has nothing against it.
619
+ */
620
+ function tokenGroup(ds, token) {
621
+ const family = ds.tokenIndex.scopedFamilyOf(token);
622
+ return family === void 0 ? token.category : `${SCOPED}${family}`;
623
+ }
624
+ /** Marks a scoped family's group: no category starts with it. */
625
+ const SCOPED = "\0";
626
+ function groupHeading(ds, group, count) {
627
+ if (!group.startsWith(SCOPED)) return `## ${group} (${count})`;
628
+ const family = group.slice(1);
629
+ if (family === "chart") return `## chart (${count}): for charts only, not for status colors`;
630
+ if (family === "sidebar") return `## sidebar (${count}): for the sidebar only, not for status colors`;
631
+ const [file] = ds.tokenIndex.familyFiles.get(family) ?? [];
632
+ return `## ${family} (${count}): for one part of the UI only${file ? ` (${file} uses them)` : ""}, not for status colors`;
633
+ }
468
634
  /** Tokens at a workspace root, grouped by category, each with the package that defines it. */
469
635
  function renderCatalogTokens(catalog, tokens, filter = {}) {
470
636
  if (!catalog.tokens.length) return `No design tokens found in any project of this monorepo (root: ${catalog.root}). Set "tokens" in a project's config: https://onsystem.vercel.app/docs/configuration`;
@@ -473,10 +639,18 @@ function renderCatalogTokens(catalog, tokens, filter = {}) {
473
639
  return `No tokens match${what ? ` ${what}` : ""} in any project; call get_tokens without filters to see them.`;
474
640
  }
475
641
  const groups = /* @__PURE__ */ new Map();
476
- for (const t of tokens) groups.set(t.token.category, [...groups.get(t.token.category) ?? [], t]);
642
+ for (const t of tokens) {
643
+ const key = tokenGroup(t.ds, t.token);
644
+ const group = groups.get(key) ?? {
645
+ ds: t.ds,
646
+ list: []
647
+ };
648
+ group.list.push(t);
649
+ groups.set(key, group);
650
+ }
477
651
  const lines = [PICK_PROJECT, ""];
478
- for (const [category, list] of groups) {
479
- lines.push(`## ${category} (${list.length})`);
652
+ for (const [key, { ds, list }] of groups) {
653
+ lines.push(groupHeading(ds, key, list.length));
480
654
  for (const { token, package: pkg } of list) {
481
655
  const usage = token.usage.length ? ` → ${token.usage.join(", ")}` : "";
482
656
  lines.push(`- ${token.name}: ${token.value}${usage} (${pkg})`);
@@ -495,10 +669,13 @@ function renderTokens(ds, tokens, filter = {}) {
495
669
  return `No tokens match${what ? ` ${what}` : ""}. The design system has ${categories}; call get_tokens without filters to see them.`;
496
670
  }
497
671
  const groups = /* @__PURE__ */ new Map();
498
- for (const token of tokens) groups.set(token.category, [...groups.get(token.category) ?? [], token]);
672
+ for (const token of tokens) {
673
+ const group = tokenGroup(ds, token);
674
+ groups.set(group, [...groups.get(group) ?? [], token]);
675
+ }
499
676
  const lines = [];
500
- for (const [category, list] of groups) {
501
- lines.push(`## ${category} (${list.length})`);
677
+ for (const [group, list] of groups) {
678
+ lines.push(groupHeading(ds, group, list.length));
502
679
  for (const token of list) {
503
680
  const px = token.category === "spacing" || token.category === "radius" ? ds.tokenIndex.toPx(token.value) : void 0;
504
681
  const value = px !== void 0 && !/^-?[\d.]+px$/.test(token.value) ? `${token.value} = ${formatPx(px)}` : token.value;
@@ -548,7 +725,7 @@ const INSTRUCTIONS = `This server describes the project's design system: its Rea
548
725
  When writing or changing UI in this project:
549
726
  1. Find components with search_components (by intent) or list_components.
550
727
  2. Read get_component before using a component: use only the props and variant values it lists.
551
- 3. Style with tokens from get_tokens, never with hex/rgb colors, arbitrary px values or Tailwind's default palette.
728
+ 3. Style with tokens from get_tokens, never with hex/rgb colors, arbitrary px values or Tailwind's default palette. chart-* and sidebar-* tokens are for charts and the sidebar only: for a status color the theme has no token for (success, warning, info), add a token or keep the Tailwind color as check_ui says, and never use a chart-* or sidebar-* token for it.
552
729
  4. Run check_ui on every snippet or file you write and fix all errors before you finish.
553
730
  In a monorepo served from its root, pass \`path\` (the file you are editing) to list_components, get_component, search_components and get_tokens, and \`path\` or \`filename\` to check_ui, so each answer comes from that file's own project.`;
554
731
  const READ_ONLY = {
@@ -575,9 +752,12 @@ const MAX_CODE = 1e6;
575
752
  /** Diagnostics check_ui returns by default: a 3,000-line file can have thousands, more than a context holds. */
576
753
  const DEFAULT_LIMIT = 50;
577
754
  const MAX_LIMIT = 1e3;
578
- /** check_ui's answer for a path or filename that is not JS or TS. */
579
- function notScript(name) {
580
- return errorResult(`check_ui checks .tsx, .jsx, .ts and .js files; ${name} is not one, so it was not checked.`);
755
+ /**
756
+ * check_ui's answer for a path or filename that is not JS or TS. `fix`: what to do instead,
757
+ * for a `filename` given with code (the code itself may be fine).
758
+ */
759
+ function notScript(name, fix = "") {
760
+ return errorResult(`check_ui checks .tsx, .jsx, .ts and .js files; ${name} is not one, so it was not checked.${fix}`);
581
761
  }
582
762
  /**
583
763
  * An integer, without the ±9007199254740991 bounds zod adds to the JSON Schema,
@@ -675,8 +855,8 @@ const componentSummarySchema = z.object({
675
855
  description: z.string().optional(),
676
856
  importPath: z.string(),
677
857
  element: z.string().optional(),
678
- variants: z.record(z.string(), z.array(z.string())),
679
- subcomponents: z.array(z.string()),
858
+ variants: z.record(z.string(), z.array(z.string())).optional().describe("Left out when none."),
859
+ subcomponents: z.array(z.string()).optional().describe("Left out when none."),
680
860
  ...originShape
681
861
  });
682
862
  const tokenSchema = z.object({
@@ -733,14 +913,58 @@ function origin(entry) {
733
913
  projects: entry.projects
734
914
  };
735
915
  }
916
+ /** A component in list_components, without the fields it has nothing in. */
736
917
  function summary(c) {
918
+ const description = descriptionOf(c);
737
919
  return {
738
920
  name: c.name,
739
- ...c.description ? { description: c.description } : {},
921
+ ...description ? { description } : {},
740
922
  importPath: c.importPath,
741
923
  ...c.element ? { element: c.element } : {},
742
- variants: Object.fromEntries(c.variants.map((v) => [v.name, v.values])),
743
- subcomponents: c.subcomponents
924
+ ...c.variants.length ? { variants: Object.fromEntries(c.variants.map((v) => [v.name, v.values])) } : {},
925
+ ...c.subcomponents.length ? { subcomponents: c.subcomponents } : {}
926
+ };
927
+ }
928
+ /** list_components' structured content: what its text lists, as it lists it. */
929
+ function listContent(listed, component, originOf) {
930
+ return {
931
+ components: listed.shown.map((item) => ({
932
+ ...summary(component(item)),
933
+ ...originOf?.(item)
934
+ })),
935
+ ...listed.icons.length ? { icons: listed.icons.map(({ from, items, folder }) => ({
936
+ from,
937
+ names: items.map((item) => component(item).name),
938
+ ...folder ? { modules: items.map((item) => component(item).importPath) } : {},
939
+ ...originOf && items[0] ? { package: originOf(items[0]).package } : {}
940
+ })) } : {},
941
+ ...listed.omitted ? { omitted: listed.omitted } : {}
942
+ };
943
+ }
944
+ /**
945
+ * Said above a component asked for by a name that is not how its design system writes it:
946
+ * `Card.Header` finds CardHeader, and code written as asked would not compile.
947
+ */
948
+ function nameNotice(asked, component) {
949
+ if (asked === component.name || component.aliases.includes(asked)) return "";
950
+ if (!asked.includes(".") && !component.name.includes(".")) return "";
951
+ return `There is no \`${asked}\` in this design system: write \`<${component.name}>\`.\n\n`;
952
+ }
953
+ function resourceDescription(component) {
954
+ const description = descriptionOf(component);
955
+ return description ? { description } : {};
956
+ }
957
+ /** A search_components result. */
958
+ function result(hit) {
959
+ const description = descriptionOf(hit.component);
960
+ return {
961
+ name: hit.component.name,
962
+ ...hit.component.parent ? { parent: hit.component.parent } : {},
963
+ ...description ? { description } : {},
964
+ importPath: hit.component.importPath,
965
+ score: hit.score,
966
+ matched: hit.matched,
967
+ ...hit.weak ? { weak: true } : {}
744
968
  };
745
969
  }
746
970
  /** Builds the MCP server: five tools, two resources and one prompt. Transport-agnostic. */
@@ -798,33 +1022,51 @@ function createServer({ getDesignSystem, getProjects }) {
798
1022
  };
799
1023
  server.registerTool("list_components", {
800
1024
  title: "List design-system components",
801
- description: "List every component in the project design system with a one-line description, the native element it renders, its variant values, its parts and its import. Call this before writing UI in this project. At a monorepo root, pass `path` for the project of the file you are editing; without it, every project is listed, each component with its package.",
802
- inputSchema: z.object({ path: projectPath }),
803
- outputSchema: z.object({ components: z.array(componentSummarySchema) }),
1025
+ description: "List every component in the project design system with a one-line description, the native element it renders, its variant values, its parts and its import. Icons (components that render an <svg>) are named on one line per import, or per folder for icons with a module each, five or more to a line. Call this before writing UI in this project. `query`, `kind` and `limit` narrow a long list. At a monorepo root, pass `path` for the project of the file you are editing; without it, every project is listed, each component with its package.",
1026
+ inputSchema: z.object({
1027
+ query: z.string().max(MAX_QUERY).optional().describe("Only components with this text in their name, their parts' names or their description."),
1028
+ kind: z.enum(["component", "icon"]).optional().describe("\"icon\": only the components that render an <svg>, one by one. \"component\": all the others."),
1029
+ limit: z.number().int().min(1).max(MAX_LIMIT).optional().describe("Most components to list one by one."),
1030
+ path: projectPath
1031
+ }),
1032
+ outputSchema: z.object({
1033
+ components: z.array(componentSummarySchema).describe("The components listed one by one, after `query`, `kind` and `limit`."),
1034
+ icons: z.array(z.object({
1035
+ from: z.string().describe("The module they are imported from, or, with `modules`, the folder of their modules."),
1036
+ names: z.array(z.string()),
1037
+ modules: z.array(z.string()).optional().describe("For icons with a module each: each one's module, in the order of `names`."),
1038
+ package: originShape.package
1039
+ })).optional().describe("Without `kind`: icons, the components that render an <svg>, by name, five or more per module, or per folder for icons with a module each."),
1040
+ omitted: int().optional().describe("Matching components left out by `limit`.")
1041
+ }),
804
1042
  annotations: READ_ONLY
805
- }, async ({ path: target }) => {
1043
+ }, async ({ query, kind, limit, path: target }) => {
806
1044
  const found = await scoped(target);
807
1045
  if (isResult(found)) return found;
1046
+ const filter = {
1047
+ query,
1048
+ kind,
1049
+ limit
1050
+ };
808
1051
  if ("catalog" in found) {
809
1052
  const { catalog } = found;
1053
+ const listed = listComponents(catalog.roots(), (entry) => entry.component, filter, (entry) => entry.package);
810
1054
  return {
811
1055
  content: [{
812
1056
  type: "text",
813
- text: renderCatalogList(catalog)
1057
+ text: renderCatalogList(catalog, filter)
814
1058
  }],
815
- structuredContent: { components: catalog.roots().map((entry) => ({
816
- ...summary(entry.component),
817
- ...origin(entry)
818
- })) }
1059
+ structuredContent: listContent(listed, (entry) => entry.component, origin)
819
1060
  };
820
1061
  }
821
1062
  const { ds } = found;
1063
+ const listed = listComponents(ds.roots(), (c) => c, filter);
822
1064
  return {
823
1065
  content: [{
824
1066
  type: "text",
825
- text: renderComponentList(ds, found.project?.dir)
1067
+ text: renderComponentList(ds, found.project?.dir, filter)
826
1068
  }],
827
- structuredContent: { components: ds.roots().map(summary) }
1069
+ structuredContent: listContent(listed, (c) => c)
828
1070
  };
829
1071
  });
830
1072
  server.registerTool("get_component", {
@@ -849,13 +1091,15 @@ function createServer({ getDesignSystem, getProjects }) {
849
1091
  return errorResult(`No component named "${name}" in any project.` + (guess ? ` Did you mean "${guess}"?` : "") + ` Available: ${names.join(", ")}.`);
850
1092
  }
851
1093
  if (matches.length > 1) return errorResult(renderAmbiguous(name, matches));
1094
+ const description = descriptionOf(entry.component);
852
1095
  return {
853
1096
  content: [{
854
1097
  type: "text",
855
- text: renderComponent(entry.ds, entry.component, entry)
1098
+ text: nameNotice(name, entry.component) + renderComponent(entry.ds, entry.component, entry)
856
1099
  }],
857
1100
  structuredContent: {
858
1101
  ...entry.component,
1102
+ ...description ? { description } : {},
859
1103
  import: importStatement(entry.component),
860
1104
  tokens: entry.ds.relatedTokens(entry.component).map((t) => t.name),
861
1105
  ...origin(entry)
@@ -875,13 +1119,15 @@ function createServer({ getDesignSystem, getProjects }) {
875
1119
  };
876
1120
  }
877
1121
  const shown = fromRoot(ds, component, found.root);
1122
+ const description = descriptionOf(component);
878
1123
  return {
879
1124
  content: [{
880
1125
  type: "text",
881
- text: renderComponent(ds, shown)
1126
+ text: nameNotice(name, component) + renderComponent(ds, shown)
882
1127
  }],
883
1128
  structuredContent: {
884
1129
  ...shown,
1130
+ ...description ? { description } : {},
885
1131
  import: importStatement(component),
886
1132
  tokens: ds.relatedTokens(component).map((t) => t.name)
887
1133
  }
@@ -889,7 +1135,7 @@ function createServer({ getDesignSystem, getProjects }) {
889
1135
  });
890
1136
  server.registerTool("search_components", {
891
1137
  title: "Search components by intent",
892
- description: "Find components by what you want to build, e.g. \"confirm a destructive action\", \"status label\", \"text field with validation\". Ranks names, descriptions, docs, props and variant values with BM25 (local, no API key). Use it when you do not know which component fits.",
1138
+ description: "Find components by what you want to build, e.g. \"confirm a destructive action\", \"status label\", \"text field with validation\". Ranks names, descriptions, docs, props and variant values with BM25 (local, no API key). When no component clearly matches, it says so and lists the nearest, marked `weak`. Use it when you do not know which component fits.",
893
1139
  inputSchema: z.object({
894
1140
  query: z.string().min(1).max(MAX_QUERY).describe("What the UI should do, in plain words."),
895
1141
  limit: z.number().int().min(1).max(20).default(5).describe("Maximum results."),
@@ -902,6 +1148,7 @@ function createServer({ getDesignSystem, getProjects }) {
902
1148
  importPath: z.string(),
903
1149
  score: z.number(),
904
1150
  matched: z.array(z.string()),
1151
+ weak: z.boolean().optional().describe("Set when no component clearly matches the query: each result has only a word or two in common with it."),
905
1152
  ...originShape
906
1153
  })) }),
907
1154
  annotations: READ_ONLY
@@ -916,12 +1163,7 @@ function createServer({ getDesignSystem, getProjects }) {
916
1163
  text: renderCatalogSearch(query, hits)
917
1164
  }],
918
1165
  structuredContent: { results: hits.map((h) => ({
919
- name: h.component.name,
920
- ...h.component.parent ? { parent: h.component.parent } : {},
921
- ...h.component.description ? { description: h.component.description } : {},
922
- importPath: h.component.importPath,
923
- score: h.score,
924
- matched: h.matched,
1166
+ ...result(h),
925
1167
  ...origin(h.entry)
926
1168
  })) }
927
1169
  };
@@ -933,19 +1175,12 @@ function createServer({ getDesignSystem, getProjects }) {
933
1175
  type: "text",
934
1176
  text: renderSearch(query, hits)
935
1177
  }],
936
- structuredContent: { results: hits.map((h) => ({
937
- name: h.component.name,
938
- ...h.component.parent ? { parent: h.component.parent } : {},
939
- ...h.component.description ? { description: h.component.description } : {},
940
- importPath: h.component.importPath,
941
- score: h.score,
942
- matched: h.matched
943
- })) }
1178
+ structuredContent: { results: hits.map(result) }
944
1179
  };
945
1180
  });
946
1181
  server.registerTool("get_tokens", {
947
1182
  title: "Get design tokens",
948
- description: "List design tokens with resolved values, dark-mode values and how to use each (Tailwind class or CSS variable). Filter by category (color, spacing, radius, typography, shadow, other) or a substring. Use these instead of hex codes, px values or Tailwind default palette classes.",
1183
+ description: "List design tokens with resolved values, dark-mode values and how to use each (Tailwind class or CSS variable). Filter by category (color, spacing, radius, typography, shadow, other) or a substring. Use these instead of hex codes, px values or Tailwind default palette classes. Chart and sidebar colors are listed under their own headings: they are for charts and the sidebar, not for status colors.",
949
1184
  inputSchema: z.object({
950
1185
  category: z.enum(TOKEN_CATEGORIES).optional().describe("Only tokens of this category."),
951
1186
  query: z.string().max(MAX_QUERY).optional().describe("Substring to match in names, usages or descriptions."),
@@ -1010,10 +1245,10 @@ function createServer({ getDesignSystem, getProjects }) {
1010
1245
  });
1011
1246
  server.registerTool("check_ui", {
1012
1247
  title: "Check UI code against the design system",
1013
- description: "Lint TSX/JSX against the design system. Reports hardcoded colors, spacing and radius (with the nearest token), native elements that have a design-system component, unknown components, props and variant values, and icon-only buttons without an accessible name. Each diagnostic has line/column, a rule id, a message and a suggested fix. Run it on every snippet or file you write and fix all errors. Pass `code` for unsaved code, or `path` for a file in the project.",
1248
+ description: "Lint TSX/JSX against the design system. Reports hardcoded colors, spacing and radius (with the nearest token), native elements that have a design-system component, unknown components, props and variant values, and icon-only buttons without an accessible name. Each diagnostic has line/column, a rule id, a message and a suggested fix. Run it on every snippet or file you write and fix all errors. Pass `path` for a file in the project, or `code` for unsaved code, with `path` (or `filename`) naming the file it is for or its project folder.",
1014
1249
  inputSchema: z.object({
1015
1250
  code: z.string().max(MAX_CODE).optional().describe(`TSX/JSX source to check (up to ${MAX_CODE.toLocaleString("en")} characters).`),
1016
- path: z.string().max(MAX_PATH).optional().describe("Path of a .tsx, .jsx, .ts or .js file to check: relative to the project root (the monorepo root, when served from there), or absolute."),
1251
+ path: z.string().max(MAX_PATH).optional().describe("Path of a .tsx, .jsx, .ts or .js file, relative to the project root (the monorepo root, when served from there) or absolute: the file to check, or, with `code`, the file the code is for, as `filename`. With `code`, a project folder only picks the project."),
1017
1252
  filename: z.string().max(MAX_PATH).optional().describe("Name to report `code` under; a .jsx extension parses it as JSX. At a monorepo root, the path the code will be saved under, which picks the project whose design system checks it. When the server was started outside your project, pass it as an absolute path: the project is found from it."),
1018
1253
  limit: z.number().int().min(1).max(MAX_LIMIT).default(DEFAULT_LIMIT).describe(`Most diagnostics to return, errors first (default ${DEFAULT_LIMIT}). The counts and byRule cover all of them.`)
1019
1254
  }),
@@ -1036,11 +1271,14 @@ function createServer({ getDesignSystem, getProjects }) {
1036
1271
  notice: z.string().optional().describe("Set when no components or no color tokens were found, so rules did not run, when components came from modules the model does not include, when the project depends on a component library onsystem does not read yet, or when its dependencies are not installed.")
1037
1272
  }),
1038
1273
  annotations: READ_ONLY
1039
- }, async ({ code, path: filePath, filename, limit }) => {
1274
+ }, async ({ code, path: filePath, filename: given, limit }) => {
1040
1275
  if (code === void 0 && !filePath) return errorResult("Pass either `code` or `path`.");
1041
- if (code !== void 0 && filename !== void 0 && !SCRIPT_FILE.test(filename)) return notScript(filename);
1042
- const routeBy = code === void 0 ? filePath : filename ?? void 0;
1276
+ if (code !== void 0 && given !== void 0 && !SCRIPT_FILE.test(given)) return notScript(given, " Pass as `filename` the .tsx path the code is for, or leave `filename` out.");
1277
+ const folder = code !== void 0 && filePath && !SCRIPT_FILE.test(filePath) ? filePath : void 0;
1278
+ const filename = code === void 0 || folder !== void 0 ? given : given ?? (filePath || void 0);
1279
+ let routeBy = code === void 0 ? filePath : filename ?? folder;
1043
1280
  const set = await getProjects?.(routeBy);
1281
+ if (set && folder !== void 0 && routeBy === folder && path.resolve(set.root, folder) === set.root && !set.dirs.includes(".")) routeBy = void 0;
1044
1282
  let ds;
1045
1283
  if (!set) ds = await getDesignSystem(routeBy);
1046
1284
  else if (routeBy) {
@@ -1128,14 +1366,14 @@ function createServer({ getDesignSystem, getProjects }) {
1128
1366
  uri: `ds://components/${c.name}${ambiguous ? `?path=${encodeURIComponent(projects[0] ?? ".")}` : ""}`,
1129
1367
  name: c.name,
1130
1368
  title: c.name,
1131
- ...c.description ? { description: c.description } : {},
1369
+ ...resourceDescription(c),
1132
1370
  mimeType: "text/markdown"
1133
1371
  })) };
1134
1372
  return { resources: found.ds.components.map((c) => ({
1135
1373
  uri: `ds://components/${c.name}`,
1136
1374
  name: c.name,
1137
1375
  title: c.name,
1138
- ...c.description ? { description: c.description } : {},
1376
+ ...resourceDescription(c),
1139
1377
  mimeType: "text/markdown"
1140
1378
  })) };
1141
1379
  },
@@ -1216,8 +1454,8 @@ function createServer({ getDesignSystem, getProjects }) {
1216
1454
 
1217
1455
  Work like this:
1218
1456
  1. Find components: search_components for each piece of the UI (for example "confirm a destructive action"), or list_components for an overview.
1219
- 2. Read the contract: get_component for every component you use. Use only the props and variant values it lists, and compose parts the way its examples do.
1220
- 3. Style with tokens: get_tokens when you need a color, spacing or radius. Never write hex/rgb colors, arbitrary px values or Tailwind default palette classes such as bg-blue-500.
1457
+ 2. Read the contract: get_component for every component you use. Use only the props and variant values it lists, and compose its parts as its docs and examples show, when it has them.
1458
+ 3. Style with tokens: get_tokens when you need a color, spacing or radius. Never write hex/rgb colors, arbitrary px values or Tailwind default palette classes such as bg-blue-500. chart-* and sidebar-* tokens are for charts and the sidebar only: for a status color the theme has no token for (success, warning, info), add a token or keep the Tailwind color as check_ui says, and never use a chart-* or sidebar-* token for it.
1221
1459
  4. Use design-system components instead of native elements (<Button>, not <button>).
1222
1460
  5. Verify: run check_ui on the code you wrote, fix every error, and run it again until it reports none.
1223
1461