import type { Meta, StoryObj } from 'storybook-solidjs-vite'; import { createSignal, onMount, For, type JSX } from 'solid-js'; import './register'; // side effect: registers the custom elements import { conversations, context, models, slashCommands, thread, } from '../stories/examples/sample-data'; import type { ArtifactFile } from '../components/artifact'; /** * Examples / Composed chat shell — THE headline. A real chat assembled from leaf * `kc-*` components inside a `` layout, wired with sample data + * event handlers in the story script. Paired with the `` drop-in so the * "batteries-included vs compose-your-own — when do I use which?" contrast is * explicit. Both stories are source-visible (Show code). */ type AnyEl = HTMLElement & Record; /** Set JS properties (objects/arrays) on an element. */ const setProps = (el: HTMLElement, props: Record) => { for (const k in props) (el as AnyEl)[k] = props[k]; }; /** Set string/boolean attributes on an element. */ const setAttrs = (el: HTMLElement, a: Record) => { for (const k in a) { const v = a[k]; if (v === false) el.removeAttribute(k); else el.setAttribute(k, v === true ? '' : v); } }; const BASE = new URL('artifact-fixtures', document.baseURI).href; // served by .storybook/main.ts staticDirs const ARTIFACT_FILES: ArtifactFile[] = [ { path: 'index.html', url: `${BASE}/index.html`, type: 'html', language: 'html', code: '\nStarboard\n

Starboard

', }, { path: 'css/site.css', url: `${BASE}/css/site.css`, type: 'other', language: 'css', code: 'body { font-family: system-ui; }' }, { path: 'assets/logo.svg', url: `${BASE}/assets/logo.svg`, type: 'image' }, ]; /** A bordered frame the shell fills. */ function Frame(props: { children: JSX.Element; height?: string }) { return (
{props.children}
); } const meta = { title: 'Examples/Composed chat shell', parameters: { layout: 'padded', docs: { description: { component: [ '# Build your own chat', 'Two ways to ship a chat, side by side:', '- **Compose your own** (`Composed shell`) — a `` laying out `` │ a chat column (`` list + `` meter + `` + ``) │ ``. You own the data flow and event wiring; you control every panel. **Reach for this when the flagship doesn\'t fit** — custom layout, an inspector/canvas panel, bespoke header.', '- **Batteries-included** (`Drop-in `) — the whole chat surface in one tag. Set `messages`, listen for `submit`. **Reach for this for the 90% path** — a working chat in minutes.', 'Same leaf components underneath; the flagship just pre-wires them. See **Examples / Choosing components** for the full decision guide.', 'Both stories are source-visible — open **Show code** to read the exact composition + wiring.', ].join('\n\n'), }, }, }, } satisfies Meta; export default meta; type Story = StoryObj; // ── B1 · Composed shell ────────────────────────────────────────────────────── const SHELL_SNIPPET = `
`; export const ComposedShell: Story = { name: 'Composed shell', render: () => { const [messages, setMessages] = createSignal[]>(thread); let list!: HTMLElement, ctx!: HTMLElement, suggs!: HTMLElement, input!: HTMLElement, artifact!: HTMLElement; const msgRefs: HTMLElement[] = []; onMount(() => { setProps(list, { conversations, activeId: 'c1' }); list.addEventListener('kc-select', (e) => ((list as AnyEl).activeId = (e as CustomEvent).detail.id)); setProps(ctx, { context }); setProps(suggs, { suggestions: ['Summarize this thread', 'What changed in v0.3?', 'Show me the layout code'] }); suggs.addEventListener('kc-select', (e) => ((input as AnyEl).value = (e as CustomEvent).detail.value)); setProps(input, { slashCommands }); setAttrs(input, { search: true, voice: true, placeholder: 'Message the assistant…' }); input.addEventListener('kc-submit', (e) => { const value = (e as unknown as CustomEvent).detail.value as string; if (!value) return; setMessages((m) => [...m, { id: Date.now() + '', role: 'user', content: value }]); (input as AnyEl).value = ''; setTimeout(() => { setMessages((m) => [ ...m, { id: Date.now() + 'a', role: 'assistant', content: 'Echo: ' + value, actions: ['copy'] }, ]); }, 500); }); setProps(artifact, { src: `${BASE}/index.html`, files: ARTIFACT_FILES }); setAttrs(artifact, { 'iframe-title': 'Artifact preview' }); }); // Keep each 's `message` property in sync as the thread grows. const syncMessages = () => { const list = messages(); list.forEach((m, i) => { if (msgRefs[i]) (msgRefs[i] as AnyEl).message = m; }); }; return ( (list = e)} style={{ display: 'block', height: '100%' }} />
{(m, i) => ( { msgRefs[i()] = e; (e as AnyEl).message = m; queueMicrotask(syncMessages); }} /> )}
(ctx = e)} /> (suggs = e)} /> (input = e)} />
(artifact = e)} style={{ display: 'block', height: '100%' }} />
); }, parameters: { docs: { source: { code: SHELL_SNIPPET, language: 'html' } } }, }; // ── B2 · Drop-in ─────────────────────────────────────────────────── const DROPIN_SNIPPET = ` `; export const DropInChat: Story = { name: 'Drop-in ', render: () => { let chat!: HTMLElement; onMount(() => { setProps(chat, { models, currentModel: 'opus', context, suggestions: ['Summarize this thread', 'What changed in v0.3?'], messages: [ { id: '1', role: 'assistant', content: "Hi! I'm the **drop-in** ``. Ask me anything.", actions: ['copy', 'like', 'dislike'] }, ], }); setAttrs(chat, { 'chat-title': 'Assistant', search: true, voice: true }); chat.addEventListener('kc-submit', (e) => { const value = (e as unknown as CustomEvent).detail.value as string; const cur = (chat as AnyEl).messages as unknown[]; (chat as AnyEl).messages = [...cur, { id: Date.now() + '', role: 'user', content: value }]; (chat as AnyEl).loading = true; setTimeout(() => { const cur2 = (chat as AnyEl).messages as unknown[]; (chat as AnyEl).messages = [...cur2, { id: Date.now() + 'a', role: 'assistant', content: 'Echo: ' + value, actions: ['copy'] }]; (chat as AnyEl).loading = false; }, 600); }); chat.addEventListener('kc-model-change', (e) => ((chat as AnyEl).currentModel = (e as CustomEvent).detail.modelId)); }); return ( (chat = e)} style={{ display: 'block', height: '100%' }} /> ); }, parameters: { docs: { source: { code: DROPIN_SNIPPET, language: 'html' } } }, };