View components API
Custom views can use the same components the artifuncs app is built from — the ones
showcased on the kitchen-sink page — through the af-* elements. They're registered by the
host page, so there's nothing to import or install: write the tag, and the real component
renders, fully styled and theme-aware in light/dark.
import { useState } from "react"
export default function View() {
const [range, setRange] = useState([40, 50])
return (
<div className="grid gap-4 p-4">
<af-slider label="Height range" value={range} onChange={(e: any) => setRange(e.detail[0])} />
<af-button label="Run" variant="primary" onClick={go} />
</div>
)
}
The value / change contract
Every input element takes its state through the value property and reports changes via
a change event with the new value in e.detail[0]:
<af-text-field label="Name" value={name} onChange={(e: any) => setName(e.detail[0])} />
Text controls (af-text-field, af-textarea) additionally emit input on every keystroke
with the same payload. af-button needs no bridge — its click is a normal DOM click
(onClick). Complex values (arrays, objects) work because JSX sets DOM properties:
value={[40, 50]} passes the tuple as-is.
Elements
af-button
<af-button label="Run" variant="primary" onClick={go} />
<af-button label="Cancel" variant="outline" size="sm" />
<af-button label="Delete" variant="danger" disabled={busy} />
<af-button label="Working…" variant="primary" loading={busy} />
| Prop | Values |
|---|---|
label | button text |
variant | primary (default), secondary, outline, ghost, link, danger, danger-outline, success, warn |
size | xs, sm, md (default), lg |
loading, disabled, icon-only | booleans |
af-slider
<af-slider label="Volume" value={vol} min={0} max={100} step={1} unit="%"
onChange={(e: any) => setVol(e.detail[0])} />
Range mode: pass a tuple — value={[40, 50]} — and you get two draggable thumbs; the
change payload stays an ordered [lo, hi] pair (thumbs can't cross).
Props: label, min (0), max (100), step (1, floats OK), unit (readout suffix),
hint, disabled.
af-text-field
<af-text-field label="API key" placeholder="sk-…" value={key}
onInput={(e: any) => setKey(e.detail[0])} />
Props: label, placeholder, hint, type (e.g. "password"), disabled, readonly,
error. Events: input (per keystroke), change.
af-textarea
<af-textarea label="Prompt" value={prompt} onInput={(e: any) => setPrompt(e.detail[0])} />
Props: label, placeholder, hint, disabled, readonly, error.
af-select
<af-select label="Mode" items={["fast", "thorough"]} value={mode}
onChange={(e: any) => setMode(e.detail[0])} />
items accepts an array property (objects too — set item-title / item-value for the
display/value keys) or a JSON string attribute. Props: label, placeholder, hint,
disabled.
af-checkbox
<af-checkbox label="Enable feature" value={on} onChange={(e: any) => setOn(e.detail[0])} />
Props: label, hint, disabled, readonly. Value: boolean.
af-date
<af-date label="Due date" value={date} onChange={(e: any) => setDate(e.detail[0])} />
Value in and out is a YYYY-MM-DD string — JSON-friendly for run(). Props: label,
hint, disabled, readonly, clearable.
af-color / af-color-palette
<af-color label="Accent" value={hex} onChange={(e: any) => setHex(e.detail[0])} />
<af-color-palette label="Palette" value={colors} onChange={(e: any) => setColors(e.detail[0])} />
af-color holds one hex string (show-swatches for preset swatches; show-text-field
defaults on; variant="minimal" renders just the rounded-square swatch — no hex text).
af-color-palette holds a hex string[].
af-multi-select
<af-multi-select label="Formats" items={["png", "jpg", "webp"]} value={formats}
onChange={(e: any) => setFormats(e.detail[0])} />
Like af-select but the value is an array. Same items handling (item-title/item-value
for object items).
af-tags-input
<af-tags-input label="Tags" value={tags} onChange={(e: any) => setTags(e.detail[0])} />
Free-text tag entry; value is a string[].
af-file-field
const [file, setFile] = useState<File | null>(null)
<af-file-field label="Upload" accept=".pdf,.png" onChange={(e: any) => setFile(e.detail[0])} />
<af-button label="Process" onClick={async () => setOut(await run({ upload: file }))} />
The value is a browser File (or File[] with multiple). Two ways to get it to
process():
- Just pass it to
run()— anyFilein the input is uploaded automatically and replaced with its filename, soprocess()receivesinput["upload"]as a filename string (identical to declaredfilefields). - Eager upload —
const { name } = await files.upload(file, { onProgress })uploads immediately (e.g. on select, with a progress bar) and gives you the filename to pass in a laterrun().
Props: label, hint, accept, multiple, max-size (bytes), max-files, disabled.
Reading files process() wrote (see custom views for the
full files API): files.text(name), files.blob(name), files.url(name) (object URL —
drop into <img src>), files.save(name) (browser download).
Display elements — af-code-block, af-tag, af-timeline
<af-code-block code={result.json} language="json" filename="result.json" />
<af-tag label="nlp" />
<af-timeline steps={[
{ state: "done", title: "Fetched" },
{ state: "active", title: "Processing" },
{ state: "pending", title: "Render" },
]} />
af-code-block: code, language, filename, show-copy, show-header. af-tag:
label, hash. af-timeline: steps array of
{ state: 'done'|'active'|'pending'|'failed', title, body?, substeps?, collapsible?, defaultOpen? }.
TypeScript note
The af-* tags aren't in preact's JSX types — either type the event handlers' arg as any
(as above) or add a one-liner to your view:
declare module "react" { namespace JSX { interface IntrinsicElements { [tag: `af-${string}`]: any } } }
Layout & custom styling
Use Tailwind utilities (grid, gap-4, p-4,
bg-surface, text-ink, …) for layout and anything bespoke, and theme tokens
(var(--brand), var(--surface), …) in raw CSS. The af-* elements render in the page's
light DOM, so they compose with both.
The live reference for how each component looks in every state is the app's kitchen-sink
page (/kitchen-sink when signed in).