import type { Meta, StoryObj } from 'storybook-solidjs-vite'; import { createSignal, onMount, type JSX } from 'solid-js'; import './register'; // side effect: registers the custom elements import { argTypesFor, specDescription } from '../stories/docs/element-controls'; import { Resizable, ResizablePanel } from '../ui/resizable'; import type { ArtifactFile } from '../components/artifact'; import { Artifact } from '../components/artifact'; // The web components are custom DOM elements, so declare the tags for JSX. declare module 'solid-js' { // eslint-disable-next-line @typescript-eslint/no-namespace namespace JSX { interface IntrinsicElements { 'kc-resizable': JSX.HTMLAttributes & { orientation?: string }; 'kc-resizable-item': JSX.HTMLAttributes & { size?: string; min?: string; max?: string; locked?: boolean | string; hidden?: boolean | string; }; // `kc-artifact` JSX type is augmented (with the full attr set) in // artifact.stories.tsx — declaring it again here with a different shape // would trip TS2717 (module-augmentation merges must match). Reuse that one. } } } // Fixture base URL (served by Storybook staticDirs from examples/artifact-fixtures). const BASE = new URL('artifact-fixtures', document.baseURI).href; const ARTIFACT_FILES: ArtifactFile[] = [ { path: 'index.html', url: `${BASE}/index.html`, type: 'html', language: 'html', code: `Preview

Starboard

` }, { path: 'about.html', url: `${BASE}/about.html`, type: 'html', language: 'html', code: `About

About

