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