From 9f5a9e580c8ebd207475e8bc3d2052ef783e1846 Mon Sep 17 00:00:00 2001 From: "vorflux[bot]" <249966464+vorflux[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 08:47:19 -0700 Subject: [PATCH 1/6] fix: graph controls gap on compact viewports + model selector z-index (#982) Co-authored-by: Vorflux AI Co-authored-by: Vedant Mahajan --- apps/web/components/chat/model-selector.tsx | 2 +- packages/memory-graph/package.json | 2 +- .../memory-graph/src/components/legend.tsx | 4 -- .../src/components/memory-graph.tsx | 52 ++++++++++--------- 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/apps/web/components/chat/model-selector.tsx b/apps/web/components/chat/model-selector.tsx index a9dd2d45..07465e7b 100644 --- a/apps/web/components/chat/model-selector.tsx +++ b/apps/web/components/chat/model-selector.tsx @@ -90,7 +90,7 @@ export default function ChatModelSelector({ return (
{trigger} diff --git a/packages/memory-graph/package.json b/packages/memory-graph/package.json index fcb2667e..7d537b7d 100644 --- a/packages/memory-graph/package.json +++ b/packages/memory-graph/package.json @@ -1,6 +1,6 @@ { "name": "@supermemory/memory-graph", - "version": "0.2.1", + "version": "0.2.2", "description": "Interactive graph visualization component for Supermemory - visualize and explore your memory connections", "type": "module", "main": "./src/index.tsx", diff --git a/packages/memory-graph/src/components/legend.tsx b/packages/memory-graph/src/components/legend.tsx index 0290226e..724efdd7 100644 --- a/packages/memory-graph/src/components/legend.tsx +++ b/packages/memory-graph/src/components/legend.tsx @@ -200,11 +200,7 @@ export const Legend = memo(function Legend({ const connectionCount = edges.length const outerStyle: React.CSSProperties = { - position: "absolute", - zIndex: 20, overflow: "hidden", - bottom: 16, - left: 16, width: 214, } diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx index 110a0194..ed5ec094 100644 --- a/packages/memory-graph/src/components/memory-graph.tsx +++ b/packages/memory-graph/src/components/memory-graph.tsx @@ -642,11 +642,15 @@ export function MemoryGraph({ justifyContent: "center", } - const navControlsStyle: React.CSSProperties = { + const bottomLeftStackStyle: React.CSSProperties = { position: "absolute", - bottom: isCompactViewport ? 148 : 72, + bottom: 16, left: 16, - zIndex: 15, + zIndex: 20, + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + gap: 8, } return ( @@ -703,28 +707,26 @@ export function MemoryGraph({ /> )} -
- {containerSize.width > 0 && ( -
- -
- )} - -
+ {containerSize.width > 0 && ( +
+ + +
+ )}
) From 670d8bf76e30f89152080ab29a74dd4c952a0518 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Sun, 24 May 2026 00:19:52 +0530 Subject: [PATCH 2/6] enable timeline memory selection (#990) --- apps/web/components/memories-grid.tsx | 3 + apps/web/components/timeline-view.tsx | 239 ++++++++++++++++++++------ 2 files changed, 190 insertions(+), 52 deletions(-) diff --git a/apps/web/components/memories-grid.tsx b/apps/web/components/memories-grid.tsx index d2f93d54..a4e7f1ba 100644 --- a/apps/web/components/memories-grid.tsx +++ b/apps/web/components/memories-grid.tsx @@ -775,6 +775,9 @@ export function MemoriesGrid({ hasNextPage={hasNextPage} isFetchingNextPage={isFetchingNextPage} onLoadMore={loadMoreDocuments} + isSelectionMode={isSelectionMode} + selectedDocumentIds={selectedDocumentIds} + onToggleSelection={onToggleSelection} /> ) : ( type DocumentWithMemories = DocumentsResponse["documents"][0] @@ -100,6 +100,39 @@ function getPreviewText(doc: DocumentWithMemories): string { return doc.summary || doc.content || doc.title || "" } +function isTemporaryId(id: string | null | undefined): boolean { + if (!id) return false + return id.startsWith("temp-") || id.startsWith("temp-file-") +} + +function SelectionBox({ + isSelected, + isPartial = false, +}: { + isSelected: boolean + isPartial?: boolean +}) { + return ( + + {isSelected ? ( + + ) : isPartial ? ( + + ) : null} + + ) +} + // ─── Grouped data structures ───────────────────────────────────────────────── type TypeGroup = { categoryInfo: CategoryInfo; docs: DocumentWithMemories[] } @@ -126,7 +159,7 @@ function groupDocuments( } return periodOrder.map((label) => { - const docs = periodMap.get(label)! + const docs = periodMap.get(label) ?? [] const categoryMap = new Map< string, { info: CategoryInfo; docs: DocumentWithMemories[] } @@ -144,9 +177,9 @@ function groupDocuments( return { label, - typeGroups: categoryOrder.map((key) => { - const entry = categoryMap.get(key)! - return { categoryInfo: entry.info, docs: entry.docs } + typeGroups: categoryOrder.flatMap((key) => { + const entry = categoryMap.get(key) + return entry ? [{ categoryInfo: entry.info, docs: entry.docs }] : [] }), } }) @@ -157,10 +190,16 @@ function groupDocuments( function TimelineCard({ doc, onOpenDocument, + isSelectionMode = false, + isSelected = false, + onToggleSelection, indent = false, }: { doc: DocumentWithMemories onOpenDocument: (doc: DocumentWithMemories) => void + isSelectionMode?: boolean + isSelected?: boolean + onToggleSelection?: (doc: DocumentWithMemories) => void indent?: boolean }) { const preview = getPreviewText(doc) @@ -168,19 +207,37 @@ function TimelineCard({ ? doc.type.charAt(0).toUpperCase() + doc.type.slice(1).replace(/_/g, " ") : "Document" const totalMemories = doc.memoryEntries.length + const canSelect = !isTemporaryId(doc.id) && !isTemporaryId(doc.customId) + + const handleClick = () => { + if (isSelectionMode && canSelect) { + onToggleSelection?.(doc) + return + } + onOpenDocument(doc) + } return ( + )} - 0 ? "pl-0" : "pl-4", + dmSansClassName(), )} - /> - + onClick={onToggle} + aria-expanded={isExpanded} + > +
+ + + {countLabel} + + {preview && ( + + · {preview} + + )} + {totalMemories > 0 && ( + + {totalMemories} + + )} +
+ + + + {isExpanded && (
{ + if (doc.id) onToggleSelection?.(doc.id) + }} indent /> ))} @@ -341,6 +453,9 @@ interface TimelineViewProps { hasNextPage?: boolean isFetchingNextPage?: boolean onLoadMore?: () => void + isSelectionMode?: boolean + selectedDocumentIds?: Set + onToggleSelection?: (documentId: string) => void } export function TimelineView({ @@ -349,6 +464,9 @@ export function TimelineView({ hasNextPage, isFetchingNextPage, onLoadMore, + isSelectionMode = false, + selectedDocumentIds = new Set(), + onToggleSelection, }: TimelineViewProps) { const [now] = useState(() => new Date()) const [expandedGroups, setExpandedGroups] = useState>(new Set()) @@ -378,6 +496,12 @@ export function TimelineView({ }, []) const periodGroups = groupDocuments(documents, now) + const handleTimelineCardSelection = useCallback( + (doc: DocumentWithMemories) => { + if (doc.id) onToggleSelection?.(doc.id) + }, + [onToggleSelection], + ) return (
) } @@ -416,6 +548,9 @@ export function TimelineView({ isExpanded={expandedGroups.has(expandKey)} onToggle={() => toggleGroup(expandKey)} onOpenDocument={onOpenDocument} + isSelectionMode={isSelectionMode} + selectedDocumentIds={selectedDocumentIds} + onToggleSelection={onToggleSelection} /> ) })} From a8d36df38e8321ebd8e71969485a4537721a2b18 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Sun, 24 May 2026 00:24:17 +0530 Subject: [PATCH 3/6] Improve quick note editor interactions & fix filter chips responsiveness (#989) --- apps/web/components/memories-grid.tsx | 18 +- apps/web/components/quick-note-card.tsx | 400 +++++++++++++----- .../web/components/text-editor/extensions.tsx | 30 +- apps/web/components/text-editor/index.tsx | 24 +- .../components/text-editor/slash-command.tsx | 2 +- 5 files changed, 356 insertions(+), 118 deletions(-) diff --git a/apps/web/components/memories-grid.tsx b/apps/web/components/memories-grid.tsx index a4e7f1ba..29d8b0e8 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,33 +16,74 @@ 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) => { - if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { - e.preventDefault() - if (draft.trim() && !isSaving) { - onSave(draft) - } - } - }, - [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) { @@ -48,112 +92,278 @@ 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 ( -
+ <>
- - -