[Fix] Color-Picker: When we cancel, press on anywhere on screen the component causing layout shifting

This commit is contained in:
=AhmedAshraf 2026-03-22 10:33:47 +02:00
parent 89d43a2ef7
commit 810b099707
4 changed files with 161 additions and 34 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1,23 +1,121 @@
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { Brush } from "lucide-react"
import { useState } from "react";
import { AlertCircle, Brush } from "lucide-react";
import { useEffect, useState } from "react";
import { HexColorPicker } from "react-colorful";
interface Props {
// memo: Memo;
className?: string;
onOpenChange?: (open: boolean) => void;
onSavePreferences?: (colors: { bgColor: string; textColor: string }) => Promise<void> | void;
}
function MemoCustomizeColor(props:Props) {
const {className,onOpenChange} = props;
const [open, setOpen] = useState(false);
const [bgColor, setBgColor] = useState("#121212");
const STORAGE_KEY = "memo-customize-color";
const MIN_CONTRAST_RATIO = 4.5;
const parseHexColor = (hex: string) => {
const normalized = hex.trim().replace("#", "");
if (normalized.length !== 6) {
return null;
}
const r = Number.parseInt(normalized.slice(0, 2), 16);
const g = Number.parseInt(normalized.slice(2, 4), 16);
const b = Number.parseInt(normalized.slice(4, 6), 16);
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
return null;
}
return { r, g, b };
};
const relativeLuminance = (hex: string) => {
const rgb = parseHexColor(hex);
if (!rgb) {
return null;
}
const transform = (channel: number) => {
const sRgb = channel / 255;
return sRgb <= 0.03928 ? sRgb / 12.92 : Math.pow((sRgb + 0.055) / 1.055, 2.4);
};
const r = transform(rgb.r);
const g = transform(rgb.g);
const b = transform(rgb.b);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const getContrastRatio = (foreground: string, background: string) => {
const l1 = relativeLuminance(foreground);
const l2 = relativeLuminance(background);
if (l1 == null || l2 == null) {
return null;
}
const light = Math.max(l1, l2);
const dark = Math.min(l1, l2);
return (light + 0.05) / (dark + 0.05);
};
function MemoCustomizeColor(props: Props) {
const { className, onOpenChange, onSavePreferences } = props;
const [open, setOpen] = useState(false);
const [bgColor, setBgColor] = useState("#121212");
const [textColor, setTextColor] = useState("#FFFFFF");
const [showBgPicker, setShowBgPicker] = useState(false);
const [showTextPicker, setShowTextPicker] = useState(false);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
try {
const stored = window.localStorage.getItem(STORAGE_KEY);
if (!stored) {
return;
}
const parsed = JSON.parse(stored) as {
bgColor?: string;
textColor?: string;
};
if (parsed.bgColor) {
setBgColor(parsed.bgColor);
}
if (parsed.textColor) {
setTextColor(parsed.textColor);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error("Failed to load memo color preferences", error);
}
}, []);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
try {
window.localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
bgColor,
textColor,
}),
);
} catch {
// Ignore write errors (e.g., private mode)
}
}, [bgColor, textColor]);
const bgPresets = ["#121212", "#2c2f33", "#1d3557", "#2d6a4f", "#601010", "#000000"];
const textPresets = ["#FFFFFF", "#E1E8ED", "#89CFF0", "#C7F9CC", "#FEFAE0", "#FAD2E1"];
@ -25,26 +123,44 @@ const [open, setOpen] = useState(false);
setOpen(newOpen);
onOpenChange?.(newOpen);
};
const handleCancel = () => {
handleOpenChange(false);
};
const handleSave = async () => {
try {
if (onSavePreferences) {
await onSavePreferences({
bgColor,
textColor,
});
}
handleOpenChange(false);
} catch (error) {
console.error("Failed to save memo color preferences", error);
}
};
const contrastRatio = getContrastRatio(textColor, bgColor);
return (
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<span
className={cn(
"h-7 w-7 flex justify-center items-center rounded-full border cursor-pointer transition-all hover:opacity-80",
className,
)}
>
<Brush className="w-4 h-4 mx-auto text-muted-foreground" />
</span>
<PopoverTrigger asChild>
<span
className={cn(
"h-7 w-7 flex justify-center items-center rounded-full border cursor-pointer transition-all hover:opacity-80",
className,
)}
>
<Brush className="w-4 h-4 mx-auto text-muted-foreground" />
</span>
</PopoverTrigger>
</PopoverTrigger>
<PopoverContent align="start" className="max-w-[90vw] sm:max-w-md ">
<div className="flex items-center justify-center ">
<PopoverContent
side="bottom"
align="end"
className="max-w-[90vw] sm:max-w-md data-[state=open]:animate-none data-[state=closed]:animate-none"
>
<div className="w-full max-w-md bg-[#1a1a1a] rounded-xl border border-gray-800 overflow-hidden shadow-2xl">
{/* Header */}
<div className="p-4 border-b border-gray-800">
@ -62,12 +178,18 @@ const [open, setOpen] = useState(false);
<div className="flex gap-2">🎨 </div>
</div>
<p className="text-xl font-bold mb-4" style={{ color: textColor }}>
EL DONIA DH BTA3TIIIIIIIIII
Content Should be here...
</p>
<div className="flex gap-2">
<span className="bg-gray-800/50 p-1 px-3 rounded-full text-xs">👍 1</span>
<span className="bg-gray-800/50 p-1 px-3 rounded-full text-xs"> 1</span>
</div>
{contrastRatio != null && contrastRatio < MIN_CONTRAST_RATIO && (
<div className="mt-3 flex items-center gap-2 text-xs text-amber-400">
<AlertCircle className="w-3 h-3" />
<span>Low contrast may affect readability.</span>
</div>
)}
</div>
{/* Background Color Section */}
@ -129,13 +251,22 @@ const [open, setOpen] = useState(false);
{/* Footer Actions */}
<div className="flex justify-end gap-3 p-4 bg-black/20 border-t border-gray-800">
<button className="text-gray-400 hover:text-white px-4 py-2 transition text-sm">Cancel</button>
<button className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium transition text-sm">
<button
type="button"
className="text-gray-400 hover:text-white px-4 py-2 transition text-sm"
onClick={handleCancel}
>
Cancel
</button>
<button
type="button"
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium transition text-sm"
onClick={handleSave}
>
Save Preferences
</button>
</div>
</div>
</div>
</PopoverContent>
</Popover>

View File

@ -22,7 +22,6 @@ import MemoCustomizeColor from "./MemoCustomizeColor";
const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, showPinned }) => {
const t = useTranslate();
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
const [customizeColorToggle,setCustomizeColorToggle]= useState(false);
const { memo, creator, currentUser, parentPage, isArchived, readonly, openEditor } = useMemoViewContext();
const { relativeTimeFormat } = useMemoViewDerived();
@ -61,10 +60,7 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
onOpenChange={setReactionSelectorOpen}
/>
)}
<MemoCustomizeColor
className={cn("border-none w-auto h-auto", customizeColorToggle && "block!", "block sm:hidden sm:group-hover:block")}
onOpenChange={setCustomizeColorToggle}
/>
<MemoCustomizeColor className="border-none w-auto h-auto" />
{showVisibility && memo.visibility !== Visibility.PRIVATE && (
<Tooltip>
<TooltipTrigger>