onsystem 0.8.0 → 0.9.1

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-DZm2aP1z.js";
2
+ import { n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-Dqu3nkYM.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,127 @@ 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
+ }
330
+ /** The shared explanation when a project has no design-system components to show. */
331
+ function noComponentsMessage(root, project, monorepo = false) {
332
+ if (monorepo) return `No design-system components found in any project of this monorepo (root: ${root}). Check the "components" globs in the config. Run \`onsystem inspect\` there to see what was looked at.`;
333
+ return `No design-system components found${project ? ` in ${project}` : ""}. Check the "components" globs in the config (root: ${root}).`;
334
+ }
235
335
  /** `project`: the folder of a workspace root's project the list is for. */
236
- function renderComponentList(ds, project) {
336
+ function renderComponentList(ds, project, filter = {}) {
237
337
  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}).`;
338
+ const where = project ? ` in ${project}` : "";
339
+ if (!roots.length) return noComponentsMessage(ds.root, project);
340
+ const listed = listComponents(roots, (c) => c, filter);
341
+ if (!listed.matched) return `No components match ${filterText(filter)}${where} (${roots.length} in all). Call list_components without filters to see them.`;
239
342
  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);
343
+ const lines = [`${listHead(roots.length, parts, listed, filter)}${where}. Call get_component before using one.`, ""];
344
+ const own = listed.shown.filter((c) => !c.library);
242
345
  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] : []))) {
346
+ for (const library of new Set(listed.shown.flatMap((c) => c.library ? [c.library] : []))) {
244
347
  if (own.length) lines.push("");
245
348
  lines.push(`From ${library} (import { Name } from "${library}"):`, "");
246
- for (const component of roots.filter((c) => c.library === library)) lines.push(...listItem(component, "", false));
349
+ for (const component of listed.shown.filter((c) => c.library === library)) lines.push(...listItem(component, "", false));
350
+ }
351
+ for (const group of listed.icons) {
352
+ if (lines.at(-1) !== "") lines.push("");
353
+ lines.push(iconLine(group));
247
354
  }
355
+ lines.push(...listTail(listed));
248
356
  return lines.join("\n");
249
357
  }
250
358
  /** ` (from @signoff-ui, read-only)` for a component copied from a shadcn registry. */
@@ -254,7 +362,8 @@ function registryNote(ds, component) {
254
362
  }
255
363
  function listItem(component, note = "", withImport = true) {
256
364
  const element = component.element ? ` <${component.element}>` : "";
257
- const lines = [`${component.name}${element}${note} — ${firstSentence(component.description) || "No description."}`];
365
+ const description = firstSentence(descriptionOf(component));
366
+ const lines = [`${component.name}${element}${note}${description ? ` — ${description}` : ""}`];
258
367
  for (const variant of variantSummary(component)) lines.push(` ${variant}`);
259
368
  if (component.subcomponents.length) lines.push(` parts: ${component.subcomponents.join(", ")}`);
260
369
  if (withImport) lines.push(` ${importStatement(component)}`);
@@ -268,22 +377,44 @@ function listOf$1(items, max = 3) {
268
377
  }
269
378
  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
379
  /** Every project's components at a workspace root, grouped by the package they come from. */
271
- function renderCatalogList(catalog) {
380
+ function renderCatalogList(catalog, filter = {}) {
272
381
  const roots = catalog.roots();
273
- 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]);
382
+ if (!roots.length) return noComponentsMessage(catalog.root, void 0, true);
383
+ const listed = listComponents(roots, (e) => e.component, filter, (e) => e.package);
384
+ if (!listed.matched) return `No components match ${filterText(filter)} in any project (${roots.length} in all). Call list_components without filters to see them.`;
385
+ const packages = new Set(roots.map((e) => e.package));
276
386
  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))];
387
+ const lines = [`${listHead(roots.length, parts, listed, filter)} from ${plural(packages.size, "package")}, used by ${plural(catalog.projects.length, "project")}. ${PICK_PROJECT}`];
388
+ const byPackage = /* @__PURE__ */ new Map();
389
+ const section = (pkg) => {
390
+ const known = byPackage.get(pkg) ?? {
391
+ entries: [],
392
+ icons: []
393
+ };
394
+ byPackage.set(pkg, known);
395
+ return known;
396
+ };
397
+ for (const entry of listed.shown) section(entry.package).entries.push(entry);
398
+ for (const group of listed.icons) {
399
+ const [first] = group.items;
400
+ const items = group.items.map((e) => e.component);
401
+ if (first) section(first.package).icons.push(iconLine({
402
+ ...group,
403
+ items
404
+ }));
405
+ }
406
+ for (const [pkg, { entries, icons }] of byPackage) {
407
+ const projects = [...new Set(roots.filter((e) => e.package === pkg).flatMap((e) => e.projects))];
280
408
  lines.push("", `## ${pkg} (used by ${listOf$1(projects)})`);
