Skip to content

feat(banner): compact phone layout for the notice buttons - #307

Open
fabiodalez-dev wants to merge 2 commits into
mainfrom
feat/mobile-compact-layout
Open

fabiodalez-dev wants to merge 2 commits into
mainfrom
feat/mobile-compact-layout

Conversation

@fabiodalez-dev

@fabiodalez-dev fabiodalez-dev commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Reported by a user whose banner took more than half the screen on a phone while rendering correctly on desktop (cabinhub.co.uk). Below 440px the shipped templates give each notice button a full-width row of its own, so the notice is 372px tall on a 390×844 phone — 44% of the viewport, and past half of it on a 375×667 screen.

This adds banner_control.mobile_layout with two values. comfortable is the default and is byte-for-byte the current behaviour, so no installed site changes appearance on update; compact lays the buttons out on a shared row.

Measured against a live 1.32.0 site at five widths, before and after:

width before after rows
430px 351px (41.6%) 223px (26.4%) one
390px 372px (44.1%) 244px (28.9%) one
375px 372px (43.0%) 244px (28.2%) one
360px 372px (41.3%) 296px (32.9%) two
320px 336px (33.2%) 260px (25.7%) two

Three constraints shaped the CSS, and none of them is cosmetic.

flex: 1 1 0 makes accept and reject exactly as wide as each other because one layout pass sizes them, not because two widths were written to match. EDPB Guidelines 03/2022 require equal prominence between accepting and refusing, and that has to survive translation into any language — which hardcoded widths do not.

min-height: 44px keeps the tap target at the size a finger needs. Reclaiming height by shrinking the target would trade one usability problem for a worse one.

Under 360px three buttons no longer fit side by side, so accept and reject stay paired on their own row and the customise button — the longest label, and the one that takes no part in the equal-prominence comparison — drops to a row of its own.

The rules are emitted after boost_css_specificity(), already carrying their #faz-consent prefix, so they are specificity 1-1-0 against the template's own rules and win on document order without a single !important. A first prototype written at class specificity failed at 360px and below against a live site, which is what the prefix is there to prevent.

Two things worth knowing about how this was tested. The setting is part of the prepare_banner_styles() transient key, because without that a site toggling the layout would keep being served yesterday's stylesheet for a day. And the first control run of the E2E spec passed with the CSS disabled — it was reading a stale faz_boosted_css_v3_ transient. After clearing it both tests went red with the right message, which is the only reason I trust them: a test that cannot fail proves nothing.

Covered by tests/e2e/specs/mobile-compact-layout.spec.ts (viewport height ratio, single row at 390px, accept and reject identical in width, height and top offset, every button ≥44px, no clipped labels, plus the paired fallback at 320px) and 13 assertions in the settings sanitiser suite, since the value is interpolated into a CSS class name and so is whitelisted rather than passed through.

Summary by CodeRabbit

  • Nuove funzionalità
    • Aggiunta un’opzione per scegliere tra layout «Confortevole» e «Compatto» per il banner su dispositivi mobili. Il layout compatto affianca i pulsanti di accettazione e rifiuto; sugli schermi più piccoli, il pulsante di personalizzazione passa a una riga separata.
    • Nel layout compatto, il controllo «Do Not Sell» occupa l’ultima riga e può andare a capo.
    • Il layout predefinito resta «Confortevole» e le modifiche non influiscono sulla visualizzazione desktop.

Below 440px the shipped templates give each notice button a full-width row of
its own, so the notice is 372px tall on a 390x844 phone - 44% of the viewport,
and past half of it on a 375x667 screen. Reported by a user whose banner took
more than half the screen on mobile while rendering correctly on desktop.

Add banner_control.mobile_layout with two values. 'comfortable' is the default
and is byte-for-byte the current behaviour, so no installed site changes
appearance on update; 'compact' lays the buttons out on a shared row.

Measured against a live 1.32.0 site at five widths, before and after:

  430px  351px 41.6% -> 223px 26.4%   one row
  390px  372px 44.1% -> 244px 28.9%   one row
  375px  372px 43.0% -> 244px 28.2%   one row
  360px  372px 41.3% -> 296px 32.9%   two rows
  320px  336px 33.2% -> 260px 25.7%   two rows

Three constraints shaped the CSS, and none of them is cosmetic:

- `flex: 1 1 0` makes accept and reject exactly as wide as each other because
  one layout pass sizes them, not because two widths were written to match.
  EDPB Guidelines 03/2022 require equal prominence between accepting and
  refusing, and that has to survive translation into any language - which
  hardcoded widths do not.
