Dedupe theme stylesheets via content-derived scopes#5440
Conversation
Preview deploymentsHost Test Results 1 files ± 0 1 suites ±0 2h 53m 45s ⏱️ + 3m 2s Results for commit 2609046. ± Comparison against earlier commit b69a22b. Realm Server Test Results 1 files ± 0 1 suites ±0 11m 0s ⏱️ -40s Results for commit 2609046. ± Comparison against earlier commit b69a22b. |
45bc6fb to
7517789
Compare
7517789 to
b69a22b
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b69a22b877
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
This PR updates Boxel’s theme scoping so cards that share the same theme produce the same data-boxel-theme-scope (theme card id + CSS fingerprint), enabling host-side deduplication of identical theme <style> blocks while keeping prerendered fragments self-contained and safe across theme edits.
Changes:
- Introduces
themeScope(themeId, css)(theme id + 64-bit FNV-1a fingerprint) and updates theme-scoped rendering paths to use it consistently. - Adds a host
ThemeStyleDeduper(wired via an instance initializer) to keep only one identical theme stylesheet active in the live DOM by togglingHTMLStyleElement.disabled. - Adds unit + acceptance coverage for the scope format contract and for dedup/survivor-promotion behavior.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/host/tests/acceptance/theme-card-test.gts | Adds acceptance coverage for shared theme scope identity and live DOM stylesheet dedup/survivor promotion. |
| packages/host/app/lib/theme-style-deduper.ts | New MutationObserver-driven deduper that disables redundant identical theme <style> tags. |
| packages/host/app/instance-initializers/dedupe-theme-styles.ts | Starts/stops the deduper for the app instance lifecycle. |
| packages/boxel-ui/test-app/tests/unit/theme-scoped-css-test.ts | Adds unit tests that pin the themeScope serialization format and behavior. |
| packages/boxel-ui/addon/src/helpers/theme-scoped-css.ts | Implements themeScope() and the 64-bit CSS fingerprint used for stable scoping. |
| packages/boxel-ui/addon/src/helpers.ts | Re-exports themeScope from the addon helpers barrel. |
| packages/base/style-reference.gts | Passes @themeId into ThemeDashboard so its scope matches the container scope. |
| packages/base/structured-theme.gts | Passes @themeId into ThemeDashboard. |
| packages/base/detailed-style-reference.gts | Passes @themeId into ThemeDashboard. |
| packages/base/brand-guide.gts | Passes @themeId into ThemeDashboard. |
| packages/base/default-templates/theme-dashboard.gts | Uses themeScope(themeId, themeCss) (with guid fallback) for persisted-safe scoping and emits scoped theme CSS using that scope. |
| packages/base/field-component.gts | Updates CardContainer theme scope to be theme-derived (theme id + CSS hash) instead of card-id/guid-based. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Cards sharing a theme previously each emitted their own copy of the theme stylesheet under a per-card scope, so the CSS payload scaled with rendered card instances — especially costly for compositions embedding many themed cards, where every prerendered fragment carries the copies. Deriving the scope from the theme card's id plus an fnv1a hash of its CSS makes all cards on one theme emit byte-identical style elements (harmless as duplicate rules, dedupable by consumers), while the hash keeps scopes from different versions of a theme distinct so cached prerendered fragments from before and after a theme edit cannot restyle each other's cards. The theme dashboard's inner dark-toggle re-scope now also derives from content instead of guidFor, which could repeat across prerender jobs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ThemeDashboard previously derived its theme scope from the constant 'theme-dashboard', so distinct themes were separated only by the CSS hash and the dashboard could never share a scope with the same theme rendered through field-component. Callers now pass @ThemeID={{@model.id}} and the guid fallback covers unsaved theme cards previewing their own CSS, matching field-component's semantics. The scope's CSS fingerprint grows from one 32-bit FNV-1a pass to two passes with different seeds, concatenated as fixed-width hex. Scope equality implies declaration equality is the invariant that keeps mixed-vintage prerendered fragments from restyling each other; at 32 bits a collision between two versions of a theme was left to chance, at 64 it is not a practical concern.
Recognize theme stylesheets by marker attribute, not CSS text prefix
b69a22b to
2609046
Compare
Themed cards each emit their own copy of the theme stylesheet so prerendered fragments stay self-contained, but only one copy per theme needs to be active on a page.
data-boxel-theme-scopeis now derived from the theme card's id plus a 64-bit fingerprint of its CSS, instead of the rendered card's id. Cards sharing a theme emit byte-identical stylesheets; different versions of a theme keep distinct scopes, so cached fragments from before and after a theme edit can't restyle each other.ThemeDashboardderives its scope from the same identity, so both render paths agree.ThemeStyleDeduperkeeps one copy of each identical theme stylesheet active in the live DOM, promoting a survivor if the active copy is removed. It disables via thedisabledproperty (not an attribute), so serialized prerendered HTML keeps the full stylesheet.Unit tests pin the scope format as a serialization contract; acceptance tests cover shared-scope stylesheet identity, single-active-copy dedup, and survivor promotion.