onsystem 0.7.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 { $ as loadConfig, Ct as relativePath, F as TOKEN_CATEGORIES, G as DEFAULT_SEVERITY, H as CONFIG_FILES, J as LEGACY_CONFIG_FILES, Tt as defaultProjectConfig, Y as RULE_IDS, _ as NAME, _t as closest, at as LIBRARY_IDS, bt as unique, d as searchComponents, dt as detectProject, ft as findDesignSystemPackage, it as LIBRARIES, m as uncheckedNotice, n as DesignSystemHost, nt as formatPx, ot as libraryTitle, st as notReadYet, u as buildSearchIndex, v as VERSION, vt as plural, xt as isInside, yt as truncate } from "./design-system-C4bhqLLw.js";
2
- import { n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-Z0tZf_Dv.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";
@@ -47,7 +47,10 @@ function explainProject(ds, cwd, indent = "") {
47
47
  field("importPath", importPaths(ds));
48
48
  field("libraries", config.libraryIds === void 0 ? `every library onsystem reads (${LIBRARY_IDS.join(", ")})` : config.libraryIds !== false && config.libraryIds.length ? config.libraryIds.join(", ") : `none ("libraries": ${JSON.stringify(config.libraryIds)})`);
49
49
  if (config.tailwindConfig) field("tailwind", display(config.root, config.tailwindConfig));
50
- if (config.designSystems) field("primary", config.designSystems.map((d, i) => `${i ? "then " : ""}${d.source} (${d.imports} imports)`).join("; "));
50
+ if (config.designSystems) {
51
+ const ranked = config.designSystems.filter((d) => !d.registry).length > 1;
52
+ field("primary", config.designSystems.map((d, i) => `${i ? "then " : ""}${d.source}${d.registry ? ", read-only" : ranked ? ` (${d.imports} imports)` : ""}`).join("; "));
53
+ }
51
54
  const changed = RULE_IDS.filter((id) => config.rules[id].severity !== DEFAULT_SEVERITY[id]);
52
55
  field("rules", `${RULE_IDS.map((id) => `${id} ${config.rules[id].severity}`).join(", ")}${changed.length ? ` (changed: ${changed.join(", ")})` : ""}`);
53
56
  if (config.overrides?.length) for (const override of config.overrides) field("override", `${override.files.join(", ")}: ${Object.entries(override.rules).map(([id, rule]) => `${id} ${rule.severity}`).join(", ")}`);
@@ -229,24 +232,133 @@ function importStatement(component) {
229
232
  function variantSummary(component) {
230
233
  return component.variants.map((v) => `${v.name}: ${v.values.join(" | ")}${v.default ? ` (default ${v.default})` : ""}`);
231
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
+ }
232
330
  /** `project`: the folder of a workspace root's project the list is for. */
233
- function renderComponentList(ds, project) {
331
+ function renderComponentList(ds, project, filter = {}) {
234
332
  const roots = ds.roots();
235
- 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.`;
236
337
  const parts = ds.components.length - roots.length;
237
- const lines = [`${plural(roots.length, "component")}${parts ? ` (+${plural(parts, "part")})` : ""}${project ? ` in ${project}` : ""}. Call get_component before using one.`, ""];
238
- const own = roots.filter((c) => !c.library);
239
- for (const component of own) lines.push(...listItem(component));
240
- for (const library of new Set(roots.flatMap((c) => c.library ? [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);
340
+ for (const component of own) lines.push(...listItem(component, registryNote(ds, component)));
341
+ for (const library of new Set(listed.shown.flatMap((c) => c.library ? [c.library] : []))) {
241
342
  if (own.length) lines.push("");
242
343
  lines.push(`From ${library} (import { Name } from "${library}"):`, "");
243
- 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));
244
349
  }
350
+ lines.push(...listTail(listed));
245
351
  return lines.join("\n");
246
352
  }
353
+ /** ` (from @signoff-ui, read-only)` for a component copied from a shadcn registry. */
354
+ function registryNote(ds, component) {
355
+ const from = ds.registryOf(component);
356
+ return from ? ` (from ${from.registry}, read-only)` : "";
357
+ }
247
358
  function listItem(component, note = "", withImport = true) {
248
359
  const element = component.element ? ` <${component.element}>` : "";
249
- 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}` : ""}`];
250
362
  for (const variant of variantSummary(component)) lines.push(` ${variant}`);
251
363
  if (component.subcomponents.length) lines.push(` parts: ${component.subcomponents.join(", ")}`);
252
364
  if (withImport) lines.push(` ${importStatement(component)}`);
@@ -260,22 +372,44 @@ function listOf$1(items, max = 3) {
260
372
  }
261
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.";
262
374
  /** Every project's components at a workspace root, grouped by the package they come from. */
263
- function renderCatalogList(catalog) {
375
+ function renderCatalogList(catalog, filter = {}) {
264
376
  const roots = catalog.roots();
265
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.`;
266
- const byPackage = /* @__PURE__ */ new Map();
267
- 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));
268
381
  const parts = catalog.entries.length - roots.length;
269
- const lines = [`${plural(roots.length, "component")}${parts ? ` (+${plural(parts, "part")})` : ""} from ${plural(byPackage.size, "package")}, used by ${plural(catalog.projects.length, "project")}. ${PICK_PROJECT}`];
270
- for (const [pkg, entries] of byPackage) {
271
- 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))];
272
403
  lines.push("", `## ${pkg} (used by ${listOf$1(projects)})`);
