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