Compare commits

...

4 Commits

Author SHA1 Message Date
MangoPig
248a0b1828 Feat: Add notifications menu 2026-06-16 17:00:51 +01:00
MangoPig
fd429bdcdd Merge branch 'Features/Frontend/ProfileMenu' 2026-06-16 16:39:41 +01:00
MangoPig
bbebccfcf3 Feat: Add profile menu 2026-06-16 16:38:26 +01:00
MangoPig
fd67af7101 Merge branch 'Features/Server-Shell' 2026-06-16 13:11:59 +01:00
15 changed files with 929 additions and 7 deletions

View File

@@ -0,0 +1,66 @@
.button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 2.75rem;
height: 2.75rem;
aspect-ratio: 1;
padding: 0;
border: 0;
border-radius: 999px;
flex-shrink: 0;
cursor: pointer;
background: transparent;
color: var(--color-text-muted);
transition:
background-color 220ms var(--easing-standard),
color 220ms var(--easing-standard),
transform 180ms var(--easing-standard);
}
.button:hover {
background: color-mix(in srgb, var(--color-text) 8%, transparent);
color: var(--color-text);
}
.buttonOpen {
background: color-mix(in srgb, var(--color-surface) 92%, transparent);
color: var(--color-text);
}
.button:focus-visible {
outline: none;
background: color-mix(in srgb, var(--color-accent-strong) 12%, transparent);
box-shadow: 0 0 0 0.12rem color-mix(in srgb, var(--color-accent-strong) 28%, transparent);
color: var(--color-text);
}
.iconWrap {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
}
.badge {
@include text-caption;
position: absolute;
top: -0.45rem;
right: -0.7rem;
min-width: 1rem;
height: 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 0.24rem;
border: 1px solid color-mix(in srgb, var(--color-surface) 68%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--color-primary-3) 84%, black 16%);
color: white;
font-weight: var(--font-weight-semibold);
line-height: 1;
box-shadow: 0 6px 14px color-mix(in srgb, black 18%, transparent);
pointer-events: none;
}

View File

@@ -0,0 +1,36 @@
import type { JSX } from "solid-js";
import { Bell } from "../../../lib/icons";
import { unreadNotificationCount } from "../data/shell.data";
import styles from "./NotificationsButton.module.scss";
type NotificationsButtonProps = {
isOpen: boolean;
menuId: string;
onToggle: () => void;
};
export const NotificationsButton = (props: NotificationsButtonProps): JSX.Element => {
const hasUnread = unreadNotificationCount > 0;
const unreadLabel = hasUnread ? `, ${unreadNotificationCount} unread` : "";
return (
<button
classList={{
[styles.button]: true,
[styles.buttonOpen]: props.isOpen,
}}
type="button"
aria-label={`${props.isOpen ? "Close" : "Open"} notifications${unreadLabel}`}
title={`${props.isOpen ? "Close" : "Open"} notifications`}
aria-haspopup="menu"
aria-controls={props.menuId}
aria-expanded={props.isOpen}
onClick={props.onToggle}
>
<span class={styles.iconWrap} aria-hidden="true">
<Bell size={18} strokeWidth={2} />
{hasUnread ? <span class={styles.badge}>{unreadNotificationCount}</span> : null}
</span>
</button>
);
};

View File

