Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion apps/web/app/(app)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -548,13 +549,17 @@ export default function NewPage() {
const isDashboardShell =
viewMode === "dashboard" || (viewMode === "graph" && isMobile)
const isGraphMode = viewMode === "graph"
const showBottomNav = isMobile && !isChatView && !!session

return (
<HotkeysProvider>
<div
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 && (
Expand Down Expand Up @@ -723,14 +728,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)}
Expand Down
202 changes: 202 additions & 0 deletions apps/web/components/bottom-nav.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
4 changes: 2 additions & 2 deletions apps/web/components/dashboard-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,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}
Expand All @@ -779,7 +779,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
Expand Down
4 changes: 2 additions & 2 deletions apps/web/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
33 changes: 32 additions & 1 deletion apps/web/components/highlights-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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">
Expand Down
8 changes: 4 additions & 4 deletions apps/web/components/memories-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]",
)}
Expand All @@ -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]",
)}
Expand All @@ -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"
Expand Down
Loading
Loading