// src/elements/cards.tsx // — the web-component list dispatcher. Renders one child kc-* element per // envelope (by type→tag), propagates its theme, and routes children's bubbling // `kc-card` events through an optional `policy`. The raw events keep bubbling past // (composed) so document-level listeners still work. Unknown types render // the Solid CardFallback inline and emit a contract `error`. import { For, Show, createEffect, onCleanup, onMount, type JSX } from 'solid-js'; import { Dynamic } from 'solid-js/web'; import { defineWebComponent } from './define'; import type { CardEnvelope, CardEvent, CardPolicy } from '../primitives/card-contract'; import { CARD_EVENT_NAME, emitCardEvent, routeCardEvent } from '../primitives/card-routing'; import { mergeCardTags } from '../primitives/card-registry'; import { CardFallback } from '../components/card-fallback'; // Register the built-in child card elements so that importing is self-contained. import './form'; import './confirm-card'; import './tasks'; import './choice'; import './link-preview'; import './embed'; interface Props extends Record { /** The stream of card envelopes to render. Set as a JS PROPERTY: `el.cards = [...]`. */ cards?: CardEnvelope[]; /** Optional type→tag overrides/additions (merged over the built-ins). Property: `el.types`. * Typed as a plain string map (not the `CardTagMap` alias) so the generated React * wrapper inlines it instead of emitting an unresolved named type. */ types?: Record; /** Optional CardPolicy handling child events. Property: `el.policy`. */ policy?: CardPolicy; } /** A single resolved child: a known kc-* tag (props set imperatively) or the fallback. */ function CardSlot(props: { envelope: CardEnvelope; tag?: string; theme: string; emit: (e: CardEvent) => void }): JSX.Element { let ref: HTMLElement | undefined; // Set object/string props as DOM properties on the custom element (reactive). createEffect(() => { if (!ref) return; (ref as unknown as { data: unknown }).data = props.envelope.data; (ref as unknown as { cardId: string }).cardId = props.envelope.id; if (props.envelope.title != null) (ref as unknown as { heading: string }).heading = props.envelope.title; (ref as unknown as { resolution: unknown }).resolution = props.envelope.resolution; ref.setAttribute('theme', props.theme); }); // Hoist the unknown-type error emit to onMount to avoid side-effect-in-JSX lint issue // and to ensure exactly one error fires. onMount(() => { if (!props.tag) { props.emit({ kind: 'error', cardId: props.envelope.id, message: `Unsupported card type: ${props.envelope.type}` }); } }); return ( } > {(tag) => } ); } defineWebComponent( 'kc-cards', { cards: undefined, types: undefined, policy: undefined }, (props, { element }) => { // Route children's bubbling kc-card events through the policy. Attached to the host // element so composed events from each child's shadow root are caught as they bubble. // The handler reads `props.policy` at EVENT time (not mount time) so setting // `el.policy` after the element is in the DOM — the standard host pattern — works. onMount(() => { const handler = (e: Event) => routeCardEvent(props.policy ?? {}, (e as CustomEvent).detail); element.addEventListener(CARD_EVENT_NAME, handler as EventListener); onCleanup(() => element.removeEventListener(CARD_EVENT_NAME, handler as EventListener)); }); // Read the facade's REACTIVE `theme` prop, not element.getAttribute (which is // not a tracked dependency) — otherwise a theme change on after the // children first render never propagates, leaving each child card stuck on its // initial 'auto' (which follows the OS, so cards looked "always dark"). const theme = () => ((props as { theme?: string }).theme ?? 'auto'); const tags = () => mergeCardTags(props.types); return (
{(env) => ( emitCardEvent(element, e)} /> )}
); }, );