The AtmosphereConf talks your skyline missed
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: continuous glow + opacity fade for talk cards (#38)

Replace 3-tier glow classes with CSS custom property (--glow) that
scales box-shadow continuously with intensity. Cards also fade in
opacity (1.0 at full intensity → 0.5 at zero), so covered talks
recede into the background while missed talks glow bright.

Adds always-visible coverage percentage in lower-right corner of
scored cards. Adds text-label-lg design token. Clamps glowIntensity
to [0,1]. Guards mock crawl route against production and malformed JSON.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+54 -22
+9 -5
src/app/api/crawl/route.ts
··· 25 25 26 26 export async function GET(request: NextRequest) { 27 27 // Dev-only: serve mock crawl data without authentication 28 - if (MOCK_CRAWL) { 29 - const mockPath = path.resolve(process.cwd(), "data", "mock-crawl.json"); 30 - if (fs.existsSync(mockPath)) { 31 - const mock = JSON.parse(fs.readFileSync(mockPath, "utf-8")); 32 - return NextResponse.json({ ...mock, cached: true }); 28 + if (MOCK_CRAWL && process.env.NODE_ENV !== "production") { 29 + try { 30 + const mockPath = path.resolve(process.cwd(), "data", "mock-crawl.json"); 31 + if (fs.existsSync(mockPath)) { 32 + const mock = JSON.parse(fs.readFileSync(mockPath, "utf-8")); 33 + return NextResponse.json({ ...mock, cached: true }); 34 + } 35 + } catch (err) { 36 + console.warn("Mock crawl data failed to load, falling through:", err); 33 37 } 34 38 } 35 39
+23 -1
src/app/globals.css
··· 121 121 font-size: 1rem; 122 122 line-height: 1.6; 123 123 } 124 + .text-label-lg { 125 + font-family: var(--font-label); 126 + font-size: 1rem; 127 + line-height: 1.4; 128 + letter-spacing: 0.02em; 129 + } 124 130 .text-label-md { 125 131 font-family: var(--font-label); 126 132 font-size: 0.875rem; ··· 154 160 radial-gradient(circle at 90% 80%, rgba(255, 223, 144, 0.03) 0%, transparent 40%); 155 161 } 156 162 163 + /* Continuous glow driven by --glow custom property (0–1). 164 + Cards scale smoothly from no glow to full bioluminescence. */ 157 165 .biolume-glow { 158 166 box-shadow: 0 0 20px rgba(108, 252, 178, 0.15); 159 167 } 160 168 .biolume-glow-strong { 161 - box-shadow: 0 0 40px rgba(108, 252, 178, 0.3); 169 + box-shadow: 170 + 0 0 12px rgba(255, 255, 255, 0.15), 171 + 0 0 30px rgba(180, 255, 215, 0.25), 172 + 0 0 60px rgba(108, 252, 178, 0.3), 173 + 0 0 100px rgba(108, 252, 178, 0.15); 174 + } 175 + 176 + /* Continuous glow for LumeCards — scales with --glow (0–1). 177 + Applied via [style*="--glow"] so it only hits elements with the prop. */ 178 + [style*="--glow"] { 179 + box-shadow: 180 + 0 0 calc(16px * var(--glow)) rgba(255, 255, 255, calc(0.25 * var(--glow))), 181 + 0 0 calc(40px * var(--glow)) rgba(180, 255, 215, calc(0.35 * var(--glow))), 182 + 0 0 calc(80px * var(--glow)) rgba(108, 252, 178, calc(0.4 * var(--glow))), 183 + 0 0 calc(120px * var(--glow)) rgba(108, 252, 178, calc(0.2 * var(--glow))); 162 184 } 163 185 164 186 .ghost-border {
+22 -16
src/components/ui/lume-card.tsx
··· 12 12 score?: TalkScore | null; 13 13 } 14 14 15 - function glowStyle(intensity: number): string { 16 - if (intensity > 0.7) return "biolume-glow-strong"; 17 - if (intensity > 0.3) return "biolume-glow"; 18 - return ""; 19 - } 20 - 21 15 function ScoreDetail({ score }: { score: TalkScore }) { 22 16 if (score.state === "unknown") return null; 23 17 ··· 49 43 children, 50 44 ...props 51 45 }: LumeCardProps) { 52 - const isUnderstory = glowIntensity > 0.3; 46 + const glow = Math.min(Math.max(glowIntensity, 0), 1); 47 + const isUnderstory = glow > 0.3; 53 48 const hasDetail = score && score.state !== "unknown"; 49 + 50 + // Opacity fades covered talks into the background. 51 + // Range: 1.0 at intensity 1 → 0.5 at intensity 0. 52 + const opacity = 0.5 + glow * 0.5; 54 53 55 54 return ( 56 55 <div ··· 58 57 "group relative rounded-lg", 59 58 "bg-surface-container-low/60 backdrop-blur-[20px]", 60 59 "border-t-2", 61 - glowIntensity > 0.3 60 + glow > 0.3 62 61 ? "border-primary-fixed-dim" 63 - : glowIntensity > 0 62 + : glow > 0 64 63 ? "border-primary-fixed-dim/50" 65 64 : "border-primary-fixed-dim/20", 66 65 "transition-all duration-500", 67 - "hover:biolume-glow-strong", 66 + "hover:biolume-glow-strong hover:!opacity-100", 68 67 "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-fixed", 69 - glowStyle(glowIntensity), 70 68 isUnderstory ? "animate-breathe" : "", 71 69 className, 72 70 ].join(" ")} 73 - style={ 74 - tileIndex !== undefined 75 - ? ({ "--tile-index": tileIndex } as React.CSSProperties) 76 - : undefined 77 - } 71 + style={{ 72 + "--glow": glow, 73 + opacity, 74 + ...(tileIndex !== undefined 75 + ? { "--tile-index": tileIndex } 76 + : {}), 77 + } as React.CSSProperties} 78 78 {...props} 79 79 > 80 80 {interestMatch && ( ··· 84 84 /> 85 85 )} 86 86 {children} 87 + 88 + {score && score.state !== "unknown" && score.normalizedCoverage != null && ( 89 + <span className="absolute bottom-3 right-4 text-label-lg tabular-nums text-on-surface-variant/60"> 90 + {Math.round(score.normalizedCoverage * 100)}% 91 + </span> 92 + )} 87 93 88 94 {hasDetail && ( 89 95 <div