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