From 810b099707c02f1a0a7d7054fafadcedcde394dd Mon Sep 17 00:00:00 2001 From: =AhmedAshraf <=ahmedashrafelgendy25@gmail.com> Date: Sun, 22 Mar 2026 10:33:47 +0200 Subject: [PATCH] [Fix] Color-Picker: When we cancel, press on anywhere on screen the component causing layout shifting --- memos_prod.db-shm | Bin 32768 -> 32768 bytes memos_prod.db-wal | Bin 181312 -> 189552 bytes .../components/MemoCustomizeColor.tsx | 189 +++++++++++++++--- .../MemoView/components/MemoHeader.tsx | 6 +- 4 files changed, 161 insertions(+), 34 deletions(-) diff --git a/memos_prod.db-shm b/memos_prod.db-shm index e24ab1c907c64574043c774436678f17b94c2204..99ea35d8089af232d6bca72d4facc314cb389ed1 100644 GIT binary patch delta 165 zcmZo@U}|V!s+V}A%K!rWK+MR%AfN}NC4l(#l)&c?4zE~cx5x8-uD;C=+0AS3wVYVK zhg9`Iqrd=UE(rgJ0*ENX#QL3+Wf(a&TQE*>*nGuNPncPkL2t7o<7=kPAHoGR0E|F2 A{r~^~ delta 161 zcmZo@U}|V!s+V}A%K!qrK+MR%AfN-JC4l%sT*y`C1>Dwldpz&w>f8KyrX=QVf3I{o wsp^47fdR-|5dIGZ5K)GS^*cAaFivoo9Kgu4`HG{S@Mb5*cTAfEkD39{cdK6-`oRPySwRacBSR}=6FqZ7V{=0b z6Jv|%`yH5M`H_Sz3=GV+zjk0^VVqv)%p}C|JaEeERVxlZ*zT|eYH2j%^v**}CbY4X HDYg~>fm2OX delta 13 Ucmexxg8M)RcS8%~7AA)y04;3=)&Kwi diff --git a/web/src/components/MemoView/components/MemoCustomizeColor.tsx b/web/src/components/MemoView/components/MemoCustomizeColor.tsx index 21596d88d..4e53f75b9 100644 --- a/web/src/components/MemoView/components/MemoCustomizeColor.tsx +++ b/web/src/components/MemoView/components/MemoCustomizeColor.tsx @@ -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; } -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 ( - - - - + + + + + - - - - - - - -
+
{/* Header */}
@@ -62,12 +178,18 @@ const [open, setOpen] = useState(false);
🎨 ⚙️ ︙

- EL DONIA DH BTA3TIIIIIIIIII + Content Should be here...

👍 1 ❤️ 1
+ {contrastRatio != null && contrastRatio < MIN_CONTRAST_RATIO && ( +
+ + Low contrast may affect readability. +
+ )}
{/* Background Color Section */} @@ -129,13 +251,22 @@ const [open, setOpen] = useState(false); {/* Footer Actions */}
- - +
-
diff --git a/web/src/components/MemoView/components/MemoHeader.tsx b/web/src/components/MemoView/components/MemoHeader.tsx index 4e9746e91..e069812be 100644 --- a/web/src/components/MemoView/components/MemoHeader.tsx +++ b/web/src/components/MemoView/components/MemoHeader.tsx @@ -22,7 +22,6 @@ import MemoCustomizeColor from "./MemoCustomizeColor"; const MemoHeader: React.FC = ({ 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 = ({ showCreator, showVisibility, sh onOpenChange={setReactionSelectorOpen} /> )} - + {showVisibility && memo.visibility !== Visibility.PRIVATE && (