diff --git a/.fork/customizations.yaml b/.fork/customizations.yaml index 499af9ecc54ea..a7a97d615cc63 100644 --- a/.fork/customizations.yaml +++ b/.fork/customizations.yaml @@ -1632,9 +1632,14 @@ live in theme.custom.palettes.css (two-block stage + v2 panel). index.html pre-paints #141618. - The sidebar's project chrome — the Projects row, each project header and - each thread card's meta line — rests on white at 65% rather than the - panel's opaque #8a8a8a muted ink. + The sidebar's project chrome — the Projects row, the recent-projects + strip's + marks, each project and status section header, and each + thread card's meta line and hover actions — rests on white at 65% rather + than the panel's opaque #8a8a8a muted ink (a status section's count stays + a step fainter at muted/60). The footer's icons (Settings, Pull Requests, + the update pill at rest), the draft pencil, the idle circle, receded card + titles and Gray project icons (the server's "No project" default) take + the same white. The thread you are on is a pane of glass (Figma 532:6527): a hairline glint on the top and bottom runs, a dark 1px halo, soft inner sheens, and @@ -1678,6 +1683,8 @@ - apps/web/index.html # Fenced data-fork-glass-rim on the details card. - apps/web/src/components/chat/ThreadDetailsCard.tsx + # Fenced data-fork-thread-card-actions on the row's hover actions cell. + - apps/web/src/components/Sidebar.tsx # Imports the glass surfaces sheet after the palettes sheet (fenced). - apps/web/src/main.tsx # Stamps data-chat-composer-floating-layer on every composer popup, which diff --git a/apps/web/src/__fork_guards__/forkCoolDarkerTheme.test.ts b/apps/web/src/__fork_guards__/forkCoolDarkerTheme.test.ts index bc0e5146ee950..03a3a07c0785d 100644 --- a/apps/web/src/__fork_guards__/forkCoolDarkerTheme.test.ts +++ b/apps/web/src/__fork_guards__/forkCoolDarkerTheme.test.ts @@ -145,26 +145,57 @@ describe("fork guard: fork-cool-darker-theme", () => { ); }); - it("inks the project chrome and card meta as white at 65%, on the derived token", () => { - const projectChrome = ruleBodyFor(themeRules, [ - ...DARKER_PANEL, - ":is([data-fork-projects-row], [data-fork-project-section-header], [data-fork-thread-card-meta])", - ]); + it("states the panel's quiet ink once, as white at 65%", () => { + const panel = ruleBodyFor(themeRules, DARKER_PANEL); + expect(panel).toMatch(/\s--fork-glass-quiet-ink:\s*rgb\(255 255 255 \/ 65%\);/u); + // The panel's muted channel itself stays opaque for everything else. + expect(declarationHex(panel, "--muted-foreground")).toBe("#8a8a8a"); + }); + + it("inks the opted-in sidebar chrome with the quiet ink", () => { + const quietInk = /:\s*var\(--fork-glass-quiet-ink\);/u; + const panel = ruleBodyFor(themeRules, DARKER_PANEL); + expect(panel).toMatch(new RegExp(`--fork-sidebar-card-title-receded${quietInk.source}`, "u")); + expect(panel).toMatch(new RegExp(`--color-gray-400${quietInk.source}`, "u")); // The derived token: text-muted-foreground reads it, and restating // --muted-foreground below [data-app-sidebar] would not re-derive it. - expect(projectChrome).toMatch(/\s--contrast-muted-foreground:\s*rgb\(255 255 255 \/ 65%\);/u); - // The panel's muted channel itself stays opaque for everything else. - expect(declarationHex(ruleBodyFor(themeRules, DARKER_PANEL), "--muted-foreground")).toBe( - "#8a8a8a", + expect( + ruleBodyFor(themeRules, [ + ...DARKER_PANEL, + ":is( [data-fork-projects-row], [data-fork-recent-projects], [data-fork-project-section-header], [data-fork-status-section-header], [data-fork-thread-card-meta], [data-fork-thread-card-actions], [data-fork-idle-mark] )", + ]), + ).toMatch(new RegExp(`--contrast-muted-foreground${quietInk.source}`, "u")); + // The draft pencil is drawn at muted/70, so it takes the ink directly. + expect(ruleBodyFor(themeRules, [...DARKER_PANEL, "[data-fork-draft-pencil]"])).toMatch( + new RegExp(`\\scolor${quietInk.source}`, "u"), ); - // Every hook has to exist for the rule to land on anything. - expect(readSibling("../custom/SidebarV2ChromeRows.tsx")).toContain('data-fork-projects-row=""'); - expect(readSibling("../custom/SidebarV2ProjectGroupHeader.tsx")).toContain( - 'data-fork-project-section-header=""', - ); - expect(readSibling("../custom/SidebarV2ThreadCardMeta.tsx")).toContain( - 'data-fork-thread-card-meta=""', + // The footer icons ink from --sidebar-icon-color, so they take it there. + expect(ruleBodyFor(themeRules, [...DARKER_PANEL, '[data-slot="sidebar-footer"]'])).toMatch( + new RegExp(`--sidebar-icon-color${quietInk.source}`, "u"), ); + // Every hook has to exist for the rule to land on anything. + for (const [file, hook] of [ + ["../custom/SidebarV2ChromeRows.tsx", "data-fork-projects-row"], + ["../custom/SidebarV2RecentProjectsStrip.tsx", "data-fork-recent-projects"], + ["../custom/SidebarV2ProjectGroupHeader.tsx", "data-fork-project-section-header"], + ["../custom/SidebarV2StatusSectionHeader.tsx", "data-fork-status-section-header"], + ["../custom/SidebarV2ThreadCardMeta.tsx", "data-fork-thread-card-meta"], + ["../components/Sidebar.tsx", "data-fork-thread-card-actions"], + ["../custom/SidebarV2StatusIndicator.tsx", "data-fork-idle-mark"], + ["../custom/SidebarV2StatusIndicator.tsx", "data-fork-draft-pencil"], + ] as const) { + expect(readSibling(file), `${hook} in ${file}`).toContain(`${hook}=""`); + } + }); + + it("keeps gray-400 to the Gray project colour, since Glass repaints it", () => { + // Glass redefines --color-gray-400 for the whole v2 sidebar, which is only + // safe while nothing else in the web app draws with it. + const srcRoot = NodeURL.fileURLToPath(new URL("..", import.meta.url)); + const users = (NodeFS.readdirSync(srcRoot, { recursive: true }) as string[]) + .filter((file) => /\.tsx?$/u.test(file) && !/\.test\.tsx?$/u.test(file)) + .filter((file) => NodeFS.readFileSync(`${srcRoot}/${file}`, "utf8").includes("gray-400")); + expect(users).toEqual(["projectIconColors.ts"]); }); it("pre-paints Glass from the palette key so the load flash matches the stage", () => { diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index c5de95303ba94..ec57c53aa0e78 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1989,6 +1989,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP {hasHoverActions ? ( /* fork:end sidebar-v2-draft-rows */ + ); @@ -406,7 +406,7 @@ function SidebarV2WokeMark() { export function SidebarV2DraftMark() { return ( - + ); } diff --git a/apps/web/src/theme.custom.palettes.css b/apps/web/src/theme.custom.palettes.css index bf08df837c648..2d89de330703a 100644 --- a/apps/web/src/theme.custom.palettes.css +++ b/apps/web/src/theme.custom.palettes.css @@ -243,7 +243,14 @@ --secondary-label: var(--muted-foreground); --icon-muted: var(--muted-foreground); --sidebar-muted-foreground: #d0d4d6; - --fork-sidebar-card-title-receded: #aeb2b6; + /* The panel's quiet ink where it should tint with the glass under it. The + opaque #8a8a8a above stays the default; named elements opt in below. */ + --fork-glass-quiet-ink: rgb(255 255 255 / 65%); + --fork-sidebar-card-title-receded: var(--fork-glass-quiet-ink); + /* Gray project icons (the server's "No project" default) would otherwise + be the one flat grey left among them. gray-400 is only that colour's + dark ink in apps/web; the Glass guard holds that. */ + --color-gray-400: var(--fork-glass-quiet-ink); --sidebar-control-surface: #2f3437; --sidebar-row-hover: #282d30; --sidebar-row-active: #2c3134; @@ -258,11 +265,13 @@ border-radius: 9999px; } -/* Project chrome — the Projects row, every project header and each thread - card's meta line (project chip, branch, model) — rests on white at 65% - instead of the panel's #8a8a8a, so the labels, marks and icon buttons tint - with the surface under them. The panel's opaque ink above stays for - everything else that reads the muted channel. +/* Project chrome — the Projects row, the recent-projects strip, every project + and status section header, each thread card's meta line (project chip, + branch, model), its hover actions (pin, snooze, settle) and the idle + circle — rests on the quiet ink instead of the panel's #8a8a8a, so the + labels, marks, carets and icon buttons tint with the surface under them. + This is an allowlist: anything not named here or below keeps the opaque + ink. The derived token, not --muted-foreground: index.css computes --contrast-muted-foreground on [data-app-sidebar] and a descendant inherits @@ -270,8 +279,33 @@ so the appearance-contrast slider does not move these labels. */ :root[data-fork="noahhendrickson-t3code"].dark[data-fork-theme="cool-darker"] [data-sidebar-version="v2"] - :is([data-fork-projects-row], [data-fork-project-section-header], [data-fork-thread-card-meta]) { - --contrast-muted-foreground: rgb(255 255 255 / 65%); + :is( + [data-fork-projects-row], + [data-fork-recent-projects], + [data-fork-project-section-header], + [data-fork-status-section-header], + [data-fork-thread-card-meta], + [data-fork-thread-card-actions], + [data-fork-idle-mark] + ) { + --contrast-muted-foreground: var(--fork-glass-quiet-ink); +} + +/* The draft pencil too. It is drawn at muted/70, which would multiply the + token above down to 45%, so it takes the ink directly. */ +:root[data-fork="noahhendrickson-t3code"].dark[data-fork-theme="cool-darker"] + [data-sidebar-version="v2"] + [data-fork-draft-pencil] { + color: var(--fork-glass-quiet-ink); +} + +/* The footer's icons (Settings, Pull Requests, the update pill at rest) ink + from --sidebar-icon-color, not the muted channel, which the vibrancy block + freezes to opaque #8a8a8a for the rest of the panel. */ +:root[data-fork="noahhendrickson-t3code"].dark[data-fork-theme="cool-darker"] + [data-sidebar-version="v2"] + [data-slot="sidebar-footer"] { + --sidebar-icon-color: var(--fork-glass-quiet-ink); } /* --------------------------------------------------------------------------- @@ -1180,8 +1214,11 @@ 60% plus 40% of the panel's alpha, and the rest is wallpaper showing through the icon itself, which reads as the icons taking on the desktop's colour. Frozen to the value that mix resolves to against the opaque panel (#d0d4d6 60% over - #181b1e) so icons stay glass-independent. Anything else derived from the - panel fill needs the same treatment. */ + #181b1e). The freeze guards against that accidental leak of the panel's + alpha, so anything else derived from the panel fill needs the same + treatment. The deliberate flat white wash some elements take instead + (--fork-glass-quiet-ink, the footer included) is a separate, intended + choice, not a regression of this one. */ --sidebar-icon-color: #8a8a8a; /* Upstream's appearance-contrast control (#7906) derives these two from --sidebar the same way, so they inherit the panel's alpha and every