@@ -0,0 +1,227 @@
.menu {
position: absolute;
top: calc(100% + var(--space-2));
right: 0;
width: min(24rem, 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;
}
.header,
.footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
}
.header {
padding-bottom: var(--space-3);
border-bottom: 1px solid var(--color-border);
}
.headerCopy {
min-width: 0;
display: grid;
gap: 0.08rem;
}
.title {
@include text-label;
color: var(--color-text);
}
.subtitle,
.sectionLabel,
.itemMeta,
.itemTime {
@include text-caption;
color: var(--color-text-muted);
}
.headerAction,
.footerAction {
@include text-caption;
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: 0;
border: 0;
background: transparent;
color: var(--color-text-muted);
transition: color 160ms var(--easing-standard);
}
.headerAction:hover,
.headerAction:focus-visible,
.footerAction:hover,
.footerAction:focus-visible {
outline: none;
color: var(--color-text);
}
.listWrap {
display: grid;
gap: var(--space-3);
max-height: min(24rem, 60vh);
overflow-y: auto;
padding-right: var(--space-1);
margin-right: calc(var(--space-1) * -1);
}
.stateCard {
display: grid;
justify-items: start;
gap: var(--space-2);
padding: var(--space-3);
border: 1px solid color-mix(in srgb, var(--color-border) 18%, transparent);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--color-surface) 92%, transparent);
}
.stateIcon {
width: 2.2rem;
height: 2.2rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: color-mix(in srgb, var(--color-surface-muted) 88%, transparent);
color: var(--color-text);
}
.stateTitle {
@include text-label;
color: var(--color-text);
}
.stateCopy {
@include text-caption;
color: var(--color-text-muted);
}
.section {
display: grid;
gap: var(--space-2);
}
.section + .section {
padding-top: var(--space-3);
border-top: 1px solid var(--color-border);
}
.sectionLabel {
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-subtle);
}
.list {
display: grid;
gap: 0.3rem;
}
.item {
width: 100%;
min-width: 0;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: start;
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);
}
.itemUnread {
background: color-mix(in srgb, var(--color-surface) 90%, transparent);
border-color: color-mix(in srgb, var(--color-border) 16%, transparent);
}
.itemMarker,
.itemMarkerMuted {
width: 0.5rem;
height: 0.5rem;
margin-top: 0.45rem;
border-radius: 999px;
flex-shrink: 0;
background: color-mix(in srgb, var(--color-primary-2) 78%, white 22%);
}
.itemMarkerMuted {
background: color-mix(in srgb, var(--color-text-subtle) 36%, transparent);
}
.itemBody {
min-width: 0;
display: grid;
gap: 0.12rem;
}
.itemTitle {
@include text-label;
color: var(--color-text);
}
.itemTime {
padding-top: 0.05rem;
white-space: nowrap;
color: var(--color-text-subtle);
}
.footer {
padding-top: var(--space-3);
border-top: 1px solid var(--color-border);
justify-content: space-between;
flex-wrap: wrap;
}
@include respond-down(mobile) {
.menu {
width: min(22rem, calc(100vw - (var(--space-3) * 2)));
}
.item {
grid-template-columns: auto minmax(0, 1fr);
}
.itemTime {
grid-column: 2;
padding-top: 0;
}
.footer {
align-items: flex-start;
flex-direction: column;
}
.footerAction {
padding: var(--space-1) 0;
}
}

View File

