anentrypoint-design v0.0.272 — Standardized component prop naming and API surface.
This document describes all exported components, their prop signatures, and standardized naming conventions. All components are pure factories that return webjsx vnodes.
The freddie.js re-exports (FREDDIE_PAGES, home/chat/voice/sessions/projects/agents/analytics/models/cron/skills/config/env/tools/batch/gateway/chains, skillLabel, getRecentPaths, saveRecentPath, renderChatMessages) are the kit's own docs/marketing demo-site page builders, not general-purpose components — not intended for external composition.
Boolean props follow one of two patterns:
- State props (indicate component state): use
stateenum pattern or explicit state prop- Examples:
state: 'active' | 'default',expanded: true | false,typing: true | false
- Examples:
- Semantic boolean props: use
is*prefix for clarity- Examples:
isLoading,isOpen,isDisabled
- Examples:
Callback props always use on* camelCase:
onClick,onInput,onSubmit,onChange, etc.
onAction is prop-name-only, not a shared contract — its call signature varies by component and each is correct for its own use, not a bug to unify: Banner's onAction(rawEvent) is a generic action-button click handler; FileRow's onAction(action) is package-internal (FileGrid wraps it into its own public onAction(action, file), the two-arg shape external consumers should wire against); FileViewer/FilePreviewPane's onAction('download') is a fixed single-purpose download trigger since the file is already available via the file prop. Check the specific component's signature below rather than assuming a shared shape.
Variant/mode props use enum values, never multiple boolean flags:
- Old:
primary={true},ghost={true}[ ] - New:
variant: 'primary' | 'ghost' | 'default'[x]
Multi-select contract (shared by FileGrid and SessionDashboard):
selectable(bool),selected(Set keyed by entity id/path),onToggleSelect(item, { range }),onSelectAll(),onClearSelection()- FileGrid also accepts
marked/onMarkas aliases ofselected/onToggleSelect.
The flagship application surfaces (an agent-chat product is composed from these).
A Claude-Desktop-style three-(or four-)column app shell.
WorkspaceShell({
rail, // left nav vnode (usually WorkspaceRail())
sessions, // OPTIONAL second column (ConversationList); null hides it
main, // primary content column (vnode or array)
pane, // OPTIONAL right context pane; null hides it
crumb, // optional thin top chrome bar over the content column
status, // optional footer (Status())
narrow, // caller's isNarrow() - drives drawer auto-close behavior
railCollapsed, paneCollapsed, // initial collapse (persisted state wins)
stableFrame, // keep the pane grid TRACK at width 0 when this tab has
// no pane, so the column count never re-flows on tab switch
mainFlush, // remove .ws-main's content gutter (chat self-gutters)
railLabel, paneLabel, // aria labels
})Responsive: the pane track yields at <=1480px (right overlay drawer via
.ws-pane-open), the sessions track at <=1100px (left drawer via
.ws-sessions-open), and below 900px the rail drops to a fixed icon strip with
a single content column. The crumb bar hosts the drawer toggles.
WorkspaceRail({ brand, action: {label, icon, onClick}, items: [{key, label, icon, active, count, onClick}], footer })
The persistent conversation rail ("Chats" column).
ConversationList({ sessions: [{sid, title, project, agent, time, running, unread, rail}], selected, groups: [{label, sids}], search: {value, placeholder, onInput}, caption, onSelect(session), onNew(), newLabel, emptyText, loading, loadingText, error })
Loading renders shape-matched skeleton rows. The body is one stable keyed wrapper across loading/empty/populated states (webjsx applyDiff requirement).
The live multi-session command center.
SessionDashboard({ sessions, onStop, onOpen, onView, onStopAll, onStopSelected, onArmStopAll, onArmStopSelected, confirmingStopAll, confirmingStopSelected, sort: {value, onChange}, filter: {value, placeholder, onInput}, errorsOnly, onErrorsOnly, selectable, selected: Set, onToggleSelect, onSelectAll, onClearSelection, activeSid, streamState: 'connected'|'connecting'|'lost'|'offline', emptyText, emptyAction: {label, onClick}, offline })
Session shape: { sid, realSid, title, agent, model, cwd, elapsedMs, counter, lastActivity, currentTool, status: 'running'|'stale'|'error', stopping, external, isNew, cost, tokens }.
Status-sorted renders bucketed groups (Errored/Running/Idle/External); other sorts render flat.
A middot-separated metadata strip for a session detail surface (compact fact display, e.g. version/uptime/roots).
SessionMeta({ items: [{ label, value, title, onCopy }] })
Each item renders as a span with a dimmed label + mono value; passing onCopy adds a per-item copy button. Returns null when items is empty. Class .ds-session-meta-strip (the bare .ds-session-meta class is already used by ConversationList row meta).
One running session. Same session shape as above; external suppresses stop and renders a read-only card.
ContextPane({ agent, model, cwd, toolCount, usage: {inputTokens, outputTokens, costUsd, turns, durationMs}, session: {turns, cost}, onSetCwd })
The change-cwd affordance rides ON the working-dir row; all-zero session totals hide the conversation block.
The reusable multi-agent chat surface (pure component, host wires transport).
AgentChat({
agents, selectedAgent, models, selectedModel, modelsLoading,
messages, // [{id, role, content, time, parts}] - parts may be
// strings or {kind:'tool'|'md'|...} structured parts
busy, draft, status, banners,
cwd, cwdEditing, cwdDraft, cwdError, cwdChecking,
agentName, placeholder, canSend,
suggestions, onSuggestionClick,
onSelectAgent, onSelectModel, onSend, onStop, onNewChat, onInput,
onCwdEdit, onCwdSave, onCwdCancel, onCwdClear, onCwdDraft,
onCopyMessage, onRetryMessage, onEditMessage, confirmEdit, onArmEdit,
avatar, composerContext, // {bits: [string | {text|label, title, onClick}]}
followups, onFollowupClick,
installHint, exportActions,
onPasteFiles(files), onDropFiles(files), // NOTE the names - not onPasteImage
shownMessages, onShowEarlier, // windowed thread (default cap 100)
})Beyond the base listing props:
FileGrid({ ..., selectable, selected: Set (keyed by path), onToggleSelect(f, {range}), onSelectAll(keys), onClearSelection, density: 'list'|'compact'|'thumb', onDensity, thumbUrl(f) })
marked/onMark are accepted aliases for selected/onToggleSelect.
loading with rows present dims the grid in place (is-refreshing); skeleton renders only on a cold load.
FileRow row actions: onAction(action, file) fires with action in ['download','rename','move','delete']; 'move' opens the host's bulk-move flow (agentgui seeds a single-path selection into its existing multi-select move dialog).
BulkBar({ count, noun, nounPlural, actions: [{label, danger, onClick}], onClear, busy }) - pluralizes -y nouns.
FileToolbar({ left, right }); RootsPicker({ roots: [{id, label}], selected, onSelect, label });
DropZone({ children, dragover, onDrop(files), onDragOver, onDragLeave, label, onPick }) - with children it is a
passive wrapper whose dashed affordance overlays ONLY during a drag; without children it is an explicit picker block.
Inline (non-modal) split-view file preview; the modal FileViewer remains the <900px fallback.
Creates a branded heading with optional leaf text.
Brand({ name = '247420', leaf })| Prop | Type | Default | Description |
|---|---|---|---|
name |
string | '247420' |
Brand name |
leaf |
string | - | Optional suffix (appears after slash) |
Example:
Brand({ name: 'acme', leaf: 'dashboard' })
// -> "acme / dashboard"Inline colored badge/label. size: 'sm' | 'md' | 'lg' scales it; tag: true renders a rectangular sentence-case variant (--r-0, no caps) for dense data like table cells, in place of the default all-caps pill.
Chip({ tone = '', size = 'md', tag = false, children })| Prop | Type | Default | Description |
|---|---|---|---|
tone |
string | '' |
CSS tone class (e.g., 'live', 'idle') |
children |
ReactNode | - | Chip label/content |
Example:
Chip({ tone: 'live', children: 'online' })Plain non-interactive label chip — tag-like annotation (a phase name, an id, a subsystem tag). Distinct from Chip (status-tone indicator), Badge (count/variant marker), and FilterPills (interactive toggle-group): Pill renders no button and carries no pressed/active state.
Pill({ tone = '', children })| Prop | Type | Default | Description |
|---|---|---|---|
tone |
string | '' |
'' | 'accent' | 'muted' |
children |
ReactNode | - | Pill label/content |
Example:
Pill({ tone: 'accent', children: 'PLAN' })Flexible button component with support for multiple variants. size: 'sm' | 'md' | 'lg' snaps the height to the --ctl-sm/md/lg ladder (28/34/42px, density-scaled) with horizontal-only padding — md is the base.
Btn({ href, variant = 'default', size = 'md', children, onClick, 'aria-label': ariaLabel, primary, ghost, danger, disabled, class: className, key })| Prop | Type | Default | Description |
|---|---|---|---|
variant |
'primary' | 'ghost' | 'danger' | 'default' |
'default' |
Button style variant. 'danger' renders btn-primary danger. If both variant and a legacy boolean (primary/ghost/danger) are passed, variant wins. |
href |
string | undefined |
Navigation link. A real navigational value renders <a>; omitted, '', or literal '#' renders a native <button> (not a link) |
children |
ReactNode | - | Button label/content |
onClick |
function | - | Click handler callback |
aria-label |
string | - | Accessible label (auto-filled from children if string) |
disabled |
boolean | - | Disables click/keyboard activation; adds is-disabled, aria-disabled, and (for links) tabindex="-1" |
class |
string | - | Extra class(es) appended to the resolved variant class (matches Panel/Heading's class prop naming) |
key |
string | - | webjsx diff key |
primary |
boolean | - | Deprecated: use variant="primary" |
ghost |
boolean | - | Deprecated: use variant="ghost" |
danger |
boolean | - | Deprecated: use variant="danger" |
Example:
// New way
Btn({ variant: 'primary', children: 'save', onClick: handleSave })
// Legacy (still supported)
Btn({ primary: true, children: 'save', onClick: handleSave })Colored icon/symbol with optional custom color.
Glyph({ children, color })| Prop | Type | Default | Description |
|---|---|---|---|
children |
string | - | Icon/symbol (emoji or Unicode) |
color |
string | - | CSS color value |
Monochrome inline-SVG line icon (stroke = currentColor). Both call forms are accepted:
Icon('folder', { size: 16 }) // positional (original)
Icon({ name: 'folder', size: 16 }) // props-object (matches every sibling component)Unknown names render a blank decorative span (no throw). The canonical name list
is the ICON_PATHS table in src/components/shell.js. iconMarkup(name|{name,size})
returns the same SVG as a markup string for raw-DOM consumers.
Application header with brand, search, and nav items.
Topbar({ brand = '247420', leaf = '', items = [], active = '', onNav, search })| Prop | Type | Default | Description |
|---|---|---|---|
brand |
string | '247420' |
Brand name |
leaf |
string | '' |
Brand suffix |
items |
[[label, href], ...] |
[] |
Navigation items |
active |
string | '' |
Currently active nav item label |
onNav |
function | - | Nav click callback (label passed) |
search |
string | - | Search placeholder (shows search input if set) |
Example:
Topbar({
brand: 'myapp',
items: [['home', '#/'], ['docs', '#/docs']],
active: 'home',
onNav: (label) => navigate(label),
search: 'search…'
})Sidebar navigation with grouped items.
Side({ sections = [] })| Prop | Type | Default | Description |
|---|---|---|---|
sections |
array | [] |
Group objects with { group, items: [] } |
Section item shape:
{
group: 'group label',
items: [
{ label, glyph, href, active, count, color, onClick },
...
]
}Example:
Side({
sections: [
{
group: 'main',
items: [
{ label: 'home', glyph: '⌂', href: '#/', active: true },
{ label: 'settings', glyph: '⚙', href: '#/settings', count: 3 }
]
}
]
})Footer status bar with left/right content.
Status({ left = [], right = [] })| Prop | Type | Default | Description |
|---|---|---|---|
left |
ReactNode[] | [] |
Left-aligned items |
right |
ReactNode[] | [] |
Right-aligned items |
Complete app layout with header, sidebar, main, and footer.
AppShell({ topbar, crumb, side, main, status, narrow })| Prop | Type | Default | Description |
|---|---|---|---|
topbar |
ReactNode | - | Header component |
crumb |
ReactNode | - | Breadcrumb component |
side |
ReactNode | - | Sidebar component |
main |
ReactNode | - | Main content area |
status |
ReactNode | - | Footer status bar |
narrow |
boolean | - | Narrow main width layout |
Semantic heading with configurable level.
Heading({ level = 1, children, style = '' })| Prop | Type | Default | Description |
|---|---|---|---|
level |
1-6 | 1 |
Heading level (h1-h6) |
children |
ReactNode | - | Heading text |
style |
string | '' |
CSS style string |
Lead paragraph (large introductory text).
Lede({ children })Status indicator (live/idle).
Dot({ tone = 'live' })| Prop | Type | Default | Description |
|---|---|---|---|
tone |
`'live' | 'idle'` | 'live' |
Colored accent bar (decorative).
Rail({ tone = 'green' })| Prop | Type | Default | Description |
|---|---|---|---|
tone |
string | 'green' |
CSS tone class |
Container with optional header and content.
Panel({ title, count, right, style = '', children, kind })| Prop | Type | Default | Description |
|---|---|---|---|
title |
string | - | Panel header |
count |
number | - | Right-align count |
right |
ReactNode | - | Right header content |
style |
string | '' |
CSS style |
children |
ReactNode | - | Panel content |
kind |
string | - | Panel style variant |
List item with optional link/click handler.
Row({ code, rank, title, sub, meta, state = 'default', onClick, href, kind, cols, leading, trailing, target, selected, active, rail, expanded, highlight, actions, detail, key, style })| Prop | Type | Default | Description |
|---|---|---|---|
state |
'active' | 'default' | 'disabled' | 'error' |
'default' |
Row state; 'disabled' strips onClick/role=button/tab-stop and sets aria-disabled |
title |
string | - | Primary text |
sub |
string | - | Secondary text |
code |
ReactNode | - | Leading code/glyph |
rank |
ReactNode | - | Alias for code (the leading monospace index) |
meta |
string | - | Trailing meta text |
href |
string | - | Link destination |
onClick |
function | - | Click handler |
kind |
string | - | Row style variant |
cols |
string | - | CSS grid-template-columns |
leading |
ReactNode | - | Custom leading content |
trailing |
ReactNode | - | Custom trailing content |
target |
string | - | Link target (_blank, etc.) |
active |
boolean | - | Deprecated: use state="active" |
selected |
boolean | - | Deprecated: use state="active" |
rail |
string | - | Leading status-bar tone: 'green' (ok/selected) | 'purple' (subagent) | 'flame' (error/unavailable) | any CSS color token |
expanded |
boolean | - | Disclosure-toggle state; sets aria-expanded and gates actions rendering. Omit entirely for plain action rows (not a toggle) |
highlight |
string | - | Case-insensitive substring to wrap in <mark class="ds-hl"> within title |
actions |
array | - | Action-button specs rendered as a sibling strip; only shown when expanded === true |
detail |
ReactNode | - | Sibling block rendered after title/meta on its own line (.ds-row-detail) |
Example:
// New way
Row({ title: 'item', state: 'active' })
// Legacy
Row({ title: 'item', active: true })Link-only row (shorthand for Row with href and kind='link').
RowLink({ code, title, sub, meta, href = '#', key, target })Semantic section with eyebrow and title.
Section({ title, eyebrow, children })Large introductory section with title, body, accent, and actions.
Hero({ eyebrow, title, body, accent, badge, badgeCount, actions })Code snippet with copy button.
Install({ cmd, copied, onCopy })Key-value table (invoice-like display).
Receipt({ rows = [] })Version history table.
Changelog({ entries = [] })Expandable work items list.
WorksList({ works = [], openedIndex = -1, onToggle })Blog post/article list.
WritingList({ posts = [] })Prose paragraphs with optional dim styling.
Manifesto({ paragraphs = [], maxWidth })Key performance indicators grid.
Kpi({ items = [] })Items shape:
[
[value, label],
[value, label, { delta: '+12.4%', tone: 'up' | 'down', spark: [8, 11, 9, 14, 16] }],
...
]The third element is optional and additive — a bare [value, label] tuple is unchanged. When present, delta renders a toned trend pill (--success/--danger) with an up/down arrow, and spark renders an inline Sparkline.
Minimal inline SVG trend line — no chart library, token-stroke only.
Sparkline({ values = [], width = 72, height = 24, tone })tone: 'up' | 'down' colors the stroke via --success/--danger (default up). Returns null for an empty values array. Decorative (aria-hidden) — pair with adjacent text for the actual value.
Horizontal token-only progress-bar breakdown — category/channel comparisons.
BarChart({ items = [{ label, value, display }], emptyText = 'no data yet' })Bars scale relative to the largest value in the set; display overrides the trailing numeric label (defaults to String(value)). Fill width is set via a --bar-pct custom-property write (never a raw inline width:), keeping it clear of the inline-styles lint gate.
Data table with optional row click handler. striped/compact are opt-in density modifiers (default false, existing calls byte-unchanged) — striped alternates row background on even rows, compact tightens header/cell padding onto the --space-1-5 token.
Table({ headers = [], rows = [], onRowClick, emptyText = 'nothing here yet', rowLabels, striped = false, compact = false })Example:
Table({ headers: ['id', 'status'], rows: [['a1', 'live'], ['a2', 'idle']], striped: true, compact: true })Search input field with submit callback.
SearchInput({ value = '', placeholder = 'search…', onInput, onSubmit, name = 'q', key })Labeled input or textarea. size: 'sm' | 'md' | 'lg' snaps the control height to the --ctl-* ladder (via .ds-field--sm/--lg).
TextField({ label, value = '', type = 'text', placeholder = '', onInput, onChange, name, key, hint, multiline, rows = 4, size = 'md' })Dropdown select input. size: 'sm' | 'md' | 'lg' snaps the control height to the --ctl-* ladder (via .ds-field--sm/--lg).
Select({ label, value = '', options = [], onChange, name, key, placeholder, hint, size = 'md' })Numbered event list (alias for multi-row display).
EventList({ items, events, emptyText = 'no events', rankPad = 3 })Page section header with title, lede, and right content.
PageHeader({ title, lede, eyebrow, right, compact, dense, id })| Prop | Type | Description |
|---|---|---|
compact |
boolean | Strips the section's leading/trailing margin (for a header used as a page's first element). Keeps the display H1-over-paragraph layout. |
dense |
boolean | Content-first single-row form (small heading + inline lede) instead of the display H1-over-paragraph layout. When dense is true, compact is ignored — the dense row always renders margin-stripped. |
id |
string | Placed on the outer section as a deep-link anchor. |
Simple form with fields and submit button.
Form({ fields = [], submit = 'submit', onSubmit })Complete home page layout.
HomeView({ state = {}, onNav, onToggleWork, works = [], posts = [], manifesto = [], currentlyShipping })Project detail view.
ProjectView({ project = {}, copied, onCopy })Single chat message with avatar, content, reactions, and receipts.
ChatMessage({ role, who = 'them', avatar, text, parts, time, typing, key, aicat, reactions, receipt, name })| Prop | Type | Default | Description |
|---|---|---|---|
role |
`'user' | 'assistant' | 'them' |
who |
`'you' | 'them'` | 'them' |
avatar |
string | - | Avatar character/emoji |
text |
string | - | Plain text message |
parts |
array | - | Rich message parts (text, md, code, image, etc.) |
time |
string | - | Timestamp |
typing |
boolean | - | Show typing indicator |
aicat |
boolean | - | AI cat styling |
reactions |
array | - | Emoji reactions |
receipt |
`'sent' | 'read'` | - |
name |
string | - | Sender name (for 'them') |
key |
string | - | React key |
Example:
// New way
ChatMessage({ role: 'user', text: 'hello!' })
ChatMessage({ role: 'assistant', text: 'hi there!' })
// Legacy
ChatMessage({ who: 'you', text: 'hello!' })
ChatMessage({ who: 'them', text: 'hi there!' })Message parts shape:
{
kind: 'text' | 'md' | 'code' | 'image' | 'pdf' | 'file' | 'link',
text?, code?, lang?, filename?,
src?, alt?, caption?, href?, title?, desc?, thumb?,
name?, size?, kindLabel?
}Text input for composing messages.
ChatComposer({ value, onInput, onSend, placeholder = 'message…', disabled })Complete chat container with header and message thread.
Chat({ title = 'chat', sub, messages = [], composer, header })AI assistant chat interface with typing indicator.
AICat({ name = 'aicat', messages = [], thinking, composer, status = 'online · purring' })AI cat avatar display.
AICatPortrait({ name = 'aicat', status = 'idle', face })Flexible toolbar with leading, trailing, and center sections.
Toolbar({ leading = [], trailing = [], dense = false, children })Flat, wrapping row of arbitrary action nodes (buttons/inputs/chips) with no leading/center/trailing slot structure — the shape to reach for when a caller just wants "this row of controls, left to right, wrapping on narrow viewports" and Toolbar's three-slot split is unwanted overhead. Accepts children as varargs or a single array.
ToolbarRow(...actions)Example:
ToolbarRow(searchInput, filterSelect, refreshButton)
// or
ToolbarRow([searchInput, filterSelect, refreshButton])Tabbed interface.
Tabs({ items = [], active, onChange, children })Item shape:
{ id, label }Hierarchical tree navigation container.
TreeView({ children })Single expandable tree node.
TreeItem({ label, glyph, tag, depth = 0, selected = false, expanded = false, onSelect, onToggle, children, hasChildren })| Prop | Type | Default | Description |
|---|---|---|---|
label |
string | - | Node label |
glyph |
ReactNode | - | Leading icon |
tag |
ReactNode | - | Trailing tag |
depth |
number | 0 |
Indentation level |
selected |
boolean | false |
Selection state |
expanded |
boolean | false |
Expansion state |
onSelect |
function | - | Selection callback |
onToggle |
function | - | Expand/collapse callback |
children |
ReactNode | - | Child nodes |
hasChildren |
boolean | - | Explicitly mark as having children (inferred from children prop) |
Property inspector grid container.
PropertyGrid({ children })Property grid field item.
PropertyField({ label, hint, inline = false, children })A PropertyGrid row wrapper with a bottom-border divider (last-child border suppressed) — for editors that need stronger per-row visual separation than the default PropertyGrid gap gives (e.g. a list of independently-editable records like PRD/mutable rows).
PropertyGridRow({ children })Borderless-until-focus text input inheriting surrounding font, with an explicit error state (aria-invalid + danger-token border) for live per-field validation. Renders a <textarea> when multiline is set, else a single-line <input>.
InlineEditableField({ value = '', placeholder, onInput, onChange, error, multiline = false, rows = 3, ariaLabel, disabled = false })| Prop | Type | Default | Description |
|---|---|---|---|
value |
string | '' |
Current field value |
placeholder |
string | - | Placeholder text |
onInput |
function | - | (value, event) => void, fires on every keystroke |
onChange |
function | - | (value, event) => void, fires on commit (blur/change) |
error |
boolean | - | Sets aria-invalid="true" + .has-error class |
multiline |
boolean | false |
Renders a <textarea> instead of <input> |
rows |
number | 3 |
Textarea row count (multiline only) |
ariaLabel |
string | - | Accessible name |
disabled |
boolean | false |
Disables the field |
Prev/next paginator with a page label. page is 1-indexed; pageCount<=1 disables both buttons (no divide-by-zero, no dead-end enabled control). total (optional) renders an item-count suffix.
numbered: true switches to a compact numbered-button row (screen-real-estate dense mode) instead of the prev/next label — always shows first, last, current page, and up to siblingCount (default 1) neighbors either side, collapsing gaps into an ellipsis. Falls back to prev/next automatically when pageCount<=1. The default prev/next contract is unchanged.
Pager({ page = 1, pageCount = 1, onPage, total, itemLabel = 'items', numbered = false, siblingCount = 1 })Example:
Pager({ page: 2, pageCount: 5, total: 42, onPage: (p) => setPage(p) })
Pager({ page: 5, pageCount: 40, numbered: true, onPage: (p) => setPage(p) })24-column responsive layout primitive (screen-real-estate density: dense multi-column panels without hand-rolled grid-template-columns per consumer). Grid is a flex-row wrapper; GridItem's column-span props are integers 1-24 evaluated at four tiers mirroring BP_SM/BP_MD/BP_LG/BP_XL (480/768/1024/1440) via CSS custom properties + media queries — no JS-side matchMedia, degrades gracefully without hydration. A span of true means full-width/auto-grow at that tier; 0 hides the item at that tier.
Grid({ gap, justify, align, children })
GridItem({ xs, sm, md, lg, xl, children })Example:
Grid({ children: [
GridItem({ xs: true, sm: 6, md: 4, children: PanelA() }),
GridItem({ xs: true, sm: 6, md: 8, children: PanelB() }),
]})Progressive-disclosure toggle panel (screen-real-estate density: property inspectors, nested settings, FAQ-style panels). Controlled component — caller owns expanded state and passes onToggle, same pattern as Drawer/Dialog's open/onClose.
CollapseGroup lays out a list of {id, title, children} items. accordion: true enforces single-open-at-a-time via openId/onOpenChange(id); without it, each item manages its own expanded flag and onOpenChange(id, next) fires per-item.
Collapse({ title, expanded = false, onToggle, children })
CollapseGroup({ items = [], openId, onOpenChange, accordion = false })Example:
CollapseGroup({
accordion: true,
openId: state.openId,
onOpenChange: (id) => setState({ openId: id }),
items: [
{ id: 'general', title: 'General', children: GeneralSettings() },
{ id: 'advanced', title: 'Advanced', children: AdvancedSettings() },
],
})Plain rule, optional centered text label, optional vertical orientation — for segmenting dense panels without a full Section wrapper.
Divider({ label, vertical = false })Example:
Divider()
Divider({ label: 'OR' })
Divider({ vertical: true })Monospace data preview (max-height + scroll). Accepts a pre-stringified string OR any value — objects/arrays get JSON.stringify(v, null, 2); null/undefined render emptyText rather than the literal "undefined"/"null".
mode selects rendering. 'plain' (default) is the historical contract — flat <pre>, raw text, string input verbatim. 'highlight' tokenizes the JSON into ds-ep-json-k/s/n/b/z spans (key/string/number/boolean/null; linear single-pass scan, no regex); a string that does not parse as JSON falls back to plain text, so arbitrary prose is never falsely tokenized. 'tree' renders nested objects/arrays as native collapsible <details> nodes with child-count tags, open above treeDepth (default 2); scalars fall back to highlight. copyable: true wraps the viewer with a copy-to-clipboard button (transient copied/failed feedback).
JsonViewer({ value, emptyText = 'no data', maxHeight, mode = 'plain', copyable = false, treeDepth = 2 })Five-panel layout (top, left, center, right, bottom).
Dock({ top, left, right, bottom, center })Toggle button group.
IconButtonGroup({ items = [], value, onChange, dense = false })Item shape:
{ id, glyph?, label?, title?, disabled? }src/components/form-primitives.js — standard form controls (previously undocumented).
Checkbox({ checked, indeterminate, disabled, label, hint, onChange, ariaLabel, key, name, id })
Radio({ name, value, checked, disabled, label, hint, onChange, ariaLabel, key, id })
RadioGroup({ legend, name, value, options = [], onChange, orientation = 'vertical', key })
Toggle({ checked, disabled, label, hint, onChange, ariaLabel, kind = 'switch', key, id })
Field({ label, hint, error, required, requiredMarker = '*', htmlFor, children, key })
useFormValidation(schema = {})RadioGroup's options items are { value, label, hint?, disabled? }. Field wraps any input with a label/hint/required-marker/error row. useFormValidation returns a validator driven by a { field: (value) => errorString|null } schema map.
src/components/interaction-primitives.js — drag/drop, scrub, reorder, and keyboard-shortcut helpers (previously undocumented).
useDraggable(el, { data, kind, onDragStart, onDragEnd })
useDropTarget(el, { accepts = [], onDrop, onDragOver })
useNumberScrub(el, { getValue, onChange, step = 0.01, threshold = 3 })
usePointerDrag(el, { onStart, onMove, onEnd, button = 0 })
Reorderable({ items = [], getKey, renderItem, onReorder, axis = 'vertical', kind = 'reorder' })
useKeyboardShortcut(map = {}, { scope = 'global', enabled = true })
formatShortcut(combo)
ShortcutHint({ combo, kind = 'kbd' })
ShortcutList({ shortcuts = [] })
useKeyboardShortcutHelp()
ShortcutHelpDialog({ open = false, onClose, registry })useDraggable/useDropTarget/usePointerDrag are imperative hooks over a DOM element ref, not vnode factories — call them in an effect, not inside render. useKeyboardShortcut registers combos into a module-level registry that useKeyboardShortcutHelp/ShortcutHelpDialog read back to render a live shortcuts-overlay (the pattern agentgui's ?-overlay uses).
src/components/overlay-primitives.js — floating UI, focus-trap, and modal/popover surfaces (previously undocumented).
useFloating(anchorEl, contentEl, { placement = 'bottom-start', offset = 8 })
useLongPress(targetEl, callback, { ms = 500 })
Tooltip({ children, label, placement = 'top', delay = 350, kind = 'default' })
Popover({ open, anchorEl, onClose, placement = 'bottom-start', children, ariaLabel })
Dropdown({ trigger, items = [], onSelect, placement = 'bottom-start', ariaLabel })
trapTab(el, e)
CommandPalette({ open, items = [], onSelect, onClose })
EmojiPicker({ open, anchorX = 0, anchorY = 0, onSelect, onClose, query = '' })
BootOverlay({ progress = 0, phase = '', errored = false, visible = false })
SettingsPopover({ title = 'Settings', open, anchorX = 0, anchorY = 0, sections = [], onClose })
AuthModal({ mode = 'extension', error = '', busy = false, open = false, onModeChange, onConnectExtension, onGenerate, onImport, onClose })
VideoLightbox({ src, label = '', open = false, onClose })trapTab is the Tab-focus-trap primitive exported for reuse by any modal/drawer (agentgui's mobile drawer and files-modals both wire it directly — see AGENTS.md's webjsx/focus-trap notes). useFloating computes anchored position for Popover/Dropdown/Tooltip; call it with live DOM refs, not vnodes.
Server/guild icon button.
ServerIcon({ id, name, icon, active, badge, onClick })Vertical server icon rail.
ServerRail({ servers = [], activeId, onSelect, onAdd })Channel list item with optional voice state.
ChannelItem({ id, name, type = 'text', active, voiceActive, voiceConnecting, badge, draggable, actions = [], participants = [], onClick, onContext })| Prop | Type | Default | Description |
|---|---|---|---|
type |
`'text' | 'voice' | 'forum' |
active |
boolean | - | Active state |
voiceActive |
boolean | - | Voice channel active |
voiceConnecting |
boolean | - | Voice connection pending |
badge |
number | - | Unread count badge |
draggable |
boolean | - | Enable drag-and-drop |
actions |
array | - | Action button array |
participants |
array | - | Voice participants |
onClick |
function | - | Click handler |
onContext |
function | - | Right-click handler |
Collapsible channel group.
ChannelCategory({ id, name, channels = [], collapsed, activeId, onToggle, onAddChannel, onChannelClick, onChannelContext, onContextMenu, extraButton, channelDraggable })Voice channel participant.
VoiceUser({ identity, speaking, color })User profile panel.
UserPanel({ identity, name, color, status = 'online' })Voice channel toolbar.
VoiceStrip({ channelName, status, muted, deafened, onMute, onDeafen, onLeave, open })Complete channel sidebar with categories.
ChannelSidebar({ categories = [], open })Member list item.
MemberItem({ id, name, channels = [], collapsed, activeId, onToggle, onAddChannel, onChannelClick, onChannelContext, onContextMenu, extraButton, channelDraggable })Members list container.
MemberList({ members = [], onMemberClick })Chat header (community).
ChatHeader({ channelName, status, muted, deafened, onMute, onDeafen, onLeave, open })Complete community layout.
CommunityShell({ topbar, crumb, side, main, status, narrow })Exports from already-documented modules above that previously had zero mention here. Signatures only (see the module's own source comments for behavior detail); expand into a full section if a consumer needs deeper docs for one of these.
Shell (shell.js): IconButton({ icon, onClick, title, size = 'base', variant = 'ghost', disabled = false }), Badge({ children, variant = 'default', tone = 'neutral', size = 'md' }) (size 'sm' | 'md' | 'lg'), Crumb({ trail = [], leaf = '', right }).
Content (content.js): Card, PanelFromItems({ heading, items = [], keyPrefix = 'i', count, style, kind, emptyText }), HeroFromPageData(hero), Marquee({ items = [], sep = '/' }), CliBlock({ lines = [], heading = 'quick start', className = '' }), Spinner({ size = 'base', tone = 'accent', label = 'loading', key }), Skeleton({ height = '1em', width = '100%', count = 1, label = 'loading content', key }), Alert({ kind = 'info', children, onDismiss, title, key }), FilterPills({ options = [], selected, onSelect, label = 'filters' }).
Editor primitives (editor-primitives.js): Drawer({ side = 'left', open = false, onClose, children, ariaLabel }), Dialog({ title, open = false, onClose, children, actions = [], dismissible = false, ariaLabel }), FocusTrap({ children }), Toast({ message, kind = 'info', duration = 3000, onClose }) / toast({ message, kind = 'info', duration = 3000 }) (imperative fire-and-forget variant), ContextMenu({ items = [], anchor = { x: 0, y: 0 }, onClose }) / useContextMenu(targetEl, items, openCb), ResizeHandle({ axis = 'horizontal', onResize, ariaLabel }), SplitPanel({ orientation = 'horizontal', initial = '50%', min = 80, max = Infinity, children }), useMediaQuery(query), BP_SM/BP_MD/BP_LG/BP_XL (480/768/1024/1440 breakpoint constants).
Files (files.js): FileRow({ name, type = 'other', size, modified, code, onOpen, onAction, active, key, permissions, locked, selected, onToggleSelect, ... }) (see files-modals section above for the full multi-select contract), FileIcon({ type = 'other' }), FileSkeleton({ rows = 12 }), EmptyState({ text = 'nothing here', glyph = Icon('circle'), action }), BreadcrumbPath({ segments = [], onNav, root = 'root' }), sortFiles(files = [], sort = 'name', dir = 'asc'), fileGlyph(type).
Files modals (files-modals.js): ConfirmDialog({ title = 'Are you sure?', message, confirmLabel = 'confirm', cancelLabel = 'cancel', destructive, onConfirm, onCancel, error, busy = false, busyLabel = 'working…' }), PromptDialog({ title = 'Enter a name', value = '', placeholder = '', confirmLabel = 'ok', cancelLabel = 'cancel', onConfirm, onCancel, onInput, error, busy = false, busyLabel = 'working…' }), FileViewer({ file, body, onClose, onAction, onPrev, onNext }), FilePreviewMedia({ src, type = 'other', name }), FilePreviewCode({ content = '', lang, filename }), FilePreviewText({ content = '', truncated }).
Community (community.js): MobileHeader({ title, channelType, channelName, onMenu, onMembers }), ReplyBar({ quotedMessage, quotedAuthor, onCancel }), Banner({ tone = 'info', message, visible, actionLabel, onAction, onClick }), ThreadPanel({ threads = [], activeId = null, title = 'Threads', onSelect, onCreate, onClose, loading = false }), ForumView({ posts = [], onSearch, onSort, onSelect, onNewPost, loading = false }), PageView({ title = '', html = '', isAdmin = false, onEdit }).
Format byte size to human-readable string.
fmtBytes(n: number): stringExample:
fmtBytes(1024) // '1.0 KB'
fmtBytes(1048576) // '1.0 MB'Render inline markdown subset (bold, italic, code, links).
renderInline(text: string): ReactNode[]Status: Backward compatible (v0.0.127+)
Old API:
Btn({ primary: true, children: 'Save' })
Btn({ ghost: true, children: 'Cancel' })
Btn({ children: 'Default' })New API:
Btn({ variant: 'primary', children: 'Save' })
Btn({ variant: 'ghost', children: 'Cancel' })
Btn({ variant: 'default', children: 'Default' })Migration Path:
- Old
primaryandghostprops still work (resolved silently;variantwins when both are present) - Update gradually per codebase
- Full removal planned for v1.0
Status: Backward compatible (v0.0.127+)
Old API:
Row({ title: 'item', active: true })New API:
Row({ title: 'item', state: 'active' })Migration Path:
active: true->state: 'active'active: false->state: 'default'selected: true->state: 'active'
Status: Backward compatible (v0.0.127+)
Old API:
ChatMessage({ who: 'you', text: 'hello' })
ChatMessage({ who: 'them', text: 'hi' })New API:
ChatMessage({ role: 'user', text: 'hello' })
ChatMessage({ role: 'assistant', text: 'hi' })Migration Path:
who: 'you'->role: 'user'who: 'them'->role: 'assistant'- Both still supported;
roletakes precedence if both provided
Status: Backward compatible (v0.0.127+)
Internal logic:
hasKidscomputed fromchildrenpresence- Optional
hasChildrenprop available for explicit control - No breaking changes needed (internal only)
The real class taxonomy (enforced by scripts/lint-classes.mjs at build time):
ds-*— scoped primitives (rows, grids, dialogs, dashboards, previews)app-*— AppShell chrome (topbar, status bar, crumb)ws-*— WorkspaceShell chrome (rail, sessions, content, pane, drawers, scrim)chat-*/agentchat-*/aicat-*— chat surfacescm-*— community surfaces;ov-*— overlay primitives;vx-*— voice;fd-*— freddie- Public utility classes (intentional, stable API):
.btn,.btn-primary,.btn-ghost,.row,.panel,.seg,.crumb,.glyph,.status-dot-disc - Legacy bare internals (frozen — always styled under a prefixed parent; do
NOT add new ones, and avoid global consumer rules on these words inside the
mount root):
name,size,icon,count,sep,leaf,cap,thumb,desc,tick,meta,sub,title,spread, and the rest of the allowlist inscripts/lint-classes.mjs.
State modifiers use is-* / .active / .show; rail tones use rail-*; badge tones use tone-*.
scripts/build.mjs concatenates the root CSS files into the scoped dist/247420.css:
colors_and_type.css (tokens) -> app-shell.css -> community.css -> chat.css ->
editor-primitives.css -> community-app.css -> app-surfaces.css -> the spoint kit sheets.
app-surfaces.css holds consumer-app application-surface styling (the agentgui pills, cwd bar,
resume banner, health chips, settings grid, history empty state, boot splash, scrollbar theming,
focus rings, print) so a consuming app keeps NO design content of its own; its selectors are
written pre-scoped .ds-247420 ... (the build prefixer leaves already-scoped selectors untouched).
lint-glyphs scans these root CSS files (via SCAN_ROOT_FILES) in addition to src/, so a
decorative glyph in shipped CSS fails the build.
- Unified focus tokens (colors_and_type.css):
--focus-color/--focus-w/--focus-offsetfor outset rings, and--focus-ring-insetfor bordered text fields. Keyboard rings use:focus-visible(never bare:focus, so a mouse click draws no ring). .ds-check-box— a CSS-drawn multi-select checkbox (bordered box, accent fill + border-drawn tick on.is-markedor[aria-checked="true"], dash on[aria-checked="mixed"]). Used by FileRow/FileCell, the file/session select-all, and SessionCard select. Replaces[x]/[ ]text; AT keeps therole=checkbox+aria-checkedname/state.- Dashboard card status rails (chat.css):
is-error(flame inset, strongest) >is-stale(amber inset) >is-active/is-new(accent inset). Tool-card status pills:tool-running(accent) /tool-error(flame) /tool-done(success).Row()rail tones differentiate by SHAPE (taller bar = error, gapped fill = subagent) plus an sr-only status word, not hue alone.
PTT/VAD/webcam/voice-settings/queue primitives for a voice-enabled chat surface. Class prefix vx-*. Pure factories, no transport — the host wires media streams and device enumeration.
Push-to-talk button; hold-to-talk (pointer/touch) or click, depending on mode.
| Prop | Type | Description |
|---|---|---|
state |
'idle'|'live'|'recording'|'vad' |
Visual/announced state; anything but idle is treated as active. |
mode |
'ptt'|'vad'|'live' |
Drives the vx-ptt-mode-* class only (interaction wiring is the same). |
onHoldStart |
(e) => void |
Fired on pointerdown/touchstart. |
onHoldEnd |
(e) => void |
Fired on pointerup/pointerleave/touchend. |
onClick |
(e) => void |
Fired on click (for non-hold flows). |
label |
string |
Button label + aria-label (default 'Hold to talk'). |
Voice-activity level meter with a draggable threshold marker.
| Prop | Type | Description |
|---|---|---|
level |
number 0-1 |
Current input level (clamped). |
threshold |
number 0-1 |
VAD trigger threshold (clamped); levels >= threshold render the "over" fill state. |
onThresholdChange |
(value: number) => void |
Fired from the underlying <input type=range>. |
Live video preview with resolution/fps selects and an enable/disable toggle.
| Prop | Type | Description |
|---|---|---|
videoStream |
MediaStream|null |
Bound to the <video> srcObject via ref. |
resolution |
string |
Current resolution value (e.g. '640x480'), shown in the select. |
fps |
number |
Current frame rate, shown in the select. |
enabled |
bool |
When false, shows a "Camera off" placeholder instead of <video>. |
resolutions |
string[] |
Options for the resolution select; falls back to [resolution]. |
fpsOptions |
number[] |
Options for the fps select; falls back to [fps]. |
onResolutionChange |
(value: string) => void |
|
onFpsChange |
(value: number) => void |
|
onToggle |
() => void |
Enable/Disable button. |
Modal dialog for voice mode, devices, VAD threshold, processing toggles, bitrate, and volume. Returns null when open is false.
| Prop | Type | Description |
|---|---|---|
open |
bool |
Gate; component renders nothing when false. |
mode |
'ptt'|'vad'|'live' |
Current mode segmented control selection. |
inputId / outputId |
string |
Selected input/output device id. |
inputDevices / outputDevices |
[{value, label}] |
Device select options. |
vadThreshold |
number 0-1 |
Only shown when mode === 'vad'. |
rnnoise / autoGain / forceTurn |
bool |
Processing toggle rows. |
bitrate |
number |
kbps range (8-256). |
volume |
number 0-1 |
Master volume range; defaults to 1 when null. |
onChange |
(patch: object) => void |
Fired with a partial-state patch from every control. |
onSave / onCancel / onClose |
() => void |
Footer/close actions; onClose also fires on backdrop click and Escape. |
Toolbar of call controls: mic, deafen, camera, screen share, settings, leave.
| Prop | Type | Description |
|---|---|---|
muted / deafened / cameraOn / screenShareOn |
bool |
Toggle states driving each button's pressed/on class. |
onMic / onDeafen / onCamera / onScreenShare / onSettings |
(e) => void |
A button is disabled when its handler is not supplied. |
onLeave |
(e) => void |
Leave-voice button. |
Horizontal strip of queued/replayable audio segments with pause/resume/skip transport controls. Renders an empty state ("No audio queued") when segments is empty.
| Prop | Type | Description |
|---|---|---|
segments |
[{id, speaker, duration, color, isLive}] |
Queue items rendered as chips; isLive shows a LIVE tag instead of a duration. |
currentSegmentId |
string |
Highlights the matching chip as current. |
paused |
bool |
Drives the transport button's play/pause icon and label. |
onReplay |
(id) => void |
Fired on chip click. |
onSkip |
() => void |
Skip-forward button. |
onResume / onPause |
() => void |
Transport button, dispatched based on paused. |
Segmented auto/paper/ink theme switch bound to src/theme.js; reads current mode via getTheme() and applies changes via applyTheme() (which persists, sets <html data-theme>, and notifies listeners).
ThemeToggle({ compact = false, onChange } = {})
| Prop | Type | Description |
|---|---|---|
compact |
bool |
false (default) renders a 3-way role=radiogroup segmented control (auto/paper/ink). true renders a single icon-only cycling button (auto -> paper -> ink -> auto) with a CSS-drawn half-disc glyph and a live label, for icon-only rail contexts. |
onChange |
(mode: 'auto'|'paper'|'ink') => void |
Called after applyTheme(), in addition to the theme-change side effect. |
The compact variant auto-refreshes its glyph when the OS theme changes while in auto mode (subscribes to onThemeChange once per module).
Dense observability/dashboard primitives (ported from the gmsniff GUI): phase-progress, tree/timeline entries, bar charts, KPI tiles, sub-nav grids, session rows, deviation callouts, and a live log stream. All theme-aware (colors ride var(--token), never raw hex). CSS lives in app-shell.css under "data density" (ds- prefix).
Compact horizontal phase-progress indicator.
PhaseWalk({ phases = DEFAULT_PHASES, reached = [], gapKinds = [] }) — DEFAULT_PHASES is ['PLAN','EXECUTE','EMIT','VERIFY','CONSOLIDATE','COMPLETE']. reached[i] (bool) marks a phase already hit; gapKinds names phases that are a known gap (rendered red, overrides reached).
Indented timeline/tree entry with left-border variant coloring.
TreeNode({ ts, kind, variant = '', phase, id, keyLabel, reason, deviationLabel, residuals }) — variant is one of ''|'phase'|'deviation'|'mutable-resolve'|'prd-add'; phase/id/keyLabel render as pills; residuals (string array) is joined with , when present.
Inline horizontal bar-chart row (label + track + value).
BarRow({ label, value, pct = 0, tone }) — pct is clamped to 0-100; tone is a CSS color value (var(--token) or color-mix(...)), never a bare hex string.
Compact KPI tile and its grid wrapper (denser than .kpi).
StatTile({ val, lbl, cls = '' }) — cls selects an accent variant: ''|'rate-big'|'err-rate'.
StatsGrid({ items = [] }) — renders items.map(StatTile); shows an empty state ("no stats") when items is empty.
Small button grid: big number + label, for category navigation.
SubGrid({ items: [{ key, count, label, onClick }] }) — renders an empty state ("no items") when items is empty.
Compact single-line session summary row with an inline phase strip.
SessionRow({ sessId, phaseWalkProps, events, verbs, prd, muts, resid, deviations, firstTs, lastTs, onClick }) — events/verbs/prd/muts/resid are counts joined middot-separated; phaseWalkProps is forwarded to PhaseWalk; onClick makes the row a keyboard-activatable button (role=button, Enter/Space).
Deviation/error callout row. Uses the danger-surface token, never a bare hex background.
DevRow({ ts, event, sess, operation, residuals }) — sess/operation render as pills; residuals (string array) joined with , .
Scrollable dense log stream: colored subsystem tag + bold event name + muted payload preview.
LiveLogEntry({ ts, sub, tone, event, preview }) — tone is a CSS color value; the tag background derives from it via color-mix at render time (no hex-alpha-suffix hack).
LiveLog({ entries = [], autoScroll = true }) — renders an empty state ("no log entries") when entries is empty; autoScroll pins scrollTop to scrollHeight on mount/update via ref.
Non-component function exports useful to hosts wiring the kit's chat/session/file surfaces.
fmtDuration(ms)(sessions.js) — one duration format for every surface (live cards, running panel, session meta, context pane):<60s -> 'Ns',<1h -> 'Nm Ss', else'Nh Nm'. Returns''for null/non-finite/negative input.fmtFileSize(bytes)(files.js) — canonical byte formatter (chat.jsre-exports it asfmtBytes). Returns'—'fornullbytes,'0 B'for zero, else'N.N UNIT'scaled through B/KB/MB/GB/TB.safeUrl(url)(chat.js) — sanitizes a URL for use in an inline (non-DOMPurify-passed) markdown link or image src. Allows relative/anchor/protocol-relative links andhttp(s)/mailto/telschemes; rejects everything else (e.g.javascript:,data:,vbscript:,file:) by returningnull.makeThreadAutoScroll(getCount)(chat.js) — returns a ref callback that pins a scroll container to the bottom when new messages arrive and the user is already at the bottom (via anIntersectionObserveron a bottom sentinel).getCountis a function returning the current message count. Auto-scroll pauses whilehasSelectionInsidereports an active text selection inside the container, and resumes once it collapses. Shared byChat,AICat, andAgentChat.injectCodeCopy(container)(chat.js) — walks every<pre>inside a rendered-markdown container and wraps it with a hover-revealed per-block "copy" button (plus a language tab when the highlighter set alanguage-xx/lang-xxclass). Idempotent via adata-copy-wiredmarker so re-renders don't stack buttons; the button label flips to "copied" for ~1.6s after a successful clipboard write (falls back to a hidden-textareaexecCommand('copy')when the Clipboard API is unavailable).hasSelectionInside(el)(chat.js) — returnstruewhen the document has a non-collapsed text selection anchored insideel. Used to pause auto-scroll and streaming re-renders so a user's select-and-copy from a still-streaming message isn't wiped every frame.
All components include ARIA attributes where applicable:
- Buttons:
role="button",aria-label,aria-pressed - Navigation:
aria-current="page",aria-label - Trees:
role="treeitem",aria-selected,aria-expanded - Tabs:
role="tablist",role="tab",aria-selected - Chat:
role="log",aria-live="polite"
- v0.0.127 (2026-05-21): Standardized prop naming with backward compatibility
- Btn: Added
variantprop (oldprimary/ghoststill work) - Row: Added
stateprop (oldactive/selectedstill work) - ChatMessage: Added
roleprop (oldwhostill works) - TreeItem: Added optional
hasChildrenprop - Full accessibility audit complete
- Btn: Added