diff --git a/README.md b/README.md
index 02f0c55..60fc3fd 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ custom properties.
pnpm add sve-ui
```
-➡️ **Usage, components and theming:** see the [package README](./packages/sve-ui/README.md).
+➡️ **Usage, all 60 components and theming:** see the [package README](./packages/sve-ui/README.md).
## Upgrading from 0.1.x
@@ -39,6 +39,7 @@ packages/
typescript-config/ # shared @repo/typescript-config
apps/
docs/ # documentation site (SvelteKit 2 + Tailwind 4) → sveui.org
+ example/ # a real app built with the library — dogfooding with latency
```
Shared dependency versions live in the pnpm **catalog** in `pnpm-workspace.yaml`
diff --git a/ROADMAP.md b/ROADMAP.md
index 4c1b3bc..982cde0 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -87,7 +87,7 @@ exported symbol, which handles both the bare and the explicitly-annotated form.
### 3. No visual regression coverage — PARTLY DONE
- [x] `apps/docs/scripts/check-render.mjs` — a structural render guard over all
- **65** prerendered pages, wired into CI after `build`. It compares a digest
+ **67** prerendered pages, wired into CI after `build`. It compares a digest
(ordered element skeleton plus attribute names, values and text dropped)
against a committed baseline, so copy edits are free while a changed tag,
lost attribute or reordered element fails. Normalises the things that churn
diff --git a/apps/docs/README.md b/apps/docs/README.md
index 7edb3a4..84a438c 100644
--- a/apps/docs/README.md
+++ b/apps/docs/README.md
@@ -1,41 +1,47 @@
-# create-svelte
+# docs
-Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
+The documentation site for `sve-ui` — [sveui.org](https://sveui.org). SvelteKit 2
+with Tailwind 4, fully prerendered, deployed on Vercel.
-## Creating a project
+Tailwind is used **here only**, for page chrome. It is deliberately absent from
+the library and from consumer projects; that is the whole pitch.
-If you're seeing this, you've probably already done this step. Congrats!
-
-```bash
-# create a new project in the current directory
-npm create svelte@latest
-
-# create a new project in my-app
-npm create svelte@latest my-app
+```sh
+pnpm dev # localhost:5173
+pnpm build # prerenders every route into .vercel/output/static
+pnpm preview
```
-## Developing
+## What is generated, and from what
-Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
+Nothing on this site states a fact twice. Anything that could drift is derived
+from a single source, because it already drifted once: a hardcoded "13
+components" shipped against a registry of 58.
-```bash
-npm run dev
+| Output | Source | Command |
+| ------------------------------ | --------------------------------------------------------------------------------------------------- | ---------------------------------- |
+| Prop tables | the library's TypeScript types, via the compiler's checker (so inherited Bits UI props resolve too) | `pnpm gen:props` — runs on `build` |
+| `/sitemap.xml` | `src/lib/docs/registry.ts` + `src/lib/docs/guides.ts` | prerendered on `build` |
+| `/llms.txt` | the same registry and guide nav | prerendered on `build` |
+| Component counts on every page | `readyComponents` from the registry | — |
+| `static/og.png` (social card) | `scripts/gen-og.mjs`, rendered with Playwright's Chromium | `pnpm gen:og` |
-# or start the server and open the app in a new browser tab
-npm run dev -- --open
-```
+`gen:og` is **not** part of `build` — the Vercel builder has no browser — so the
+PNG is committed. Re-run it after changing the tagline, the count or the
+branding.
-## Building
+## Guards
-To create a production version of your app:
-
-```bash
-npm run build
+```sh
+pnpm check:render # structural: element skeleton of all prerendered pages
+pnpm test:visual # appearance: screenshots of every live preview
+pnpm gen:props:check # fails if the committed prop data is stale
```
-You can preview the production build with `npm run preview`.
-
-> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
+`check:render` compares an ordered element skeleton (attribute values and text
+dropped) against `render-baseline.json`, so copy edits are free while a changed
+tag, a lost attribute or a reordered element fails. Accept an intended
+structural change with `pnpm check:render:update`.
## Visual regression
diff --git a/packages/sve-ui/README.md b/packages/sve-ui/README.md
index 6cfe51c..2a53db2 100644
--- a/packages/sve-ui/README.md
+++ b/packages/sve-ui/README.md
@@ -68,14 +68,26 @@ without it):
## Components
-**Display & form** — `Button`, `Input`, `Card`, `Badge`, `Avatar`, `Spinner`,
-`Text`, `Heading`, `Alert`.
-
-**Form controls** (on Bits UI) — `Switch`, `Checkbox`, `RadioGroup`.
-
-**Overlays** (on Bits UI) — `Dialog`, `DropdownMenu`, `Tooltip`, `Popover`.
-
-Most components take `variant`, `color` and `size` props, e.g.:
+**60 components**, every one styled and accessible out of the box. Names ending
+in `.*` are namespace compositions — import the namespace and compose its parts
+(`Dialog.Root`, `Dialog.Trigger`, `Dialog.Content`). The rest are default
+imports.
+
+| Group | Components |
+| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Display** | `Avatar.*`, `Badge`, `Card.*`, `Heading`, `Text`, `Skeleton` |
+| **Forms** | `Button`, `Input`, `Field`, `Checkbox.*`, `RadioGroup.*`, `Switch.*`, `Select.*`, `Combobox.*`, `Slider`, `Textarea`, `Label`, `Toggle`, `ToggleGroup.*`, `Calendar.*`, `DateField.*`, `DatePicker.*`, `RangeCalendar.*`, `DateRangeField.*`, `DateRangePicker.*`, `TimeField.*`, `TimeRangeField.*`, `PinInput.*`, `RatingGroup.*` |
+| **Feedback** | `Alert.*`, `Busy`, `Spinner`, `Toast.*`, `Progress`, `Meter` |
+| **Navigation** | `Tabs.*`, `Accordion.*`, `Sidebar.*`, `Breadcrumb.*`, `NavigationMenu.*`, `Menubar.*`, `Collapsible.*`, `Toolbar.*` |
+| **Overlays** | `Dialog.*`, `DropdownMenu.*`, `Popover.*`, `Tooltip.*`, `AlertDialog.*`, `Command.*`, `Sheet.*`, `ContextMenu.*`, `LinkPreview.*` |
+| **Data** | `Table.*`, `Pagination.*` |
+| **Layout** | `Stack`, `Flex`, `Separator`, `ScrollArea.*`, `AspectRatio` |
+| **Utilities** | `Code` |
+
+Every component has a live page with props and examples at
+[sveui.org/components](https://sveui.org/components).
+
+Most take `variant`, `color` and `size`:
```svelte
@@ -84,9 +96,14 @@ Most components take `variant`, `color` and `size` props, e.g.:
```
-`Avatar`, `Card`, `Alert`, the overlays, and the form controls are **namespaced**
-compositions — import the namespace and compose its parts (`Dialog.Root`,
-`Dialog.Trigger`, `Dialog.Content`; `RadioGroup.Root`, `RadioGroup.Item`; …).
+Three that are easy to miss:
+
+- **`Field`** is the only thing here that wires `aria-describedby` — use it to
+ attach help text or a validation message to any control.
+- **`Busy`** covers the gap between "nothing yet" and "loaded": a region that
+ announces itself while its content is in flight.
+- **`Command.*`** is the command palette, and its `Command.Status` announces
+ result counts to a screen reader as you filter.
## Theming
diff --git a/packages/sve-ui/package.json b/packages/sve-ui/package.json
index 843c5fb..805e590 100644
--- a/packages/sve-ui/package.json
+++ b/packages/sve-ui/package.json
@@ -85,7 +85,7 @@
},
"scripts": {
"build": "vite build && pnpm package",
- "package": "svelte-kit sync && svelte-package && node scripts/check-dts.mjs && node scripts/check-treeshake.mjs && node scripts/check-package-files.mjs && node scripts/check-css-coverage.mjs && node scripts/check-docs-coverage.mjs && publint",
+ "package": "svelte-kit sync && svelte-package && node scripts/check-dts.mjs && node scripts/check-treeshake.mjs && node scripts/check-package-files.mjs && node scripts/check-css-coverage.mjs && node scripts/check-docs-coverage.mjs && node scripts/check-readme-coverage.mjs && publint",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"test": "vitest run",
"lint": "eslint .",
diff --git a/packages/sve-ui/scripts/check-readme-coverage.mjs b/packages/sve-ui/scripts/check-readme-coverage.mjs
new file mode 100644
index 0000000..ea3b750
--- /dev/null
+++ b/packages/sve-ui/scripts/check-readme-coverage.mjs
@@ -0,0 +1,91 @@
+/**
+ * The README is the npm page. Every component must appear on it.
+ *
+ * This exists because it had already rotted all the way through: the README
+ * listed 18 of 61 exports and sold a 60-component library as sixteen. Forty-three
+ * components — `Table`, `Toast`, `Sidebar`, `Combobox`, `Command`, the whole date
+ * family — were not named anywhere in the file. Nobody noticed, because nothing
+ * was looking. `check-docs-coverage` guards the registry, the docs pages and the
+ * agent skill; the one surface a person actually lands on had no guard at all.
+ *
+ * That surface is also the only one a search engine indexes for the package, so
+ * an incomplete README is both a documentation bug and a discovery bug.
+ *
+ * Like its sibling, this checks that the prose EXISTS, not that it is good.
+ */
+
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const PKG = fileURLToPath(new URL('..', import.meta.url));
+const LIB_INDEX = join(PKG, 'src/lib/index.ts');
+const README = join(PKG, 'README.md');
+
+/**
+ * Exports that legitimately need no entry in the component table. Each needs a
+ * reason, so the list cannot quietly absorb a component someone forgot.
+ */
+const NOT_IN_TABLE = new Map([
+ ['ThemeProvider', 'infrastructure, not a component: documented in Quick start and Theming']
+]);
+
+function exportedSymbols() {
+ const src = readFileSync(LIB_INDEX, 'utf8');
+ return [
+ ...[...src.matchAll(/export \* as (\w+) from/g)].map((m) => ({ name: m[1], namespace: true })),
+ ...[...src.matchAll(/export \{ default as (\w+) \}/g)].map((m) => ({
+ name: m[1],
+ namespace: false
+ }))
+ ];
+}
+
+const readme = readFileSync(README, 'utf8');
+const exported = exportedSymbols();
+const problems = [];
+
+for (const { name, namespace } of exported) {
+ if (NOT_IN_TABLE.has(name)) continue;
+
+ // Namespaces are written `Dialog.*` in the table; singles as plain `Button`.
+ // Requiring the backticks is deliberate — a component named only in passing
+ // prose is not the same as one listed where a reader goes looking.
+ const listed = namespace
+ ? readme.includes(`\`${name}.*\``)
+ : new RegExp('`' + name + '`').test(readme);
+
+ if (!listed) {
+ problems.push(
+ namespace
+ ? `${name} is exported but never listed as \`${name}.*\` in the README`
+ : `${name} is exported but never listed as \`${name}\` in the README`
+ );
+ }
+}
+
+// The headline count is prose, and prose drifts. Check it against the exports.
+const components = exported.length - NOT_IN_TABLE.size;
+const claimed = readme.match(/\*\*(\d+) components\*\*/);
+if (!claimed) {
+ problems.push('the README no longer states a component count in the form **N components**');
+} else if (Number(claimed[1]) !== components) {
+ problems.push(`the README claims ${claimed[1]} components but the library exports ${components}`);
+}
+
+if (problems.length > 0) {
+ console.error('check-readme-coverage: FAILED\n');
+ for (const p of problems) console.error(` - ${p}`);
+ console.error(
+ '\nThe README is the npm page — a component missing from it is a component\n' +
+ 'nobody installing this package will know exists. Add it to the component\n' +
+ 'table, or, if the export genuinely is not a component, add it to\n' +
+ 'NOT_IN_TABLE in this script with a reason.'
+ );
+ process.exit(1);
+}
+
+console.log(
+ `check-readme-coverage: ${components} components, all listed in the README, ` +
+ 'and the stated count matches.'
+);