diff --git a/AGENTS.md b/AGENTS.md index 0755c215..aaf7766a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -104,6 +104,19 @@ Reading failures: locators fail with an ARIA snapshot; traces are in `__traces__ --- +## The gallery index (demo facets) + +Every numbered demo (`site/src/demos///.svelte`, id in `site/src/demos/catalog.ts`) has a `Variant.meta.json` sidecar, and the sidecar carries an `index` block: the facets the gallery filters and searches on. `bun run --cwd packages/ui manifest -- --check` (and every build) fails when the block is missing or wrong, so a new demo cannot ship without it. + +- **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": { "": "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. +- **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/.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). + +--- + ## Critical rules 0. **Test first, always.** Every bug fix and issue starts with a regression test that reproduces the problem. Run it and confirm it fails on the current code, then fix, then confirm it passes. A PR reports that fail-before/pass-after evidence. Tests exist to prove a behavior or a fix, never to pad coverage: no tests of wiring, copies, mock echoes or incidental defaults. If a test cannot fail for a plausible bug, delete it. diff --git a/packages/ui/scripts/build-manifest.ts b/packages/ui/scripts/build-manifest.ts index 851461ef..09a92a06 100644 --- a/packages/ui/scripts/build-manifest.ts +++ b/packages/ui/scripts/build-manifest.ts @@ -27,6 +27,8 @@ import { loadMigrations, migrationMarkdown } from "../src/cli/migrations.ts"; import { STYLES, type SurfaceStyle } from "../src/lib/theme/define.ts"; import { DESIGN_STYLES } from "../src/lib/theme/styles.ts"; import { ENTRY_PAGES, parseRecipes, type Recipe } from "./ai-entry.ts"; +import { buildIndexFiles } from "./index-build.ts"; +import { checkIdRange, checkIndexFacets, ID_PATTERN, type IndexFacets } from "./index-schema.ts"; import { demoSources, mdxToMarkdown } from "./mdx-to-markdown.ts"; // CJS require keeps svelte2tsx out of Vite's transform pipeline (its sourceMappingURL points at a file it does not ship). @@ -1148,6 +1150,8 @@ export interface ManifestDemo { /** Exact timing of the motion, printed in e/ and p/ so an AI never guesses it. */ motion?: ManifestMotion[]; shape: "square" | "wide" | "tall"; + /** Index facets of the sidecar (`index.json`, `/gallery/.md`). */ + index: IndexFacets; components: string[]; imports: { from: string; names: string[] }[]; interactive: boolean; @@ -1197,7 +1201,7 @@ function demoUsages() { type DemoMeta = Pick< ManifestDemo, - "summary" | "use" | "spec" | "tags" | "shape" | "names" | "motion" + "summary" | "use" | "spec" | "tags" | "shape" | "names" | "motion" | "index" >; const META_KEYS: Record = { @@ -1208,6 +1212,7 @@ const META_KEYS: Record = { shape: true, names: true, motion: true, + index: true, }; const MOTION_REQUIRED = ["property", "duration", "easing"] as const; const MOTION_OPTIONAL = ["delay", "stagger", "spring"] as const; @@ -1217,7 +1222,11 @@ const isLines = (v: unknown): v is string[] => Array.isArray(v) && v.every((s) => typeof s === "string" && s.trim() !== ""); /** Problems of one sidecar against its demo source: shape of every field, and spec tokens quoted from the source. */ -export function checkMeta(meta: unknown, source: string): string[] { +export function checkMeta( + meta: unknown, + source: string, + ctx: { known?: ReadonlySet; ids?: ReadonlySet; self?: string } = {} +): string[] { if (!meta || typeof meta !== "object" || Array.isArray(meta)) return ["not an object"]; const m = meta as Record; const errors = Object.keys(m) @@ -1235,6 +1244,7 @@ export function checkMeta(meta: unknown, source: string): string[] { if (typeof m.shape !== "string" || !SHAPES[m.shape]) errors.push("shape: square | wide | tall"); if ("names" in m && (!isLines(m.names) || m.names.length < 1)) errors.push("names: non-empty strings, at least one"); + if ("index" in m) errors.push(...checkIndexFacets(m.index, ctx)); if ("motion" in m) { if (!Array.isArray(m.motion) || m.motion.length < 1) errors.push("motion: at least one entry when present"); @@ -1263,9 +1273,36 @@ export function checkMeta(meta: unknown, source: string): string[] { return errors; } -/** Problems that make the registry invalid: unregistered demos, missing files, duplicate ids/paths, missing or inaccurate sidecars. */ -export function checkCatalog(catalog: readonly { id: string; path: string }[]): string[] { +/** Library exports a demo imports from the package (the `components` facet is derived from these, never hand-typed). */ +export function importedNames(source: string): string[] { + return [ + ...new Set( + [ + ...source.matchAll(/import\s*\{([^}]+)\}\s*from\s*["']@codeatlasdev\/ui[^"']*["']/g), + ].flatMap(([, names]) => + names + .split(",") + .map( + (n) => + n + .trim() + .replace(/^type\s+/, "") + .split(/\s+as\s+/)[0] + ) + .filter(Boolean) + ) + ), + ]; +} + +/** Problems that make the registry invalid: unregistered demos, missing files, duplicate ids/paths, missing or inaccurate sidecars, missing or inconsistent index facets. */ +export function checkCatalog( + catalog: readonly { id: string; path: string }[], + components: readonly { name: string }[] +): string[] { const errors: string[] = []; + const ids = new Set(catalog.map((d) => d.id)); + const known = new Set(components.map((c) => c.name)); const walk = (dir: string, ext: string): string[] => readdirSync(dir, { withFileTypes: true }).flatMap((e) => e.isDirectory() @@ -1279,10 +1316,11 @@ export function checkCatalog(catalog: readonly { id: string; path: string }[]): if (!HELPERS.includes(p) && !paths.has(p)) errors.push(`demo without id: ${p}`); for (const p of walk(DEMOS_DIR, ".meta.json")) if (!paths.has(p)) errors.push(`sidecar without demo: ${p}.meta.json`); - const ids = new Set(); + const seen = new Set(); for (const d of catalog) { - if (ids.has(d.id)) errors.push(`duplicate id: ${d.id}`); - ids.add(d.id); + if (seen.has(d.id)) errors.push(`duplicate id: ${d.id}`); + seen.add(d.id); + if (!ID_PATTERN.test(d.id)) errors.push(`#${d.id} id: 3 digits up to 999, 4 digits from 1000`); if (!existsSync(join(DEMOS_DIR, `${d.path}.svelte`))) { errors.push(`#${d.id} missing file: ${d.path}`); continue; @@ -1300,7 +1338,21 @@ export function checkCatalog(catalog: readonly { id: string; path: string }[]): continue; } const source = readFileSync(join(DEMOS_DIR, `${d.path}.svelte`), "utf8"); - for (const e of checkMeta(meta, source)) errors.push(`#${d.id} ${d.path}.meta.json: ${e}`); + for (const e of checkMeta(meta, source, { known, ids, self: d.id })) + errors.push(`#${d.id} ${d.path}.meta.json: ${e}`); + const facets = (meta as { index?: IndexFacets }).index; + if (!facets) { + errors.push(`#${d.id} ${d.path}.meta.json: index: facets required (bun run index:backfill)`); + continue; + } + const range = checkIdRange(d.id, facets.kind, facets.subtype); + if (range) errors.push(`#${d.id} ${d.path}.meta.json: index: ${range}`); + const used = importedNames(source).filter((n) => known.has(n)); + const listed = [...facets.components].sort().join(); + if (listed !== [...used].sort().join()) + errors.push( + `#${d.id} ${d.path}.meta.json: index.components must be the imports of the demo: ${used.join(", ") || "none"}` + ); } if (paths.size !== catalog.length) errors.push("duplicate path in catalog"); return errors; @@ -1359,6 +1411,7 @@ export function buildDemos( ...(meta.names && { names: meta.names }), ...(meta.motion && { motion: meta.motion }), shape: meta.shape, + index: meta.index, components, imports, interactive: use?.interactive ?? false, @@ -1594,7 +1647,7 @@ if (import.meta.main) { ? (await import("../../../site/src/demos/catalog.ts")).CATALOG : []; // Every run (build included) refuses an invalid registry: a demo without an id must never ship. - const errors = catalog.length ? checkCatalog(catalog) : []; + const errors = catalog.length ? checkCatalog(catalog, manifest.components) : []; if (errors.length) { console.error(`demo catalog (site/src/demos/catalog.ts):\n ${errors.join("\n ")}`); process.exit(1); @@ -1634,6 +1687,12 @@ if (import.meta.main) { : { demos: [], files: {} }; if (demos.length) { write("demos.json", demos); + const indexFiles = buildIndexFiles(demos, manifest.version); + const siteRoot = resolve(ROOT, "../../site/public"); + for (const [name, body] of Object.entries(indexFiles)) { + mkdirSync(dirname(join(siteRoot, name)), { recursive: true }); + writeFileSync(join(siteRoot, name), body); + } for (const base of [join(dist, "api/v1"), resolve(ROOT, "../../site/public/api/v1")]) for (const [name, body] of Object.entries(files)) { mkdirSync(dirname(join(base, name)), { recursive: true }); diff --git a/packages/ui/scripts/index-backfill.ts b/packages/ui/scripts/index-backfill.ts new file mode 100644 index 00000000..6870d0a4 --- /dev/null +++ b/packages/ui/scripts/index-backfill.ts @@ -0,0 +1,140 @@ +/** + * Writes the `index` facet block into every demo sidecar. + * + * bun packages/ui/scripts/index-backfill.ts [--infer] [--apply ] + * + * `--infer` derives what the code can prove: kind, subtype, components (the package imports), tech, cost, + * the reduced-motion flag and the added date (first commit that registered the id). It keeps any value a + * human already curated in the sidecar. `--apply` merges a hand-read file `{ "": { style, industry, + * motion, layout, colorMode, density, interaction, aliases, keyboard, related } }` over it. The result is + * validated by `checkCatalog` (`bun run --cwd packages/ui manifest -- --check`). + */ +import { execFileSync } from "node:child_process"; +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { CATALOG } from "../../../site/src/demos/catalog.ts"; +import { buildManifest, importedNames } from "./build-manifest.ts"; +import { type IndexFacets, PENDING_FACETS, SUBTYPES } from "./index-schema.ts"; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const DEMOS = join(ROOT, "site/src/demos"); + +const MOTION_SUBTYPE: Record = { + basics: "basics", + text: "text", + scroll: "scroll", + micro: "micro", + transitions: "transition", + "hero-ambient": "ambient", +}; +const COMPONENT_SUBTYPE: Record = { + inputs: "input", + display: "display", + feedback: "feedback", + navigation: "navigation", + surfaces: "surface", + layout: "layout", + charts: "chart", + foundation: "foundation", + map: "map", + "docs-kit": "display", +}; + +/** `kind` and `subtype` from the demo's folder. */ +export function inferKind(path: string): Pick { + const [group, page] = path.split("/"); + if (group === "motion") return { kind: "motion", subtype: MOTION_SUBTYPE[page] ?? "micro" }; + const subtype = COMPONENT_SUBTYPE[group]; + if (!subtype || !SUBTYPES.component.includes(subtype)) throw new Error(`no subtype for ${path}`); + return { kind: "component", subtype }; +} + +/** What the source proves about how the demo is built. */ +export function inferTech(source: string): Pick { + const tech = new Set(["css"]); + if ( + /\bspring\b|Spring\b|Tween\b|\banimate\(|requestAnimationFrame|IntersectionObserver|MotionStage/.test( + source + ) + ) + tech.add("svelte-motion"); + if (/ { + const log = execFileSync( + "git", + ["log", "--reverse", "--format=COMMIT %as", "-p", "-U0", "--", "site/src/demos/catalog.ts"], + { cwd: ROOT, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 } + ); + const out = new Map(); + let date = ""; + for (const line of log.split("\n")) { + if (line.startsWith("COMMIT ")) date = line.slice(7); + else { + const m = line.match(/^\+\s*\{ id: "(\d+)"/); + if (m && !out.has(m[1])) out.set(m[1], date); + } + } + return out; +} + +if (import.meta.main) { + const applyAt = process.argv.indexOf("--apply"); + const curated: Record & { keyboard?: boolean }> = + applyAt > 0 ? JSON.parse(readFileSync(process.argv[applyAt + 1], "utf8")) : {}; + const known = new Set(buildManifest().components.map((c) => c.name)); + const dates = addedDates(); + let written = 0; + for (const { id, path } of CATALOG) { + const file = join(DEMOS, `${path}.meta.json`); + const meta = JSON.parse(readFileSync(file, "utf8")); + const source = readFileSync(join(DEMOS, `${path}.svelte`), "utf8"); + const prior = (meta.index ?? {}) as Partial; + const hand = curated[id] ?? {}; + const { keyboard, ...handFacets } = hand; + const inferred = { + ...inferKind(path), + ...inferTech(source), + components: importedNames(source).filter((n) => known.has(n)), + added: dates.get(id) ?? new Date().toISOString().slice(0, 10), + }; + const reducedMotion = + /prefers-reduced-motion|\.reduced\b|MotionStage/.test(source) || + !inferred.tech.includes("svelte-motion"); + const index = { + kind: inferred.kind, + subtype: inferred.subtype, + industry: handFacets.industry ?? prior.industry, + style: handFacets.style ?? prior.style, + motion: handFacets.motion ?? prior.motion, + interaction: handFacets.interaction ?? prior.interaction, + layout: handFacets.layout ?? prior.layout, + colorMode: handFacets.colorMode ?? prior.colorMode, + density: handFacets.density ?? prior.density, + tech: inferred.tech, + cost: inferred.cost, + components: inferred.components, + aliases: handFacets.aliases ?? prior.aliases ?? [], + related: handFacets.related ?? prior.related ?? [], + a11y: { reducedMotion, keyboard: keyboard ?? prior.a11y?.keyboard ?? false }, + added: prior.added ?? inferred.added, + ...(PENDING_FACETS.length && { + review: Object.fromEntries(PENDING_FACETS.map((f) => [f, "pending"])), + }), + }; + const { index: _drop, ...rest } = meta; + writeFileSync(file, `${JSON.stringify({ ...rest, index }, null, "\t")}\n`); + written++; + } + // Same formatter as the rest of the repo, so the diff of a sidecar is only its `index` key. + execFileSync("bunx", ["biome", "format", "--write", DEMOS], { cwd: ROOT, stdio: "ignore" }); + console.log(`index facets written to ${written} sidecars`); +} diff --git a/packages/ui/scripts/index-build.ts b/packages/ui/scripts/index-build.ts new file mode 100644 index 00000000..d224f7e5 --- /dev/null +++ b/packages/ui/scripts/index-build.ts @@ -0,0 +1,167 @@ +/** + * The gallery index files, built from the demo records: + * api/v1/index.json every entry with its facets, plus per-facet counts (the file a search UI loads once) + * gallery/.md one markdown twin per id: facets, what it shows, spec, components, full source + * + * Pure: takes the demo records, returns `{ path: body }`. The caller decides where the files go. + */ +import { type IndexFacets, PENDING_FACETS } from "./index-schema.ts"; + +/** The fields of a demo record the index reads (a subset of `ManifestDemo`). */ +export interface IndexDemo { + id: string; + group: string; + page: string; + variant: string; + title: string; + summary: string; + use: string; + spec: string[]; + tags: string[]; + names?: string[]; + index: IndexFacets; + components: string[]; + needs: string; + demo: string; + source: string; +} + +export interface IndexEntry extends Partial { + id: string; + path: string; + title: string; + summary: string; + use: string; + tags: string[]; + names: string[]; +} + +const SITE = "https://ui.codeatlas.com.br"; + +/** Facets that are filter chips, and how to read their values off an entry. A pending facet is absent from the entry, so it has no counts. */ +const COUNTED: Record string[] | undefined> = { + kind: (e) => [e.kind as string], + subtype: (e) => [`${e.kind}/${e.subtype}`], + industry: (e) => e.industry, + style: (e) => e.style, + motion: (e) => e.motion && [e.motion], + interaction: (e) => e.interaction, + layout: (e) => e.layout && [e.layout], + colorMode: (e) => e.colorMode && [e.colorMode], + density: (e) => e.density && [e.density], + tech: (e) => e.tech, + cost: (e) => [e.cost as string], + component: (e) => e.components, +}; + +/** The facets of a demo without the ones still pending review (see `PENDING_FACETS`). */ +export function publishedFacets(index: IndexFacets): Partial { + const out: Partial = { ...index }; + for (const f of PENDING_FACETS) delete out[f as keyof IndexFacets]; + return out; +} + +export function indexEntries(demos: readonly IndexDemo[]): IndexEntry[] { + return demos.map((d) => ({ + ...publishedFacets(d.index), + id: d.id, + path: `${d.group}/${d.page}/${d.variant}`, + title: d.title, + summary: d.summary, + use: d.use, + tags: d.tags, + names: d.names ?? [], + })); +} + +/** Count of entries per value of every filter facet; only values that occur are listed. */ +export function facetCounts( + entries: readonly IndexEntry[] +): Record> { + const out: Record> = {}; + for (const [facet, read] of Object.entries(COUNTED)) { + const counts: Record = {}; + for (const e of entries) for (const v of read(e) ?? []) counts[v] = (counts[v] ?? 0) + 1; + if (Object.keys(counts).length === 0) continue; + out[facet] = Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b))); + } + return out; +} + +/** Markdown twin of one entry: what a person or an agent reads without loading the page. */ +export function entryMarkdown(d: IndexDemo): string { + const f = d.index; + const shown = (facet: string) => !(PENDING_FACETS as readonly string[]).includes(facet); + const list = (v: string[]) => (v.length ? v.join(", ") : "none"); + return `${[ + ``, + `# #${d.id} ${d.title}`, + "", + d.summary, + "", + `When to use: ${d.use}`, + "", + "## Facets", + "", + `- Kind: ${f.kind} / ${f.subtype}`, + ...(shown("industry") ? [`- Industry: ${list(f.industry)}`] : []), + ...(shown("style") ? [`- Style: ${list(f.style)}`] : []), + ...(shown("motion") ? [`- Motion: ${f.motion}`] : []), + ...(shown("interaction") ? [`- Interaction: ${list(f.interaction)}`] : []), + ...(shown("layout") ? [`- Layout: ${f.layout}`] : []), + ...(shown("colorMode") ? [`- Color mode: ${f.colorMode}`] : []), + ...(shown("density") ? [`- Density: ${f.density}`] : []), + `- Tech: ${list(f.tech)} (cost: ${f.cost})`, + `- Reduced motion: ${f.a11y.reducedMotion ? "handled" : "not handled"}; keyboard: ${f.a11y.keyboard ? "operable" : "not interactive"}`, + `- Components: ${list(d.components)}`, + ...(shown("aliases") && f.aliases.length + ? [`- Also searched as: ${f.aliases.join(", ")}`] + : []), + "", + "## Spec", + "", + ...d.spec.map((s) => `- ${s}`), + "", + `Needs: ${d.needs}`, + `Live demo: ${d.demo}`, + `Reference: ${SITE}/api/v1/en/e/${d.id}.txt`, + "", + "## Source", + "", + "```svelte", + d.source.trimEnd(), + "```", + ].join("\n")}\n`; +} + +/** Header and facet counts indented for reading, one entry per line so the file stays small and diffs per demo. */ +function indexJson(entries: readonly IndexEntry[], version: string): string { + const head = JSON.stringify( + { + version, + count: entries.length, + urls: { + page: `${SITE}/gallery/{id}/`, + markdown: `${SITE}/gallery/{id}.md`, + reference: `${SITE}/api/v1/en/e/{id}.txt`, + }, + facets: facetCounts(entries), + }, + null, + "\t" + ); + return `${head.slice(0, -2)},\n\t"entries": [\n${entries.map((e) => `\t\t${JSON.stringify(e)}`).join(",\n")}\n\t]\n}\n`; +} + +/** `{ "api/v1/index.json": …, "gallery/.md": … }` for every demo. */ +export function buildIndexFiles( + demos: readonly IndexDemo[], + version: string +): Record { + const entries = indexEntries(demos); + const files: Record = { + "api/v1/index.json": indexJson(entries, version), + }; + for (const d of demos) files[`gallery/${d.id}.md`] = entryMarkdown(d); + return files; +} diff --git a/packages/ui/scripts/index-schema.ts b/packages/ui/scripts/index-schema.ts new file mode 100644 index 00000000..374a5a26 --- /dev/null +++ b/packages/ui/scripts/index-schema.ts @@ -0,0 +1,468 @@ +/** + * The gallery index: the facets every demo carries in its `Variant.meta.json` under the `index` key, + * the closed vocabulary they draw from, and the 4-digit id ranges reserved per kind. + * + * One module owns the schema: `checkMeta` (build-manifest.ts) validates with `checkIndexFacets`, + * `scripts/index-backfill.ts` writes with the same vocabulary, and `buildIndex` reads the result. + */ +import { DESIGN_STYLES } from "../src/lib/theme/styles.ts"; + +export const KINDS = [ + "component", + "section", + "page", + "motion", + "background", + "technique", + "3d", + "shader", + "style", + "template", +] as const; +export type Kind = (typeof KINDS)[number]; + +/** Allowed `subtype` values per kind (a closed list, so the facet chips stay stable and countable). */ +export const SUBTYPES: Record = { + component: [ + "input", + "display", + "feedback", + "navigation", + "surface", + "layout", + "chart", + "media", + "foundation", + "map", + ], + section: [ + "hero", + "logos", + "feature", + "bento", + "how-it-works", + "stats", + "testimonial", + "pricing", + "comparison", + "faq", + "cta", + "newsletter", + "team", + "careers", + "changelog", + "blog-list", + "contact", + "integrations", + "showcase", + "footer", + "navbar", + "banner", + "empty", + ], + page: [ + "landing", + "pricing", + "about", + "blog-post", + "blog-index", + "changelog", + "docs", + "auth", + "dashboard", + "settings", + "checkout", + "product", + "portfolio", + "waitlist", + "legal", + "error", + ], + motion: [ + "basics", + "text", + "scroll", + "micro", + "transition", + "ambient", + "gesture", + "cursor", + "loading", + "celebration", + ], + background: [ + "gradient", + "aurora", + "grid", + "noise", + "pattern", + "beams", + "particles", + "shader-field", + "ascii", + "image-treatment", + ], + technique: [ + "glass", + "spotlight", + "border-beam", + "mask-reveal", + "tilt", + "parallax", + "marquee", + "sticky-stack", + "magnetic", + "progressive-blur", + "duotone", + "kinetic-type", + ], + "3d": ["globe", "card-stack", "turntable", "carousel-wall", "terrain", "particle-object"], + shader: ["gradient-field", "distortion", "noise-flow", "dither", "ripple", "refraction"], + style: ["concept"], + template: ["multi-page"], +}; + +export const INDUSTRIES = [ + "saas", + "ai", + "devtools", + "fintech", + "e-commerce", + "health", + "education", + "agency", + "portfolio", + "media", + "travel", + "food", + "real-estate", + "nonprofit", + "gaming", + "crypto", + "general", +] as const; +/** `designStyle` ids plus the recipes the index adds ahead of their pages. */ +export const STYLES_VOCAB: readonly string[] = [ + ...Object.keys(DESIGN_STYLES), + "minimal", + "maximal", + "skeuo", + "aurora", + "luxe", + "pixel", + "memphis", + "flat", + "organic", +]; +export const MOTION_LEVELS = ["none", "subtle", "expressive", "immersive"] as const; +export const INTERACTIONS = [ + "hover", + "press", + "drag", + "scroll", + "cursor", + "keyboard", + "gyroscope", + "none", +] as const; +export const LAYOUTS = [ + "single-column", + "split", + "grid", + "bento", + "stack", + "carousel", + "full-bleed", + "sticky", + "overlay", +] as const; +export const COLOR_MODES = ["light", "dark", "both"] as const; +export const DENSITIES = ["compact", "regular", "airy"] as const; +export const TECHS = [ + "css", + "svelte-motion", + "canvas2d", + "svg-filter", + "webgl", + "view-transitions", +] as const; +export const COSTS = ["css", "js-light", "gpu"] as const; + +/** The facets of one demo, stored in its sidecar as `"index": {…}`. Title, summary, id and path come from elsewhere. */ +export interface IndexFacets { + kind: Kind; + subtype: string; + industry: string[]; + style: string[]; + /** How much the demo moves: none · subtle (state change) · expressive (entrance/scroll) · immersive (physics/3D/shader). */ + motion: (typeof MOTION_LEVELS)[number]; + interaction: string[]; + layout: (typeof LAYOUTS)[number]; + colorMode: (typeof COLOR_MODES)[number]; + density: (typeof DENSITIES)[number]; + tech: string[]; + cost: (typeof COSTS)[number]; + /** Library exports the demo uses (inferred from its imports, never hand-typed). */ + components: string[]; + /** Search synonyms ("disclosure" for an accordion). */ + aliases: string[]; + /** Ids of related demos. */ + related: string[]; + a11y: { reducedMotion: boolean; keyboard: boolean }; + /** ISO date the demo entered the catalog. */ + added: string; + /** Hand-read facets withheld from search until their audited error rate is at most 5% (see `AUDIT`). */ + review?: Partial>; +} + +/** + * Publishing gate. A hand-read facet is exposed in `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 `MAX_ERROR_RATE`. + * The bound, not the point estimate: 0 wrong of 73 reads is not "0% wrong", it is "at most ~4% wrong". A field + * above the limit stays in the sidecar (validated) as `pending` and is hidden from search until it is corrected and + * re-audited. A confidently wrong filter is worse than none. + * + * Inferred facets (kind, subtype, components, tech, cost, added, a11y.reducedMotion) are derived from the code and + * are always published. Re-audit a pending field with a new seed, update `AUDIT`, and flip it to `published`. + * + * What the audit does NOT cover: only the entries in `AUDIT.read` were re-read by a person; every other entry holds + * the value the first pass and the correction rules gave it, and is unverified beyond those rules. + */ +export const MAX_ERROR_RATE = 0.05; + +export type Review = "published" | "pending"; + +/** One-sided 95% upper confidence bound of a rate with `wrong` errors in `checked` reads (exact binomial, Clopper-Pearson). */ +export function errorUpperBound(wrong: number, checked: number): number { + if (wrong >= checked) return 1; + const cdf = (p: number) => { + let term = (1 - p) ** checked; + let sum = term; + for (let i = 1; i <= wrong; i++) { + term *= ((checked - i + 1) / i) * (p / (1 - p)); + sum += term; + } + return sum; + }; + let lo = wrong / checked; + let hi = 1; + for (let i = 0; i < 60; i++) { + const mid = (lo + hi) / 2; + if (cdf(mid) > 0.05) lo = mid; + else hi = mid; + } + return hi; +} + +/** The audited hand-read facets: errors found among the entries of the holdout (`wrong` of `checked`). */ +export const AUDIT = { + seeds: { first: 569, holdout: 1031 }, + /** + * Distinct entries a person read: `first` = 66 random + 64 immersive/expressive (their errors were fixed, then rules + * 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 }, + fields: { + industry: { wrong: 5, checked: 73, review: "pending" as Review }, + style: { wrong: 0, checked: 73, review: "published" as Review }, + motion: { wrong: 0, checked: 73, review: "published" as Review }, + interaction: { wrong: 0, checked: 73, review: "published" as Review }, + layout: { wrong: 0, checked: 73, review: "published" as Review }, + colorMode: { wrong: 0, checked: 73, review: "published" as Review }, + density: { wrong: 0, checked: 73, review: "published" as Review }, + aliases: { wrong: 0, checked: 73, review: "published" as Review }, + related: { wrong: 0, checked: 73, review: "published" as Review }, + }, +} as const; + +/** Hand-read facets withheld from every public output. */ +export const PENDING_FACETS: readonly (keyof typeof AUDIT.fields)[] = ( + Object.keys(AUDIT.fields) as (keyof typeof AUDIT.fields)[] +).filter((f) => AUDIT.fields[f].review === "pending"); + +const FACET_KEYS = [ + "kind", + "subtype", + "industry", + "style", + "motion", + "interaction", + "layout", + "colorMode", + "density", + "tech", + "cost", + "components", + "aliases", + "related", + "a11y", + "added", + "review", +] as const; + +const isStrings = (v: unknown): v is string[] => + Array.isArray(v) && v.every((s) => typeof s === "string" && s.trim() !== ""); +const oneOf = (v: unknown, list: readonly string[]) => typeof v === "string" && list.includes(v); + +/** + * Problems of one `index` facet block. `known` is the set of real component names (components must be + * real exports) and `ids` the set of catalog ids (related must point at one); both are skipped when omitted. + */ +export function checkIndexFacets( + facets: unknown, + ctx: { known?: ReadonlySet; ids?: ReadonlySet; self?: string } = {} +): string[] { + if (!facets || typeof facets !== "object" || Array.isArray(facets)) + return ["index: object required"]; + const f = facets as Record; + const errors: string[] = Object.keys(f) + .filter((k) => !(FACET_KEYS as readonly string[]).includes(k)) + .map((k) => `index: unknown key "${k}"`); + const multi = (key: string, list: readonly string[], min: number) => { + const v = f[key]; + if (!isStrings(v) || v.length < min) + errors.push(`index.${key}: at least ${min} of ${list.join(" | ")}`); + else + for (const x of v) + if (!list.includes(x)) + errors.push(`index.${key}: "${x}" is not one of ${list.join(" | ")}`); + if (isStrings(v) && new Set(v).size !== v.length) errors.push(`index.${key}: duplicate value`); + }; + if (!oneOf(f.kind, KINDS)) errors.push(`index.kind: ${KINDS.join(" | ")}`); + else if (!oneOf(f.subtype, SUBTYPES[f.kind as Kind])) + errors.push( + `index.subtype: for kind "${f.kind}" one of ${SUBTYPES[f.kind as Kind].join(" | ")}` + ); + multi("industry", INDUSTRIES, 1); + multi("style", STYLES_VOCAB, 1); + if (!oneOf(f.motion, MOTION_LEVELS)) errors.push(`index.motion: ${MOTION_LEVELS.join(" | ")}`); + multi("interaction", INTERACTIONS, 1); + if (!oneOf(f.layout, LAYOUTS)) errors.push(`index.layout: ${LAYOUTS.join(" | ")}`); + if (!oneOf(f.colorMode, COLOR_MODES)) errors.push(`index.colorMode: ${COLOR_MODES.join(" | ")}`); + if (!oneOf(f.density, DENSITIES)) errors.push(`index.density: ${DENSITIES.join(" | ")}`); + multi("tech", TECHS, 1); + if (!oneOf(f.cost, COSTS)) errors.push(`index.cost: ${COSTS.join(" | ")}`); + if (!isStrings(f.components)) errors.push("index.components: array of component names"); + else if (ctx.known) + for (const c of f.components) + if (!ctx.known.has(c)) errors.push(`index.components: "${c}" is not a library component`); + if (!isStrings(f.aliases) || f.aliases.some((a) => a !== a.toLowerCase())) + errors.push("index.aliases: array of lowercase strings"); + if (!isStrings(f.related)) errors.push("index.related: array of catalog ids"); + else if (ctx.ids) + for (const r of f.related) { + if (!ctx.ids.has(r)) errors.push(`index.related: "${r}" is not a catalog id`); + if (r === ctx.self) errors.push("index.related: a demo cannot relate to itself"); + } + const a = f.a11y as Record | undefined; + if ( + !a || + typeof a !== "object" || + typeof a.reducedMotion !== "boolean" || + typeof a.keyboard !== "boolean" + ) + errors.push("index.a11y: { reducedMotion: boolean, keyboard: boolean }"); + if ( + typeof f.added !== "string" || + !/^\d{4}-\d{2}-\d{2}$/.test(f.added) || + Number.isNaN(Date.parse(f.added)) + ) + errors.push("index.added: ISO date YYYY-MM-DD"); + // The pending marker is the audit ledger written into the sidecar, never hand-edited. + const marked = Object.keys((f.review as object | undefined) ?? {}) + .sort() + .join(); + if (marked !== [...PENDING_FACETS].sort().join()) + errors.push( + `index.review: must mark exactly the pending facets (${PENDING_FACETS.join(", ") || "none"})` + ); + else if (f.review && Object.values(f.review as object).some((v) => v !== "pending")) + errors.push('index.review: values are "pending"'); + // Consistency a reader would otherwise trip over. + if (f.motion === "none" && f.cost === "gpu") + errors.push('index: motion "none" cannot cost "gpu"'); + if (isStrings(f.tech) && f.tech.includes("webgl") && f.cost !== "gpu") + errors.push('index: tech "webgl" costs "gpu"'); + return errors; +} + +/** + * Reserved id ranges per kind. Ids stay append-only and 3 digits widen to 4 only for new kinds; an + * existing id keeps its value and its range is the one of its group below. + * + * Legacy (3 digits): every component demo keeps the id it has, `001-299` and `900-999` (components); + * `300-399` is motion (text 300-319, scroll 320-339, micro 340-359, transitions 360-379, hero/ambient 380-399). + * New (4 digits): see `ID_RANGES`. + */ +export interface IdRange { + kind: Kind; + /** Subtype this block is for, or `*` for the whole kind. */ + subtype: string; + from: number; + to: number; +} + +export const ID_RANGES: readonly IdRange[] = [ + { kind: "component", subtype: "*", from: 1, to: 299 }, + { kind: "motion", subtype: "text", from: 300, to: 319 }, + { kind: "motion", subtype: "scroll", from: 320, to: 339 }, + { kind: "motion", subtype: "micro", from: 340, to: 359 }, + { kind: "motion", subtype: "transition", from: 360, to: 379 }, + { kind: "motion", subtype: "ambient", from: 380, to: 399 }, + { kind: "component", subtype: "*", from: 400, to: 999 }, + // Sections: blocks of 100 per subtype. + { kind: "section", subtype: "hero", from: 1000, to: 1099 }, + { kind: "section", subtype: "feature", from: 1100, to: 1199 }, + { kind: "section", subtype: "pricing", from: 1200, to: 1299 }, + { kind: "section", subtype: "cta", from: 1300, to: 1399 }, + { kind: "section", subtype: "testimonial", from: 1400, to: 1499 }, + { kind: "section", subtype: "footer", from: 1500, to: 1599 }, + { kind: "section", subtype: "faq", from: 1600, to: 1699 }, + { kind: "section", subtype: "navbar", from: 1700, to: 1799 }, + { kind: "section", subtype: "logos", from: 1800, to: 1899 }, + { kind: "section", subtype: "*", from: 1900, to: 2999 }, + { kind: "background", subtype: "*", from: 3000, to: 3199 }, + { kind: "technique", subtype: "*", from: 3200, to: 3399 }, + { kind: "3d", subtype: "*", from: 3400, to: 3499 }, + { kind: "shader", subtype: "*", from: 3500, to: 3599 }, + { kind: "page", subtype: "*", from: 4000, to: 4499 }, + { kind: "template", subtype: "*", from: 4500, to: 4999 }, +]; + +/** Ids are 3 digits up to 999 and 4 digits from 1000; `"001"` and `"1000"` are both valid, `"01"` and `"10000"` are not. */ +export const ID_PATTERN = /^(\d{3}|[1-9]\d{3})$/; + +/** The reserved block an id falls in, or undefined for an id outside every range. */ +export function rangeOf(id: string): IdRange | undefined { + const n = Number(id); + return ( + ID_RANGES.find((r) => n >= r.from && n <= r.to && r.subtype !== "*") ?? + ID_RANGES.find((r) => n >= r.from && n <= r.to) + ); +} + +/** + * Ids assigned before the ranges existed that sit outside the block of their kind. They keep their value + * (ids are never renumbered) and are the only exemptions: a new id must live in its kind's range. + */ +export const LEGACY_OUT_OF_RANGE: Readonly> = { + "256": "motion", // motion/basics/FadeRise + "387": "component", // inputs/checkbox/RangeSelect + "388": "component", // inputs/file-upload/ExternalTrigger + "389": "component", // inputs/text-field/HiddenLabel +}; + +/** Whether an id may carry these facets: its range must match the kind, and the subtype when the block names one. */ +export function checkIdRange(id: string, kind: string, subtype: string): string | undefined { + if (LEGACY_OUT_OF_RANGE[id]) + return LEGACY_OUT_OF_RANGE[id] === kind + ? undefined + : `legacy id ${id} is a "${LEGACY_OUT_OF_RANGE[id]}"`; + const r = rangeOf(id); + if (!r) return `id ${id} is outside every reserved range`; + if (r.kind !== kind) return `id ${id} is reserved for kind "${r.kind}", not "${kind}"`; + if (r.subtype !== "*" && r.subtype !== subtype) + return `id ${id} is reserved for subtype "${r.subtype}", not "${subtype}"`; + return undefined; +} diff --git a/packages/ui/tests/demos-catalog.test.ts b/packages/ui/tests/demos-catalog.test.ts index 35b68b6b..595b6c0c 100644 --- a/packages/ui/tests/demos-catalog.test.ts +++ b/packages/ui/tests/demos-catalog.test.ts @@ -9,6 +9,7 @@ import { checkMeta, HELPERS, } from "../scripts/build-manifest.ts"; +import { ID_PATTERN } from "../scripts/index-schema.ts"; const DEMOS = resolve(import.meta.dir, "../../../site/src/demos"); @@ -27,9 +28,9 @@ const manifest = buildManifest(); const { demos, files } = buildDemos(manifest, CATALOG); describe("demo catalog", () => { - it("ids are unique, 3-digit and strictly increasing", () => { + it("ids are unique, 3 or 4 digits and strictly increasing", () => { const ids = CATALOG.map((d) => d.id); - for (const id of ids) expect(id).toMatch(/^\d{3}$/); + for (const id of ids) expect(id).toMatch(ID_PATTERN); for (let i = 1; i < ids.length; i++) expect(Number(ids[i])).toBeGreaterThan(Number(ids[i - 1])); }); @@ -70,7 +71,7 @@ describe("demo catalog", () => { describe("demo meta sidecars", () => { it("every registered demo has an accurate sidecar and none is orphaned", () => { - expect(checkCatalog(CATALOG)).toEqual([]); + expect(checkCatalog(CATALOG, manifest.components)).toEqual([]); }); const source = ''; diff --git a/packages/ui/tests/demos-index.test.ts b/packages/ui/tests/demos-index.test.ts new file mode 100644 index 00000000..ad850beb --- /dev/null +++ b/packages/ui/tests/demos-index.test.ts @@ -0,0 +1,332 @@ +import { describe, expect, it } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { CATALOG } from "../../../site/src/demos/catalog.ts"; +import { buildDemos, buildManifest, checkMeta, importedNames } from "../scripts/build-manifest.ts"; +import { + buildIndexFiles, + facetCounts, + indexEntries, + publishedFacets, +} from "../scripts/index-build.ts"; +import { + AUDIT, + checkIdRange, + checkIndexFacets, + errorUpperBound, + ID_PATTERN, + ID_RANGES, + LEGACY_OUT_OF_RANGE, + MAX_ERROR_RATE, + PENDING_FACETS, + rangeOf, + SUBTYPES, +} from "../scripts/index-schema.ts"; + +const DEMOS = resolve(import.meta.dir, "../../../site/src/demos"); +const manifest = buildManifest(); +const { demos } = buildDemos(manifest, CATALOG); + +const ok = { + kind: "section", + subtype: "pricing", + industry: ["saas"], + style: ["default", "bento"], + motion: "expressive", + interaction: ["hover", "scroll"], + layout: "grid", + colorMode: "both", + density: "regular", + tech: ["css", "svelte-motion"], + cost: "js-light", + components: ["Button"], + aliases: ["plans"], + related: ["001"], + a11y: { reducedMotion: true, keyboard: true }, + added: "2026-10-25", + review: Object.fromEntries(PENDING_FACETS.map((f) => [f, "pending"])), +}; +const ctx = { known: new Set(["Button"]), ids: new Set(["001", "002"]), self: "002" }; + +describe("index facet schema", () => { + it("accepts a complete block", () => { + expect(checkIndexFacets(ok, ctx)).toEqual([]); + }); + + it("rejects a subtype that does not belong to the kind", () => { + expect(checkIndexFacets({ ...ok, subtype: "input" }, ctx)[0]).toContain('for kind "section"'); + expect(checkIndexFacets({ ...ok, kind: "nope" }, ctx)[0]).toContain("index.kind"); + }); + + it("rejects values outside the closed vocabulary, empty and duplicate lists", () => { + expect(checkIndexFacets({ ...ok, industry: ["banking"] }, ctx)).toEqual([ + expect.stringContaining('"banking" is not one of'), + ]); + expect(checkIndexFacets({ ...ok, style: [] }, ctx)[0]).toContain("index.style: at least 1"); + expect(checkIndexFacets({ ...ok, tech: ["css", "css"] }, ctx)).toContain( + "index.tech: duplicate value" + ); + expect(checkIndexFacets({ ...ok, layout: "masonry" }, ctx)[0]).toContain("index.layout"); + expect(checkIndexFacets({ ...ok, colorMode: "auto" }, ctx)[0]).toContain("index.colorMode"); + }); + + it("rejects unknown components, dangling or self related ids and uppercase aliases", () => { + expect(checkIndexFacets({ ...ok, components: ["Nope"] }, ctx)).toEqual([ + 'index.components: "Nope" is not a library component', + ]); + expect(checkIndexFacets({ ...ok, related: ["999"] }, ctx)).toEqual([ + 'index.related: "999" is not a catalog id', + ]); + expect(checkIndexFacets({ ...ok, related: ["002"] }, ctx)).toEqual([ + "index.related: a demo cannot relate to itself", + ]); + expect(checkIndexFacets({ ...ok, aliases: ["Plans"] }, ctx)[0]).toContain("index.aliases"); + }); + + it("rejects a bad date, a missing a11y flag and contradictory motion/cost/tech", () => { + expect(checkIndexFacets({ ...ok, added: "25/10/2026" }, ctx)[0]).toContain("index.added"); + expect(checkIndexFacets({ ...ok, added: "2026-13-45" }, ctx)[0]).toContain("index.added"); + expect(checkIndexFacets({ ...ok, a11y: { reducedMotion: true } }, ctx)[0]).toContain( + "index.a11y" + ); + expect(checkIndexFacets({ ...ok, motion: "none", cost: "gpu" }, ctx)).toContain( + 'index: motion "none" cannot cost "gpu"' + ); + expect(checkIndexFacets({ ...ok, tech: ["webgl"], cost: "js-light" }, ctx)).toContain( + 'index: tech "webgl" costs "gpu"' + ); + }); + + it("rejects unknown keys and non-objects", () => { + expect(checkIndexFacets({ ...ok, mood: "calm" }, ctx)).toEqual(['index: unknown key "mood"']); + expect(checkIndexFacets([], ctx)).toEqual(["index: object required"]); + }); + + it("checkMeta validates the block when a sidecar carries one", () => { + const meta = { + summary: "A pricing card with a button.", + use: "Plan pickers on a pricing page.", + spec: ["`Button`"], + tags: ["pricing", "button"], + shape: "wide", + }; + expect(checkMeta({ ...meta, index: ok }, " + + + + +``` diff --git a/site/public/gallery/002.md b/site/public/gallery/002.md new file mode 100644 index 00000000..21c64768 --- /dev/null +++ b/site/public/gallery/002.md @@ -0,0 +1,52 @@ + +# #002 With icons + +Buttons with an icon before the label, after the label, and with a ghost download action. + +When to use: Actions that benefit from an icon cue: favourite, continue to next step, download. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: hover, press, keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Button +- Also searched as: icon button, button with icon + +## Spec + +- Leading icon goes in `{#snippet leading()}` (`Star` from `@lucide/svelte`, `size={15}`) +- Trailing icon goes in `{#snippet trailing()}` (`ArrowRight`, `size={15}`) +- Variants shown: `variant="filled"`, `variant="outlined"`, `variant="ghost"` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/button#with-icons +Reference: https://ui.codeatlas.com.br/api/v1/en/e/002.txt + +## Source + +```svelte + + + + + +``` diff --git a/site/public/gallery/003.md b/site/public/gallery/003.md new file mode 100644 index 00000000..32d2ad74 --- /dev/null +++ b/site/public/gallery/003.md @@ -0,0 +1,52 @@ + +# #003 Loading + +Buttons in the loading state, including one that toggles loading for two seconds when clicked. + +When to use: Submit and save buttons that wait on a request and must block repeat clicks while keeping their label. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Button +- Also searched as: spinner, pending, busy + +## Spec + +- `loading` on `variant="filled"` and `variant="outlined"` shows the spinner in place of the leading icon +- Bound to state: `loading={saving}` with `let saving = $state(false)` +- `onclick={save}` sets `saving = true` and resets it with `setTimeout(() => (saving = false), 2000)` +- Label switches with state: `{saving ? 'Saving…' : 'Save (2s)'}` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/button#loading +Reference: https://ui.codeatlas.com.br/api/v1/en/e/003.txt + +## Source + +```svelte + + + + + +``` diff --git a/site/public/gallery/004.md b/site/public/gallery/004.md new file mode 100644 index 00000000..59b0b770 --- /dev/null +++ b/site/public/gallery/004.md @@ -0,0 +1,42 @@ + +# #004 Sizes + +The three button sizes side by side: small, medium and large. + +When to use: Choosing button density: `sm` in toolbars and tables, `md` by default, `lg` for hero or touch-first actions. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: hover, press, keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Button +- Also searched as: small medium large + +## Spec + +- `size="sm"` labelled Small +- `size="md"` labelled Medium +- `size="lg"` labelled Large + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/button#sizes +Reference: https://ui.codeatlas.com.br/api/v1/en/e/004.txt + +## Source + +```svelte + + + + + +``` diff --git a/site/public/gallery/005.md b/site/public/gallery/005.md new file mode 100644 index 00000000..56814b17 --- /dev/null +++ b/site/public/gallery/005.md @@ -0,0 +1,48 @@ + +# #005 States + +Disabled buttons, a destructive action with an icon, and a link rendered as a button. + +When to use: Showing non-interactive and dangerous actions, or navigating with button styling via `href`. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: hover, press, keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Button +- Also searched as: inactive, danger, link + +## Spec + +- `disabled` on `variant="filled"`, `variant="outlined"` and `variant="glass"` +- `variant="destructive"` with a `Trash2` icon in `{#snippet leading()}` (`size={15}`) +- `href="https://github.com/codeatlasdev/ui"` with `target="_blank"` renders an anchor + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/button#states +Reference: https://ui.codeatlas.com.br/api/v1/en/e/005.txt + +## Source + +```svelte + + + + + + + +``` diff --git a/site/public/gallery/006.md b/site/public/gallery/006.md new file mode 100644 index 00000000..e06f53e0 --- /dev/null +++ b/site/public/gallery/006.md @@ -0,0 +1,46 @@ + +# #006 Variants + +All five button variants in a row: filled, glass, outlined, ghost and destructive. + +When to use: Picking the emphasis of an action: filled for primary, outlined or ghost for secondary, destructive for irreversible. + +## Facets + +- Kind: component / input +- Style: default, glassmorphism +- Motion: subtle +- Interaction: hover, press, keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Button +- Also searched as: button styles, primary secondary + +## Spec + +- `variant="filled"` labelled Filled +- `variant="glass"` labelled Glass +- `variant="outlined"` labelled Outlined +- `variant="ghost"` labelled Ghost +- `variant="destructive"` labelled Destructive + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/button#variants +Reference: https://ui.codeatlas.com.br/api/v1/en/e/006.txt + +## Source + +```svelte + + + + + + + +``` diff --git a/site/public/gallery/007.md b/site/public/gallery/007.md new file mode 100644 index 00000000..23d94d83 --- /dev/null +++ b/site/public/gallery/007.md @@ -0,0 +1,43 @@ + +# #007 Disabled + +Three disabled checkboxes: unchecked, checked and indeterminate. + +When to use: Showing a read-only option whose value the user cannot change, such as a setting locked by plan or permission. + +## Facets + +- Kind: component / input +- Style: default +- Motion: none +- Interaction: none +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: not interactive +- Components: Checkbox +- Also searched as: tick box + +## Spec + +- `disabled` with `label="Disabled unchecked"` +- `checked disabled` with `label="Disabled checked"` +- `indeterminate disabled` with `label="Disabled indeterminate"` +- Three ` + import { Checkbox } from '@codeatlasdev/ui'; + + + + + +``` diff --git a/site/public/gallery/008.md b/site/public/gallery/008.md new file mode 100644 index 00000000..652cd762 --- /dev/null +++ b/site/public/gallery/008.md @@ -0,0 +1,46 @@ + +# #008 States + +Checkboxes in unchecked, pre-checked and indeterminate states, the first two bound to local state. + +When to use: Boolean options in a form, such as terms acceptance or feature toggles, including a mixed state for partial selection. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Checkbox +- Also searched as: tick box, tri-state + +## Spec + +- `bind:checked={basic}` with `let basic = $state(false)` and `label="Unchecked (click me)"` +- `bind:checked` bound to `let checked = $state(true)` with `label="Pre-checked"` +- `indeterminate` with `label="Indeterminate"` +- Three ` + import { Checkbox } from '@codeatlasdev/ui'; + + let basic = $state(false); + let checked = $state(true); + + + + + +``` diff --git a/site/public/gallery/009.md b/site/public/gallery/009.md new file mode 100644 index 00000000..f6f8d7c1 --- /dev/null +++ b/site/public/gallery/009.md @@ -0,0 +1,45 @@ + +# #009 Without label + +Three checkboxes with no visible label, named only through aria-label: bound, checked and indeterminate. + +When to use: Row selection checkboxes in tables or lists where the label is implied by the row and only an accessible name is needed. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Checkbox +- Also searched as: unlabelled, aria label only + +## Spec + +- `bind:checked={value}` with `aria-label="Unlabelled checkbox"` +- `checked` with `aria-label="Unlabelled checked checkbox"` +- `indeterminate` with `aria-label="Unlabelled indeterminate checkbox"` +- No `label` prop on any of the three + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/checkbox#without-label +Reference: https://ui.codeatlas.com.br/api/v1/en/e/009.txt + +## Source + +```svelte + + + + + +``` diff --git a/site/public/gallery/010.md b/site/public/gallery/010.md new file mode 100644 index 00000000..e503cdf2 --- /dev/null +++ b/site/public/gallery/010.md @@ -0,0 +1,73 @@ + +# #010 Basic + +A searchable Combobox over eight frameworks that prints the selected value below the field. + +When to use: Picking one item from a long list where typing to filter is faster than scrolling a select. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: keyboard, press +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Combobox +- Also searched as: autocomplete, typeahead, searchable select + +## Spec + +- `` +- `const frameworks` has 8 options with `value` and `label` keys, from `svelte` to `sveltekit` +- `let selected = $state('')` starts empty +- `{#if selected}` renders `Selected:` with the value in `` +- Wrapper `.col` has `max-width: 320px` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/combobox#basic +Reference: https://ui.codeatlas.com.br/api/v1/en/e/010.txt + +## Source + +```svelte + + +
+ + {#if selected} +

Selected: {selected}

+ {/if} +
+ + +``` diff --git a/site/public/gallery/011.md b/site/public/gallery/011.md new file mode 100644 index 00000000..d04624d9 --- /dev/null +++ b/site/public/gallery/011.md @@ -0,0 +1,72 @@ + +# #011 States + +Three Combobox states: with description and custom search and empty texts, with error and required, and disabled. + +When to use: Documenting validation, helper text and unavailable states of a searchable select in a form. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: keyboard, press +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Combobox +- Also searched as: validation, empty results + +## Spec + +- `frameworks` has 4 options, the last is `{ value: 'astro', label: 'Astro', disabled: true }` +- First field: `bind:value`, `description="Astro is disabled."`, `searchPlaceholder="Filter frameworks…"`, `emptyText="No framework matches."` +- Second field: `error="Pick a framework."` and `required` +- Third field: `disabled` with `placeholder="Unavailable"` +- Wrapper `.col` has `max-width: 320px` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/combobox#states +Reference: https://ui.codeatlas.com.br/api/v1/en/e/011.txt + +## Source + +```svelte + + +
+ + + +
+ + +``` diff --git a/site/public/gallery/012.md b/site/public/gallery/012.md new file mode 100644 index 00000000..b94c2b3f --- /dev/null +++ b/site/public/gallery/012.md @@ -0,0 +1,39 @@ + +# #012 Default + +A default CopyButton that copies an npm install command. + +When to use: Next to install commands, tokens or IDs so users can copy them in one click. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: CopyButton +- Also searched as: clipboard, copy to clipboard + +## Spec + +- `` +- Only the `value` prop is set, no other props + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/copy-button#default +Reference: https://ui.codeatlas.com.br/api/v1/en/e/012.txt + +## Source + +```svelte + + + +``` diff --git a/site/public/gallery/013.md b/site/public/gallery/013.md new file mode 100644 index 00000000..3bee2f31 --- /dev/null +++ b/site/public/gallery/013.md @@ -0,0 +1,42 @@ + +# #013 Icon only and toast + +An icon-only CopyButton and a labelled one that shows a toast after copying, with a Toaster mounted. + +When to use: Copying links or snippets where a compact icon is enough or where a visible confirmation toast is wanted. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: CopyButton, Toaster +- Also searched as: copied toast + +## Spec + +- `` is rendered alongside the buttons +- `` is icon-only via an empty `label` +- `` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/copy-button#icon-only-and-toast +Reference: https://ui.codeatlas.com.br/api/v1/en/e/013.txt + +## Source + +```svelte + + + + + +``` diff --git a/site/public/gallery/014.md b/site/public/gallery/014.md new file mode 100644 index 00000000..ac258ac5 --- /dev/null +++ b/site/public/gallery/014.md @@ -0,0 +1,46 @@ + +# #014 Variants and sizes + +CopyButton in three variants and two extra sizes, side by side. + +When to use: Choosing the emphasis and size of a copy action to match the surrounding UI. + +## Facets + +- Kind: component / input +- Style: default, glassmorphism +- Motion: subtle +- Interaction: hover, press, keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: CopyButton +- Also searched as: clipboard + +## Spec + +- `variant="filled"` with `value="filled"` +- `variant="glass"` with `value="glass"` +- `variant="outlined"` with `value="outlined"` +- `size="sm"` with `value="small"` +- `size="lg"` with `value="large"` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/copy-button#variants-and-sizes +Reference: https://ui.codeatlas.com.br/api/v1/en/e/014.txt + +## Source + +```svelte + + + + + + + +``` diff --git a/site/public/gallery/015.md b/site/public/gallery/015.md new file mode 100644 index 00000000..29eabc98 --- /dev/null +++ b/site/public/gallery/015.md @@ -0,0 +1,79 @@ + +# #015 Calendar and RangeCalendar + +An inline single-date Calendar and a RangeCalendar, each printing its selected value below. + +When to use: Always-visible date selection such as booking a stay or choosing an event day without a popover. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: grid +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Calendar, RangeCalendar +- Also searched as: month grid, date range, inline calendar + +## Spec + +- `` +- `let value = $state(today(getLocalTimeZone()))` preselects today +- `` +- `let range = $state({ start: undefined, end: undefined })` starts empty +- Output lines show `fmt(value)`, `fmt(range.start)` and `fmt(range.end)`, with `–` when unset + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/date-picker#calendar-and-rangecalendar +Reference: https://ui.codeatlas.com.br/api/v1/en/e/015.txt + +## Source + +```svelte + + +
+ +

value: {fmt(value)}

+
+ +
+ +

start: {fmt(range.start)} · end: {fmt(range.end)}

+
+ + +``` diff --git a/site/public/gallery/016.md b/site/public/gallery/016.md new file mode 100644 index 00000000..d21a5d5d --- /dev/null +++ b/site/public/gallery/016.md @@ -0,0 +1,74 @@ + +# #016 Min, max, error and disabled + +Date and time inputs with constraints: a bounded DatePicker, a required DatePicker with an error, a disabled DateField and a read-only TimeField. + +When to use: Forms that limit selectable dates, require a date of birth or show locked date and time values. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: DateField, DatePicker, TimeField +- Also searched as: min max date, bounded, readonly + +## Spec + +- `minValue={min}` and `maxValue={max}` with `const max = today(tz).add({ days: 30 })` and `description="Only the next 30 days."` +- `required` with `error={requiredError}` derived as `'Enter your date of birth.'` while empty +- `` +- `` +- Wrapper `.col` has `max-width: 400px` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/date-picker#min-max-error-and-disabled +Reference: https://ui.codeatlas.com.br/api/v1/en/e/016.txt + +## Source + +```svelte + + +
+ + + + +
+ + +``` diff --git a/site/public/gallery/017.md b/site/public/gallery/017.md new file mode 100644 index 00000000..844138a4 --- /dev/null +++ b/site/public/gallery/017.md @@ -0,0 +1,68 @@ + +# #017 DateField and TimeField + +Segmented DateField and TimeField inputs with bound values, minute granularity and 24h and 12h hour cycles. + +When to use: Typing a date or time directly in a form, such as a birthday or an appointment start time. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: DateField, TimeField +- Also searched as: segmented date, time input, 12 hour 24 hour + +## Spec + +- `` initialised with `new CalendarDate(2026, 10, 10)` +- `` +- `` initialised with `new Time(14, 30)` +- `` +- Two `value:` output lines print `birthday.toString()` and `time.toString()` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/date-picker#datefield-and-timefield +Reference: https://ui.codeatlas.com.br/api/v1/en/e/017.txt + +## Source + +```svelte + + +
+ + +

value: {birthday ? birthday.toString() : '–'}

+ + +

value: {time ? time.toString() : '–'}

+
+ + +``` diff --git a/site/public/gallery/018.md b/site/public/gallery/018.md new file mode 100644 index 00000000..6caa1668 --- /dev/null +++ b/site/public/gallery/018.md @@ -0,0 +1,81 @@ + +# #018 DatePicker and DateRangePicker + +A DatePicker and a DateRangePicker with popover calendars, each printing its selected value. + +When to use: Choosing a delivery date or a reporting period where the user can type or pick from a calendar popover. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: DatePicker, DateRangePicker +- Also searched as: calendar popover, date range, datepicker + +## Spec + +- `()` +- `` +- `let range = $state({ start: undefined, end: undefined })` +- Wrappers: `.col` has `max-width: 400px`, `.col--wide` has `max-width: 460px` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/date-picker#datepicker-and-daterangepicker +Reference: https://ui.codeatlas.com.br/api/v1/en/e/018.txt + +## Source + +```svelte + + +
+ +

value: {fmt(picked)}

+
+ +
+ +

start: {fmt(range.start)} · end: {fmt(range.end)}

+
+ + +``` diff --git a/site/public/gallery/019.md b/site/public/gallery/019.md new file mode 100644 index 00000000..4f16b187 --- /dev/null +++ b/site/public/gallery/019.md @@ -0,0 +1,62 @@ + +# #019 Default + +An ExportMenu exporting three domain rows with custom column headers and a status line reporting the last export. + +When to use: Table toolbars that let users download the current data set as a file in a chosen format. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: ExportMenu +- Also searched as: download, csv, xlsx, dropdown + +## Spec + +- `data={rows}` with `const rows` of 3 objects with `domain`, `price` and `available` fields +- `filename="domains"` +- `columns` has 3 entries with `header` `Domain`, `Price (USD)` and `Available` +- `onexport` sets `last` to the exported `e.filename` and `e.rows` count +- Idle status text is `Pick a format to download.` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/export-menu#default +Reference: https://ui.codeatlas.com.br/api/v1/en/e/019.txt + +## Source + +```svelte + + +
+ (last = `${e.filename}: ${e.rows} rows`)} + /> +

