Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitf2cd046

Browse files
chore: add notification UI components (#16818)
Related tocoder/internal#336This PR adds the base components for the Notifications UI below (you canclick on the image to open the related Figma design) based on theresponse structure defined on this [notiondoc](https://www.notion.so/coderhq/Coder-Inbox-Endpoints-1a1d579be592809eb921f13baf18f783).[![new notifications includinghover](https://github.com/user-attachments/assets/885fb055-544e-4d9e-b5bf-be986e8b9fc0)](https://www.figma.com/design/5kRpzK8Qr1k38nNz7H0HSh/Inbox-notifications?node-id=2-1098&m=dev)**What is not included**- Support for infinite scrolling (pending on BE definition)**How to test the components?**- The only way to test the components is to use Chromatic or downloadingthe branch and running Storybook locally.
1 parent78df786 commitf2cd046

16 files changed

+930
-0
lines changed

‎site/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"@radix-ui/react-dropdown-menu":"2.1.4",
5757
"@radix-ui/react-label":"2.1.0",
5858
"@radix-ui/react-popover":"1.1.5",
59+
"@radix-ui/react-scroll-area":"1.2.3",
5960
"@radix-ui/react-select":"2.1.4",
6061
"@radix-ui/react-slider":"1.2.2",
6162
"@radix-ui/react-slot":"1.1.1",

‎site/pnpm-lock.yaml‎

Lines changed: 71 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎site/src/components/Button/Button.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const buttonVariants = cva(
3131
lg:"min-w-20 h-10 px-3 py-2 [&_svg]:size-icon-lg",
3232
sm:"min-w-20 h-8 px-2 py-1.5 text-xs [&_svg]:size-icon-sm",
3333
icon:"size-8 px-1.5 [&_svg]:size-icon-sm",
34+
"icon-lg":"size-10 px-2 [&_svg]:size-icon-lg",
3435
},
3536
},
3637
defaultVariants:{
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Copied from shadc/ui on 03/05/2025
3+
*@see {@link https://ui.shadcn.com/docs/components/scroll-area}
4+
*/
5+
import*asScrollAreaPrimitivefrom"@radix-ui/react-scroll-area";
6+
import*asReactfrom"react";
7+
import{cn}from"utils/cn";
8+
9+
exportconstScrollArea=React.forwardRef<
10+
React.ElementRef<typeofScrollAreaPrimitive.Root>,
11+
React.ComponentPropsWithoutRef<typeofScrollAreaPrimitive.Root>
12+
>(({ className, children, ...props},ref)=>(
13+
<ScrollAreaPrimitive.Root
14+
ref={ref}
15+
className={cn("relative overflow-hidden",className)}
16+
{...props}
17+
>
18+
<ScrollAreaPrimitive.ViewportclassName="h-full w-full rounded-[inherit]">
19+
{children}
20+
</ScrollAreaPrimitive.Viewport>
21+
<ScrollBar/>
22+
<ScrollAreaPrimitive.Corner/>
23+
</ScrollAreaPrimitive.Root>
24+
));
25+
ScrollArea.displayName=ScrollAreaPrimitive.Root.displayName;
26+
27+
exportconstScrollBar=React.forwardRef<
28+
React.ElementRef<typeofScrollAreaPrimitive.ScrollAreaScrollbar>,
29+
React.ComponentPropsWithoutRef<typeofScrollAreaPrimitive.ScrollAreaScrollbar>
30+
>(({ className, orientation="vertical", ...props},ref)=>(
31+
<ScrollAreaPrimitive.ScrollAreaScrollbar
32+
ref={ref}
33+
orientation={orientation}
34+
className={cn(
35+
"border-0 border-solid border-border flex touch-none select-none transition-colors",
36+
orientation==="vertical"&&
37+
"h-full w-2.5 border-l border-l-transparent p-[1px]",
38+
orientation==="horizontal"&&
39+
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
40+
className,
41+
)}
42+
{...props}
43+
>
44+
<ScrollAreaPrimitive.ScrollAreaThumbclassName="relative flex-1 rounded-full bg-border"/>
45+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
46+
));
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
importtype{Meta,StoryObj}from"@storybook/react";
2+
import{InboxButton}from"./InboxButton";
3+
4+
constmeta:Meta<typeofInboxButton>={
5+
title:"modules/notifications/NotificationsInbox/InboxButton",
6+
component:InboxButton,
7+
};
8+
9+
exportdefaultmeta;
10+
typeStory=StoryObj<typeofInboxButton>;
11+
12+
exportconstAllRead:Story={};
13+
14+
exportconstUnread:Story={
15+
args:{
16+
unreadCount:3,
17+
},
18+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import{Button,typeButtonProps}from"components/Button/Button";
2+
import{BellIcon}from"lucide-react";
3+
import{typeFC,forwardRef}from"react";
4+
import{UnreadBadge}from"./UnreadBadge";
5+
6+
typeInboxButtonProps={
7+
unreadCount:number;
8+
}&ButtonProps;
9+
10+
exportconstInboxButton=forwardRef<HTMLButtonElement,InboxButtonProps>(
11+
({ unreadCount, ...props},ref)=>{
12+
return(
13+
<Button
14+
size="icon-lg"
15+
variant="outline"
16+
className="relative"
17+
ref={ref}
18+
{...props}
19+
>
20+
<BellIcon/>
21+
{unreadCount>0&&(
22+
<UnreadBadge
23+
count={unreadCount}
24+
className="absolute top-0 right-0 -translate-y-1/2 translate-x-1/2"
25+
/>
26+
)}
27+
</Button>
28+
);
29+
},
30+
);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
importtype{Meta,StoryObj}from"@storybook/react";
2+
import{expect,fn,userEvent,within}from"@storybook/test";
3+
import{MockNotification}from"testHelpers/entities";
4+
import{InboxItem}from"./InboxItem";
5+
6+
constmeta:Meta<typeofInboxItem>={
7+
title:"modules/notifications/NotificationsInbox/InboxItem",
8+
component:InboxItem,
9+
render:(args)=>{
10+
return(
11+
<divclassName="max-w-[460px] border-solid border-border rounded">
12+
<InboxItem{...args}/>
13+
</div>
14+
);
15+
},
16+
};
17+
18+
exportdefaultmeta;
19+
typeStory=StoryObj<typeofInboxItem>;
20+
21+
exportconstRead:Story={
22+
args:{
23+
notification:{
24+
...MockNotification,
25+
read_status:"read",
26+
},
27+
},
28+
};
29+
30+
exportconstUnread:Story={
31+
args:{
32+
notification:{
33+
...MockNotification,
34+
read_status:"unread",
35+
},
36+
},
37+
};
38+
39+
exportconstUnreadFocus:Story={
40+
args:{
41+
notification:{
42+
...MockNotification,
43+
read_status:"unread",
44+
},
45+
},
46+
play:async({ canvasElement})=>{
47+
constcanvas=within(canvasElement);
48+
constnotification=canvas.getByRole("menuitem");
49+
awaituserEvent.click(notification);
50+
},
51+
};
52+
53+
exportconstOnMarkNotificationAsRead:Story={
54+
args:{
55+
notification:{
56+
...MockNotification,
57+
read_status:"unread",
58+
},
59+
onMarkNotificationAsRead:fn(),
60+
},
61+
play:async({ canvasElement, args})=>{
62+
constcanvas=within(canvasElement);
63+
constnotification=canvas.getByRole("menuitem");
64+
awaituserEvent.click(notification);
65+
constmarkButton=canvas.getByRole("button",{name:/markasread/i});
66+
awaituserEvent.click(markButton);
67+
awaitexpect(args.onMarkNotificationAsRead).toHaveBeenCalledTimes(1);
68+
awaitexpect(args.onMarkNotificationAsRead).toHaveBeenCalledWith(
69+
args.notification.id,
70+
);
71+
},
72+
parameters:{
73+
chromatic:{
74+
disableSnapshot:true,
75+
},
76+
},
77+
};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import{Avatar}from"components/Avatar/Avatar";
2+
import{Button}from"components/Button/Button";
3+
import{SquareCheckBig}from"lucide-react";
4+
importtype{FC}from"react";
5+
import{LinkasRouterLink}from"react-router-dom";
6+
import{relativeTime}from"utils/time";
7+
importtype{Notification}from"./types";
8+
9+
typeInboxItemProps={
10+
notification:Notification;
11+
onMarkNotificationAsRead:(notificationId:string)=>void;
12+
};
13+
14+
exportconstInboxItem:FC<InboxItemProps>=({
15+
notification,
16+
onMarkNotificationAsRead,
17+
})=>{
18+
return(
19+
<div
20+
className="flex items-stretch gap-3 p-3 group"
21+
role="menuitem"
22+
tabIndex={-1}
23+
>
24+
<divclassName="flex-shrink-0">
25+
<Avatarfallback="AR"/>
26+
</div>
27+
28+
<divclassName="flex flex-col gap-3">
29+
<spanclassName="text-content-secondary text-sm font-medium">
30+
{notification.content}
31+
</span>
32+
<divclassName="flex items-center gap-1">
33+
{notification.actions.map((action)=>{
34+
return(
35+
<Buttonvariant="outline"size="sm"key={action.label}asChild>
36+
<RouterLinkto={action.url}>{action.label}</RouterLink>
37+
</Button>
38+
);
39+
})}
40+
</div>
41+
</div>
42+
43+
<divclassName="w-12 flex flex-col items-end flex-shrink-0">
44+
{notification.read_status==="unread"&&(
45+
<>
46+
<divclassName="group-focus:hidden group-hover:hidden size-2.5 rounded-full bg-highlight-sky">
47+
<spanclassName="sr-only">Unread</span>
48+
</div>
49+
50+
<Button
51+
onClick={()=>onMarkNotificationAsRead(notification.id)}
52+
className="hidden group-focus:flex group-hover:flex bg-surface-primary"
53+
variant="outline"
54+
size="sm"
55+
>
56+
<SquareCheckBig/>
57+
mark as read
58+
</Button>
59+
</>
60+
)}
61+
62+
<spanclassName="mt-auto text-content-secondary text-xs font-medium whitespace-nowrap">
63+
{relativeTime(newDate(notification.created_at))}
64+
</span>
65+
</div>
66+
</div>
67+
);
68+
};

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp