Skip to content
118 linesCodeBlameRaw
1/**
2 * Artifacts as lists show them (docs/ARTIFACTS_MODE.md section 6.2): rows
3 * grouped by the day they were last edited, in the viewer's time zone, or
4 * cards with a picture drawn from each one's preview. Each has its kind,
5 * who can see it, its space, when it was edited and by whom, and its ⋯
6 * menu.
7 */
8import type { Folio } from "@g1t/contracts";
9import { useMemo } from "react";
10import { Link } from "react-router";
11
12import { dayGroups } from "../../lib/folios";
13import { TimeAgo } from "../ui";
14import { Hint } from "../ui/hint";
15import { FolioThumbnail, KindIcon } from "./kinds";
16import { FolioMenu } from "./menu";
17import { AccessMark } from "./parts";
18
19function Edited({ folio }: { folio: Folio }) {
20 const by = folio.edited_by?.display_name;
21 return (
22 <Hint label={by ? `Edited by ${by}` : "Edited"}>
23 <span className="shrink-0 text-xs whitespace-nowrap text-faint" tabIndex={-1}>
24 Edited <TimeAgo at={folio.edited_at} />
25 </span>
26 </Hint>
27 );
28}
29
30export function FolioRow({ slug, folio, onError }: { slug: string; folio: Folio; onError?: (message: string) => void }) {
31 return (
32 <li className="group relative flex items-center gap-3 px-3 py-2 transition-colors hover:bg-raised/60 sm:px-4">
33 <KindIcon kind={folio.kind} />
34 <span className="flex min-w-0 grow items-center gap-2">
35 <Link to={folio.path} prefetch="intent" className="min-w-0 truncate text-sm text-fg after:absolute after:inset-0 hover:text-accent">
36 {folio.icon && <span className="mr-1.5">{folio.icon}</span>}
37 {folio.title || "Untitled"}
38 </Link>
39 {folio.stale && (
40 <Hint label="Possibly out of date: code it cites changed">
41 <span className="relative size-1.5 shrink-0 rounded-full bg-warn" aria-label="Possibly out of date" />
42 </Hint>
43 )}
44 </span>
45 <span className="relative hidden shrink-0 sm:flex">
46 <AccessMark folio={folio} />
47 </span>
48 <span className="hidden w-32 shrink-0 truncate text-xs text-faint md:block">{folio.space?.name ?? "Private"}</span>
49 <span className="relative w-28 shrink-0 text-right max-sm:w-auto">
50 <Edited folio={folio} />
51 </span>
52 <span className="relative">
53 <FolioMenu slug={slug} folio={folio} onError={onError} />
54 </span>
55 </li>
56 );
57}
58
59/** Rows under Today, Yesterday, then dates. */
60export function FolioDays({ slug, items, zone, onError }: { slug: string; items: Folio[]; zone: string; onError?: (message: string) => void }) {
61 const days = useMemo(() => dayGroups(items, (f) => f.edited_at, zone), [items, zone]);
62 return (
63 <div className="space-y-6">
64 {days.map((day) => (
65 <section key={day.key} aria-label={day.label}>
66 <h3 className="mb-1.5 px-1 text-xs font-medium text-faint">{day.label}</h3>
67 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
68 {day.items.map((f) => (
69 <FolioRow key={f.id} slug={slug} folio={f} onError={onError} />
70 ))}
71 </ul>
72 </section>
73 ))}
74 </div>
75 );
76}
77
78export function FolioCard({ slug, folio, onError }: { slug: string; folio: Folio; onError?: (message: string) => void }) {
79 return (
80 <div className="group relative flex flex-col overflow-hidden rounded-xl border border-line bg-surface transition-colors hover:border-line-strong">
81 <div className="pointer-events-none h-32 shrink-0 border-b border-line">
82 <FolioThumbnail folio={folio} />
83 </div>
84 <div className="flex items-start gap-2.5 px-3.5 pt-3 pb-3">
85 <KindIcon kind={folio.kind} size={14} box={24} />
86 <span className="min-w-0 grow">
87 <Link to={folio.path} prefetch="intent" className="line-clamp-1 text-sm font-medium text-fg after:absolute after:inset-0 group-hover:text-accent">
88 {folio.icon && <span className="mr-1">{folio.icon}</span>}
89 {folio.title || "Untitled"}
90 </Link>
91 <span className="mt-0.5 flex items-center gap-1.5 text-[0.6875rem] text-faint">
92 <span className="truncate">{folio.space?.name ?? "Private"}</span>
93 <span aria-hidden="true">·</span>
94 <span className="relative">
95 <Edited folio={folio} />
96 </span>
97 </span>
98 </span>
99 <span className="relative -mt-1 -mr-1.5 flex items-center">
100 <span className="relative">
101 <AccessMark folio={folio} />
102 </span>
103 <FolioMenu slug={slug} folio={folio} onError={onError} />
104 </span>
105 </div>
106 </div>
107 );
108}
109
110export function FolioGrid({ slug, items, onError }: { slug: string; items: Folio[]; onError?: (message: string) => void }) {
111 return (
112 <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
113 {items.map((f) => (
114 <FolioCard key={f.id} slug={slug} folio={f} onError={onError} />
115 ))}
116 </div>
117 );
118}