{last || 'Pick a format to download.'}

+
+ + +``` diff --git a/site/public/gallery/020.md b/site/public/gallery/020.md new file mode 100644 index 00000000..f5cce826 --- /dev/null +++ b/site/public/gallery/020.md @@ -0,0 +1,68 @@ + +# #020 Basic + +A multi-file FileUpload with a size limit and hint that reports how many files are selected. + +When to use: Attachment and document upload areas that accept several files and cap their size. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, drag, keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: FileUpload +- Also searched as: dropzone, attachment, drag and drop + +## Spec + +- `bind:files` with `let files = $state([])` +- `multiple` +- `hint="PNG, JPG, PDF up to 10 MB"` +- `maxSize={10 * 1024 * 1024}` +- `{#if files.length > 0}` shows the selected file count with a plural `s` +- Wrapper `.wrap` has `max-width: 480px` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/file-upload#basic +Reference: https://ui.codeatlas.com.br/api/v1/en/e/020.txt + +## Source + +```svelte + + +
+ + {#if files.length > 0} +

{files.length} file{files.length > 1 ? 's' : ''} selected

+ {/if} +
+ + +``` diff --git a/site/public/gallery/021.md b/site/public/gallery/021.md new file mode 100644 index 00000000..e21ce61d --- /dev/null +++ b/site/public/gallery/021.md @@ -0,0 +1,53 @@ + +# #021 Images only and disabled + +Two FileUpload dropzones: one restricted to images with custom label and hint, and one disabled. + +When to use: Image-only upload fields and upload areas that are unavailable to the current user. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, drag, keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: FileUpload +- Also searched as: image upload, drop area + +## Spec + +- `accept="image/*"` with `label="Drop images here"` and `hint="PNG, JPG, GIF, WebP"` +- `disabled` with `label="Upload unavailable"` +- Wrapper `.wrap` stacks both with `max-width: 480px` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/file-upload#images-only-and-disabled +Reference: https://ui.codeatlas.com.br/api/v1/en/e/021.txt + +## Source + +```svelte + + +
+ + +
+ + +``` diff --git a/site/public/gallery/022.md b/site/public/gallery/022.md new file mode 100644 index 00000000..08dbad96 --- /dev/null +++ b/site/public/gallery/022.md @@ -0,0 +1,49 @@ + +# #022 Role filter + +A FilterGroup of three role options bound to a value that is printed below. + +When to use: Filtering a list or table by one category, such as user role, with a short set of choices. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: single-column +- Color mode: both +- Density: compact +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: FilterGroup +- Also searched as: chips, category filter, pill filter + +## Spec + +- `` +- `const roles` has 3 options with `value` and `label`: `admin`, `editor`, `viewer` +- `let role = $state('all')` starts at `all`, which is not one of the options +- `

Selected: {role}

` prints the current value + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/filter-group#role-filter +Reference: https://ui.codeatlas.com.br/api/v1/en/e/022.txt + +## Source + +```svelte + + + +

Selected: {role}

+``` diff --git a/site/public/gallery/023.md b/site/public/gallery/023.md new file mode 100644 index 00000000..bf88b8d9 --- /dev/null +++ b/site/public/gallery/023.md @@ -0,0 +1,93 @@ + +# #023 Documents and dates + +Five MaskedField inputs for Brazilian documents: CPF, CNPJ, combined CPF or CNPJ, birth date and RG, with validators on three. + +When to use: Brazilian registration and checkout forms that collect formatted identity documents and dates. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: MaskedField +- Also searched as: cpf, cnpj, input mask, id number + +## Spec + +- `mask={maskCPF}` with `validate={(v) => (isValidCPF(v) ? undefined : 'Invalid CPF')}` and `placeholder="000.000.000-00"` +- `mask={maskCNPJ}` with `validate={(v) => (isValidCNPJ(v) ? undefined : 'Invalid CNPJ')}` and `placeholder="00.000.000/0000-00"` +- `mask={maskCPFCNPJ}` with `description="Switches mask after 11 digits."` +- `mask={maskDateBR}` with `isValidDateBR(v)` validation and `placeholder="dd/mm/yyyy"` +- `mask={maskRG}` with `placeholder="00.000.000-0"` and no `validate` +- Imports come from `@codeatlasdev/ui/forms`, each field uses `bind:value` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/masked-field#documents-and-dates +Reference: https://ui.codeatlas.com.br/api/v1/en/e/023.txt + +## Source + +```svelte + + +
+ (isValidCPF(v) ? undefined : 'Invalid CPF')} + bind:value={cpf} + /> + (isValidCNPJ(v) ? undefined : 'Invalid CNPJ')} + bind:value={cnpj} + /> + + (isValidDateBR(v) ? undefined : 'Invalid date')} + bind:value={birth} + /> + +
+ + +``` diff --git a/site/public/gallery/024.md b/site/public/gallery/024.md new file mode 100644 index 00000000..df8ae7b5 --- /dev/null +++ b/site/public/gallery/024.md @@ -0,0 +1,79 @@ + +# #024 use:mask action + +A native input masked with the mask action, showing the bound value below. + +When to use: Adding input masking to a custom or third-party input that is not a MaskedField. + +## Facets + +- Kind: component / input +- Style: default +- Motion: none +- Interaction: keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: none +- Also searched as: input mask, action, format as you type + +## Spec + +- `use:mask={maskCPF}` on a plain `` is linked to `id="mask-action-input"` +- Output line prints `value = {value || '–'}` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/masked-field#use-mask-action +Reference: https://ui.codeatlas.com.br/api/v1/en/e/024.txt + +## Source + +```svelte + + +
+ + +

