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 { getColorContrast } 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 primary = useColorAttribute(props.entityID, "theme/primary");
69
70 let highlight1 = useEntity(props.entityID, "theme/highlight-1");
71 let highlight2 = useColorAttribute(props.entityID, "theme/highlight-2");
72 let highlight3 = useColorAttribute(props.entityID, "theme/highlight-3");
73
74 let accent1 = useColorAttribute(props.entityID, "theme/accent-background");
75 let accent2 = useColorAttribute(props.entityID, "theme/accent-text");
76
77 return (
78 <CardBorderHiddenContext.Provider value={!!cardBorderHiddenValue}>
79 <BaseThemeProvider
80 local={props.local}
81 bgLeaflet={bgLeaflet}
82 bgPage={bgPage}
83 primary={primary}
84 highlight2={highlight2}
85 highlight3={highlight3}
86 highlight1={highlight1?.data.value}
87 accent1={accent1}
88 accent2={accent2}
89 showPageBackground={showPageBackground}
90 >
91 {props.children}
92 </BaseThemeProvider>
93 </CardBorderHiddenContext.Provider>
94 );
95}
96
97// handles setting all the Aria Color values to CSS Variables and wrapping the page the theme providers
98export const BaseThemeProvider = ({
99 local,
100 bgLeaflet,
101 bgPage,
102 primary,
103 accent1,
104 accent2,
105 highlight1,
106 highlight2,
107 highlight3,
108 showPageBackground,
109 children,
110}: {
111 local?: boolean;
112 showPageBackground?: boolean;
113 bgLeaflet: AriaColor;
114 bgPage: AriaColor;
115 primary: AriaColor;
116 accent1: AriaColor;
117 accent2: AriaColor;
118 highlight1?: string;
119 highlight2: AriaColor;
120 highlight3: AriaColor;
121 children: React.ReactNode;
122}) => {
123 // set accent contrast to the accent color that has the highest contrast with the page background
124 let accentContrast;
125
126 //sorting the accents by contrast on background
127 let sortedAccents = [accent1, accent2].sort((a, b) => {
128 return (
129 getColorContrast(
130 colorToString(b, "rgb"),
131 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
132 ) -
133 getColorContrast(
134 colorToString(a, "rgb"),
135 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
136 )
137 );
138 });
139
140 // if the contrast-y accent is too similar to the primary text color,
141 // and the not contrast-y option is different from the backgrond,
142 // then use the not contrasty option
143
144 if (
145 getColorContrast(
146 colorToString(sortedAccents[0], "rgb"),
147 colorToString(primary, "rgb"),
148 ) < 30 &&
149 getColorContrast(
150 colorToString(sortedAccents[1], "rgb"),
151 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
152 ) > 12
153 ) {
154 accentContrast = sortedAccents[1];
155 } else accentContrast = sortedAccents[0];
156
157 useEffect(() => {
158 if (local) return;
159 let el = document.querySelector(":root") as HTMLElement;
160 if (!el) return;
161 setCSSVariableToColor(el, "--bg-leaflet", bgLeaflet);
162 setCSSVariableToColor(el, "--bg-page", bgPage);
163 document.body.style.backgroundColor = `rgb(${colorToString(bgLeaflet, "rgb")})`;
164 document
165 .querySelector('meta[name="theme-color"]')
166 ?.setAttribute("content", `rgb(${colorToString(bgLeaflet, "rgb")})`);
167 el?.style.setProperty(
168 "--bg-page-alpha",
169 bgPage.getChannelValue("alpha").toString(),
170 );
171 setCSSVariableToColor(el, "--primary", primary);
172
173 setCSSVariableToColor(el, "--highlight-2", highlight2);
174 setCSSVariableToColor(el, "--highlight-3", highlight3);
175
176 //highlight 1 is special because its default value is a calculated value
177 if (highlight1) {
178 let color = parseColor(`hsba(${highlight1})`);
179 el?.style.setProperty(
180 "--highlight-1",
181 `rgb(${colorToString(color, "rgb")})`,
182 );
183 } else {
184 el?.style.setProperty(
185 "--highlight-1",
186 "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 75%)",
187 );
188 }
189 setCSSVariableToColor(el, "--accent-1", accent1);
190 setCSSVariableToColor(el, "--accent-2", accent2);
191 el?.style.setProperty(
192 "--accent-contrast",
193 colorToString(accentContrast, "rgb"),
194 );
195 el?.style.setProperty(
196 "--accent-1-is-contrast",
197 accentContrast === accent1 ? "1" : "0",
198 );
199 }, [
200 local,
201 bgLeaflet,
202 bgPage,
203 primary,
204 highlight1,
205 highlight2,
206 highlight3,
207 accent1,
208 accent2,
209 accentContrast,
210 ]);
211 return (
212 <div
213 className="leafletWrapper w-full text-primary h-full min-h-fit flex flex-col bg-center items-stretch "
214 style={
215 {
216 "--bg-leaflet": colorToString(bgLeaflet, "rgb"),
217 "--bg-page": colorToString(bgPage, "rgb"),
218 "--bg-page-alpha": bgPage.getChannelValue("alpha"),
219 "--primary": colorToString(primary, "rgb"),
220 "--accent-1": colorToString(accent1, "rgb"),
221 "--accent-2": colorToString(accent2, "rgb"),
222 "--accent-contrast": colorToString(accentContrast, "rgb"),
223 "--accent-1-is-contrast": accentContrast === accent1 ? 1 : 0,
224 "--highlight-1": highlight1
225 ? `rgb(${colorToString(parseColor(`hsba(${highlight1})`), "rgb")})`
226 : "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 75%)",
227 "--highlight-2": colorToString(highlight2, "rgb"),
228 "--highlight-3": colorToString(highlight3, "rgb"),
229 } as CSSProperties
230 }
231 >
232 {" "}
233 {children}{" "}
234 </div>
235 );
236};
237
238let CardThemeProviderContext = createContext<null | string>(null);
239export function NestedCardThemeProvider(props: { children: React.ReactNode }) {
240 let card = useContext(CardThemeProviderContext);
241 if (!card) return props.children;
242 return (
243 <CardThemeProvider entityID={card}>{props.children}</CardThemeProvider>
244 );
245}
246
247export function CardThemeProvider(props: {
248 entityID: string;
249 children: React.ReactNode;
250}) {
251 let bgPage = useColorAttributeNullable(
252 props.entityID,
253 "theme/card-background",
254 );
255 let primary = useColorAttributeNullable(props.entityID, "theme/primary");
256 let accent1 = useColorAttributeNullable(
257 props.entityID,
258 "theme/accent-background",
259 );
260 let accent2 = useColorAttributeNullable(props.entityID, "theme/accent-text");
261 let accentContrast =
262 bgPage && accent1 && accent2
263 ? [accent1, accent2].sort((a, b) => {
264 return (
265 getColorContrast(
266 colorToString(b, "rgb"),
267 colorToString(bgPage, "rgb"),
268 ) -
269 getColorContrast(
270 colorToString(a, "rgb"),
271 colorToString(bgPage, "rgb"),
272 )
273 );
274 })[0]
275 : null;
276
277 return (
278 <CardThemeProviderContext.Provider value={props.entityID}>
279 <div
280 className="contents text-primary"
281 style={
282 {
283 "--accent-1": accent1 ? colorToString(accent1, "rgb") : undefined,
284 "--accent-2": accent2 ? colorToString(accent2, "rgb") : undefined,
285 "--accent-contrast": accentContrast
286 ? colorToString(accentContrast, "rgb")
287 : undefined,
288 "--bg-page": bgPage ? colorToString(bgPage, "rgb") : undefined,
289 "--bg-page-alpha": bgPage
290 ? bgPage.getChannelValue("alpha")
291 : undefined,
292 "--primary": primary ? colorToString(primary, "rgb") : undefined,
293 } as CSSProperties
294 }
295 >
296 {props.children}
297 </div>
298 </CardThemeProviderContext.Provider>
299 );
300}
301
302// Wrapper within the Theme Wrapper that provides background image data
303export const ThemeBackgroundProvider = (props: {
304 entityID: string;
305 children: React.ReactNode;
306}) => {
307 let { data: pub } = useLeafletPublicationData();
308 let backgroundImage = useEntity(props.entityID, "theme/background-image");
309 let backgroundImageRepeat = useEntity(
310 props.entityID,
311 "theme/background-image-repeat",
312 );
313 if (pub?.publications) {
314 return (
315 <PublicationBackgroundProvider
316 pub_creator={pub?.publications.identity_did || ""}
317 theme={
318 (pub.publications?.record as PubLeafletPublication.Record)?.theme
319 }
320 >
321 {props.children}
322 </PublicationBackgroundProvider>
323 );
324 }
325 return (
326 <div
327 className="LeafletBackgroundWrapper w-full bg-bg-leaflet text-primary h-full flex flex-col bg-cover bg-center bg-no-repeat items-stretch"
328 style={
329 {
330 backgroundImage: backgroundImage
331 ? `url(${backgroundImage?.data.src}), url(${backgroundImage?.data.fallback})`
332 : undefined,
333 backgroundPosition: "center",
334 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat",
335 backgroundSize: !backgroundImageRepeat
336 ? "cover"
337 : backgroundImageRepeat?.data.value,
338 } as CSSProperties
339 }
340 >
341 {props.children}
342 </div>
343 );
344};