返回 presentation-ai
font-color-toolbar-button.tsx
根目录 / src / components / plate / ui / font-color-toolbar-button.tsx
1 "use client";
2
3 import {
4 type DropdownMenuItemProps,
5 type DropdownMenuProps,
6 } from "@radix-ui/react-dropdown-menu";
7 import debounce from "lodash.debounce";
8 import { EraserIcon, PlusIcon } from "lucide-react";
9 import { useEditorRef, useEditorSelector } from "platejs/react";
10 import React from "react";
11
12 import { buttonVariants } from "@/components/plate/ui/button";
13 import {
14 DropdownMenu,
15 DropdownMenuContent,
16 DropdownMenuItem,
17 DropdownMenuTrigger,
18 } from "@/components/plate/ui/dropdown-menu";
19 import {
20 Tooltip,
21 TooltipContent,
22 TooltipProvider,
23 TooltipTrigger,
24 } from "@/components/plate/ui/tooltip";
25 import { cn } from "@/lib/utils";
26 import { ToolbarButton, ToolbarMenuGroup } from "./toolbar";
27
28 export function FontColorToolbarButton({
29 children,
30 nodeType,
31 tooltip,
32 }: {
33 nodeType: string;
34 tooltip?: string;
35 } & DropdownMenuProps) {
36 const editor = useEditorRef();
37
38 const selectionDefined = useEditorSelector(
39 (editor) => !!editor.selection,
40 [],
41 );
42
43 const color = useEditorSelector(
44 (editor) => editor.api.mark(nodeType) as string,
45 [nodeType],
46 );
47
48 const [selectedColor, setSelectedColor] = React.useState<string>();
49 const [open, setOpen] = React.useState(false);
50
51 const onToggle = React.useCallback(
52 (value = !open) => {
53 setOpen(value);
54 },
55 [open, setOpen],
56 );
57
58 const updateColor = React.useCallback(
59 (value: string) => {
60 if (editor.selection) {
61 setSelectedColor(value);
62
63 editor.tf.select(editor.selection);
64 editor.tf.focus();
65
66 editor.tf.addMarks({ [nodeType]: value });
67 }
68 },
69 [editor, nodeType],
70 );
71
72 const updateColorAndClose = React.useCallback(
73 (value: string) => {
74 updateColor(value);
75 onToggle();
76 },
77 [onToggle, updateColor],
78 );
79
80 const clearColor = React.useCallback(() => {
81 if (editor.selection) {
82 editor.tf.select(editor.selection);
83 editor.tf.focus();
84
85 if (selectedColor) {
86 editor.tf.removeMarks(nodeType);
87 }
88
89 onToggle();
90 }
91 }, [editor, selectedColor, onToggle, nodeType]);
92
93 React.useEffect(() => {
94 if (selectionDefined) {
95 setSelectedColor(color);
96 }
97 }, [color, selectionDefined]);
98
99 return (
100 <DropdownMenu
101 open={open}
102 onOpenChange={(value) => {
103 setOpen(value);
104 }}
105 modal={false}
106 >
107 <DropdownMenuTrigger asChild>
108 <ToolbarButton pressed={open} tooltip={tooltip}>
109 {children}
110 </ToolbarButton>
111 </DropdownMenuTrigger>
112
113 <DropdownMenuContent align="start">
114 <ColorPicker
115 color={selectedColor || color}
116 clearColor={clearColor}
117 colors={DEFAULT_COLORS}
118 customColors={DEFAULT_CUSTOM_COLORS}
119 updateColor={updateColorAndClose}
120 updateCustomColor={updateColor}
121 />
122 </DropdownMenuContent>
123 </DropdownMenu>
124 );
125 }
126
127 function PureColorPicker({
128 className,
129 clearColor,
130 color,
131 colors,
132 customColors,
133 updateColor,
134 updateCustomColor,
135 ...props
136 }: React.ComponentProps<"div"> & {
137 colors: TColor[];
138 customColors: TColor[];
139 clearColor: () => void;
140 updateColor: (color: string) => void;
141 updateCustomColor: (color: string) => void;
142 color?: string;
143 }) {
144 return (
145 <div className={cn("flex flex-col", className)} {...props}>
146 <ToolbarMenuGroup label="Custom Colors">
147 <ColorCustom
148 color={color}
149 className="px-2"
150 colors={colors}
151 customColors={customColors}
152 updateColor={updateColor}
153 updateCustomColor={updateCustomColor}
154 />
155 </ToolbarMenuGroup>
156 <ToolbarMenuGroup label="Default Colors">
157 <ColorDropdownMenuItems
158 color={color}
159 className="px-2"
160 colors={colors}
161 updateColor={updateColor}
162 />
163 </ToolbarMenuGroup>
164 {color && (
165 <ToolbarMenuGroup>
166 <DropdownMenuItem className="p-2" onClick={clearColor}>
167 <EraserIcon />
168 <span>Clear</span>
169 </DropdownMenuItem>
170 </ToolbarMenuGroup>
171 )}
172 </div>
173 );
174 }
175
176 const ColorPicker = React.memo(
177 PureColorPicker,
178 (prev, next) =>
179 prev.color === next.color &&
180 prev.colors === next.colors &&
181 prev.customColors === next.customColors,
182 );
183
184 function ColorCustom({
185 className,
186 color,
187 colors,
188 customColors,
189 updateColor,
190 updateCustomColor,
191 ...props
192 }: {
193 colors: TColor[];
194 customColors: TColor[];
195 updateColor: (color: string) => void;
196 updateCustomColor: (color: string) => void;
197 color?: string;
198 } & React.ComponentPropsWithoutRef<"div">) {
199 const [customColor, setCustomColor] = React.useState<string>();
200 const [value, setValue] = React.useState<string>(color || "#000000");
201
202 React.useEffect(() => {
203 if (
204 !color ||
205 customColors.some((c) => c.value === color) ||
206 colors.some((c) => c.value === color)
207 ) {
208 return;
209 }
210
211 setCustomColor(color);
212 }, [color, colors, customColors]);
213
214 const computedColors = React.useMemo(
215 () =>
216 customColor
217 ? [
218 ...customColors,
219 {
220 isBrightColor: false,
221 name: "",
222 value: customColor,
223 },
224 ]
225 : customColors,
226 [customColor, customColors],
227 );
228
229 const updateCustomColorDebounced = React.useMemo(
230 () => debounce(updateCustomColor, 100),
231 [updateCustomColor],
232 );
233
234 React.useEffect(() => {
235 return () => {
236 updateCustomColorDebounced.cancel();
237 };
238 }, [updateCustomColorDebounced]);
239
240 return (
241 <div className={cn("relative flex flex-col gap-4", className)} {...props}>
242 <ColorDropdownMenuItems
243 color={color}
244 colors={computedColors}
245 updateColor={updateColor}
246 >
247 <ColorInput
248 value={value}
249 onChange={(e) => {
250 setValue(e.target.value);
251 updateCustomColorDebounced(e.target.value);
252 }}
253 >
254 <DropdownMenuItem
255 className={cn(
256 buttonVariants({
257 size: "icon",
258 variant: "outline",
259 }),
260 "absolute top-1 right-2 bottom-2 flex size-8 items-center justify-center rounded-full",
261 )}
262 onSelect={(e) => {
263 e.preventDefault();
264 }}
265 >
266 <span className="sr-only">Custom</span>
267 <PlusIcon />
268 </DropdownMenuItem>
269 </ColorInput>
270 </ColorDropdownMenuItems>
271 </div>
272 );
273 }
274
275 function ColorInput({
276 children,
277 className,
278 value = "#000000",
279 ...props
280 }: React.ComponentProps<"input">) {
281 const inputId = React.useId();
282
283 return (
284 <div className="flex flex-col items-center">
285 <label htmlFor={inputId} className="contents cursor-pointer">
286 {children}
287 </label>
288 <input
289 {...props}
290 id={inputId}
291 className={cn("size-0 overflow-hidden border-0 p-0", className)}
292 value={value}
293 type="color"
294 />
295 </div>
296 );
297 }
298
299 type TColor = {
300 isBrightColor: boolean;
301 name: string;
302 value: string;
303 };
304
305 function ColorDropdownMenuItem({
306 className,
307 isBrightColor,
308 isSelected,
309 name,
310 updateColor,
311 value,
312 ...props
313 }: {
314 isBrightColor: boolean;
315 isSelected: boolean;
316 value: string;
317 updateColor: (color: string) => void;
318 name?: string;
319 } & DropdownMenuItemProps) {
320 const content = (
321 <DropdownMenuItem
322 className={cn(
323 buttonVariants({
324 size: "icon",
325 variant: "outline",
326 }),
327 "my-1 flex size-6 items-center justify-center rounded-full border border-solid border-muted p-0 transition-all hover:scale-125",
328 !isBrightColor && "border-transparent",
329 isSelected && "border-2 border-primary",
330 className,
331 )}
332 style={{ backgroundColor: value }}
333 onSelect={(e) => {
334 e.preventDefault();
335 updateColor(value);
336 }}
337 {...props}
338 />
339 );
340
341 return name ? (
342 <Tooltip>
343 <TooltipTrigger>{content}</TooltipTrigger>
344 <TooltipContent className="mb-1 capitalize">{name}</TooltipContent>
345 </Tooltip>
346 ) : (
347 content
348 );
349 }
350
351 export function ColorDropdownMenuItems({
352 className,
353 color,
354 colors,
355 updateColor,
356 ...props
357 }: {
358 colors: TColor[];
359 updateColor: (color: string) => void;
360 color?: string;
361 } & React.ComponentProps<"div">) {
362 return (
363 <div
364 className={cn(
365 "grid grid-cols-[repeat(10,1fr)] place-items-center gap-x-1",
366 className,
367 )}
368 {...props}
369 >
370 <TooltipProvider>
371 {colors.map(({ isBrightColor, name, value }) => (
372 <ColorDropdownMenuItem
373 name={name}
374 key={name ?? value}
375 value={value}
376 isBrightColor={isBrightColor}
377 isSelected={color === value}
378 updateColor={updateColor}
379 />
380 ))}
381 {props.children}
382 </TooltipProvider>
383 </div>
384 );
385 }
386
387 export const DEFAULT_COLORS = [
388 {
389 isBrightColor: false,
390 name: "black",
391 value: "#000000",
392 },
393 {
394 isBrightColor: false,
395 name: "dark grey 4",
396 value: "#434343",
397 },
398 {
399 isBrightColor: false,
400 name: "dark grey 3",
401 value: "#666666",
402 },
403 {
404 isBrightColor: false,
405 name: "dark grey 2",
406 value: "#999999",
407 },
408 {
409 isBrightColor: false,
410 name: "dark grey 1",
411 value: "#B7B7B7",
412 },
413 {
414 isBrightColor: false,
415 name: "grey",
416 value: "#CCCCCC",
417 },
418 {
419 isBrightColor: false,
420 name: "light grey 1",
421 value: "#D9D9D9",
422 },
423 {
424 isBrightColor: true,
425 name: "light grey 2",
426 value: "#EFEFEF",
427 },
428 {
429 isBrightColor: true,
430 name: "light grey 3",
431 value: "#F3F3F3",
432 },
433 {
434 isBrightColor: true,
435 name: "white",
436 value: "#FFFFFF",
437 },
438 {
439 isBrightColor: false,
440 name: "red berry",
441 value: "#980100",
442 },
443 {
444 isBrightColor: false,
445 name: "red",
446 value: "#FE0000",
447 },
448 {
449 isBrightColor: false,
450 name: "orange",
451 value: "#FE9900",
452 },
453 {
454 isBrightColor: true,
455 name: "yellow",
456 value: "#FEFF00",
457 },
458 {
459 isBrightColor: false,
460 name: "green",
461 value: "#00FF00",
462 },
463 {
464 isBrightColor: false,
465 name: "cyan",
466 value: "#00FFFF",
467 },
468 {
469 isBrightColor: false,
470 name: "cornflower blue",
471 value: "#4B85E8",
472 },
473 {
474 isBrightColor: false,
475 name: "blue",
476 value: "#1300FF",
477 },
478 {
479 isBrightColor: false,
480 name: "purple",
481 value: "#9900FF",
482 },
483 {
484 isBrightColor: false,
485 name: "magenta",
486 value: "#FF00FF",
487 },
488
489 {
490 isBrightColor: false,
491 name: "light red berry 3",
492 value: "#E6B8AF",
493 },
494 {
495 isBrightColor: false,
496 name: "light red 3",
497 value: "#F4CCCC",
498 },
499 {
500 isBrightColor: true,
501 name: "light orange 3",
502 value: "#FCE4CD",
503 },
504 {
505 isBrightColor: true,
506 name: "light yellow 3",
507 value: "#FFF2CC",
508 },
509 {
510 isBrightColor: true,
511 name: "light green 3",
512 value: "#D9EAD3",
513 },
514 {
515 isBrightColor: false,
516 name: "light cyan 3",
517 value: "#D0DFE3",
518 },
519 {
520 isBrightColor: false,
521 name: "light cornflower blue 3",
522 value: "#C9DAF8",
523 },
524 {
525 isBrightColor: true,
526 name: "light blue 3",
527 value: "#CFE1F3",
528 },
529 {
530 isBrightColor: true,
531 name: "light purple 3",
532 value: "#D9D2E9",
533 },
534 {
535 isBrightColor: true,
536 name: "light magenta 3",
537 value: "#EAD1DB",
538 },
539
540 {
541 isBrightColor: false,
542 name: "light red berry 2",
543 value: "#DC7E6B",
544 },
545 {
546 isBrightColor: false,
547 name: "light red 2",
548 value: "#EA9999",
549 },
550 {
551 isBrightColor: false,
552 name: "light orange 2",
553 value: "#F9CB9C",
554 },
555 {
556 isBrightColor: true,
557 name: "light yellow 2",
558 value: "#FFE598",
559 },
560 {
561 isBrightColor: false,
562 name: "light green 2",
563 value: "#B7D6A8",
564 },
565 {
566 isBrightColor: false,
567 name: "light cyan 2",
568 value: "#A1C4C9",
569 },
570 {
571 isBrightColor: false,
572 name: "light cornflower blue 2",
573 value: "#A4C2F4",
574 },
575 {
576 isBrightColor: false,
577 name: "light blue 2",
578 value: "#9FC5E8",
579 },
580 {
581 isBrightColor: false,
582 name: "light purple 2",
583 value: "#B5A7D5",
584 },
585 {
586 isBrightColor: false,
587 name: "light magenta 2",
588 value: "#D5A6BD",
589 },
590
591 {
592 isBrightColor: false,
593 name: "light red berry 1",
594 value: "#CC4125",
595 },
596 {
597 isBrightColor: false,
598 name: "light red 1",
599 value: "#E06666",
600 },
601 {
602 isBrightColor: false,
603 name: "light orange 1",
604 value: "#F6B26B",
605 },
606 {
607 isBrightColor: false,
608 name: "light yellow 1",
609 value: "#FFD966",
610 },
611 {
612 isBrightColor: false,
613 name: "light green 1",
614 value: "#93C47D",
615 },
616 {
617 isBrightColor: false,
618 name: "light cyan 1",
619 value: "#76A5AE",
620 },
621 {
622 isBrightColor: false,
623 name: "light cornflower blue 1",
624 value: "#6C9EEB",
625 },
626 {
627 isBrightColor: false,
628 name: "light blue 1",
629 value: "#6FA8DC",
630 },
631 {
632 isBrightColor: false,
633 name: "light purple 1",
634 value: "#8D7CC3",
635 },
636 {
637 isBrightColor: false,
638 name: "light magenta 1",
639 value: "#C27BA0",
640 },
641
642 {
643 isBrightColor: false,
644 name: "dark red berry 1",
645 value: "#A61B00",
646 },
647 {
648 isBrightColor: false,
649 name: "dark red 1",
650 value: "#CC0000",
651 },
652 {
653 isBrightColor: false,
654 name: "dark orange 1",
655 value: "#E59138",
656 },
657 {
658 isBrightColor: false,
659 name: "dark yellow 1",
660 value: "#F1C231",
661 },
662 {
663 isBrightColor: false,
664 name: "dark green 1",
665 value: "#6AA74F",
666 },
667 {
668 isBrightColor: false,
669 name: "dark cyan 1",
670 value: "#45818E",
671 },
672 {
673 isBrightColor: false,
674 name: "dark cornflower blue 1",
675 value: "#3B78D8",
676 },
677 {
678 isBrightColor: false,
679 name: "dark blue 1",
680 value: "#3E84C6",
681 },
682 {
683 isBrightColor: false,
684 name: "dark purple 1",
685 value: "#664EA6",
686 },
687 {
688 isBrightColor: false,
689 name: "dark magenta 1",
690 value: "#A64D78",
691 },
692
693 {
694 isBrightColor: false,
695 name: "dark red berry 2",
696 value: "#84200D",
697 },
698 {
699 isBrightColor: false,
700 name: "dark red 2",
701 value: "#990001",
702 },
703 {
704 isBrightColor: false,
705 name: "dark orange 2",
706 value: "#B45F05",
707 },
708 {
709 isBrightColor: false,
710 name: "dark yellow 2",
711 value: "#BF9002",
712 },
713 {
714 isBrightColor: false,
715 name: "dark green 2",
716 value: "#38761D",
717 },
718 {
719 isBrightColor: false,
720 name: "dark cyan 2",
721 value: "#124F5C",
722 },
723 {
724 isBrightColor: false,
725 name: "dark cornflower blue 2",
726 value: "#1155CB",
727 },
728 {
729 isBrightColor: false,
730 name: "dark blue 2",
731 value: "#0C5394",
732 },
733 {
734 isBrightColor: false,
735 name: "dark purple 2",
736 value: "#351C75",
737 },
738 {
739 isBrightColor: false,
740 name: "dark magenta 2",
741 value: "#741B47",
742 },
743
744 {
745 isBrightColor: false,
746 name: "dark red berry 3",
747 value: "#5B0F00",
748 },
749 {
750 isBrightColor: false,
751 name: "dark red 3",
752 value: "#660000",
753 },
754 {
755 isBrightColor: false,
756 name: "dark orange 3",
757 value: "#783F04",
758 },
759 {
760 isBrightColor: false,
761 name: "dark yellow 3",
762 value: "#7E6000",
763 },
764 {
765 isBrightColor: false,
766 name: "dark green 3",
767 value: "#274E12",
768 },
769 {
770 isBrightColor: false,
771 name: "dark cyan 3",
772 value: "#0D343D",
773 },
774 {
775 isBrightColor: false,
776 name: "dark cornflower blue 3",
777 value: "#1B4487",
778 },
779 {
780 isBrightColor: false,
781 name: "dark blue 3",
782 value: "#083763",
783 },
784 {
785 isBrightColor: false,
786 name: "dark purple 3",
787 value: "#1F124D",
788 },
789 {
790 isBrightColor: false,
791 name: "dark magenta 3",
792 value: "#4C1130",
793 },
794 ];
795
796 const DEFAULT_CUSTOM_COLORS = [
797 {
798 isBrightColor: false,
799 name: "dark orange 3",
800 value: "#783F04",
801 },
802 {
803 isBrightColor: false,
804 name: "dark grey 3",
805 value: "#666666",
806 },
807 {
808 isBrightColor: false,
809 name: "dark grey 2",
810 value: "#999999",
811 },
812 {
813 isBrightColor: false,
814 name: "light cornflower blue 1",
815 value: "#6C9EEB",
816 },
817 {
818 isBrightColor: false,
819 name: "dark magenta 3",
820 value: "#4C1130",
821 },
822 ];
823
823 lines Plain Text