Skip to content

fix: emit override CSS unlayered when flat bundle is served - #221

Merged
jackgranatowski merged 5 commits into
mainfrom
claude/plugin-configurator-controls-sule1f
Jul 28, 2026
Merged

fix: emit override CSS unlayered when flat bundle is served#221
jackgranatowski merged 5 commits into
mainfrom
claude/plugin-configurator-controls-sule1f

Conversation

@jackgranatowski

@jackgranatowski jackgranatowski commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

The flat CSS bundles (with css_flat = true) have all @layer declarations stripped, making any layered CSS inert against them — an unlayered declaration beats every layered one regardless of specificity or source order. Token overrides were being emitted wrapped in @layer slashed.overrides, which is correct for the layered bundles but silently fails against flat bundles, causing all configurator controls to stop working when flat mode is enabled.

This change makes the override CSS wrapper follow the bundle actually being served: layered when Slashed_CSS_Loader::layers_enabled() is true (layered bundle), unlayered when false (flat bundle). The same logic is applied to the Bricks and Gutenberg dark-mode bridges, which had the same vulnerability.

Changes

  • class-css-generator.php: Added use_cascade_layer() method that checks Slashed_CSS_Loader::layers_enabled() and emits the override block wrapped in @layer slashed.overrides only when the served bundle has layers. Against flat bundles, the same declarations are emitted unlayered.
  • class-css-loader.php: Added layers_enabled() public method (reads Slashed_Settings::get_css_flat()) and wrap_layer() helper for inline CSS that needs conditional wrapping.
  • Bricks/Gutenberg integrations: Updated dark-mode bridges to use wrap_layer() so they follow the same bundle-aware logic.
  • Tests: Added CssGeneratorFlatBundleTest to verify the wrapper switches correctly, and override-effect-probe.mjs — a manual QA tool that measures whether each configurator control actually changes anything on the page by diffing live tokens in a headless browser against both layered and flat bundles.
  • Docs: Added troubleshooting-token-overrides.md and diagnose-page-tokens.js (browser console snippet) to help diagnose why a control reads back correctly but changes nothing on a specific site.

Type

  • fix
  • feat
  • docs
  • chore / tooling

Checklist

  • Conventional Commit messages (feat:, fix:, docs:, …)
  • npm test passes
  • npm run lint passes (stylelint + php -l)
  • npm run verify passes (version metadata in sync)
  • Generated artifacts not hand-edited
  • CHANGELOG.md updated under ## [Unreleased]
  • Built SPA assets committed if editor-app/admin-app source changed (no SPA changes)

Notes

The flat-bundle regression was introduced when flat CSS variants were added to the framework but the plugin's override emission and dark-mode bridges were not updated to match. This fix ensures both code paths (layered and flat) work correctly by making the wrapper choice data-driven: it reads the same css_flat setting that determines which bundle is served, so they stay in sync.

The override-effect-probe.mjs tool is a manual dev/QA aid (not part of npm test or CI) that mechanically verifies every control group can actually move the page. It caught this regression and will prevent it from recurring.

https://claude.ai/code/session_01DftXg4tjRkey2c3ybxAWjN

Summary by CodeRabbit

  • Improvements

    • Improved CSS override behavior across layered and flat bundle configurations.
    • Ensured theme and editor styling remains correctly scoped when cascade layers are supported.
    • Flat bundles now apply overrides reliably without relying on cascade-layer syntax.
  • Documentation

    • Added troubleshooting guidance for diagnosing token overrides that do not visibly affect a page.
    • Documented a local verification workflow for confirming override behavior.
  • Testing

    • Added automated and manual checks covering layered and flat bundles, derived values, and rendered style changes.

