mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
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:
@@ -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}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user