diff --git a/web/src/components/MasonryView/MasonryColumn.tsx b/web/src/components/MasonryView/MasonryColumn.tsx index 9876435f2..4fbc19dc1 100644 --- a/web/src/components/MasonryView/MasonryColumn.tsx +++ b/web/src/components/MasonryView/MasonryColumn.tsx @@ -8,11 +8,12 @@ export function MasonryColumn({ renderContext, onHeightChange, isFirstColumn, + listMode, prefixElement, prefixElementRef, }: MasonryColumnProps) { return ( -
+
{/* Prefix element (like memo editor) goes in first column */} {isFirstColumn && prefixElement &&
{prefixElement}
} diff --git a/web/src/components/MasonryView/MasonryView.tsx b/web/src/components/MasonryView/MasonryView.tsx index f20067b16..f94c49b24 100644 --- a/web/src/components/MasonryView/MasonryView.tsx +++ b/web/src/components/MasonryView/MasonryView.tsx @@ -36,6 +36,7 @@ const MasonryView = ({ memoList, renderer, prefixElement, listMode = false }: Ma renderContext={renderContext} onHeightChange={handleHeightChange} isFirstColumn={columnIndex === 0} + listMode={listMode} prefixElement={prefixElement} prefixElementRef={prefixElementRef} /> diff --git a/web/src/components/MasonryView/types.ts b/web/src/components/MasonryView/types.ts index 862ea4ddd..7810049e7 100644 --- a/web/src/components/MasonryView/types.ts +++ b/web/src/components/MasonryView/types.ts @@ -26,6 +26,7 @@ export interface MasonryColumnProps { renderContext: MemoRenderContext; onHeightChange: (memoName: string, height: number) => void; isFirstColumn: boolean; + listMode?: boolean; prefixElement?: JSX.Element; prefixElementRef?: React.RefObject; } diff --git a/web/src/components/MemoContent/MermaidBlock.tsx b/web/src/components/MemoContent/MermaidBlock.tsx index 48ec20bb1..cbd210e42 100644 --- a/web/src/components/MemoContent/MermaidBlock.tsx +++ b/web/src/components/MemoContent/MermaidBlock.tsx @@ -11,7 +11,7 @@ interface MermaidBlockProps { } const getMermaidTheme = (appTheme: string): "default" | "dark" => { - return appTheme === "default-dark" ? "dark" : "default"; + return appTheme.includes("dark") ? "dark" : "default"; }; export const MermaidBlock = ({ children, className }: MermaidBlockProps) => { diff --git a/web/src/components/PagedMemoList/PagedMemoList.tsx b/web/src/components/PagedMemoList/PagedMemoList.tsx index 88f6d4590..67afa122b 100644 --- a/web/src/components/PagedMemoList/PagedMemoList.tsx +++ b/web/src/components/PagedMemoList/PagedMemoList.tsx @@ -1,6 +1,6 @@ import { useQueryClient } from "@tanstack/react-query"; import toast from "react-hot-toast"; -import { ArchiveIcon, ArrowUpIcon, BookmarkPlusIcon, TrashIcon, XIcon } from "lucide-react"; +import { ArchiveIcon, ArrowUpIcon, BookmarkPlusIcon, ChevronDownIcon, ChevronRightIcon, TrashIcon, XIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { matchPath } from "react-router-dom"; @@ -33,6 +33,7 @@ interface Props { pageSize?: number; showCreator?: boolean; enabled?: boolean; + collapsiblePinned?: boolean; } function useAutoFetchWhenNotScrollable({ @@ -112,6 +113,29 @@ const PagedMemoList = (props: Props) => { // Apply custom sorting if provided, otherwise use memos directly const sortedMemoList = useMemo(() => (props.listSort ? props.listSort(memos) : memos), [memos, props.listSort]); + const enablePinnedSection = props.collapsiblePinned === true; + const pinnedStorageKey = "memos.ui.pinsCollapsed"; + + const [isPinnedCollapsed, setIsPinnedCollapsed] = useState(() => { + if (!enablePinnedSection) return false; + if (typeof window === "undefined") return false; + return window.localStorage.getItem(pinnedStorageKey) === "true"; + }); + + const pinnedMemos = useMemo(() => { + if (!enablePinnedSection) return []; + return sortedMemoList.filter((memo) => memo.pinned); + }, [enablePinnedSection, sortedMemoList]); + + const unpinnedMemos = useMemo(() => { + if (!enablePinnedSection) return sortedMemoList; + return sortedMemoList.filter((memo) => !memo.pinned); + }, [enablePinnedSection, sortedMemoList]); + + useEffect(() => { + if (!enablePinnedSection || typeof window === "undefined") return; + window.localStorage.setItem(pinnedStorageKey, String(isPinnedCollapsed)); + }, [enablePinnedSection, isPinnedCollapsed]); const selectionContextValue = useMemo(() => { const selectedCount = selectedMemoNames.size; @@ -221,19 +245,71 @@ const PagedMemoList = (props: Props) => { ) : ( <> - { + const hasPinned = pinnedMemos.length > 0; + const pinnedToggle = enablePinnedSection && hasPinned && ( +
+ + + ); + + const prefixElement = ( <> {showMemoEditor ? ( ) : undefined} + {pinnedToggle} + ); + + if (!enablePinnedSection) { + return ; } - listMode={layout === "LIST"} - /> + + if (layout === "LIST") { + const listMemoList = isPinnedCollapsed ? unpinnedMemos : sortedMemoList; + const lastPinnedName = !isPinnedCollapsed && hasPinned ? pinnedMemos[pinnedMemos.length - 1]?.name : undefined; + const listRenderer = lastPinnedName + ? (memo: Memo, context?: MemoRenderContext) => ( + <> + {props.renderer(memo, context)} + {memo.name === lastPinnedName && ( +