diff --git a/AGENTS.md b/AGENTS.md index aaf7766a..7cacccad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -179,6 +179,20 @@ Every numbered demo (`site/src/demos///.svelte`, id in `si --- +## Writing the Concepts and the atlas (evidence rules) + +Concept pages (`site/src/content/docs/concepts/`) and the design atlas (`site/src/data/atlas/`) make claims about users. Full text: https://ui.codeatlas.com.br/docs/concepts/evidence/. Binding for every page, dataset entry and agent answer you write: + +1. **Never brand as science.** The section is "Evidence", the atlas is a "design atlas". Never write "scientifically proven", "science says", "science-based", "studies show", "research proves" or "research says" (`BANNED_PHRASES` in `site/src/data/evidence/references.ts`; `packages/ui/tests/evidence-rules.test.ts` fails on them). +2. **Claim-level sourcing.** Every empirical claim about users is a ``. No verified source: `heuristic` (no `refs`) or cut it. +3. **Scope with every claim**: who was studied, the task, the context ("brief exposures in a lab; no preference for any style"). Never generalize beyond it. +4. **Standards are not evidence of preference.** WCAG, specs and platform guides are `practice`: what is required or recommended. +5. **No causal or demographic claims about taste.** An audience is a labelled hypothesis. +6. **Each concept page ends with `` and ``**, in the template order: definition and vocabulary, evidence, contract, how the library implements it, anti-patterns, checklist, prompt for your AI, limits, references. +7. **Verify before publishing.** Open the DOI or URL, read the wording you rely on, and record `verified.date`, `verified.level` (`claim-read`, or `identity-only` when the text was not reachable: then no `` may cite it) and the `excerpt` in `site/src/data/evidence/references.json`. Unverifiable sources are not added. A source's kind caps its strength (`checkReferences`): a standard or guideline is `practice`, a preprint or a vendor study is `preliminary`. +8. **No number you cannot trace.** A figure goes with its study and its conditions, or it does not go in. +9. Design inspiration sites are never named; science and standards are cited by name where they are primary sources. + ## Glass and frosted styles `glass`, `frosted` and the `liquid` recipe are opt-in surface styles (the package default is `solid`: opaque, no blur; a page with no `data-style` on `` is `solid` too). The rules below apply only when blur is used: `html[data-style]` is `glass` or `frosted`, a component sets `data-ca-blur`, or a surface has `material="glass" | "frosted" | "liquid"`. An app on the default style never needs them, and a new example must not add `material="glass"` just to look finished. diff --git a/packages/ui/scripts/llms-full.ts b/packages/ui/scripts/llms-full.ts index 7fe49e38..597c1851 100644 --- a/packages/ui/scripts/llms-full.ts +++ b/packages/ui/scripts/llms-full.ts @@ -58,6 +58,27 @@ export function propsSummary(markdown: string): string { .join("\n\n"); } +/** + * 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. + */ +export const CONCEPT_DIGEST_SECTIONS: readonly string[] = [ + "The contract", + "Anti-patterns", + "Checklist", + "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 { + const [lead, ...sections] = markdown.split(/\n(?=## )/); + const keep = sections.filter((s) => + CONCEPT_DIGEST_SECTIONS.includes(s.match(/^## (.+)$/m)?.[1]?.trim() ?? "") + ); + return [lead, ...keep].join("\n\n").trim(); +} + export function buildLlmsFull(opts: { site: string; docs: LlmsFullDoc[] }): string { const parts = [ "# CodeAtlas UI: full documentation", @@ -67,7 +88,8 @@ export function buildLlmsFull(opts: { site: string; docs: LlmsFullDoc[] }): stri ]; for (const d of opts.docs) { const markdown = mdxToMarkdown(d.body); - const props = CORE_PAGES.includes(d.slug) ? "" : propsSummary(markdown); + const props = + CORE_PAGES.includes(d.slug) || d.group === "Concepts" ? "" : propsSummary(markdown); parts.push( "---", "", @@ -79,7 +101,11 @@ export function buildLlmsFull(opts: { site: string; docs: LlmsFullDoc[] }): stri "", d.description, "", - CORE_PAGES.includes(d.slug) ? markdown : props, + CORE_PAGES.includes(d.slug) + ? markdown + : d.group === "Concepts" + ? conceptDigest(markdown) + : props, "" ); } diff --git a/packages/ui/scripts/mdx-to-markdown.ts b/packages/ui/scripts/mdx-to-markdown.ts index 75193e25..7ab9ff24 100644 --- a/packages/ui/scripts/mdx-to-markdown.ts +++ b/packages/ui/scripts/mdx-to-markdown.ts @@ -1,4 +1,10 @@ // MDX docs body to plain markdown (shared by the site's llms-full.txt and the MCP guide bundle). +import { + getReference, + parseRefIds, + STRENGTH_LABEL, + type Strength, +} from "../../../site/src/data/evidence/references.ts"; /** * Raw `.svelte` demo sources by import name, for the `` examples of one MDX page. @@ -37,6 +43,32 @@ function propsTable(heading: string, block: string): string { ].join("\n"); } +/** `strength="x"`, `refs="E01"` or `scope="..."` of a JSX opening tag. */ +const attr = (tag: string, name: string) => tag.match(new RegExp(`\\b${name}="([^"]*)"`))?.[1]; + +/** "Tuch et al. 2012": the short citation shown next to a claim. */ +function shortCite(id: string): string { + const r = getReference(id); + return r ? `${r.authors.split(",")[0]}${r.authors.includes(",") ? " et al." : ""} ${r.year}` : id; +} + +/** The References list of a page as markdown: what each source supports, what it does not, and when it was checked. */ +function refsMarkdown(ids: string, title = "References"): string { + const items = parseRefIds(ids).map((id) => { + const r = getReference(id); + if (!r) return `- ${id}: unknown reference`; + const checked = + r.verified.level === "identity-only" ? "record only, text not read" : "text read"; + return [ + `- [${r.id}] ${r.authors} (${r.year}). ${r.title}. ${r.venue}. ${r.url}`, + ` - Strength: ${STRENGTH_LABEL[r.strength]}. Supports: ${r.supports}`, + ` - Does not support: ${r.doesNotSupport}`, + ` - Checked ${r.verified.date} (${checked}).`, + ].join("\n"); + }); + return ["", `## ${title}`, "", ...items, ""].join("\n"); +} + /** MDX body to plain markdown: drop imports and JSX, keep prose and code fences untouched. */ export function mdxToMarkdown(mdx: string, sources: Record = {}): string { const out: string[] = []; @@ -45,6 +77,9 @@ export function mdxToMarkdown(mdx: string, sources: Record = {}) let skipping = false; // inside a multi-line JSX tag such as let propsHeading = ""; let propsLines: string[] | null = null; + let claimClose = ""; // the scope line to emit at the that closes the open claim + let inClaim = false; + let limitsReviewed = ""; for (const raw of mdx.split("\n")) { const fence = raw.match(/^(\s*)```/); if (fence) { @@ -92,13 +127,51 @@ export function mdxToMarkdown(mdx: string, sources: Record = {}) } continue; } + const claim = line.match(/^]*>/); + if (claim) { + const strength = attr(claim[0], "strength") as Strength | undefined; + const refs = parseRefIds(attr(claim[0], "refs") ?? ""); + const scope = attr(claim[0], "scope"); + const label = strength ? STRENGTH_LABEL[strength] : "Claim"; + out.push( + `> **${label}**${refs.length ? `: ${refs.map(shortCite).join("; ")} [${refs.join(", ")}]` : ""}` + ); + claimClose = scope ? `> *Scope: ${scope}*` : ""; + // Body lines of the claim are quoted until the closing tag. + out.push(">"); + inClaim = true; + continue; + } + if (line === "") { + if (claimClose) out.push(">", claimClose); + claimClose = ""; + inClaim = false; + continue; + } + const refsTag = line.match(/^]*\/>/); + if (refsTag) { + out.push(refsMarkdown(attr(refsTag[0], "ids") ?? "", attr(refsTag[0], "title"))); + continue; + } + const limits = line.match(/^]*>/); + if (limits) { + out.push("", "## Limits", ""); + limitsReviewed = attr(limits[0], "reviewed") ?? ""; + continue; + } + if (line === "") { + if (limitsReviewed) out.push("", `Last reviewed ${limitsReviewed}.`); + limitsReviewed = ""; + continue; + } // Any other JSX tag (, , , ) is layout only. // A tag that does not end on its own line (multi-line props) is skipped until its closing `>`. if (/^<\/?[A-Z]/.test(line)) { if (!line.endsWith(">")) skipping = true; continue; } - out.push(raw.replace(/^ {2}(?=\S)/, "")); + const text = raw.replace(/^ {2}(?=\S)/, ""); + out.push(inClaim ? (text ? `> ${text}` : ">") : text); } return out .join("\n") diff --git a/packages/ui/tests/evidence-rules.test.ts b/packages/ui/tests/evidence-rules.test.ts new file mode 100644 index 00000000..1fef555c --- /dev/null +++ b/packages/ui/tests/evidence-rules.test.ts @@ -0,0 +1,179 @@ +import { describe, expect, it } from "bun:test"; +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join, relative } from "node:path"; +import { + BANNED_PHRASES, + checkReferences, + getReference, + parseRefIds, + REFERENCES, + STRENGTHS, +} from "../../../site/src/data/evidence/references.ts"; +import { mdxToMarkdown } from "../scripts/mdx-to-markdown.ts"; + +// The editorial rules of the Evidence section (site/src/content/docs/concepts/evidence.mdx), as checks. A claim +// about users must name its source, its strength and its scope; a source must have been opened; and the site +// never presents opinion as proof. +const SITE = join(import.meta.dir, "../../../site/src"); +const DOCS = join(SITE, "content/docs"); + +const walk = (dir: string, ext: string[]): string[] => + readdirSync(dir).flatMap((f) => { + const p = join(dir, f); + return statSync(p).isDirectory() ? walk(p, ext) : ext.some((e) => p.endsWith(e)) ? [p] : []; + }); +const pages = walk(DOCS, [".mdx"]).map((file) => ({ + slug: relative(DOCS, file) + .replaceAll("\\", "/") + .replace(/\.mdx$/, ""), + src: readFileSync(file, "utf8"), +})); +/** The page without code fences and inline code: a tag written as an example is not a tag used. */ +const markup = (src: string) => src.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, ""); + +describe("references dataset", () => { + it("passes every structural rule", () => { + expect(checkReferences(REFERENCES)).toEqual([]); + }); + + it("rejects a standard presented as evidence and a source nobody opened", () => { + const base = REFERENCES.find((r) => r.id === "E07"); + if (!base) throw new Error("E07 missing"); + const bad = checkReferences([ + { ...base, strength: "established" }, + { ...base, id: "E98", verified: { ...base.verified, excerpt: "" } }, + ]); + expect(bad.some((e) => e.includes('is "practice"'))).toBe(true); + expect(bad.some((e) => e.includes("excerpt is empty"))).toBe(true); + }); + + it("has read the wording of every source it records", () => { + const unread = REFERENCES.filter((r) => r.verified.level !== "claim-read").map((r) => r.id); + expect(unread).toEqual([]); + // An excerpt is the wording relied on, so it is never a bare citation line. + const thin = REFERENCES.filter((r) => r.verified.excerpt.trim().length < 60).map((r) => r.id); + expect(thin).toEqual([]); + }); +}); + +describe("site content never presents opinion as proof", () => { + // The rules page quotes the banned phrases to forbid them, so it is the one exemption. + const files = [ + ...pages + .filter((p) => p.slug !== "concepts/evidence") + .map((p) => ({ name: `docs/${p.slug}`, text: p.src })), + ...walk(join(SITE, "components"), [".astro", ".svelte"]).map((f) => ({ + name: relative(SITE, f), + text: readFileSync(f, "utf8"), + })), + ]; + it("has none of the banned phrases", () => { + const hits = files.flatMap((f) => + BANNED_PHRASES.filter((b) => f.text.toLowerCase().includes(b)).map((b) => `${f.name}: "${b}"`) + ); + expect(hits).toEqual([]); + }); + + it("never calls a section or a page 'science'", () => { + const hits = pages.flatMap((p) => { + const fm = p.src.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? ""; + return /^(title|group):.*\bscien(ce|tific)\b/im.test(fm) ? [p.slug] : []; + }); + expect(hits).toEqual([]); + }); +}); + +describe("concept checklists", () => { + it("are plain lists: a GFM task list renders checkboxes with no label (axe: critical)", () => { + const hits = pages.filter((p) => /^\s*[-*] \[[ xX]\] /m.test(markup(p.src))).map((p) => p.slug); + expect(hits).toEqual([]); + }); +}); + +describe("every claim on a page is sourced", () => { + const tag = (src: string, name: string) => + [...markup(src).matchAll(new RegExp(`<${name}\\b[^>]*>`, "g"))].map((m) => m[0]); + const attr = (t: string, n: string) => t.match(new RegExp(`\\b${n}="([^"]*)"`))?.[1]; + + for (const p of pages.filter((x) => /<(Claim|Refs|Limits)\b/.test(markup(x.src)))) { + describe(p.slug, () => { + it("uses a known strength, and scope plus sources on every non-heuristic claim", () => { + for (const t of tag(p.src, "Claim")) { + const strength = attr(t, "strength") ?? ""; + expect(STRENGTHS as readonly string[]).toContain(strength); + if (strength === "heuristic") { + expect(attr(t, "refs")).toBeUndefined(); + } else { + expect(attr(t, "scope")?.trim().length ?? 0).toBeGreaterThan(10); + expect(parseRefIds(attr(t, "refs") ?? "").length).toBeGreaterThan(0); + } + } + }); + + it("only cites sources that exist and whose text was read", () => { + const ids = [ + ...tag(p.src, "Claim").flatMap((t) => parseRefIds(attr(t, "refs") ?? "")), + ...tag(p.src, "Refs").flatMap((t) => parseRefIds(attr(t, "ids") ?? "")), + ]; + for (const id of ids) { + const r = getReference(id); + expect(r?.id).toBe(id); + expect(r?.verified.date).toMatch(/^\d{4}-\d{2}-\d{2}$/); + } + // A claim may rest only on a source whose text was read. + for (const t of tag(p.src, "Claim")) + for (const id of parseRefIds(attr(t, "refs") ?? "")) + expect(getReference(id)?.verified.level).toBe("claim-read"); + }); + + it("lists every source it cites in its References", () => { + const cited = new Set( + tag(p.src, "Claim").flatMap((t) => parseRefIds(attr(t, "refs") ?? "")) + ); + const listed = new Set( + tag(p.src, "Refs").flatMap((t) => parseRefIds(attr(t, "ids") ?? "")) + ); + expect([...cited].filter((id) => !listed.has(id))).toEqual([]); + }); + }); + } +}); + +describe("concept and evidence pages carry the template", () => { + for (const p of pages.filter( + (x) => x.slug.startsWith("concepts/") && x.slug !== "concepts/index" + )) { + it(`${p.slug} has References and a dated Limits box`, () => { + expect(p.src).toMatch(/ { + const mdx = [ + '', + "Appeal ratings agree between exposures.", + "", + "", + '', + "We did not test this.", + "", + "", + '', + ].join("\n"); + const md = mdxToMarkdown(mdx); + + it("keeps strength, source and scope next to the claim", () => { + expect(md).toContain("> **Supported**: Lindgaard et al. 2006 [E05]"); + expect(md).toContain("> Appeal ratings agree between exposures."); + expect(md).toContain("> *Scope: Lab ratings of 50 and 500 ms exposures of homepages.*"); + }); + + it("keeps the limits, its review date and the reference with what it does not support", () => { + expect(md).toContain("## Limits"); + expect(md).toContain("Last reviewed 2026-10-25."); + expect(md).toContain("[E05] Lindgaard"); + expect(md).toContain("Does not support:"); + }); +}); diff --git a/packages/ui/tests/llms-full.test.ts b/packages/ui/tests/llms-full.test.ts index d389f57c..59e3c542 100644 --- a/packages/ui/tests/llms-full.test.ts +++ b/packages/ui/tests/llms-full.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test"; import { CONSUMER_GUIDE } from "../scripts/ai-entry.ts"; -import { buildLlmsFull } from "../scripts/llms-full.ts"; +import { buildLlmsFull, conceptDigest } from "../scripts/llms-full.ts"; describe("llms-full.txt", () => { const out = buildLlmsFull({ @@ -39,3 +39,61 @@ describe("llms-full.txt", () => { ); }); }); + +describe("a concept page in llms-full.txt", () => { + const body = [ + "Lead paragraph that defines the concept.", + "", + "## Evidence", + "", + "A long evidence section with sources that stays in the page's own file.", + "", + "## The contract", + "", + "Use min-height, never a fixed height.", + "", + "## Checklist", + "", + "- [ ] Reflows at 320 CSS px", + "", + "## Prompt for your AI", + "", + "Build it with min-height.", + "", + "## References", + "", + "- [E01] A source.", + ].join("\n"); + + it("keeps the lead, the contract, the checklist and the prompt, and drops evidence and references", () => { + const d = conceptDigest(body); + for (const kept of [ + "Lead paragraph", + "## The contract", + "## Checklist", + "## Prompt for your AI", + ]) + expect(d).toContain(kept); + for (const dropped of ["## Evidence", "## References", "A long evidence section"]) + expect(d).not.toContain(dropped); + }); + + it("is what the dump carries, with the link to the complete file", () => { + const out = buildLlmsFull({ + site: "https://ui.codeatlas.com.br", + docs: [ + { + group: "Concepts", + title: "Viewport", + url: "/docs/concepts/viewport/", + slug: "concepts/viewport", + description: "d", + body, + }, + ], + }); + expect(out).toContain("Use min-height, never a fixed height."); + expect(out).not.toContain("A long evidence section"); + expect(out).toContain("https://ui.codeatlas.com.br/llms/concepts/viewport.txt"); + }); +}); diff --git a/site/src/components/docs/Claim.astro b/site/src/components/docs/Claim.astro new file mode 100644 index 00000000..076692d8 --- /dev/null +++ b/site/src/components/docs/Claim.astro @@ -0,0 +1,29 @@ +--- +import { getReference, parseRefIds, STRENGTH_LABEL, STRENGTH_MEANING, type Strength } from "~/data/evidence/references"; + +interface Props { + /** established | supported | preliminary | practice | heuristic. Describes this claim only. */ + strength: Strength; + /** Reference ids (`"E01, E06"`). Required unless the strength is `heuristic`. */ + refs?: string; + /** Who or what was studied, which task, which context. Required unless the strength is `heuristic`. */ + scope?: string; +} +const { strength, refs = "", scope } = Astro.props; +const ids = parseRefIds(refs); +const sources = ids.map((id) => { + const ref = getReference(id); + if (!ref) throw new Error(`: unknown reference id "${id}"`); + return ref; +}); +if (strength === "heuristic" ? ids.length > 0 : ids.length === 0 || !scope) + throw new Error(`: ${strength === "heuristic" ? "a heuristic has no sources" : "needs refs and scope"}`); +--- +
+