claude added 4 commits July 28, 2026 08:11
The generated override CSS was always wrapped in @layer slashed.overrides.
That is correct against the layered bundles, where the framework declares
every token inside @layer slashed.tokens and reserves slashed.overrides as
the last layer — but the flat bundles are the same rules with every @layer
stripped, and an unlayered declaration beats any layered one regardless of
specificity or source order. With the flat variant enabled, the whole
override block was therefore silently inert: every configurator control —
colours, gap/gutter, the spacing and typography modular scales, every scale
knob — saved fine and changed nothing on the page, while the SPA's own live
preview (which injects unlayered :root CSS) kept showing the change.

Wrap the block only when the served bundle has layers, and route the same
decision through Slashed_CSS_Loader so the Bricks and Gutenberg dark-mode
bridges — layered inline CSS with the same defect — follow the bundle too.

Verified in a headless browser against the committed dist bundles: before
this change all 18 probed control groups came out DEAD on the flat bundle
and OK on the layered one; after it, both modes are OK. That measurement is
now a committed tool, tests/override-effect-probe.mjs, which asks the real
PHP emitter for the CSS a site would serve and diffs every live --sf-* token
against the un-overridden page. It carries a must-be-DEAD control case so a
noisy measurement can't make the run pass vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DftXg4tjRkey2c3ybxAWjN
The override-effect probe proves whether the emitted CSS can move the page at
all. When it reports every control group OK and a control still does nothing on
a real site, the conflict is in that page's CSS environment — which no amount of
reading plugin code will find.

Add scripts/diagnose-page-tokens.js, a console snippet that lists every
stylesheet, every rule declaring a watched token together with the cascade layer
it sits in, and the computed source-knob vs derived-output values at :root. An
unlayered concrete --sf-space-* declaration beats every @layer, so it shadows the
modular-scale knobs while the knob itself reads back correctly — which is exactly
what "the control saves but nothing changes" looks like. Verified against a page
with that interference injected: the snippet names the shadowing rule.

docs/troubleshooting-token-overrides.md turns the snippet's output into a
decision table, and records what is already measured-good so it doesn't get
re-audited: the configurator writes only live token names, the vendored tree
matches the pinned framework ref, and the live preview injects unlayered CSS
(which is why a preview/page mismatch indicates a cascade conflict rather than
a broken control).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DftXg4tjRkey2c3ybxAWjN
…e knobs

The existing sections could show a source knob holding the user's value and
still not answer whether that value reaches the ladder. A reported case had
--sf-space-base-max at 1.9 (not the framework's 2, so an override was clearly
landing at :root) while every --sf-space-* step was inconsistent with ANY
setting of the knobs: no interpolation factor fits, best residual 31.7% RMS,
and the implied per-step ratio wandered 1.71 → 1.54 → 1.37, which a modular
scale cannot produce. The ladder was being declared by something else, so the
knobs fed a formula whose result was discarded.

That case is invisible to a "did the output tokens stay at their defaults?"
test, because the shadowing ladder is not the default one either. Fit the
framework's own formula to the measured steps instead: the interpolation factor
and the rem size both cancel when steps are taken relative to --sf-space-m, so
the shape of the ladder alone decides it.

Models clamp()'s lower-bound collapse when the knobs put the mobile end above
the desktop end, without which a healthy page reads as a 9.6% mismatch.
Verified: 0.0% on healthy pages across the fluid range (320px, 1280px, 2200px),
under a 62.5% rem base, and with the reported base-max override; 46.5% against
an injected hand-set ladder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DftXg4tjRkey2c3ybxAWjN
…e the map

Two defects the first real-world run exposed.

Rules were reported as shadowing the scale regardless of whether their selector
can match the root element. Bricks ships long `unset` resets for --sf-* custom
properties on its own components, so a run on a Bricks site accused
`#bricks-settings #tab-custom-code tr.code-note` (from admin.min.css, a selector
that cannot match :root) of killing the spacing scale. Test each rule with
documentElement.matches() and use that for the verdict; the rows stay in the
listing as context.

