Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/codeatlas-ui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Icons passed to components (`icon: Settings`) are `@lucide/svelte` components. T

Never guess a prop name. This skill and `API.md` track the `main` branch, which can be ahead of the `@codeatlasdev/ui` the project has installed: a prop, snippet or MCP tool listed here may not exist in the installed version. Check the installed version first (`node_modules/@codeatlasdev/ui/package.json`) and prefer what that package answers over this file. Two sources, both generated from the source types:

1. **MCP server (preferred).** `ca-ui-mcp` answers `get_component("Button")` (props, defaults, snippets, `styling`: stable `ca-*` parts, scoped variables, `data-*` attributes), `list_parts("Button")` (just the override surface: call it before writing a `.ca-*` selector), `get_export("setLocale")` (signature and summary of any function, store, constant or type: `caErrorPlugin`, `createErrorHandler`, `nativeFeel`, `primeKeyboard`, `toast`...), `list_components`, `search_tokens("safe-bottom")`, `list_surfaces()` (the `data-style` surface styles and which draw a blur) and `get_guide("theming")`. Setup: `claude mcp add codeatlas-ui -- npx -y -p @codeatlasdev/ui ca-ui-mcp`.
1. **MCP server (preferred).** `ca-ui-mcp` answers `get_component("Button")` (props, defaults, snippets, `styling`: stable `ca-*` parts, scoped variables, `data-*` attributes), `list_parts("Button")` (just the override surface: call it before writing a `.ca-*` selector), `get_export("setLocale")` (signature and summary of any function, store, constant or type: `caErrorPlugin`, `createErrorHandler`, `nativeFeel`, `primeKeyboard`, `toast`...), `list_components`, `search_tokens("safe-bottom")`, `list_surfaces()` (the `data-style` surface styles and which draw a blur) `get_guide("theming")`, and for the design atlas (269 terms; the 1-5 scales are heuristic, the sources support principles not styles) `search_terms("swiss")`, `get_term("DS001")` and `get_reference("E05")`. Setup: `claude mcp add codeatlas-ui -- npx -y -p @codeatlasdev/ui ca-ui-mcp`.
2. **[API.md](./API.md)**: the same data as one markdown file; its first lines name the version it was generated for. Offline fallback; for the installed version read `node_modules/@codeatlasdev/ui/dist/components.json` instead. Large, search it instead of reading it whole. Components first, then the `# Non-component exports` section grouped by import path. Regenerate with `bun run --cwd packages/ui manifest -- --skill` after changing a component.

## Mobile, theme, SEO
Expand Down
5 changes: 5 additions & 0 deletions .changeset/design-atlas-mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codeatlasdev/ui": minor
---

