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 colors = usePubTheme(props.theme, props.isStandalone);
106 let cardBorderHidden = !colors.showPageBackground;
107 return (
108 <CardBorderHiddenContext.Provider value={cardBorderHidden}>
109 <BaseThemeProvider local={props.local} {...colors}>
110 {props.children}
111 </BaseThemeProvider>
112 </CardBorderHiddenContext.Provider>
113 );
114}
115
116export const usePubTheme = (
117 theme?: PubLeafletPublication.Record["theme"] | null,
118 isStandalone?: boolean,
119) => {
120 let bgLeaflet = useColor(theme, "backgroundColor");
121 let bgPage = useColor(theme, "pageBackground");
122 // For standalone documents, use the editor default page background (#FFFFFF)
123 // For publications without explicit pageBackground, use bgLeaflet
124 if (isStandalone && !theme?.pageBackground) {
125 bgPage = parseColor(StandalonePageBackground);
126 } else if (theme && !theme.pageBackground) {
127 bgPage = bgLeaflet;
128 }
129 let showPageBackground = theme?.showPageBackground;
130
131 let primary = useColor(theme, "primary");
132
133 let accent1 = useColor(theme, "accentBackground");
134 let accent2 = useColor(theme, "accentText");
135
136 let highlight1 = useEntity(null, "theme/highlight-1")?.data.value;
137 let highlight2 = useColorAttribute(null, "theme/highlight-2");
138 let highlight3 = useColorAttribute(null, "theme/highlight-3");
139
140 return {
141 bgLeaflet,
142 bgPage,
143 primary,
144 accent1,
145 accent2,
146 highlight1,
147 highlight2,
148 highlight3,
149 showPageBackground,
150 };
151};
152
153export const useLocalPubTheme = (
154 theme: PubLeafletPublication.Record["theme"] | undefined,
155 showPageBackground?: boolean,
156) => {
157 const pubTheme = usePubTheme(theme);
158 const [localOverrides, setTheme] = useState<Partial<typeof pubTheme>>({});
159
160 const mergedTheme = useMemo(() => {
161 let newTheme = {
162 ...pubTheme,
163 ...localOverrides,
164 showPageBackground,
165 };
166 let newAccentContrast;
167 let sortedAccents = [newTheme.accent1, newTheme.accent2].sort((a, b) => {
168 return (
169 getColorContrast(
170 colorToString(b, "rgb"),
171 colorToString(
172 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet,
173 "rgb",
174 ),
175 ) -
176 getColorContrast(
177 colorToString(a, "rgb"),
178 colorToString(
179 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet,
180 "rgb",
181 ),
182 )
183 );
184 });
185 if (
186 getColorContrast(
187 colorToString(sortedAccents[0], "rgb"),
188 colorToString(newTheme.primary, "rgb"),
189 ) < 30 &&
190 getColorContrast(
191 colorToString(sortedAccents[1], "rgb"),
192 colorToString(
193 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet,
194 "rgb",
195 ),
196 ) > 12
197 ) {
198 newAccentContrast = sortedAccents[1];
199 } else newAccentContrast = sortedAccents[0];
200 return {
201 ...newTheme,
202 accentContrast: newAccentContrast,
203 };
204 }, [pubTheme, localOverrides, showPageBackground]);
205 return {
206 theme: mergedTheme,
207 setTheme,
208 changes: Object.keys(localOverrides).length > 0,
209 };
210};