@@ -0,0 +1,112 @@
import { For, Show, type JSX } from "solid-js";
import { Bell, Settings } from "../../../lib/icons";
import { notificationItems, unreadNotificationCount } from "../data/shell.data";
import styles from "./NotificationsMenu.module.scss";
type NotificationsMenuProps = {
id: string;
menuRef: (element: HTMLDivElement) => void;
onSelect: () => void;
};
export const NotificationsMenu = (props: NotificationsMenuProps): JSX.Element => {
const unreadItems = notificationItems.filter((item) => item.unread);
const earlierItems = notificationItems.filter((item) => !item.unread);
const hasNotifications = notificationItems.length > 0;
const isCaughtUp = unreadItems.length === 0 && hasNotifications;
return (
<div id={props.id} class={styles.menu} role="menu" aria-label="Notifications" ref={props.menuRef}>
<div class={styles.header}>
<div class={styles.headerCopy}>
<strong class={styles.title}>Notifications</strong>
<span class={styles.subtitle}>
{unreadNotificationCount > 0
? `You have ${unreadNotificationCount} unread`
: "Youre all caught up"}
</span>
</div>
<Show when={unreadNotificationCount > 0}>
<button type="button" role="menuitem" class={styles.headerAction} onClick={props.onSelect}>
Mark all read
</button>
</Show>
</div>
<div class={styles.listWrap}>
<Show when={!hasNotifications}>
<div class={styles.stateCard}>
<span class={styles.stateIcon} aria-hidden="true">
<Bell size={18} strokeWidth={2} />
</span>
<strong class={styles.stateTitle}>No notifications yet</strong>
<span class={styles.stateCopy}>When activity starts across your workspace, itll show up here.</span>
</div>
</Show>
<Show when={isCaughtUp}>
<div class={styles.stateCard}>
<span class={styles.stateIcon} aria-hidden="true">
<Bell size={18} strokeWidth={2} />
</span>
<strong class={styles.stateTitle}>Youre all caught up</strong>
<span class={styles.stateCopy}>No unread notifications right now. Earlier activity is still available below.</span>
</div>
</Show>
<Show when={unreadItems.length > 0}>
<section class={styles.section} aria-label="Unread notifications">
<span class={styles.sectionLabel}>Unread</span>
<div class={styles.list}>
<For each={unreadItems}>
{(item): JSX.Element => (
<button type="button" role="menuitem" classList={{ [styles.item]: true, [styles.itemUnread]: true }} onClick={props.onSelect}>
<span class={styles.itemMarker} aria-hidden="true" />
<div class={styles.itemBody}>
<span class={styles.itemTitle}>{item.title}</span>
<span class={styles.itemMeta}>{item.contextLabel}</span>
</div>
<span class={styles.itemTime}>{item.timeLabel}</span>
</button>
)}
</For>
</div>
</section>
</Show>
<Show when={earlierItems.length > 0}>
<section class={styles.section} aria-label="Earlier notifications">
<span class={styles.sectionLabel}>Earlier</span>
<div class={styles.list}>
<For each={earlierItems}>
{(item): JSX.Element => (
<button type="button" role="menuitem" class={styles.item} onClick={props.onSelect}>
<span class={styles.itemMarkerMuted} aria-hidden="true" />
<div class={styles.itemBody}>
<span class={styles.itemTitle}>{item.title}</span>
<span class={styles.itemMeta}>{item.contextLabel}</span>
</div>
<span class={styles.itemTime}>{item.timeLabel}</span>
</button>
)}
</For>
</div>
</section>
</Show>
</div>
<div class={styles.footer}>
<button type="button" role="menuitem" class={styles.footerAction} onClick={props.onSelect}>
<Settings size={16} strokeWidth={2} />
<span>Notification settings</span>
</button>
<button type="button" role="menuitem" class={styles.footerAction} onClick={props.onSelect}>
<Bell size={16} strokeWidth={2} />
<span>View all notifications</span>
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1,5 @@
.root {
position: relative;
display: inline-flex;
align-items: center;
}

View File

@@ -0,0 +1,56 @@
import { createEffect, createSignal, createUniqueId, onCleanup, type JSX } from "solid-js";
import { NotificationsButton } from "./NotificationsButton";
import { NotificationsMenu } from "./NotificationsMenu";
import styles from "./NotificationsNav.module.scss";
export const NotificationsNav = (): 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<HTMLButtonElement>("[role='menuitem']")?.focus();
onCleanup(() => {
document.removeEventListener("pointerdown", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown);
});
});
return (
<div class={styles.root} ref={rootRef}>
<NotificationsButton isOpen={isOpen()} menuId={menuId} onToggle={toggleMenu} />
{isOpen() ? (
<NotificationsMenu id={menuId} menuRef={(element) => (menuRef = element)} onSelect={closeMenu} />
) : null}
</div>
);
};

View File

@@ -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)));
}
}

View File