value = {value || '–'}

+
+ + +``` diff --git a/site/public/gallery/025.md b/site/public/gallery/025.md new file mode 100644 index 00000000..32aaa4da --- /dev/null +++ b/site/public/gallery/025.md @@ -0,0 +1,86 @@ + +# #025 Phone and CEP + +A PhoneField and a CepField that looks up an address asynchronously and prints the resolved street, district and city. + +When to use: Brazilian address and contact forms that autofill street data from a postal code lookup. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: CepField, PhoneField +- Also searched as: phone, postal code, address lookup, zip + +## Spec + +- `` +- ` (address = a)}` +- `async function lookup(raw: string)` waits 600 ms and returns a `CepAddress` only for `01310100`, otherwise `null` +- Result line shows `Looking up…` while pending, then `logradouro`, `bairro`, `localidade` and `uf` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/masked-field#phone-and-cep +Reference: https://ui.codeatlas.com.br/api/v1/en/e/025.txt + +## Source + +```svelte + + +
+ +

value = {phone || '–'}

+ (address = a)} + /> +

+ {#if pending}Looking up…{:else if address}{address.logradouro}, {address.bairro} · {address.localidade}/{address.uf}{:else}–{/if} +

+
+ + +``` diff --git a/site/public/gallery/026.md b/site/public/gallery/026.md new file mode 100644 index 00000000..bf87c099 --- /dev/null +++ b/site/public/gallery/026.md @@ -0,0 +1,66 @@ + +# #026 Min, max, negative, error and disabled + +Four MoneyInput fields showing min and max bounds, negative values, an error message and a disabled state. + +When to use: Amount fields in payment and ledger forms that limit ranges, allow adjustments or lock a value. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: MoneyInput +- Also searched as: currency input, min max, amount + +## Spec + +- `label="Deposit"` with `min={1000}`, `max={10000}` and `description="Between R$ 10,00 and R$ 100,00."` +- `` with `let adjustment = $state(-2500)` +- `` +- `` +- Wrapper `.col` has `max-width: 400px` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/money-input#min-max-negative-error-and-disabled +Reference: https://ui.codeatlas.com.br/api/v1/en/e/026.txt + +## Source + +```svelte + + +
+ + + + +
+ + +``` diff --git a/site/public/gallery/027.md b/site/public/gallery/027.md new file mode 100644 index 00000000..310c2bf4 --- /dev/null +++ b/site/public/gallery/027.md @@ -0,0 +1,57 @@ + +# #027 Currency and locale + +MoneyInput in three currencies and locales: USD en-US, JPY without decimals and EUR de-DE. + +When to use: Price fields in multi-currency products that must format and parse per locale. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: MoneyInput +- Also searched as: locale, price input, multi currency + +## Spec + +- `currency="USD" locale="en-US"` with `bind:value={usd}` starting at `$state(123456)` +- `currency="JPY" locale="en-US"` with `bind:value={jpy}` starting at `$state(1234)` +- `currency="EUR" locale="de-DE"` with `bind:value={eur}` starting at `$state(123456)` +- Labels: `Price (USD, en-US)`, `Price (JPY, no decimals)` and `Preis (EUR, de-DE)` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/money-input#currency-and-locale +Reference: https://ui.codeatlas.com.br/api/v1/en/e/027.txt + +## Source + +```svelte + + +
+ + + +
+ + +``` diff --git a/site/public/gallery/028.md b/site/public/gallery/028.md new file mode 100644 index 00000000..e810c61b --- /dev/null +++ b/site/public/gallery/028.md @@ -0,0 +1,63 @@ + +# #028 Default + +A default MoneyInput bound to a cents value, with the raw cents and formatted display printed below. + +When to use: A price or amount field where the model stores integer cents and the UI shows a formatted amount. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: MoneyInput +- Also searched as: price input, cents, currency + +## Spec + +- `` +- `let price = $state(123456)` holds cents +- Output line prints `{price} cents → {centsToDisplay(price)}` +- Wrapper `.col` has `max-width: 400px` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/money-input#default +Reference: https://ui.codeatlas.com.br/api/v1/en/e/028.txt + +## Source + +```svelte + + +
+ +

value = {price} cents → {centsToDisplay(price)}

+
+ + +``` diff --git a/site/public/gallery/029.md b/site/public/gallery/029.md new file mode 100644 index 00000000..9e884b33 --- /dev/null +++ b/site/public/gallery/029.md @@ -0,0 +1,59 @@ + +# #029 Helpers + +Two money helper functions evaluated inline: centsToDisplay and parseInputToCents. + +When to use: Formatting stored cents for display or parsing user-typed currency strings outside a MoneyInput. + +## Facets + +- Kind: component / input +- Style: default +- Motion: none +- Interaction: none +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: not interactive +- Components: none +- Also searched as: format, parse, utility + +## Spec + +- `centsToDisplay(123456)` is printed with its result +- `parseInputToCents('R$ 1.234,56')` is printed with its result +- Both helpers are imported from `@codeatlasdev/ui/forms` +- No input component is rendered + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/money-input#helpers +Reference: https://ui.codeatlas.com.br/api/v1/en/e/029.txt + +## Source + +```svelte + + +
+

centsToDisplay(123456) = {centsToDisplay(123456)}

+

parseInputToCents("R$ 1.234,56") = {parseInputToCents('R$ 1.234,56')}

+
+ + +``` diff --git a/site/public/gallery/030.md b/site/public/gallery/030.md new file mode 100644 index 00000000..cd48a7be --- /dev/null +++ b/site/public/gallery/030.md @@ -0,0 +1,46 @@ + +# #030 Basic + +Two NumberField inputs in a Stack: an integer quantity with bounds and a decimal price with a step. + +When to use: Quantity and price steppers in order or cart forms. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: NumberField, Stack +- Also searched as: stepper, quantity, spinbutton + +## Spec + +- `` wraps both fields +- `` with `let qty = $state(1)` +- `` with `let price = $state(9.99)` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/number-field#basic +Reference: https://ui.codeatlas.com.br/api/v1/en/e/030.txt + +## Source + +```svelte + + + + + + +``` diff --git a/site/public/gallery/031.md b/site/public/gallery/031.md new file mode 100644 index 00000000..99777e6a --- /dev/null +++ b/site/public/gallery/031.md @@ -0,0 +1,50 @@ + +# #031 States + +Four NumberField states in a Stack: with description, with error and required, disabled, and small. + +When to use: Documenting helper text, validation, locked and compact variants of numeric fields such as seat counts. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: NumberField, Stack +- Also searched as: stepper, validation, seats + +## Spec + +- `` +- `bind:value={seats}` with `let seats = $state(60)`, `error="Plans support up to 50 seats."` and `required` +- `` +- `` +- `` wraps the four fields + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/number-field#states +Reference: https://ui.codeatlas.com.br/api/v1/en/e/031.txt + +## Source + +```svelte + + + + + + + + +``` diff --git a/site/public/gallery/032.md b/site/public/gallery/032.md new file mode 100644 index 00000000..257a438b --- /dev/null +++ b/site/public/gallery/032.md @@ -0,0 +1,64 @@ + +# #032 Default + +A verification-code PinInput that reports its value and completion, plus a masked four-digit PIN. + +When to use: One-time code entry for sign-in verification, or a short secret PIN that must be hidden. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: PinInput +- Also searched as: otp, verification code, one time code, passcode + +## Spec + +- ` (done = v)} />` +- Output line prints `value: {code || '–'} · completed: {done || '–'}` +- `` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/pin-input#default +Reference: https://ui.codeatlas.com.br/api/v1/en/e/032.txt + +## Source + +```svelte + + +
+
+ (done = v)} /> +

