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