a tool for shared writing and social publishing
at update/looseleafs 205 lines 6.3 kB view raw
1"use client"; 2import { useMemo, useState } from "react"; 3import { parseColor } from "react-aria-components"; 4import { useEntity } from "src/replicache"; 5import { getColorContrast } from "./ThemeProvider"; 6import { useColorAttribute, colorToString } from "./useColorAttribute"; 7import { BaseThemeProvider } from "./ThemeProvider"; 8import { PubLeafletPublication, PubLeafletThemeColor } from "lexicons/api"; 9import { usePublicationData } from "app/lish/[did]/[publication]/dashboard/PublicationSWRProvider"; 10import { blobRefToSrc } from "src/utils/blobRefToSrc"; 11 12const PubThemeDefaults = { 13 backgroundColor: "#FDFCFA", 14 pageBackground: "#FDFCFA", 15 primary: "#272727", 16 accentText: "#FFFFFF", 17 accentBackground: "#0000FF", 18}; 19 20// Default page background for standalone leaflets (matches editor default) 21const StandalonePageBackground = "#FFFFFF"; 22function parseThemeColor( 23 c: PubLeafletThemeColor.Rgb | PubLeafletThemeColor.Rgba, 24) { 25 if (c.$type === "pub.leaflet.theme.color#rgba") { 26 return parseColor(`rgba(${c.r}, ${c.g}, ${c.b}, ${c.a / 100})`); 27 } 28 return parseColor(`rgb(${c.r}, ${c.g}, ${c.b})`); 29} 30 31let useColor = ( 32 theme: PubLeafletPublication.Record["theme"] | null | undefined, 33 c: keyof typeof PubThemeDefaults, 34) => { 35 return useMemo(() => { 36 let v = theme?.[c]; 37 if (isColor(v)) { 38 return parseThemeColor(v); 39 } else return parseColor(PubThemeDefaults[c]); 40 }, [theme?.[c]]); 41}; 42let isColor = ( 43 c: any, 44): c is PubLeafletThemeColor.Rgb | PubLeafletThemeColor.Rgba => { 45 return ( 46 c?.$type === "pub.leaflet.theme.color#rgb" || 47 c?.$type === "pub.leaflet.theme.color#rgba" 48 ); 49}; 50 51export function PublicationThemeProviderDashboard(props: { 52 children: React.ReactNode; 53}) { 54 let { data } = usePublicationData(); 55 let { publication: pub } = data || {}; 56 return ( 57 <PublicationThemeProvider 58 pub_creator={pub?.identity_did || ""} 59 theme={(pub?.record as PubLeafletPublication.Record)?.theme} 60 > 61 <PublicationBackgroundProvider 62 theme={(pub?.record as PubLeafletPublication.Record)?.theme} 63 pub_creator={pub?.identity_did || ""} 64 > 65 {props.children} 66 </PublicationBackgroundProvider> 67 </PublicationThemeProvider> 68 ); 69} 70 71export function PublicationBackgroundProvider(props: { 72 theme?: PubLeafletPublication.Record["theme"] | null; 73 pub_creator: string; 74 className?: string; 75 children: React.ReactNode; 76}) { 77 let backgroundImage = props.theme?.backgroundImage?.image?.ref 78 ? blobRefToSrc(props.theme?.backgroundImage?.image?.ref, props.pub_creator) 79 : null; 80 81 let backgroundImageRepeat = props.theme?.backgroundImage?.repeat; 82 let backgroundImageSize = props.theme?.backgroundImage?.width || 500; 83 return ( 84 <div 85 className="PubBackgroundWrapper w-full bg-bg-leaflet text-primary h-full flex flex-col bg-cover bg-center bg-no-repeat items-stretch" 86 style={{ 87 backgroundImage: `url(${backgroundImage})`, 88 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 89 backgroundSize: `${backgroundImageRepeat ? `${backgroundImageSize}px` : "cover"}`, 90 }} 91 > 92 {props.children} 93 </div> 94 ); 95} 96export function PublicationThemeProvider(props: { 97 local?: boolean; 98 children: React.ReactNode; 99 theme?: PubLeafletPublication.Record["theme"] | null; 100 pub_creator: string; 101 isStandalone?: boolean; 102}) { 103 let colors = usePubTheme(props.theme, props.isStandalone); 104 return ( 105 <BaseThemeProvider local={props.local} {...colors}> 106 {props.children} 107 </BaseThemeProvider> 108 ); 109} 110 111export const usePubTheme = ( 112 theme?: PubLeafletPublication.Record["theme"] | null, 113 isStandalone?: boolean, 114) => { 115 let bgLeaflet = useColor(theme, "backgroundColor"); 116 let bgPage = useColor(theme, "pageBackground"); 117 // For standalone documents, use the editor default page background (#FFFFFF) 118 // For publications without explicit pageBackground, use bgLeaflet 119 if (isStandalone && !theme?.pageBackground) { 120 bgPage = parseColor(StandalonePageBackground); 121 } else if (theme && !theme.pageBackground) { 122 bgPage = bgLeaflet; 123 } 124 let showPageBackground = theme?.showPageBackground; 125 126 let primary = useColor(theme, "primary"); 127 128 let accent1 = useColor(theme, "accentBackground"); 129 let accent2 = useColor(theme, "accentText"); 130 131 let highlight1 = useEntity(null, "theme/highlight-1")?.data.value; 132 let highlight2 = useColorAttribute(null, "theme/highlight-2"); 133 let highlight3 = useColorAttribute(null, "theme/highlight-3"); 134 135 return { 136 bgLeaflet, 137 bgPage, 138 primary, 139 accent1, 140 accent2, 141 highlight1, 142 highlight2, 143 highlight3, 144 showPageBackground, 145 }; 146}; 147 148export const useLocalPubTheme = ( 149 theme: PubLeafletPublication.Record["theme"] | undefined, 150 showPageBackground?: boolean, 151) => { 152 const pubTheme = usePubTheme(theme); 153 const [localOverrides, setTheme] = useState<Partial<typeof pubTheme>>({}); 154 155 const mergedTheme = useMemo(() => { 156 let newTheme = { 157 ...pubTheme, 158 ...localOverrides, 159 showPageBackground, 160 }; 161 let newAccentContrast; 162 let sortedAccents = [newTheme.accent1, newTheme.accent2].sort((a, b) => { 163 return ( 164 getColorContrast( 165 colorToString(b, "rgb"), 166 colorToString( 167 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 168 "rgb", 169 ), 170 ) - 171 getColorContrast( 172 colorToString(a, "rgb"), 173 colorToString( 174 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 175 "rgb", 176 ), 177 ) 178 ); 179 }); 180 if ( 181 getColorContrast( 182 colorToString(sortedAccents[0], "rgb"), 183 colorToString(newTheme.primary, "rgb"), 184 ) < 30 && 185 getColorContrast( 186 colorToString(sortedAccents[1], "rgb"), 187 colorToString( 188 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 189 "rgb", 190 ), 191 ) > 12 192 ) { 193 newAccentContrast = sortedAccents[1]; 194 } else newAccentContrast = sortedAccents[0]; 195 return { 196 ...newTheme, 197 accentContrast: newAccentContrast, 198 }; 199 }, [pubTheme, localOverrides, showPageBackground]); 200 return { 201 theme: mergedTheme, 202 setTheme, 203 changes: Object.keys(localOverrides).length > 0, 204 }; 205};