mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-21 15:33:30 +10:00
feat: add text color support to the rich text editor (#2903)
* feat: add text color support to the rich text editor * improve design of text color picker * Update translations for color picker features in multiple languages --------- Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
@@ -176,3 +176,26 @@
|
||||
text-underline-offset: 0.15rem;
|
||||
}
|
||||
}
|
||||
|
||||
.color_picker {
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-muted);
|
||||
padding: 0.5rem;
|
||||
|
||||
> :global(*) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
:global(.w-color-saturation) {
|
||||
height: 8.5rem !important;
|
||||
border-radius: calc(var(--radius-lg) - 0.25rem) !important;
|
||||
}
|
||||
|
||||
:global(.w-color-hue),
|
||||
:global(.w-color-alpha) {
|
||||
height: 0.75rem !important;
|
||||
margin-top: 0.5rem !important;
|
||||
border-radius: 9999px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,11 @@ import {
|
||||
TextUnderlineIcon,
|
||||
TrashSimpleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import Color from "@tiptap/extension-color";
|
||||
import Highlight from "@tiptap/extension-highlight";
|
||||
import { TableKit } from "@tiptap/extension-table";
|
||||
import TextAlign from "@tiptap/extension-text-align";
|
||||
import { TextStyle } from "@tiptap/extension-text-style";
|
||||
import {
|
||||
type Editor,
|
||||
EditorContent,
|
||||
@@ -50,6 +52,8 @@ import {
|
||||
useEditorState,
|
||||
} from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { type ColorResult, hsvaToRgbaString, rgbaStringToHsva } from "@uiw/color-convert";
|
||||
import ReactColorColorful from "@uiw/react-color-colorful";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { match } from "ts-pattern";
|
||||
@@ -65,6 +69,8 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Popover, PopoverContent, PopoverHeader, PopoverTitle, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { usePrompt } from "@/hooks/use-prompt";
|
||||
import { isRTL } from "@/utils/locale";
|
||||
import { sanitizeHtml } from "@/utils/sanitize";
|
||||
@@ -73,6 +79,27 @@ import { cn } from "@/utils/style";
|
||||
import { Toggle } from "../ui/toggle";
|
||||
import styles from "./rich-input.module.css";
|
||||
|
||||
const defaultTextColor = "rgba(0, 0, 0, 1)";
|
||||
|
||||
const richTextColorOptions = [
|
||||
"rgba(0, 0, 0, 1)",
|
||||
"rgba(231, 0, 11, 1)",
|
||||
"rgba(245, 73, 0, 1)",
|
||||
"rgba(225, 113, 0, 1)",
|
||||
"rgba(208, 135, 0, 1)",
|
||||
"rgba(94, 165, 0, 1)",
|
||||
"rgba(0, 166, 62, 1)",
|
||||
"rgba(0, 153, 102, 1)",
|
||||
"rgba(0, 146, 184, 1)",
|
||||
"rgba(0, 132, 209, 1)",
|
||||
"rgba(21, 93, 252, 1)",
|
||||
"rgba(79, 57, 246, 1)",
|
||||
"rgba(127, 34, 254, 1)",
|
||||
"rgba(200, 0, 222, 1)",
|
||||
"rgba(230, 0, 118, 1)",
|
||||
"rgba(69, 85, 108, 1)",
|
||||
] as const;
|
||||
|
||||
const extensions = [
|
||||
StarterKit.configure({
|
||||
heading: {
|
||||
@@ -88,6 +115,8 @@ const extensions = [
|
||||
protocols: ["http", "https"],
|
||||
},
|
||||
}),
|
||||
TextStyle,
|
||||
Color,
|
||||
Highlight.configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md px-0.5 py-px",
|
||||
@@ -231,6 +260,12 @@ function EditorToolbar({ editor, isFullscreen }: { editor: Editor; isFullscreen:
|
||||
canHighlight: ctx.editor.can().chain().toggleHighlight().run() ?? false,
|
||||
toggleHighlight: () => ctx.editor.chain().focus().toggleHighlight().run(),
|
||||
|
||||
// Text Color
|
||||
textColor: (ctx.editor.getAttributes("textStyle").color as string | undefined) ?? null,
|
||||
canTextColor: ctx.editor.can().chain().setColor(defaultTextColor).run() ?? false,
|
||||
setTextColor: (color: string) => ctx.editor.chain().focus().setColor(color).run(),
|
||||
unsetTextColor: () => ctx.editor.chain().focus().unsetColor().run(),
|
||||
|
||||
// Heading 1
|
||||
isHeading1: ctx.editor.isActive("heading", { level: 1 }) ?? false,
|
||||
canHeading1: ctx.editor.can().chain().toggleHeading({ level: 1 }).run() ?? false,
|
||||
@@ -426,6 +461,104 @@ function EditorToolbar({ editor, isFullscreen }: { editor: Editor; isFullscreen:
|
||||
<HighlighterCircleIcon className="size-3.5" />
|
||||
</Toggle>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
size={isFullscreen ? "lg" : "sm"}
|
||||
tabIndex={-1}
|
||||
variant="ghost"
|
||||
className={cn("rounded-none px-2", state.textColor && "bg-muted text-foreground")}
|
||||
title={t`Text Color`}
|
||||
disabled={!state.canTextColor}
|
||||
>
|
||||
<span className="flex flex-col items-center leading-none">
|
||||
<span className="text-xs font-semibold">A</span>
|
||||
<span
|
||||
className="mt-0.5 h-0.5 w-3 rounded-full"
|
||||
style={{ backgroundColor: state.textColor ?? "currentColor" }}
|
||||
/>
|
||||
</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<PopoverContent sideOffset={8} className="w-80 gap-3 rounded-xl p-3 shadow-xl">
|
||||
<PopoverHeader className="flex-row items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
className="grid size-9 place-items-center rounded-lg border border-border bg-muted/60 text-sm font-semibold shadow-xs"
|
||||
style={{ color: state.textColor ?? "currentColor" }}
|
||||
>
|
||||
A
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<PopoverTitle>
|
||||
<Trans>Text Color</Trans>
|
||||
</PopoverTitle>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<Trans comment="Preset or custom shade refer to the color picker">
|
||||
Choose a preset or custom shade.
|
||||
</Trans>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
className="shrink-0"
|
||||
onClick={state.unsetTextColor}
|
||||
disabled={!state.textColor}
|
||||
>
|
||||
<Trans comment="Clear the text color">Clear</Trans>
|
||||
</Button>
|
||||
</PopoverHeader>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
<Trans>Presets</Trans>
|
||||
</span>
|
||||
|
||||
<div className="grid grid-cols-8 gap-2 rounded-lg bg-muted/40 p-2">
|
||||
{richTextColorOptions.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
className={cn(
|
||||
"size-7 rounded-full border border-border shadow-xs ring-offset-background transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-hidden",
|
||||
state.textColor === color && "ring-2 ring-ring ring-offset-2",
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
title={color}
|
||||
aria-label={t`Use color ${color}`}
|
||||
aria-pressed={state.textColor === color}
|
||||
onClick={() => state.setTextColor(color)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
<Trans>Custom</Trans>
|
||||
</span>
|
||||
|
||||
<div className={styles.color_picker}>
|
||||
<ReactColorColorful
|
||||
color={rgbaStringToHsva(state.textColor ?? defaultTextColor)}
|
||||
onChange={(color: ColorResult) => {
|
||||
state.setTextColor(hsvaToRgbaString(color.hsva));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<div className="mx-1 h-5 w-px bg-border" />
|
||||
|
||||
<DropdownMenu>
|
||||
|
||||
Reference in New Issue
Block a user