Move the style axis's 69 material rules under `@scope (…) to ([data-style])` — and re-point `styleRegistry.test.ts`'s parser, which cannot survive the move
Builds the mechanism MOTIR-3947 decided. Read docs/decisions/scoped-preview-isolation.md first — it carries the measurement, the two rejected candidates, and the reason a base token block is the wrong remedy.
What to do
packages/design-system/theme.css carries 69 rules of the form [data-style='X'] <descendant>. A descendant combinator does not stop at a nested data-style, so all 69 match through a scoped StyleVignette. Rewrite each one as:
@scope ([data-style='X']) to ([data-style]) {
<the rest of the selector> { … }
}
The rewrite is mechanical and scriptable; the judgement is in the two things that ride with it.
Do NOT touch: the 10 bare [data-style='…'] token blocks, the compound [data-style='neumorphism'][data-theme='dark'] block, or the 4 already-anchored .style-vignette[data-style='…'] > .sv-canvas rules. Those 15 are the exact complement of the 69 — a rewriter that converts 70 or 68 has mis-parsed something.
tests/theme/styleRegistry.test.ts must land in the SAME commit. Its material-layer finder is a text regex — /\[data-style='[^']+'\]\s+[^{};]+\{/ — which stops matching the moment the anchor moves into an @scope prelude, so materialRulesChecked drops to 0 against its own toBeGreaterThanOrEqual(4) floor. Re-point it at the @scope form, keep both existing assertions (palette-derived: var(--color-*|--el-*) or currentColor; and no raw hex hue) asserting over the same population, and ratchet the count floor to 69 so a material rule authored outside a scope in future fails here instead of going uncounted.
And correct the two comments that state the leak as open, both of which become false with this commit:
packages/design-system/src/components/theme/StyleVignette.tsx— the ⚠️ header's third bullet (SHAPE / MATERIAL (styleId) — NOT ISOLATED, and this is open). Rewrite it to say what the axis IS isolated by, and point at the ADR. Keep the instruction not to add a[data-style='warm-editorial']base block, and keep it as a MEASURED statement rather than a claim about the current consumer set — that framing is what MOTIR-3933 corrected and it should not regress.packages/design-system/src/components/theme/AppearancePickers.tsx(~line 285) — thePalettePickercomment cites "the StyleVignette nested-base caveat" as a live reason for the hardcoded swatch. The swatch stays hardcoded (it is also theme-independent, which is the other half of the reason); the citation needs updating.
Acceptance criteria
-
packages/design-system/theme.csscontains 69@scope ([data-style='…']) to ([data-style])blocks and zero remaining[data-style='X'] <descendant>rules outside the four.style-vignette … > .sv-canvasones. Show the count from a script, not from agrep -cover a tree you have been editing. -
tests/theme/styleRegistry.test.tspasses, reads the@scopeform, and its material-rule count floor is 69. Removing one@scopewrapper in the working tree makes it fail — demonstrate that, then restore.⚠️ AMENDED ON THE RECORD (2026-08-30, MOTIR-3997's own run). The 69 could not go on
materialRulesChecked, which this card's What to do section names. Measured atorigin/main00e949a68with a postcss classifier: there are 69 material rule BLOCKS (88 selectors across them — 14 blocks carry a selector list sharing one anchor), and only 32 of the 69 carry a colour-bearing declaration.materialRulesCheckedcounts that colour-bearing subset — it is what the two palette-derivation assertions iterate — sotoBeGreaterThanOrEqual(69)is unpassable on a correct tree. The 69 therefore went on the population this card's own REASON describes ("so a material rule authored outside a scope in future fails here instead of going uncounted", which quantifies over all material rules): the test now carriesMATERIAL_RULES.length >= 69as the ratchet and the matcher guard, with the two existing assertions unchanged over the same colour-bearing population and their floor still 4. Both sentences of this card hold; only the counter the 69 attaches to moved. Demonstrated red at 68 and restored, per this criterion. Evidence: motir-core#2482. -
pnpm testis not run locally over the full suite (the PR's CI is the authority); run the theme lane's changed files only:tests/theme/styleRegistry.test.ts,tests/theme/scopedPreviewIsolation.test.ts,tests/theme/designSystemParity.test.ts,packages/design-system/test/themeCssExport.test.ts. -
pnpm lint,pnpm typecheckandprettier --checkpass on the changed files. -
The app shape is unchanged. State in the PR body which check establishes it — CI's theme suite is the gate; the ADR's own 5610-declaration prototype comparison is prior art to cite, not a substitute.
-
No consumer changes.
StyleVignette,TokensSpecimen,app/tokens/page.tsxandcomponents/onboarding/DesignStep.tsxkeep their current markup — the wrapper's existingdata-styleIS the scope limit. A diff that touches a call site means the mechanism was mis-applied. -
Ships in
motir-coreonly. Nomotir-marketingchange and nonpm publish— that is the release card.