| 1 | import { ArrowUpRight, X } from "lucide-react"; |
| 2 | |
| 3 | import type { CardAction, FeedNotification } from "@g1t/contracts"; |
| 4 | |
| 5 | import { Button, type ButtonVariant } from "../ui/button"; |
| 6 | import { Card } from "../ui/card"; |
| 7 | import { Hint } from "../ui/hint"; |
| 8 | import { cn } from "../../lib/cn"; |
| 9 | import { currentSink, dismiss, settle, useWaitingCards } from "../../lib/notify-client"; |
| 10 | import { notificationActions } from "../../lib/notify-store"; |
| 11 | |
| 12 | // A chat card's links on a notification about it (Open, say). Following |
| 13 | // one puts the notification away. Buttons that ran something were an |
| 14 | // agent's, and nothing answers them any more, so they are not shown. |
| 15 | |
| 16 | /** A notification's links are the smallest buttons, taller to a finger on a phone; the action's style names the variant. */ |
| 17 | const VARIANT: Record<NonNullable<CardAction["style"]>, ButtonVariant> = { primary: "accent", danger: "destructive", default: "outline" }; |
| 18 | |
| 19 | export function NotificationCardActions({ |
| 20 | notification, |
| 21 | onNavigate, |
| 22 | className, |
| 23 | }: { |
| 24 | notification: FeedNotification; |
| 25 | /** After a link is followed: closes the panel it is in. */ |
| 26 | onNavigate?: () => void; |
| 27 | className?: string; |
| 28 | }) { |
| 29 | const actions = notificationActions(notification); |
| 30 | if (!actions.length) return null; |
| 31 | return ( |
| 32 | <div role="group" aria-label={`${notification.title}: links`} className={cn("flex flex-wrap items-center gap-1.5", className)}> |
| 33 | {actions.map((action) => ( |
| 34 | <Button |
| 35 | key={action.id} |
| 36 | type="button" |
| 37 | variant={VARIANT[action.style ?? "default"]} |
| 38 | size="xs" |
| 39 | className="max-sm:h-9 max-sm:px-3" |
| 40 | onClick={() => { |
| 41 | dismiss(notification.id); |
| 42 | onNavigate?.(); |
| 43 | currentSink().open(action.href!); |
| 44 | }} |
| 45 | > |
| 46 | {action.label} |
| 47 | <ArrowUpRight size={12} aria-hidden="true" className="-mr-0.5 text-faint" /> |
| 48 | </Button> |
| 49 | ))} |
| 50 | </div> |
| 51 | ); |
| 52 | } |
| 53 | |
| 54 | /** |
| 55 | * The chat cards waiting on the person, at the top of the notifications |
| 56 | * panel: each one's title and preview, a link to where it is, and its |
| 57 | * actions. Gone once acted on here, or after a day. |
| 58 | */ |
| 59 | export function WaitingCards({ onNavigate }: { onNavigate?: () => void }) { |
| 60 | const waiting = useWaitingCards(); |
| 61 | if (!waiting.length) return null; |
| 62 | return ( |
| 63 | <section aria-labelledby="waiting-cards" className="mb-4"> |
| 64 | <h3 id="waiting-cards" className="mb-2 text-xs font-medium text-muted"> |
| 65 | Waiting on you in chat |
| 66 | </h3> |
| 67 | <ul className="space-y-2"> |
| 68 | {waiting.map((notification) => ( |
| 69 | <Card asChild key={notification.id} radius="lg" className="relative p-3"> |
| 70 | <li> |
| 71 | <span aria-hidden="true" className="absolute inset-y-2 left-0 w-[3px] rounded-r bg-accent" /> |
| 72 | <div className="flex items-start gap-2"> |
| 73 | <button |
| 74 | type="button" |
| 75 | onClick={() => { |
| 76 | onNavigate?.(); |
| 77 | currentSink().open(notification.href); |
| 78 | }} |
| 79 | className="min-w-0 grow rounded text-left outline-none focus-visible:ring-2 focus-visible:ring-accent/50" |
| 80 | > |
| 81 | <span className="block truncate text-sm font-medium text-fg hover:underline">{notification.title}</span> |
| 82 | {notification.body && <span className="mt-0.5 line-clamp-2 block text-xs text-muted">{notification.body}</span>} |
| 83 | </button> |
| 84 | <Hint label="Put away"> |
| 85 | <Button |
| 86 | type="button" |
| 87 | aria-label="Put away" |
| 88 | onClick={() => settle(notification.id)} |
| 89 | variant="ghost" size="icon" |
| 90 | className="size-6 text-faint" |
| 91 | > |
| 92 | <X size={14} /> |
| 93 | </Button> |
| 94 | </Hint> |
| 95 | </div> |
| 96 | <NotificationCardActions notification={notification} onNavigate={onNavigate} className="mt-2.5" /> |
| 97 | </li> |
| 98 | </Card> |
| 99 | ))} |
| 100 | </ul> |
| 101 | </section> |
| 102 | ); |
| 103 | } |