import type { Meta, StoryObj } from 'storybook-solidjs-vite'; import { onMount, type JSX } from 'solid-js'; import './register'; // side effect: registers the custom elements import { attachments, conversations, context, cotSteps, models, assistantMessage, userMessage, slashCommands, sources, } from '../stories/examples/sample-data'; /** * Examples / Catalog — the `examples/composable/index.html` showcase, ported into * Storybook as source-visible web-component stories. Every kc-* element rendered * with minimal sample data, grouped by category, so a developer can answer * "what exists?" without leaving Storybook — and read the exact markup via the * "Show code" panel on each story. * * Convention (matches the per-element stories): the kc-* tags are custom DOM * elements declared as JSX intrinsics elsewhere with only the standard * attributes; element-specific attributes and object/array PROPERTIES are set * imperatively through a `ref` (see `wire`/`attrs`/`props`), so this file stays * type-safe without re-declaring the tags. */ type AnyEl = HTMLElement & Record; /** Set JS properties (objects/arrays) on an element. */ const props = (el: HTMLElement, p: Record) => { for (const k in p) (el as AnyEl)[k] = p[k]; }; /** Set string/boolean attributes on an element. */ const attrs = (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); } }; // The kc-* tags are declared as JSX intrinsics by the per-element story files // (global `declare module 'solid-js'` augmentations). We only ever pass `style` // and `ref` here, so no local re-declaration is needed. // ── helpers ──────────────────────────────────────────────────────────────── /** A bordered tile that frames one component with a caption. */ function Spec(props: { tag: string; note?: string; children: JSX.Element; tall?: boolean }) { return (
{props.tag} {props.note ? ( {props.note} ) : null}
{props.children}
); } /** Responsive grid of Spec tiles. */ function Grid(props: { children: JSX.Element }) { return (
{props.children}
); } const meta = { title: 'Examples/Catalog', parameters: { layout: 'padded', docs: { description: { component: [ '# Component catalog', 'Every `kc-*` web component rendered with minimal sample data, grouped by category — the in-Storybook port of `examples/composable/index.html`. Use it to answer **"what exists?"**, then open each story\'s **Show code** panel to read the exact composition markup.', 'Data goes **in via properties** (set with `el.someProp = …` — see each snippet), interactions come **out via events** (`el.addEventListener("kc-submit", …)`). Register everything once with `import "@kitn.ai/chat/elements"`.', 'Next: see **Examples / Composed chat shell** to watch these leaves assemble into a real chat, and **Examples / Choosing components** for the mental model of which tier to reach for.', ].join('\n\n'), }, }, }, } satisfies Meta; export default meta; type Story = StoryObj; // ── 01 · Batteries-included ────────────────────────────────────────────────── const DROPIN_SNIPPET = ` `; export const BatteriesIncluded: Story = { name: '01 · Batteries-included', render: () => { let list!: HTMLElement; let input!: HTMLElement; onMount(() => { props(list, { conversations, activeId: 'c1' }); list.addEventListener('kc-conversation-select', (e) => ((list as AnyEl).activeId = (e as CustomEvent).detail.id)); props(input, { slashCommands }); attrs(input, { placeholder: 'Ask anything… (try typing /)' }); }); return (
(list = e)} style={{ display: 'block', height: '100%' }} />
(input = e)} />
); }, parameters: { docs: { source: { code: DROPIN_SNIPPET, language: 'html' } } }, }; // ── 02 · Messages ──────────────────────────────────────────────────────────── const MESSAGES_SNIPPET = ` `; export const Messages: Story = { name: '02 · Messages', render: () => { let msgA!: HTMLElement, msgU!: HTMLElement, md!: HTMLElement, reason!: HTMLElement; let cot!: HTMLElement, code!: HTMLElement, tool!: HTMLElement; onMount(() => { props(msgA, { message: assistantMessage }); props(msgU, { message: userMessage }); props(md, { content: '### Markdown\nRenders **bold**, _italic_, `code`, and lists:\n- one\n- two\n\n> and blockquotes.' }); props(reason, { text: 'First I parse the request, then I plan the steps, then I execute and verify.' }); attrs(reason, { label: 'Reasoning', streaming: true }); props(cot, { steps: cotSteps }); props(code, { code: 'export function add(a: number, b: number): number {\n return a + b;\n}' }); attrs(code, { language: 'ts' }); props(tool, { tool: { type: 'database_query', state: 'output-available', input: { table: 'users', limit: 10 }, output: { rows: 10, ms: 42 } } }); attrs(tool, { open: true }); }); return ( (msgA = e)} /> (msgU = e)} /> (md = e)} /> (reason = e)} /> (cot = e)} /> (code = e)} /> (tool = e)} /> ); }, parameters: { docs: { source: { code: MESSAGES_SNIPPET, language: 'html' } } }, }; // ── 03 · Attachments & media ───────────────────────────────────────────────── const MEDIA_SNIPPET = ` `; export const AttachmentsAndMedia: Story = { name: '03 · Attachments & media', render: () => { let inline!: HTMLElement, grid!: HTMLElement, img!: HTMLElement, src!: HTMLElement, srcs!: HTMLElement; onMount(() => { props(inline, { items: attachments }); attrs(inline, { variant: 'inline', 'hover-card': true }); props(grid, { items: attachments }); attrs(grid, { variant: 'grid', removable: true }); grid.addEventListener('kc-remove', (e) => { const id = (e as CustomEvent).detail.id; (grid as AnyEl).items = (attachments as { id: string }[]).filter((x) => x.id !== id); }); props(img, { base64: btoa(unescape(encodeURIComponent( ''))), }); attrs(img, { alt: 'demo', 'media-type': 'image/svg+xml' }); attrs(src, { href: 'https://kitn.dev', headline: 'kitn — the kit', description: 'Composable SolidJS + web-component chat UI.', 'show-favicon': true }); props(srcs, { sources }); }); return ( (inline = e)} /> (grid = e)} /> (img = e)} style={{ width: '96px' }} /> (src = e)} /> (srcs = e)} /> ); }, parameters: { docs: { source: { code: MEDIA_SNIPPET, language: 'html' } } }, }; // ── 04 · Composer ──────────────────────────────────────────────────────────── const COMPOSER_SNIPPET = ` `; export const Composer: Story = { name: '04 · Composer', render: () => { let suggs!: HTMLElement, voice!: HTMLElement, think!: HTMLElement; onMount(() => { props(suggs, { suggestions: ['Explain the architecture', 'Show me a code example', "What's deferred?"] }); props(voice, { transcribe: async () => { await new Promise((r) => setTimeout(r, 400)); return 'transcribed text'; } }); attrs(think, { text: 'Thinking…', stoppable: true }); }); return ( (suggs = e)} />
(voice = e)} /> (think = e)} style={{ flex: '1' }} />
); }, parameters: { docs: { source: { code: COMPOSER_SNIPPET, language: 'html' } } }, }; // ── 05 · Header & meta ─────────────────────────────────────────────────────── const META_SNIPPET = ` `; export const HeaderAndMeta: Story = { name: '05 · Header & meta', render: () => { let ms!: HTMLElement, ctx!: HTMLElement, scope!: HTMLElement, cp!: HTMLElement, skills!: HTMLElement, fb!: HTMLElement; onMount(() => { props(ms, { models, currentModel: 'opus' }); ms.addEventListener('kc-model-change', (e) => ((ms as AnyEl).currentModel = (e as CustomEvent).detail.modelId)); props(ctx, { context }); props(scope, { availableAuthors: ['Rob', 'Alex'], availableTags: ['design', 'api'] }); attrs(cp, { label: 'Checkpoint', tooltip: 'Restore' }); props(skills, { skills: [{ id: 's1', name: 'web-search' }, { id: 's2', name: 'code' }] }); attrs(fb, { 'bar-title': 'Was this helpful?' }); }); return ( (ms = e)} /> (ctx = e)} /> (scope = e)} />
(cp = e)} /> (skills = e)} />
(fb = e)} />
); }, parameters: { docs: { source: { code: META_SNIPPET, language: 'html' } } }, }; // ── 06 · Status & motion ───────────────────────────────────────────────────── const STATUS_SNIPPET = ` `; export const StatusAndMotion: Story = { name: '06 · Status & motion', render: () => { let l1!: HTMLElement, l2!: HTMLElement, l3!: HTMLElement, l4!: HTMLElement, l5!: HTMLElement; let shimmer!: HTMLElement, stream!: HTMLElement, empty!: HTMLElement; onMount(() => { attrs(l1, { variant: 'circular' }); attrs(l2, { variant: 'dots' }); attrs(l3, { variant: 'wave' }); attrs(l4, { variant: 'bars' }); attrs(l5, { variant: 'pulse-dot' }); attrs(shimmer, { text: 'Generating response…' }); attrs(stream, { speed: '30' }); props(stream, { text: "This text reveals with a typewriter animation, streamed character by character — exactly how you'd render a live assistant reply." }); attrs(empty, { 'empty-title': 'No conversations yet', description: 'Start chatting to see them here.' }); }); return (
(l1 = e)} /> (l2 = e)} /> (l3 = e)} /> (l4 = e)} /> (l5 = e)} />
(shimmer = e)} /> (stream = e)} /> (empty = e)}>
); }, parameters: { docs: { source: { code: STATUS_SNIPPET, language: 'html' } } }, };