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