diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index fd2fd1716..768506236 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -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" @@ -548,6 +549,7 @@ export default function NewPage() { const isDashboardShell = viewMode === "dashboard" || (viewMode === "graph" && isMobile) const isGraphMode = viewMode === "graph" + const showBottomNav = isMobile && !isChatView && !!session return ( @@ -555,6 +557,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 && ( @@ -723,14 +728,37 @@ export default function NewPage() { + {isDashboardShell && showBottomNav && ( +
+ )} {isDashboardShell && ( -
+
)} + {showBottomNav && ( + { + analytics.addDocumentModalOpened() + setAddDoc("note") + }} + onOpenSearch={() => { + analytics.searchOpened({ source: "header" }) + setIsSearchOpen(true) + }} + /> + )} + setAddDoc(null)} diff --git a/apps/web/components/bottom-nav.tsx b/apps/web/components/bottom-nav.tsx new file mode 100644 index 000000000..a134b54ae --- /dev/null +++ b/apps/web/components/bottom-nav.tsx @@ -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 ( + + ) +} + +function NavTab({ + label, + icon: Icon, + active, + onClick, +}: { + label: string + icon: React.ComponentType<{ className?: string }> + active: boolean + onClick: () => void +}) { + return ( + + + + ) +} + +function NavTabButton({ + label, + active, + onClick, + children, + ...props +}: { + label: string + active: boolean + onClick?: () => void + children: React.ReactNode +} & React.ComponentProps<"button">) { + return ( + + ) +} + +function MoreItem({ + icon: Icon, + label, + onClick, +}: { + icon: React.ComponentType<{ className?: string }> + label: string + onClick?: () => void +}) { + return ( + + + {label} + + ) +} diff --git a/apps/web/components/dashboard-view.tsx b/apps/web/components/dashboard-view.tsx index 64f601fba..907b5d87a 100644 --- a/apps/web/components/dashboard-view.tsx +++ b/apps/web/components/dashboard-view.tsx @@ -767,7 +767,7 @@ export function DashboardView({ Home

{homeHeadline} @@ -779,7 +779,7 @@ export function DashboardView({

- + + + + + +

+ Daily Brief +

+

+ AI-generated highlights and questions drawn from your memories. It + refreshes automatically every few hours โ€” tap the refresh icon to + update it now. +

+
+
diff --git a/apps/web/components/memories-grid.tsx b/apps/web/components/memories-grid.tsx index 6d2f1038d..795056cf9 100644 --- a/apps/web/components/memories-grid.tsx +++ b/apps/web/components/memories-grid.tsx @@ -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" > -
+
-
+
{/* View mode toggle โ€” segmented control */}
(null) const editingContainerTag = editingProject?.containerTag const currentSelection = selectedProjects[0] ?? "" + const isMobile = useIsMobile() const pluginTags = useMemo( () => @@ -645,7 +649,11 @@ export function SelectSpacesModal({ {isEditing ? (
- {project.emoji || "๐Ÿ“"} + ) : ( - - {project.emoji || "๐Ÿ“"} - + )} { + const isActive = activeCategory === category.id + return ( + + ) + } + + const rightPanelContent = activeCategory.startsWith("discover:") ? ( + { + if (activeDiscoverId) connectMutation.mutate(activeDiscoverId) + }} + onDismissKey={() => setNewKey(null)} + /> + ) : ( + <> +
+ + 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(), + )} + /> +
+
+ {filteredProjects.length === 0 ? ( +

+ No spaces found +

+ ) : ( +
+ {showAutoRow && ( + <> +
+ Mode +
+ {renderAutoRow()} +
+ + )} + {recentProjects.length > 0 && ( + <> +
+ + Recently used +
+ {recentProjects.map(renderRow)} +
+
+ All spaces +
+ + )} + {mainList.map(renderRow)} +
+ )} +
+ + ) + + const footerContent = !activeCategory.startsWith("discover:") && + (isBulkDeleteMode || (showNewSpace && onNewSpace)) && ( +
+ {isBulkDeleteMode ? ( + <> +

+ {bulkDeleteCount === 0 + ? "No spaces selected" + : `${bulkDeleteCount} ${ + bulkDeleteCount === 1 ? "space" : "spaces" + } selected`} +

+
+ + +
+ + ) : ( + <> + + {showNewSpace && onNewSpace && ( + + )} + + )} +
+ ) + + if (isMobile) { + return ( + + + Select Space +
+
+

+ Select Space +

+

+ {isBulkDeleteMode + ? "Choose spaces to permanently delete" + : "Filter your memories by space"} +

+
+
+ {enableDelete && onBulkDeleteRequest && !activeDiscoverId && ( + + )} + +
+
+ +
+ {categories.map((category) => renderCategoryChip(category, false))} + {discoverCategories.length > 0 && ( + <> +
+ {discoverCategories.map((category) => + renderCategoryChip(category, true), + )} + + )} +
+ +
+ {rightPanelContent} +
+ + {footerContent} + + + ) + } + return ( ) : category.emoji ? ( - {category.emoji} + ) : category.id.startsWith("plugin:") ? ( + Space + {/* paper tab peeking above the folder */} + + {/* folder back with tab */} + + {/* lighter front flap for depth */} + + + ) +} diff --git a/apps/web/components/space-glyph.tsx b/apps/web/components/space-glyph.tsx new file mode 100644 index 000000000..5d789c0b2 --- /dev/null +++ b/apps/web/components/space-glyph.tsx @@ -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 + } + return ( + + {emoji} + + ) +} diff --git a/apps/web/components/space-selector.tsx b/apps/web/components/space-selector.tsx index 60f65c535..5a18937e6 100644 --- a/apps/web/components/space-selector.tsx +++ b/apps/web/components/space-selector.tsx @@ -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({ ) ) : ( - - {displayInfo.emoji} - - )} - {!compact && ( - - {isLoading ? "โ€ฆ" : displayInfo.name} - + )} + + {isLoading ? "โ€ฆ" : displayInfo.name} + {!compact && spaceCountData !== undefined && spaceCountData > 0 && ( ยท {formatCount(spaceCountData)} )} - {!compact && ( - - )} - {compact && ( - - {isLoading ? "Loading" : displayInfo.name} - - )} + @@ -633,7 +620,7 @@ export function SpaceSelector({ className="shrink-0 blur-[0.45px]!" /> ) : ( - {p.emoji || "๐Ÿ“"} + )} {p.containerTag === DEFAULT_PROJECT_ID ? (