| 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | import { useId } from "react"; |
| 3 | |
| 4 | import { APP_STAGE_LABEL } from "../branding"; |
| 5 | import { resolveServerBackedAppStageLabel } from "../branding.logic"; |
| 6 | import { primaryServerConfigAtom } from "../state/server"; |
| 7 | |
| 8 | export type SidebarStageBackdropVariant = "nightly" | "dev"; |
| 9 | export 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. |
| 13 | const STAGE_BACKDROP_VIEW_BOX = "0 0 8192 96"; |
| 14 | |
| 15 | export 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 | |
| 26 | export 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 | |
| 35 | export 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 | |
| 45 | export function useSidebarStageBackdropVariant(enabled = true): SidebarStageBackdropVariant | null { |
| 46 | return resolveSidebarStageBackdropVariant(useEnvironmentStageLabel(), enabled); |
| 47 | } |
| 48 | |
| 49 | export function StageBackdropArt({ variant }: { variant: SidebarStageBackdropVariant }) { |
| 50 | return variant === "nightly" ? <NightlySkyArt /> : <DevBlueprintArt />; |
| 51 | } |
| 52 | |
| 53 | export function StageBackdropButtonArt({ variant }: { variant: SidebarStageBackdropVariant }) { |
| 54 | return variant === "nightly" ? <NightlySkyArt compact /> : <DevBlueprintArt compact />; |
| 55 | } |
| 56 | |
| 57 | const 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 | |
| 82 | const NIGHTLY_SPARKLES: ReadonlyArray<{ x: number; y: number }> = [ |
| 83 | { x: 70, y: 28 }, |
| 84 | { x: 160, y: 36 }, |
| 85 | { x: 246, y: 26 }, |
| 86 | ]; |
| 87 | |
| 88 | function 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 | |
| 200 | function 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 | } |