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(
``,
)}`
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: ,
},
}