From 49eb5aeed408334d2c5db40b5694fa02f7b52a9b Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 08:16:35 +0000 Subject: [PATCH 01/18] fix(webapp): clip org avatar and fall back on image load error --- apps/webapp/app/components/primitives/Avatar.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/primitives/Avatar.tsx b/apps/webapp/app/components/primitives/Avatar.tsx index 728626271f..9c2da786f8 100644 --- a/apps/webapp/app/components/primitives/Avatar.tsx +++ b/apps/webapp/app/components/primitives/Avatar.tsx @@ -9,6 +9,7 @@ import { StarIcon, } from "@heroicons/react/20/solid"; import type { Prisma } from "@trigger.dev/database"; +import { useState } from "react"; import { z } from "zod"; import { cn } from "~/utils/cn"; @@ -190,20 +191,26 @@ function AvatarIcon({ } function AvatarImage({ avatar, size }: { avatar: ImageAvatar; size: number }) { - if (!avatar.url) { + const [failed, setFailed] = useState(false); + + if (!avatar.url || failed) { return ( - + ); } return ( - + Organization avatar setFailed(true)} /> ); From 7101d376b01ed755a7a504c8951d35dfaf3d0c63 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 08:18:44 +0000 Subject: [PATCH 02/18] fix(webapp): allow gstatic favicon redirects and changelog images in img-src --- .server-changes/org-avatar-csp.md | 6 ++++ apps/webapp/app/utils/cspImageOrigins.test.ts | 33 +++++++++++++++++-- apps/webapp/app/utils/cspImageOrigins.ts | 13 ++++++-- .../test/dashboardAgentImageCsp.test.ts | 9 +++++ 4 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 .server-changes/org-avatar-csp.md diff --git a/.server-changes/org-avatar-csp.md b/.server-changes/org-avatar-csp.md new file mode 100644 index 0000000000..d08ab40e2a --- /dev/null +++ b/.server-changes/org-avatar-csp.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Organization avatars and changelog images now display again in the dashboard instead of showing as broken images. diff --git a/apps/webapp/app/utils/cspImageOrigins.test.ts b/apps/webapp/app/utils/cspImageOrigins.test.ts index 0ad1e34697..c08c6cde4b 100644 --- a/apps/webapp/app/utils/cspImageOrigins.test.ts +++ b/apps/webapp/app/utils/cspImageOrigins.test.ts @@ -17,7 +17,11 @@ function directivePermits(directive: string, imageUrl: string): boolean { if (!source.startsWith("http")) return false; const parsed = new URL(source); if (parsed.protocol !== url.protocol || parsed.host !== url.host) return false; - return parsed.pathname === "/" || parsed.pathname === url.pathname; + // CSP path matching: a source path ending in "/" matches by prefix, otherwise it + // must match exactly. The query string is never part of the match. + return parsed.pathname.endsWith("/") + ? url.pathname.startsWith(parsed.pathname) + : parsed.pathname === url.pathname; }); } @@ -104,9 +108,9 @@ describe("parseCspImageOrigins", () => { }); describe("buildImgSrcDirective", () => { - it("is self, data, blob, the SSO avatar hosts and the favicon endpoint by default", () => { + it("is self, data, blob, the SSO avatar hosts, the favicon endpoints and the changelog by default", () => { expect(buildImgSrcDirective()).toBe( - "img-src 'self' data: blob: https://avatars.githubusercontent.com https://lh3.googleusercontent.com https://www.google.com/s2/favicons" + "img-src 'self' data: blob: https://avatars.githubusercontent.com https://lh3.googleusercontent.com https://www.google.com/s2/favicons https://t0.gstatic.com/faviconV2 https://t1.gstatic.com/faviconV2 https://t2.gstatic.com/faviconV2 https://t3.gstatic.com/faviconV2 https://trigger.dev/changelog/" ); }); @@ -120,6 +124,29 @@ describe("buildImgSrcDirective", () => { ); }); + it("permits the gstatic shard the favicon endpoint redirects to", () => { + expect( + directivePermits( + buildImgSrcDirective(), + "https://t2.gstatic.com/faviconV2?url=https://example.com&size=128" + ) + ).toBe(true); + }); + + it("permits nothing else on a gstatic shard, and no shard we did not list", () => { + const directive = buildImgSrcDirective(); + expect(directivePermits(directive, "https://t2.gstatic.com/beacon.png")).toBe(false); + expect(directivePermits(directive, "https://t9.gstatic.com/faviconV2")).toBe(false); + }); + + it("permits changelog images by path prefix, and nothing else on our domain", () => { + const directive = buildImgSrcDirective(); + expect(directivePermits(directive, "https://trigger.dev/changelog/some-post/image.png")).toBe( + true + ); + expect(directivePermits(directive, "https://trigger.dev/anything.png")).toBe(false); + }); + it("permits both OAuth avatar hosts", () => { const directive = buildImgSrcDirective(); expect(directivePermits(directive, "https://avatars.githubusercontent.com/u/1?v=4")).toBe(true); diff --git a/apps/webapp/app/utils/cspImageOrigins.ts b/apps/webapp/app/utils/cspImageOrigins.ts index ed6fc03061..d7a70077b6 100644 --- a/apps/webapp/app/utils/cspImageOrigins.ts +++ b/apps/webapp/app/utils/cspImageOrigins.ts @@ -6,9 +6,11 @@ */ /** - * Always allowed: own origin, inline data, object URLs, the SSO avatar hosts, and the - * favicon endpoint org avatars are stored as (see `utils/favicon.ts`). The path pins - * that one endpoint — CSP matches the path and ignores the query string. + * Always allowed: own origin, inline data, object URLs, the SSO avatar hosts, the + * favicon endpoint org avatars are stored as (see `utils/favicon.ts`), and our own + * changelog images. The path pins each endpoint — CSP matches the path and ignores the + * query string. The favicon endpoint 302s to a `tN.gstatic.com` shard and CSP re-checks + * the redirect target, so the shards are listed too. A trailing "/" matches by prefix. */ export const BASE_IMG_SRC_SOURCES = [ "'self'", @@ -17,6 +19,11 @@ export const BASE_IMG_SRC_SOURCES = [ "https://avatars.githubusercontent.com", "https://lh3.googleusercontent.com", "https://www.google.com/s2/favicons", + "https://t0.gstatic.com/faviconV2", + "https://t1.gstatic.com/faviconV2", + "https://t2.gstatic.com/faviconV2", + "https://t3.gstatic.com/faviconV2", + "https://trigger.dev/changelog/", ] as const; export type RejectedOrigin = { value: string; reason: string }; diff --git a/apps/webapp/test/dashboardAgentImageCsp.test.ts b/apps/webapp/test/dashboardAgentImageCsp.test.ts index 52406fffd3..d2e4ffefe1 100644 --- a/apps/webapp/test/dashboardAgentImageCsp.test.ts +++ b/apps/webapp/test/dashboardAgentImageCsp.test.ts @@ -24,6 +24,15 @@ describe("document image CSP", () => { expect(directive).not.toMatch(/(^|\s)https?:(\s|$)/); }); + it("permits a changelog image", () => { + const imageUrl = "https://trigger.dev/changelog/some-post/image.png"; + const permitted = buildImgSrcDirective() + .split(" ") + .slice(1) + .some((source) => source.endsWith("/") && imageUrl.startsWith(source)); + expect(permitted).toBe(true); + }); + it("sets the header on every document response, not only on /login", () => { // The set() call must sit outside the /login branch. const loginBranch = source.slice( From bb93590b10c690071d0eab086f9554b8ea81032c Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 08:20:27 +0000 Subject: [PATCH 03/18] fix(webapp): reset avatar error state when the image url changes --- apps/webapp/app/components/primitives/Avatar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/Avatar.tsx b/apps/webapp/app/components/primitives/Avatar.tsx index 9c2da786f8..9b3226de09 100644 --- a/apps/webapp/app/components/primitives/Avatar.tsx +++ b/apps/webapp/app/components/primitives/Avatar.tsx @@ -77,7 +77,7 @@ export function Avatar({ /> ); case "image": - return ; + return ; } } From 50af0b06c6da2c5f5924e6216c9fee757a36e24b Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 08:22:42 +0000 Subject: [PATCH 04/18] fix(webapp): detect avatar images that failed before hydration --- apps/webapp/app/components/primitives/Avatar.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/Avatar.tsx b/apps/webapp/app/components/primitives/Avatar.tsx index 9b3226de09..1fc15d36c1 100644 --- a/apps/webapp/app/components/primitives/Avatar.tsx +++ b/apps/webapp/app/components/primitives/Avatar.tsx @@ -9,7 +9,7 @@ import { StarIcon, } from "@heroicons/react/20/solid"; import type { Prisma } from "@trigger.dev/database"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { z } from "zod"; import { cn } from "~/utils/cn"; @@ -193,6 +193,13 @@ function AvatarIcon({ function AvatarImage({ avatar, size }: { avatar: ImageAvatar; size: number }) { const [failed, setFailed] = useState(false); + // A server-rendered image can finish failing before hydration, so onError never fires. + const detectFailedLoad = useCallback((node: HTMLImageElement | null) => { + if (node && node.complete && node.naturalWidth === 0) { + setFailed(true); + } + }, []); + if (!avatar.url || failed) { return ( Organization avatar Date: Thu, 13 Aug 2026 08:43:15 +0000 Subject: [PATCH 05/18] fix(webapp): correct img-src redirect comment and tighten changelog test --- apps/webapp/app/utils/cspImageOrigins.ts | 3 ++- apps/webapp/test/dashboardAgentImageCsp.test.ts | 9 ++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/utils/cspImageOrigins.ts b/apps/webapp/app/utils/cspImageOrigins.ts index d7a70077b6..267c72df92 100644 --- a/apps/webapp/app/utils/cspImageOrigins.ts +++ b/apps/webapp/app/utils/cspImageOrigins.ts @@ -10,7 +10,8 @@ * favicon endpoint org avatars are stored as (see `utils/favicon.ts`), and our own * changelog images. The path pins each endpoint — CSP matches the path and ignores the * query string. The favicon endpoint 302s to a `tN.gstatic.com` shard and CSP re-checks - * the redirect target, so the shards are listed too. A trailing "/" matches by prefix. + * only the host on a redirect, so the shards are listed too; their path pin limits + * direct loads only. A trailing "/" matches by prefix. */ export const BASE_IMG_SRC_SOURCES = [ "'self'", diff --git a/apps/webapp/test/dashboardAgentImageCsp.test.ts b/apps/webapp/test/dashboardAgentImageCsp.test.ts index d2e4ffefe1..ff716623a7 100644 --- a/apps/webapp/test/dashboardAgentImageCsp.test.ts +++ b/apps/webapp/test/dashboardAgentImageCsp.test.ts @@ -24,13 +24,8 @@ describe("document image CSP", () => { expect(directive).not.toMatch(/(^|\s)https?:(\s|$)/); }); - it("permits a changelog image", () => { - const imageUrl = "https://trigger.dev/changelog/some-post/image.png"; - const permitted = buildImgSrcDirective() - .split(" ") - .slice(1) - .some((source) => source.endsWith("/") && imageUrl.startsWith(source)); - expect(permitted).toBe(true); + it("allows changelog images", () => { + expect(buildImgSrcDirective().split(" ")).toContain("https://trigger.dev/changelog/"); }); it("sets the header on every document response, not only on /login", () => { From edcf7f8d04414624e6f316918e50518d34974355 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 08:43:17 +0000 Subject: [PATCH 06/18] fix(webapp): keep the avatar fallback accessible --- apps/webapp/app/components/primitives/Avatar.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/webapp/app/components/primitives/Avatar.tsx b/apps/webapp/app/components/primitives/Avatar.tsx index 1fc15d36c1..fdd6981293 100644 --- a/apps/webapp/app/components/primitives/Avatar.tsx +++ b/apps/webapp/app/components/primitives/Avatar.tsx @@ -203,6 +203,8 @@ function AvatarImage({ avatar, size }: { avatar: ImageAvatar; size: number }) { if (!avatar.url || failed) { return ( From 13f3ad15e678f544f7b112d6ee996e93076cb8cb Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 08:52:12 +0000 Subject: [PATCH 07/18] chore(webapp): drop the server-changes note --- .server-changes/org-avatar-csp.md | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .server-changes/org-avatar-csp.md diff --git a/.server-changes/org-avatar-csp.md b/.server-changes/org-avatar-csp.md deleted file mode 100644 index d08ab40e2a..0000000000 --- a/.server-changes/org-avatar-csp.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -area: webapp -type: fix ---- - -Organization avatars and changelog images now display again in the dashboard instead of showing as broken images. From 74a882a719cd985f2b3d13e8b771dcf9371641a8 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 10:58:20 +0000 Subject: [PATCH 08/18] fix(webapp): highlight the checked radio card in dark themes --- apps/webapp/app/components/primitives/RadioButton.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/components/primitives/RadioButton.tsx b/apps/webapp/app/components/primitives/RadioButton.tsx index d936a81b92..7592226c83 100644 --- a/apps/webapp/app/components/primitives/RadioButton.tsx +++ b/apps/webapp/app/components/primitives/RadioButton.tsx @@ -22,7 +22,7 @@ const variants = { }, "button/small": { button: - "flex items-center w-fit h-8 pl-2 pr-3 rounded-md border border-border-bright/50 shadow-xs bg-secondary transition hover:bg-background-raised data-disabled:opacity-70 data-disabled:hover:bg-secondary hover:data-[state=checked]:bg-text-bright/4 data-[state=checked]:bg-text-bright/4", + "flex items-center w-fit h-8 pl-2 pr-3 rounded-md border border-border-bright/50 shadow-xs bg-secondary transition hover:bg-background-raised data-disabled:opacity-70 data-disabled:hover:bg-secondary hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control", label: "text-sm text-text-bright select-none", description: "text-text-dimmed", inputPosition: "mt-0", @@ -30,7 +30,7 @@ const variants = { }, button: { button: - "w-fit py-2 pl-3 pr-4 rounded border border-border-bright/50 shadow-xs bg-secondary hover:bg-background-raised transition data-[state=checked]:bg-background-dimmed data-disabled:opacity-70", + "w-fit py-2 pl-3 pr-4 rounded border border-border-bright/50 shadow-xs bg-secondary hover:bg-background-raised transition hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control data-disabled:opacity-70", label: "text-text-bright select-none", description: "text-text-dimmed", inputPosition: "mt-1", @@ -38,7 +38,7 @@ const variants = { }, description: { button: - "w-full p-2.5 rounded-md border border-border-bright/50 shadow-xs bg-secondary transition hover:bg-background-raised data-disabled:opacity-70 hover:data-[state=checked]:bg-text-bright/4 data-[state=checked]:bg-text-bright/4", + "w-full p-2.5 rounded-md border border-border-bright/50 shadow-xs bg-secondary transition hover:bg-background-raised data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control", label: "text-text-bright font-semibold -mt-0.5 text-left text-sm", description: "text-text-dimmed mt-0 text-left", inputPosition: "mt-0", @@ -46,7 +46,7 @@ const variants = { }, icon: { button: - "w-full p-2.5 pb-4 rounded-sm border border-border-bright/50 shadow-xs bg-secondary hover:bg-background-raised transition data-disabled:opacity-70 data-[state=checked]:bg-background-dimmed", + "w-full p-2.5 pb-4 rounded-sm border border-border-bright/50 shadow-xs bg-secondary hover:bg-background-raised transition data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control", label: "text-text-bright font-semibold -mt-1 text-left", description: "text-text-dimmed mt-0 text-left", inputPosition: "mt-0", From 7eb5a91a8aa0bd998a02a567ea535501c4643221 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 11:02:23 +0000 Subject: [PATCH 09/18] fix(webapp): keep the selected api key card indigo on hover --- .../route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx index f1f267425c..c27eb15497 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx @@ -1028,7 +1028,7 @@ function PresetOptions({ id={`api-key-access-${preset.id}`} value={preset.id} variant="description" - className="h-full min-h-[3.5rem] items-start border-grid-bright bg-background-bright p-3 shadow-none [&_p]:mt-0.5 [&_p]:text-xs [&_p]:leading-snug hover:border-border-bright hover:bg-background-hover data-[state=checked]:border-indigo-500/70 data-[state=checked]:bg-indigo-500/10" + className="h-full min-h-[3.5rem] items-start border-grid-bright bg-background-bright p-3 shadow-none [&_p]:mt-0.5 [&_p]:text-xs [&_p]:leading-snug hover:border-border-bright hover:bg-background-hover data-[state=checked]:border-indigo-500/70 data-[state=checked]:bg-indigo-500/10 hover:data-[state=checked]:bg-indigo-500/15" label={ preset.id === "FULL_ACCESS" ? ( From 75fd9cc894bd15125a027953a0ea9438370ab3f7 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 11:05:27 +0000 Subject: [PATCH 10/18] fix(webapp): darken unchecked radio cards in dark themes --- apps/webapp/app/components/primitives/RadioButton.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/components/primitives/RadioButton.tsx b/apps/webapp/app/components/primitives/RadioButton.tsx index 7592226c83..0f486b0d53 100644 --- a/apps/webapp/app/components/primitives/RadioButton.tsx +++ b/apps/webapp/app/components/primitives/RadioButton.tsx @@ -22,7 +22,7 @@ const variants = { }, "button/small": { button: - "flex items-center w-fit h-8 pl-2 pr-3 rounded-md border border-border-bright/50 shadow-xs bg-secondary transition hover:bg-background-raised data-disabled:opacity-70 data-disabled:hover:bg-secondary hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control", + "flex items-center w-fit h-8 pl-2 pr-3 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-background-hover data-disabled:opacity-70 data-disabled:hover:bg-background-bright hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control", label: "text-sm text-text-bright select-none", description: "text-text-dimmed", inputPosition: "mt-0", @@ -30,7 +30,7 @@ const variants = { }, button: { button: - "w-fit py-2 pl-3 pr-4 rounded border border-border-bright/50 shadow-xs bg-secondary hover:bg-background-raised transition hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control data-disabled:opacity-70", + "w-fit py-2 pl-3 pr-4 rounded border border-border-bright/50 shadow-xs bg-background-bright hover:bg-background-hover transition hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control data-disabled:opacity-70", label: "text-text-bright select-none", description: "text-text-dimmed", inputPosition: "mt-1", @@ -38,7 +38,7 @@ const variants = { }, description: { button: - "w-full p-2.5 rounded-md border border-border-bright/50 shadow-xs bg-secondary transition hover:bg-background-raised data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control", + "w-full p-2.5 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-background-hover data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control", label: "text-text-bright font-semibold -mt-0.5 text-left text-sm", description: "text-text-dimmed mt-0 text-left", inputPosition: "mt-0", @@ -46,7 +46,7 @@ const variants = { }, icon: { button: - "w-full p-2.5 pb-4 rounded-sm border border-border-bright/50 shadow-xs bg-secondary hover:bg-background-raised transition data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control", + "w-full p-2.5 pb-4 rounded-sm border border-border-bright/50 shadow-xs bg-background-bright hover:bg-background-hover transition data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control", label: "text-text-bright font-semibold -mt-1 text-left", description: "text-text-dimmed mt-0 text-left", inputPosition: "mt-0", From 5ab86b4d2d0056da1a1c262f924d43ad84286285 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 11:17:18 +0000 Subject: [PATCH 11/18] fix(webapp): tone down the checked radio card grey --- apps/webapp/app/components/primitives/RadioButton.tsx | 8 ++++---- apps/webapp/app/tailwind.css | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/components/primitives/RadioButton.tsx b/apps/webapp/app/components/primitives/RadioButton.tsx index 0f486b0d53..97a561794c 100644 --- a/apps/webapp/app/components/primitives/RadioButton.tsx +++ b/apps/webapp/app/components/primitives/RadioButton.tsx @@ -22,7 +22,7 @@ const variants = { }, "button/small": { button: - "flex items-center w-fit h-8 pl-2 pr-3 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-background-hover data-disabled:opacity-70 data-disabled:hover:bg-background-bright hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control", + "flex items-center w-fit h-8 pl-2 pr-3 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-background-hover data-disabled:opacity-70 data-disabled:hover:bg-background-bright hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected", label: "text-sm text-text-bright select-none", description: "text-text-dimmed", inputPosition: "mt-0", @@ -30,7 +30,7 @@ const variants = { }, button: { button: - "w-fit py-2 pl-3 pr-4 rounded border border-border-bright/50 shadow-xs bg-background-bright hover:bg-background-hover transition hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control data-disabled:opacity-70", + "w-fit py-2 pl-3 pr-4 rounded border border-border-bright/50 shadow-xs bg-background-bright hover:bg-background-hover transition hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected data-disabled:opacity-70", label: "text-text-bright select-none", description: "text-text-dimmed", inputPosition: "mt-1", @@ -38,7 +38,7 @@ const variants = { }, description: { button: - "w-full p-2.5 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-background-hover data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control", + "w-full p-2.5 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-background-hover data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected", label: "text-text-bright font-semibold -mt-0.5 text-left text-sm", description: "text-text-dimmed mt-0 text-left", inputPosition: "mt-0", @@ -46,7 +46,7 @@ const variants = { }, icon: { button: - "w-full p-2.5 pb-4 rounded-sm border border-border-bright/50 shadow-xs bg-background-bright hover:bg-background-hover transition data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-control-hover data-[state=checked]:bg-surface-control", + "w-full p-2.5 pb-4 rounded-sm border border-border-bright/50 shadow-xs bg-background-bright hover:bg-background-hover transition data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected", label: "text-text-bright font-semibold -mt-1 text-left", description: "text-text-dimmed mt-0 text-left", inputPosition: "mt-0", diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css index e9955747a8..e4556fa653 100644 --- a/apps/webapp/app/tailwind.css +++ b/apps/webapp/app/tailwind.css @@ -158,6 +158,8 @@ --color-surface-control: var(--color-charcoal-600); --color-surface-control-hover: var(--color-charcoal-550); --color-surface-control-active: var(--color-charcoal-500); + --color-surface-selected: var(--color-charcoal-650); + --color-surface-selected-hover: var(--color-charcoal-600); --color-input-bg: var(--color-charcoal-750); /* Borders, from subtlest to most visible */ @@ -770,6 +772,8 @@ --color-surface-control: #dcdee3; --color-surface-control-hover: #cfd2d9; --color-surface-control-active: #b8bcc6; + --color-surface-selected: #dcdee3; + --color-surface-selected-hover: #cfd2d9; /* Borders */ --color-grid-dimmed: #eceef1; From 3e6c1ba87bcdfa4e669f3633904b206ea0477b72 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 11:21:02 +0000 Subject: [PATCH 12/18] fix(webapp): wire the selected surface tokens into the contrast slider --- apps/webapp/app/tailwind.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css index e4556fa653..604a0bae99 100644 --- a/apps/webapp/app/tailwind.css +++ b/apps/webapp/app/tailwind.css @@ -155,11 +155,11 @@ --color-background-bright: var(--color-charcoal-800); --color-background-hover: var(--color-charcoal-750); --color-background-raised: var(--color-charcoal-700); + --color-surface-selected: var(--color-charcoal-650); + --color-surface-selected-hover: var(--color-charcoal-600); --color-surface-control: var(--color-charcoal-600); --color-surface-control-hover: var(--color-charcoal-550); --color-surface-control-active: var(--color-charcoal-500); - --color-surface-selected: var(--color-charcoal-650); - --color-surface-selected-hover: var(--color-charcoal-600); --color-input-bg: var(--color-charcoal-750); /* Borders, from subtlest to most visible */ @@ -772,6 +772,7 @@ --color-surface-control: #dcdee3; --color-surface-control-hover: #cfd2d9; --color-surface-control-active: #b8bcc6; + /* Light keeps the control values for selection; only dark tones them down */ --color-surface-selected: #dcdee3; --color-surface-selected-hover: #cfd2d9; @@ -966,6 +967,8 @@ --color-surface-control: color-mix(in srgb, #dcdee3, #000 calc(var(--theme-contrast, 0) * 25%)); --color-surface-control-hover: color-mix(in srgb, #cfd2d9, #000 calc(var(--theme-contrast, 0) * 28%)); --color-surface-control-active: color-mix(in srgb, #b8bcc6, #000 calc(var(--theme-contrast, 0) * 32%)); + --color-surface-selected: color-mix(in srgb, #dcdee3, #000 calc(var(--theme-contrast, 0) * 25%)); + --color-surface-selected-hover: color-mix(in srgb, #cfd2d9, #000 calc(var(--theme-contrast, 0) * 28%)); --color-grid-dimmed: color-mix(in srgb, #eceef1, #000 calc(var(--theme-contrast, 0) * 28%)); --color-grid-bright: color-mix(in srgb, #e2e4e9, #000 calc(var(--theme-contrast, 0) * 32%)); --color-border-bright: color-mix(in srgb, #d2d5db, #000 calc(var(--theme-contrast, 0) * 38%)); From 2cb5e31ae38fe6802b5e21a6519ea36cf12116a2 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 11:38:08 +0000 Subject: [PATCH 13/18] fix(webapp): lighten the selected radio card in the light theme --- apps/webapp/app/tailwind.css | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css index 604a0bae99..93f67574f4 100644 --- a/apps/webapp/app/tailwind.css +++ b/apps/webapp/app/tailwind.css @@ -772,9 +772,10 @@ --color-surface-control: #dcdee3; --color-surface-control-hover: #cfd2d9; --color-surface-control-active: #b8bcc6; - /* Light keeps the control values for selection; only dark tones them down */ - --color-surface-selected: #dcdee3; - --color-surface-selected-hover: #cfd2d9; + /* Selection sits lighter than the controls here - on white a soft grey is + already enough to read as selected */ + --color-surface-selected: #e8e9ec; + --color-surface-selected-hover: #dcdee3; /* Borders */ --color-grid-dimmed: #eceef1; @@ -967,8 +968,8 @@ --color-surface-control: color-mix(in srgb, #dcdee3, #000 calc(var(--theme-contrast, 0) * 25%)); --color-surface-control-hover: color-mix(in srgb, #cfd2d9, #000 calc(var(--theme-contrast, 0) * 28%)); --color-surface-control-active: color-mix(in srgb, #b8bcc6, #000 calc(var(--theme-contrast, 0) * 32%)); - --color-surface-selected: color-mix(in srgb, #dcdee3, #000 calc(var(--theme-contrast, 0) * 25%)); - --color-surface-selected-hover: color-mix(in srgb, #cfd2d9, #000 calc(var(--theme-contrast, 0) * 28%)); + --color-surface-selected: color-mix(in srgb, #e8e9ec, #000 calc(var(--theme-contrast, 0) * 25%)); + --color-surface-selected-hover: color-mix(in srgb, #dcdee3, #000 calc(var(--theme-contrast, 0) * 28%)); --color-grid-dimmed: color-mix(in srgb, #eceef1, #000 calc(var(--theme-contrast, 0) * 28%)); --color-grid-bright: color-mix(in srgb, #e2e4e9, #000 calc(var(--theme-contrast, 0) * 32%)); --color-border-bright: color-mix(in srgb, #d2d5db, #000 calc(var(--theme-contrast, 0) * 38%)); From 068a8100a0ebfb70336895381be381405ffc95db Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 11:43:46 +0000 Subject: [PATCH 14/18] fix(webapp): soften the selected radio card further in light --- apps/webapp/app/tailwind.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css index 93f67574f4..c0958b48dd 100644 --- a/apps/webapp/app/tailwind.css +++ b/apps/webapp/app/tailwind.css @@ -774,8 +774,8 @@ --color-surface-control-active: #b8bcc6; /* Selection sits lighter than the controls here - on white a soft grey is already enough to read as selected */ - --color-surface-selected: #e8e9ec; - --color-surface-selected-hover: #dcdee3; + --color-surface-selected: #ebedf0; + --color-surface-selected-hover: #e3e5ea; /* Borders */ --color-grid-dimmed: #eceef1; @@ -968,8 +968,8 @@ --color-surface-control: color-mix(in srgb, #dcdee3, #000 calc(var(--theme-contrast, 0) * 25%)); --color-surface-control-hover: color-mix(in srgb, #cfd2d9, #000 calc(var(--theme-contrast, 0) * 28%)); --color-surface-control-active: color-mix(in srgb, #b8bcc6, #000 calc(var(--theme-contrast, 0) * 32%)); - --color-surface-selected: color-mix(in srgb, #e8e9ec, #000 calc(var(--theme-contrast, 0) * 25%)); - --color-surface-selected-hover: color-mix(in srgb, #dcdee3, #000 calc(var(--theme-contrast, 0) * 28%)); + --color-surface-selected: color-mix(in srgb, #ebedf0, #000 calc(var(--theme-contrast, 0) * 25%)); + --color-surface-selected-hover: color-mix(in srgb, #e3e5ea, #000 calc(var(--theme-contrast, 0) * 28%)); --color-grid-dimmed: color-mix(in srgb, #eceef1, #000 calc(var(--theme-contrast, 0) * 28%)); --color-grid-bright: color-mix(in srgb, #e2e4e9, #000 calc(var(--theme-contrast, 0) * 32%)); --color-border-bright: color-mix(in srgb, #d2d5db, #000 calc(var(--theme-contrast, 0) * 38%)); From c5e4316baba6f689afdd9394e430d3b5458fb9f6 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 11:48:52 +0000 Subject: [PATCH 15/18] fix(webapp): give radio cards their own hover surface --- .../app/components/primitives/RadioButton.tsx | 8 ++++---- apps/webapp/app/tailwind.css | 13 +++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/components/primitives/RadioButton.tsx b/apps/webapp/app/components/primitives/RadioButton.tsx index 97a561794c..1d242e39a8 100644 --- a/apps/webapp/app/components/primitives/RadioButton.tsx +++ b/apps/webapp/app/components/primitives/RadioButton.tsx @@ -22,7 +22,7 @@ const variants = { }, "button/small": { button: - "flex items-center w-fit h-8 pl-2 pr-3 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-background-hover data-disabled:opacity-70 data-disabled:hover:bg-background-bright hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected", + "flex items-center w-fit h-8 pl-2 pr-3 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-surface-hover-subtle data-disabled:opacity-70 data-disabled:hover:bg-background-bright hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected", label: "text-sm text-text-bright select-none", description: "text-text-dimmed", inputPosition: "mt-0", @@ -30,7 +30,7 @@ const variants = { }, button: { button: - "w-fit py-2 pl-3 pr-4 rounded border border-border-bright/50 shadow-xs bg-background-bright hover:bg-background-hover transition hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected data-disabled:opacity-70", + "w-fit py-2 pl-3 pr-4 rounded border border-border-bright/50 shadow-xs bg-background-bright hover:bg-surface-hover-subtle transition hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected data-disabled:opacity-70", label: "text-text-bright select-none", description: "text-text-dimmed", inputPosition: "mt-1", @@ -38,7 +38,7 @@ const variants = { }, description: { button: - "w-full p-2.5 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-background-hover data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected", + "w-full p-2.5 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-surface-hover-subtle data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected", label: "text-text-bright font-semibold -mt-0.5 text-left text-sm", description: "text-text-dimmed mt-0 text-left", inputPosition: "mt-0", @@ -46,7 +46,7 @@ const variants = { }, icon: { button: - "w-full p-2.5 pb-4 rounded-sm border border-border-bright/50 shadow-xs bg-background-bright hover:bg-background-hover transition data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected", + "w-full p-2.5 pb-4 rounded-sm border border-border-bright/50 shadow-xs bg-background-bright hover:bg-surface-hover-subtle transition data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected", label: "text-text-bright font-semibold -mt-1 text-left", description: "text-text-dimmed mt-0 text-left", inputPosition: "mt-0", diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css index c0958b48dd..833074c145 100644 --- a/apps/webapp/app/tailwind.css +++ b/apps/webapp/app/tailwind.css @@ -154,6 +154,7 @@ --color-background-dimmed: var(--color-charcoal-850); --color-background-bright: var(--color-charcoal-800); --color-background-hover: var(--color-charcoal-750); + --color-surface-hover-subtle: var(--color-charcoal-750); --color-background-raised: var(--color-charcoal-700); --color-surface-selected: var(--color-charcoal-650); --color-surface-selected-hover: var(--color-charcoal-600); @@ -768,14 +769,16 @@ --color-background-dimmed: #fbfbfc; --color-background-bright: #ffffff; --color-background-hover: #f2f3f5; + /* Cards on white need a gentler hover than the app-wide surfaces */ + --color-surface-hover-subtle: #f7f8f9; --color-background-raised: #e9eaee; --color-surface-control: #dcdee3; --color-surface-control-hover: #cfd2d9; --color-surface-control-active: #b8bcc6; /* Selection sits lighter than the controls here - on white a soft grey is already enough to read as selected */ - --color-surface-selected: #ebedf0; - --color-surface-selected-hover: #e3e5ea; + --color-surface-selected: #eff0f2; + --color-surface-selected-hover: #e7e9ec; /* Borders */ --color-grid-dimmed: #eceef1; @@ -937,6 +940,7 @@ --color-background-dimmed: color-mix(in srgb, var(--color-charcoal-850), #000 calc(var(--theme-contrast, 0) * 55%)); --color-background-bright: color-mix(in srgb, var(--color-charcoal-800), #000 calc(var(--theme-contrast, 0) * 45%)); --color-background-hover: color-mix(in srgb, var(--color-charcoal-750), #000 calc(var(--theme-contrast, 0) * 35%)); + --color-surface-hover-subtle: color-mix(in srgb, var(--color-charcoal-750), #000 calc(var(--theme-contrast, 0) * 35%)); --color-background-raised: color-mix(in srgb, var(--color-charcoal-700), #000 calc(var(--theme-contrast, 0) * 25%)); --color-input-bg: color-mix(in srgb, var(--color-charcoal-750), #000 calc(var(--theme-contrast, 0) * 35%)); @@ -961,6 +965,7 @@ page-behind surfaces darken a touch so panels separate */ --color-background-deep: color-mix(in srgb, #f1f2f4, #000 calc(var(--theme-contrast, 0) * 10%)); --color-background-hover: color-mix(in srgb, #f2f3f5, #000 calc(var(--theme-contrast, 0) * 8%)); + --color-surface-hover-subtle: color-mix(in srgb, #f7f8f9, #000 calc(var(--theme-contrast, 0) * 8%)); --color-background-raised: color-mix(in srgb, #e9eaee, #000 calc(var(--theme-contrast, 0) * 10%)); /* Controls and borders push hard toward black - this is where light-mode @@ -968,8 +973,8 @@ --color-surface-control: color-mix(in srgb, #dcdee3, #000 calc(var(--theme-contrast, 0) * 25%)); --color-surface-control-hover: color-mix(in srgb, #cfd2d9, #000 calc(var(--theme-contrast, 0) * 28%)); --color-surface-control-active: color-mix(in srgb, #b8bcc6, #000 calc(var(--theme-contrast, 0) * 32%)); - --color-surface-selected: color-mix(in srgb, #ebedf0, #000 calc(var(--theme-contrast, 0) * 25%)); - --color-surface-selected-hover: color-mix(in srgb, #e3e5ea, #000 calc(var(--theme-contrast, 0) * 28%)); + --color-surface-selected: color-mix(in srgb, #eff0f2, #000 calc(var(--theme-contrast, 0) * 25%)); + --color-surface-selected-hover: color-mix(in srgb, #e7e9ec, #000 calc(var(--theme-contrast, 0) * 28%)); --color-grid-dimmed: color-mix(in srgb, #eceef1, #000 calc(var(--theme-contrast, 0) * 28%)); --color-grid-bright: color-mix(in srgb, #e2e4e9, #000 calc(var(--theme-contrast, 0) * 32%)); --color-border-bright: color-mix(in srgb, #d2d5db, #000 calc(var(--theme-contrast, 0) * 38%)); From 0b6ed3e1c1dc528369b886357c3395de88795e0f Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 11:53:33 +0000 Subject: [PATCH 16/18] fix(webapp): soften the selected radio card under the contrast slider --- apps/webapp/app/tailwind.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css index 833074c145..0809e6681d 100644 --- a/apps/webapp/app/tailwind.css +++ b/apps/webapp/app/tailwind.css @@ -973,8 +973,10 @@ --color-surface-control: color-mix(in srgb, #dcdee3, #000 calc(var(--theme-contrast, 0) * 25%)); --color-surface-control-hover: color-mix(in srgb, #cfd2d9, #000 calc(var(--theme-contrast, 0) * 28%)); --color-surface-control-active: color-mix(in srgb, #b8bcc6, #000 calc(var(--theme-contrast, 0) * 32%)); - --color-surface-selected: color-mix(in srgb, #eff0f2, #000 calc(var(--theme-contrast, 0) * 25%)); - --color-surface-selected-hover: color-mix(in srgb, #e7e9ec, #000 calc(var(--theme-contrast, 0) * 28%)); + /* Selection stays light across the slider - it marks state, it is not a + filled control, so it does not follow surface-control's steep ramp */ + --color-surface-selected: color-mix(in srgb, #eff0f2, #000 calc(var(--theme-contrast, 0) * 10%)); + --color-surface-selected-hover: color-mix(in srgb, #e7e9ec, #000 calc(var(--theme-contrast, 0) * 12%)); --color-grid-dimmed: color-mix(in srgb, #eceef1, #000 calc(var(--theme-contrast, 0) * 28%)); --color-grid-bright: color-mix(in srgb, #e2e4e9, #000 calc(var(--theme-contrast, 0) * 32%)); --color-border-bright: color-mix(in srgb, #d2d5db, #000 calc(var(--theme-contrast, 0) * 38%)); From f5772b7a59441153a76ee2962c4f3b20e0d29842 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 12:11:41 +0000 Subject: [PATCH 17/18] fix(webapp): flatten the radio ramp at high contrast in light --- apps/webapp/app/tailwind.css | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css index 0809e6681d..a40e04aced 100644 --- a/apps/webapp/app/tailwind.css +++ b/apps/webapp/app/tailwind.css @@ -965,7 +965,8 @@ page-behind surfaces darken a touch so panels separate */ --color-background-deep: color-mix(in srgb, #f1f2f4, #000 calc(var(--theme-contrast, 0) * 10%)); --color-background-hover: color-mix(in srgb, #f2f3f5, #000 calc(var(--theme-contrast, 0) * 8%)); - --color-surface-hover-subtle: color-mix(in srgb, #f7f8f9, #000 calc(var(--theme-contrast, 0) * 8%)); + /* Radio card surface - shallow ramp, see surface-selected below */ + --color-surface-hover-subtle: color-mix(in srgb, #f7f8f9, #000 calc(var(--theme-contrast, 0) * 4%)); --color-background-raised: color-mix(in srgb, #e9eaee, #000 calc(var(--theme-contrast, 0) * 10%)); /* Controls and borders push hard toward black - this is where light-mode @@ -973,10 +974,10 @@ --color-surface-control: color-mix(in srgb, #dcdee3, #000 calc(var(--theme-contrast, 0) * 25%)); --color-surface-control-hover: color-mix(in srgb, #cfd2d9, #000 calc(var(--theme-contrast, 0) * 28%)); --color-surface-control-active: color-mix(in srgb, #b8bcc6, #000 calc(var(--theme-contrast, 0) * 32%)); - /* Selection stays light across the slider - it marks state, it is not a - filled control, so it does not follow surface-control's steep ramp */ - --color-surface-selected: color-mix(in srgb, #eff0f2, #000 calc(var(--theme-contrast, 0) * 10%)); - --color-surface-selected-hover: color-mix(in srgb, #e7e9ec, #000 calc(var(--theme-contrast, 0) * 12%)); + /* Selection marks state, it is not a filled control: the radio trio keeps a + shallow ramp so the cards stay light even at full contrast */ + --color-surface-selected: color-mix(in srgb, #eff0f2, #000 calc(var(--theme-contrast, 0) * 6%)); + --color-surface-selected-hover: color-mix(in srgb, #e7e9ec, #000 calc(var(--theme-contrast, 0) * 8%)); --color-grid-dimmed: color-mix(in srgb, #eceef1, #000 calc(var(--theme-contrast, 0) * 28%)); --color-grid-bright: color-mix(in srgb, #e2e4e9, #000 calc(var(--theme-contrast, 0) * 32%)); --color-border-bright: color-mix(in srgb, #d2d5db, #000 calc(var(--theme-contrast, 0) * 38%)); From f549a981b5310da1f7ff3468cb755a0785b45a29 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 13 Aug 2026 12:29:57 +0000 Subject: [PATCH 18/18] fix(webapp): carry light high-contrast on radio borders instead of backgrounds --- .../app/components/primitives/RadioButton.tsx | 8 ++++---- apps/webapp/app/tailwind.css | 15 +++++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/components/primitives/RadioButton.tsx b/apps/webapp/app/components/primitives/RadioButton.tsx index 1d242e39a8..8a0a89eae7 100644 --- a/apps/webapp/app/components/primitives/RadioButton.tsx +++ b/apps/webapp/app/components/primitives/RadioButton.tsx @@ -22,7 +22,7 @@ const variants = { }, "button/small": { button: - "flex items-center w-fit h-8 pl-2 pr-3 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-surface-hover-subtle data-disabled:opacity-70 data-disabled:hover:bg-background-bright hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected", + "flex items-center w-fit h-8 pl-2 pr-3 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-surface-hover-subtle data-disabled:opacity-70 data-disabled:hover:bg-background-bright hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:border-border-selected data-[state=checked]:bg-surface-selected", label: "text-sm text-text-bright select-none", description: "text-text-dimmed", inputPosition: "mt-0", @@ -30,7 +30,7 @@ const variants = { }, button: { button: - "w-fit py-2 pl-3 pr-4 rounded border border-border-bright/50 shadow-xs bg-background-bright hover:bg-surface-hover-subtle transition hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected data-disabled:opacity-70", + "w-fit py-2 pl-3 pr-4 rounded border border-border-bright/50 shadow-xs bg-background-bright hover:bg-surface-hover-subtle transition hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:border-border-selected data-[state=checked]:bg-surface-selected data-disabled:opacity-70", label: "text-text-bright select-none", description: "text-text-dimmed", inputPosition: "mt-1", @@ -38,7 +38,7 @@ const variants = { }, description: { button: - "w-full p-2.5 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-surface-hover-subtle data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected", + "w-full p-2.5 rounded-md border border-border-bright/50 shadow-xs bg-background-bright transition hover:bg-surface-hover-subtle data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:border-border-selected data-[state=checked]:bg-surface-selected", label: "text-text-bright font-semibold -mt-0.5 text-left text-sm", description: "text-text-dimmed mt-0 text-left", inputPosition: "mt-0", @@ -46,7 +46,7 @@ const variants = { }, icon: { button: - "w-full p-2.5 pb-4 rounded-sm border border-border-bright/50 shadow-xs bg-background-bright hover:bg-surface-hover-subtle transition data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:bg-surface-selected", + "w-full p-2.5 pb-4 rounded-sm border border-border-bright/50 shadow-xs bg-background-bright hover:bg-surface-hover-subtle transition data-disabled:opacity-70 hover:data-[state=checked]:bg-surface-selected-hover data-[state=checked]:border-border-selected data-[state=checked]:bg-surface-selected", label: "text-text-bright font-semibold -mt-1 text-left", description: "text-text-dimmed mt-0 text-left", inputPosition: "mt-0", diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css index a40e04aced..19f3f5b7d6 100644 --- a/apps/webapp/app/tailwind.css +++ b/apps/webapp/app/tailwind.css @@ -166,6 +166,9 @@ /* Borders, from subtlest to most visible */ --color-grid-dimmed: var(--color-charcoal-750); --color-grid-bright: var(--color-charcoal-700); + /* Blend of border-bright over the selected card, so it tracks whatever ramp + border-bright is on - the dark themes need no contrast entry of their own */ + --color-border-selected: color-mix(in srgb, var(--color-border-bright) 50%, var(--color-surface-selected)); --color-border-bright: var(--color-charcoal-600); --color-border-brighter: var(--color-charcoal-550); --color-border-brightest: var(--color-charcoal-500); @@ -783,6 +786,7 @@ /* Borders */ --color-grid-dimmed: #eceef1; --color-grid-bright: #e2e4e9; + --color-border-selected: #e0e2e6; --color-border-bright: #d2d5db; --color-border-brighter: #b9bdc7; --color-border-brightest: #9ba1ad; @@ -965,8 +969,9 @@ page-behind surfaces darken a touch so panels separate */ --color-background-deep: color-mix(in srgb, #f1f2f4, #000 calc(var(--theme-contrast, 0) * 10%)); --color-background-hover: color-mix(in srgb, #f2f3f5, #000 calc(var(--theme-contrast, 0) * 8%)); - /* Radio card surface - shallow ramp, see surface-selected below */ - --color-surface-hover-subtle: color-mix(in srgb, #f7f8f9, #000 calc(var(--theme-contrast, 0) * 4%)); + /* Radio card surfaces take no ramp at all on white - their contrast is + carried by border-selected below. Deliberately absent: surface-hover-subtle, + surface-selected, surface-selected-hover. */ --color-background-raised: color-mix(in srgb, #e9eaee, #000 calc(var(--theme-contrast, 0) * 10%)); /* Controls and borders push hard toward black - this is where light-mode @@ -974,12 +979,10 @@ --color-surface-control: color-mix(in srgb, #dcdee3, #000 calc(var(--theme-contrast, 0) * 25%)); --color-surface-control-hover: color-mix(in srgb, #cfd2d9, #000 calc(var(--theme-contrast, 0) * 28%)); --color-surface-control-active: color-mix(in srgb, #b8bcc6, #000 calc(var(--theme-contrast, 0) * 32%)); - /* Selection marks state, it is not a filled control: the radio trio keeps a - shallow ramp so the cards stay light even at full contrast */ - --color-surface-selected: color-mix(in srgb, #eff0f2, #000 calc(var(--theme-contrast, 0) * 6%)); - --color-surface-selected-hover: color-mix(in srgb, #e7e9ec, #000 calc(var(--theme-contrast, 0) * 8%)); --color-grid-dimmed: color-mix(in srgb, #eceef1, #000 calc(var(--theme-contrast, 0) * 28%)); --color-grid-bright: color-mix(in srgb, #e2e4e9, #000 calc(var(--theme-contrast, 0) * 32%)); + /* Steeper than border-bright so the selected card stays the loudest edge */ + --color-border-selected: color-mix(in srgb, #e0e2e6, #000 calc(var(--theme-contrast, 0) * 45%)); --color-border-bright: color-mix(in srgb, #d2d5db, #000 calc(var(--theme-contrast, 0) * 38%)); --color-border-brighter: color-mix(in srgb, #b9bdc7, #000 calc(var(--theme-contrast, 0) * 42%)); --color-border-brightest: color-mix(in srgb, #9ba1ad, #000 calc(var(--theme-contrast, 0) * 46%));