import { useVisibility } from "@charcuterie/logic" import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters" import type { Meta, StoryObj } from "@storybook/react" import type { ReactNode } from "react" import { Button } from "../Button/Button.tsx" import { StoryRow } from "../board.storyHelpers.tsx" import { Lightbox } from "./Lightbox.tsx" /** * An inline SVG rather than a fixture file or a placeholder service: * it decodes synchronously and needs no network, so the enlarged * image is deterministic in CI — the same reason `MediaTile`'s * stories draw their posters this way. */ const toPosterSrc = (label: string) => `data:image/svg+xml;utf8,${encodeURIComponent( `${label}`, )}` const POSTER = toPosterSrc("THE OUTFIT") /** * A small poster thumbnail, exactly as a card would render it — * `object-cover` at a fixed poster trim. It is the child the * `Lightbox` trigger wraps. */ const PosterThumb = (): ReactNode => ( ) const meta = { title: "Components/Overlays/Lightbox", component: Lightbox, parameters: { layout: "padded" }, args: { alt: "THE OUTFIT poster", src: POSTER, }, } satisfies Meta export default meta type Story = StoryObj /** * The uncontrolled default: the thumbnail *is* the trigger, and the * component owns its open state. This is what a rip card passes — * one `Lightbox` with the poster as its `thumbnail`. */ export const Playground: Story = { parameters: playgroundParameters, args: { thumbnail: , }, } export const WithCaption: Story = { args: { caption: "2022 · Blu-ray · 1080p", thumbnail: , }, } /** * Boards here are a trigger plus one open overlay, not a grid — a * lightbox is a `Dialog` underneath, and a dialog covers the viewport, * so two side by side is not the real arrangement. */ export const AllStates: Story = { render: () => ( } /> } /> ), } /** * The controlled case: some other control opens the lightbox, and * this renders only the overlay. `isOpen` present makes the internal * store go unread — the caller's `useVisibility` is the one truth. */ const ControlledDemo = (): ReactNode => { const { hide, isVisible, show } = useVisibility() return ( <> { if (!isNextOpen) { hide() } }} src={POSTER} /> ) } export const Controlled: Story = { render: () => , } /** * The keyboard contract, inherited from `Dialog` and asserted here so * the skin cannot quietly drop it: the trigger opens, Escape routes * through `onClose`, and focus returns to the thumbnail button. */ export const Interactive: Story = { args: { thumbnail: , }, } /** Any thumbnail can start the same ordered gallery at its own image. */ export const Gallery: Story = { args: { heading: "Poster gallery", initialIndex: 1, images: [ { alt: "First poster", src: toPosterSrc("FIRST"), caption: "First view", }, { alt: "Second poster", src: toPosterSrc("SECOND"), caption: "Second view", }, { alt: "Third poster", src: toPosterSrc("THIRD"), caption: "Third view", }, ], thumbnail: , }, }