"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); }