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