`ca-ui-mcp` gains `search_terms`, `get_term` and `get_reference`: the 269-term design atlas and the verified sources behind it, offline from `dist/atlas.json`. Scores are labelled heuristic, every term carries its limits and review date, and a reference returns what it does not support and the wording that was read.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ Every numbered demo (`site/src/demos/<group>/<page>/<Variant>.svelte`, id in `si

**A11y tests**: test every state (open, error, empty, light, dark) with `await expectNoA11yViolations(container)` from `@codeatlasdev/ui/testing` (needs the optional peer `axe-core`). Inside this repo `packages/ui/tests/a11y.ts` re-exports the same `checkA11y`; never fork the axe options.

13. **Discovering the API**: never guess a prop. Use the `ca-ui-mcp` MCP server (`get_component`, `get_export`, `get_equivalent`, `list_components`, `list_parts`, `get_migration`, `search_tokens`, `list_surfaces`, `get_guide`) or `.agents/skills/codeatlas-ui/API.md`; both cover components and every non-component export (stores, `kit`, `vite`, `astro`, `actions`, `seo`, `forms`, `tokens`: signature + summary) and are generated from the code by `packages/ui/scripts/build-manifest.ts` (runs on `build`). `[ca-ui]` dev warnings (via `devWarn`, `esm-env`) flag an `IconButton` without `aria-label` and a `Dialog` without `title`.
13. **Discovering the API**: never guess a prop. Use the `ca-ui-mcp` MCP server (`get_component`, `get_export`, `get_equivalent`, `list_components`, `list_parts`, `get_migration`, `search_tokens`, `list_surfaces`, `get_guide`, and for the design atlas and its verified sources `search_terms`, `get_term`, `get_reference`) or `.agents/skills/codeatlas-ui/API.md`; both cover components and every non-component export (stores, `kit`, `vite`, `astro`, `actions`, `seo`, `forms`, `tokens`: signature + summary) and are generated from the code by `packages/ui/scripts/build-manifest.ts` (runs on `build`). `[ca-ui]` dev warnings (via `devWarn`, `esm-env`) flag an `IconButton` without `aria-label` and a `Dialog` without `title`.

14. **Agent skills layout** (`npx skills add codeatlasdev/ui`): one folder per skill under `.agents/skills/`, named exactly like the `name:` in its `SKILL.md` (`codeatlas-ui`, `codeatlas-dx`, `codeatlas-writing`). Never put a `SKILL.md` directly in `.agents/skills/` (the CLI then treats the directory as a single skill and lists nothing else), and quote any `description:` that contains `: ` (an unquoted one is a YAML error and the CLI silently skips the skill). `packages/ui/tests/skills.test.ts` enforces this.

Expand Down
84 changes: 83 additions & 1 deletion packages/ui/bin/ca-ui-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* claude mcp add codeatlas-ui -- npx -y -p @codeatlasdev/ui ca-ui-mcp
*
* Tools: get_recipe, list_components, get_component, get_export, get_equivalent, list_parts, search_tokens, list_surfaces, get_guide,
* search_demos, get_demo, search_ui, list_facets, get_related, report_issue (drafts a sanitized GitHub issue from a library problem; it never posts anything).
* search_terms, get_term, get_reference (the design atlas and its verified sources), search_demos, get_demo, search_ui, list_facets, get_related, report_issue (drafts a sanitized GitHub issue from a library problem; it never posts anything).
*/
import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
Expand All @@ -15,6 +15,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import type { Recipe } from "../scripts/ai-entry.ts";
import type { AtlasFile } from "../scripts/atlas-markdown.ts";
import type {
Manifest,
ManifestDemo,
Expand Down Expand Up @@ -61,6 +62,9 @@ const recipes = existsSync(join(DIST, "recipes.json"))
const demos = existsSync(join(DIST, "demos.json")) ? load<ManifestDemo[]>("demos.json") : [];
// The published facets only (a facet still under review is never searchable), same entries as api/v1/index.json.
const entries = indexEntries(demos);
// Like demos: a build from the tarball has no site/ data, and the other tools must keep working.
const atlas = existsSync(join(DIST, "atlas.json")) ? load<AtlasFile>("atlas.json") : undefined;
const noAtlas = "No design atlas in this build (dist/atlas.json is missing).";
const noDemos = "No demo catalog in this build (dist/demos.json is missing).";

const text = (s: string) => ({ content: [{ type: "text" as const, text: s }] });
Expand Down Expand Up @@ -474,6 +478,84 @@ server.registerTool(
}
);

server.registerTool(
"search_terms",
{
description:
"Search the design atlas: 269 terms for aesthetics, layouts, interaction patterns and design-system languages. Matches term, category, definition, traits and sectors (every word of the query must match). Returns one line per term with its status and the three 1-5 scales, which are heuristic editorial judgements, not study results. Use get_term for the full entry.",
inputSchema: { query: z.string().optional(), category: z.string().optional() },
},
({ query, category }) => {
if (!atlas) return fail(noAtlas);
const words = (query ?? "").toLowerCase().split(/\s+/).filter(Boolean);
const cat = category ? squash(category) : "";
const hits = atlas.terms.filter(
(t) => (!cat || squash(t.category) === cat) && words.every((w) => t.search.includes(w))
);
if (!hits.length)
return fail(
`No atlas term matches "${query ?? ""}"${category ? ` in "${category}"` : ""}. Categories: ${[...new Set(atlas.terms.map((t) => t.category))].join(", ")}.`
);
return text(
`${hits.length} of ${atlas.terms.length} terms${hits.length > 40 ? " (first 40)" : ""}:\n${hits
.slice(0, 40)
.map((t) => t.line)
.join("\n")}`
);
}
);

server.registerTool(
"get_term",
{
description:
"One design atlas term as markdown: definition, traits, when to use it and when not, UX risk and mitigation, evidence with what the sources do not support, the limits, the heuristic scales, our design-style recipes and a prompt for your AI. `id` is DS001 to DS269, a slug (swiss-style) or the term's name.",
inputSchema: { id: z.string() },
},
({ id }) => {
if (!atlas) return fail(noAtlas);
const n = squash(id);
const hit = atlas.terms.find(
(t) =>
t.id.toLowerCase() === id.trim().toLowerCase() ||
squash(t.slug) === n ||
squash(t.term) === n
);
if (!hit) {
const near = atlas.terms.filter((t) => squash(t.term).includes(n)).slice(0, 8);
return fail(
`No atlas term "${id}".${near.length ? ` Did you mean: ${near.map((t) => `${t.id} ${t.term}`).join(", ")}?` : " Use search_terms."}`
);
}
return text(hit.text);
}
);

server.registerTool(
"get_reference",
{
description:
"One verified source behind the atlas and the concept pages (ids E01-E22, N, W, M, D): citation, kind, strength label, what it supports, what it does not support, the date it was opened and the wording read. Without `id`, lists every source.",
inputSchema: { id: z.string().optional() },
},
({ id }) => {
if (!atlas) return fail(noAtlas);
if (!id)
return text(
atlas.references
.map((r) => {
const [head] = r.text.split("\n");
return `- ${head.replace(/^# /, "")}`;
})
.join("\n")
);
const hit = atlas.references.find((r) => r.id.toLowerCase() === id.trim().toLowerCase());
return hit
? text(hit.text)
: fail(`No reference "${id}". Call get_reference without an id to list them.`);
}
);

