import { useVisibility } from "@charcuterie/logic" import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters" import type { Placement } from "@floating-ui/react" import type { Meta, StoryObj } from "@storybook/react" import type { ReactNode } from "react" import { placementArgType } from "../argTypes.storyHelpers.ts" import { Badge } from "../Badge/Badge.tsx" import { Button } from "../Button/Button.tsx" import { StoryCell, StoryGrid, StoryRow, } from "../board.storyHelpers.tsx" import { Popover } from "./Popover.tsx" const meta = { title: "Components/Overlays/Popover", component: Popover, parameters: { layout: "padded" }, argTypes: { placement: placementArgType }, args: { placement: "bottom-start" }, } satisfies Meta export default meta type Story = StoryObj /** * One `useVisibility` per popover, and the trigger is a real * `Button` that the component clones rather than wraps. */ const PopoverDemo = ({ children, heading, placement, triggerLabel, }: { children?: ReactNode heading: string placement?: Placement triggerLabel: string }): ReactNode => { const { hide, isVisible, toggle } = useVisibility() return ( {triggerLabel} } > {children ?? (

Filters apply to this bay only.

)}
) } export const Playground: Story = { parameters: playgroundParameters, args: { children: null, heading: "Filters", isVisible: false, onDismiss: () => {}, trigger: , }, render: () => ( ), } const PLACEMENTS: Placement[] = [ "top", "bottom-start", "right", "left-end", ] export const AllVariants: Story = { args: { children: null, heading: "Filters", isVisible: false, onDismiss: () => {}, trigger: , }, render: () => ( {PLACEMENTS.map((placement) => (

Requested {placement}. `flip` and `shift` will overrule it rather than let the panel leave the viewport.

))}
), } export const AllStates: Story = { args: { children: null, heading: "Filters", isVisible: false, onDismiss: () => {}, trigger: , }, render: () => (

MakeMKV reported a read error on title 4 at 00:41:12. Eight of nine titles completed.

degraded

One read retry in the last hour.

), } /** * The board a container query cannot help with, and for the * opposite reason to `Modal`'s. A popover is positioned against the * **viewport** — `flip` and `shift` measure collisions with it — so * what varies is where the trigger sits on screen, not how wide its * container is. Three triggers pinned to three edges is the honest * instrument. */ export const Responsive: Story = { args: { children: null, heading: "Filters", isVisible: false, onDismiss: () => {}, trigger: , }, render: () => (

Asked for `top`, has no room, so `flip` sends it down.

Asked for `right`, has no room, so `shift` and `flip` bring it back inside.

Room below, so it gets what it asked for.

), } /** * The dismiss layer, driven. Outside press and Escape both route * through `useDismiss` → `onDismiss` → `hide()`, so the panel is * never closed by anything except the state saying so. * * Escape *is* pressable here, unlike `Modal`'s: `useDismiss` * listens for a keydown rather than relying on a UA default * action, and a synthetic keydown is a real keydown as far as a * listener is concerned. */ export const Interactive: Story = { args: { children: null, heading: "Filters", isVisible: false, onDismiss: () => {}, trigger: , }, render: () => ( ), } /** * The bug the whole M8 portal reversal exists to fix. The trigger sits * inside a small `overflow: hidden` box — the top layer was still * clipped by it; a `FloatingPortal` to `document.body` is not. Open it * and the panel escapes the clip whole. */ export const EscapesOverflowClip: Story = { args: { children: null, heading: "Filters", isVisible: false, onDismiss: () => {}, trigger: , }, render: () => (

This box is `overflow: hidden`.

Portalled to the body, so the clip cannot reach it.

), }