g1t/apps/web/app/components/checklist.tsx

99 lines3,974 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1import { Check, ChevronRight, X } from "lucide-react";
2import { useCallback, useSyncExternalStore } from "react";
3import { Link } from "react-router";
4
5import { type ChecklistItem, dismiss, isDismissed, progress } from "../lib/checklist";
6import { cn } from "../lib/cn";
7
8const storage = () => (typeof window === "undefined" ? null : window.localStorage);
9const listeners = new Set<() => void>();
10const subscribe = (listener: () => void) => {
11 listeners.add(listener);
12 return () => listeners.delete(listener);
13};
14
15/**
A project is an app or a library: libraries show their package and how to ship a release, not production16 * The steps to production, or to a library's first release, for one
17 * project, as `3/6`, until they are all done or someone dismisses it.
18 * Dismissing is per project and remembered by this browser.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look19 */
A project is an app or a library: libraries show their package and how to ship a release, not production20export function ProductionChecklist({
21 base,
22 items,
23 title = "Get to production",
24}: {
25 base: string;
26 items: ChecklistItem[];
27 /** "Ship a release" for a library. */
28 title?: string;
29}) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look30 const hidden = useSyncExternalStore(
31 subscribe,
32 useCallback(() => isDismissed(storage, base), [base]),
33 () => false,
34 );
35 const { done, total, complete } = progress(items);
36 if (hidden || complete) return null;
37 const next = items.find((item) => !item.done)?.key;
38
39 return (
40 <section aria-labelledby="checklist-title" className="rounded-2xl border border-line bg-surface">
41 <div className="flex items-center gap-3 px-5 pt-4 pb-3 sm:px-6">
42 <h2 id="checklist-title" className="text-sm font-semibold">
A project is an app or a library: libraries show their package and how to ship a release, not production43 {title}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look44 </h2>
45 <span className="rounded-full bg-raised px-2 py-px text-xs font-medium tabular-nums text-muted">
46 {done}/{total}
47 </span>
48 <span className="hidden h-1.5 w-24 overflow-hidden rounded-full bg-line sm:block" aria-hidden>
49 <span className="block h-full rounded-full bg-accent" style={{ width: `${(done / total) * 100}%` }} />
50 </span>
51 <button
52 type="button"
53 onClick={() => {
54 dismiss(storage, base);
55 for (const listener of listeners) listener();
56 }}
57 className="ml-auto rounded-md p-1 text-faint transition-colors hover:bg-raised hover:text-fg"
58 aria-label="Dismiss the checklist for this project"
59 title="Dismiss for this project"
60 >
61 <X size={14} />
62 </button>
63 </div>
64 <ol className="grid border-t border-line sm:grid-cols-2 lg:grid-cols-3">
65 {items.map((item) => (
66 <li key={item.key} className="border-b border-line last:border-b-0 sm:[&:nth-last-child(-n+2)]:border-b-0 lg:[&:nth-last-child(-n+3)]:border-b-0">
67 <Link
68 to={item.to}
69 className={cn(
70 "group flex h-full items-start gap-3 px-5 py-3 transition-colors hover:bg-raised/50 sm:px-6",
71 item.done && "opacity-70",
72 )}
73 >
74 <span
75 className={cn(
76 "mt-px flex size-4.5 shrink-0 items-center justify-center rounded-full border",
77 item.done ? "border-accent bg-accent text-bg" : item.key === next ? "border-accent" : "border-line-strong",
78 )}
79 aria-hidden
80 >
81 {item.done && <Check size={11} strokeWidth={3} />}
82 </span>
83 <span className="min-w-0 grow">
84 <span className={cn("block text-[0.8125rem] font-medium", item.done && "text-muted line-through decoration-line-strong")}>
85 {item.title}
86 <span className="sr-only">{item.done ? " (done)" : ""}</span>
87 </span>
88 {!item.done && <span className="mt-0.5 block text-xs leading-5 text-muted">{item.detail}</span>}
89 </span>
90 {!item.done && (
91 <ChevronRight size={14} className="mt-0.5 shrink-0 text-faint transition-colors group-hover:text-fg" />
92 )}
93 </Link>
94 </li>
95 ))}
96 </ol>
97 </section>
98 );
99}