import type { Meta, StoryObj } from "@storybook/react-vite"; import { useState } from "react"; import { ArrowLeft, Check, Layers, Package, Search, X } from "lucide-react"; import { ArtifactCard } from "@/components/artifacts/ArtifactCard"; import { EmptyState } from "@/components/EmptyState"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { cn, formatDate } from "@/lib/utils"; import type { CompanyArtifact } from "@/api/artifacts"; import { ARTIFACT_GROUP_OPTIONS, ARTIFACT_KIND_FILTERS, artifactGroupByLabel, } from "@/pages/Artifacts"; /** * Storybook coverage for the company Artifacts page. Covers: * - the flat grid (PAP-10359) * - the new group-by control, stack cards, and selected stack view (PAP-10440 / PAP-10442) * * Each story is renderable standalone so UX/QA can capture desktop and mobile * screenshots without booting a live backend. */ type StoryArtifactKindFilter = (typeof ARTIFACT_KIND_FILTERS)[number]["value"]; type StoryArtifactGroupBy = (typeof ARTIFACT_GROUP_OPTIONS)[number]["value"]; const SAMPLE_IMAGE = "data:image/svg+xml;utf8," + encodeURIComponent( `Hero render.png`, ); const SAMPLE_IMAGE_TEAL = "data:image/svg+xml;utf8," + encodeURIComponent( `nav-revised.png`, ); const SAMPLE_IMAGE_AMBER = "data:image/svg+xml;utf8," + encodeURIComponent( `hero-warm.png`, ); function makeArtifact(overrides: Partial): CompanyArtifact { return { id: "art", source: "attachment", mediaKind: "image", title: "Artifact", previewText: null, contentType: null, contentPath: null, openPath: null, downloadPath: null, issue: { id: "issue-1", identifier: "PAP-10306", title: "Landing visuals refresh" }, project: { id: "proj-1", name: "Paperclip App" }, createdByAgent: { id: "agent-1", name: "ClaudeCoder" }, updatedAt: new Date("2026-06-04T12:00:00Z").toISOString(), href: "/issues/PAP-10306#attachment-art", ...overrides, }; } const ARTIFACTS: CompanyArtifact[] = [ makeArtifact({ id: "wp-video", source: "work_product", mediaKind: "video", title: "Product demo — primary cut.mp4", contentType: "video/mp4", contentPath: null, openPath: "/files/demo.mp4", downloadPath: "/files/demo.mp4?download=1", issue: { id: "issue-2", identifier: "PAP-10205", title: "Record the launch walkthrough" }, href: "/issues/PAP-10205#work-product-wp-video", }), makeArtifact({ id: "img-hero", mediaKind: "image", title: "Hero render.png", contentType: "image/png", contentPath: SAMPLE_IMAGE, openPath: SAMPLE_IMAGE, downloadPath: SAMPLE_IMAGE, }), makeArtifact({ id: "doc-plan", source: "document", mediaKind: "document", title: "Artifacts Page Plan", previewText: "Build a company-level Artifacts page at /{companyPrefix}/artifacts, with a sidebar item below Goals and a three-column artifact grid. The page should make agent-produced work easy to find without becoming another attachment dump.", contentType: "text/markdown", issue: { id: "issue-3", identifier: "PAP-10341", title: "Draft the rollout plan" }, createdByAgent: { id: "agent-2", name: "CodexCoder" }, href: "/issues/PAP-10341#document-plan", }), makeArtifact({ id: "txt-notes", mediaKind: "text", title: "review-notes.txt", previewText: "Reviewed the primary cut. Color grade looks good; trim the first 1.2s of dead air. Re-export at 1080p and attach the final to the issue.", contentType: "text/plain", openPath: "/files/review-notes.txt", downloadPath: "/files/review-notes.txt?download=1", issue: { id: "issue-2", identifier: "PAP-10205", title: "Record the launch walkthrough" }, }), makeArtifact({ id: "file-zip", mediaKind: "file", title: "design-assets.zip", contentType: "application/zip", openPath: "/files/design-assets.zip", downloadPath: "/files/design-assets.zip?download=1", issue: { id: "issue-1", identifier: "PAP-10306", title: "Landing visuals refresh" }, }), makeArtifact({ id: "img-broken", mediaKind: "image", title: "missing-preview.png (broken source)", contentType: "image/png", contentPath: "/files/does-not-exist.png", openPath: "/files/does-not-exist.png", downloadPath: "/files/does-not-exist.png?download=1", }), ]; function ArtifactsGrid({ artifacts }: { artifacts: CompanyArtifact[] }) { return (
{artifacts.map((artifact) => ( ))}
); } // --------------------------------------------------------------------------- // Grouping mock components (PAP-10442). These mirror the production artifact // grouping controls so Storybook stays useful for visual review. // --------------------------------------------------------------------------- /** * Toolbar replica matching the existing Artifacts page (search + kind filters) * with the group-by icon control placed before the filter chips. */ function ArtifactsToolbar({ query, onQueryChange, kind, onKindChange, groupBy, onGroupByChange, }: { query: string; onQueryChange: (value: string) => void; kind: StoryArtifactKindFilter; onKindChange: (value: StoryArtifactKindFilter) => void; groupBy: StoryArtifactGroupBy; onGroupByChange: (value: StoryArtifactGroupBy) => void; }) { return (
onQueryChange(event.currentTarget.value)} placeholder="Search artifacts..." aria-label="Search artifacts" className="h-9 pl-9 pr-9 text-sm" /> {query.length > 0 ? ( ) : null}
Group by {ARTIFACT_GROUP_OPTIONS.map(({ value, label }) => ( onGroupByChange(value)} className="justify-between" > {label} {groupBy === value ? : null} ))}
{ARTIFACT_KIND_FILTERS.map((filter) => ( ))}
); } interface MockGroup { id: string; groupBy: Exclude; issueIdentifier: string; issueTitle: string; count: number; preview: CompanyArtifact; updatedAt: string; href: string; } /** * Stack card mock for grouped views. * * Visual rules (matching ArtifactCard footprint): * - Same border, radius (rounded-[8px]), border + bg-card surface, hover treatment. * - Preview frame is the same `aspect-video` PreviewFrame as ArtifactCard. * - Footer block has the same vertical rhythm. * - When `count > 1`, render two stacked sibling layers behind the main card, * offset by 4px / 8px on both axes, with reduced opacity. This produces a * subtle "stack" without competing with the preview content. * - Stack count badge sits in the top-right of the preview as a small pill. * - The body line replaces the artifact title with the issue identifier and * title; metadata line shows the artifact count and most-recent-edit time. */ function ArtifactStackCard({ group }: { group: MockGroup }) { const isStacked = group.count > 1; return (
{isStacked ? ( <>