feat(web): mobile UX for dashboard, nav, and spaces (#1000)

This commit is contained in:
Mahesh Sanikommu 2026-05-25 03:39:34 -07:00 committed by GitHub
parent 9b310e9538
commit f646f1a80f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 637 additions and 44 deletions

View file

@ -11,6 +11,7 @@ import {
import { AnimatePresence, motion } from "motion/react"
import { useQueryState } from "nuqs"
import { Header, PublicHeader } from "@/components/header"
import { MobileBottomNav } from "@/components/bottom-nav"
import { ChatSidebar, HomeChatComposer } from "@/components/chat"
import { DashboardView } from "@/components/dashboard-view"
import { MemoriesGrid } from "@/components/memories-grid"
@ -561,6 +562,7 @@ export default function NewPage() {
const isDashboardShell =
viewMode === "dashboard" || (viewMode === "graph" && isMobile)
const isGraphMode = viewMode === "graph"
const showBottomNav = isMobile && !isChatView && !!session
return (
<HotkeysProvider>
@ -568,6 +570,9 @@ export default function NewPage() {
className={cn(
"relative flex min-h-dvh flex-col bg-[#05080D]",
isGraphMode && "h-dvh overflow-hidden",
showBottomNav &&
!isGraphMode &&
"pb-[calc(5.5rem+env(safe-area-inset-bottom))]",
)}
>
{showNovaBackdrop && (
@ -737,14 +742,37 @@ export default function NewPage() {
</motion.main>
</AnimatePresence>
{isDashboardShell && showBottomNav && (
<div className="pointer-events-none fixed inset-x-0 bottom-0 z-20 h-64 bg-gradient-to-t from-[#05080D] via-[#05080D]/95 to-transparent" />
)}
{isDashboardShell && (
<div className="pointer-events-none fixed inset-x-0 bottom-0 z-30 bg-gradient-to-t from-black via-black/40 to-transparent pt-12">
<div
className={cn(
"pointer-events-none fixed inset-x-0 z-30",
showBottomNav
? "bottom-[4.25rem]"
: "bottom-0 bg-gradient-to-t from-black via-black/40 to-transparent pt-12",
)}
>
<div className="pointer-events-auto">
<HomeChatComposer onStartChat={handleHomeChatStart} />
</div>
</div>
)}
{showBottomNav && (
<MobileBottomNav
onAddMemory={() => {
analytics.addDocumentModalOpened()
setAddDoc("note")
}}
onOpenSearch={() => {
analytics.searchOpened({ source: "header" })
setIsSearchOpen(true)
}}
/>
)}
<AddDocumentModal
isOpen={addDoc !== null}
onClose={() => setAddDoc(null)}

View file

@ -0,0 +1,202 @@
"use client"
import {
Home,
LayoutGrid,
Plus,
MessageCircleIcon,
MoreHorizontal,
SearchIcon,
Sun,
LifeBuoy,
Settings,
} from "lucide-react"
import { useRouter } from "next/navigation"
import { useQueryState } from "nuqs"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { GraphIcon } from "@/components/integration-icons"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@ui/components/dropdown-menu"
import { useViewMode, type ViewMode } from "@/lib/view-mode-context"
import { feedbackParam } from "@/lib/search-params"
const INTEGRATION_VIEWS: ViewMode[] = [
"integrations",
"mcp",
"plugins",
"chrome",
"connections",
"shortcuts",
"raycast",
"import",
]
interface BottomNavProps {
onAddMemory?: () => void
onOpenSearch?: () => void
}
export function MobileBottomNav({ onAddMemory, onOpenSearch }: BottomNavProps) {
const router = useRouter()
const { viewMode, setViewMode } = useViewMode()
const [, setFeedbackOpen] = useQueryState("feedback", feedbackParam)
const isHome = viewMode === "dashboard"
const isMemories = viewMode === "list" || viewMode === "graph"
const isChat = viewMode === "chat"
const isMore = INTEGRATION_VIEWS.includes(viewMode)
return (
<nav
aria-label="Primary"
className={cn(
"fixed inset-x-0 bottom-[calc(0.75rem+env(safe-area-inset-bottom))] z-40 flex justify-center px-3 md:hidden",
dmSansClassName(),
)}
>
<div className="flex w-full items-center justify-around rounded-full border border-[#161F2C] bg-muted/95 px-2.5 py-2 shadow-[0_10px_30px_rgba(0,0,0,0.55)] backdrop-blur-xl">
<NavTab
label="Home"
icon={Home}
active={isHome}
onClick={() => void setViewMode("dashboard")}
/>
<NavTab
label="Memories"
icon={LayoutGrid}
active={isMemories}
onClick={() => void setViewMode("list")}
/>
<button
type="button"
aria-label="Add memory"
onClick={onAddMemory}
className="flex size-11 shrink-0 items-center justify-center self-center rounded-full text-white outline-none transition-colors hover:bg-white/5"
>
<Plus className="size-7" strokeWidth={2.25} />
</button>
<NavTab
label="Chat"
icon={MessageCircleIcon}
active={isChat}
onClick={() => void setViewMode("chat")}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<NavTabButton label="More" active={isMore}>
<MoreHorizontal className="size-6" />
</NavTabButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="end"
sideOffset={12}
className={cn(
"min-w-[200px] rounded-2xl border border-[#263348]/60 p-1.5 shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
dmSansClassName(),
)}
style={{
background: "linear-gradient(180deg, #101822 0%, #0A0E14 100%)",
}}
>
<MoreItem icon={SearchIcon} label="Search" onClick={onOpenSearch} />
<MoreItem
icon={GraphIcon}
label="Graph"
onClick={() => void setViewMode("graph")}
/>
<MoreItem
icon={Sun}
label="Integrations"
onClick={() => void setViewMode("integrations")}
/>
<DropdownMenuSeparator className="bg-[#263348]/50" />
<MoreItem
icon={LifeBuoy}
label="Feedback"
onClick={() => setFeedbackOpen(true)}
/>
<MoreItem
icon={Settings}
label="Settings"
onClick={() => router.push("/settings")}
/>
</DropdownMenuContent>
</DropdownMenu>
</div>
</nav>
)
}
function NavTab({
label,
icon: Icon,
active,
onClick,
}: {
label: string
icon: React.ComponentType<{ className?: string }>
active: boolean
onClick: () => void
}) {
return (
<NavTabButton label={label} active={active} onClick={onClick}>
<Icon className="size-6" />
</NavTabButton>
)
}
function NavTabButton({
label,
active,
onClick,
children,
...props
}: {
label: string
active: boolean
onClick?: () => void
children: React.ReactNode
} & React.ComponentProps<"button">) {
return (
<button
type="button"
aria-current={active ? "page" : undefined}
onClick={onClick}
className={cn(
"flex shrink-0 flex-col items-center gap-1 rounded-full px-3 py-1.5 outline-none transition-colors",
active ? "text-white" : "text-[#737373] hover:text-white",
)}
{...props}
>
{children}
<span className="text-[10px] font-medium leading-none">{label}</span>
</button>
)
}
function MoreItem({
icon: Icon,
label,
onClick,
}: {
icon: React.ComponentType<{ className?: string }>
label: string
onClick?: () => void
}) {
return (
<DropdownMenuItem
onClick={onClick}
className="gap-2 rounded-md px-3 py-2.5 text-sm font-medium text-white hover:bg-[#293952]/40"
>
<Icon className="size-4 text-[#737373]" />
{label}
</DropdownMenuItem>
)
}

View file

@ -1297,7 +1297,7 @@ export function DashboardView({
Home
</p>
<h1
className="max-w-2xl text-xl font-medium tracking-tight text-white md:text-2xl"
className="max-w-2xl text-lg font-medium leading-snug tracking-tight text-white md:text-2xl md:leading-tight"
title={spaceLabel}
>
{homeHeadline}
@ -1309,7 +1309,7 @@ export function DashboardView({
<button
type="button"
onClick={onNavigateToGraph}
className="group relative shrink-0 w-[140px] h-[56px] rounded-xl overflow-hidden border border-surface-border hover:border-[#3A4A63] transition-all bg-surface-card hover:scale-[1.02]"
className="group relative hidden h-[56px] w-[140px] shrink-0 overflow-hidden rounded-xl border border-surface-border bg-surface-card transition-all hover:scale-[1.02] hover:border-[#3A4A63] md:block"
aria-label="Open graph view"
>
<StaticGraphPreview

View file

@ -76,12 +76,12 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
className="flex shrink-0 cursor-pointer items-center rounded-lg px-1.5 py-1 transition-colors hover:bg-white/5 focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none md:-ml-2"
>
<Logo className="h-6 md:h-7" />
{!isMobile && userName && (
{userName && (
<div className="ml-1.5 flex flex-col items-start justify-center sm:ml-2">
<p className="text-[10px] leading-tight text-[#6B6B6B] sm:text-[11px]">
{userName}
</p>
<p className="-mt-0.5 text-base leading-none font-medium text-white/90 sm:text-lg">
<p className="-mt-0.5 text-sm leading-none font-medium text-white/90 sm:text-lg">
supermemory
</p>
</div>

View file

@ -13,6 +13,7 @@ import {
X,
} from "lucide-react"
import { Logo } from "@ui/assets/Logo"
import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
import { analytics } from "@/lib/analytics"
export type HighlightFormat = "paragraph" | "bullets" | "quote" | "one_liner"
@ -213,7 +214,37 @@ export function HighlightsCard({
</span>
</div>
</div>
<Info className="size-[14px] text-fg-subtle" />
<Popover>
<PopoverTrigger asChild>
<button
type="button"
aria-label="About the Daily Brief"
className="shrink-0 rounded-full p-0.5 text-fg-subtle transition-colors hover:text-fg-primary focus-visible:outline-none"
>
<Info className="size-[14px]" />
</button>
</PopoverTrigger>
<PopoverContent
align="end"
side="bottom"
className={cn(
"w-64 rounded-xl border border-[#263348]/60 p-3 shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
dmSansClassName(),
)}
style={{
background: "linear-gradient(180deg, #101822 0%, #0A0E14 100%)",
}}
>
<p className="mb-1 text-[12px] font-semibold text-fg-primary">
Daily Brief
</p>
<p className="text-[12px] leading-relaxed text-fg-subtle">
AI-generated highlights and questions drawn from your memories. It
refreshes automatically every few hours tap the refresh icon to
update it now.
</p>
</PopoverContent>
</Popover>
</div>
<div id="highlights-body" className="flex flex-col gap-1.5">

View file

@ -547,11 +547,11 @@ export function MemoriesGrid({
id="filter-pills"
className="mb-3 flex flex-col gap-2 pr-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4"
>
<div className="order-2 flex w-full min-w-0 flex-wrap items-center gap-1.5 sm:order-1">
<div className="order-2 flex w-full min-w-0 items-center gap-1.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden sm:order-1 sm:flex-wrap sm:overflow-visible">
<Button
className={cn(
dmSansClassName(),
"rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs h-auto hover:bg-[#00173C] hover:border-[#2261CA33]",
"shrink-0 whitespace-nowrap rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs h-auto hover:bg-[#00173C] hover:border-[#2261CA33]",
selectedCategories.length === 0 &&
"bg-[#00173C] border-[#2261CA33]",
)}
@ -569,7 +569,7 @@ export function MemoriesGrid({
key={facet.category}
className={cn(
dmSansClassName(),
"rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs h-auto hover:bg-[#00173C] hover:border-[#2261CA33]",
"shrink-0 whitespace-nowrap rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs h-auto hover:bg-[#00173C] hover:border-[#2261CA33]",
selectedCategoriesSet.has(facet.category) &&
"bg-[#00173C] border-[#2261CA33]",
)}
@ -580,7 +580,7 @@ export function MemoriesGrid({
</Button>
))}
</div>
<div className="order-1 flex shrink-0 items-center gap-2 self-end sm:order-2 sm:self-start">
<div className="order-1 flex w-full items-center justify-between gap-2 sm:order-2 sm:w-auto sm:justify-start sm:self-start">
{/* View mode toggle — segmented control */}
<div
role="tablist"

View file

@ -4,7 +4,9 @@ import { useState, useMemo, useEffect, useCallback, useRef } from "react"
import Image from "next/image"
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
import { Dialog, DialogContent } from "@repo/ui/components/dialog"
import { Drawer, DrawerContent, DrawerTitle } from "@repo/ui/components/drawer"
import { cn } from "@lib/utils"
import { useIsMobile } from "@hooks/use-mobile"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import {
XIcon,
@ -47,6 +49,7 @@ import { useProjectMutations } from "@/hooks/use-project-mutations"
import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space"
import NovaOrb from "@/components/nova/nova-orb"
import { AutoSpaceIcon } from "@/components/nova/auto-space-icon"
import { SpaceGlyph } from "./space-glyph"
interface SelectSpacesModalProps {
isOpen: boolean
@ -116,6 +119,7 @@ export function SelectSpacesModal({
const editInputRef = useRef<HTMLInputElement | null>(null)
const editingContainerTag = editingProject?.containerTag
const currentSelection = selectedProjects[0] ?? ""
const isMobile = useIsMobile()
const pluginTags = useMemo(
() =>
@ -645,7 +649,11 @@ export function SelectSpacesModal({
</button>
{isEditing ? (
<div className="flex min-w-0 flex-1 items-center gap-2">
<span className="shrink-0 text-lg">{project.emoji || "📁"}</span>
<SpaceGlyph
emoji={project.emoji}
size={18}
className="shrink-0"
/>
<input
type="text"
value={editingProject.name}
@ -709,9 +717,11 @@ export function SelectSpacesModal({
) : isOwnSpace ? (
<NovaOrb size={20} className="shrink-0 blur-[0.55px]!" />
) : (
<span className="shrink-0 text-lg">
{project.emoji || "📁"}
</span>
<SpaceGlyph
emoji={project.emoji}
size={20}
className="shrink-0"
/>
)}
<span
className="min-w-0 flex-1 truncate text-[#fafafa] text-sm font-medium"
@ -825,6 +835,283 @@ export function SelectSpacesModal({
)
}, [currentSelection, handleSelectAuto])
const renderCategoryChip = (
category: {
id: string
label: string
count?: number
iconSrc?: string
emoji?: string
},
isDiscover: boolean,
) => {
const isActive = activeCategory === category.id
return (
<button
key={category.id}
type="button"
onClick={() => setActiveCategory(category.id)}
className={cn(
"flex shrink-0 items-center gap-2 whitespace-nowrap rounded-full border px-3 py-2 transition-colors",
isActive
? "border-[#2261CA33] bg-[#00173C] text-[#fafafa]"
: "border-[#161F2C] bg-[#0D121A] text-[#A1A1AA]",
isDiscover && !isActive && "opacity-60",
)}
>
<span className="flex size-[18px] shrink-0 items-center justify-center">
{category.id === "all" ? (
<LayoutGrid
className={cn(
"size-4",
isActive ? "text-[#fafafa]" : "text-[#737373]",
)}
/>
) : category.iconSrc ? (
<Image
src={category.iconSrc}
alt=""
width={18}
height={18}
className="rounded-[3px]"
aria-hidden
/>
) : category.emoji ? (
<SpaceGlyph emoji={category.emoji} size={16} />
) : category.id.startsWith("plugin:") ? (
<span
className="flex h-[18px] w-[18px] items-center justify-center rounded-[3px] bg-[#1E232B] text-[10px] font-semibold uppercase text-[#FAFAFA]"
aria-hidden
>
{pluginInitial(category.label)}
</span>
) : (
<FolderIcon
className={cn(
"size-4",
isActive ? "text-[#fafafa]" : "text-[#737373]",
)}
/>
)}
</span>
<span className="text-[13px] font-medium">{category.label}</span>
{isDiscover ? (
<ArrowRight className="size-3.5 text-[#737373]" />
) : (
<span className="text-[11px] tabular-nums text-[#737373]">
{category.count}
</span>
)}
</button>
)
}
const rightPanelContent = activeCategory.startsWith("discover:") ? (
<DiscoverPanel
catalogId={activeDiscoverId ?? ""}
isConnecting={connectingPluginId === activeDiscoverId}
newKey={newKey?.pluginId === activeDiscoverId ? newKey.key : null}
onConnect={() => {
if (activeDiscoverId) connectMutation.mutate(activeDiscoverId)
}}
onDismissKey={() => setNewKey(null)}
/>
) : (
<>
<div className="relative shrink-0">
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-[#737373]" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search spaces..."
className={cn(
"w-full rounded-[12px] bg-[#14161A] py-2.5 pl-10 pr-4 text-[14px] text-[#fafafa] shadow-inside-out placeholder:text-[#737373] focus:outline-none",
dmSansClassName(),
)}
/>
</div>
<div className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto scrollbar-thin pr-1">
{filteredProjects.length === 0 ? (
<p className="py-8 text-center text-sm text-[#737373]">
No spaces found
</p>
) : (
<div className="flex flex-col gap-1">
{showAutoRow && (
<>
<div className="px-3 pt-1 pb-0.5 text-[10px] uppercase tracking-[0.08em] text-[#737373]">
Mode
</div>
{renderAutoRow()}
<div className="my-1.5 h-px bg-[rgba(82,89,102,0.18)]" />
</>
)}
{recentProjects.length > 0 && (
<>
<div className="flex items-center gap-1.5 px-3 pt-1 pb-0.5 text-[10px] uppercase tracking-[0.08em] text-[#737373]">
<Clock className="size-3" />
Recently used
</div>
{recentProjects.map(renderRow)}
<div className="my-1.5 h-px bg-[rgba(82,89,102,0.18)]" />
<div className="px-3 pt-0.5 pb-0.5 text-[10px] uppercase tracking-[0.08em] text-[#737373]">
All spaces
</div>
</>
)}
{mainList.map(renderRow)}
</div>
)}
</div>
</>
)
const footerContent = !activeCategory.startsWith("discover:") &&
(isBulkDeleteMode || (showNewSpace && onNewSpace)) && (
<div className="flex shrink-0 items-center justify-between gap-3 border-t border-[rgba(82,89,102,0.18)] px-4 py-3">
{isBulkDeleteMode ? (
<>
<p className="min-w-0 text-[13px] font-medium text-[#737373]">
{bulkDeleteCount === 0
? "No spaces selected"
: `${bulkDeleteCount} ${
bulkDeleteCount === 1 ? "space" : "spaces"
} selected`}
</p>
<div className="flex shrink-0 items-center gap-2">
<button
type="button"
onClick={handleBulkModeToggle}
className={cn(
"px-3 py-2 text-[13px] font-medium text-[#737373] transition-colors hover:text-[#fafafa]",
dmSansClassName(),
)}
>
Cancel
</button>
<button
type="button"
disabled={bulkDeleteCount === 0}
onClick={() => {
if (bulkDeleteCount === 0) return
onBulkDeleteRequest?.(bulkDeleteProjects)
setIsBulkDeleteMode(false)
setBulkDeleteTags(new Set())
setLastBulkDeleteTag(null)
}}
className={cn(
"flex items-center gap-2 rounded-full bg-red-600 px-4 py-2 text-[13px] font-medium text-white transition-colors hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-40",
dmSansClassName(),
)}
>
<Trash2 className="size-4" />
Delete selected
</button>
</div>
</>
) : (
<>
<span />
{showNewSpace && onNewSpace && (
<button
type="button"
onClick={onNewSpace}
className={cn(
"flex items-center gap-2 rounded-full bg-[#14161A] px-4 py-2 text-[13px] font-medium text-[#fafafa] shadow-inside-out transition-colors hover:bg-[#121820] focus:outline-none focus:ring-0",
dmSansClassName(),
)}
>
<Plus className="size-4" />
New space
</button>
)}
</>
)}
</div>
)
if (isMobile) {
return (
<Drawer open={isOpen} onOpenChange={handleOpenChange}>
<DrawerContent
className={cn(
"flex h-[85dvh] flex-col gap-0 overflow-hidden border-none bg-[#1B1F24] p-0",
dmSansClassName(),
)}
>
<DrawerTitle className="sr-only">Select Space</DrawerTitle>
<div className="flex shrink-0 items-start justify-between gap-3 px-4 pt-1">
<div className="space-y-1">
<p
className={cn(
"font-semibold text-[#fafafa]",
dmSans125ClassName(),
)}
>
Select Space
</p>
<p className="text-[13px] font-medium leading-[1.35] text-[#737373]">
{isBulkDeleteMode
? "Choose spaces to permanently delete"
: "Filter your memories by space"}
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
{enableDelete && onBulkDeleteRequest && !activeDiscoverId && (
<button
type="button"
onClick={handleBulkModeToggle}
className={cn(
"flex h-8 items-center gap-1.5 rounded-full bg-[#0D121A] px-2.5 text-[12px] font-medium transition-colors",
isBulkDeleteMode ? "text-[#fafafa]" : "text-[#737373]",
)}
style={{
boxShadow:
"inset 1.313px 1.313px 3.938px 0px rgba(0,0,0,0.7)",
}}
>
<Trash2 className="size-3.5" />
{isBulkDeleteMode ? "Cancel" : "Bulk delete"}
</button>
)}
<button
type="button"
onClick={() => handleOpenChange(false)}
aria-label="Close"
className="flex size-8 shrink-0 items-center justify-center rounded-full border border-[rgba(115,115,115,0.2)] bg-[#0D121A]"
style={{
boxShadow:
"inset 1.313px 1.313px 3.938px 0px rgba(0,0,0,0.7)",
}}
>
<XIcon stroke="#737373" className="size-4" />
</button>
</div>
</div>
<div className="mt-3 flex shrink-0 gap-1.5 overflow-x-auto px-4 pb-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{categories.map((category) => renderCategoryChip(category, false))}
{discoverCategories.length > 0 && (
<>
<div className="mx-0.5 my-1 w-px shrink-0 bg-[rgba(82,89,102,0.25)]" />
{discoverCategories.map((category) =>
renderCategoryChip(category, true),
)}
</>
)}
</div>
<div className="mt-3 flex min-h-0 flex-1 flex-col gap-3 overflow-hidden px-4 pb-2">
{rightPanelContent}
</div>
{footerContent}
</DrawerContent>
</Drawer>
)
}
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent
@ -921,7 +1208,7 @@ export function SelectSpacesModal({
aria-hidden
/>
) : category.emoji ? (
<span className="text-base">{category.emoji}</span>
<SpaceGlyph emoji={category.emoji} size={16} />
) : category.id.startsWith("plugin:") ? (
<span
className="w-[18px] h-[18px] flex items-center justify-center rounded-[3px] bg-[#1E232B] text-[#FAFAFA] text-[10px] font-semibold uppercase"

View file

@ -0,0 +1,34 @@
export function SpaceFolderIcon({
size = 16,
className,
}: {
size?: number
className?: string
}) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
className={className}
aria-hidden
>
<title>Space</title>
{/* paper tab peeking above the folder */}
<rect x="7" y="3.2" width="10" height="5" rx="1.6" fill="#F5C518" />
{/* folder back with tab */}
<path
d="M2.6 8.8a2 2 0 0 1 2-2h4.2a2 2 0 0 1 1.5.68l1 1.13a2 2 0 0 0 1.5.69h6.6a2 2 0 0 1 2 2V17a2 2 0 0 1-2 2H4.6a2 2 0 0 1-2-2Z"
fill="#161D29"
stroke="#3C4658"
strokeWidth="1.3"
/>
{/* lighter front flap for depth */}
<path
d="M2.6 11.6h18.8V17a2 2 0 0 1-2 2H4.6a2 2 0 0 1-2-2Z"
fill="#27313F"
/>
</svg>
)
}

View file

@ -0,0 +1,24 @@
import { cn } from "@lib/utils"
import { SpaceFolderIcon } from "./space-folder-icon"
export function SpaceGlyph({
emoji,
size = 16,
className,
}: {
emoji?: string | null
size?: number
className?: string
}) {
if (!emoji || emoji === "📁") {
return <SpaceFolderIcon size={size} className={cn("shrink-0", className)} />
}
return (
<span
className={cn("shrink-0 leading-none", className)}
style={{ fontSize: size }}
>
{emoji}
</span>
)
}

View file

@ -12,6 +12,7 @@ import type { ContainerTagListType } from "@lib/types"
import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space"
import { AddSpaceModal } from "./add-space-modal"
import { SelectSpacesModal } from "./select-spaces-modal"
import { SpaceGlyph } from "./space-glyph"
import { useProjectMutations } from "@/hooks/use-project-mutations"
import { useContainerTags } from "@/hooks/use-container-tags"
import { motion } from "motion/react"
@ -418,40 +419,26 @@ export function SpaceSelector({
</span>
)
) : (
<span
className="shrink-0 text-sm font-bold tracking-[-0.98px]"
aria-hidden
>
{displayInfo.emoji}
</span>
)}
{!compact && (
<span
className={cn(
"min-w-0 truncate text-sm font-medium text-white",
"max-w-[10rem] md:max-w-[15rem]",
)}
title={isLoading ? undefined : displayInfo.name}
>
{isLoading ? "…" : displayInfo.name}
</span>
<SpaceGlyph emoji={displayInfo.emoji} size={compact ? 16 : 18} />
)}
<span
className={cn(
"min-w-0 truncate text-sm font-medium text-white",
compact ? "max-w-[7rem]" : "max-w-[10rem] md:max-w-[15rem]",
)}
title={isLoading ? undefined : displayInfo.name}
>
{isLoading ? "…" : displayInfo.name}
</span>
{!compact && spaceCountData !== undefined && spaceCountData > 0 && (
<span className="shrink-0 text-[11px] text-[#737373] tabular-nums">
· {formatCount(spaceCountData)}
</span>
)}
{!compact && (
<ChevronDownIcon
className="size-3.5 shrink-0 text-[#737373]"
aria-hidden
/>
)}
{compact && (
<span className="sr-only">
{isLoading ? "Loading" : displayInfo.name}
</span>
)}
<ChevronDownIcon
className="size-3.5 shrink-0 text-[#737373]"
aria-hidden
/>
</button>
</TooltipTrigger>
<TooltipContent side="bottom" className={dmSansClassName()}>
@ -633,7 +620,7 @@ export function SpaceSelector({
className="shrink-0 blur-[0.45px]!"
/>
) : (
<span>{p.emoji || "📁"}</span>
<SpaceGlyph emoji={p.emoji} size={16} />
)}
<span className="truncate">
{p.containerTag === DEFAULT_PROJECT_ID ? (