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.' +);