import type { Meta, StoryObj } from 'storybook-solidjs-vite'; import { createSignal, Show } from 'solid-js'; import { Portal } from 'solid-js/web'; import { createPresence, usePosition, useDismiss } from './overlay'; import { buttonVariants } from './button'; /** * `overlay.tsx` is not a component — it's the small DIY toolkit that every * floating surface in the kit (Tooltip, HoverCard, Dropdown) is built from, * replacing the former third-party UI dependency. Three primitives: * * - `usePosition(ref, floating, opts)` — anchors `floating` to `ref` via * @floating-ui/dom with flip/shift, tracking it on scroll/resize. * - `createPresence(open)` — keeps a node mounted through its CSS exit * animation, then unmounts on `animationend`. * - `useDismiss({ enabled, onDismiss, refs })` — Escape + outside-pointerdown * dismissal (no scroll lock). * * Plus an `As` polymorphic helper for trigger elements. */ const meta: Meta = { title: 'Solid (Advanced)/Primitives/Overlay Core', tags: ['autodocs'], parameters: { layout: 'padded', docs: { description: { component: [ "The shared, dependency-free foundation behind every floating surface in the kit — `Tooltip`, `HoverCard`, and `Dropdown` are all assembled from these primitives. It's a toolkit of hooks, not a renderable component.", '**`usePosition(ref, floating, opts)`** anchors a floating node to a trigger via `@floating-ui/dom` (flip/shift, fixed strategy, tracks on scroll/resize). **`createPresence(open)`** keeps the node mounted through its CSS exit animation. **`useDismiss({ enabled, onDismiss, refs })`** handles Escape + outside-click. An **`As`** helper renders a polymorphic trigger.', '**When to use:** only when you need a floating surface the prebuilt components don\'t cover. Reach for `Tooltip` / `HoverCard` / `Dropdown` first — they already wire these together with the correct ARIA and focus behavior.', 'The demo below composes all three into a minimal popover; everything portals into the active shadow root via `ChatConfig`.', ].join('\n\n'), }, }, }, }; export default meta; type Story = StoryObj; function PopoverDemo() { const [open, setOpen] = createSignal(false); const [trigger, setTrigger] = createSignal(); const [content, setContent] = createSignal(); const presence = createPresence(open); const position = usePosition(trigger, content, { placement: 'bottom-start', gutter: 6 }); useDismiss({ enabled: open, onDismiss: () => setOpen(false), refs: () => [trigger(), content()] }); return ( <>
{ setContent(el); presence.setRef(el); }} data-expanded={presence.state() === 'open' ? '' : undefined} data-closed={presence.state() === 'closed' ? '' : undefined} style={{ position: 'fixed', left: `${position.pos().x}px`, top: `${position.pos().y}px` }} class="z-50 w-64 rounded-lg bg-card p-3 text-sm text-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95" > Positioned by usePosition, kept mounted through its exit animation by createPresence, and closed on Escape / outside-click by useDismiss.
); } /** A minimal popover hand-built from the three overlay primitives. Click to toggle; click outside or press Escape to dismiss. */ export const MinimalPopover: Story = { render: () => , parameters: { docs: { source: { code: `import { createPresence, usePosition, useDismiss } from '@kitn.ai/chat'; function PopoverDemo() { const [open, setOpen] = createSignal(false); const [trigger, setTrigger] = createSignal(); const [content, setContent] = createSignal(); const presence = createPresence(open); const position = usePosition(trigger, content, { placement: 'bottom-start', gutter: 6 }); useDismiss({ enabled: open, onDismiss: () => setOpen(false), refs: () => [trigger(), content()] }); return ( <>
{ setContent(el); presence.setRef(el); }} style={{ position: 'fixed', left: \`\${position.pos().x}px\`, top: \`\${position.pos().y}px\` }} > …content…
); }`, language: 'tsx', }, }, }, };