value: {code || '–'} · completed: {done || '–'}

+
+ +
+ + +``` diff --git a/site/public/gallery/033.md b/site/public/gallery/033.md new file mode 100644 index 00000000..6faae41b --- /dev/null +++ b/site/public/gallery/033.md @@ -0,0 +1,56 @@ + +# #033 States + +Four PinInput states: with description, with error, disabled with a partial value, and a text-type five-character key. + +When to use: Documenting helper text, error feedback and non-numeric code variants of one-time code inputs. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: PinInput +- Also searched as: otp, error state, access code + +## Spec + +- `description="Check your email."` with `bind:value={hinted}` starting at `$state('123')` +- `error="Invalid or expired code."` with `value="123456"` +- `disabled` with `value="12"` +- `` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/pin-input#states +Reference: https://ui.codeatlas.com.br/api/v1/en/e/033.txt + +## Source + +```svelte + + +
+ + + + +
+ + +``` diff --git a/site/public/gallery/034.md b/site/public/gallery/034.md new file mode 100644 index 00000000..0c81b8a5 --- /dev/null +++ b/site/public/gallery/034.md @@ -0,0 +1,49 @@ + +# #034 Cards + +Radio group in `cards` variant with three plans, one disabled, bound to a selected plan. + +When to use: Choosing a subscription plan or any option that needs a title and a short description. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: RadioGroup +- Also searched as: plan picker, selectable cards, pricing choice + +## Spec + +- ` + import { RadioGroup } from '@codeatlasdev/ui'; + + let plan = $state('pro'); + + const plans = [ + { value: 'free', label: 'Free', description: 'For side projects.' }, + { value: 'pro', label: 'Pro', description: 'For teams that ship weekly.' }, + { value: 'team', label: 'Team', description: 'Shared billing and audit log.', disabled: true }, + ]; + + + +``` diff --git a/site/public/gallery/035.md b/site/public/gallery/035.md new file mode 100644 index 00000000..24acb022 --- /dev/null +++ b/site/public/gallery/035.md @@ -0,0 +1,56 @@ + +# #035 Disabled + +Two radio groups: one with a single disabled item and one fully disabled group. + +When to use: Showing unavailable choices in a form: one option locked, or a whole question locked. + +## Facets + +- Kind: component / input +- Style: default +- Motion: none +- Interaction: keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: RadioGroup +- Also searched as: inactive options + +## Spec + +- First group `items={yesNo}` with `orientation="horizontal"` and `bind:value={answer}` +- `yesNo` has Yes, No, Maybe, and Maybe carries `disabled: true` +- Second group `items={fruits}` with `value="banana"` and `disabled` on the whole group +- `fruits` has 3 items: Apple, Banana, Cherry + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/radio#disabled +Reference: https://ui.codeatlas.com.br/api/v1/en/e/035.txt + +## Source + +```svelte + + + + +``` diff --git a/site/public/gallery/036.md b/site/public/gallery/036.md new file mode 100644 index 00000000..f78349fe --- /dev/null +++ b/site/public/gallery/036.md @@ -0,0 +1,48 @@ + +# #036 Horizontal + +Radio group laid out in a row with Small, Medium and Large options. + +When to use: Short choice lists of 2 to 4 options, such as a size picker, where a row fits the layout. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: RadioGroup +- Also searched as: inline radio, row + +## Spec + +- ` + import { RadioGroup } from '@codeatlasdev/ui'; + + let size = $state('md'); + + const sizes = [ + { value: 'sm', label: 'Small' }, + { value: 'md', label: 'Medium' }, + { value: 'lg', label: 'Large' }, + ]; + + + +``` diff --git a/site/public/gallery/037.md b/site/public/gallery/037.md new file mode 100644 index 00000000..257a358e --- /dev/null +++ b/site/public/gallery/037.md @@ -0,0 +1,59 @@ + +# #037 Vertical + +Default vertical radio group of three fruits with the selected value printed below. + +When to use: Single choice from a short list in a form or settings panel. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: RadioGroup +- Also searched as: option list, single choice + +## Spec + +- `{fruit}
` under the group + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/radio#vertical +Reference: https://ui.codeatlas.com.br/api/v1/en/e/037.txt + +## Source + +```svelte + + + +