server.registerTool(
"report_issue",
{
Expand Down
17 changes: 17 additions & 0 deletions packages/ui/scripts/ai-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,23 @@ export function buildLlmsIndex(i: {
heading: "Setup",
links: [page(ENTRY_PAGES.install, "Installation"), page(ENTRY_PAGES.aiAgents, "AI agents")],
},
{
heading: "Design atlas",
links: [
{
title: "Design atlas",
href: `${SITE}/llms/atlas.txt`,
description:
"269 terms for aesthetics, layouts, interaction patterns and design-system languages, by category; one file per term at /llms/atlas/<slug>.txt, or ask the MCP server (`search_terms`, `get_term`). The 1 to 5 scores are heuristic and an audience is a hypothesis",
},
{
title: "How we use evidence",
href: `${SITE}${llmsPagePath("concepts/evidence")}`,
description:
"the five strength labels and the rules every concept and atlas entry follows: read it before you quote a claim about users",
},
],
},
{
heading: "Machine-readable endpoints",
links: [
Expand Down
184 changes: 184 additions & 0 deletions packages/ui/scripts/atlas-markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// The atlas as plain markdown for agents: one file per term (`/llms/atlas/<slug>.txt`), an index of all terms
// (`/llms/atlas.txt`) and the compact line the MCP `search_terms` returns. Pure functions over the dataset, so the
// site endpoints, the MCP build and the tests share one text.
import {
ATLAS_LIMITS,
ATLAS_REVIEWED,
EVIDENCE_LABEL,
guidance,
SCORE_LABEL,
SCORE_NAMES,
STANDARDIZATION_LABEL,
STATUS_LABEL,
STATUS_MEANING,
stylesFor,
TERMS,
type Term,
TREND_LABEL,
termPrompt,
termReferences,
termSlug,
} from "../../../site/src/data/atlas/atlas.ts";
import {
REFERENCES,
type Reference,
STRENGTH_LABEL,
} from "../../../site/src/data/evidence/references.ts";

export const SITE = "https://ui.codeatlas.com.br";

/** Path of a term's markdown twin. */
export const atlasTermPath = (t: Pick<Term, "term">) => `/llms/atlas/${termSlug(t)}.txt`;

/** One term: everything the page shows, with the caveats attached to the values they qualify. */
export function buildTermMarkdown(t: Term): string {
const g = guidance(t);
const styles = stylesFor(t.id);
const refs = termReferences(t);
return [
`# ${t.term}`,
"",
`${t.id}. ${t.category}: ${t.subcategory}. Page: ${SITE}/atlas/${termSlug(t)}/. Index: ${SITE}/llms/atlas.txt`,
"",
`${t.definition}.`,
"",
`- Status in 2026: ${STATUS_LABEL[t.status]}. ${STATUS_MEANING[t.status]}`,
`- Trend 2027 to 2030: ${TREND_LABEL[t.trend]} (an editorial projection, not a forecast).`,
`- The term: ${STANDARDIZATION_LABEL[t.standardization]}.`,
"",
"## Traits",
"",
...t.traits.map((x) => `- ${x}`),
"",
`Typical settings: ${t.sectors.join(", ")}.`,
"",
"## When to use it, and when not",
"",
g.whenToUse,
"",
g.whenNot,
"",
`Audience (a hypothesis about motivation and task, never measured, not a statement about any group): ${t.audience}.`,
"",
"## UX risk and how to reduce it",
"",
`- Risk: ${t.risk}.`,
`- Mitigation and accessibility: ${t.mitigation}.`,
`- How to check: ${g.validate}`,
"",
"## Evidence",
"",
`${EVIDENCE_LABEL[t.evidence]}.`,
"",
"The sources support the principles that apply to this term. None shows that this style works better or is preferred.",
"",
...refs.map(
(r) =>
`- [${r.id}] ${r.authors} (${r.year}). ${r.title}. ${r.url} (${STRENGTH_LABEL[r.strength]}). Does not support: ${r.doesNotSupport}`
),
"",
"## Limits",
"",
...ATLAS_LIMITS.map((x) => `- ${x}`),
"",
`Last reviewed ${ATLAS_REVIEWED}.`,
"",
"## Heuristic scales",
"",
"Relative editorial judgements from 1 to 5, not results of a study.",
"",
...SCORE_NAMES.map((n) => `- ${SCORE_LABEL[n]}: ${t.scores[n]} / 5 (heuristic)`),
"",
"## In our library",
"",
...(styles.length
? styles.map((s) => `- designStyle("${s.id}"): ${s.label}. ${s.description}`)
: ["No design-style recipe implements this term yet; build it from `--ca-*` tokens."]),
"",
"## Prompt for your AI",
"",
"```text",
termPrompt(t),
"```",
"",
].join("\n");
}

/** One line per term: what `search_terms` returns and what the index lists. */
export function termLine(t: Term): string {
return `${t.id} ${t.term} (${t.category}: ${t.subcategory}; ${STATUS_LABEL[t.status].toLowerCase()}; complexity ${t.scores.complexity}, expressiveness ${t.scores.expressiveness}, density ${t.scores.density}, heuristic): ${t.definition}`;
}

/** The index of the atlas by category, linking each term's own file. */
export function buildAtlasIndexMarkdown(terms: readonly Term[] = TERMS): string {
const cats = [...new Set(terms.map((t) => t.category))];
return [
"# Design atlas",
"",
`> ${terms.length} terms for aesthetics, layouts, interaction patterns and design-system languages. A map, not a verdict. Page: ${SITE}/atlas/. Index: ${SITE}/llms.txt`,
"",
"How to read it: the 1 to 5 scores are heuristic editorial judgements; an audience is a hypothesis about motivation and task, never measured; the references behind a term support underlying principles, not the style. Validate a choice with your own users.",
`Method and limits: ${SITE}/llms/concepts/evidence.txt`,
"",
"Limits:",
...ATLAS_LIMITS.map((x) => `- ${x}`),
`Last reviewed ${ATLAS_REVIEWED}.`,
"",
...cats.flatMap((c) => [
`## ${c}`,
"",
...terms
.filter((t) => t.category === c)
.map((t) => `- [${t.term}](${SITE}${atlasTermPath(t)}): ${t.definition}`),
"",
]),
].join("\n");
}

/** One reference as markdown: the source, what it supports and does not, and what was read when. */
export function buildReferenceMarkdown(r: Reference): string {
return [
`# [${r.id}] ${r.title}`,
"",
`${r.authors} (${r.year}). ${r.venue}. ${r.url}`,
"",
`- Kind: ${r.kind}. Strength: ${STRENGTH_LABEL[r.strength]}.`,
`- Topic: ${r.topic}.`,
`- Supports: ${r.supports}`,
`- Does not support: ${r.doesNotSupport}`,
`- Checked ${r.verified.date} (${r.verified.level === "claim-read" ? "wording read at the source" : "record only; text not read, so no claim may rest on it"}): ${r.verified.how}.`,
`- Excerpt: "${r.verified.excerpt}"`,
"",
].join("\n");
}

/** What `dist/atlas.json` holds: enough for the three MCP tools to answer with no network. */
export interface AtlasFile {
terms: {
id: string;
slug: string;
term: string;
category: string;
line: string;
text: string;
search: string;
}[];
references: { id: string; text: string }[];
}

export function buildAtlasFile(terms: readonly Term[] = TERMS): AtlasFile {
return {
terms: terms.map((t) => ({
id: t.id,
slug: termSlug(t),
term: t.term,
category: t.category,
line: termLine(t),
text: buildTermMarkdown(t),
search: [t.term, t.category, t.subcategory, t.definition, ...t.traits, ...t.sectors]
.join(" ")
.toLowerCase(),
})),
references: REFERENCES.map((r) => ({ id: r.id, text: buildReferenceMarkdown(r) })),
};
}
4 changes: 4 additions & 0 deletions packages/ui/scripts/build-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* dist/tokens.json every `--ca-*` custom property (tokens.css + built-in presets + brand)
* dist/surfaces.json every `<html data-style>` surface style, from materials.css (offline `list_surfaces`)
* dist/guides.json the docs site pages as markdown (offline `get_guide`)
* dist/atlas.json the design atlas as one markdown text per term + the verified references (offline
* `search_terms`, `get_term`, `get_reference`); only in the monorepo (needs site/)
* --skill also rewrites .agents/skills/codeatlas-ui/API.md (components, then non-component exports)
*
* bun scripts/build-manifest.ts [--check]
Expand All @@ -27,6 +29,7 @@ import { loadMigrations, migrationMarkdown } from "../src/cli/migrations.ts";
import { STYLES, type SurfaceStyle } from "../src/lib/theme/define.ts";
import { DESIGN_STYLES } from "../src/lib/theme/styles.ts";
import { ENTRY_PAGES, parseRecipes, type Recipe } from "./ai-entry.ts";
import { buildAtlasFile } from "./atlas-markdown.ts";
import { buildIndexFiles } from "./index-build.ts";
import { checkIdRange, checkIndexFacets, ID_PATTERN, type IndexFacets } from "./index-schema.ts";
import { demoSources, mdxToMarkdown } from "./mdx-to-markdown.ts";
Expand Down Expand Up @@ -1682,6 +1685,7 @@ if (import.meta.main) {
recipes: buildRecipes(guides),
});
}
if (existsSync(DEMOS_DIR)) write("atlas.json", buildAtlasFile());
const { demos, files } = catalog.length
? buildDemos(manifest, catalog)
: { demos: [], files: {} };
Expand Down
1 change: 1 addition & 0 deletions packages/ui/tests/ai-entry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ describe("llms.txt links the agent-readable .txt form of every page and recipe (
...slugs.map((s) => `${SITE}${llmsPagePath(s)}`),
...recipes.map((r) => `${SITE}${llmsRecipePath(r.id)}`),
`${SITE}/llms-full.txt`,
`${SITE}/llms/atlas.txt`,
]);
for (const m of out.matchAll(/\]\((https:\/\/[^)]*\.txt)\)/g)) {
const url = m[1];
Expand Down
Loading