* One artifact, as a gallery card. * * The whole card is the link — the preview is the biggest, most obvious target, * and a title-only hit area would waste it. The row actions therefore cannot be * nested inside it (a button inside an anchor is invalid, and the click would * navigate through t
(props: {
readonly artifact: ArtifactSummary;
readonly onRename: (title: string) => void;
readonly onRemove: () => void;
})
| 77 | * one focus stop, with the actions as their own stops after it. |
| 78 | */ |
| 79 | function ArtifactCard(props: { |
| 80 | readonly artifact: ArtifactSummary; |
| 81 | readonly onRename: (title: string) => void; |
| 82 | readonly onRemove: () => void; |
| 83 | }) { |
| 84 | const { artifact } = props; |
| 85 | return ( |
| 86 | <div |
| 87 | data-slot="artifact-card" |
| 88 | className="group/artifact-card relative isolate flex flex-col overflow-hidden rounded-lg border border-border bg-card transition-colors duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] focus-within:border-input hover:border-input" |
| 89 | > |
| 90 | {/* 16:10 preview, on its own plane, separated by the same hairline the |
| 91 | rest of the console uses. */} |
| 92 | <div className="relative aspect-[16/10] overflow-hidden border-b border-border bg-muted/40"> |
| 93 | <ArtifactPreview artifactId={artifact.id} preview={artifact.preview} /> |
| 94 | </div> |
| 95 | |
| 96 | <div className="flex min-w-0 flex-col gap-1 px-4 py-3"> |
| 97 | <h3 className="truncate text-sm font-medium text-foreground"> |
| 98 | <Link |
| 99 | to="/{-$orgSlug}/artifacts/$artifactId" |
| 100 | params={{ artifactId: artifact.id }} |
| 101 | aria-label={`Open artifact ${artifact.title}`} |
| 102 | className="outline-none after:absolute after:inset-0 after:content-[''] focus-visible:underline" |
| 103 | onClick={() => trackEvent("artifact_opened", { surface: "list" })} |
| 104 | > |
| 105 | {artifact.title} |
| 106 | </Link> |
| 107 | </h3> |
| 108 | {artifact.description ? ( |
| 109 | <p className="line-clamp-2 text-xs leading-relaxed text-muted-foreground"> |
| 110 | {artifact.description} |
| 111 | </p> |
| 112 | ) : null} |
| 113 | <span className="mt-1 font-mono text-[11px] text-muted-foreground"> |
| 114 | {formatRelativeTime(artifact.updatedAt)} |
| 115 | </span> |
| 116 | </div> |
| 117 | |
| 118 | {/* Above the link overlay, so these stay clickable. Revealed on hover or |
| 119 | focus, and pinned open while their own dialog is. */} |
| 120 | <div className="absolute right-2 top-2 z-10 flex items-center gap-1 opacity-0 transition-opacity duration-150 focus-within:opacity-100 group-hover/artifact-card:opacity-100 has-[[data-state=open]]:opacity-100"> |
| 121 | <RenameArtifactDialog |
| 122 | currentTitle={artifact.title} |
| 123 | onRename={props.onRename} |
| 124 | trigger={ |
| 125 | <Button |
| 126 | type="button" |
| 127 | variant="secondary" |
| 128 | size="xs" |
| 129 | className="bg-card text-muted-foreground hover:text-foreground" |
| 130 | > |
| 131 | Rename |
| 132 | </Button> |
| 133 | } |
| 134 | /> |
| 135 | <AlertDialog> |
| 136 | <AlertDialogTrigger asChild> |
nothing calls this directly
no test coverage detected