Selected: {fruit}

+ + +``` diff --git a/site/public/gallery/038.md b/site/public/gallery/038.md new file mode 100644 index 00000000..59795132 --- /dev/null +++ b/site/public/gallery/038.md @@ -0,0 +1,80 @@ + +# #038 Default + +Two segmented controls: a text-only Week/Month/Year period and a List/Grid view switch with icons. + +When to use: Switching a view mode or time range inline above a list, chart or dashboard. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: SegmentedControl +- Also searched as: tabs switch, view toggle, period selector + +## Spec + +- Period control `aria-label="Period"` with `bind:value={period}` and 3 items: Week, Month, Year +- Current value is shown as `value: {period}` +- View control `aria-label="View"` with 2 items using `icon: LayoutList` and `icon: LayoutGrid` +- Icons come from `@lucide/svelte` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/segmented-control#default +Reference: https://ui.codeatlas.com.br/api/v1/en/e/038.txt + +## Source + +```svelte + + +
+
+ +

value: {period}

+
+ +
+ + +``` diff --git a/site/public/gallery/039.md b/site/public/gallery/039.md new file mode 100644 index 00000000..31191121 --- /dev/null +++ b/site/public/gallery/039.md @@ -0,0 +1,64 @@ + +# #039 Small, full width, disabled item + +Small full-width segmented control with four items, one disabled. + +When to use: Compact filter bar that should stretch to its container, such as a table toolbar. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: single-column +- Color mode: both +- Density: compact +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: SegmentedControl +- Also searched as: full width switch, disabled segment + +## Spec + +- ` + import { SegmentedControl } from '@codeatlasdev/ui'; + + let size = $state('b'); + + +
+ +
+ + +``` diff --git a/site/public/gallery/040.md b/site/public/gallery/040.md new file mode 100644 index 00000000..2a5669c7 --- /dev/null +++ b/site/public/gallery/040.md @@ -0,0 +1,76 @@ + +# #040 Basic + +Select with five framework options and the chosen value printed below. + +When to use: Picking one value from a known list in a form, such as a framework or category. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Select +- Also searched as: dropdown, picker, listbox + +## Spec + +- ` +