281
409
  for (const entry of entries) {
282
- const others = entry.ambiguous ? catalog.roots().filter((e) => e !== entry && e.component.name === entry.component.name) : [];
410
+ const others = entry.ambiguous ? roots.filter((e) => e !== entry && e.component.name === entry.component.name) : [];
283
411
  const note = others.length ? ` (also in ${[...new Set(others.map((e) => e.package))].join(", ")})` : "";
284
412
  lines.push(...listItem(entry.component, note));
285
413
  }
414
+ if (entries.length && icons.length) lines.push("");
415
+ lines.push(...icons);
286
416
  }
417
+ lines.push(...listTail(listed));
287
418
  return lines.join("\n");
288
419
  }
289
420
  /** Several components under one name at a workspace root, and how to pick one. */
@@ -362,18 +493,26 @@ const REACT_ARIA_NOTABLE = [
362
493
  "isReadOnly",
363
494
  "autoFocus"
364
495
  ];
496
+ /**
497
+ * A prop's description longer than this is cut to its first sentence: a primitive library's
498
+ * JSDoc (Base UI's, Radix's) runs to paragraphs per prop, and the structured content has it all.
499
+ */
500
+ const PROP_LINE = 160;
365
501
  function renderProp(prop) {
366
502
  const optional = prop.required ? "" : "?";
367
503
  const fallback = prop.default !== void 0 ? ` = ${prop.default}` : "";
368
504
  const deprecated = prop.deprecated ? ` DEPRECATED${prop.deprecated === true ? "" : `: ${prop.deprecated}`}.` : "";
369
- const description = prop.description ? ` — ${prop.description.replace(/\s*\n\s*/g, " ")}` : "";
505
+ const text = prop.description?.replace(/\s*\n\s*/g, " ").trim();
506
+ const line = text && text.length > PROP_LINE ? firstSentence(text, PROP_LINE) : text;
507
+ const description = line ? ` — ${line}` : "";
370
508
  return `- ${prop.name}${optional}: ${prop.type}${fallback}${description}${deprecated}`;
371
509
  }
372
510
  /** `origin`: at a workspace root, the package the component comes from and who uses it. */
