import { createSignal, onMount, onCleanup } from 'solid-js'; import { defineWebComponent } from './define'; import { ChainOfThoughtAccordion, type ChainOfThoughtController, type ChainOfThoughtStepData, type ChainOfThoughtType, } from '../components/chain-of-thought'; /** * A reasoning step descriptor. * * `id` is an OPTIONAL stable key. The Accordion's open-set keys (used by `value`, * `defaultValue`, the `kai-value-change` payload, and the index→key resolution * behind the methods) are `step.id` when present, else `String(index)`. Set `id` * if you want to drive/observe a step by a meaningful key rather than position. */ type Step = ChainOfThoughtStepData; interface Props extends Record { /** The reasoning steps. Set as a JS property. Compound sub-parts collapse to * this one data model (Route 1). Each `{ label, content?, id? }`. */ steps: Step[]; /** Open mode: `'multiple'` (default — any number of steps open at once) or * `'single'` (at most one open; opening a step closes the others). */ type?: ChainOfThoughtType; /** Controlled open step key(s). When set, it WINS over user interaction (the * consumer owns the open set). String in `single` mode, string[] in * `multiple` mode. Set as a JS property. */ value?: string | string[]; /** Uncontrolled INITIAL open step key(s) — seeds which steps render expanded. * Ignored once `value` is provided. Set as a JS property. */ defaultValue?: string | string[]; } /** Events fired by ``. */ interface Events { /** The open set changed — by user click OR an expand()/collapse()/toggle() * call. `value` is a string in `single` mode, a string[] in `multiple` mode. * (Maps Radix Accordion's onValueChange.) */ 'kai-value-change': { value: string | string[] }; } /** Parse a single light-DOM `` element into a `Step` descriptor. * Attribute mapping: * - `label` → Step.label (the always-visible heading) * - `step-id` → Step.id (optional stable open-set key) * - textContent → Step.content (optional expandable detail) */ export function parseKaiStepElement(n: Element): Step { return { label: n.getAttribute('label') ?? '', content: n.textContent?.trim() || undefined, id: n.getAttribute('step-id') ?? undefined, }; } /** * `` — step-by-step reasoning with connectors and * per-step collapsible detail. An Accordion: by default every step is * independently collapsible (`type="multiple"`, all closed); set * `type="single"` for one-open-at-a-time. * * **Route 1 — JS property:** set the `steps` property to an array of * `{ label, content?, id? }` objects. * * **Route 2 — declarative children:** compose `` child elements in * light DOM (hidden by the Shadow DOM — pure data carriers). The `label` * attribute becomes the step heading; `textContent` becomes the expandable * detail; an optional `step-id` becomes the open-set key. Children are merged * after any prop steps. * * ```html * * The user wants composable web components. * Route 1: variant + flags; rich data via properties. * * * ``` */ defineWebComponent('kai-chain-of-thought', { steps: [], type: undefined, value: undefined, defaultValue: undefined, }, (props, { element, dispatch, expose }) => { // Read declarative children from light DOM. // Shadow DOM with no suppresses them visually — they're invisible data carriers. const [slottedSteps, setSlottedSteps] = createSignal([]); onMount(() => { const read = () => { const nodes = [...element.querySelectorAll('kai-step')]; setSlottedSteps(nodes.map(parseKaiStepElement)); }; read(); const observer = new MutationObserver(read); observer.observe(element, { childList: true, attributes: true, subtree: true }); onCleanup(() => observer.disconnect()); }); // Prop steps first; declarative children appended after. const allSteps = () => [...props.steps, ...slottedSteps()]; // ── Imperative API (instance methods on the host) ────────────────────────── // Pattern C: the Accordion component owns the open-set; the facade captures its // controller and exposes index-based methods. Every setter routes through the // component's onValueChange (handleValueChange below) — the single emit point — // so user clicks AND method calls both fire kai-value-change. No prop collisions: // the only props are steps/type/value/defaultValue. let controller: ChainOfThoughtController | undefined; expose({ /** Open one step's detail by index, or — with no arg — ALL steps. In `single` * mode opening one step closes the others (expand-all keeps the last). */ expand: (index?: number) => controller?.expand(index), /** Close one step's detail by index, or — with no arg — ALL steps. */ collapse: (index?: number) => controller?.collapse(index), /** Flip one step's open state by index. */ toggle: (index?: number) => controller?.toggle(index), }); // Single emit point for the open set — both user clicks and the methods above // flow through the component's onValueChange to here. const handleValueChange = (value: string | string[]) => { dispatch('kai-value-change', { value }); }; return ( (controller = c)} /> ); });