273
404
  for (const entry of entries) {
274
- 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) : [];
275
406
  const note = others.length ? ` (also in ${[...new Set(others.map((e) => e.package))].join(", ")})` : "";
276
407
  lines.push(...listItem(entry.component, note));
277
408
  }
409
+ if (entries.length && icons.length) lines.push("");
410
+ lines.push(...icons);
278
411
  }
412
+ lines.push(...listTail(listed));
279
413
  return lines.join("\n");
280
414
  }
281
415
  /** Several components under one name at a workspace root, and how to pick one. */
@@ -354,18 +488,26 @@ const REACT_ARIA_NOTABLE = [
354
488
  "isReadOnly",
355
489
  "autoFocus"
356
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;
357
496
  function renderProp(prop) {
358
497
  const optional = prop.required ? "" : "?";
359
498
  const fallback = prop.default !== void 0 ? ` = ${prop.default}` : "";
360
499
  const deprecated = prop.deprecated ? ` DEPRECATED${prop.deprecated === true ? "" : `: ${prop.deprecated}`}.` : "";
361
- 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}` : "";
362
503
  return `- ${prop.name}${optional}: ${prop.type}${fallback}${description}${deprecated}`;
363
504
  }
364
505
  /** `origin`: at a workspace root, the package the component comes from and who uses it. */
365
506
  function renderComponent(ds, component, origin) {
366
507
  const lines = [`# ${component.name}`];
367
508
  if (component.deprecated) lines.push(`DEPRECATED${component.deprecated === true ? "" : `: ${component.deprecated}`}`);
368
- if (component.description) lines.push(component.description);
509
+ const description = descriptionOf(component);
510
+ if (description) lines.push(description);
369
511
  lines.push("", importStatement(component));
370
512
  const facts = [
371
513
  component.element ? `Renders <${component.element}>` : void 0,
@@ -376,6 +518,15 @@ function renderComponent(ds, component, origin) {
376
518
  ].filter(Boolean);
377
519
  lines.push(facts.join(" · "));
378
520
  if (origin) lines.push(`From ${origin.package}, used by ${listOf$1(origin.projects)}.`);
521
+ const registry = ds.registryOf(component);
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
+ }
379
530
  lines.push("", "## Props");
380
531
  if (!component.props.length && !component.inherits.length) lines.push("No props.");
381
532
  for (const prop of component.props) lines.push(renderProp(prop));
@@ -401,13 +552,6 @@ function renderComponent(ds, component, origin) {
401
552
  lines.push(` when ${when}: ${compound.classes}`);
402
553
  }
403
554
  }
404
- if (component.subcomponents.length) {
405
- lines.push("", "## Parts");
406
- for (const name of component.subcomponents) {
407
- const part = ds.getComponent(name);
408
- lines.push(`- ${name}${part?.description ? ` — ${firstSentence(part.description)}` : ""}`);
409
- }
410
- }
411
555
  const tokens = ds.relatedTokens(component);
