aboutsummaryrefslogtreecommitdiff
path: root/packages/memory-graph/src/components/spaces-dropdown.tsx
diff options
context:
space:
mode:
authornexxeln <[email protected]>2025-12-02 18:37:24 +0000
committernexxeln <[email protected]>2025-12-02 18:37:24 +0000
commitdfb0c05ab33cb20537002eaeb896e6b2ab35af25 (patch)
tree49ecaa46903671d96f2f9ebc5af688ab2ea2c7bd /packages/memory-graph/src/components/spaces-dropdown.tsx
parentFix: Update discord links in README.md and CONTRIBUTING.md (#598) (diff)
downloadsupermemory-update-memory-graph.tar.xz
supermemory-update-memory-graph.zip
add spaces selector with search (#600)update-memory-graph
relevant files to review: \- memory-graph.tsx \- spaces-dropdown.tsx \- spaces-dropdown.css.ts
Diffstat (limited to 'packages/memory-graph/src/components/spaces-dropdown.tsx')
-rw-r--r--packages/memory-graph/src/components/spaces-dropdown.tsx235
1 files changed, 186 insertions, 49 deletions
diff --git a/packages/memory-graph/src/components/spaces-dropdown.tsx b/packages/memory-graph/src/components/spaces-dropdown.tsx
index b70059f5..d8a56fe3 100644
--- a/packages/memory-graph/src/components/spaces-dropdown.tsx
+++ b/packages/memory-graph/src/components/spaces-dropdown.tsx
@@ -1,15 +1,19 @@
-"use client";
+"use client"
-import { Badge } from "@/ui/badge";
-import { ChevronDown, Eye } from "lucide-react";
-import { memo, useEffect, useRef, useState } from "react";
-import type { SpacesDropdownProps } from "@/types";
-import * as styles from "./spaces-dropdown.css";
+import { Badge } from "@/ui/badge"
+import { ChevronDown, Eye, Search, X } from "lucide-react"
+import { memo, useEffect, useRef, useState } from "react"
+import type { SpacesDropdownProps } from "@/types"
+import * as styles from "./spaces-dropdown.css"
export const SpacesDropdown = memo<SpacesDropdownProps>(
({ selectedSpace, availableSpaces, spaceMemoryCounts, onSpaceChange }) => {
- const [isOpen, setIsOpen] = useState(false);
- const dropdownRef = useRef<HTMLDivElement>(null);
+ const [isOpen, setIsOpen] = useState(false)
+ const [searchQuery, setSearchQuery] = useState("")
+ const [highlightedIndex, setHighlightedIndex] = useState(-1)
+ const dropdownRef = useRef<HTMLDivElement>(null)
+ const searchInputRef = useRef<HTMLInputElement>(null)
+ const itemRefs = useRef<Map<number, HTMLButtonElement>>(new Map())
// Close dropdown when clicking outside
useEffect(() => {
@@ -18,38 +22,115 @@ export const SpacesDropdown = memo<SpacesDropdownProps>(
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
- setIsOpen(false);
+ setIsOpen(false)
}
- };
+ }
- document.addEventListener("mousedown", handleClickOutside);
- return () =>
- document.removeEventListener("mousedown", handleClickOutside);
- }, []);
+ document.addEventListener("mousedown", handleClickOutside)
+ return () => document.removeEventListener("mousedown", handleClickOutside)
+ }, [])
+
+ // Focus search input when dropdown opens
+ useEffect(() => {
+ if (isOpen && searchInputRef.current) {
+ searchInputRef.current.focus()
+ }
+ }, [isOpen])
+
+ // Clear search query and reset highlighted index when dropdown closes
+ useEffect(() => {
+ if (!isOpen) {
+ setSearchQuery("")
+ setHighlightedIndex(-1)
+ }
+ }, [isOpen])
+
+ // Filter spaces based on search query (client-side)
+ const filteredSpaces = searchQuery
+ ? availableSpaces.filter((space) =>
+ space.toLowerCase().includes(searchQuery.toLowerCase()),
+ )
+ : availableSpaces
const totalMemories = Object.values(spaceMemoryCounts).reduce(
(sum, count) => sum + count,
0,
- );
+ )
+
+ // Total items including "Latest" option
+ const totalItems = filteredSpaces.length + 1
+
+ // Scroll highlighted item into view
+ useEffect(() => {
+ if (highlightedIndex >= 0 && highlightedIndex < totalItems) {
+ const element = itemRefs.current.get(highlightedIndex)
+ if (element) {
+ element.scrollIntoView({
+ block: "nearest",
+ behavior: "smooth",
+ })
+ }
+ }
+ }, [highlightedIndex, totalItems])
+
+ // Handle keyboard navigation
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (!isOpen) return
+
+ switch (e.key) {
+ case "ArrowDown":
+ e.preventDefault()
+ setHighlightedIndex((prev) => (prev < totalItems - 1 ? prev + 1 : 0))
+ break
+ case "ArrowUp":
+ e.preventDefault()
+ setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : totalItems - 1))
+ break
+ case "Enter":
+ e.preventDefault()
+ if (highlightedIndex === 0) {
+ onSpaceChange("all")
+ setIsOpen(false)
+ } else if (
+ highlightedIndex > 0 &&
+ highlightedIndex <= filteredSpaces.length
+ ) {
+ const selectedSpace = filteredSpaces[highlightedIndex - 1]
+ if (selectedSpace) {
+ onSpaceChange(selectedSpace)
+ setIsOpen(false)
+ }
+ }
+ break
+ case "Escape":
+ e.preventDefault()
+ setIsOpen(false)
+ break
+ }
+ }
return (
- <div className={styles.container} ref={dropdownRef}>
+ <div
+ className={styles.container}
+ ref={dropdownRef}
+ onKeyDown={handleKeyDown}
+ >
<button
className={styles.trigger}
onClick={() => setIsOpen(!isOpen)}
type="button"
>
- {/*@ts-ignore */}
- <Eye className={styles.triggerIcon} />
+ {/*@ts-ignore */}
+ <Eye className={styles.triggerIcon} />
<div className={styles.triggerContent}>
<span className={styles.triggerLabel}>
{selectedSpace === "all"
- ? "All Spaces"
+ ? "Latest"
: selectedSpace || "Select space"}
</span>
<div className={styles.triggerSubtext}>
{selectedSpace === "all"
- ? `${totalMemories} total memories`
+ ? ""
: `${spaceMemoryCounts[selectedSpace] || 0} memories`}
</div>
</div>
@@ -62,49 +143,105 @@ export const SpacesDropdown = memo<SpacesDropdownProps>(
{isOpen && (
<div className={styles.dropdown}>
<div className={styles.dropdownInner}>
- <button
- className={
- selectedSpace === "all"
- ? styles.dropdownItemActive
- : styles.dropdownItem
- }
- onClick={() => {
- onSpaceChange("all");
- setIsOpen(false);
- }}
- type="button"
- >
- <span className={styles.dropdownItemLabel}>All Spaces</span>
- <Badge className={styles.dropdownItemBadge}>
- {totalMemories}
- </Badge>
- </button>
- {availableSpaces.map((space) => (
+ {/* Search Input - Always show for filtering */}
+ <div className={styles.searchContainer}>
+ <div className={styles.searchForm}>
+ {/*@ts-ignore */}
+ <Search className={styles.searchIcon} />
+ <input
+ className={styles.searchInput}
+ onChange={(e) => setSearchQuery(e.target.value)}
+ placeholder="Search spaces..."
+ ref={searchInputRef}
+ type="text"
+ value={searchQuery}
+ />
+ {searchQuery && (
+ <button
+ className={styles.searchClearButton}
+ onClick={() => setSearchQuery("")}
+ type="button"
+ aria-label="Clear search"
+ >
+ {/*@ts-ignore */}
+ <X className={styles.searchIcon} />
+ </button>
+ )}
+ </div>
+ </div>
+
+ {/* Spaces List */}
+ <div className={styles.dropdownList}>
+ {/* Always show "Latest" option */}
<button
+ ref={(el) => {
+ if (el) itemRefs.current.set(0, el)
+ }}
className={
- selectedSpace === space
+ selectedSpace === "all"
? styles.dropdownItemActive
- : styles.dropdownItem
+ : highlightedIndex === 0
+ ? styles.dropdownItemHighlighted
+ : styles.dropdownItem
}
- key={space}
onClick={() => {
- onSpaceChange(space);
- setIsOpen(false);
+ onSpaceChange("all")
+ setIsOpen(false)
}}
+ onMouseEnter={() => setHighlightedIndex(0)}
type="button"
>
- <span className={styles.dropdownItemLabelTruncate}>{space}</span>
+ <span className={styles.dropdownItemLabel}>Latest</span>
<Badge className={styles.dropdownItemBadge}>
- {spaceMemoryCounts[space] || 0}
+ {totalMemories}
</Badge>
</button>
- ))}
+
+ {/* Show all spaces, filtered by search query */}
+ {filteredSpaces.length > 0
+ ? filteredSpaces.map((space, index) => {
+ const itemIndex = index + 1
+ return (
+ <button
+ ref={(el) => {
+ if (el) itemRefs.current.set(itemIndex, el)
+ }}
+ className={
+ selectedSpace === space
+ ? styles.dropdownItemActive
+ : highlightedIndex === itemIndex
+ ? styles.dropdownItemHighlighted
+ : styles.dropdownItem
+ }
+ key={space}
+ onClick={() => {
+ onSpaceChange(space)
+ setIsOpen(false)
+ }}
+ onMouseEnter={() => setHighlightedIndex(itemIndex)}
+ type="button"
+ >
+ <span className={styles.dropdownItemLabelTruncate}>
+ {space}
+ </span>
+ <Badge className={styles.dropdownItemBadge}>
+ {spaceMemoryCounts[space] || 0}
+ </Badge>
+ </button>
+ )
+ })
+ : searchQuery && (
+ <div className={styles.emptyState}>
+ No spaces found matching "{searchQuery}"
+ </div>
+ )}
+ </div>
</div>
</div>
)}
</div>
- );
+ )
},
-);
+)
-SpacesDropdown.displayName = "SpacesDropdown";
+SpacesDropdown.displayName = "SpacesDropdown"