- `min-height: 44px` keeps the tap target at the size a finger needs.
  Reclaiming height by shrinking the target would trade one usability problem
  for a worse one.
- Under 360px three buttons no longer fit side by side, so accept and reject
  stay paired on their own row and the customise button - the longest label,
  and the one that takes no part in the equal-prominence comparison - drops to
  a row of its own.

The rules are emitted after boost_css_specificity(), already carrying their
`#faz-consent` prefix, so they are specificity 1-1-0 against the template's own
rules and win on document order without a single `!important`. A first
prototype written at class specificity failed at 360px and below against a live
site, which is what the prefix is there to prevent.

The mobile layout is part of the boosted-CSS transient key. Keyed on the
template alone, toggling the setting would keep serving yesterday's stylesheet
for a day and the setting would look broken; the generated banner-<md5>.css
filename already derives from the assembled CSS, so a switch mints a new file
rather than overwriting a cached one. It is a site-wide value, not a
per-visitor one, so it stays invariant under Cache Compatibility Mode.

The stored value is whitelisted on save and re-checked on read. It decides
which CSS is emitted, so a row written before the sanitiser existed, or by
hand, must not reach the stylesheet.

Tests: 13 sanitiser assertions covering the whitelist, array input through the
full settings pipeline, case and whitespace normalisation and the shipped
default; a new E2E spec asserting the three properties that a later CSS edit
could silently break - that compact actually shortens the notice, that accept
and reject keep identical size on a shared row, and that every button stays
44px tall - plus that no label is clipped. Both suites were confirmed to fail
when the compact CSS is disabled, which on the E2E side required flushing the
boosted-CSS transient: the first control run passed against a stale cached
stylesheet and proved nothing.
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: fabiodalez-dev/FAZ-Cookie-Manager/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 7ff6e29e-c770-4a4f-ab20-592dd800eb86

📥 Commits

Reviewing files that changed from the base of the PR and between 1532818 and c911863.