@@ -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 (
<div id={props.id} class={styles.menu} role="menu" aria-label="Profile menu" ref={props.menuRef}>
<div class={styles.summary}>
<div class={styles.avatar} aria-hidden="true">
<span class={styles.avatarRing} />
<span class={styles.avatarCore}>
<User size={16} strokeWidth={2} />
</span>
</div>
<div class={styles.summaryCopy}>
<strong class={styles.name}>{activeUserProfile.name}</strong>
<span class={styles.email}>{activeUserProfile.email}</span>
<span class={styles.role}>{activeUserProfile.roleLabel}</span>
<span class={styles.context}>{activeUserProfile.contextLabel}</span>
</div>
</div>
<For each={profileMenuSections}>
{(section): JSX.Element => (
<div class={styles.section}>
<For each={section.items}>
{(item): JSX.Element => {
const Icon = item.icon;
return (
<button
type="button"
role="menuitem"
classList={{
[styles.item]: true,
[styles.itemDanger]: item.tone === "danger",
}}
onClick={props.onSelect}
>
<span class={styles.itemIcon} aria-hidden="true">
<Icon size={16} strokeWidth={2} />
</span>
<span class={styles.itemLabel}>{item.label}</span>
</button>
);
}}
</For>
</div>
)}
</For>
</div>
);
};

View File

@@ -4,8 +4,9 @@ import { For, type JSX } from "solid-js";
import type { Theme } from "../../../theme/runtime";
import { topBarActions } from "../data/shell.data";
import { DepartmentSelector } from "../DepartmentSelector/DepartmentSelector";
import { NotificationsNav } from "./NotificationsNav";
import { ThemeToggle } from "./ThemeToggle";
import { UserNavButton } from "./UserNavButton";
import { UserNav } from "./UserNav";
import styles from "./TopBar.module.scss";
type TopBarProps = {
@@ -36,8 +37,9 @@ export const TopBar = (props: TopBarProps): JSX.Element => {
</For>
</div>
<NotificationsNav />
<ThemeToggle theme={props.theme} onToggle={props.onToggleTheme} />
<UserNavButton />
<UserNav />
</div>
</header>
);

View File

@@ -0,0 +1,5 @@
.root {
position: relative;
display: inline-flex;
align-items: center;
}

View File

@@ -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<HTMLButtonElement>("[role='menuitem']")?.focus();
onCleanup(() => {
document.removeEventListener("pointerdown", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown);
});
});
return (
<div class={styles.root} ref={rootRef}>
<UserNavButton isOpen={isOpen()} menuId={menuId} onToggle={toggleMenu} />
{isOpen() ? <ProfileMenu id={menuId} menuRef={(element) => (menuRef = element)} onSelect={closeMenu} /> : null}
</div>
);
};

View File

@@ -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;
}

View File

