diff --git a/memos_prod.db-shm b/memos_prod.db-shm index 99ea35d80..ad56e3d15 100644 Binary files a/memos_prod.db-shm and b/memos_prod.db-shm differ diff --git a/memos_prod.db-wal b/memos_prod.db-wal index 27df24d2f..63af6c026 100644 Binary files a/memos_prod.db-wal and b/memos_prod.db-wal differ diff --git a/web/src/components/MemoActionMenu/MemoActionMenu.tsx b/web/src/components/MemoActionMenu/MemoActionMenu.tsx index 1f2739db2..90686c861 100644 --- a/web/src/components/MemoActionMenu/MemoActionMenu.tsx +++ b/web/src/components/MemoActionMenu/MemoActionMenu.tsx @@ -57,7 +57,7 @@ const MemoActionMenu = (props: MemoActionMenuProps) => { diff --git a/web/src/components/MemoContent/index.tsx b/web/src/components/MemoContent/index.tsx index b54bd8147..60906c8b9 100644 --- a/web/src/components/MemoContent/index.tsx +++ b/web/src/components/MemoContent/index.tsx @@ -35,7 +35,7 @@ const MemoContent = (props: MemoContentProps) => { const compactLabel = useCompactLabel(showCompactMode, t as (key: string) => string); return ( -
+
{ className, )} > - + diff --git a/web/src/components/MemoReactionListView/ReactionView.tsx b/web/src/components/MemoReactionListView/ReactionView.tsx index c0beae916..824eb291a 100644 --- a/web/src/components/MemoReactionListView/ReactionView.tsx +++ b/web/src/components/MemoReactionListView/ReactionView.tsx @@ -35,7 +35,7 @@ const ReactionView = (props: Props) => { type="button" className={cn( "h-7 border px-2 py-0.5 rounded-full flex flex-row justify-center items-center gap-1", - "text-sm text-muted-foreground", + "text-sm", isClickable && "cursor-pointer", !isClickable && "cursor-default", hasReaction && "bg-accent border-border", diff --git a/web/src/components/MemoView/MemoView.tsx b/web/src/components/MemoView/MemoView.tsx index f202ea7ab..359a40a70 100644 --- a/web/src/components/MemoView/MemoView.tsx +++ b/web/src/components/MemoView/MemoView.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useMemo, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useLocation } from "react-router-dom"; import useCurrentUser from "@/hooks/useCurrentUser"; import { useUser } from "@/hooks/useUserQueries"; @@ -14,7 +14,16 @@ import { computeCommentAmount, MemoViewContext } from "./MemoViewContext"; import type { MemoViewProps } from "./types"; const MemoView: React.FC = (props: MemoViewProps) => { - const { memo: memoData, className, parentPage: parentPageProp, compact, showCreator, showVisibility, showPinned } = props; + const { + memo: memoData, + className, + parentPage: parentPageProp, + compact, + showCreator, + showVisibility, + showPinned, + colorKey, + } = props; const cardRef = useRef(null); const [showEditor, setShowEditor] = useState(false); @@ -38,6 +47,61 @@ const MemoView: React.FC = (props: MemoViewProps) => { const isInMemoDetailPage = location.pathname.startsWith(`/${memoData.name}`); const showCommentPreview = !isInMemoDetailPage && computeCommentAmount(memoData) > 0; + const [customColors, setCustomColors] = useState<{ bgColor?: string; textColor?: string } | null>(null); + + useEffect(() => { + if (typeof window === "undefined") { + return; + } + + const storageKey = colorKey || memoData.name; + + try { + const stored = window.localStorage.getItem(storageKey); + if (!stored) { + return; + } + + const parsed = JSON.parse(stored) as { bgColor?: string; textColor?: string }; + setCustomColors({ + bgColor: parsed.bgColor, + textColor: parsed.textColor, + }); + } catch { + // Ignore malformed values + } + }, [colorKey, memoData.name]); + + useEffect(() => { + if (typeof window === "undefined") { + return; + } + + const storageKey = colorKey || memoData.name; + + const handleColorChange = (event: Event) => { + const customEvent = event as CustomEvent<{ + key: string; + colors: { bgColor?: string; textColor?: string }; + }>; + + if (!customEvent.detail || customEvent.detail.key !== storageKey) { + return; + } + + setCustomColors({ + bgColor: customEvent.detail.colors.bgColor, + textColor: customEvent.detail.colors.textColor, + }); + }; + + window.addEventListener("memo-colors-changed", handleColorChange as EventListener); + + return () => { + window.removeEventListener("memo-colors-changed", handleColorChange as EventListener); + }; + }, [colorKey, memoData.name]); + const contextValue = useMemo( () => ({ memo: memoData, @@ -85,8 +149,20 @@ const MemoView: React.FC = (props: MemoViewProps) => { className={cn(MEMO_CARD_BASE_CLASSES, showCommentPreview ? "mb-0 rounded-b-none" : "mb-2", className)} ref={cardRef} tabIndex={readonly ? -1 : 0} + style={ + customColors?.bgColor || customColors?.textColor + ? { backgroundColor: customColors?.bgColor, color: customColors?.textColor } + : undefined + } > - + setCustomColors(colors)} + /> diff --git a/web/src/components/MemoView/components/MemoCustomizeColor.tsx b/web/src/components/MemoView/components/MemoCustomizeColor.tsx index 4e53f75b9..6cd568155 100644 --- a/web/src/components/MemoView/components/MemoCustomizeColor.tsx +++ b/web/src/components/MemoView/components/MemoCustomizeColor.tsx @@ -5,12 +5,11 @@ import { useEffect, useState } from "react"; import { HexColorPicker } from "react-colorful"; interface Props { + name:string; className?: string; onOpenChange?: (open: boolean) => void; onSavePreferences?: (colors: { bgColor: string; textColor: string }) => Promise | void; } - -const STORAGE_KEY = "memo-customize-color"; const MIN_CONTRAST_RATIO = 4.5; const parseHexColor = (hex: string) => { @@ -63,7 +62,7 @@ const getContrastRatio = (foreground: string, background: string) => { }; function MemoCustomizeColor(props: Props) { - const { className, onOpenChange, onSavePreferences } = props; + const { className, onOpenChange, onSavePreferences,name } = props; const [open, setOpen] = useState(false); const [bgColor, setBgColor] = useState("#121212"); const [textColor, setTextColor] = useState("#FFFFFF"); @@ -76,7 +75,7 @@ function MemoCustomizeColor(props: Props) { } try { - const stored = window.localStorage.getItem(STORAGE_KEY); + const stored = window.localStorage.getItem(name); if (!stored) { return; } @@ -96,25 +95,7 @@ function MemoCustomizeColor(props: Props) { // 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]); + }, [name]); const bgPresets = ["#121212", "#2c2f33", "#1d3557", "#2d6a4f", "#601010", "#000000"]; const textPresets = ["#FFFFFF", "#E1E8ED", "#89CFF0", "#C7F9CC", "#FEFAE0", "#FAD2E1"]; @@ -130,6 +111,32 @@ function MemoCustomizeColor(props: Props) { const handleSave = async () => { try { + if (typeof window !== "undefined") { + try { + window.localStorage.setItem( + name, + JSON.stringify({ + bgColor, + textColor, + }), + ); + + window.dispatchEvent( + new CustomEvent("memo-colors-changed", { + detail: { + key: name, + colors: { + bgColor, + textColor, + }, + }, + }), + ); + } catch { + // Ignore write errors + } + } + if (onSavePreferences) { await onSavePreferences({ bgColor, @@ -152,7 +159,7 @@ function MemoCustomizeColor(props: Props) { className, )} > - + diff --git a/web/src/components/MemoView/components/MemoHeader.tsx b/web/src/components/MemoView/components/MemoHeader.tsx index e069812be..36419cf4e 100644 --- a/web/src/components/MemoView/components/MemoHeader.tsx +++ b/web/src/components/MemoView/components/MemoHeader.tsx @@ -19,7 +19,7 @@ import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext"; import type { MemoHeaderProps } from "../types"; import MemoCustomizeColor from "./MemoCustomizeColor"; -const MemoHeader: React.FC = ({ showCreator, showVisibility, showPinned }) => { +const MemoHeader: React.FC = ({ name, showCreator, showVisibility, showPinned, onColorPreferencesChange, showColorCustomizer = true }) => { const t = useTranslate(); const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false); const { memo, creator, currentUser, parentPage, isArchived, readonly, openEditor } = useMemoViewContext(); @@ -60,7 +60,13 @@ const MemoHeader: React.FC = ({ showCreator, showVisibility, sh onOpenChange={setReactionSelectorOpen} /> )} - + {showColorCustomizer && ( + + )} {showVisibility && memo.visibility !== Visibility.PRIVATE && ( @@ -79,7 +85,7 @@ const MemoHeader: React.FC = ({ showCreator, showVisibility, sh - + @@ -108,7 +114,7 @@ const CreatorDisplay: React.FC = ({ creator, displayTime, o
@@ -116,7 +122,7 @@ const CreatorDisplay: React.FC = ({ creator, displayTime, o