mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-06-01 23:38:48 +02:00
feat: Implement a custom ColorPicker component and integrate it into the SidePanel.
This commit is contained in:
@@ -59,6 +59,7 @@
|
||||
"puppeteer": "^23.9.0",
|
||||
"puppeteer-core": "^23.9.0",
|
||||
"react": "^18",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18",
|
||||
"react-resizable-panels": "^2.0.20",
|
||||
|
||||
Generated
+14
@@ -146,6 +146,9 @@ importers:
|
||||
react:
|
||||
specifier: ^18
|
||||
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:
|
||||
specifier: ^8.10.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==}
|
||||
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:
|
||||
resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==}
|
||||
peerDependencies:
|
||||
@@ -8689,6 +8698,11 @@ snapshots:
|
||||
iconv-lite: 0.4.24
|
||||
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):
|
||||
dependencies:
|
||||
date-fns: 3.6.0
|
||||
|
||||
@@ -20,6 +20,7 @@ import LayoutSetting from "./layout/LayoutSetting";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { THEME_COLORS } from "@/types/resume";
|
||||
import { ColorPicker } from "@/components/ui/color-picker";
|
||||
|
||||
const fontOptions = [
|
||||
{ value: "sans", label: "无衬线体" },
|
||||
@@ -98,7 +99,7 @@ export function SidePanel() {
|
||||
|
||||
const debouncedSetColor = useMemo(
|
||||
() =>
|
||||
debounce((value) => {
|
||||
debounce((value: string) => {
|
||||
setThemeColor(value);
|
||||
}, 100),
|
||||
[]
|
||||
@@ -201,11 +202,9 @@ export function SidePanel() {
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("theme.custom")}
|
||||
</div>
|
||||
<motion.input
|
||||
type="color"
|
||||
<ColorPicker
|
||||
value={themeColor}
|
||||
onChange={(e) => debouncedSetColor(e.target.value)}
|
||||
className="w-[40px] h-[40px] rounded-lg cursor-pointer overflow-hidden hover:scale-105 transition-transform"
|
||||
onChange={(value) => debouncedSetColor(value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user