"use client";
import {
createContext,
CSSProperties,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import {
colorToString,
useColorAttribute,
useColorAttributeNullable,
} from "./useColorAttribute";
import { Color as AriaColor, parseColor } from "react-aria-components";
import { parse, contrastLstar, ColorSpace, sRGB } from "colorjs.io/fn";
import { useEntity } from "src/replicache";
import { useLeafletPublicationData } from "components/PageSWRDataProvider";
import {
PublicationBackgroundProvider,
PublicationThemeProvider,
} from "./PublicationThemeProvider";
import { PubLeafletPublication } from "lexicons/api";
type CSSVariables = {
"--bg-leaflet": string;
"--bg-page": string;
"--primary": string;
"--accent-1": string;
"--accent-2": string;
"--accent-contrast": string;
"--highlight-1": string;
"--highlight-2": string;
"--highlight-3": string;
};
// define the color defaults for everything
export const ThemeDefaults = {
"theme/page-background": "#FDFCFA",
"theme/card-background": "#FFFFFF",
"theme/primary": "#272727",
"theme/highlight-1": "#FFFFFF",
"theme/highlight-2": "#EDD280",
"theme/highlight-3": "#FFCDC3",
//everywhere else, accent-background = accent-1 and accent-text = accent-2.
// we just need to create a migration pipeline before we can change this
"theme/accent-text": "#FFFFFF",
"theme/accent-background": "#0000FF",
"theme/accent-contrast": "#0000FF",
};
// define a function to set an Aria Color to a CSS Variable in RGB
function setCSSVariableToColor(
el: HTMLElement,
name: string,
value: AriaColor,
) {
el?.style.setProperty(name, colorToString(value, "rgb"));
}
//Create a wrapper that applies a theme to each page
export function ThemeProvider(props: {
entityID: string | null;
local?: boolean;
children: React.ReactNode;
className?: string;
}) {
let { data: pub } = useLeafletPublicationData();
if (!pub || !pub.publications) return ;
return (
);
}
// for PUBLICATIONS: define Aria Colors for each value and use BaseThemeProvider to wrap the content of the page in the theme
// for LEAFLETS : define Aria Colors for each value and use BaseThemeProvider to wrap the content of the page in the theme
export function LeafletThemeProvider(props: {
entityID: string | null;
local?: boolean;
children: React.ReactNode;
}) {
let bgLeaflet = useColorAttribute(props.entityID, "theme/page-background");
let bgPage = useColorAttribute(props.entityID, "theme/card-background");
let showPageBackground = !useEntity(
props.entityID,
"theme/card-border-hidden",
)?.data.value;
let primary = useColorAttribute(props.entityID, "theme/primary");
let highlight1 = useEntity(props.entityID, "theme/highlight-1");
let highlight2 = useColorAttribute(props.entityID, "theme/highlight-2");
let highlight3 = useColorAttribute(props.entityID, "theme/highlight-3");
let accent1 = useColorAttribute(props.entityID, "theme/accent-background");
let accent2 = useColorAttribute(props.entityID, "theme/accent-text");
return (
{props.children}
);
}
// handles setting all the Aria Color values to CSS Variables and wrapping the page the theme providers
export const BaseThemeProvider = ({
local,
bgLeaflet,
bgPage,
primary,
accent1,
accent2,
highlight1,
highlight2,
highlight3,
showPageBackground,
children,
}: {
local?: boolean;
showPageBackground?: boolean;
bgLeaflet: AriaColor;
bgPage: AriaColor;
primary: AriaColor;
accent1: AriaColor;
accent2: AriaColor;
highlight1?: string;
highlight2: AriaColor;
highlight3: AriaColor;
children: React.ReactNode;
}) => {
// set accent contrast to the accent color that has the highest contrast with the page background
let accentContrast;
//sorting the accents by contrast on background
let sortedAccents = [accent1, accent2].sort((a, b) => {
return (
getColorContrast(
colorToString(b, "rgb"),
colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
) -
getColorContrast(
colorToString(a, "rgb"),
colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
)
);
});
// if the contrast-y accent is too similar to the primary text color,
// and the not contrast-y option is different from the backgrond,
// then use the not contrasty option
if (
getColorContrast(
colorToString(sortedAccents[0], "rgb"),
colorToString(primary, "rgb"),
) < 30 &&
getColorContrast(
colorToString(sortedAccents[1], "rgb"),
colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
) > 12
) {
accentContrast = sortedAccents[1];
} else accentContrast = sortedAccents[0];
useEffect(() => {
if (local) return;
let el = document.querySelector(":root") as HTMLElement;
if (!el) return;
setCSSVariableToColor(el, "--bg-leaflet", bgLeaflet);
setCSSVariableToColor(el, "--bg-page", bgPage);
document.body.style.backgroundColor = `rgb(${colorToString(bgLeaflet, "rgb")})`;
document
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", `rgb(${colorToString(bgLeaflet, "rgb")})`);
el?.style.setProperty(
"--bg-page-alpha",
bgPage.getChannelValue("alpha").toString(),
);
setCSSVariableToColor(el, "--primary", primary);
setCSSVariableToColor(el, "--highlight-2", highlight2);
setCSSVariableToColor(el, "--highlight-3", highlight3);
//highlight 1 is special because its default value is a calculated value
if (highlight1) {
let color = parseColor(`hsba(${highlight1})`);
el?.style.setProperty(
"--highlight-1",
`rgb(${colorToString(color, "rgb")})`,
);
} else {
el?.style.setProperty(
"--highlight-1",
"color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 75%)",
);
}
setCSSVariableToColor(el, "--accent-1", accent1);
setCSSVariableToColor(el, "--accent-2", accent2);
el?.style.setProperty(
"--accent-contrast",
colorToString(accentContrast, "rgb"),
);
el?.style.setProperty(
"--accent-1-is-contrast",
accentContrast === accent1 ? "1" : "0",
);
}, [
local,
bgLeaflet,
bgPage,
primary,
highlight1,
highlight2,
highlight3,
accent1,
accent2,
accentContrast,
]);
return (
{" "}
{children}{" "}
);
};
let CardThemeProviderContext = createContext(null);
export function NestedCardThemeProvider(props: { children: React.ReactNode }) {
let card = useContext(CardThemeProviderContext);
if (!card) return props.children;
return (
{props.children}
);
}
export function CardThemeProvider(props: {
entityID: string;
children: React.ReactNode;
}) {
let bgPage = useColorAttributeNullable(
props.entityID,
"theme/card-background",
);
let primary = useColorAttributeNullable(props.entityID, "theme/primary");
let accent1 = useColorAttributeNullable(
props.entityID,
"theme/accent-background",
);
let accent2 = useColorAttributeNullable(props.entityID, "theme/accent-text");
let accentContrast =
bgPage && accent1 && accent2
? [accent1, accent2].sort((a, b) => {
return (
getColorContrast(
colorToString(b, "rgb"),
colorToString(bgPage, "rgb"),
) -
getColorContrast(
colorToString(a, "rgb"),
colorToString(bgPage, "rgb"),
)
);
})[0]
: null;
return (
{props.children}
);
}
// Wrapper within the Theme Wrapper that provides background image data
export const ThemeBackgroundProvider = (props: {
entityID: string;
children: React.ReactNode;
}) => {
let { data: pub } = useLeafletPublicationData();
let backgroundImage = useEntity(props.entityID, "theme/background-image");
let backgroundImageRepeat = useEntity(
props.entityID,
"theme/background-image-repeat",
);
if (pub?.publications) {
return (
{props.children}
);
}
return (
{props.children}
);
};
// used to calculate the contrast between page and accent1, accent2, and determin which is higher contrast
export function getColorContrast(color1: string, color2: string) {
ColorSpace.register(sRGB);
let parsedColor1 = parse(`rgb(${color1})`);
let parsedColor2 = parse(`rgb(${color2})`);
return contrastLstar(parsedColor1, parsedColor2);
}