373
511
  function renderComponent(ds, component, origin) {
374
512
  const lines = [`# ${component.name}`];
375
513
  if (component.deprecated) lines.push(`DEPRECATED${component.deprecated === true ? "" : `: ${component.deprecated}`}`);
376
- if (component.description) lines.push(component.description);
514
+ const description = descriptionOf(component);
515
+ if (description) lines.push(description);
377
516
  lines.push("", importStatement(component));
378
517
  const facts = [
379
518
  component.element ? `Renders <${component.element}>` : void 0,
@@ -386,6 +525,13 @@ function renderComponent(ds, component, origin) {
386
525
  if (origin) lines.push(`From ${origin.package}, used by ${listOf$1(origin.projects)}.`);
387
526
  const registry = ds.registryOf(component);
388
527
  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.`);
528
+ if (component.subcomponents.length) {
529
+ lines.push("", "## Parts");
530
+ for (const name of component.subcomponents) {
531
+ const part = ds.getComponent(name);
532
+ lines.push(`- ${name}${part?.description ? ` — ${firstSentence(part.description)}` : ""}`);
533
+ }
534
+ }
389
535
  lines.push("", "## Props");
390
536
  if (!component.props.length && !component.inherits.length) lines.push("No props.");
391
537
  for (const prop of component.props) lines.push(renderProp(prop));
@@ -411,13 +557,6 @@ function renderComponent(ds, component, origin) {
411
557
  lines.push(` when ${when}: ${compound.classes}`);
412
558
  }
413
559
  }
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
560
  const tokens = ds.relatedTokens(component);
422
561
  if (tokens.length) {
423
562
  lines.push("", "## Design tokens used");
@@ -440,31 +579,63 @@ function renderComponent(ds, component, origin) {
440
579
  }
441
580
  return lines.join("\n");
442
581
  }
582
+ /** The first line of search results: best first, or, when none clearly matches, that none does. */
583
+ function searchHead(query, hits, where = "") {
584
+ 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:`;
585
+ }
586
+ /** The last line of search results. */
587
+ function searchTail(hits) {
588
+ 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.";
589
+ }
590
+ /** `1. CardHeader (part of Card) — Top section of a card.` */
591
+ function searchItem(index, component, from = "") {
592
+ const parent = component.parent ? ` (part of ${component.parent})` : "";
593
+ const description = firstSentence(descriptionOf(component));
594
+ return `${index + 1}. ${component.name}${parent}${from}${description ? ` — ${description}` : ""}`;
595
+ }
443
596
  function renderSearch(query, hits) {
444
597
  if (!hits.length) return `No components match "${query}". Try other words, or list_components to browse.`;
445
- const lines = [`Components for "${query}", best first:`, ""];
598
+ const lines = [searchHead(query, hits), ""];
446
599
  hits.forEach((hit, i) => {
447
600
  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."}`);
601
+ lines.push(searchItem(i, c));
450
602
  lines.push(` score ${hit.score} · matched: ${hit.matched.join(", ")} · ${importStatement(c)}`);
451
603
  });
452
- lines.push("", "Call get_component for props, variants and examples.");
604
+ lines.push("", searchTail(hits));
453
605
  return lines.join("\n");
454
606
  }
455
607
  /** Search hits at a workspace root, each with the package it comes from. */
456
608
  function renderCatalogSearch(query, hits) {
457
609
  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:`, ""];
610
+ const lines = [searchHead(query, hits, " across the monorepo's projects"), ""];
459
611
  hits.forEach((hit, i) => {
460
612
  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."}`);
613
+ lines.push(searchItem(i, c, ` from ${hit.entry.package}`));
463
614
  lines.push(` score ${hit.score} · matched: ${hit.matched.join(", ")} · used by ${listOf$1(hit.entry.projects)} · ${importStatement(c)}`);
464
615
  });
465
- lines.push("", `Call get_component for props, variants and examples. ${PICK_PROJECT}`);
616
+ lines.push("", `${searchTail(hits)} ${PICK_PROJECT}`);
466
617
  return lines.join("\n");
467
618
  }
619
+ /**
620
+ * Where a token is listed in get_tokens: under its category, or, for a color of a scoped
621
+ * family (`chart-*`, `sidebar-*`, the syntax colors one file uses), under that family, with
622
+ * what it is for. An agent looking for a green with no `--success` in the theme otherwise finds
623
+ * `chart-2` among the colors, takes its dark value, and check_ui has nothing against it.
624
+ */
625
+ function tokenGroup(ds, token) {
626
+ const family = ds.tokenIndex.scopedFamilyOf(token);
627
+ return family === void 0 ? token.category : `${SCOPED}${family}`;
628
+ }
629
+ /** Marks a scoped family's group: no category starts with it. */
630
+ const SCOPED = "\0";
631
+ function groupHeading(ds, group, count) {
632
+ if (!group.startsWith(SCOPED)) return `## ${group} (${count})`;
633
+ const family = group.slice(1);
634
+ if (family === "chart") return `## chart (${count}): for charts only, not for status colors`;
635
+ if (family === "sidebar") return `## sidebar (${count}): for the sidebar only, not for status colors`;
636
+ const [file] = ds.tokenIndex.familyFiles.get(family) ?? [];
637
+ return `## ${family} (${count}): for one part of the UI only${file ? ` (${file} uses them)` : ""}, not for status colors`;
638
+ }
468
639
  /** Tokens at a workspace root, grouped by category, each with the package that defines it. */
