diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index f6f9336e29..cae0f35090 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -672,7 +672,7 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie
| 2026-08-06 | a24f74fdf0134487a03dce37dd9f1e9bd18502f5 | a24f74fdf0134487a03dce37dd9f1e9bd18502f5 | PR #1614 post-merge RAG index restoration audit | Pass - guard-only migration, no DDL, no ranking/RPC change; pr-policy ragRanking=false so no eval-canary required; 1 P3 doc nit (#248 renumber note says 237->246, row is #248) | check:migration-role; npx vitest run tests/supabase-schema.test.ts (74 passed); check:outstanding-issues |
| 2026-08-06 | PR #1614 / codex/restore-rag-indexes-20260804 | a24f74fdf0134487a03dce37dd9f1e9bd18502f5 | PR #1614 post-merge RAG index restoration audit | Pass - guard-only migration, no DDL, no ranking/RPC change; pr-policy ragRanking=false so no eval-canary required; 1 P3 doc nit (#248 renumber note says 237->246, row is #248); supersedes 2026-08-06 row (ref column mistakenly held commit SHA instead of PR ref, breaking ledger:lookup per Devin/Sentry review on PR #1636) | check:migration-role; npx vitest run tests/supabase-schema.test.ts (74 passed); check:outstanding-issues |
| 2026-08-06 | claude/implement-97vpz7 | 00ab7bfd34684bc854d15a3f28987674098a7130 | PR #1646 soft-tail answer-cache skip + soft-tail test hardening | fixed — answer-path soft-tail skip via rag-query-guard helpers; soft-tail fixture pins; duplicate memo test removed; in-corpus assert narrowed; budget 4362 | test:rag-query-guard+unsupported-cache+classifier-memo 22/22,check:maintainability-budgets 4362/4362 |
-| 2026-08-06 | cursor/mode-nav-pr-1647-a5eb | 4c8d70612f3be4a1267ed16b81e926a9f2e1ef50 | PR #1647 mode-nav | no high-confidence defects; medium: record pages lose mode destinations after Subnav removal (section-nav early return); addon-slot guard still coincidence-tested not runtime; includes() activeId fragile for future slugs | read mode-nav/*, page-secondary-navigation, mode-secondary-navigation, specifier/formulation record anchors + tests; catalog slug collision scan (0 hits); test:focused blocked (test paths changed) |
+| 2026-08-06 | claude/ds-truth-fixes | 7a9c41a971aa9111f050cd39d4429ada1b3f4409 | PR #1655 heavy review-and-fix | fixed DownloadLink tone DOM leak + stale ToggleSwitch/Links §9 docs; merge-tree clean; verify:cheap+verify:pr-local green; CI re-queued after push | bugbot+deep-review; vitest ui-primitives+ui-v2 60p; verify:cheap 5448p; verify:pr-local format+lint+typecheck+test+build+rag-fixtures; no provider gates |
| 2026-08-06 | temp-rebase | 868a8a2800351ce85a2ad13e14d80550cbc3e668 | Merge conflict resolution and CI fixes | Verified and ready for PR | verify:pr-local |
| 2026-08-06 | claude/pr-handoff-stop-hook (PR #1649) | fff524d73ebfafeef7bdc50752a6d14f253ebc2e | Run PR sweep: CI fix + threads + drift | before: 5 unresolved threads (Devin create-from-output + 4 CodeRabbit), mergeable/BLOCKED, Actions major outage leaving CI pending; after: hardened jq-less input/output separation + session fail-open + prefix unlock + tests (9 passed) in fff524d73ebfafeef7bdc50752a6d14f253ebc2e, threads replied+resolved, branch current with main, CI re-triggered (Actions outage — not babysat) | npx vitest run tests/pr-handoff-stop.test.ts (9 passed); bash -n hook OK; no provider-backed checks run |
| 2026-08-06 | claude/pr-handoff-stop-hook (PR #1649) | f67e5c9103c3e1483ad8e034fe7a6757c2362d74 | Run PR sweep: CI fix + threads + drift | before: 5 unresolved threads (Devin create-from-output + 4 CodeRabbit), mergeable/BLOCKED, Actions major outage leaving CI pending; after: hardened jq-less input/output separation + session fail-open + prefix unlock + tests (9 passed) in f67e5c9103c3e1483ad8e034fe7a6757c2362d74, threads replied+resolved, branch current with main, CI re-triggered (Actions outage — not babysat) | npx vitest run tests/pr-handoff-stop.test.ts (9 passed); bash -n hook OK; no provider-backed checks run |
@@ -682,3 +682,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie
| 2026-08-06 | claude/pr-handoff-stop-hook (PR #1649) | 057a5579e538fcacc15feb8a0ac7fa580b3decb4 | Run PR sweep: CI fix + threads + drift | supersede: prior stacked Run PR rows used unresolvable HEADs (fff524d7/f67e5c91/3403126b); reviewed product tip is this SHA (ledger bookkeeping may sit one commit above); Bugbot prune+context+comment/review fixes landed | vitest:pr-handoff-stop 11/11; verify:cheap pass; verify:pr-local pass |
| 2026-08-06 | claude/settings-nav-freeze-desktop-tdzh7z (PR #1641) | d4ecd2b5fc48a8d8f37f081cac2d80beb749df01 | Run PR sweep: CI fix + threads + drift | before: mergeable/BLOCKED, 0 unresolved threads, 0 behind main; prior CI reds were infra (npm ECONNRESET on Unit coverage; e7f380f3 cancelled at Set up job). after: pushed d4ecd2b5 ResizeObserver pin-clamp distance reset; all review threads remain resolved; CI re-triggered but GitHub Actions major_outage — runs pending/queued, no product failure to fix; merge tree clean | local typecheck PASS; local lint (settings-dialog) PASS; format unchanged; no provider-backed checks run; hosted CI awaiting Actions recovery (run 31120931239) |
| 2026-08-06 | claude/settings-nav-freeze-desktop-tdzh7z (PR #1641) | c31827e63c20788c5dabb22b96ddbf239c65f893 | Run PR sweep: CI fix + threads + drift | before: mergeable/BLOCKED, 0 unresolved threads, 0 behind main; prior CI reds were infra (npm ECONNRESET; cancelled Set up job). after: product fix d4ecd2b5 (ResizeObserver pin-clamp distance reset) + ledger row; all review threads resolved; merge tree clean; hosted CI pending on GitHub Actions major_outage (no product failure) | local typecheck PASS; local lint (settings-dialog) PASS; no provider-backed checks run; hosted CI awaiting Actions recovery |
+| 2026-08-06 | cursor/mode-nav-pr-1647-a5eb | 4c8d70612f3be4a1267ed16b81e926a9f2e1ef50 | PR #1647 mode-nav | no high-confidence defects; medium: record pages lose mode destinations after Subnav removal (section-nav early return); addon-slot guard still coincidence-tested not runtime; includes() activeId fragile for future slugs | read mode-nav/*, page-secondary-navigation, mode-secondary-navigation, specifier/formulation record anchors + tests; catalog slug collision scan (0 hits); test:focused blocked (test paths changed) |
diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md
index 860e53ed04..5520d37a7a 100644
--- a/docs/design-system/COMPONENTS.md
+++ b/docs/design-system/COMPONENTS.md
@@ -60,32 +60,32 @@ print primitives (`PrintHeader`, `PrintFooter`, `CitationFootnote`, `PrintOnly`,
### 0.4 Open-defect ledger (existing components → closing PR)
-| Component | Open defects (compressed) | Closes in |
-| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
-| Button | danger hover/active tokens and the 48px comment are resolved; ref forwarding and the client boundary remain | follow-on |
-| AsyncButton | `type` applied after spread (default `button`; explicit `submit` preserved). Prefer `Button` busy API for new sites. | **done** (PR-A) |
-| IconButton | disabled encoding uses `controlDisabled` (opacity retired in PR-A) | **done** (PR-A) |
-| ToggleSwitch | operable branch requires `aria-label`; opacity disabled retired; knob still animates `left`/`right` | PR 9 (motion) |
-| Chip | final `appearance`/size API, removable label contract, tap target and full-value title | **done** |
-| TextField/SearchField/Select | hint dropped on error (comment promises otherwise) · describedBy overwritten · no external id/refs (placeholder off decoration tier in PR-A) | PR 7 |
-| Checkbox/RadioGroup | RadioGroup controlled/uncontrolled union done; raw dimensions · unsanitised ids · no group hint/error | PR 7 |
-| Citation/CitationList | required interactive handler and stable-id list keys are resolved; static span labelling and route/source modes remain | follow-on |
-| DoseLine | must compose `Quantity` · structured dose model · overdue text + non-colour mark + open action | **done** (PR 6) |
-| StatusMark | app-type coupling · inline styles/raw geometry · HCM token remaps proven (computed suite); mark shape still visual | PR 12 |
-| AnswerCard | required verification/state, structured actions and five-state vocabulary landed | **done** (PR 6) |
-| AnswerFooter | machine ISO values composed through `DateDisplay` + `MissingValue` landed | **done** (PR 6) |
-| PageHeader/Breadcrumb | `
` truncates · actions starve title · eyebrow ink moved off decoration (PR 3); layout starve remains | PR 7-adjacent layout fix |
-| Tabs | `aria-controls` to unrendered panels · invalid selected value can empty the tab order | PR 4-adjacent |
-| Pagination | unclamped props · 320px overflow · opacity disabled retired · no focus/announce policy | PR 8 |
-| Links | `tone` leaks to DOM · `download` overridable by spread · `gap` animation · new-tab policy implicit | PR 9 |
-| Tooltip | composed handlers/description, string content, OverlayRoot portal, collision and delay contract | **done** (PR 10) |
-| Toast | independent tone/priority/persistence, OverlayRoot portal, pause, dedupe and queue contract | **done** (PR 10) |
-| Sheet/ConfirmDialog | required names/action labels, portal default, tokened layers/duration and wrapping titles | **done** (PR 10) |
-| Disclosure | configurable heading level is resolved; print behaviour and truncation remain | PR 11 |
-| Progress/StageList | determinate width animation is resolved via `scaleX`; hardcoded indeterminate timing, "step 0 of N" and whole-list live region remain | PR 8, PR 9 |
-| EmptyState | static live-off default with explicit polite/assertive opt-in | **done** (PR 8) |
-| AccessibleTable | required semantic caption and `MissingValue` cells landed; dense headers, content-role widths and Button-based expander remain | PR 6/PR 12 remainder |
-| ui-primitives.tsx | 572-line module mixing recipes/actions/feedback/clinical — split | PR 12 |
+| Component | Open defects (compressed) | Closes in |
+| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
+| Button | danger hover/active tokens and the 48px comment are resolved; ref forwarding and the client boundary remain | follow-on |
+| AsyncButton | `type` applied after spread (default `button`; explicit `submit` preserved). Prefer `Button` busy API for new sites. | **done** (PR-A) |
+| IconButton | disabled encoding uses `controlDisabled` (opacity retired in PR-A) | **done** (PR-A) |
+| ToggleSwitch | operable branch requires `aria-label`; opacity disabled retired; knob now travels on `transform` (`translate-x-4`), tokenised duration, reduced-motion opt-out | **done** (motion) |
+| Chip | final `appearance`/size API, removable label contract, tap target and full-value title | **done** |
+| TextField/SearchField/Select | hint dropped on error (comment promises otherwise) · describedBy overwritten · no external id/refs (placeholder off decoration tier in PR-A) | PR 7 |
+| Checkbox/RadioGroup | RadioGroup controlled/uncontrolled union done; raw dimensions · unsanitised ids · no group hint/error | PR 7 |
+| Citation/CitationList | required interactive handler and stable-id list keys are resolved; static span labelling and route/source modes remain | follow-on |
+| DoseLine | must compose `Quantity` · structured dose model · overdue text + non-colour mark + open action | **done** (PR 6) |
+| StatusMark | app-type coupling · inline styles/raw geometry · HCM token remaps proven (computed suite); mark shape still visual | PR 12 |
+| AnswerCard | required verification/state, structured actions and five-state vocabulary landed | **done** (PR 6) |
+| AnswerFooter | machine ISO values composed through `DateDisplay` + `MissingValue` landed | **done** (PR 6) |
+| PageHeader/Breadcrumb | `` truncates · actions starve title · eyebrow ink moved off decoration (PR 3); layout starve remains | PR 7-adjacent layout fix |
+| Tabs | `aria-controls` to unrendered panels is resolved (emitted only for the selected tab when it owns a panel); invalid selected value can still empty the tab order | PR 4-adjacent |
+| Pagination | unclamped props · 320px overflow · opacity disabled retired · no focus/announce policy | PR 8 |
+| Links | `tone` leak is resolved (destructured before the spread in every link); `download` still overridable by spread · `gap` animation · new-tab policy implicit | PR 9 |
+| Tooltip | composed handlers/description, string content, OverlayRoot portal, collision and delay contract | **done** (PR 10) |
+| Toast | independent tone/priority/persistence, OverlayRoot portal, pause, dedupe and queue contract | **done** (PR 10) |
+| Sheet/ConfirmDialog | required names/action labels, portal default, tokened layers/duration and wrapping titles | **done** (PR 10) |
+| Disclosure | configurable heading level is resolved; print behaviour and truncation remain | PR 11 |
+| Progress/StageList | determinate width animation is resolved via `scaleX`; hardcoded indeterminate timing, "step 0 of N" and whole-list live region remain | PR 8, PR 9 |
+| EmptyState | static live-off default with explicit polite/assertive opt-in | **done** (PR 8) |
+| AccessibleTable | required semantic caption and `MissingValue` cells landed; dense headers, content-role widths and Button-based expander remain | PR 6/PR 12 remainder |
+| ui-primitives.tsx | 686-line module mixing recipes/actions/feedback/clinical — split. 200 product files import it, against the 53 registered components' 27 adopted; this module, not the registry, is what the product actually runs on | PR 12 |
---
@@ -647,8 +647,11 @@ on it.
`interactive` (label + checked + onCheckedChange required) · `status` (label + checked;
`role="img"`-style read-only, still named). **Rules.** Compact 40×24 face is fine; the
_target_ meets the tap floor via a transparent hit area · knob motion is `transform` with
-the physical curve · reduced motion snaps. **Open defects → PR.** unnameable, `left`/
-`right` animation, opacity disabled → PR 4, PR 9.
+the physical curve · reduced motion snaps. **Resolved (6 Aug 2026).** operable branch
+requires `aria-label`; opacity disabled retired; knob travels on `transform`
+(`translate-x-0` / `translate-x-4`) with tokenised duration and `motion-reduce` opt-out —
+no longer animates `left`/`right`. **Open defects → PR.** none remaining on this
+component; broader Gate 9 motion sweep still tracks other surfaces.
### 9.5 `Chip`
@@ -810,8 +813,10 @@ focus/announce policy → PR 3, PR 8.
links carry the `ExternalLink` icon and an explicit new-tab policy, not an accidental
universal · `DownloadLink`'s `download` semantics are not overridable by spread; `tone`
is destructured, never leaked to the DOM · `LinkAction`'s arrow animates with `transform`,
-never `gap`. **Open defects → PR.** tone leak, spread override, gap animation, raw
-underline offset → PR 9.
+never `gap`. **Resolved (6 Aug 2026).** `tone` is destructured before the spread in
+`TextLink`, `ExternalTextLink`, and `DownloadLink`. **Open defects → PR.** `download`
+still overridable by spread · `LinkAction` `gap` animation · new-tab policy implicit /
+raw underline offset → PR 9.
### 9.21 `Tooltip`
diff --git a/docs/design-system/GATES.md b/docs/design-system/GATES.md
index ee09d396b3..76dc1528e2 100644
--- a/docs/design-system/GATES.md
+++ b/docs/design-system/GATES.md
@@ -30,10 +30,23 @@ than CI, two of them by the second reader; review does not scale past two carefu
| `npm run verify:ui` (+ `verify:phone-chrome`) | Chromium production journeys; phone-chrome owners | implemented-blocking (journey scope) |
| Visual-baseline harness (PR #1404) | Screenshot baselines exist but ship **zero committed baselines with `continue-on-error` on, deliberately** — a held gate until the design is declared final (issue `#118`). Includes the `--spacing-tap` probe floor. | implemented-partial (non-blocking by hold) |
-⚠️ **Named-but-absent checks.** Project docs mention "type-scale" and "icon-scale" lint
-rules; no such rule files exist in `eslint-rules/` **[verified: glob]**. Type-step and
-icon-step discipline is currently **unenforced** outside the token contract — do not cite
-those rules as gates.
+⚠️ **Type-scale and icon-scale enforcement — corrected 6 Aug 2026.** The previous wording
+here ("no such rule files exist in `eslint-rules/`… currently **unenforced**") was true about
+ESLint and wrong about enforcement, which is the more expensive error of the two.
+`npm run check:type-scale` and `npm run check:icon-scale` (`scripts/check-type-scale.mjs`,
+`scripts/check-icon-scale.mjs`, both `--strict`) have run inside `verify:cheap` since
+30 July **[verified: package.json]**. They are hard-zero gates with no baseline, unlike the
+ratcheting design-system contract.
+
+**Enforced:** arbitrary font-size and icon-size utilities (`text-[12px]`, `text-[1.45rem]`)
+fail the build. `text-[color:var(--…)]` is the sanctioned token form and is deliberately not
+flagged.
+
+**Not enforced:** which named step a component picks. Nothing stops a surface choosing
+`text-sm-minus` over `text-sm` — 1 318 call sites across the eight non-standard steps
+**[verified: grep]**. That is a step-_selection_ lint and it does not exist. Do not describe
+the scale as ungated, and do not write an ESLint rule duplicating the arbitrary-value check
+that already ships.
## 2 · The twelve system gates, labelled
@@ -41,7 +54,7 @@ those rules as gates.
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Contrast ≥4.5:1 for every text/background pair, both themes, live **and** v2; `--decoration-soft` asserted below 4.5 and never on a text node | **implemented-blocking** | v2 pairs + `--text-placeholder` + `--decoration-soft` tier + recipe AST (`decoration-on-text.contract.test.ts`); live full matrix remains follow-on. |
| 2 | Tap targets ≥48px interactive (token + declared carriers today; fixed-height `h-10` controls and full interactive enumeration not yet blocked); static pills never carry `min-h-tap`; no production target reduced | **implemented-partial** | Enforced today: `--spacing-tap` is 48px and pinned from both sides (`ckb-v2-token-contract` asserts the `@theme` knob is ≥48 and that `--tap-min` is its alias), `ui-style-contract` measures the rendered floor for declared `min-height` carriers in Chromium, and the legacy-class / literal ratchets still run. **Not blocked today:** the Chromium audit skips elements whose declared height is below the floor (a fixed `h-10` control would not fail it), and `test:e2e:style-contract` is not part of `verify:cheap`. Path to blocking: enumerate rendered interactive elements with an explicit exception list (phone composer below 431px is already written — SPEC §4.10), then wire that audit into required CI. Per-surface geometry stays in the held visual harness. |
-| 3 | Focus outline present, `--focus`, no companion ring | **planned** | `--focus` is referenced nowhere in the DS export (finding N3). Global fix first, then the check. |
+| 3 | Focus outline present, `--focus`, no companion ring | **planned** | Corrected 6 Aug 2026: the previous evidence ("`--focus` is referenced nowhere in the DS export", finding N3) was false — **[verified: grep]** 4 declarations (2 theme, 2 forced-colours) against **273** `var(--focus)` consumers, 260 of them in `.tsx`. The token is adopted; the check is what is missing. Path to blocking: assert a visible focus outline on every interactive role and reject a `ring-*` companion on the same node. A row that understates shipped work costs the document its authority as surely as one that overstates it, and nobody files a bug against pessimism. |
| 4 | Non-colour encoding on every status indicator | **implemented-partial** | Blocked today by `ui-v2-answer-safety.dom.test.tsx`: an overdue `DoseLine` row is asserted to carry all three channels (amber inset rule **plus** the words "Source review overdue" **plus** a `StatusMark` shape), `MissingValue` is asserted never to contract to a dash at any density, `FieldError` is asserted to pair its text with an icon, and `RetrievalStateBanner` is asserted to carry its state in the headline text rather than the tone alone. Off-vocabulary status still degrades to a phrase (`source-badges-off-vocab.dom.test.tsx`, Gate 6). **Not blocked today:** there is no repository-wide enumeration of status indicators, so a _new_ colour-only indicator elsewhere in `src/components/**` — the bare `statusDot*` recipes are the obvious candidates — would not fail anything. Path to blocking: an AST sweep that pairs every status-bearing tone class with a sibling text or shape channel, wired into `verify:cheap`. Deliberately not claimed as blocking on the strength of per-component tests: on a clinical safety gate an overstated label is worse than an honest partial. |
| 5 | Tables: semantic caption, associated headers, `aria-controls` on the expander | **implemented-blocking** | `AccessibleTableProps.caption` is required; DOM and alignment tests prove the semantic ``, associated headers, and expander relationship. |
| 6 | Enum resilience — neutral fallback, never throws | **implemented-blocking** | `source-badges-off-vocab.dom.test.tsx`. |
diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json
index 908dd988ce..3c555aa824 100644
--- a/docs/design-system/adoption-manifest.json
+++ b/docs/design-system/adoption-manifest.json
@@ -602,7 +602,7 @@
"preview": ".design-sync/previews/DownloadLink.tsx",
"previewValid": true
},
- "testFiles": ["tests/design-sync-visual-exports.test.ts"],
+ "testFiles": ["tests/design-sync-visual-exports.test.ts", "tests/ui-v2-components.dom.test.tsx"],
"baseline": {
"targetLayer": "v2",
"liveLayer": "v2",
@@ -718,7 +718,7 @@
"preview": ".design-sync/previews/ExternalTextLink.tsx",
"previewValid": true
},
- "testFiles": ["tests/design-sync-visual-exports.test.ts"],
+ "testFiles": ["tests/design-sync-visual-exports.test.ts", "tests/ui-v2-components.dom.test.tsx"],
"baseline": {
"targetLayer": "v2",
"liveLayer": "v2",
@@ -1746,7 +1746,7 @@
"preview": ".design-sync/previews/TextLink.tsx",
"previewValid": true
},
- "testFiles": ["tests/design-sync-visual-exports.test.ts"],
+ "testFiles": ["tests/design-sync-visual-exports.test.ts", "tests/ui-v2-components.dom.test.tsx"],
"baseline": {
"targetLayer": "v2",
"liveLayer": "v2",
diff --git a/src/components/ui-primitives.tsx b/src/components/ui-primitives.tsx
index 0b3a14f177..90285d25e9 100644
--- a/src/components/ui-primitives.tsx
+++ b/src/components/ui-primitives.tsx
@@ -337,8 +337,11 @@ export function ToggleSwitch({
);
diff --git a/src/components/ui/link.tsx b/src/components/ui/link.tsx
index 14dd2fff9a..c2054ca391 100644
--- a/src/components/ui/link.tsx
+++ b/src/components/ui/link.tsx
@@ -84,10 +84,18 @@ export function ExternalTextLink({ href, children, tone = "accent", className, .
* on hospital wifi deciding whether to tap a 40 MB PDF needs that before the tap,
* not after.
*/
-export function DownloadLink({ href, children, format, size, className, ...props }: DownloadLinkProps) {
+export function DownloadLink({
+ href,
+ children,
+ format,
+ size,
+ tone = "accent",
+ className,
+ ...props
+}: DownloadLinkProps) {
const detail = [format, size].filter(Boolean).join(", ");
return (
-
+
{children}
{detail ? (
diff --git a/tests/ui-primitives.dom.test.tsx b/tests/ui-primitives.dom.test.tsx
index a83a558db1..60f0a99728 100644
--- a/tests/ui-primitives.dom.test.tsx
+++ b/tests/ui-primitives.dom.test.tsx
@@ -113,4 +113,17 @@ describe("ToggleSwitch", () => {
expect(screen.queryByRole("switch")).not.toBeInTheDocument();
expect(screen.getByRole("img", { name: "Available: on" })).toBeInTheDocument();
});
+
+ it("moves the knob on transform, never left/right", () => {
+ const { rerender } = render( undefined} aria-label="Notify" />);
+ const offKnob = screen.getByRole("switch", { name: "Notify" }).querySelector("[aria-hidden]");
+ expect(offKnob?.className).toMatch(/translate-x-0/);
+ expect(offKnob?.className).not.toMatch(/\bright-1\b/);
+ expect(offKnob?.className).toMatch(/\bleft-1\b/);
+
+ rerender( undefined} aria-label="Notify" />);
+ const onKnob = screen.getByRole("switch", { name: "Notify" }).querySelector("[aria-hidden]");
+ expect(onKnob?.className).toMatch(/translate-x-4/);
+ expect(onKnob?.className).not.toMatch(/\bright-1\b/);
+ });
});
diff --git a/tests/ui-v2-components.dom.test.tsx b/tests/ui-v2-components.dom.test.tsx
index d6d4c7376e..25f6fbce30 100644
--- a/tests/ui-v2-components.dom.test.tsx
+++ b/tests/ui-v2-components.dom.test.tsx
@@ -11,6 +11,7 @@ import { Chip } from "@/components/ui/chip";
import { Checkbox, RadioGroup } from "@/components/ui/choice";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { Disclosure } from "@/components/ui/disclosure";
+import { DownloadLink, ExternalTextLink, TextLink } from "@/components/ui/link";
import { OverlayPortal, OverlayRoot } from "@/components/ui/overlay-root";
import { Pagination } from "@/components/ui/pagination";
import { Progress } from "@/components/ui/progress";
@@ -862,3 +863,26 @@ describe("AnswerFooter", () => {
expect(within(footer).getAllByTestId("missing-value").length).toBeGreaterThan(0);
});
});
+
+describe("Links tone contract", () => {
+ it("does not leak tone onto the DOM for TextLink, ExternalTextLink, or DownloadLink", () => {
+ render(
+ <>
+
+ Internal
+
+
+ External
+
+
+ Download
+
+ >,
+ );
+
+ for (const name of ["Internal", "External", "Download"]) {
+ const link = screen.getByRole("link", { name: new RegExp(name, "i") });
+ expect(link).not.toHaveAttribute("tone");
+ }
+ });
+});