@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.
138 lines (137 loc) • 6.59 kB
TypeScript
import { JSX } from 'solid-js';
import { CardEnvelope, CardHost, CardResolution } from '../primitives/card-contract';
/** A field definition (the JSON Schema subset kai-form renders). */
export interface FormField {
type: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
title?: string;
description?: string;
default?: unknown;
enum?: unknown[];
format?: 'email' | 'uri' | 'url' | 'date' | 'date-time' | 'time';
minimum?: number;
maximum?: number;
minLength?: number;
maxLength?: number;
pattern?: string;
minItems?: number;
maxItems?: number;
items?: FormField | {
enum: unknown[];
};
properties?: Record<string, FormField>;
required?: string[];
readOnly?: boolean;
'x-kai-widget'?: 'textarea' | 'slider' | 'rating' | 'radio' | 'select' | 'checkbox' | 'password' | 'switch';
'x-kai-placeholder'?: string;
'x-kai-step'?: number;
}
/** The form definition = CardEnvelope.data for type:'form'. */
export interface FormDefinition {
type: 'object';
title?: string;
description?: string;
required?: string[];
properties: Record<string, FormField>;
'x-kai-order'?: string[];
'x-kai-inlineMax'?: number;
'x-kai-submitLabel'?: string;
'x-kai-dismissible'?: boolean;
'x-kai-actions'?: {
id: string;
label: string;
variant?: 'default' | 'ghost' | 'outline';
}[];
}
export type FormCardEnvelope = CardEnvelope<'form', FormDefinition>;
/** The internal widget identifiers `widgetFor` resolves to. */
export type WidgetKind = 'text' | 'textarea' | 'password' | 'email' | 'url' | 'date' | 'datetime' | 'time' | 'number' | 'slider' | 'rating' | 'switch' | 'checkbox' | 'radio' | 'select' | 'checkbox-group' | 'multiselect' | 'repeater' | 'taglist' | 'fieldset' | 'unsupported';
export declare const DEFAULT_INLINE_MAX = 4;
/** Resolve the widget for a field. An explicit valid `x-kai-widget` always wins;
* otherwise the type/format/enum/constraint combination selects the widget. */
export declare function widgetFor(field: FormField, inlineMax: number): WidgetKind;
/** Humanize a camelCase / snake_case property key into a label. */
export declare function humanize(key: string): string;
/** Field order: `x-kai-order` (filtered to known keys, missing appended) if
* present, else `required` first then schema declaration order. */
export declare function orderedKeys(def: FormDefinition): string[];
/** Coerce a raw control value to the field's JSON type. Empty number string →
* undefined; number/integer → Number; boolean → real boolean. */
export declare function coerceValue(field: FormField, raw: unknown): unknown;
export interface FormValidation {
valid: boolean;
fieldErrors: Record<string, string>;
}
/** Full client-side validation of `values` against the form definition. Returns a
* per-field error map (the contract validator subset, applied field-by-field so
* each field can show its own inline message). */
export declare function validateForm(def: FormDefinition, values: Record<string, unknown>): FormValidation;
export interface FormSummaryRow {
key: string;
label: string;
value: string;
}
/** Format one field's value for the read-only summary. */
export declare function formatFieldValue(field: FormField | undefined, raw: unknown): string;
/** Build the label→value rows for a submitted form, honoring x-kai-order. */
export declare function summarizeForm(def: FormDefinition, data: Record<string, unknown>): FormSummaryRow[];
/** Build the result object: coerced values with empty optional fields omitted.
* `false` and `0` are kept (they are real values, not "empty"). */
export declare function buildResult(def: FormDefinition, values: Record<string, unknown>): Record<string, unknown>;
/** Imperative handle exposed via `controllerRef` — surfaces the form's latent
* capabilities (focus the first/first-invalid control, validate, programmatic
* send, reset to defaults, dismiss/reopen) so the `<kai-form>` facade can forward
* them as instance methods. */
export interface FormController {
/** Focus the first control, or the first INVALID control after a failed validation. */
focus(options?: FocusOptions): void;
/** Run full validation + submit: focus the first invalid field on failure, else emit `submit`. */
send(): void;
/** Run client-side validation now and return per-field errors WITHOUT submitting. */
validate(): {
valid: boolean;
errors?: Record<string, string>;
};
/** Re-seed from each field's `default` and clear errors. */
reset(): void;
/** Trigger the dismiss path (emit `dismiss` + collapse to the re-openable stub). */
dismiss(): void;
/** Re-open a dismissed card from its stub (emit `reopen`). */
reopen(): void;
}
export interface FormProps {
/** The form definition (CardEnvelope.data). */
data?: FormDefinition;
/** The card id used to correlate every emitted CardEvent. */
cardId?: string;
/** The envelope title rendered in the card chrome. */
heading?: string;
/** Optional explicit CardHost (otherwise read from a CardProvider, otherwise the
* bubbling `kai-card` CustomEvent off `hostElement`). */
host?: CardHost;
/** The custom-element host node, for the bubbling `kai-card` fallback emit. */
hostElement?: HTMLElement;
class?: string;
/** When set, render the chromed read-only view instead of the form inputs. */
resolution?: CardResolution;
/** Controlled field values — when set, this wins over internal/uncontrolled state. */
values?: Record<string, unknown>;
/** Initial values overlaying the schema defaults (uncontrolled seed). */
defaultValues?: Record<string, unknown>;
/** Disable all fields + submit. */
disabled?: boolean;
/** Fires on input with the current coerced values + validity (distinct from submit). */
onValuesChange?: (payload: {
values: Record<string, unknown>;
valid: boolean;
}) => void;
/** Receive the imperative controller once mounted. */
controllerRef?: (controller: FormController) => void;
}
/**
* `Form` — renders a JSON-Schema form definition into themed, accessible widgets
* inside `Card` chrome, validates input against that schema, and emits the
* collected, coerced, validated object up the Card contract as `submit`.
* Reads context/emits via a `CardProvider` when present, else the bubbling
* `kai-card` CustomEvent.
*/
export declare function Form(props: FormProps): JSX.Element;