a tool for shared writing and social publishing
1"use client";
2
3import {
4 ColorPicker as SpectrumColorPicker,
5 parseColor,
6 Color,
7 ColorThumb,
8 ColorSlider,
9 Input,
10 ColorField,
11 SliderTrack,
12 ColorSwatch,
13} from "react-aria-components";
14import { Checkbox } from "components/Checkbox";
15import { useMemo, useState } from "react";
16import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache";
17import { useColorAttribute } from "components/ThemeManager/useColorAttribute";
18import { Separator } from "components/Layout";
19import { onMouseDown } from "src/utils/iosInputMouseDown";
20import { pickers, setColorAttribute } from "../ThemeSetter";
21import { ImageInput, ImageSettings } from "./ImagePicker";
22
23import { ColorPicker, thumbStyle } from "./ColorPicker";
24import { BlockImageSmall } from "components/Icons/BlockImageSmall";
25import { Replicache } from "replicache";
26import { CanvasBackgroundPattern } from "components/Canvas";
27import { Toggle } from "components/Toggle";
28import { DeleteSmall } from "components/Icons/DeleteSmall";
29
30export const PageThemePickers = (props: {
31 entityID: string;
32 openPicker: pickers;
33 setOpenPicker: (thisPicker: pickers) => void;
34}) => {
35 let { rep } = useReplicache();
36 let set = useMemo(() => {
37 return setColorAttribute(rep, props.entityID);
38 }, [rep, props.entityID]);
39
40 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc";
41 let primaryValue = useColorAttribute(props.entityID, "theme/primary");
42
43 return (
44 <div
45 className="pageThemeBG flex flex-col gap-2 h-full text-primary bg-bg-leaflet p-2 rounded-md border border-primary shadow-[0_0_0_1px_rgb(var(--bg-page))]"
46 style={{ backgroundColor: "rgba(var(--bg-page), 0.6)" }}
47 >
48 {pageType === "canvas" && (
49 <>
50 <CanvasBGPatternPicker entityID={props.entityID} rep={rep} />{" "}
51 <hr className="border-border-light w-full" />
52 </>
53 )}
54 <PageTextPicker
55 value={primaryValue}
56 setValue={set("theme/primary")}
57 openPicker={props.openPicker}
58 setOpenPicker={props.setOpenPicker}
59 />
60 </div>
61 );
62};
63
64export const PageBackgroundPicker = (props: {
65 entityID: string;
66 setValue: (c: Color) => void;
67 openPicker: pickers;
68 setOpenPicker: (p: pickers) => void;
69 home?: boolean;
70}) => {
71 let pageValue = useColorAttribute(props.entityID, "theme/card-background");
72 let pageBGImage = useEntity(props.entityID, "theme/card-background-image");
73 let pageBorderHidden = useEntity(props.entityID, "theme/card-border-hidden");
74
75 return (
76 <>
77 {pageBGImage && pageBGImage !== null && (
78 <PageBackgroundImagePicker
79 disabled={pageBorderHidden?.data.value}
80 entityID={props.entityID}
81 thisPicker={"page-background-image"}
82 openPicker={props.openPicker}
83 setOpenPicker={props.setOpenPicker}
84 closePicker={() => props.setOpenPicker("null")}
85 setValue={props.setValue}
86 home={props.home}
87 />
88 )}
89 <div className="relative">
90 <PageBackgroundColorPicker
91 label={pageBorderHidden?.data.value ? "Menus" : "Page"}
92 value={pageValue}
93 setValue={props.setValue}
94 thisPicker={"page"}
95 openPicker={props.openPicker}
96 setOpenPicker={props.setOpenPicker}
97 alpha
98 />
99 {(pageBGImage === null ||
100 (!pageBGImage && !pageBorderHidden?.data.value && !props.home)) && (
101 <label
102 className={`
103 hover:cursor-pointer text-[#969696] shrink-0
104 absolute top-0 right-0
105 `}
106 >
107 <BlockImageSmall />
108 <div className="hidden">
109 <ImageInput
110 entityID={props.entityID}
111 onChange={() => props.setOpenPicker("page-background-image")}
112 card
113 />
114 </div>
115 </label>
116 )}
117 </div>
118 </>
119 );
120};
121
122export const PageBackgroundColorPicker = (props: {
123 disabled?: boolean;
124 label: string;
125 openPicker: pickers;
126 thisPicker: pickers;
127 setOpenPicker: (thisPicker: pickers) => void;
128 setValue: (c: Color) => void;
129 value: Color;
130 alpha?: boolean;
131}) => {
132 return (
133 <ColorPicker
134 disabled={props.disabled}
135 label={props.label}
136 value={props.value}
137 setValue={props.setValue}
138 thisPicker={"page"}
139 openPicker={props.openPicker}
140 setOpenPicker={props.setOpenPicker}
141 closePicker={() => props.setOpenPicker("null")}
142 alpha={props.alpha}
143 />
144 );
145};
146
147export const PageBackgroundImagePicker = (props: {
148 disabled?: boolean;
149 entityID: string;
150 openPicker: pickers;
151 thisPicker: pickers;
152 setOpenPicker: (thisPicker: pickers) => void;
153 closePicker: () => void;
154 setValue: (c: Color) => void;
155 home?: boolean;
156}) => {
157 let bgImage = useEntity(props.entityID, "theme/card-background-image");
158 let bgRepeat = useEntity(
159 props.entityID,
160 "theme/card-background-image-repeat",
161 );
162 let bgColor = useColorAttribute(props.entityID, "theme/card-background");
163 let bgAlpha =
164 useEntity(props.entityID, "theme/card-background-image-opacity")?.data
165 .value || 1;
166 let alphaColor = useMemo(() => {
167 return parseColor(`rgba(0,0,0,${bgAlpha})`);
168 }, [bgAlpha]);
169 let open = props.openPicker == props.thisPicker;
170 let { rep } = useReplicache();
171
172 return (
173 <>
174 <div className="bgPickerColorLabel flex gap-2 items-center">
175 <button
176 disabled={props.disabled}
177 onClick={() => {
178 if (props.openPicker === props.thisPicker) {
179 props.setOpenPicker("null");
180 } else {
181 props.setOpenPicker(props.thisPicker);
182 }
183 }}
184 className="flex gap-2 items-center disabled:text-[#969696]"
185 >
186 <ColorSwatch
187 color={bgColor}
188 className={`w-6 h-6 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C] ${props.disabled ? "opacity-50" : ""}`}
189 style={{
190 backgroundImage: bgImage?.data.src
191 ? `url(${bgImage.data.src})`
192 : undefined,
193 backgroundPosition: "center",
194 backgroundSize: "cover",
195 }}
196 />
197 <strong
198 className={`${props.disabled ? "text-[#969696]" : " text-[#272727] "}`}
199 >
200 Page
201 </strong>
202 <div className="">Image</div>
203 </button>
204
205 <SpectrumColorPicker
206 value={alphaColor}
207 onChange={(c) => {
208 let alpha = c.getChannelValue("alpha");
209 rep?.mutate.assertFact({
210 entity: props.entityID,
211 attribute: "theme/card-background-image-opacity",
212 data: { type: "number", value: alpha },
213 });
214 }}
215 >
216 <Separator classname="h-4! my-1 border-[#C3C3C3]!" />
217 <ColorField className="w-fit pl-[6px]" channel="alpha">
218 <Input
219 disabled={props.disabled}
220 onMouseDown={onMouseDown}
221 onFocus={(e) => {
222 e.currentTarget.setSelectionRange(
223 0,
224 e.currentTarget.value.length - 1,
225 );
226 }}
227 onKeyDown={(e) => {
228 if (e.key === "Enter") {
229 e.currentTarget.blur();
230 } else return;
231 }}
232 className={`w-[48px] bg-transparent outline-hidden disabled:text-[#969696]`}
233 />
234 </ColorField>
235 </SpectrumColorPicker>
236 <div className="flex gap-1 justify-end grow text-[#969696]">
237 <button
238 onClick={() => {
239 if (bgImage) rep?.mutate.retractFact({ factID: bgImage.id });
240 if (bgRepeat) rep?.mutate.retractFact({ factID: bgRepeat.id });
241 }}
242 >
243 <DeleteSmall />
244 </button>
245 <label>
246 <BlockImageSmall />
247 <div className="hidden">
248 <ImageInput
249 entityID={props.entityID}
250 onChange={() => props.setOpenPicker("page-background-image")}
251 card
252 />
253 </div>
254 </label>
255 </div>
256 </div>
257 {open && (
258 <div className="pageImagePicker flex flex-col gap-2">
259 <ImageSettings
260 entityID={props.entityID}
261 card
262 setValue={props.setValue}
263 />
264 <div className="flex flex-col gap-2 pr-2 pl-8 -mt-2 mb-2">
265 <hr className="border-[#DBDBDB]" />
266 <SpectrumColorPicker
267 value={alphaColor}
268 onChange={(c) => {
269 let alpha = c.getChannelValue("alpha");
270 rep?.mutate.assertFact({
271 entity: props.entityID,
272 attribute: "theme/card-background-image-opacity",
273 data: { type: "number", value: alpha },
274 });
275 }}
276 >
277 <ColorSlider
278 colorSpace="hsb"
279 className="w-full mt-1 rounded-full"
280 style={{
281 backgroundImage: `url(/transparent-bg.png)`,
282 backgroundRepeat: "repeat",
283 backgroundSize: "8px",
284 }}
285 channel="alpha"
286 >
287 <SliderTrack className="h-2 w-full rounded-md">
288 <ColorThumb className={`${thumbStyle} mt-[4px]`} />
289 </SliderTrack>
290 </ColorSlider>
291 </SpectrumColorPicker>
292 </div>
293 </div>
294 )}
295 </>
296 );
297};
298
299const CanvasBGPatternPicker = (props: {
300 entityID: string;
301 rep: Replicache<ReplicacheMutators> | null;
302}) => {
303 let selectedPattern = useEntity(props.entityID, "canvas/background-pattern")
304 ?.data.value;
305 return (
306 <div className="flex gap-2 h-8 ">
307 <button
308 className={`w-full rounded-md bg-bg-page border ${selectedPattern === "grid" ? "outline-solid outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`}
309 onMouseDown={() => {
310 props.rep &&
311 props.rep.mutate.assertFact({
312 entity: props.entityID,
313 attribute: "canvas/background-pattern",
314 data: { type: "canvas-pattern-union", value: "grid" },
315 });
316 }}
317 >
318 <CanvasBackgroundPattern pattern="grid" scale={0.5} />
319 </button>
320 <button
321 className={`w-full rounded-md bg-bg-page border ${selectedPattern === "dot" ? "outline-solid outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`}
322 onMouseDown={() => {
323 props.rep &&
324 props.rep.mutate.assertFact({
325 entity: props.entityID,
326 attribute: "canvas/background-pattern",
327 data: { type: "canvas-pattern-union", value: "dot" },
328 });
329 }}
330 >
331 <CanvasBackgroundPattern pattern="dot" scale={0.5} />
332 </button>
333 <button
334 className={`w-full rounded-md bg-bg-page border ${selectedPattern === "plain" ? "outline-solid outline-tertiary border-tertiary" : "transparent-outline hover:outline-border border-border "}`}
335 onMouseDown={() => {
336 props.rep &&
337 props.rep.mutate.assertFact({
338 entity: props.entityID,
339 attribute: "canvas/background-pattern",
340 data: { type: "canvas-pattern-union", value: "plain" },
341 });
342 }}
343 >
344 <CanvasBackgroundPattern pattern="plain" />
345 </button>
346 </div>
347 );
348};
349
350export const PageTextPicker = (props: {
351 openPicker: pickers;
352 setOpenPicker: (thisPicker: pickers) => void;
353 value: Color;
354 setValue: (c: Color) => void;
355}) => {
356 return (
357 <ColorPicker
358 label="Text"
359 value={props.value}
360 setValue={props.setValue}
361 thisPicker={"text"}
362 openPicker={props.openPicker}
363 setOpenPicker={props.setOpenPicker}
364 closePicker={() => props.setOpenPicker("null")}
365 />
366 );
367};
368
369export const PageBorderHider = (props: {
370 entityID: string;
371 setOpenPicker: (p: pickers) => void;
372 openPicker: pickers;
373}) => {
374 let { rep, rootEntity } = useReplicache();
375 let rootPageBorderHidden = useEntity(rootEntity, "theme/card-border-hidden");
376 let entityPageBorderHidden = useEntity(
377 props.entityID,
378 "theme/card-border-hidden",
379 );
380 let pageBorderHidden =
381 (entityPageBorderHidden || rootPageBorderHidden)?.data.value || false;
382
383 function handleToggle() {
384 rep?.mutate.assertFact({
385 entity: props.entityID,
386 attribute: "theme/card-border-hidden",
387 data: { type: "boolean", value: !pageBorderHidden },
388 });
389
390 (pageBorderHidden && props.openPicker === "page") ||
391 (props.openPicker === "page-background-image" &&
392 props.setOpenPicker("null"));
393 }
394
395 return (
396 <>
397 <div className="flex gap-2 items-center">
398 <Toggle
399 toggleOn={!pageBorderHidden}
400 setToggleOn={() => {
401 handleToggle();
402 }}
403 disabledColor1="#8C8C8C"
404 disabledColor2="#DBDBDB"
405 />
406 <button
407 className="flex gap-2 items-center"
408 onClick={() => {
409 handleToggle();
410 }}
411 >
412 <div className="font-bold">Page Background</div>
413 <div className="italic text-[#8C8C8C]">
414 {pageBorderHidden ? "hidden" : ""}
415 </div>
416 </button>
417 </div>
418 </>
419 );
420};