The verdict also pointed at "a rule outside @layer slashed.tokens", implying a
third-party stylesheet — but the confirmed cause was a stale per-step ladder
stored in the plugin's own override map, emitted into @layer slashed.overrides at
:root. Report that case separately and explain it: an explicit per-step value
beats the knob that would have generated it by design, so base and ratio read
back correctly and do nothing. Points at `wp option get slashed_overrides` and
says which keys to drop.

Verified against a faithful reproduction of the reported page — the stored
clamps plus the Bricks unset noise — which reproduces the measured ladder
exactly (4.828 / 11.808 / 20.14 / 30.9 / 107.6px at 872px viewport): the
override-map warning fires and the Bricks false positive is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DftXg4tjRkey2c3ybxAWjN

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jackgranatowski, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a151664-ed20-409c-a524-a1076ad97fa1

📥 Commits

Reviewing files that changed from the base of the PR and between b43ea81 and 8d47704.

📒 Files selected for processing (5)
  • SLASHED-for-WP/includes/class-css-loader.php
  • tests-php/CssGeneratorFlatBundleTest.php
  • tests-php/bootstrap.php
  • tests/override-effect-probe.mjs
  • tests/php-harness/emit-override-css.php
📝 Walkthrough

Walkthrough

The PR adds bundle-aware cascade-layer handling for generated overrides and inline theme bridges, tests layered and flat output, and introduces Playwright, PHP, and DevTools diagnostics for verifying rendered token effects.

Changes

Cascade-Layer Override Handling

Layer / File(s) Summary
Layer selection and override generation
SLASHED-for-WP/includes/class-css-loader.php, SLASHED-for-WP/includes/class-css-generator.php, tests-php/CssGeneratorFlatBundleTest.php
The loader exposes layer-mode helpers, the generator emits layered or unlayered overrides based on css_flat, and PHPUnit coverage verifies declarations and derived tokens in both modes.
Inline theme bridge integration
SLASHED-for-WP/integrations/bricks/..., SLASHED-for-WP/integrations/gutenberg/...
Bricks and Gutenberg bridge styles use conditional layer wrapping with a fallback for unavailable loader classes.
Rendered override effect verification
tests/php-harness/emit-override-css.php, tests/override-effect-probe.mjs
A PHP harness emits override CSS, while the Playwright probe compares computed token and probe-property changes across layered and flat bundles.
Page token troubleshooting
scripts/diagnose-page-tokens.js, docs/troubleshooting-token-overrides.md, CLAUDE.md
DevTools diagnostics inspect token declarations, computed values, ladder consistency, and shadowing; documentation describes the diagnostic workflow and probe usage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Configurator
  participant PHP_Harness
  participant CSS_Bundle
  participant Browser
  Configurator->>PHP_Harness: Submit token overrides
  PHP_Harness->>CSS_Bundle: Generate layered or flat override CSS
  PHP_Harness-->>Configurator: Return override CSS
  Configurator->>Browser: Inject override CSS
  Browser-->>Configurator: Report computed token changes
Loading

Possibly related PRs

Suggested labels: codex

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the core change: emitting override CSS unlayered for flat bundles.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/plugin-configurator-controls-sule1f

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added the codex label Jul 28, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/override-effect-probe.mjs (1)

115-115: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Browser/server aren't closed on the early-exit and error paths.

browser (line 115) is launched before the per-bundle loop; the missing-bundle check (lines 140-143) calls process.exit(1) without closing it or the HTTP server, and there's no try/finally around the loop to guarantee cleanup if page.evaluate/measure throws. For a script that spawns a real Chromium process this can leave it orphaned.

Also applies to: 138-186

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/override-effect-probe.mjs` at line 115, Ensure the browser launched in
the main probe flow and the HTTP server are always closed. Wrap the per-bundle
processing loop, including page.evaluate and measure calls, in try/finally
cleanup, and replace the missing-bundle process.exit path with cleanup before
exiting; preserve the existing success behavior while preventing orphaned
resources on errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@SLASHED-for-WP/includes/class-css-loader.php`:
- Around line 87-89: Update layers_enabled() and the CSS bundle URL resolution
flow so layer capability is derived from the same resolved bundle URL, including
slashed/css_bundle_url and integration-level URL override paths, rather than
only from get_css_flat(). Carry the resolved layer capability alongside the URL
and ensure mismatched flat/layered overrides emit the corresponding CSS
behavior.

