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