1import { useAtomValue } from "@effect/atom-react";
2import { useId } from "react";
3
4import { APP_STAGE_LABEL } from "../branding";
5import { resolveServerBackedAppStageLabel } from "../branding.logic";
6import { primaryServerConfigAtom } from "../state/server";
7
8export type SidebarStageBackdropVariant = "nightly" | "dev";
9export type EnvironmentIdentificationPillLabel = "Dev" | "Nightly";
10
11// A wide viewBox keeps the 96-unit art height at a fixed scale while sidebar resizing reveals
12// more horizontal canvas instead of zooming the scene.
13const STAGE_BACKDROP_VIEW_BOX = "0 0 8192 96";
14
15export function resolveSidebarStageBackdropVariant(
16 stageLabel: string,
17 enabled = true,
18): SidebarStageBackdropVariant | null {
19 if (!enabled) return null;
20 const normalized = stageLabel.trim().toLowerCase();
21 if (normalized === "nightly") return "nightly";
22 if (normalized === "dev") return "dev";
23 return null;
24}
25
26export function resolveEnvironmentIdentificationPillLabel(
27 stageLabel: string,
28): EnvironmentIdentificationPillLabel | null {
29 const normalized = stageLabel.trim().toLowerCase();
30 if (normalized === "dev") return "Dev";
31 if (normalized === "nightly") return "Nightly";
32 return null;
33}
34
35export function useEnvironmentStageLabel(): string {
36 const primaryServerVersion =
37 useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null;
38
39 return resolveServerBackedAppStageLabel({
40 primaryServerVersion,
41 fallbackStageLabel: APP_STAGE_LABEL,
42 });
43}
44
45export function useSidebarStageBackdropVariant(enabled = true): SidebarStageBackdropVariant | null {
46 return resolveSidebarStageBackdropVariant(useEnvironmentStageLabel(), enabled);
47}
48
49export function StageBackdropArt({ variant }: { variant: SidebarStageBackdropVariant }) {
50 return variant === "nightly" ? <NightlySkyArt /> : <DevBlueprintArt />;
51}
52
53export function StageBackdropButtonArt({ variant }: { variant: SidebarStageBackdropVariant }) {
54 return variant === "nightly" ? <NightlySkyArt compact /> : <DevBlueprintArt compact />;
55}
56
57const NIGHTLY_STARS: ReadonlyArray<{
58 cx: number;
59 cy: number;
60 r: number;
61 opacity: number;
62}> = [
63 { cx: 14, cy: 10, r: 0.6, opacity: 0.85 },
64 { cx: 38, cy: 22, r: 0.4, opacity: 0.55 },
65 { cx: 58, cy: 8, r: 0.5, opacity: 0.7 },
66 { cx: 84, cy: 16, r: 0.4, opacity: 0.5 },
67 { cx: 104, cy: 7, r: 0.6, opacity: 0.8 },
68 { cx: 126, cy: 20, r: 0.4, opacity: 0.55 },
69 { cx: 148, cy: 11, r: 0.5, opacity: 0.7 },
70 { cx: 170, cy: 24, r: 0.4, opacity: 0.5 },
71 { cx: 192, cy: 9, r: 0.6, opacity: 0.8 },
72 { cx: 214, cy: 18, r: 0.4, opacity: 0.55 },
73 { cx: 236, cy: 8, r: 0.5, opacity: 0.7 },
74 { cx: 258, cy: 20, r: 0.45, opacity: 0.6 },
75 { cx: 278, cy: 11, r: 0.55, opacity: 0.75 },
76 { cx: 26, cy: 34, r: 0.4, opacity: 0.45 },
77 { cx: 118, cy: 34, r: 0.4, opacity: 0.45 },
78 { cx: 202, cy: 32, r: 0.4, opacity: 0.5 },
79 { cx: 268, cy: 34, r: 0.4, opacity: 0.45 },
80];
81
82const NIGHTLY_SPARKLES: ReadonlyArray<{ x: number; y: number }> = [
83 { x: 70, y: 28 },
84 { x: 160, y: 36 },
85 { x: 246, y: 26 },
86];
87
88function NightlySkyArt({ compact = false }: { compact?: boolean }) {
89 const idPrefix = useId().replaceAll(":", "");
90 const skyId = `${idPrefix}-stage-night-sky`;
91 const glowId = `${idPrefix}-stage-night-glow`;
92 const cloudId = `${idPrefix}-stage-night-cloud`;
93 const softId = `${idPrefix}-stage-night-soft`;
94 const starsId = `${idPrefix}-stage-night-stars`;
95 const glowsId = `${idPrefix}-stage-night-glows`;
96
97 return (
98 <svg
99 data-stage-art="nightly"
100 className="h-full w-full"
101 fill="none"
102 preserveAspectRatio="xMinYMin slice"
103 viewBox={compact ? "96 0 8192 96" : STAGE_BACKDROP_VIEW_BOX}
104 xmlns="http://www.w3.org/2000/svg"
105 >
106 <defs>
107 <linearGradient
108 id={skyId}
109 x1="24"
110 y1="0"
111 x2="264"
112 y2="96"
113 gradientUnits="userSpaceOnUse"
114 spreadMethod="reflect"
115 >
116 <stop style={{ stopColor: "var(--stage-night-bottom)" }} />
117 <stop offset="0.5" style={{ stopColor: "var(--stage-night-mid)" }} />
118 <stop offset="1" style={{ stopColor: "var(--stage-night-top)" }} />
119 </linearGradient>
120 <radialGradient
121 id={glowId}
122 cx="0"
123 cy="0"
124 r="1"
125 gradientTransform="translate(216 18) rotate(137) scale(120 84)"
126 gradientUnits="userSpaceOnUse"
127 >
128 <stop style={{ stopColor: "var(--stage-night-glow-highlight)" }} stopOpacity="0.4" />
129 <stop
130 offset="0.5"
131 style={{ stopColor: "var(--stage-night-glow-secondary)" }}
132 stopOpacity="0.16"
133 />
134 <stop offset="1" style={{ stopColor: "var(--stage-night-bottom)" }} stopOpacity="0" />
135 </radialGradient>
136 <linearGradient id={cloudId} x1="0" y1="60" x2="288" y2="96" gradientUnits="userSpaceOnUse">
137 <stop style={{ stopColor: "var(--stage-night-highlight)" }} stopOpacity="0.5" />
138 <stop
139 offset="0.52"
140 style={{ stopColor: "var(--stage-night-secondary)" }}
141 stopOpacity="0.62"
142 />
143 <stop offset="1" style={{ stopColor: "var(--stage-night-tertiary)" }} stopOpacity="0.5" />
144 </linearGradient>
145 <filter id={softId} x="-24" y="-24" width="336" height="144" filterUnits="userSpaceOnUse">
146 <feGaussianBlur stdDeviation="4" />
147 </filter>
148 <pattern id={starsId} width="288" height="96" patternUnits="userSpaceOnUse">
149 <g style={{ fill: "var(--stage-night-line)" }}>
150 {NIGHTLY_STARS.map((star) => (
151 <circle
152 key={`${star.cx}-${star.cy}`}
153 cx={star.cx}
154 cy={star.cy}
155 r={star.r}
156 fillOpacity={star.opacity}
157 />
158 ))}
159 </g>
160 <g
161 style={{ stroke: "var(--stage-night-sparkle)" }}
162 strokeLinecap="round"
163 strokeOpacity="0.7"
164 strokeWidth="0.6"
165 >
166 {NIGHTLY_SPARKLES.map((sparkle) => (
167 <g key={`${sparkle.x}-${sparkle.y}`}>
168 <path d={`M${sparkle.x - 1.5} ${sparkle.y}H${sparkle.x + 1.5}`} />
169 <path d={`M${sparkle.x} ${sparkle.y - 1.5}V${sparkle.y + 1.5}`} />
170 </g>
171 ))}
172 </g>
173 </pattern>
174 <pattern id={glowsId} width="640" height="96" patternUnits="userSpaceOnUse">
175 <rect width="640" height="96" fill={`url(#${glowId})`} />
176 </pattern>
177 </defs>
178
179 <rect width="100%" height="96" fill={`url(#${skyId})`} />
180 <rect width="100%" height="96" fill={`url(#${glowsId})`} />
181 <rect width="100%" height="96" fill={`url(#${starsId})`} />
182
183 <g filter={`url(#${softId})`}>
184 <path
185 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"
186 fill={`url(#${cloudId})`}
187 />
188 </g>
189 <g filter={`url(#${softId})`}>
190 <path
191 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"
192 fill={`url(#${cloudId})`}
193 fillOpacity="0.8"
194 />
195 </g>
196 </svg>
197 );
198}
199
200function DevBlueprintArt({ compact = false }: { compact?: boolean }) {
201 const idPrefix = useId().replaceAll(":", "");
202 const paperId = `${idPrefix}-stage-bp-paper`;
203 const glowId = `${idPrefix}-stage-bp-glow`;
204 const celesteGlowId = `${idPrefix}-stage-bp-glow-celeste`;
205 const violetGlowId = `${idPrefix}-stage-bp-glow-violet`;
206 const minorGridId = `${idPrefix}-stage-bp-grid-minor`;
207 const majorGridId = `${idPrefix}-stage-bp-grid-major`;
208 const rulerId = `${idPrefix}-stage-bp-ruler`;
209 const glowsId = `${idPrefix}-stage-bp-glows`;
210 const annotationsId = `${idPrefix}-stage-bp-annotations`;
211
212 return (
213 <svg
214 data-stage-art="blueprint"
215 className="h-full w-full"
216 fill="none"
217 preserveAspectRatio="xMinYMin slice"
218 viewBox={compact ? "64 0 8192 96" : STAGE_BACKDROP_VIEW_BOX}
219 xmlns="http://www.w3.org/2000/svg"
220 >
221 <defs>
222 <linearGradient
223 id={paperId}
224 x1="60"
225 y1="0"
226 x2="220"
227 y2="96"
228 gradientUnits="userSpaceOnUse"
229 spreadMethod="reflect"
230 >
231 <stop style={{ stopColor: "var(--stage-art-bottom)" }} />
232 <stop offset="0.5" style={{ stopColor: "var(--stage-art-mid)" }} />
233 <stop offset="1" style={{ stopColor: "var(--stage-art-top)" }} />
234 </linearGradient>
235 <radialGradient
236 id={glowId}
237 cx="0"
238 cy="0"
239 r="1"
240 gradientTransform="translate(216 14) rotate(137) scale(120 84)"
241 gradientUnits="userSpaceOnUse"
242 >
243 <stop style={{ stopColor: "var(--stage-art-highlight)" }} stopOpacity="0.4" />
244 <stop
245 offset="0.52"
246 style={{ stopColor: "var(--stage-art-secondary)" }}
247 stopOpacity="0.16"
248 />
249 <stop offset="1" style={{ stopColor: "var(--stage-art-bottom)" }} stopOpacity="0" />
250 </radialGradient>
251 <radialGradient
252 id={celesteGlowId}
253 cx="0"
254 cy="0"
255 r="1"
256 gradientTransform="translate(474 44) rotate(166) scale(156 92)"
257 gradientUnits="userSpaceOnUse"
258 >
259 <stop style={{ stopColor: "var(--stage-art-celeste-highlight)" }} stopOpacity="0.34" />
260 <stop
261 offset="0.5"
262 style={{ stopColor: "var(--stage-art-celeste-secondary)" }}
263 stopOpacity="0.18"
264 />
265 <stop offset="1" style={{ stopColor: "var(--stage-art-bottom)" }} stopOpacity="0" />
266 </radialGradient>
267 <radialGradient
268 id={violetGlowId}
269 cx="0"
270 cy="0"
271 r="1"
272 gradientTransform="translate(704 18) rotate(145) scale(132 88)"
273 gradientUnits="userSpaceOnUse"
274 >
275 <stop style={{ stopColor: "var(--stage-art-violet-highlight)" }} stopOpacity="0.3" />
276 <stop
277 offset="0.52"
278 style={{ stopColor: "var(--stage-art-tertiary)" }}
279 stopOpacity="0.14"
280 />
281 <stop offset="1" style={{ stopColor: "var(--stage-art-bottom)" }} stopOpacity="0" />
282 </radialGradient>
283 <pattern id={minorGridId} width="8" height="8" patternUnits="userSpaceOnUse">
284 <path
285 d="M8 0H0V8"
286 style={{ stroke: "var(--stage-art-grid-line)" }}
287 strokeOpacity="0.14"
288 strokeWidth="0.5"
289 />
290 </pattern>
291 <pattern id={majorGridId} width="32" height="32" patternUnits="userSpaceOnUse">
292 <path
293 d="M32 0H0V32"
294 style={{ stroke: "var(--stage-art-grid-line)" }}
295 strokeOpacity="0.26"
296 strokeWidth="0.6"
297 />
298 </pattern>
299 <pattern id={rulerId} width="32" height="6" patternUnits="userSpaceOnUse">
300 <path
301 d="M4 0V2.5M12 0V2.5M20 0V4M28 0V2.5"
302 style={{ stroke: "var(--stage-art-line)" }}
303 strokeOpacity="0.5"
304 strokeWidth="0.5"
305 />
306 </pattern>
307 <pattern id={glowsId} width="768" height="96" patternUnits="userSpaceOnUse">
308 <rect width="768" height="96" fill={`url(#${glowId})`} />
309 <rect width="768" height="96" fill={`url(#${celesteGlowId})`} />
310 <rect width="768" height="96" fill={`url(#${violetGlowId})`} />
311 </pattern>
312 <pattern id={annotationsId} width="768" height="96" patternUnits="userSpaceOnUse">
313 <g
314 style={{ stroke: "var(--stage-art-line)" }}
315 strokeLinecap="round"
316 strokeOpacity="0.6"
317 strokeWidth="0.7"
318 >
319 <path d="M180 64H264" strokeDasharray="5 4" />
320 <path d="M180 61V67M264 61V67" />
321 <path d="M276 10V44" strokeDasharray="4 4" strokeOpacity="0.5" />
322 <path d="M273 10H279M273 44H279" strokeOpacity="0.5" />
323 <path d="M348 30H428" strokeDasharray="3.5 5" strokeOpacity="0.5" />
324 <path d="M348 27V33M428 27V33" strokeOpacity="0.5" />
325 <path d="M512 48V80" strokeDasharray="5 3" strokeOpacity="0.45" />
326 <path d="M509 48H515M509 80H515" strokeOpacity="0.45" />
327 <path d="M590 70H724" strokeDasharray="7 4" strokeOpacity="0.55" />
328 <path d="M590 67V73M724 67V73" strokeOpacity="0.55" />
329 </g>
330
331 <g
332 style={{ stroke: "var(--stage-art-line)" }}
333 strokeLinecap="round"
334 strokeOpacity="0.55"
335 strokeWidth="0.6"
336 >
337 <g>
338 <path d="M34 60L38 64M38 60L34 64" />
339 </g>
340 <g>
341 <path d="M228 26H234M231 23V29" />
342 </g>
343 <g>
344 <path d="M143 51H149M146 48V54" />
345 </g>
346 <g>
347 <path d="M316 16L322 22M322 16L316 22" />
348 </g>
349 <g>
350 <path d="M468 70H476M472 66V74" />
351 </g>
352 <g>
353 <path d="M558 28L564 34M564 28L558 34" />
354 </g>
355 <g>
356 <path d="M742 44H750M746 40V48" />
357 </g>
358 </g>
359
360 <g style={{ stroke: "var(--stage-art-line)" }} strokeOpacity="0.35" strokeWidth="0.6">
361 <circle cx="196" cy="38" r="13" strokeDasharray="3.5 4" />
362 <path d="M196 33V43M191 38H201" strokeOpacity="0.6" strokeWidth="0.4" />
363 <circle cx="414" cy="64" r="10" strokeDasharray="2.5 3.5" />
364 <path d="M414 60V68M410 64H418" strokeOpacity="0.6" strokeWidth="0.4" />
365 <circle cx="648" cy="32" r="15" strokeDasharray="4 5" />
366 <path d="M648 26V38M642 32H654" strokeOpacity="0.6" strokeWidth="0.4" />
367 </g>
368 </pattern>
369 </defs>
370
371 <rect width="100%" height="96" fill={`url(#${paperId})`} />
372 <rect width="100%" height="96" fill={`url(#${glowsId})`} />
373 <rect width="100%" height="96" fill={`url(#${minorGridId})`} />
374 <rect width="100%" height="96" fill={`url(#${majorGridId})`} />
375 <rect width="100%" height="6" fill={`url(#${rulerId})`} />
376 <rect width="100%" height="96" fill={`url(#${annotationsId})`} />
377 </svg>
378 );
379}