In `@tests/override-effect-probe.mjs`:
- Around line 101-113: Update the probe server created by createServer to pass
the loopback host explicitly when calling listen, while preserving the existing
ephemeral-port behavior and local consumer URLs.

In `@tests/php-harness/emit-override-css.php`:
- Around line 26-48: Configure the PHP harness before loading or executing
Slashed_Token_Store and Slashed_CSS_Generator so display_errors is disabled or
redirected to stderr, keeping stdout exclusively for the json_encode output
consumed by the probe. Ensure notices, warnings, and deprecations cannot corrupt
the JSON stream while preserving their visibility for diagnosing PHP issues.

---

Nitpick comments:
In `@tests/override-effect-probe.mjs`:
- Line 115: Ensure the browser launched in the main probe flow and the HTTP
server are always closed. Wrap the per-bundle processing loop, including
page.evaluate and measure calls, in try/finally cleanup, and replace the
missing-bundle process.exit path with cleanup before exiting; preserve the
existing success behavior while preventing orphaned resources on errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 30afa462-0fc0-4b56-95c5-c5e9dd58bc5c

📥 Commits

Reviewing files that changed from the base of the PR and between 8913b76 and b43ea81.

📒 Files selected for processing (10)
  • CLAUDE.md
  • SLASHED-for-WP/includes/class-css-generator.php
  • SLASHED-for-WP/includes/class-css-loader.php
  • SLASHED-for-WP/integrations/bricks/includes/class-enqueue.php
  • SLASHED-for-WP/integrations/gutenberg/includes/class-enqueue.php
  • docs/troubleshooting-token-overrides.md
  • scripts/diagnose-page-tokens.js
  • tests-php/CssGeneratorFlatBundleTest.php
  • tests/override-effect-probe.mjs
  • tests/php-harness/emit-override-css.php

Comment thread SLASHED-for-WP/includes/class-css-loader.php
Comment thread tests/override-effect-probe.mjs
Comment thread tests/php-harness/emit-override-css.php
Review feedback on #221.

layers_enabled() read css_flat, but get_url() lets slashed/css_bundle_url (and
the per-integration filters on top of it) serve a bundle that setting does not
describe. A flat bundle served while css_flat is false gets layered overrides
that can never win — the exact defect this PR exists to fix — and the inverse
puts unlayered rules above the framework's @media-scoped rules. Derive the mode
from the resolved URL whenever it is recognisably one of SLASHED's own bundles,
fall back to the setting for a URL that cannot be read, and add
slashed/css_layers_enabled so a host serving an unrecognisable bundle can state
it outright. url_layer_mode() is pure so the naming contract is tested directly.

That made layers_enabled() reach SLASHED_PATH, which only slashed.php defines —
an integration plugin running standalone loads this class without it, so the
dark-mode bridges would have fataled on an undefined constant. get_url() now
returns the empty-URL path there, as it already does for a missing bundle file.

Also from the review, in the probe and its PHP harness: bind the fixture server
to loopback instead of every interface (awaiting the listening event, since
passing a host makes listen() resolve asynchronously and address() would still
be null), always close the browser and server via try/finally so no exit path
orphans a Chromium, and send PHP diagnostics to stderr so a stray notice cannot
corrupt the JSON stdout the probe parses.

Verified: 228 PHPUnit assertions including seven new URL cases, 198 node tests,
the probe green in both bundle modes with no orphaned processes, and the
missing-bundle bail-out exiting 1 after cleanup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DftXg4tjRkey2c3ybxAWjN
@jackgranatowski
jackgranatowski merged commit 478b18e into main Jul 28, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants