fix(css): scope baseline CSS to cascade layers so imported sheets win, in publisher and canvas - #426
Draft
fosferon wants to merge 2 commits into
Draft
fix(css): scope baseline CSS to cascade layers so imported sheets win, in publisher and canvas#426fosferon wants to merge 2 commits into
fosferon wants to merge 2 commits into
Conversation
…ts win Unlayered CSS outranks every cascade layer regardless of specificity or source order. The publisher emitted `reset` and `framework` unlayered, so the CMS baseline silently overrode any imported stylesheet that uses `@layer` — which is every Tailwind v4 build, whose entire output is wrapped in `@layer theme/base/components/utilities`. The effect on a site brought in through Site Import: the imported CSS shipped intact but inert. Pages rendered with the CMS's own tokens — `--primary` bleeding through as unexplained blocks, the font stack falling back to system-ui, surfaces and spacing collapsing — while the author's design was present in the bundle and simply outranked. Wrap the baseline in `instatic-reset` and `instatic-framework`, with the layer-order statement emitted at the top of `reset` (the first stylesheet on the page) so relative order is fixed before either layer is populated. Imported sheets declare their own layers afterwards and therefore win. Deliberately unchanged: - `style` (Styles-panel + imported class rules) stays unlayered, so explicit authoring decisions keep beating both baseline and imports. - `userStyles` is not re-wrapped; nesting a kept Tailwind sheet inside our layer would re-invert the precedence this fixes. Measured on a 29-route Astro/Tailwind v4 site imported into a clean install, comparing rendered height against the source site: mean fidelity 54.7% -> 95.6%, with 24/29 routes landing within 8% of the original and 14 matching exactly. Inter went from 4 styled elements to 2,310; the brand colour from 0 occurrences to 104. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FPTjUK1YJcwK83zYoANRbb
…e too
Same defect as the published bundle, second code path. ClassStyleInjector
wraps everything `buildCanvasClassCSS` returns in `@layer user-authored`,
and UserStylesheetInjector puts imported stylesheets in that same layer.
Rules sitting DIRECTLY in `user-authored` outrank rules in its nested
sublayers. The canvas baseline was emitted unlayered, so once wrapped it
sat directly in `user-authored` while an imported Tailwind sheet — whose
output is entirely `@layer theme/base/utilities` — ended up one level
deeper as `user-authored.theme` etc. The baseline won everything.
Net effect: the editor canvas previewed CMS defaults even after the
published page was fixed. `:where(body) { font-family: system-ui }` beat
the site's own font, sections collapsed, and an imported site rendered as
near-blank containers — uneditable, because you cannot edit what you
cannot see. The published page and the canvas disagreed, which is exactly
what the canvas exists to prevent.
Name the sublayers so the iframe reproduces the published cascade:
reset -> `instatic-reset`, fonts + framework root -> `instatic-framework`,
order statement first. Author class CSS stays unlayered within
`user-authored`, mirroring the published bundle's unlayered `style`.
Measured on a 29-route Tailwind v4 site imported into a clean install,
desktop canvas frame vs the published page (6403px):
before 3429px, body font system-ui
after 6503px, body font Inter
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPTjUK1YJcwK83zYoANRbb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Wraps the CMS baseline CSS in named cascade layers (
instatic-reset,instatic-framework) in both places it is assembled — the published bundle and the editor canvas.Why
Unlayered CSS outranks every cascade layer, regardless of specificity or source order. The baseline was emitted unlayered, so it silently overrode any imported stylesheet that uses
@layer— which is every Tailwind v4 build, whose entire output is wrapped in@layer theme/base/components/utilities.For a site brought in through Site Import, the imported CSS shipped intact but inert. Pages rendered with the CMS's own tokens:
--primarybleeding through as unexplained coloured blocks, the font stack falling back tosystem-ui, surfaces and spacing collapsing — while the author's design sat in the bundle, fully present and simply outranked.The failure is quiet and easy to misread as a broken import. The diagnostic tell is that element and image counts match the source site almost exactly while every page renders dramatically shorter.
Two code paths, same defect
1. Published bundle (
server/publish/siteCssBundle.ts) —resetandframeworkwere emitted unlayered alongside a layereduserStyles.2. Editor canvas (
src/admin/pages/site/canvas/canvasClassCss.ts) — subtler.ClassStyleInjectorwraps everything the builder returns in@layer user-authored, andUserStylesheetInjectorputs imported sheets in that same layer. Rules sitting directly inuser-authoredoutrank rules in its nested sublayers, so the unlayered baseline sat atuser-authoredwhile Tailwind landed one level deeper atuser-authored.theme— and won everything.The second one matters independently: after fixing only the publisher, the canvas still previewed CMS defaults, so the published page and the editor disagreed — precisely what the canvas exists to prevent. An imported site appeared as near-blank containers in the editor: uneditable, because you cannot edit what you cannot see.
How
reset.cssis the first stylesheet on the page (and the canvas emits its order statement first), so layer order is fixed before either layer is populated. Imported sheets declare their own layers afterwards and therefore win.Deliberately unchanged in both paths:
style/ the canvas's class registry output) stays unlayered, so explicit authoring decisions keep beating both the baseline and imported sheets.userStylesis not re-wrapped. Nesting a kept Tailwind sheet inside our layer would re-invert the precedence this fixes.Impact
frameworkbundle and is therefore layered too, so an imported stylesheet can now override module defaults. For a CMS that imports whole sites that ordering seems right — flagging it explicitly as a judgement call worth a maintainer's eye.Measured
A 29-route Astro + Tailwind v4 site imported into a clean install, comparing rendered height against the source site.
Published pages:
Interstyled elementsEditor canvas (desktop frame, published page is 6403px):
system-uiInterVerification
bun test— 6667 pass. One pre-existing failure incollabRelayIntegration("refuses a read-only edit AND resets the viewer"), confirmed failing identically on cleanmainwith these changes stashed.bun run build— clean.bun run lint— clean.siteCssBundle.test.ts, three inclassStyleInjector.test.ts— layer-order statement position, wrappers present, author CSS staying unlayered,userStylesnot re-wrapped.🤖 Generated with Claude Code
https://claude.ai/code/session_01FPTjUK1YJcwK83zYoANRbb