mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-07 02:58:11 +00:00
chore: ux improvements and space selector (#684)
This commit is contained in:
parent
87b361c26b
commit
43cb9feecb
7 changed files with 238 additions and 193 deletions
|
|
@ -19,7 +19,7 @@ export default function NewPage() {
|
|||
|
||||
return (
|
||||
<HotkeysProvider>
|
||||
<div className="h-screen overflow-hidden bg-black">
|
||||
<div className="bg-black">
|
||||
<AnimatedGradientBackground
|
||||
topPosition="15%"
|
||||
animateFromBottom={false}
|
||||
|
|
@ -28,21 +28,23 @@ export default function NewPage() {
|
|||
onAddMemory={() => setIsAddDocumentOpen(true)}
|
||||
onOpenMCP={() => setIsMCPModalOpen(true)}
|
||||
/>
|
||||
<main className="relative">
|
||||
<div key={`main-container-${isChatOpen}`} className="relative z-10">
|
||||
<div className="flex flex-row h-[calc(100vh-90px)] relative">
|
||||
<div className="flex-1 flex flex-col justify-start p-6 pr-0">
|
||||
<MemoriesGrid isChatOpen={isChatOpen} />
|
||||
</div>
|
||||
<AnimatePresence mode="popLayout">
|
||||
<ChatSidebar
|
||||
isChatOpen={isChatOpen}
|
||||
setIsChatOpen={setIsChatOpen}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<main
|
||||
key={`main-container-${isChatOpen}`}
|
||||
className="z-10 flex flex-row relative"
|
||||
>
|
||||
<div className="flex-1 p-6 pr-0">
|
||||
<MemoriesGrid isChatOpen={isChatOpen} />
|
||||
</div>
|
||||
<div className="sticky top-0 h-screen">
|
||||
<AnimatePresence mode="popLayout">
|
||||
<ChatSidebar
|
||||
isChatOpen={isChatOpen}
|
||||
setIsChatOpen={setIsChatOpen}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<AddDocumentModal
|
||||
isOpen={isAddDocumentOpen}
|
||||
onClose={() => setIsAddDocumentOpen(false)}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,21 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from "react"
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
import {
|
||||
FileTextIcon,
|
||||
GlobeIcon,
|
||||
ZapIcon,
|
||||
ChevronsUpDownIcon,
|
||||
FolderIcon,
|
||||
Loader2,
|
||||
} from "lucide-react"
|
||||
import { FileTextIcon, GlobeIcon, ZapIcon, Loader2 } from "lucide-react"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { ConnectContent } from "./connections"
|
||||
import { NoteContent } from "./note"
|
||||
import { LinkContent, type LinkData } from "./link"
|
||||
import { FileContent, type FileData } from "./file"
|
||||
import { useProject } from "@/stores"
|
||||
import { $fetch } from "@lib/api"
|
||||
import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"
|
||||
import type { Project } from "@repo/lib/types"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { motion } from "motion/react"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/ui/components/dropdown-menu"
|
||||
import { toast } from "sonner"
|
||||
import { useDocumentMutations } from "../../../hooks/use-document-mutations"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { useMemoriesUsage } from "@/hooks/use-memories-usage"
|
||||
import { SpaceSelector } from "../space-selector"
|
||||
|
||||
type TabType = "note" | "link" | "file" | "connect"
|
||||
|
||||
|
|
@ -115,7 +98,6 @@ export function AddDocument({
|
|||
const [localSelectedProject, setLocalSelectedProject] = useState<string>(
|
||||
globalSelectedProject,
|
||||
)
|
||||
const [isProjectSelectorOpen, setIsProjectSelectorOpen] = useState(false)
|
||||
|
||||
// Form data state for button click handling
|
||||
const [noteContent, setNoteContent] = useState("")
|
||||
|
|
@ -147,33 +129,6 @@ export function AddDocument({
|
|||
setLocalSelectedProject(globalSelectedProject)
|
||||
}, [globalSelectedProject])
|
||||
|
||||
const { data: projects = [], isLoading: isLoadingProjects } = useQuery({
|
||||
queryKey: ["projects"],
|
||||
queryFn: async () => {
|
||||
const response = await $fetch("@get/projects")
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to load projects")
|
||||
}
|
||||
|
||||
return response.data?.projects || []
|
||||
},
|
||||
staleTime: 30 * 1000,
|
||||
})
|
||||
|
||||
const projectName = useMemo(() => {
|
||||
if (localSelectedProject === DEFAULT_PROJECT_ID) return "Default Project"
|
||||
const found = projects.find(
|
||||
(p: Project) => p.containerTag === localSelectedProject,
|
||||
)
|
||||
return found?.name ?? localSelectedProject
|
||||
}, [projects, localSelectedProject])
|
||||
|
||||
const handleProjectSelect = (containerTag: string) => {
|
||||
setLocalSelectedProject(containerTag)
|
||||
setIsProjectSelectorOpen(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultTab) {
|
||||
setActiveTab(defaultTab)
|
||||
|
|
@ -334,65 +289,11 @@ export function AddDocument({
|
|||
<ConnectContent selectedProject={localSelectedProject} />
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<DropdownMenu
|
||||
open={isProjectSelectorOpen}
|
||||
onOpenChange={setIsProjectSelectorOpen}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="insideOut"
|
||||
className="gap-2"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<FolderIcon className="size-4" />
|
||||
<span className="max-w-[120px] truncate">
|
||||
{isLoadingProjects ? "..." : projectName}
|
||||
</span>
|
||||
<motion.div
|
||||
animate={{ rotate: isProjectSelectorOpen ? 180 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<ChevronsUpDownIcon className="size-4" color="#737373" />
|
||||
</motion.div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="w-56 bg-[#1B1F24] border border-[#2A2E35] rounded-[12px] p-1.5 max-h-64 overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleProjectSelect(DEFAULT_PROJECT_ID)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-[8px] cursor-pointer",
|
||||
localSelectedProject === DEFAULT_PROJECT_ID
|
||||
? "bg-[#4BA0FA]/20 text-white"
|
||||
: "text-[#737373] hover:bg-[#14161A] hover:text-white",
|
||||
)}
|
||||
>
|
||||
<FolderIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">Default Project</span>
|
||||
</DropdownMenuItem>
|
||||
{projects
|
||||
.filter((p: Project) => p.containerTag !== DEFAULT_PROJECT_ID)
|
||||
.map((project: Project) => (
|
||||
<DropdownMenuItem
|
||||
key={project.id}
|
||||
onClick={() => handleProjectSelect(project.containerTag)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-[8px] cursor-pointer",
|
||||
localSelectedProject === project.containerTag
|
||||
? "bg-[#4BA0FA]/20 text-white"
|
||||
: "text-[#737373] hover:bg-[#14161A] hover:text-white",
|
||||
)}
|
||||
>
|
||||
<FolderIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium truncate">
|
||||
{project.name}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<SpaceSelector
|
||||
value={localSelectedProject}
|
||||
onValueChange={setLocalSelectedProject}
|
||||
variant="insideOut"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -94,11 +94,28 @@ export function ChatSidebar({
|
|||
>({})
|
||||
const [isInputExpanded, setIsInputExpanded] = useState(false)
|
||||
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true)
|
||||
const [heightOffset, setHeightOffset] = useState(95)
|
||||
const pendingFollowUpGenerations = useRef<Set<string>>(new Set())
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
||||
const { selectedProject } = useProject()
|
||||
const { setCurrentChatId } = usePersistentChat()
|
||||
|
||||
// Adjust chat height based on scroll position
|
||||
useEffect(() => {
|
||||
const handleWindowScroll = () => {
|
||||
const scrollThreshold = 80
|
||||
const scrollY = window.scrollY
|
||||
const progress = Math.min(scrollY / scrollThreshold, 1)
|
||||
const newOffset = 95 - progress * (95 - 15)
|
||||
setHeightOffset(newOffset)
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", handleWindowScroll, { passive: true })
|
||||
handleWindowScroll()
|
||||
|
||||
return () => window.removeEventListener("scroll", handleWindowScroll)
|
||||
}, [])
|
||||
|
||||
const { messages, sendMessage, status, setMessages, stop } = useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/v2`,
|
||||
|
|
@ -364,7 +381,7 @@ export function ChatSidebar({
|
|||
>
|
||||
<motion.button
|
||||
onClick={toggleChat}
|
||||
className="flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium border border-[#17181A] text-white cursor-pointer"
|
||||
className="flex items-center gap-3 rounded-full px-3 py-1.5 text-sm font-medium border border-[#17181A] text-white cursor-pointer whitespace-nowrap"
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
}}
|
||||
|
|
@ -377,9 +394,12 @@ export function ChatSidebar({
|
|||
<motion.div
|
||||
key="open"
|
||||
className={cn(
|
||||
"w-[450px] h-[calc(100vh-95px)] bg-[#05070A] backdrop-blur-md flex flex-col rounded-2xl m-4 mt-2 border border-[#17181AB2] relative pt-4",
|
||||
"w-[450px] bg-[#05070A] backdrop-blur-md flex flex-col rounded-2xl m-4 mt-2 border border-[#17181AB2] relative pt-4",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
height: `calc(100vh - ${heightOffset}px)`,
|
||||
}}
|
||||
initial={{ x: "100px", opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: "100px", opacity: 0 }}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,9 @@ import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
|
|||
import { useAuth } from "@lib/auth-context"
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
ChevronsLeftRight,
|
||||
LayoutGridIcon,
|
||||
Plus,
|
||||
SearchIcon,
|
||||
FolderIcon,
|
||||
LogOut,
|
||||
Settings,
|
||||
Home,
|
||||
|
|
@ -20,22 +18,18 @@ import { Button } from "@ui/components/button"
|
|||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
import { Tabs, TabsList, TabsTrigger } from "@ui/components/tabs"
|
||||
import { useProjectName } from "@/hooks/use-project-name"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/components/dropdown-menu"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { $fetch } from "@repo/lib/api"
|
||||
import { authClient } from "@lib/auth"
|
||||
import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"
|
||||
import { useProjectMutations } from "@/hooks/use-project-mutations"
|
||||
import { useProject } from "@/stores"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import type { Project } from "@repo/lib/types"
|
||||
import { SpaceSelector } from "./space-selector"
|
||||
|
||||
interface HeaderProps {
|
||||
onAddMemory?: () => void
|
||||
|
|
@ -45,23 +39,9 @@ interface HeaderProps {
|
|||
export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
|
||||
const { user } = useAuth()
|
||||
const [name, setName] = useState<string>("")
|
||||
const projectName = useProjectName()
|
||||
const { selectedProject } = useProject()
|
||||
const { switchProject } = useProjectMutations()
|
||||
const router = useRouter()
|
||||
const { data: projects = [] } = useQuery({
|
||||
queryKey: ["projects"],
|
||||
queryFn: async () => {
|
||||
const response = await $fetch("@get/projects")
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to load projects")
|
||||
}
|
||||
|
||||
return response.data?.projects || []
|
||||
},
|
||||
staleTime: 30 * 1000,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const storedName =
|
||||
|
|
@ -130,47 +110,11 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
|
|||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="self-stretch w-px bg-[#FFFFFF33]" />
|
||||
<div className="flex items-center gap-2">
|
||||
<p>📁 {projectName}</p>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer hover:opacity-70 transition-opacity"
|
||||
aria-label="Change project"
|
||||
>
|
||||
<ChevronsLeftRight className="size-4 rotate-90" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
<DropdownMenuItem
|
||||
onClick={() => switchProject(DEFAULT_PROJECT_ID)}
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
selectedProject === DEFAULT_PROJECT_ID && "bg-accent",
|
||||
)}
|
||||
>
|
||||
<FolderIcon className="h-3.5 w-3.5 mr-2" />
|
||||
<span className="text-sm">Default Project</span>
|
||||
</DropdownMenuItem>
|
||||
{projects
|
||||
.filter((p: Project) => p.containerTag !== DEFAULT_PROJECT_ID)
|
||||
.map((project: Project) => (
|
||||
<DropdownMenuItem
|
||||
key={project.id}
|
||||
onClick={() => switchProject(project.containerTag)}
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
selectedProject === project.containerTag && "bg-accent",
|
||||
)}
|
||||
>
|
||||
<FolderIcon className="h-3.5 w-3.5 mr-2 opacity-70" />
|
||||
<span className="text-sm truncate">{project.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<SpaceSelector
|
||||
value={selectedProject}
|
||||
onValueChange={switchProject}
|
||||
showChevron
|
||||
/>
|
||||
</div>
|
||||
<Tabs defaultValue="grid">
|
||||
<TabsList className="rounded-full border border-[#161F2C] h-11! z-10!">
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
|
|||
import { Dialog, DialogContent, DialogFooter } from "@repo/ui/components/dialog"
|
||||
import { cn } from "@lib/utils"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { ChevronsUpDownIcon, XIcon } from "lucide-react"
|
||||
import { XIcon } from "lucide-react"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { MCPSteps } from "./mcp-detail-view"
|
||||
import { SpaceSelector } from "../space-selector"
|
||||
import { useProject } from "@/stores"
|
||||
import { useProjectMutations } from "@/hooks/use-project-mutations"
|
||||
|
||||
export function MCPModal({
|
||||
isOpen,
|
||||
|
|
@ -13,6 +16,8 @@ export function MCPModal({
|
|||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
const { selectedProject } = useProject()
|
||||
const { switchProject } = useProjectMutations()
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent
|
||||
|
|
@ -50,9 +55,11 @@ export function MCPModal({
|
|||
</div>
|
||||
<DialogFooter className="justify-between!">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="insideOut">
|
||||
My Space <ChevronsUpDownIcon className="size-4" color="#737373" />
|
||||
</Button>
|
||||
<SpaceSelector
|
||||
value={selectedProject}
|
||||
onValueChange={switchProject}
|
||||
variant="insideOut"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-[#737373] cursor-pointer rounded-full"
|
||||
|
|
@ -60,7 +67,7 @@ export function MCPModal({
|
|||
Migrate from MCP v1
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="insideOut" className="px-6 py-[10px]">
|
||||
<Button variant="insideOut" className="px-6 py-[10px]" onClick={onClose}>
|
||||
Done
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export function MemoriesGrid({ isChatOpen }: { isChatOpen: boolean }) {
|
|||
const response = await $fetch("@post/documents/documents", {
|
||||
body: {
|
||||
page: pageParam as number,
|
||||
limit: (pageParam as number) === 1 ? (IS_DEV ? 500 : 500) : PAGE_SIZE,
|
||||
limit: PAGE_SIZE,
|
||||
sort: "createdAt",
|
||||
order: "desc",
|
||||
containerTags: selectedProject ? [selectedProject] : undefined,
|
||||
|
|
@ -91,6 +91,8 @@ export function MemoriesGrid({ isChatOpen }: { isChatOpen: boolean }) {
|
|||
)
|
||||
}, [data])
|
||||
|
||||
const isLoadingMore = isFetchingNextPage
|
||||
|
||||
const loadMoreDocuments = useCallback(async (): Promise<void> => {
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
await fetchNextPage()
|
||||
|
|
@ -148,7 +150,7 @@ export function MemoriesGrid({ isChatOpen }: { isChatOpen: boolean }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className="relative">
|
||||
<Button
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
|
|
@ -189,7 +191,7 @@ export function MemoriesGrid({ isChatOpen }: { isChatOpen: boolean }) {
|
|||
onRender={maybeLoadMore}
|
||||
/>
|
||||
|
||||
{isFetchingNextPage && (
|
||||
{isLoadingMore && (
|
||||
<div className="py-8 flex items-center justify-center">
|
||||
<SuperLoader />
|
||||
</div>
|
||||
|
|
|
|||
169
apps/web/components/new/space-selector.tsx
Normal file
169
apps/web/components/new/space-selector.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useMemo } from "react"
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
import { $fetch } from "@repo/lib/api"
|
||||
import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { ChevronsLeftRight, Plus } from "lucide-react"
|
||||
import type { Project } from "@repo/lib/types"
|
||||
import { CreateProjectDialog } from "@/components/create-project-dialog"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/components/dropdown-menu"
|
||||
|
||||
export interface SpaceSelectorProps {
|
||||
value: string
|
||||
onValueChange: (containerTag: string) => void
|
||||
variant?: "default" | "insideOut"
|
||||
showChevron?: boolean
|
||||
triggerClassName?: string
|
||||
contentClassName?: string
|
||||
showNewSpace?: boolean
|
||||
}
|
||||
|
||||
const triggerVariants = {
|
||||
default: "px-3 py-2 rounded-md hover:bg-white/5",
|
||||
insideOut: "px-3 py-2 rounded-full bg-[#0D121A] shadow-inside-out",
|
||||
}
|
||||
|
||||
export function SpaceSelector({
|
||||
value,
|
||||
onValueChange,
|
||||
variant = "default",
|
||||
showChevron = false,
|
||||
triggerClassName,
|
||||
contentClassName,
|
||||
showNewSpace = true,
|
||||
}: SpaceSelectorProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
|
||||
const { data: projects = [], isLoading } = useQuery({
|
||||
queryKey: ["projects"],
|
||||
queryFn: async () => {
|
||||
const response = await $fetch("@get/projects")
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to load projects")
|
||||
}
|
||||
|
||||
return response.data?.projects || []
|
||||
},
|
||||
staleTime: 30 * 1000,
|
||||
})
|
||||
|
||||
const selectedProjectName = useMemo(() => {
|
||||
if (value === DEFAULT_PROJECT_ID) return "My Space"
|
||||
const found = projects.find((p: Project) => p.containerTag === value)
|
||||
return found?.name ?? value
|
||||
}, [projects, value])
|
||||
|
||||
const handleSelect = (containerTag: string) => {
|
||||
onValueChange(containerTag)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const handleNewSpace = () => {
|
||||
setIsOpen(false)
|
||||
setShowCreateDialog(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex items-center gap-2 cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
|
||||
triggerVariants[variant],
|
||||
dmSansClassName(),
|
||||
triggerClassName,
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-bold tracking-[-0.98px]">📁</span>
|
||||
<span className="text-sm font-medium text-white">
|
||||
{isLoading ? "..." : selectedProjectName}
|
||||
</span>
|
||||
{showChevron && (
|
||||
<ChevronsLeftRight className="size-4 rotate-90 text-white/70" />
|
||||
)}
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className={cn(
|
||||
"min-w-[200px] p-3 rounded-[11px] border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
|
||||
dmSansClassName(),
|
||||
contentClassName,
|
||||
)}
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col">
|
||||
{/* Default Project */}
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleSelect(DEFAULT_PROJECT_ID)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer text-white text-sm font-medium",
|
||||
value === DEFAULT_PROJECT_ID
|
||||
? "bg-[#161E2B] border border-[rgba(115,115,115,0.1)]"
|
||||
: "opacity-50 hover:opacity-100 hover:bg-[#161E2B]/50",
|
||||
)}
|
||||
>
|
||||
<span className="font-bold tracking-[-0.98px]">📁</span>
|
||||
<span>My Space</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* User Projects */}
|
||||
{projects
|
||||
.filter((p: Project) => p.containerTag !== DEFAULT_PROJECT_ID)
|
||||
.map((project: Project) => (
|
||||
<DropdownMenuItem
|
||||
key={project.id}
|
||||
onClick={() => handleSelect(project.containerTag)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer text-white text-sm font-medium",
|
||||
value === project.containerTag
|
||||
? "bg-[#161E2B] border border-[rgba(115,115,115,0.1)]"
|
||||
: "opacity-50 hover:opacity-100 hover:bg-[#161E2B]/50",
|
||||
)}
|
||||
>
|
||||
<span className="font-bold tracking-[-0.98px]">📁</span>
|
||||
<span className="truncate">{project.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showNewSpace && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNewSpace}
|
||||
className="flex items-center justify-center gap-2 px-3 py-2 rounded-md cursor-pointer text-white text-sm font-medium border border-[#161F2C] hover:bg-[#0D121A]/80 transition-colors"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, #0D121A 0%, #000000 100%)",
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
<span>New Space</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<CreateProjectDialog
|
||||
open={showCreateDialog}
|
||||
onOpenChange={setShowCreateDialog}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue