feat: Implement a custom ColorPicker component and integrate it into the SidePanel.

This commit is contained in:
JOYCEQL
2026-02-11 14:56:57 +08:00
parent 993dcb919c
commit f6cf5bfcdf
5 changed files with 107 additions and 5 deletions
+1
View File
@@ -59,6 +59,7 @@
"puppeteer": "^23.9.0", "puppeteer": "^23.9.0",
"puppeteer-core": "^23.9.0", "puppeteer-core": "^23.9.0",
"react": "^18", "react": "^18",
"react-colorful": "^5.6.1",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18", "react-dom": "^18",
"react-resizable-panels": "^2.0.20", "react-resizable-panels": "^2.0.20",
+14
View File
@@ -146,6 +146,9 @@ importers:
react: react:
specifier: ^18 specifier: ^18
version: 18.3.1 version: 18.3.1
react-colorful:
specifier: ^5.6.1
version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-day-picker: react-day-picker:
specifier: ^8.10.1 specifier: ^8.10.1
version: 8.10.1(date-fns@3.6.0)(react@18.3.1) version: 8.10.1(date-fns@3.6.0)(react@18.3.1)
@@ -4014,6 +4017,12 @@ packages:
resolution: {integrity: sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==} resolution: {integrity: sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
react-colorful@5.6.1:
resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
react-day-picker@8.10.1: react-day-picker@8.10.1:
resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==}
peerDependencies: peerDependencies:
@@ -8689,6 +8698,11 @@ snapshots:
iconv-lite: 0.4.24 iconv-lite: 0.4.24
unpipe: 1.0.0 unpipe: 1.0.0
react-colorful@5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-day-picker@8.10.1(date-fns@3.6.0)(react@18.3.1): react-day-picker@8.10.1(date-fns@3.6.0)(react@18.3.1):
dependencies: dependencies:
date-fns: 3.6.0 date-fns: 3.6.0
+4 -5
View File
@@ -20,6 +20,7 @@ import LayoutSetting from "./layout/LayoutSetting";
import { useResumeStore } from "@/store/useResumeStore"; import { useResumeStore } from "@/store/useResumeStore";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { THEME_COLORS } from "@/types/resume"; import { THEME_COLORS } from "@/types/resume";
import { ColorPicker } from "@/components/ui/color-picker";
const fontOptions = [ const fontOptions = [
{ value: "sans", label: "无衬线体" }, { value: "sans", label: "无衬线体" },
@@ -98,7 +99,7 @@ export function SidePanel() {
const debouncedSetColor = useMemo( const debouncedSetColor = useMemo(
() => () =>
debounce((value) => { debounce((value: string) => {
setThemeColor(value); setThemeColor(value);
}, 100), }, 100),
[] []
@@ -201,11 +202,9 @@ export function SidePanel() {
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{t("theme.custom")} {t("theme.custom")}
</div> </div>
<motion.input <ColorPicker
type="color"
value={themeColor} value={themeColor}
onChange={(e) => debouncedSetColor(e.target.value)} onChange={(value) => debouncedSetColor(value)}
className="w-[40px] h-[40px] rounded-lg cursor-pointer overflow-hidden hover:scale-105 transition-transform"
/> />
</div> </div>
</div> </div>
+71
View File
@@ -0,0 +1,71 @@
"use client";
import { forwardRef, useMemo, useState } from "react";
import { HexColorPicker } from "react-colorful";
import { cn } from "@/lib/utils";
import type { ButtonProps } from "@/components/ui/button";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
import { useForwardedRef } from "../../lib/use-forwarded-ref";
interface ColorPickerProps
extends Omit<ButtonProps, "value" | "onChange" | "onBlur"> {
value: string;
onChange: (value: string) => void;
onBlur?: () => void;
}
const ColorPicker = forwardRef<HTMLInputElement, ColorPickerProps>(
(
{ disabled, value, onChange, onBlur, name, className, ...props },
forwardedRef
) => {
const ref = useForwardedRef(forwardedRef);
const [open, setOpen] = useState(false);
const parsedValue = useMemo(() => {
return value || "#FFFFFF";
}, [value]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
{...props}
disabled={disabled}
className={cn(
"w-[40px] h-[40px] rounded-lg cursor-pointer overflow-hidden p-0 border-2 transition-all hover:scale-105",
className
)}
onClick={() => setOpen(true)}
style={{ backgroundColor: parsedValue }}
>
<span className="sr-only">Pick a color</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-3">
<HexColorPicker color={parsedValue} onChange={onChange} />
<div className="mt-3 flex items-center gap-2">
<span className="text-muted-foreground text-sm">#</span>
<Input
maxLength={7}
onChange={(e) => {
onChange(e.currentTarget.value.startsWith("#") ? e.currentTarget.value : `#${e.currentTarget.value}`);
}}
value={parsedValue.replace("#", "")}
className="h-8"
/>
</div>
</PopoverContent>
</Popover>
);
}
);
ColorPicker.displayName = "ColorPicker";
export { ColorPicker };
+17
View File
@@ -0,0 +1,17 @@
import { useRef, useEffect } from "react";
import type { ForwardedRef } from "react";
export function useForwardedRef<T>(ref: ForwardedRef<T>) {
const innerRef = useRef<T>(null);
useEffect(() => {
if (!ref) return;
if (typeof ref === "function") {
ref(innerRef.current);
} else {
ref.current = innerRef.current;
}
});
return innerRef;
}