469
640
  function renderCatalogTokens(catalog, tokens, filter = {}) {
470
641
  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 +644,18 @@ function renderCatalogTokens(catalog, tokens, filter = {}) {
473
644
  return `No tokens match${what ? ` ${what}` : ""} in any project; call get_tokens without filters to see them.`;
474
645
  }
475
646
  const groups = /* @__PURE__ */ new Map();
476
- for (const t of tokens) groups.set(t.token.category, [...groups.get(t.token.category) ?? [], t]);
647
+ for (const t of tokens) {
648
+ const key = tokenGroup(t.ds, t.token);
649
+ const group = groups.get(key) ?? {
650
+ ds: t.ds,
651
+ list: []
652
+ };
653
+ group.list.push(t);
654
+ groups.set(key, group);
655
+ }
477
656
  const lines = [PICK_PROJECT, ""];
478
- for (const [category, list] of groups) {
479
- lines.push(`## ${category} (${list.length})`);
657
+ for (const [key, { ds, list }] of groups) {
658
+ lines.push(groupHeading(ds, key, list.length));
480
659
  for (const { token, package: pkg } of list) {
481
660
  const usage = token.usage.length ? ` → ${token.usage.join(", ")}` : "";
482
661
  lines.push(`- ${token.name}: ${token.value}${usage} (${pkg})`);
@@ -495,10 +674,13 @@ function renderTokens(ds, tokens, filter = {}) {
495
674
  return `No tokens match${what ? ` ${what}` : ""}. The design system has ${categories}; call get_tokens without filters to see them.`;
496
675
  }
497
676
  const groups = /* @__PURE__ */ new Map();
498
- for (const token of tokens) groups.set(token.category, [...groups.get(token.category) ?? [], token]);
677
+ for (const token of tokens) {
678
+ const group = tokenGroup(ds, token);
679
+ groups.set(group, [...groups.get(group) ?? [], token]);
680
+ }
499
681
  const lines = [];
500
- for (const [category, list] of groups) {
501
- lines.push(`## ${category} (${list.length})`);
682
+ for (const [group, list] of groups) {
683
+ lines.push(groupHeading(ds, group, list.length));
502
684
  for (const token of list) {
503
685
  const px = token.category === "spacing" || token.category === "radius" ? ds.tokenIndex.toPx(token.value) : void 0;
504
686
  const value = px !== void 0 && !/^-?[\d.]+px$/.test(token.value) ? `${token.value} = ${formatPx(px)}` : token.value;
@@ -548,7 +730,7 @@ const INSTRUCTIONS = `This server describes the project's design system: its Rea
548
730
  When writing or changing UI in this project:
549
731
  1. Find components with search_components (by intent) or list_components.
550
732
  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.
733
+ 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
734
  4. Run check_ui on every snippet or file you write and fix all errors before you finish.
553
735
  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
736
  const READ_ONLY = {
@@ -575,9 +757,12 @@ const MAX_CODE = 1e6;
575
757
  /** Diagnostics check_ui returns by default: a 3,000-line file can have thousands, more than a context holds. */
576
758
  const DEFAULT_LIMIT = 50;
577
759
  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.`);
760
+ /**
761
+ * check_ui's answer for a path or filename that is not JS or TS. `fix`: what to do instead,
762
+ * for a `filename` given with code (the code itself may be fine).
763
+ */
764
+ function notScript(name, fix = "") {
765
+ return errorResult(`check_ui checks .tsx, .jsx, .ts and .js files; ${name} is not one, so it was not checked.${fix}`);
581
766
  }
582
767
  /**
583
768
  * An integer, without the ±9007199254740991 bounds zod adds to the JSON Schema,
@@ -675,8 +860,8 @@ const componentSummarySchema = z.object({
675
860
  description: z.string().optional(),
676
861
  importPath: z.string(),
677
862
  element: z.string().optional(),
678
- variants: z.record(z.string(), z.array(z.string())),
679
- subcomponents: z.array(z.string()),
863
+ variants: z.record(z.string(), z.array(z.string())).optional().describe("Left out when none."),
864
+ subcomponents: z.array(z.string()).optional().describe("Left out when none."),
680
865
  ...originShape
681
866
  });
682
867
  const tokenSchema = z.object({
@@ -733,14 +918,58 @@ function origin(entry) {
733
918
  projects: entry.projects
734
919
  };
735
920
  }
921
+ /** A component in list_components, without the fields it has nothing in. */
736
922
  function summary(c) {
923
+ const description = descriptionOf(c);
737
924
  return {
738
925
  name: c.name,
739
- ...c.description ? { description: c.description } : {},
926
+ ...description ? { description } : {},
740
927
  importPath: c.importPath,
741
928
  ...c.element ? { element: c.element } : {},
742
- variants: Object.fromEntries(c.variants.map((v) => [v.name, v.values])),
743
- subcomponents: c.subcomponents
929
+ ...c.variants.length ? { variants: Object.fromEntries(c.variants.map((v) => [v.name, v.values])) } : {},
930
+ ...c.subcomponents.length ? { subcomponents: c.subcomponents } : {}
931
+ };
932
+ }
933
+ /** list_components' structured content: what its text lists, as it lists it. */
934
+ function listContent(listed, component, originOf) {
935
+ return {
936
+ components: listed.shown.map((item) => ({
937
+ ...summary(component(item)),
938
+ ...originOf?.(item)
939
+ })),
940
+ ...listed.icons.length ? { icons: listed.icons.map(({ from, items, folder }) => ({
941
+ from,
942
+ names: items.map((item) => component(item).name),
943
+ ...folder ? { modules: items.map((item) => component(item).importPath) } : {},
944
+ ...originOf && items[0] ? { package: originOf(items[0]).package } : {}
945
+ })) } : {},
946
+ ...listed.omitted ? { omitted: listed.omitted } : {}
947
+ };
948
+ }
949
+ /**
950
+ * Said above a component asked for by a name that is not how its design system writes it:
951
+ * `Card.Header` finds CardHeader, and code written as asked would not compile.
952
+ */
953
+ function nameNotice(asked, component) {
954
+ if (asked === component.name || component.aliases.includes(asked)) return "";
955
+ if (!asked.includes(".") && !component.name.includes(".")) return "";
956
+ return `There is no \`${asked}\` in this design system: write \`<${component.name}>\`.\n\n`;
957
+ }
958
+ function resourceDescription(component) {
959
+ const description = descriptionOf(component);
960
+ return description ? { description } : {};
961
+ }
962
+ /** A search_components result. */
963
+ function result(hit) {
964
+ const description = descriptionOf(hit.component);
965
+ return {
966
+ name: hit.component.name,
967
+ ...hit.component.parent ? { parent: hit.component.parent } : {},
968
+ ...description ? { description } : {},
969
+ importPath: hit.component.importPath,
970
+ score: hit.score,
971
+ matched: hit.matched,
972
+ ...hit.weak ? { weak: true } : {}
744
973
  };
745
974
  }
746
975
  /** Builds the MCP server: five tools, two resources and one prompt. Transport-agnostic. */
@@ -798,33 +1027,51 @@ function createServer({ getDesignSystem, getProjects }) {
798
1027
  };
799
1028
  server.registerTool("list_components", {
800
1029
  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) }),
1030
+ 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.",
1031
+ inputSchema: z.object({
1032
+ query: z.string().max(MAX_QUERY).optional().describe("Only components with this text in their name, their parts' names or their description."),
1033
+ kind: z.enum(["component", "icon"]).optional().describe("\"icon\": only the components that render an <svg>, one by one. \"component\": all the others."),
1034
+ limit: z.number().int().min(1).max(MAX_LIMIT).optional().describe("Most components to list one by one."),
1035
+ path: projectPath
1036
+ }),
1037
+ outputSchema: z.object({
1038
+ components: z.array(componentSummarySchema).describe("The components listed one by one, after `query`, `kind` and `limit`."),
1039
+ icons: z.array(z.object({
1040
+ from: z.string().describe("The module they are imported from, or, with `modules`, the folder of their modules."),
1041
+ names: z.array(z.string()),
1042
+ modules: z.array(z.string()).optional().describe("For icons with a module each: each one's module, in the order of `names`."),
1043
+ package: originShape.package
1044
+ })).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."),
1045
+ omitted: int().optional().describe("Matching components left out by `limit`.")
1046
+ }),
804
1047
  annotations: READ_ONLY
805
- }, async ({ path: target }) => {
1048
+ }, async ({ query, kind, limit, path: target }) => {
806
1049
  const found = await scoped(target);
807
1050
  if (isResult(found)) return found;
1051
+ const filter = {
1052
+ query,
1053
+ kind,
1054
+ limit
1055
+ };
808
1056
  if ("catalog" in found) {
809
1057
  const { catalog } = found;
1058
+ const listed = listComponents(catalog.roots(), (entry) => entry.component, filter, (entry) => entry.package);
810
1059
  return {
811
1060
  content: [{
812
1061
  type: "text",
813
- text: renderCatalogList(catalog)
1062
+ text: renderCatalogList(catalog, filter)
814
1063
  }],
815
- structuredContent: { components: catalog.roots().map((entry) => ({
816
- ...summary(entry.component),
817
- ...origin(entry)
818
- })) }
1064
+ structuredContent: listContent(listed, (entry) => entry.component, origin)
819
1065
  };
820
1066
  }
821
1067
  const { ds } = found;
1068
+ const listed = listComponents(ds.roots(), (c) => c, filter);
822
1069
  return {
823
1070
  content: [{
824
1071
  type: "text",
825
- text: renderComponentList(ds, found.project?.dir)
1072
+ text: renderComponentList(ds, found.project?.dir, filter)
826
1073
  }],
827
- structuredContent: { components: ds.roots().map(summary) }
1074
+ structuredContent: listContent(listed, (c) => c)
828
1075
  };
829
1076
  });
830
1077
  server.registerTool("get_component", {
@@ -844,18 +1091,21 @@ function createServer({ getDesignSystem, getProjects }) {
844
1091
  const matches = catalog.find(name);
845
1092
  const [entry] = matches;
846
1093
  if (!entry) {
1094
+ if (!catalog.roots().length) return errorResult(noComponentsMessage(catalog.root, void 0, true));
847
1095
  const guess = catalog.suggest(name);
848
1096
  const names = [...new Set(catalog.roots().map((e) => e.component.name))];
849
1097
  return errorResult(`No component named "${name}" in any project.` + (guess ? ` Did you mean "${guess}"?` : "") + ` Available: ${names.join(", ")}.`);
850
1098
  }
851
1099
  if (matches.length > 1) return errorResult(renderAmbiguous(name, matches));
1100
+ const description = descriptionOf(entry.component);
852
1101
  return {
853
1102
  content: [{
854
1103
  type: "text",
855
- text: renderComponent(entry.ds, entry.component, entry)
1104
+ text: nameNotice(name, entry.component) + renderComponent(entry.ds, entry.component, entry)
856
1105
  }],
857
1106
  structuredContent: {
858
1107
  ...entry.component,
1108
+ ...description ? { description } : {},
859
1109
  import: importStatement(entry.component),
860
1110
  tokens: entry.ds.relatedTokens(entry.component).map((t) => t.name),
861
1111
  ...origin(entry)
@@ -865,6 +1115,13 @@ function createServer({ getDesignSystem, getProjects }) {
865
1115
  const { ds } = found;
866
1116
  const component = ds.getComponent(name);
867
1117
  if (!component) {
1118
+ if (!ds.roots().length) return {
1119
+ isError: true,
1120
+ content: [{
1121
+ type: "text",
1122
+ text: noComponentsMessage(ds.root, found.project?.dir)
1123
+ }]
1124
+ };
868
1125
  const guess = ds.suggestComponent(name);
869
1126
  return {
870
1127
  isError: true,
@@ -875,13 +1132,15 @@ function createServer({ getDesignSystem, getProjects }) {
875
1132
  };
876
1133
  }
877
1134
  const shown = fromRoot(ds, component, found.root);
1135
+ const description = descriptionOf(component);
878
1136
  return {
879
1137
  content: [{
880
1138
  type: "text",
881
- text: renderComponent(ds, shown)
1139
+ text: nameNotice(name, component) + renderComponent(ds, shown)
882
1140
  }],
883
1141
  structuredContent: {
884
1142
  ...shown,
1143
+ ...description ? { description } : {},
885
1144
  import: importStatement(component),
886
1145
  tokens: ds.relatedTokens(component).map((t) => t.name)
887
1146
  }
@@ -889,7 +1148,7 @@ function createServer({ getDesignSystem, getProjects }) {
889
1148
  });
890
1149
  server.registerTool("search_components", {
891
1150
  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.",
1151
+ 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
1152
  inputSchema: z.object({
894
1153
  query: z.string().min(1).max(MAX_QUERY).describe("What the UI should do, in plain words."),
895
1154
  limit: z.number().int().min(1).max(20).default(5).describe("Maximum results."),
@@ -902,6 +1161,7 @@ function createServer({ getDesignSystem, getProjects }) {
902
1161
  importPath: z.string(),
903
1162
  score: z.number(),
904
1163
  matched: z.array(z.string()),
1164
+ 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
1165
  ...originShape
906
1166
  })) }),
907
1167
  annotations: READ_ONLY
@@ -916,12 +1176,7 @@ function createServer({ getDesignSystem, getProjects }) {
916
1176
  text: renderCatalogSearch(query, hits)
917
1177
  }],
918
1178
  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,
1179
+ ...result(h),
925
1180
  ...origin(h.entry)
926
1181
  })) }
927
1182
  };
@@ -933,19 +1188,12 @@ function createServer({ getDesignSystem, getProjects }) {
933
1188
  type: "text",
934
1189
  text: renderSearch(query, hits)
935
1190
  }],
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
- })) }
1191
+ structuredContent: { results: hits.map(result) }
944
1192
  };
