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}) {
51 let { data } = usePublicationData();
52 let { publication: pub } = data || {};
53 return (
54 <PublicationThemeProvider
55 pub_creator={pub?.identity_did || ""}
56 record={pub?.record as PubLeafletPublication.Record}
57 >
58 <PublicationBackgroundProvider
59 record={pub?.record as PubLeafletPublication.Record}
60 pub_creator={pub?.identity_did || ""}
61 >
62 {props.children}
63 </PublicationBackgroundProvider>
64 </PublicationThemeProvider>
65 );
66}
67
68export function PublicationBackgroundProvider(props: {
69 record?: PubLeafletPublication.Record | null;
70 pub_creator: string;
71 className?: string;
72 children: React.ReactNode;
73}) {
74 let backgroundImage = props.record?.theme?.backgroundImage?.image?.ref
75 ? blobRefToSrc(
76 props.record?.theme?.backgroundImage?.image?.ref,
77 props.pub_creator,
78 )
79 : null;
80
81 let backgroundImageRepeat = props.record?.theme?.backgroundImage?.repeat;
82 let backgroundImageSize = props.record?.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 record?: PubLeafletPublication.Record | null;
100 pub_creator: string;
101}) {
102 let colors = usePubTheme(props.record);
103 return (
104 <BaseThemeProvider local={props.local} {...colors}>
105 {props.children}
106 </BaseThemeProvider>
107 );
108}
109
110export const usePubTheme = (record?: PubLeafletPublication.Record | null) => {
111 let bgLeaflet = useColor(record, "backgroundColor");
112 let bgPage = useColor(record, "pageBackground");
113 bgPage = record?.theme?.pageBackground ? bgPage : bgLeaflet;
114 let showPageBackground = record?.theme?.showPageBackground;
115
116 let primary = useColor(record, "primary");
117
118 let accent1 = useColor(record, "accentBackground");
119 let accent2 = useColor(record, "accentText");
120
121 let highlight1 = useEntity(null, "theme/highlight-1")?.data.value;
122 let highlight2 = useColorAttribute(null, "theme/highlight-2");
123 let highlight3 = useColorAttribute(null, "theme/highlight-3");
124
125 return {
126 bgLeaflet,
127 bgPage,
128 primary,
129 accent1,
130 accent2,
131 highlight1,
132 highlight2,
133 highlight3,
134 showPageBackground,
135 };
136};
137
138export const useLocalPubTheme = (
139 record: PubLeafletPublication.Record | undefined,
140 showPageBackground?: boolean,
141) => {
142 const pubTheme = usePubTheme(record);
143 const [localOverrides, setTheme] = useState<Partial<typeof pubTheme>>({});
144
145 const mergedTheme = useMemo(() => {
146 let newTheme = {
147 ...pubTheme,
148 ...localOverrides,
149 showPageBackground,
150 };
151 let newAccentContrast;
152 let sortedAccents = [newTheme.accent1, newTheme.accent2].sort((a, b) => {
153 return (
154 getColorContrast(
155 colorToString(b, "rgb"),
156 colorToString(
157 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet,
158 "rgb",
159 ),
160 ) -
161 getColorContrast(
162 colorToString(a, "rgb"),
163 colorToString(
164 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet,
165 "rgb",
166 ),
167 )
168 );
169 });
170 if (
171 getColorContrast(
172 colorToString(sortedAccents[0], "rgb"),
173 colorToString(newTheme.primary, "rgb"),
174 ) < 30 &&
175 getColorContrast(
176 colorToString(sortedAccents[1], "rgb"),
177 colorToString(
178 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet,
179 "rgb",
180 ),
181 ) > 12
182 ) {
183 newAccentContrast = sortedAccents[1];
184 } else newAccentContrast = sortedAccents[0];
185 return {
186 ...newTheme,
187 accentContrast: newAccentContrast,
188 };
189 }, [pubTheme, localOverrides, showPageBackground]);
190 return {
191 theme: mergedTheme,
192 setTheme,
193 changes: Object.keys(localOverrides).length > 0,
194 };
195};