⛔ Files ignored due to path filters (7)
  • languages/faz-cookie-manager-cs_CZ.po is excluded by !languages/*.po
  • languages/faz-cookie-manager-de_DE.po is excluded by !languages/*.po
  • languages/faz-cookie-manager-fr_FR.po is excluded by !languages/*.po
  • languages/faz-cookie-manager-hr.po is excluded by !languages/*.po
  • languages/faz-cookie-manager-it_IT.mo is excluded by !languages/*.mo
  • languages/faz-cookie-manager-it_IT.po is excluded by !languages/*.po
  • languages/faz-cookie-manager-nl_NL.po is excluded by !languages/*.po
📒 Files selected for processing (6)
  • admin/modules/settings/includes/class-settings.php
  • admin/views/settings.php
  • frontend/class-frontend.php
  • languages/faz-cookie-manager.pot
  • tests/e2e/specs/mobile-compact-layout.spec.ts
  • tests/unit/test-settings-sanitize.php

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.


Walkthrough

Il banner aggiunge l’opzione mobile_layout, con comfortable come valore predefinito e compact come alternativa. In modalità compatta, il CSS adatta la disposizione dei pulsanti agli schermi stretti. I test coprono la sanitizzazione e i layout a 390 px e 320 px.

Changes

Layout mobile del banner

Layer / File(s) Summary
Opzione e impostazioni
admin/modules/settings/includes/class-settings.php, admin/views/settings.php, tests/unit/test-settings-sanitize.php, languages/faz-cookie-manager.pot
Aggiunge l’opzione predefinita comfortable, ne limita i valori a comfortable e compact e introduce il selettore nelle impostazioni. I test verificano la sanitizzazione e il valore predefinito. Il catalogo POT aggiunge le nuove stringhe e aggiorna i riferimenti sorgente.
Generazione e verifica del CSS compatto
frontend/class-frontend.php, tests/e2e/specs/mobile-compact-layout.spec.ts
La cache CSS distingue i layout e usa la revisione v4. In modalità compact, i pulsanti si dispongono secondo le soglie di 440 px e 360 px; Do Not Sell occupa l’ultima riga. I test end-to-end verificano dimensioni, disposizione e assenza di sovrapposizioni.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Suggested reviewers: claude

Merge Risk: ⚪ Minimal · up to c9118

The compact layout is opt-in and preserves comfortable as the default. No actionable merge-blocking issue remains in the available evidence; merge after normal checks pass.

Security Architecture Review

Security architecture risk: 🔵 Low · up to c9118

The option selects between two fixed presentation layouts, retains the existing appearance by default and remains behind administrator permissions. No newly expanded attack path was identified. Interrupted updates and external page-cache recovery remain unverified, so the assessment does not declare zero risk.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — An authorized layout change affects banner presentation for visitors to the WordPress site. The selector is site-wide rather than visitor-specific, and the traced change does not grant visitors settings-write authority.

Security Findings and Attack Paths

  • inferred — No introduced settings-write or option-to-CSS injection path was identified in the traced flow. The routed PHP-public methods are internal settings operations; the inspected browser helpers and sanitization assertions belong to tests, not newly exposed HTTP handlers.

Trust Boundaries and Controls

  • observed — The settings REST write route uses a permission callback requiring manage_options and nonce verification. Submitted settings are sanitized before persistence, and the visitor-facing CSS consumer revalidates the layout before choosing fixed stylesheet rules.

Resilience and Maintainability Implications

  • observed — Settings persistence precedes cache-invalidation hooks and does not check the write result or provide a transaction. This ordering also exists in the inspected pre-feature ancestor. Normal completion reaches template and external-cache listeners, but recovery after interruption is unverified; it is not established as a newly weakened privacy guarantee.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 5 files. (1 skipped: … 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 Il titolo descrive in modo conciso la modifica principale: aggiunge un layout compatto per i pulsanti del banner su telefono.
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 5 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

@pullfrog pullfrog 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.

Important

The compact rules assume the notice has exactly three buttons. A combined "GDPR + US" banner (the documented applicableLaw='gdpr' + donotSell mode) renders a fourth .faz-btn — the Do-Not-Sell button — which the max-width:360px override does not re-base, so it collapses to a sliver at ≤360px. See the inline comment.

Reviewed changes

  • banner_control.mobile_layout setting — new comfortable (default, byte-identical to current behaviour) / compact value; whitelisted in sanitize_option(), added to get_excludes() so an array payload is coerced rather than recursed against a scalar default, and defaulted in get_defaults().
  • Compact CSS — compact_mobile_css() returns a fixed stylesheet appended last in prepare_banner_styles(), already carrying the #faz-consent prefix so it wins on document order at equal-or-higher specificity. Cache key bumped to faz_boosted_css_v3_ and now includes the layout.
  • Settings UI — new "Mobile Layout" card with a data-path select; populated through the existing FAZ.populateForm path, so the hardcoded selected is harmless.
  • Tests — a Playwright spec (row count, viewport-height ratio, accept/reject parity, ≥44px tap targets, no clipped labels, 320px fallback) plus sanitizer/get_defaults() unit assertions. The spec asserts the comfortable baseline stacks and the compact layout actually shortens the notice, so it can fail if the CSS stops applying.
  • Translations — POT/PO churn from regeneration plus the new strings (only it_IT.mo recompiled). Mechanical.

I confirmed the default path emits an empty string so installed sites do not change appearance, that the REST endpoint (banner-rest) and the PHP render share the assembled CSS, and that the compact selectors really are specificity 1-2-0 and emitted after boost_css_specificity().

ℹ️ Verification scope is the three-button English GDPR notice

The layout switch is global, but the E2E spec only exercises the accept/reject/customize notice in English. The CCPA notice and combined banners render a Do-Not-Sell button (the case below), and translated/custom labels are never measured — so a fixed-width free-space layout that relies on white-space:nowrap has no regression guard for those. Consider at least a fourth-button assertion in the existing spec.

Technical details
# Coverage gap: non-GDPR banners and non-English labels

## Affected sites
- `tests/e2e/specs/mobile-compact-layout.spec.ts` — buttons are selected only by `.faz-notice-btn-wrapper .faz-btn` and identified by `/accept/`, `/reject|decline|refuse/`, so only the shipped English labels are covered.
- `frontend/class-frontend.php`:6908-6925 — layout is width-driven with `white-space:nowrap`; nothing bounds a label wider than its flex-basis.

## Required outcome
- The compact layout is exercised against a banner that carries a Do-Not-Sell control (CCPA or combined) at ≤360px, and against at least one label longer than the shipped English strings.

## Open questions for the human
- Is the compact layout intended for CCPA / combined banners too, or should the setting be gated to GDPR-only notices?

ℹ️ Nitpicks

  • admin/modules/settings/includes/class-settings.php:516-518 — the comment says the value is "interpolated into a CSS class name on the banner container", but mobile_layout is never interpolated anywhere; it selects which CSS is emitted and forms part of the transient key (frontend/class-frontend.php:6852-6864 describes this correctly). Worth correcting so the whitelist rationale matches the real attack surface.
  • frontend/class-frontend.php:6908-6911 — compact forces white-space:nowrap on the notice buttons, where the template's mobile rule had white-space:unset. Combined with min-width:0 and flex:1 1 0, a label longer than its one-third basis overflows and overlaps the next button instead of wrapping. Fine for the shipped English labels; worth a second look for narrower buttons.

Important

Pullfrog covered this run's model usage. DeepSeek Flash is fast and cheap — expect lighter work than a frontier model. This model allowance is temporary and separate from your Pullfrog plan. Connect a model-provider subscription or API key →

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (model usage covered by Pullfrog) | 𝕏

Comment thread frontend/class-frontend.php
Comment thread admin/modules/settings/includes/class-settings.php Outdated
…layout

Addresses both findings on the compact-layout PR.

The Do-Not-Sell button is the fourth control in the notice wrapper and the
only one with no `order` of its own — not in the template's mobile rules
either, so at order:0 it already sorted ahead of Accept. Stacked full-width
that was merely odd. On a single row it broke: it had to share the first line
with the accept/reject pair, and under 360px their 40% bases left it almost
nothing to grow into, so it collapsed to a few pixels while the inherited
white-space:nowrap pushed its label across Accept.

It now takes a full-width row of its own, last, behind the two options the
equal-prominence rule actually compares (EDPB 03/2022 governs accept against
reject, not this control). `white-space` is reset there as well: "Do Not Sell
or Share My Personal Information" does not fit 270px at 13px, the wording is
statutory and cannot be shortened, so the box gives instead. The selector
matches on data-faz-tag because the two variants of the control share nothing
else — the shortcode emits either a `.faz-btn` button or a bare `<a>` with no
class at all.

This is not a hand-built configuration. Geo_Runtime turns donotSell on for a
US visitor even when applicableLaw stays 'gdpr', and class-template.php then
keeps the button precisely because its status is true, so once jurisdiction
routing lands this is the ordinary rendering for part of the audience — and
the row where a US visitor's opt-out lives.

Also bumps the assembled-CSS pipeline revision to v4. The compact rules are
appended after $raw_css is hashed into the cache key, so without the bump an
install that had already cached v3 within this same plugin version would keep
serving the stylesheet in which the control collapses. That revision exists
for exactly this case.

The second finding was a comment, not behaviour: the mobile_layout sanitiser
claimed the value is interpolated into a CSS class name on the banner
container. It never reaches markup — it selects which stylesheet
compact_mobile_css() returns and forms part of the cache key. The whitelist is
still right; the stated reason was not, so it now says what the guard is
actually for.

Covered by a third case in the compact-layout spec, at 320px with the combined
mode configured: the control must not share a row with either half of the
pair, must span the row, must come last, and must neither overflow nor overlap.
It restores the banner in a finally so a mid-test throw cannot leak a fourth
button into later specs, and it clears the assembled-CSS transient first, so a
red means the CSS is wrong rather than stale. Control run: removing the new
rule turns exactly the shared-row assertion red.

@pullfrog pullfrog 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.

✅ No new issues found. Both earlier threads are addressed and resolved.

Reviewed changes

This run covers c9118632, the commit pushed after the initial review.

  • Do-Not-Sell control gets its own row: a new compact rule matches [data-faz-tag="donotsell-button"] and gives the control order:4; flex:1 1 100%; white-space:normal. I checked both shortcode variants in shortcodes.json (6.0.0 and 6.2.0), and each one carries that attribute, so the classless <a> variant is covered too. The rule is specificity 1-2-0 and is emitted after the .faz-btn rule, and the 360px block doesn't override it.
  • Cache key bumped to faz_boosted_css_v4_: sites that already cached the v3 stylesheet within the same plugin version will rebuild it.
  • Sanitizer comment corrected: the mobile_layout whitelist comment now says what the value is actually used for. It picks which stylesheet is emitted and is part of the cache key. It never reaches markup.
  • New 320px E2E case for the combined mode: the test turns on donotSell under applicableLaw='gdpr', deletes the stale assembled-CSS transients, and checks four things: the control sits on a separate row below the accept/reject pair, spans the full row, doesn't overflow, and doesn't overlap the pair. The banner row is restored from a snapshot in finally.

ℹ️ Nitpicks

  • frontend/class-frontend.php:6896: the compact_mobile_css() docblock still says the rules are "specificity 1-1-0". The selectors are #faz-consent .faz-notice-btn-wrapper .faz-btn*, and the new attribute selector is the same weight, so all of them are 1-2-0. Document order still decides the result, so this is only a wording fix.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant