changes after review, factor the color picker component and add validation for the input
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import Block from "@uiw/react-color-block";
|
||||
import Colorful from "@uiw/react-color-colorful";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "./button";
|
||||
import { Input } from "./input";
|
||||
|
||||
export default function ColorPicker({
|
||||
selectedColor,
|
||||
colorPalette,
|
||||
translations,
|
||||
clearBackgroundOption = false,
|
||||
onUpdateColor,
|
||||
}: {
|
||||
selectedColor: string;
|
||||
colorPalette: string[];
|
||||
translations: Record<"colorWheel" | "colorPalette", string> &
|
||||
Partial<Record<"clearBackground", string>>;
|
||||
clearBackgroundOption?: boolean;
|
||||
onUpdateColor: (color: string) => void;
|
||||
}) {
|
||||
const [colorMode, setColorMode] = useState<"wheel" | "palette">("wheel");
|
||||
const [hexInput, setHexInput] = useState(selectedColor);
|
||||
|
||||
useEffect(() => {
|
||||
setHexInput(selectedColor);
|
||||
}, [selectedColor]);
|
||||
|
||||
const getTextColor = (color: string) => {
|
||||
if (color === "transparent") return "#ffffff";
|
||||
const r = parseInt(color.slice(1, 3), 16);
|
||||
const g = parseInt(color.slice(3, 5), 16);
|
||||
const b = parseInt(color.slice(5, 7), 16);
|
||||
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
|
||||
if (luminance > 186) return "#000000";
|
||||
return "#ffffff";
|
||||
};
|
||||
|
||||
// Normalize the hex input.
|
||||
// Adds a # at the beginning of the input if it's not there.
|
||||
const normalizeHexDraft = (raw: string) => {
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed === "") return "";
|
||||
if (/^[0-9A-Fa-f]/.test(trimmed[0])) return `#${trimmed}`;
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const handleColorInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const normalized = normalizeHexDraft(e.target.value);
|
||||
setHexInput(normalized);
|
||||
// Check if the normalized hex is a valid hex color.
|
||||
// It should follow the format #RRGGBB or #RGB.
|
||||
const isValidHexColor =
|
||||
/^#[0-9A-Fa-f]{3}$/.test(normalized) || /^#[0-9A-Fa-f]{6}$/.test(normalized);
|
||||
if (isValidHexColor) {
|
||||
onUpdateColor(normalized);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="p-1 flex flex-col gap-4 items-center">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
|
||||
onClick={() => setColorMode("wheel")}
|
||||
style={{
|
||||
backgroundColor: colorMode === "wheel" ? "#34B27B" : "transparent",
|
||||
}}
|
||||
>
|
||||
<span className="text-xs text-slate-300 truncate flex-1 text-left">
|
||||
{translations.colorWheel}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
|
||||
onClick={() => setColorMode("palette")}
|
||||
style={{
|
||||
backgroundColor: colorMode === "palette" ? "#34B27B" : "transparent",
|
||||
}}
|
||||
>
|
||||
<span className="text-xs text-slate-300 truncate flex-1 text-left">
|
||||
{translations.colorPalette}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
{colorMode === "wheel" && (
|
||||
<>
|
||||
<div
|
||||
className={`w-full h-20 flex items-center justify-center border border-white/10 rounded-lg`}
|
||||
style={{ backgroundColor: selectedColor }}
|
||||
>
|
||||
<span style={{ color: getTextColor(selectedColor) }}>{selectedColor}</span>
|
||||
</div>
|
||||
<Colorful
|
||||
color={selectedColor}
|
||||
onChange={(color) => {
|
||||
onUpdateColor(color.hex);
|
||||
}}
|
||||
style={{
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
disableAlpha={true}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={hexInput}
|
||||
className="w-full h-9 rounded-md border border-white/10 bg-white/5 px-2 text-xs text-slate-200 outline-none focus:border-[#34B27B]/50 focus:ring-1 focus:ring-[#34B27B]/30 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
onChange={handleColorInputChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{colorMode === "palette" && (
|
||||
<Block
|
||||
color={selectedColor}
|
||||
colors={colorPalette}
|
||||
onChange={(color) => {
|
||||
onUpdateColor(color.hex);
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{clearBackgroundOption && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full mt-2 text-xs h-7 hover:bg-white/5 text-slate-400"
|
||||
onClick={() => {
|
||||
onUpdateColor("transparent");
|
||||
}}
|
||||
>
|
||||
{translations.clearBackground}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import Block from "@uiw/react-color-block";
|
||||
import Colorful from "@uiw/react-color-colorful";
|
||||
import {
|
||||
AlignCenter,
|
||||
AlignLeft,
|
||||
@@ -31,6 +30,7 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { useScopedT } from "@/contexts/I18nContext";
|
||||
import { type CustomFont, getCustomFonts } from "@/lib/customFonts";
|
||||
import { cn } from "@/lib/utils";
|
||||
import ColorPicker from "../ui/color-picker";
|
||||
import { AddCustomFontDialog } from "./AddCustomFontDialog";
|
||||
import { getArrowComponent } from "./ArrowSvgs";
|
||||
import type { AnnotationRegion, AnnotationType, ArrowDirection, FigureData } from "./types";
|
||||
@@ -68,7 +68,6 @@ export function AnnotationSettingsPanel({
|
||||
const t = useScopedT("settings");
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [customFonts, setCustomFonts] = useState<CustomFont[]>([]);
|
||||
const [colorMode, setColorMode] = useState<"wheel" | "palette">("wheel");
|
||||
const fontStyleLabels: Record<string, string> = {
|
||||
classic: t("fontStyles.classic"),
|
||||
editor: t("fontStyles.editor"),
|
||||
@@ -140,15 +139,6 @@ export function AnnotationSettingsPanel({
|
||||
event.target.value = "";
|
||||
};
|
||||
|
||||
const getTextColor = (color: string) => {
|
||||
if (color === "transparent") return "#ffffff";
|
||||
const r = parseInt(color.slice(1, 3), 16);
|
||||
const g = parseInt(color.slice(3, 5), 16);
|
||||
const b = parseInt(color.slice(5, 7), 16);
|
||||
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
|
||||
if (luminance > 186) return "#000000";
|
||||
return "#ffffff";
|
||||
};
|
||||
return (
|
||||
<div className="flex-[2] min-w-0 bg-[#09090b] border border-white/5 rounded-2xl p-4 flex flex-col shadow-xl h-full overflow-y-auto custom-scrollbar">
|
||||
<div className="mb-6">
|
||||
@@ -394,64 +384,17 @@ export function AnnotationSettingsPanel({
|
||||
side="top"
|
||||
className="w-[260px] p-3 bg-[#1a1a1c] border border-white/10 rounded-xl shadow-xl"
|
||||
>
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
{colorMode === "palette" && (
|
||||
<Block
|
||||
color={annotation.style.color}
|
||||
colors={colorPalette}
|
||||
onChange={(color) => {
|
||||
onStyleChange({ color: color.hex });
|
||||
}}
|
||||
style={{
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{colorMode === "wheel" && (
|
||||
<>
|
||||
<div
|
||||
className={`w-full h-20 flex items-center justify-center border border-white/10 rounded-lg`}
|
||||
style={{ backgroundColor: annotation.style.color }}
|
||||
>
|
||||
<span style={{ color: getTextColor(annotation.style.color) }}>
|
||||
{annotation.style.color}
|
||||
</span>
|
||||
</div>
|
||||
<Colorful
|
||||
color={annotation.style.color}
|
||||
onChange={(color) => {
|
||||
onStyleChange({ color: color.hex });
|
||||
}}
|
||||
style={{
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
disableAlpha={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
|
||||
onClick={() => setColorMode("wheel")}
|
||||
>
|
||||
<span className="text-xs text-slate-300 truncate flex-1 text-left">
|
||||
{t("annotation.colorWheel")}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
|
||||
onClick={() => setColorMode("palette")}
|
||||
>
|
||||
<span className="text-xs text-slate-300 truncate flex-1 text-left">
|
||||
{t("annotation.colorPalette")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ColorPicker
|
||||
selectedColor={annotation.style.color}
|
||||
colorPalette={colorPalette}
|
||||
translations={{
|
||||
colorWheel: t("annotation.colorWheel"),
|
||||
colorPalette: t("annotation.colorPalette"),
|
||||
}}
|
||||
onUpdateColor={(color) => {
|
||||
onStyleChange({ color: color });
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
@@ -484,80 +427,19 @@ export function AnnotationSettingsPanel({
|
||||
side="top"
|
||||
className="w-[260px] p-3 bg-[#1a1a1c] border border-white/10 rounded-xl shadow-xl"
|
||||
>
|
||||
<div className="flex flex-col gap-4 items-center items-center">
|
||||
{colorMode === "palette" && (
|
||||
<Block
|
||||
color={
|
||||
annotation.style.backgroundColor === "transparent"
|
||||
? "#000000"
|
||||
: annotation.style.backgroundColor
|
||||
}
|
||||
colors={colorPalette}
|
||||
onChange={(color) => {
|
||||
onStyleChange({ backgroundColor: color.hex });
|
||||
}}
|
||||
style={{
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{colorMode === "wheel" && (
|
||||
<>
|
||||
<div
|
||||
className={`w-full h-20 flex items-center justify-center border border-white/10 rounded-lg`}
|
||||
style={{ backgroundColor: annotation.style.backgroundColor }}
|
||||
>
|
||||
<span
|
||||
style={{ color: getTextColor(annotation.style.backgroundColor) }}
|
||||
>
|
||||
{annotation.style.backgroundColor}
|
||||
</span>
|
||||
</div>
|
||||
<Colorful
|
||||
color={annotation.style.backgroundColor}
|
||||
onChange={(color) => {
|
||||
onStyleChange({ backgroundColor: color.hex });
|
||||
}}
|
||||
style={{
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
disableAlpha={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
|
||||
onClick={() => setColorMode("wheel")}
|
||||
>
|
||||
<span className="text-xs text-slate-300 truncate flex-1 text-left">
|
||||
{t("annotation.colorWheel")}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
|
||||
onClick={() => setColorMode("palette")}
|
||||
>
|
||||
<span className="text-xs text-slate-300 truncate flex-1 text-left">
|
||||
{t("annotation.colorPalette")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full mt-2 text-xs h-7 hover:bg-white/5 text-slate-400"
|
||||
onClick={() => {
|
||||
onStyleChange({ backgroundColor: "transparent" });
|
||||
<ColorPicker
|
||||
selectedColor={annotation.style.backgroundColor}
|
||||
colorPalette={colorPalette}
|
||||
translations={{
|
||||
colorWheel: t("annotation.colorWheel"),
|
||||
colorPalette: t("annotation.colorPalette"),
|
||||
clearBackground: t("annotation.clearBackground"),
|
||||
}}
|
||||
>
|
||||
{t("annotation.clearBackground")}
|
||||
</Button>
|
||||
clearBackgroundOption={true}
|
||||
onUpdateColor={(color) => {
|
||||
onStyleChange({ backgroundColor: color });
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import Block from "@uiw/react-color-block";
|
||||
import Colorful from "@uiw/react-color-colorful";
|
||||
import {
|
||||
Bug,
|
||||
Crop,
|
||||
@@ -42,7 +40,7 @@ import { GIF_FRAME_RATES, GIF_SIZE_PRESETS } from "@/lib/exporter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type AspectRatio, isPortraitAspectRatio } from "@/utils/aspectRatioUtils";
|
||||
import { getTestId } from "@/utils/getTestId";
|
||||
import { Input } from "../ui/input";
|
||||
import ColorPicker from "../ui/color-picker";
|
||||
import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel";
|
||||
import { CropControl } from "./CropControl";
|
||||
import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp";
|
||||
@@ -229,7 +227,6 @@ export function SettingsPanel({
|
||||
const t = useScopedT("settings");
|
||||
const [wallpaperPaths, setWallpaperPaths] = useState<string[]>([]);
|
||||
const [customImages, setCustomImages] = useState<string[]>([]);
|
||||
const [backgroundColorMode, setBackgroundColorMode] = useState<"wheel" | "palette">("wheel");
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -322,16 +319,6 @@ export function SettingsPanel({
|
||||
[cropRegion, onCropChange, videoWidth, videoHeight, cropAspectLocked],
|
||||
);
|
||||
|
||||
const getTextColor = (color: string) => {
|
||||
if (color === "transparent") return "#ffffff";
|
||||
const r = parseInt(color.slice(1, 3), 16);
|
||||
const g = parseInt(color.slice(3, 5), 16);
|
||||
const b = parseInt(color.slice(5, 7), 16);
|
||||
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
|
||||
if (luminance > 186) return "#000000";
|
||||
return "#ffffff";
|
||||
};
|
||||
|
||||
const applyCropAspectPreset = useCallback(
|
||||
(preset: string) => {
|
||||
if (!cropRegion || !onCropChange) return;
|
||||
@@ -1001,84 +988,18 @@ export function SettingsPanel({
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="color" className="mt-0">
|
||||
<div className="p-1 flex flex-col gap-4 items-center">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
|
||||
onClick={() => setBackgroundColorMode("wheel")}
|
||||
style={{
|
||||
backgroundColor:
|
||||
backgroundColorMode === "wheel" ? "#34B27B" : "transparent",
|
||||
}}
|
||||
>
|
||||
<span className="text-xs text-slate-300 truncate flex-1 text-left">
|
||||
{t("background.colorWheel")}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
|
||||
onClick={() => setBackgroundColorMode("palette")}
|
||||
style={{
|
||||
backgroundColor:
|
||||
backgroundColorMode === "palette" ? "#34B27B" : "transparent",
|
||||
}}
|
||||
>
|
||||
<span className="text-xs text-slate-300 truncate flex-1 text-left">
|
||||
{t("background.colorPalette")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
{backgroundColorMode === "wheel" && (
|
||||
<>
|
||||
<div
|
||||
className={`w-full h-20 flex items-center justify-center border border-white/10 rounded-lg`}
|
||||
style={{ backgroundColor: selectedColor }}
|
||||
>
|
||||
<span style={{ color: getTextColor(selectedColor) }}>
|
||||
{selectedColor}
|
||||
</span>
|
||||
</div>
|
||||
<Colorful
|
||||
color={selectedColor}
|
||||
onChange={(color) => {
|
||||
setSelectedColor(color.hex);
|
||||
onWallpaperChange(color.hex);
|
||||
}}
|
||||
style={{
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
disableAlpha={true}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={selectedColor}
|
||||
className="w-full h-9 rounded-md border border-white/10 bg-white/5 px-2 text-xs text-slate-200 outline-none focus:border-[#34B27B]/50 focus:ring-1 focus:ring-[#34B27B]/30 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
onChange={(e) => {
|
||||
setSelectedColor(e.target.value);
|
||||
onWallpaperChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{backgroundColorMode === "palette" && (
|
||||
<Block
|
||||
color={selectedColor}
|
||||
colors={colorPalette}
|
||||
onChange={(color) => {
|
||||
setSelectedColor(color.hex);
|
||||
onWallpaperChange(color.hex);
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ColorPicker
|
||||
selectedColor={selectedColor}
|
||||
colorPalette={colorPalette}
|
||||
translations={{
|
||||
colorWheel: t("background.colorWheel"),
|
||||
colorPalette: t("background.colorPalette"),
|
||||
}}
|
||||
onUpdateColor={(color) => {
|
||||
setSelectedColor(color);
|
||||
onWallpaperChange(color);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="gradient" className="mt-0">
|
||||
|
||||
Reference in New Issue
Block a user