412
556
  if (tokens.length) {
413
557
  lines.push("", "## Design tokens used");
@@ -430,31 +574,63 @@ function renderComponent(ds, component, origin) {
430
574
  }
431
575
  return lines.join("\n");
432
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
+ }
433
591
  function renderSearch(query, hits) {
434
592
  if (!hits.length) return `No components match "${query}". Try other words, or list_components to browse.`;
435
- const lines = [`Components for "${query}", best first:`, ""];
593
+ const lines = [searchHead(query, hits), ""];
436
594
  hits.forEach((hit, i) => {
437
595
  const c = hit.component;
438
- const parent = c.parent ? ` (part of ${c.parent})` : "";
439
- lines.push(`${i + 1}. ${c.name}${parent} — ${firstSentence(c.description) || "No description."}`);
596
+ lines.push(searchItem(i, c));
440
597
  lines.push(` score ${hit.score} · matched: ${hit.matched.join(", ")} · ${importStatement(c)}`);
441
598
  });
442
- lines.push("", "Call get_component for props, variants and examples.");
599
+ lines.push("", searchTail(hits));
443
600
  return lines.join("\n");
444
601
  }
445
602
  /** Search hits at a workspace root, each with the package it comes from. */
446
603
  function renderCatalogSearch(query, hits) {
447
604
  if (!hits.length) return `No components match "${query}" in any project. Try other words, or list_components to browse.`;
448
- const lines = [`Components for "${query}" across the monorepo's projects, best first:`, ""];
605
+ const lines = [searchHead(query, hits, " across the monorepo's projects"), ""];
449
606
  hits.forEach((hit, i) => {
450
607
  const c = hit.component;
451
- const parent = c.parent ? ` (part of ${c.parent})` : "";
452
- 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}`));
453
609
  lines.push(` score ${hit.score} · matched: ${hit.matched.join(", ")} · used by ${listOf$1(hit.entry.projects)} · ${importStatement(c)}`);
454
610
  });
455
- lines.push("", `Call get_component for props, variants and examples. ${PICK_PROJECT}`);
611
+ lines.push("", `${searchTail(hits)} ${PICK_PROJECT}`);
456
612
  return lines.join("\n");
457
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
+ }
458
634
  /** Tokens at a workspace root, grouped by category, each with the package that defines it. */
459
635
  function renderCatalogTokens(catalog, tokens, filter = {}) {
460
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`;
@@ -463,10 +639,18 @@ function renderCatalogTokens(catalog, tokens, filter = {}) {
463
639
  return `No tokens match${what ? ` ${what}` : ""} in any project; call get_tokens without filters to see them.`;
464
640
  }
465
641
  const groups = /* @__PURE__ */ new Map();
466
- 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
+ }
467
651
  const lines = [PICK_PROJECT, ""];
468
- for (const [category, list] of groups) {
469
- lines.push(`## ${category} (${list.length})`);
652
+ for (const [key, { ds, list }] of groups) {
653
+ lines.push(groupHeading(ds, key, list.length));
470
654
  for (const { token, package: pkg } of list) {
471
655
  const usage = token.usage.length ? ` → ${token.usage.join(", ")}` : "";
472
656
  lines.push(`- ${token.name}: ${token.value}${usage} (${pkg})`);
@@ -485,10 +669,13 @@ function renderTokens(ds, tokens, filter = {}) {
485
669
  return `No tokens match${what ? ` ${what}` : ""}. The design system has ${categories}; call get_tokens without filters to see them.`;
486
670
  }
487
671
  const groups = /* @__PURE__ */ new Map();
488
- 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
+ }
489
676
  const lines = [];
490
- for (const [category, list] of groups) {
491
- lines.push(`## ${category} (${list.length})`);
677
+ for (const [group, list] of groups) {
678
+ lines.push(groupHeading(ds, group, list.length));
492
679
  for (const token of list) {
493
680
  const px = token.category === "spacing" || token.category === "radius" ? ds.tokenIndex.toPx(token.value) : void 0;
494
681
  const value = px !== void 0 && !/^-?[\d.]+px$/.test(token.value) ? `${token.value} = ${formatPx(px)}` : token.value;
@@ -538,7 +725,7 @@ const INSTRUCTIONS = `This server describes the project's design system: its Rea
538
725
  When writing or changing UI in this project:
539
726
  1. Find components with search_components (by intent) or list_components.
540
727
  2. Read get_component before using a component: use only the props and variant values it lists.
541
- 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.
542
729
  4. Run check_ui on every snippet or file you write and fix all errors before you finish.
543
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.`;
544
731
  const READ_ONLY = {
@@ -566,6 +753,13 @@ const MAX_CODE = 1e6;
566
753
  const DEFAULT_LIMIT = 50;
567
754
  const MAX_LIMIT = 1e3;
568
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}`);
761
+ }
762
+ /**
569
763
  * An integer, without the ±9007199254740991 bounds zod adds to the JSON Schema,
570
764
  * which every client would otherwise read in each tool definition.
571
765
  */
@@ -661,8 +855,8 @@ const componentSummarySchema = z.object({
661
855
  description: z.string().optional(),
662
856
  importPath: z.string(),
663
857
  element: z.string().optional(),
664
- variants: z.record(z.string(), z.array(z.string())),
665
- 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."),
666
860
  ...originShape
667
861
  });
668
862
  const tokenSchema = z.object({
@@ -719,14 +913,58 @@ function origin(entry) {
719
913
  projects: entry.projects
720
914
  };
721
915
  }
916
+ /** A component in list_components, without the fields it has nothing in. */
722
917
  function summary(c) {
918
+ const description = descriptionOf(c);
723
919
  return {
724
920
  name: c.name,
725
- ...c.description ? { description: c.description } : {},
921
+ ...description ? { description } : {},
726
922
  importPath: c.importPath,
727
923
  ...c.element ? { element: c.element } : {},
728
- variants: Object.fromEntries(c.variants.map((v) => [v.name, v.values])),
729
- 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 } : {}
730
968
  };
731
969
  }
732
970
  /** Builds the MCP server: five tools, two resources and one prompt. Transport-agnostic. */
@@ -784,33 +1022,51 @@ function createServer({ getDesignSystem, getProjects }) {
784
1022
  };
785
1023
  server.registerTool("list_components", {
786
1024
  title: "List design-system components",
787
- 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.",
788
- inputSchema: z.object({ path: projectPath }),
789
- 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
+ }),
790
1042
  annotations: READ_ONLY
791
- }, async ({ path: target }) => {
1043
+ }, async ({ query, kind, limit, path: target }) => {
792
1044
  const found = await scoped(target);
793
1045
  if (isResult(found)) return found;
1046
+ const filter = {
1047
+ query,
1048
+ kind,
1049
+ limit
1050
+ };
794
1051
  if ("catalog" in found) {
795
1052
  const { catalog } = found;
1053
+ const listed = listComponents(catalog.roots(), (entry) => entry.component, filter, (entry) => entry.package);
796
1054
  return {
797
1055
  content: [{
798
1056
  type: "text",
799
- text: renderCatalogList(catalog)
1057
+ text: renderCatalogList(catalog, filter)
800
1058
  }],
801
- structuredContent: { components: catalog.roots().map((entry) => ({
802
- ...summary(entry.component),
803
- ...origin(entry)
804
- })) }
1059
+ structuredContent: listContent(listed, (entry) => entry.component, origin)
805
1060
  };
806
1061
  }
807
1062
  const { ds } = found;
1063
+ const listed = listComponents(ds.roots(), (c) => c, filter);
808
1064
  return {
809
1065
  content: [{
810
1066
  type: "text",
811
- text: renderComponentList(ds, found.project?.dir)
1067
+ text: renderComponentList(ds, found.project?.dir, filter)
812
1068
  }],
813
- structuredContent: { components: ds.roots().map(summary) }
1069
+ structuredContent: listContent(listed, (c) => c)
814
1070
  };
815
1071
  });
816
1072
  server.registerTool("get_component", {
@@ -835,13 +1091,15 @@ function createServer({ getDesignSystem, getProjects }) {
835
1091
  return errorResult(`No component named "${name}" in any project.` + (guess ? ` Did you mean "${guess}"?` : "") + ` Available: ${names.join(", ")}.`);
836
1092
  }
837
1093
  if (matches.length > 1) return errorResult(renderAmbiguous(name, matches));
1094
+ const description = descriptionOf(entry.component);
838
1095
  return {
839
1096
  content: [{
840
1097
  type: "text",
841
- text: renderComponent(entry.ds, entry.component, entry)
1098
+ text: nameNotice(name, entry.component) + renderComponent(entry.ds, entry.component, entry)
842
1099
  }],
843
1100
  structuredContent: {
844
1101
  ...entry.component,
1102
+ ...description ? { description } : {},
845
1103
  import: importStatement(entry.component),
846
1104
  tokens: entry.ds.relatedTokens(entry.component).map((t) => t.name),
847
1105
  ...origin(entry)
@@ -861,13 +1119,15 @@ function createServer({ getDesignSystem, getProjects }) {
861
1119
  };
862
1120
  }
863
1121
  const shown = fromRoot(ds, component, found.root);
1122
+ const description = descriptionOf(component);
864
1123
  return {
865
1124
  content: [{
866
1125
  type: "text",
867
- text: renderComponent(ds, shown)
1126
+ text: nameNotice(name, component) + renderComponent(ds, shown)
868
1127
  }],
869
1128
  structuredContent: {
870
1129
  ...shown,
1130
+ ...description ? { description } : {},
871
1131
  import: importStatement(component),
872
1132
  tokens: ds.relatedTokens(component).map((t) => t.name)
873
1133
  }
@@ -875,7 +1135,7 @@ function createServer({ getDesignSystem, getProjects }) {
875
1135
  });
876
1136
  server.registerTool("search_components", {
877
1137
  title: "Search components by intent",
878
- 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.",
879
1139
  inputSchema: z.object({
880
1140
  query: z.string().min(1).max(MAX_QUERY).describe("What the UI should do, in plain words."),
881
1141
  limit: z.number().int().min(1).max(20).default(5).describe("Maximum results."),
@@ -888,6 +1148,7 @@ function createServer({ getDesignSystem, getProjects }) {
888
1148
  importPath: z.string(),
889
1149
  score: z.number(),
890
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."),
891
1152
  ...originShape
892
1153
  })) }),
893
1154
  annotations: READ_ONLY
@@ -902,12 +1163,7 @@ function createServer({ getDesignSystem, getProjects }) {
902
1163
  text: renderCatalogSearch(query, hits)
903
1164
  }],
904
1165
  structuredContent: { results: hits.map((h) => ({
905
- name: h.component.name,
906
- ...h.component.parent ? { parent: h.component.parent } : {},
907
- ...h.component.description ? { description: h.component.description } : {},
908
- importPath: h.component.importPath,
909
- score: h.score,
910
- matched: h.matched,
1166
+ ...result(h),
911
1167
  ...origin(h.entry)
912
1168
  })) }
913
1169
  };
@@ -919,19 +1175,12 @@ function createServer({ getDesignSystem, getProjects }) {
919
1175
  type: "text",
920
1176
  text: renderSearch(query, hits)
921
1177
  }],
922
- structuredContent: { results: hits.map((h) => ({
923
- name: h.component.name,
924
- ...h.component.parent ? { parent: h.component.parent } : {},
925
- ...h.component.description ? { description: h.component.description } : {},
926
- importPath: h.component.importPath,
927
- score: h.score,
928
- matched: h.matched
929
- })) }
1178
+ structuredContent: { results: hits.map(result) }
930
1179
  };
931
1180
  });
932
1181
  server.registerTool("get_tokens", {
933
1182
  title: "Get design tokens",
934
- 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.",
935
1184
  inputSchema: z.object({
936
1185
  category: z.enum(TOKEN_CATEGORIES).optional().describe("Only tokens of this category."),
937
1186
  query: z.string().max(MAX_QUERY).optional().describe("Substring to match in names, usages or descriptions."),
@@ -996,10 +1245,10 @@ function createServer({ getDesignSystem, getProjects }) {
996
1245
  });
997
1246
  server.registerTool("check_ui", {
998
1247
  title: "Check UI code against the design system",
999
- 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.",
1000
1249
  inputSchema: z.object({
1001
1250
  code: z.string().max(MAX_CODE).optional().describe(`TSX/JSX source to check (up to ${MAX_CODE.toLocaleString("en")} characters).`),
1002
- path: z.string().max(MAX_PATH).optional().describe("Path of a 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."),
1003
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."),
1004
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.`)
1005
1254
  }),
@@ -1014,7 +1263,7 @@ function createServer({ getDesignSystem, getProjects }) {
1014
1263
  errors: int(),
1015
1264
  warnings: int()
1016
1265
  })).describe("Every finding per rule, shown or not."),
1017
- skipped: z.string().optional().describe("Why the file was not checked: it renders an image (next/og) or an email."),
1266
+ skipped: z.string().optional().describe("Why the file was not checked: it renders an image (next/og) or an email, or the config's `exclude` leaves it out."),
1018
1267
  unchecked: z.object({
1019
1268
  names: z.array(z.string()),
1020
1269
  modules: z.array(z.string())
@@ -1022,10 +1271,14 @@ function createServer({ getDesignSystem, getProjects }) {
1022
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.")
1023
1272
  }),
1024
1273
  annotations: READ_ONLY
1025
- }, async ({ code, path: filePath, filename, limit }) => {
1274
+ }, async ({ code, path: filePath, filename: given, limit }) => {
1026
1275
  if (code === void 0 && !filePath) return errorResult("Pass either `code` or `path`.");
1027
- 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;
1028
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;
1029
1282
  let ds;
1030
1283
  if (!set) ds = await getDesignSystem(routeBy);
1031
1284
  else if (routeBy) {
@@ -1048,6 +1301,7 @@ function createServer({ getDesignSystem, getProjects }) {
1048
1301
  const absolute = path.resolve(base, filePath);
1049
1302
  const outside = errorResult(`${filePath} is outside the project root (${base}).`);
1050
1303
  if (!isInside(base, absolute)) return outside;
1304
+ if (!SCRIPT_FILE.test(absolute)) return notScript(filePath);
1051
1305
  try {
1052
1306
  const [root, real] = await Promise.all([fsp.realpath(base), fsp.realpath(absolute)]);
1053
1307
  if (!isInside(root, real)) return outside;
@@ -1071,7 +1325,14 @@ function createServer({ getDesignSystem, getProjects }) {
1071
1325
  file = relativePath(ds.root, absolute);
1072
1326
  shown = relativePath(base, absolute);
1073
1327
  }
1074
- const checked = ds.check(source, file);
1328
+ const excluded = (filename !== void 0 || code === void 0) && !path.isAbsolute(file) ? excludedBy(ds.config, file) : void 0;
1329
+ const checked = excluded ? {
1330
+ file,
1331
+ diagnostics: [],
1332
+ errorCount: 0,
1333
+ warningCount: 0,
1334
+ skipped: `It is left out by "${excluded}" in the config's exclude, as in check, the ESLint plugin and the Claude Code hook.`
1335
+ } : ds.check(source, file);
1075
1336
  const result = file === shown ? checked : {
1076
1337
  ...checked,
1077
1338
  file: shown
@@ -1105,14 +1366,14 @@ function createServer({ getDesignSystem, getProjects }) {
1105
1366
  uri: `ds://components/${c.name}${ambiguous ? `?path=${encodeURIComponent(projects[0] ?? ".")}` : ""}`,
1106
1367
  name: c.name,
1107
1368
  title: c.name,
1108
- ...c.description ? { description: c.description } : {},
1369
+ ...resourceDescription(c),
1109
1370
  mimeType: "text/markdown"
1110
1371
  })) };
1111
1372
  return { resources: found.ds.components.map((c) => ({
1112
1373
  uri: `ds://components/${c.name}`,
1113
1374
  name: c.name,
1114
1375
  title: c.name,
1115
- ...c.description ? { description: c.description } : {},
1376
+ ...resourceDescription(c),
1116
1377
  mimeType: "text/markdown"
1117
1378
  })) };
1118
1379
  },
@@ -1193,8 +1454,8 @@ function createServer({ getDesignSystem, getProjects }) {
1193
1454
 
1194
1455
  Work like this:
1195
1456
  1. Find components: search_components for each piece of the UI (for example "confirm a destructive action"), or list_components for an overview.
1196
- 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.
1197
- 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.
1198
1459
  4. Use design-system components instead of native elements (<Button>, not <button>).
1199
1460
  5. Verify: run check_ui on the code you wrote, fix every error, and run it again until it reports none.
1200
1461