authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-07-19 06:35:58-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-19 12:35:58+02:00
log53e3c98a55237ab7327dc1fb55f1f7cf500560a1
treed6104fb012e65ef4215ca5248b2bdabd58f9eef9
parent4f11740510d9040475936a24b08f60852bf5fdc4
signature Signed by PGP key B5690EEEBB952194

feat(web): themed sidebar header art for nightly and dev builds (#4130)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: Julius Marminge <jmarminge@gmail.com>

4 files changed, 413 insertions(+), 20 deletions(-)

apps/web/src/components/AppSidebarLayout.tsx+21-3
......@@ -4,10 +4,18 @@ import { useLocation, useNavigate } from "@tanstack/react-router";
44
55import { isElectron } from "../env";
66import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings";
7import { isMacPlatform } from "../lib/utils";
7import { cn, isMacPlatform } from "../lib/utils";
88import { primaryServerKeybindingsAtom } from "../state/server";
99import ThreadSidebar from "./Sidebar";
10import { Sidebar, SidebarProvider, SidebarRail, SidebarTrigger, useSidebar } from "./ui/sidebar";
10import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop";
11import {
12 Sidebar,
13 SidebarProvider,
14 SidebarRail,
15 SidebarTrigger,
16 useSidebar,
17 useSidebarVisibility,
18} from "./ui/sidebar";
1119import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
1220
1321const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width";
......@@ -18,6 +26,8 @@ const MACOS_TRAFFIC_LIGHTS_LEFT_INSET = "90px";
1826function SidebarControl() {
1927 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
2028 const { toggleSidebar } = useSidebar();
29 const isSidebarVisible = useSidebarVisibility();
30 const stageBackdropVariant = useSidebarStageBackdropVariant();
2131 const shortcutLabel = shortcutLabelForCommand(keybindings, "sidebar.toggle");
2232
2333 useEffect(() => {
......@@ -42,7 +52,15 @@ function SidebarControl() {
4252 <Tooltip>
4353 <TooltipTrigger
4454 render={
45 <SidebarTrigger className="pointer-events-auto" aria-label="Toggle main sidebar" />
55 <SidebarTrigger
56 className={cn(
57 "pointer-events-auto",
58 isSidebarVisible &&
59 stageBackdropVariant &&
60 "hover:bg-white/15 [&_svg]:text-white/85! [&_svg]:hover:text-white!",
61 )}
62 aria-label="Toggle main sidebar"
63 />
4664 }
4765 />
4866 <TooltipPopup side="bottom">
apps/web/src/components/Sidebar.tsx+40-17
......@@ -77,7 +77,7 @@ import { isElectron } from "../env";
7777import { APP_STAGE_LABEL } from "../branding";
7878import { useOpenPrLink } from "../lib/openPullRequestLink";
7979import { isTerminalFocused } from "../lib/terminalFocus";
80import { isMacPlatform } from "../lib/utils";
80import { cn, isMacPlatform } from "../lib/utils";
8181import {
8282 readThreadShell,
8383 useProject,
......@@ -126,6 +126,7 @@ import {
126126import { stackedThreadToast, toastManager } from "./ui/toast";
127127import { formatRelativeTimeLabel } from "../timestampFormat";
128128import { SettingsSidebarNav } from "./settings/SettingsSidebarNav";
129import { SidebarStageBackdrop, resolveSidebarStageBackdropVariant } from "./SidebarStageBackdrop";
129130import { Kbd } from "./ui/kbd";
130131import {
131132 getArm64IntelBuildWarningDescription,
......@@ -2744,33 +2745,55 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({
27442745}: {
27452746 isElectron: boolean;
27462747}) {
2747 return isElectron ? (
2748 <SidebarHeader className="@container/sidebar-header drag-region h-[var(--workspace-topbar-height)] shrink-0 flex-row items-center px-3 py-0 md:px-0">
2749 <SidebarTrigger className="md:hidden" />
2750 <SidebarBrand />
2751 </SidebarHeader>
2752 ) : (
2753 <SidebarHeader className="@container/sidebar-header h-[var(--workspace-topbar-height)] shrink-0 flex-row items-center px-3 py-0 md:px-0">
2754 <SidebarTrigger className="md:hidden" />
2755 <SidebarBrand />
2748 const stageLabel = useSidebarStageLabel();
2749 const backdropVariant = resolveSidebarStageBackdropVariant(stageLabel);
2750
2751 return (
2752 <SidebarHeader
2753 className={cn(
2754 "@container/sidebar-header relative h-[var(--workspace-topbar-height)] shrink-0 flex-row items-center px-3 py-0 md:px-0",
2755 isElectron && "drag-region",
2756 )}
2757 >
2758 {backdropVariant ? <SidebarStageBackdrop variant={backdropVariant} /> : null}
2759 <SidebarTrigger
2760 className={cn(
2761 "relative z-10 md:hidden",
2762 backdropVariant && "hover:bg-white/15 [&_svg]:text-white/85! [&_svg]:hover:text-white!",
2763 )}
2764 />
2765 <SidebarBrand onBackdrop={backdropVariant !== null} stageLabel={stageLabel} />
27562766 </SidebarHeader>
27572767 );
27582768});
27592769
2760function SidebarBrand() {
2761 const stageLabel = useSidebarStageLabel();
2762
2770function SidebarBrand({ stageLabel, onBackdrop }: { stageLabel: string; onBackdrop: boolean }) {
27632771 return (
27642772 <Link
27652773 aria-label="Go to threads"
2766 className="sidebar-brand ml-[var(--workspace-titlebar-content-left)] h-7 w-fit min-w-0 shrink-0 items-center gap-1 overflow-hidden rounded-md text-foreground outline-hidden ring-ring focus-visible:ring-2"
2774 className={cn(
2775 "sidebar-brand relative z-10 ml-[var(--workspace-titlebar-content-left)] h-7 w-fit min-w-0 shrink-0 items-center gap-1 overflow-hidden rounded-md outline-hidden ring-ring focus-visible:ring-2",
2776 onBackdrop ? "text-white" : "text-foreground",
2777 )}
27672778 to="/"
27682779 >
27692780 <T3Wordmark />
2770 <span className="truncate text-sm font-medium tracking-tight text-muted-foreground">
2781 <span
2782 className={cn(
2783 "truncate text-sm font-medium tracking-tight",
2784 onBackdrop ? "text-white/70" : "text-muted-foreground",
2785 )}
2786 >
27712787 Code
27722788 </span>
2773 <span className="sidebar-brand-stage shrink-0 items-center whitespace-nowrap rounded-full bg-muted/50 px-1.5 py-0.5 text-[8px] font-medium uppercase tracking-[0.18em] text-muted-foreground/60">
2789 <span
2790 className={cn(
2791 "sidebar-brand-stage shrink-0 items-center whitespace-nowrap rounded-full px-1.5 py-0.5 text-[8px] font-medium uppercase tracking-[0.18em]",
2792 onBackdrop
2793 ? "bg-white/15 text-white/80 backdrop-blur-sm"
2794 : "bg-muted/50 text-muted-foreground/60",
2795 )}
2796 >
27742797 {stageLabel}
27752798 </span>
27762799 </Link>
......@@ -2791,7 +2814,7 @@ function T3Wordmark() {
27912814 return (
27922815 <svg
27932816 aria-label="T3"
2794 className="h-2.5 w-auto shrink-0 text-foreground"
2817 className="h-2.5 w-auto shrink-0"
27952818 viewBox="15.5309 37 94.3941 56.96"
27962819 xmlns="http://www.w3.org/2000/svg"
27972820 >
apps/web/src/components/SidebarStageBackdrop.tsx created+316
......@@ -0,0 +1,316 @@
1import { useAtomValue } from "@effect/atom-react";
2
3import { APP_STAGE_LABEL } from "../branding";
4import { resolveServerBackedAppStageLabel } from "../branding.logic";
5import { primaryServerConfigAtom } from "../state/server";
6
7export type SidebarStageBackdropVariant = "nightly" | "dev";
8
9// A wide viewBox keeps the 96-unit art height at a fixed scale while sidebar resizing reveals
10// more horizontal canvas instead of zooming the scene.
11const STAGE_BACKDROP_VIEW_BOX = "0 0 8192 96";
12
13export function resolveSidebarStageBackdropVariant(
14 stageLabel: string,
15): SidebarStageBackdropVariant | null {
16 const normalized = stageLabel.trim().toLowerCase();
17 if (normalized === "nightly") return "nightly";
18 if (normalized === "dev") return "dev";
19 return null;
20}
21
22export function useSidebarStageBackdropVariant(): SidebarStageBackdropVariant | null {
23 const primaryServerVersion =
24 useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null;
25
26 return resolveSidebarStageBackdropVariant(
27 resolveServerBackedAppStageLabel({
28 primaryServerVersion,
29 fallbackStageLabel: APP_STAGE_LABEL,
30 }),
31 );
32}
33
34/** Stage-channel header art; palettes mirror the per-channel app icons in `assets/`. */
35export function SidebarStageBackdrop({ variant }: { variant: SidebarStageBackdropVariant }) {
36 return (
37 <div
38 aria-hidden
39 className="sidebar-stage-backdrop pointer-events-none absolute inset-x-0 top-0 z-0 h-20 select-none overflow-hidden"
40 >
41 {variant === "nightly" ? <NightlySkyArt /> : <DevBlueprintArt />}
42 </div>
43 );
44}
45
46const NIGHTLY_STARS: ReadonlyArray<{
47 cx: number;
48 cy: number;
49 r: number;
50 opacity: number;
51}> = [
52 { cx: 14, cy: 10, r: 0.6, opacity: 0.85 },
53 { cx: 38, cy: 22, r: 0.4, opacity: 0.55 },
54 { cx: 58, cy: 8, r: 0.5, opacity: 0.7 },
55 { cx: 84, cy: 16, r: 0.4, opacity: 0.5 },
56 { cx: 104, cy: 7, r: 0.6, opacity: 0.8 },
57 { cx: 126, cy: 20, r: 0.4, opacity: 0.55 },
58 { cx: 148, cy: 11, r: 0.5, opacity: 0.7 },
59 { cx: 170, cy: 24, r: 0.4, opacity: 0.5 },
60 { cx: 192, cy: 9, r: 0.6, opacity: 0.8 },
61 { cx: 214, cy: 18, r: 0.4, opacity: 0.55 },
62 { cx: 236, cy: 8, r: 0.5, opacity: 0.7 },
63 { cx: 258, cy: 20, r: 0.45, opacity: 0.6 },
64 { cx: 278, cy: 11, r: 0.55, opacity: 0.75 },
65 { cx: 26, cy: 34, r: 0.4, opacity: 0.45 },
66 { cx: 118, cy: 34, r: 0.4, opacity: 0.45 },
67 { cx: 202, cy: 32, r: 0.4, opacity: 0.5 },
68 { cx: 268, cy: 34, r: 0.4, opacity: 0.45 },
69];
70
71const NIGHTLY_SPARKLES: ReadonlyArray<{ x: number; y: number }> = [
72 { x: 70, y: 28 },
73 { x: 160, y: 36 },
74 { x: 246, y: 26 },
75];
76
77function NightlySkyArt() {
78 return (
79 <svg
80 className="h-full w-full"
81 fill="none"
82 preserveAspectRatio="xMinYMin slice"
83 viewBox={STAGE_BACKDROP_VIEW_BOX}
84 xmlns="http://www.w3.org/2000/svg"
85 >
86 <defs>
87 <linearGradient
88 id="stage-night-sky"
89 x1="24"
90 y1="0"
91 x2="264"
92 y2="96"
93 gradientUnits="userSpaceOnUse"
94 spreadMethod="reflect"
95 >
96 <stop stopColor="#07152F" />
97 <stop offset="0.5" stopColor="#151443" />
98 <stop offset="1" stopColor="#32155B" />
99 </linearGradient>
100 <radialGradient
101 id="stage-night-glow"
102 cx="0"
103 cy="0"
104 r="1"
105 gradientTransform="translate(216 18) rotate(137) scale(120 84)"
106 gradientUnits="userSpaceOnUse"
107 >
108 <stop stopColor="#5165D8" stopOpacity="0.4" />
109 <stop offset="0.5" stopColor="#283075" stopOpacity="0.16" />
110 <stop offset="1" stopColor="#111635" stopOpacity="0" />
111 </radialGradient>
112 <linearGradient
113 id="stage-night-cloud"
114 x1="0"
115 y1="60"
116 x2="288"
117 y2="96"
118 gradientUnits="userSpaceOnUse"
119 >
120 <stop stopColor="#4EA4FF" stopOpacity="0.5" />
121 <stop offset="0.52" stopColor="#696FEA" stopOpacity="0.62" />
122 <stop offset="1" stopColor="#A85BEA" stopOpacity="0.5" />
123 </linearGradient>
124 <filter
125 id="stage-night-soft"
126 x="-24"
127 y="-24"
128 width="336"
129 height="144"
130 filterUnits="userSpaceOnUse"
131 >
132 <feGaussianBlur stdDeviation="4" />
133 </filter>
134 <pattern id="stage-night-stars" width="288" height="96" patternUnits="userSpaceOnUse">
135 <g fill="#E4EAFF">
136 {NIGHTLY_STARS.map((star) => (
137 <circle
138 key={`${star.cx}-${star.cy}`}
139 cx={star.cx}
140 cy={star.cy}
141 r={star.r}
142 fillOpacity={star.opacity}
143 />
144 ))}
145 </g>
146 <g stroke="#C8D7FF" strokeLinecap="round" strokeOpacity="0.7" strokeWidth="0.6">
147 {NIGHTLY_SPARKLES.map((sparkle) => (
148 <g key={`${sparkle.x}-${sparkle.y}`}>
149 <path d={`M${sparkle.x - 1.5} ${sparkle.y}H${sparkle.x + 1.5}`} />
150 <path d={`M${sparkle.x} ${sparkle.y - 1.5}V${sparkle.y + 1.5}`} />
151 </g>
152 ))}
153 </g>
154 </pattern>
155 <pattern id="stage-night-glows" width="640" height="96" patternUnits="userSpaceOnUse">
156 <rect width="640" height="96" fill="url(#stage-night-glow)" />
157 </pattern>
158 </defs>
159
160 <rect width="100%" height="96" fill="url(#stage-night-sky)" />
161 <rect width="100%" height="96" fill="url(#stage-night-glows)" />
162 <rect width="100%" height="96" fill="url(#stage-night-stars)" />
163
164 <g filter="url(#stage-night-soft)">
165 <path
166 d="M-12 88C-12 74 0 63 14 63C18 50 30 41 44 41C58 41 70 49 74 62C79 57 86 54 94 54C110 54 123 66 124 82C132 83 138 88 141 96H-12V88Z"
167 fill="url(#stage-night-cloud)"
168 />
169 </g>
170 <g filter="url(#stage-night-soft)">
171 <path
172 d="M150 96C151 84 161 75 173 75C176 64 186 57 198 57C210 57 220 64 223 75C231 75 238 80 241 87C250 87 257 91 260 96H150Z"
173 fill="url(#stage-night-cloud)"
174 fillOpacity="0.8"
175 />
176 </g>
177 </svg>
178 );
179}
180
181function DevBlueprintArt() {
182 return (
183 <svg
184 className="stage-blueprint h-full w-full"
185 fill="none"
186 preserveAspectRatio="xMinYMin slice"
187 viewBox={STAGE_BACKDROP_VIEW_BOX}
188 xmlns="http://www.w3.org/2000/svg"
189 >
190 <defs>
191 <linearGradient
192 id="stage-bp-paper"
193 x1="60"
194 y1="0"
195 x2="220"
196 y2="96"
197 gradientUnits="userSpaceOnUse"
198 spreadMethod="reflect"
199 >
200 <stop style={{ stopColor: "var(--stage-bp-top)" }} />
201 <stop offset="0.5" style={{ stopColor: "var(--stage-bp-mid)" }} />
202 <stop offset="1" style={{ stopColor: "var(--stage-bp-bottom)" }} />
203 </linearGradient>
204 <radialGradient
205 id="stage-bp-glow"
206 cx="0"
207 cy="0"
208 r="1"
209 gradientTransform="translate(216 14) rotate(137) scale(120 84)"
210 gradientUnits="userSpaceOnUse"
211 >
212 <stop stopColor="#D4F6FF" stopOpacity="0.4" />
213 <stop offset="0.52" stopColor="#65C8FF" stopOpacity="0.16" />
214 <stop offset="1" stopColor="#276AF1" stopOpacity="0" />
215 </radialGradient>
216 <radialGradient
217 id="stage-bp-glow-celeste"
218 cx="0"
219 cy="0"
220 r="1"
221 gradientTransform="translate(474 44) rotate(166) scale(156 92)"
222 gradientUnits="userSpaceOnUse"
223 >
224 <stop stopColor="#D2FFFF" stopOpacity="0.34" />
225 <stop offset="0.5" stopColor="#48DCF5" stopOpacity="0.18" />
226 <stop offset="1" stopColor="#277EF1" stopOpacity="0" />
227 </radialGradient>
228 <radialGradient
229 id="stage-bp-glow-violet"
230 cx="0"
231 cy="0"
232 r="1"
233 gradientTransform="translate(704 18) rotate(145) scale(132 88)"
234 gradientUnits="userSpaceOnUse"
235 >
236 <stop stopColor="#D9D8FF" stopOpacity="0.3" />
237 <stop offset="0.52" stopColor="#7C8BFF" stopOpacity="0.14" />
238 <stop offset="1" stopColor="#3155DF" stopOpacity="0" />
239 </radialGradient>
240 <pattern id="stage-bp-grid-minor" width="8" height="8" patternUnits="userSpaceOnUse">
241 <path d="M8 0H0V8" stroke="#EAF6FF" strokeOpacity="0.14" strokeWidth="0.5" />
242 </pattern>
243 <pattern id="stage-bp-grid-major" width="32" height="32" patternUnits="userSpaceOnUse">
244 <path d="M32 0H0V32" stroke="#EAF6FF" strokeOpacity="0.26" strokeWidth="0.6" />
245 </pattern>
246 <pattern id="stage-bp-ruler" width="32" height="6" patternUnits="userSpaceOnUse">
247 <path
248 d="M4 0V2.5M12 0V2.5M20 0V4M28 0V2.5"
249 stroke="#DDF7FF"
250 strokeOpacity="0.5"
251 strokeWidth="0.5"
252 />
253 </pattern>
254 <pattern id="stage-bp-glows" width="768" height="96" patternUnits="userSpaceOnUse">
255 <rect width="768" height="96" fill="url(#stage-bp-glow)" />
256 <rect width="768" height="96" fill="url(#stage-bp-glow-celeste)" />
257 <rect width="768" height="96" fill="url(#stage-bp-glow-violet)" />
258 </pattern>
259 <pattern id="stage-bp-annotations" width="768" height="96" patternUnits="userSpaceOnUse">
260 <g stroke="#DDF7FF" strokeLinecap="round" strokeOpacity="0.6" strokeWidth="0.7">
261 <path d="M180 64H264" strokeDasharray="5 4" />
262 <path d="M180 61V67M264 61V67" />
263 <path d="M276 10V44" strokeDasharray="4 4" strokeOpacity="0.5" />
264 <path d="M273 10H279M273 44H279" strokeOpacity="0.5" />
265 <path d="M348 30H428" strokeDasharray="3.5 5" strokeOpacity="0.5" />
266 <path d="M348 27V33M428 27V33" strokeOpacity="0.5" />
267 <path d="M512 48V80" strokeDasharray="5 3" strokeOpacity="0.45" />
268 <path d="M509 48H515M509 80H515" strokeOpacity="0.45" />
269 <path d="M590 70H724" strokeDasharray="7 4" strokeOpacity="0.55" />
270 <path d="M590 67V73M724 67V73" strokeOpacity="0.55" />
271 </g>
272
273 <g stroke="#DDF7FF" strokeLinecap="round" strokeOpacity="0.55" strokeWidth="0.6">
274 <g>
275 <path d="M34 60L38 64M38 60L34 64" />
276 </g>
277 <g>
278 <path d="M228 26H234M231 23V29" />
279 </g>
280 <g>
281 <path d="M143 51H149M146 48V54" />
282 </g>
283 <g>
284 <path d="M316 16L322 22M322 16L316 22" />
285 </g>
286 <g>
287 <path d="M468 70H476M472 66V74" />
288 </g>
289 <g>
290 <path d="M558 28L564 34M564 28L558 34" />
291 </g>
292 <g>
293 <path d="M742 44H750M746 40V48" />
294 </g>
295 </g>
296
297 <g stroke="#DDF7FF" strokeOpacity="0.35" strokeWidth="0.6">
298 <circle cx="196" cy="38" r="13" strokeDasharray="3.5 4" />
299 <path d="M196 33V43M191 38H201" strokeOpacity="0.6" strokeWidth="0.4" />
300 <circle cx="414" cy="64" r="10" strokeDasharray="2.5 3.5" />
301 <path d="M414 60V68M410 64H418" strokeOpacity="0.6" strokeWidth="0.4" />
302 <circle cx="648" cy="32" r="15" strokeDasharray="4 5" />
303 <path d="M648 26V38M642 32H654" strokeOpacity="0.6" strokeWidth="0.4" />
304 </g>
305 </pattern>
306 </defs>
307
308 <rect width="100%" height="96" fill="url(#stage-bp-paper)" />
309 <rect width="100%" height="96" fill="url(#stage-bp-glows)" />
310 <rect width="100%" height="96" fill="url(#stage-bp-grid-minor)" />
311 <rect width="100%" height="96" fill="url(#stage-bp-grid-major)" />
312 <rect width="100%" height="6" fill="url(#stage-bp-ruler)" />
313 <rect width="100%" height="96" fill="url(#stage-bp-annotations)" />
314 </svg>
315 );
316}
apps/web/src/index.css+36
......@@ -231,6 +231,42 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
231231 }
232232 }
233233
234 /* Stage-channel sidebar art; ::after ramps to the sidebar bg color and the
235 mask lets the surface grain show through at the boundary. */
236 .sidebar-stage-backdrop {
237 mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%);
238 -webkit-mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%);
239 }
240
241 .sidebar-stage-backdrop::after {
242 content: "";
243 position: absolute;
244 inset: 0;
245 background: linear-gradient(
246 to bottom,
247 transparent 0%,
248 transparent 28%,
249 color-mix(in srgb, var(--app-chrome-background) 10%, transparent) 40%,
250 color-mix(in srgb, var(--app-chrome-background) 30%, transparent) 52%,
251 color-mix(in srgb, var(--app-chrome-background) 58%, transparent) 64%,
252 color-mix(in srgb, var(--app-chrome-background) 82%, transparent) 75%,
253 color-mix(in srgb, var(--app-chrome-background) 96%, transparent) 85%,
254 var(--app-chrome-background) 93%
255 );
256 }
257
258 .stage-blueprint {
259 --stage-bp-top: #67c2ff;
260 --stage-bp-mid: #347ff8;
261 --stage-bp-bottom: #1538d0;
262 }
263
264 .dark .stage-blueprint {
265 --stage-bp-top: #3a7ad1;
266 --stage-bp-mid: #2050ae;
267 --stage-bp-bottom: #101f6e;
268 }
269
234270 .workspace-topbar {
235271 display: flex;
236272 height: var(--workspace-topbar-height);