[Feature] Color-Picker: Added Color Picker, For Memos and it's Comments as it's depending on the color pref. of specific memo

This commit is contained in:
=AhmedAshraf 2026-03-22 11:34:10 +02:00
parent 810b099707
commit 967febf131
14 changed files with 141 additions and 39 deletions

Binary file not shown.

Binary file not shown.

View File

@ -57,7 +57,7 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-4">
<MoreVerticalIcon className="text-muted-foreground" />
<MoreVerticalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={2}>

View File

@ -35,7 +35,7 @@ const MemoContent = (props: MemoContentProps) => {
const compactLabel = useCompactLabel(showCompactMode, t as (key: string) => string);
return (
<div className={`w-full flex flex-col justify-start items-start text-foreground ${className || ""}`}>
<div className={`w-full flex flex-col justify-start items-start ${className || ""}`}>
<div
ref={memoContentContainerRef}
data-memo-content

View File

@ -36,7 +36,7 @@ const ReactionSelector = (props: Props) => {
className,
)}
>
<SmilePlusIcon className="w-4 h-4 mx-auto text-muted-foreground" />
<SmilePlusIcon className="w-4 h-4 mx-auto" />
</span>
</PopoverTrigger>
<PopoverContent align="center" className="max-w-[90vw] sm:max-w-md">

View File

@ -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",

View File

@ -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<MemoViewProps> = (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<HTMLDivElement>(null);
const [showEditor, setShowEditor] = useState(false);
@ -38,6 +47,61 @@ const MemoView: React.FC<MemoViewProps> = (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<MemoViewProps> = (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
}
>
<MemoHeader showCreator={showCreator} showVisibility={showVisibility} showPinned={showPinned} />
<MemoHeader
name={memoData.name}
showCreator={showCreator}
showVisibility={showVisibility}
showPinned={showPinned}
showColorCustomizer={!memoData.parent}
onColorPreferencesChange={(colors) => setCustomColors(colors)}
/>
<MemoBody compact={compact} />

View File

@ -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> | 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,
)}
>
<Brush className="w-4 h-4 mx-auto text-muted-foreground" />
<Brush className="w-4 h-4 mx-auto" />
</span>
</PopoverTrigger>

View File

@ -19,7 +19,7 @@ import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext";
import type { MemoHeaderProps } from "../types";
import MemoCustomizeColor from "./MemoCustomizeColor";
const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, showPinned }) => {
const MemoHeader: React.FC<MemoHeaderProps> = ({ 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<MemoHeaderProps> = ({ showCreator, showVisibility, sh
onOpenChange={setReactionSelectorOpen}
/>
)}
<MemoCustomizeColor className="border-none w-auto h-auto" />
{showColorCustomizer && (
<MemoCustomizeColor
name={name}
className="border-none w-auto h-auto"
onSavePreferences={onColorPreferencesChange}
/>
)}
{showVisibility && memo.visibility !== Visibility.PRIVATE && (
<Tooltip>
<TooltipTrigger>
@ -79,7 +85,7 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-pointer">
<BookmarkIcon className="w-4 h-auto text-primary" onClick={unpinMemo} />
<BookmarkIcon className="w-4 h-auto" onClick={unpinMemo} />
</span>
</TooltipTrigger>
<TooltipContent>
@ -108,7 +114,7 @@ const CreatorDisplay: React.FC<CreatorDisplayProps> = ({ creator, displayTime, o
</Link>
<div className="w-full flex flex-col justify-center items-start">
<Link
className="block leading-tight hover:opacity-80 rounded-md transition-colors truncate text-muted-foreground"
className="block leading-tight hover:opacity-80 rounded-md transition-colors truncate"
to={`/u/${encodeURIComponent(creator.username)}`}
viewTransition
>
@ -116,7 +122,7 @@ const CreatorDisplay: React.FC<CreatorDisplayProps> = ({ creator, displayTime, o
</Link>
<button
type="button"
className="w-auto -mt-0.5 text-xs leading-tight text-muted-foreground select-none cursor-pointer hover:opacity-80 transition-colors text-left"
className="w-auto -mt-0.5 text-xs leading-tight select-none cursor-pointer hover:opacity-80 transition-colors text-left"
onClick={onGotoDetail}
>
{displayTime}
@ -133,7 +139,7 @@ interface TimeDisplayProps {
const TimeDisplay: React.FC<TimeDisplayProps> = ({ displayTime, onGotoDetail }) => (
<button
type="button"
className="w-full text-sm leading-tight text-muted-foreground select-none cursor-pointer hover:text-foreground transition-colors text-left"
className="w-full text-sm leading-tight select-none cursor-pointer hover:opacity-80 transition-colors text-left"
onClick={onGotoDetail}
>
{displayTime}

View File

@ -8,12 +8,16 @@ export interface MemoViewProps {
showPinned?: boolean;
className?: string;
parentPage?: string;
colorKey?: string;
}
export interface MemoHeaderProps {
name:string;
showCreator?: boolean;
showVisibility?: boolean;
showPinned?: boolean;
onColorPreferencesChange?: (colors: { bgColor: string; textColor: string }) => void;
showColorCustomizer?: boolean;
}
export interface MemoBodyProps {

View File

@ -97,6 +97,7 @@ const PagedMemoList = (props: Props) => {
// Flatten pages into a single array of memos
const memos = useMemo(() => data?.pages.flatMap((page) => page.memos) || [], [data]);
console.log("Memoss: ",memos);
// Apply custom sorting if provided, otherwise use memos directly
const sortedMemoList = useMemo(() => (props.listSort ? props.listSort(memos) : memos), [memos, props.listSort]);

View File

@ -22,7 +22,7 @@ const VisibilityIcon = (props: Props) => {
return null;
}
return <VIcon className={cn("w-4 h-auto text-muted-foreground", className)} />;
return <VIcon className={cn("w-4 h-auto", className)} />;
};
export default VisibilityIcon;

View File

@ -17,6 +17,13 @@ export const memoKeys = {
};
export function useMemos(request: Partial<ListMemosRequest> = {}) {
console.log(`Memos: `,useQuery({
queryKey: memoKeys.list(request),
queryFn: async () => {
const response = await memoServiceClient.listMemos(create(ListMemosRequestSchema, request as Record<string, unknown>));
return response;
},
}));
return useQuery({
queryKey: memoKeys.list(request),
queryFn: async () => {

View File

@ -122,6 +122,7 @@ const MemoDetail = () => {
showCreator
showVisibility
showPinned
colorKey={memo.name}
/>
<div className="pt-8 pb-16 w-full">
<h2 id="comments" className="sr-only">
@ -165,7 +166,7 @@ const MemoDetail = () => {
)}
{comments.map((comment) => (
<div className="w-full" key={`${comment.name}-${comment.displayTime}`} id={extractMemoIdFromName(comment.name)}>
<MemoView memo={comment} parentPage={locationState?.from} showCreator compact />
<MemoView memo={comment} parentPage={locationState?.from} showCreator compact colorKey={memo.name} />
</div>
))}
</div>