Polish Buzz sidebar theme (#1671)

Signed-off-by: npub13fn4ahfnvaa2qwylvegdgeajqs0mph6v4qsw4jcqnw4mjh3hzh2quuucm5 <8a675edd33677aa0389f6650d467b2041fb0df4ca820eacb009babb95e3715d4@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub13fn4ahfnvaa2qwylvegdgeajqs0mph6v4qsw4jcqnw4mjh3hzh2quuucm5 <8a675edd33677aa0389f6650d467b2041fb0df4ca820eacb009babb95e3715d4@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
klopez4212
2026-07-09 08:10:24 -07:00
committed by GitHub
co-authored by npub13fn4ahfnvaa2qwylvegdgeajqs0mph6v4qsw4jcqnw4mjh3hzh2quuucm5
parent e0f76b0e9c
commit cdba2a08e1
11 changed files with 364 additions and 60 deletions
+1
View File
@@ -860,6 +860,7 @@ export function AppShell() {
<SidebarInset
ref={mainInsetRef}
className="isolate min-h-0 min-w-0 overflow-hidden bg-sidebar"
data-buzz-glass-inset
style={chromeCssVarDefaults as React.CSSProperties}
>
<div className="relative z-10 mb-2 ml-px mr-2 mt-px flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl bg-background shadow-[-1px_-1px_0_0_hsl(var(--sidebar-border)/0.45)]">
@@ -829,7 +829,7 @@ export function TopbarSearch({
<>
<span
className={cn(
"min-w-0 flex-1 translate-y-px truncate transition-colors duration-150 ease-out",
"min-w-0 flex-1 truncate transition-colors duration-150 ease-out",
query
? "text-sidebar-foreground"
: "text-sidebar-foreground/55",
@@ -803,7 +803,7 @@ export function AppSidebar({
</SidebarContent>
</div>
<div className="relative z-30 shrink-0">
<div className="relative z-30 shrink-0" data-buzz-glass-footer-wrap>
{unreadBelowCount > 0 ? (
<MoreUnreadButton
bottomClassName="bottom-full"
+230 -50
View File
@@ -165,7 +165,7 @@
}
/*
* Buzz theme — sidebar gradient.
* Buzz theme - sidebar gradient.
*
* Buzz reuses the GitHub Light palette for every base color; its one
* distinguishing feature is this gradient painted across the sidebar/nav
@@ -176,16 +176,14 @@
* lives in Tailwind's `utilities` layer, and unlayered declarations win over
* any layered ones, so this reliably overrides it without an `!important`.
*
* The gradient is applied to every `bg-sidebar` canvas surface INSIDE the app
* shell (`.group\/sidebar-wrapper` — the top chrome bar, the sidebar column,
* and the inset margin behind the white content card), PLUS two chrome
* surfaces rendered OUTSIDE that wrapper: the portaled mobile sidebar (the
* `SheetContent` rendered by the offcanvas branch on narrow viewports, marked
* `[data-sidebar="sidebar"][data-mobile="true"]`) and the workspace rail (the
* Discord-style relay column on the far left, `[data-testid="workspace-rail"]`,
* a sibling of the wrapper gated behind the `workspaceRail` feature flag).
* Both live outside `.group\/sidebar-wrapper`, so each needs its own precise
* selector rather than re-broadening to every portal.
* The gradient is applied to the shell wrapper plus every `bg-sidebar` canvas
* surface inside it (top chrome, sidebar column, and the inset margin behind
* the white content card). A few panes that do not literally carry
* `bg-sidebar` are marked explicitly (`data-buzz-glass-inset` /
* `data-buzz-glass-footer-wrap`) so they join the same visual layer. Two
* chrome surfaces rendered outside the wrapper also get precise selectors: the
* portaled mobile sidebar (`[data-sidebar="sidebar"][data-mobile="true"]`) and
* the workspace rail (`[data-testid="workspace-rail"]`).
* Scoping this way keeps the branding on the app chrome and off unrelated
* `bg-sidebar` consumers rendered in portals outside the shell (e.g. the
* persona catalog dialog). `background-attachment: fixed` anchors the gradient
@@ -194,7 +192,7 @@
* grey `--sidebar-background` never shows through.
*
* Small transient `bg-sidebar` chips inside the shell that are NOT canvas
* surfaces (the drag-overlay pills) opt out via `[data-buzz-flat]` — a
* surfaces (the drag-overlay pills) opt out via `[data-buzz-flat]` - a
* viewport-fixed gradient behind a small floating chip would show an arbitrary
* slice of the ramp.
*
@@ -204,6 +202,18 @@
:root[data-buzz-sidebar] {
--buzz-gradient-top: #e6e6b6;
--buzz-gradient-bottom: #c4d0da;
--buzz-channel-fg: inherit;
--buzz-dm-fg: inherit;
--buzz-nav-fg: inherit;
--buzz-hover-fill: #ffffff;
--buzz-hover-alpha: 48%;
--buzz-hover-surface: color-mix(
in srgb,
var(--buzz-hover-fill) var(--buzz-hover-alpha),
transparent
);
--buzz-active-fill: 0 0% 100%;
--buzz-active-foreground: var(--foreground);
}
/*
@@ -215,8 +225,21 @@
:root[data-buzz-sidebar].dark {
--buzz-gradient-top: #2b2b18;
--buzz-gradient-bottom: #1b2530;
--buzz-hover-alpha: 9.6%;
--buzz-active-fill: 0 0% 100%;
--buzz-active-foreground: 0 0% 100%;
}
:root[data-buzz-sidebar]
[data-testid="app-sidebar"]
[data-testid="sidebar-pinned-header"] {
padding-top: 0.75rem;
}
:root[data-buzz-sidebar] .group\/sidebar-wrapper,
:root[data-buzz-sidebar] [data-testid="app-sidebar"],
:root[data-buzz-sidebar] [data-buzz-glass-inset],
:root[data-buzz-sidebar] [data-buzz-glass-footer-wrap],
:root[data-buzz-sidebar]
.group\/sidebar-wrapper
.bg-sidebar:not([data-buzz-flat]),
@@ -267,35 +290,18 @@
background: none;
}
/*
* Shared translucent-white fill for sidebar hover states and the search box.
* Only the sidebar rules below consume it, but it is safe at :root and other
* Buzz sidebar rules reference it, so it stays here.
*/
:root[data-buzz-sidebar] {
--buzz-hover-surface: rgb(255 255 255 / 0.6);
}
/*
* Dark variant: 60%-white fills are too harsh on the dark gradient, so use a
* lower-opacity white for hover / search surfaces.
*/
:root[data-buzz-sidebar].dark {
--buzz-hover-surface: rgb(255 255 255 / 0.12);
}
/*
* Active nav item. The accent system (`applyAccentColor` in ThemeProvider)
* sets `--sidebar-active` to the theme foreground as an INLINE style on
* :root, which renders the selected page as a solid black pill under GitHub
* Light. For the Buzz theme we want a white pill instead. Override the two
* active tokens — `!important` is required because an important stylesheet
* declaration is what beats the non-important inline style set by the accent
* system. Text flips to the theme foreground so it stays legible on the white
* pill; the derived `bg-sidebar-active-foreground/20` badge on active items
* follows automatically.
* active tokens on the sidebar element itself; element-local custom properties
* win over the inherited inline values from :root. Text flips to the theme
* foreground so it stays legible on the white pill; the derived
* `bg-sidebar-active-foreground/20` badge on active items follows
* automatically.
*
* SCOPED to the app sidebar container, NOT :root — the `bg-sidebar-active` /
* SCOPED to the app sidebar container, NOT :root - the `bg-sidebar-active` /
* `text-sidebar-active-foreground` tokens are also consumed by non-sidebar
* controls (avatar edit buttons in ProfileSettingsCard / AgentCreationPreview,
* the selected persona row in PersonaCatalogDialog). A root-level override
@@ -303,8 +309,8 @@
* the normal accent-driven active colors.
*/
:root[data-buzz-sidebar] [data-testid="app-sidebar"] {
--sidebar-active: 0 0% 100% !important;
--sidebar-active-foreground: var(--foreground) !important;
--sidebar-active: var(--buzz-active-fill);
--sidebar-active-foreground: var(--buzz-active-foreground);
}
/*
@@ -312,17 +318,8 @@
* (The pill itself is repainted to a translucent tint by the rule below.)
*/
:root[data-buzz-sidebar].dark [data-testid="app-sidebar"] {
--sidebar-active: 0 0% 100% !important;
--sidebar-active-foreground: 0 0% 100% !important;
}
/*
* On dark the active pill is a translucent white tint rather than a solid
* fill, so it sits on the gradient without blowing out. The `!important`
* background overrides the `bg-sidebar-active` utility for this state only.
*/
:root[data-buzz-sidebar].dark [data-testid="app-sidebar"] [data-active="true"] {
background-color: rgb(255 255 255 / 0.16) !important;
--sidebar-active: var(--buzz-active-fill);
--sidebar-active-foreground: var(--buzz-active-foreground);
}
/*
@@ -334,10 +331,22 @@
box-shadow: none;
}
/*
* On dark the active pill is a translucent white tint rather than a solid
* fill, so it sits on the gradient without blowing out.
*/
:root[data-buzz-sidebar].dark [data-testid="app-sidebar"] [data-active="true"] {
background-color: color-mix(
in srgb,
hsl(var(--buzz-active-fill)) 16%,
transparent
);
}
/*
* Hover on non-active nav items uses a translucent white fill (see
* `--buzz-hover-surface`) instead of the grey `--sidebar-accent`. Active
* items are excluded — they stay solid on hover (their
* items are excluded - they stay solid on hover (their
* `data-[active=true]:hover:bg-sidebar-active` rule). Covers both top-level
* menu buttons and channel sub-buttons.
*/
@@ -368,7 +377,7 @@
}
/*
* Section action buttons (the "+" and "…" at the right edge of each sidebar
* Section action buttons (the "+" and "..." at the right edge of each sidebar
* section header): match the same translucent white fill (see
* `--buzz-hover-surface`) on hover/focus instead of the grey
* `bg-sidebar-border/35` fill. Unlayered + `[data-buzz-sidebar]` prefix beats
@@ -382,3 +391,174 @@
.sidebar-section-action:focus-visible {
background-color: var(--buzz-hover-surface);
}
/*
* Per-row-type text colors (Buzz theme).
*
* By default the whole sidebar inherits a single `--sidebar-foreground`, so
* channels, direct messages, and the Inbox/Agents nav rows all share one text
* color. These variables let each row type carry its own foreground while
* keeping the default value unchanged.
*
* Each var defaults to `inherit`, so with nothing set the sidebar looks
* exactly as before. Scoped to the Buzz sidebar container so other themes are
* untouched.
*/
/* Channels: rows live inside the `stream-list` / `starred-list` menus. */
:root[data-buzz-sidebar]
[data-testid="app-sidebar"]
[data-testid="stream-list"]
[data-sidebar="menu-button"]:not([data-active="true"]),
:root[data-buzz-sidebar]
[data-testid="app-sidebar"]
[data-testid="stream-list"]
[data-sidebar="menu-sub-button"]:not([data-active="true"]),
:root[data-buzz-sidebar]
[data-testid="app-sidebar"]
[data-testid="starred-list"]
[data-sidebar="menu-button"]:not([data-active="true"]),
:root[data-buzz-sidebar]
[data-testid="app-sidebar"]
[data-testid="starred-list"]
[data-sidebar="menu-sub-button"]:not([data-active="true"]) {
color: var(--buzz-channel-fg);
}
/* Direct messages: rows live inside the `dm-list` menu. */
:root[data-buzz-sidebar]
[data-testid="app-sidebar"]
[data-testid="dm-list"]
[data-sidebar="menu-button"]:not([data-active="true"]),
:root[data-buzz-sidebar]
[data-testid="app-sidebar"]
[data-testid="dm-list"]
[data-sidebar="menu-sub-button"]:not([data-active="true"]) {
color: var(--buzz-dm-fg);
}
/*
* Inbox + Agents nav rows: the pinned-header nav buttons (Inbox, Pulse,
* Projects, Agents, Workflows). Excludes the active row so the active-pill
* foreground still wins.
*/
:root[data-buzz-sidebar]
[data-testid="app-sidebar"]
[data-testid="sidebar-pinned-header"]
[data-sidebar="menu-button"]:not([data-active="true"]) {
color: var(--buzz-nav-fg);
}
/*
* Translucency (Buzz theme).
*
* ThemeProvider toggles `data-buzz-translucent` for Buzz themes. Keep the
* frosted layer on the outer canvases only; nested sidebar/header/footer
* surfaces pass through so the nav reads as one continuous pane.
*/
:root[data-buzz-translucent] {
/* Neutral frosted wash; alpha applied via color-mix below. */
--buzz-glass-tint: #ffffff;
--buzz-glass-intensity: 1;
--buzz-translucency-gradient-alpha: 70%;
--buzz-translucency-wash-alpha: 2.4%;
}
:root[data-buzz-translucent].dark {
--buzz-glass-tint: #12161d;
}
:root[data-buzz-translucent] .group\/sidebar-wrapper,
:root[data-buzz-translucent]
[data-sidebar="sidebar"][data-mobile="true"].bg-sidebar,
:root[data-buzz-translucent] [data-testid="workspace-rail"].bg-sidebar {
background-color: transparent;
/* Buzz color wash above a neutral frost wash. */
background-image:
linear-gradient(
to bottom,
color-mix(
in srgb,
var(--buzz-gradient-top) var(--buzz-translucency-gradient-alpha, 70%),
transparent
),
color-mix(
in srgb,
var(--buzz-gradient-bottom) var(--buzz-translucency-gradient-alpha, 70%),
transparent
)
),
linear-gradient(
to bottom,
color-mix(
in srgb,
var(--buzz-glass-tint) var(--buzz-translucency-wash-alpha, 2.4%),
transparent
),
color-mix(
in srgb,
var(--buzz-glass-tint) var(--buzz-translucency-wash-alpha, 2.4%),
transparent
)
);
background-attachment: fixed, fixed;
background-size:
100vw 100vh,
100vw 100vh;
background-repeat: no-repeat, no-repeat;
}
:root[data-buzz-translucent] [data-testid="app-sidebar"],
:root[data-buzz-translucent] [data-buzz-glass-inset],
:root[data-buzz-translucent] [data-buzz-glass-footer-wrap],
:root[data-buzz-translucent]
.group\/sidebar-wrapper
.bg-sidebar:not([data-buzz-flat]),
:root[data-buzz-translucent]
[data-testid="app-sidebar"]
[data-testid="sidebar-pinned-header"],
:root[data-buzz-translucent]
[data-testid="app-sidebar"]
[data-sidebar="footer"] {
background-color: transparent;
background-image: none;
}
/* Remove edge fades and stacking contexts that made header/footer look separate. */
:root[data-buzz-translucent]
[data-testid="app-sidebar"]
[data-testid="sidebar-pinned-header"],
:root[data-buzz-translucent] [data-buzz-glass-inset],
:root[data-buzz-translucent]
[data-testid="app-sidebar"]
[data-buzz-glass-footer-wrap],
:root[data-buzz-translucent]
[data-testid="app-sidebar"]
[data-sidebar="footer"] {
isolation: auto;
z-index: auto;
}
:root[data-buzz-translucent]
[data-testid="app-sidebar"]
[data-testid="sidebar-pinned-header"]::before,
:root[data-buzz-translucent]
[data-testid="app-sidebar"]
[data-sidebar="footer"]::before {
background: none;
}
/* Let the native transparent window show through behind the sidebar glass. */
:root[data-buzz-translucent],
:root[data-buzz-translucent] body {
background-color: transparent;
background-image: none;
}
/* Shell wrappers stay clear; the main content card remains opaque. */
:root[data-buzz-translucent] .buzz-huddle-shell,
:root[data-buzz-translucent] .buzz-huddle-app-surface {
background: transparent;
}
:root[data-buzz-translucent] .buzz-huddle-app-surface::after {
background: transparent;
}
+49 -2
View File
@@ -7,6 +7,9 @@ import {
useRef,
useState,
} from "react";
import { isTauri } from "@tauri-apps/api/core";
import { invokeTauri } from "@/shared/api/tauri";
import { isMacPlatform } from "@/shared/lib/platform";
import { createThemeVars, hexToHsl } from "./adaptive-theme";
import {
SYNTAX_THEMES,
@@ -26,6 +29,7 @@ const VIDEO_REVIEW_NEUTRAL_ACCENT = "0 0% 98%";
const VIDEO_REVIEW_CHIP_SURFACE = "#161616";
const VIDEO_REVIEW_TEXT_CONTRAST = 4.5;
const VIDEO_REVIEW_CHIP_BACKGROUND_ALPHAS = [0.15, 0.3] as const;
const BUZZ_VIBRANCY_MATERIAL = "sidebar";
export const ACCENT_COLORS = [
{ name: "Neutral", value: NEUTRAL_ACCENT },
@@ -206,13 +210,51 @@ function applyAccentColor(value: string) {
root.style.setProperty("--sidebar-active-foreground", fgHsl);
}
/** Toggle the Buzz sidebar-gradient marker on the document root. */
function isBuzzTheme(themeName: string): boolean {
return themeName === "buzz" || themeName === "buzz-dark";
}
/** Toggle the Buzz sidebar-gradient and translucency markers on the root. */
function applyBuzzSidebar(themeName: string) {
const root = document.documentElement;
if (themeName === "buzz" || themeName === "buzz-dark") {
if (isBuzzTheme(themeName)) {
root.setAttribute("data-buzz-sidebar", "");
// The translucent treatment (transparent root/body + semi-transparent
// sidebar gradient) relies on the native macOS `NSVisualEffectView`
// vibrancy layer painting behind the webview. On Windows/Linux
// `set_window_vibrancy` is a no-op, but the window is transparent
// globally (tauri.conf.json), so a transparent root would show raw
// desktop content through the UI. Gate the translucent marker and
// transparent root background to macOS; other platforms fall back to the
// opaque Buzz gradient (`data-buzz-sidebar` paints solid colors) with the
// normal `bg-background` body fill.
if (isMacPlatform()) {
root.setAttribute("data-buzz-translucent", "");
root.style.setProperty("background-color", "transparent");
root.style.setProperty("background-image", "none");
} else {
root.removeAttribute("data-buzz-translucent");
root.style.removeProperty("background-color");
root.style.removeProperty("background-image");
}
} else {
root.removeAttribute("data-buzz-sidebar");
root.removeAttribute("data-buzz-translucent");
root.style.removeProperty("background-color");
root.style.removeProperty("background-image");
}
}
async function applyBuzzVibrancy(themeName: string) {
if (!isTauri()) return;
try {
await invokeTauri<void>("set_window_vibrancy", {
enabled: isBuzzTheme(themeName),
material: BUZZ_VIBRANCY_MATERIAL,
});
} catch (error) {
console.warn("set_window_vibrancy failed", error);
}
}
@@ -328,6 +370,11 @@ export function ThemeProvider({
});
}, [effectiveTheme]);
useEffect(() => {
if (!isValidThemeName(effectiveTheme)) return;
void applyBuzzVibrancy(effectiveTheme);
}, [effectiveTheme]);
// Listen for system color scheme changes when followSystem is enabled
useEffect(() => {
if (!followSystem) return;