Repository Public MIT
1"use client";2import { Maximize2, X } from "lucide-react";3import { useRef, useState } from "react";4import { CopyButton } from "./copy-button";56export interface ExampleItem {7 title: string;8 client: string;9 clientLabel: string;10 src: string;11 alt: string;12 prompt: string | null;13 caption: string | null;14 capturedAt: string | null;15 width: number | null;16 height: number | null;17}1819/**20 * Screenshots a publisher shipped inside the release. Images come from this registry only (the21 * release archive), are lazy-loaded with their intrinsic size reserved, and open full size in a22 * native modal dialog: Esc closes it and focus returns to the thumbnail that opened it.23 */24export function ExamplesGallery({ groups }: { groups: { client: string; label: string; items: ExampleItem[] }[] }) {25 const dialog = useRef<HTMLDialogElement>(null);26 const opener = useRef<HTMLButtonElement | null>(null);27 const [current, setCurrent] = useState<ExampleItem | null>(null);2829 function open(item: ExampleItem, from: HTMLButtonElement) {30 opener.current = from;31 setCurrent(item);32 dialog.current?.showModal();33 }34 const close = () => dialog.current?.close();3536 return (37 <div className="space-y-8" data-testid="examples">38 {groups.map((g) => (39 <div key={g.client} data-client={g.client}>40 <h3 className="mb-3 flex items-center gap-2 text-sm font-medium">41 <span className="chip chip-accent">{g.label}</span>42 <span className="font-mono text-xs text-faint">{g.items.length} {g.items.length === 1 ? "example" : "examples"}</span>43 </h3>44 <ul className="space-y-5">45 {g.items.map((it) => (46 <li key={it.src} className="card overflow-hidden">47 <figure>48 <button49 type="button"50 className="example-shot group relative block w-full cursor-zoom-in"51 onClick={(e) => open(it, e.currentTarget)}52 aria-label={`View full size: ${it.title}`}53 data-testid="example-open"54 >55 {/* eslint-disable-next-line @next/next/no-img-element -- served by this registry from the immutable archive; next/image would re-encode it */}56 <img57 src={it.src}58 alt={it.alt}59 loading="lazy"60 decoding="async"61 width={it.width ?? undefined}62 height={it.height ?? undefined}63 style={it.width && it.height ? { aspectRatio: `${it.width} / ${it.height}` } : { aspectRatio: "16 / 10" }}64 className="mx-auto block h-auto max-h-[34rem] w-auto max-w-full rounded-md"65 />66 <span className="pointer-events-none absolute right-3 top-3 inline-flex items-center gap-1.5 rounded-md border border-line-strong bg-graphite px-2 py-1 text-[11px] text-muted opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100" aria-hidden>67 <Maximize2 size={12} /> Full size68 </span>69 </button>70 <figcaption className="space-y-3 border-t border-line p-4 sm:p-5">71 <div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">72 <p className="text-[15px] font-medium text-text">{it.title}</p>73 {it.capturedAt && <time dateTime={it.capturedAt} className="font-mono text-[11px] text-faint">captured {it.capturedAt}</time>}74 </div>75 {it.caption && <p className="break-words text-sm text-muted">{it.caption}</p>}76 {it.prompt && (77 <div className="code-frame">78 <div className="code-head">79 <span className="font-mono text-[11px] text-faint">Prompt given to {g.label}</span>80 <CopyButton text={it.prompt} />81 </div>82 <pre className="code-body max-h-48 whitespace-pre-wrap break-words" tabIndex={0}>83 <code>{it.prompt}</code>84 </pre>85 </div>86 )}87 </figcaption>88 </figure>89 </li>90 ))}91 </ul>92 </div>93 ))}9495 <dialog96 ref={dialog}97 className="example-dialog"98 aria-label={current ? `Screenshot: ${current.title}` : "Screenshot"}99 onClose={() => { opener.current?.focus(); setCurrent(null); }}100 onClick={(e) => { if (e.target === e.currentTarget) close(); }}101 data-testid="example-dialog"102 >103 {current && (104 <div className="flex max-h-[calc(100dvh-2rem)] flex-col">105 <div className="flex items-center justify-between gap-3 border-b border-line px-4 py-2.5">106 <p className="min-w-0 truncate text-sm font-medium text-text"><span className="mr-2 font-mono text-[11px] text-faint">{current.clientLabel}</span>{current.title}</p>107 <button type="button" onClick={close} className="grid h-8 w-8 shrink-0 place-items-center rounded-md border border-line-strong text-muted hover:text-text" aria-label="Close full-size view" autoFocus>108 <X size={15} aria-hidden />109 </button>110 </div>111 <div className="example-shot min-h-0 flex-1 overflow-auto" tabIndex={0} role="group" aria-label="Full-size screenshot">112 {/* eslint-disable-next-line @next/next/no-img-element */}113 <img src={current.src} alt={current.alt} width={current.width ?? undefined} height={current.height ?? undefined} className="mx-auto block h-auto max-w-full rounded-md" />114 </div>115 </div>116 )}117 </dialog>118 </div>119 );120}121