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": "^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",
|
||||||
|
|||||||
Generated
+14
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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