"use client"; import { MotionConfig } from "framer-motion"; import * as React from "react"; /** * The provider for Coline Sites (SDK 2.1). A site has no Coline host to * hand it a theme, so this follows the visitor's light or dark preference * (or a fixed `theme`) by toggling `.dark` on the root element, which is * how every component's tokens switch. Wrap the site's page in it once, * instead of `ColineAppProvider`, which only works inside Coline. * * `accent` is the one part of Coline's look a site may change: the color * of selection, radio choices, the selected date, steps, info badges and * the first chart series (the `--action` token, `bg-action` / `text-action` * in Tailwind). * Everything else stays Coline's. */ export type ColineSiteTheme = "system" | "light" | "dark"; /** * Any CSS color. One color is used as given in light mode and lifted for * dark mode the way Coline lifts its own blue; pass both to choose each. */ export type ColineSiteAccent = string | { light: string; dark: string }; const SiteColorSchemeContext = React.createContext<"light" | "dark">("light"); function systemScheme(): "light" | "dark" { return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } function isColor(value: string): boolean { return typeof CSS !== "undefined" && CSS.supports("color", value); } function accentColor(accent: ColineSiteAccent, scheme: "light" | "dark"): string | null { const value = typeof accent === "string" ? accent : accent[scheme]; if (!isColor(value)) { console.warn(`ColineSiteProvider: accent "${value}" is not a CSS color; using Coline's.`); return null; } if (typeof accent !== "string" || scheme === "light") return value; // Coline's blue is oklch(0.6 0.19 255) in light and oklch(0.7 0.15 255) in dark. const lifted = `oklch(from ${value} max(l, 0.7) min(c, 0.15) h)`; return isColor(lifted) ? lifted : value; } export function ColineSiteProvider({ theme = "system", accent, children, }: { /** `system` (the default) follows the visitor's device and changes with it. */ theme?: ColineSiteTheme; /** The site's one accent color. Leave it out to use Coline's blue. */ accent?: ColineSiteAccent; children: React.ReactNode; }) { const [system, setSystem] = React.useState<"light" | "dark">(systemScheme); React.useEffect(() => { if (theme !== "system") return; const media = window.matchMedia("(prefers-color-scheme: dark)"); const update = () => setSystem(media.matches ? "dark" : "light"); update(); media.addEventListener("change", update); return () => media.removeEventListener("change", update); }, [theme]); const scheme = theme === "system" ? system : theme; React.useLayoutEffect(() => { const root = document.documentElement; root.classList.toggle("dark", scheme === "dark"); root.style.colorScheme = scheme; }, [scheme]); const light = accent === undefined ? undefined : typeof accent === "string" ? accent : accent.light; const dark = accent === undefined || typeof accent === "string" ? undefined : accent.dark; React.useLayoutEffect(() => { if (light === undefined) return; const color = accentColor(dark === undefined ? light : { light, dark }, scheme); if (color === null) return; const root = document.documentElement; root.style.setProperty("--action", color); return () => { root.style.removeProperty("--action"); }; }, [light, dark, scheme]); return ( {children} ); } /** The light or dark scheme a site is showing. */ export function useSiteColorScheme(): "light" | "dark" { return React.useContext(SiteColorSchemeContext); }