a tool for shared writing and social publishing
at debug/datetime 377 lines 12 kB view raw
1"use client"; 2 3import { 4 createContext, 5 CSSProperties, 6 useContext, 7 useEffect, 8 useMemo, 9 useState, 10} from "react"; 11import { 12 colorToString, 13 useColorAttribute, 14 useColorAttributeNullable, 15} from "./useColorAttribute"; 16import { Color as AriaColor, parseColor } from "react-aria-components"; 17import { parse, contrastLstar, ColorSpace, sRGB } from "colorjs.io/fn"; 18 19import { useEntity } from "src/replicache"; 20import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 21import { 22 PublicationBackgroundProvider, 23 PublicationThemeProvider, 24} from "./PublicationThemeProvider"; 25import { PubLeafletPublication } from "lexicons/api"; 26 27type CSSVariables = { 28 "--bg-leaflet": string; 29 "--bg-page": string; 30 "--primary": string; 31 "--accent-1": string; 32 "--accent-2": string; 33 "--accent-contrast": string; 34 "--highlight-1": string; 35 "--highlight-2": string; 36 "--highlight-3": string; 37}; 38 39// define the color defaults for everything 40export const ThemeDefaults = { 41 "theme/page-background": "#FDFCFA", 42 "theme/card-background": "#FFFFFF", 43 "theme/primary": "#272727", 44 "theme/highlight-1": "#FFFFFF", 45 "theme/highlight-2": "#EDD280", 46 "theme/highlight-3": "#FFCDC3", 47 48 //everywhere else, accent-background = accent-1 and accent-text = accent-2. 49 // we just need to create a migration pipeline before we can change this 50 "theme/accent-text": "#FFFFFF", 51 "theme/accent-background": "#0000FF", 52 "theme/accent-contrast": "#0000FF", 53}; 54 55// define a function to set an Aria Color to a CSS Variable in RGB 56function setCSSVariableToColor( 57 el: HTMLElement, 58 name: string, 59 value: AriaColor, 60) { 61 el?.style.setProperty(name, colorToString(value, "rgb")); 62} 63 64//Create a wrapper that applies a theme to each page 65export function ThemeProvider(props: { 66 entityID: string | null; 67 local?: boolean; 68 children: React.ReactNode; 69 className?: string; 70}) { 71 let { data: pub } = useLeafletPublicationData(); 72 if (!pub || !pub.publications) return <LeafletThemeProvider {...props} />; 73 return ( 74 <PublicationThemeProvider 75 {...props} 76 record={pub.publications?.record as PubLeafletPublication.Record} 77 pub_creator={pub.publications?.identity_did} 78 /> 79 ); 80} 81// for PUBLICATIONS: define Aria Colors for each value and use BaseThemeProvider to wrap the content of the page in the theme 82 83// for LEAFLETS : define Aria Colors for each value and use BaseThemeProvider to wrap the content of the page in the theme 84export function LeafletThemeProvider(props: { 85 entityID: string | null; 86 local?: boolean; 87 children: React.ReactNode; 88}) { 89 let bgLeaflet = useColorAttribute(props.entityID, "theme/page-background"); 90 let bgPage = useColorAttribute(props.entityID, "theme/card-background"); 91 let showPageBackground = !useEntity( 92 props.entityID, 93 "theme/card-border-hidden", 94 )?.data.value; 95 let primary = useColorAttribute(props.entityID, "theme/primary"); 96 97 let highlight1 = useEntity(props.entityID, "theme/highlight-1"); 98 let highlight2 = useColorAttribute(props.entityID, "theme/highlight-2"); 99 let highlight3 = useColorAttribute(props.entityID, "theme/highlight-3"); 100 101 let accent1 = useColorAttribute(props.entityID, "theme/accent-background"); 102 let accent2 = useColorAttribute(props.entityID, "theme/accent-text"); 103 104 return ( 105 <BaseThemeProvider 106 local={props.local} 107 bgLeaflet={bgLeaflet} 108 bgPage={bgPage} 109 primary={primary} 110 highlight2={highlight2} 111 highlight3={highlight3} 112 highlight1={highlight1?.data.value} 113 accent1={accent1} 114 accent2={accent2} 115 showPageBackground={showPageBackground} 116 > 117 {props.children} 118 </BaseThemeProvider> 119 ); 120} 121 122// handles setting all the Aria Color values to CSS Variables and wrapping the page the theme providers 123export const BaseThemeProvider = ({ 124 local, 125 bgLeaflet, 126 bgPage, 127 primary, 128 accent1, 129 accent2, 130 highlight1, 131 highlight2, 132 highlight3, 133 showPageBackground, 134 children, 135}: { 136 local?: boolean; 137 showPageBackground?: boolean; 138 bgLeaflet: AriaColor; 139 bgPage: AriaColor; 140 primary: AriaColor; 141 accent1: AriaColor; 142 accent2: AriaColor; 143 highlight1?: string; 144 highlight2: AriaColor; 145 highlight3: AriaColor; 146 children: React.ReactNode; 147}) => { 148 // set accent contrast to the accent color that has the highest contrast with the page background 149 let accentContrast; 150 151 //sorting the accents by contrast on background 152 let sortedAccents = [accent1, accent2].sort((a, b) => { 153 return ( 154 getColorContrast( 155 colorToString(b, "rgb"), 156 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 157 ) - 158 getColorContrast( 159 colorToString(a, "rgb"), 160 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 161 ) 162 ); 163 }); 164 165 // if the contrast-y accent is too similar to the primary text color, 166 // and the not contrast-y option is different from the backgrond, 167 // then use the not contrasty option 168 169 if ( 170 getColorContrast( 171 colorToString(sortedAccents[0], "rgb"), 172 colorToString(primary, "rgb"), 173 ) < 30 && 174 getColorContrast( 175 colorToString(sortedAccents[1], "rgb"), 176 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 177 ) > 12 178 ) { 179 accentContrast = sortedAccents[1]; 180 } else accentContrast = sortedAccents[0]; 181 182 useEffect(() => { 183 if (local) return; 184 let el = document.querySelector(":root") as HTMLElement; 185 if (!el) return; 186 setCSSVariableToColor(el, "--bg-leaflet", bgLeaflet); 187 setCSSVariableToColor(el, "--bg-page", bgPage); 188 document.body.style.backgroundColor = `rgb(${colorToString(bgLeaflet, "rgb")})`; 189 document 190 .querySelector('meta[name="theme-color"]') 191 ?.setAttribute("content", `rgb(${colorToString(bgLeaflet, "rgb")})`); 192 el?.style.setProperty( 193 "--bg-page-alpha", 194 bgPage.getChannelValue("alpha").toString(), 195 ); 196 setCSSVariableToColor(el, "--primary", primary); 197 198 setCSSVariableToColor(el, "--highlight-2", highlight2); 199 setCSSVariableToColor(el, "--highlight-3", highlight3); 200 201 //highlight 1 is special because its default value is a calculated value 202 if (highlight1) { 203 let color = parseColor(`hsba(${highlight1})`); 204 el?.style.setProperty( 205 "--highlight-1", 206 `rgb(${colorToString(color, "rgb")})`, 207 ); 208 } else { 209 el?.style.setProperty( 210 "--highlight-1", 211 "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 75%)", 212 ); 213 } 214 setCSSVariableToColor(el, "--accent-1", accent1); 215 setCSSVariableToColor(el, "--accent-2", accent2); 216 el?.style.setProperty( 217 "--accent-contrast", 218 colorToString(accentContrast, "rgb"), 219 ); 220 el?.style.setProperty( 221 "--accent-1-is-contrast", 222 accentContrast === accent1 ? "1" : "0", 223 ); 224 }, [ 225 local, 226 bgLeaflet, 227 bgPage, 228 primary, 229 highlight1, 230 highlight2, 231 highlight3, 232 accent1, 233 accent2, 234 accentContrast, 235 ]); 236 return ( 237 <div 238 className="leafletWrapper w-full text-primary h-full min-h-fit flex flex-col bg-center items-stretch " 239 style={ 240 { 241 "--bg-leaflet": colorToString(bgLeaflet, "rgb"), 242 "--bg-page": colorToString(bgPage, "rgb"), 243 "--bg-page-alpha": bgPage.getChannelValue("alpha"), 244 "--primary": colorToString(primary, "rgb"), 245 "--accent-1": colorToString(accent1, "rgb"), 246 "--accent-2": colorToString(accent2, "rgb"), 247 "--accent-contrast": colorToString(accentContrast, "rgb"), 248 "--accent-1-is-contrast": accentContrast === accent1 ? 1 : 0, 249 "--highlight-1": highlight1 250 ? `rgb(${colorToString(parseColor(`hsba(${highlight1})`), "rgb")})` 251 : "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 75%)", 252 "--highlight-2": colorToString(highlight2, "rgb"), 253 "--highlight-3": colorToString(highlight3, "rgb"), 254 } as CSSProperties 255 } 256 > 257 {" "} 258 {children}{" "} 259 </div> 260 ); 261}; 262 263let CardThemeProviderContext = createContext<null | string>(null); 264export function NestedCardThemeProvider(props: { children: React.ReactNode }) { 265 let card = useContext(CardThemeProviderContext); 266 if (!card) return props.children; 267 return ( 268 <CardThemeProvider entityID={card}>{props.children}</CardThemeProvider> 269 ); 270} 271 272export function CardThemeProvider(props: { 273 entityID: string; 274 children: React.ReactNode; 275}) { 276 let bgPage = useColorAttributeNullable( 277 props.entityID, 278 "theme/card-background", 279 ); 280 let primary = useColorAttributeNullable(props.entityID, "theme/primary"); 281 let accent1 = useColorAttributeNullable( 282 props.entityID, 283 "theme/accent-background", 284 ); 285 let accent2 = useColorAttributeNullable(props.entityID, "theme/accent-text"); 286 let accentContrast = 287 bgPage && accent1 && accent2 288 ? [accent1, accent2].sort((a, b) => { 289 return ( 290 getColorContrast( 291 colorToString(b, "rgb"), 292 colorToString(bgPage, "rgb"), 293 ) - 294 getColorContrast( 295 colorToString(a, "rgb"), 296 colorToString(bgPage, "rgb"), 297 ) 298 ); 299 })[0] 300 : null; 301 302 return ( 303 <CardThemeProviderContext.Provider value={props.entityID}> 304 <div 305 className="contents text-primary" 306 style={ 307 { 308 "--accent-1": accent1 ? colorToString(accent1, "rgb") : undefined, 309 "--accent-2": accent2 ? colorToString(accent2, "rgb") : undefined, 310 "--accent-contrast": accentContrast 311 ? colorToString(accentContrast, "rgb") 312 : undefined, 313 "--bg-page": bgPage ? colorToString(bgPage, "rgb") : undefined, 314 "--bg-page-alpha": bgPage 315 ? bgPage.getChannelValue("alpha") 316 : undefined, 317 "--primary": primary ? colorToString(primary, "rgb") : undefined, 318 } as CSSProperties 319 } 320 > 321 {props.children} 322 </div> 323 </CardThemeProviderContext.Provider> 324 ); 325} 326 327// Wrapper within the Theme Wrapper that provides background image data 328export const ThemeBackgroundProvider = (props: { 329 entityID: string; 330 children: React.ReactNode; 331}) => { 332 let { data: pub } = useLeafletPublicationData(); 333 let backgroundImage = useEntity(props.entityID, "theme/background-image"); 334 let backgroundImageRepeat = useEntity( 335 props.entityID, 336 "theme/background-image-repeat", 337 ); 338 if (pub?.publications) { 339 return ( 340 <PublicationBackgroundProvider 341 pub_creator={pub?.publications.identity_did || ""} 342 record={pub?.publications.record as PubLeafletPublication.Record} 343 > 344 {props.children} 345 </PublicationBackgroundProvider> 346 ); 347 } 348 return ( 349 <div 350 className="LeafletBackgroundWrapper w-full bg-bg-leaflet text-primary h-full flex flex-col bg-cover bg-center bg-no-repeat items-stretch" 351 style={ 352 { 353 backgroundImage: backgroundImage 354 ? `url(${backgroundImage?.data.src}), url(${backgroundImage?.data.fallback})` 355 : undefined, 356 backgroundPosition: "center", 357 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 358 backgroundSize: !backgroundImageRepeat 359 ? "cover" 360 : backgroundImageRepeat?.data.value, 361 } as CSSProperties 362 } 363 > 364 {props.children} 365 </div> 366 ); 367}; 368 369// used to calculate the contrast between page and accent1, accent2, and determin which is higher contrast 370export function getColorContrast(color1: string, color2: string) { 371 ColorSpace.register(sRGB); 372 373 let parsedColor1 = parse(`rgb(${color1})`); 374 let parsedColor2 = parse(`rgb(${color2})`); 375 376 return contrastLstar(parsedColor1, parsedColor2); 377}