diff --git a/components/dashboard/Sidebar.tsx b/components/dashboard/Sidebar.tsx index e8d080b..fb74190 100644 --- a/components/dashboard/Sidebar.tsx +++ b/components/dashboard/Sidebar.tsx @@ -386,31 +386,28 @@ export default function Sidebar({ {/* Bottom — utility items */}
- {/* Theme, Notifications, Profile */} + {/* Theme, Notifications, Profile — same layout as nav items */}
-
- + -
-
- + + -
-
- router.push('/onboarding')} - allowPersonalOrganization={false} - onOpenSettings={openSettings} - compact - anchor="right" - /> + + router.push('/onboarding')} + allowPersonalOrganization={false} + onOpenSettings={openSettings} + compact + anchor="right" + > -
+
{/* Settings + Collapse */} diff --git a/components/notifications/NotificationCenter.tsx b/components/notifications/NotificationCenter.tsx index 2b74a18..945bf79 100644 --- a/components/notifications/NotificationCenter.tsx +++ b/components/notifications/NotificationCenter.tsx @@ -4,7 +4,7 @@ * @file Notification center: bell icon with dropdown of recent notifications. */ -import { useEffect, useState, useRef } from 'react' +import { useEffect, useState, useRef, useCallback } from 'react' import Link from 'next/link' import { listNotifications, markNotificationRead, markAllNotificationsRead, type Notification } from '@/lib/api/notifications' import { getAuthErrorMessage } from '@ciphera-net/ui' @@ -37,13 +37,31 @@ function BellIcon({ className }: { className?: string }) { const LOADING_DELAY_MS = 250 const POLL_INTERVAL_MS = 90_000 -export default function NotificationCenter({ anchor = 'bottom' }: { anchor?: 'bottom' | 'right' }) { +interface NotificationCenterProps { + /** Where the dropdown opens. 'right' uses fixed positioning to escape overflow:hidden containers. */ + anchor?: 'bottom' | 'right' + /** Render variant. 'sidebar' matches NavLink styling. */ + variant?: 'default' | 'sidebar' + /** Optional label content rendered after the icon (useful for sidebar variant with fading labels). */ + children?: React.ReactNode +} + +export default function NotificationCenter({ anchor = 'bottom', variant = 'default', children }: NotificationCenterProps) { const [open, setOpen] = useState(false) const [notifications, setNotifications] = useState([]) const [unreadCount, setUnreadCount] = useState(0) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const dropdownRef = useRef(null) + const buttonRef = useRef(null) + const [fixedPos, setFixedPos] = useState<{ left: number; top: number } | null>(null) + + const updatePosition = useCallback(() => { + if (anchor === 'right' && buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect() + setFixedPos({ left: rect.right + 8, top: rect.top }) + } + }, [anchor]) const fetchUnreadCount = async () => { try { @@ -74,8 +92,9 @@ export default function NotificationCenter({ anchor = 'bottom' }: { anchor?: 'bo useEffect(() => { if (open) { fetchNotifications() + updatePosition() } - }, [open]) + }, [open, updatePosition]) // * Poll unread count in background (when authenticated) useEffect(() => { @@ -130,20 +149,40 @@ export default function NotificationCenter({ anchor = 'bottom' }: { anchor?: 'bo setOpen(false) } + const isSidebar = variant === 'sidebar' + return (
@@ -152,11 +191,12 @@ export default function NotificationCenter({ anchor = 'bottom' }: { anchor?: 'bo id="notification-dropdown" role="dialog" aria-label="Notifications" - className={`fixed left-4 right-4 top-16 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl overflow-hidden z-[100] ${ + className={`bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl overflow-hidden z-[100] ${ anchor === 'right' - ? 'sm:absolute sm:left-full sm:top-0 sm:ml-2 sm:right-auto sm:w-96' - : 'sm:absolute sm:left-auto sm:right-0 sm:top-full sm:mt-2 sm:w-96' + ? 'fixed w-96 origin-top-left' + : 'fixed left-4 right-4 top-16 sm:absolute sm:left-auto sm:right-0 sm:top-full sm:mt-2 sm:w-96' }`} + style={anchor === 'right' && fixedPos ? { left: fixedPos.left, top: fixedPos.top } : undefined} >

Notifications

diff --git a/package-lock.json b/package-lock.json index f0d99e0..30681a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "pulse-frontend", "version": "0.15.0-alpha", "dependencies": { - "@ciphera-net/ui": "^0.2.12", + "@ciphera-net/ui": "^0.2.13", "@ducanh2912/next-pwa": "^10.2.9", "@phosphor-icons/react": "^2.1.10", "@simplewebauthn/browser": "^13.2.2", @@ -1670,9 +1670,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.2.12", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.2.12/d6780a19e3f0128431d6ddb3166798f01fba28dc", - "integrity": "sha512-LQCG2ErTXBGB7PDRoco9uK6cE+9IenER9Yrxkc0BfvSG46irUwmI19H1WPzLohMQdy2Wu99eu/PP+0X7fQw3wA==", + "version": "0.2.13", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.2.13/710acb1221e5906b8fd622ff6c77b469781c449d", + "integrity": "sha512-pHo0DkkjAr7qlnPy1GBtHm3XFQlUC8b/FQM5PCu9JvW3pHg6znufyCP/8Wqbu6jOIt4C+c6CSKvMKzeYPKi42Q==", "dependencies": { "@phosphor-icons/react": "^2.1.10", "class-variance-authority": "^0.7.1", diff --git a/package.json b/package.json index 9b2c9b9..7f9eb1a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "test:watch": "vitest" }, "dependencies": { - "@ciphera-net/ui": "^0.2.12", + "@ciphera-net/ui": "^0.2.13", "@ducanh2912/next-pwa": "^10.2.9", "@phosphor-icons/react": "^2.1.10", "@simplewebauthn/browser": "^13.2.2",