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