novita-anysite / src /components /color-panel.tsx
viktor-hu's picture
Update UI (#8)
25c879d verified
"use client";
import { useState, useEffect, useRef } from "react";
import { Plus, X } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
} from "@/components/ui/tooltip";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
interface ColorCircleProps {
color: string;
onClick?: () => void;
onDelete?: () => void;
className?: string;
size?: "sm" | "md";
showDeleteIcon?: boolean;
}
function ColorCircle({
color,
onClick,
onDelete,
className,
size = "sm",
showDeleteIcon = false,
}: ColorCircleProps) {
const sizeClasses = {
sm: "w-5 h-5",
md: "w-6 h-6",
};
return (
<div className="relative group h-5">
<button
type="button"
onClick={onClick}
className={cn(
sizeClasses[size],
"rounded-full border border-white/20 transition-transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-white/30",
className,
)}
style={{ backgroundColor: color }}
aria-label={`Color ${color}`}
/>
{showDeleteIcon && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onDelete?.();
}}
className="absolute -top-1 -right-1 bg-novita-dark rounded-full w-3.5 h-3.5 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
aria-label={`Remove color ${color}`}
>
<X className="h-2 w-2 text-white" />
</button>
)}
</div>
);
}
const PRESET_COLORS = [
"#23d57c", // Novita green
"#f43f5e", // Rose
"#3b82f6", // Blue
"#eab308", // Yellow
"#8b5cf6", // Purple
"#ec4899", // Pink
"#06b6d4", // Cyan
"#ef4444", // Red
"#84cc16", // Lime
"#14b8a6", // Teal
"#f97316", // Orange
"#6366f1", // Indigo
];
interface ColorPanelProps {
onColorsChange?: (colors: string[]) => void;
}
export function ColorPanel({ onColorsChange }: ColorPanelProps) {
const maxColors = 6;
const [colors, setColors] = useState<string[]>([]);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [selectedColor, setSelectedColor] = useState<string | null>(null);
const [isAddingDisabled, setIsAddingDisabled] = useState(false);
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
return () => {
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
};
}, []);
useEffect(() => {
// Call onColorsChange when colors change
onColorsChange?.(colors);
}, [colors, onColorsChange]);
const addColor = () => {
if (colors.length >= maxColors || !selectedColor || isAddingDisabled)
return;
if (!colors.includes(selectedColor)) {
setIsAddingDisabled(true);
setColors([...colors, selectedColor]);
setSelectedColor(null);
setIsPopoverOpen(false);
// Debounce to prevent rapid clicking
debounceTimeoutRef.current = setTimeout(() => {
setIsAddingDisabled(false);
}, 500);
}
};
const selectColor = (color: string) => {
setSelectedColor(color);
};
const removeColor = (indexToRemove: number) => {
setColors(colors.filter((_, index) => index !== indexToRemove));
};
return (
<TooltipProvider>
<div className="absolute top-2.5 left-3 flex items-center gap-1.5 z-10">
{colors.map((color, index) => (
<ColorCircle
key={`${color}-${index}`}
color={color}
onClick={() => removeColor(index)}
onDelete={() => removeColor(index)}
showDeleteIcon={true}
/>
))}
{colors.length < maxColors && (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<button
type="button"
className="w-5 h-5 rounded-full bg-novita-gray/30 border border-novita-gray/50 flex items-center justify-center transition-transform hover:scale-110 hover:bg-novita-gray/40 focus:outline-none focus:ring-2 focus:ring-white/30"
aria-label="Add color"
>
<Plus className="h-2.5 w-2.5 text-white" />
</button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent className="bg-novita-dark border-novita-gray/30 text-white">
<p>Choose Site Color palette</p>
</TooltipContent>
</Tooltip>
<PopoverContent
className="w-64 p-3 bg-novita-dark border-novita-gray/30 rounded-md shadow-lg"
align="start"
sideOffset={5}
>
<div className="space-y-3">
<h3 className="text-sm font-medium text-white">Color Picker</h3>
<div className="grid grid-cols-6 gap-2">
{PRESET_COLORS.map((color) => (
<ColorCircle
key={color}
color={color}
size="md"
onClick={() => selectColor(color)}
className={cn(
selectedColor === color && "ring-2 ring-white/50",
colors.includes(color) && "opacity-50",
)}
/>
))}
</div>
<div className="pt-2 border-t border-novita-gray/30">
<label className="block text-xs text-novita-gray mb-1.5">
Custom color
</label>
<input
type="color"
className="w-full h-8 bg-transparent border border-novita-gray/30 rounded cursor-pointer"
onChange={(e) => selectColor(e.target.value)}
value={selectedColor || "#ffffff"}
/>
</div>
<div className="mt-3">
<Button
onClick={addColor}
disabled={!selectedColor || isAddingDisabled}
className="w-full h-6 bg-novita-white hover:bg-novita-gray/90 text-white rounded border border-novita-gray/90"
>
Add
</Button>
</div>
</div>
</PopoverContent>
</Popover>
)}
</div>
</TooltipProvider>
);
}