diff --git a/apps/web/app/(app)/settings/page.tsx b/apps/web/app/(app)/settings/page.tsx
index 26e646fc..9d47b838 100644
--- a/apps/web/app/(app)/settings/page.tsx
+++ b/apps/web/app/(app)/settings/page.tsx
@@ -1,6 +1,6 @@
"use client"
import { Logo } from "@ui/assets/Logo"
-import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
+import { UserProfileMenu } from "@/components/user-profile-menu"
import { useAuth } from "@lib/auth-context"
import { motion } from "motion/react"
import NovaOrb from "@/components/nova/nova-orb"
@@ -14,6 +14,7 @@ import Support from "@/components/settings/support"
import { ErrorBoundary } from "@/components/error-boundary"
import { useRouter } from "next/navigation"
import { useIsMobile } from "@hooks/use-mobile"
+import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
import { analytics } from "@/lib/analytics"
import { Sun } from "lucide-react"
@@ -137,6 +138,7 @@ export default function SettingsPage() {
const hasInitialized = useRef(false)
const router = useRouter()
const isMobile = useIsMobile()
+ const localStorageUsername = useLocalStorageUsername()
useEffect(() => {
if (hasInitialized.current) return
@@ -163,28 +165,57 @@ export default function SettingsPage() {
window.addEventListener("hashchange", handleHashChange)
return () => window.removeEventListener("hashchange", handleHashChange)
}, [])
+
+ const headerDisplayName =
+ user?.displayUsername || localStorageUsername || user?.name || ""
+ const headerPossessive = headerDisplayName
+ ? `${headerDisplayName.split(" ")[0]}'s`
+ : ""
+
return (
-
-
-
- {user && (
-
-
- {user?.name?.charAt(0)}
-
+
+
+
-
+
{!isMobile && (
{
diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx
index a94d7e2d..38c5c68c 100644
--- a/apps/web/components/header.tsx
+++ b/apps/web/components/header.tsx
@@ -1,22 +1,18 @@
"use client"
import { Logo } from "@ui/assets/Logo"
-import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
import { useAuth } from "@lib/auth-context"
import {
LayoutGridIcon,
Plus,
SearchIcon,
- LogOut,
Settings,
Home,
Code2,
Sun,
ExternalLink,
- HelpCircle,
MenuIcon,
MessageCircleIcon,
- RotateCcw,
} from "lucide-react"
import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
@@ -30,13 +26,13 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@ui/components/dropdown-menu"
-import { authClient } from "@lib/auth"
import { useProject } from "@/stores"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { SpaceSelector } from "./space-selector"
import { useIsMobile } from "@hooks/use-mobile"
-import { useOrgOnboarding } from "@hooks/use-org-onboarding"
+import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
+import { UserProfileMenu } from "@/components/user-profile-menu"
import { FeedbackModal } from "./feedback-modal"
import { useViewMode } from "@/lib/view-mode-context"
import { useQueryState } from "nuqs"
@@ -53,7 +49,6 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
const { selectedProjects, setSelectedProjects } = useProject()
const router = useRouter()
const isMobile = useIsMobile()
- const { resetOrgOnboarded } = useOrgOnboarding()
const [feedbackOpen, setFeedbackOpen] = useQueryState(
"feedback",
feedbackParam,
@@ -61,18 +56,11 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
const isFeedbackOpen = feedbackOpen ?? false
const { viewMode, setViewMode } = useViewMode()
- const handleTryOnboarding = () => {
- resetOrgOnboarded()
- router.push("/onboarding?step=input&flow=welcome")
- }
-
const handleFeedback = () => setFeedbackOpen(true)
+ const localStorageUsername = useLocalStorageUsername()
const displayName =
- user?.displayUsername ||
- (typeof window !== "undefined" && localStorage.getItem("username")) ||
- (typeof window !== "undefined" && localStorage.getItem("userName")) ||
- ""
+ user?.displayUsername || localStorageUsername || user?.name || ""
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
return (
@@ -316,96 +304,7 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
>
)}
- {user && (
-
-
-
-
-
-
-
- {user?.name}
-
-
{user?.email}
-
-
- router.push("/settings")}
- className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
- >
-
- Settings
-
-
-
- Restart Onboarding
-
-
-
-
-
- Help & Support
-
-
-
-
-
- Discord
-
-
-
- {
- authClient.signOut()
- router.push("/login/new")
- }}
- className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
- >
-
- Logout
-
-
-
- )}
+
-
-
- {displayName && (
-
-
- {userName}
-
-
- supermemory
-
-
+
{user && (
-
-
- {user?.name?.charAt(0)}
-
+
)}
)
diff --git a/apps/web/components/share-modal.tsx b/apps/web/components/share-modal.tsx
index 4f09c7e5..f565b56b 100644
--- a/apps/web/components/share-modal.tsx
+++ b/apps/web/components/share-modal.tsx
@@ -14,6 +14,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon, Download, Copy, Check } from "lucide-react"
import { GradientLogo } from "@ui/assets/Logo"
import { useAuth } from "@lib/auth-context"
+import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
import { toast } from "sonner"
import * as htmlToImage from "html-to-image"
@@ -32,6 +33,7 @@ const XIcon2 = ({ className }: { className?: string }) => (
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
+ aria-hidden="true"
>
(
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
+ aria-hidden="true"
>
(
viewBox="0 0 13 13"
fill="none"
xmlns="http://www.w3.org/2000/svg"
+ aria-hidden="true"
>
(null)
+ const localStorageUsername = useLocalStorageUsername()
const displayName =
- user?.displayUsername ||
- (typeof window !== "undefined" ? localStorage.getItem("username") : null) ||
- (typeof window !== "undefined" ? localStorage.getItem("userName") : null) ||
- ""
+ user?.displayUsername || localStorageUsername || user?.name || ""
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "Your"
const capturePreview = useCallback(async (): Promise => {
diff --git a/apps/web/components/user-profile-menu.tsx b/apps/web/components/user-profile-menu.tsx
new file mode 100644
index 00000000..6ffbf108
--- /dev/null
+++ b/apps/web/components/user-profile-menu.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
+import { useAuth } from "@lib/auth-context"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@ui/components/dropdown-menu"
+import { authClient } from "@lib/auth"
+import { useRouter } from "next/navigation"
+import { LogOut, Settings, RotateCcw, HelpCircle } from "lucide-react"
+import { cn } from "@lib/utils"
+import { dmSansClassName } from "@/lib/fonts"
+import { useOrgOnboarding } from "@hooks/use-org-onboarding"
+
+export function UserProfileMenu({
+ className,
+ avatarClassName,
+}: {
+ className?: string
+ avatarClassName?: string
+}) {
+ const { user } = useAuth()
+ const router = useRouter()
+ const { resetOrgOnboarded } = useOrgOnboarding()
+
+ const handleTryOnboarding = () => {
+ resetOrgOnboarded()
+ router.push("/onboarding?step=input&flow=welcome")
+ }
+
+ const handleSignOut = () => {
+ void (async () => {
+ try {
+ await authClient.signOut()
+ } catch {
+ // still navigate if the request fails (offline, etc.)
+ }
+ router.push("/login/new")
+ })()
+ }
+
+ if (!user) return null
+
+ return (
+
+
+
+
+
+
+
{user.name}
+
{user.email}
+
+
+ router.push("/settings")}
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
+ >
+
+ Settings
+
+
+
+ Restart Onboarding
+
+
+
+
+
+ Help & Support
+
+
+
+
+
+ Discord
+
+
+
+
+
+ Logout
+
+
+
+ )
+}
diff --git a/apps/web/globals.css b/apps/web/globals.css
index e1adf870..f0597bfb 100644
--- a/apps/web/globals.css
+++ b/apps/web/globals.css
@@ -10,19 +10,47 @@
:root {
--color-placeholder-onboarding: #525966;
+ --sm-scrollbar-thumb: rgb(41 57 82 / 0.55);
+ --sm-scrollbar-thumb-hover: rgb(55 90 130 / 0.75);
+ --sm-scrollbar-thumb-active: rgb(70 110 160 / 0.85);
+}
+
+* {
+ scrollbar-width: thin;
+ scrollbar-color: var(--sm-scrollbar-thumb) transparent;
+}
+
+*::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+*::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+*::-webkit-scrollbar-thumb {
+ background-color: var(--sm-scrollbar-thumb);
+ border-radius: 100px;
+ border: 2px solid transparent;
+ background-clip: padding-box;
+}
+
+*::-webkit-scrollbar-thumb:hover {
+ background-color: var(--sm-scrollbar-thumb-hover);
+}
+
+*::-webkit-scrollbar-thumb:active {
+ background-color: var(--sm-scrollbar-thumb-active);
+}
+
+*::-webkit-scrollbar-corner {
+ background: transparent;
}
.scrollbar-thin {
scrollbar-width: thin;
- scrollbar-color: #293952 transparent;
-}
-
-.scrollbar-thin::-webkit-scrollbar {
- width: 6px;
-}
-
-.scrollbar-thin::-webkit-scrollbar-track {
- background: transparent;
+ scrollbar-color: var(--sm-scrollbar-thumb) transparent;
}
.sm-tweet-theme .react-tweet-theme {
diff --git a/biome.json b/biome.json
index c4e233d6..27f83933 100644
--- a/biome.json
+++ b/biome.json
@@ -100,6 +100,16 @@
}
}
}
+ },
+ {
+ "includes": ["**/globals.css"],
+ "linter": {
+ "rules": {
+ "complexity": {
+ "noImportantStyles": "off"
+ }
+ }
+ }
}
],
"vcs": {
diff --git a/packages/hooks/use-local-storage-username.ts b/packages/hooks/use-local-storage-username.ts
new file mode 100644
index 00000000..43743fd3
--- /dev/null
+++ b/packages/hooks/use-local-storage-username.ts
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+/**
+ * `username` / `userName` keys written by the app. Populated only after mount so
+ * server HTML and the first client render stay aligned (no hydration mismatch).
+ */
+export function useLocalStorageUsername(): string {
+ const [value, setValue] = React.useState("")
+
+ React.useEffect(() => {
+ setValue(
+ localStorage.getItem("username") ??
+ localStorage.getItem("userName") ??
+ "",
+ )
+ }, [])
+
+ return value
+}