返回 presentation-ai
ColorSettingsPanel.tsx
根目录 / src / components / notebook / presentation / components / theme / create-theme / ColorSettingsPanel.tsx
1 "use client";
2
3 import debounce from "lodash.debounce";
4 import { RotateCcw } from "lucide-react";
5 import { useCallback, useMemo, useRef, useState } from "react";
6
7 import ColorPicker from "@/components/ui/color-picker";
8 import { Input } from "@/components/ui/input";
9 import {
10 type ThemeBackground,
11 type ThemeColors,
12 } from "@/lib/presentation/themes";
13 import { type ColorKey } from "../types";
14 import { CompactBackgroundSelector } from "./CompactBackgroundSelector";
15
16 interface ColorSettingsPanelProps {
17 colors: ThemeColors;
18 onColorChange: (key: ColorKey, value: string) => void;
19 defaultColors?: Partial<ThemeColors>;
20 background?: ThemeBackground | null;
21 onBackgroundChange?: (value: ThemeBackground | undefined | null) => void;
22 }
23
24 // Default colors for reset functionality
25 const DEFAULT_THEME_COLORS: Record<ColorKey, string> = {
26 primary: "#6366f1",
27 accent: "#8b5cf6",
28 heading: "#ffffff",
29 text: "#f1f5f9",
30 cardBackground: "#ffffff",
31 background: "none",
32 smartLayout: "#6366f1",
33 };
34
35 export function ColorSettingsPanel({
36 colors,
37 onColorChange,
38 defaultColors = DEFAULT_THEME_COLORS,
39 background,
40 onBackgroundChange,
41 }: ColorSettingsPanelProps) {
42 return (
43 <div className="space-y-6">
44 {/* Theme Palette Section */}
45 <section className="space-y-4">
46 <h3 className="text-sm font-semibold text-foreground">Theme palette</h3>
47 <ColorInputField
48 label="Primary Color"
49 color={colors.primary}
50 onChange={(color) => onColorChange("primary", color)}
51 defaultColor={defaultColors.primary ?? DEFAULT_THEME_COLORS.primary}
52 />
53 <ColorInputField
54 label="Secondary Colors (optional)"
55 color={colors.accent}
56 onChange={(color) => onColorChange("accent", color)}
57 defaultColor={defaultColors.accent ?? DEFAULT_THEME_COLORS.accent}
58 />
59 </section>
60
61 {/* Text Section */}
62 <section className="space-y-4">
63 <h3 className="text-sm font-semibold text-foreground">Text</h3>
64 <ColorInputField
65 label="Heading color"
66 color={colors.heading}
67 onChange={(color) => onColorChange("heading", color)}
68 defaultColor={defaultColors.heading ?? DEFAULT_THEME_COLORS.heading}
69 />
70 <ColorInputField
71 label="Body color"
72 color={colors.text}
73 onChange={(color) => onColorChange("text", color)}
74 defaultColor={defaultColors.text ?? DEFAULT_THEME_COLORS.text}
75 />
76 </section>
77
78 {/* Smart Layout Section (optional) */}
79 <section className="space-y-4">
80 <h3 className="text-sm font-semibold text-foreground">Smart Layout</h3>
81 <ColorInputField
82 label="Smart layout "
83 color={colors.smartLayout}
84 onChange={(color) => onColorChange("smartLayout", color)}
85 defaultColor={
86 defaultColors.smartLayout ?? DEFAULT_THEME_COLORS.smartLayout
87 }
88 />
89 </section>
90
91 {/* Background Section */}
92 <section className="space-y-4">
93 <h3 className="text-sm font-semibold text-foreground">Background</h3>
94 <ColorInputField
95 label="Content Background"
96 color={colors.cardBackground}
97 onChange={(color) => onColorChange("cardBackground", color)}
98 defaultColor={
99 defaultColors.cardBackground ?? DEFAULT_THEME_COLORS.cardBackground
100 }
101 />
102
103 <div className="space-y-2">
104 <label className="text-sm text-muted-foreground">
105 Slide background
106 </label>
107
108 <ColorInputField
109 color={colors.background}
110 onChange={(color) => onColorChange("background", color)}
111 defaultColor={
112 defaultColors.background ?? DEFAULT_THEME_COLORS.background
113 }
114 onReset={() => {
115 onColorChange(
116 "background",
117 defaultColors.background ?? DEFAULT_THEME_COLORS.background,
118 );
119 }}
120 />
121 </div>
122
123 <div className="space-y-3">
124 <label className="text-sm text-muted-foreground">
125 Page background
126 </label>
127
128 <CompactBackgroundSelector
129 value={background}
130 onChange={onBackgroundChange}
131 />
132 </div>
133 </section>
134 </div>
135 );
136 }
137
138 interface ColorInputFieldProps {
139 label?: string;
140 labelClassName?: string;
141 color: string;
142 onChange: (color: string) => void;
143 defaultColor: string;
144 onReset?: () => void;
145 }
146
147 function ColorInputField({
148 label,
149 labelClassName,
150 color,
151 onChange,
152 defaultColor,
153 onReset,
154 }: ColorInputFieldProps) {
155 const [inputValue, setInputValue] = useState(color);
156 const lastExternalColor = useRef(color);
157
158 // Update input when external color changes
159 if (lastExternalColor.current !== color) {
160 lastExternalColor.current = color;
161 setInputValue(color);
162 }
163
164 const debouncedOnChange = useMemo(
165 () => debounce((val: string) => onChange(val), 100),
166 [onChange],
167 );
168
169 const handleInputChange = useCallback(
170 (value: string) => {
171 setInputValue(value);
172 const hexPattern = /^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
173 const normalizedValue = value.startsWith("#") ? value : `#${value}`;
174 if (hexPattern.test(normalizedValue)) {
175 debouncedOnChange(normalizedValue);
176 }
177 },
178 [debouncedOnChange],
179 );
180
181 const handleInputBlur = useCallback(() => {
182 setInputValue(color);
183 }, [color]);
184
185 const handlePickerChange = useCallback(
186 (newColor: string) => {
187 setInputValue(newColor);
188 debouncedOnChange(newColor);
189 },
190 [debouncedOnChange],
191 );
192
193 const handleReset = () => {
194 if (onReset) {
195 onReset();
196 } else {
197 onChange(defaultColor);
198 setInputValue(defaultColor);
199 }
200 };
201
202 return (
203 <div className="space-y-2">
204 {label && (
205 <label
206 className={`text-sm text-muted-foreground ${labelClassName ?? ""}`}
207 >
208 {label}
209 </label>
210 )}
211 <div className="flex items-center gap-2 rounded-lg border border-border bg-background p-2">
212 <ColorPicker value={color} onChange={handlePickerChange} />
213
214 <Input
215 type="text"
216 value={inputValue}
217 onChange={(e) => handleInputChange(e.target.value)}
218 onBlur={handleInputBlur}
219 onFocus={(e) => e.target.select()}
220 className="flex-1 border-0 bg-transparent px-2 py-1 font-mono text-sm shadow-none focus-visible:ring-0"
221 placeholder="#000000"
222 />
223
224 <button
225 type="button"
226 onClick={handleReset}
227 className="shrink-0 p-2 text-muted-foreground transition-colors hover:text-foreground"
228 title="Reset to default"
229 >
230 <RotateCcw className="size-4" />
231 </button>
232 </div>
233 </div>
234 );
235 }
236
236 lines Plain Text