Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
103 linesCodeBlameRaw
1import { ArrowUpRight, X } from "lucide-react";
2
3import type { CardAction, FeedNotification } from "@g1t/contracts";
4
5import { Button, type ButtonVariant } from "../ui/button";
6import { Card } from "../ui/card";
7import { Hint } from "../ui/hint";
8import { cn } from "../../lib/cn";
9import { currentSink, dismiss, settle, useWaitingCards } from "../../lib/notify-client";
10import { 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. */
17const VARIANT: Record<NonNullable<CardAction["style"]>, ButtonVariant> = { primary: "accent", danger: "destructive", default: "outline" };
18
19export 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 */
59export 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}