diff --git a/apps/web/components/memories-grid.tsx b/apps/web/components/memories-grid.tsx index d2f93d544..ef617f809 100644 --- a/apps/web/components/memories-grid.tsx +++ b/apps/web/components/memories-grid.tsx @@ -394,11 +394,14 @@ export function MemoriesGrid({ return items }, [documents, isMobile, hasQuickNote, isSelectionMode, selectedDocumentIds]) - // Stable key for Masonry based on document IDs, not item values + // Reset Masonry when the actual rendered item set changes. Masonic caches + // positions by index, so mobile removing the quick note must remount it. const masonryKey = useMemo(() => { - const docIds = documents.map((d) => d.id).join(",") - return `masonry-${documents.length}-${docIds}-${isChatOpen}-${hasQuickNote}` - }, [documents, isChatOpen, hasQuickNote]) + const itemIds = masonryItems.map((item) => item.id).join(",") + return `masonry-${isMobile ? "mobile" : "desktop"}-${masonryItems.length}-${itemIds}-${isChatOpen}` + }, [masonryItems, isChatOpen, isMobile]) + + const getMasonryItemKey = useCallback((item: MasonryItem) => item.id, []) const isLoadingMore = isFetchingNextPage @@ -542,9 +545,9 @@ export function MemoriesGrid({ {!isEmpty && !isSelectionMode && (
-
+
-
+
{/* View mode toggle — segmented control */}
void @@ -13,24 +16,56 @@ interface QuickNoteCardProps { isSaving?: boolean } +type NoteRect = { + left: number + top: number + width: number + height: number +} + +function getExpandedRect(source: NoteRect): NoteRect { + const viewportWidth = window.innerWidth + const viewportHeight = window.innerHeight + const margin = viewportWidth < 768 ? 16 : 32 + const availableWidth = viewportWidth - source.left - margin + const availableHeight = viewportHeight - source.top - margin + const width = Math.min(1008, Math.max(source.width, availableWidth)) + const height = Math.min(720, Math.max(source.height, availableHeight)) + + return { + left: source.left, + top: source.top, + width, + height, + } +} + export function QuickNoteCard({ onSave, onMaximize, isSaving = false, }: QuickNoteCardProps) { - const textareaRef = useRef(null) + const cardRef = useRef(null) + const wasSavingRef = useRef(isSaving) const { selectedProject } = useProject() const { draft, setDraft } = useQuickNoteDraft(selectedProject) + const [isExpanded, setIsExpanded] = useState(false) + const [sourceRect, setSourceRect] = useState(null) + const [targetRect, setTargetRect] = useState(null) + const [expandedInitialContent, setExpandedInitialContent] = useState< + string | undefined + >(undefined) + const [isMounted, setIsMounted] = useState(false) const handleChange = useCallback( - (e: React.ChangeEvent) => { - setDraft(e.target.value) + (content: string) => { + setDraft(content) }, [setDraft], ) const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { + (e: React.KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault() if (draft.trim() && !isSaving) { @@ -41,6 +76,27 @@ export function QuickNoteCard({ [draft, isSaving, onSave], ) + const handleExpand = useCallback(() => { + const rect = cardRef.current?.getBoundingClientRect() + if (!rect) return + + const nextSourceRect = { + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + } + + setSourceRect(nextSourceRect) + setTargetRect(getExpandedRect(nextSourceRect)) + setExpandedInitialContent(draft || undefined) + setIsExpanded(true) + }, [draft]) + + const handleClose = useCallback(() => { + setIsExpanded(false) + }, []) + const handleSaveClick = useCallback(() => { if (draft.trim() && !isSaving) { onSave(draft) @@ -48,112 +104,279 @@ export function QuickNoteCard({ }, [draft, isSaving, onSave]) const handleMaximizeClick = useCallback(() => { + setIsExpanded(false) onMaximize(draft) }, [draft, onMaximize]) const canSave = draft.trim().length > 0 && !isSaving + const previewText = useMemo(() => { + const trimmed = draft.trim() + if (!trimmed) return null + return trimmed.replace(/\s+/g, " ") + }, [draft]) + + useEffect(() => { + setIsMounted(true) + }, []) + + useEffect(() => { + if (!isExpanded || !sourceRect) return + + const handleResize = () => setTargetRect(getExpandedRect(sourceRect)) + window.addEventListener("resize", handleResize) + return () => window.removeEventListener("resize", handleResize) + }, [isExpanded, sourceRect]) + + useEffect(() => { + if (!isExpanded) return + + const previousOverflow = document.body.style.overflow + document.body.style.overflow = "hidden" + + const handleGlobalKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault() + handleClose() + } + } + + document.addEventListener("keydown", handleGlobalKeyDown) + + return () => { + document.body.style.overflow = previousOverflow + document.removeEventListener("keydown", handleGlobalKeyDown) + } + }, [isExpanded, handleClose]) + + useEffect(() => { + if (wasSavingRef.current && !isSaving && draft.trim().length === 0) { + setIsExpanded(false) + } + wasSavingRef.current = isSaving + }, [draft, isSaving]) return ( -
+ <>
- - -