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
3 changes: 3 additions & 0 deletions .agents/skills/codeatlas-ui/DEMOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,3 +387,6 @@ Every demo has a stable number. Fetch one with `get_demo` (MCP) or https://ui.co
| 990 | Brand color | inputs/color-field | ColorField |
| 991 | Text color | inputs/color-field | ColorField |
| 992 | Label and number | inputs/slider | SliderField |
| 993 | Viewport simulator | concepts/viewport | SegmentedControl, Switch, Slider |
| 994 | A hero that is a floor | concepts/viewport | Button, Switch |
| 995 | A form with the keyboard open | concepts/viewport | Button, StickyActions, TextField |
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) `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`.
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")`, `list_concepts()` and `get_concept("viewport")` (why a screen is built a certain way: each claim labelled with strength, scope and sources), 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/concepts-mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codeatlasdev/ui": minor
---

`ca-ui-mcp` gains `list_concepts` and `get_concept`: the Concepts pages (viewport-complete UX, motion that respects people, targets and focus, appeal is not usability) with every claim's strength label, scope, sources, limits and last-reviewed date. The llms-full concept digest now carries the contract and the prompt only, so the file stays under its size cap.
5 changes: 5 additions & 0 deletions .changeset/llms-full-props-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codeatlasdev/ui": patch
---

`llms-full.txt` keeps only the props table of each non-core page. The summary also carried the prose and examples that follow a table (the SEO page alone added 9 KB), which pushed the file past its size cap once the Concepts pages were added; the descriptions stay in each page's own file.
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Every numbered demo (`site/src/demos/<group>/<page>/<Variant>.svelte`, id in `si
- **Facets:** `kind` + `subtype`, `industry[]`, `style[]` (a `designStyle` id or one of the planned recipes), `motion` (`none` state-free, `subtle` state change, `expressive` entrance/scroll/loop, `immersive` physics/3D/shader/cursor-driven scene), `interaction[]`, `layout`, `colorMode`, `density`, `tech[]`, `cost`, `components[]`, `aliases[]` (lowercase search synonyms), `related[]` (catalog ids), `a11y { reducedMotion, keyboard }`, `added` (ISO date). The closed vocabularies live in `packages/ui/scripts/index-schema.ts`; an unknown value fails the check.
- **Inferred, never hand-typed:** `kind`, `subtype`, `components` (must equal the package imports of the demo), `tech`, `cost`, `added`. For a new demo run `bun packages/ui/scripts/index-backfill.ts` (it keeps every value already in a sidecar and fills these), then write `industry`, `style`, `motion`, `interaction`, `layout`, `colorMode`, `density`, `aliases`, `related` and `a11y.keyboard` by reading what the demo renders. Do not copy another demo's block.
- **Id ranges:** ids stay append-only; 3 digits to 999, 4 digits from 1000. `ID_RANGES` in `index-schema.ts` reserves a block per kind (components 001-299 and 400-999, motion 300-399 by subtype, sections 1000-2999 in blocks of 100 per subtype, backgrounds 3000-3199, techniques 3200-3399, 3D 3400-3499, shaders 3500-3599, pages 4000-4499, templates 4500-4999). The check fails when an id sits outside the block of its `kind`/`subtype`. Four legacy ids (`256`, `387-389`) are exempt (`LEGACY_OUT_OF_RANGE`); a new id never is.
- **Publishing gate:** a hand-read facet reaches `index.json`, the markdown twins and every filter only when the one-sided 95% upper bound of its error rate on a seeded, folder-stratified audit is at most 5% (`AUDIT`, `errorUpperBound`, `MAX_ERROR_RATE` in `index-schema.ts`). 0 wrong of 73 reads is "at most ~4% wrong", never "0%". A field above the limit stays in the sidecar, marked `"review": { "<field>": "pending" }`, validated but hidden until it is corrected and re-audited with a new seed; flip it in `AUDIT` only with the new numbers. Only 203 of 382 entries were ever re-read; the other 179 hold the first pass's value as corrected by rules, and are unverified beyond them. Today `industry` is pending (5 wrong of 73, upper bound ~14%). Never fix a field by editing the sample you measured it on: re-audit on entries you have not read.
- **Publishing gate:** a hand-read facet reaches `index.json`, the markdown twins and every filter only when the one-sided 95% upper bound of its error rate on a seeded, folder-stratified audit is at most 5% (`AUDIT`, `errorUpperBound`, `MAX_ERROR_RATE` in `index-schema.ts`). 0 wrong of 73 reads is "at most ~4% wrong", never "0%". A field above the limit stays in the sidecar, marked `"review": { "<field>": "pending" }`, validated but hidden until it is corrected and re-audited with a new seed; flip it in `AUDIT` only with the new numbers. Only 203 of 385 entries were ever re-read; the other 182 hold the first pass's value as corrected by rules, and are unverified beyond them. Today `industry` is pending (5 wrong of 73, upper bound ~14%). Never fix a field by editing the sample you measured it on: re-audit on entries you have not read.
- **Output:** the manifest build writes `site/public/api/v1/index.json` (every entry with its facets and per-facet counts) and one markdown twin per id at `site/public/gallery/<id>.md`. Both are generated: never edit them; commit the regenerated files.
- **Originality:** write every title, summary, alias and prompt in our own words. Never name, link to or credit an external site or product in a demo, sidecar or generated file (`tests/demos-index.test.ts` scans the sidecars).

Expand Down 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`, 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`.
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`, `list_concepts`, `get_concept`, 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
46 changes: 45 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_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).
* list_concepts, get_concept (the Concepts pages with their claims, limits and sources), 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 Down Expand Up @@ -478,6 +478,50 @@ server.registerTool(
}
);

