From 5245b55c03447db13f75bf478b1efe99c99a3d4a Mon Sep 17 00:00:00 2001 From: aromko Date: Wed, 2 Sep 2026 16:10:28 +0200 Subject: [PATCH 1/9] ci(DST-1526): lint docs prose with Vale in pre-commit and CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encodes the prose rules that until now lived in review comments: no em dashes, no semicolons in prose, no en dash asides. `pnpm lint:prose` runs Vale over the docs site, the changesets and the published READMEs, wired into lint-staged and a CI check. Scope is prose written for a reader. Prose written for an agent (CLAUDE.md, .memory/, .claude/, docs/superpowers/, packages/*/src/**/README.md) is out, as is docs/legal/ and everything generated. Changesets are linted at the source, because the release notes they generate are not tracked. The binary is pinned to 3.19.0 and downloaded by scripts/install-vale.mjs with checksum verification, rather than via the @vvago/vale wrapper. The wrapper derives its download URL from its own package version and is stalled at 3.17.1, which has two defects that matter here: it ignores the table.cell scope exclusion, so it flags the em dash in `| — (no class) |`, and it reports only the first match per paragraph, missing 10 findings in this repo. Pinning 3.19.0 also means MDX parses natively, so no mdx2vast bridge and no `vale sync`. Notes for anyone changing this: - Vale matches the path sections in .vale.ini against the path it is handed, so an absolute path matches no section and lints with no rules. That is why the hook passes `--relative`; without it the gate silently passes everything. - Exclusion is an empty `BasedOnStyles =`. `= NONE` is a fatal error. - A path-section `*` spans `/`, so `[packages/*/README.md]` also matches packages/cli/src/lib/validate/README.md, hence the later `[packages/*/src/**]` section. - The lint-staged commands are one array, not two globs: separate glob entries run concurrently, which would race prettier's write against Vale's read. - Use `pnpm lint:prose`, not `pnpm exec vale`. pnpm exec puts a relative ./node_modules/.bin first on PATH, and Go refuses to resolve an executable through a relative PATH entry. lint-staged was previously undeclared and resolved through the npx cache, so it is now a devDependency. It pins 17.3.0 rather than latest because .npmrc sets minimum-release-age to 7 days and 17.4.1 is newer than that. --- .github/workflows/prose.yml | 33 +++++++++ .gitignore | 3 + .husky/pre-commit | 6 +- .lintstagedrc | 2 +- .prettierignore | 1 + .vale.ini | 48 ++++++++++++ .vale/styles/Marigold/EmDash.yml | 15 ++++ .vale/styles/Marigold/EnDash.yml | 10 +++ .vale/styles/Marigold/Semicolon.yml | 13 ++++ package.json | 7 +- pnpm-lock.yaml | 22 ++++++ scripts/install-vale.mjs | 109 ++++++++++++++++++++++++++++ 12 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/prose.yml create mode 100644 .vale.ini create mode 100644 .vale/styles/Marigold/EmDash.yml create mode 100644 .vale/styles/Marigold/EnDash.yml create mode 100644 .vale/styles/Marigold/Semicolon.yml create mode 100644 scripts/install-vale.mjs diff --git a/.github/workflows/prose.yml b/.github/workflows/prose.yml new file mode 100644 index 0000000000..5e3d9c35be --- /dev/null +++ b/.github/workflows/prose.yml @@ -0,0 +1,33 @@ +name: Prose + +# On push, like Lint and Format. A `pull_request`-only trigger drops the synchronize event +# for a second commit and the check silently does not run — a stale green check is +# indistinguishable from a fresh one (see the same note in validate-links.yml). +# +# Deliberately unfiltered by path: `vale .` over the whole repo takes 0.2s, so there is +# nothing to save, and a path filter on a required check means a PR that misses the filter +# waits forever for a check that will never run. +on: ['push'] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + prose: + name: Check prose style + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Setup Node (using .node-version) + uses: actions/setup-node@v6 + with: + node-version-file: '.node-version' + + # No `pnpm install` and no build, like theme-conventions.yml and memory-store.yml: + # the installer and Vale read the working tree and need nothing from node_modules. + - name: Install Vale (pinned in the script, checksum verified) + run: node scripts/install-vale.mjs + + - name: Check prose style + run: .vale/bin/vale . diff --git a/.gitignore b/.gitignore index 6ea87a2f1e..3f4780ee21 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ next-env.d.ts # Vitest browser-mode failure artifacts **/__screenshots__ .vitest-attachments + +# Vale binary, downloaded by scripts/install-vale.mjs (pinned in that file) +.vale/bin/ diff --git a/.husky/pre-commit b/.husky/pre-commit index 303d13c3bc..55a4faa720 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,8 @@ #!/bin/sh export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" -npx lint-staged +# `--relative` is required, not cosmetic: Vale matches the path sections in `.vale.ini` +# against the path it is handed, and an absolute path matches no section at all — so it +# lints with no rules and reports a clean file. Without this flag the prose gate silently +# passes everything. +npx lint-staged --relative diff --git a/.lintstagedrc b/.lintstagedrc index 7db0bc7512..d6337b8d5b 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -1,3 +1,3 @@ { - "*": "prettier --write" + "*": ["prettier --write", "node scripts/install-vale.mjs", ".vale/bin/vale"] } diff --git a/.prettierignore b/.prettierignore index 9d35150660..ecdba4d57c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,6 +13,7 @@ .npmrc .nvmrc .prettierignore +.vale.ini .claude coverage diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 0000000000..93bb7f1507 --- /dev/null +++ b/.vale.ini @@ -0,0 +1,48 @@ +# Prose style as lint (DST-1526). The rules live in `.vale/styles/Marigold/`; the writing +# guidance and the reasoning behind this scope are in CLAUDE.md under "Prose Style". +# +# Every path section below is matched against the path Vale is handed, relative to where it +# runs — so `pnpm lint:prose` runs `vale .` from the repo root, and the pre-commit hook passes +# `--relative`. An absolute path matches no section at all and is silently linted with no +# rules, which looks exactly like a clean file. +StylesPath = .vale/styles + +# Print everything, including advisory rules. Vale's exit code is driven by `error` alerts +# only, so a `warning` rule is visible in CI without blocking a merge. New style rules land +# at `warning`, get cleaned up, then get promoted to `error`. +MinAlertLevel = suggestion + +# --------------------------------------------------------------------------------------- +# In scope: prose written for a reader. +# --------------------------------------------------------------------------------------- + +[docs/content/**/*.mdx] +BasedOnStyles = Marigold + +# Changeset bodies become the release notes on the docs site, so they are reader-facing prose +# and are linted at the source rather than in the generated output. +[.changeset/*.md] +BasedOnStyles = Marigold + +[{README.md,contributing.md,CODE-OF-CONDUCT.md,MIGRATION-v18.md}] +BasedOnStyles = Marigold + +# Package READMEs ship to npmjs.com. +[{packages,themes,config}/*/README.md] +BasedOnStyles = Marigold + +# --------------------------------------------------------------------------------------- +# Out of scope. Each section below needs an *empty* `BasedOnStyles`; `= NONE` is a fatal +# error, because Vale looks for a style literally named "NONE" on the StylesPath. +# --------------------------------------------------------------------------------------- + +# Generated from changesets by `docs/scripts/build-changelog.mjs`, and untracked. The prose +# is guarded in `.changeset/*.md` instead, where a human writes it. +[docs/content/releases/**/release.mdx] +BasedOnStyles = + +# Internal engineering notes, never published: `@marigold/cli` ships `files: ["dist"]`. +# This section exists because a path-section `*` spans `/`, so the published-README section +# above also matches `packages/cli/src/lib/validate/README.md`. +[packages/*/src/**] +BasedOnStyles = diff --git a/.vale/styles/Marigold/EmDash.yml b/.vale/styles/Marigold/EmDash.yml new file mode 100644 index 0000000000..3f1a06cd09 --- /dev/null +++ b/.vale/styles/Marigold/EmDash.yml @@ -0,0 +1,15 @@ +# Prose punctuation, enforced. See the "Prose Style" section in CLAUDE.md for scope. +# +# `table.cell` is deliberately absent from the scope list: in a table an em dash is a +# legitimate "not applicable" marker (`| — (no class) |`), not the parenthetical-aside +# habit this rule exists to catch. +extends: existence +message: 'Rephrase with a comma, a colon, or a second sentence instead of an em dash.' +level: error +nonword: true +scope: + - paragraph + - list + - heading +tokens: + - '—' diff --git a/.vale/styles/Marigold/EnDash.yml b/.vale/styles/Marigold/EnDash.yml new file mode 100644 index 0000000000..07558abd71 --- /dev/null +++ b/.vale/styles/Marigold/EnDash.yml @@ -0,0 +1,10 @@ +extends: existence +message: 'Use a comma, a colon, or a second sentence instead of an en dash aside. German uses a spaced en dash where English uses an em dash; ranges like `4–9` or `Jan 1 – Dec 31` are fine.' +level: error +nonword: true +scope: + - paragraph + - list + - heading +tokens: + - '[a-zA-Z] – [a-z]' diff --git a/.vale/styles/Marigold/Semicolon.yml b/.vale/styles/Marigold/Semicolon.yml new file mode 100644 index 0000000000..0eb67f8d4b --- /dev/null +++ b/.vale/styles/Marigold/Semicolon.yml @@ -0,0 +1,13 @@ +# Prose punctuation, enforced. See the "Prose Style" section in CLAUDE.md for scope. +# +# Only prose is scoped, so semicolons in code keep working: fenced blocks, code spans and +# MDX `import { X } from './x';` statements are all outside `paragraph` and `list`. +extends: existence +message: 'Use a period or a comma instead of a semicolon in prose.' +level: error +nonword: true +scope: + - paragraph + - list +tokens: + - ';' diff --git a/package.json b/package.json index fad8bcd433..b189fd4d61 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "clean": "rm -rf `find . -type d -name 'node_modules' -o -name 'dist' -o -name '.next' -o -name '.turbo' -o -name 'storybook-static' -o -name 'coverage'`", "clean:build": "rm -rf `find . -type d -name 'dist'` coverage", "lint": "eslint . --no-warn-ignored", + "lint:prose": "node scripts/install-vale.mjs && .vale/bin/vale .", "check:memory": "node scripts/check-memory-store.mjs", "check:rac-imports": "node scripts/check-rac-first-imports.mjs", "check:theme-zindex": "node scripts/check-theme-zindex.mjs", @@ -87,10 +88,12 @@ "typecheck:docs": "pnpm --filter @marigold/docs types:check", "changeset": "changeset", "release:notification": "zx scripts/create-slack-notification.mjs", - "prepare": "husky install" + "prepare": "husky install", + "postinstall": "node scripts/install-vale.mjs" }, "devDependencies": { "@storybook/addon-mcp": "^0.6.0", - "@vitest/coverage-istanbul": "4.1.10" + "@vitest/coverage-istanbul": "4.1.10", + "lint-staged": "17.3.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b7dca70fc..79f0698025 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,9 @@ importers: '@vitest/coverage-istanbul': specifier: 4.1.10 version: 4.1.10(vitest@4.1.10) + lint-staged: + specifier: 17.3.0 + version: 17.3.0 config/eslint: dependencies: @@ -6158,6 +6161,11 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lint-staged@17.3.0: + resolution: {integrity: sha512-woZS3vNe3UKqBaLPvbLOtKRY4tLANpWQhom12MGWqC8Mh1lCOO+WgSwmX2amjJAqTY9BkXYW87fCUH5H9Ph6xw==} + engines: {node: '>=22.22.1'} + hasBin: true + local-pkg@1.2.1: resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} @@ -7769,6 +7777,10 @@ packages: vite-plus: optional: true + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-ts@2.3.1: resolution: {integrity: sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw==} @@ -14299,6 +14311,14 @@ snapshots: lines-and-columns@1.2.4: {} + lint-staged@17.3.0: + dependencies: + picomatch: 4.0.5 + string-argv: 0.3.2 + tinyexec: 1.3.0 + optionalDependencies: + yaml: 2.9.0 + local-pkg@1.2.1: dependencies: mlly: 1.8.2 @@ -16289,6 +16309,8 @@ snapshots: - react-dom - utf-8-validate + string-argv@0.3.2: {} + string-ts@2.3.1: {} string-width@4.2.3: diff --git a/scripts/install-vale.mjs b/scripts/install-vale.mjs new file mode 100644 index 0000000000..c8bc48cd2c --- /dev/null +++ b/scripts/install-vale.mjs @@ -0,0 +1,109 @@ +/** + * Downloads the pinned Vale binary into `.vale/bin/` (gitignored). + * + * We own this rather than depending on the `@vvago/vale` npm wrapper, because that wrapper + * derives its download URL from its *own* package version and is stalled at 3.17.1 — and + * 3.17.1 is materially wrong for us on two counts: + * + * 1. It ignores the `table.cell` scope exclusion, so it flags the em dash in + * `| — (no class) |`, which is a legitimate "not applicable" marker. + * 2. It reports only the first match per paragraph, missing every later em dash in a + * wrapped paragraph. That under-reported this repo by 10 findings. + * + * Native MDX parsing arrived in 3.18.0 and JSX-children scoping in 3.19.0, so pinning 3.19.0 + * also means no `mdx2vast` bridge and no `Packages = MDX` key. + * + * Idempotent: exits immediately when the pinned version is already in place, so it is cheap + * to run from `postinstall` on every install. + */ +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; + +const VERSION = '3.19.0'; +const REPO = 'https://github.com/vale-cli/vale/releases/download'; + +const BIN_DIR = path.join(import.meta.dirname, '..', '.vale', 'bin'); +const BIN = path.join(BIN_DIR, 'vale'); + +/** Release assets are named by Vale's own platform labels, not Node's. */ +const ASSETS = { + 'darwin-arm64': `vale_${VERSION}_macOS_arm64.tar.gz`, + 'darwin-x64': `vale_${VERSION}_macOS_64-bit.tar.gz`, + 'linux-arm64': `vale_${VERSION}_Linux_arm64.tar.gz`, + 'linux-x64': `vale_${VERSION}_Linux_64-bit.tar.gz`, +}; + +const alreadyInstalled = () => { + if (!existsSync(BIN)) return false; + try { + return execFileSync(BIN, ['--version'], { encoding: 'utf8' }).includes( + VERSION + ); + } catch { + return false; + } +}; + +const fetchOrDie = async (url, what) => { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Could not download ${what}: ${response.status} ${url}`); + } + return response; +}; + +/** The release publishes one checksums file for every asset; find our line in it. */ +const expectedSha = async asset => { + const list = await ( + await fetchOrDie( + `${REPO}/v${VERSION}/vale_${VERSION}_checksums.txt`, + 'checksums' + ) + ).text(); + const line = list.split('\n').find(l => l.trim().endsWith(asset)); + if (!line) throw new Error(`No checksum published for ${asset}`); + return line.trim().split(/\s+/)[0]; +}; + +const main = async () => { + const key = `${process.platform}-${process.arch}`; + const asset = ASSETS[key]; + if (!asset) { + // Windows ships a .zip and needs a different extract path. Nobody on the team is on + // Windows; skip rather than fail, so `pnpm install` still succeeds there. + console.log( + `[vale] no pinned build for ${key}; skipping. Install Vale ${VERSION} manually.` + ); + return; + } + + if (alreadyInstalled()) return; + + console.log(`[vale] downloading ${VERSION} for ${key}`); + const [tarball, sha] = await Promise.all([ + fetchOrDie(`${REPO}/v${VERSION}/${asset}`, asset).then(r => r.bytes()), + expectedSha(asset), + ]); + + const actual = createHash('sha256').update(tarball).digest('hex'); + if (actual !== sha) { + throw new Error( + `Checksum mismatch for ${asset}\n expected ${sha}\n actual ${actual}` + ); + } + + mkdirSync(BIN_DIR, { recursive: true }); + const archive = path.join(BIN_DIR, asset); + writeFileSync(archive, tarball); + // `vale` sits at the archive root, alongside LICENSE and README. + execFileSync('tar', ['xzf', archive, '-C', BIN_DIR, 'vale']); + rmSync(archive); + + console.log( + `[vale] ${execFileSync(BIN, ['--version'], { encoding: 'utf8' }).trim()}` + ); +}; + +await main(); From 14ac02841336b658ebb3b81a5e68379e901c3eb0 Mon Sep 17 00:00:00 2001 From: aromko Date: Wed, 2 Sep 2026 16:10:34 +0200 Subject: [PATCH 2/9] docs(DST-1526): rewrite the 135 prose-style violations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes every violation the new prose lint reports, so the check starts green rather than accumulating suppressions as files are touched. 105 em dashes, 27 prose semicolons and 3 en dashes across 28 files. Punctuation only. The rewrites replace a dash or semicolon with a comma, a colon, or a sentence break, and four words change beyond that: two connectives where a dash was joining a list, one relative pronoun where a dash introduced a parenthetical, and a slash spelled out as "or". Line counts and code fences are unchanged in every file. Two thirds of the em dashes were the `- **Term** — definition` list form, which these docs already write as `- **Term**: definition` 122 times against 13 with a dash. One factual correction rides along: numericformat/index.mdx documented `"1,000 – 5,000"`, but Intl.NumberFormat.formatRange returns "1,000–5,000" with no spaces. The spaced en dash in dateformat/index.mdx and release-2025-09-01.mdx is left alone, because Intl.DateTimeFormat.formatRange really does emit U+2013 with spaces and those lines quote rendered output. That is the same call #5496 made by hand; the en dash rule now encodes it by matching only the aside form. --- MIGRATION-v18.md | 4 +- README.md | 4 +- .../__internal__/component-guidelines.mdx | 14 +-- .../actions/toggle-button/index.mdx | 2 +- .../components/collection/actionbar/index.mdx | 4 +- .../components/collection/card/index.mdx | 2 +- .../content/section-message/index.mdx | 2 +- .../formatters/numericformat/index.mdx | 2 +- docs/content/components/layout/page/index.mdx | 2 +- .../content/components/layout/panel/index.mdx | 4 +- .../components/overlay/action-menu/index.mdx | 2 +- .../components/overlay/dialog/index.mdx | 2 +- docs/content/foundations/layouts/index.mdx | 4 +- docs/content/foundations/spacing/index.mdx | 6 +- .../foundations/token-overview/index.mdx | 14 +-- docs/content/getting-started/cli/index.mdx | 20 +-- .../getting-started/usage-with-ai/index.mdx | 2 +- .../patterns/layout/app-frame/index.mdx | 2 +- .../patterns/user-input/filter/index.mdx | 6 +- .../releases/blog/release-2024-11-27.mdx | 2 +- .../releases/blog/release-2025-01-22.mdx | 2 +- .../releases/blog/release-2025-02-14.mdx | 4 +- .../releases/blog/release-2025-05-08.mdx | 2 +- .../releases/blog/release-2025-07-08.mdx | 2 +- .../releases/blog/release-2025-07-22.mdx | 2 +- .../releases/blog/release-2025-11-25.mdx | 2 +- .../releases/blog/release-2026-08-11.mdx | 4 +- packages/cli/README.md | 114 +++++++++--------- 28 files changed, 116 insertions(+), 116 deletions(-) diff --git a/MIGRATION-v18.md b/MIGRATION-v18.md index b2769ad0b0..b669f14ccc 100644 --- a/MIGRATION-v18.md +++ b/MIGRATION-v18.md @@ -597,7 +597,7 @@ must remove the `SectionMessage.close` slot (see B6). - **`SelectList` `layout` / `keyboardNavigationBehavior`** removed. The list is always a `layout="grid"` GridList and react-aria derives `keyboardNavigationBehavior: 'tab'` from that, so neither value ever reached the - DOM. Drop them; behavior is unchanged. + DOM. Drop them. Behavior is unchanged. - **`Loader` `loaderType`**: the v17 JSDoc documented a `cycle` value that never existed. The type only ever accepted `xloader` and `circle` (default `circle`), so this is a doc fix, not a code change. Unrelated to the `Spinner` → `Loader` @@ -735,7 +735,7 @@ keeps every call site valid: | `Deal` | `BadgePercent` | `Zoom` | `ZoomIn` | | `Print` | `Printer` | | | -`Close` is not in the official mapping table; it maps to Lucide's `X`. Three legacy +`Close` is not in the official mapping table. It maps to Lucide's `X`. Three legacy names have no Lucide equivalent and each needs a decision: `PauseAlt` → `CirclePause`, `PlayAlt` → `CirclePlay`, and `Whatsapp` is gone with no replacement (Lucide dropped brand icons and it is not among the 13 retained ones, so vendor the diff --git a/README.md b/README.md index d1fe95de16..c9ef9276e3 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ To start documentation follow these steps: Releases are published by CI, not locally. `.github/workflows/release.yml` runs [`changesets/action`](https://github.com/changesets/action) on every push to `main`: it either opens a -"release: version packages" PR that consumes the pending changesets, or — once that PR is merged — +"release: version packages" PR that consumes the pending changesets, or, once that PR is merged, publishes the bumped packages to npm. So the only thing you do by hand is add a changeset with `pnpm changeset` when your change should @@ -97,7 +97,7 @@ cause a version bump. Prereleases use [changesets pre mode](https://github.com/changesets/changesets/blob/main/docs/prereleases.md), which lives on `main` (there is no separate release branch). The mode is recorded in -`.changeset/pre.json`, and `pnpm changeset publish` derives the npm dist-tag from its `tag` field — +`.changeset/pre.json`, and `pnpm changeset publish` derives the npm dist-tag from its `tag` field, so while pre mode is active, everything publishes to that tag instead of `latest`. ```sh diff --git a/docs/content/components/__internal__/component-guidelines.mdx b/docs/content/components/__internal__/component-guidelines.mdx index 5352ddc777..3b525fb34f 100644 --- a/docs/content/components/__internal__/component-guidelines.mdx +++ b/docs/content/components/__internal__/component-guidelines.mdx @@ -116,24 +116,24 @@ adds no signal. already captured by another, kept story (e.g. `Dialog.Basic` vs. the open-state Dialog stories). - The frame is **inherently flaky or non-deterministic** and can't be stabilized - (animations, time, random data) — and the same surface is covered by a stable story. + (animations, time, random data), and the same surface is covered by a stable story. -**Keep the snapshot when** the story is the only one capturing a particular visual — +**Keep the snapshot when** the story is the only one capturing a particular visual: a unique variant, size, open overlay, error/edge state, or layout. When in doubt, keep it. -**This only turns off the snapshot.** Always keep the `play` test and any docs example — +**This only turns off the snapshot.** Always keep the `play` test and any docs example. `disableSnapshot` does not affect them. **Mechanics:** -- **Named / base stories** — add the parameter directly. Generated `.test()` children +- **Named / base stories**: add the parameter directly. Generated `.test()` children inherit it via `combineParameters`, so disabling a base also disables its tests. -- **A single generated `.test()` story** — use the 3-arg overload +- **A single generated `.test()` story**: use the 3-arg overload `.test(name, annotations, fn)` so only the derivative is disabled, not its base. -- **Re-enabling a kept child** — if a base is disabled but one of its `.test()` children +- **Re-enabling a kept child**: if a base is disabled but one of its `.test()` children is the only frame capturing a state worth keeping, re-enable it explicitly with `chromatic: { disableSnapshot: false }`. -- **Don't leave dead config** — when you disable a snapshot, drop any now-unused +- **Don't leave dead config**: when you disable a snapshot, drop any now-unused `chromatic.delay` (or similar) that only existed to stabilize the removed capture. Example of how we write stories: diff --git a/docs/content/components/actions/toggle-button/index.mdx b/docs/content/components/actions/toggle-button/index.mdx index 5be332985d..1e5c1cd715 100644 --- a/docs/content/components/actions/toggle-button/index.mdx +++ b/docs/content/components/actions/toggle-button/index.mdx @@ -13,7 +13,7 @@ Use `` in toolbars, editors, and interactive surfaces, where the u `` is not a form field or a filter. It has no label, description, error message, or validation state. Reach for [Checkbox](/components/form/checkbox), [Radio](/components/form/radio), or [Switch](/components/form/switch) when you need a form field, and use a `` for filtering. -`` is a toolbar of independent on/off actions, not a selection control. Each button in the group toggles on its own, like formatting options, view options, or visible layers. When exactly one option must be selected at a time — switching a view, mode, or filter — use a [SegmentedControl](/components/form/segmented-control) instead. +`` is a toolbar of independent on/off actions, not a selection control. Each button in the group toggles on its own, like formatting options, view options, or visible layers. When exactly one option must be selected at a time, switching a view, mode, or filter, use a [SegmentedControl](/components/form/segmented-control) instead. ## Anatomy diff --git a/docs/content/components/collection/actionbar/index.mdx b/docs/content/components/collection/actionbar/index.mdx index d3790448af..37dc1e42f6 100644 --- a/docs/content/components/collection/actionbar/index.mdx +++ b/docs/content/components/collection/actionbar/index.mdx @@ -52,7 +52,7 @@ The most common home for an `` is a [``](/components/collectio -Inside the render prop you write only the actions; the selection count and the clear button are filled in for you. The bar stays pinned to the bottom of the scroll area as the user works down a long list. +Inside the render prop you write only the actions. The selection count and the clear button are filled in for you. The bar stays pinned to the bottom of the scroll area as the user works down a long list. When the user toggles the Table's select-all checkbox, the selection becomes @@ -101,7 +101,7 @@ Give every icon-only action a text label as well, either visible next to the ico ### Actions -Actions are plain [`
` pulls its first and last cell in while row backgrounds still reach the edge, and `` insets its header and content while its dividers span the full width. Custom content can read the same variable — see [Reading container spacing](/foundations/spacing#reading-container-spacing). +A bled `` publishes the card's horizontal padding as `--bleed-px`, so edge-aware children realign with the title on their own: `
` pulls its first and last cell in while row backgrounds still reach the edge, and `` insets its header and content while its dividers span the full width. Custom content can read the same variable. See [Reading container spacing](/foundations/spacing#reading-container-spacing). ## Accessibility diff --git a/docs/content/components/content/section-message/index.mdx b/docs/content/components/content/section-message/index.mdx index a262ec8594..5fca1a79fa 100644 --- a/docs/content/components/content/section-message/index.mdx +++ b/docs/content/components/content/section-message/index.mdx @@ -15,7 +15,7 @@ A `` is a container with a severity icon, an optional title, the - **Container**: Wraps the message on a neutral surface with a muted variant-colored border. - **Icon**: Variant icon that reinforces the severity. -- **Title** _(optional)_: Short headline summarizing the message. Renders as a semantic heading; adjust its level with the `headingLevel` prop so it fits the surrounding document outline. Don't end the title with a period, and don't repeat it in the content. +- **Title** _(optional)_: Short headline summarizing the message. Renders as a semantic heading. Adjust its level with the `headingLevel` prop so it fits the surrounding document outline. Don't end the title with a period, and don't repeat it in the content. - **Description** _(optional)_: A short summary directly below the title. - **Content**: Descriptive body text. This is the required core of the message. - **Close button** _(optional)_: Dismiss control when `closeButton` is set. diff --git a/docs/content/components/formatters/numericformat/index.mdx b/docs/content/components/formatters/numericformat/index.mdx index 0f2883d692..e7452ac01e 100644 --- a/docs/content/components/formatters/numericformat/index.mdx +++ b/docs/content/components/formatters/numericformat/index.mdx @@ -27,7 +27,7 @@ The `` component can also format a pair of values as a localized -For accessibility, screen readers will treat the formatted range as one continuous string (for example, "1,000 – 5,000"). This works well in most cases, but if your interface needs to convey the start and end values as distinct pieces of information, you can render them as two separate `` components and supply explicit ARIA labels. This ensures that assistive technologies announce each value in context. +For accessibility, screen readers will treat the formatted range as one continuous string (for example, "1,000–5,000"). This works well in most cases, but if your interface needs to convey the start and end values as distinct pieces of information, you can render them as two separate `` components and supply explicit ARIA labels. This ensures that assistive technologies announce each value in context. ```tsx diff --git a/docs/content/components/layout/page/index.mdx b/docs/content/components/layout/page/index.mdx index 9d100badcd..72625b9d97 100644 --- a/docs/content/components/layout/page/index.mdx +++ b/docs/content/components/layout/page/index.mdx @@ -99,7 +99,7 @@ Reach for an override only when a screen has a reason the defaults cannot antici - **Tighter padding** with `p` (or `px` and `py` for one axis) when the page is embedded in a surface that already supplies its own outer spacing. Set one or the other. Passing `p` together with `px` or `py` is a type error, since they would fight over the same edges. - **A different rhythm** with `space` when a sparse screen wants more air between the header and a lone panel, or when a dense screen reads better pulled tight. -Whatever those props resolve to is published as `--page-px`, `--page-py` and `--page-gap` on the Page root, so content in the subtree can line up with the page frame without repeating the value. They are read-only — see [Reading container spacing](/foundations/spacing#reading-container-spacing). +Whatever those props resolve to is published as `--page-px`, `--page-py` and `--page-gap` on the Page root, so content in the subtree can line up with the page frame without repeating the value. They are read-only. See [Reading container spacing](/foundations/spacing#reading-container-spacing). ### Content diff --git a/docs/content/components/layout/panel/index.mdx b/docs/content/components/layout/panel/index.mdx index 91ac1ea615..573d57b845 100644 --- a/docs/content/components/layout/panel/index.mdx +++ b/docs/content/components/layout/panel/index.mdx @@ -154,7 +154,7 @@ Tables, media, charts, and status bars are the exceptions. They look cramped whe -Tables get a small bonus: when bled, their cell edges line up with the Panel's header and footer, so what's inside a row still reads in rhythm with the title above it. That works because a bled content area publishes the Panel's horizontal padding as `--bleed-px`, which your own content can read too — see [Reading container spacing](/foundations/spacing#reading-container-spacing). +Tables get a small bonus: when bled, their cell edges line up with the Panel's header and footer, so what's inside a row still reads in rhythm with the title above it. That works because a bled content area publishes the Panel's horizontal padding as `--bleed-px`, which your own content can read too. See [Reading container spacing](/foundations/spacing#reading-container-spacing). An `` behaves the same way. Drop one into a bled content area and its item dividers span edge-to-edge while the header and content stay inset, aligned with the Panel title. No extra prop is needed: the Accordion picks up the Panel's horizontal padding automatically, exactly like Table cells do. @@ -262,7 +262,7 @@ Panel publishes what those props resolved to as three read-only custom propertie You need them when a wrapper interrupts the Panel's own rhythm. Panel spaces its slots with a flex gap, so a `
` around several `` siblings becomes a single flex item and the gap between them collapses. Re-declare it with `gap-(--panel-gap)` and the group stays in rhythm with every other Panel, where a hardcoded value would drift as soon as the `space` default changes. -Set spacing through the props, not by declaring these yourself — Panel writes them inline on its root, which outranks a declaration on any ancestor. For content that should align with the Panel's padding after bleeding out of it, reach for `--bleed-px` instead. See [Reading container spacing](/foundations/spacing#reading-container-spacing). +Set spacing through the props, not by declaring these yourself. Panel writes them inline on its root, which outranks a declaration on any ancestor. For content that should align with the Panel's padding after bleeding out of it, reach for `--bleed-px` instead. See [Reading container spacing](/foundations/spacing#reading-container-spacing). ## Accessibility diff --git a/docs/content/components/overlay/action-menu/index.mdx b/docs/content/components/overlay/action-menu/index.mdx index 684528e821..eb10fe0f0b 100644 --- a/docs/content/components/overlay/action-menu/index.mdx +++ b/docs/content/components/overlay/action-menu/index.mdx @@ -31,7 +31,7 @@ import { ActionMenuAnatomy } from './action-menu-anatomy'; ActionMenu's trigger is a slot-aware [`