+ {STRENGTH_LABEL[strength]} + {sources.map((r, i) => (<>{i > 0 && ", "}{r.authors.split(",")[0]} {r.year}))} +

+
+ {scope &&

Scope: {scope}

} +
diff --git a/site/src/components/docs/Limits.astro b/site/src/components/docs/Limits.astro new file mode 100644 index 00000000..f92bc8d5 --- /dev/null +++ b/site/src/components/docs/Limits.astro @@ -0,0 +1,13 @@ +--- +interface Props { + /** ISO date (YYYY-MM-DD) of the last review of this page's evidence. */ + reviewed: string; +} +const { reviewed } = Astro.props; +if (!/^\d{4}-\d{2}-\d{2}$/.test(reviewed)) throw new Error(`: use YYYY-MM-DD`); +--- + diff --git a/site/src/components/docs/Refs.astro b/site/src/components/docs/Refs.astro new file mode 100644 index 00000000..8140c61a --- /dev/null +++ b/site/src/components/docs/Refs.astro @@ -0,0 +1,33 @@ +--- +import { getReference, parseRefIds, STRENGTH_LABEL } from "~/data/evidence/references"; + +interface Props { + /** Reference ids used on the page (`"E01, E06"`). */ + ids: string; + title?: string; +} +const { ids, title = "References" } = Astro.props; +const refs = parseRefIds(ids).map((id) => { + const ref = getReference(id); + if (!ref) throw new Error(`: unknown reference id "${id}"`); + return ref; +}); +--- +
+

