a tool for shared writing and social publishing
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 { getColorDifference } 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
138 let accentContrast;
139 let sortedAccents = [accent1, accent2].sort((a, b) => {
140 // sort accents by contrast against the background
141 return (
142 getColorDifference(
143 colorToString(b, "rgb"),
144 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
145 ) -
146 getColorDifference(
147 colorToString(a, "rgb"),
148 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
149 )
150 );
151 });
152 if (
153 // if the contrast-y accent is too similar to text color
154 getColorDifference(
155 colorToString(sortedAccents[0], "rgb"),
156 colorToString(primary, "rgb"),
157 ) < 0.15 &&
158 // and if the other accent is different enough from the background
159 getColorDifference(
160 colorToString(sortedAccents[1], "rgb"),
161 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
162 ) > 0.31
163 ) {
164 //then choose the less contrast-y accent
165 accentContrast = sortedAccents[1];
166 } else {
167 // otherwise, choose the more contrast-y option
168 accentContrast = sortedAccents[0];
169 }
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 getColorDifference(
290 colorToString(b, "rgb"),
291 colorToString(bgPage, "rgb"),
292 ) -
293 getColorDifference(
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};