Selected: {framework}

+ + + +``` diff --git a/site/public/gallery/041.md b/site/public/gallery/041.md new file mode 100644 index 00000000..9166ab2d --- /dev/null +++ b/site/public/gallery/041.md @@ -0,0 +1,65 @@ + +# #041 Empty-value option + +Select whose first option has an empty value labelled All projects, with the raw value printed. + +When to use: Filters where an empty value means no filter, such as All projects. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Select +- Also searched as: all option, filter dropdown, reset option + +## Spec + +- ` +

Filter value: {JSON.stringify(project)}

+ + + +``` diff --git a/site/public/gallery/042.md b/site/public/gallery/042.md new file mode 100644 index 00000000..9220d3ca --- /dev/null +++ b/site/public/gallery/042.md @@ -0,0 +1,59 @@ + +# #042 Placeholder + +Select with no initial value that shows placeholder text until a country is chosen. + +When to use: Required choice in a form where no default should be preselected. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: single-column +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Select +- Also searched as: country picker, dropdown, empty state + +## Spec + +- ` + + + +``` diff --git a/site/public/gallery/043.md b/site/public/gallery/043.md new file mode 100644 index 00000000..d0e08207 --- /dev/null +++ b/site/public/gallery/043.md @@ -0,0 +1,71 @@ + +# #043 States + +Three selects showing a description, an error and a disabled state, with one disabled option. + +When to use: Showing helper text, validation errors and locked fields on dropdowns in a form. + +## Facets + +- Kind: component / input +- Style: default +- Motion: subtle +- Interaction: press, keyboard +- Layout: stack +- Color mode: both +- Density: regular +- Tech: css (cost: css) +- Reduced motion: handled; keyboard: operable +- Components: Select +- Also searched as: validation, disabled option, helper text + +## Spec + +- Plan select has `description="Enterprise is disabled."` and `bind:value={plan}` +- `plans` has Free, Pro, Enterprise, and Enterprise has `disabled: true` +- Region select has `error="Pick a region to continue."` and no bound value +- Size select has `disabled` with `bind:value={size}` +- 3 selects stacked in `.col` with `max-width: 320px` + +Needs: @codeatlasdev/ui@4.5.0 +Live demo: https://ui.codeatlas.com.br/docs/inputs/select#states +Reference: https://ui.codeatlas.com.br/api/v1/en/e/043.txt + +## Source + +```svelte + + +
+ +