@kitn.ai/ui
Version:
Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.
45 lines (41 loc) • 2.41 kB
text/typescript
// Shared resolution controller for interactive cards. Precedence:
// props.resolution (host-driven / re-hydrated) > local optimistic flip > none
// A new `data` identity clears the local flip (a fresh card definition is interactive
// again), but an explicit prop keeps it resolved. `isOptimistic` is true only for a
// flip made this session (used to announce via role="status"; silent on re-hydrate).
//
// A resolution is `isResolved` when present at all; `isTerminal` for the finished
// states (action | submit | expired) and `isDeferred` for `dismissed` — a set-aside
// card that still offers a Reopen affordance.
import { createSignal, createMemo, createEffect, on, type Accessor } from 'solid-js';
import type { CardResolution } from '../primitives/card-contract';
export interface ResolutionController<R extends CardResolution = CardResolution> {
resolution: Accessor<R | undefined>;
isResolved: Accessor<boolean>;
/** A finished resolution the user can't take back here (action | submit | expired). */
isTerminal: Accessor<boolean>;
/** A deferred resolution (`dismissed`) — set aside, still re-openable. */
isDeferred: Accessor<boolean>;
isOptimistic: Accessor<boolean>;
setLocal: (r: R) => void;
}
/** The resolution kinds that are finished and not re-openable from the card. */
const TERMINAL_KINDS = new Set<CardResolution['kind']>(['action', 'submit', 'expired']);
export function useCardResolution<R extends CardResolution = CardResolution>(opts: {
prop: Accessor<R | undefined>;
data: Accessor<unknown>;
}): ResolutionController<R> {
const [local, setLocal] = createSignal<R | undefined>(undefined);
// Reset the optimistic flip when a NEW data identity arrives (deferred so mount
// doesn't clobber an initial prop). The prop still wins via the memo below.
createEffect(on(opts.data, () => setLocal(undefined), { defer: true }));
const resolution = createMemo(() => opts.prop() ?? local());
const isResolved = createMemo(() => resolution() !== undefined);
const isTerminal = createMemo(() => {
const r = resolution();
return r !== undefined && TERMINAL_KINDS.has(r.kind);
});
const isDeferred = createMemo(() => resolution()?.kind === 'dismissed');
const isOptimistic = createMemo(() => opts.prop() === undefined && local() !== undefined);
return { resolution, isResolved, isTerminal, isDeferred, isOptimistic, setLocal };
}