945
1193
  });
946
1194
  server.registerTool("get_tokens", {
947
1195
  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.",
1196
+ 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
1197
  inputSchema: z.object({
950
1198
  category: z.enum(TOKEN_CATEGORIES).optional().describe("Only tokens of this category."),
951
1199
  query: z.string().max(MAX_QUERY).optional().describe("Substring to match in names, usages or descriptions."),
@@ -1010,10 +1258,10 @@ function createServer({ getDesignSystem, getProjects }) {
1010
1258
  });
1011
1259
  server.registerTool("check_ui", {
1012
1260
  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.",
1261
+ 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
1262
  inputSchema: z.object({
1015
1263
  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."),
1264
+ 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
1265
  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
1266
  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
1267
  }),
@@ -1036,11 +1284,14 @@ function createServer({ getDesignSystem, getProjects }) {
1036
1284
  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
1285
  }),
1038
1286
  annotations: READ_ONLY
1039
- }, async ({ code, path: filePath, filename, limit }) => {
1287
+ }, async ({ code, path: filePath, filename: given, limit }) => {
1040
1288
  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;
1289
+ 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.");
1290
+ const folder = code !== void 0 && filePath && !SCRIPT_FILE.test(filePath) ? filePath : void 0;
1291
+ const filename = code === void 0 || folder !== void 0 ? given : given ?? (filePath || void 0);
1292
+ let routeBy = code === void 0 ? filePath : filename ?? folder;
1043
1293
  const set = await getProjects?.(routeBy);
1294
+ if (set && folder !== void 0 && routeBy === folder && path.resolve(set.root, folder) === set.root && !set.dirs.includes(".")) routeBy = void 0;
1044
1295
  let ds;
1045
1296
  if (!set) ds = await getDesignSystem(routeBy);
1046
1297
  else if (routeBy) {
@@ -1128,14 +1379,14 @@ function createServer({ getDesignSystem, getProjects }) {
1128
1379
  uri: `ds://components/${c.name}${ambiguous ? `?path=${encodeURIComponent(projects[0] ?? ".")}` : ""}`,
1129
1380
  name: c.name,
1130
1381
  title: c.name,
1131
- ...c.description ? { description: c.description } : {},
1382
+ ...resourceDescription(c),
1132
1383
  mimeType: "text/markdown"
1133
1384
  })) };
1134
1385
  return { resources: found.ds.components.map((c) => ({
1135
1386
  uri: `ds://components/${c.name}`,
1136
1387
  name: c.name,
1137
1388
  title: c.name,
1138
- ...c.description ? { description: c.description } : {},
1389
+ ...resourceDescription(c),
1139
1390
  mimeType: "text/markdown"
1140
1391
  })) };
1141
1392
  },
@@ -1216,8 +1467,8 @@ function createServer({ getDesignSystem, getProjects }) {
1216
1467
 
1217
1468
  Work like this:
1218
1469
  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.
1470
+ 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.
1471
+ 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
1472
  4. Use design-system components instead of native elements (<Button>, not <button>).
1222
1473
  5. Verify: run check_ui on the code you wrote, fix every error, and run it again until it reports none.
1223
1474