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