diff --git a/Frontend/src/components/shell/TopBar/ProfileMenu.module.scss b/Frontend/src/components/shell/TopBar/ProfileMenu.module.scss new file mode 100644 index 0000000..b12fd01 --- /dev/null +++ b/Frontend/src/components/shell/TopBar/ProfileMenu.module.scss @@ -0,0 +1,165 @@ +.menu { + position: absolute; + top: calc(100% + var(--space-2)); + right: 0; + width: min(21rem, calc(100vw - (var(--space-4) * 2))); + display: grid; + gap: var(--space-3); + padding: var(--space-3); + border: 1px solid var(--color-border-strong); + border-radius: calc(var(--radius-lg) + 0.1rem); + background: color-mix(in srgb, var(--color-surface-muted) 96%, transparent); + box-shadow: 0 18px 36px color-mix(in srgb, black 16%, transparent); + backdrop-filter: blur(18px); + z-index: 30; +} + +.summary { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: var(--space-3); + align-items: center; + padding-bottom: var(--space-3); + border-bottom: 1px solid var(--color-border); +} + +.avatar { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 3rem; + height: 3rem; + align-self: center; + margin-right: var(--space-1); +} + +.avatarRing { + position: absolute; + inset: 0; + border-radius: 999px; + background: + conic-gradient( + from 0deg, + transparent 0deg 24deg, + var(--color-primary-1) 24deg 118deg, + transparent 118deg 144deg, + var(--color-primary-2) 144deg 238deg, + transparent 238deg 264deg, + var(--color-primary-3) 264deg 356deg, + transparent 356deg 360deg + ); + mask: radial-gradient(circle, transparent 64%, black 67%); + -webkit-mask: radial-gradient(circle, transparent 64%, black 67%); +} + +.avatarCore { + @include text-label; + position: relative; + z-index: 1; + display: inline-flex; + align-items: center; + justify-content: center; + width: 78%; + height: 78%; + border-radius: 999px; + background: var(--color-surface); + color: var(--color-text); + font-weight: var(--font-weight-semibold); +} + +.summaryCopy { + min-width: 0; + display: grid; + gap: 0.08rem; +} + +.name, +.itemLabel { + @include text-label; +} + +.name { + color: var(--color-text); +} + +.email, +.role, +.context { + @include text-caption; + color: var(--color-text-muted); +} + +.context { + margin-top: var(--space-1); + color: var(--color-text-subtle); +} + +.section { + display: grid; + gap: 0.2rem; +} + +.section + .section { + padding-top: var(--space-3); + border-top: 1px solid var(--color-border); +} + +.item { + width: 100%; + min-width: 0; + min-height: 2.65rem; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: var(--space-2); + padding: var(--space-2); + border: 1px solid transparent; + border-radius: var(--radius-sm); + background: transparent; + color: var(--color-text); + text-align: left; + transition: + background-color 160ms var(--easing-standard), + border-color 160ms var(--easing-standard), + color 160ms var(--easing-standard); +} + +.item:hover { + background: color-mix(in srgb, var(--color-surface) 88%, transparent); + border-color: color-mix(in srgb, var(--color-border) 20%, transparent); +} + +.item:focus-visible { + outline: none; + background: color-mix(in srgb, var(--color-surface) 92%, transparent); + border-color: color-mix(in srgb, var(--color-accent-soft) 52%, transparent); + box-shadow: 0 0 0 0.12rem color-mix(in srgb, var(--color-accent-soft) 14%, transparent); +} + +.itemDanger { + color: color-mix(in srgb, var(--color-primary-3) 74%, var(--color-text) 26%); +} + +.itemDanger:hover, +.itemDanger:focus-visible { + background: color-mix(in srgb, var(--color-primary-3) 8%, transparent); + border-color: color-mix(in srgb, var(--color-primary-3) 16%, transparent); +} + +.itemIcon { + width: 1.9rem; + height: 1.9rem; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + background: color-mix(in srgb, var(--color-surface) 92%, transparent); + color: currentColor; +} + +@include respond-down(mobile) { + .menu { + width: min(20rem, calc(100vw - (var(--space-3) * 2))); + } +} diff --git a/Frontend/src/components/shell/TopBar/ProfileMenu.tsx b/Frontend/src/components/shell/TopBar/ProfileMenu.tsx new file mode 100644 index 0000000..5d462a1 --- /dev/null +++ b/Frontend/src/components/shell/TopBar/ProfileMenu.tsx @@ -0,0 +1,61 @@ +import { For, type JSX } from "solid-js"; +import { User } from "../../../lib/icons"; +import { activeUserProfile, profileMenuSections } from "../data/shell.data"; +import styles from "./ProfileMenu.module.scss"; + +type ProfileMenuProps = { + id: string; + menuRef: (element: HTMLDivElement) => void; + onSelect: () => void; +}; + +export const ProfileMenu = (props: ProfileMenuProps): JSX.Element => { + return ( + + ); +}; diff --git a/Frontend/src/components/shell/TopBar/TopBar.tsx b/Frontend/src/components/shell/TopBar/TopBar.tsx index 4bb49a2..7b6bd13 100644 --- a/Frontend/src/components/shell/TopBar/TopBar.tsx +++ b/Frontend/src/components/shell/TopBar/TopBar.tsx @@ -5,7 +5,7 @@ import type { Theme } from "../../../theme/runtime"; import { topBarActions } from "../data/shell.data"; import { DepartmentSelector } from "../DepartmentSelector/DepartmentSelector"; import { ThemeToggle } from "./ThemeToggle"; -import { UserNavButton } from "./UserNavButton"; +import { UserNav } from "./UserNav"; import styles from "./TopBar.module.scss"; type TopBarProps = { @@ -37,7 +37,7 @@ export const TopBar = (props: TopBarProps): JSX.Element => { - + ); diff --git a/Frontend/src/components/shell/TopBar/UserNav.module.scss b/Frontend/src/components/shell/TopBar/UserNav.module.scss new file mode 100644 index 0000000..bbfe5ed --- /dev/null +++ b/Frontend/src/components/shell/TopBar/UserNav.module.scss @@ -0,0 +1,5 @@ +.root { + position: relative; + display: inline-flex; + align-items: center; +} diff --git a/Frontend/src/components/shell/TopBar/UserNav.tsx b/Frontend/src/components/shell/TopBar/UserNav.tsx new file mode 100644 index 0000000..13bf17e --- /dev/null +++ b/Frontend/src/components/shell/TopBar/UserNav.tsx @@ -0,0 +1,54 @@ +import { createEffect, createSignal, createUniqueId, onCleanup, type JSX } from "solid-js"; +import { ProfileMenu } from "./ProfileMenu"; +import { UserNavButton } from "./UserNavButton"; +import styles from "./UserNav.module.scss"; + +export const UserNav = (): JSX.Element => { + const [isOpen, setIsOpen] = createSignal(false); + const menuId = createUniqueId(); + let rootRef: HTMLDivElement | undefined; + let menuRef: HTMLDivElement | undefined; + + const closeMenu = (): void => { + setIsOpen(false); + }; + + const toggleMenu = (): void => { + setIsOpen((open) => !open); + }; + + createEffect(() => { + if (!isOpen()) return; + + const handlePointerDown = (event: PointerEvent): void => { + if (!rootRef) return; + + const target = event.target; + if (target instanceof Node && !rootRef.contains(target)) { + closeMenu(); + } + }; + + const handleKeyDown = (event: KeyboardEvent): void => { + if (event.key === "Escape") { + closeMenu(); + } + }; + + document.addEventListener("pointerdown", handlePointerDown); + document.addEventListener("keydown", handleKeyDown); + menuRef?.querySelector("[role='menuitem']")?.focus(); + + onCleanup(() => { + document.removeEventListener("pointerdown", handlePointerDown); + document.removeEventListener("keydown", handleKeyDown); + }); + }); + + return ( +
+ + {isOpen() ? (menuRef = element)} onSelect={closeMenu} /> : null} +
+ ); +}; diff --git a/Frontend/src/components/shell/TopBar/UserNavButton.module.scss b/Frontend/src/components/shell/TopBar/UserNavButton.module.scss index d33f387..64107d4 100644 --- a/Frontend/src/components/shell/TopBar/UserNavButton.module.scss +++ b/Frontend/src/components/shell/TopBar/UserNavButton.module.scss @@ -23,7 +23,12 @@ color: var(--color-text); } -.userButton:hover .spinContainer { +.userButtonOpen { + color: var(--color-text); +} + +.userButton:hover .spinContainer, +.userButtonOpen .spinContainer { animation-play-state: running; opacity: 1; } diff --git a/Frontend/src/components/shell/TopBar/UserNavButton.tsx b/Frontend/src/components/shell/TopBar/UserNavButton.tsx index 06b3955..6d28932 100644 --- a/Frontend/src/components/shell/TopBar/UserNavButton.tsx +++ b/Frontend/src/components/shell/TopBar/UserNavButton.tsx @@ -4,9 +4,27 @@ import type { JSX } from "solid-js"; import { User } from "../../../lib/icons"; import styles from "./UserNavButton.module.scss"; -export const UserNavButton = (): JSX.Element => { +type UserNavButtonProps = { + isOpen: boolean; + menuId: string; + onToggle: () => void; +}; + +export const UserNavButton = (props: UserNavButtonProps): JSX.Element => { return ( -