a tool for shared writing and social publishing
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