@kitn.ai/chat
Version:
Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.
122 lines (108 loc) • 4.62 kB
text/typescript
import meta from '../../elements/element-meta.json';
type Prop = { name: string; type: string; default?: string; scalar: boolean };
type ElementMeta = { tag: string; props: Prop[] };
const all = meta as unknown as ElementMeta[];
// A small, consistent pointer at the top of every Web Component's Docs tab,
// directing readers to the dedicated API tab (which holds the full generated
// spec). Keeps the Docs tab focused on examples while keeping the spec one click
// away — and discoverable, since everyone reads the description first.
const API_POINTER =
'> **Full API reference** — every property, event, default and token is on the **API** tab above.';
/**
* Build a Storybook `docs.description` from an element's intro paragraphs, with
* the API-tab pointer prepended. The generated spec itself lives in the **API**
* tab (see `.storybook/api-tab.tsx`), not inline here.
* (`tag` is kept in the signature so callers don't churn if we re-inline later.)
*/
export function specDescription(_tag: string, paragraphs: string[]): { component: string } {
return { component: [API_POINTER, ...paragraphs].join('\n\n') };
}
// The Components/UI sibling of API_POINTER — same idea, component vocabulary
// (props/callbacks/slots rather than properties/events/attributes).
const COMPONENT_POINTER =
'> **Full API reference** — every prop, callback, slot and token is on the **API** tab above.';
/**
* Build a Storybook `docs.description` for a SolidJS/UI component story, with the
* API-tab pointer prepended. The generated spec (props/callbacks/slots/tokens)
* lives on the **API** tab (see `.storybook/api-tab.tsx`), from
* `src/components/component-meta.json`.
*/
export function componentDescription(paragraphs: string[]): { component: string } {
return { component: [COMPONENT_POINTER, ...paragraphs].join('\n\n') };
}
/**
* Parse a type string from element-meta.json into meaningful parts.
*
* The meta generator writes types like:
* - `"undefined | false | true"` for booleans (NOT "boolean")
* - `"undefined | \"preview\" | \"code\""` for enums (double-quoted, with leading undefined)
*
* We normalise by splitting on `|`, trimming, and dropping `undefined` / `null`.
*/
const normaliseParts = (type: string): string[] =>
type
.split('|')
.map((s) => s.trim())
.filter((s) => s !== 'undefined' && s !== 'null');
const enumValues = (parts: string[]): string[] | null => {
// Accept both single-quoted ('foo') and double-quoted ("foo") string literals.
if (parts.length > 0 && parts.every((p) => /^(['"])[^'"]*\1$/.test(p)))
return parts.map((p) => p.slice(1, -1));
return null;
};
const isBooleanParts = (parts: string[], rawType: string): boolean => {
// Explicit boolean keyword (future-proof)
if (/\bboolean\b/.test(rawType)) return true;
// element-meta encodes booleans as "undefined | false | true"
const sorted = [...parts].sort().join(',');
return sorted === 'false,true';
};
/**
* Storybook argTypes for an element's props (theme select, booleans, text,
* number, object/JSON for complex types, and no control for functions).
*
* Classification order:
* 1. enum → select
* 2. boolean → boolean
* 3. number → number
* 4. contains `=>` (function / function-bearing object) → control: false
* 5. not all-string after normalisation (arrays, records, generics…) → object
* 6. all-string → text
*/
export function argTypesFor(tag: string): Record<string, unknown> {
const el = all.find((e) => e.tag === tag);
if (!el) return {};
const out: Record<string, unknown> = {};
for (const p of el.props) {
const parts = normaliseParts(p.type);
// 1–3: scalar classifications (enums, booleans, numbers only appear in scalar props)
if (p.scalar) {
const values = enumValues(parts);
if (values) {
out[p.name] = { control: { type: 'select' }, options: values };
continue;
}
if (isBooleanParts(parts, p.type)) {
out[p.name] = { control: 'boolean' };
continue;
}
if (/\bnumber\b/.test(p.type)) {
out[p.name] = { control: 'number' };
continue;
}
}
// 4. Function / function-bearing object: contains `=>` in raw type string
if (p.type.includes('=>')) {
out[p.name] = { control: false };
continue;
}
// 5. Complex/object: parts are not all exactly the string "string"
if (!parts.every((part) => part === 'string')) {
out[p.name] = { control: 'object' };
continue;
}
// 6. Pure string
out[p.name] = { control: 'text' };
}
return out;
}