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