` }, ]; /** A labelled placeholder pane so the layout is visible in stories. */ function Pane(props: { label: string; tone?: 'muted' | 'plain' }) { return (
{props.label}
); } /** A bordered, sized frame the group fills. */ function Frame(props: { children: JSX.Element; tall?: boolean }) { return (
{props.children}
); } const HTML_SNIPPET = ` ...list... ...chat... ...preview... `; const meta = { title: 'Components/Resizable', tags: ['autodocs'], argTypes: argTypesFor('kc-resizable'), parameters: { layout: 'padded', docs: { description: specDescription('kc-resizable', [ '`` is the framework-agnostic **web component** for a composable, resizable multi-panel layout (up to **3** `` panels) with **auto-inserted draggable dividers** — isolated in **Shadow DOM**.', '**When to use:** to compose an app shell out of slotted regions without hand-wiring panels and handles — e.g. `list | chat | preview`. In SolidJS, use the `Resizable` convenience (UI/Resizable) directly.', "**How to use:** register once with `import '@kitn.ai/chat/elements'`, set `orientation` (`horizontal` row / `vertical` column), and put a `` per panel. Each item carries `size` (px or %, e.g. `\"280px\"` or `\"25%\"`), `min`/`max`, `locked` (fixed size + non-draggable neighbour), and `hidden` (drops the panel + its divider). Listen for the **`change`** event (`detail.sizes`, percent).", '**Anatomy:** one or more **``** light children (each a config-carrier: `size`, `min`, `max`, `locked`, `hidden` attributes; renders its slotted content) with auto-inserted **draggable divider handles** between each visible, unlocked pair. A panel is omitted from layout when `hidden`; its adjacent divider is dropped. Nesting `` inside an item allows more than 3 panels.', '**Placement:** the layout spine for compose-your-own-chat shells — sidebar + conversation, conversation + inspector, or a three-up list/chat/preview.', 'See the **Code** tab for HTML usage.', ]), }, }, args: { orientation: 'horizontal' }, } satisfies Meta; export default meta; type Story = StoryObj; /** Interactive playground — flip orientation, then drag the dividers. */ export const Playground: Story = { render: (args: { orientation?: string }) => ( ), parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } }, }; /** Two panels: a sized list beside a flexible chat. */ export const ListChat: Story = { name: 'Sidebar + chat', render: () => ( ), }; /** Three panels, two draggable dividers. */ export const ListChatPreview: Story = { name: 'List + chat + preview', render: () => ( ), }; /** A locked, fixed-px sidebar — its divider is a static (non-draggable) separator. */ export const LockedSidebar: Story = { name: 'Locked sidebar', render: () => ( ), }; /** Stacked top/bottom split. */ export const Vertical: Story = { name: 'Vertical split', render: () => ( ), }; /** Toggle the preview panel — its divider drops and the rest reflow. */ export const HiddenToggle: Story = { name: 'Show / hide a panel', render: () => { const [showPreview, setShowPreview] = createSignal(true); let previewItem: HTMLElement | undefined; const toggle = () => { setShowPreview((v) => !v); // Drive the boolean attribute directly so the group's MutationObserver // re-lays out (Solid sets the `hidden` IDL property, which doesn't reflect // to the attribute on a custom element). if (previewItem) { if (showPreview()) previewItem.removeAttribute('hidden'); else previewItem.setAttribute('hidden', ''); } }; return (
(previewItem = e as HTMLElement)} size="30%">
); }, }; const EXPAND_TO_FILL_SNIPPET = ` …list… …chat… `; /** * **Expand to fill** — the headline integration: the artifact's expand button * fills the preview panel to the full container width. No wiring between the * two elements is needed — clicking **Expand** fires a `kc-maximize-intent` event * that bubbles up to the nearest enclosing ``, which hides siblings * and lets the preview panel fill. **Collapse** (or **Escape**) restores the * original layout. * * **Cross-element protocol (hand-authored docs):** * - `kc-maximize-intent` — `bubbles:true, composed:true`; fired by `` * when Expand/Collapse is toggled. `detail: { requested: boolean }`. * - `kc-maximize-state` — `bubbles:false, composed:true`; dispatched by * `` back down to the affected `` so the * artifact can reconcile its button. `detail: { maximized: boolean }`. * * These protocol events are NOT in the generated `web-components.md` * (the generator only documents per-element `dispatch` events — resolved decision #1). */ export const ExpandToFill: Story = { name: 'Expand to fill', render: () => { const [log, setLog] = createSignal([]); let artifactEl: HTMLElement & { files?: ArtifactFile[] }; let resizableEl: HTMLElement; onMount(() => { if (artifactEl) artifactEl.files = ARTIFACT_FILES; if (resizableEl) { resizableEl.addEventListener('kc-change', (e: Event) => setLog((l) => [`change → ${JSON.stringify((e as CustomEvent).detail.sizes)}`, ...l].slice(0, 6)), ); resizableEl.addEventListener('kc-maximize-change', (e: Event) => setLog((l) => [`kc-maximize-change → ${JSON.stringify((e as CustomEvent).detail)}`, ...l].slice(0, 6)), ); } if (artifactEl) { artifactEl.addEventListener('kc-maximize-change', (e: Event) => setLog((l) => [`artifact kc-maximize-change → ${JSON.stringify((e as CustomEvent).detail)}`, ...l].slice(0, 6)), ); } }); return (
(resizableEl = e as HTMLElement)} orientation="horizontal" > (artifactEl = e as HTMLElement & { files?: ArtifactFile[] })} src={`${BASE}/index.html`} iframe-title="Starboard artifact preview" expandable />
          {log().length ? log().join('\n') : '(click the ⤢ Expand button in the preview toolbar…)'}
        
); }, parameters: { docs: { source: { code: EXPAND_TO_FILL_SNIPPET, language: 'html' } } }, }; const SOLID_PARITY_SNIPPET = `// SolidJS — Artifact inside Resizable with maximizedIndex/onMaximizeChange. // No web components needed; works in a pure-Solid app. import { createSignal } from 'solid-js'; import { Artifact } from '@kitn.ai/chat/components'; import { Resizable, ResizablePanel } from '@kitn.ai/chat/ui'; function App() { const [maximizedIndex, setMaximizedIndex] = createSignal(null); return ( …list… …chat… setMaximizedIndex(m ? 2 : null)} /> ); }`; /** * **SolidJS parity** — the same `list | chat | artifact` layout using the Solid * `Resizable` convenience and the `Artifact` component directly (no web components). * `maximizedIndex` / `onMaximizeChange` on `Resizable` mirror the web-component * protocol at the Solid level. Wire `Artifact`'s `onMaximizeChange` to set the * index and pass `maximized` back down to drive the button. */ export const SolidParity: Story = { name: 'SolidJS parity (Resizable + Artifact)', render: () => { const [maximizedIndex, setMaximizedIndex] = createSignal(null); return (
setMaximizedIndex(m ? 2 : null)} />
); }, parameters: { docs: { source: { code: SOLID_PARITY_SNIPPET, language: 'tsx' } } }, };