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