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