Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/components/privacy-quiet-signal-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,15 @@ export function PrivacyQuietSignalPage() {
data-sticky={chromeSticky ? "true" : "false"}
className={cn(
chromeSticky && "sticky top-0",
"z-30 border-b border-[color:var(--border)] bg-[color:var(--surface-glass)]/95 shadow-[var(--e1)] backdrop-blur-xl",
"z-30 border-b border-[color:var(--border-strong)] shadow-[var(--e2)]",
// Phone baseline: a full-width opaque bar with no blur, matching the
// `.edge-glass-header` / `.universal-header` phone rule in globals.css.
// This page had carried translucent glass at every width, so scrolled
// content ghosted straight through the title — visible against the
// amber obligation band, which showed through the header text.
"bg-[color:var(--surface)] backdrop-blur-none",
// From sm the shared glass treatment is correct again.
"sm:bg-[color-mix(in_srgb,var(--surface)_72%,transparent)] sm:backdrop-blur-xl",
"print:hidden",
)}
>
Expand Down
41 changes: 41 additions & 0 deletions tests/ui-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2466,6 +2466,47 @@ test.describe("PsychSift UI smoke coverage", () => {
});
}

test("privacy sticky chrome is an opaque bar on phones and glass from sm", async ({ page }) => {
// Proven in a browser, not by class presence: tailwind-merge keeps both the
// base and the `sm:` utility, so stylesheet order — not the class list —
// decides which one wins, and jsdom cannot resolve either.
//
// The phone case is the one that matters. This band carried translucent
// glass at every width, so scrolled content ghosted through the title; it
// was visible against the amber obligation band, whose fill read straight
// through "Data handling". The repo's own header rule for phones
// (`.edge-glass-header` / `.universal-header` in globals.css) is an opaque
// bar with no blur, and this page now follows it.
const chrome = page.getByTestId("privacy-sticky-chrome");

await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/privacy", { waitUntil: "domcontentloaded" });
await expect(chrome).toBeVisible();

const phone = await chrome.evaluate((element) => {
const style = getComputedStyle(element);
return { background: style.backgroundColor, blur: style.backdropFilter };
});
// Fully opaque: no alpha channel at all, so nothing can read through.
expect(phone.background).not.toMatch(/\/\s*0?\.\d+|rgba?\([^)]*,\s*0?\.\d+\s*\)/);
expect(phone.blur).toBe("none");

// Scrolled, the band must actually hide what passes behind it.
await page.evaluate(() => window.scrollTo(0, 600));
await expect(chrome).toBeVisible();
await expect(page.getByRole("heading", { level: 1, name: /handles your data/i })).not.toBeInViewport();

// From sm the shared glass treatment is correct again.
await page.setViewportSize({ width: 900, height: 900 });
await page.goto("/privacy", { waitUntil: "domcontentloaded" });
const wide = await chrome.evaluate((element) => {
const style = getComputedStyle(element);
return { background: style.backgroundColor, blur: style.backdropFilter };
});
expect(wide.blur).toContain("blur(");
expect(wide.background).not.toBe(phone.background);
});

test("privacy trust brief remains operable with reduced motion and forced colours", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.emulateMedia({ reducedMotion: "reduce", forcedColors: "active" });
Expand Down
Loading