a tool for shared writing and social publishing
1import { pickers } from "../ThemeSetter";
2import { theme } from "tailwind.config";
3import { PageBackgroundColorPicker } from "../Pickers/PageThemePickers";
4import { Color, ColorSwatch } from "react-aria-components";
5import { BlockImageSmall } from "components/Icons/BlockImageSmall";
6import { ColorPicker } from "../Pickers/ColorPicker";
7import { CloseContrastSmall } from "components/Icons/CloseContrastSmall";
8import * as Slider from "@radix-ui/react-slider";
9import { Toggle } from "components/Toggle";
10import { DeleteSmall } from "components/Icons/DeleteSmall";
11import { ImageState } from "../PubThemeSetter";
12import { Radio } from "components/Checkbox";
13import { Input } from "components/Input";
14
15export const BackgroundPicker = (props: {
16 backgroundColor: Color;
17 setBackgroundColor: (c: Color) => void;
18 pageBackground: Color;
19 setPageBackground: (c: Color) => void;
20 openPicker: pickers;
21 setOpenPicker: (p: pickers) => void;
22 bgImage: ImageState | null;
23 setBgImage: (i: ImageState | null) => void;
24 hasPageBackground: boolean;
25 setHasPageBackground: (s: boolean) => void;
26}) => {
27 // When showPageBackground is false (hasPageBackground=false) and no background image, show leafletBg picker
28 let showLeafletBgPicker = !props.hasPageBackground && !props.bgImage;
29
30 return (
31 <>
32 {props.bgImage && props.bgImage !== null ? (
33 <BackgroundImagePicker
34 bgColor={props.backgroundColor}
35 bgImage={props.bgImage}
36 setBgImage={props.setBgImage}
37 thisPicker={"page-background-image"}
38 openPicker={props.openPicker}
39 setOpenPicker={props.setOpenPicker}
40 closePicker={() => props.setOpenPicker("null")}
41 setValue={props.setBackgroundColor}
42 />
43 ) : (
44 <div className="relative">
45 <ColorPicker
46 label={"Background"}
47 value={props.backgroundColor}
48 setValue={props.setBackgroundColor}
49 thisPicker={"leaflet"}
50 openPicker={props.openPicker}
51 setOpenPicker={props.setOpenPicker}
52 closePicker={() => props.setOpenPicker("null")}
53 alpha={!!props.bgImage}
54 />
55 {!props.bgImage && (
56 <label
57 className={`
58 text-[#969696] hover:cursor-pointer shrink-0
59 absolute top-0 right-0
60 `}
61 >
62 <BlockImageSmall />
63 <div className="hidden">
64 <input
65 type="file"
66 accept="image/*"
67 hidden
68 onChange={async (e) => {
69 let file = e.currentTarget.files?.[0];
70 if (file) {
71 const reader = new FileReader();
72 reader.onload = (e) => {
73 props.setBgImage({
74 src: e.target?.result as string,
75 file,
76 repeat: null,
77 });
78 props.setOpenPicker("page-background-image");
79 };
80 reader.readAsDataURL(file);
81 }
82 }}
83 />
84 </div>
85 </label>
86 )}
87 </div>
88 )}
89 {!showLeafletBgPicker && (
90 // When there's a background image and page background hidden, label should say "Containers"
91 <PageBackgroundColorPicker
92 label={props.hasPageBackground ? "Page" : "Containers"}
93 helpText={
94 props.hasPageBackground
95 ? undefined
96 : "Affects menus, tooltips and some block backgrounds"
97 }
98 value={props.pageBackground}
99 setValue={props.setPageBackground}
100 thisPicker={"page"}
101 openPicker={props.openPicker}
102 setOpenPicker={props.setOpenPicker}
103 alpha={props.hasPageBackground ? true : false}
104 />
105 )}
106 <hr className="border-border-light" />
107 <div className="flex gap-2 items-center">
108 <Toggle
109 toggle={props.hasPageBackground}
110 onToggle={() => {
111 props.setHasPageBackground(!props.hasPageBackground);
112 props.hasPageBackground &&
113 props.openPicker === "page" &&
114 props.setOpenPicker("null");
115 }}
116 disabledColor1="#8C8C8C"
117 disabledColor2="#DBDBDB"
118 >
119 <div className="flex gap-2">
120 <div className="font-bold">Page Background</div>
121 <div className="italic text-[#8C8C8C]">
122 {props.hasPageBackground ? "" : "none"}
123 </div>
124 </div>
125 </Toggle>
126 </div>
127 </>
128 );
129};
130
131const BackgroundImagePicker = (props: {
132 disabled?: boolean;
133 bgImage: ImageState | null;
134 setBgImage: (i: ImageState | null) => void;
135 bgColor: Color;
136 openPicker: pickers;
137 thisPicker: pickers;
138 setOpenPicker: (thisPicker: pickers) => void;
139 closePicker: () => void;
140 setValue: (c: Color) => void;
141}) => {
142 let open = props.openPicker == props.thisPicker;
143
144 return (
145 <>
146 <div className="bgPickerColorLabel flex gap-2 items-center">
147 <button
148 disabled={props.disabled}
149 onClick={() => {
150 if (props.openPicker === props.thisPicker) {
151 props.setOpenPicker("null");
152 } else {
153 props.setOpenPicker(props.thisPicker);
154 }
155 }}
156 className="flex gap-2 items-center disabled:text-tertiary grow"
157 >
158 <ColorSwatch
159 color={props.bgColor}
160 className={`w-6 h-6 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C] ${props.disabled ? "opacity-50" : ""}`}
161 style={{
162 backgroundImage: props.bgImage
163 ? `url(${props.bgImage.src})`
164 : undefined,
165 backgroundPosition: "center",
166 backgroundSize: "cover",
167 }}
168 />
169 <strong className={` text-[#595959]`}>Background</strong>
170 <div className="italic text-[#8C8C8C]">image</div>
171 </button>
172 <div className="flex gap-1 text-[#8C8C8C]">
173 <button onClick={() => props.setBgImage(null)}>
174 <DeleteSmall />
175 </button>
176 <label className="hover:cursor-pointer ">
177 <BlockImageSmall />
178 <div className="hidden">
179 <input
180 type="file"
181 accept="image/*"
182 hidden
183 onChange={async (e) => {
184 let file = e.currentTarget.files?.[0];
185 if (file) {
186 const reader = new FileReader();
187 reader.onload = (e) => {
188 if (!props.bgImage) return;
189 props.setBgImage({
190 ...props.bgImage,
191 src: e.target?.result as string,
192 file,
193 });
194 };
195 reader.readAsDataURL(file);
196 }
197 }}
198 />
199 </div>
200 </label>
201 </div>
202 </div>
203 {open && (
204 <div className="pageImagePicker flex flex-col gap-2">
205 <ImageSettings
206 bgImage={props.bgImage}
207 setBgImage={props.setBgImage}
208 />
209 </div>
210 )}
211 </>
212 );
213};
214
215export const ImageSettings = (props: {
216 bgImage: ImageState | null;
217 setBgImage: (i: ImageState | null) => void;
218}) => {
219 return (
220 <>
221 <div className="themeBGImageControls font-bold flex flex-col gap-1 items-center px-3">
222 <label htmlFor="cover" className="w-full">
223 <Radio
224 radioCheckedClassName="text-[#595959]!"
225 radioEmptyClassName="text-[#969696]!"
226 type="radio"
227 id="cover"
228 name="bg-image-options"
229 value="cover"
230 checked={!props.bgImage?.repeat}
231 onChange={async (e) => {
232 if (!e.currentTarget.checked) return;
233 if (!props.bgImage) return;
234 props.setBgImage({ ...props.bgImage, repeat: null });
235 }}
236 >
237 <div
238 className={`w-full cursor-pointer ${!props.bgImage?.repeat ? "text-[#595959]" : " text-[#969696]"}`}
239 >
240 cover
241 </div>
242 </Radio>
243 </label>
244 <label htmlFor="repeat" className="pb-3 w-full">
245 <Radio
246 type="radio"
247 id="repeat"
248 name="bg-image-options"
249 value="repeat"
250 radioCheckedClassName="text-[#595959]!"
251 radioEmptyClassName="text-[#969696]!"
252 checked={!!props.bgImage?.repeat}
253 onChange={async (e) => {
254 if (!e.currentTarget.checked) return;
255 if (!props.bgImage) return;
256 props.setBgImage({ ...props.bgImage, repeat: 500 });
257 }}
258 >
259 <div className="flex flex-col w-full">
260 <div className="flex gap-2">
261 <div
262 className={`shink-0 grow-0 w-fit z-10 cursor-pointer ${props.bgImage?.repeat ? "text-[#595959]" : " text-[#969696]"}`}
263 >
264 repeat
265 </div>
266 <div
267 className={`flex font-normal ${props.bgImage?.repeat ? "text-[#969696]" : " text-[#C3C3C3]"}`}
268 >
269 <Input
270 type="number"
271 className="w-10 text-right appearance-none"
272 max={3000}
273 min={10}
274 value={props.bgImage?.repeat || 500}
275 onChange={(e) => {
276 if (!props.bgImage) return;
277 props.setBgImage({
278 ...props.bgImage,
279 repeat: parseInt(e.currentTarget.value),
280 });
281 }}
282 />{" "}
283 px
284 </div>
285 </div>
286 <Slider.Root
287 className={`relative grow flex items-center select-none touch-none w-full h-fit px-1 `}
288 value={[props.bgImage?.repeat || 500]}
289 max={3000}
290 min={10}
291 step={10}
292 onValueChange={(value) => {
293 if (!props.bgImage) return;
294 props.setBgImage({ ...props.bgImage, repeat: value[0] });
295 }}
296 >
297 <Slider.Track
298 className={`${props.bgImage?.repeat ? "bg-[#595959]" : " bg-[#C3C3C3]"} relative grow rounded-full h-[3px] my-2`}
299 ></Slider.Track>
300 <Slider.Thumb
301 className={`
302 flex w-4 h-4 rounded-full border-2 border-white cursor-pointer
303 ${props.bgImage?.repeat ? "bg-[#595959]" : " bg-[#C3C3C3] "}
304 ${props.bgImage?.repeat && "shadow-[0_0_0_1px_#8C8C8C,inset_0_0_0_1px_#8C8C8C]"} `}
305 aria-label="Volume"
306 />
307 </Slider.Root>
308 </div>
309 </Radio>
310 </label>
311 </div>
312 </>
313 );
314};