Tint title page accent colors to poster's vibrant color

Override CSS custom properties (--primary, --ring, --primary-foreground,
--status-watching) on the title detail page wrapper using the vibrant
color from the poster's node-vibrant palette. All Tailwind classes
referencing these variables automatically pick up the new color.

Also await lazy color extraction in getTitleWithChildren() so palettes
are available on first visit to never-before-opened titles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 17:42:02 -05:00
co-authored by Claude Opus 4.6
parent 5f45214d07
commit 2ee20a6aff
3 changed files with 42 additions and 5 deletions
+4 -1
View File
@@ -10,6 +10,7 @@ import { titles } from "@/lib/db/schema";
import { getTitleWithChildren, importTitle } from "@/lib/services/metadata"; import { getTitleWithChildren, importTitle } from "@/lib/services/metadata";
import { getUserTitleInfo } from "@/lib/services/tracking"; import { getUserTitleInfo } from "@/lib/services/tracking";
import { tmdbImageUrl } from "@/lib/tmdb/image"; import { tmdbImageUrl } from "@/lib/tmdb/image";
import { getTitleThemeStyle } from "@/lib/utils/title-theme";
import { TitleActions } from "./_components/title-actions"; import { TitleActions } from "./_components/title-actions";
import { TitleAvailability } from "./_components/title-availability"; import { TitleAvailability } from "./_components/title-availability";
import { TitleHero } from "./_components/title-hero"; import { TitleHero } from "./_components/title-hero";
@@ -72,8 +73,10 @@ export default async function TitleDetailPage({
const { title, seasons, availability } = result; const { title, seasons, availability } = result;
const themeStyle = getTitleThemeStyle(title.colorPalette);
return ( return (
<div className="relative space-y-10"> <div className="relative space-y-10" style={themeStyle}>
<TitleInteractionProvider <TitleInteractionProvider
titleId={title.id} titleId={title.id}
titleType={title.type} titleType={title.type}
+8 -4
View File
@@ -566,9 +566,13 @@ export async function getTitleWithChildren(id: string): Promise<{
}), }),
); );
// Lazy color extraction // Lazy color extraction — await so the palette is available for theming
if (!title.colorPalette && title.posterPath) { let palette = parseColorPalette(title.colorPalette);
extractAndStoreColors(title.id, title.posterPath).catch(() => {}); if (!palette && title.posterPath) {
palette =
(await extractAndStoreColors(title.id, title.posterPath).catch(
() => null,
)) ?? null;
} }
const resolvedTitle: ResolvedTitle = { const resolvedTitle: ResolvedTitle = {
@@ -586,7 +590,7 @@ export async function getTitleWithChildren(id: string): Promise<{
voteAverage: title.voteAverage, voteAverage: title.voteAverage,
voteCount: title.voteCount, voteCount: title.voteCount,
status: title.status, status: title.status,
colorPalette: parseColorPalette(title.colorPalette), colorPalette: palette,
}; };
return { title: resolvedTitle, seasons: titleSeasons, availability }; return { title: resolvedTitle, seasons: titleSeasons, availability };
+30
View File
@@ -0,0 +1,30 @@
import type { ColorPalette } from "@/lib/types/title";
function hexToRelativeLuminance(hex: string): number {
const r = Number.parseInt(hex.slice(1, 3), 16) / 255;
const g = Number.parseInt(hex.slice(3, 5), 16) / 255;
const b = Number.parseInt(hex.slice(5, 7), 16) / 255;
const toLinear = (c: number) =>
c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
}
export function getTitleThemeStyle(
palette: ColorPalette | null,
): React.CSSProperties {
const color = palette?.vibrant;
if (!color) return {};
const luminance = hexToRelativeLuminance(color);
const foreground =
luminance > 0.3
? "oklch(0.13 0.006 55)" // dark (current --primary-foreground)
: "oklch(0.93 0.015 80)"; // light (current --foreground)
return {
"--primary": color,
"--ring": color,
"--primary-foreground": foreground,
"--status-watching": color,
} as React.CSSProperties;
}