Skip to content
122 linesCodeBlameRaw

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.

Docs: a workspace knowledge base people and agents write together1/**
2 * Pieces Docs' pages share: a page card, a space card, a face for a
3 * person or an agent, a page's breadcrumbs, the template gallery.
4 */
5import type { DocPage, DocSpace, DocTemplate, MemberProfile } from "@g1t/contracts";
6import { ChevronRight, Lock, Users } from "lucide-react";
7import type { ReactNode } from "react";
8import { Link } from "react-router";
9
10import { coverStyle } from "../../lib/docs";
11import { AgentAvatar } from "../agent-avatar";
12import { Avatar, TimeAgo } from "../ui";
13import { Hint } from "../ui/hint";
14import { PageIcon, SpaceIcon } from "./sidebar";
15
16export function Face({ who, size = 20 }: { who: Pick<MemberProfile, "kind" | "id" | "name" | "avatar" | "avatar_seed">; size?: number }) {
17 if (who.kind === "agent") return <AgentAvatar agent={{ id: who.id, handle: who.name, avatar: who.avatar, avatar_seed: who.avatar_seed ?? null }} size={size} />;
18 return <Avatar name={who.name} image={who.avatar} size={size} />;
19}
20
21export function Faces({ people, size = 22, max = 4 }: { people: MemberProfile[]; size?: number; max?: number }) {
22 const shown = people.slice(0, max);
23 return (
24 <span className="flex items-center -space-x-1.5">
25 {shown.map((p) => (
26 <Hint key={`${p.kind}:${p.id}`} label={p.display_name}>
27 <span className="rounded-full ring-2 ring-bg">
28 <Face who={p} size={size} />
29 </span>
30 </Hint>
31 ))}
32 {people.length > max && <span className="ml-2 text-xs text-faint">+{people.length - max}</span>}
33 </span>
34 );
35}
36
37export function PageCard({ page, space }: { page: DocPage; space?: Pick<DocSpace, "name"> | null }) {
38 const cover = coverStyle(page.cover);
39 return (
40 <Link to={page.path} prefetch="intent" className="group flex flex-col overflow-hidden rounded-xl border border-line bg-surface transition-colors hover:border-line-strong">
41 <div className="h-14 shrink-0 border-b border-line" style={{ background: cover ?? "linear-gradient(120deg, color-mix(in srgb, var(--g1t-accent) 10%, var(--g1t-surface)), var(--g1t-surface))" }} />
42 <div className="-mt-5 flex min-h-0 grow flex-col px-4 pb-3.5">
43 <span className="flex size-9 items-center justify-center rounded-lg border border-line bg-bg text-lg">
44 <PageIcon icon={page.icon} size={18} />
45 </span>
46 <span className="mt-2 line-clamp-1 text-sm font-medium text-fg group-hover:text-accent">{page.title || "Untitled"}</span>
47 <span className="mt-1 line-clamp-2 min-h-[2.5em] text-xs leading-relaxed text-muted">{page.excerpt || "Nothing written yet."}</span>
48 <span className="mt-3 flex items-center gap-1.5 text-[0.6875rem] text-faint">
49 {page.updated_by && <Face who={page.updated_by} size={14} />}
50 <span className="truncate">
51 {space ? `${space.name} · ` : ""}
52 <TimeAgo at={page.updated_at} />
53 </span>
54 </span>
55 </div>
56 </Link>
57 );
58}
59
60export function SpaceCard({ slug, space }: { slug: string; space: DocSpace }) {
61 return (
62 <Link to={`/${slug}/-/docs/${space.slug}`} prefetch="intent" className="group flex gap-3 rounded-xl border border-line bg-surface p-4 transition-colors hover:border-line-strong">
63 <span className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-raised text-lg">
64 <SpaceIcon space={space} size={18} />
65 </span>
66 <span className="min-w-0 grow">
67 <span className="flex items-center gap-1.5">
68 <span className="truncate text-sm font-medium text-fg group-hover:text-accent">{space.name}</span>
69 {space.kind === "private" && <Lock size={12} className="shrink-0 text-faint" aria-label="Private" />}
70 {space.kind === "team" && <Users size={12} className="shrink-0 text-faint" aria-label="Team space" />}
71 </span>
72 <span className="mt-0.5 line-clamp-2 text-xs leading-relaxed text-muted">{space.description || "No description."}</span>
73 <span className="mt-2 block text-[0.6875rem] text-faint">
74 {space.page_count} {space.page_count === 1 ? "page" : "pages"}
75 {space.projects.length > 0 && ` · ${space.projects.join(", ")}`}
76 </span>
77 </span>
78 </Link>
79 );
80}
81
82export function TemplateCard({ template, action }: { template: DocTemplate; action: ReactNode }) {
83 return (
84 <div className="flex flex-col rounded-xl border border-line bg-surface p-4">
85 <span className="text-2xl leading-none" aria-hidden="true">
86 {template.icon}
87 </span>
88 <span className="mt-3 text-sm font-medium">{template.name}</span>
89 <span className="mt-1 grow text-xs leading-relaxed text-muted">{template.description || (template.builtin ? "" : "Saved by your workspace.")}</span>
90 <div className="mt-3 flex items-center gap-2">{action}</div>
91 </div>
92 );
93}
94
95export function Crumbs({ items }: { items: { label: ReactNode; to?: string }[] }) {
96 return (
97 <nav aria-label="Breadcrumbs" className="flex min-w-0 flex-wrap items-center gap-1 text-xs text-faint">
98 {items.map((item, i) => (
99 <span key={i} className="flex min-w-0 items-center gap-1">
100 {i > 0 && <ChevronRight size={12} className="shrink-0" aria-hidden="true" />}
101 {item.to ? (
102 <Link to={item.to} prefetch="intent" className="truncate hover:text-fg">
103 {item.label}
104 </Link>
105 ) : (
106 <span className="truncate text-muted">{item.label}</span>
107 )}
108 </span>
109 ))}
110 </nav>
111 );
112}
113
114/** A heading for a section of a Docs page. */
115export function SectionTitle({ children, action }: { children: ReactNode; action?: ReactNode }) {
116 return (
117 <div className="mb-3 flex items-center justify-between gap-3">
118 <h2 className="text-sm font-semibold text-fg">{children}</h2>
119 {action}
120 </div>
121 );
122}