import type { Meta, StoryObj } from '@storybook/react-vite' import { AttachmentCard } from './AttachmentCard' const svg = (w: number, h: number, body: string) => `data:image/svg+xml;utf8,${encodeURIComponent( `${body}`, )}` /** Placeholder pictures — no real data in a story. */ const WIDE = svg(480, 270, ``) const TALL = svg(270, 480, ``) const SHOT = svg( 480, 270, ``, ) const noop = () => {} const meta = { title: 'Components/AttachmentCard', component: AttachmentCard, parameters: { layout: 'padded' }, args: { name: 'Q3-billing-summary.pdf', size: 2_412_000, href: 'https://example.com/f', onDownload: noop }, argTypes: { state: { control: 'inline-radio', options: ['ready', 'loading', 'unreadable', 'uploading', 'failed', 'warning'] }, }, } satisfies Meta export default meta type Story = StoryObj /** A document: a type tile, the name, and what it is. Hover it for the download control. */ export const Document: Story = {} /** An image: its own picture, 180px wide, opening full screen on a click. */ export const Image: Story = { args: { name: 'checkout-flow.png', size: 840_000, contentType: 'image/png', src: WIDE, onOpen: noop, href: undefined } } /** A tall picture keeps the card's height: it is cropped to fit, not stretched. */ export const TallImage: Story = { args: { name: 'signup-screen.png', size: 1_180_000, contentType: 'image/png', src: TALL, onOpen: noop, href: undefined } } /** Without an address it is dimmed and opens nothing. */ export const WithoutAnAddress: Story = { args: { name: 'annual-report.pdf', size: 4_800_000, href: undefined, onDownload: undefined } } /** Tabler's file-type icons at stroke 1.5. A type with no icon of its own lands on the plain file glyph rather than a wrong one. */ export const EveryType: Story = { parameters: { controls: { disable: true } }, render: () => (
{['proposal.pdf', 'contract.docx', 'forecast.xlsx', 'kickoff.pptx', 'export.csv', 'notes.txt', 'README.md', 'manifest.json', 'assets.zip', 'photo.heic'].map((name) => ( ))}
), } /** A name longer than the card truncates, so every card in a row keeps the same two lines. The full name is on hover, because it is cut off. */ export const LongName: Story = { args: { name: '2026-Q3-billing-reconciliation-and-invoice-summary-FINAL-v4.xlsx', size: 3_100_000 } } /** Several together, in one flow: images and rows are different widths on purpose. */ export const SeveralTogether: Story = { parameters: { controls: { disable: true } }, render: () => (
), } /** An image on its way. */ export const Loading: Story = { args: { name: 'checkout-flow.png', contentType: 'image/png', state: 'loading', href: undefined } } /** It could not be read: said, not left as a gap, with the dashed hairline every unreadable card has. */ export const Unreadable: Story = { args: { name: 'checkout-flow.png', contentType: 'image/png', state: 'unreadable', href: undefined } } /** Waiting to be sent, and ready: its own thumbnail and its size. Hover it for the remove control. */ export const Pending: Story = { args: { pending: true, name: 'Screenshot 2026-09-07 14-32-05.png', size: 248_000, contentType: 'image/png', src: SHOT, onRemove: noop, href: undefined, onDownload: undefined }, } /** Waiting to be sent: a document shows its type in the tile. */ export const PendingDocument: Story = { args: { ...Pending.args, name: 'Q3-billing-summary.pdf', contentType: 'application/pdf', size: 2_412_000, src: undefined } } export const Uploading: Story = { args: { ...Pending.args, state: 'uploading', src: undefined } } /** It could not be attached: the reason, in place of the size. */ export const Failed: Story = { args: { ...Pending.args, state: 'failed', name: 'Screenshot 2026-09-07 14-32-05.tiff', note: "TIFF files aren't supported.", src: undefined } } /** Attached, with something the sender should know: a warning, not an error — nothing failed. The full words are on hover. */ export const Warning: Story = { args: { ...Pending.args, state: 'warning', note: 'Not everyone can see this', noteHint: 'Only this app has a copy of this file.' } } /** * **Press the picture.** Without an `onOpen` the card opens it itself, full * screen, in the package's `Lightbox` — Escape, the ✕ or the scrim closes it * (UIG-35). Peek had written that viewer by hand and Ship had none at all. */ export const OpenItFullScreen: Story = { parameters: { layout: 'fullscreen', // The viewer portals over the page — iframe it so it does not cover the docs. docs: { story: { inline: false, height: '420px' } }, }, args: { name: 'checkout-flow.png', size: 840_000, contentType: 'image/png', src: WIDE, href: undefined, onOpen: undefined }, render: (args) => (
), } /** * **A picture the reader has to be allowed to see**, which is every file on the * relay: `remoteSrc` is where it lives and `fetchImage` is how this app fetches * one with the reader's own authorization. The card waits, then draws it. * * Here the fetch takes a second, so the wait is visible. The two apps had * written this same wait, once each. */ export const FetchedWithPermission: Story = { parameters: { controls: { disable: true } }, args: { name: 'checkout-flow.png', size: 840_000, contentType: 'image/png', src: undefined, href: undefined, remoteSrc: 'relay://checkout-flow.png', fetchImage: () => new Promise((resolve) => setTimeout(() => resolve(WIDE), 1000)), }, } /** The same request, refused — a channel this reader is not in. The card says so, and does not leave a gap. */ export const CouldNotBeFetched: Story = { parameters: { controls: { disable: true } }, args: { name: 'checkout-flow.png', size: 840_000, contentType: 'image/png', src: undefined, href: undefined, remoteSrc: 'relay://checkout-flow.png', fetchImage: () => Promise.reject(new Error('relay_membership_required')), }, }