From 53ec88ac7d98fc544a891518b5a64185ddc0756e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 14:58:48 +0000 Subject: [PATCH] fix(web): keep secondary shell text readable in the light theme Secondary shell text was muted text dimmed further with opacity: the section labels, the projects caption, the sidebar "+" icons, the privacy panel, the trading subtitle and the Heute brief's Mission Control link. Muted text measures 4.4:1 on the light sidebar, so these fell to 1.9 to 3.4:1 in the light theme; a few were below 4.5:1 in dark as well. - Secondary sidebar text and icons share `rk-sidebar-muted`, the sidebar foreground at 65% that row times already used (5.0:1 on the light sidebar). Row times now use the class too. - Section labels and space or project group titles use the dimmed foreground in light and keep the 80% muted color in dark; group titles move up from 60%, which was 3.4:1 in dark. - The privacy panel heading is plain text with a success-colored check instead of emerald text, and its details pass 4.5:1 in both themes. - Needs-you chips keep the warning tint and border; their label uses the foreground color in light. - The Mission Control link uses the full muted color. - E2E: the shell test checks every text in the sidebar except the logo, and the Heute brief, in both themes, and captures `shell-contrast-{light,dark}-heute`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SSVQGR4WU92LaddTP19GcQ --- apps/web/e2e/chat-contrast.spec.ts | 58 ++++++++++--------- apps/web/src/components/DataTrustPanel.tsx | 12 ++-- .../src/components/TradingModeIndicator.tsx | 2 +- apps/web/src/pages/HeuteBrief.tsx | 4 +- apps/web/src/pages/Shell.tsx | 24 ++++---- apps/web/src/pages/shell/roster-row-style.ts | 4 +- apps/web/src/styles.css | 8 ++- 7 files changed, 59 insertions(+), 53 deletions(-) diff --git a/apps/web/e2e/chat-contrast.spec.ts b/apps/web/e2e/chat-contrast.spec.ts index c950ab6..675f974 100644 --- a/apps/web/e2e/chat-contrast.spec.ts +++ b/apps/web/e2e/chat-contrast.spec.ts @@ -16,11 +16,12 @@ test.use({ viewport: { width: 1280, height: 1000 } }); /** * Texts in `locator` below WCAG AA against the surface they are painted on. With `inside`, every - * visible text and typed value within it counts, except in disabled controls, which WCAG exempts. + * visible text and typed value within it counts, except in disabled controls, which WCAG exempts, + * and in elements matching `skip`. */ -function unreadableTexts(locator: Locator, inside: boolean) { +function unreadableTexts(locator: Locator, inside: boolean, skip: string | null) { return locator.evaluate( - (root, { inside, min }) => { + (root, { inside, skip, min }) => { const context = document .createElement("canvas") .getContext("2d", { willReadFrequently: true }); @@ -81,7 +82,11 @@ function unreadableTexts(locator: Locator, inside: boolean) { }; const elements = inside ? [root, ...root.querySelectorAll("*")].filter( - (element) => ownText(element) && shown(element) && !element.closest(":disabled"), + (element) => + ownText(element) && + shown(element) && + !element.closest(":disabled") && + !(skip && element.closest(skip)), ) : [root]; return elements.flatMap((element) => { @@ -90,14 +95,18 @@ function unreadableTexts(locator: Locator, inside: boolean) { return ratio < min ? [`${ratio.toFixed(2)}:1 ${text.slice(0, 48)}`] : []; }); }, - { inside, min: MIN_CONTRAST }, + { inside, skip, min: MIN_CONTRAST }, ); } -async function expectReadable(locator: Locator, what: string, { inside = false } = {}) { +async function expectReadable( + locator: Locator, + what: string, + { inside = false, skip = null }: { inside?: boolean; skip?: string | null } = {}, +) { await expect(locator).toBeVisible(); await expect - .poll(() => unreadableTexts(locator, inside), { message: `${what} is readable` }) + .poll(() => unreadableTexts(locator, inside, skip), { message: `${what} is readable` }) .toEqual([]); } @@ -213,6 +222,9 @@ test("shell surfaces stay readable in light and dark themes", async ({ page }, t await composer.fill("Draft a note about owls"); const sidebar = page.getByTestId("bots-sidebar"); const sidePanel = page.getByTestId("side-panel"); + await expect(sidebar.locator("[data-main-conversation]")).toBeVisible(); + await expect(sidebar.getByRole("button", { name: "Researcher, 1:1 conversation" })).toBeVisible(); + await expect(page.getByTestId("trading-mode-status")).toBeVisible(); await inEachTheme(page, async (theme) => { await captureScreenshot(page, testInfo, `shell-contrast-${theme}`); @@ -220,27 +232,9 @@ test("shell surfaces stay readable in light and dark themes", async ({ page }, t await expectReadable(page.getByTestId("composer-bar"), `${theme}: the composer`, { inside: true, }); - await expectReadable( - sidebar.getByRole("button", { name: "Heute", exact: true }), - `${theme}: Heute`, - { inside: true }, - ); - await expectReadable( - sidebar.locator("[data-main-conversation]"), - `${theme}: the main conversation row`, - { inside: true }, - ); - await expectReadable( - sidebar.getByRole("button", { name: "Researcher, 1:1 conversation" }), - `${theme}: the selected bot row`, - { inside: true }, - ); - await expectReadable(page.getByTestId("trading-mode-status"), `${theme}: trading status`, { - inside: true, - }); - await expectReadable(page.getByTestId("user-menu-trigger"), `${theme}: the user menu`, { - inside: true, - }); + // The header carries the logo, which WCAG exempts; its create button is checked on its own. + await expectReadable(sidebar, `${theme}: the sidebar`, { inside: true, skip: ".app-drag" }); + await expectReadable(page.getByTestId("create-menu-trigger"), `${theme}: the create button`); }); await openNewBot(page); @@ -259,4 +253,12 @@ test("shell surfaces stay readable in light and dark themes", async ({ page }, t await captureScreenshot(page, testInfo, `shell-contrast-${theme}-settings`); await expectReadable(sidePanel, `${theme}: the bot settings form`, { inside: true }); }); + + await sidebar.getByRole("button", { name: "Heute", exact: true }).click(); + const heute = page.getByTestId("heute-brief"); + await expect(heute.getByTestId("heute-mission-control-link")).toBeVisible(); + await inEachTheme(page, async (theme) => { + await captureScreenshot(page, testInfo, `shell-contrast-${theme}-heute`); + await expectReadable(heute, `${theme}: the Heute brief`, { inside: true }); + }); }); diff --git a/apps/web/src/components/DataTrustPanel.tsx b/apps/web/src/components/DataTrustPanel.tsx index 5400308..885fc91 100644 --- a/apps/web/src/components/DataTrustPanel.tsx +++ b/apps/web/src/components/DataTrustPanel.tsx @@ -10,16 +10,16 @@ const TRUST_ITEMS = [ export const DataTrustPanel = memo(function DataTrustPanel() { return (
-
- +
+ Datenschutz aktiv
{TRUST_ITEMS.map(({ icon: Icon, label, desc }) => ( -
- - {label} - · {desc} +
+ + {label} + · {desc}
))}
diff --git a/apps/web/src/components/TradingModeIndicator.tsx b/apps/web/src/components/TradingModeIndicator.tsx index 6780aa1..90c9622 100644 --- a/apps/web/src/components/TradingModeIndicator.tsx +++ b/apps/web/src/components/TradingModeIndicator.tsx @@ -45,7 +45,7 @@ export function TradingModeIndicator({ {presentation.subtitle ? ( - + ) : null} diff --git a/apps/web/src/pages/HeuteBrief.tsx b/apps/web/src/pages/HeuteBrief.tsx index cdf12b7..b6e9664 100644 --- a/apps/web/src/pages/HeuteBrief.tsx +++ b/apps/web/src/pages/HeuteBrief.tsx @@ -178,7 +178,7 @@ export function HeuteBrief({ href={MISSION_CONTROL_URL} target="_blank" rel="noreferrer" - className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-ui-label text-muted-foreground/70 transition-colors hover:text-muted-foreground" + className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-ui-label text-muted-foreground transition-colors hover:text-foreground" data-testid="heute-mission-control-link" > Mission Control @@ -235,7 +235,7 @@ function EmptyDay({ onStartChat, hasBots }: { onStartChat: () => void; hasBots: function ActionableChip({ label }: { label: string }) { return ( - + {label} ); diff --git a/apps/web/src/pages/Shell.tsx b/apps/web/src/pages/Shell.tsx index ae34218..f50bf3b 100644 --- a/apps/web/src/pages/Shell.tsx +++ b/apps/web/src/pages/Shell.tsx @@ -3456,7 +3456,7 @@ export function ShellPage() { className={`app-no-drag flex size-control-sm items-center justify-center rounded-full ${ activityMode ? "bg-primary text-primary-foreground" - : "text-muted-foreground/70 hover:text-foreground/75" + : "rk-sidebar-muted hover:text-foreground/75" }`} > @@ -3592,7 +3592,7 @@ export function ShellPage() { type="button" aria-label={t`Neue Unterhaltung`} data-testid="conversation-picker-trigger" - className="text-muted-foreground/60 hover:text-foreground/90 transition-colors" + className="rk-sidebar-muted hover:text-foreground/90 transition-colors" > @@ -3633,7 +3633,7 @@ export function ShellPage() {
) : null} {conversationSidebarGroups.length === 0 && !mainConversationEntry ? ( -
+

{t`Noch keine Unterhaltung.`}

{projectSidebarGroups.length === 0 ? ( -
+

{t`No projects yet.`}