diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 07ec0dc9077..587ec65cbc9 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -4,10 +4,18 @@ import { useLocation, useNavigate } from "@tanstack/react-router"; import { isElectron } from "../env"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; -import { isMacPlatform } from "../lib/utils"; +import { cn, isMacPlatform } from "../lib/utils"; import { primaryServerKeybindingsAtom } from "../state/server"; import ThreadSidebar from "./Sidebar"; -import { Sidebar, SidebarProvider, SidebarRail, SidebarTrigger, useSidebar } from "./ui/sidebar"; +import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; +import { + Sidebar, + SidebarProvider, + SidebarRail, + SidebarTrigger, + useSidebar, + useSidebarVisibility, +} from "./ui/sidebar"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width"; @@ -18,6 +26,8 @@ const MACOS_TRAFFIC_LIGHTS_LEFT_INSET = "90px"; function SidebarControl() { const keybindings = useAtomValue(primaryServerKeybindingsAtom); const { toggleSidebar } = useSidebar(); + const isSidebarVisible = useSidebarVisibility(); + const stageBackdropVariant = useSidebarStageBackdropVariant(); const shortcutLabel = shortcutLabelForCommand(keybindings, "sidebar.toggle"); useEffect(() => { @@ -42,7 +52,15 @@ function SidebarControl() { + } /> diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 6a76494daa3..38e9f7a828a 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -77,7 +77,7 @@ import { isElectron } from "../env"; import { APP_STAGE_LABEL } from "../branding"; import { useOpenPrLink } from "../lib/openPullRequestLink"; import { isTerminalFocused } from "../lib/terminalFocus"; -import { isMacPlatform } from "../lib/utils"; +import { cn, isMacPlatform } from "../lib/utils"; import { readThreadShell, useProject, @@ -126,6 +126,7 @@ import { import { stackedThreadToast, toastManager } from "./ui/toast"; import { formatRelativeTimeLabel } from "../timestampFormat"; import { SettingsSidebarNav } from "./settings/SettingsSidebarNav"; +import { SidebarStageBackdrop, resolveSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; import { Kbd } from "./ui/kbd"; import { getArm64IntelBuildWarningDescription, @@ -2744,33 +2745,55 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({ }: { isElectron: boolean; }) { - return isElectron ? ( - - - - - ) : ( - - - + const stageLabel = useSidebarStageLabel(); + const backdropVariant = resolveSidebarStageBackdropVariant(stageLabel); + + return ( + + {backdropVariant ? : null} + + ); }); -function SidebarBrand() { - const stageLabel = useSidebarStageLabel(); - +function SidebarBrand({ stageLabel, onBackdrop }: { stageLabel: string; onBackdrop: boolean }) { return ( - + Code - + {stageLabel} @@ -2791,7 +2814,7 @@ function T3Wordmark() { return ( diff --git a/apps/web/src/components/SidebarStageBackdrop.tsx b/apps/web/src/components/SidebarStageBackdrop.tsx new file mode 100644 index 00000000000..963a8c2a951 --- /dev/null +++ b/apps/web/src/components/SidebarStageBackdrop.tsx @@ -0,0 +1,316 @@ +import { useAtomValue } from "@effect/atom-react"; + +import { APP_STAGE_LABEL } from "../branding"; +import { resolveServerBackedAppStageLabel } from "../branding.logic"; +import { primaryServerConfigAtom } from "../state/server"; + +export type SidebarStageBackdropVariant = "nightly" | "dev"; + +// A wide viewBox keeps the 96-unit art height at a fixed scale while sidebar resizing reveals +// more horizontal canvas instead of zooming the scene. +const STAGE_BACKDROP_VIEW_BOX = "0 0 8192 96"; + +export function resolveSidebarStageBackdropVariant( + stageLabel: string, +): SidebarStageBackdropVariant | null { + const normalized = stageLabel.trim().toLowerCase(); + if (normalized === "nightly") return "nightly"; + if (normalized === "dev") return "dev"; + return null; +} + +export function useSidebarStageBackdropVariant(): SidebarStageBackdropVariant | null { + const primaryServerVersion = + useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null; + + return resolveSidebarStageBackdropVariant( + resolveServerBackedAppStageLabel({ + primaryServerVersion, + fallbackStageLabel: APP_STAGE_LABEL, + }), + ); +} + +/** Stage-channel header art; palettes mirror the per-channel app icons in `assets/`. */ +export function SidebarStageBackdrop({ variant }: { variant: SidebarStageBackdropVariant }) { + return ( +
+ {variant === "nightly" ? : } +
+ ); +} + +const NIGHTLY_STARS: ReadonlyArray<{ + cx: number; + cy: number; + r: number; + opacity: number; +}> = [ + { cx: 14, cy: 10, r: 0.6, opacity: 0.85 }, + { cx: 38, cy: 22, r: 0.4, opacity: 0.55 }, + { cx: 58, cy: 8, r: 0.5, opacity: 0.7 }, + { cx: 84, cy: 16, r: 0.4, opacity: 0.5 }, + { cx: 104, cy: 7, r: 0.6, opacity: 0.8 }, + { cx: 126, cy: 20, r: 0.4, opacity: 0.55 }, + { cx: 148, cy: 11, r: 0.5, opacity: 0.7 }, + { cx: 170, cy: 24, r: 0.4, opacity: 0.5 }, + { cx: 192, cy: 9, r: 0.6, opacity: 0.8 }, + { cx: 214, cy: 18, r: 0.4, opacity: 0.55 }, + { cx: 236, cy: 8, r: 0.5, opacity: 0.7 }, + { cx: 258, cy: 20, r: 0.45, opacity: 0.6 }, + { cx: 278, cy: 11, r: 0.55, opacity: 0.75 }, + { cx: 26, cy: 34, r: 0.4, opacity: 0.45 }, + { cx: 118, cy: 34, r: 0.4, opacity: 0.45 }, + { cx: 202, cy: 32, r: 0.4, opacity: 0.5 }, + { cx: 268, cy: 34, r: 0.4, opacity: 0.45 }, +]; + +const NIGHTLY_SPARKLES: ReadonlyArray<{ x: number; y: number }> = [ + { x: 70, y: 28 }, + { x: 160, y: 36 }, + { x: 246, y: 26 }, +]; + +function NightlySkyArt() { + return ( + + + + + + + + + + + + + + + + + + + + + + + {NIGHTLY_STARS.map((star) => ( + + ))} + + + {NIGHTLY_SPARKLES.map((sparkle) => ( + + + + + ))} + + + + + + + + + + + + + + + + + + + ); +} + +function DevBlueprintArt() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/web/src/index.css b/apps/web/src/index.css index c500eb68d7f..77a5f027c13 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -231,6 +231,42 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil } } + /* Stage-channel sidebar art; ::after ramps to the sidebar bg color and the + mask lets the surface grain show through at the boundary. */ + .sidebar-stage-backdrop { + mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%); + -webkit-mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%); + } + + .sidebar-stage-backdrop::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient( + to bottom, + transparent 0%, + transparent 28%, + color-mix(in srgb, var(--app-chrome-background) 10%, transparent) 40%, + color-mix(in srgb, var(--app-chrome-background) 30%, transparent) 52%, + color-mix(in srgb, var(--app-chrome-background) 58%, transparent) 64%, + color-mix(in srgb, var(--app-chrome-background) 82%, transparent) 75%, + color-mix(in srgb, var(--app-chrome-background) 96%, transparent) 85%, + var(--app-chrome-background) 93% + ); + } + + .stage-blueprint { + --stage-bp-top: #67c2ff; + --stage-bp-mid: #347ff8; + --stage-bp-bottom: #1538d0; + } + + .dark .stage-blueprint { + --stage-bp-top: #3a7ad1; + --stage-bp-mid: #2050ae; + --stage-bp-bottom: #101f6e; + } + .workspace-topbar { display: flex; height: var(--workspace-topbar-height);