a tool for shared writing and social publishing
1import { useEditorStates } from "src/state/useEditorState";
2import { useUIState } from "src/useUIState";
3import { schema } from "components/Blocks/TextBlock/schema";
4import { TextSelection } from "prosemirror-state";
5import {
6 TextDecorationButton,
7 toggleMarkInFocusedBlock,
8} from "./TextDecorationButton";
9import * as Popover from "@radix-ui/react-popover";
10import * as Tooltip from "@radix-ui/react-tooltip";
11import { theme } from "../../tailwind.config";
12import {
13 pickers,
14 SectionArrow,
15 setColorAttribute,
16} from "components/ThemeManager/ThemeSetter";
17import { ColorPicker } from "components/ThemeManager/Pickers/ColorPicker";
18import { useEntity, useReplicache } from "src/replicache";
19import { useEffect, useMemo, useState } from "react";
20import { useColorAttribute } from "components/ThemeManager/useColorAttribute";
21import { rangeHasMark } from "src/utils/prosemirror/rangeHasMark";
22
23import { Separator, ShortcutKey } from "components/Layout";
24import { ToolbarButton } from ".";
25import { NestedCardThemeProvider } from "components/ThemeManager/ThemeProvider";
26import { Props } from "components/Icons/Props";
27import { PopoverArrow } from "components/Icons/PopoverArrow";
28import { ArrowRightTiny } from "components/Icons/ArrowRightTiny";
29import { PaintSmall } from "components/Icons/PaintSmall";
30import { isMac } from "src/utils/isDevice";
31
32export const HighlightButton = (props: {
33 lastUsedHighlight: string;
34 setToolbarState: (s: "highlight") => void;
35}) => {
36 return (
37 <div className="flex items-center gap-1">
38 <TextDecorationButton
39 tooltipContent={
40 <div className="flex flex-col gap-1 justify-center">
41 <div className="text-center bg-border-light w-fit rounded-md px-0.5 mx-auto ">
42 Highlight
43 </div>
44 <div className="flex gap-1">
45 {isMac() ? (
46 <>
47 <ShortcutKey>⌘</ShortcutKey> +{" "}
48 <ShortcutKey> Ctrl </ShortcutKey> +{" "}
49 <ShortcutKey> H </ShortcutKey>
50 </>
51 ) : (
52 <>
53 <ShortcutKey> Ctrl </ShortcutKey> +{" "}
54 <ShortcutKey> Meta </ShortcutKey> +{" "}
55 <ShortcutKey> H </ShortcutKey>
56 </>
57 )}
58 </div>
59 </div>
60 }
61 attrs={{ color: props.lastUsedHighlight }}
62 mark={schema.marks.highlight}
63 icon={
64 <HighlightSmall
65 highlightColor={
66 props.lastUsedHighlight === "1"
67 ? theme.colors["highlight-1"]
68 : props.lastUsedHighlight === "2"
69 ? theme.colors["highlight-2"]
70 : theme.colors["highlight-3"]
71 }
72 />
73 }
74 />
75
76 <ToolbarButton
77 tooltipContent="Change Highlight Color"
78 onClick={() => {
79 props.setToolbarState("highlight");
80 }}
81 className="-ml-1"
82 >
83 <ArrowRightTiny />
84 </ToolbarButton>
85 </div>
86 );
87};
88
89export const HighlightToolbar = (props: {
90 onClose: () => void;
91 lastUsedHighlight: "1" | "2" | "3";
92 setLastUsedHighlight: (color: "1" | "2" | "3") => void;
93 pageID: string;
94}) => {
95 let focusedBlock = useUIState((s) => s.focusedEntity);
96 let focusedEditor = useEditorStates((s) =>
97 focusedBlock ? s.editorStates[focusedBlock.entityID] : null,
98 );
99 let [initialRender, setInitialRender] = useState(true);
100 useEffect(() => {
101 setInitialRender(false);
102 }, []);
103
104 useEffect(() => {
105 // we're not returning initialRender in the dependancy array on purpose! although admittedly, can't remember why not...
106 if (initialRender) return;
107 if (focusedEditor) props.onClose();
108 }, [focusedEditor, props]);
109
110 return (
111 <div className="flex w-full justify-between items-center gap-4 text-secondary">
112 <div className="flex items-center gap-[6px]">
113 <HighlightColorButton
114 color="1"
115 lastUsedHighlight={props.lastUsedHighlight}
116 setLastUsedHightlight={props.setLastUsedHighlight}
117 />
118 <HighlightColorButton
119 color="2"
120 lastUsedHighlight={props.lastUsedHighlight}
121 setLastUsedHightlight={props.setLastUsedHighlight}
122 />
123 <HighlightColorButton
124 color="3"
125 lastUsedHighlight={props.lastUsedHighlight}
126 setLastUsedHightlight={props.setLastUsedHighlight}
127 />
128
129 <Separator classname="h-6!" />
130 <HighlightColorSettings pageID={props.pageID} />
131 </div>
132 </div>
133 );
134};
135
136export const HighlightColorButton = (props: {
137 color: "1" | "2" | "3";
138 lastUsedHighlight: "1" | "2" | "3";
139 setLastUsedHightlight: (color: "1" | "2" | "3") => void;
140}) => {
141 let focusedBlock = useUIState((s) => s.focusedEntity);
142 let focusedEditor = useEditorStates((s) =>
143 focusedBlock ? s.editorStates[focusedBlock.entityID] : null,
144 );
145 let hasMark: boolean = false;
146 if (focusedEditor) {
147 let { to, from, $cursor } = focusedEditor.editor.selection as TextSelection;
148
149 let mark = rangeHasMark(
150 focusedEditor.editor,
151 schema.marks.highlight,
152 from,
153 to,
154 );
155 if ($cursor)
156 hasMark = !!schema.marks.highlight.isInSet(
157 focusedEditor.editor.storedMarks || $cursor.marks(),
158 );
159 else {
160 hasMark = !!mark;
161 }
162 }
163 return (
164 <button
165 onMouseDown={(e) => {
166 e.preventDefault();
167 toggleMarkInFocusedBlock(schema.marks.highlight, {
168 color: props.color,
169 });
170 props.setLastUsedHightlight(props.color);
171 }}
172 >
173 <div
174 className={`w-6 h-6 rounded-md flex items-center justify-center ${props.lastUsedHighlight === props.color ? "bg-border" : ""}`}
175 >
176 <div
177 className={`w-5 h-5 rounded-full border-2 border-white shadow-[0_0_0_1px_#8C8C8C]`}
178 style={{
179 backgroundColor:
180 props.color === "1"
181 ? theme.colors["highlight-1"]
182 : props.color === "2"
183 ? theme.colors["highlight-2"]
184 : theme.colors["highlight-3"],
185 }}
186 />
187 </div>
188 </button>
189 );
190};
191
192export const HighlightColorSettings = (props: { pageID: string }) => {
193 let { rep, rootEntity } = useReplicache();
194 let set = useMemo(() => {
195 return setColorAttribute(rep, rootEntity);
196 }, [rep, rootEntity]);
197
198 let [openPicker, setOpenPicker] = useState<pickers>("null");
199
200 let backgroundImage = useEntity(rootEntity, "theme/background-image");
201 let backgroundRepeat = useEntity(rootEntity, "theme/background-image-repeat");
202 let pageBGImage = useEntity(props.pageID, "theme/card-background-image");
203 let pageBGRepeat = useEntity(
204 props.pageID,
205 "theme/card-background-image-repeat",
206 );
207 let pageBGOpacity = useEntity(
208 props.pageID,
209 "theme/card-background-image-opacity",
210 );
211
212 let highlight1Value = useColorAttribute(rootEntity, "theme/highlight-1");
213 let highlight2Value = useColorAttribute(rootEntity, "theme/highlight-2");
214 let highlight3Value = useColorAttribute(rootEntity, "theme/highlight-3");
215
216 return (
217 <Popover.Root>
218 <Tooltip.Root>
219 <Popover.Trigger asChild>
220 <Tooltip.Trigger
221 className={`rounded-md active:bg-border active:text-primary text-secondary hover:bg-border hover:text-primary`}
222 onMouseDown={(e) => {
223 e.preventDefault();
224 }}
225 >
226 <PaintSmall />
227 </Tooltip.Trigger>
228 </Popover.Trigger>
229
230 <Tooltip.Portal>
231 <NestedCardThemeProvider>
232 <Tooltip.Content
233 sideOffset={6}
234 alignOffset={12}
235 className="z-10 bg-border rounded-md py-1 px-[6px] font-bold text-secondary text-sm"
236 >
237 Change Highlight Colors
238 <Tooltip.Arrow asChild width={16} height={8} viewBox="0 0 16 8">
239 <PopoverArrow
240 arrowFill={theme.colors["border"]}
241 arrowStroke="transparent"
242 />
243 </Tooltip.Arrow>
244 </Tooltip.Content>
245 </NestedCardThemeProvider>
246 </Tooltip.Portal>
247
248 <Popover.Portal>
249 <NestedCardThemeProvider>
250 <Popover.Content
251 className="themeSetterWrapper z-20 w-80 h-fit max-h-[80vh] bg-white rounded-md border border-border flex"
252 align="center"
253 sideOffset={8}
254 collisionPadding={16}
255 >
256 <div
257 className="bg-bg-leaflet w-full m-2 p-3 pb-0 flex flex-col rounded-md border border-border"
258 style={{
259 backgroundImage: `url(${backgroundImage?.data.src})`,
260 backgroundRepeat: backgroundRepeat ? "repeat" : "no-repeat",
261 backgroundPosition: "center",
262 backgroundSize: !backgroundRepeat
263 ? "cover"
264 : `calc(${backgroundRepeat.data.value}px / 2 )`,
265 }}
266 >
267 <div className="flex flex-col -mb-[6px] z-10">
268 <div
269 className="themeHighlightControls 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))]"
270 style={{ backgroundColor: "rgba(var(--bg-page), 0.6)" }}
271 >
272 <ColorPicker
273 label="Highlight 1"
274 value={highlight1Value}
275 setValue={(color) => {
276 set("theme/highlight-1")(
277 color.withChannelValue("alpha", 1),
278 );
279 }}
280 thisPicker={"highlight-1"}
281 openPicker={openPicker}
282 setOpenPicker={setOpenPicker}
283 closePicker={() => setOpenPicker("null")}
284 />
285 <ColorPicker
286 label="Highlight 2"
287 value={highlight2Value}
288 setValue={set("theme/highlight-2")}
289 thisPicker={"highlight-2"}
290 openPicker={openPicker}
291 setOpenPicker={setOpenPicker}
292 closePicker={() => setOpenPicker("null")}
293 />
294 <ColorPicker
295 label="Highlight 3"
296 value={highlight3Value}
297 setValue={set("theme/highlight-3")}
298 thisPicker={"highlight-3"}
299 openPicker={openPicker}
300 setOpenPicker={setOpenPicker}
301 closePicker={() => setOpenPicker("null")}
302 />
303 </div>
304 <SectionArrow
305 fill={theme.colors["primary"]}
306 stroke={theme.colors["bg-page"]}
307 className="ml-2"
308 />
309 </div>
310
311 <div
312 className="rounded-t-lg p-2 relative border border-border border-b-transparent shadow-md text-primary"
313 style={{
314 backgroundColor:
315 "rgba(var(--bg-page), var(--bg-page-alpha))",
316 }}
317 >
318 <div
319 className="background absolute top-0 right-0 bottom-0 left-0 z-0 rounded-t-lg"
320 style={{
321 backgroundImage: `url(${pageBGImage?.data.src})`,
322 backgroundRepeat: pageBGRepeat ? "repeat" : "no-repeat",
323 backgroundPosition: "center",
324 backgroundSize: !pageBGRepeat
325 ? "cover"
326 : `calc(${pageBGRepeat.data.value}px / 2 )`,
327 opacity: pageBGOpacity?.data.value || 1,
328 }}
329 />
330 <div className="relative flex flex-col">
331 <p className="font-bold">Pick your highlights!</p>
332 <small className="">
333 This is what{" "}
334 <span className="highlight bg-highlight-1">
335 Highlights look like
336 </span>
337 <br />
338 Make them{" "}
339 <span className="highlight bg-highlight-2">
340 whatever you want!
341 </span>
342 <br />
343 <span className="highlight bg-highlight-3">
344 Happy theming!
345 </span>
346 </small>
347 </div>
348 </div>
349 </div>
350 <Popover.Arrow asChild width={16} height={8} viewBox="0 0 16 8">
351 <PopoverArrow
352 arrowFill={theme.colors["white"]}
353 arrowStroke={theme.colors["border"]}
354 />
355 </Popover.Arrow>
356 </Popover.Content>
357 </NestedCardThemeProvider>
358 </Popover.Portal>
359 </Tooltip.Root>
360 </Popover.Root>
361 );
362};
363
364const HighlightSmall = (props: { highlightColor: string } & Props) => {
365 let { highlightColor, ...svgProps } = props;
366 return (
367 <svg
368 width="24"
369 height="24"
370 viewBox="0 0 24 24"
371 fill="none"
372 xmlns="http://www.w3.org/2000/svg"
373 {...svgProps}
374 >
375 <path
376 fillRule="evenodd"
377 clipRule="evenodd"
378 d="M20.5 12C20.5 16.6944 16.6944 20.5 12 20.5C7.30558 20.5 3.5 16.6944 3.5 12C3.5 7.93802 6.34926 4.54153 10.1582 3.70008L9.84791 4.18297C9.77021 4.3039 9.72889 4.44461 9.72888 4.58836L9.72851 10.0094L8.63101 11.7402C8.62242 11.7538 8.61431 11.7675 8.60666 11.7815C8.60016 11.7908 8.59388 11.8004 8.58781 11.8101C8.38051 12.1418 8.46302 12.6162 8.76836 13.1359L7.24363 15.6498C6.74131 16.4779 7.3311 17.5381 8.29965 17.548L11.6224 17.5818C12.2379 17.5881 12.8114 17.2706 13.1328 16.7456L13.4985 16.148C13.9182 16.1939 14.2769 16.1567 14.5378 16.0308C14.6916 15.9756 14.8266 15.8704 14.9178 15.7265L16.0178 13.9918L20.4981 11.8165C20.4994 11.8775 20.5 11.9387 20.5 12ZM20.3169 10.2369L15.1709 12.7355C15.0456 12.7963 14.9397 12.8909 14.8651 13.0086L14.3138 13.878C14.2071 13.7494 14.0889 13.6201 13.9603 13.4913C13.554 13.0742 13.0329 12.6692 12.5492 12.3837C12.4992 12.3542 12.4466 12.3239 12.3919 12.2929C12.0663 12.101 11.7406 11.938 11.4239 11.8053C11.2273 11.719 11.024 11.6386 10.8321 11.5784C10.7486 11.5522 10.6549 11.5258 10.5563 11.505L11.1119 10.6287C11.188 10.5086 11.2285 10.3694 11.2285 10.2272L11.2289 4.80862L12.0696 3.50028C12.4231 3.50311 12.7716 3.52754 13.1136 3.57229C13.0973 3.59203 13.082 3.61297 13.0679 3.63511L12.2981 4.84369C12.2341 4.94402 12.2002 5.06052 12.2002 5.17948V7.82698C12.2002 8.17215 12.48 8.45198 12.8252 8.45198C13.1704 8.45198 13.4502 8.17215 13.4502 7.82698V5.36164L14.1222 4.3067C14.225 4.14534 14.2445 3.95474 14.1919 3.7853C17.2663 4.60351 19.6556 7.10172 20.3169 10.2369ZM22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM12.6338 9.14563C12.979 9.14563 13.2588 9.42545 13.2588 9.77063V10.2416C13.2588 10.5868 12.979 10.8666 12.6338 10.8666C12.2886 10.8666 12.0088 10.5868 12.0088 10.2416V9.77063C12.0088 9.42545 12.2886 9.14563 12.6338 9.14563ZM12.7875 14.4388C12.5032 14.1651 12.1383 13.8821 11.7882 13.6755C11.5214 13.5181 11.1621 13.3284 10.8268 13.1808C10.733 13.1395 10.6444 13.103 10.5629 13.072L8.75514 16.0525L11.6391 16.0819C11.727 16.0828 11.809 16.0374 11.8549 15.9625L12.7875 14.4388Z"
379 fill="currentColor"
380 />
381 <path
382 fillRule="evenodd"
383 clipRule="evenodd"
384 d="M20.5 12C20.5 16.6944 16.6944 20.5 12 20.5C7.30558 20.5 3.5 16.6944 3.5 12C3.5 7.93801 6.34926 4.54152 10.1582 3.70007L9.84791 4.18296C9.77021 4.30389 9.72889 4.4446 9.72888 4.58835L9.72851 10.0093L8.63101 11.7402C8.62242 11.7537 8.61431 11.7675 8.60666 11.7814C8.60016 11.7908 8.59388 11.8004 8.58781 11.8101C8.38051 12.1418 8.46302 12.6162 8.76836 13.1359L7.24363 15.6498C6.74131 16.4779 7.3311 17.5381 8.29965 17.5479L11.6224 17.5818C12.2379 17.5881 12.8114 17.2705 13.1328 16.7455L13.4985 16.148C13.9182 16.1939 14.2769 16.1567 14.5378 16.0308C14.6916 15.9756 14.8266 15.8704 14.9178 15.7265L16.0178 13.9918L20.4981 11.8164C20.4994 11.8775 20.5 11.9387 20.5 12ZM12.7875 14.4388C12.5032 14.1651 12.1383 13.8821 11.7882 13.6755C11.5214 13.5181 11.1621 13.3284 10.8268 13.1808C10.733 13.1395 10.6444 13.1029 10.5629 13.072L8.75514 16.0525L11.6391 16.0819C11.727 16.0828 11.809 16.0374 11.8549 15.9624L12.7875 14.4388Z"
385 fill={highlightColor}
386 />
387 </svg>
388 );
389};