@@ -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 (
<button class={styles.userButton} type="button" aria-label="Open profile" title="Open profile">
<button
classList={{
[styles.userButton]: true,
[styles.userButtonOpen]: props.isOpen,
}}
type="button"
aria-label={props.isOpen ? "Close profile menu" : "Open profile menu"}
title={props.isOpen ? "Close profile menu" : "Open profile menu"}
aria-haspopup="menu"
aria-controls={props.menuId}
aria-expanded={props.isOpen}
onClick={props.onToggle}
>
<span class={styles.spinContainer} aria-hidden="true">
<span class={styles.spinRing} />
</span>

View File

@@ -1,7 +1,20 @@
// Path: Frontend/src/components/shell/data/shell.data.ts
import type { Component } from "solid-js";
import { Bell, Folder, Home, LayoutGrid, Search, Settings, User } from "../../../lib/icons";
import {
Bell,
CircleHelp,
Folder,
Home,
Keyboard,
LayoutGrid,
LogOut,
Repeat,
Search,
Settings,
Shield,
User,
} from "../../../lib/icons";
type ShellIconProps = {
class?: string;
@@ -74,6 +87,33 @@ export type TopBarAction = {
icon: ShellIcon;
};
export type NotificationItem = {
id: string;
title: string;
contextLabel: string;
timeLabel: string;
unread?: boolean;
};
export type ProfileMenuAction = {
id: string;
label: string;
icon: ShellIcon;
tone?: "default" | "danger";
};
export type ProfileMenuSection = {
id: string;
items: readonly ProfileMenuAction[];
};
export type ActiveUserProfile = {
name: string;
email: string;
roleLabel: string;
contextLabel: string;
};
const personalDockActions: readonly ServerDockAction[] = [
{ id: "account", label: "Account", icon: User },
{ id: "settings", label: "Settings", icon: Settings },
@@ -134,5 +174,69 @@ export const serverSidebarItems: readonly SidebarItem[] = [
export const topBarActions: readonly TopBarAction[] = [
{ id: "search", label: "Search", icon: Search },
{ id: "inbox", label: "Inbox", icon: Bell },
] as const;
export const notificationItems: readonly NotificationItem[] = [
{
id: "comment-design-systems",
title: "New comment on Design Systems",
contextLabel: "Product • Review thread updated",
timeLabel: "2m ago",
unread: true,
},
{
id: "sprint-platform",
title: "Sprint updated in Platform",
contextLabel: "Engineering • Scope changed",
timeLabel: "15m ago",
unread: true,
},
{
id: "member-joined",
title: "New member joined Operations",
contextLabel: "Organization Name • Access granted",
timeLabel: "1h ago",
},
{
id: "daily-summary",
title: "Daily summary is ready",
contextLabel: "General • 8 updates across boards",
timeLabel: "Today, 8:00 AM",
},
] as const;
export const unreadNotificationCount = notificationItems.filter((item) => item.unread).length;
export const activeUserProfile: ActiveUserProfile = {
name: "Demo Account",
email: "demo@moku.work",
roleLabel: "Founder · Product",
contextLabel: "Organization Name • Design Systems",
};
export const profileMenuSections: readonly ProfileMenuSection[] = [
{
id: "account",
items: [
{ id: "profile", label: "Profile", icon: User },
{ id: "account-settings", label: "Account Settings", icon: Settings },
{ id: "notifications", label: "Notifications", icon: Bell },
{ id: "security", label: "Security", icon: Shield },
],
},
{
id: "preferences",
items: [
{ id: "keyboard-shortcuts", label: "Keyboard Shortcuts", icon: Keyboard },
{ id: "theme-preferences", label: "Theme Preferences", icon: Settings },
{ id: "help-support", label: "Help & Support", icon: CircleHelp },
],
},
{
id: "session",
items: [
{ id: "switch-account", label: "Switch Account", icon: Repeat },
{ id: "sign-out", label: "Sign Out", icon: LogOut, tone: "danger" },
],
},
] as const;

View File

@@ -1,13 +1,19 @@
// Path: Frontend/src/lib/icons/index.ts
export { default as Bell } from "lucide-solid/icons/bell";
export { default as CircleHelp } from "lucide-solid/icons/circle-help";
export { default as ChevronDown } from "lucide-solid/icons/chevron-down";
export { default as Folder } from "lucide-solid/icons/folder";
export { default as Home } from "lucide-solid/icons/house";
export { default as Keyboard } from "lucide-solid/icons/keyboard";
export { default as LayoutGrid } from "lucide-solid/icons/layout-grid";
export { default as LogOut } from "lucide-solid/icons/log-out";
export { default as Moon } from "lucide-solid/icons/moon";
export { default as Plus } from "lucide-solid/icons/plus";
export { default as Repeat } from "lucide-solid/icons/repeat";
export { default as Search } from "lucide-solid/icons/search";
export { default as Server } from "lucide-solid/icons/server";
export { default as Settings } from "lucide-solid/icons/settings";
export { default as Shield } from "lucide-solid/icons/shield";
export { default as Sun } from "lucide-solid/icons/sun";
export { default as User } from "lucide-solid/icons/user";