"use client"; import { createContext, CSSProperties, useContext, useEffect } from "react"; // Context for cardBorderHidden export const CardBorderHiddenContext = createContext(false); export function useCardBorderHiddenContext() { return useContext(CardBorderHiddenContext); } // Context for hasBackgroundImage export const HasBackgroundImageContext = createContext(false); export function useHasBackgroundImageContext() { return useContext(HasBackgroundImageContext); } import { colorToString, useColorAttribute, useColorAttributeNullable, } from "./useColorAttribute"; import { Color as AriaColor, parseColor } from "react-aria-components"; import { useEntity } from "src/replicache"; import { useLeafletPublicationData } from "components/PageSWRDataProvider"; import { PublicationBackgroundProvider, PublicationThemeProvider, } from "./PublicationThemeProvider"; import { getColorDifference } from "./themeUtils"; import { getFontConfig, getGoogleFontsUrl, getFontFamilyValue, getFontBaseSize, defaultFontId } from "src/fonts"; // 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; initialHeadingFontId?: string; initialBodyFontId?: string; }) { let { data: pub, normalizedPublication } = 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; initialHeadingFontId?: string; initialBodyFontId?: string; }) { let bgLeaflet = useColorAttribute(props.entityID, "theme/page-background"); let bgPage = useColorAttribute(props.entityID, "theme/card-background"); let cardBorderHiddenValue = useEntity( props.entityID, "theme/card-border-hidden", )?.data.value; let showPageBackground = !cardBorderHiddenValue; let backgroundImage = useEntity(props.entityID, "theme/background-image"); let hasBackgroundImage = !!backgroundImage; 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"); let pageWidth = useEntity(props.entityID, "theme/page-width"); // Use initial font IDs as fallback until Replicache syncs let headingFontId = useEntity(props.entityID, "theme/heading-font")?.data.value ?? props.initialHeadingFontId; let bodyFontId = useEntity(props.entityID, "theme/body-font")?.data.value ?? props.initialBodyFontId; 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: bgPageProp, primary, accent1, accent2, highlight1, highlight2, highlight3, showPageBackground, pageWidth, hasBackgroundImage, headingFontId, bodyFontId, className, children, }: { local?: boolean; showPageBackground?: boolean; hasBackgroundImage?: boolean; bgLeaflet: AriaColor; bgPage: AriaColor; primary: AriaColor; accent1: AriaColor; accent2: AriaColor; highlight1?: string; highlight2: AriaColor; highlight3: AriaColor; pageWidth?: number; headingFontId?: string; bodyFontId?: string; className?: string; children: React.ReactNode; }) => { // When showPageBackground is false and there's no background image, // pageBg should inherit from leafletBg const bgPage = !showPageBackground && !hasBackgroundImage ? bgLeaflet : bgPageProp; let accentContrast; let sortedAccents = [accent1, accent2].sort((a, b) => { // sort accents by contrast against the background return ( getColorDifference( colorToString(b, "rgb"), colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), ) - getColorDifference( colorToString(a, "rgb"), colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), ) ); }); if ( // if the contrast-y accent is too similar to text color getColorDifference( colorToString(sortedAccents[0], "rgb"), colorToString(primary, "rgb"), ) < 0.15 && // and if the other accent is different enough from the background getColorDifference( colorToString(sortedAccents[1], "rgb"), colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), ) > 0.31 ) { //then choose the less contrast-y accent accentContrast = sortedAccents[1]; } else { // otherwise, choose the more contrast-y option accentContrast = sortedAccents[0]; } // Get font configs for CSS variables. // When using the default font (Quattro), use var(--font-quattro) which is // always available via next/font/local in layout.tsx, rather than the raw // font-family name 'iA Writer Quattro V' which depends on a dynamic @font-face. // When both heading and body are default, omit the variables entirely so the // CSS fallback var(--theme-font, var(--font-quattro)) resolves naturally. const isDefaultBody = !bodyFontId || bodyFontId === defaultFontId; const isDefaultHeading = !headingFontId || headingFontId === defaultFontId; const headingFontConfig = getFontConfig(headingFontId); const bodyFontConfig = getFontConfig(bodyFontId); const headingFontValue = isDefaultHeading ? (isDefaultBody ? undefined : "var(--font-quattro)") : getFontFamilyValue(headingFontConfig); const bodyFontValue = isDefaultBody ? (isDefaultHeading ? undefined : "var(--font-quattro)") : getFontFamilyValue(bodyFontConfig); const bodyFontBaseSize = isDefaultBody ? undefined : getFontBaseSize(bodyFontConfig); const headingGoogleFontsUrl = getGoogleFontsUrl(headingFontConfig); const bodyGoogleFontsUrl = getGoogleFontsUrl(bodyFontConfig); // Dynamically load Google Fonts when fonts change useEffect(() => { const loadGoogleFont = (url: string | null, fontFamily: string) => { if (!url) return; // Check if this font stylesheet is already in the document const existingLink = document.querySelector(`link[href="${url}"]`); if (existingLink) return; // Add preconnect hints if not present if (!document.querySelector('link[href="https://fonts.googleapis.com"]')) { const preconnect1 = document.createElement("link"); preconnect1.rel = "preconnect"; preconnect1.href = "https://fonts.googleapis.com"; document.head.appendChild(preconnect1); const preconnect2 = document.createElement("link"); preconnect2.rel = "preconnect"; preconnect2.href = "https://fonts.gstatic.com"; preconnect2.crossOrigin = "anonymous"; document.head.appendChild(preconnect2); } // Load the Google Font stylesheet const link = document.createElement("link"); link.rel = "stylesheet"; link.href = url; document.head.appendChild(link); // Wait for the font to actually load before it gets applied if (document.fonts?.load) { document.fonts.load(`1em "${fontFamily}"`); } }; loadGoogleFont(headingGoogleFontsUrl, headingFontConfig.fontFamily); loadGoogleFont(bodyGoogleFontsUrl, bodyFontConfig.fontFamily); }, [headingGoogleFontsUrl, bodyGoogleFontsUrl, headingFontConfig.fontFamily, bodyFontConfig.fontFamily]); 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", ); // Set page width CSS variable el?.style.setProperty( "--page-width-setting", (pageWidth || 624).toString(), ); }, [ local, bgLeaflet, bgPage, primary, highlight1, highlight2, highlight3, accent1, accent2, accentContrast, pageWidth, ]); 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 ( getColorDifference( colorToString(b, "rgb"), colorToString(bgPage, "rgb"), ) - getColorDifference( 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, normalizedPublication } = 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}
); };