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 let showPageBackground = theme?.showPageBackground;
133 let pageWidth = theme?.pageWidth;
134
135 let primary = useColor(theme, "primary");
136
137 let accent1 = useColor(theme, "accentBackground");
138 let accent2 = useColor(theme, "accentText");
139
140 let highlight1 = useEntity(null, "theme/highlight-1")?.data.value;
141 let highlight2 = useColorAttribute(null, "theme/highlight-2");
142 let highlight3 = useColorAttribute(null, "theme/highlight-3");
143
144 return {
145 bgLeaflet,
146 bgPage,
147 primary,
148 accent1,
149 accent2,
150 highlight1,
151 highlight2,
152 highlight3,
153 showPageBackground,
154 pageWidth,
155 };
156};
157
158export const useLocalPubTheme = (
159 theme: PubLeafletPublication.Record["theme"] | undefined,
160 showPageBackground?: boolean,
161) => {
162 const pubTheme = usePubTheme(theme);
163 const [localOverrides, setTheme] = useState<Partial<typeof pubTheme>>({});
164
165 const mergedTheme = useMemo(() => {
166 let newTheme = {
167 ...pubTheme,
168 ...localOverrides,
169 showPageBackground,
170 };
171
172 return {
173 ...newTheme,
174 };
175 }, [pubTheme, localOverrides, showPageBackground]);
176 return {
177 theme: mergedTheme,
178 setTheme,
179 changes: Object.keys(localOverrides).length > 0,
180 };
181};