mirror of https://github.com/usememos/memos.git
[Fix] Color-Picker: When we cancel, press on anywhere on screen the component causing layout shifting
This commit is contained in:
parent
89d43a2ef7
commit
810b099707
Binary file not shown.
Binary file not shown.
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue