| 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 | */ |
| 8 | import type { Folio } from "@g1t/contracts"; |
| 9 | import { useMemo } from "react"; |
| 10 | import { Link } from "react-router"; |
| 11 | |
| 12 | import { dayGroups } from "../../lib/folios"; |
| 13 | import { TimeAgo } from "../ui"; |
| 14 | import { Hint } from "../ui/hint"; |
| 15 | import { FolioThumbnail, KindIcon } from "./kinds"; |
| 16 | import { FolioMenu } from "./menu"; |
| 17 | import { AccessMark } from "./parts"; |
| 18 | |
| 19 | function 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 | |
| 30 | export 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. */ |
| 60 | export 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 | |
| 78 | export 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 | |
| 110 | export 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 | } |