{title}

+
    + {refs.map((r) => ( +
  1. +

    + {r.authors} ({r.year}). {r.title}. {r.venue}. + {STRENGTH_LABEL[r.strength]} +

    +

    Supports: {r.supports}

    +

    Does not support: {r.doesNotSupport}

    +

    + Checked {r.verified.date}{r.verified.level === "identity-only" ? " (record only; text not read)" : ""}. +

    +
  2. + ))} +
+
diff --git a/site/src/content/docs/concepts/evidence.mdx b/site/src/content/docs/concepts/evidence.mdx new file mode 100644 index 00000000..93e32e2b --- /dev/null +++ b/site/src/content/docs/concepts/evidence.mdx @@ -0,0 +1,104 @@ +--- +title: How we use evidence +description: The strength labels, the limits and the rules every concept page and every atlas entry follows, so a claim about users always comes with its source, its scope and what it does not show. +group: Concepts +order: 1 +--- +import { Callout } from "@codeatlasdev/ui"; +import Claim from "~/components/docs/Claim.astro"; +import Limits from "~/components/docs/Limits.astro"; +import Refs from "~/components/docs/Refs.astro"; + +The Concepts section explains why interfaces are built the way they are. A claim about users carries weight: people ship products and write accessibility policy from it. This page is the contract that keeps the claims honest, for readers and for the AI agents that quote these pages. + +## What a concept page is + +Every concept follows the same order, so you can jump to the part you need: + +1. **Definition and vocabulary.** The terms, with what each one does and does not mean. +2. **Evidence.** The claims about users, each next to its source, a strength label and its scope. +3. **The contract.** The principles we derive, kept apart from the evidence so you can tell a finding from a rule of thumb. +4. **How the library implements it.** The tokens, components and live demos that apply the principle. +5. **Anti-patterns.** What goes wrong, and what to do instead. +6. **Checklist.** Things you can verify. +7. **Prompt for your AI.** A paste-in instruction that carries the contract into a coding agent. +8. **Limits.** What we do not know, and the date the page was last reviewed. +9. **References.** Every source cited, with what it supports and what it does not. + +## The five strength labels + +A label belongs to one claim, never to a whole page. The same page can hold a standard, a single study and our own opinion; each says which it is. + + +Replicated findings, or a meta-analysis or authoritative review of them. Few claims about interface design reach this level. + + + +One or more peer-reviewed studies. Read the scope before you generalize. + + + +A single or small study, a preprint, or a vendor reporting its own research. + + + +WCAG, a specification, a platform guide or a practitioner article. It says what is required or recommended. It is not evidence of what users prefer or what performs better. + + + +A rule of thumb we use, with no source behind it. Test it with your own users before you rely on it. + + +## The rules + +These are binding on every page in this section and on every entry in the [design atlas](/atlas/). Tests enforce the ones a machine can check. + +1. **No branding as science.** This section is called Evidence, and the atlas is a design atlas. We do not write "scientifically proven", "studies show" or "research says": they tell you to stop checking. The banned phrases are listed in `site/src/data/evidence/references.ts` and a test fails the build if one appears. +2. **Every empirical claim is claim-level.** It sits next to its source and carries one of the labels above. A claim with no verified source is labelled *our judgement*, or it is cut. +3. **The scope travels with the claim.** Who was studied, on which task, in which context. "Brief exposures in a lab; it does not show a preference for any style." We never generalize beyond it. +4. **Standards are not evidence of preference.** WCAG and specifications say what is required. We cite them as standards. +5. **No causal or demographic claims about taste.** The audience listed for an atlas entry is a hypothesis about motivation, task and context. It is not measured and it is not a statement about age, gender or any group. +6. **Limits and a review date.** Each concept page ends with a Limits box: what we do not know, and when the page was last reviewed. +7. **Verified before published.** Each source was opened on the date recorded in the [references dataset](https://github.com/codeatlasdev/ui/blob/main/site/src/data/evidence/references.json), with the wording we relied on kept as an excerpt. A source whose text we could not read is marked *record only* and no claim may rest on it. Anything we cannot verify is excluded. +8. **No number we cannot trace.** A figure appears only with its study and its conditions. "50 ms" is a presentation time in particular lab studies, not "how long users take to decide". +9. **Tests.** A unit test fails if a banned phrase appears in site content, and another fails if a claim names a source that is missing or was not read. + + + The labels order how much to trust a claim, not how important it is. A *Standard or guideline* claim can be a hard requirement of your accessibility policy, while an *Established* one may not change your design at all. + + +## How to read the design atlas + +The [design atlas](/atlas/) is a broad, living catalogue of aesthetics, layouts, interaction patterns and design-system languages. It is not the list of every style that exists: no authority enumerates them, and new or local terms keep appearing. + +- **Terms belong to different classes.** A historical movement, an aesthetic, a design-system language, a layout, an interaction pattern and a UX principle are different kinds of thing. Categories and subcategories overlap, so do not read them as exclusive alternatives. +- **The sources support the principle, not the style.** The references behind an entry support underlying principles: visual complexity, aggregate visual preference, legibility, accessibility, grouping, platform conventions, technologies under study. They do not show that a particular aesthetic works. +- **Scores are heuristic.** The 1 to 5 scales for complexity, expressiveness and density are relative editorial judgements. They are not results from a study and not a measure of quality. +- **Audience is a hypothesis.** It describes the motivation, task, context and likely repertoire for which a style tends to be chosen. No audience in the atlas is shown to prefer its style. +- **The trend is an editorial projection.** The 2027 to 2030 column is a judgement, not a forecast. +- **How standard is the term.** Swiss Style or Bauhaus are well documented; "post-brutalism" or "hairline UI" are informal labels. The atlas states which. + +## How to validate before you commit + +An atlas entry or a concept page tells you what to consider. Your users decide. + +1. Pick styles whose task and motivation match your product. +2. Build three prototypes with the same content and behavior: a functional baseline, an expressive variation and a hybrid. +3. Test with a representative sample, including frequent and occasional users and people with accessibility needs. +4. Measure task success, time, errors, calibrated confidence, recall, satisfaction and preference, and keep preference apart from performance. +5. For agents and adaptive interfaces, add auditability, reversibility, unwanted surprise and unexpected state change. +6. Do not decide on a preference vote alone: check the real flow, the devices and the critical scenarios. + + +Appeal can mask problems. Users are more tolerant of minor usability issues in an interface they find visually appealing, so a preference result is not a usability result. + + + +- Most studies behind the sources are laboratory studies with screenshots or short tasks. They show that a factor matters; they do not show how much it matters in your product. +- Several sources are preprints, vendor reports or practitioner articles. They are labelled as such and none is labelled stronger than its kind. +- For some sources we read the abstract or summary, not the full text (a paywalled article, or the preprint of a published survey). The record says which, in its `how` field, and no claim goes beyond the wording we read. +- A checked source can change after the date we checked it. If a page no longer says what we quote, open an issue. +- This section covers interface design for screens. It says little about voice, spatial or physical interfaces. + + + diff --git a/site/src/content/docs/concepts/index.mdx b/site/src/content/docs/concepts/index.mdx new file mode 100644 index 00000000..62c8df85 --- /dev/null +++ b/site/src/content/docs/concepts/index.mdx @@ -0,0 +1,37 @@ +--- +title: Concepts +description: "Why interfaces are built the way they are: each concept follows one template, labels every claim about users with its source and strength, and links to the components that implement it." +group: Concepts +order: 0 +--- +import { Callout, CardGrid, LinkCard } from "@codeatlasdev/ui"; + +The component pages tell you what to call. The concept pages tell you what to build and why, so a person or an AI agent can make the call that no prop table answers: how tall a first screen should be, where an action bar goes when the keyboard is open, when to hide detail. + + + The five strength labels, the nine rules and how to validate before you commit. Read this first. + + +## How a concept page is built + +Every concept uses the same nine parts in the same order. + +| Part | What it holds | +|---|---| +| Definition and vocabulary | The terms, and what each means and does not mean. | +| Evidence | Claims about users, each with a source, a [strength label](/docs/concepts/evidence/#the-five-strength-labels) and its scope. | +| The contract | The principles we derive. Kept apart from the evidence so a finding is never confused with a rule of thumb. | +| How the library implements it | Tokens, components and live demos. | +| Anti-patterns | What goes wrong, and the alternative. | +| Checklist | Things you can verify. | +| Prompt for your AI | A paste-in instruction that carries the contract into a coding agent. | +| Limits | What we do not know and the date of the last review. | +| References | Every source cited, with what it supports and what it does not. | + +## Writing a concept + +Add `site/src/content/docs/concepts/.mdx` with `group: Concepts`. Wrap each claim about users in ``, close with `` and ``, and add any new source to `site/src/data/evidence/references.json` only after opening it and recording the wording you relied on. The rules are on [How we use evidence](/docs/concepts/evidence/); `bun test packages/ui/tests/evidence-rules.test.ts` checks them. + + + Each page is also plain markdown at `/llms/concepts/.txt`, with its claims, scope, limits and references intact. The `ca-ui-mcp` server returns it with `get_guide`. + diff --git a/site/src/data/evidence/references.json b/site/src/data/evidence/references.json new file mode 100644 index 00000000..1db30e0c --- /dev/null +++ b/site/src/data/evidence/references.json @@ -0,0 +1,860 @@ +{ + "$comment": "Primary sources cited by the Concepts and the design atlas. Every entry was opened on `verified.date`; `verified.excerpt` is wording read at that source (abstract, summary or page text), `verified.level` says how much was read. Rules: site/src/content/docs/concepts/evidence.mdx. Ids E01-E22 keep the numbering of the owner's source list; EXX = added after.", + "references": [ + { + "id": "E01", + "authors": "Tuch, Presslaber, Stöcklin, Opwis, Bargas-Avila", + "year": 2012, + "title": "The role of visual complexity and prototypicality regarding first impression of websites: Working towards understanding aesthetic judgments", + "venue": "International Journal of Human-Computer Studies", + "kind": "peer-reviewed", + "url": "https://doi.org/10.1016/j.ijhcs.2012.06.003", + "topic": "Visual complexity and prototypicality in first impressions", + "supports": "Two lab experiments with screenshots of real websites (119 in the first) found that visual complexity and prototypicality affected aesthetic ratings at presentation times of 50 ms and, in the second study, down to 17 ms.", + "doesNotSupport": "That people understand a value proposition in that time, or that any named style is preferred. The measure is a rating of aesthetics, not comprehension or task success.", + "strength": "supported", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Abstract on the publisher-listed Google Research record, reached from the DOI", + "excerpt": "119 screenshots of real websites varying in VC (low vs. medium vs. high) and PT (low vs. high) were rated on perceived aesthetics. Screenshot presentation time was varied as a between-subject factor (50 ms vs. 500 ms vs. 1000 ms). Results reveal that VC and PT affect participants' aesthetics ratings within the first 50 ms of exposure. [...] In the second study presentation times were shortened to 17, 33 and 50ms. Results suggest that VC and PT affect aesthetic perception even within 17ms, though the effect of PT is less pronounced than the one of VC." + } + }, + { + "id": "E02", + "authors": "Lavie, Tractinsky", + "year": 2004, + "title": "Assessing dimensions of perceived visual aesthetics of web sites", + "venue": "International Journal of Human-Computer Studies", + "kind": "peer-reviewed", + "url": "https://doi.org/10.1016/j.ijhcs.2003.09.002", + "topic": "Dimensions of perceived web aesthetics", + "supports": "Four studies developed a measurement instrument for perceived web site aesthetics. Exploratory and confirmatory factor analyses found two main dimensions of users' perceptions: classical aesthetics (orderly, clear design, closely related to many usability design rules) and expressive aesthetics (designers' creativity and originality, breaking conventions). They are clearly distinguishable, each measured by a five-item scale.", + "doesNotSupport": "That either dimension is preferred, or that a named style is classical or expressive. It measures how people perceive aesthetics, in 2003-era web sites, not task success or the effect of any style.", + "strength": "supported", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Abstract on the first page of the author-hosted copy of the published article (Ben-Gurion University), reached from the DOI record", + "excerpt": "users' perceptions consist of two main dimensions, which we termed \"classical aesthetics\" and \"expressive aesthetics\". The classical aesthetics dimension pertains to aesthetic notions that presided from antiquity until the 18th century. These notions emphasize orderly and clear design and are closely related to many of the design rules advocated by usability experts. The expressive aesthetics dimension is manifested by the designers' creativity and originality and by the ability to break design conventions." + } + }, + { + "id": "E03", + "authors": "Reinecke, Gajos", + "year": 2014, + "title": "Quantifying visual preferences around the world", + "venue": "Proceedings of the SIGCHI Conference on Human Factors in Computing Systems (CHI '14), pp. 11-20", + "kind": "peer-reviewed", + "url": "https://doi.org/10.1145/2556288.2557052", + "topic": "Visual preferences across groups", + "supports": "About 2.4 million ratings of the visual appeal of websites from nearly 40 thousand participants: the level of colorfulness and visual complexity at which appeal is highest varies between groups.", + "doesNotSupport": "Predicting one person's taste from a group label. Group differences in ratings of website screenshots do not justify stereotypes or per-style audience claims.", + "strength": "supported", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Abstract from the OpenAlex record of the DOI", + "excerpt": "we collected 2.4 million ratings of the visual appeal of websites from nearly 40 thousand participants of diverse backgrounds. [...] the level of colorfulness and visual complexity at which visual appeal is highest strongly varies" + } + }, + { + "id": "E04", + "authors": "Hassenzahl", + "year": 2004, + "title": "The Interplay of Beauty, Goodness, and Usability in Interactive Products", + "venue": "Human-Computer Interaction, 19(4)", + "kind": "peer-reviewed", + "url": "https://doi.org/10.1207/s15327051hci1904_2", + "topic": "Pragmatic and hedonic qualities", + "supports": "Two studies with four MP3-player skins: goodness depended on perceived usability and hedonic attributes, while beauty largely depended on identification, a hedonic attribute. Beauty and goodness were related but not the same.", + "doesNotSupport": "Results for other products, tasks or populations. The products were four skins of one kind of player.", + "strength": "supported", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Abstract from the OpenAlex record of the DOI", + "excerpt": "Two studies considered the interplay between user-perceived usability (i.e., pragmatic attributes), hedonic attributes (e.g., stimulation, identification), goodness (i.e., satisfaction), and beauty of 4 different MP3-player skins." + } + }, + { + "id": "E05", + "authors": "Lindgaard, Fernandes, Dudek, Brown", + "year": 2006, + "title": "Attention web designers: You have 50 milliseconds to make a good first impression!", + "venue": "Behaviour & Information Technology", + "kind": "peer-reviewed", + "url": "https://doi.org/10.1080/01449290500330448", + "topic": "Speed of visual-appeal judgments", + "supports": "In three studies, ratings of the visual appeal of web homepages were highly correlated between exposures of 50 ms and 500 ms.", + "doesNotSupport": "That people decide whether to stay, trust or buy in 50 ms. The measure is a rating of visual appeal for a set of homepages in a lab, not behavior on a live site.", + "strength": "supported", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Abstract from the OpenAlex record of the DOI", + "excerpt": "visual appeal ratings were highly correlated from one phase to the next as were the correlations between the 50 ms and 500 ms conditions." + } + }, + { + "id": "E06", + "authors": "Wagemans, Elder, Kubovy, Palmer, Peterson, Singh, von der Heydt", + "year": 2012, + "title": "A century of Gestalt psychology in visual perception: I. Perceptual grouping and figure-ground organization", + "venue": "Psychological Bulletin, 138(6)", + "kind": "review", + "url": "https://doi.org/10.1037/a0029333", + "topic": "Perceptual grouping and figure-ground", + "supports": "A review of the grouping principles (proximity, similarity, common fate and others) and of figure-ground organization in contemporary perception research.", + "doesNotSupport": "Any claim about interface styles. It is a review of perception, not of screens, and says nothing about preference.", + "strength": "established", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Abstract from the OpenAlex record of the DOI", + "excerpt": "we review the principles of grouping, both classical (e.g., proximity, similarity, common fate, good con[tinuation]) [...] and figure-ground organization." + } + }, + { + "id": "E07", + "authors": "W3C Web Accessibility Initiative", + "year": 2023, + "title": "WCAG 2.2: How to Meet WCAG (Quick Reference)", + "venue": "W3C Recommendation", + "kind": "standard", + "url": "https://www.w3.org/WAI/WCAG22/quickref/", + "topic": "Accessibility requirements", + "supports": "What conformant content must do, as testable success criteria with a level (A, AA, AAA): for example 1.4.10 Reflow (no two-dimensional scrolling at a width equivalent to 320 CSS pixels, except content that needs two-dimensional layout), 2.4.11 Focus Not Obscured (Minimum) and 2.5.8 Target Size (Minimum) (at least 24 by 24 CSS pixels, with listed exceptions).", + "doesNotSupport": "What users prefer or what performs better. A standard says what is required, not what is popular.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Full page of the W3C quick reference, read for the criteria the library and the concept pages rely on", + "excerpt": "1.4.10 Reflow (Level AA): Content can be presented without loss of information or functionality, and without requiring scrolling in two dimensions for: Vertical scrolling content at a width equivalent to 320 CSS pixels. 2.5.8 Target Size (Minimum) (Level AA): The size of the target for pointer inputs is at least 24 by 24 CSS pixels, except when: Spacing, Equivalent, Inline, User Agent Control, Essential. 2.4.11 Focus Not Obscured (Minimum) (Level AA): When a user interface component receives keyboard focus, the component is not entirely hidden due to author-created content." + } + }, + { + "id": "E08", + "authors": "W3C Web Accessibility Initiative", + "year": 2021, + "title": "WCAG 2 supplemental guidance", + "venue": "W3C WAI", + "kind": "guideline", + "url": "https://www.w3.org/WAI/WCAG2/supplemental/", + "topic": "Cognitive accessibility guidance", + "supports": "W3C WAI lists supplemental guidance for improving accessibility beyond what WCAG 2 requires, including a cognitive accessibility guidance set.", + "doesNotSupport": "Normative requirements: supplemental guidance is advisory, not required for conformance. This page is an index; a specific recommendation needs the linked document.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "This pages lists supplemental guidance for improving accessibility beyond what is required by Web Content Accessibility Guidelines WCAG 2. Cognitive Accessibility Guidance" + } + }, + { + "id": "E09", + "authors": "W3C Web Accessibility Initiative", + "year": 2023, + "title": "Understanding Success Criterion 2.3.3: Animation from Interactions", + "venue": "W3C WAI, WCAG 2.2 Understanding (Level AAA)", + "kind": "standard", + "url": "https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions", + "topic": "Motion and vestibular disorders", + "supports": "Non-essential motion triggered by interaction, such as parallax scrolling, can trigger vestibular disorders (dizziness, nausea, headaches); content should let users turn it off. The criterion is Level AAA.", + "doesNotSupport": "A prevalence figure, or that all animation is harmful. Essential motion is excepted, and the criterion is above the AA level most programs target.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text of the W3C Understanding document", + "excerpt": "Support user preferences for motion, and eliminate unnecessary motion effects. [...] it can trigger vestibular disorders. Vestibular (inner ear) disorder reactions include dizziness, nausea and headaches." + } + }, + { + "id": "E10", + "authors": "Elliot, Maier", + "year": 2014, + "title": "Color Psychology: Effects of Perceiving Color on Psychological Functioning in Humans", + "venue": "Annual Review of Psychology, 65", + "kind": "review", + "url": "https://doi.org/10.1146/annurev-psych-010213-115035", + "topic": "Color and psychological functioning", + "supports": "A review that sets the history and methodological problems of earlier color research and covers newer work on color in achievement and affiliation contexts, consumer behavior and food.", + "doesNotSupport": "A fixed color-to-emotion mapping. The review stresses methodological limits; do not read it as 'blue means trust'.", + "strength": "established", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Abstract from the OpenAlex record of the DOI", + "excerpt": "we review theoretical and empirical work that looks beyond color aesthetics to the link between color and psychological functioning in humans. We begin by setting a historical context for research in this area, particularly highlighting methodological issues that hampered earlier empirical work." + } + }, + { + "id": "E11", + "authors": "Google Design", + "year": 2025, + "title": "Expressive Design: Google's UX Research", + "venue": "design.google", + "kind": "vendor-research", + "url": "https://design.google/library/expressive-material-design-google-research", + "topic": "Material 3 Expressive", + "supports": "The vendor reports 46 research studies and more than 18,000 participants behind Material 3 Expressive.", + "doesNotSupport": "Independent replication. The studies are the vendor's, the data are not published on this page, and the result concerns their own design system.", + "strength": "preliminary", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "Through 46 separate research studies with hundreds of designs, and more than 18,000 participants from around the world" + } + }, + { + "id": "E12", + "authors": "Apple", + "year": 2025, + "title": "Materials, Human Interface Guidelines", + "venue": "Apple Developer Documentation", + "kind": "guideline", + "url": "https://developer.apple.com/design/human-interface-guidelines/materials", + "topic": "Liquid Glass and materials", + "supports": "Platform guidance: Liquid Glass forms a distinct functional layer for controls and navigation that floats above the content layer, establishing a clear visual hierarchy between functional elements and content.", + "doesNotSupport": "Evidence of better outcomes for users. It is the platform owner's design guidance, not a study.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page content from Apple's documentation data endpoint for this page", + "excerpt": "Liquid Glass forms a distinct functional layer for controls and navigation elements, like tab bars and sidebars, that floats above the content layer, establishing a clear visual hierarchy between functional elements and content." + } + }, + { + "id": "E13", + "authors": "Apple", + "year": 2025, + "title": "Spatial layout, Human Interface Guidelines", + "venue": "Apple Developer Documentation", + "kind": "guideline", + "url": "https://developer.apple.com/design/human-interface-guidelines/spatial-layout/", + "topic": "Layout for spatial computing", + "supports": "Platform guidance for Apple Vision Pro: field of view is the space a person can see without moving their head, and depth is conveyed with cues such as distance, occlusion and shadow.", + "doesNotSupport": "Layout for flat screens, or evidence of outcomes. It is device-specific platform guidance.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page content from Apple's documentation data endpoint for this page", + "excerpt": "A person's field of view is the space they can see without moving their head. [...] People rely on visual cues like distance, occlusion, and shadow to perceive depth" + } + }, + { + "id": "E14", + "authors": "Baymard Institute", + "year": 2025, + "title": "Product List UX 2025: 8 Common Pitfalls & Best Practices", + "venue": "Baymard Institute research article (updated Sep 9, 2025)", + "kind": "applied-research", + "url": "https://baymard.com/research-articles/current-state-product-list-and-filtering", + "topic": "Product lists and filters in e-commerce", + "supports": "From Baymard's own benchmark and usability testing of e-commerce sites: two-thirds of the sites they scored perform 'mediocre' or worse on product-list usability, and some test participants overlooked an 'All Filters' button.", + "doesNotSupport": "Anything outside e-commerce product lists, or any visual style. The sample is Baymard's chosen sites and the scoring is theirs.", + "strength": "preliminary", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "two-thirds of ecommerce sites perform \"mediocre\" or worse when it comes to product list usability." + } + }, + { + "id": "E15", + "authors": "Baymard Institute", + "year": 2025, + "title": "Checkout UX Best Practices 2025", + "venue": "Baymard Institute research article", + "kind": "applied-research", + "url": "https://baymard.com/research-articles/current-state-of-checkout-ux", + "topic": "Checkout flows in e-commerce", + "supports": "From Baymard's benchmark and 16 years of checkout usability testing: about 64% of desktop sites score 'mediocre' or worse on checkout UX, and they report checkout design as frequently the sole cause of cart abandonment in their tests.", + "doesNotSupport": "Abandonment rates for any particular site, or conclusions outside e-commerce checkout. The scoring and sample are Baymard's.", + "strength": "preliminary", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "64% of Desktop sites have a \"mediocre\" or worse Checkout UX performance" + } + }, + { + "id": "E16", + "authors": "Aljabri, Hijazi, Al-Salamin, Khemaja", + "year": 2026, + "title": "AI-MDD Approaches for Generating Adaptive and Intelligent UIs: A Systematic Literature Review", + "venue": "Journal of Advanced Computational Intelligence and Intelligent Informatics, 30", + "kind": "peer-reviewed", + "url": "https://doi.org/10.20965/jaciii.2026.p1620", + "topic": "Adaptive UI generated with AI and model-driven development", + "supports": "A bibliometric analysis and systematic review that narrowed 3,335 Scopus records to 51 studies and reports rapid growth of research on AI and model-driven generation of adaptive UIs.", + "doesNotSupport": "That adaptive generated UIs perform better for users. It maps the literature; the abstract read here does not report user outcomes.", + "strength": "supported", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Abstract from the OpenAlex record of the DOI", + "excerpt": "Based on an initial set of 3,335 records retrieved from Scopus, a structured filtering process following PRISMA guidelines resulted in 51 relevant studies for in-depth analysis." + } + }, + { + "id": "E17", + "authors": "Lee", + "year": 2025, + "title": "Towards a Working Definition of Designing Generative User Interfaces", + "venue": "arXiv preprint 2505.15049", + "kind": "preprint", + "url": "https://arxiv.org/abs/2505.15049", + "topic": "Definition of generative UI", + "supports": "A multi-method qualitative study (systematic review of 127 publications, 18 expert interviews, 12 case studies) that proposes a working definition of generative UI as an iterative, co-creative process.", + "doesNotSupport": "Effectiveness of generative UI. It is a definitional, qualitative study and a preprint.", + "strength": "preliminary", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Abstract on arXiv", + "excerpt": "a systematic literature review of 127 publications, expert interviews with 18 participants, and analyses of 12 case studies" + } + }, + { + "id": "E18", + "authors": "Luera, Rossi, Siu, Dernoncourt, Yu, Kim, Zhang, Chen, Salehy, Lipka, Basu, Mathur, Zhao", + "year": 2025, + "title": "Survey on User Interface Design and Interactions for Generative AI Applications", + "venue": "Foundations and Trends in Human-Computer Interaction", + "kind": "peer-reviewed", + "url": "https://doi.org/10.1561/1100000106", + "topic": "Interaction patterns for generative AI", + "supports": "A survey that presents taxonomies of how a human interacts with AI and of the user-interaction patterns designed for a variety of generative AI use cases, as a reference for designers and developers. It focuses primarily on user-guided interactions, those the user initiates.", + "doesNotSupport": "That any pattern works better, or that a generative interface should act without the user's direction: the survey leaves out implicit signals from the user and reports no outcome comparison. The abstract read is the arXiv version of the same survey (earlier title); the published monograph's own text was not reachable.", + "strength": "supported", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Abstract of the arXiv version (2410.22370, 'Survey of User Interface Design and Interaction Techniques in Generative AI Applications') of the survey published as the monograph; the published page is paywalled", + "excerpt": "we present a survey that comprehensively presents taxonomies of how a human interacts with AI and the user interaction patterns designed to meet the needs of a variety of relevant use cases. We focus primarily on user-guided interactions, surveying interactions that are initiated by the user and do not include any implicit signals given by the user." + } + }, + { + "id": "E19", + "authors": "Nielsen Norman Group", + "year": 2026, + "title": "The Aesthetic-Usability Effect", + "venue": "NN/g article (revised 2026)", + "kind": "applied-research", + "url": "https://www.nngroup.com/articles/aesthetic-usability-effect/", + "topic": "Appeal and perceived usability", + "supports": "Users are more tolerant of minor usability issues in an interface they find visually appealing, and this can mask problems during usability testing; illustrated with a single test participant on one e-commerce site.", + "doesNotSupport": "An effect size, or that appeal improves task performance. The article's example is one participant; it is a practitioner synthesis, not a controlled study.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "Users are more tolerant of minor usability issues when they find an interface visually appealing, and this effect can mask UI problems during usability testing." + } + }, + { + "id": "E20", + "authors": "Google Research", + "year": 2026, + "title": "Self-Evolving Systems: Moving Beyond Deterministic Interfaces to Adaptive Generative Interfaces", + "venue": "Google Research publication", + "kind": "vendor-research", + "url": "https://research.google/pubs/self-evolving-systems-moving-beyond-deterministic-interfaces-to-adaptive-generative-interfaces/", + "topic": "Adaptive generative interfaces", + "supports": "Introduces the concept of adaptive generative interfaces that expand their own feature sets at runtime through directed synthesis, inferred synthesis and real-time adaptation.", + "doesNotSupport": "That such interfaces are usable or safe. The page presents a concept, not user outcomes.", + "strength": "preliminary", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Abstract on the publication page", + "excerpt": "We introduce the concept of Adaptive generative interfaces that move beyond static artifacts to autonomously expand their own feature sets at runtime." + } + }, + { + "id": "E21", + "authors": "Romero, Wiese, Balancieiri and others", + "year": 2026, + "title": "Usable but Conventional: An Empirical Study on the UX of AI-Generated Interface Prototypes", + "venue": "arXiv preprint 2605.15124", + "kind": "preprint", + "url": "https://arxiv.org/abs/2605.15124", + "topic": "UX of AI-generated prototypes", + "supports": "A survey of 92 participants who rated AI-generated and human-created prototypes without knowing authorship, measured with the UEQ-S: positive on pragmatic aspects, neutral or negative on hedonic aspects such as originality.", + "doesNotSupport": "A general verdict on AI-generated interfaces: one survey, particular prototypes and tools, and a preprint.", + "strength": "preliminary", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Abstract on arXiv", + "excerpt": "An empirical survey with 92 participants evaluated AI-generated and human-created prototypes without prior identification of authorship. [...] positive evaluations in pragmatic aspects [...] and neutral or negative evaluations in hedonic aspects, including originality and innovation." + } + }, + { + "id": "E22", + "authors": "Chen, Zhang, Zhang and others", + "year": 2025, + "title": "Generative Interfaces for Language Models", + "venue": "arXiv preprint 2508.19227", + "kind": "preprint", + "url": "https://arxiv.org/abs/2508.19227", + "topic": "LLM-generated user interfaces", + "supports": "Proposes a paradigm in which language models respond to queries by generating task-specific user interfaces, with a framework of structured interface representations and iterative refinement, and reports an evaluation.", + "doesNotSupport": "That generated interfaces beat conversation in general. The abstract read here is a proposal and a preprint; its evaluation was not examined.", + "strength": "preliminary", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Abstract on arXiv", + "excerpt": "we propose Generative Interfaces for Language Models, a paradigm in which LLMs respond to user queries by proactively generating user interfaces (UIs) that enable more adaptive and interactive engagement." + } + }, + { + "id": "N01", + "authors": "Fessenden", + "year": 2018, + "title": "Scrolling and Attention", + "venue": "Nielsen Norman Group, April 15, 2018", + "kind": "applied-research", + "url": "https://www.nngroup.com/articles/scrolling-and-attention/", + "topic": "Attention above and below the fold", + "supports": "In NN/g's eyetracking study, users spent about 57% of page-viewing time above the fold and 74% in the first two screenfuls; the second screenful got about 17%.", + "doesNotSupport": "A universal split. The figures are from one study on its own pages, sample and devices; the article itself notes the analysis disregards page length.", + "strength": "preliminary", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "In our most recent study, users spent about 57% of their page-viewing time above the fold. 74% of the viewing time was spent in the first two screenfuls" + } + }, + { + "id": "N02", + "authors": "Flaherty", + "year": 2016, + "title": "The Illusion of Completeness: What It Is and How to Avoid It", + "venue": "Nielsen Norman Group, January 17, 2016", + "kind": "applied-research", + "url": "https://www.nngroup.com/articles/illusion-of-completeness/", + "topic": "False floor and scroll discovery", + "supports": "Users can think they see the entire page although more content exists off-screen. In one NN/g usability study, six out of eight users did not realize they could scroll a particular page.", + "doesNotSupport": "A rate for any other site. It is one qualitative study of eight users on one page.", + "strength": "preliminary", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "Six out of eight users did not realize they could scroll down this page." + } + }, + { + "id": "N03", + "authors": "Paul", + "year": 2023, + "title": "Scroll Fading 101", + "venue": "Nielsen Norman Group, December 8, 2023", + "kind": "applied-research", + "url": "https://www.nngroup.com/articles/scroll-fading-101/", + "topic": "Elements that appear on scroll", + "supports": "Whether scroll fading helps or distracts depends on its persistence, responsiveness and how sparingly it is applied.", + "doesNotSupport": "A ban on the pattern. NN/g reports it can also improve perception and loading when used well.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page summary", + "excerpt": "Whether scroll fading is more distracting than usable depends on the following factors: its persistence, responsiveness, and how sparingly it is applied to elements on the page." + } + }, + { + "id": "N04", + "authors": "Nielsen", + "year": 2006, + "title": "Progressive Disclosure", + "venue": "Nielsen Norman Group, December 3, 2006", + "kind": "applied-research", + "url": "https://www.nngroup.com/articles/progressive-disclosure/", + "topic": "Deferring advanced features", + "supports": "Progressive disclosure defers advanced or rarely used features to a secondary screen, which Nielsen argues makes applications easier to learn and less error-prone.", + "doesNotSupport": "That hiding things always helps. It is a practitioner article from 2006; it does not license hiding prices, limits or frequent actions.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page summary", + "excerpt": "Progressive disclosure defers advanced or rarely used features to a secondary screen, making applications easier to learn and less error-prone." + } + }, + { + "id": "N05", + "authors": "Paul", + "year": 2023, + "title": "Scrolljacking 101", + "venue": "Nielsen Norman Group, August 6, 2023", + "kind": "applied-research", + "url": "https://www.nngroup.com/articles/scrolljacking-101/", + "topic": "Altered scroll behavior", + "supports": "NN/g reports that altering the normal pace or direction of scrolling can contradict user expectations, control and freedom, and generate disorientation.", + "doesNotSupport": "A measured rate of disorientation. It is a practitioner analysis with examples.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "Altering the normal pace or direction of scrolling can contradict user expectations, control, and freedom." + } + }, + { + "id": "N06", + "authors": "Nielsen Norman Group", + "year": 2023, + "title": "The Negative Impact of Mobile-First Web Design on Desktop (content dispersion)", + "venue": "Nielsen Norman Group, October 13, 2023", + "kind": "applied-research", + "url": "https://www.nngroup.com/articles/content-dispersion/", + "topic": "Content dispersion on desktop", + "supports": "NN/g reports that mobile-first designs viewed on desktop can disperse content across long pages with expansive white space and enlarged images and fonts, making it harder to consume and understand.", + "doesNotSupport": "That white space is bad in general. The article also lists positive effects of dispersed content; the finding is about mobile-first layouts on desktop.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page summary", + "excerpt": "Content becomes overly dispersed across long scrolling pages with expansive white space and enlarged images and fonts, making it difficult for users to consume and understand the information." + } + }, + { + "id": "N07", + "authors": "Budiu", + "year": 2015, + "title": "Progress in Mobile User Experience", + "venue": "Nielsen Norman Group, March 22, 2015", + "kind": "applied-research", + "url": "https://www.nngroup.com/articles/mobile-usability-update/", + "topic": "State of mobile usability in 2015", + "supports": "NN/g's 2015 research reported an improvement in mobile UX as responsive design led sites to prioritize content over UI elements.", + "doesNotSupport": "Current mobile problems. The article is from 2015 and reports improvement; it does not document the problems of orientation or page length on its own.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page summary", + "excerpt": "Our newest research on mobile usability shows an improvement in mobile UX." + } + }, + { + "id": "W01", + "authors": "W3C WAI", + "year": 2023, + "title": "Understanding SC 1.4.10: Reflow", + "venue": "W3C WAI, WCAG 2.2 Understanding (Level AA)", + "kind": "standard", + "url": "https://www.w3.org/WAI/WCAG22/Understanding/reflow.html", + "topic": "Reflow at 320 CSS pixels", + "supports": "Content must be presentable without loss of information or functionality and without scrolling in two dimensions for vertical-scrolling content at a width equivalent to 320 CSS pixels, with exceptions for content that needs two-dimensional layout.", + "doesNotSupport": "A list of device models or breakpoints. The criterion is a width equivalent, not a phone.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "without requiring scrolling in two dimensions for: Vertical scrolling content at a width equivalent to 320 CSS pixels" + } + }, + { + "id": "W02", + "authors": "W3C WAI", + "year": 2023, + "title": "Understanding SC 1.4.4: Resize Text", + "venue": "W3C WAI, WCAG 2.2 Understanding (Level AA)", + "kind": "standard", + "url": "https://www.w3.org/WAI/WCAG22/Understanding/resize-text.html", + "topic": "Text resized to 200 percent", + "supports": "Text can be resized without assistive technology up to 200 percent without loss of content or functionality.", + "doesNotSupport": "Behavior of page zoom versus text-only zoom in a given browser; check the criterion text for what counts.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "text can be resized without assistive technology up to 200 percent without loss of content or functionality." + } + }, + { + "id": "W03", + "authors": "W3C WAI", + "year": 2023, + "title": "Understanding SC 1.4.12: Text Spacing", + "venue": "W3C WAI, WCAG 2.2 Understanding (Level AA)", + "kind": "standard", + "url": "https://www.w3.org/WAI/WCAG22/Understanding/text-spacing.html", + "topic": "Text spacing overrides", + "supports": "No loss of content or functionality occurs when a user sets line height to at least 1.5 times the font size, spacing after paragraphs to 2 times, letter spacing to 0.12 times and word spacing to 0.16 times, changing no other style property.", + "doesNotSupport": "A requirement that the page itself uses those values; it must tolerate them when the user applies them. Languages and scripts that do not use a property are excepted.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "Line height (line spacing) to at least 1.5 times the font size; Spacing following paragraphs to at least 2 times the font size; Letter spacing (tracking) to at least 0.12 times the font size; Word spacing to at least 0.16 times the font size." + } + }, + { + "id": "W04", + "authors": "W3C WAI", + "year": 2023, + "title": "Understanding SC 1.4.11: Non-text Contrast", + "venue": "W3C WAI, WCAG 2.2 Understanding (Level AA)", + "kind": "standard", + "url": "https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html", + "topic": "Contrast of controls and graphics", + "supports": "Meaningful visual cues must achieve 3:1 against the background. The document notes authors do not control font smoothing and anti-aliasing, so evaluation uses the colors obtained from the user agent or the markup.", + "doesNotSupport": "That thin lines are always inadequate. The document does not set a minimum line width.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "Ensure meaningful visual cues achieve 3:1 against the background." + } + }, + { + "id": "W05", + "authors": "W3C WAI", + "year": 2023, + "title": "Understanding SC 2.4.11: Focus Not Obscured (Minimum)", + "venue": "W3C WAI, WCAG 2.2 Understanding (Level AA)", + "kind": "standard", + "url": "https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum", + "topic": "Focused elements hidden by sticky content", + "supports": "When a user interface component receives keyboard focus, it is not entirely hidden due to author-created content.", + "doesNotSupport": "That partly hidden focus is fine: the minimum level only forbids it being entirely hidden.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "the component is not entirely hidden due to author-created content." + } + }, + { + "id": "W06", + "authors": "W3C WAI", + "year": 2023, + "title": "Understanding SC 2.5.8: Target Size (Minimum)", + "venue": "W3C WAI, WCAG 2.2 Understanding (Level AA)", + "kind": "standard", + "url": "https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum", + "topic": "Pointer target size", + "supports": "The target for pointer inputs is at least 24 by 24 CSS pixels, except where spacing, equivalence, inline, user-agent or essential exceptions apply.", + "doesNotSupport": "That 44 px is required. 44 px is a larger recommendation, not this criterion.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "The size of the target for pointer inputs is at least 24 by 24 CSS pixels, except when:" + } + }, + { + "id": "W07", + "authors": "W3C WAI", + "year": 2023, + "title": "Understanding SC 1.3.4: Orientation", + "venue": "W3C WAI, WCAG 2.2 Understanding (Level AA)", + "kind": "standard", + "url": "https://www.w3.org/WAI/WCAG22/Understanding/orientation.html", + "topic": "Not locking portrait or landscape", + "supports": "Content must not restrict its view to a single orientation unless a specific orientation is essential; people may have devices mounted in a fixed orientation.", + "doesNotSupport": "A requirement to design every screen for both orientations when one is essential (for example a piano keyboard).", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "Don't lock content to either portrait or landscape presentation." + } + }, + { + "id": "W08", + "authors": "W3C WAI-ARIA", + "year": 2024, + "title": "Dialog (Modal) Pattern, ARIA Authoring Practices Guide", + "venue": "W3C WAI-ARIA APG", + "kind": "guideline", + "url": "https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/", + "topic": "Keyboard and focus in modal dialogs", + "supports": "In a modal dialog focus moves into the dialog on open, Tab and Shift+Tab stay inside, and Escape closes it.", + "doesNotSupport": "A requirement: the APG documents recommended patterns, not conformance criteria.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "Escape: Closes the dialog. [...] Tab and Shift + Tab do not move focus outside the dialog." + } + }, + { + "id": "M01", + "authors": "MDN contributors", + "year": 2026, + "title": "CSS values and units: length (viewport units)", + "venue": "MDN Web Docs", + "kind": "guideline", + "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length", + "topic": "svh, lvh and dvh", + "supports": "The viewport-percentage units for small, large and dynamic viewport sizes are svh, lvh and dvh; vh is equivalent to lvh. Using the large size can obscure content while the browser interface is expanded.", + "doesNotSupport": "Support or rendering in a given browser version; check compatibility data before relying on it.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "The viewport-percentage units for small, large, and dynamic viewport sizes are svh, lvh, and dvh. vh is equivalent to lvh" + } + }, + { + "id": "M02", + "authors": "MDN contributors", + "year": 2026, + "title": "VisualViewport API", + "venue": "MDN Web Docs", + "kind": "guideline", + "url": "https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport", + "topic": "Visual viewport versus layout viewport", + "supports": "The VisualViewport interface represents the visual viewport of a window and exposes its offset from the layout viewport, among other properties.", + "doesNotSupport": "That scripts should replace CSS. The page describes the API, not when to use it.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "The VisualViewport interface of the CSSOM view API represents the visual viewport for a given window." + } + }, + { + "id": "M03", + "authors": "MDN contributors", + "year": 2026, + "title": "env() CSS function", + "venue": "MDN Web Docs", + "kind": "guideline", + "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/env", + "topic": "Safe-area insets and viewport segments", + "supports": "env() reads user-agent environment variables including safe-area-inset-* and viewport-segment-* with an optional fallback.", + "doesNotSupport": "Non-zero insets by default: insets are only non-zero when the page opts into edge-to-edge drawing (viewport-fit=cover).", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "env(safe-area-inset-top); [...] env(viewport-segment-right 0 0); /* With a fallback value */ env(safe-area-inset-right, 1em);" + } + }, + { + "id": "M04", + "authors": "MDN contributors", + "year": 2026, + "title": "CSS container queries", + "venue": "MDN Web Docs", + "kind": "guideline", + "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_queries", + "topic": "Components that respond to their container", + "supports": "Container queries apply styles to an element based on attributes of its container (its name, its size, its styles or scroll-state), and are an alternative to media queries, which key on viewport size or other device characteristics.", + "doesNotSupport": "Which container sizes to choose, or browser support for the non-size query types; those are design and compatibility decisions.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "Container queries enable you to apply styles to an element based on certain attributes of its container: The container-name. The container's size. [...] Container queries are an alternative to media queries, which apply styles to elements based on viewport size or other device characteristics." + } + }, + { + "id": "M05", + "authors": "MDN contributors", + "year": 2026, + "title": "VirtualKeyboard API", + "venue": "MDN Web Docs", + "kind": "guideline", + "url": "https://developer.mozilla.org/en-US/docs/Web/API/VirtualKeyboard_API", + "topic": "Controlling the on-screen keyboard from script", + "supports": "The API is marked limited availability (not Baseline). Setting navigator.virtualKeyboard.overlaysContent to true stops the browser resizing the viewport for the keyboard so it overlays content.", + "doesNotSupport": "Cross-browser availability. MDN marks it as not working in some of the most widely used browsers.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "Limited availability. This feature is not Baseline because it does not work in some of the most widely-used browsers." + } + }, + { + "id": "M06", + "authors": "MDN contributors", + "year": 2026, + "title": "prefers-reduced-motion", + "venue": "MDN Web Docs", + "kind": "guideline", + "url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-reduced-motion", + "topic": "Detecting a reduced-motion preference", + "supports": "The media feature detects whether a user enabled a device setting to minimize non-essential motion.", + "doesNotSupport": "What counts as essential motion; that is a design decision.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "The prefers-reduced-motion CSS media feature is used to detect if a user has enabled a setting on their device to minimize the amount of non-essential motion." + } + }, + { + "id": "D01", + "authors": "Google web.dev", + "year": 2025, + "title": "How the Core Web Vitals metric thresholds were defined", + "venue": "web.dev", + "kind": "guideline", + "url": "https://web.dev/articles/defining-core-web-vitals-thresholds", + "topic": "Core Web Vitals thresholds", + "supports": "Good thresholds at the 75th percentile: Largest Contentful Paint at most 2500 ms, Interaction to Next Paint at most 200 ms, Cumulative Layout Shift at most 0.1.", + "doesNotSupport": "That meeting them improves conversion or satisfaction for your product.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "Largest Contentful Paint ≤2500 ms >4000 ms 75 Interaction to Next Paint ≤200 ms >500 ms 75 Cumulative Layout Shift ≤0.1 >0.25 75" + } + }, + { + "id": "D02", + "authors": "Google web.dev", + "year": 2025, + "title": "Typography, Learn Design", + "venue": "web.dev", + "kind": "guideline", + "url": "https://web.dev/learn/design/typography", + "topic": "Responsive type", + "supports": "Tying font size to viewport width (the vw unit) makes text grow and shrink with the browser width but makes the size hard to predict at a given width.", + "doesNotSupport": "A specific clamp() recipe; the page explains the trade-off.", + "strength": "practice", + "verified": { + "date": "2026-10-25", + "level": "claim-read", + "how": "Page text", + "excerpt": "Connecting font sizes to the viewport's width means that the text will grow and shrink in proportion to the browser width. This makes it difficult to predict what the text size will be at any specific width" + } + } + ] +} diff --git a/site/src/data/evidence/references.ts b/site/src/data/evidence/references.ts new file mode 100644 index 00000000..ece4ae5e --- /dev/null +++ b/site/src/data/evidence/references.ts @@ -0,0 +1,137 @@ +// The references dataset behind every Evidence block, concept page and atlas term. Plain data and pure +// functions only: the Astro site, the markdown twins (packages/ui/scripts/mdx-to-markdown.ts), the MCP build +// and the tests all import this file, so a source has one definition. Rules for using it: +// site/src/content/docs/concepts/evidence.mdx. +import data from "./references.json"; + +/** + * How strongly a claim is backed. A label belongs to a claim, never to a whole page. + * - established: replicated findings or a meta-analysis / authoritative review of them + * - supported: one or more peer-reviewed studies + * - preliminary: a single or small study, a preprint, or a vendor's own report + * - practice: a standard or a published guideline (WCAG, a spec, a platform guide, a practitioner article): + * it says what is required or recommended, not what users prefer + * - heuristic: our own judgement, with no source + */ +export const STRENGTHS = [ + "established", + "supported", + "preliminary", + "practice", + "heuristic", +] as const; +export type Strength = (typeof STRENGTHS)[number]; + +export const STRENGTH_LABEL: Record = { + established: "Established", + supported: "Supported", + preliminary: "Preliminary", + practice: "Standard or guideline", + heuristic: "Our judgement", +}; + +export const STRENGTH_MEANING: Record = { + established: "Replicated findings, or a meta-analysis or authoritative review of them.", + supported: "One or more peer-reviewed studies. Read the scope before generalizing.", + preliminary: "A single or small study, a preprint, or a vendor reporting its own research.", + practice: + "A standard or guideline. It says what is required or recommended, not what users prefer.", + heuristic: "Our own judgement. No source backs it, so test it with your users.", +}; + +export const REFERENCE_KINDS = [ + "peer-reviewed", + "review", + "preprint", + "standard", + "guideline", + "applied-research", + "vendor-research", +] as const; +export type ReferenceKind = (typeof REFERENCE_KINDS)[number]; + +/** `claim-read`: the wording the page relies on was read at the source. `identity-only`: only the record exists. */ +export type VerificationLevel = "claim-read" | "identity-only"; + +export interface Reference { + /** `E01` owner list, `N` NN/g, `W` WCAG, `M` MDN, `D` web.dev. Stable: pages and atlas terms link to it. */ + id: string; + authors: string; + year: number; + title: string; + venue: string; + kind: ReferenceKind; + url: string; + topic: string; + /** What the source supports, with who or what was studied. */ + supports: string; + /** What it does not support: the limit of the scope. */ + doesNotSupport: string; + strength: Strength; + verified: { date: string; level: VerificationLevel; how: string; excerpt: string }; +} + +export const REFERENCES: readonly Reference[] = data.references as Reference[]; +const BY_ID = new Map(REFERENCES.map((r) => [r.id, r])); + +export const getReference = (id: string): Reference | undefined => BY_ID.get(id); + +/** Ids of a comma, semicolon or space separated list (`"E01, E06"`), in order and without duplicates. */ +export function parseRefIds(list: string): string[] { + return [...new Set(list.split(/[\s,;]+/).filter(Boolean))]; +} + +/** Error messages for every problem in a dataset; empty means valid. Used by the unit test and the build. */ +export function checkReferences(refs: readonly Reference[]): string[] { + const errors: string[] = []; + const seen = new Set(); + const isoDate = /^\d{4}-\d{2}-\d{2}$/; + for (const r of refs) { + const at = `reference ${r.id}`; + if (!/^[A-Z]\d{2}$/.test(r.id)) errors.push(`${at}: id must be a letter and two digits`); + if (seen.has(r.id)) errors.push(`${at}: duplicate id`); + seen.add(r.id); + for (const f of ["authors", "title", "venue", "topic", "supports", "doesNotSupport"] as const) + if (!r[f]?.trim()) errors.push(`${at}: ${f} is empty`); + if (!Number.isInteger(r.year) || r.year < 1900 || r.year > 2100) errors.push(`${at}: bad year`); + if (!REFERENCE_KINDS.includes(r.kind)) errors.push(`${at}: unknown kind "${r.kind}"`); + if (!STRENGTHS.includes(r.strength) || r.strength === "heuristic") + errors.push(`${at}: strength must be one of established, supported, preliminary, practice`); + if (!/^https:\/\//.test(r.url)) errors.push(`${at}: url must be https`); + if (!r.verified || !isoDate.test(r.verified.date)) + errors.push(`${at}: verified.date is not YYYY-MM-DD`); + else { + if (!r.verified.excerpt?.trim()) errors.push(`${at}: verified.excerpt is empty`); + if (!r.verified.how?.trim()) errors.push(`${at}: verified.how is empty`); + if (r.verified.level !== "claim-read" && r.verified.level !== "identity-only") + errors.push(`${at}: verified.level must be claim-read or identity-only`); + } + // A standard or guideline is never evidence of what users prefer, and a vendor's own study is never + // better than preliminary: the kind caps the strength. + if ((r.kind === "standard" || r.kind === "guideline") && r.strength !== "practice") + errors.push(`${at}: a ${r.kind} is "practice", not "${r.strength}"`); + if (r.kind === "vendor-research" && r.strength !== "preliminary") + errors.push(`${at}: vendor research is "preliminary"`); + if (r.kind === "preprint" && r.strength !== "preliminary") + errors.push(`${at}: a preprint is "preliminary"`); + } + return errors; +} + +/** + * Words that present opinion or a single study as proof, or brand the site as science. Matched + * case-insensitively in site content by tests/evidence-rules.test.ts. + */ +export const BANNED_PHRASES: readonly string[] = [ + "scientifically proven", + "science says", + "science-based", + "science-backed", + "studies show", + "studies prove", + "research proves", + "research shows that", + "research says", + "proven to", + "backed by science", +]; diff --git a/site/src/lib/nav.ts b/site/src/lib/nav.ts index 0baa551e..97109f89 100644 --- a/site/src/lib/nav.ts +++ b/site/src/lib/nav.ts @@ -3,6 +3,7 @@ import type { DocsNavGroup, DocsSection } from "@codeatlasdev/ui"; /** Sidebar groups, in display order. A doc's frontmatter `group` must be one of these. */ export const GROUPS = [ "Getting started", + "Concepts", "Foundation", "Surfaces", "Inputs", diff --git a/site/src/styles/site.css b/site/src/styles/site.css index 953a99b5..6207b5dd 100644 --- a/site/src/styles/site.css +++ b/site/src/styles/site.css @@ -54,3 +54,40 @@ /* Build-time shiki output (dual themes): pick the palette from the context's color-scheme. */ .shiki span { color: light-dark(var(--shiki-light), var(--shiki-dark)); } .shiki { background: transparent !important; padding: var(--ca-space-4); overflow-x: auto; font-family: var(--ca-font-mono); font-size: var(--ca-font-size-body2); } + +/* ── Evidence blocks (Claim, Refs, Limits) ────────────────────────────── */ +.claim { + margin: var(--ca-space-5) 0; + padding: var(--ca-space-3) var(--ca-space-4); + border-inline-start: var(--ca-space-1) solid var(--ca-outline-strong); + background: var(--ca-surface-sunken); + border-radius: var(--ca-radius-sm); +} +.claim[data-strength="heuristic"] { border-inline-start-style: dashed; } +.claim__label { margin: 0 0 var(--ca-space-2); font-size: var(--ca-font-size-body2); color: var(--ca-text-secondary); } +.claim__badge { + display: inline-block; + margin-inline-end: var(--ca-space-2); + padding: 0 var(--ca-space-2); + border: var(--ca-border-width) solid var(--ca-outline-strong); + border-radius: var(--ca-radius-sm); + font-size: var(--ca-font-size-caption); + font-weight: 600; + color: var(--ca-text-primary); +} +.claim__body > :first-child { margin-top: 0; } +.claim__body > :last-child { margin-bottom: 0; } +.claim__scope { margin: var(--ca-space-2) 0 0; font-size: var(--ca-font-size-body2); color: var(--ca-text-secondary); } +.refs__list { padding-inline-start: 0; list-style: none; display: grid; gap: var(--ca-space-4); } +.refs__item { padding-bottom: var(--ca-space-4); border-bottom: var(--ca-border-width) solid var(--ca-outline-subtle); } +.refs__item p { margin: 0 0 var(--ca-space-1); } +.refs__line { font-size: var(--ca-font-size-body2); color: var(--ca-text-body); } +.refs__verified { color: var(--ca-text-secondary); font-size: var(--ca-font-size-caption); } +.limits { + margin: var(--ca-space-8) 0; + padding: var(--ca-space-4) var(--ca-space-5); + border: var(--ca-border-width) solid var(--ca-outline-strong); + border-radius: var(--ca-radius-md); +} +.limits > h2 { margin-top: 0; } +.limits__reviewed { margin: var(--ca-space-3) 0 0; font-size: var(--ca-font-size-caption); color: var(--ca-text-secondary); }