/** The Concepts docs group: pages whose claims about users carry a strength label, a scope and a review date. */
const concepts = guides.filter((g) => g.group === "Concepts" && g.slug !== "concepts/index");
const conceptId = (g: ManifestGuide) => g.slug.replace(/^concepts\//, "");
const reviewedOf = (g: ManifestGuide) =>
g.markdown.match(/Last reviewed (\d{4}-\d{2}-\d{2})\./)?.[1];

server.registerTool(
"list_concepts",
{
description:
"List the concept pages (why interfaces are built a certain way): id, title, last-reviewed date and what each covers. Each claim in a concept carries a strength label (established, supported, preliminary, practice, heuristic), its scope and its sources. Read `evidence` first for the rules. Get one with get_concept.",
inputSchema: {},
},
() =>
text(
concepts
.map(
(g) =>
`- ${conceptId(g)}: ${g.title}${reviewedOf(g) ? ` (reviewed ${reviewedOf(g)})` : ""}. ${g.description}`
)
.join("\n") || "No concepts in this build."
)
);

server.registerTool(
"get_concept",
{
description:
"One concept page as markdown with its definition, evidence claims (strength label, sources, scope), the contract, library implementation, anti-patterns, checklist, a prompt for your AI, limits with the last-reviewed date, and references with what each does not support. `id` is from list_concepts (viewport, evidence).",
inputSchema: { id: z.string() },
},
({ id }) => {
const n = squash(id.replace(/^concepts\//, ""));
const hit =
concepts.find((g) => squash(conceptId(g)) === n) ??
concepts.find((g) => squash(g.title) === n);
if (!hit)
return fail(
`No concept "${id}". Concepts: ${concepts.map(conceptId).join(", ") || "none in this build"}.`
);
return text(`# ${hit.title}\n\n${hit.description}\n\n${hit.markdown}`);
}
);

server.registerTool(
"search_terms",
{
Expand Down
11 changes: 11 additions & 0 deletions packages/ui/scripts/ai-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ export function buildLlmsIndex(i: {
heading: "Setup",
links: [page(ENTRY_PAGES.install, "Installation"), page(ENTRY_PAGES.aiAgents, "AI agents")],
},
{
heading: "Concepts",
links: [
{
title: "Viewport-complete UX",
href: `${SITE}${llmsPagePath("concepts/viewport")}`,
description:
"the first screen, viewport units, safe areas, the keyboard, reflow and the cue that there is more below; every claim carries a strength label, scope and sources, or ask the MCP server (`list_concepts`, `get_concept`)",
},
],
},
{
heading: "Design atlas",
links: [
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/scripts/index-backfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const COMPONENT_SUBTYPE: Record<string, string> = {
foundation: "foundation",
map: "map",
"docs-kit": "display",
// The Concepts pages' simulators teach a design foundation (viewport, motion, targets).
concepts: "foundation",
};

/** `kind` and `subtype` from the demo's folder. */
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/scripts/index-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export const AUDIT = {
* were applied to all entries), `holdout` = 73 random entries not in `first`. `catalog` is the total; the rest,
* `catalog - read`, were never re-read.
*/
read: { first: 130, holdout: 73, total: 203, catalog: 382, unread: 179 },
read: { first: 130, holdout: 73, total: 203, catalog: 385, unread: 182 },
fields: {
industry: { wrong: 5, checked: 73, review: "pending" as Review },
style: { wrong: 0, checked: 73, review: "published" as Review },
Expand Down
14 changes: 6 additions & 8 deletions packages/ui/scripts/llms-full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ export function propsSummary(markdown: string): string {
.split(/\n(?=### )/)
.filter((block) => /^### [^\n]*\n\n\| Prop \|/.test(block))
.map((block) =>
// The heading and the table only: prose and examples after the table are in the page's file.
block
.split("\n")
.filter((line, i) => i < 2 || line.startsWith("|"))
.map((line) => {
if (!line.startsWith("|")) return line;
// Cells are split on `|` that is not escaped as `\|` (a union type in a cell).
Expand All @@ -60,15 +62,11 @@ export function propsSummary(markdown: string): string {

/**
* What a concept page contributes to llms-full.txt: its definition (the text before the first `## `) and the
* sections an agent acts on, in page order. The evidence, demos, limits and references stay in the page's own
* file (linked from the section), so a concept costs a few KB and the file does not grow with every page.
* sections an agent acts on, in page order: the contract and the paste-in prompt. The evidence, anti-patterns,
* checklist, demos, limits and references stay in the page's own file (linked from the section), so a concept
* costs about 3 KB and the file stays under its cap as concepts are added.
*/
export const CONCEPT_DIGEST_SECTIONS: readonly string[] = [
"The contract",
"Anti-patterns",
"Checklist",
"Prompt for your AI",
];
export const CONCEPT_DIGEST_SECTIONS: readonly string[] = ["The contract", "Prompt for your AI"];

/** The digest of a concept page's markdown: the lead and the `CONCEPT_DIGEST_SECTIONS`. */
export function conceptDigest(markdown: string): string {
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 @@ -252,6 +252,7 @@ describe("llms.txt links the agent-readable .txt form of every page and recipe (
...recipes.map((r) => `${SITE}${llmsRecipePath(r.id)}`),
`${SITE}/llms-full.txt`,
`${SITE}/llms/atlas.txt`,
`${SITE}/llms/concepts/viewport.txt`,
]);
for (const m of out.matchAll(/\]\((https:\/\/[^)]*\.txt)\)/g)) {
const url = m[1];
Expand Down
22 changes: 21 additions & 1 deletion packages/ui/tests/llms-budget.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative } from "node:path";
import { buildLlmsPage, llmsPagePath } from "../scripts/ai-entry.ts";
import { buildLlmsFull, CORE_PAGES } from "../scripts/llms-full.ts";
import { buildLlmsFull, CORE_PAGES, propsSummary } from "../scripts/llms-full.ts";
import { demoSources, mdxToMarkdown } from "../scripts/mdx-to-markdown.ts";

// llms-full.txt is the file an agent pastes whole into its context window, so its size is a hard limit of
Expand Down Expand Up @@ -101,6 +101,26 @@ describe("llms-full.txt has a fixed shape (#436)", () => {
expect(s).not.toContain("| Description |");
});

it("a props table keeps only the table: the prose that follows it stays in the page's file", () => {
const md = [
"### Props",
"",
"| Prop | Type | Default | Description |",
"|---|---|---|---|",
"| `title` | `string` | required | The page title. |",
"",
"A paragraph that explains the table and belongs to the page, not to the summary.",
"",
"```svelte",
"<Seo title='x' />",
"```",
].join("\n");
const out = propsSummary(md);
expect(out).toContain("| `title` | `string` | required |");
expect(out).not.toContain("A paragraph that explains");
expect(out).not.toContain("<Seo");
});

describe("a page costs its summary, not its size", () => {
const synthetic = (slug: string, group: string, extra = 0) => ({
slug,
Expand Down
15 changes: 8 additions & 7 deletions packages/ui/tests/llms-full.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,17 @@ describe("a concept page in llms-full.txt", () => {
"- [E01] A source.",
].join("\n");

it("keeps the lead, the contract, the checklist and the prompt, and drops evidence and references", () => {
it("keeps the lead, the contract and the prompt, and drops evidence, checklist and references", () => {
const d = conceptDigest(body);
for (const kept of [
"Lead paragraph",
"## The contract",
for (const kept of ["Lead paragraph", "## The contract", "## Prompt for your AI"])
expect(d).toContain(kept);
for (const dropped of [
"## Evidence",
"## Checklist",
"## Prompt for your AI",
"## References",
"A long evidence section",
"Reflows at 320 CSS px",
])
expect(d).toContain(kept);
for (const dropped of ["## Evidence", "## References", "A long evidence section"])
expect(d).not.toContain(dropped);
});

Expand Down
Loading