g1t/apps/web/app/components/ui/index.tsx

174 lines5,116 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.

Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)1import { Check, Copy } from "lucide-react";
2import { type ComponentProps, type ReactNode, useState } from "react";
3import { Link, type LinkProps } from "react-router";
4
5export function Field({
6 label,
7 hint,
8 children,
9}: {
10 label: string;
11 hint?: string;
12 children: ReactNode;
13}) {
14 return (
15 <label className="block">
16 <span className="mb-1.5 block text-sm font-medium text-muted">
17 {label}
18 </span>
19 {children}
20 {hint && <span className="mt-1.5 block text-xs text-faint">{hint}</span>}
21 </label>
22 );
23}
24
25const CONTROL =
26 "w-full rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim";
27
28export function Input(props: ComponentProps<"input">) {
29 return <input {...props} className={CONTROL} />;
30}
31
32export function Textarea(props: ComponentProps<"textarea">) {
33 return <textarea {...props} className={`${CONTROL} font-mono`} />;
34}
35
36type Variant = "primary" | "accent" | "quiet";
37
38const BUTTON_BASE =
39 "inline-flex items-center justify-center gap-2 rounded-md px-3.5 py-2 text-sm font-medium transition-colors disabled:opacity-50";
40
41const BUTTON_VARIANTS: Record<Variant, string> = {
42 primary: "bg-fg text-bg hover:bg-white",
43 accent: "bg-accent text-bg hover:bg-[#c6ff5c]",
44 quiet:
45 "border border-line text-fg/80 hover:border-line-strong hover:bg-surface hover:text-fg",
46};
47
48export function Button({
49 variant = "primary",
50 ...props
51}: ComponentProps<"button"> & { variant?: Variant }) {
52 return (
53 <button {...props} className={`${BUTTON_BASE} ${BUTTON_VARIANTS[variant]}`} />
54 );
55}
56
57/** A link that looks like a button. */
58export function ButtonLink({
59 variant = "primary",
60 large,
61 ...props
62}: LinkProps & { variant?: Variant; large?: boolean }) {
63 return (
64 <Link
65 {...props}
66 className={`${BUTTON_BASE} ${BUTTON_VARIANTS[variant]} ${
67 large ? "rounded-full px-5 py-2.5" : ""
68 }`}
69 />
70 );
71}
72
73export function ErrorText({ children }: { children: ReactNode }) {
74 return children ? <p className="text-sm text-danger">{children}</p> : null;
75}
76
77/** A small outlined label. */
78export function Pill({ children }: { children: ReactNode }) {
79 return (
80 <span className="inline-flex items-center rounded-full border border-line px-2 py-0.5 text-xs text-muted">
81 {children}
82 </span>
83 );
84}
85
86const AVATAR_HUES = [82, 200, 262, 28, 330, 160];
87
88/** A letter avatar whose colour is stable for a given name. */
89export function Avatar({ name, size = 20 }: { name: string; size?: number }) {
90 let hash = 0;
91 for (const char of name) hash = (hash * 31 + char.charCodeAt(0)) | 0;
92 const hue = AVATAR_HUES[Math.abs(hash) % AVATAR_HUES.length];
93 return (
94 <span
95 aria-hidden="true"
96 className="inline-flex shrink-0 items-center justify-center rounded-full font-mono font-semibold uppercase"
97 style={{
98 width: size,
99 height: size,
100 fontSize: size * 0.5,
101 background: `oklch(0.4 0.09 ${hue})`,
102 color: `oklch(0.93 0.08 ${hue})`,
103 }}
104 >
105 {name[0]}
106 </span>
107 );
108}
109
110/** A line of text (usually a command) with a copy button. */
111export function CopyLine({
112 text,
113 prompt,
114}: {
115 text: string;
116 prompt?: boolean;
117}) {
118 const [copied, setCopied] = useState(false);
119 return (
120 <div className="group flex items-center gap-3 rounded-lg border border-line bg-surface py-2 pr-2 pl-3.5 font-mono text-[0.8125rem]">
121 <code className="min-w-0 grow overflow-x-auto whitespace-nowrap">
122 {prompt && <span className="mr-2 text-faint select-none">$</span>}
123 {text}
124 </code>
125 <button
126 type="button"
127 aria-label="Copy"
128 className="shrink-0 rounded-md p-1.5 text-faint transition-colors hover:bg-raised hover:text-fg"
129 onClick={() => {
130 void navigator.clipboard.writeText(text);
131 setCopied(true);
132 setTimeout(() => setCopied(false), 1500);
133 }}
134 >
135 {copied ? <Check size={14} className="text-accent" /> : <Copy size={14} />}
136 </button>
137 </div>
138 );
139}
140
141const UNITS: [string, number][] = [
142 ["d", 86_400_000],
143 ["h", 3_600_000],
144 ["m", 60_000],
145];
146
147/** Compact relative time such as "5m ago". */
RFC 3339 timestamps in identity and repos148export function TimeAgo({ at }: { at: string | number }) {
149 // Timestamps are RFC 3339 strings; a few older ones are still numbers.
150 const elapsed = Date.now() - new Date(at).getTime();
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)151 const unit = UNITS.find(([, size]) => elapsed >= size);
152 const label = unit ? `${Math.floor(elapsed / unit[1])}${unit[0]} ago` : "just now";
153 return (
154 // The server and the browser render at slightly different times.
155 <time dateTime={new Date(at).toISOString()} suppressHydrationWarning>
156 {label}
157 </time>
158 );
159}
160
161export function EmptyState({
162 title,
163 children,
164}: {
165 title: string;
166 children?: ReactNode;
167}) {
168 return (
169 <div className="rounded-lg border border-dashed border-line px-6 py-12 text-center">
170 <p className="font-medium">{title}</p>
171 {children && <div className="mt-1.5 text-sm text-muted">{children}</div>}
172 </div>
173 );
174}