2bec205a30
- Bell icon in icon registry (Phosphor Bell) - NotificationProvider context with polling (15s unread / 30s list) - useNotifications() hook with optimistic updates - NotificationCenter component: bell + badge + dropdown panel - NotificationItem component: severity icon, title, relative time, actions - AppShell integration: mount in header-actions, hidden on mobile - CSS styles: dropdown, items, unread/read states, empty state - formatRelativeTime utility (custom, no new deps) - 25 frontend tests (9 hook + 6 item + 10 center) Quality gates: vitest 25 passed, tsc clean, eslint clean
177 lines
2.6 KiB
TypeScript
177 lines
2.6 KiB
TypeScript
import React from "react";
|
|
import {
|
|
House,
|
|
Folder,
|
|
GitBranch,
|
|
Gear,
|
|
User,
|
|
SignOut,
|
|
Plus,
|
|
PencilSimple,
|
|
Trash,
|
|
FloppyDisk,
|
|
X,
|
|
ArrowsClockwise,
|
|
Copy,
|
|
MagnifyingGlass,
|
|
List,
|
|
Check,
|
|
Warning,
|
|
Info,
|
|
Spinner,
|
|
GitCommit,
|
|
GitMerge,
|
|
ClockCounterClockwise,
|
|
ArrowDown,
|
|
ArrowUp,
|
|
File,
|
|
FileText,
|
|
Image,
|
|
Binary,
|
|
Code,
|
|
ArrowSquareOut,
|
|
Play,
|
|
Stop,
|
|
Terminal,
|
|
ArrowLeft,
|
|
DotsSixVertical,
|
|
Bell,
|
|
} from "@phosphor-icons/react";
|
|
|
|
export type IconName =
|
|
| "dashboard"
|
|
| "projects"
|
|
| "repositories"
|
|
| "settings"
|
|
| "profile"
|
|
| "logout"
|
|
| "add"
|
|
| "edit"
|
|
| "delete"
|
|
| "save"
|
|
| "cancel"
|
|
| "refresh"
|
|
| "copy"
|
|
| "search"
|
|
| "menu"
|
|
| "close"
|
|
| "success"
|
|
| "error"
|
|
| "warning"
|
|
| "info"
|
|
| "loading"
|
|
| "branch"
|
|
| "commit"
|
|
| "merge"
|
|
| "history"
|
|
| "pull"
|
|
| "push"
|
|
| "fetch"
|
|
| "file"
|
|
| "folder"
|
|
| "code"
|
|
| "document"
|
|
| "image"
|
|
| "binary"
|
|
| "external"
|
|
| "play"
|
|
| "stop"
|
|
| "terminal"
|
|
| "arrow-left"
|
|
| "drag"
|
|
| "bell";
|
|
|
|
const iconMap: Record<
|
|
IconName,
|
|
React.ComponentType<{
|
|
size?: number | string;
|
|
weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone";
|
|
}>
|
|
> = {
|
|
dashboard: House,
|
|
projects: Folder,
|
|
repositories: GitBranch,
|
|
settings: Gear,
|
|
profile: User,
|
|
logout: SignOut,
|
|
add: Plus,
|
|
edit: PencilSimple,
|
|
delete: Trash,
|
|
save: FloppyDisk,
|
|
cancel: X,
|
|
refresh: ArrowsClockwise,
|
|
copy: Copy,
|
|
search: MagnifyingGlass,
|
|
menu: List,
|
|
close: X,
|
|
success: Check,
|
|
error: X,
|
|
warning: Warning,
|
|
info: Info,
|
|
loading: Spinner,
|
|
branch: GitBranch,
|
|
commit: GitCommit,
|
|
merge: GitMerge,
|
|
history: ClockCounterClockwise,
|
|
pull: ArrowDown,
|
|
push: ArrowUp,
|
|
fetch: ArrowsClockwise,
|
|
file: File,
|
|
folder: Folder,
|
|
code: Code,
|
|
document: FileText,
|
|
image: Image,
|
|
binary: Binary,
|
|
external: ArrowSquareOut,
|
|
play: Play,
|
|
stop: Stop,
|
|
terminal: Terminal,
|
|
"arrow-left": ArrowLeft,
|
|
drag: DotsSixVertical,
|
|
bell: Bell,
|
|
};
|
|
|
|
export interface IconProps {
|
|
name: IconName;
|
|
size?: "sm" | "md" | "lg" | "xl";
|
|
color?: string;
|
|
weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone";
|
|
className?: string;
|
|
ariaLabel?: string;
|
|
}
|
|
|
|
const sizeMap: Record<NonNullable<IconProps["size"]>, number> = {
|
|
sm: 16,
|
|
md: 20,
|
|
lg: 24,
|
|
xl: 32,
|
|
};
|
|
|
|
export const Icon: React.FC<IconProps> = ({
|
|
name,
|
|
size = "md",
|
|
color,
|
|
weight = "regular",
|
|
className,
|
|
ariaLabel,
|
|
}) => {
|
|
const IconComponent = iconMap[name];
|
|
const sizeValue = sizeMap[size];
|
|
|
|
if (!IconComponent) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<span
|
|
className={`icon icon-${size}${className ? ` ${className}` : ""}`}
|
|
style={{ color }}
|
|
aria-label={ariaLabel}
|
|
aria-hidden={!ariaLabel}
|
|
role="img"
|
|
>
|
|
<IconComponent size={sizeValue} weight={weight} />
|
|
</span>
|
|
);
|
|
};
|