@llui/components
Current package version: 0.16.0
66 headless UI components for LLui. Pure state machines with no DOM opinions -- you own the markup and styling via data-scope / data-part attributes. Component state is JSON-serializable: numeric inputs reject non-finite runtime values, use finite-or-absent bounds, and validate positive divisors such as pagination pageSize and numeric crop aspectRatio.
Install
pnpm add @llui/components @llui/dom @llui/interactions
@llui/dom and @llui/interactions are peer dependencies. Resolve one instance of each across the application and its libraries so signal state and the focus/dismissal/nested-layer registries are shared.
Usage
Each component exports init, update, connect, and a barrel object:
import { component, div, button, text } from '@llui/dom'
import { tabs } from '@llui/components/tabs'
type State = { tabs: tabs.TabsState }
type Msg = { type: 'tabs'; msg: tabs.TabsMsg }
const App = component<State, Msg>({
name: 'App',
init: () => ({ tabs: tabs.init({ items: ['a', 'b', 'c'], value: 'a' }) }),
update: (s, m) => {
const [t] = tabs.update(s.tabs, m.msg)
return [{ tabs: t }, []]
},
// `connect` takes the sliced SIGNAL handle (`state.at('tabs')`), not an accessor.
view: ({ state, send }) => {
const t = tabs.connect(state.at('tabs'), (m) => send({ type: 'tabs', msg: m }), { id: 'demo' })
return [
div({ ...t.root }, [
div({ ...t.list }, [
button({ ...t.item('a').trigger }, [text('Tab A')]),
button({ ...t.item('b').trigger }, [text('Tab B')]),
button({ ...t.item('c').trigger }, [text('Tab C')]),
]),
div({ ...t.item('a').panel }, [text('Content A')]),
div({ ...t.item('b').panel }, [text('Content B')]),
div({ ...t.item('c').panel }, [text('Content C')]),
]),
]
},
})
Pattern
init(opts?)-- creates the initial stateupdate(state, msg)-- pure reducer, returns[newState, effects[]]connect(state, send, opts?)-- takes the slicedSignalhandle; returns parts objects with reactive props, ARIA attributes, and event handlers. Spread parts onto your elements:div({ ...parts.root }, [...])- Overlay helpers (dialog, popover, menu, etc.) --
overlay()wires up portals, focus traps, dismiss layers, and positioning
Composition: delegated update
The parent owns the component's state as a slice and routes its messages through the
parent's Msg union — a flat switch, no special combinator. connect(state, send, opts?)
takes the sliced signal handle and returns spreadable, signal-based parts.
import { tabs } from '@llui/components/tabs'
import { div } from '@llui/dom'
type State = { tabs: tabs.TabsState /* … */ }
type Msg = { type: 'tabs'; msg: tabs.TabsMsg } /* | … */
update: (state, msg) => {
switch (msg.type) {
case 'tabs':
return [{ ...state, tabs: tabs.update(state.tabs, msg.msg)[0] }, []]
// … other slices
}
}
// view — connect() takes the sliced signal and a routed `send`:
view: ({ state, send }) => {
const parts = tabs.connect(state.at('tabs'), (m) => send({ type: 'tabs', msg: m }))
return [
div({ ...parts.root }, [
/* … */
]),
]
}
Components (66)
Form controls
accordion, checkbox, collapsible, editable, field, fieldset, form, number-input, password-input, pin-input, radio-group, rating-group, search-field, slider, switch, tabs, tags-input, theme-switch, toggle, toggle-group
Overlays
alert-dialog, combobox, context-menu, dialog, drawer, hover-card, menu, menubar, navigation-menu, popover, select, toast, tooltip, tour
Data display
async-list, avatar, breadcrumbs, carousel, cascade-select, clipboard, in-view, listbox, meter, pagination, progress, qr-code, scroll-area, sortable, steps, table, toc, toolbar, tree-view
Pickers
color-picker, date-input, date-picker, time-picker, angle-slider
Media / canvas
file-upload, floating-panel, image-cropper, marquee, presence, signature-pad, splitter, timer
Patterns
@llui/components/patterns/confirm-dialog -- pre-wired alert-dialog for destructive confirmations.
Utilities
Shared helpers used internally and exported for advanced use:
| Utility | Purpose |
|---|---|
typeahead |
First-letter search across menu, select, listbox, tree-view |
TreeCollection |
Indexed tree traversal -- visibleItems, labels, indeterminate computation |
floating |
@floating-ui/dom wrapper for popover/menu positioning |
focus-trap |
Stack-based focus containment for modals |
dismissable |
Esc / outside-click dismiss layer stack |
aria-hidden |
aria-hidden on siblings of a modal for screen readers |
remove-scroll |
Body scroll lock for modals/drawers |
Styling (opt-in)
Components are fully headless by default. An opt-in styling layer provides two complementary mechanisms:
CSS theme -- theme.css
Import once at your app root for a complete default look based on data-scope/data-part attribute selectors:
import '@llui/components/styles/theme.css'
Includes design tokens (@theme) and enter/exit animations for overlays. Override any token in your own CSS:
@theme {
--color-primary: #8b5cf6;
--radius-lg: 1rem;
}
For dark mode, import the separate dark theme file after Tailwind and theme.css:
import '@llui/components/styles/theme-dark.css'
This activates automatically via prefers-color-scheme: dark. Force light with <html data-theme="light">, force dark with <html data-theme="dark">. The dark file is separate because Tailwind 4's @theme scanner would otherwise merge dark tokens into the root theme.
JS class helpers -- Tailwind utility strings
Each component has a class helper that returns Tailwind utility strings per part, with size/variant props:
import { tabsClasses } from '@llui/components/styles/tabs'
const cls = tabsClasses({ size: 'sm', variant: 'pill' })
// cls.root, cls.list, cls.trigger, cls.panel, cls.indicator
div({ ...t.root, class: cls.root }, [
div({ ...t.list, class: cls.list }, [
button({ ...t.item('a').trigger, class: cls.trigger }, [text('Tab A')]),
]),
div({ ...t.item('a').panel, class: cls.panel }, [text('Content A')]),
])
Or import everything from the barrel:
import { tabsClasses, dialogClasses, cx } from '@llui/components/styles'
Variant engine
The createVariants utility powers all class helpers and is exported for custom components:
import { createVariants, cx } from '@llui/components/styles'
const button = createVariants({
base: 'inline-flex items-center font-medium',
variants: {
size: { sm: 'px-2 py-1 text-sm', md: 'px-4 py-2' },
intent: { primary: 'bg-primary text-white', ghost: 'bg-transparent' },
},
defaultVariants: { size: 'md', intent: 'primary' },
compoundVariants: [{ size: 'sm', intent: 'ghost', class: 'font-normal' }],
})
button({ size: 'sm', intent: 'ghost' }) // -> class string
Sub-path imports
Every component has its own entry point for tree-shaking:
import { tabs } from '@llui/components/tabs'
import { dialog } from '@llui/components/dialog'
import { timer } from '@llui/components/timer'
Validation
Input components accept an optional validate callback on ConnectOptions that gates state changes:
const parts = editable.connect<S>(get, send, {
validate: (value) => {
if (value.length < 3) return ['Too short']
return null // valid
},
})
Supported on: editable, number-input, tags-input, pin-input, file-upload.
Component Reference
All 66 components follow the same pattern:
import { componentName } from '@llui/components/component-name'
// State machine
const state = componentName.init({ /* options */ })
const [newState, effects] = componentName.update(state, msg)
// Connect to DOM
const parts = componentName.connect(state.at('component'), send, { id: '...' })
// Use parts: div({ ...parts.root }, [button({ ...parts.trigger }, [...])])
Accordion
State (AccordionState):
| Field | Type |
|---|---|
value |
string[] |
multiple |
boolean |
collapsible |
boolean |
disabled |
boolean |
items |
string[] |
Messages: toggle, open, close, setValue, setItems, focusNext, focusPrev, focusFirst, focusLast
Init options: value?: string[], multiple?: boolean, collapsible?: boolean, disabled?: boolean, items?: string[]
Connect options: ConnectOptions
Parts: root, item
Utilities: focusTarget()
Alert Dialog
State: AlertDialogState (see parent component)
Connect options: AlertDialogConnectOptions
Utilities: overlay(), isMounted(), isPresent()
Angle Slider
State (AngleSliderState):
| Field | Type |
|---|---|
value |
number |
min |
number |
max |
number |
step |
number |
disabled |
boolean |
readonly |
boolean |
dir |
'ltr' | 'rtl' |
Messages: setValue, increment, decrement, setMin, setMax, setDir
Init options: value?: number, min?: number, max?: number, step?: number, disabled?: boolean, readonly?: boolean, dir?: 'ltr' | 'rtl'
Connect options: ConnectOptions
Parts: root, control, thumb, valueText, hiddenInput
Utilities: angleFromPoint(), pointFromAngle()
Async List
State (AsyncListState):
| Field | Type |
|---|---|
items |
T[] |
page |
number |
hasMore |
boolean |
status |
AsyncStatus |
error |
string | null |
Messages: loadMore, pageLoaded, pageFailed, reset, setItems, retry
Init options: items?: T[], page?: number, hasMore?: boolean
Parts: root, sentinel, loadMoreTrigger, retryTrigger, errorText
Utilities: isLoading(), isError(), isEmpty(), watchSentinel()
Avatar
State (AvatarState):
| Field | Type |
|---|---|
status |
ImageStatus |
Messages: loadStart, loaded, error, reset
Init options: status?: ImageStatus
Connect options: ConnectOptions
Parts: root, image, fallback
Breadcrumbs
State (BreadcrumbsState):
| Field | Type |
|---|---|
items |
BreadcrumbItem[] |
maxVisible |
number | null |
expanded |
boolean |
Messages: setItems, expand, collapse
Init options: items?: BreadcrumbItem[], maxVisible?: number | null, expanded?: boolean
Connect options: ConnectOptions
Parts: root, list, item, link, separator, ellipsisTrigger
Utilities: visibleItems()
Carousel
State (CarouselState):
| Field | Type |
|---|---|
current |
number |
count |
number |
loop |
boolean |
autoplay |
boolean |
interval |
number |
paused |
boolean |
direction |
'forward' | 'backward' |
swipeThreshold |
number |
dragging |
CarouselDrag | null |
dir |
'ltr' | 'rtl' |
Messages: goTo, next, prev, setCount, pause, resume, setAutoplay, autoplayTick, dragStart, dragMove, dragEnd, setDir
Init options: current?: number, count?: number, loop?: boolean, autoplay?: boolean, interval?: number, swipeThreshold?: number, dir?: 'ltr' | 'rtl'
Connect options: ConnectOptions
Parts: root, viewport, indicatorGroup, nextTrigger, prevTrigger, slide
Utilities: canGoNext(), canGoPrev(), swipeDecision(), isAutoplayRunning(), autoplayEffects()
Cascade Select
State (CascadeSelectState):
| Field | Type |
|---|---|
levels |
CascadeLevel[] |
values |
(string | null)[] |
disabled |
boolean |
Messages: setLevels, setValue, clear
Init options: levels?: CascadeLevel[], values?: (string | null)[], disabled?: boolean
Connect options: ConnectOptions
Parts: root, clearTrigger, level
Utilities: isLevelReady(), isComplete(), completeValues()
Checkbox
State (CheckboxState):
| Field | Type |
|---|---|
checked |
CheckedState |
disabled |
boolean |
required |
boolean |
Messages: toggle, setChecked, setDisabled
Init options: checked?: CheckedState, disabled?: boolean, required?: boolean
Parts: root, hiddenInput, indicator
Clipboard
State (ClipboardState):
| Field | Type |
|---|---|
value |
string |
copied |
boolean |
Messages: setValue, copy, copied, reset
Init options: value?: string
Connect options: ConnectOptions
Parts: root, trigger, input, indicator
Utilities: copyToClipboard()
Collapsible
State (CollapsibleState):
| Field | Type |
|---|---|
open |
boolean |
disabled |
boolean |
Messages: toggle, open, close, setOpen
Init options: open?: boolean, disabled?: boolean
Connect options: ConnectOptions
Parts: root, trigger, content
Color Picker
State (ColorPickerState):
| Field | Type |
|---|---|
hsv |
Hsv |
alpha |
number |
disabled |
boolean |
Messages: setHsl, setHue, setSaturation, setLightness, setAlpha, setHex, setSv, nudgeSv, setColor
Init options: hsl?: Hsl, hsv?: Hsv, alpha?: number, disabled?: boolean
Connect options: ConnectOptions
Parts: root, hueSlider, saturationSlider, lightnessSlider, hexInput, preview, area, areaThumb, alphaSlider, swatchGroup, swatch
Utilities: stateHsl(), toHex(), toHex8(), hexToHsl(), hslToRgb(), hslToHsv(), hsvToHsl(), colorFromPoint(), parseColor()
Combobox
State (ComboboxState):
| Field | Type |
|---|---|
open |
boolean |
value |
string[] |
inputValue |
string |
items |
string[] |
groups |
ComboboxGroup[] |
disabledItems |
string[] |
filteredItems |
string[] |
highlightedValue |
string | null |
selectionMode |
SelectionMode |
disabled |
boolean |
allowCreate |
boolean |
status |
AsyncStatus |
requestId |
number |
error |
string | null |
Messages: open, close, setInputValue, selectOption, setValue, clear, highlightNext, highlightPrev, highlightFirst, highlightLast, highlight, selectHighlighted, setItems, loadStart, loadSuccess, loadError
Init options: value?: string[], inputValue?: string, items?: string[], groups?: ComboboxGroup[], disabledItems?: string[], selectionMode?: SelectionMode, disabled?: boolean, allowCreate?: boolean
Connect options: ConnectOptions
Parts: root, input, trigger, positioner, content, item, group, liveRegion, empty
Utilities: overlay(), isCreateOption()
Constants: CREATE_OPTION_VALUE
Context Menu
State (ContextMenuState):
| Field | Type |
|---|---|
x |
number |
y |
number |
Messages: openAt, close, highlight, highlightNext, highlightPrev, highlightFirst, highlightLast, selectHighlighted, select, openSub, closeSub, setItems, typeahead, setDir, animationEnd
Init options: items?: ContextMenuItem[], checked?: string[], closeOnSelect?: boolean, dir?: TextDirection | null, skipAnimations?: boolean
Connect options: ConnectOptions
Parts: trigger, positioner, content, item, checkboxItem, radioItem, group, separator, subTrigger, subPositioner, subContent
Utilities: overlay(), isPresent(), isMounted()
Date Input
State (DateInputState):
| Field | Type |
|---|---|
input |
string |
value |
IsoDate | null |
min |
IsoDate | null |
max |
IsoDate | null |
error |
DateError |
disabled |
boolean |
readonly |
boolean |
required |
boolean |
Messages: setInput, setValue, clear, setMin, setMax, setDisabled
Init options: input?: string, value?: IsoDate | null, min?: IsoDate | null, max?: IsoDate | null, disabled?: boolean, readonly?: boolean, required?: boolean
Connect options: ConnectOptions
Parts: root, input, clearTrigger, errorText
Utilities: parseDate(), formatDate(), toIsoDate()
Date Picker
State (DatePickerState):
| Field | Type |
|---|---|
mode |
DatePickerMode |
value |
string | null |
start |
string | null |
end |
string | null |
hoverDate |
string | null |
visibleMonth |
number |
visibleYear |
number |
months |
number |
focused |
string |
min |
string | null |
max |
string | null |
weekStartsOn |
0 | 1 |
disabled |
boolean |
Messages: setValue, setRange, setFocused, setHover, clearHover, prevMonth, nextMonth, prevYear, nextYear, selectFocused, moveFocus, focusStartOfWeek, focusEndOfWeek, focusToday, clear
Init options: mode?: DatePickerMode, value?: string | null, start?: string | null, end?: string | null, visibleMonth?: number, visibleYear?: number, months?: number, min?: string | null, max?: string | null, weekStartsOn?: 0 | 1, disabled?: boolean
Connect options: ConnectOptions
Parts: root, grid, row, prevMonthTrigger, nextMonthTrigger, dayCell, preset
Utilities: monthGrid(), weekRows(), monthLabel(), weekdayLabels()
Dialog
State (DialogState):
| Field | Type |
|---|---|
open |
boolean |
status |
PresenceStatus |
skipAnimations |
boolean |
Messages: open, close, toggle, setOpen, animationEnd, transitionEnd
Init options: open?: boolean, skipAnimations?: boolean
Connect options: ConnectOptions
Parts: trigger, backdrop, positioner, content, title, description, closeTrigger
Utilities: overlay(), isMounted(), isPresent()
Drawer
State (DrawerState):
| Field | Type |
|---|---|
open |
boolean |
status |
PresenceStatus |
skipAnimations |
boolean |
Messages: open, close, toggle, setOpen, animationEnd, transitionEnd
Init options: open?: boolean, skipAnimations?: boolean
Connect options: ConnectOptions
Parts: trigger, backdrop, positioner, content, title, description, closeTrigger
Utilities: overlay(), isMounted(), isPresent()
Editable
State (EditableState):
| Field | Type |
|---|---|
value |
string |
editing |
boolean |
draft |
string |
disabled |
boolean |
Messages: edit, setDraft, submit, cancel, setValue
Init options: value?: string, editing?: boolean, disabled?: boolean
Connect options: ConnectOptions
Parts: root, preview, input, submitTrigger, cancelTrigger, editTrigger
Field
State (FieldState):
| Field | Type |
|---|---|
id |
string |
invalid |
boolean |
required |
boolean |
disabled |
boolean |
readonly |
boolean |
touched |
boolean |
Messages: setInvalid, setRequired, setDisabled, setReadonly, setTouched
Init options: id: string, invalid?: boolean, required?: boolean, disabled?: boolean, readonly?: boolean, touched?: boolean
Connect options: FieldConnectOptions
Parts: root, label, control, description, errorText
Fieldset
State (FieldsetState):
| Field | Type |
|---|---|
id |
string |
disabled |
boolean |
invalid |
boolean |
Messages: setDisabled, setInvalid
Init options: id: string, disabled?: boolean, invalid?: boolean
Connect options: FieldsetConnectOptions
Parts: root, legend, errorText
File Upload
State (FileUploadState):
| Field | Type |
|---|---|
files |
FileMeta[] |
rejectedFiles |
RejectedFile[] |
disabled |
boolean |
multiple |
boolean |
accept |
AcceptValue |
maxFiles |
number |
maxSize |
number |
minFileSize |
number |
required |
boolean |
readonly |
boolean |
invalid |
boolean |
dragging |
boolean |
dragDepth |
number |
Messages: setFiles, addFiles, removeFile, removeRejected, clear, clearRejected, dragEnter, dragLeave, drop, setInvalid
Init options: files?: FileMeta[], disabled?: boolean, multiple?: boolean, accept?: AcceptValue, maxFiles?: number, maxSize?: number, minFileSize?: number, required?: boolean, readonly?: boolean, invalid?: boolean
Connect options: ConnectOptions
Parts: root, dropzone, trigger, hiddenInput, label, clearTrigger, itemGroup, item
Utilities: totalSize(), acceptToString(), fileMatchesAccept(), validateFiles(), preventDocumentDrop(), trackFile(), trackFiles(), getFile(), releaseFile(), releaseFiles(), releaseAllFiles(), trackedFileCount(), releaseDropped(), releaseUnlanded(), effectiveMaxFiles()
Floating Panel
State (FloatingPanelState):
| Field | Type |
|---|---|
position |
{ x: number; y: number } |
size |
{ width: number; height: number } |
minSize |
{ width: number; height: number } |
maxSize |
{ width?: number; height?: number } | null |
open |
boolean |
minimized |
boolean |
maximized |
boolean |
dragging |
boolean |
resizing |
ResizeHandle | null |
restoreBounds |
{ x: number; y: number; width: number; height: number } | null |
disabled |
boolean |
Messages: open, close, minimize, restoreFromMinimized, maximize, restoreFromMaximized, toggleMinimize, toggleMaximize, dragStart, dragMove, dragEnd, resizeStart, resizeMove, resizeEnd, setPosition, setSize
Init options: position?: { x: number; y: number }, size?: { width: number; height: number }, minSize?: { width?: number; height?: number }, maxSize?: { width?: number; height?: number } | null, open?: boolean, disabled?: boolean
Connect options: ConnectOptions
Parts: root, dragHandle, content, minimizeTrigger, maximizeTrigger, closeTrigger, resizeHandle
Form
State (FormState):
| Field | Type |
|---|---|
status |
FormStatus |
touched |
Record<string, boolean> |
submitError |
string | null |
Messages: touch, touchAll, submit, submitSuccess, submitError, reset
Connect options: ConnectOptions
Parts: root, field, submit
Utilities: validateSchema(), validateSchemaAsync()
Hover Card
State (HoverCardState):
| Field | Type |
|---|---|
open |
boolean |
status |
PresenceStatus |
skipAnimations |
boolean |
Messages: show, hide, setOpen, animationEnd, transitionEnd
Init options: open?: boolean, skipAnimations?: boolean
Connect options: ConnectOptions
Parts: trigger, positioner, content, arrow
Utilities: overlay(), isMounted(), isPresent()
Image Cropper
State (ImageCropperState):
| Field | Type |
|---|---|
image |
{ width: number; height: number } |
crop |
CropRect |
aspectRatio |
number | null |
minSize |
number |
dragging |
boolean |
resizing |
ResizeHandle | null |
disabled |
boolean |
Messages: setImage, setCrop, setAspectRatio, dragStart, dragMove, dragEnd, resizeStart, resizeMove, resizeEnd, reset, centerFill
Init options: image?: { width: number; height: number }, crop?: CropRect, aspectRatio?: number | null, minSize?: number, disabled?: boolean
Connect options: ConnectOptions
Parts: root, image, cropBox, resizeHandle, resetTrigger
Utilities: centerFill()
In View
State (InViewState):
| Field | Type |
|---|---|
visible |
boolean |
Messages: enter, leave
Connect options: ConnectOptions
Parts: root
Utilities: createObserver()
Listbox
State (ListboxState):
| Field | Type |
|---|---|
value |
string[] |
items |
string[] |
disabledItems |
string[] |
disabled |
boolean |
selectionMode |
SelectionMode |
highlightedIndex |
number | null |
typeahead |
string |
typeaheadExpiresAt |
number |
Messages: select, setValue, clear, highlight, highlightNext, highlightPrev, highlightFirst, highlightLast, selectHighlighted, setItems, typeahead
Init options: value?: string[], items?: string[], disabledItems?: string[], disabled?: boolean, selectionMode?: SelectionMode
Connect options: ConnectOptions
Parts: root, item
Marquee
State (MarqueeState):
| Field | Type |
|---|---|
running |
boolean |
direction |
MarqueeDirection |
durationSec |
number |
pauseOnHover |
boolean |
hovered |
boolean |
disabled |
boolean |
Messages: play, pause, toggle, hoverPause, hoverResume, setDirection, setDuration
Init options: running?: boolean, direction?: MarqueeDirection, durationSec?: number, pauseOnHover?: boolean, disabled?: boolean
Parts: root, content
Utilities: isRunning(), cssAnimationDirection(), axis()
Menu
State (MenuState):
| Field | Type |
|---|---|
open |
boolean |
status |
PresenceStatus |
skipAnimations |
boolean |
items |
MenuItem[] |
highlights |
Record<string, string | null> |
openPath |
string[] |
checked |
string[] |
closeOnSelect |
boolean |
typeahead |
string |
typeaheadExpiresAt |
number |
dir |
TextDirection | null |
Messages: open, close, toggle, highlight, highlightNext, highlightPrev, highlightFirst, highlightLast, selectHighlighted, select, openSub, closeSub, setItems, typeahead, setDir, animationEnd
Init options: open?: boolean, items?: MenuItem[], highlighted?: string | null, checked?: string[], closeOnSelect?: boolean, dir?: TextDirection | null, skipAnimations?: boolean
Connect options: ConnectOptions
Parts: trigger, positioner, content, item, checkboxItem, radioItem, group, separator, subTrigger, subPositioner, subContent
Utilities: overlay(), isPresent(), isMounted(), floatingDir()
Menubar
State (MenubarState):
| Field | Type |
|---|---|
menus |
string[] |
open |
string | null |
focused |
string | null |
disabledMenus |
string[] |
menuStates |
Record<string, MenuState> |
Messages: openMenu, closeMenu, focusMenu, focusNext, focusPrev, menuMsg
Init options: menus: MenubarMenu[], focused?: string | null
Connect options: ConnectOptions
Parts: root, menuTrigger, menu
Utilities: overlay()
Meter
State (MeterState):
| Field | Type |
|---|---|
value |
number |
min |
number |
max |
number |
low |
number |
high |
number |
optimum |
number |
Messages: setValue, setMax
Init options: value?: number, min?: number, max?: number, low?: number, high?: number, optimum?: number
Connect options: ConnectOptions
Parts: root, track, range, label, valueText
Utilities: percent(), thresholdState()
Navigation Menu
State (NavMenuState):
| Field | Type |
|---|---|
open |
string[] |
focused |
string | null |
items |
string[] |
disabled |
boolean |
dir |
'ltr' | 'rtl' |
Messages: openBranch, closeBranch, toggleBranch, closeAll, focus, setDir, setItems
Init options: open?: string[], focused?: string | null, items?: string[], disabled?: boolean, dir?: 'ltr' | 'rtl'
Connect options: ConnectOptions
Parts: root, item
Utilities: isOpen()
Number Input
State (NumberInputState):
| Field | Type |
|---|---|
value |
number | null |
min |
number |
max |
number |
step |
number |
disabled |
boolean |
readonly |
boolean |
rawText |
string |
Messages: setValue, setRawText, commit, increment, decrement, toMin, toMax, setDisabled
Init options: value?: number | null, min?: number, max?: number, step?: number, disabled?: boolean, readonly?: boolean
Connect options: ConnectOptions
Parts: root, input, increment, decrement
Pagination
State (PaginationState):
| Field | Type |
|---|---|
page |
number |
pageSize |
number |
total |
number |
siblings |
number |
boundaries |
number |
disabled |
boolean |
dir |
TextDirection |
Messages: goTo, next, prev, first, last, setPageSize, setTotal, setDir
Init options: page?: number, pageSize?: number, total?: number, siblings?: number, boundaries?: number, disabled?: boolean, dir?: TextDirection
Connect options: ConnectOptions
Parts: root, prevTrigger, nextTrigger, item, ellipsis
Utilities: totalPages(), pageItems(), onControlKeyDown()
Password Input
State (PasswordInputState):
| Field | Type |
|---|---|
value |
string |
visible |
boolean |
disabled |
boolean |
Messages: setValue, toggleVisibility, setVisible
Init options: value?: string, visible?: boolean, disabled?: boolean
Connect options: ConnectOptions
Parts: root, input, visibilityTrigger
Pin Input
State (PinInputState):
| Field | Type |
|---|---|
values |
string[] |
length |
number |
type |
PinType |
mask |
boolean |
disabled |
boolean |
focusedIndex |
number |
Messages: setValue, setAll, focus, clear, backspace, setDisabled
Init options: length?: number, type?: PinType, mask?: boolean, disabled?: boolean, values?: string[]
Connect options: ConnectOptions
Parts: root, label, input
Utilities: isComplete(), getValue(), acceptedChars()
Popover
State (PopoverState):
| Field | Type |
|---|---|
open |
boolean |
status |
PresenceStatus |
skipAnimations |
boolean |
Messages: open, close, toggle, setOpen, animationEnd, transitionEnd
Init options: open?: boolean, skipAnimations?: boolean
Connect options: ConnectOptions
Parts: trigger, positioner, content, title, description, arrow, closeTrigger
Utilities: overlay(), isMounted(), isPresent()
Presence
State (PresenceState):
| Field | Type |
|---|---|
status |
PresenceStatus |
unmountOnExit |
boolean |
Messages: open, close, toggle, animationEnd, setPresent
Init options: present?: boolean, unmountOnExit?: boolean
Parts: root
Utilities: isMounted(), isVisible(), isAnimating(), presenceOpen(), presenceClose(), presenceEnd()
Progress
State (ProgressState):
| Field | Type |
|---|---|
value |
number | null |
min |
number |
max |
number |
orientation |
ProgressOrientation |
Messages: setValue, setMax
Init options: value?: number | null, min?: number, max?: number, orientation?: ProgressOrientation
Connect options: ConnectOptions
Parts: root, track, range, label, valueText
Utilities: percent(), valueState()
Qr Code
State (QrCodeState):
| Field | Type |
|---|---|
value |
string |
matrix |
boolean[][] |
errorCorrection |
ErrorCorrectionLevel |
Messages: setValue, setMatrix, setErrorCorrection
Init options: value?: string, matrix?: boolean[][], errorCorrection?: ErrorCorrectionLevel
Connect options: ConnectOptions
Parts: root, svg, background, foreground, downloadTrigger
Utilities: size(), toSvgPath(), toDataUrl()
Radio Group
State (RadioGroupState):
| Field | Type |
|---|---|
value |
string | null |
items |
string[] |
disabledItems |
string[] |
disabled |
boolean |
orientation |
Orientation |
loopFocus |
boolean |
dir |
'ltr' | 'rtl' |
Messages: setValue, setItems, selectNext, selectPrev, selectFirst, selectLast, setDir
Init options: value?: string | null, items?: string[], disabledItems?: string[], disabled?: boolean, orientation?: Orientation, loopFocus?: boolean, dir?: 'ltr' | 'rtl'
Connect options: ConnectOptions
Parts: root, item
Rating Group
State (RatingGroupState):
| Field | Type |
|---|---|
value |
number |
count |
number |
allowHalf |
boolean |
disabled |
boolean |
readonly |
boolean |
hoveredValue |
number | null |
dir |
'ltr' | 'rtl' |
Messages: setValue, hover, clickItem, hoverItem, incrementValue, decrementValue, toEnd, setDir
Init options: value?: number, count?: number, allowHalf?: boolean, disabled?: boolean, readonly?: boolean, dir?: 'ltr' | 'rtl'
Connect options: ConnectOptions
Parts: root, item
Utilities: itemFill()
Scroll Area
State (ScrollAreaState):
| Field | Type |
|---|---|
overflowX |
boolean |
overflowY |
boolean |
scrolling |
boolean |
hovered |
boolean |
visibility |
ScrollbarVisibility |
Messages: setScroll, setScrolling, setHovered
Init options: visibility?: ScrollbarVisibility
Parts: root, viewport, content, scrollbarX, scrollbarY, thumbX, thumbY, corner
Utilities: showScrollbars(), thumbPosition(), thumbSize()
Search Field
State (SearchFieldState):
| Field | Type |
|---|---|
value |
string |
disabled |
boolean |
Messages: setValue, clear, submit
Init options: value?: string, disabled?: boolean
Connect options: ConnectOptions
Parts: root, label, input, clearTrigger
Select
State (SelectState):
| Field | Type |
|---|---|
open |
boolean |
value |
string[] |
items |
string[] |
groups |
SelectGroup[] |
disabledItems |
string[] |
selectionMode |
SelectionMode |
highlightedValue |
string | null |
disabled |
boolean |
required |
boolean |
typeahead |
string |
typeaheadExpiresAt |
number |
Messages: open, close, toggle, selectOption, setValue, clear, highlight, highlightNext, highlightPrev, highlightFirst, highlightLast, selectHighlighted, setItems, typeahead
Init options: value?: string[], items?: string[], groups?: SelectGroup[], disabledItems?: string[], selectionMode?: SelectionMode, disabled?: boolean, required?: boolean
Connect options: ConnectOptions
Parts: trigger, positioner, content, hiddenSelect, hiddenOption, item, group, valueText
Utilities: overlay()
Signature Pad
State (SignaturePadState):
| Field | Type |
|---|---|
strokes |
Stroke[] |
current |
Stroke | null |
drawing |
boolean |
disabled |
boolean |
readonly |
boolean |
Messages: strokeStart, strokePoint, strokeEnd, strokeCancel, undo, redo, clear, setStrokes
Init options: strokes?: Stroke[], disabled?: boolean, readonly?: boolean
Connect options: ConnectOptions
Parts: root, control, clearTrigger, undoTrigger, guide, hiddenInput
Utilities: isEmpty(), pointCount(), getBounds()
Slider
State (SliderState):
| Field | Type |
|---|---|
value |
number[] |
min |
number |
max |
number |
step |
number |
disabled |
boolean |
orientation |
Orientation |
minStepsBetweenThumbs |
number |
dir |
'ltr' | 'rtl' |
Messages: setValue, setThumb, increment, decrement, toMin, toMax, setDisabled, setDir
Init options: value?: number[], min?: number, max?: number, step?: number, disabled?: boolean, orientation?: Orientation, minStepsBetweenThumbs?: number, dir?: 'ltr' | 'rtl'
Parts: thumb, root, control, track, range, thumb, value
Utilities: valueFromPoint(), closestThumbIndex()
Sortable
State (SortableState):
| Field | Type |
|---|---|
id |
string |
startIndex |
number |
currentIndex |
number |
fromContainer |
string |
toContainer |
string |
startX |
number |
startY |
number |
currentX |
number |
currentY |
number |
dragging |
DragState | null |
Messages: start, move, drop, cancel, toggleGrab, moveBy
Connect options: ConnectOptions
Parts: root, item, handle
Utilities: reorder()
Splitter
State (SplitterState):
| Field | Type |
|---|---|
position |
number |
min |
number |
max |
number |
step |
number |
orientation |
Orientation |
disabled |
boolean |
dragging |
boolean |
dir |
'ltr' | 'rtl' |
Messages: setPosition, increment, decrement, toMin, toMax, startDrag, endDrag, setDir
Init options: position?: number, min?: number, max?: number, step?: number, orientation?: Orientation, disabled?: boolean, dir?: 'ltr' | 'rtl'
Parts: root, primaryPanel, secondaryPanel, resizeTrigger
Utilities: positionFromPoint()
Steps
State (StepsState):
| Field | Type |
|---|---|
current |
number |
completed |
number[] |
errors |
number[] |
steps |
string[] |
linear |
boolean |
disabled |
boolean |
Messages: goTo, next, prev, complete, markError, clearError, reset
Init options: current?: number, completed?: number[], steps?: string[], linear?: boolean, disabled?: boolean
Connect options: ConnectOptions
Parts: root, nextTrigger, prevTrigger, item
Utilities: stepStatus()
Switch
State (SwitchState):
| Field | Type |
|---|---|
checked |
boolean |
disabled |
boolean |
Messages: toggle, setChecked, setDisabled
Init options: checked?: boolean, disabled?: boolean
Parts: root, track, thumb, hiddenInput
Table
State (TableState):
| Field | Type |
|---|---|
columns |
TableColumn[] |
rows |
string[] |
sort |
TableSort | null |
selection |
string[] |
selectionMode |
TableSelectionMode |
focusedCell |
TableCellCoord | null |
rangeAnchor |
number | null |
pageSize |
number |
descFirst |
boolean |
disabled |
boolean |
Messages: toggleSort, setSort, toggleRow, selectAll, clearSelection, toggleAll, setSelection, selectRange, activateRow, setRows, setColumns, focusCell, moveCell, rowStart, rowEnd, gridStart, gridEnd, pageDown, pageUp
Init options: columns?: TableColumn[], rows?: string[], sort?: TableSort | null, selection?: string[], selectionMode?: TableSelectionMode, focusedCell?: TableCellCoord | null, pageSize?: number, descFirst?: boolean, disabled?: boolean
Connect options: ConnectOptions
Parts: root, columnHeader, row, cell, selectAllCheckbox, rowCheckbox
Utilities: isRowSelected(), isAllSelected(), isSomeSelected(), sortDirectionFor()
Constants: HEADER_ROW_INDEX
Tabs
State (TabsState):
| Field | Type |
|---|---|
value |
string |
items |
string[] |
disabledItems |
string[] |
orientation |
Orientation |
activation |
Activation |
focused |
string | null |
loopFocus |
boolean |
deselectable |
boolean |
dir |
'ltr' | 'rtl' |
Messages: setValue, setItems, focusTab, activateTab, focusNext, focusPrev, focusFirst, focusLast, activateFocused, setDir
Init options: value?: string, items?: string[], disabledItems?: string[], orientation?: Orientation, activation?: Activation, loopFocus?: boolean, deselectable?: boolean, dir?: 'ltr' | 'rtl'
Connect options: ConnectOptions
Parts: root, list, indicator, item
Utilities: watchTabIndicator()
Tags Input
State (TagsInputState):
| Field | Type |
|---|---|
value |
string[] |
inputValue |
string |
disabled |
boolean |
max |
number |
unique |
boolean |
focusedIndex |
number | null |
Messages: setInput, addTag, removeTag, removeLast, setValue, focusTag, focusTagNext, focusTagPrev, clearAll
Init options: value?: string[], inputValue?: string, disabled?: boolean, max?: number, unique?: boolean
Connect options: ConnectOptions
Parts: root, input, tag, clearTrigger
Theme Switch
State (ThemeSwitchState):
| Field | Type |
|---|---|
theme |
Theme |
Messages: setTheme, toggle
Connect options: ConnectOptions
Parts: root, option, toggle
Utilities: resolveTheme(), applyTheme(), watchSystemTheme()
Time Picker
State (TimePickerState):
| Field | Type |
|---|---|
value |
TimeValue |
format |
TimeFormat |
minuteStep |
number |
secondStep |
number |
showSeconds |
boolean |
disabled |
boolean |
Messages: setValue, setHours, setMinutes, setSeconds, incrementHours, decrementHours, incrementMinutes, decrementMinutes, toggleAmPm, setDisabled
Init options: value?: TimeValue, format?: TimeFormat, minuteStep?: number, secondStep?: number, showSeconds?: boolean, disabled?: boolean
Connect options: ConnectOptions
Parts: root, hoursInput, minutesInput, periodTrigger
Utilities: displayHours(), hoursFromDisplay(), period(), formatTime()
Timer
State (TimerState):
| Field | Type |
|---|---|
running |
boolean |
direction |
Direction |
targetMs |
number |
elapsedMs |
number |
startedAt |
number | null |
Messages: start, pause, reset, tick, setTarget
Init options: direction?: Direction, targetMs?: number, elapsedMs?: number
Connect options: ConnectOptions
Parts: root, display, startTrigger, pauseTrigger, resetTrigger
Utilities: display(), isComplete(), parts(), formatMs()
Toast
State (ToasterState):
| Field | Type |
|---|---|
toasts |
Toast[] |
max |
number |
placement |
ToastPlacement |
animated |
boolean |
Messages: create, dismiss, dismissAll, update, tick, pause, resume, pauseAll, resumeAll, animationEnd
Init options: max?: number, placement?: ToastPlacement, animated?: boolean
Connect options: ConnectOptions
Parts: region, toast, progress, isPresent
Utilities: nextToastId(), politeness(), progress(), isPresent()
Toc
State (TocState):
| Field | Type |
|---|---|
items |
TocEntry[] |
activeId |
string | null |
expanded |
string[] |
Messages: setItems, setActive, toggleExpanded, expandAll, collapseAll
Init options: items?: TocEntry[], activeId?: string | null, expanded?: string[]
Connect options: ConnectOptions
Parts: root, list, item
Utilities: isActive(), isExpanded(), watchActiveHeading()
Toggle Group
State (ToggleGroupState):
| Field | Type |
|---|---|
value |
string[] |
type |
'single' | 'multiple' |
items |
string[] |
disabledItems |
string[] |
disabled |
boolean |
orientation |
Orientation |
deselectable |
boolean |
focused |
string | null |
loopFocus |
boolean |
dir |
'ltr' | 'rtl' |
Messages: toggle, setValue, setItems, focusNext, focusPrev, focusItem, setDir
Init options: value?: string[], type?: 'single' | 'multiple', items?: string[], disabledItems?: string[], disabled?: boolean, orientation?: Orientation, deselectable?: boolean, focused?: string | null, loopFocus?: boolean, dir?: 'ltr' | 'rtl'
Parts: root, item
Toggle
State (ToggleState):
| Field | Type |
|---|---|
pressed |
boolean |
disabled |
boolean |
Messages: toggle, setPressed, setDisabled
Init options: pressed?: boolean, disabled?: boolean
Parts: root
Toolbar
State (ToolbarState):
| Field | Type |
|---|---|
items |
string[] |
disabledItems |
string[] |
focused |
string | null |
orientation |
Orientation |
loopFocus |
boolean |
disabled |
boolean |
Messages: setItems, setFocused, focusNext, focusPrev, focusFirst, focusLast
Init options: items?: string[], disabledItems?: string[], focused?: string | null, orientation?: Orientation, loopFocus?: boolean, disabled?: boolean
Connect options: ConnectOptions
Parts: root, separator, item, group
Tooltip
State (TooltipState):
| Field | Type |
|---|---|
open |
boolean |
status |
PresenceStatus |
animated |
boolean |
Messages: show, hide, toggle, setOpen, animationEnd
Init options: open?: boolean, animated?: boolean
Connect options: ConnectOptions
Parts: trigger, positioner, content, arrow
Utilities: overlay(), isMounted()
Tour
State (TourState):
| Field | Type |
|---|---|
steps |
TourStep[] |
open |
boolean |
index |
number |
visited |
string[] |
Messages: start, stop, next, prev, goto, setSteps
Init options: steps?: TourStep[], open?: boolean, index?: number
Connect options: ConnectOptions
Parts: root, backdrop, spotlight, title, description, progressText, prevTrigger, nextTrigger, closeTrigger
Utilities: currentStep(), isFirst(), isLast(), progress()
Tree View
State (TreeViewState):
| Field | Type |
|---|---|
expanded |
string[] |
selected |
string[] |
checked |
string[] |
indeterminate |
string[] |
focused |
string | null |
selectionMode |
SelectionMode |
visibleItems |
string[] |
visibleLabels |
string[] |
disabled |
boolean |
typeahead |
string |
typeaheadExpiresAt |
number |
renaming |
string | null |
renameDraft |
string |
loading |
string[] |
nodes |
Record<string, TreeNodeMeta> |
roots |
string[] |
loaded |
string[] |
loadFailed |
string[] |
Messages: toggleBranch, expand, collapse, expandAll, collapseAll, select, setSelected, focus, focusNext, focusPrev, focusFirst, focusLast, setVisibleItems, typeahead, arrowLeftFrom, arrowRightFrom, toggleChecked, setChecked, setIndeterminate, renameStart, renameChange, renameCommit, renameCancel, loadingStart, loadingEnd, setNodes, childrenLoaded, childrenLoadFailed
Init options: expanded?: string[], selected?: string[], checked?: string[], indeterminate?: string[], selectionMode?: SelectionMode, disabled?: boolean, visibleItems?: string[], visibleLabels?: string[], nodes?: Record<string, TreeNodeMeta>, roots?: string[], loaded?: string[], loadFailed?: string[]
Connect options: ConnectOptions
Parts: root, item
Utilities: isExpanded(), isSelected(), isChecked(), isIndeterminate(), isRenaming(), isLoading(), isLoaded(), isLoadFailed()
Top-Level Exports
Besides the per-component objects (import { menu } from '@llui/components'), the components barrel re-exports these members directly, and @llui/components passes them through. Where the barrel name differs from the component's own, the barrel name is the only one that resolves on @llui/components — the component's own spelling is reachable through its component object or its subpath entry (@llui/components/table).
This table covers the component modules only. The package root additionally re-exports the locale surface (en, LocaleContext), the format/ helpers and the shared utils/ helpers (see Utilities above); those are not listed here.
From @llui/components |
Component | Declared as |
|---|---|---|
validateSchema() |
form |
— |
validateSchemaAsync() |
form |
— |
reorder() |
sortable |
— |
resolveTheme() |
theme-switch |
— |
applyTheme() |
theme-switch |
— |
watchSystemTheme() |
theme-switch |
— |
visibleItems() |
breadcrumbs |
— |
isRowSelected() |
table |
— |
isAllSelected() |
table |
— |
isSomeSelected() |
table |
— |
sortDirectionFor() |
table |
— |
TABLE_HEADER_ROW_INDEX |
table |
HEADER_ROW_INDEX (aliased) |
menubarInit() |
menubar |
init() (aliased) |
menubarUpdate() |
menubar |
update() (aliased) |
menubarConnect() |
menubar |
connect() (aliased) |
menubarOverlay() |
menubar |
overlay() (aliased) |
menubarMachine |
menubar |
menubar (aliased) |
Complete Public API
The component summaries above optimize for everyday authoring. This appendix is generated from every concrete TypeScript entry point declared in package.json#exports and includes the complete importable surface.
@llui/components
Functions
allFiniteNumbers() from @llui/components
Whether every number nested in one atomic runtime payload is usable. Non-numeric leaves are ignored; arrays and plain payload objects are walked so callers cannot accidentally validate one coordinate while committing a bad sibling. Cycles are harmless because an already-seen object contains no new numeric leaves.
function allFiniteNumbers(...values: readonly unknown[]): boolean
anatomy() from @llui/components
function anatomy<P extends string>(name: string, parts: readonly P[]): Anatomy<P>
applySelection() from @llui/components
Apply a click/Enter on value to the current selection. Single mode
replaces, multiple toggles, and a disabled item changes nothing — returning
the SAME array reference so the reducer's no-op stays a no-op for the
reference-equality reconciler.
function applySelection(current: string[], value: string, opts: { mode: SelectionMode; disabled?: readonly string[] }): string[]
applyTheme() from @llui/components
Set data-theme="light" or data-theme="dark" on <html>. CSS selectors
like [data-theme='dark'] { ... } will then take effect.
function applyTheme(resolved: ResolvedTheme): void
attachFloating() from @llui/components
Position floating relative to anchor with live updates on scroll/resize.
Applies left + top styles to the floating element. Returns a cleanup.
export declare function attachFloating(opts: FloatingOptions): () => void;
clamp() from @llui/components
Bound n into [min, max]. The result is always FINITE: a non-finite input
maps to a defined legal value instead of being stored verbatim.
Every comparison against NaN is false, so NaN used to fall straight
through to return n and land in state — package-wide, since this is the one
clamp every mutation path routes through (#152). It is not merely a wrong
number: JSON.stringify(NaN) (and Infinity) is null, so a non-finite
value breaks the State-is-JSON-serializable invariant and with it devtools
time-travel, @llui/test replay, agent state snapshots and SSR rehydration.
Rejecting at this boundary is what lets every caller state its own
postcondition — e.g. slider's withThumb — without a finiteness caveat.
function clamp(n: number, min: number, max: number): number
clampToStep() from @llui/components
Clamp into the range AND snap onto the grid. The result is always within
[min, max]: snapping can leave the range when an endpoint is not itself on
the grid (min 0, max 10, step 4 → 10 snaps up to 12), and the answer there is
the last grid value INSIDE the range, not an out-of-range or off-grid one.
It is always FINITE too — the clamp rejects a non-finite input first (#152).
function clampToStep(value: number, grid: NumericGrid): number
decimalPlaces() from @llui/components
Fraction digits n is written with, INCLUDING exponential notation —
String(1e-7) is '1e-7', which a scan for '.' reads as zero decimals.
function decimalPlaces(n: number): number
deriveOnce() from @llui/components
Wrap a ONE-ARGUMENT compute so that repeating a call with the same argument
returns the previous result. One cell — the first item of an update pays for
the derivation and the rest read it, so the cost is per UPDATE, not per item
and not per render.
This is the shape that runs per ROW per BINDING per update, so it takes a
FIXED parameter and compares with one Object.is. The variadic deriveOnceN
below materialises a fresh arguments array on every call — one per row per
binding per update — which is pure overhead on the hit path: over a pass of
N items x 4 bindings, 0.00056 -> 0.00041 ms at N=20, 0.00467 -> 0.00337 at
N=200 and 0.05680 -> 0.03823 at N=2000 (~25-33%). Reach for deriveOnceN
only where the derivation genuinely takes several inputs.
NO RUNTIME ARITY GUARD, deliberately. Calling the returned function with a
second argument silently ignores it — g(1,'x') and g(1,'y') both return
the g(1) result from one computation — so a JS consumer, or a TS consumer
who casts, can get a wrong answer with no error. That is accepted because
the only ways to observe arity at runtime are an arguments object (absent
in an arrow) or a rest parameter, and a rest parameter re-materialises the
per-row-per-binding array whose removal is this function's entire reason to
exist — paying the 25-33% back on the hottest path in the package, on every
hit, to catch a call TypeScript already rejects (TS2554). Do NOT "fix" this
by widening the signature. If a derivation needs more than one input, that
is what deriveOnceN is for.
function deriveOnce<A, R>(compute: (arg: A) => R): (arg: A) => R
deriveOnceN() from @llui/components
deriveOnce for a derivation with several inputs (the roving tab stop reads
three or four). Memoized on ARGUMENT IDENTITY, position by position.
function deriveOnceN<A extends readonly unknown[], R>(compute: (...args: A) => R): (...args: A) => R
engineFocus() from @llui/components
Focus el as an engine-initiated move (see runEngineFocus).
export declare function engineFocus(el: HTMLElement, options?: FocusOptions): void;
finiteBound() from @llui/components
A bound as STATE may hold it: the finite number itself, or undefined for
"no bound on this side". THE ONE normalizer for a bound, mirroring clamp's
role for a value (#177).
±Infinity and an ABSENT bound already mean the same thing to every clamp in
the package — clampToStep expands grid.min ?? -Infinity — but only one of
the two spellings survives JSON.stringify, which writes null for both
Infinity and NaN. State must be JSON-serializable (devtools time-travel,
@llui/test replay, agent state snapshots, SSR rehydration all compare
serialized state), so the infinite spelling belongs to the RUNTIME expansion
and never to state: normalize at every write, let the grid expand the absence
again. An unbounded number-input used to store min: -Infinity and
rehydrate as min: null — a number field holding null, on the DEFAULT
configuration.
NaN collapses here too, and that is the half a ?? cannot rescue: NaN is
not nullish, so a NaN bound reached clamp, every comparison against it
was false, and THAT SIDE OF THE RANGE STOPPED CLAMPING — angle-slider after
setMin: NaN stored -9999 for setValue(-9999).
Callers decide what an absent bound means for them, and there are exactly two idioms:
- UNBOUNDED-CAPABLE (
number-input): store theundefinedby OMITTING the key, so the state shape IS aNumericGridand round-trips identically. - INTRINSICALLY BOUNDED (
angle-slider,slider,splitter, …): a requiredmin: numbercannot spell "unbounded", so?? DEFAULTatinitand REJECT the write in asetMin/setMaxreducer — dropping a meaningless bound keeps the range the component already had, which is the only answer that cannot silently disable clamping.
function finiteBound(raw: number | null | undefined): number | undefined
finiteOrDefault() from @llui/components
A component-owned number that has no range to clamp into. Initialization
replaces an unusable input with the field's ordinary default; runtime
reducers use {@link allFiniteNumbers} to refuse the whole message instead.
Keeping those two policies here prevents a free position or timestamp from
accidentally inheriting either the grid-value policy (clamp) or the
optional-bound policy (finiteBound).
function finiteOrDefault(raw: number | null | undefined, fallback: number): number
firstEnabled() from @llui/components
Internal value navigation used by the public roving-focus primitive.
export declare function firstEnabled(items: readonly string[], disabled: readonly string[]): string | null;
firstEnabledIndex() from @llui/components
function firstEnabledIndex(items: readonly string[], disabled: readonly string[]): number | null
flipArrow() from @llui/components
Map a horizontal arrow key to its logical direction, accounting for RTL. This is the SINGLE SOURCE OF TRUTH every component routes horizontal arrow interpretation through. Under rtl, ArrowLeft and ArrowRight swap meaning; vertical arrows (Up/Down), Home/End, PageUp/PageDown and every non-arrow key pass through unchanged.
The second argument is the direction source:
- an explicit
'ltr' | 'rtl'— used directly (the authoritative form when a component storesdirin its own State and passes it in); - an
Element— direction is resolved by walking up the DOM (dir="rtl"ancestor ordocument.documentElement.dir); null— treated as'ltr'(no-op).
export declare function flipArrow(key: string, source: Element | null | TextDirection): string;
focusLingeredInside() from @llui/components
Whether focus LINGERED INSIDE the layer, i.e. whether restoring it to the anchor respects the user rather than overriding them.
function focusLingeredInside(query: FocusRestoreQuery): boolean
focusRovingItem() from @llui/components
Move DOM focus to the roving item identified by value within the same
widget instance as origin.
Roving-tabindex widgets track the active index in STATE, but assistive tech
follows real DOM focus — so after a keyboard move the handler MUST also move
focus, or arrow keys are silent for AT. origin is the event's
currentTarget (the item that received the key); its closest
[data-scope][data-part="root"] ancestor scopes the search so sibling
widgets of the same scope never cross-focus. No-op if nothing matches.
send() is synchronous and items already exist in the DOM, so this can be
called immediately after the navigation send.
export declare function focusRovingItem(origin: Element | null, scope: string, value: string, opts?: {
itemPart?: string;
attr?: string;
}): void;
focusRovingTab() from @llui/components
Move DOM focus to the trigger whose data-value matches, within
container. Relies only on the role="tab" + data-value contract
(shared by components/tabs and any hand-rolled tablist). No-op when no
trigger matches. Call after the DOM reflects the new active tab (e.g. in
a microtask if activation triggers a re-render).
export declare function focusRovingTab(container: Element, value: string): void;
formatDate() from @llui/components
function formatDate(value: DateValue, opts: FormatDateOptions = {}): string
formatDateTime() from @llui/components
function formatDateTime(value: DateValue, opts: FormatDateTimeOptions = {}): string
formatDisplayName() from @llui/components
function formatDisplayName(value: string, type: DisplayNameType, opts: FormatDisplayNameOptions = {}): string | undefined
formatFileSize() from @llui/components
function formatFileSize(value: number | bigint, opts: FormatFileSizeOptions = {}): string
formatList() from @llui/components
function formatList(value: string[], opts: FormatListOptions = {}): string
formatNumber() from @llui/components
function formatNumber(value: number, opts: FormatNumberOptions = {}): string
formatPlural() from @llui/components
function formatPlural(value: number, messages: PluralMessages, opts: FormatPluralOptions = {}): string
formatRelativeTime() from @llui/components
function formatRelativeTime(value: number, unit: RelativeTimeUnit, opts: FormatRelativeTimeOptions = {}): string
formatTime() from @llui/components
function formatTime(value: DateValue, opts: FormatTimeOptions = {}): string
getFocusables() from @llui/components
export declare function getFocusables(container: Element): HTMLElement[];
getNestedLayers() from @llui/components
Currently-registered nested-layer elements (resolvers re-read live).
With an aspect, only registrations that participate in it; without one, all
of them. With a within boundary, only registrations nested inside it (see
the module comment); without one, the flat, layer-agnostic answer.
export declare function getNestedLayers(aspect?: NestedLayerAspect, within?: NestedLayerScope): Element[];
indexMap() from @llui/components
A position lookup over a state array: positions(s.items).get(item) in place
of s.items.indexOf(item). First occurrence wins, matching indexOf.
function indexMap<T>(): (values: readonly T[] | null | undefined) => ReadonlyMap<T, number>
isAllSelected() from @llui/components
function isAllSelected(state: TableState): boolean
isDateOnly() from @llui/components
function isDateOnly(value: DateValue): value is string
isEnabledItem() from @llui/components
An item counts as navigable only while it is in the list AND not disabled.
function isEnabledItem(items: readonly string[], disabled: readonly string[], value: string): boolean
isEngineFocusInProgress() from @llui/components
Whether an engine-initiated focus move is in flight. Consulted by
watchInteractOutside to gate its focusin path.
export declare function isEngineFocusInProgress(): boolean;
isFocusable() from @llui/components
Find focusable descendants within a container.
export declare function isFocusable(el: Element): boolean;
isInNestedLayer() from @llui/components
Whether target is inside (or equal to) a registered nested layer that
participates in aspect (any layer when aspect is omitted) and is nested
inside within (any layer when within is omitted).
export declare function isInNestedLayer(target: Node | null, aspect?: NestedLayerAspect, within?: NestedLayerScope): boolean;
isRowSelected() from @llui/components
function isRowSelected(state: TableState, id: string): boolean
isSomeSelected() from @llui/components
function isSomeSelected(state: TableState): boolean
isTypeaheadKey() from @llui/components
Returns true if the key event should trigger a typeahead query — i.e., a
single printable character that isn't a modified keyboard shortcut. Use
this in onKeyDown handlers to decide whether to dispatch a typeahead
message.
function isTypeaheadKey(e: KeyboardEvent): boolean
lastEnabled() from @llui/components
export declare function lastEnabled(items: readonly string[], disabled: readonly string[]): string | null;
lastEnabledIndex() from @llui/components
function lastEnabledIndex(items: readonly string[], disabled: readonly string[]): number | null
lockBodyScroll() from @llui/components
Lock body scroll while an overlay is open, preserving scrollbar width to avoid layout shift. Reference-counted so nested locks compose cleanly.
export declare function lockBodyScroll(): () => void;
membershipSet() from @llui/components
A membership lookup over a state array: set(s.value).has(item) in place of
s.value.includes(item). An absent collection reads as empty.
An EMPTY array shares EMPTY_SET rather than allocating: most of the ~16
call sites are a disabled/disabledItems list that is empty in the common
case, and an empty Set is the one input where the memo would otherwise pay
an allocation to answer false to everything.
function membershipSet<T>(): (values: readonly T[] | null | undefined) => ReadonlySet<T>
menubarConnect() from @llui/components
function menubarConnect(state: Signal<MenubarState>, send: Send<MenubarMsg>, opts: ConnectOptions): MenubarParts
menubarInit() from @llui/components
function menubarInit(opts: MenubarInit): MenubarState
menubarOverlay() from @llui/components
Render one top-level menu's dropdown. Mirrors menu.overlay but is gated on
state.open === menuId and dismisses by closing the menubar (returning
focus to the top-level trigger). Submenu unwinding goes through the same
dismissable stack the menu machine uses.
function menubarOverlay(opts: MenubarOverlayOptions): Mountable
menubarUpdate() from @llui/components
function menubarUpdate(state: MenubarState, msg: MenubarMsg): [MenubarState, never[]]
nextEnabled() from @llui/components
export declare function nextEnabled(items: readonly string[], disabled: readonly string[], from: string, delta: 1 | -1, loop: boolean): string | null;
nextEnabledIndex() from @llui/components
The index of the next enabled item delta steps from from, wrapping.
from === null starts before the first item (delta 1) or after the last
(delta -1), so the first/last enabled index comes back.
function nextEnabledIndex(items: readonly string[], disabled: readonly string[], from: number | null, delta: 1 | -1): number | null
parseDateValue() from @llui/components
function parseDateValue(value: DateValue): ParsedDateValue
positiveFinite() from @llui/components
A finite number strictly greater than zero, or undefined when unusable.
function positiveFinite(raw: number | null | undefined): number | undefined
positiveFiniteOrDefault() from @llui/components
A positive finite number, or the field's ordinary initialization default.
function positiveFiniteOrDefault(raw: number | null | undefined, fallback: number): number
presenceEndHandler() from @llui/components
Guard a presence "animation/transition ended" handler so it only advances the
presence machine when the event fired on the element the listener is bound to
(e.target === e.currentTarget) — never on a bubbling descendant.
Overlay content (dialog, popover, menu, toast) reflects its exit phase via
data-state="closing" and stays mounted until an animationend/transitionend
dispatches animationEnd/transitionEnd. Without this guard, ANY descendant
animation or transition ending during the exit — a spinner, a ripple, a child
fade — bubbles up and prematurely unmounts the overlay before its own exit
animation completes.
Mirrors the e.target === el guard the transitions runtime applies in
waitForEnd (@llui/transitions).
function presenceEndHandler<E extends AnimationEvent | TransitionEvent>(handler: (e: E) => void): (e: E) => void
pruneToEnabled() from @llui/components
Keep value only while it still names an enabled item, else null. Every
reducer that replaces the item list owes this to whatever it holds as
focused/selected — a dangling reference is the tab-stop bug.
function pruneToEnabled(items: readonly string[], disabled: readonly string[], value: string | null): string | null
pushDismissable() from @llui/components
Register a dismissable layer. Escape is offered to the layers top-down until
one CLAIMS it (a layer declines via disableEscape or an onEscape router
returning false, and the key then falls through to the layer beneath);
outside-click is topmost-only. Returns a cleanup that removes the layer from
the stack.
Push a layer even when both dismissal routes are disabled: the layer is the caller's PLACE ON THE STACK, which is what stops the layer beneath from treating an interaction inside this one as an outside interaction.
export declare function pushDismissable(opts: DismissableOptions): () => void;
pushFocusTrap() from @llui/components
Push a focus trap onto the stack. Tab/Shift+Tab will cycle within the container's focusable descendants. Returns a cleanup that removes the trap and (optionally) restores focus to the element active before push.
export declare function pushFocusTrap(opts: FocusTrapOptions): () => void;
registerNestedLayer() from @llui/components
Register source (an element, array of elements, or a resolver returning
either) as a nested layer. Returns a cleanup that removes the registration.
Prefer the resolver form for a portaled overlay: register once on mount and
return the live root only while open ([] when closed), so a single
registration tracks the overlay's open/closed lifecycle without churn.
Pass opts.owner when scoped consumers must exempt the layer. Missing or
unresolved ownership fails closed and warns in development.
export declare function registerNestedLayer(source: ElementSource, opts?: NestedLayerOptions): () => void;
reorder() from @llui/components
Move an item in an array from one index to another, returning a new array. Out-of-range indices are clamped to array bounds.
function reorder<T>(arr: readonly T[], from: number, to: number): T[]
resetAnatomyIdCounter() from @llui/components
Reset the internal id counter — tests only.
function resetAnatomyIdCounter(): void
resolveDir() from @llui/components
Resolve the text direction for an element by walking up the DOM tree. Returns 'rtl' or 'ltr' (default).
export declare function resolveDir(el: Element): TextDirection;
resolvePluralCategory() from @llui/components
function resolvePluralCategory(value: number, opts: FormatPluralOptions = {}): PluralCategory
resolveRovingMove() from @llui/components
Map a keyboard key + the current tab value to a roving-tablist move,
or null when the key isn't a navigation/activation key or the move is
a no-op (empty list, no enabled sibling). Pure — does not touch the DOM
or call preventDefault; the caller decides (typically: prevent default
iff the result is non-null).
export declare function resolveRovingMove(key: string, current: string, items: readonly RovingItem[], opts?: RovingOptions): RovingMove | null;
resolveTextDirection() from @llui/components
Normalize any accepted direction source to a concrete TextDirection.
An explicit 'ltr' | 'rtl' wins; an Element is resolved from the DOM;
null / undefined default to 'ltr'.
export declare function resolveTextDirection(source: Element | null | undefined | TextDirection): TextDirection;
resolveTheme() from @llui/components
Resolve a theme preference to the actual theme to apply. Returns 'dark' or
'light' based on the user's setting, consulting prefers-color-scheme for
'system'.
function resolveTheme(theme: Theme): ResolvedTheme
rovingTabStop() from @llui/components
The single item that carries tabindex="0".
WAI-ARIA's roving-tabindex pattern requires EXACTLY ONE tab stop in a
composite widget: with none, Tab skips the widget entirely and it becomes
keyboard-unreachable. So a preferred candidate (the focused item, the
checked radio, …) is honoured only while it is still an enabled member, and
the first enabled item answers otherwise. Null only when nothing is enabled.
Every roving-tabindex widget in the package routes through here (#145 closed
the last three: menubar, navigation-menu and tags-input). Keep it that way —
an inline focused === x ? 0 : -1 has no fallback, and since nothing prunes
focused against the current list, removing or disabling the focused item
leaves EVERY item at -1 and the widget disappears from the Tab order.
tags-input is index-keyed and passes String(i) as the item identity (its
data-index, and the only identity that survives duplicate tag values);
navigation-menu passes either the membership list its consumer maintains or
the ids handed to its own item(), filtered first to the ones not sealed
inside a closed submenu — membership alone would seat the stop on an element
inside a hidden panel, which is present, unique and untabbable.
Null when nothing is enabled is deliberate and is a caller's problem to
notice: a widget whose items are ALL disabled ends up with no tab stop at
all. That is right for radio-group/toggle-group/toolbar/tree-view,
whose items are genuinely disabled and therefore unfocusable anyway, and it
is a 1 -> 0 change for menubar, whose triggers carry only aria-disabled
and stay focusable. Any revision belongs here, applying to every caller at
once — not in one component.
function rovingTabStop(items: readonly string[], disabled: readonly string[], ...preferred: readonly (string | null | undefined)[]): string | null
runEngineFocus() from @llui/components
Run body with engine-focus suppression active. Any focusin raised inside
it is invisible to watchInteractOutside — including one raised by a focus
move that re-entrant consumer code makes from a focusin listener (see the
module comment: the window is the synchronous transitive closure, not just
the .focus() call).
SYNCHRONOUS BY CONTRACT, AND THE CONTRACT IS ENFORCED (#172). The suppression
is released when body RETURNS. An async body returns its promise at the
first await, so the depth counter drops immediately and the focus move that
eventually happens gets NO protection at all — a call that looks correct,
compiles, and does nothing. The failure is safe (no protection, never a stuck
guard: the decrement is in a finally), which is exactly why it is invisible,
and this is a PUBLIC export documented as the thing a custom overlay "must"
route its engine-initiated focus moves through. A consumer following that
advice with an async body would reintroduce #155 in their app while believing
they had prevented it.
Two guards, because neither covers the other's case:
- The SIGNATURE rejects a promise-returning body at compile time. It is the real guard — it fires before the code ever runs. Its one blind spot is a body whose return type is an unresolved type parameter (a generic pass-through wrapper): the conditional is deferred, so such a wrapper is rejected too and must carry its own constraint. No caller does.
- A DEV-MODE warning catches what the type system cannot see: a JavaScript
consumer, an
any-typed body, or a body that returns a thenable without being declared as returning one. It cannot restore the protection — by the time a thenable is in hand the guard is already released — so it only reports.
Deliberately NOT offered: an async-aware variant that holds the guard across
an await. The guard is safe because no user event can be delivered inside
its window (see the module comment); holding it across a suspension point
hands the event loop back and would start swallowing genuine interactions.
export declare function runEngineFocus<T>(body: () => SyncEngineFocusBody<T>): T;
setAriaHiddenOutside() from @llui/components
Hide sibling subtrees from assistive tech while an overlay is open.
Walks from target up to the document root, applying aria-hidden="true"
and inert to every sibling at each level. Previous attribute values are
recorded and restored on cleanup.
Two kinds of element are EXEMPT: registered nested layers (see
registerNestedLayer) and live regions — aria-live, role="alert",
role="status", role="log". A live region under aria-hidden is simply
never read out, so a modal that sweeps one silences the app's announcement
channel for exactly as long as it is open (#123). Live regions are matched by
selector rather than registration because they are plain part bags with no
mount hook of their own, and because that also covers consumer-authored ones.
An exempt element does NOT spare its whole ancestor subtree: the sweep
descends through any element that merely CONTAINS one and hides everything
hanging off the path down to it. (Skipping the ancestor wholesale would leave
the entire app interactive behind the modal the moment a form somewhere held
an aria-live error message.) inert and aria-hidden both inherit, so
leaving the path clear is the only way an exempt element stays reachable.
Nested calls are supported — each layer only touches elements that haven't been claimed by a higher layer (tracked via a WeakMap reference count).
TWO KNOWN LIMITS of the live-region exemption, both deliberate:
inertcannot be split fromaria-hidden— sparing a region spares its WHOLE SUBTREE from both, so an interactive live region (arole="log"transcript containing links) stays Tab-reachable behind a modal. Keep live regions to announcement text; put controls outside them, or register the modal's own layer for the interactive part.document.querySelectorAlldoes not pierce shadow roots, so a live region inside one is not exempt. The sweep itself only ever walks light-DOM ancestors oftarget, so this only bites when the region and the modal live in different trees.
export declare function setAriaHiddenOutside(target: Element): () => void;
snapToStep() from @llui/components
Nearest multiple of step from origin. A non-positive step is a no-op.
A non-finite value names no position on the grid, so it snaps to the grid's
own anchor — the same policy clamp applies to the range (#152). This is
unreachable from clampToStep, which clamps first; it keeps the util's
direct consumers on the same rule.
function snapToStep(value: number, step: number, origin = 0): number
sortDirectionFor() from @llui/components
function sortDirectionFor(state: TableState, columnId: string): SortDirection | null
stepBy() from @llui/components
Move count whole steps (negative to step down), then clamp+snap.
From an OFF-GRID value one call moves to the nearest grid value in the
direction of travel and stops there — that jump is the whole change, however
large count is. This is HTML's stepUp/stepDown (step 3 of the
value-stepping algorithm) and it is what makes increment land on the grid
instead of dragging an off-grid value along forever.
ONE DELIBERATE DIVERGENCE from the spec: HTML's step base falls back min ->
the value CONTENT ATTRIBUTE -> 0; gridOrigin goes min -> 0. A headless
machine has no content attributes — the seed value is just the initial state,
and anchoring the grid on it would make two components with the same
min/max/step disagree about which values are legal depending on where they
happened to start.
function stepBy(value: number, count: number, grid: NumericGrid): number
typeaheadAccumulate() from @llui/components
Advance the typeahead query based on a new keystroke and the previous
expiration time. Returns the new query string; callers combine this with
typeaheadMatch() to produce a new highlight index.
function typeaheadAccumulate(prev: string, char: string, now: number, expiresAt: number): string
typeaheadMatch() from @llui/components
Find the first enabled item whose label starts with the query
(case-insensitive). labels and disabledMask are parallel arrays.
startFrom is the current highlighted index; for single-character
queries the search begins at startFrom + 1 (so repeated "s" keys
cycle), for multi-character queries it begins at startFrom (inclusive).
Returns the matching index, or null if no enabled item matches.
function typeaheadMatch(labels: string[], disabledMask: boolean[], query: string, startFrom: number | null): number | null
typeaheadMatchByItems() from @llui/components
Convenience: pass a disabled list of values instead of a boolean mask.
Builds the mask by checking membership via === on the raw string values.
function typeaheadMatchByItems(items: string[], disabled: readonly string[], query: string, startFrom: number | null): number | null
validateSchema() from @llui/components
Run a Standard Schema synchronously against a values object. Throws if the schema returns a Promise — use sync validation only for form submit.
Works with any library implementing the Standard Schema spec: Zod (v3.24+), Valibot (v1+), ArkType, etc.
function validateSchema<T>(schema: StandardSchemaV1<T>, values: unknown): ValidateResult<T>
validateSchemaAsync() from @llui/components
Async variant — returns a Promise. Use when the schema performs async validation (e.g. uniqueness checks against a backend).
function validateSchemaAsync<T>(schema: StandardSchemaV1<T>, values: unknown): Promise<ValidateResult<T>>
visibleItems() from @llui/components
Compute the visible breadcrumb trail. When maxVisible is set and exceeded
and the trail is not expanded, collapse the middle to:
[first] … [last (maxVisible - 1) items]. The final item is always current.
function visibleItems(state: BreadcrumbsState): VisibleBreadcrumb[]
watchInteractOutside() from @llui/components
Watch for pointer or focus events outside a given element. Returns a
cleanup function. Uses the capture phase so upstream stopPropagation
calls cannot hide events.
- pointerdown (or mousedown/touchstart fallback) triggers "outside" if the
target is not contained by
elementorignore. - focusin triggers "outside" when focus moves outside the element, except
when the new target is in
ignore.
export declare function watchInteractOutside(opts: InteractOutsideOptions): () => void;
watchSystemTheme() from @llui/components
Listen for system theme changes (when user has selected 'system'). Returns
a cleanup function. Call this in onMount and dispatch setTheme on
change if you want the UI to auto-follow OS settings.
function watchSystemTheme(callback: (theme: ResolvedTheme) => void): () => void
Types
AcceptValue from @llui/components
File upload — input element + drag-and-drop zone. Tracks selected files, drag state, accept filters, validation errors. Multiple or single selection.
accept can be either a raw HTML-accept string ("image/*,.pdf") or a
MIME-object ({ 'image/*': ['.png', '.jpg'], 'application/pdf': [] }).
The object form is validated client-side per file; the raw string form
only drives the browser's native picker filter.
Files that fail validation (too large, too small, wrong type, over the
count limit) flow into rejectedFiles with a list of FileError codes
attached. The view can render them alongside accepted files.
State holds FileMeta records — plain JSON — never the live File
objects: State must be JSON-serializable (CLAUDE.md), and a File came
back from a round-trip as {}, wiping every name and turning totalSize
into NaN (#119). The handles live in a module-scoped registry keyed by
FileMeta.id; see trackFile/getFile/releaseDropped.
A restored State has no handles. Serializability is exactly what makes
that so: SSR hydration, replayTrace and an agent state snapshot carry the
FileMeta records and nothing else, so after a restore getFile() returns
undefined for every one of them. That is correct and unavoidable — a File
cannot cross the wire — but it is invisible unless you are told, so a view
must treat a missing handle as normal (render the metadata, skip the object-URL
preview) and a re-upload needs a fresh selection from the user.
export type AcceptValue = string | Record<string, string[]>
AccordionMsg from @llui/components
export type AccordionMsg =
/** @intent("Toggle the named accordion item open/closed") */
| { type: 'toggle'; value: string }
/** @intent("Open the named accordion item") */
| { type: 'open'; value: string }
/** @intent("Close the named accordion item") */
| { type: 'close'; value: string }
/** @intent("Replace the set of currently-open items with the provided values") */
| { type: 'setValue'; value: string[] }
/** @humanOnly */
| { type: 'setItems'; items: string[] }
/** @humanOnly */
| { type: 'focusNext'; value: string }
/** @humanOnly */
| { type: 'focusPrev'; value: string }
/** @humanOnly */
| { type: 'focusFirst' }
/** @humanOnly */
| { type: 'focusLast' }
Activation from @llui/components
export type Activation = 'automatic' | 'manual'
AlertDialogConnectOptions from @llui/components
Connect options — the dialog options minus role (fixed to alertdialog).
export type AlertDialogConnectOptions = Omit<DialogConnectOptions, 'role'>
AlertDialogMsg from @llui/components
export type DialogMsg =
/** @intent("Open the dialog") */
| { type: 'open' }
/** @intent("Close the dialog") */
| { type: 'close' }
/** @intent("Toggle the dialog open/closed") */
| { type: 'toggle' }
/** @intent("Set the dialog's open state to a specific value") */
| { type: 'setOpen'; open: boolean }
/** @humanOnly */
| { type: 'animationEnd' }
/** @humanOnly */
| { type: 'transitionEnd' }
AlertDialogParts from @llui/components
export type AlertDialogParts = DialogParts
AngleSliderMsg from @llui/components
export type AngleSliderMsg =
/** @intent("Set the angle in degrees (clamped to min/max, snapped to step)") */
| { type: 'setValue'; value: number }
/** @intent("Increase the angle by `steps` × step (default: 1 step)") */
| { type: 'increment'; steps?: number }
/** @intent("Decrease the angle by `steps` × step (default: 1 step)") */
| { type: 'decrement'; steps?: number }
/** @humanOnly */
| { type: 'setMin'; min: number }
/** @humanOnly */
| { type: 'setMax'; max: number }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
AsyncListMsg from @llui/components
export type AsyncListMsg<T = unknown> =
/** @intent("Request the next page of items") */
| { type: 'loadMore' }
/** @humanOnly */
| { type: 'pageLoaded'; items: T[]; hasMore: boolean }
/** @humanOnly */
| { type: 'pageFailed'; error: string }
/** @intent("Discard the loaded items and reset back to page 0") */
| { type: 'reset' }
/** @humanOnly */
| { type: 'setItems'; items: T[]; hasMore?: boolean }
/** @intent("Retry the last failed page request") */
| { type: 'retry' }
AsyncStatus from @llui/components
Async list — paginated/infinite-scroll list that accumulates pages.
The machine is generic over the item type; the consumer runs the
actual fetch in response to loadMore (via a custom handler or
effect) and dispatches pageLoaded/pageFailed when the request
completes.
Typical flow in consumer's update handler:
(state, msg) => {
if (msg.type === 'loadMore') {
fetch(/api/items?page=${state.list.page + 1})
.then(r => r.json())
.then(items => send({type: 'pageLoaded', items, hasMore: items.length === PAGE_SIZE}))
.catch(e => send({type: 'pageFailed', error: String(e)}))
}
}
export type AsyncStatus = 'idle' | 'loading' | 'loaded' | 'error'
AvatarMsg from @llui/components
export type AvatarMsg =
/** @humanOnly */
| { type: 'loadStart' }
/** @humanOnly */
| { type: 'loaded' }
/** @humanOnly */
| { type: 'error' }
/** @intent("Reset the avatar's load status back to idle") */
| { type: 'reset' }
BreadcrumbsMsg from @llui/components
export type BreadcrumbsMsg =
/** @intent("Replace the breadcrumb trail with a new list of items") */
| { type: 'setItems'; items: BreadcrumbItem[] }
/** @intent("Expand the collapsed middle of the trail to reveal all items") */
| { type: 'expand' }
/** @intent("Collapse the trail back to its truncated form") */
| { type: 'collapse' }
CarouselEffect from @llui/components
Effects emitted by the carousel machine. Running the timer is the consumer's job — the machine only says when it should run (see the module header).
export type CarouselEffect =
/**
* Run the autoplay timer: dispatch `autoplayTick` every `interval` ms.
* Re-emitted to RESTART a timer already running (manual navigation, an
* `interval` change), so the handler must replace rather than add.
*/
| { type: 'startAutoplay'; interval: number }
/** Retire the autoplay timer. */
| { type: 'stopAutoplay' }
CarouselMsg from @llui/components
export type CarouselMsg =
/** @intent("Jump to a specific slide by zero-based index") */
| { type: 'goTo'; index: number }
/** @intent("Advance to the next slide (wraps if loop is enabled)") */
| { type: 'next' }
/** @intent("Go back to the previous slide (wraps if loop is enabled)") */
| { type: 'prev' }
/** @humanOnly */
| { type: 'setCount'; count: number }
/** @intent("Pause autoplay (typically while user hovers or focuses the carousel)") */
| { type: 'pause' }
/** @intent("Resume autoplay after a pause") */
| { type: 'resume' }
/** @intent("Turn autoplay on or off") */
| { type: 'setAutoplay'; autoplay: boolean }
/**
* The autoplay timer fired. Advances exactly like `next`, but does NOT
* restart the timer — it IS the timer. Manual navigation restarts it; a tick
* must not, or the period would be re-armed on every fire.
*
* `@humanOnly` because it is the TIMER's message, not a user intent: with no
* tag it defaulted to dispatchMode `'shared'` and an agent could fire the
* timer directly, advancing the carousel without re-arming the period
* (#138 review, item 8). An agent that wants the next slide sends `next`.
*
* @humanOnly
*/
| { type: 'autoplayTick' }
/** @humanOnly */
| { type: 'dragStart'; x: number }
/** @humanOnly */
| { type: 'dragMove'; x: number }
/** @humanOnly */
| { type: 'dragEnd' }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
CascadeSelectMsg from @llui/components
export type CascadeSelectMsg =
/** @humanOnly */
| { type: 'setLevels'; levels: CascadeLevel[] }
/** @intent("Pick a value at the given level (clears selections at deeper levels)") */
| { type: 'setValue'; levelIndex: number; value: string | null }
/** @intent("Clear every level's selection") */
| { type: 'clear' }
CheckboxMsg from @llui/components
export type CheckboxMsg =
/** @intent("Toggle the checkbox between checked and unchecked") */
| { type: 'toggle' }
/** @intent("Set the checkbox state to checked, unchecked, or indeterminate") */
| { type: 'setChecked'; checked: CheckedState }
/** @humanOnly */
| { type: 'setDisabled'; disabled: boolean }
CheckedState from @llui/components
Checkbox — a tri-state form control (checked / unchecked / indeterminate).
The indeterminate state is a visual-only state used to represent "partial"
selection (e.g. a parent whose children are mixed checked).
Rendering typically uses two elements: a visual indicator (the styled box)
and a hidden native <input type="checkbox"> for form participation +
accessibility. connect() returns props for both.
export type CheckedState = boolean | 'indeterminate'
ClipboardMsg from @llui/components
export type ClipboardMsg =
/** @intent("Update the value to be copied") */
| { type: 'setValue'; value: string }
/** @intent("Initiate a clipboard copy of the current value") */
| { type: 'copy' }
/** @humanOnly */
| { type: 'copied' }
/** @intent("Clear the transient \"copied\" feedback state") */
| { type: 'reset' }
CollapsibleMsg from @llui/components
export type CollapsibleMsg =
/** @intent("Toggle the collapsible panel open/closed") */
| { type: 'toggle' }
/** @intent("Expand the collapsible panel") */
| { type: 'open' }
/** @intent("Collapse the panel") */
| { type: 'close' }
/** @intent("Set the panel's open state to a specific value") */
| { type: 'setOpen'; open: boolean }
ColorPickerMsg from @llui/components
export type ColorPickerMsg =
/** @intent("Set the full HSL color at once") */
| { type: 'setHsl'; hsl: Hsl }
/** @intent("Set the hue channel (0–360)") */
| { type: 'setHue'; h: number }
/** @intent("Set the saturation channel (0–100)") */
| { type: 'setSaturation'; s: number }
/** @intent("Set the lightness channel (0–100)") */
| { type: 'setLightness'; l: number }
/** @intent("Set the alpha channel (0–1)") */
| { type: 'setAlpha'; alpha: number }
/** @intent("Set the color from a hex string (#RRGGBB or #RGB)") */
| { type: 'setHex'; hex: string }
/** @intent("Set saturation and value (HSV, 0–100 each) from the 2D area") */
| { type: 'setSv'; s: number; v: number }
/** @intent("Nudge saturation/value (HSV) by signed deltas — used by area arrow keys") */
| { type: 'nudgeSv'; ds: number; dv: number }
/** @intent("Set the color from a swatch or hex string (#RGB, #RRGGBB, or #RRGGBBAA)") */
| { type: 'setColor'; color: string }
ComboboxAsyncStatus from @llui/components
export type AsyncStatus = 'idle' | 'loading' | 'loaded' | 'error'
ComboboxEffect from @llui/components
Effects emitted by the combobox machine. Creation is owned by the consumer:
when a create sentinel is selected the machine surfaces the typed text as a
createOption effect rather than mutating its own value.
export type ComboboxEffect =
/** @intent("The user asked to create a brand-new option from the typed text") */
{ type: 'createOption'; value: string }
ComboboxMsg from @llui/components
export type ComboboxMsg =
/** @intent("Open the combobox dropdown") */
| { type: 'open' }
/** @intent("Close the combobox dropdown") */
| { type: 'close' }
/** @intent("Set the text input contents (re-runs the filter)") */
| { type: 'setInputValue'; value: string }
/** @intent("Pick the option with the given value (toggles in multi-select)") */
| { type: 'selectOption'; value: string }
/** @intent("Replace the selected values with the provided list") */
| { type: 'setValue'; value: string[] }
/** @intent("Clear all selected values and the input text") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'highlightNext' }
/** @humanOnly */
| { type: 'highlightPrev' }
/** @humanOnly */
| { type: 'highlightFirst' }
/** @humanOnly */
| { type: 'highlightLast' }
/** @humanOnly */
| { type: 'highlight'; value: string | null }
/** @intent("Pick the currently-highlighted option in the filtered list") */
| { type: 'selectHighlighted' }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @intent("Mark an async option fetch as started; pass the request's id") */
| { type: 'loadStart'; requestId: number }
/** @humanOnly */
| { type: 'loadSuccess'; requestId: number; items: string[] }
/** @humanOnly */
| { type: 'loadError'; requestId: number; error: string }
ContextMenuCheckItemParts from @llui/components
export type ContextMenuCheckItemParts = MenuCheckItemPartsOf<'context-menu'>
ContextMenuGroupParts from @llui/components
export type ContextMenuGroupParts = MenuGroupPartsOf<'context-menu'>
ContextMenuItem from @llui/components
A single node in the context-menu item tree (JSON-serializable). Shared with
menu via the {@link MenuNode} machine type.
export type ContextMenuItem = MenuNode
ContextMenuItemKind from @llui/components
Kind of a context-menu item.
export type ContextMenuItemKind = MenuNodeKind
ContextMenuItemParts from @llui/components
export type ContextMenuItemParts = MenuItemPartsOf<'context-menu'>
ContextMenuMsg from @llui/components
export type ContextMenuMsg =
/** @humanOnly */
| { type: 'openAt'; x: number; y: number }
/** @intent("Close the context menu") */
| { type: 'close' }
/** @humanOnly */
| { type: 'highlight'; level: string; value: string | null }
/** @humanOnly */
| { type: 'highlightNext'; level: string }
/** @humanOnly */
| { type: 'highlightPrev'; level: string }
/** @humanOnly */
| { type: 'highlightFirst'; level: string }
/** @humanOnly */
| { type: 'highlightLast'; level: string }
/** @intent("Activate the currently-highlighted item at the given level") */
| { type: 'selectHighlighted'; level: string }
/** @intent("Activate the menu item with the given value") */
| { type: 'select'; value: string }
/** @intent("Open the submenu for the given parent item") */
| { type: 'openSub'; value: string }
/** @intent("Close the deepest open submenu") */
| { type: 'closeSub' }
/** @humanOnly */
| { type: 'setItems'; items: ContextMenuItem[] }
/** @humanOnly */
| { type: 'typeahead'; level: string; char: string; now: number }
/** @intent("Set the reading direction — 'ltr'/'rtl', or null to follow the page") */
| { type: 'setDir'; dir: TextDirection | null }
/** @humanOnly */
| { type: 'animationEnd' }
ContextMenuSeparatorParts from @llui/components
export type ContextMenuSeparatorParts = MenuSeparatorPartsOf<'context-menu'>
ContextMenuSubContentParts from @llui/components
export type ContextMenuSubContentParts = MenuSubContentPartsOf<'context-menu'>
ContextMenuSubPositionerParts from @llui/components
export type ContextMenuSubPositionerParts = MenuSubPositionerPartsOf<'context-menu'>
ContextMenuSubTriggerParts from @llui/components
export type ContextMenuSubTriggerParts = MenuSubTriggerPartsOf<'context-menu'>
DateError from @llui/components
Date input — keyboard-only date field with masked parsing. Unlike date-picker, this is a plain that parses ISO-ish date strings as the user types. Separate from date-picker to keep each focused.
The machine holds the raw input string + the parsed date as an ISO
YYYY-MM-DD string (null until a complete/valid value is entered) —
never a Date, so the state stays JSON-serializable like date-picker's
(#119). Min/max bounds are validated on every change, populating error
when out of range, and an unparseable value — from init or setValue
alike — sets error: 'invalid' rather than vanishing.
export type DateError = 'invalid' | 'before-min' | 'after-max' | null
DateInputMsg from @llui/components
export type DateInputMsg =
/** @intent("Update the raw text the user has typed (re-parses to a date)") */
| { type: 'setInput'; value: string }
/** @intent("Set the parsed date directly as YYYY-MM-DD (also updates the displayed text)") */
| { type: 'setValue'; value: IsoDate | null }
/** @intent("Clear the input and the parsed date") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'setMin'; min: IsoDate | null }
/** @humanOnly */
| { type: 'setMax'; max: IsoDate | null }
/** @humanOnly */
| { type: 'setDisabled'; disabled: boolean }
DatePickerMode from @llui/components
Selection mode: a single date or a start/end range.
export type DatePickerMode = 'single' | 'range'
DatePickerMsg from @llui/components
export type DatePickerMsg =
/** @intent("Set the selected date (YYYY-MM-DD), or null to clear") */
| { type: 'setValue'; value: string | null }
/** @intent("Set the selected date range (YYYY-MM-DD start/end); endpoints are normalized so start <= end") */
| { type: 'setRange'; start: string | null; end: string | null }
/** @humanOnly */
| { type: 'setFocused'; date: string }
/** @humanOnly */
| { type: 'setHover'; date: string }
/** @humanOnly */
| { type: 'clearHover' }
/** @intent("Show the previous month in the calendar") */
| { type: 'prevMonth' }
/** @intent("Show the next month in the calendar") */
| { type: 'nextMonth' }
/** @intent("Show the previous year (same month)") */
| { type: 'prevYear' }
/** @intent("Show the next year (same month)") */
| { type: 'nextYear' }
/** @intent("Select the currently-focused date (anchors or completes a range in range mode)") */
| { type: 'selectFocused' }
/** @humanOnly */
| { type: 'moveFocus'; days: number }
/** @humanOnly */
| { type: 'focusStartOfWeek' }
/** @humanOnly */
| { type: 'focusEndOfWeek' }
/** @humanOnly */
| { type: 'focusToday' }
/** @intent("Clear the current selection") */
| { type: 'clear' }
DateValue from @llui/components
Date-only handling — the ONE place that decides whether a value denotes an INSTANT or a bare CALENDAR DATE.
new Date('2026-01-15') parses a date-only string as UTC midnight (ES spec),
so formatting it against the ambient zone renders the PREVIOUS day everywhere
west of UTC: formatDate('2026-01-15') printed "January 14, 2026" under
America/New_York and the right answer under Europe/Rome, which is why it
survived (#125 defect 4).
A calendar date carries no instant and therefore no zone, so it is anchored
at UTC midnight and its consumers must render it in UTC — see dateOnly.
export type DateValue = Date | string | number
DialogMsg from @llui/components
export type DialogMsg =
/** @intent("Open the dialog") */
| { type: 'open' }
/** @intent("Close the dialog") */
| { type: 'close' }
/** @intent("Toggle the dialog open/closed") */
| { type: 'toggle' }
/** @intent("Set the dialog's open state to a specific value") */
| { type: 'setOpen'; open: boolean }
/** @humanOnly */
| { type: 'animationEnd' }
/** @humanOnly */
| { type: 'transitionEnd' }
DismissSource from @llui/components
Reason a dismissable layer was closed.
export type DismissSource = 'escape' | 'outside';
DisplayNameType from @llui/components
export type DisplayNameType =
| 'language'
| 'region'
| 'script'
| 'currency'
| 'calendar'
| 'dateTimeField'
DrawerMsg from @llui/components
export type DrawerMsg =
/** @intent("Open the drawer") */
| { type: 'open' }
/** @intent("Close the drawer") */
| { type: 'close' }
/** @intent("Toggle the drawer open/closed") */
| { type: 'toggle' }
/** @intent("Set the drawer's open state to a specific value") */
| { type: 'setOpen'; open: boolean }
/** @humanOnly */
| { type: 'animationEnd' }
/** @humanOnly */
| { type: 'transitionEnd' }
DrawerSide from @llui/components
Drawer — a panel that slides in from a screen edge. Structurally
identical to dialog (portal + focus trap + dismissable + aria-hidden +
scroll lock), but adds a side so styling can animate from that edge.
export type DrawerSide = 'left' | 'right' | 'top' | 'bottom'
EditableMsg from @llui/components
export type EditableMsg =
/** @intent("Enter edit mode (seeds the draft from the current value)") */
| { type: 'edit' }
/** @intent("Update the in-progress draft as the user types") */
| { type: 'setDraft'; draft: string }
/** @intent("Commit the draft as the new value and exit edit mode") */
| { type: 'submit' }
/** @intent("Discard the draft and exit edit mode without changing the value") */
| { type: 'cancel' }
/** @intent("Set the value directly without going through edit mode") */
| { type: 'setValue'; value: string }
ElementSource from @llui/components
Shared DOM helpers used by interaction utilities.
export type ElementSource<T extends Element = Element> = T | T[] | (() => T | T[] | null)
ErrorCorrectionLevel from @llui/components
QR code — renders a QR matrix as SVG. llui does not bundle a QR
encoder (encoders are sizable and consumer apps typically already
have one); instead, the consumer provides the encoded matrix via
setMatrix (or through the optional encode callback on
ConnectOptions, invoked when the value changes).
Minimum usage with a BYOE (bring-your-own-encoder) library — the
consumer dispatches setMatrix with the encoded bits from their
update handler:
import QRCode from 'qrcode-generator'
update: (state, msg) => { if (msg.type === 'updateQr') { const q = QRCode(0, state.qr.errorCorrection) q.addData(msg.value); q.make() const n = q.getModuleCount() const matrix: boolean[][] = [] for (let y = 0; y < n; y++) { const row: boolean[] = [] for (let x = 0; x < n; x++) row.push(q.isDark(y, x)) matrix.push(row) } return [{ ...state, qr: { ...state.qr, value: msg.value, matrix } }, []] } }
export type ErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H'
FieldMsg from @llui/components
export type FieldMsg =
/** @intent("Mark the field as valid or invalid (drives aria-invalid + the error region)") */
| { type: 'setInvalid'; invalid: boolean }
/** @intent("Mark the field as required or optional") */
| { type: 'setRequired'; required: boolean }
/** @intent("Enable or disable the field's control") */
| { type: 'setDisabled'; disabled: boolean }
/** @intent("Make the field's control read-only or editable") */
| { type: 'setReadonly'; readonly: boolean }
/** @intent("Mark the field as touched (typically after first blur)") */
| { type: 'setTouched'; touched: boolean }
FieldsetMsg from @llui/components
export type FieldsetMsg =
/** @intent("Enable or disable the whole group (propagates to every contained control)") */
| { type: 'setDisabled'; disabled: boolean }
/** @intent("Mark the group as valid or invalid (drives the group-level error region)") */
| { type: 'setInvalid'; invalid: boolean }
FileError from @llui/components
export type FileError =
| { code: 'TOO_LARGE'; max: number }
| { code: 'TOO_SMALL'; min: number }
| { code: 'INVALID_TYPE' }
| { code: 'TOO_MANY'; max: number }
| { code: 'CUSTOM'; message: string }
FileUploadMsg from @llui/components
export type FileUploadMsg =
/** @humanOnly */
| { type: 'setFiles'; files: FileMeta[]; customRejected?: RejectedFile[] }
/** @humanOnly */
| { type: 'addFiles'; files: FileMeta[]; customRejected?: RejectedFile[] }
/** @intent("Remove the accepted file at the given index") */
| { type: 'removeFile'; index: number }
/** @intent("Remove the rejected file at the given index") */
| { type: 'removeRejected'; index: number }
/** @intent("Clear all accepted files") */
| { type: 'clear' }
/** @intent("Clear the rejected-files list") */
| { type: 'clearRejected' }
/** @humanOnly */
| { type: 'dragEnter' }
/** @humanOnly */
| { type: 'dragLeave' }
/** @humanOnly */
| { type: 'drop' }
/** @humanOnly */
| { type: 'setInvalid'; invalid: boolean }
FloatingPanelHandle from @llui/components
Floating panel — a draggable + resizable window-like surface, useful for dev tools overlays, pop-out inspectors, preview panels, etc. The state machine tracks position and size; the view layer wires pointer events on the drag handle and resize grips and dispatches the corresponding messages.
Coordinates are in pixels relative to the positioning container
(typically position: fixed relative to the viewport).
export type ResizeHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
FloatingPanelMsg from @llui/components
export type FloatingPanelMsg =
/** @intent("Open the floating panel") */
| { type: 'open' }
/** @intent("Close the floating panel") */
| { type: 'close' }
/** @intent("Minimize the panel (collapses to title bar)") */
| { type: 'minimize' }
/** @intent("Restore the panel from its minimized state") */
| { type: 'restoreFromMinimized' }
/** @intent("Maximize the panel (fills the viewport)") */
| { type: 'maximize' }
/** @intent("Restore the panel to its pre-maximize geometry") */
| { type: 'restoreFromMaximized' }
/** @intent("Toggle between minimized and normal") */
| { type: 'toggleMinimize' }
/** @intent("Toggle between maximized and normal") */
| { type: 'toggleMaximize' }
/** @humanOnly */
| { type: 'dragStart' }
/** @humanOnly */
| { type: 'dragMove'; dx: number; dy: number }
/** @humanOnly */
| { type: 'dragEnd' }
/** @humanOnly */
| { type: 'resizeStart'; handle: ResizeHandle }
/** @humanOnly */
| { type: 'resizeMove'; dx: number; dy: number }
/** @humanOnly */
| { type: 'resizeEnd' }
/** @intent("Set the panel's top-left position in pixels") */
| { type: 'setPosition'; x: number; y: number }
/** @intent("Set the panel's size in pixels (clamped to min/max)") */
| { type: 'setSize'; width: number; height: number }
FormMsg from @llui/components
export type FormMsg =
/** @intent("Mark a single field as touched (typically on blur)") */
| { type: 'touch'; field: string }
/** @intent("Mark every named field as touched (typically on submit attempt)") */
| { type: 'touchAll'; fields: string[] }
/** @intent("Begin form submission — validate and dispatch the save effect") */
| { type: 'submit' }
/** @intent("Mark the in-flight submission as successful") */
| { type: 'submitSuccess' }
/** @intent("Mark the in-flight submission as failed with the given error message") */
| { type: 'submitError'; error: string }
/** @intent("Reset the form to its initial state (clears touched flags and submit status)") */
| { type: 'reset' }
FormStatus from @llui/components
Form — submit lifecycle + touched tracking + Standard Schema validation.
Values live in the parent component's state; form tracks submit status
and which fields have been interacted with (blur), so errors are shown
only after touch instead of immediately.
Bring your own validation library — any Standard Schema-compatible schema works (Zod, Valibot, ArkType, etc.). See https://standardschema.dev.
import { z } from 'zod'
import { form, validateSchema } from '@llui/components/form'
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
type Values = z.infer<typeof schema>
type State = { values: Values; form: FormState }
update: (state, msg) => {
switch (msg.type) {
case 'submit': {
const result = validateSchema(schema, state.values)
if (!result.isValid) {
return [{ ...state, form: { ...state.form, touched: { email: true, password: true } } }, []]
}
return [{ ...state, form: { ...state.form, status: 'submitting' } }, [saveUserEffect]]
}
}
}
export type FormStatus = 'idle' | 'submitting' | 'submitted' | 'error'
HoverCardMsg from @llui/components
export type HoverCardMsg =
| { type: 'show' }
| { type: 'hide' }
| { type: 'setOpen'; open: boolean }
/** @humanOnly */
| { type: 'animationEnd' }
/** @humanOnly */
| { type: 'transitionEnd' }
ImageCropperMsg from @llui/components
export type ImageCropperMsg =
/** @humanOnly */
| { type: 'setImage'; width: number; height: number }
/** @intent("Set the crop rectangle (x/y/width/height in image-native pixels)") */
| { type: 'setCrop'; crop: CropRect }
/** @intent("Lock the crop to a specific aspect ratio (width/height), or null for free-form") */
| { type: 'setAspectRatio'; ratio: number | null }
/** @humanOnly */
| { type: 'dragStart' }
/** @humanOnly */
| { type: 'dragMove'; dx: number; dy: number }
/** @humanOnly */
| { type: 'dragEnd' }
/** @humanOnly */
| { type: 'resizeStart'; handle: ResizeHandle }
/** @humanOnly */
| { type: 'resizeMove'; dx: number; dy: number }
/** @humanOnly */
| { type: 'resizeEnd' }
/** @intent("Reset the crop to a default selection (full image or aspect-fit)") */
| { type: 'reset' }
/** @intent("Set the crop to a maximum-area centered selection") */
| { type: 'centerFill' }
ImageStatus from @llui/components
Avatar — image with automatic fallback. Tracks image load status so consumers can render the image, a fallback (initials, icon), or a loading placeholder.
export type ImageStatus = 'idle' | 'loading' | 'loaded' | 'error'
InViewMsg from @llui/components
export type InViewMsg = { type: 'enter' } | { type: 'leave' }
IsoDate from @llui/components
A calendar date as YYYY-MM-DD. Zero-padded, so plain </> order it.
export type IsoDate = string
ItemFill from @llui/components
export type ItemFill = 'full' | 'half' | 'empty'
ListboxMsg from @llui/components
export type ListboxMsg =
/** @intent("Pick the option with the given value (toggles in multi-select)") */
| { type: 'select'; value: string }
/** @intent("Replace the selected values with the provided list") */
| { type: 'setValue'; value: string[] }
/** @intent("Clear all selected values") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'highlight'; index: number | null }
/** @humanOnly */
| { type: 'highlightNext' }
/** @humanOnly */
| { type: 'highlightPrev' }
/** @humanOnly */
| { type: 'highlightFirst' }
/** @humanOnly */
| { type: 'highlightLast' }
/** @intent("Pick the currently-highlighted option") */
| { type: 'selectHighlighted' }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @humanOnly */
| { type: 'typeahead'; char: string; now: number }
MarqueeDirection from @llui/components
Marquee — continuously-scrolling content. The state machine tracks play/pause + direction + speed; the scrolling itself is driven by CSS animations or JS requestAnimationFrame (the consumer owns that).
Expose the active state via CSS custom properties the consumer reads in their stylesheet: --marquee-duration: {N}s --marquee-direction: 'normal' | 'reverse' --marquee-playstate: 'running' | 'paused'
export type MarqueeDirection = 'left' | 'right' | 'up' | 'down'
MarqueeMsg from @llui/components
export type MarqueeMsg =
/** @intent("Resume the marquee scrolling") */
| { type: 'play' }
/** @intent("Pause the marquee scrolling") */
| { type: 'pause' }
/** @intent("Toggle the marquee between playing and paused") */
| { type: 'toggle' }
/** @humanOnly */
| { type: 'hoverPause' }
/** @humanOnly */
| { type: 'hoverResume' }
/** @intent("Change the scroll direction (left/right/up/down)") */
| { type: 'setDirection'; direction: MarqueeDirection }
/** @intent("Change the loop duration in seconds (larger = slower)") */
| { type: 'setDuration'; durationSec: number }
MenubarMsg from @llui/components
export type MenubarMsg =
/** @intent("Open the menu with the given id and focus its first item") */
| { type: 'openMenu'; id: string }
/** @intent("Close the currently-open menu") */
| { type: 'closeMenu' }
/** @intent("Move roving focus to the menu with the given id (switches the open menu in open mode)") */
| { type: 'focusMenu'; id: string }
/** @humanOnly */
| { type: 'focusNext' }
/** @humanOnly */
| { type: 'focusPrev' }
/** @humanOnly */
| { type: 'menuMsg'; id: string; msg: MenuMsg }
MenuCheckItemParts from @llui/components
export type MenuCheckItemParts = MenuCheckItemPartsOf<'menu'>
MenuGroupParts from @llui/components
export type MenuGroupParts = MenuGroupPartsOf<'menu'>
MenuItem from @llui/components
A single node in the menu item tree (JSON-serializable). Shared with
context-menu (and menubar) via the {@link MenuNode} machine type.
export type MenuItem = MenuNode
MenuItemKind from @llui/components
Kind of a menu item.
export type MenuItemKind = MenuNodeKind
MenuItemParts from @llui/components
export type MenuItemParts = MenuItemPartsOf<'menu'>
MenuMsg from @llui/components
export type MenuMsg =
/** @intent("Open the menu") */
| { type: 'open' }
/** @intent("Close the menu") */
| { type: 'close' }
/** @intent("Toggle the menu open/closed") */
| { type: 'toggle' }
/** @humanOnly */
| { type: 'highlight'; level: string; value: string | null }
/** @humanOnly */
| { type: 'highlightNext'; level: string }
/** @humanOnly */
| { type: 'highlightPrev'; level: string }
/** @humanOnly */
| { type: 'highlightFirst'; level: string }
/** @humanOnly */
| { type: 'highlightLast'; level: string }
/** @intent("Activate the currently-highlighted item at the given level") */
| { type: 'selectHighlighted'; level: string }
/** @intent("Activate the menu item with the given value") */
| { type: 'select'; value: string }
/** @intent("Open the submenu for the given parent item") */
| { type: 'openSub'; value: string }
/** @intent("Close the deepest open submenu") */
| { type: 'closeSub' }
/** @humanOnly */
| { type: 'setItems'; items: MenuItem[] }
/** @humanOnly */
| { type: 'typeahead'; level: string; char: string; now: number }
/** @intent("Set the reading direction — 'ltr'/'rtl', or null to follow the page") */
| { type: 'setDir'; dir: TextDirection | null }
/** @humanOnly */
| { type: 'animationEnd' }
MenuSeparatorParts from @llui/components
export type MenuSeparatorParts = MenuSeparatorPartsOf<'menu'>
MenuSubContentParts from @llui/components
export type MenuSubContentParts = MenuSubContentPartsOf<'menu'>
MenuSubPositionerParts from @llui/components
export type MenuSubPositionerParts = MenuSubPositionerPartsOf<'menu'>
MenuSubTriggerParts from @llui/components
export type MenuSubTriggerParts = MenuSubTriggerPartsOf<'menu'>
MeterMsg from @llui/components
export type MeterMsg =
/** @humanOnly */
| { type: 'setValue'; value: number }
/** @humanOnly */
| { type: 'setMax'; max: number }
NavMenuMsg from @llui/components
export type NavMenuMsg =
/** @intent("Open the submenu identified by id, closing any open siblings") */
| { type: 'openBranch'; id: string; ancestorIds: string[] }
/** @intent("Close the submenu identified by id (also closes its descendants)") */
| { type: 'closeBranch'; id: string }
/** @intent("Toggle the submenu identified by id open/closed") */
| { type: 'toggleBranch'; id: string; ancestorIds: string[] }
/** @intent("Close every open submenu") */
| { type: 'closeAll' }
/** @humanOnly */
| { type: 'focus'; id: string | null }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
/** @intent("Replace the list of ids eligible for the roving tab stop, in document order") */
| { type: 'setItems'; items: string[] }
NestedLayerAspect from @llui/components
A consumer of the registry. A registration participates only in the aspects it
names, because a single answer is wrong for at least one consumer: engine
overlays leave outside to the ordered dismissable stack (see the module
comment).
The dialog-with-an-inner-select case is NOT what the aspect list protects.
That one is covered by a modal never registering AT ALL, whatever aspects it
would have named.
outside— {@link watchInteractOutside} does not treat interactions inside the layer as outside interactions.focus— {@link pushFocusTrap} includes the layer as an extra focusable container, so Tab/Shift+Tab can reach it.hide— {@link setAriaHiddenOutside} hides AROUND the layer rather than hiding it.
export type NestedLayerAspect = 'outside' | 'focus' | 'hide';
NestedLayerScope from @llui/components
The asking layer's own boundary — what "nested inside ME" is measured against. Omit it for the flat, layer-agnostic answer.
export type NestedLayerScope = ElementSource;
NumberInputMsg from @llui/components
export type NumberInputMsg =
/** @intent("Set the numeric value (clamped to min/max, snapped to step)") */
| { type: 'setValue'; value: number | null }
/** @humanOnly */
| { type: 'setRawText'; text: string }
/** @intent("Commit the in-progress text input — parse, clamp, snap, and update value. Ignored while disabled or readonly") */
| { type: 'commit' }
/** @intent("Increase value by step (or step × multiplier). Ignored while disabled or readonly") */
| { type: 'increment'; multiplier?: number }
/** @intent("Decrease value by step (or step × multiplier). Ignored while disabled or readonly") */
| { type: 'decrement'; multiplier?: number }
/** @intent("Snap value to the configured minimum. Ignored while disabled or readonly") */
| { type: 'toMin' }
/** @intent("Snap value to the configured maximum. Ignored while disabled or readonly") */
| { type: 'toMax' }
/** @intent("Enable or disable the input — a host/agent write, never gated") */
| { type: 'setDisabled'; disabled: boolean }
PageItem from @llui/components
export type PageItem =
| { type: 'page'; page: number }
| { type: 'ellipsis'; position: 'start' | 'end' }
PaginationMsg from @llui/components
export type PaginationMsg =
/** @intent("Jump to a specific 1-based page number") */
| { type: 'goTo'; page: number }
/** @intent("Advance to the next page") */
| { type: 'next' }
/** @intent("Go back to the previous page") */
| { type: 'prev' }
/** @intent("Jump to the first page") */
| { type: 'first' }
/** @intent("Jump to the last page") */
| { type: 'last' }
/** @intent("Change how many items each page contains") */
| { type: 'setPageSize'; pageSize: number }
/** @humanOnly */
| { type: 'setTotal'; total: number }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: TextDirection }
PasswordInputMsg from @llui/components
export type PasswordInputMsg =
/** @intent("Update the password value as the user types") */
| { type: 'setValue'; value: string }
/** @intent("Toggle the show/hide-password state") */
| { type: 'toggleVisibility' }
/** @intent("Set the show/hide-password state to a specific value") */
| { type: 'setVisible'; visible: boolean }
PinInputMsg from @llui/components
export type PinInputMsg =
/** @intent("Set the character at a given field index (auto-advances focus on accept)") */
| { type: 'setValue'; index: number; value: string }
/** @intent("Replace every field at once (typically from paste)") */
| { type: 'setAll'; values: string[] }
/** @humanOnly */
| { type: 'focus'; index: number }
/** @intent("Clear every field") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'backspace'; index: number }
/** @intent("Enable or disable the pin-input — a host/agent write, never gated") */
| { type: 'setDisabled'; disabled: boolean }
PinType from @llui/components
Pin input — a sequence of single-character fields for OTP codes, etc. Auto-advances on input, handles backspace to previous field, supports paste-to-fill across multiple fields.
export type PinType = 'numeric' | 'alphanumeric' | 'alphabetic'
Placement from @llui/components
export declare type Placement = Prettify<Side | AlignedPlacement>;
PluralCategory from @llui/components
export type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other'
PluralMessages from @llui/components
export type PluralMessages = Partial<Record<PluralCategory, string>> & { other: string }
PopoverMsg from @llui/components
export type PopoverMsg =
/** @intent("Open the popover") */
| { type: 'open' }
/** @intent("Close the popover") */
| { type: 'close' }
/** @intent("Toggle the popover open/closed") */
| { type: 'toggle' }
/** @intent("Set the popover's open state to a specific value") */
| { type: 'setOpen'; open: boolean }
/** @humanOnly */
| { type: 'animationEnd' }
/** @humanOnly */
| { type: 'transitionEnd' }
PresenceMsg from @llui/components
export type PresenceMsg =
/** @intent("Begin opening the element (closed → opening, plays enter animation)") */
| { type: 'open' }
/** @intent("Begin closing the element (open → closing, plays exit animation)") */
| { type: 'close' }
/** @intent("Toggle between open and closed states") */
| { type: 'toggle' }
/** @humanOnly */
| { type: 'animationEnd' }
/** @intent("Set the desired presence directly (true = open, false = closed)") */
| { type: 'setPresent'; present: boolean }
PresenceStatus from @llui/components
Presence — track mount/unmount lifecycle with exit-delay support.
In many components (dialogs, tooltips, menus) the consumer wants to:
- close the overlay (fire exit animation)
- keep it mounted long enough for the animation to finish
- unmount it
LLui already provides @llui/transitions for most of this, but a
presence machine is useful when you want to coordinate multiple
elements or expose state outside the transition primitive.
State flow: closed → (open) → opening → open open → (close) → closing → closed
The consumer fires animationEnd to advance past opening/closing.
If unmountOnExit is true, closed means "safe to remove from DOM";
otherwise the element stays mounted even when closed (display:none).
export type PresenceStatus = 'closed' | 'opening' | 'open' | 'closing'
ProgressMsg from @llui/components
export type ProgressMsg =
/** @humanOnly */
| { type: 'setValue'; value: number | null }
/** @humanOnly */
| { type: 'setMax'; max: number }
QrCodeMsg from @llui/components
export type QrCodeMsg =
/** @intent("Set the encoded value (consumer encodes externally and dispatches setMatrix)") */
| { type: 'setValue'; value: string }
/** @humanOnly */
| { type: 'setMatrix'; matrix: boolean[][] }
/** @intent("Change the QR error-correction level (L/M/Q/H)") */
| { type: 'setErrorCorrection'; level: ErrorCorrectionLevel }
RadioGroupMsg from @llui/components
export type RadioGroupMsg =
/** @intent("Pick the radio option with the given value") */
| { type: 'setValue'; value: string }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @intent("Move selection to the next enabled option after the given value") */
| { type: 'selectNext'; from: string }
/** @intent("Move selection to the previous enabled option before the given value") */
| { type: 'selectPrev'; from: string }
/** @intent("Select the first enabled option") */
| { type: 'selectFirst' }
/** @intent("Select the last enabled option") */
| { type: 'selectLast' }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
RatingGroupMsg from @llui/components
export type RatingGroupMsg =
/** @intent("Set the rating value directly (clamped to 0..count, snapped to 0.5 if allowHalf)") */
| { type: 'setValue'; value: number }
/** @humanOnly */
| { type: 'hover'; value: number | null }
/** @humanOnly */
| { type: 'clickItem'; index: number; isLeftHalf: boolean }
/** @humanOnly */
| { type: 'hoverItem'; index: number; isLeftHalf: boolean }
/** @intent("Increase the rating by step (default: 0.5 if allowHalf, else 1)") */
| { type: 'incrementValue'; step?: number }
/** @intent("Decrease the rating by step (default: 0.5 if allowHalf, else 1)") */
| { type: 'decrementValue'; step?: number }
/** @intent("Snap the rating to its maximum (count)") */
| { type: 'toEnd' }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
RelativeTimeUnit from @llui/components
export type RelativeTimeUnit =
| 'year'
| 'quarter'
| 'month'
| 'week'
| 'day'
| 'hour'
| 'minute'
| 'second'
ResolvedTheme from @llui/components
export type ResolvedTheme = 'light' | 'dark'
RovingMove from @llui/components
The navigation a key implies on a roving tablist.
export type RovingMove =
/** An arrow / Home / End resolved to a (different, enabled) tab value. */
{
type: 'focus';
value: string;
}
/** Enter or Space — activate the currently focused tab (manual mode). */
| {
type: 'activate';
};
RovingOrientation from @llui/components
Headless roving-tablist navigation — the keyboard logic of a WAI-ARIA tablist, decoupled from any particular DOM contract.
components/tabs.ts builds its reactive part-bags on top of this; a
consumer that wants its OWN markup (different classes, ids, no
data-scope/data-part) can drive the same keyboard behaviour by
calling resolveRovingMove from its trigger's onKeyDown and
focusRovingTab to move DOM focus — without adopting the component's
markup or its connect() state machine.
The resolver is pure (key + current value + items → a move); the only
shared DOM assumption lives in focusRovingTab, and it is the minimal
one both surfaces already satisfy: triggers carry role="tab" and
data-value="<value>".
The list walk itself lives in list-navigation.ts — this module is the
keyboard + DOM-focus surface over it, nothing more.
export type RovingOrientation = 'horizontal' | 'vertical';
ScrollAreaMsg from @llui/components
export type ScrollAreaMsg =
/** @humanOnly */
| {
type: 'setScroll'
scrollTop: number
scrollLeft: number
scrollWidth: number
scrollHeight: number
clientWidth: number
clientHeight: number
}
/** @humanOnly */
| { type: 'setScrolling'; scrolling: boolean }
/** @humanOnly */
| { type: 'setHovered'; hovered: boolean }
ScrollbarVisibility from @llui/components
Scroll area — custom-styled scroll container with scrollbars that
can be hidden/shown based on scroll activity or hover. The state
machine tracks scroll position + overflow flags (whether content
actually overflows each axis); the view layer installs listeners
that populate these via setScroll / setOverflow.
This component is primarily a structural shell — the real scrolling is done by the browser (overflow: auto) on the viewport element. The machine just tracks position so the view can render custom thumbs positioned proportionally.
Typical onMount wiring:
const viewport = root.querySelector('[data-part="viewport"]') const sync = () => send({type:'setScroll', ...dimsOf(viewport)}) viewport.addEventListener('scroll', sync) const ro = new ResizeObserver(sync); ro.observe(viewport); ro.observe(content) sync() // initial
export type ScrollbarVisibility = 'auto' | 'always' | 'hover' | 'scroll'
SearchFieldMsg from @llui/components
export type SearchFieldMsg =
/** @humanOnly */
| { type: 'setValue'; value: string }
/** @intent("Clear the search field") */
| { type: 'clear' }
/** @intent("Submit the current search query") */
| { type: 'submit'; value: string }
SelectionMode from @llui/components
Listbox — a list of selectable options. Supports single and multiple
selection, keyboard navigation (arrows, Home, End), typeahead, and
disabled items. Renders as role="listbox" with role="option" items.
export type SelectionMode = 'single' | 'multiple'
SelectMsg from @llui/components
export type SelectMsg =
/** @intent("Open the select dropdown") */
| { type: 'open' }
/** @intent("Close the select dropdown") */
| { type: 'close' }
/** @intent("Toggle the select dropdown open/closed") */
| { type: 'toggle' }
/** @intent("Pick the option with the given value (toggles in multi-select)") */
| { type: 'selectOption'; value: string }
/** @intent("Replace the selected values with the provided list") */
| { type: 'setValue'; value: string[] }
/** @intent("Clear all selected values") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'highlight'; value: string | null }
/** @humanOnly */
| { type: 'highlightNext' }
/** @humanOnly */
| { type: 'highlightPrev' }
/** @humanOnly */
| { type: 'highlightFirst' }
/** @humanOnly */
| { type: 'highlightLast' }
/** @intent("Pick the currently-highlighted option") */
| { type: 'selectHighlighted' }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @humanOnly */
| { type: 'typeahead'; char: string; now: number }
SignaturePadMsg from @llui/components
export type SignaturePadMsg =
/** @humanOnly */
| { type: 'strokeStart'; x: number; y: number; pressure?: number }
/** @humanOnly */
| { type: 'strokePoint'; x: number; y: number; pressure?: number }
/** @humanOnly */
| { type: 'strokeEnd' }
/** @humanOnly */
| { type: 'strokeCancel' }
/** @intent("Undo the last completed stroke") */
| { type: 'undo' }
/** @humanOnly */
| { type: 'redo'; stroke: Stroke }
/** @intent("Erase the entire signature") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'setStrokes'; strokes: Stroke[] }
SignatureStroke from @llui/components
export type Stroke = Point[]
SliderMsg from @llui/components
export type SliderMsg =
/** @intent("Replace all thumb values at once") */
| { type: 'setValue'; value: number[] }
/** @intent("Set the value of the thumb at the given index. Ignored while disabled") */
| { type: 'setThumb'; index: number; value: number }
/** @intent("Move the thumb at the given index up by one step (or step × multiplier). Ignored while disabled") */
| { type: 'increment'; index: number; multiplier?: number }
/** @intent("Move the thumb at the given index down by one step (or step × multiplier). Ignored while disabled") */
| { type: 'decrement'; index: number; multiplier?: number }
/** @intent("Snap the thumb at the given index to the slider's minimum. Ignored while disabled") */
| { type: 'toMin'; index: number }
/** @intent("Snap the thumb at the given index to the slider's maximum. Ignored while disabled") */
| { type: 'toMax'; index: number }
/** @intent("Enable or disable the slider — a host/agent write, never gated") */
| { type: 'setDisabled'; disabled: boolean }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
SortableMsg from @llui/components
export type SortableMsg =
/** @humanOnly */
| { type: 'start'; id: string; index: number; container: string; x: number; y: number }
/** @humanOnly */
| { type: 'move'; index: number; container: string; x: number; y: number }
/** @humanOnly */
| { type: 'drop' }
/** @humanOnly */
| { type: 'cancel' }
/** @humanOnly */
| { type: 'toggleGrab'; id: string; index: number; container: string }
/** @humanOnly */
| { type: 'moveBy'; delta: number }
SortDirection from @llui/components
Table / data grid — a headless machine for sortable columns, row
selection, and APG grid keyboard navigation. It is NOT a rendering
engine: row DATA stays in the consumer; the machine tracks only row
IDs (in display order), sort state, the selected-id set, and the
focused cell coordinate. The consumer renders the grid (via each or
virtualEach) and performs the actual data sort — so server-side sort
works by feeding pre-sorted rows back in. focusedCell is addressed
by index, robust to virtualization.
export type SortDirection = 'asc' | 'desc'
SplitterMsg from @llui/components
export type SplitterMsg =
/** @intent("Set the splitter handle position (0–100, clamped to min/max)") */
| { type: 'setPosition'; position: number }
/** @intent("Move the handle by step (or step × multiplier) toward max") */
| { type: 'increment'; multiplier?: number }
/** @intent("Move the handle by step (or step × multiplier) toward min") */
| { type: 'decrement'; multiplier?: number }
/** @intent("Snap the handle to its minimum position") */
| { type: 'toMin' }
/** @intent("Snap the handle to its maximum position") */
| { type: 'toMax' }
/** @humanOnly */
| { type: 'startDrag' }
/** @humanOnly */
| { type: 'endDrag' }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
StepsMsg from @llui/components
export type StepsMsg =
/** @intent("Jump to a specific step by zero-based index") */
| { type: 'goTo'; step: number }
/** @intent("Advance to the next step") */
| { type: 'next' }
/** @intent("Go back to the previous step") */
| { type: 'prev' }
/** @intent("Mark the given step as completed") */
| { type: 'complete'; step: number }
/** @intent("Mark the given step as having an error") */
| { type: 'markError'; step: number }
/** @intent("Clear the error flag on the given step") */
| { type: 'clearError'; step: number }
/** @intent("Reset progress back to the first step (clears completed and errors)") */
| { type: 'reset' }
StepStatus from @llui/components
Steps — progress indicator for multi-step flows (wizards, checkouts). Tracks current step and completed steps; supports linear and non-linear navigation.
export type StepStatus = 'pending' | 'current' | 'completed' | 'error'
SwitchMsg from @llui/components
export type SwitchMsg =
/** @intent("Flip the switch on/off") */
| { type: 'toggle' }
/** @intent("Set the switch's checked state to a specific value") */
| { type: 'setChecked'; checked: boolean }
/** @humanOnly */
| { type: 'setDisabled'; disabled: boolean }
SyncEngineFocusBodyRequired from @llui/components
The type an ASYNC body collapses to in {@link runEngineFocus}'s parameter
position. Nothing is assignable to it, so runEngineFocus(async () => …) is a
compile error naming the contract rather than a silently inert call.
export type SyncEngineFocusBodyRequired = {
readonly [SYNC_BODY_REQUIRED]: 'runEngineFocus requires a SYNCHRONOUS body — the guard is released the moment body returns';
};
TableMsg from @llui/components
export type TableMsg =
/** @intent("Cycle the sort on the given column (asc → desc → none, or desc → asc → none when descFirst)") */
| { type: 'toggleSort'; columnId: string }
/** @intent("Set an explicit sort, or null to clear sorting") */
| { type: 'setSort'; sort: TableSort | null }
/** @intent("Toggle selection of the row with the given id at the given display index") */
| { type: 'toggleRow'; id: string; index: number }
/** @intent("Select every row (multiple mode only)") */
| { type: 'selectAll' }
/** @intent("Clear the entire selection") */
| { type: 'clearSelection' }
/** @intent("Toggle between select-all and clear, based on whether every row is selected") */
| { type: 'toggleAll' }
/** @intent("Replace the selected-id set with the provided list") */
| { type: 'setSelection'; ids: string[] }
/** @intent("Select the inclusive range from the current anchor to the given index (Shift+click)") */
| { type: 'selectRange'; index: number }
/** @intent("Activate (open/confirm) the row with the given id at the given index") */
| { type: 'activateRow'; id: string; index: number }
/** @intent("Replace the row-id list (display order); drops selection for ids no longer present") */
| { type: 'setRows'; rows: string[] }
/** @intent("Replace the column descriptors") */
| { type: 'setColumns'; columns: TableColumn[] }
/** @humanOnly */
| { type: 'focusCell'; rowIndex: number; colIndex: number }
/** @humanOnly */
| { type: 'moveCell'; dRow: number; dCol: number }
/** @humanOnly */
| { type: 'rowStart' }
/** @humanOnly */
| { type: 'rowEnd' }
/** @humanOnly */
| { type: 'gridStart' }
/** @humanOnly */
| { type: 'gridEnd' }
/** @humanOnly */
| { type: 'pageDown' }
/** @humanOnly */
| { type: 'pageUp' }
TableSelectionMode from @llui/components
export type TableSelectionMode = 'none' | 'single' | 'multiple'
TabsMsg from @llui/components
export type TabsMsg =
/** @intent("Switch to the tab with the given value") */
| { type: 'setValue'; value: string }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @humanOnly */
| { type: 'focusTab'; value: string }
/**
* @intent("Activate the tab with the given value — the click/press action (deselects it when `deselectable` and it is already active)")
*/
| { type: 'activateTab'; value: string }
/** @humanOnly */
| { type: 'focusNext'; from: string }
/** @humanOnly */
| { type: 'focusPrev'; from: string }
/** @humanOnly */
| { type: 'focusFirst' }
/** @humanOnly */
| { type: 'focusLast' }
/** @intent("Activate the currently-focused tab (for manual activation mode)") */
| { type: 'activateFocused' }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
TagsInputMsg from @llui/components
export type TagsInputMsg =
/** @intent("Update the in-progress text in the input field") */
| { type: 'setInput'; value: string }
/** @intent("Commit a new tag (defaults to the current input value)") */
| { type: 'addTag'; value?: string }
/** @intent("Remove the tag at the given index") */
| { type: 'removeTag'; index: number }
/** @intent("Remove the last tag (typically backspace on empty input)") */
| { type: 'removeLast' }
/** @intent("Replace the full tag list with the provided values") */
| { type: 'setValue'; value: string[] }
/** @humanOnly */
| { type: 'focusTag'; index: number | null }
/** @humanOnly */
| { type: 'focusTagNext' }
/** @humanOnly */
| { type: 'focusTagPrev' }
/** @intent("Remove every tag and reset the input") */
| { type: 'clearAll' }
TextDirection from @llui/components
Text reading direction. The single shared RTL vocabulary for the package.
export type TextDirection = 'ltr' | 'rtl';
Theme from @llui/components
Theme Switch — light/dark/system theme toggle.
State machine tracks the user's explicit preference (light, dark, or
system). Use resolveTheme() to compute the effective theme (reading
prefers-color-scheme when system), and applyTheme() to set
data-theme on <html> so CSS selectors like [data-theme='dark'] work.
Typically wired via onMount or in app init:
onMount(() => {
applyTheme(resolveTheme(state.theme.theme))
})
For persistence, the app reducer reads/writes localStorage.theme in its
init/update — the state machine itself is storage-agnostic.
export type Theme = 'light' | 'dark' | 'system'
ThemeSwitchMsg from @llui/components
export type ThemeSwitchMsg = { type: 'setTheme'; theme: Theme } | { type: 'toggle' }
TimeFormat from @llui/components
Time picker — hours and minutes input with increment/decrement buttons. 12 or 24-hour format; optional seconds; step for minutes/seconds.
export type TimeFormat = '12' | '24'
TimePickerMsg from @llui/components
export type TimePickerMsg =
/** @intent("Set the full time value (hours/minutes/seconds)") */
| { type: 'setValue'; value: TimeValue }
/** @intent("Set the hours field directly — 0-23 in 24-hour format, 1-12 in 12-hour format where the current AM/PM is kept") */
| { type: 'setHours'; hours: number }
/** @intent("Set the minutes field directly") */
| { type: 'setMinutes'; minutes: number }
/** @intent("Set the seconds field directly") */
| { type: 'setSeconds'; seconds: number }
/** @intent("Bump hours up by 1 (wraps at 24/12). Ignored while disabled") */
| { type: 'incrementHours' }
/** @intent("Bump hours down by 1. Ignored while disabled") */
| { type: 'decrementHours' }
/** @intent("Bump minutes up by minuteStep. Ignored while disabled") */
| { type: 'incrementMinutes' }
/** @intent("Bump minutes down by minuteStep. Ignored while disabled") */
| { type: 'decrementMinutes' }
/** @intent("Flip between AM and PM (12-hour format only). Ignored while disabled") */
| { type: 'toggleAmPm' }
/** @intent("Enable or disable the time-picker — a host/agent write, never gated") */
| { type: 'setDisabled'; disabled: boolean }
TimerDirection from @llui/components
Timer — counts elapsed time up from zero, or down from a configured
target. The machine is pure: it doesn't own the ticking interval.
The consumer runs setInterval(() => send({type:'tick', now: Date.now()}), 100)
(or whatever granularity) while the timer is running, and dispatches
start / pause / reset in response to user input.
// `div`, `button`, `text`, `mapSend` are imports from '@llui/dom'.
view: ({ state, send }) => {
const timerState = state.at('timer')
const timerSend = mapSend<Msg, timer.TimerMsg>(send, (msg) => ({
type: 'timer',
msg,
}))
const t = timer.connect(timerState, timerSend)
return [
div({ ...t.root }, [
div({ ...t.display }, [
text(timerState.map((s) => timer.formatMs(timer.display(s), 'mm:ss'))),
]),
button({ ...t.startTrigger }, [text('Start')]),
button({ ...t.pauseTrigger }, [text('Pause')]),
button({ ...t.resetTrigger }, [text('Reset')]),
]),
]
}
export type Direction = 'up' | 'down'
TimerMsg from @llui/components
export type TimerMsg =
/** @intent("Start (or resume) the timer running") */
| { type: 'start'; now: number }
/** @intent("Pause the timer (preserves accumulated elapsed time)") */
| { type: 'pause'; now: number }
/** @intent("Reset the timer back to zero elapsed and pause it") */
| { type: 'reset' }
/** @humanOnly */
| { type: 'tick'; now: number }
/** @intent("Set the countdown target (in milliseconds; 0 disables countdown)") */
| { type: 'setTarget'; targetMs: number }
ToasterMsg from @llui/components
export type ToasterMsg =
/** @intent("Show a new toast notification") */
| { type: 'create'; toast: ToastInput }
/** @intent("Dismiss the toast with the given id") */
| { type: 'dismiss'; id: string }
/** @intent("Dismiss every toast currently visible") */
| { type: 'dismissAll' }
/** @intent("Patch fields on the toast with the given id (title, description, type, etc.)") */
| { type: 'update'; id: string; patch: Partial<Toast> }
/** @humanOnly Advance the countdown for one toast by `elapsedMs` since the last tick. */
| { type: 'tick'; id: string; elapsedMs: number }
/** @intent("Pause auto-dismiss countdown for the toast with the given id") */
| { type: 'pause'; id: string }
/** @intent("Resume auto-dismiss countdown for the toast with the given id") */
| { type: 'resume'; id: string }
/** @intent("Pause auto-dismiss for every visible toast") */
| { type: 'pauseAll' }
/** @intent("Resume auto-dismiss for every visible toast") */
| { type: 'resumeAll' }
/** @humanOnly Exit animation finished for the toast with the given id — remove it from the queue. */
| { type: 'animationEnd'; id: string }
ToastInput from @llui/components
A new toast as supplied to create. remainingMs/paused/status are
optional — seeded from duration/false/'open' when omitted.
export type ToastInput = Omit<Toast, 'remainingMs' | 'paused' | 'status'> & {
remainingMs?: number
paused?: boolean
status?: PresenceStatus
}
ToastPlacement from @llui/components
export type ToastPlacement =
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
ToastPoliteness from @llui/components
aria-live politeness for a toast's announcement region.
export type ToastPoliteness = 'polite' | 'assertive'
ToastType from @llui/components
Toast — ephemeral non-modal notifications rendered in a fixed region. Multiple toasts can be active at once. Each has a duration after which it auto-dismisses (unless paused or sticky).
Architecture (timer-free, tick-driven — same division of labor as timer.ts):
toast.toasterstate manages a collection of toasts. Each toast carries its own countdown in state:duration(null = sticky),remainingMs, andpaused.- The machine owns NO interval. The consumer drives the countdown with a
tick(id, elapsedMs)message (e.g. via @llui/effectsinterval), subtracting the elapsed wall time since the last tick. Apausedtoast freezes itsremainingMs(ticks are ignored). - When
remainingMshits 0 the REDUCER dismisses that toast itself, so there is no consumer/runtime race over who removes it.
Presence (exit animation) — additive, opt-in via init({ animated: true }):
- Each toast carries a presence
status(closed/opening/open/closing). A freshly created toast is born'open'(no enter-animation gate — toasts appear immediately). - Dismissing a toast (manually or when its countdown reaches 0) moves it to
'closing'and KEEPS IT MOUNTED so it can play an exit animation; ananimationEnd(id)message then removes it from the queue. - When the toaster is NOT animated, dismiss removes the toast SYNCHRONOUSLY (today's behavior) — never waiting for an animationend that won't fire.
export type ToastType = 'info' | 'success' | 'warning' | 'error' | 'loading' | 'custom'
TocMsg from @llui/components
export type TocMsg =
/** @humanOnly */
| { type: 'setItems'; items: TocEntry[] }
/** @humanOnly */
| { type: 'setActive'; id: string | null }
/** @intent("Toggle the expanded state of the entry with the given id") */
| { type: 'toggleExpanded'; id: string }
/** @intent("Expand every collapsible entry") */
| { type: 'expandAll' }
/** @intent("Collapse every expanded entry") */
| { type: 'collapseAll' }
ToggleGroupMsg from @llui/components
export type ToggleGroupMsg =
/** @intent("Toggle the button with the given value (in single mode, replaces selection)") */
| { type: 'toggle'; value: string }
/** @intent("Replace the pressed-value set with the provided list") */
| { type: 'setValue'; value: string[] }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @humanOnly */
| { type: 'focusNext'; from: string }
/** @humanOnly */
| { type: 'focusPrev'; from: string }
/** @humanOnly */
| { type: 'focusItem'; value: string }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
ToggleMsg from @llui/components
export type ToggleMsg =
/** @intent("Flip the toggle button's pressed state") */
| { type: 'toggle' }
/** @intent("Set the toggle's pressed state to a specific value") */
| { type: 'setPressed'; pressed: boolean }
/** @humanOnly */
| { type: 'setDisabled'; disabled: boolean }
ToolbarMsg from @llui/components
export type ToolbarMsg =
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @humanOnly */
| { type: 'setFocused'; value: string }
/** @humanOnly */
| { type: 'focusNext'; from: string }
/** @humanOnly */
| { type: 'focusPrev'; from: string }
/** @humanOnly */
| { type: 'focusFirst' }
/** @humanOnly */
| { type: 'focusLast' }
TooltipMsg from @llui/components
export type TooltipMsg =
/** @intent("Show the tooltip") */
| { type: 'show' }
/** @intent("Hide the tooltip") */
| { type: 'hide' }
/** @intent("Toggle the tooltip's visibility") */
| { type: 'toggle' }
/** @intent("Set the tooltip's open state to a specific value") */
| { type: 'setOpen'; open: boolean }
/** @humanOnly */
| { type: 'animationEnd' }
TourMsg from @llui/components
export type TourMsg =
/** @intent("Begin the tour at the first step (or current index if resuming)") */
| { type: 'start' }
/** @intent("Close the tour without finishing (does not reset progress)") */
| { type: 'stop' }
/** @intent("Advance to the next step (closes the tour after the last step)") */
| { type: 'next' }
/** @intent("Go back to the previous step") */
| { type: 'prev' }
/** @intent("Jump to a specific step by zero-based index") */
| { type: 'goto'; index: number }
/** @humanOnly */
| { type: 'setSteps'; steps: TourStep[] }
TreeViewEffect from @llui/components
Effects emitted by the tree-view machine for the consumer's onEffect.
export type TreeViewEffect =
/** Fetch the children of `id` lazily, then reply with `childrenLoaded`/`childrenLoadFailed`. */
{ type: 'loadChildren'; id: string }
TreeViewMsg from @llui/components
export type TreeViewMsg =
/** @intent("Toggle the branch with the given id expanded/collapsed") */
| { type: 'toggleBranch'; id: string }
/** @intent("Expand the branch with the given id") */
| { type: 'expand'; id: string }
/** @intent("Collapse the branch with the given id") */
| { type: 'collapse'; id: string }
/** @intent("Expand every branch in the provided id list") */
| { type: 'expandAll'; ids: string[] }
/** @intent("Collapse every expanded branch") */
| { type: 'collapseAll' }
/** @intent("Select the item with the given id (additive=true extends multi-selection)") */
| { type: 'select'; id: string; additive?: boolean }
/** @intent("Replace the selected-id set with the provided list") */
| { type: 'setSelected'; ids: string[] }
/** @humanOnly */
| { type: 'focus'; id: string | null }
/** @humanOnly */
| { type: 'focusNext' }
/** @humanOnly */
| { type: 'focusPrev' }
/** @humanOnly */
| { type: 'focusFirst' }
/** @humanOnly */
| { type: 'focusLast' }
/** @humanOnly */
| { type: 'setVisibleItems'; ids: string[]; labels?: string[] }
/** @humanOnly */
| { type: 'typeahead'; char: string; now: number }
/** @humanOnly */
| { type: 'arrowLeftFrom'; id: string; isBranch: boolean; parentId: string | null }
/** @humanOnly */
| { type: 'arrowRightFrom'; id: string }
/** @intent("Toggle the checkbox on the item with the given id (descendantIds drives recursive check)") */
| { type: 'toggleChecked'; id: string; descendantIds?: string[] }
/** @intent("Replace the checked-id set with the provided list") */
| { type: 'setChecked'; ids: string[] }
/** @humanOnly */
| { type: 'setIndeterminate'; ids: string[] }
/** @intent("Begin renaming the item with the given id (seeds the rename input with `initial`)") */
| { type: 'renameStart'; id: string; initial: string }
/** @intent("Update the rename draft as the user types") */
| { type: 'renameChange'; value: string }
/** @intent("Commit the in-progress rename (clears the rename state)") */
| { type: 'renameCommit' }
/** @intent("Cancel the in-progress rename without applying changes") */
| { type: 'renameCancel' }
/** @intent("Mark the branch with the given id as loading children (typically before an async fetch)") */
| { type: 'loadingStart'; id: string }
/** @intent("Clear the loading state for the given branch id (after async fetch completes)") */
| { type: 'loadingEnd'; id: string }
/** @intent("Replace the whole tree structure (adjacency record + root ids)") */
| { type: 'setNodes'; nodes: Record<string, TreeNodeMeta>; roots: string[] }
/** @intent("Supply the lazily-loaded children of branch `id` (clears loading, marks loaded)") */
| { type: 'childrenLoaded'; id: string; items: TreeNodeInput[] }
/** @intent("Report that the lazy load of branch `id` failed (allows retry on re-expand)") */
| { type: 'childrenLoadFailed'; id: string }
VisibleBreadcrumb from @llui/components
export type VisibleBreadcrumb =
| { type: 'item'; id: string; label: string; current: boolean }
| { type: 'ellipsis' }
Interfaces
AccordionInit from @llui/components
export interface AccordionInit {
value?: string[]
multiple?: boolean
collapsible?: boolean
disabled?: boolean
items?: string[]
}
AccordionItemParts from @llui/components
export interface AccordionItemParts {
trigger: {
type: 'button'
'aria-expanded': Signal<boolean>
'aria-controls': string
id: string
'data-state': Signal<'open' | 'closed'>
'data-disabled': Signal<'' | undefined>
disabled: Signal<boolean>
'data-scope': 'accordion'
'data-part': 'trigger'
'data-value': string
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
content: {
role: 'region'
id: string
'aria-labelledby': string
'data-state': Signal<'open' | 'closed'>
'data-scope': 'accordion'
'data-part': 'content'
hidden: Signal<boolean>
}
item: {
'data-state': Signal<'open' | 'closed'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'accordion'
'data-part': 'item'
'data-value': string
}
}
AccordionParts from @llui/components
export interface AccordionParts {
root: {
// No role on the root: an accordion is a set of disclosure buttons, and
// labelling the whole container `region` (without an accessible name) just
// adds an unlabeled landmark. The meaningful regions are the per-item
// panels, each a `role="region"` with `aria-labelledby` its trigger.
'data-scope': 'accordion'
'data-part': 'root'
'data-orientation': 'vertical'
}
item: (value: string) => AccordionItemParts
}
AccordionState from @llui/components
Accordion — a stack of expandable panels. Items are identified by a string
value. Either a single item is expandable at a time (default) or many
(multiple: true). collapsible: false prevents closing the only open
item in single mode.
Items themselves are provided by the user's view (accordion is agnostic to
item data). The connect() API returns a root prop set and an item(value)
factory that produces trigger and content prop sets scoped to that item.
export interface AccordionState {
/** Values of currently-expanded items. */
value: string[]
multiple: boolean
collapsible: boolean
disabled: boolean
/** Ordered list of item values (for keyboard navigation). */
items: string[]
}
AlertDialogOverlayOptions from @llui/components
export interface AlertDialogOverlayOptions {
/**
* Class applied to the positioner — the wrapper `div` `dialog.overlay` builds
* around the content. Forwarded verbatim; the consumer supplies the layer's
* `fixed inset-0` and `z-index`, since the part bag carries only `data-*`.
*
* This interface does not spread `DialogOverlayOptions`, so a new dialog
* option has to be restated here to reach `dialogOverlay` through the `...opts`
* below — which is exactly how this one was missed the first time.
*/
positionerClass?: string
state: Signal<DialogState>
send: Send<DialogMsg>
parts: AlertDialogParts
content: () => Renderable
/**
* Optional enter/leave transition for the alert-dialog content (from
* `@llui/transitions`), forwarded to `dialog.overlay`. `enter` animates it in
* on open; `leave` defers the unmount until its promise resolves, so the close
* plays an exit animation. Keep `skipAnimations` at its default (true) here.
*
* @example alertDialog.overlay({ state, send, parts, content, transition: fade({ duration: 150 }) })
*/
transition?: TransitionOptions
closeOnEscape?: boolean
/** Whether outside-click should dismiss (default: false for alert dialogs). */
closeOnOutsideClick?: boolean
trapFocus?: boolean
lockScroll?: boolean
hideSiblings?: boolean
target?: string | HTMLElement
initialFocus?: Element | (() => Element | null)
restoreFocus?: boolean
}
AlertDialogState from @llui/components
Dialog — modal / non-modal overlay. Ties together focus-trap, dismissable, body scroll lock, sibling aria-hidden, and portal-to-body rendering into a single view helper.
Two layers:
- state machine (
init,update,connect) — pure, minimal. overlay()view helper — opens the dialog's DOM tree inside a body portal, wires up all accessibility utilities on mount, tears them down on close, restores focus to the trigger.
view: ({ state, send }) => {
const dialogState = state.at('dialog')
const dialogSend = mapSend<Msg, dialog.DialogMsg>(send, (msg) => ({
type: 'dialog',
msg,
}))
const parts = dialog.connect(dialogState, dialogSend, { id: 'dialog' })
return [
button({ ...parts.trigger, class: 'btn' }, [text('Delete')]),
dialog.overlay({
state: dialogState,
send: dialogSend,
parts,
content: () => [
div({ ...parts.content, class: 'dialog' }, [
h2({ ...parts.title }, [text('Are you sure?')]),
button({ ...parts.closeTrigger, class: 'btn' }, [text('Cancel')]),
]),
],
}),
]
}
export interface DialogState {
open: boolean
/** Presence lifecycle — drives data-state and keeps the node mounted through exit
* animations. Optional: a partial `{ open }` bridge (e.g. a pattern passing a
* slice to `overlay`) omits it, and the runtime falls back to `open` for instant,
* backward-compatible mount/visibility. `init` always sets it. */
status?: PresenceStatus
/** When true, close transitions go straight to 'closed' (no exit-animation wait).
* Optional for the same partial-slice reason as `status`; `init` always sets it. */
skipAnimations?: boolean
}
Anatomy from @llui/components
export interface Anatomy<P extends string> {
readonly name: string
readonly parts: readonly P[]
/** Create a new scope instance. Pass an explicit id to force a value (SSR). */
scope(id?: string): AnatomyScope<P>
}
AnatomyScope from @llui/components
export interface AnatomyScope<P extends string> {
/** Instance id — unique across all anatomy scopes. */
readonly id: string
/** Resolve the id for a specific part (for ARIA wiring). */
idFor(part: P): string
/** Build the common data-attrs + id for a part. */
attrs(part: P): { id: string; 'data-scope': string; 'data-part': P }
}
AngleSliderInit from @llui/components
export interface AngleSliderInit {
value?: number
min?: number
max?: number
step?: number
disabled?: boolean
readonly?: boolean
dir?: 'ltr' | 'rtl'
}
AngleSliderParts from @llui/components
export interface AngleSliderParts {
root: {
role: 'slider'
'aria-valuemin': Signal<number>
'aria-valuemax': Signal<number>
'aria-valuenow': Signal<number>
'aria-valuetext': Signal<string>
'aria-orientation': 'horizontal'
'aria-disabled': Signal<'true' | undefined>
'aria-readonly': Signal<'true' | undefined>
tabindex: Signal<number>
'data-scope': 'angle-slider'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
onKeyDown: (e: KeyboardEvent) => void
}
control: {
'data-scope': 'angle-slider'
'data-part': 'control'
}
/**
* The draggable thumb element. Its position is typically computed via
* CSS custom properties `--angle` (0..360) that the consumer sets from
* `state.value` using pointFromAngle() or a CSS `transform: rotate()`.
*/
thumb: {
'data-scope': 'angle-slider'
'data-part': 'thumb'
'data-value': Signal<string>
}
valueText: {
'data-scope': 'angle-slider'
'data-part': 'value-text'
}
/** A hidden input for form participation. */
hiddenInput: {
type: 'hidden'
value: Signal<string>
name?: string
'data-scope': 'angle-slider'
'data-part': 'hidden-input'
}
}
AngleSliderState from @llui/components
Angle slider — a circular input that selects a value in 0..360 degrees by dragging a thumb around a control. The state machine tracks the current angle; the view layer computes angles from pointer positions (helpers exported for that purpose).
Typical view wiring: on pointerdown/pointermove, read the control
element's bounding rect, compute the angle from (pointerX, pointerY)
to the rect center via angleFromPoint(), and dispatch setValue.
Keyboard: Arrow keys adjust by step; Home/End jump to min/max;
PageUp/PageDown adjust by step * 10.
export interface AngleSliderState {
value: number
min: number
max: number
step: number
disabled: boolean
readonly: boolean
/** Reading direction. Under 'rtl' horizontal arrow keys are flipped. */
dir: 'ltr' | 'rtl'
}
AsyncListInit from @llui/components
export interface AsyncListInit<T = unknown> {
items?: T[]
page?: number
hasMore?: boolean
}
AsyncListParts from @llui/components
export interface AsyncListParts {
root: {
'data-scope': 'async-list'
'data-part': 'root'
'data-status': Signal<AsyncStatus>
}
sentinel: {
'data-scope': 'async-list'
'data-part': 'sentinel'
'aria-hidden': 'true'
}
loadMoreTrigger: {
type: 'button'
disabled: Signal<boolean>
'data-scope': 'async-list'
'data-part': 'load-more-trigger'
onClick: (e: MouseEvent) => void
}
retryTrigger: {
type: 'button'
'data-scope': 'async-list'
'data-part': 'retry-trigger'
hidden: Signal<boolean>
onClick: (e: MouseEvent) => void
}
errorText: {
role: 'alert'
'aria-live': 'polite'
'data-scope': 'async-list'
'data-part': 'error-text'
hidden: Signal<boolean>
}
}
AsyncListState from @llui/components
export interface AsyncListState<T = unknown> {
items: T[]
page: number
hasMore: boolean
status: AsyncStatus
error: string | null
}
AvatarInit from @llui/components
export interface AvatarInit {
status?: ImageStatus
}
AvatarParts from @llui/components
export interface AvatarParts {
root: {
'data-scope': 'avatar'
'data-part': 'root'
'data-status': Signal<ImageStatus>
}
image: {
'data-scope': 'avatar'
'data-part': 'image'
'data-status': Signal<ImageStatus>
hidden: Signal<boolean>
alt: string
onLoad: (e: Event) => void
onError: (e: Event) => void
onLoadStart: (e: Event) => void
}
fallback: {
'data-scope': 'avatar'
'data-part': 'fallback'
'data-status': Signal<ImageStatus>
hidden: Signal<boolean>
'aria-hidden': Signal<'true' | undefined>
}
}
AvatarState from @llui/components
export interface AvatarState {
status: ImageStatus
}
BreadcrumbItem from @llui/components
Breadcrumbs — a hierarchical trail of links to ancestor pages.
The last item is the current page. When maxVisible is set and the trail
is longer, the middle collapses to: first item + ellipsis + last N items,
until the user expands it.
export interface BreadcrumbItem {
id: string
label: string
}
BreadcrumbsInit from @llui/components
export interface BreadcrumbsInit {
items?: BreadcrumbItem[]
maxVisible?: number | null
expanded?: boolean
}
BreadcrumbsParts from @llui/components
export interface BreadcrumbsParts {
root: {
'aria-label': string
'data-scope': 'breadcrumbs'
'data-part': 'root'
}
list: {
'data-scope': 'breadcrumbs'
'data-part': 'list'
}
item: (id: string) => {
'data-scope': 'breadcrumbs'
'data-part': 'item'
'data-value': string
}
link: (id: string) => {
'aria-current': Signal<'page' | undefined>
'data-scope': 'breadcrumbs'
'data-part': 'link'
'data-value': string
'data-current': Signal<'' | undefined>
}
separator: {
'aria-hidden': 'true'
'data-scope': 'breadcrumbs'
'data-part': 'separator'
}
ellipsisTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'breadcrumbs'
'data-part': 'ellipsis-trigger'
onClick: (e: MouseEvent) => void
}
}
BreadcrumbsState from @llui/components
export interface BreadcrumbsState {
items: BreadcrumbItem[]
maxVisible: number | null
expanded: boolean
}
CarouselDrag from @llui/components
Live pointer-swipe state. JSON-serializable: just the start X and the accumulated horizontal delta (positive = dragged right, negative = left). The view supplies pointer coordinates; the machine does pure math.
export interface CarouselDrag {
startX: number
deltaX: number
}
CarouselInit from @llui/components
export interface CarouselInit {
current?: number
count?: number
loop?: boolean
autoplay?: boolean
interval?: number
swipeThreshold?: number
dir?: 'ltr' | 'rtl'
}
CarouselParts from @llui/components
export interface CarouselParts {
root: {
role: 'region'
'aria-roledescription': 'carousel'
'aria-label': string
'data-scope': 'carousel'
'data-part': 'root'
'data-paused': Signal<'' | undefined>
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onFocus: (e: FocusEvent) => void
onBlur: (e: FocusEvent) => void
}
viewport: {
'data-scope': 'carousel'
'data-part': 'viewport'
/**
* Set while a pointer swipe is in flight — consumers gate the slide-track
* transition off (`[data-dragging] { transition: none }`) so the track
* follows the finger 1:1 instead of easing.
*/
'data-dragging': Signal<'' | undefined>
/** Live track offset (px) to follow the finger: `translateX(var)`. */
'data-drag-offset': Signal<string | undefined>
onPointerDown: (e: PointerEvent) => void
onPointerMove: (e: PointerEvent) => void
onPointerUp: (e: PointerEvent) => void
onPointerCancel: (e: PointerEvent) => void
}
indicatorGroup: {
role: 'tablist'
'aria-label': string
'data-scope': 'carousel'
'data-part': 'indicator-group'
}
nextTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'carousel'
'data-part': 'next-trigger'
onClick: (e: MouseEvent) => void
}
prevTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'carousel'
'data-part': 'prev-trigger'
onClick: (e: MouseEvent) => void
}
slide: (index: number) => CarouselSlideParts
}
CarouselSlideParts from @llui/components
export interface CarouselSlideParts {
slide: {
role: 'tabpanel'
id: string
'aria-roledescription': 'slide'
'aria-label': string
'data-scope': 'carousel'
'data-part': 'slide'
'data-index': string
'data-active': Signal<'' | undefined>
hidden: Signal<boolean>
}
indicator: {
type: 'button'
role: 'tab'
'aria-label': string
'aria-selected': Signal<boolean>
'aria-controls': string
'data-scope': 'carousel'
'data-part': 'indicator'
'data-index': string
'data-active': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
}
CarouselState from @llui/components
export interface CarouselState {
current: number
count: number
loop: boolean
autoplay: boolean
interval: number
paused: boolean
/** Direction of the last transition — useful for entry animations. */
direction: 'forward' | 'backward'
/**
* Minimum absolute horizontal distance (px) a swipe must cross to commit
* to the previous/next slide. Below this the drag snaps back.
*/
swipeThreshold: number
/** Active pointer swipe, or null when idle. */
dragging: CarouselDrag | null
/** Reading direction. Under 'rtl' indicator horizontal arrow keys are flipped. */
dir: 'ltr' | 'rtl'
}
CascadeLevel from @llui/components
Cascade select — a series of dependent selects where each level's choice filters the options of the next. Classic example: country → state → city. The machine stores a flat list of selections (one per level, or null) and the options at each level; filtering logic is left to the view/consumer.
Level shape: the consumer passes an array of Level descriptors on setLevels, each with its own options. Selecting at level N clears selections at levels > N.
export interface CascadeLevel {
id: string
label: string
options: Array<{ value: string; label: string; disabled?: boolean }>
}
CascadeLevelParts from @llui/components
export interface CascadeLevelParts {
label: {
for: string
'data-scope': 'cascade-select'
'data-part': 'level-label'
}
select: {
id: string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'cascade-select'
'data-part': 'level-select'
'data-level': string
'data-ready': Signal<'' | undefined>
onChange: (e: Event) => void
}
}
CascadeSelectInit from @llui/components
export interface CascadeSelectInit {
levels?: CascadeLevel[]
values?: (string | null)[]
disabled?: boolean
}
CascadeSelectParts from @llui/components
export interface CascadeSelectParts {
root: {
'data-scope': 'cascade-select'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
'data-complete': Signal<'' | undefined>
}
clearTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'cascade-select'
'data-part': 'clear-trigger'
onClick: (e: MouseEvent) => void
}
level: (index: number) => CascadeLevelParts
}
CascadeSelectState from @llui/components
export interface CascadeSelectState {
levels: CascadeLevel[]
/** Parallel to levels: one value per level, or null. */
values: (string | null)[]
disabled: boolean
}
CheckboxInit from @llui/components
export interface CheckboxInit {
checked?: CheckedState
disabled?: boolean
required?: boolean
}
CheckboxParts from @llui/components
export interface CheckboxParts {
/** The visual box/container — `role="checkbox"` for accessibility. */
root: {
role: 'checkbox'
'aria-checked': Signal<'true' | 'false' | 'mixed'>
'aria-disabled': Signal<'true' | undefined>
'aria-required': Signal<'true' | undefined>
'data-state': Signal<'checked' | 'unchecked' | 'indeterminate'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'checkbox'
'data-part': 'root'
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
/** A native hidden input for form participation. */
hiddenInput: {
type: 'checkbox'
'aria-hidden': 'true'
tabindex: -1
style: string
checked: Signal<boolean>
indeterminate: Signal<boolean>
disabled: Signal<boolean>
required: Signal<boolean>
'data-scope': 'checkbox'
'data-part': 'hidden-input'
}
/** Optional indicator child (the checkmark). */
indicator: {
'data-state': Signal<'checked' | 'unchecked' | 'indeterminate'>
'data-scope': 'checkbox'
'data-part': 'indicator'
}
}
CheckboxState from @llui/components
export interface CheckboxState {
checked: CheckedState
disabled: boolean
required: boolean
}
ClipboardInit from @llui/components
export interface ClipboardInit {
value?: string
}
ClipboardParts from @llui/components
export interface ClipboardParts {
root: {
'data-scope': 'clipboard'
'data-part': 'root'
'data-copied': Signal<'' | undefined>
}
trigger: {
type: 'button'
'aria-label': string
'data-scope': 'clipboard'
'data-part': 'trigger'
'data-copied': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
}
input: {
type: 'text'
readonly: true
value: Signal<string>
'data-scope': 'clipboard'
'data-part': 'input'
onFocus: (e: FocusEvent) => void
}
indicator: {
'data-scope': 'clipboard'
'data-part': 'indicator'
'data-copied': Signal<'' | undefined>
'aria-live': 'polite'
}
}
ClipboardState from @llui/components
Clipboard — copy-to-clipboard with transient "copied" feedback. The actual clipboard write is performed by the consumer via an effect (or inline in the trigger's onClick handler). Reducer tracks the success state flag and an auto-reset timestamp.
export interface ClipboardState {
value: string
copied: boolean
}
CollapsibleInit from @llui/components
export interface CollapsibleInit {
open?: boolean
disabled?: boolean
}
CollapsibleParts from @llui/components
export interface CollapsibleParts {
root: {
'data-state': Signal<'open' | 'closed'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'collapsible'
'data-part': 'root'
}
trigger: {
type: 'button'
'aria-expanded': Signal<boolean>
'aria-controls': string
id: string
disabled: Signal<boolean>
'data-state': Signal<'open' | 'closed'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'collapsible'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
}
content: {
role: 'region'
id: string
'aria-labelledby': string
hidden: Signal<boolean>
'data-state': Signal<'open' | 'closed'>
'data-scope': 'collapsible'
'data-part': 'content'
}
}
CollapsibleState from @llui/components
Collapsible — a single expandable/collapsible section. Simpler than accordion (no grouping, no keyboard navigation between siblings).
export interface CollapsibleState {
open: boolean
disabled: boolean
}
ColorPickerInit from @llui/components
export interface ColorPickerInit {
/** Initial color as HSL (converted to the canonical HSV store). */
hsl?: Hsl
/** Initial color as HSV (takes precedence over `hsl`). */
hsv?: Hsv
alpha?: number
disabled?: boolean
}
ColorPickerParts from @llui/components
export interface ColorPickerParts {
root: {
'data-scope': 'color-picker'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
hueSlider: {
type: 'range'
min: 0
max: 360
step: 1
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'color-picker'
'data-part': 'hue-slider'
onInput: (e: Event) => void
}
saturationSlider: {
type: 'range'
min: 0
max: 100
step: 1
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
style: Signal<string>
'data-scope': 'color-picker'
'data-part': 'saturation-slider'
onInput: (e: Event) => void
}
lightnessSlider: {
type: 'range'
min: 0
max: 100
step: 1
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
style: Signal<string>
'data-scope': 'color-picker'
'data-part': 'lightness-slider'
onInput: (e: Event) => void
}
hexInput: {
type: 'text'
autocomplete: 'off'
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'color-picker'
'data-part': 'hex-input'
onInput: (e: Event) => void
}
/** Static preview swatch showing the currently-selected color. */
preview: {
'data-scope': 'color-picker'
'data-part': 'preview'
'aria-hidden': 'true'
style: Signal<string>
}
/** The 2D saturation/value area track. The view owns pointer events and
* calls `colorFromPoint(track.getBoundingClientRect(), x, y)` to derive S/V. */
area: {
'data-scope': 'color-picker'
'data-part': 'area'
style: Signal<string>
}
/** The draggable thumb inside the 2D area. Keyboard-operable (arrows move
* S/V; Shift = coarse) with role="slider" and a 2D aria-valuetext. */
areaThumb: {
role: 'slider'
'aria-label': string
/** ARIA 1.2 lists this as a REQUIRED property of `slider`, and the
* `aria-valuetext` definition says authors must also specify it. The area
* is 2D, so it reports the horizontal axis (saturation) numerically and
* leaves both axes to `aria-valuetext`. */
'aria-valuenow': Signal<number>
'aria-valuetext': Signal<string>
'aria-disabled': Signal<'true' | undefined>
tabindex: Signal<number>
'data-scope': 'color-picker'
'data-part': 'area-thumb'
style: Signal<string>
onKeyDown: (e: KeyboardEvent) => void
}
/** Alpha (opacity) range input, 0..1. Wired to the existing alpha state. */
alphaSlider: {
type: 'range'
min: 0
max: 1
step: number
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
style: Signal<string>
'data-scope': 'color-picker'
'data-part': 'alpha-slider'
onInput: (e: Event) => void
}
/** Container for the preset swatch buttons. */
swatchGroup: {
role: 'group'
'aria-label': string
'data-scope': 'color-picker'
'data-part': 'swatch-group'
}
/** Factory for a preset swatch button dispatching a single `setColor`. */
swatch: (color: string) => SwatchParts
}
ColorPickerState from @llui/components
export interface ColorPickerState {
/**
* Canonical color, stored in HSV so the 2D saturation/value area preserves
* S and V independently (HSL collapses both at the black/white axis). HSL is
* derived on demand via `stateHsl()` / `hsvToHsl()` for hex output + sliders.
*/
hsv: Hsv
/** Alpha channel 0..1. */
alpha: number
disabled: boolean
}
ComboboxGroup from @llui/components
A labelled section of options (rendered like <optgroup>). items are the
option VALUES belonging to the group, in visual order. Groups are an
additive, parallel structure: the flat items list always remains the
source of truth for navigation/highlight indices and item ids — when
groups is provided without an explicit items list, init derives the
flat list by concatenating each group's items in order. A plain flat
string[] (no groups) keeps working unchanged. Group LABELS are never
options, so highlight/arrow navigation skips over them for free.
Mirrors select's SelectGroup shape exactly.
export interface ComboboxGroup {
id: string
label: string
items: string[]
}
ComboboxGroupParts from @llui/components
export interface ComboboxGroupParts {
group: {
role: 'group'
'aria-labelledby': string
'data-scope': 'combobox'
'data-part': 'group'
'data-group': string
}
groupLabel: {
id: string
'aria-hidden': 'true'
'data-scope': 'combobox'
'data-part': 'group-label'
'data-group': string
}
}
ComboboxInit from @llui/components
export interface ComboboxInit {
value?: string[]
inputValue?: string
items?: string[]
/** Optional labelled sections. When provided without `items`, the flat
* `items` list is derived by concatenating each group's `items` in order. */
groups?: ComboboxGroup[]
disabledItems?: string[]
selectionMode?: SelectionMode
disabled?: boolean
/** Enable creatable mode: a synthetic create option is offered when the
* typed text matches no existing item. */
allowCreate?: boolean
}
ComboboxItemParts from @llui/components
export interface ComboboxItemParts {
item: {
role: 'option'
id: string
'aria-selected': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
'data-state': Signal<'selected' | undefined>
'data-highlighted': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
'data-create': '' | undefined
'data-scope': 'combobox'
'data-part': 'item'
'data-value': string
/** The option's live position in the FILTERED list (reactive — reused rows
* never report a stale index). */
'data-index': Signal<string>
onClick: (e: MouseEvent) => void
onPointerMove: (e: PointerEvent) => void
}
}
ComboboxOverlayOptions from @llui/components
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<ComboboxState>
send: Send<ComboboxMsg>
parts: ComboboxParts
content: () => Renderable
/**
* Optional enter/leave transition for the combobox listbox (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, giving the raw-`open` combobox an exit
* animation for free. Omitted ⇒ the listbox closes synchronously as before.
*
* @example combobox.overlay({ state, send, parts, content, transition: fade({ duration: 120 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
sameWidth?: boolean
target?: string | HTMLElement
}
ComboboxParts from @llui/components
export interface ComboboxParts {
root: {
'data-scope': 'combobox'
'data-part': 'root'
'data-state': Signal<'open' | 'closed'>
}
input: {
type: 'text'
role: 'combobox'
autocomplete: 'off'
'aria-autocomplete': 'list'
'aria-expanded': Signal<boolean>
'aria-controls': string
'aria-activedescendant': Signal<string | undefined>
'aria-disabled': Signal<'true' | undefined>
id: string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'combobox'
'data-part': 'input'
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
}
trigger: {
type: 'button'
'aria-label': string
'aria-expanded': Signal<boolean>
'aria-controls': string
tabindex: -1
'data-scope': 'combobox'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
}
positioner: {
'data-scope': 'combobox'
'data-part': 'positioner'
style: string
}
content: {
role: 'listbox'
id: string
'aria-labelledby': string
'aria-busy': Signal<'true' | undefined>
tabindex: -1
'data-state': Signal<'open' | 'closed'>
'data-status': Signal<AsyncStatus>
'data-scope': 'combobox'
'data-part': 'content'
}
/** Build the parts for an option by VALUE. The optional `index` is accepted
* for call-site convenience only — it is NOT used for identity (highlight,
* selection and ids are all value-keyed), so a reused row is never stale. */
item: (value: string, index?: number) => ComboboxItemParts
/** Parts for a labelled option group (`<optgroup>`-style section). Pass the
* group id; render the section element with `group` and its label element
* (referenced by `aria-labelledby`) with `groupLabel`. Group labels are not
* options, so navigation skips them automatically. Mirrors `select`. */
group: (id: string) => ComboboxGroupParts
/** Polite live region announcing the result count / error to screen readers
* as the async filter resolves. Render a visually-hidden element with these
* attributes and the `text` signal as its content. */
liveRegion: {
role: 'status'
'aria-live': 'polite'
'aria-atomic': 'true'
'data-scope': 'combobox'
'data-part': 'live-region'
text: Signal<string>
}
empty: {
'data-scope': 'combobox'
'data-part': 'empty'
}
}
ComboboxState from @llui/components
export interface ComboboxState {
open: boolean
value: string[]
inputValue: string
items: string[]
groups: ComboboxGroup[]
disabledItems: string[]
filteredItems: string[]
/** The highlighted option's VALUE (not its index) in the FILTERED list.
* Value-based identity keeps the highlight pinned to the right option as the
* list is filtered/reordered and value-keyed rows are reused. May hold the
* create sentinel ({@link CREATE_OPTION_VALUE}) in creatable mode. */
highlightedValue: string | null
selectionMode: SelectionMode
disabled: boolean
allowCreate: boolean
status: AsyncStatus
requestId: number
error: string | null
}
ContextMenuInit from @llui/components
export interface ContextMenuInit {
items?: ContextMenuItem[]
checked?: string[]
closeOnSelect?: boolean
/** Omit to follow the page's own direction (see `MenuState.dir`). */
dir?: TextDirection | null
/** When false, closing the menu plays an exit animation and the content stays
* mounted (status 'closing') until an `animationEnd`. Default true: instant. */
skipAnimations?: boolean
}
ContextMenuOverlayOptions from @llui/components
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<ContextMenuState>
send: Send<ContextMenuMsg>
parts: ContextMenuParts
content: () => Renderable
/**
* Optional enter/leave transition for the context-menu content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example contextMenu.overlay({ state, send, parts, content, transition: fade({ duration: 120 }) })
*/
transition?: TransitionOptions
target?: string | HTMLElement
}
ContextMenuParts from @llui/components
export interface ContextMenuParts {
/** The element users right-click to open the menu. */
trigger: {
'data-scope': 'context-menu'
'data-part': 'trigger'
onContextMenu: (e: MouseEvent) => void
}
positioner: {
'data-scope': 'context-menu'
'data-part': 'positioner'
style: Signal<string>
}
content: {
role: 'menu'
id: string
/** Virtually-focused (highlighted) item id at the root level. */
'aria-activedescendant': Signal<string | undefined>
tabindex: -1
/** Reflects the presence lifecycle: 'opening' | 'open' | 'closing' | 'closed'.
* Stays mounted while 'closing' so the exit animation can run. */
'data-state': Signal<PresenceStatus>
'data-scope': 'context-menu'
'data-part': 'content'
onKeyDown: (e: KeyboardEvent) => void
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
item: (value: string) => ContextMenuItemParts
checkboxItem: (value: string) => ContextMenuCheckItemParts
radioItem: (value: string) => ContextMenuCheckItemParts
group: (id: string) => ContextMenuGroupParts
separator: () => ContextMenuSeparatorParts
subTrigger: (value: string) => ContextMenuSubTriggerParts
subPositioner: (value: string) => ContextMenuSubPositionerParts
subContent: (value: string) => ContextMenuSubContentParts
}
ContextMenuState from @llui/components
Context-menu state — the shared menu-tree state plus the pointer (x, y) the root content is positioned at.
export interface ContextMenuState extends MenuTreeState {
x: number
y: number
}
CropRect from @llui/components
export interface CropRect {
x: number
y: number
width: number
height: number
}
DateInputInit from @llui/components
export interface DateInputInit {
input?: string
value?: IsoDate | null
min?: IsoDate | null
max?: IsoDate | null
disabled?: boolean
readonly?: boolean
required?: boolean
}
DateInputParts from @llui/components
export interface DateInputParts {
root: {
'data-scope': 'date-input'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
'data-invalid': Signal<'' | undefined>
}
input: {
type: 'text'
inputmode: 'numeric'
autocomplete: 'off'
spellcheck: false
value: Signal<string>
disabled: Signal<boolean>
readonly: Signal<boolean>
required: Signal<boolean>
'aria-invalid': Signal<'true' | undefined>
placeholder?: string
'data-scope': 'date-input'
'data-part': 'input'
onInput: (e: Event) => void
onBlur: (e: FocusEvent) => void
}
clearTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'date-input'
'data-part': 'clear-trigger'
onClick: (e: MouseEvent) => void
}
errorText: {
role: 'alert'
'aria-live': 'polite'
'data-scope': 'date-input'
'data-part': 'error-text'
hidden: Signal<boolean>
}
}
DateInputState from @llui/components
export interface DateInputState {
/** Raw string as typed by the user. */
input: string
/** Parsed date as `YYYY-MM-DD`, or null if empty/invalid. */
value: IsoDate | null
/** Optional lower bound (inclusive), `YYYY-MM-DD`. */
min: IsoDate | null
/** Optional upper bound (inclusive), `YYYY-MM-DD`. */
max: IsoDate | null
error: DateError
disabled: boolean
readonly: boolean
required: boolean
}
DatePickerInit from @llui/components
export interface DatePickerInit {
mode?: DatePickerMode
value?: string | null
start?: string | null
end?: string | null
visibleMonth?: number
visibleYear?: number
months?: number
min?: string | null
max?: string | null
weekStartsOn?: 0 | 1
disabled?: boolean
}
DatePickerParts from @llui/components
export interface DatePickerParts {
root: {
'data-scope': 'date-picker'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
/**
* Grid part factory. `offset` (default 0) selects which month this grid
* renders in a multi-month view — the `aria-label` is the localized
* "Month YYYY" of `visibleMonth + offset`.
*/
grid: (offset?: number) => {
role: 'grid'
'aria-label': Signal<string>
'data-scope': 'date-picker'
'data-part': 'grid'
'data-month-offset': number
}
row: {
role: 'row'
'data-scope': 'date-picker'
'data-part': 'row'
}
prevMonthTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'date-picker'
'data-part': 'prev-month-trigger'
onClick: (e: MouseEvent) => void
}
nextMonthTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'date-picker'
'data-part': 'next-month-trigger'
onClick: (e: MouseEvent) => void
}
dayCell: (cell: DayCell) => DayCellParts
/** Preset part factory — clicking dispatches a single `setRange`. */
preset: (range: PresetRange) => PresetParts
}
DatePickerState from @llui/components
export interface DatePickerState {
/** Selection mode. Defaults to 'single'. */
mode: DatePickerMode
/** Selected date as YYYY-MM-DD, or null. Used in 'single' mode. */
value: string | null
/** Range start as YYYY-MM-DD, or null. Used in 'range' mode. */
start: string | null
/** Range end as YYYY-MM-DD, or null. Used in 'range' mode. */
end: string | null
/** Date currently hovered/previewed while a range is being completed. */
hoverDate: string | null
/** The month currently visible (1-indexed, 1-12) — the first/leftmost month. */
visibleMonth: number
/** The year currently visible. */
visibleYear: number
/** Number of months rendered side-by-side. Defaults to 1. */
months: number
/** The date currently focused by the keyboard (YYYY-MM-DD). */
focused: string
/** Minimum selectable date, inclusive. */
min: string | null
/** Maximum selectable date, inclusive. */
max: string | null
/** 0=Sunday, 1=Monday. */
weekStartsOn: 0 | 1
disabled: boolean
}
DayCell from @llui/components
export interface DayCell {
iso: string
day: number
inMonth: boolean
isToday: boolean
isSelected: boolean
isFocused: boolean
isDisabled: boolean
/** True for the start endpoint of a (committed or previewed) range. */
isRangeStart: boolean
/** True for the end endpoint of a (committed or previewed) range. */
isRangeEnd: boolean
/** True for dates strictly between the range endpoints. */
isInRange: boolean
}
DayCellParts from @llui/components
export interface DayCellParts {
cell: {
role: 'gridcell'
// Signals, not plain values: `view()` runs once, so a snapshot here freezes
// every flag at build time and no selection, focus move or range preview
// ever reaches the DOM. See `live` in `connect`.
'aria-selected': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
tabindex: Signal<number>
'data-scope': 'date-picker'
'data-part': 'day-cell'
/** The cell's identity — the one genuinely static attribute. */
'data-date': string
'data-in-month': Signal<'' | undefined>
'data-today': Signal<'' | undefined>
'data-selected': Signal<'' | undefined>
'data-focused': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
'data-range-start': Signal<'' | undefined>
'data-range-end': Signal<'' | undefined>
'data-in-range': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
}
}
DialogInit from @llui/components
export interface DialogInit {
open?: boolean
/** Skip enter/exit animations — close unmounts synchronously (default: true). */
skipAnimations?: boolean
}
DialogOverlayOptions from @llui/components
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
/** Dialog state slice as a Signal. */
state: Signal<DialogState>
/** Send dispatcher for dialog messages. */
send: Send<DialogMsg>
/** Parts from `connect()` — used to locate the content element by id. */
parts: DialogParts
/** Content rendering. */
content: () => Renderable
/**
* Optional enter/leave transition for the dialog content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example dialog.overlay({ state, send, parts, content, transition: fade({ duration: 150 }) })
*/
transition?: TransitionOptions
/** Close on Escape key (default: true). */
closeOnEscape?: boolean
/** Close on click outside content (default: true). */
closeOnOutsideClick?: boolean
/** Trap focus inside the dialog while open (default: true for modal). */
trapFocus?: boolean
/** Lock body scroll while open (default: true for modal). */
lockScroll?: boolean
/** Apply aria-hidden to sibling trees (default: true for modal). */
hideSiblings?: boolean
/** Target element / selector for the portal (default: 'body'). */
target?: string | HTMLElement
/** Element to focus initially (default: first focusable inside content). */
initialFocus?: Element | (() => Element | null)
/** Restore focus on close (default: true). */
restoreFocus?: boolean
}
DialogParts from @llui/components
export interface DialogParts {
trigger: {
type: 'button'
'aria-haspopup': 'dialog'
'aria-expanded': Signal<boolean>
'aria-controls': string
id: string
'data-state': Signal<'open' | 'closed'>
'data-scope': 'dialog'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
}
backdrop: {
'data-state': Signal<PresenceStatus>
'data-scope': 'dialog'
'data-part': 'backdrop'
'aria-hidden': 'true'
}
positioner: {
'data-scope': 'dialog'
'data-part': 'positioner'
}
content: {
role: 'dialog' | 'alertdialog'
id: string
'aria-modal': 'true' | undefined
'aria-labelledby': string
'aria-describedby': string
tabindex: -1
'data-state': Signal<PresenceStatus>
'data-scope': 'dialog'
'data-part': 'content'
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
title: {
id: string
'data-scope': 'dialog'
'data-part': 'title'
}
description: {
id: string
'data-scope': 'dialog'
'data-part': 'description'
}
closeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'dialog'
'data-part': 'close-trigger'
onClick: (e: MouseEvent) => void
}
}
DialogState from @llui/components
Dialog — modal / non-modal overlay. Ties together focus-trap, dismissable, body scroll lock, sibling aria-hidden, and portal-to-body rendering into a single view helper.
Two layers:
- state machine (
init,update,connect) — pure, minimal. overlay()view helper — opens the dialog's DOM tree inside a body portal, wires up all accessibility utilities on mount, tears them down on close, restores focus to the trigger.
view: ({ state, send }) => {
const dialogState = state.at('dialog')
const dialogSend = mapSend<Msg, dialog.DialogMsg>(send, (msg) => ({
type: 'dialog',
msg,
}))
const parts = dialog.connect(dialogState, dialogSend, { id: 'dialog' })
return [
button({ ...parts.trigger, class: 'btn' }, [text('Delete')]),
dialog.overlay({
state: dialogState,
send: dialogSend,
parts,
content: () => [
div({ ...parts.content, class: 'dialog' }, [
h2({ ...parts.title }, [text('Are you sure?')]),
button({ ...parts.closeTrigger, class: 'btn' }, [text('Cancel')]),
]),
],
}),
]
}
export interface DialogState {
open: boolean
/** Presence lifecycle — drives data-state and keeps the node mounted through exit
* animations. Optional: a partial `{ open }` bridge (e.g. a pattern passing a
* slice to `overlay`) omits it, and the runtime falls back to `open` for instant,
* backward-compatible mount/visibility. `init` always sets it. */
status?: PresenceStatus
/** When true, close transitions go straight to 'closed' (no exit-animation wait).
* Optional for the same partial-slice reason as `status`; `init` always sets it. */
skipAnimations?: boolean
}
DismissableOptions from @llui/components
export interface DismissableOptions {
/** The layer element (e.g. a dialog content or popover). */
element: ElementSource;
/** Trigger / anchor elements that should not count as outside interactions. */
ignore?: ElementSource;
/** Called when the user dismisses the layer. */
onDismiss: (source: DismissSource, event: Event) => void;
/**
* Custom Escape router. When provided it runs for the Escape key INSTEAD of
* `onDismiss('escape', …)`, letting the layer unwind an internal level first
* (e.g. a menu closes its open submenu before closing the whole menu). Return
* `false` to decline — the event is not claimed and propagates as if this
* layer had `disableEscape`. Any other return (incl. `undefined`) claims it.
*/
onEscape?: (event: KeyboardEvent) => boolean | void;
/** Disable outside-click dismissal (default: false). */
disableOutside?: boolean;
/** Disable Escape-key dismissal (default: false). */
disableEscape?: boolean;
}
DragState from @llui/components
Sortable — pointer-based reorderable list.
State machine tracks the currently-dragged item and where it's hovering.
The app owns the actual array; listen for drop and use reorder(arr, from, to)
to compute the new order, or watch currentIndex during drag for live preview.
type State = { items: string[]; sort: SortableState }
update: (state, msg) => {
switch (msg.type) {
case 'sort':
return [{ ...state, sort: sortable.update(state.sort, msg.msg)[0] }, []]
case 'drop': {
const d = state.sort.dragging
if (!d) return [state, []]
return [{ ...state, items: reorder(state.items, d.startIndex, d.currentIndex) }, []]
}
}
}
// `each`, `ul`, `li`, `div`, `text`, `mapSend` are imports from '@llui/dom';
// the view bag provides only `state` (a Signal) and `send`.
view: ({ state, send }) => {
const sortableState = state.at('sort')
const sortableSend = mapSend<Msg, sortable.SortableMsg>(send, (msg) => ({
type: 'sort',
msg,
}))
const s = sortable.connect(sortableState, sortableSend, { id: 'list' })
return [
ul({ ...s.root, class: 'list' }, [
...each({
items: (st) => st.items,
key: (x) => x,
render: ({ item, index }) => [
li({ ...s.item(item(), index()), class: 'item' }, [
div({ ...s.handle(item(), index()), class: 'handle' }, [text('⋮⋮')]),
text(item),
]),
],
}),
]),
]
}
Hook up pointermove/pointerup at the root (attachPointerHandlers) — or
wire them directly via onPointerMove / onPointerUp on the root part.
export interface DragState {
id: string
startIndex: number
currentIndex: number
/**
* Container the drag originated from. Defaults to the connect's `id` for
* single-container sortables. Set when multiple sortables share state.
*/
fromContainer: string
/**
* Container the pointer is currently over. Same as `fromContainer` for
* single-container sortables. Differs when dragging across containers.
*/
toContainer: string
/**
* Pointer X at drag start (viewport coordinates). Used by 2D layouts
* to compute `deltaX = currentX - startX` alongside the Y axis. In 1D
* layouts X is tracked but ignored by the renderer.
*/
startX: number
/**
* Pointer Y at drag start (viewport coordinates). Used by CSS / the
* library's `style.transform` binding to make the dragged item follow
* the pointer.
*/
startY: number
/**
* Current pointer X (viewport coordinates). `deltaX = currentX - startX`.
*/
currentX: number
/**
* Current pointer Y (viewport coordinates). `deltaY = currentY - startY`.
*/
currentY: number
}
DrawerInit from @llui/components
export interface DrawerInit {
open?: boolean
/** Skip enter/exit animations — close unmounts synchronously (default: true). */
skipAnimations?: boolean
}
DrawerOverlayOptions from @llui/components
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<DrawerState>
send: Send<DrawerMsg>
parts: DrawerParts
content: () => Renderable
/**
* Optional enter/leave transition for the drawer content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example drawer.overlay({ state, send, parts, content, transition: slide({ duration: 200 }) })
*/
transition?: TransitionOptions
closeOnEscape?: boolean
closeOnOutsideClick?: boolean
trapFocus?: boolean
lockScroll?: boolean
hideSiblings?: boolean
target?: string | HTMLElement
initialFocus?: Element | (() => Element | null)
restoreFocus?: boolean
}
DrawerParts from @llui/components
export interface DrawerParts {
trigger: {
type: 'button'
'aria-haspopup': 'dialog'
'aria-expanded': Signal<boolean>
'aria-controls': string
id: string
'data-state': Signal<'open' | 'closed'>
'data-scope': 'drawer'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
}
backdrop: {
'data-state': Signal<PresenceStatus>
'data-scope': 'drawer'
'data-part': 'backdrop'
'aria-hidden': 'true'
}
positioner: {
'data-scope': 'drawer'
'data-part': 'positioner'
'data-side': DrawerSide
}
content: {
role: 'dialog'
id: string
'aria-modal': 'true'
'aria-labelledby': string
tabindex: -1
'data-state': Signal<PresenceStatus>
'data-scope': 'drawer'
'data-part': 'content'
'data-side': DrawerSide
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
title: {
id: string
'data-scope': 'drawer'
'data-part': 'title'
}
description: {
id: string
'data-scope': 'drawer'
'data-part': 'description'
}
closeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'drawer'
'data-part': 'close-trigger'
onClick: (e: MouseEvent) => void
}
}
DrawerState from @llui/components
export interface DrawerState {
open: boolean
/** Presence lifecycle — drives data-state and keeps the node mounted through exit animations. */
status: PresenceStatus
/** When true, close transitions go straight to 'closed' (no exit-animation wait). */
skipAnimations: boolean
}
EditableInit from @llui/components
export interface EditableInit {
value?: string
editing?: boolean
disabled?: boolean
}
EditableParts from @llui/components
export interface EditableParts {
root: {
'data-scope': 'editable'
'data-part': 'root'
'data-editing': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
}
preview: {
tabindex: Signal<number>
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'editable'
'data-part': 'preview'
hidden: Signal<boolean>
onClick: (e: MouseEvent) => void
onFocus: (e: FocusEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
input: {
'data-scope': 'editable'
'data-part': 'input'
hidden: Signal<boolean>
value: Signal<string>
disabled: Signal<boolean>
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
onBlur: (e: FocusEvent) => void
}
submitTrigger: {
type: 'button'
'data-scope': 'editable'
'data-part': 'submit-trigger'
onClick: (e: MouseEvent) => void
}
cancelTrigger: {
type: 'button'
'data-scope': 'editable'
'data-part': 'cancel-trigger'
onClick: (e: MouseEvent) => void
}
editTrigger: {
type: 'button'
'data-scope': 'editable'
'data-part': 'edit-trigger'
onClick: (e: MouseEvent) => void
}
}
EditableState from @llui/components
Editable — inline text editor. Click preview to enter edit mode, Enter
to commit, Escape to cancel. Reports the committed value via onSubmit.
export interface EditableState {
value: string
editing: boolean
draft: string
disabled: boolean
}
FieldConnectOptions from @llui/components
export interface FieldConnectOptions {
/** Base id; if omitted, falls back to the id stored in state. */
id?: string
/**
* Whether a description element is rendered. When true, the description id is
* always present in `aria-describedby`; when false it is omitted entirely.
*/
hasDescription?: boolean
}
FieldInit from @llui/components
export interface FieldInit {
id: string
invalid?: boolean
required?: boolean
disabled?: boolean
readonly?: boolean
touched?: boolean
}
FieldParts from @llui/components
export interface FieldParts {
/** The field wrapper. */
root: {
'data-scope': 'field'
'data-part': 'root'
'data-invalid': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
}
/** The `<label>`. `htmlFor` focuses the control on click. */
label: {
id: string
htmlFor: string
'data-scope': 'field'
'data-part': 'label'
}
/** Spread onto the input/select/textarea (or a custom control via aria-labelledby). */
control: {
id: string
'aria-labelledby': string
'aria-describedby': Signal<string | undefined>
'aria-invalid': Signal<'true' | undefined>
'aria-required': Signal<'true' | undefined>
disabled: Signal<boolean>
readOnly: Signal<boolean>
'data-scope': 'field'
'data-part': 'control'
onBlur: (e: FocusEvent) => void
}
/** The description / hint text. Render it only when there is a description to show. */
description: {
id: string
'data-scope': 'field'
'data-part': 'description'
}
/** The error message — a polite live region, intended to be rendered only while invalid. */
errorText: {
id: string
role: 'alert'
'aria-live': 'polite'
'data-scope': 'field'
'data-part': 'error'
}
}
FieldsetConnectOptions from @llui/components
export interface FieldsetConnectOptions {
/** Base id; if omitted, falls back to the id stored in state. */
id?: string
}
FieldsetInit from @llui/components
export interface FieldsetInit {
id: string
disabled?: boolean
invalid?: boolean
}
FieldsetParts from @llui/components
export interface FieldsetParts {
/** Spread onto a native `<fieldset>` element (role `group`). */
root: {
role: 'group'
'aria-labelledby': string
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-scope': 'fieldset'
'data-part': 'root'
'data-invalid': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
}
/** The `<legend>` naming the group. */
legend: {
id: string
'data-scope': 'fieldset'
'data-part': 'legend'
}
/** Group-level error message — a polite live region, rendered only while invalid. */
errorText: {
id: string
role: 'alert'
'aria-live': 'polite'
'data-scope': 'fieldset'
'data-part': 'error'
}
}
FieldsetState from @llui/components
Fieldset — group wiring for a set of related controls (e.g. an address block).
The root is a native <fieldset> (role group) labelled by a <legend>.
Setting the group disabled disables every contained control natively (the
native disabled attribute on <fieldset> propagates to descendants), and is
mirrored to aria-disabled for assistive tech. An optional group-level error
region is exposed for cross-field validation messages.
const g = fieldset.connect(state.at('billing'), send, { id: 'billing' })
el('fieldset', g.root, [
el('legend', g.legend, [text('Billing address')]),
// ...fields...
show(state.map((s) => s.billing.invalid),
() => el('p', g.errorText, [text('Address is incomplete.')])),
])
export interface FieldsetState {
/** Base id from which the legend / error ids derive. */
id: string
disabled: boolean
invalid: boolean
}
FieldState from @llui/components
Field — label / description / error ARIA wiring for a single form control.
Generates a stable family of ids from one base id and wires them together
so the consumer never hand-writes for / aria-describedby / aria-invalid:
label.htmlFor→ the control id (clicking the label focuses the control natively)label.id→ exposed ascontrol['aria-labelledby']for CUSTOM controls (combobox, listbox, etc.) that aren't a native labellable elementcontrol['aria-describedby']references the description id whenever a description is rendered, and ADDS the error id only whileinvaliderrorTextis a polite live region intended to be rendered only while invalid
const f = field.connect(state.at('field'), send, { id: 'email', hasDescription: true })
el('div', f.root, [
el('label', f.label, [text('Email')]),
el('input', { ...f.control, type: 'email' }),
el('p', f.description, [text('We never share it.')]),
show(state.map((s) => s.field.invalid),
() => el('p', f.errorText, [text('Enter a valid email.')])),
])
export interface FieldState {
/** Base id from which the control / label / description / error ids derive. */
id: string
invalid: boolean
required: boolean
disabled: boolean
readonly: boolean
touched: boolean
}
FileLike from @llui/components
Everything fileMatchesAccept needs — satisfied by both File and FileMeta.
export interface FileLike {
name: string
type: string
}
FileMeta from @llui/components
The serializable half of a selected file. id is the registry key for the
live handle; the rest mirrors the File fields a view needs.
export interface FileMeta {
id: string
name: string
size: number
type: string
lastModified: number
}
FileUploadInit from @llui/components
export interface FileUploadInit {
files?: FileMeta[]
disabled?: boolean
multiple?: boolean
accept?: AcceptValue
maxFiles?: number
maxSize?: number
minFileSize?: number
required?: boolean
readonly?: boolean
invalid?: boolean
}
FileUploadItemParts from @llui/components
export interface FileUploadItemParts {
item: {
'data-scope': 'file-upload'
'data-part': 'item'
'data-index': string
}
itemName: {
'data-scope': 'file-upload'
'data-part': 'item-name'
}
itemSizeText: {
'data-scope': 'file-upload'
'data-part': 'item-size-text'
}
itemPreview: {
'data-scope': 'file-upload'
'data-part': 'item-preview'
}
removeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'file-upload'
'data-part': 'item-remove'
onClick: (e: MouseEvent) => void
}
/** Zag-aligned alias for removeTrigger. Same wiring. */
itemDeleteTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'file-upload'
'data-part': 'item-delete-trigger'
onClick: (e: MouseEvent) => void
}
}
FileUploadParts from @llui/components
export interface FileUploadParts {
root: {
'data-scope': 'file-upload'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
'data-dragging': Signal<'' | undefined>
'data-invalid': Signal<'' | undefined>
'data-readonly': Signal<'' | undefined>
}
dropzone: {
'data-scope': 'file-upload'
'data-part': 'dropzone'
'data-dragging': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
onDragEnter: (e: DragEvent) => void
onDragOver: (e: DragEvent) => void
onDragLeave: (e: DragEvent) => void
onDrop: (e: DragEvent) => void
}
trigger: {
type: 'button'
'data-scope': 'file-upload'
'data-part': 'trigger'
disabled: Signal<boolean>
onClick: (e: MouseEvent) => void
}
hiddenInput: {
type: 'file'
tabindex: -1
'aria-hidden': 'true'
style: string
disabled: Signal<boolean>
multiple: Signal<boolean>
accept: Signal<string>
required: Signal<boolean>
'aria-invalid': Signal<'true' | undefined>
capture?: string | boolean
webkitdirectory?: '' | undefined
'data-scope': 'file-upload'
'data-part': 'hidden-input'
id: string
onChange: (e: Event) => void
}
label: {
for: string
'data-scope': 'file-upload'
'data-part': 'label'
}
clearTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'file-upload'
'data-part': 'clear-trigger'
onClick: (e: MouseEvent) => void
}
itemGroup: {
'data-scope': 'file-upload'
'data-part': 'item-group'
}
item: (index: number) => FileUploadItemParts
}
FileUploadState from @llui/components
export interface FileUploadState {
files: FileMeta[]
rejectedFiles: RejectedFile[]
disabled: boolean
multiple: boolean
accept: AcceptValue
maxFiles: number
maxSize: number
minFileSize: number
required: boolean
readonly: boolean
invalid: boolean
/** `dragDepth > 0`, materialized so views bind one boolean. */
dragging: boolean
/**
* Nesting depth of the in-flight drag. `dragenter`/`dragleave` both bubble
* and fire in the order enter@child → leave@parent, so a plain boolean flips
* off while the pointer is still inside the dropzone (#119).
*/
dragDepth: number
}
FloatingOptions from @llui/components
export interface FloatingOptions {
/** The reference element (trigger/anchor). */
anchor: Element;
/** The floating element (content). */
floating: HTMLElement;
/** Preferred placement (default: 'bottom'). */
placement?: Placement;
/** Gap between anchor and floating, in px (default: 0). */
offset?: number;
/** Flip to opposite side when there isn't enough room (default: true). */
flip?: boolean;
/** Shift along axis to stay in view (default: padding 8 unless false). */
shift?: boolean | {
padding?: number;
};
/**
* Reading direction. Under `'rtl'`, logical `*-start`/`*-end` placements
* track the inline-start/inline-end edges. When given it is AUTHORITATIVE —
* it overrides the direction the floating element happens to compute to,
* which for a portaled overlay is the direction of wherever it landed.
* Omit it to leave that decision to the page, as floating-ui does by default.
*/
dir?: TextDirection;
/** Optional arrow element to position. */
arrow?: HTMLElement;
/** Notify after each position computation. */
onUpdate?: (data: {
x: number;
y: number;
placement: Placement;
arrow?: {
x?: number;
y?: number;
};
}) => void;
}
FloatingPanelInit from @llui/components
export interface FloatingPanelInit {
position?: { x: number; y: number }
size?: { width: number; height: number }
minSize?: { width?: number; height?: number }
maxSize?: { width?: number; height?: number } | null
open?: boolean
disabled?: boolean
}
FloatingPanelParts from @llui/components
export interface FloatingPanelParts {
root: {
role: 'dialog'
'aria-label': string
'data-scope': 'floating-panel'
'data-part': 'root'
'data-dragging': Signal<'' | undefined>
'data-resizing': Signal<'' | undefined>
'data-minimized': Signal<'' | undefined>
'data-maximized': Signal<'' | undefined>
hidden: Signal<boolean>
style: Signal<string>
}
dragHandle: {
'data-scope': 'floating-panel'
'data-part': 'drag-handle'
onPointerDown: (e: PointerEvent) => void
}
content: {
'data-scope': 'floating-panel'
'data-part': 'content'
hidden: Signal<boolean>
}
minimizeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'floating-panel'
'data-part': 'minimize-trigger'
onClick: (e: MouseEvent) => void
}
maximizeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'floating-panel'
'data-part': 'maximize-trigger'
onClick: (e: MouseEvent) => void
}
closeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'floating-panel'
'data-part': 'close-trigger'
onClick: (e: MouseEvent) => void
}
resizeHandle: (handle: ResizeHandle) => {
'data-scope': 'floating-panel'
'data-part': 'resize-handle'
'data-handle': ResizeHandle
onPointerDown: (e: PointerEvent) => void
}
}
FloatingPanelState from @llui/components
export interface FloatingPanelState {
position: { x: number; y: number }
size: { width: number; height: number }
minSize: { width: number; height: number }
/**
* The upper size bound. `null` — or an absent dimension — is unbounded on
* that axis, which is what `clampSize` already spelled `?? Infinity` at the
* point of use. A bound in state is finite or absent, never an infinity
* (`JSON.stringify` writes `null` for one) and never `NaN` (which is not
* nullish, so it survives the `??` and switches that axis's clamp off) —
* #177.
*/
maxSize: { width?: number; height?: number } | null
open: boolean
minimized: boolean
maximized: boolean
dragging: boolean
resizing: ResizeHandle | null
/** Snapshot of the pre-maximize geometry (for restore). */
restoreBounds: { x: number; y: number; width: number; height: number } | null
disabled: boolean
}
FocusRestoreQuery from @llui/components
The one rule for "should this layer pull focus back to its anchor?".
Restoring focus to the trigger is right when the layer was closed with focus still resting inside it (Escape, a close button, a programmatic close) — the user's focus would otherwise be left on a detached node. It is WRONG when the dismissal was caused by focus moving somewhere the user chose: yanking it back to the trigger takes focus away from the control they just reached, and can leave a still-open layer with focus outside it (#173).
document.body (and a null activeElement) counts as "inside" because that
is where focus lands when the focused element is removed — nobody chose it, so
the anchor is a better home than the body.
Both callers must ask BEFORE tearing anything down: the focus trap's release
and the aria-hidden/inert sweep both move or invalidate activeElement,
so a decision taken after them is a decision about the engine's own cleanup.
export interface FocusRestoreQuery {
/** The region that counts as "inside" this layer. */
boundary: Element
/** The element focus would be restored TO. */
anchor?: Element | null
/**
* Also treat the anchor itself being focused as "inside" (`select`, which
* focuses its own trigger on open — without this its restore reads as "the
* user moved focus to the trigger" and never runs).
*/
allowAnchorActive?: boolean
}
FocusTrapOptions from @llui/components
export interface FocusTrapOptions {
/** The container whose focusable descendants form the trap. */
container: ElementSource;
/** Element to focus when the trap activates. Defaults to first focusable. */
initialFocus?: Element | (() => Element | null);
/** Restore focus to the previously active element on release (default: true). */
restoreFocus?: boolean;
}
FormatDateOptions from @llui/components
export interface FormatDateOptions {
locale?: string
dateStyle?: DateStyle
calendar?: string
numberingSystem?: string
timeZone?: string
weekday?: 'long' | 'short' | 'narrow'
year?: 'numeric' | '2-digit'
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'
day?: 'numeric' | '2-digit'
era?: 'long' | 'short' | 'narrow'
}
FormatDateTimeOptions from @llui/components
export interface FormatDateTimeOptions {
locale?: string
dateStyle?: DateStyle
timeStyle?: DateStyle
timeZone?: string
calendar?: string
hour12?: boolean
hourCycle?: 'h11' | 'h12' | 'h23' | 'h24'
}
FormatDisplayNameOptions from @llui/components
export interface FormatDisplayNameOptions {
locale?: string
style?: 'long' | 'short' | 'narrow'
languageDisplay?: 'dialect' | 'standard'
fallback?: 'code' | 'none'
}
FormatFileSizeOptions from @llui/components
export interface FormatFileSizeOptions {
locale?: string
units?: string[]
decimalPlaces?: number
}
FormatListOptions from @llui/components
export interface FormatListOptions {
locale?: string
type?: 'conjunction' | 'disjunction' | 'unit'
style?: 'long' | 'short' | 'narrow'
}
FormatNumberOptions from @llui/components
export interface FormatNumberOptions {
locale?: string
style?: 'decimal' | 'currency' | 'percent' | 'unit'
currency?: string
currencyDisplay?: 'symbol' | 'narrowSymbol' | 'code' | 'name'
signDisplay?: 'auto' | 'never' | 'always' | 'exceptZero'
notation?: 'standard' | 'scientific' | 'engineering' | 'compact'
compactDisplay?: 'short' | 'long'
unit?: string
unitDisplay?: 'short' | 'long' | 'narrow'
useGrouping?: boolean
minimumIntegerDigits?: number
minimumFractionDigits?: number
maximumFractionDigits?: number
minimumSignificantDigits?: number
maximumSignificantDigits?: number
}
FormatPluralOptions from @llui/components
export interface FormatPluralOptions {
locale?: string
type?: 'cardinal' | 'ordinal'
minimumIntegerDigits?: number
minimumFractionDigits?: number
maximumFractionDigits?: number
minimumSignificantDigits?: number
maximumSignificantDigits?: number
}
FormatRelativeTimeOptions from @llui/components
export interface FormatRelativeTimeOptions {
locale?: string
numeric?: 'always' | 'auto'
style?: 'long' | 'short' | 'narrow'
}
FormatTimeOptions from @llui/components
export interface FormatTimeOptions {
locale?: string
timeStyle?: DateStyle
timeZone?: string
hour12?: boolean
hourCycle?: 'h11' | 'h12' | 'h23' | 'h24'
hour?: 'numeric' | '2-digit'
minute?: 'numeric' | '2-digit'
second?: 'numeric' | '2-digit'
fractionalSecondDigits?: 0 | 1 | 2 | 3
timeZoneName?: 'long' | 'short' | 'shortOffset' | 'longOffset' | 'shortGeneric' | 'longGeneric'
dayPeriod?: 'narrow' | 'short' | 'long'
}
FormParts from @llui/components
export interface FormParts {
root: {
'data-scope': 'form'
'data-part': 'root'
'data-state': Signal<FormStatus>
'aria-busy': Signal<'true' | undefined>
}
field: (name: string) => {
'data-scope': 'form'
'data-part': 'field'
'data-touched': Signal<'' | undefined>
touched: Signal<boolean>
onBlur: (e: FocusEvent) => void
}
submit: {
type: 'submit'
'data-scope': 'form'
'data-part': 'submit'
'data-state': Signal<FormStatus>
disabled: Signal<boolean>
}
}
FormState from @llui/components
export interface FormState {
status: FormStatus
touched: Record<string, boolean>
submitError: string | null
}
HoverCardInit from @llui/components
export interface HoverCardInit {
open?: boolean
/** Skip enter/exit animations — hide unmounts synchronously (default: true). */
skipAnimations?: boolean
}
HoverCardOverlayOptions from @llui/components
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<HoverCardState>
send: Send<HoverCardMsg>
parts: HoverCardParts
content: () => Renderable
/**
* Optional enter/leave transition for the hover-card content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example hoverCard.overlay({ state, send, parts, content, transition: fade({ duration: 150 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
target?: string | HTMLElement
arrowSelector?: string
}
HoverCardParts from @llui/components
export interface HoverCardParts {
trigger: {
id: string
'aria-controls': string
'aria-expanded': Signal<boolean>
'data-state': Signal<'open' | 'closed'>
'data-scope': 'hover-card'
'data-part': 'trigger'
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onFocus: (e: FocusEvent) => void
onBlur: (e: FocusEvent) => void
}
positioner: {
'data-scope': 'hover-card'
'data-part': 'positioner'
style: string
}
content: {
id: string
'data-state': Signal<PresenceStatus>
'data-scope': 'hover-card'
'data-part': 'content'
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
arrow: {
'data-scope': 'hover-card'
'data-part': 'arrow'
}
}
HoverCardState from @llui/components
Hover card — richer tooltip-like popup triggered by hover or focus.
Unlike tooltip, it uses role="dialog" (not role="tooltip") and
allows interactive content. Content can be hovered without closing.
export interface HoverCardState {
open: boolean
/** Presence lifecycle — drives data-state and keeps the node mounted through exit animations. */
status: PresenceStatus
/** When true, hide transitions go straight to 'closed' (no exit-animation wait). */
skipAnimations: boolean
}
Hsl from @llui/components
Color picker — HSL/HSV color selection. Tracks hue (0-360), saturation (0-100), and lightness (0-100). Emits hex strings for convenience.
export interface Hsl {
h: number
s: number
l: number
}
Hsv from @llui/components
HSV color (h 0-360, s/v 0-100). The 2D area picker operates in HSV space.
export interface Hsv {
h: number
s: number
v: number
}
ImageCropperInit from @llui/components
export interface ImageCropperInit {
image?: { width: number; height: number }
crop?: CropRect
aspectRatio?: number | null
minSize?: number
disabled?: boolean
}
ImageCropperParts from @llui/components
export interface ImageCropperParts {
root: {
'data-scope': 'image-cropper'
'data-part': 'root'
'data-dragging': Signal<'' | undefined>
'data-resizing': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
}
image: {
'data-scope': 'image-cropper'
'data-part': 'image'
onLoad: (e: Event) => void
draggable: false
}
cropBox: {
'data-scope': 'image-cropper'
'data-part': 'crop-box'
style: Signal<string>
onPointerDown: (e: PointerEvent) => void
}
resizeHandle: (handle: ResizeHandle) => {
'data-scope': 'image-cropper'
'data-part': 'resize-handle'
'data-handle': ResizeHandle
onPointerDown: (e: PointerEvent) => void
}
resetTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'image-cropper'
'data-part': 'reset-trigger'
onClick: (e: MouseEvent) => void
}
}
ImageCropperState from @llui/components
export interface ImageCropperState {
/** Natural dimensions of the source image. */
image: { width: number; height: number }
crop: CropRect
/** Constrain the crop to this aspect ratio (width / height), or null to free-form. */
aspectRatio: number | null
minSize: number
dragging: boolean
resizing: ResizeHandle | null
disabled: boolean
}
InteractOutsideOptions from @llui/components
export interface InteractOutsideOptions {
/** Element(s) that define the "inside" region. */
element: ElementSource;
/** Additional elements whose interactions should not count as outside (e.g. triggers). */
ignore?: ElementSource;
/** Called on pointerdown or focus outside the inside region. */
onInteractOutside: (event: Event) => void;
/**
* If provided, called first with the event. Return `false` to suppress the
* outside callback (for an in-flight layer to claim the event).
*/
shouldDispatch?: (event: Event) => boolean;
}
InViewObserverOptions from @llui/components
export interface ObserverOptions {
threshold?: number
rootMargin?: string
once?: boolean
}
InViewParts from @llui/components
export interface InViewParts {
root: {
'data-scope': 'in-view'
'data-part': 'root'
'data-state': Signal<'visible' | 'hidden'>
}
}
InViewState from @llui/components
In View — tracks whether an element is visible in the viewport using IntersectionObserver.
State machine: { visible: false } → enter → { visible: true } → leave → …
With once: true, the observer disconnects after the first enter,
keeping visible: true permanently. Useful for lazy-load and
scroll-triggered animations.
view: ({ state, send }) => {
const inViewState = state.at('inView')
const inViewSend = mapSend<Msg, inView.InViewMsg>(send, (msg) => ({
type: 'inView',
msg,
}))
const parts = inView.connect(inViewState, inViewSend, { id: 'hero' })
return [
div({ ...parts.root, class: inViewState.at('visible').map((v) => (v ? 'fade-in' : '')) }, [
onMount((el) =>
inView.createObserver(el, inViewSend, { threshold: 0.5, once: true }),
),
]),
]
}
export interface InViewState {
visible: boolean
}
ListboxInit from @llui/components
export interface ListboxInit {
value?: string[]
items?: string[]
disabledItems?: string[]
disabled?: boolean
selectionMode?: SelectionMode
}
ListboxItemParts from @llui/components
export interface ListboxItemParts {
root: {
role: 'option'
id: string
'aria-selected': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
'data-state': Signal<'selected' | undefined>
'data-highlighted': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
'data-scope': 'listbox'
'data-part': 'item'
'data-value': string
'data-index': string
onClick: (e: MouseEvent) => void
onPointerMove: (e: PointerEvent) => void
}
}
ListboxParts from @llui/components
export interface ListboxParts {
root: {
role: 'listbox'
'aria-multiselectable': Signal<'true' | undefined>
'aria-disabled': Signal<'true' | undefined>
'aria-activedescendant': Signal<string | undefined>
tabindex: Signal<number>
id: string
'data-scope': 'listbox'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
onKeyDown: (e: KeyboardEvent) => void
}
item: (value: string, index: number) => ListboxItemParts
}
ListboxState from @llui/components
export interface ListboxState {
value: string[]
items: string[]
disabledItems: string[]
disabled: boolean
selectionMode: SelectionMode
highlightedIndex: number | null
typeahead: string
typeaheadExpiresAt: number
}
Locale from @llui/components
Per-component locale strings. Only components with user-facing text have entries.
export interface Locale {
/**
* App-wide reading direction. When unset, components fall back to their own
* `dir` option or DOM resolution; an explicit component direction wins.
*/
direction?: TextDirection
carousel: {
label: string
indicators: string
next: string
prev: string
slide: (index: number) => string
goToSlide: (index: number) => string
}
cascadeSelect: { clear: string }
clipboard: { copy: string }
colorPicker: { hue: string; saturation: string; lightness: string; hex: string }
combobox: { toggle: string; resultCount: (n: number) => string }
dateInput: { clear: string }
datePicker: {
prev: string
next: string
monthNames: string[]
grid: (year: number, month: number) => string
}
dialog: { close: string }
drawer: { close: string }
fileUpload: { remove: string; clear: string }
floatingPanel: { label: string; minimize: string; maximize: string; close: string }
imageCropper: { reset: string }
navigationMenu: { label: string }
numberInput: { increment: string; decrement: string }
pagination: { label: string; prev: string; next: string; page: (n: number) => string }
passwordInput: { show: string; hide: string }
pinInput: { input: (index: number) => string }
popover: { close: string }
progress: { loading: string }
qrCode: { label: string; download: string }
signaturePad: { label: string; clear: string; undo: string }
sortable: { handle: string }
steps: { label: string }
tagsInput: { input: string; remove: string; clear: string }
timePicker: { label: string; hours: string; minutes: string; period: string }
timer: { start: string; pause: string; reset: string }
toast: { region: string; dismiss: string }
toc: { label: string; expand: string }
tour: { close: string }
}
MarqueeInit from @llui/components
export interface MarqueeInit {
running?: boolean
direction?: MarqueeDirection
durationSec?: number
pauseOnHover?: boolean
disabled?: boolean
}
MarqueeParts from @llui/components
export interface MarqueeParts {
root: {
'data-scope': 'marquee'
'data-part': 'root'
'data-running': Signal<'' | undefined>
'data-direction': Signal<MarqueeDirection>
'data-axis': Signal<'horizontal' | 'vertical'>
'data-disabled': Signal<'' | undefined>
style: Signal<string>
onMouseEnter: (e: MouseEvent) => void
onMouseLeave: (e: MouseEvent) => void
}
content: {
'data-scope': 'marquee'
'data-part': 'content'
}
}
MarqueeState from @llui/components
export interface MarqueeState {
/** User-intended running state (what play/pause/toggle set). The actual
* effective state is derived via `isRunning()` — it combines this with
* `hovered` + `pauseOnHover`. */
running: boolean
direction: MarqueeDirection
/** Duration of one full loop in seconds. Larger = slower. */
durationSec: number
pauseOnHover: boolean
hovered: boolean
disabled: boolean
}
MenubarInit from @llui/components
export interface MenubarInit {
menus: MenubarMenu[]
/** Initially-focused menu id (defaults to the first enabled menu). */
focused?: string | null
}
MenubarMenu from @llui/components
Declarative description of one top-level menu in the bar.
export interface MenubarMenu {
id: string
items: MenuItem[]
disabled?: boolean
/** When true, selecting a checkbox/radio also closes this menu. */
closeOnSelect?: boolean
}
MenubarOverlayOptions from @llui/components
export interface MenubarOverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<MenubarState>
send: Send<MenubarMsg>
/** The menu id this overlay renders. */
menuId: string
/** The delegated per-menu bag — `connect(...).menu(menuId)`. Its
* `trigger.id` is the id the bar's `menuTrigger(menuId)` renders, so it
* doubles as the overlay's anchor. */
parts: MenuParts
content: () => Renderable
/**
* Optional enter/leave transition for the menubar menu content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example menubar.overlay({ state, send, parts, content, transition: fade({ duration: 120 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
target?: string | HTMLElement
}
MenubarParts from @llui/components
export interface MenubarParts {
root: {
role: 'menubar'
'aria-label': string
'data-scope': 'menubar'
'data-part': 'root'
}
menuTrigger: (id: string) => MenubarTriggerParts
/** Delegated per-menu part bag (content/item/checkboxItem/submenu/…). */
menu: (id: string) => MenuParts
}
MenubarState from @llui/components
export interface MenubarState {
/** Top-level menu ids, in bar order. */
menus: string[]
/** The id of the currently-open menu, or null. */
open: string | null
/** The id of the top-level trigger that holds roving focus. */
focused: string | null
/** Ids of disabled menus (cannot be opened/focused). */
disabledMenus: string[]
/** Embedded per-menu machine states, keyed by menu id. */
menuStates: Record<string, MenuState>
}
MenubarTriggerParts from @llui/components
export interface MenubarTriggerParts {
role: 'menuitem'
id: string
'aria-haspopup': 'menu'
'aria-expanded': Signal<boolean>
'aria-controls': string
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'menubar'
'data-part': 'trigger'
'data-state': Signal<'open' | 'closed'>
'data-value': string
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onPointerEnter: (e: PointerEvent) => void
onFocus: (e: FocusEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
MenuInit from @llui/components
export interface MenuInit {
open?: boolean
items?: MenuItem[]
highlighted?: string | null
checked?: string[]
closeOnSelect?: boolean
/** Omit to follow the page's own direction (see {@link MenuState.dir}). */
dir?: TextDirection | null
/** When false, closing the menu plays an exit animation and the content stays
* mounted (status 'closing') until an `animationEnd`. Default true: instant. */
skipAnimations?: boolean
}
MenuOverlayOptions from @llui/components
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<MenuState>
send: Send<MenuMsg>
parts: MenuParts
content: () => Renderable
/**
* Optional enter/leave transition for the menu content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example menu.overlay({ state, send, parts, content, transition: fade({ duration: 120 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
target?: string | HTMLElement
}
MenuParts from @llui/components
export interface MenuParts {
trigger: {
type: 'button'
'aria-haspopup': 'menu'
'aria-expanded': Signal<boolean>
'aria-controls': string
id: string
'data-state': Signal<'open' | 'closed'>
'data-scope': 'menu'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
positioner: {
'data-scope': 'menu'
'data-part': 'positioner'
style: string
}
content: {
role: 'menu'
id: string
'aria-labelledby': string
/** The id of the virtually-focused (highlighted) item at the root level, so
* assistive tech announces it while DOM focus stays on the container. */
'aria-activedescendant': Signal<string | undefined>
tabindex: -1
/** Reflects the presence lifecycle: 'opening' | 'open' | 'closing' | 'closed'.
* Stays mounted while 'closing' so the exit animation can run. */
'data-state': Signal<PresenceStatus>
'data-scope': 'menu'
'data-part': 'content'
onKeyDown: (e: KeyboardEvent) => void
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
item: (value: string) => MenuItemParts
checkboxItem: (value: string) => MenuCheckItemParts
radioItem: (value: string) => MenuCheckItemParts
group: (id: string) => MenuGroupParts
separator: () => MenuSeparatorParts
subTrigger: (value: string) => MenuSubTriggerParts
subPositioner: (value: string) => MenuSubPositionerParts
subContent: (value: string) => MenuSubContentParts
}
MenuState from @llui/components
export interface MenuState extends MenuTreeState {
open: boolean
/**
* Presence lifecycle of the root content, layered over `open` for exit
* animations. `open` stays the logical "should be visible/interactive" flag;
* `status` tracks 'opening'/'open'/'closing'/'closed' so the content can stay
* mounted while its exit animation runs (status 'closing'). When
* `skipAnimations` is true (the default) a close jumps straight to 'closed'.
*/
status: PresenceStatus
/** When true (default), a close goes straight to 'closed' synchronously — no
* exit animation, no waiting for an `animationEnd` that may never fire. */
skipAnimations: boolean
items: MenuItem[]
/** Highlighted value per open level. Key `''` is the root; otherwise the parent subTrigger value. */
highlights: Record<string, string | null>
/** Chain of subTrigger values whose submenus are open (deepest last). */
openPath: string[]
/** Checked checkbox / radio values. */
checked: string[]
/** When true, selecting a checkbox/radio also closes the menu (default false). */
closeOnSelect: boolean
/** Accumulator for typeahead search (scoped to the deepest matching level). */
typeahead: string
typeaheadExpiresAt: number
/**
* Reading direction, or `null` for "the host never said — let the page
* decide". Under 'rtl', ArrowLeft/ArrowRight swap meaning, and the overlay's
* `*-start`/`*-end` alignment tracks the inline-start/inline-end edge.
*
* `null` rather than an `'ltr'` default because the value is AUTHORITATIVE
* once it reaches `attachFloating`: a concrete default overrode the page, so
* a menu on `<html dir="rtl">` was laid out LTR (#138 review, blocking 4).
* See {@link floatingDir}.
*/
dir: TextDirection | null
}
MeterInit from @llui/components
export interface MeterInit {
value?: number
min?: number
max?: number
low?: number
high?: number
optimum?: number
}
MeterParts from @llui/components
export interface MeterParts {
root: {
role: 'meter'
'aria-valuemin': Signal<number>
'aria-valuemax': Signal<number>
'aria-valuenow': Signal<number>
'aria-valuetext': Signal<string>
'aria-label': string | undefined
'data-state': Signal<MeterThreshold>
'data-scope': 'meter'
'data-part': 'root'
}
track: {
'data-state': Signal<MeterThreshold>
'data-scope': 'meter'
'data-part': 'track'
}
range: {
'data-state': Signal<MeterThreshold>
'data-scope': 'meter'
'data-part': 'range'
style: Signal<string>
}
label: {
'data-scope': 'meter'
'data-part': 'label'
}
valueText: Signal<string>
}
MeterState from @llui/components
Meter — role="meter" gauge for a scalar measurement within a known range
(e.g. disk usage, battery level). Distinct from progressbar: a meter is never
indeterminate and represents a static measurement rather than task progress.
low/high/optimum mirror the native data-state.
export interface MeterState {
value: number
min: number
max: number
low?: number
high?: number
optimum?: number
}
NavItemParts from @llui/components
export interface NavItemParts {
trigger: {
type: 'button'
id: string
/** For a branch item this is the disclosure button controlling its panel;
* `undefined` for a plain link trigger. */
'aria-controls': string | undefined
'aria-expanded': Signal<boolean | undefined>
'data-scope': 'navigation-menu'
'data-part': 'trigger'
'data-state': Signal<'open' | 'closed'>
'data-value': string
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onPointerEnter: (e: PointerEvent) => void
onFocus: (e: FocusEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
content: {
id: string
'aria-labelledby': string
'data-scope': 'navigation-menu'
'data-part': 'content'
'data-state': Signal<'open' | 'closed'>
hidden: Signal<boolean>
onPointerEnter: (e: PointerEvent) => void
}
}
NavMenuInit from @llui/components
export interface NavMenuInit {
open?: string[]
focused?: string | null
/** Ids eligible for the roving tab stop, in document order — see
* `NavMenuState.items`. */
items?: string[]
disabled?: boolean
dir?: 'ltr' | 'rtl'
}
NavMenuParts from @llui/components
export interface NavMenuParts {
root: {
// Site navigation is NOT an application menu: it uses a `nav` landmark with
// disclosure buttons, not menubar/menu/menuitem roles. Render the root as a
// `<nav>` element; `aria-label` names the landmark.
'aria-label': string
'data-scope': 'navigation-menu'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
onPointerLeave: (e: PointerEvent) => void
onPointerEnter: (e: PointerEvent) => void
}
/**
* Parts for one trigger (+ its panel when it is a branch).
*
* `ancestorIds` is the open-path this item lives under, root-first. It drives
* sibling-closing in `openBranch` AND the roving tab stop: an item whose
* ancestors are not all open is inside a `hidden` panel and is skipped when
* the stop is resolved.
*
* REQUIRED on every NESTED item, leaf ones included. It used to be read only
* inside `isBranch` guards, so passing it on a leaf was optional in practice;
* since #145 it is what makes a leaf's tabbability knowable. Omitting it
* reads as "top level", which lets the tab stop sit inside a closed submenu
* where no Tab press can reach it.
*/
item: (id: string, options: { isBranch: boolean; ancestorIds?: string[] }) => NavItemParts
}
NavMenuState from @llui/components
Navigation menu — multi-level menu bar with hover/focus-triggered
submenus. Unlike menu (a single dropdown), navigation-menu supports
nested submenus arbitrarily deep and is typically used for primary
site navigation.
State tracks the currently focused item id and the ids of all currently-open branches. The consumer provides the tree structure (items with optional children); the machine doesn't index the hierarchy itself — it just maintains open-paths and lets the view handle traversal.
Typical interaction model (delay-based):
- Pointer enter on a branch → openBranch after openDelay
- Pointer leave of the whole tree → closeAll after closeDelay
- Click/keyboard activation → toggleBranch immediately
The consumer is responsible for debouncing via setTimeout; the machine just responds to the dispatched messages.
export interface NavMenuState {
/** Ids of open branches, in open order (root-first). Closing an
* ancestor automatically closes its descendants. */
open: string[]
focused: string | null
/**
* Ids of the items ELIGIBLE for the roving tab stop, in document order, when
* the consumer renders a DYNAMIC list — the top-level items in the usual
* case; add deeper ids if a submenu item should be able to own the stop.
*
* It is both the fallback and the membership list: while nothing is focused
* the first entry owns the nav's single tab stop, and a `focused` id that is
* not one of these entries has been removed, so the stop falls back rather
* than vanishing (#145).
*
* Leave it empty for a static menu — `connect` then uses the ids handed to
* `item()`, in call order, as the membership list instead, which is document
* order for any depth-first view. Same escape hatch as `radio-group`/`tabs`,
* which keep their `items` list in state for exactly this reason.
*
* Either way the candidates are filtered to the ones currently TABBABLE: an
* id whose `ancestorIds` are not all in `open` sits inside a `hidden`
* submenu panel and cannot carry the stop.
*/
items: string[]
disabled: boolean
/** Reading direction. Under 'rtl', ArrowLeft/ArrowRight swap meaning. */
dir: 'ltr' | 'rtl'
}
NestedLayerOptions from @llui/components
export interface NestedLayerOptions {
/**
* Consumers this registration participates in. Defaults to all of them, which
* is what a surface with no dismissable layer of its own needs. Narrow it when
* another mechanism already covers an aspect — see the module comment.
*/
aspects?: readonly NestedLayerAspect[];
/**
* The element this layer is logically nested INSIDE — its trigger/anchor, or
* the host element it belongs to. This is what makes the registry per-layer:
* an asking layer exempts this registration only when the owner is inside the
* asker's own boundary (transitively through other nested layers).
*
* The owner is NOT the layer's portal root — that is the `source` argument.
* It is the thing in the main document tree that the portal speaks for.
*
* Resolver form is supported and re-read on every lookup, so an owner that
* mounts and unmounts with its component can be named once.
*
* A missing or unresolved owner grants no scoped exemption and emits a
* development warning. Even a registration used only through the unscoped
* registry-wide view should name its logical owner to keep the contract
* explicit.
*/
owner?: ElementSource;
}
NumberInputInit from @llui/components
export interface NumberInputInit {
value?: number | null
min?: number
max?: number
step?: number
disabled?: boolean
readonly?: boolean
}
NumberInputParts from @llui/components
export interface NumberInputParts {
root: {
'data-scope': 'number-input'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
input: {
type: 'text'
role: 'spinbutton'
inputmode: 'decimal'
'aria-valuemin': Signal<number | undefined>
'aria-valuemax': Signal<number | undefined>
'aria-valuenow': Signal<number | undefined>
'aria-disabled': Signal<'true' | undefined>
'aria-readonly': Signal<'true' | undefined>
disabled: Signal<boolean>
readonly: Signal<boolean>
value: Signal<string>
'data-scope': 'number-input'
'data-part': 'input'
onInput: (e: Event) => void
onBlur: (e: FocusEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
increment: {
type: 'button'
'aria-label': string
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-scope': 'number-input'
'data-part': 'increment'
tabindex: -1
onClick: (e: MouseEvent) => void
}
decrement: {
type: 'button'
'aria-label': string
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-scope': 'number-input'
'data-part': 'decrement'
tabindex: -1
onClick: (e: MouseEvent) => void
}
}
NumberInputState from @llui/components
Number input — numeric field with increment/decrement buttons. Clamps to min/max and snaps to step. Keyboard: Arrow Up/Down, PageUp/PageDown, Home/End.
export interface NumberInputState {
value: number | null
/**
* The bounds, ABSENT when that side is unbounded — the state's `min`/`max`/
* `step` ARE a `NumericGrid`, which is what lets `clampToStep(value, state)`
* take the state object straight in.
*
* A bound is never `±Infinity` and never `NaN`: this is the ONE component in
* the package whose DEFAULT range is unbounded, and it used to spell that
* `min: -Infinity` / `max: Infinity` in state, which `JSON.stringify` writes
* as `null` — so its default state did not survive a round trip and the
* rehydrated object held `null` in a `number` field (#177). Absence is the
* serializable spelling of the same fact; `finiteBound` is the one place it
* is decided.
*/
min?: number
max?: number
step: number
disabled: boolean
readonly: boolean
/** Allow a free-text input value while the user is typing. */
rawText: string
}
NumericGrid from @llui/components
A stepped numeric range. min/max default to unbounded and step to 0
(= continuous). Component states name their fields the same way, so a state
object can be passed straight in.
export interface NumericGrid {
min?: number
max?: number
step?: number
}
PaginationInit from @llui/components
export interface PaginationInit {
page?: number
pageSize?: number
total?: number
siblings?: number
boundaries?: number
disabled?: boolean
dir?: TextDirection
}
PaginationParts from @llui/components
export interface PaginationParts {
root: {
role: 'navigation'
'aria-label': string
'data-scope': 'pagination'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
prevTrigger: {
type: 'button'
'aria-label': string
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-scope': 'pagination'
'data-part': 'prev-trigger'
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
nextTrigger: {
type: 'button'
'aria-label': string
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-scope': 'pagination'
'data-part': 'next-trigger'
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
item: (page: number) => {
type: 'button'
'aria-label': string
'aria-current': Signal<'page' | undefined>
'data-selected': Signal<'' | undefined>
'data-scope': 'pagination'
'data-part': 'item'
'data-value': string
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
ellipsis: (position: 'start' | 'end') => {
'aria-hidden': 'true'
'data-scope': 'pagination'
'data-part': 'ellipsis'
'data-position': 'start' | 'end'
}
}
PaginationState from @llui/components
Pagination — page navigation with ellipses for large ranges.
page is 1-based. Siblings are the count of pages shown on each side
of the current page. Boundaries are shown at the start/end.
export interface PaginationState {
page: number
pageSize: number
total: number
siblings: number
boundaries: number
disabled: boolean
dir: TextDirection
}
ParsedDateValue from @llui/components
export interface ParsedDateValue {
date: Date
/**
* True when the input was a bare calendar date. Formatters MUST then render
* in UTC (where the anchor was taken), otherwise the ambient zone shifts the
* rendered day.
*/
dateOnly: boolean
}
PasswordInputInit from @llui/components
export interface PasswordInputInit {
value?: string
visible?: boolean
disabled?: boolean
}
PasswordInputParts from @llui/components
export interface PasswordInputParts {
root: {
'data-scope': 'password-input'
'data-part': 'root'
'data-visible': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
}
input: {
type: Signal<'text' | 'password'>
autocomplete: string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'password-input'
'data-part': 'input'
onInput: (e: Event) => void
}
visibilityTrigger: {
type: 'button'
'aria-label': Signal<string>
'aria-pressed': Signal<boolean>
disabled: Signal<boolean>
tabindex: Signal<number>
'data-scope': 'password-input'
'data-part': 'visibility-trigger'
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
}
PasswordInputState from @llui/components
Password input — text input with show/hide visibility toggle.
export interface PasswordInputState {
value: string
visible: boolean
disabled: boolean
}
PinInputInit from @llui/components
export interface PinInputInit {
length?: number
type?: PinType
mask?: boolean
disabled?: boolean
values?: string[]
}
PinInputParts from @llui/components
export interface PinInputParts {
root: {
role: 'group'
'aria-labelledby': string
'data-scope': 'pin-input'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
label: {
id: string
'data-scope': 'pin-input'
'data-part': 'label'
}
/** Props for the input at a given index. */
input: (index: number) => {
type: Signal<'text' | 'password'>
inputmode: Signal<'numeric' | 'text'>
pattern: Signal<string>
maxlength: 1
autocomplete: 'off'
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'pin-input'
'data-part': 'input'
'data-index': string
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
onPaste: (e: ClipboardEvent) => void
}
}
PinInputState from @llui/components
export interface PinInputState {
values: string[]
length: number
type: PinType
mask: boolean
disabled: boolean
focusedIndex: number
}
PopoverInit from @llui/components
export interface PopoverInit {
open?: boolean
/** Skip enter/exit animations — close unmounts synchronously (default: true). */
skipAnimations?: boolean
}
PopoverOverlayOptions from @llui/components
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<PopoverState>
send: Send<PopoverMsg>
parts: PopoverParts
content: () => Renderable
/**
* Optional enter/leave transition for the popover content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example popover.overlay({ state, send, parts, content, transition: fade({ duration: 150 }) })
*/
transition?: TransitionOptions
/** Placement preference — bottom | top | right | left with -start/-end variants. */
placement?: Placement
/** Offset between trigger and content, px (default: 8). */
offset?: number
/** Auto-flip to opposite side (default: true). */
flip?: boolean
/** Shift to keep in viewport (default: true). */
shift?: boolean
/** Close on Escape (default: true). */
closeOnEscape?: boolean
/** Close on outside click (default: true). */
closeOnOutsideClick?: boolean
/** Trap focus inside popover while open (default: false — non-modal). */
trapFocus?: boolean
/** Restore focus to trigger on close (default: true). */
restoreFocus?: boolean
/** Portal target (default: 'body'). */
target?: string | HTMLElement
/** Arrow element selector within content (optional). */
arrowSelector?: string
}
PopoverParts from @llui/components
export interface PopoverParts {
trigger: {
type: 'button'
'aria-haspopup': 'dialog'
'aria-expanded': Signal<boolean>
'aria-controls': string
id: string
'data-state': Signal<'open' | 'closed'>
'data-scope': 'popover'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
}
positioner: {
'data-scope': 'popover'
'data-part': 'positioner'
style: string
}
content: {
role: 'dialog'
id: string
'aria-labelledby': string
tabindex: -1
'data-state': Signal<PresenceStatus>
'data-scope': 'popover'
'data-part': 'content'
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
title: {
id: string
'data-scope': 'popover'
'data-part': 'title'
}
description: {
id: string
'data-scope': 'popover'
'data-part': 'description'
}
arrow: {
'data-scope': 'popover'
'data-part': 'arrow'
}
closeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'popover'
'data-part': 'close-trigger'
onClick: (e: MouseEvent) => void
}
}
PopoverState from @llui/components
Popover — click-triggered, non-modal floating overlay anchored to its trigger. Use for menus, date pickers, color pickers, filters, etc.
Like dialog, has a pure state machine + a view helper (overlay()) that
wires floating-ui positioning, dismissable, and optional focus trapping.
export interface PopoverState {
open: boolean
/** Presence lifecycle — drives data-state and keeps the node mounted through exit animations. */
status: PresenceStatus
/** When true, close transitions go straight to 'closed' (no exit-animation wait). */
skipAnimations: boolean
}
PresenceInit from @llui/components
export interface PresenceInit {
/** Initial presence — true starts in 'open', false starts in 'closed'. */
present?: boolean
/** Whether 'closed' means "unmount" (true) or "hidden but mounted" (false). Default: true. */
unmountOnExit?: boolean
}
PresenceParts from @llui/components
export interface PresenceParts {
root: {
'data-scope': 'presence'
'data-part': 'root'
'data-state': Signal<PresenceStatus>
hidden: Signal<boolean>
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
}
PresenceState from @llui/components
export interface PresenceState {
status: PresenceStatus
unmountOnExit: boolean
}
PresetParts from @llui/components
export interface PresetParts {
type: 'button'
'data-scope': 'date-picker'
'data-part': 'preset'
onClick: (e: MouseEvent) => void
}
PresetRange from @llui/components
A named preset range a consumer can render as a quick-select button.
export interface PresetRange {
start: string | null
end: string | null
}
ProgressInit from @llui/components
export interface ProgressInit {
value?: number | null
min?: number
max?: number
orientation?: ProgressOrientation
}
ProgressParts from @llui/components
export interface ProgressParts {
root: {
role: 'progressbar'
'aria-valuemin': Signal<number>
'aria-valuemax': Signal<number>
'aria-valuenow': Signal<number | undefined>
'aria-label': string | undefined
'data-state': Signal<'indeterminate' | 'complete' | 'loading'>
'data-orientation': Signal<ProgressOrientation>
'data-scope': 'progress'
'data-part': 'root'
}
track: {
'data-state': Signal<'indeterminate' | 'complete' | 'loading'>
'data-orientation': Signal<ProgressOrientation>
'data-scope': 'progress'
'data-part': 'track'
}
range: {
'data-state': Signal<'indeterminate' | 'complete' | 'loading'>
'data-orientation': Signal<ProgressOrientation>
'data-scope': 'progress'
'data-part': 'range'
style: Signal<string>
}
label: {
'data-scope': 'progress'
'data-part': 'label'
}
valueText: Signal<string>
}
ProgressState from @llui/components
export interface ProgressState {
value: number | null
min: number
max: number
orientation: ProgressOrientation
}
QrCodeInit from @llui/components
export interface QrCodeInit {
value?: string
matrix?: boolean[][]
errorCorrection?: ErrorCorrectionLevel
}
QrCodeParts from @llui/components
export interface QrCodeParts {
root: {
'data-scope': 'qr-code'
'data-part': 'root'
'aria-label': string
}
svg: {
'data-scope': 'qr-code'
'data-part': 'svg'
role: 'img'
viewBox: Signal<string>
'shape-rendering': 'crispEdges'
}
background: {
'data-scope': 'qr-code'
'data-part': 'background'
}
foreground: {
'data-scope': 'qr-code'
'data-part': 'foreground'
d: Signal<string>
}
downloadTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'qr-code'
'data-part': 'download-trigger'
onClick: (e: MouseEvent) => void
}
}
QrCodeState from @llui/components
export interface QrCodeState {
value: string
/** NxN boolean matrix — true means dark (filled) module. */
matrix: boolean[][]
errorCorrection: ErrorCorrectionLevel
}
RadioGroupInit from @llui/components
export interface RadioGroupInit {
value?: string | null
items?: string[]
disabledItems?: string[]
disabled?: boolean
orientation?: Orientation
loopFocus?: boolean
dir?: 'ltr' | 'rtl'
}
RadioGroupParts from @llui/components
export interface RadioGroupParts {
root: {
role: 'radiogroup'
'aria-orientation': Signal<Orientation>
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'radio-group'
'data-part': 'root'
'data-orientation': Signal<Orientation>
'data-disabled': Signal<'' | undefined>
}
item: (value: string) => RadioItemParts
}
RadioGroupState from @llui/components
export interface RadioGroupState {
value: string | null
items: string[]
disabledItems: string[]
disabled: boolean
orientation: Orientation
/** Whether arrow navigation wraps at the ends. Default true (WAI-ARIA radio
* groups wrap). Present on every roving widget in the package — its absence
* here was pure drift between the copies of the navigation code (#126). */
loopFocus: boolean
/** Reading direction. Under 'rtl', ArrowLeft/ArrowRight swap meaning. */
dir: 'ltr' | 'rtl'
}
RadioItemParts from @llui/components
export interface RadioItemParts {
root: {
role: 'radio'
id: string
'aria-checked': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
'data-state': Signal<'checked' | 'unchecked'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'radio-group'
'data-part': 'item'
'data-value': string
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
label: {
'data-scope': 'radio-group'
'data-part': 'label'
'data-value': string
for: string
}
indicator: {
'data-state': Signal<'checked' | 'unchecked'>
'data-scope': 'radio-group'
'data-part': 'indicator'
}
}
RatingGroupInit from @llui/components
export interface RatingGroupInit {
value?: number
count?: number
allowHalf?: boolean
disabled?: boolean
readonly?: boolean
dir?: 'ltr' | 'rtl'
}
RatingGroupParts from @llui/components
export interface RatingGroupParts {
root: {
role: 'radiogroup'
'aria-label': string | undefined
'aria-disabled': Signal<'true' | undefined>
'aria-readonly': Signal<'true' | undefined>
'data-scope': 'rating-group'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
'data-readonly': Signal<'' | undefined>
}
item: (index: number) => RatingItemParts
}
RatingGroupState from @llui/components
Rating group — a sequence of clickable items (stars) representing a discrete rating. Supports half-step ratings and keyboard navigation.
export interface RatingGroupState {
value: number
count: number
/** If true, allows values like 1.5 (half-stars). */
allowHalf: boolean
disabled: boolean
readonly: boolean
hoveredValue: number | null
/** Reading direction. Under 'rtl', ArrowLeft/ArrowRight swap meaning. */
dir: 'ltr' | 'rtl'
}
RatingItemParts from @llui/components
export interface RatingItemParts {
root: {
role: 'radio'
'aria-checked': Signal<boolean>
'data-fill': Signal<ItemFill>
'data-scope': 'rating-group'
'data-part': 'item'
'data-value': string
'data-disabled': Signal<'' | undefined>
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onPointerMove: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
}
RejectedFile from @llui/components
export interface RejectedFile {
file: FileMeta
errors: FileError[]
}
RovingItem from @llui/components
export interface RovingItem {
value: string;
/** Disabled items are skipped by arrow/Home/End navigation. */
disabled?: boolean;
}
RovingOptions from @llui/components
export interface RovingOptions {
/** Arrow axis — 'horizontal' uses Left/Right, 'vertical' uses Up/Down. Default 'horizontal'. */
orientation?: RovingOrientation;
/** Whether arrow navigation wraps at the ends. Default true. */
loop?: boolean;
/**
* An element used to resolve text direction for RTL arrow flipping
* (typically the event's `currentTarget`). When it resolves to
* `dir="rtl"`, ArrowLeft/ArrowRight swap. Optional.
*/
element?: Element | null;
}
ScrollAreaInit from @llui/components
export interface ScrollAreaInit {
visibility?: ScrollbarVisibility
}
ScrollAreaParts from @llui/components
export interface ScrollAreaParts {
root: {
'data-scope': 'scroll-area'
'data-part': 'root'
'data-scrolling': Signal<'' | undefined>
'data-hovered': Signal<'' | undefined>
onMouseEnter: (e: MouseEvent) => void
onMouseLeave: (e: MouseEvent) => void
}
viewport: {
tabindex: 0
'data-scope': 'scroll-area'
'data-part': 'viewport'
onScroll: (e: Event) => void
}
content: {
'data-scope': 'scroll-area'
'data-part': 'content'
}
scrollbarX: {
'data-scope': 'scroll-area'
'data-part': 'scrollbar'
'data-axis': 'x'
'data-visible': Signal<'' | undefined>
}
scrollbarY: {
'data-scope': 'scroll-area'
'data-part': 'scrollbar'
'data-axis': 'y'
'data-visible': Signal<'' | undefined>
}
thumbX: {
'data-scope': 'scroll-area'
'data-part': 'thumb'
'data-axis': 'x'
style: Signal<string>
}
thumbY: {
'data-scope': 'scroll-area'
'data-part': 'thumb'
'data-axis': 'y'
style: Signal<string>
}
corner: {
'data-scope': 'scroll-area'
'data-part': 'corner'
'data-visible': Signal<'' | undefined>
}
}
ScrollAreaState from @llui/components
export interface ScrollAreaState extends ScrollDims {
overflowX: boolean
overflowY: boolean
/** Whether the user is currently scrolling (set/cleared by the consumer
* via a debounced scroll handler). */
scrolling: boolean
/** Whether the pointer is over the scroll area. */
hovered: boolean
visibility: ScrollbarVisibility
}
ScrollDims from @llui/components
export interface ScrollDims {
scrollTop: number
scrollLeft: number
scrollWidth: number
scrollHeight: number
clientWidth: number
clientHeight: number
}
SearchFieldInit from @llui/components
export interface SearchFieldInit {
value?: string
disabled?: boolean
}
SearchFieldParts from @llui/components
export interface SearchFieldParts {
root: {
role: 'search'
'data-scope': 'search-field'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
label: {
'data-scope': 'search-field'
'data-part': 'label'
}
input: {
type: 'search'
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'search-field'
'data-part': 'input'
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
}
clearTrigger: {
type: 'button'
'aria-label': string
hidden: Signal<boolean>
tabindex: -1
'data-scope': 'search-field'
'data-part': 'clear-trigger'
onClick: (e: MouseEvent) => void
}
}
SearchFieldState from @llui/components
Search field — a role="search" landmark wrapping a type="search" input
with a clear button. Escape clears the field (when non-empty), Enter submits
the current value.
Debounced live search is intentionally NOT built into this machine. Keep it
consumer-side: debounce the setValue message (or a derived "search" effect)
with debounce from @llui/effects so the search trigger fires once the user
pauses typing, rather than on every keystroke.
export interface SearchFieldState {
value: string
disabled: boolean
}
SelectGroup from @llui/components
A labelled section of options (rendered like <optgroup>). items are the
option VALUES belonging to the group, in visual order. Groups are an
additive, parallel structure: the flat items list always remains the
source of truth for navigation/highlight indices and item ids — when
groups is provided without an explicit items list, init derives the
flat list by concatenating each group's items in order. A plain flat
string[] (no groups) keeps working unchanged. Group LABELS are never
options, so highlight/typeahead/arrow navigation skips over them for free.
export interface SelectGroup {
id: string
label: string
items: string[]
}
SelectGroupParts from @llui/components
export interface SelectGroupParts {
group: {
role: 'group'
'aria-labelledby': string
'data-scope': 'select'
'data-part': 'group'
'data-group': string
}
groupLabel: {
id: string
'aria-hidden': 'true'
'data-scope': 'select'
'data-part': 'group-label'
'data-group': string
}
}
SelectInit from @llui/components
export interface SelectInit {
value?: string[]
items?: string[]
/** Optional labelled sections. When provided without `items`, the flat
* `items` list is derived by concatenating each group's `items` in order. */
groups?: SelectGroup[]
disabledItems?: string[]
selectionMode?: SelectionMode
disabled?: boolean
required?: boolean
}
SelectItemParts from @llui/components
export interface SelectItemParts {
item: {
role: 'option'
id: string
'aria-selected': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
'data-state': Signal<'selected' | undefined>
'data-highlighted': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
'data-scope': 'select'
'data-part': 'item'
'data-value': string
/** The option's live position in the flat item list (reactive — reused rows
* never report a stale index). */
'data-index': Signal<string>
onClick: (e: MouseEvent) => void
onPointerMove: (e: PointerEvent) => void
}
}
SelectOverlayOptions from @llui/components
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<SelectState>
send: Send<SelectMsg>
parts: SelectParts
content: () => Renderable
/**
* Optional enter/leave transition for the select listbox (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, giving the raw-`open` select an exit
* animation for free. Omitted ⇒ the listbox closes synchronously as before.
*
* @example select.overlay({ state, send, parts, content, transition: fade({ duration: 120 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
/** Match content width to trigger width (default: true). */
sameWidth?: boolean
target?: string | HTMLElement
}
SelectParts from @llui/components
export interface SelectParts {
trigger: {
type: 'button'
role: 'combobox'
'aria-haspopup': 'listbox'
'aria-expanded': Signal<boolean>
'aria-controls': string
'aria-activedescendant': Signal<string | undefined>
'aria-disabled': Signal<'true' | undefined>
'aria-required': Signal<'true' | undefined>
id: string
disabled: Signal<boolean>
'data-state': Signal<'open' | 'closed'>
/** Present while the trigger is showing the PLACEHOLDER rather than a
* value. `valueText` already falls back to the placeholder string, but a
* string is not something CSS can branch on, so without this the
* placeholder renders at full foreground weight and reads as a real
* selection. This is the attribute every shadcn Select greys it from. */
'data-placeholder': Signal<'' | undefined>
'data-scope': 'select'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
positioner: {
'data-scope': 'select'
'data-part': 'positioner'
style: string
}
content: {
role: 'listbox'
id: string
'aria-multiselectable': Signal<'true' | undefined>
'aria-labelledby': string
tabindex: -1
'data-state': Signal<'open' | 'closed'>
'data-scope': 'select'
'data-part': 'content'
onKeyDown: (e: KeyboardEvent) => void
}
hiddenSelect: {
'aria-hidden': 'true'
tabindex: -1
style: string
/** Native form field name, or `undefined` when `name` was not supplied. */
name: string | undefined
disabled: Signal<boolean>
multiple: Signal<boolean>
required: Signal<boolean>
'data-scope': 'select'
'data-part': 'hidden-select'
}
/** An `<option>` for the hidden native `<select>`. Render one per item inside
* `hiddenSelect` so the browser submits the selection under the form `name`. */
hiddenOption: (value: string) => {
value: string
selected: Signal<boolean>
'data-scope': 'select'
'data-part': 'hidden-option'
}
/** Build the parts for an option by VALUE. The optional `index` is accepted
* for call-site convenience only — it is NOT used for identity (highlight,
* selection and ids are all value-keyed), so a reused row is never stale. */
item: (value: string, index?: number) => SelectItemParts
/** Parts for a labelled option group (`<optgroup>`-style section). Pass the
* group id; render the section element with `group` and its label element
* (referenced by `aria-labelledby`) with `groupLabel`. Group labels are not
* options, so navigation skips them automatically. */
group: (id: string) => SelectGroupParts
/** Selected value(s) — use for rendering the trigger label. */
valueText: Signal<string>
}
SelectState from @llui/components
export interface SelectState {
open: boolean
value: string[]
items: string[]
groups: SelectGroup[]
disabledItems: string[]
selectionMode: SelectionMode
/** The highlighted option's VALUE (not its index). Value-based identity keeps
* the highlight pinned to the right option when the list is filtered or
* reordered and rows are reused (value-keyed `each`). */
highlightedValue: string | null
disabled: boolean
required: boolean
typeahead: string
typeaheadExpiresAt: number
}
SignaturePadInit from @llui/components
export interface SignaturePadInit {
strokes?: Stroke[]
disabled?: boolean
readonly?: boolean
}
SignaturePadParts from @llui/components
export interface SignaturePadParts {
root: {
role: 'application'
'aria-label': string
'data-scope': 'signature-pad'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
'data-readonly': Signal<'' | undefined>
'data-drawing': Signal<'' | undefined>
}
control: {
'data-scope': 'signature-pad'
'data-part': 'control'
}
clearTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'signature-pad'
'data-part': 'clear-trigger'
onClick: (e: MouseEvent) => void
}
undoTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'signature-pad'
'data-part': 'undo-trigger'
onClick: (e: MouseEvent) => void
}
guide: {
'data-scope': 'signature-pad'
'data-part': 'guide'
'aria-hidden': 'true'
}
hiddenInput: {
type: 'hidden'
value: Signal<string>
name?: string
'data-scope': 'signature-pad'
'data-part': 'hidden-input'
}
}
SignaturePadPoint from @llui/components
Signature pad — capture free-form strokes on a canvas. The state machine tracks strokes as arrays of points; the view renders them onto a
Pointer event wiring in the view layer:
onPointerDown: (e) => { canvas.setPointerCapture(e.pointerId) send({ type: 'strokeStart', x: e.offsetX, y: e.offsetY }) } onPointerMove: (e) => { if (state.drawing) send({ type: 'strokePoint', x: e.offsetX, y: e.offsetY }) } onPointerUp: () => send({ type: 'strokeEnd' })
export interface Point {
x: number
y: number
/** Pressure 0..1 (optional; from PointerEvent.pressure). */
pressure?: number
}
SignaturePadState from @llui/components
export interface SignaturePadState {
strokes: Stroke[]
/** Stroke currently being drawn, or null. */
current: Stroke | null
drawing: boolean
disabled: boolean
readonly: boolean
}
SliderInit from @llui/components
export interface SliderInit {
value?: number[]
min?: number
max?: number
step?: number
disabled?: boolean
orientation?: Orientation
minStepsBetweenThumbs?: number
dir?: 'ltr' | 'rtl'
}
SliderParts from @llui/components
export interface SliderParts {
root: {
'data-scope': 'slider'
'data-part': 'root'
'data-orientation': Signal<Orientation>
'data-disabled': Signal<'' | undefined>
}
control: {
'data-scope': 'slider'
'data-part': 'control'
'data-orientation': Signal<Orientation>
onPointerDown: (e: PointerEvent) => void
}
track: {
'data-scope': 'slider'
'data-part': 'track'
'data-orientation': Signal<Orientation>
}
range: {
'data-scope': 'slider'
'data-part': 'range'
'data-orientation': Signal<Orientation>
style: Signal<string>
}
thumb: (index: number) => SliderThumbParts
/** Current raw values — reactive convenience. */
value: Signal<number[]>
}
SliderState from @llui/components
export interface SliderState {
/** One value per thumb. For a single-value slider, a one-element array. */
value: number[]
min: number
max: number
step: number
disabled: boolean
orientation: Orientation
/** Minimum gap enforced between adjacent thumbs (range slider). */
minStepsBetweenThumbs: number
/** Reading direction. Under 'rtl' horizontal arrow keys are flipped. */
dir: 'ltr' | 'rtl'
}
SliderThumbParts from @llui/components
export interface SliderThumbParts {
thumb: {
role: 'slider'
'aria-valuemin': Signal<number>
'aria-valuemax': Signal<number>
'aria-valuenow': Signal<number>
'aria-orientation': Signal<Orientation>
'aria-disabled': Signal<'true' | undefined>
'data-orientation': Signal<Orientation>
'data-disabled': Signal<'' | undefined>
'data-scope': 'slider'
'data-part': 'thumb'
'data-index': string
tabindex: Signal<number>
onKeyDown: (e: KeyboardEvent) => void
style: Signal<string>
}
}
SortableParts from @llui/components
export interface SortableParts {
root: {
'data-scope': 'sortable'
'data-part': 'root'
'data-container-id': string
'data-dragging': Signal<'' | undefined>
onPointerMove: (e: PointerEvent) => void
onPointerUp: (e: PointerEvent) => void
onPointerCancel: (e: PointerEvent) => void
}
item: (
id: string,
index: number,
) => {
'data-scope': 'sortable'
'data-part': 'item'
'data-index': string
'data-id': string
'data-dragging': Signal<'' | undefined>
'data-over': Signal<'' | undefined>
'data-shift': Signal<'up' | 'down' | undefined>
'style.transform': Signal<string | undefined>
'style.zIndex': Signal<string | undefined>
}
handle: (
id: string,
index: number,
) => {
'data-scope': 'sortable'
'data-part': 'handle'
role: 'button'
tabindex: 0
'aria-grabbed': Signal<boolean>
'aria-label': string
onPointerDown: (e: PointerEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
}
SortableState from @llui/components
export interface SortableState {
dragging: DragState | null
}
SplitterInit from @llui/components
export interface SplitterInit {
position?: number
min?: number
max?: number
step?: number
orientation?: Orientation
disabled?: boolean
dir?: 'ltr' | 'rtl'
}
SplitterParts from @llui/components
export interface SplitterParts {
root: {
'data-scope': 'splitter'
'data-part': 'root'
'data-orientation': Signal<Orientation>
'data-disabled': Signal<'' | undefined>
'data-dragging': Signal<'' | undefined>
}
primaryPanel: {
'data-scope': 'splitter'
'data-part': 'primary-panel'
style: Signal<string>
}
secondaryPanel: {
'data-scope': 'splitter'
'data-part': 'secondary-panel'
style: Signal<string>
}
resizeTrigger: {
role: 'separator'
'aria-orientation': Signal<Orientation>
'aria-valuemin': Signal<number>
'aria-valuemax': Signal<number>
'aria-valuenow': Signal<number>
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'splitter'
'data-part': 'resize-trigger'
'data-orientation': Signal<Orientation>
tabindex: Signal<number>
onKeyDown: (e: KeyboardEvent) => void
onPointerDown: (e: PointerEvent) => void
}
}
SplitterState from @llui/components
export interface SplitterState {
position: number
min: number
max: number
step: number
orientation: Orientation
disabled: boolean
dragging: boolean
/** Reading direction. Under 'rtl' horizontal arrow keys are flipped. */
dir: 'ltr' | 'rtl'
}
StepsInit from @llui/components
export interface StepsInit {
current?: number
completed?: number[]
steps?: string[]
linear?: boolean
disabled?: boolean
}
StepsItemParts from @llui/components
export interface StepsItemParts {
item: {
'data-scope': 'steps'
'data-part': 'item'
'data-status': Signal<StepStatus>
'data-index': string
'aria-current': Signal<'step' | undefined>
}
trigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'steps'
'data-part': 'trigger'
'data-status': Signal<StepStatus>
onClick: (e: MouseEvent) => void
}
separator: {
'data-scope': 'steps'
'data-part': 'separator'
'data-status': Signal<StepStatus>
'aria-hidden': 'true'
}
}
StepsParts from @llui/components
export interface StepsParts {
root: {
role: 'group'
'aria-label': string
'data-scope': 'steps'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
nextTrigger: {
type: 'button'
disabled: Signal<boolean>
'data-scope': 'steps'
'data-part': 'next-trigger'
onClick: (e: MouseEvent) => void
}
prevTrigger: {
type: 'button'
disabled: Signal<boolean>
'data-scope': 'steps'
'data-part': 'prev-trigger'
onClick: (e: MouseEvent) => void
}
item: (index: number) => StepsItemParts
}
StepsState from @llui/components
export interface StepsState {
current: number
completed: number[]
errors: number[]
steps: string[]
/** If linear, users cannot skip steps. */
linear: boolean
disabled: boolean
}
SwatchParts from @llui/components
export interface SwatchParts {
type: 'button'
'aria-label': string
'aria-pressed': Signal<boolean>
'data-scope': 'color-picker'
'data-part': 'swatch'
'data-value': string
'data-state': Signal<'selected' | undefined>
style: string
onClick: (e: MouseEvent) => void
}
SwitchInit from @llui/components
export interface SwitchInit {
checked?: boolean
disabled?: boolean
}
SwitchParts from @llui/components
export interface SwitchParts {
root: {
role: 'switch'
'aria-checked': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
'data-state': Signal<'checked' | 'unchecked'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'switch'
'data-part': 'root'
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
track: {
'data-state': Signal<'checked' | 'unchecked'>
'data-scope': 'switch'
'data-part': 'track'
}
thumb: {
'data-state': Signal<'checked' | 'unchecked'>
'data-scope': 'switch'
'data-part': 'thumb'
}
hiddenInput: {
type: 'checkbox'
role: 'switch'
'aria-hidden': 'true'
tabindex: -1
style: string
checked: Signal<boolean>
disabled: Signal<boolean>
'data-scope': 'switch'
'data-part': 'hidden-input'
}
}
SwitchState from @llui/components
Switch — two-state on/off control. Semantically like a checkbox but
visually a toggle track + thumb. Uses role="switch" for ARIA.
export interface SwitchState {
checked: boolean
disabled: boolean
}
TableCellCoord from @llui/components
export interface TableCellCoord {
/** Row index into `rows`, or {@link HEADER_ROW_INDEX} for the header row. */
rowIndex: number
colIndex: number
}
TableCellParts from @llui/components
export interface TableCellParts {
role: 'gridcell'
'aria-colindex': number
tabindex: Signal<number>
'data-scope': 'table'
'data-part': 'cell'
/** 0-based row index — addresses the cell for roving DOM focus. */
'data-row-index': number
/** 0-based column index — addresses the cell for roving DOM focus. */
'data-col-index': number
'data-focused': Signal<'' | undefined>
onFocus: (e: FocusEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
TableCheckboxParts from @llui/components
export interface TableCheckboxParts {
role: 'checkbox'
'aria-checked': Signal<'true' | 'false' | 'mixed'>
'data-scope': 'table'
'data-part': 'select-all' | 'row-checkbox'
'data-state': Signal<'checked' | 'unchecked' | 'indeterminate'>
/** Always `-1`: a `role="grid"` has exactly ONE tab stop, the roving cell. */
tabindex: -1
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
TableColumn from @llui/components
export interface TableColumn {
/** Opaque column id. */
id: string
/** Whether this column participates in sorting. Defaults to false. */
sortable?: boolean
}
TableColumnHeaderParts from @llui/components
export interface TableColumnHeaderParts {
role: 'columnheader'
id: string
'aria-sort': Signal<'ascending' | 'descending' | 'none' | undefined>
/**
* Roving tab stop. The header row participates in the grid's single-tab-stop
* sequence, because it hosts controls — the sort toggle on every sortable
* column, and the select-all checkbox — that are otherwise unreachable by
* keyboard.
*/
tabindex: Signal<number>
'data-scope': 'table'
'data-part': 'column-header'
'data-column': string
/** Always {@link HEADER_ROW_INDEX} — addresses the header for roving DOM focus. */
'data-row-index': typeof HEADER_ROW_INDEX
/** 0-based column index (`-1` for a column not in `columns`). */
'data-col-index': Signal<number>
'data-focused': Signal<'' | undefined>
'data-sortable': Signal<'' | undefined>
'data-sort': Signal<SortDirection | undefined>
onFocus: (e: FocusEvent) => void
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
TableConnectOptions from @llui/components
export interface ConnectOptions {
id: string
}
TableInit from @llui/components
export interface TableInit {
columns?: TableColumn[]
rows?: string[]
sort?: TableSort | null
selection?: string[]
selectionMode?: TableSelectionMode
focusedCell?: TableCellCoord | null
pageSize?: number
descFirst?: boolean
disabled?: boolean
}
TableParts from @llui/components
export interface TableParts {
root: {
role: 'grid'
id: string
'aria-multiselectable': Signal<'true' | undefined>
'aria-rowcount': Signal<number>
'aria-colcount': Signal<number>
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'table'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
columnHeader: (columnId: string) => TableColumnHeaderParts
row: (id: string, index: number) => TableRowParts
cell: (rowIndex: number, colIndex: number) => TableCellParts
/**
* The select-all checkbox, for the `columnheader` of `columnId`.
*
* The column id is a PARAMETER rather than a `connect()` option because the
* checkbox is unreachable by keyboard without it: it has no gridcell of its
* own, and every part inside a `role="grid"` except the one roving stop is
* `tabindex="-1"`, so its only keyboard route is Enter/Space on the roving
* header that hosts it — and the machine can only route that key if it knows
* which header. As an option it was forgettable, and forgetting it failed
* SILENTLY (no warning, no error; the key sorted the column or did nothing).
* As a required argument you cannot render the checkbox without answering the
* question, so the failure mode is gone at compile time.
*
* `columnId` must be a column in `state.columns` — that is what gives the
* header a colIndex to rove to. A column not in the list can never take the
* roving stop, and its header will not send `toggleAll` either.
*/
selectAllCheckbox: (columnId: string) => TableCheckboxParts
rowCheckbox: (id: string, index: number) => TableCheckboxParts
}
TableRowParts from @llui/components
export interface TableRowParts {
role: 'row'
'aria-selected': Signal<boolean | undefined>
'aria-rowindex': number
'data-scope': 'table'
'data-part': 'row'
'data-row': string
'data-selected': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
}
TableSort from @llui/components
export interface TableSort {
columnId: string
direction: SortDirection
}
TableState from @llui/components
export interface TableState {
/** Column descriptors in display order. */
columns: TableColumn[]
/** Row IDs in display order. Row DATA stays in the consumer. */
rows: string[]
/** Active sort, or null when unsorted. */
sort: TableSort | null
/** Selected row IDs. */
selection: string[]
selectionMode: TableSelectionMode
/** Focused cell coordinate (header row excluded; rowIndex addresses `rows`). */
focusedCell: TableCellCoord | null
/** Index of the last row toggled — the anchor for shift-range selection. */
rangeAnchor: number | null
/** Rows moved per PageUp/PageDown. */
pageSize: number
/** When true, the sort cycle starts at desc instead of asc. */
descFirst: boolean
disabled: boolean
}
TabsInit from @llui/components
export interface TabsInit {
value?: string
items?: string[]
disabledItems?: string[]
orientation?: Orientation
activation?: Activation
loopFocus?: boolean
deselectable?: boolean
dir?: 'ltr' | 'rtl'
}
TabsItemParts from @llui/components
export interface TabsItemParts {
trigger: {
type: 'button'
role: 'tab'
'aria-selected': Signal<boolean>
'aria-controls': string
'aria-disabled': Signal<'true' | undefined>
id: string
'data-state': Signal<'active' | 'inactive'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'tabs'
'data-part': 'trigger'
'data-value': string
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
}
panel: {
role: 'tabpanel'
id: string
'aria-labelledby': string
tabindex: 0
hidden: Signal<boolean>
'data-state': Signal<'active' | 'inactive'>
'data-scope': 'tabs'
'data-part': 'panel'
'data-value': string
}
}
TabsParts from @llui/components
export interface TabsParts {
root: {
'data-scope': 'tabs'
'data-part': 'root'
'data-orientation': Signal<Orientation>
}
/**
* A movable underline/highlight element. Position tracks the active
* trigger via CSS custom properties written by `watchTabIndicator()`:
* `--indicator-left`, `--indicator-top`, `--indicator-width`,
* `--indicator-height` — all in pixels.
* The consumer styles the indicator using these properties (e.g.
* `transform: translateX(var(--indicator-left))`).
*/
indicator: {
'data-scope': 'tabs'
'data-part': 'indicator'
'data-orientation': Signal<Orientation>
}
list: {
role: 'tablist'
'aria-orientation': Signal<Orientation>
'data-scope': 'tabs'
'data-part': 'list'
}
item: (value: string) => TabsItemParts
}
TabsState from @llui/components
export interface TabsState {
value: string
items: string[]
disabledItems: string[]
orientation: Orientation
activation: Activation
/** The currently focused (but not necessarily active) tab. For manual mode. */
focused: string | null
/** Whether Arrow navigation wraps at the ends of the tab list. Default: true. */
loopFocus: boolean
/** Whether clicking the active tab deselects it (empty value). Default: false. */
deselectable: boolean
/** Reading direction. Under 'rtl', ArrowLeft/ArrowRight swap meaning. */
dir: 'ltr' | 'rtl'
}
TagItemParts from @llui/components
export interface TagItemParts {
root: {
tabindex: Signal<number>
'data-scope': 'tags-input'
'data-part': 'tag'
'data-value': string
'data-index': string
'data-focused': Signal<'' | undefined>
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
}
remove: {
type: 'button'
'aria-label': string
tabindex: -1
'data-scope': 'tags-input'
'data-part': 'tag-remove'
onClick: (e: MouseEvent) => void
}
}
TagsInputInit from @llui/components
export interface TagsInputInit {
value?: string[]
inputValue?: string
disabled?: boolean
max?: number
unique?: boolean
}
TagsInputParts from @llui/components
export interface TagsInputParts {
root: {
role: 'group'
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'tags-input'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
input: {
type: 'text'
autocomplete: 'off'
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'tags-input'
'data-part': 'input'
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
onBlur: (e: FocusEvent) => void
}
tag: (value: string, index: number) => TagItemParts
clearTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'tags-input'
'data-part': 'clear-trigger'
onClick: (e: MouseEvent) => void
}
}
TagsInputState from @llui/components
Tags input — text input that creates chips (tags) on commit keys (Enter, comma, blur). Backspace on empty input removes the last tag. Each tag is focusable via arrow keys.
export interface TagsInputState {
value: string[]
inputValue: string
disabled: boolean
/** Maximum tag count. 0 = unlimited. */
max: number
/** Only allow unique values. */
unique: boolean
/** Currently-focused tag index, or null. */
focusedIndex: number | null
}
ThemeSwitchParts from @llui/components
export interface ThemeSwitchParts {
root: {
'data-scope': 'theme-switch'
'data-part': 'root'
role: 'group'
'aria-label': string
}
option: (theme: Theme) => {
type: 'button'
'data-scope': 'theme-switch'
'data-part': 'option'
'data-theme': Theme
'aria-pressed': Signal<boolean>
'aria-label': string
onClick: (e: MouseEvent) => void
}
toggle: {
type: 'button'
'data-scope': 'theme-switch'
'data-part': 'toggle'
'data-theme': Signal<Theme>
'aria-label': string
onClick: (e: MouseEvent) => void
}
}
ThemeSwitchState from @llui/components
export interface ThemeSwitchState {
theme: Theme
}
TimePickerInit from @llui/components
export interface TimePickerInit {
value?: TimeValue
format?: TimeFormat
minuteStep?: number
secondStep?: number
showSeconds?: boolean
disabled?: boolean
}
TimePickerParts from @llui/components
export interface TimePickerParts {
root: {
role: 'group'
'aria-label': string
'data-scope': 'time-picker'
'data-part': 'root'
'data-format': Signal<TimeFormat>
}
hoursInput: {
type: 'number'
role: 'spinbutton'
'aria-label': string
'aria-valuemin': Signal<number>
'aria-valuemax': Signal<number>
'aria-valuenow': Signal<number>
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'time-picker'
'data-part': 'hours-input'
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
}
minutesInput: {
type: 'number'
role: 'spinbutton'
'aria-label': string
'aria-valuemin': 0
'aria-valuemax': 59
'aria-valuenow': Signal<number>
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'time-picker'
'data-part': 'minutes-input'
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
}
periodTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'time-picker'
'data-part': 'period-trigger'
'data-period': Signal<'AM' | 'PM'>
onClick: (e: MouseEvent) => void
hidden: Signal<boolean>
}
}
TimePickerState from @llui/components
export interface TimePickerState {
value: TimeValue
format: TimeFormat
minuteStep: number
secondStep: number
showSeconds: boolean
disabled: boolean
}
TimerInit from @llui/components
export interface TimerInit {
direction?: Direction
targetMs?: number
elapsedMs?: number
}
TimerParts from @llui/components
export interface TimerParts {
root: {
'data-scope': 'timer'
'data-part': 'root'
'data-running': Signal<'' | undefined>
'data-direction': Signal<Direction>
}
display: {
role: 'timer'
'aria-live': 'off' | 'polite'
'data-scope': 'timer'
'data-part': 'display'
}
startTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'timer'
'data-part': 'start-trigger'
disabled: Signal<boolean>
onClick: (e: MouseEvent) => void
}
pauseTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'timer'
'data-part': 'pause-trigger'
disabled: Signal<boolean>
onClick: (e: MouseEvent) => void
}
resetTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'timer'
'data-part': 'reset-trigger'
onClick: (e: MouseEvent) => void
}
}
TimerState from @llui/components
export interface TimerState {
running: boolean
direction: Direction
/** Target in milliseconds for countdown (0 = no target, runs indefinitely). */
targetMs: number
/** Accumulated elapsed time, excluding the current running interval. */
elapsedMs: number
/** Timestamp when the current running interval started (null when paused). */
startedAt: number | null
}
TimeValue from @llui/components
export interface TimeValue {
hours: number
minutes: number
seconds: number
}
Toast from @llui/components
export interface Toast {
id: string
type: ToastType
title?: string
description?: string
/** ms until auto-dismiss. `null` = sticky (never auto-dismisses). */
duration: number | null
/** ms left before auto-dismiss. Counts down via `tick`. */
remainingMs: number
/** Whether the toast can be manually dismissed. */
dismissable: boolean
/** Pause flag — frozen countdown while set (consumer sets on hover/focus). */
paused: boolean
/** Optional per-toast politeness override; otherwise derived from `type`. */
ariaLive?: ToastPoliteness
/**
* Presence lifecycle for this toast (closed/opening/open/closing). Born
* `'open'`; a dismiss moves it to `'closing'` (when the toaster is animated)
* so it can play an exit animation before `animationEnd` removes it.
*/
status: PresenceStatus
}
ToasterInit from @llui/components
export interface ToasterInit {
max?: number
placement?: ToastPlacement
/** Play an exit animation on dismiss (toasts go to `'closing'` and stay
* mounted until `animationEnd`). Default false — instant removal. */
animated?: boolean
}
ToasterParts from @llui/components
export interface ToasterParts {
region: {
role: 'region'
'aria-label': string
tabindex: -1
'data-scope': 'toast'
'data-part': 'region'
'data-placement': Signal<ToastPlacement>
}
/**
* Build the per-row part descriptors for one toast. Takes the row's
* `Signal<Toast>` (e.g. the `item` from `each`) rather than a snapshot, so
* consumers don't `.peek()` in a reactive slot (which the signal compiler
* rejects). A toast's `id`/`type`/`ariaLive` are immutable for its lifetime —
* created then dismissed, never structurally replaced — so this reads the
* value once internally to build the id/role wiring; the keyed `each`
* rebuilds the row if `id` changes.
*/
toast: (toast: Signal<Toast>) => ToastItemParts
/**
* Reactive fraction (in [0,1]) of the countdown remaining for the toast with
* `id` — for a countdown progress bar. Sticky toasts report 1; a dismissed /
* missing toast reports 0.
*/
progress: (id: string) => Signal<number>
/**
* Reactive presence: whether the toast with `id` is still in the queue (i.e.
* should be mounted). Stays true through `'closing'` so the exit animation can
* play; flips false once `animationEnd` removes it. The keyed `each` over
* `toasts` already handles the actual mount/unmount — this is for consumers
* coordinating other elements off a single toast's lifecycle.
*/
isPresent: (id: string) => Signal<boolean>
}
ToasterState from @llui/components
export interface ToasterState {
toasts: Toast[]
max: number
placement: ToastPlacement
/**
* Whether dismissed toasts play an exit animation. When true a dismiss moves
* the toast to `'closing'` (kept mounted) until `animationEnd` removes it;
* when false (default) dismiss removes synchronously — today's behavior, no
* wait for an animationend that won't fire.
*/
animated: boolean
}
ToastItemParts from @llui/components
export interface ToastItemParts {
root: {
role: 'status' | 'alert'
'aria-atomic': 'true'
'aria-live': ToastPoliteness
id: string
'data-scope': 'toast'
'data-part': 'root'
'data-type': ToastType
'data-id': string
/** Reactive presence status (closed/opening/open/closing) for CSS-driven
* enter/exit animations. */
'data-state': Signal<PresenceStatus>
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onFocus: (e: FocusEvent) => void
onBlur: (e: FocusEvent) => void
/** Advance past the exit animation: a `'closing'` toast is removed from the
* queue once its animation/transition ends. */
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
title: {
id: string
'data-scope': 'toast'
'data-part': 'title'
}
description: {
id: string
'data-scope': 'toast'
'data-part': 'description'
}
closeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'toast'
'data-part': 'close-trigger'
onClick: (e: MouseEvent) => void
}
}
TocEntry from @llui/components
Table of contents — a navigation list that tracks which heading is
currently visible in the main scroll area and highlights it. The
state machine tracks the flat list of heading ids and the currently
active one; the view layer installs an IntersectionObserver in
onMount to detect which heading is on screen and dispatches
setActive.
Typical setup in onMount:
const headings = document.querySelectorAll('h2[id], h3[id]') const io = new IntersectionObserver((entries) => { for (const e of entries) { if (e.isIntersecting) send({ type: 'setActive', id: e.target.id }) } }, { rootMargin: '0px 0px -80% 0px' }) headings.forEach((h) => io.observe(h)) return () => io.disconnect()
export interface TocEntry {
id: string
label: string
/** Nesting level (1 = top-level). */
level: number
}
TocInit from @llui/components
export interface TocInit {
items?: TocEntry[]
activeId?: string | null
expanded?: string[]
}
TocParts from @llui/components
export interface TocParts {
root: {
role: 'navigation'
'aria-label': string
'data-scope': 'toc'
'data-part': 'root'
}
list: {
role: 'list'
'data-scope': 'toc'
'data-part': 'list'
}
item: (entry: TocEntry) => TocItemParts
}
TocState from @llui/components
export interface TocState {
items: TocEntry[]
activeId: string | null
/** Ids of entries the user has manually expanded (for collapsible sub-levels). */
expanded: string[]
}
ToggleGroupInit from @llui/components
export interface ToggleGroupInit {
value?: string[]
type?: 'single' | 'multiple'
items?: string[]
disabledItems?: string[]
disabled?: boolean
orientation?: Orientation
deselectable?: boolean
focused?: string | null
loopFocus?: boolean
dir?: 'ltr' | 'rtl'
}
ToggleGroupItemParts from @llui/components
export interface ToggleGroupItemParts {
root: {
type: 'button'
role: 'button'
'aria-pressed': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-state': Signal<'on' | 'off'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'toggle-group'
'data-part': 'item'
'data-value': string
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
}
}
ToggleGroupParts from @llui/components
export interface ToggleGroupParts {
root: {
role: 'group'
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'toggle-group'
'data-part': 'root'
'data-orientation': Signal<Orientation>
'data-disabled': Signal<'' | undefined>
}
item: (value: string) => ToggleGroupItemParts
}
ToggleGroupState from @llui/components
export interface ToggleGroupState {
value: string[]
type: 'single' | 'multiple'
items: string[]
disabledItems: string[]
disabled: boolean
orientation: Orientation
/** In single mode, whether the active item can be deselected. */
deselectable: boolean
/** The currently roving-focused item (independent of the pressed value). */
focused: string | null
/** Whether Arrow navigation wraps at the ends of the group. Default: true. */
loopFocus: boolean
/** Reading direction. Under 'rtl', ArrowLeft/ArrowRight swap meaning. */
dir: 'ltr' | 'rtl'
}
ToggleInit from @llui/components
export interface ToggleInit {
pressed?: boolean
disabled?: boolean
}
ToggleParts from @llui/components
export interface ToggleParts {
root: {
type: 'button'
role: 'button'
'aria-pressed': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-state': Signal<'on' | 'off'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'toggle'
'data-part': 'root'
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
}
ToggleState from @llui/components
Toggle button — a button that can be pressed or not. Unlike a checkbox, a toggle represents an action that is applied immediately (e.g. "bold" in a text editor toolbar).
export interface ToggleState {
pressed: boolean
disabled: boolean
}
ToolbarGroupParts from @llui/components
export interface ToolbarGroupParts {
root: {
role: 'group'
'data-scope': 'toolbar'
'data-part': 'group'
'aria-labelledby': string
}
label: {
id: string
'data-scope': 'toolbar'
'data-part': 'group-label'
}
}
ToolbarInit from @llui/components
export interface ToolbarInit {
items?: string[]
disabledItems?: string[]
focused?: string | null
orientation?: Orientation
loopFocus?: boolean
disabled?: boolean
}
ToolbarItemParts from @llui/components
export interface ToolbarItemParts {
root: {
'data-scope': 'toolbar'
'data-part': 'item'
'data-value': string
'data-disabled': Signal<'' | undefined>
'aria-disabled': Signal<'true' | undefined>
tabindex: Signal<number>
onKeyDown: (e: KeyboardEvent) => void
onFocus: () => void
}
}
ToolbarParts from @llui/components
export interface ToolbarParts {
root: {
role: 'toolbar'
'aria-orientation': Signal<Orientation>
'aria-label': string | undefined
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'toolbar'
'data-part': 'root'
'data-orientation': Signal<Orientation>
'data-disabled': Signal<'' | undefined>
}
separator: {
role: 'separator'
'aria-orientation': Signal<Orientation>
'data-scope': 'toolbar'
'data-part': 'separator'
}
item: (value: string) => ToolbarItemParts
group: (label: string) => ToolbarGroupParts
}
ToolbarState from @llui/components
export interface ToolbarState {
items: string[]
disabledItems: string[]
focused: string | null
orientation: Orientation
loopFocus: boolean
disabled: boolean
}
TooltipInit from @llui/components
export interface TooltipInit {
open?: boolean
/**
* Enable the exit-animation lifecycle: a close enters `closing` and stays
* mounted until `animationEnd`. Default false (instant unmount). The
* `overlay()` helper turns this on automatically when given a `transition`.
*/
animated?: boolean
}
TooltipOverlayOptions from @llui/components
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<TooltipState>
send: Send<TooltipMsg>
parts: TooltipParts
content: () => Renderable
/**
* Optional enter/leave transition for the tooltip content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Opt-in only — supplying this does not turn animation on automatically.
*
* @example tooltip.overlay({ state, send, parts, content, transition: fade({ duration: 100 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
target?: string | HTMLElement
arrowSelector?: string
/** Dismiss on Escape regardless of where focus is (default: true). */
closeOnEscape?: boolean
}
TooltipParts from @llui/components
export interface TooltipParts {
trigger: {
id: string
'aria-describedby': Signal<string | undefined>
'data-state': Signal<'open' | 'closed'>
'data-scope': 'tooltip'
'data-part': 'trigger'
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onFocus: (e: FocusEvent) => void
onBlur: (e: FocusEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
positioner: {
'data-scope': 'tooltip'
'data-part': 'positioner'
style: string
}
content: {
role: 'tooltip'
id: string
style: string
'data-state': Signal<PresenceStatus>
'data-scope': 'tooltip'
'data-part': 'content'
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onKeyDown: (e: KeyboardEvent) => void
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
arrow: {
'data-scope': 'tooltip'
'data-part': 'arrow'
}
}
TooltipState from @llui/components
Tooltip — hover / focus-triggered, positioned. Opens after a short delay to avoid flicker from passing pointers, closes immediately on blur or after a grace period on pointer leave.
Pure reducer handles only the boolean open state; timing (delays,
debouncing) lives in the event handlers returned from connect(), which
close over per-instance timers.
export interface TooltipState {
/**
* Whether the tooltip is intended to be visible (true while `opening`/`open`,
* false once a close is requested). Flips to false at close-request time —
* exactly as today — so existing consumers reading `open` are unaffected.
* The DOM node is kept mounted through an exit animation via `status`, not
* `open`. Backed by the presence lifecycle in `status`.
*/
open: boolean
/**
* Full presence lifecycle. `closed → opening → open → closing → closed`.
* When `animated` is false (the default) a close skips `closing` and lands
* on `closed` synchronously, matching today's instant unmount.
*/
status: PresenceStatus
/**
* Whether an exit animation is configured. When false, closing is
* synchronous (no `closing` state, no waiting for `animationEnd`).
*/
animated: boolean
}
TourInit from @llui/components
export interface TourInit {
steps?: TourStep[]
open?: boolean
index?: number
}
TourParts from @llui/components
export interface TourParts {
root: {
role: 'dialog'
'aria-modal': 'false'
'aria-labelledby': string
'aria-describedby': string
'data-scope': 'tour'
'data-part': 'root'
hidden: Signal<boolean>
}
backdrop: {
'data-scope': 'tour'
'data-part': 'backdrop'
'aria-hidden': 'true'
onClick: (e: MouseEvent) => void
}
spotlight: {
'data-scope': 'tour'
'data-part': 'spotlight'
'aria-hidden': 'true'
}
title: {
id: string
'data-scope': 'tour'
'data-part': 'title'
}
description: {
id: string
'data-scope': 'tour'
'data-part': 'description'
}
progressText: {
'data-scope': 'tour'
'data-part': 'progress-text'
}
prevTrigger: {
type: 'button'
disabled: Signal<boolean>
'data-scope': 'tour'
'data-part': 'prev-trigger'
onClick: (e: MouseEvent) => void
}
nextTrigger: {
type: 'button'
'data-scope': 'tour'
'data-part': 'next-trigger'
'data-last': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
}
closeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'tour'
'data-part': 'close-trigger'
onClick: (e: MouseEvent) => void
}
}
TourState from @llui/components
export interface TourState {
steps: TourStep[]
open: boolean
index: number
/** Ids of steps already visited. */
visited: string[]
}
TourStep from @llui/components
Tour — guided walkthrough over a sequence of steps, each targeting an element on the page with a pop-up explanation. The state machine tracks the current step index and open/closed; positioning of the pop-up relative to the target selector is done in the view layer (typically via onMount + attachFloating).
There is no overlay() helper: connect() returns the part bags and you
render/position the pop-up yourself (typically onMount + attachFloating
against the current step's target).
view: ({ state, send }) => {
const t = tour.connect(state.at('tour'), send, { id: 'tour' })
const step = tour.currentStep(state.peek().tour)
return [
div({ ...t.root }, [
h3({ ...t.title }, [text(step.title)]),
p({ ...t.description }, [text(step.description)]),
button({ ...t.prevTrigger }, [text('Back')]),
button({ ...t.nextTrigger }, [text('Next')]),
]),
]
}
export interface TourStep {
id: string
title: string
description: string
/** CSS selector or element ref for the tour target. */
target: string
/** Placement hint for the pop-up. */
placement?: 'top' | 'bottom' | 'left' | 'right'
/** Whether to show the highlight ring around the target. */
spotlight?: boolean
}
TreeItemParts from @llui/components
export interface TreeItemParts {
item: {
role: 'treeitem'
id: string
'aria-expanded': Signal<boolean | undefined>
'aria-selected': Signal<boolean | undefined>
'aria-level': number
'aria-busy': Signal<'true' | undefined>
tabindex: Signal<number>
'data-scope': 'tree-view'
'data-part': 'item'
'data-value': string
'data-depth': string
'data-selected': Signal<'' | undefined>
'data-focused': Signal<'' | undefined>
'data-loading': Signal<'' | undefined>
'data-load-failed': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
}
/** For branch items — expand/collapse disclosure trigger. */
branchTrigger: {
'data-scope': 'tree-view'
'data-part': 'branch-trigger'
'data-state': Signal<'open' | 'closed'>
onClick: (e: MouseEvent) => void
}
/**
* Checkbox element (only meaningful when `selectionMode === 'checkbox'`).
* `aria-checked` is the tri-state string ('true' | 'false' | 'mixed').
* The consumer must render a checkbox input or a visual proxy and
* dispatch `toggleChecked` via the `onClick` binding. For branches,
* pass the branch's descendant ids via `descendantIds` on the message
* so children are propagated in a single reducer step.
*/
checkbox: {
role: 'checkbox'
'aria-checked': Signal<'true' | 'false' | 'mixed'>
'data-scope': 'tree-view'
'data-part': 'checkbox'
'data-state': Signal<'checked' | 'unchecked' | 'indeterminate'>
}
}
TreeNode from @llui/components
TreeCollection — helper for building tree-view payloads from a nested data structure. Tree-view's state machine only knows about flat lists (visibleItems, visibleLabels) and opaque ids; the collection owns the structure and derives those flat arrays on demand.
Typical flow:
const col = new TreeCollection(data) state.visibleItems = col.visibleItems(state.expanded) state.visibleLabels = col.visibleLabels(state.expanded)
After every expand / collapse message, the consumer dispatches
setVisibleItems with the updated arrays. The collection itself is
immutable — build a new one when the tree structure changes.
export interface TreeNode {
id: string
label?: string
disabled?: boolean
children?: TreeNode[]
}
TreeNodeInput from @llui/components
Shape of a lazily-loaded child handed back via childrenLoaded.
export interface TreeNodeInput {
id: string
/** Eagerly-known children of this freshly-loaded node, if any. */
children?: string[]
disabled?: boolean
/** Mark this freshly-loaded node as itself lazily-loadable. */
hasChildren?: boolean
}
TreeNodeMeta from @llui/components
JSON-serializable adjacency entry for one tree node. The reducer owns the
tree structure (as a flat record) so it can traverse descendants/ancestors
for automatic indeterminate derivation and lazy-load bookkeeping without
any external collection. Build the record from a {@link TreeCollection} or
by hand; seed it via init({ nodes, roots }) or the setNodes message.
export interface TreeNodeMeta {
/** Ordered ids of this node's loaded children (empty until loaded). */
children: string[]
/** Parent id, or null for a root. */
parentId: string | null
/** When true, descendant cascade and checked-derivation skip this node. */
disabled?: boolean
/**
* Declares the node as a branch whose children are loaded lazily. Expanding
* a `hasChildren` node that has not yet been loaded emits a `loadChildren`
* effect; the consumer fetches and replies with `childrenLoaded`.
*/
hasChildren?: boolean
}
TreeViewInit from @llui/components
export interface TreeViewInit {
expanded?: string[]
selected?: string[]
checked?: string[]
indeterminate?: string[]
selectionMode?: SelectionMode
disabled?: boolean
visibleItems?: string[]
visibleLabels?: string[]
nodes?: Record<string, TreeNodeMeta>
roots?: string[]
loaded?: string[]
loadFailed?: string[]
}
TreeViewParts from @llui/components
export interface TreeViewParts {
root: {
role: 'tree'
'aria-multiselectable': Signal<'true' | undefined>
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'tree-view'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
item: (id: string, depth: number, isBranch: boolean, parentId?: string | null) => TreeItemParts
}
TreeViewState from @llui/components
export interface TreeViewState {
/** Ids of expanded branches. */
expanded: string[]
/** Ids of selected items. */
selected: string[]
/** Ids of checked items (checkbox selection mode). */
checked: string[]
/** Ids known to be in the indeterminate tri-state (some-but-not-all
* descendants checked). Consumer-computed via propagation logic or the
* `toggleChecked` message's `descendantIds` parameter. */
indeterminate: string[]
/** Currently focused item id. */
focused: string | null
selectionMode: SelectionMode
/** Ordered list of currently-visible item ids (updated by consumer via setVisible). */
visibleItems: string[]
/** Parallel array of visible-item labels for typeahead. If empty, typeahead
* matches against ids directly. Updated alongside visibleItems via the
* optional `labels` field on `setVisibleItems`. */
visibleLabels: string[]
disabled: boolean
/** Typeahead accumulator buffer. */
typeahead: string
typeaheadExpiresAt: number
/** Id of item currently being renamed, or null. */
renaming: string | null
/** Draft value during rename. */
renameDraft: string
/**
* Ids of branches currently loading their children asynchronously. Item
* parts expose `aria-busy` while loading so assistive tech announces the
* in-progress state. This is now driven by the machine itself: expanding a
* `hasChildren` node sets `loading` and emits a `loadChildren` effect; the
* `childrenLoaded` / `childrenLoadFailed` replies clear it. The legacy
* `loadingStart` / `loadingEnd` messages remain for manual control.
*/
loading: string[]
/**
* Flat tree structure (adjacency record). Owned by the reducer for
* descendant/ancestor traversal. JSON-serializable.
*/
nodes: Record<string, TreeNodeMeta>
/** Ids of the top-level (root) nodes, in order. */
roots: string[]
/**
* Ids of branches whose children have been loaded (distinguishes a
* loaded-but-empty branch from a not-yet-fetched one so we never refetch).
*/
loaded: string[]
/**
* Ids of branches whose last lazy load failed. Re-expanding such a branch
* retries the load (clears the flag and re-emits `loadChildren`).
*/
loadFailed: string[]
}
ValidateResult from @llui/components
export interface ValidateResult<T> {
isValid: boolean
/** Field name → first error message. Field name is derived from the issue's path. */
errors: Partial<Record<keyof T, string>>
/** All issues from the schema validator, unaltered. */
issues: readonly StandardSchemaV1.Issue[]
}
Classes
TreeCollection from @llui/components
class TreeCollection {
roots: TreeNode[]
info: Map<string, NodeInfo>
constructor(roots: TreeNode | TreeNode[])
index(): void
getNode(id: string): TreeNode | null
getLabel(id: string): string
getParent(id: string): string | null
getDepth(id: string): number
getChildren(id: string): string[]
getDescendants(id: string): string[]
isBranch(id: string): boolean
isDisabled(id: string): boolean
visibleItems(expanded: string[]): string[]
visibleLabels(expanded: string[]): string[]
computeIndeterminate(checked: Set<string>): string[]
}
Constants
accordion from @llui/components
const accordion
alertDialog from @llui/components
const alertDialog
ALL_NESTED_LAYER_ASPECTS from @llui/components
Every aspect — the default for a registration that names none.
const ALL_NESTED_LAYER_ASPECTS: readonly NestedLayerAspect[]
angleSlider from @llui/components
const angleSlider
asyncList from @llui/components
const asyncList
avatar from @llui/components
const avatar
breadcrumbs from @llui/components
const breadcrumbs
carousel from @llui/components
const carousel
cascadeSelect from @llui/components
const cascadeSelect
checkbox from @llui/components
const checkbox
clipboard from @llui/components
const clipboard
collapsible from @llui/components
const collapsible
colorPicker from @llui/components
const colorPicker
combobox from @llui/components
const combobox
contextMenu from @llui/components
const contextMenu
dateInput from @llui/components
const dateInput
datePicker from @llui/components
const datePicker
dialog from @llui/components
const dialog
drawer from @llui/components
const drawer
editable from @llui/components
const editable
en from @llui/components
English locale — used as the default when no provider is in the tree.
const en: Locale
field from @llui/components
const field
fieldset from @llui/components
const fieldset
fileUpload from @llui/components
const fileUpload
floatingPanel from @llui/components
const floatingPanel
form from @llui/components
const form
hoverCard from @llui/components
const hoverCard
imageCropper from @llui/components
const imageCropper
inView from @llui/components
const inView
listbox from @llui/components
const listbox
LocaleContext from @llui/components
Locale context. Components resolve their own English fallback so component
subpath bundles only carry that component's strings. This public context and
the lightweight component context share an id, preserving provide()
behavior while retaining the complete English locale as the public default.
const LocaleContext: Context<Locale>
marquee from @llui/components
const marquee
menu from @llui/components
const menu
menubar from @llui/components
const menubar
menubarMachine from @llui/components
const menubarMachine
meter from @llui/components
const meter
navigationMenu from @llui/components
const navigationMenu
numberInput from @llui/components
const numberInput
pagination from @llui/components
const pagination
passwordInput from @llui/components
const passwordInput
pinInput from @llui/components
const pinInput
popover from @llui/components
const popover
presence from @llui/components
const presence
progress from @llui/components
const progress
qrCode from @llui/components
const qrCode
radioGroup from @llui/components
const radioGroup
ratingGroup from @llui/components
const ratingGroup
scrollArea from @llui/components
const scrollArea
searchField from @llui/components
const searchField
select from @llui/components
const select
signaturePad from @llui/components
const signaturePad
slider from @llui/components
const slider
sortable from @llui/components
const sortable
splitter from @llui/components
const splitter
steps from @llui/components
const steps
switchMachine from @llui/components
const switchMachine
table from @llui/components
The namespace object. {@link HEADER_ROW_INDEX} is a member again now that
issue #151 is fixed — site/src/generate-api.ts classifies namespace members
by kind instead of rendering every one with a hard-coded (). It remains a
module-level export too, re-exported from the barrel as
TABLE_HEADER_ROW_INDEX.
const table
TABLE_HEADER_ROW_INDEX from @llui/components
The row index of the HEADER row. The header is part of the grid's roving
sequence — APG's data-grid examples make column headers focusable exactly
because they carry controls (sort here, plus the select-all checkbox) — so it
needs a coordinate. -1 is the natural one: aria-rowindex already models
the header as row 1 with data row i at i + 2, so the header sits one row
above data row 0.
const TABLE_HEADER_ROW_INDEX
tabs from @llui/components
const tabs
tagsInput from @llui/components
const tagsInput
themeSwitch from @llui/components
const themeSwitch
timePicker from @llui/components
const timePicker
timer from @llui/components
const timer
toast from @llui/components
const toast
toc from @llui/components
const toc
toggle from @llui/components
const toggle
toggleGroup from @llui/components
const toggleGroup
toolbar from @llui/components
const toolbar
tooltip from @llui/components
const tooltip
tour from @llui/components
const tour
treeView from @llui/components
const treeView
TYPEAHEAD_TIMEOUT_MS from @llui/components
Typeahead search — accumulates keystrokes into a query while the user types rapidly, then matches the first item whose label starts with the query. Used by listbox, menu, select, combobox, tree-view to support WAI-ARIA keyboard navigation patterns.
Behavior:
- If a keystroke arrives within
TYPEAHEAD_TIMEOUT_MSof the previous one, append to the existing query (so typing "sa" finds "Saturn" even if the highlight is currently on "Jupiter"). - Otherwise, start a fresh single-character query.
- Single-character queries advance past the current position (jump to the next item starting with that letter), which is the standard WAI-ARIA behavior — rapid repeated presses of "s" cycle through items beginning with "s".
- Multi-character queries search from the current cursor position (inclusive) so if the cursor is already on a matching item, it stays — typing "ap" while on "apricot" keeps focus on "apricot".
const TYPEAHEAD_TIMEOUT_MS
@llui/components/utils
Functions
allFiniteNumbers() from @llui/components/utils
Whether every number nested in one atomic runtime payload is usable. Non-numeric leaves are ignored; arrays and plain payload objects are walked so callers cannot accidentally validate one coordinate while committing a bad sibling. Cycles are harmless because an already-seen object contains no new numeric leaves.
function allFiniteNumbers(...values: readonly unknown[]): boolean
anatomy() from @llui/components/utils
function anatomy<P extends string>(name: string, parts: readonly P[]): Anatomy<P>
applySelection() from @llui/components/utils
Apply a click/Enter on value to the current selection. Single mode
replaces, multiple toggles, and a disabled item changes nothing — returning
the SAME array reference so the reducer's no-op stays a no-op for the
reference-equality reconciler.
function applySelection(current: string[], value: string, opts: { mode: SelectionMode; disabled?: readonly string[] }): string[]
attachFloating() from @llui/components/utils
Position floating relative to anchor with live updates on scroll/resize.
Applies left + top styles to the floating element. Returns a cleanup.
export declare function attachFloating(opts: FloatingOptions): () => void;
clamp() from @llui/components/utils
Bound n into [min, max]. The result is always FINITE: a non-finite input
maps to a defined legal value instead of being stored verbatim.
Every comparison against NaN is false, so NaN used to fall straight
through to return n and land in state — package-wide, since this is the one
clamp every mutation path routes through (#152). It is not merely a wrong
number: JSON.stringify(NaN) (and Infinity) is null, so a non-finite
value breaks the State-is-JSON-serializable invariant and with it devtools
time-travel, @llui/test replay, agent state snapshots and SSR rehydration.
Rejecting at this boundary is what lets every caller state its own
postcondition — e.g. slider's withThumb — without a finiteness caveat.
function clamp(n: number, min: number, max: number): number
clampToStep() from @llui/components/utils
Clamp into the range AND snap onto the grid. The result is always within
[min, max]: snapping can leave the range when an endpoint is not itself on
the grid (min 0, max 10, step 4 → 10 snaps up to 12), and the answer there is
the last grid value INSIDE the range, not an out-of-range or off-grid one.
It is always FINITE too — the clamp rejects a non-finite input first (#152).
function clampToStep(value: number, grid: NumericGrid): number
decimalPlaces() from @llui/components/utils
Fraction digits n is written with, INCLUDING exponential notation —
String(1e-7) is '1e-7', which a scan for '.' reads as zero decimals.
function decimalPlaces(n: number): number
deriveOnce() from @llui/components/utils
Wrap a ONE-ARGUMENT compute so that repeating a call with the same argument
returns the previous result. One cell — the first item of an update pays for
the derivation and the rest read it, so the cost is per UPDATE, not per item
and not per render.
This is the shape that runs per ROW per BINDING per update, so it takes a
FIXED parameter and compares with one Object.is. The variadic deriveOnceN
below materialises a fresh arguments array on every call — one per row per
binding per update — which is pure overhead on the hit path: over a pass of
N items x 4 bindings, 0.00056 -> 0.00041 ms at N=20, 0.00467 -> 0.00337 at
N=200 and 0.05680 -> 0.03823 at N=2000 (~25-33%). Reach for deriveOnceN
only where the derivation genuinely takes several inputs.
NO RUNTIME ARITY GUARD, deliberately. Calling the returned function with a
second argument silently ignores it — g(1,'x') and g(1,'y') both return
the g(1) result from one computation — so a JS consumer, or a TS consumer
who casts, can get a wrong answer with no error. That is accepted because
the only ways to observe arity at runtime are an arguments object (absent
in an arrow) or a rest parameter, and a rest parameter re-materialises the
per-row-per-binding array whose removal is this function's entire reason to
exist — paying the 25-33% back on the hottest path in the package, on every
hit, to catch a call TypeScript already rejects (TS2554). Do NOT "fix" this
by widening the signature. If a derivation needs more than one input, that
is what deriveOnceN is for.
function deriveOnce<A, R>(compute: (arg: A) => R): (arg: A) => R
deriveOnceN() from @llui/components/utils
deriveOnce for a derivation with several inputs (the roving tab stop reads
three or four). Memoized on ARGUMENT IDENTITY, position by position.
function deriveOnceN<A extends readonly unknown[], R>(compute: (...args: A) => R): (...args: A) => R
engineFocus() from @llui/components/utils
Focus el as an engine-initiated move (see runEngineFocus).
export declare function engineFocus(el: HTMLElement, options?: FocusOptions): void;
finiteBound() from @llui/components/utils
A bound as STATE may hold it: the finite number itself, or undefined for
"no bound on this side". THE ONE normalizer for a bound, mirroring clamp's
role for a value (#177).
±Infinity and an ABSENT bound already mean the same thing to every clamp in
the package — clampToStep expands grid.min ?? -Infinity — but only one of
the two spellings survives JSON.stringify, which writes null for both
Infinity and NaN. State must be JSON-serializable (devtools time-travel,
@llui/test replay, agent state snapshots, SSR rehydration all compare
serialized state), so the infinite spelling belongs to the RUNTIME expansion
and never to state: normalize at every write, let the grid expand the absence
again. An unbounded number-input used to store min: -Infinity and
rehydrate as min: null — a number field holding null, on the DEFAULT
configuration.
NaN collapses here too, and that is the half a ?? cannot rescue: NaN is
not nullish, so a NaN bound reached clamp, every comparison against it
was false, and THAT SIDE OF THE RANGE STOPPED CLAMPING — angle-slider after
setMin: NaN stored -9999 for setValue(-9999).
Callers decide what an absent bound means for them, and there are exactly two idioms:
- UNBOUNDED-CAPABLE (
number-input): store theundefinedby OMITTING the key, so the state shape IS aNumericGridand round-trips identically. - INTRINSICALLY BOUNDED (
angle-slider,slider,splitter, …): a requiredmin: numbercannot spell "unbounded", so?? DEFAULTatinitand REJECT the write in asetMin/setMaxreducer — dropping a meaningless bound keeps the range the component already had, which is the only answer that cannot silently disable clamping.
function finiteBound(raw: number | null | undefined): number | undefined
finiteOrDefault() from @llui/components/utils
A component-owned number that has no range to clamp into. Initialization
replaces an unusable input with the field's ordinary default; runtime
reducers use {@link allFiniteNumbers} to refuse the whole message instead.
Keeping those two policies here prevents a free position or timestamp from
accidentally inheriting either the grid-value policy (clamp) or the
optional-bound policy (finiteBound).
function finiteOrDefault(raw: number | null | undefined, fallback: number): number
firstEnabled() from @llui/components/utils
Internal value navigation used by the public roving-focus primitive.
export declare function firstEnabled(items: readonly string[], disabled: readonly string[]): string | null;
firstEnabledIndex() from @llui/components/utils
function firstEnabledIndex(items: readonly string[], disabled: readonly string[]): number | null
flipArrow() from @llui/components/utils
Map a horizontal arrow key to its logical direction, accounting for RTL. This is the SINGLE SOURCE OF TRUTH every component routes horizontal arrow interpretation through. Under rtl, ArrowLeft and ArrowRight swap meaning; vertical arrows (Up/Down), Home/End, PageUp/PageDown and every non-arrow key pass through unchanged.
The second argument is the direction source:
- an explicit
'ltr' | 'rtl'— used directly (the authoritative form when a component storesdirin its own State and passes it in); - an
Element— direction is resolved by walking up the DOM (dir="rtl"ancestor ordocument.documentElement.dir); null— treated as'ltr'(no-op).
export declare function flipArrow(key: string, source: Element | null | TextDirection): string;
focusLingeredInside() from @llui/components/utils
Whether focus LINGERED INSIDE the layer, i.e. whether restoring it to the anchor respects the user rather than overriding them.
function focusLingeredInside(query: FocusRestoreQuery): boolean
focusRovingItem() from @llui/components/utils
Move DOM focus to the roving item identified by value within the same
widget instance as origin.
Roving-tabindex widgets track the active index in STATE, but assistive tech
follows real DOM focus — so after a keyboard move the handler MUST also move
focus, or arrow keys are silent for AT. origin is the event's
currentTarget (the item that received the key); its closest
[data-scope][data-part="root"] ancestor scopes the search so sibling
widgets of the same scope never cross-focus. No-op if nothing matches.
send() is synchronous and items already exist in the DOM, so this can be
called immediately after the navigation send.
export declare function focusRovingItem(origin: Element | null, scope: string, value: string, opts?: {
itemPart?: string;
attr?: string;
}): void;
focusRovingTab() from @llui/components/utils
Move DOM focus to the trigger whose data-value matches, within
container. Relies only on the role="tab" + data-value contract
(shared by components/tabs and any hand-rolled tablist). No-op when no
trigger matches. Call after the DOM reflects the new active tab (e.g. in
a microtask if activation triggers a re-render).
export declare function focusRovingTab(container: Element, value: string): void;
getFocusables() from @llui/components/utils
export declare function getFocusables(container: Element): HTMLElement[];
getNestedLayers() from @llui/components/utils
Currently-registered nested-layer elements (resolvers re-read live).
With an aspect, only registrations that participate in it; without one, all
of them. With a within boundary, only registrations nested inside it (see
the module comment); without one, the flat, layer-agnostic answer.
export declare function getNestedLayers(aspect?: NestedLayerAspect, within?: NestedLayerScope): Element[];
indexMap() from @llui/components/utils
A position lookup over a state array: positions(s.items).get(item) in place
of s.items.indexOf(item). First occurrence wins, matching indexOf.
function indexMap<T>(): (values: readonly T[] | null | undefined) => ReadonlyMap<T, number>
isDateOnly() from @llui/components/utils
function isDateOnly(value: DateValue): value is string
isEnabledItem() from @llui/components/utils
An item counts as navigable only while it is in the list AND not disabled.
function isEnabledItem(items: readonly string[], disabled: readonly string[], value: string): boolean
isEngineFocusInProgress() from @llui/components/utils
Whether an engine-initiated focus move is in flight. Consulted by
watchInteractOutside to gate its focusin path.
export declare function isEngineFocusInProgress(): boolean;
isFocusable() from @llui/components/utils
Find focusable descendants within a container.
export declare function isFocusable(el: Element): boolean;
isInNestedLayer() from @llui/components/utils
Whether target is inside (or equal to) a registered nested layer that
participates in aspect (any layer when aspect is omitted) and is nested
inside within (any layer when within is omitted).
export declare function isInNestedLayer(target: Node | null, aspect?: NestedLayerAspect, within?: NestedLayerScope): boolean;
isTypeaheadKey() from @llui/components/utils
Returns true if the key event should trigger a typeahead query — i.e., a
single printable character that isn't a modified keyboard shortcut. Use
this in onKeyDown handlers to decide whether to dispatch a typeahead
message.
function isTypeaheadKey(e: KeyboardEvent): boolean
lastEnabled() from @llui/components/utils
export declare function lastEnabled(items: readonly string[], disabled: readonly string[]): string | null;
lastEnabledIndex() from @llui/components/utils
function lastEnabledIndex(items: readonly string[], disabled: readonly string[]): number | null
lockBodyScroll() from @llui/components/utils
Lock body scroll while an overlay is open, preserving scrollbar width to avoid layout shift. Reference-counted so nested locks compose cleanly.
export declare function lockBodyScroll(): () => void;
membershipSet() from @llui/components/utils
A membership lookup over a state array: set(s.value).has(item) in place of
s.value.includes(item). An absent collection reads as empty.
An EMPTY array shares EMPTY_SET rather than allocating: most of the ~16
call sites are a disabled/disabledItems list that is empty in the common
case, and an empty Set is the one input where the memo would otherwise pay
an allocation to answer false to everything.
function membershipSet<T>(): (values: readonly T[] | null | undefined) => ReadonlySet<T>
nextEnabled() from @llui/components/utils
export declare function nextEnabled(items: readonly string[], disabled: readonly string[], from: string, delta: 1 | -1, loop: boolean): string | null;
nextEnabledIndex() from @llui/components/utils
The index of the next enabled item delta steps from from, wrapping.
from === null starts before the first item (delta 1) or after the last
(delta -1), so the first/last enabled index comes back.
function nextEnabledIndex(items: readonly string[], disabled: readonly string[], from: number | null, delta: 1 | -1): number | null
parseDateValue() from @llui/components/utils
function parseDateValue(value: DateValue): ParsedDateValue
positiveFinite() from @llui/components/utils
A finite number strictly greater than zero, or undefined when unusable.
function positiveFinite(raw: number | null | undefined): number | undefined
positiveFiniteOrDefault() from @llui/components/utils
A positive finite number, or the field's ordinary initialization default.
function positiveFiniteOrDefault(raw: number | null | undefined, fallback: number): number
presenceEndHandler() from @llui/components/utils
Guard a presence "animation/transition ended" handler so it only advances the
presence machine when the event fired on the element the listener is bound to
(e.target === e.currentTarget) — never on a bubbling descendant.
Overlay content (dialog, popover, menu, toast) reflects its exit phase via
data-state="closing" and stays mounted until an animationend/transitionend
dispatches animationEnd/transitionEnd. Without this guard, ANY descendant
animation or transition ending during the exit — a spinner, a ripple, a child
fade — bubbles up and prematurely unmounts the overlay before its own exit
animation completes.
Mirrors the e.target === el guard the transitions runtime applies in
waitForEnd (@llui/transitions).
function presenceEndHandler<E extends AnimationEvent | TransitionEvent>(handler: (e: E) => void): (e: E) => void
pruneToEnabled() from @llui/components/utils
Keep value only while it still names an enabled item, else null. Every
reducer that replaces the item list owes this to whatever it holds as
focused/selected — a dangling reference is the tab-stop bug.
function pruneToEnabled(items: readonly string[], disabled: readonly string[], value: string | null): string | null
pushDismissable() from @llui/components/utils
Register a dismissable layer. Escape is offered to the layers top-down until
one CLAIMS it (a layer declines via disableEscape or an onEscape router
returning false, and the key then falls through to the layer beneath);
outside-click is topmost-only. Returns a cleanup that removes the layer from
the stack.
Push a layer even when both dismissal routes are disabled: the layer is the caller's PLACE ON THE STACK, which is what stops the layer beneath from treating an interaction inside this one as an outside interaction.
export declare function pushDismissable(opts: DismissableOptions): () => void;
pushFocusTrap() from @llui/components/utils
Push a focus trap onto the stack. Tab/Shift+Tab will cycle within the container's focusable descendants. Returns a cleanup that removes the trap and (optionally) restores focus to the element active before push.
export declare function pushFocusTrap(opts: FocusTrapOptions): () => void;
registerNestedLayer() from @llui/components/utils
Register source (an element, array of elements, or a resolver returning
either) as a nested layer. Returns a cleanup that removes the registration.
Prefer the resolver form for a portaled overlay: register once on mount and
return the live root only while open ([] when closed), so a single
registration tracks the overlay's open/closed lifecycle without churn.
Pass opts.owner when scoped consumers must exempt the layer. Missing or
unresolved ownership fails closed and warns in development.
export declare function registerNestedLayer(source: ElementSource, opts?: NestedLayerOptions): () => void;
resetAnatomyIdCounter() from @llui/components/utils
Reset the internal id counter — tests only.
function resetAnatomyIdCounter(): void
resolveDir() from @llui/components/utils
Resolve the text direction for an element by walking up the DOM tree. Returns 'rtl' or 'ltr' (default).
export declare function resolveDir(el: Element): TextDirection;
resolveRovingMove() from @llui/components/utils
Map a keyboard key + the current tab value to a roving-tablist move,
or null when the key isn't a navigation/activation key or the move is
a no-op (empty list, no enabled sibling). Pure — does not touch the DOM
or call preventDefault; the caller decides (typically: prevent default
iff the result is non-null).
export declare function resolveRovingMove(key: string, current: string, items: readonly RovingItem[], opts?: RovingOptions): RovingMove | null;
resolveTextDirection() from @llui/components/utils
Normalize any accepted direction source to a concrete TextDirection.
An explicit 'ltr' | 'rtl' wins; an Element is resolved from the DOM;
null / undefined default to 'ltr'.
export declare function resolveTextDirection(source: Element | null | undefined | TextDirection): TextDirection;
rovingTabStop() from @llui/components/utils
The single item that carries tabindex="0".
WAI-ARIA's roving-tabindex pattern requires EXACTLY ONE tab stop in a
composite widget: with none, Tab skips the widget entirely and it becomes
keyboard-unreachable. So a preferred candidate (the focused item, the
checked radio, …) is honoured only while it is still an enabled member, and
the first enabled item answers otherwise. Null only when nothing is enabled.
Every roving-tabindex widget in the package routes through here (#145 closed
the last three: menubar, navigation-menu and tags-input). Keep it that way —
an inline focused === x ? 0 : -1 has no fallback, and since nothing prunes
focused against the current list, removing or disabling the focused item
leaves EVERY item at -1 and the widget disappears from the Tab order.
tags-input is index-keyed and passes String(i) as the item identity (its
data-index, and the only identity that survives duplicate tag values);
navigation-menu passes either the membership list its consumer maintains or
the ids handed to its own item(), filtered first to the ones not sealed
inside a closed submenu — membership alone would seat the stop on an element
inside a hidden panel, which is present, unique and untabbable.
Null when nothing is enabled is deliberate and is a caller's problem to
notice: a widget whose items are ALL disabled ends up with no tab stop at
all. That is right for radio-group/toggle-group/toolbar/tree-view,
whose items are genuinely disabled and therefore unfocusable anyway, and it
is a 1 -> 0 change for menubar, whose triggers carry only aria-disabled
and stay focusable. Any revision belongs here, applying to every caller at
once — not in one component.
function rovingTabStop(items: readonly string[], disabled: readonly string[], ...preferred: readonly (string | null | undefined)[]): string | null
runEngineFocus() from @llui/components/utils
Run body with engine-focus suppression active. Any focusin raised inside
it is invisible to watchInteractOutside — including one raised by a focus
move that re-entrant consumer code makes from a focusin listener (see the
module comment: the window is the synchronous transitive closure, not just
the .focus() call).
SYNCHRONOUS BY CONTRACT, AND THE CONTRACT IS ENFORCED (#172). The suppression
is released when body RETURNS. An async body returns its promise at the
first await, so the depth counter drops immediately and the focus move that
eventually happens gets NO protection at all — a call that looks correct,
compiles, and does nothing. The failure is safe (no protection, never a stuck
guard: the decrement is in a finally), which is exactly why it is invisible,
and this is a PUBLIC export documented as the thing a custom overlay "must"
route its engine-initiated focus moves through. A consumer following that
advice with an async body would reintroduce #155 in their app while believing
they had prevented it.
Two guards, because neither covers the other's case:
- The SIGNATURE rejects a promise-returning body at compile time. It is the real guard — it fires before the code ever runs. Its one blind spot is a body whose return type is an unresolved type parameter (a generic pass-through wrapper): the conditional is deferred, so such a wrapper is rejected too and must carry its own constraint. No caller does.
- A DEV-MODE warning catches what the type system cannot see: a JavaScript
consumer, an
any-typed body, or a body that returns a thenable without being declared as returning one. It cannot restore the protection — by the time a thenable is in hand the guard is already released — so it only reports.
Deliberately NOT offered: an async-aware variant that holds the guard across
an await. The guard is safe because no user event can be delivered inside
its window (see the module comment); holding it across a suspension point
hands the event loop back and would start swallowing genuine interactions.
export declare function runEngineFocus<T>(body: () => SyncEngineFocusBody<T>): T;
setAriaHiddenOutside() from @llui/components/utils
Hide sibling subtrees from assistive tech while an overlay is open.
Walks from target up to the document root, applying aria-hidden="true"
and inert to every sibling at each level. Previous attribute values are
recorded and restored on cleanup.
Two kinds of element are EXEMPT: registered nested layers (see
registerNestedLayer) and live regions — aria-live, role="alert",
role="status", role="log". A live region under aria-hidden is simply
never read out, so a modal that sweeps one silences the app's announcement
channel for exactly as long as it is open (#123). Live regions are matched by
selector rather than registration because they are plain part bags with no
mount hook of their own, and because that also covers consumer-authored ones.
An exempt element does NOT spare its whole ancestor subtree: the sweep
descends through any element that merely CONTAINS one and hides everything
hanging off the path down to it. (Skipping the ancestor wholesale would leave
the entire app interactive behind the modal the moment a form somewhere held
an aria-live error message.) inert and aria-hidden both inherit, so
leaving the path clear is the only way an exempt element stays reachable.
Nested calls are supported — each layer only touches elements that haven't been claimed by a higher layer (tracked via a WeakMap reference count).
TWO KNOWN LIMITS of the live-region exemption, both deliberate:
inertcannot be split fromaria-hidden— sparing a region spares its WHOLE SUBTREE from both, so an interactive live region (arole="log"transcript containing links) stays Tab-reachable behind a modal. Keep live regions to announcement text; put controls outside them, or register the modal's own layer for the interactive part.document.querySelectorAlldoes not pierce shadow roots, so a live region inside one is not exempt. The sweep itself only ever walks light-DOM ancestors oftarget, so this only bites when the region and the modal live in different trees.
export declare function setAriaHiddenOutside(target: Element): () => void;
snapToStep() from @llui/components/utils
Nearest multiple of step from origin. A non-positive step is a no-op.
A non-finite value names no position on the grid, so it snaps to the grid's
own anchor — the same policy clamp applies to the range (#152). This is
unreachable from clampToStep, which clamps first; it keeps the util's
direct consumers on the same rule.
function snapToStep(value: number, step: number, origin = 0): number
stepBy() from @llui/components/utils
Move count whole steps (negative to step down), then clamp+snap.
From an OFF-GRID value one call moves to the nearest grid value in the
direction of travel and stops there — that jump is the whole change, however
large count is. This is HTML's stepUp/stepDown (step 3 of the
value-stepping algorithm) and it is what makes increment land on the grid
instead of dragging an off-grid value along forever.
ONE DELIBERATE DIVERGENCE from the spec: HTML's step base falls back min ->
the value CONTENT ATTRIBUTE -> 0; gridOrigin goes min -> 0. A headless
machine has no content attributes — the seed value is just the initial state,
and anchoring the grid on it would make two components with the same
min/max/step disagree about which values are legal depending on where they
happened to start.
function stepBy(value: number, count: number, grid: NumericGrid): number
typeaheadAccumulate() from @llui/components/utils
Advance the typeahead query based on a new keystroke and the previous
expiration time. Returns the new query string; callers combine this with
typeaheadMatch() to produce a new highlight index.
function typeaheadAccumulate(prev: string, char: string, now: number, expiresAt: number): string
typeaheadMatch() from @llui/components/utils
Find the first enabled item whose label starts with the query
(case-insensitive). labels and disabledMask are parallel arrays.
startFrom is the current highlighted index; for single-character
queries the search begins at startFrom + 1 (so repeated "s" keys
cycle), for multi-character queries it begins at startFrom (inclusive).
Returns the matching index, or null if no enabled item matches.
function typeaheadMatch(labels: string[], disabledMask: boolean[], query: string, startFrom: number | null): number | null
typeaheadMatchByItems() from @llui/components/utils
Convenience: pass a disabled list of values instead of a boolean mask.
Builds the mask by checking membership via === on the raw string values.
function typeaheadMatchByItems(items: string[], disabled: readonly string[], query: string, startFrom: number | null): number | null
watchInteractOutside() from @llui/components/utils
Watch for pointer or focus events outside a given element. Returns a
cleanup function. Uses the capture phase so upstream stopPropagation
calls cannot hide events.
- pointerdown (or mousedown/touchstart fallback) triggers "outside" if the
target is not contained by
elementorignore. - focusin triggers "outside" when focus moves outside the element, except
when the new target is in
ignore.
export declare function watchInteractOutside(opts: InteractOutsideOptions): () => void;
Types
DateValue from @llui/components/utils
Date-only handling — the ONE place that decides whether a value denotes an INSTANT or a bare CALENDAR DATE.
new Date('2026-01-15') parses a date-only string as UTC midnight (ES spec),
so formatting it against the ambient zone renders the PREVIOUS day everywhere
west of UTC: formatDate('2026-01-15') printed "January 14, 2026" under
America/New_York and the right answer under Europe/Rome, which is why it
survived (#125 defect 4).
A calendar date carries no instant and therefore no zone, so it is anchored
at UTC midnight and its consumers must render it in UTC — see dateOnly.
export type DateValue = Date | string | number
DismissSource from @llui/components/utils
Reason a dismissable layer was closed.
export type DismissSource = 'escape' | 'outside';
ElementSource from @llui/components/utils
Shared DOM helpers used by interaction utilities.
export type ElementSource<T extends Element = Element> = T | T[] | (() => T | T[] | null)
NestedLayerAspect from @llui/components/utils
A consumer of the registry. A registration participates only in the aspects it
names, because a single answer is wrong for at least one consumer: engine
overlays leave outside to the ordered dismissable stack (see the module
comment).
The dialog-with-an-inner-select case is NOT what the aspect list protects.
That one is covered by a modal never registering AT ALL, whatever aspects it
would have named.
outside— {@link watchInteractOutside} does not treat interactions inside the layer as outside interactions.focus— {@link pushFocusTrap} includes the layer as an extra focusable container, so Tab/Shift+Tab can reach it.hide— {@link setAriaHiddenOutside} hides AROUND the layer rather than hiding it.
export type NestedLayerAspect = 'outside' | 'focus' | 'hide';
NestedLayerScope from @llui/components/utils
The asking layer's own boundary — what "nested inside ME" is measured against. Omit it for the flat, layer-agnostic answer.
export type NestedLayerScope = ElementSource;
Placement from @llui/components/utils
export declare type Placement = Prettify<Side | AlignedPlacement>;
RovingMove from @llui/components/utils
The navigation a key implies on a roving tablist.
export type RovingMove =
/** An arrow / Home / End resolved to a (different, enabled) tab value. */
{
type: 'focus';
value: string;
}
/** Enter or Space — activate the currently focused tab (manual mode). */
| {
type: 'activate';
};
RovingOrientation from @llui/components/utils
Headless roving-tablist navigation — the keyboard logic of a WAI-ARIA tablist, decoupled from any particular DOM contract.
components/tabs.ts builds its reactive part-bags on top of this; a
consumer that wants its OWN markup (different classes, ids, no
data-scope/data-part) can drive the same keyboard behaviour by
calling resolveRovingMove from its trigger's onKeyDown and
focusRovingTab to move DOM focus — without adopting the component's
markup or its connect() state machine.
The resolver is pure (key + current value + items → a move); the only
shared DOM assumption lives in focusRovingTab, and it is the minimal
one both surfaces already satisfy: triggers carry role="tab" and
data-value="<value>".
The list walk itself lives in list-navigation.ts — this module is the
keyboard + DOM-focus surface over it, nothing more.
export type RovingOrientation = 'horizontal' | 'vertical';
SyncEngineFocusBodyRequired from @llui/components/utils
The type an ASYNC body collapses to in {@link runEngineFocus}'s parameter
position. Nothing is assignable to it, so runEngineFocus(async () => …) is a
compile error naming the contract rather than a silently inert call.
export type SyncEngineFocusBodyRequired = {
readonly [SYNC_BODY_REQUIRED]: 'runEngineFocus requires a SYNCHRONOUS body — the guard is released the moment body returns';
};
TextDirection from @llui/components/utils
Text reading direction. The single shared RTL vocabulary for the package.
export type TextDirection = 'ltr' | 'rtl';
Interfaces
Anatomy from @llui/components/utils
export interface Anatomy<P extends string> {
readonly name: string
readonly parts: readonly P[]
/** Create a new scope instance. Pass an explicit id to force a value (SSR). */
scope(id?: string): AnatomyScope<P>
}
AnatomyScope from @llui/components/utils
export interface AnatomyScope<P extends string> {
/** Instance id — unique across all anatomy scopes. */
readonly id: string
/** Resolve the id for a specific part (for ARIA wiring). */
idFor(part: P): string
/** Build the common data-attrs + id for a part. */
attrs(part: P): { id: string; 'data-scope': string; 'data-part': P }
}
DismissableOptions from @llui/components/utils
export interface DismissableOptions {
/** The layer element (e.g. a dialog content or popover). */
element: ElementSource;
/** Trigger / anchor elements that should not count as outside interactions. */
ignore?: ElementSource;
/** Called when the user dismisses the layer. */
onDismiss: (source: DismissSource, event: Event) => void;
/**
* Custom Escape router. When provided it runs for the Escape key INSTEAD of
* `onDismiss('escape', …)`, letting the layer unwind an internal level first
* (e.g. a menu closes its open submenu before closing the whole menu). Return
* `false` to decline — the event is not claimed and propagates as if this
* layer had `disableEscape`. Any other return (incl. `undefined`) claims it.
*/
onEscape?: (event: KeyboardEvent) => boolean | void;
/** Disable outside-click dismissal (default: false). */
disableOutside?: boolean;
/** Disable Escape-key dismissal (default: false). */
disableEscape?: boolean;
}
FloatingOptions from @llui/components/utils
export interface FloatingOptions {
/** The reference element (trigger/anchor). */
anchor: Element;
/** The floating element (content). */
floating: HTMLElement;
/** Preferred placement (default: 'bottom'). */
placement?: Placement;
/** Gap between anchor and floating, in px (default: 0). */
offset?: number;
/** Flip to opposite side when there isn't enough room (default: true). */
flip?: boolean;
/** Shift along axis to stay in view (default: padding 8 unless false). */
shift?: boolean | {
padding?: number;
};
/**
* Reading direction. Under `'rtl'`, logical `*-start`/`*-end` placements
* track the inline-start/inline-end edges. When given it is AUTHORITATIVE —
* it overrides the direction the floating element happens to compute to,
* which for a portaled overlay is the direction of wherever it landed.
* Omit it to leave that decision to the page, as floating-ui does by default.
*/
dir?: TextDirection;
/** Optional arrow element to position. */
arrow?: HTMLElement;
/** Notify after each position computation. */
onUpdate?: (data: {
x: number;
y: number;
placement: Placement;
arrow?: {
x?: number;
y?: number;
};
}) => void;
}
FocusRestoreQuery from @llui/components/utils
The one rule for "should this layer pull focus back to its anchor?".
Restoring focus to the trigger is right when the layer was closed with focus still resting inside it (Escape, a close button, a programmatic close) — the user's focus would otherwise be left on a detached node. It is WRONG when the dismissal was caused by focus moving somewhere the user chose: yanking it back to the trigger takes focus away from the control they just reached, and can leave a still-open layer with focus outside it (#173).
document.body (and a null activeElement) counts as "inside" because that
is where focus lands when the focused element is removed — nobody chose it, so
the anchor is a better home than the body.
Both callers must ask BEFORE tearing anything down: the focus trap's release
and the aria-hidden/inert sweep both move or invalidate activeElement,
so a decision taken after them is a decision about the engine's own cleanup.
export interface FocusRestoreQuery {
/** The region that counts as "inside" this layer. */
boundary: Element
/** The element focus would be restored TO. */
anchor?: Element | null
/**
* Also treat the anchor itself being focused as "inside" (`select`, which
* focuses its own trigger on open — without this its restore reads as "the
* user moved focus to the trigger" and never runs).
*/
allowAnchorActive?: boolean
}
FocusTrapOptions from @llui/components/utils
export interface FocusTrapOptions {
/** The container whose focusable descendants form the trap. */
container: ElementSource;
/** Element to focus when the trap activates. Defaults to first focusable. */
initialFocus?: Element | (() => Element | null);
/** Restore focus to the previously active element on release (default: true). */
restoreFocus?: boolean;
}
InteractOutsideOptions from @llui/components/utils
export interface InteractOutsideOptions {
/** Element(s) that define the "inside" region. */
element: ElementSource;
/** Additional elements whose interactions should not count as outside (e.g. triggers). */
ignore?: ElementSource;
/** Called on pointerdown or focus outside the inside region. */
onInteractOutside: (event: Event) => void;
/**
* If provided, called first with the event. Return `false` to suppress the
* outside callback (for an in-flight layer to claim the event).
*/
shouldDispatch?: (event: Event) => boolean;
}
NestedLayerOptions from @llui/components/utils
export interface NestedLayerOptions {
/**
* Consumers this registration participates in. Defaults to all of them, which
* is what a surface with no dismissable layer of its own needs. Narrow it when
* another mechanism already covers an aspect — see the module comment.
*/
aspects?: readonly NestedLayerAspect[];
/**
* The element this layer is logically nested INSIDE — its trigger/anchor, or
* the host element it belongs to. This is what makes the registry per-layer:
* an asking layer exempts this registration only when the owner is inside the
* asker's own boundary (transitively through other nested layers).
*
* The owner is NOT the layer's portal root — that is the `source` argument.
* It is the thing in the main document tree that the portal speaks for.
*
* Resolver form is supported and re-read on every lookup, so an owner that
* mounts and unmounts with its component can be named once.
*
* A missing or unresolved owner grants no scoped exemption and emits a
* development warning. Even a registration used only through the unscoped
* registry-wide view should name its logical owner to keep the contract
* explicit.
*/
owner?: ElementSource;
}
NumericGrid from @llui/components/utils
A stepped numeric range. min/max default to unbounded and step to 0
(= continuous). Component states name their fields the same way, so a state
object can be passed straight in.
export interface NumericGrid {
min?: number
max?: number
step?: number
}
ParsedDateValue from @llui/components/utils
export interface ParsedDateValue {
date: Date
/**
* True when the input was a bare calendar date. Formatters MUST then render
* in UTC (where the anchor was taken), otherwise the ambient zone shifts the
* rendered day.
*/
dateOnly: boolean
}
RovingItem from @llui/components/utils
export interface RovingItem {
value: string;
/** Disabled items are skipped by arrow/Home/End navigation. */
disabled?: boolean;
}
RovingOptions from @llui/components/utils
export interface RovingOptions {
/** Arrow axis — 'horizontal' uses Left/Right, 'vertical' uses Up/Down. Default 'horizontal'. */
orientation?: RovingOrientation;
/** Whether arrow navigation wraps at the ends. Default true. */
loop?: boolean;
/**
* An element used to resolve text direction for RTL arrow flipping
* (typically the event's `currentTarget`). When it resolves to
* `dir="rtl"`, ArrowLeft/ArrowRight swap. Optional.
*/
element?: Element | null;
}
TreeNode from @llui/components/utils
TreeCollection — helper for building tree-view payloads from a nested data structure. Tree-view's state machine only knows about flat lists (visibleItems, visibleLabels) and opaque ids; the collection owns the structure and derives those flat arrays on demand.
Typical flow:
const col = new TreeCollection(data) state.visibleItems = col.visibleItems(state.expanded) state.visibleLabels = col.visibleLabels(state.expanded)
After every expand / collapse message, the consumer dispatches
setVisibleItems with the updated arrays. The collection itself is
immutable — build a new one when the tree structure changes.
export interface TreeNode {
id: string
label?: string
disabled?: boolean
children?: TreeNode[]
}
Classes
TreeCollection from @llui/components/utils
class TreeCollection {
roots: TreeNode[]
info: Map<string, NodeInfo>
constructor(roots: TreeNode | TreeNode[])
index(): void
getNode(id: string): TreeNode | null
getLabel(id: string): string
getParent(id: string): string | null
getDepth(id: string): number
getChildren(id: string): string[]
getDescendants(id: string): string[]
isBranch(id: string): boolean
isDisabled(id: string): boolean
visibleItems(expanded: string[]): string[]
visibleLabels(expanded: string[]): string[]
computeIndeterminate(checked: Set<string>): string[]
}
Constants
ALL_NESTED_LAYER_ASPECTS from @llui/components/utils
Every aspect — the default for a registration that names none.
const ALL_NESTED_LAYER_ASPECTS: readonly NestedLayerAspect[]
TYPEAHEAD_TIMEOUT_MS from @llui/components/utils
Typeahead search — accumulates keystrokes into a query while the user types rapidly, then matches the first item whose label starts with the query. Used by listbox, menu, select, combobox, tree-view to support WAI-ARIA keyboard navigation patterns.
Behavior:
- If a keystroke arrives within
TYPEAHEAD_TIMEOUT_MSof the previous one, append to the existing query (so typing "sa" finds "Saturn" even if the highlight is currently on "Jupiter"). - Otherwise, start a fresh single-character query.
- Single-character queries advance past the current position (jump to the next item starting with that letter), which is the standard WAI-ARIA behavior — rapid repeated presses of "s" cycle through items beginning with "s".
- Multi-character queries search from the current cursor position (inclusive) so if the cursor is already on a matching item, it stays — typing "ap" while on "apricot" keeps focus on "apricot".
const TYPEAHEAD_TIMEOUT_MS
@llui/components/utils/anatomy
Functions
anatomy() from @llui/components/utils/anatomy
function anatomy<P extends string>(name: string, parts: readonly P[]): Anatomy<P>
resetAnatomyIdCounter() from @llui/components/utils/anatomy
Reset the internal id counter — tests only.
function resetAnatomyIdCounter(): void
Interfaces
Anatomy from @llui/components/utils/anatomy
export interface Anatomy<P extends string> {
readonly name: string
readonly parts: readonly P[]
/** Create a new scope instance. Pass an explicit id to force a value (SSR). */
scope(id?: string): AnatomyScope<P>
}
AnatomyScope from @llui/components/utils/anatomy
export interface AnatomyScope<P extends string> {
/** Instance id — unique across all anatomy scopes. */
readonly id: string
/** Resolve the id for a specific part (for ARIA wiring). */
idFor(part: P): string
/** Build the common data-attrs + id for a part. */
attrs(part: P): { id: string; 'data-scope': string; 'data-part': P }
}
@llui/components/utils/aria-hidden
Functions
setAriaHiddenOutside() from @llui/components/utils/aria-hidden
Hide sibling subtrees from assistive tech while an overlay is open.
Walks from target up to the document root, applying aria-hidden="true"
and inert to every sibling at each level. Previous attribute values are
recorded and restored on cleanup.
Two kinds of element are EXEMPT: registered nested layers (see
registerNestedLayer) and live regions — aria-live, role="alert",
role="status", role="log". A live region under aria-hidden is simply
never read out, so a modal that sweeps one silences the app's announcement
channel for exactly as long as it is open (#123). Live regions are matched by
selector rather than registration because they are plain part bags with no
mount hook of their own, and because that also covers consumer-authored ones.
An exempt element does NOT spare its whole ancestor subtree: the sweep
descends through any element that merely CONTAINS one and hides everything
hanging off the path down to it. (Skipping the ancestor wholesale would leave
the entire app interactive behind the modal the moment a form somewhere held
an aria-live error message.) inert and aria-hidden both inherit, so
leaving the path clear is the only way an exempt element stays reachable.
Nested calls are supported — each layer only touches elements that haven't been claimed by a higher layer (tracked via a WeakMap reference count).
TWO KNOWN LIMITS of the live-region exemption, both deliberate:
inertcannot be split fromaria-hidden— sparing a region spares its WHOLE SUBTREE from both, so an interactive live region (arole="log"transcript containing links) stays Tab-reachable behind a modal. Keep live regions to announcement text; put controls outside them, or register the modal's own layer for the interactive part.document.querySelectorAlldoes not pierce shadow roots, so a live region inside one is not exempt. The sweep itself only ever walks light-DOM ancestors oftarget, so this only bites when the region and the modal live in different trees.
export declare function setAriaHiddenOutside(target: Element): () => void;
@llui/components/utils/date
Functions
isDateOnly() from @llui/components/utils/date
function isDateOnly(value: DateValue): value is string
parseDateValue() from @llui/components/utils/date
function parseDateValue(value: DateValue): ParsedDateValue
Types
DateValue from @llui/components/utils/date
Date-only handling — the ONE place that decides whether a value denotes an INSTANT or a bare CALENDAR DATE.
new Date('2026-01-15') parses a date-only string as UTC midnight (ES spec),
so formatting it against the ambient zone renders the PREVIOUS day everywhere
west of UTC: formatDate('2026-01-15') printed "January 14, 2026" under
America/New_York and the right answer under Europe/Rome, which is why it
survived (#125 defect 4).
A calendar date carries no instant and therefore no zone, so it is anchored
at UTC midnight and its consumers must render it in UTC — see dateOnly.
export type DateValue = Date | string | number
Interfaces
ParsedDateValue from @llui/components/utils/date
export interface ParsedDateValue {
date: Date
/**
* True when the input was a bare calendar date. Formatters MUST then render
* in UTC (where the anchor was taken), otherwise the ambient zone shifts the
* rendered day.
*/
dateOnly: boolean
}
@llui/components/utils/derive
Functions
deriveOnce() from @llui/components/utils/derive
Wrap a ONE-ARGUMENT compute so that repeating a call with the same argument
returns the previous result. One cell — the first item of an update pays for
the derivation and the rest read it, so the cost is per UPDATE, not per item
and not per render.
This is the shape that runs per ROW per BINDING per update, so it takes a
FIXED parameter and compares with one Object.is. The variadic deriveOnceN
below materialises a fresh arguments array on every call — one per row per
binding per update — which is pure overhead on the hit path: over a pass of
N items x 4 bindings, 0.00056 -> 0.00041 ms at N=20, 0.00467 -> 0.00337 at
N=200 and 0.05680 -> 0.03823 at N=2000 (~25-33%). Reach for deriveOnceN
only where the derivation genuinely takes several inputs.
NO RUNTIME ARITY GUARD, deliberately. Calling the returned function with a
second argument silently ignores it — g(1,'x') and g(1,'y') both return
the g(1) result from one computation — so a JS consumer, or a TS consumer
who casts, can get a wrong answer with no error. That is accepted because
the only ways to observe arity at runtime are an arguments object (absent
in an arrow) or a rest parameter, and a rest parameter re-materialises the
per-row-per-binding array whose removal is this function's entire reason to
exist — paying the 25-33% back on the hottest path in the package, on every
hit, to catch a call TypeScript already rejects (TS2554). Do NOT "fix" this
by widening the signature. If a derivation needs more than one input, that
is what deriveOnceN is for.
function deriveOnce<A, R>(compute: (arg: A) => R): (arg: A) => R
deriveOnceN() from @llui/components/utils/derive
deriveOnce for a derivation with several inputs (the roving tab stop reads
three or four). Memoized on ARGUMENT IDENTITY, position by position.
function deriveOnceN<A extends readonly unknown[], R>(compute: (...args: A) => R): (...args: A) => R
indexMap() from @llui/components/utils/derive
A position lookup over a state array: positions(s.items).get(item) in place
of s.items.indexOf(item). First occurrence wins, matching indexOf.
function indexMap<T>(): (values: readonly T[] | null | undefined) => ReadonlyMap<T, number>
membershipSet() from @llui/components/utils/derive
A membership lookup over a state array: set(s.value).has(item) in place of
s.value.includes(item). An absent collection reads as empty.
An EMPTY array shares EMPTY_SET rather than allocating: most of the ~16
call sites are a disabled/disabledItems list that is empty in the common
case, and an empty Set is the one input where the memo would otherwise pay
an allocation to answer false to everything.
function membershipSet<T>(): (values: readonly T[] | null | undefined) => ReadonlySet<T>
@llui/components/utils/direction
Functions
flipArrow() from @llui/components/utils/direction
Map a horizontal arrow key to its logical direction, accounting for RTL. This is the SINGLE SOURCE OF TRUTH every component routes horizontal arrow interpretation through. Under rtl, ArrowLeft and ArrowRight swap meaning; vertical arrows (Up/Down), Home/End, PageUp/PageDown and every non-arrow key pass through unchanged.
The second argument is the direction source:
- an explicit
'ltr' | 'rtl'— used directly (the authoritative form when a component storesdirin its own State and passes it in); - an
Element— direction is resolved by walking up the DOM (dir="rtl"ancestor ordocument.documentElement.dir); null— treated as'ltr'(no-op).
export declare function flipArrow(key: string, source: Element | null | TextDirection): string;
resolveDir() from @llui/components/utils/direction
Resolve the text direction for an element by walking up the DOM tree. Returns 'rtl' or 'ltr' (default).
export declare function resolveDir(el: Element): TextDirection;
resolveTextDirection() from @llui/components/utils/direction
Normalize any accepted direction source to a concrete TextDirection.
An explicit 'ltr' | 'rtl' wins; an Element is resolved from the DOM;
null / undefined default to 'ltr'.
export declare function resolveTextDirection(source: Element | null | undefined | TextDirection): TextDirection;
Types
TextDirection from @llui/components/utils/direction
Text reading direction. The single shared RTL vocabulary for the package.
export type TextDirection = 'ltr' | 'rtl';
@llui/components/utils/dismissable
Functions
_dismissableStackSize() from @llui/components/utils/dismissable
@internal — for tests
export declare function _dismissableStackSize(): number;
pushDismissable() from @llui/components/utils/dismissable
Register a dismissable layer. Escape is offered to the layers top-down until
one CLAIMS it (a layer declines via disableEscape or an onEscape router
returning false, and the key then falls through to the layer beneath);
outside-click is topmost-only. Returns a cleanup that removes the layer from
the stack.
Push a layer even when both dismissal routes are disabled: the layer is the caller's PLACE ON THE STACK, which is what stops the layer beneath from treating an interaction inside this one as an outside interaction.
export declare function pushDismissable(opts: DismissableOptions): () => void;
Types
DismissSource from @llui/components/utils/dismissable
Reason a dismissable layer was closed.
export type DismissSource = 'escape' | 'outside';
Interfaces
DismissableOptions from @llui/components/utils/dismissable
export interface DismissableOptions {
/** The layer element (e.g. a dialog content or popover). */
element: ElementSource;
/** Trigger / anchor elements that should not count as outside interactions. */
ignore?: ElementSource;
/** Called when the user dismisses the layer. */
onDismiss: (source: DismissSource, event: Event) => void;
/**
* Custom Escape router. When provided it runs for the Escape key INSTEAD of
* `onDismiss('escape', …)`, letting the layer unwind an internal level first
* (e.g. a menu closes its open submenu before closing the whole menu). Return
* `false` to decline — the event is not claimed and propagates as if this
* layer had `disableEscape`. Any other return (incl. `undefined`) claims it.
*/
onEscape?: (event: KeyboardEvent) => boolean | void;
/** Disable outside-click dismissal (default: false). */
disableOutside?: boolean;
/** Disable Escape-key dismissal (default: false). */
disableEscape?: boolean;
}
@llui/components/utils/dom
Functions
composedTarget() from @llui/components/utils/dom
The innermost, non-retargeted target of an event.
At a shadow-DOM boundary the browser RETARGETS event.target to the shadow
host for any listener outside the shadow tree — so a document-level capture
listener sees the host element, not the element actually interacted with
inside the shadow root. event.composedPath()[0] is the real deepest target
(the path is composed across shadow boundaries and is populated while the
event is dispatching, which is when capture-phase handlers run). Fall back to
event.target when composedPath is unavailable or empty (non-DOM env / an
already-dispatched event). Essential for outside-interaction detection to work
when a component is mounted inside a shadow root (isolate mode) — otherwise
every in-shadow interaction reads as the host and is misclassified.
function composedTarget(event: Event): Node | null
containsOrEquals() from @llui/components/utils/dom
function containsOrEquals(container: Element, target: Node | null): boolean
isInAnyElement() from @llui/components/utils/dom
function isInAnyElement(target: Node | null, elements: Element[]): boolean
resolveElements() from @llui/components/utils/dom
function resolveElements<T extends Element>(source: ElementSource<T>): T[]
Types
ElementSource from @llui/components/utils/dom
Shared DOM helpers used by interaction utilities.
export type ElementSource<T extends Element = Element> = T | T[] | (() => T | T[] | null)
@llui/components/utils/engine-focus
Functions
engineFocus() from @llui/components/utils/engine-focus
Focus el as an engine-initiated move (see runEngineFocus).
export declare function engineFocus(el: HTMLElement, options?: FocusOptions): void;
isEngineFocusInProgress() from @llui/components/utils/engine-focus
Whether an engine-initiated focus move is in flight. Consulted by
watchInteractOutside to gate its focusin path.
export declare function isEngineFocusInProgress(): boolean;
runEngineFocus() from @llui/components/utils/engine-focus
Run body with engine-focus suppression active. Any focusin raised inside
it is invisible to watchInteractOutside — including one raised by a focus
move that re-entrant consumer code makes from a focusin listener (see the
module comment: the window is the synchronous transitive closure, not just
the .focus() call).
SYNCHRONOUS BY CONTRACT, AND THE CONTRACT IS ENFORCED (#172). The suppression
is released when body RETURNS. An async body returns its promise at the
first await, so the depth counter drops immediately and the focus move that
eventually happens gets NO protection at all — a call that looks correct,
compiles, and does nothing. The failure is safe (no protection, never a stuck
guard: the decrement is in a finally), which is exactly why it is invisible,
and this is a PUBLIC export documented as the thing a custom overlay "must"
route its engine-initiated focus moves through. A consumer following that
advice with an async body would reintroduce #155 in their app while believing
they had prevented it.
Two guards, because neither covers the other's case:
- The SIGNATURE rejects a promise-returning body at compile time. It is the real guard — it fires before the code ever runs. Its one blind spot is a body whose return type is an unresolved type parameter (a generic pass-through wrapper): the conditional is deferred, so such a wrapper is rejected too and must carry its own constraint. No caller does.
- A DEV-MODE warning catches what the type system cannot see: a JavaScript
consumer, an
any-typed body, or a body that returns a thenable without being declared as returning one. It cannot restore the protection — by the time a thenable is in hand the guard is already released — so it only reports.
Deliberately NOT offered: an async-aware variant that holds the guard across
an await. The guard is safe because no user event can be delivered inside
its window (see the module comment); holding it across a suspension point
hands the event loop back and would start swallowing genuine interactions.
export declare function runEngineFocus<T>(body: () => SyncEngineFocusBody<T>): T;
Types
SyncEngineFocusBodyRequired from @llui/components/utils/engine-focus
The type an ASYNC body collapses to in {@link runEngineFocus}'s parameter
position. Nothing is assignable to it, so runEngineFocus(async () => …) is a
compile error naming the contract rather than a silently inert call.
export type SyncEngineFocusBodyRequired = {
readonly [SYNC_BODY_REQUIRED]: 'runEngineFocus requires a SYNCHRONOUS body — the guard is released the moment body returns';
};
@llui/components/utils/floating
Functions
attachFloating() from @llui/components/utils/floating
Position floating relative to anchor with live updates on scroll/resize.
Applies left + top styles to the floating element. Returns a cleanup.
export declare function attachFloating(opts: FloatingOptions): () => void;
Types
Placement from @llui/components/utils/floating
export declare type Placement = Prettify<Side | AlignedPlacement>;
Interfaces
FloatingOptions from @llui/components/utils/floating
export interface FloatingOptions {
/** The reference element (trigger/anchor). */
anchor: Element;
/** The floating element (content). */
floating: HTMLElement;
/** Preferred placement (default: 'bottom'). */
placement?: Placement;
/** Gap between anchor and floating, in px (default: 0). */
offset?: number;
/** Flip to opposite side when there isn't enough room (default: true). */
flip?: boolean;
/** Shift along axis to stay in view (default: padding 8 unless false). */
shift?: boolean | {
padding?: number;
};
/**
* Reading direction. Under `'rtl'`, logical `*-start`/`*-end` placements
* track the inline-start/inline-end edges. When given it is AUTHORITATIVE —
* it overrides the direction the floating element happens to compute to,
* which for a portaled overlay is the direction of wherever it landed.
* Omit it to leave that decision to the page, as floating-ui does by default.
*/
dir?: TextDirection;
/** Optional arrow element to position. */
arrow?: HTMLElement;
/** Notify after each position computation. */
onUpdate?: (data: {
x: number;
y: number;
placement: Placement;
arrow?: {
x?: number;
y?: number;
};
}) => void;
}
@llui/components/utils/focus-restore
Functions
focusLingeredInside() from @llui/components/utils/focus-restore
Whether focus LINGERED INSIDE the layer, i.e. whether restoring it to the anchor respects the user rather than overriding them.
function focusLingeredInside(query: FocusRestoreQuery): boolean
Interfaces
FocusRestoreQuery from @llui/components/utils/focus-restore
The one rule for "should this layer pull focus back to its anchor?".
Restoring focus to the trigger is right when the layer was closed with focus still resting inside it (Escape, a close button, a programmatic close) — the user's focus would otherwise be left on a detached node. It is WRONG when the dismissal was caused by focus moving somewhere the user chose: yanking it back to the trigger takes focus away from the control they just reached, and can leave a still-open layer with focus outside it (#173).
document.body (and a null activeElement) counts as "inside" because that
is where focus lands when the focused element is removed — nobody chose it, so
the anchor is a better home than the body.
Both callers must ask BEFORE tearing anything down: the focus trap's release
and the aria-hidden/inert sweep both move or invalidate activeElement,
so a decision taken after them is a decision about the engine's own cleanup.
export interface FocusRestoreQuery {
/** The region that counts as "inside" this layer. */
boundary: Element
/** The element focus would be restored TO. */
anchor?: Element | null
/**
* Also treat the anchor itself being focused as "inside" (`select`, which
* focuses its own trigger on open — without this its restore reads as "the
* user moved focus to the trigger" and never runs).
*/
allowAnchorActive?: boolean
}
@llui/components/utils/focus-trap
Functions
_focusTrapStackSize() from @llui/components/utils/focus-trap
@internal — tests only
export declare function _focusTrapStackSize(): number;
pushFocusTrap() from @llui/components/utils/focus-trap
Push a focus trap onto the stack. Tab/Shift+Tab will cycle within the container's focusable descendants. Returns a cleanup that removes the trap and (optionally) restores focus to the element active before push.
export declare function pushFocusTrap(opts: FocusTrapOptions): () => void;
Interfaces
FocusTrapOptions from @llui/components/utils/focus-trap
export interface FocusTrapOptions {
/** The container whose focusable descendants form the trap. */
container: ElementSource;
/** Element to focus when the trap activates. Defaults to first focusable. */
initialFocus?: Element | (() => Element | null);
/** Restore focus to the previously active element on release (default: true). */
restoreFocus?: boolean;
}
@llui/components/utils/focusables
Functions
getFocusables() from @llui/components/utils/focusables
export declare function getFocusables(container: Element): HTMLElement[];
isFocusable() from @llui/components/utils/focusables
Find focusable descendants within a container.
export declare function isFocusable(el: Element): boolean;
@llui/components/utils/index
Functions
allFiniteNumbers() from @llui/components/utils/index
Whether every number nested in one atomic runtime payload is usable. Non-numeric leaves are ignored; arrays and plain payload objects are walked so callers cannot accidentally validate one coordinate while committing a bad sibling. Cycles are harmless because an already-seen object contains no new numeric leaves.
function allFiniteNumbers(...values: readonly unknown[]): boolean
anatomy() from @llui/components/utils/index
function anatomy<P extends string>(name: string, parts: readonly P[]): Anatomy<P>
applySelection() from @llui/components/utils/index
Apply a click/Enter on value to the current selection. Single mode
replaces, multiple toggles, and a disabled item changes nothing — returning
the SAME array reference so the reducer's no-op stays a no-op for the
reference-equality reconciler.
function applySelection(current: string[], value: string, opts: { mode: SelectionMode; disabled?: readonly string[] }): string[]
attachFloating() from @llui/components/utils/index
Position floating relative to anchor with live updates on scroll/resize.
Applies left + top styles to the floating element. Returns a cleanup.
export declare function attachFloating(opts: FloatingOptions): () => void;
clamp() from @llui/components/utils/index
Bound n into [min, max]. The result is always FINITE: a non-finite input
maps to a defined legal value instead of being stored verbatim.
Every comparison against NaN is false, so NaN used to fall straight
through to return n and land in state — package-wide, since this is the one
clamp every mutation path routes through (#152). It is not merely a wrong
number: JSON.stringify(NaN) (and Infinity) is null, so a non-finite
value breaks the State-is-JSON-serializable invariant and with it devtools
time-travel, @llui/test replay, agent state snapshots and SSR rehydration.
Rejecting at this boundary is what lets every caller state its own
postcondition — e.g. slider's withThumb — without a finiteness caveat.
function clamp(n: number, min: number, max: number): number
clampToStep() from @llui/components/utils/index
Clamp into the range AND snap onto the grid. The result is always within
[min, max]: snapping can leave the range when an endpoint is not itself on
the grid (min 0, max 10, step 4 → 10 snaps up to 12), and the answer there is
the last grid value INSIDE the range, not an out-of-range or off-grid one.
It is always FINITE too — the clamp rejects a non-finite input first (#152).
function clampToStep(value: number, grid: NumericGrid): number
decimalPlaces() from @llui/components/utils/index
Fraction digits n is written with, INCLUDING exponential notation —
String(1e-7) is '1e-7', which a scan for '.' reads as zero decimals.
function decimalPlaces(n: number): number
deriveOnce() from @llui/components/utils/index
Wrap a ONE-ARGUMENT compute so that repeating a call with the same argument
returns the previous result. One cell — the first item of an update pays for
the derivation and the rest read it, so the cost is per UPDATE, not per item
and not per render.
This is the shape that runs per ROW per BINDING per update, so it takes a
FIXED parameter and compares with one Object.is. The variadic deriveOnceN
below materialises a fresh arguments array on every call — one per row per
binding per update — which is pure overhead on the hit path: over a pass of
N items x 4 bindings, 0.00056 -> 0.00041 ms at N=20, 0.00467 -> 0.00337 at
N=200 and 0.05680 -> 0.03823 at N=2000 (~25-33%). Reach for deriveOnceN
only where the derivation genuinely takes several inputs.
NO RUNTIME ARITY GUARD, deliberately. Calling the returned function with a
second argument silently ignores it — g(1,'x') and g(1,'y') both return
the g(1) result from one computation — so a JS consumer, or a TS consumer
who casts, can get a wrong answer with no error. That is accepted because
the only ways to observe arity at runtime are an arguments object (absent
in an arrow) or a rest parameter, and a rest parameter re-materialises the
per-row-per-binding array whose removal is this function's entire reason to
exist — paying the 25-33% back on the hottest path in the package, on every
hit, to catch a call TypeScript already rejects (TS2554). Do NOT "fix" this
by widening the signature. If a derivation needs more than one input, that
is what deriveOnceN is for.
function deriveOnce<A, R>(compute: (arg: A) => R): (arg: A) => R
deriveOnceN() from @llui/components/utils/index
deriveOnce for a derivation with several inputs (the roving tab stop reads
three or four). Memoized on ARGUMENT IDENTITY, position by position.
function deriveOnceN<A extends readonly unknown[], R>(compute: (...args: A) => R): (...args: A) => R
engineFocus() from @llui/components/utils/index
Focus el as an engine-initiated move (see runEngineFocus).
export declare function engineFocus(el: HTMLElement, options?: FocusOptions): void;
finiteBound() from @llui/components/utils/index
A bound as STATE may hold it: the finite number itself, or undefined for
"no bound on this side". THE ONE normalizer for a bound, mirroring clamp's
role for a value (#177).
±Infinity and an ABSENT bound already mean the same thing to every clamp in
the package — clampToStep expands grid.min ?? -Infinity — but only one of
the two spellings survives JSON.stringify, which writes null for both
Infinity and NaN. State must be JSON-serializable (devtools time-travel,
@llui/test replay, agent state snapshots, SSR rehydration all compare
serialized state), so the infinite spelling belongs to the RUNTIME expansion
and never to state: normalize at every write, let the grid expand the absence
again. An unbounded number-input used to store min: -Infinity and
rehydrate as min: null — a number field holding null, on the DEFAULT
configuration.
NaN collapses here too, and that is the half a ?? cannot rescue: NaN is
not nullish, so a NaN bound reached clamp, every comparison against it
was false, and THAT SIDE OF THE RANGE STOPPED CLAMPING — angle-slider after
setMin: NaN stored -9999 for setValue(-9999).
Callers decide what an absent bound means for them, and there are exactly two idioms:
- UNBOUNDED-CAPABLE (
number-input): store theundefinedby OMITTING the key, so the state shape IS aNumericGridand round-trips identically. - INTRINSICALLY BOUNDED (
angle-slider,slider,splitter, …): a requiredmin: numbercannot spell "unbounded", so?? DEFAULTatinitand REJECT the write in asetMin/setMaxreducer — dropping a meaningless bound keeps the range the component already had, which is the only answer that cannot silently disable clamping.
function finiteBound(raw: number | null | undefined): number | undefined
finiteOrDefault() from @llui/components/utils/index
A component-owned number that has no range to clamp into. Initialization
replaces an unusable input with the field's ordinary default; runtime
reducers use {@link allFiniteNumbers} to refuse the whole message instead.
Keeping those two policies here prevents a free position or timestamp from
accidentally inheriting either the grid-value policy (clamp) or the
optional-bound policy (finiteBound).
function finiteOrDefault(raw: number | null | undefined, fallback: number): number
firstEnabled() from @llui/components/utils/index
Internal value navigation used by the public roving-focus primitive.
export declare function firstEnabled(items: readonly string[], disabled: readonly string[]): string | null;
firstEnabledIndex() from @llui/components/utils/index
function firstEnabledIndex(items: readonly string[], disabled: readonly string[]): number | null
flipArrow() from @llui/components/utils/index
Map a horizontal arrow key to its logical direction, accounting for RTL. This is the SINGLE SOURCE OF TRUTH every component routes horizontal arrow interpretation through. Under rtl, ArrowLeft and ArrowRight swap meaning; vertical arrows (Up/Down), Home/End, PageUp/PageDown and every non-arrow key pass through unchanged.
The second argument is the direction source:
- an explicit
'ltr' | 'rtl'— used directly (the authoritative form when a component storesdirin its own State and passes it in); - an
Element— direction is resolved by walking up the DOM (dir="rtl"ancestor ordocument.documentElement.dir); null— treated as'ltr'(no-op).
export declare function flipArrow(key: string, source: Element | null | TextDirection): string;
focusLingeredInside() from @llui/components/utils/index
Whether focus LINGERED INSIDE the layer, i.e. whether restoring it to the anchor respects the user rather than overriding them.
function focusLingeredInside(query: FocusRestoreQuery): boolean
focusRovingItem() from @llui/components/utils/index
Move DOM focus to the roving item identified by value within the same
widget instance as origin.
Roving-tabindex widgets track the active index in STATE, but assistive tech
follows real DOM focus — so after a keyboard move the handler MUST also move
focus, or arrow keys are silent for AT. origin is the event's
currentTarget (the item that received the key); its closest
[data-scope][data-part="root"] ancestor scopes the search so sibling
widgets of the same scope never cross-focus. No-op if nothing matches.
send() is synchronous and items already exist in the DOM, so this can be
called immediately after the navigation send.
export declare function focusRovingItem(origin: Element | null, scope: string, value: string, opts?: {
itemPart?: string;
attr?: string;
}): void;
focusRovingTab() from @llui/components/utils/index
Move DOM focus to the trigger whose data-value matches, within
container. Relies only on the role="tab" + data-value contract
(shared by components/tabs and any hand-rolled tablist). No-op when no
trigger matches. Call after the DOM reflects the new active tab (e.g. in
a microtask if activation triggers a re-render).
export declare function focusRovingTab(container: Element, value: string): void;
getFocusables() from @llui/components/utils/index
export declare function getFocusables(container: Element): HTMLElement[];
getNestedLayers() from @llui/components/utils/index
Currently-registered nested-layer elements (resolvers re-read live).
With an aspect, only registrations that participate in it; without one, all
of them. With a within boundary, only registrations nested inside it (see
the module comment); without one, the flat, layer-agnostic answer.
export declare function getNestedLayers(aspect?: NestedLayerAspect, within?: NestedLayerScope): Element[];
indexMap() from @llui/components/utils/index
A position lookup over a state array: positions(s.items).get(item) in place
of s.items.indexOf(item). First occurrence wins, matching indexOf.
function indexMap<T>(): (values: readonly T[] | null | undefined) => ReadonlyMap<T, number>
isDateOnly() from @llui/components/utils/index
function isDateOnly(value: DateValue): value is string
isEnabledItem() from @llui/components/utils/index
An item counts as navigable only while it is in the list AND not disabled.
function isEnabledItem(items: readonly string[], disabled: readonly string[], value: string): boolean
isEngineFocusInProgress() from @llui/components/utils/index
Whether an engine-initiated focus move is in flight. Consulted by
watchInteractOutside to gate its focusin path.
export declare function isEngineFocusInProgress(): boolean;
isFocusable() from @llui/components/utils/index
Find focusable descendants within a container.
export declare function isFocusable(el: Element): boolean;
isInNestedLayer() from @llui/components/utils/index
Whether target is inside (or equal to) a registered nested layer that
participates in aspect (any layer when aspect is omitted) and is nested
inside within (any layer when within is omitted).
export declare function isInNestedLayer(target: Node | null, aspect?: NestedLayerAspect, within?: NestedLayerScope): boolean;
isTypeaheadKey() from @llui/components/utils/index
Returns true if the key event should trigger a typeahead query — i.e., a
single printable character that isn't a modified keyboard shortcut. Use
this in onKeyDown handlers to decide whether to dispatch a typeahead
message.
function isTypeaheadKey(e: KeyboardEvent): boolean
lastEnabled() from @llui/components/utils/index
export declare function lastEnabled(items: readonly string[], disabled: readonly string[]): string | null;
lastEnabledIndex() from @llui/components/utils/index
function lastEnabledIndex(items: readonly string[], disabled: readonly string[]): number | null
lockBodyScroll() from @llui/components/utils/index
Lock body scroll while an overlay is open, preserving scrollbar width to avoid layout shift. Reference-counted so nested locks compose cleanly.
export declare function lockBodyScroll(): () => void;
membershipSet() from @llui/components/utils/index
A membership lookup over a state array: set(s.value).has(item) in place of
s.value.includes(item). An absent collection reads as empty.
An EMPTY array shares EMPTY_SET rather than allocating: most of the ~16
call sites are a disabled/disabledItems list that is empty in the common
case, and an empty Set is the one input where the memo would otherwise pay
an allocation to answer false to everything.
function membershipSet<T>(): (values: readonly T[] | null | undefined) => ReadonlySet<T>
nextEnabled() from @llui/components/utils/index
export declare function nextEnabled(items: readonly string[], disabled: readonly string[], from: string, delta: 1 | -1, loop: boolean): string | null;
nextEnabledIndex() from @llui/components/utils/index
The index of the next enabled item delta steps from from, wrapping.
from === null starts before the first item (delta 1) or after the last
(delta -1), so the first/last enabled index comes back.
function nextEnabledIndex(items: readonly string[], disabled: readonly string[], from: number | null, delta: 1 | -1): number | null
parseDateValue() from @llui/components/utils/index
function parseDateValue(value: DateValue): ParsedDateValue
positiveFinite() from @llui/components/utils/index
A finite number strictly greater than zero, or undefined when unusable.
function positiveFinite(raw: number | null | undefined): number | undefined
positiveFiniteOrDefault() from @llui/components/utils/index
A positive finite number, or the field's ordinary initialization default.
function positiveFiniteOrDefault(raw: number | null | undefined, fallback: number): number
presenceEndHandler() from @llui/components/utils/index
Guard a presence "animation/transition ended" handler so it only advances the
presence machine when the event fired on the element the listener is bound to
(e.target === e.currentTarget) — never on a bubbling descendant.
Overlay content (dialog, popover, menu, toast) reflects its exit phase via
data-state="closing" and stays mounted until an animationend/transitionend
dispatches animationEnd/transitionEnd. Without this guard, ANY descendant
animation or transition ending during the exit — a spinner, a ripple, a child
fade — bubbles up and prematurely unmounts the overlay before its own exit
animation completes.
Mirrors the e.target === el guard the transitions runtime applies in
waitForEnd (@llui/transitions).
function presenceEndHandler<E extends AnimationEvent | TransitionEvent>(handler: (e: E) => void): (e: E) => void
pruneToEnabled() from @llui/components/utils/index
Keep value only while it still names an enabled item, else null. Every
reducer that replaces the item list owes this to whatever it holds as
focused/selected — a dangling reference is the tab-stop bug.
function pruneToEnabled(items: readonly string[], disabled: readonly string[], value: string | null): string | null
pushDismissable() from @llui/components/utils/index
Register a dismissable layer. Escape is offered to the layers top-down until
one CLAIMS it (a layer declines via disableEscape or an onEscape router
returning false, and the key then falls through to the layer beneath);
outside-click is topmost-only. Returns a cleanup that removes the layer from
the stack.
Push a layer even when both dismissal routes are disabled: the layer is the caller's PLACE ON THE STACK, which is what stops the layer beneath from treating an interaction inside this one as an outside interaction.
export declare function pushDismissable(opts: DismissableOptions): () => void;
pushFocusTrap() from @llui/components/utils/index
Push a focus trap onto the stack. Tab/Shift+Tab will cycle within the container's focusable descendants. Returns a cleanup that removes the trap and (optionally) restores focus to the element active before push.
export declare function pushFocusTrap(opts: FocusTrapOptions): () => void;
registerNestedLayer() from @llui/components/utils/index
Register source (an element, array of elements, or a resolver returning
either) as a nested layer. Returns a cleanup that removes the registration.
Prefer the resolver form for a portaled overlay: register once on mount and
return the live root only while open ([] when closed), so a single
registration tracks the overlay's open/closed lifecycle without churn.
Pass opts.owner when scoped consumers must exempt the layer. Missing or
unresolved ownership fails closed and warns in development.
export declare function registerNestedLayer(source: ElementSource, opts?: NestedLayerOptions): () => void;
resetAnatomyIdCounter() from @llui/components/utils/index
Reset the internal id counter — tests only.
function resetAnatomyIdCounter(): void
resolveDir() from @llui/components/utils/index
Resolve the text direction for an element by walking up the DOM tree. Returns 'rtl' or 'ltr' (default).
export declare function resolveDir(el: Element): TextDirection;
resolveRovingMove() from @llui/components/utils/index
Map a keyboard key + the current tab value to a roving-tablist move,
or null when the key isn't a navigation/activation key or the move is
a no-op (empty list, no enabled sibling). Pure — does not touch the DOM
or call preventDefault; the caller decides (typically: prevent default
iff the result is non-null).
export declare function resolveRovingMove(key: string, current: string, items: readonly RovingItem[], opts?: RovingOptions): RovingMove | null;
resolveTextDirection() from @llui/components/utils/index
Normalize any accepted direction source to a concrete TextDirection.
An explicit 'ltr' | 'rtl' wins; an Element is resolved from the DOM;
null / undefined default to 'ltr'.
export declare function resolveTextDirection(source: Element | null | undefined | TextDirection): TextDirection;
rovingTabStop() from @llui/components/utils/index
The single item that carries tabindex="0".
WAI-ARIA's roving-tabindex pattern requires EXACTLY ONE tab stop in a
composite widget: with none, Tab skips the widget entirely and it becomes
keyboard-unreachable. So a preferred candidate (the focused item, the
checked radio, …) is honoured only while it is still an enabled member, and
the first enabled item answers otherwise. Null only when nothing is enabled.
Every roving-tabindex widget in the package routes through here (#145 closed
the last three: menubar, navigation-menu and tags-input). Keep it that way —
an inline focused === x ? 0 : -1 has no fallback, and since nothing prunes
focused against the current list, removing or disabling the focused item
leaves EVERY item at -1 and the widget disappears from the Tab order.
tags-input is index-keyed and passes String(i) as the item identity (its
data-index, and the only identity that survives duplicate tag values);
navigation-menu passes either the membership list its consumer maintains or
the ids handed to its own item(), filtered first to the ones not sealed
inside a closed submenu — membership alone would seat the stop on an element
inside a hidden panel, which is present, unique and untabbable.
Null when nothing is enabled is deliberate and is a caller's problem to
notice: a widget whose items are ALL disabled ends up with no tab stop at
all. That is right for radio-group/toggle-group/toolbar/tree-view,
whose items are genuinely disabled and therefore unfocusable anyway, and it
is a 1 -> 0 change for menubar, whose triggers carry only aria-disabled
and stay focusable. Any revision belongs here, applying to every caller at
once — not in one component.
function rovingTabStop(items: readonly string[], disabled: readonly string[], ...preferred: readonly (string | null | undefined)[]): string | null
runEngineFocus() from @llui/components/utils/index
Run body with engine-focus suppression active. Any focusin raised inside
it is invisible to watchInteractOutside — including one raised by a focus
move that re-entrant consumer code makes from a focusin listener (see the
module comment: the window is the synchronous transitive closure, not just
the .focus() call).
SYNCHRONOUS BY CONTRACT, AND THE CONTRACT IS ENFORCED (#172). The suppression
is released when body RETURNS. An async body returns its promise at the
first await, so the depth counter drops immediately and the focus move that
eventually happens gets NO protection at all — a call that looks correct,
compiles, and does nothing. The failure is safe (no protection, never a stuck
guard: the decrement is in a finally), which is exactly why it is invisible,
and this is a PUBLIC export documented as the thing a custom overlay "must"
route its engine-initiated focus moves through. A consumer following that
advice with an async body would reintroduce #155 in their app while believing
they had prevented it.
Two guards, because neither covers the other's case:
- The SIGNATURE rejects a promise-returning body at compile time. It is the real guard — it fires before the code ever runs. Its one blind spot is a body whose return type is an unresolved type parameter (a generic pass-through wrapper): the conditional is deferred, so such a wrapper is rejected too and must carry its own constraint. No caller does.
- A DEV-MODE warning catches what the type system cannot see: a JavaScript
consumer, an
any-typed body, or a body that returns a thenable without being declared as returning one. It cannot restore the protection — by the time a thenable is in hand the guard is already released — so it only reports.
Deliberately NOT offered: an async-aware variant that holds the guard across
an await. The guard is safe because no user event can be delivered inside
its window (see the module comment); holding it across a suspension point
hands the event loop back and would start swallowing genuine interactions.
export declare function runEngineFocus<T>(body: () => SyncEngineFocusBody<T>): T;
setAriaHiddenOutside() from @llui/components/utils/index
Hide sibling subtrees from assistive tech while an overlay is open.
Walks from target up to the document root, applying aria-hidden="true"
and inert to every sibling at each level. Previous attribute values are
recorded and restored on cleanup.
Two kinds of element are EXEMPT: registered nested layers (see
registerNestedLayer) and live regions — aria-live, role="alert",
role="status", role="log". A live region under aria-hidden is simply
never read out, so a modal that sweeps one silences the app's announcement
channel for exactly as long as it is open (#123). Live regions are matched by
selector rather than registration because they are plain part bags with no
mount hook of their own, and because that also covers consumer-authored ones.
An exempt element does NOT spare its whole ancestor subtree: the sweep
descends through any element that merely CONTAINS one and hides everything
hanging off the path down to it. (Skipping the ancestor wholesale would leave
the entire app interactive behind the modal the moment a form somewhere held
an aria-live error message.) inert and aria-hidden both inherit, so
leaving the path clear is the only way an exempt element stays reachable.
Nested calls are supported — each layer only touches elements that haven't been claimed by a higher layer (tracked via a WeakMap reference count).
TWO KNOWN LIMITS of the live-region exemption, both deliberate:
inertcannot be split fromaria-hidden— sparing a region spares its WHOLE SUBTREE from both, so an interactive live region (arole="log"transcript containing links) stays Tab-reachable behind a modal. Keep live regions to announcement text; put controls outside them, or register the modal's own layer for the interactive part.document.querySelectorAlldoes not pierce shadow roots, so a live region inside one is not exempt. The sweep itself only ever walks light-DOM ancestors oftarget, so this only bites when the region and the modal live in different trees.
export declare function setAriaHiddenOutside(target: Element): () => void;
snapToStep() from @llui/components/utils/index
Nearest multiple of step from origin. A non-positive step is a no-op.
A non-finite value names no position on the grid, so it snaps to the grid's
own anchor — the same policy clamp applies to the range (#152). This is
unreachable from clampToStep, which clamps first; it keeps the util's
direct consumers on the same rule.
function snapToStep(value: number, step: number, origin = 0): number
stepBy() from @llui/components/utils/index
Move count whole steps (negative to step down), then clamp+snap.
From an OFF-GRID value one call moves to the nearest grid value in the
direction of travel and stops there — that jump is the whole change, however
large count is. This is HTML's stepUp/stepDown (step 3 of the
value-stepping algorithm) and it is what makes increment land on the grid
instead of dragging an off-grid value along forever.
ONE DELIBERATE DIVERGENCE from the spec: HTML's step base falls back min ->
the value CONTENT ATTRIBUTE -> 0; gridOrigin goes min -> 0. A headless
machine has no content attributes — the seed value is just the initial state,
and anchoring the grid on it would make two components with the same
min/max/step disagree about which values are legal depending on where they
happened to start.
function stepBy(value: number, count: number, grid: NumericGrid): number
typeaheadAccumulate() from @llui/components/utils/index
Advance the typeahead query based on a new keystroke and the previous
expiration time. Returns the new query string; callers combine this with
typeaheadMatch() to produce a new highlight index.
function typeaheadAccumulate(prev: string, char: string, now: number, expiresAt: number): string
typeaheadMatch() from @llui/components/utils/index
Find the first enabled item whose label starts with the query
(case-insensitive). labels and disabledMask are parallel arrays.
startFrom is the current highlighted index; for single-character
queries the search begins at startFrom + 1 (so repeated "s" keys
cycle), for multi-character queries it begins at startFrom (inclusive).
Returns the matching index, or null if no enabled item matches.
function typeaheadMatch(labels: string[], disabledMask: boolean[], query: string, startFrom: number | null): number | null
typeaheadMatchByItems() from @llui/components/utils/index
Convenience: pass a disabled list of values instead of a boolean mask.
Builds the mask by checking membership via === on the raw string values.
function typeaheadMatchByItems(items: string[], disabled: readonly string[], query: string, startFrom: number | null): number | null
watchInteractOutside() from @llui/components/utils/index
Watch for pointer or focus events outside a given element. Returns a
cleanup function. Uses the capture phase so upstream stopPropagation
calls cannot hide events.
- pointerdown (or mousedown/touchstart fallback) triggers "outside" if the
target is not contained by
elementorignore. - focusin triggers "outside" when focus moves outside the element, except
when the new target is in
ignore.
export declare function watchInteractOutside(opts: InteractOutsideOptions): () => void;
Types
DateValue from @llui/components/utils/index
Date-only handling — the ONE place that decides whether a value denotes an INSTANT or a bare CALENDAR DATE.
new Date('2026-01-15') parses a date-only string as UTC midnight (ES spec),
so formatting it against the ambient zone renders the PREVIOUS day everywhere
west of UTC: formatDate('2026-01-15') printed "January 14, 2026" under
America/New_York and the right answer under Europe/Rome, which is why it
survived (#125 defect 4).
A calendar date carries no instant and therefore no zone, so it is anchored
at UTC midnight and its consumers must render it in UTC — see dateOnly.
export type DateValue = Date | string | number
DismissSource from @llui/components/utils/index
Reason a dismissable layer was closed.
export type DismissSource = 'escape' | 'outside';
ElementSource from @llui/components/utils/index
Shared DOM helpers used by interaction utilities.
export type ElementSource<T extends Element = Element> = T | T[] | (() => T | T[] | null)
NestedLayerAspect from @llui/components/utils/index
A consumer of the registry. A registration participates only in the aspects it
names, because a single answer is wrong for at least one consumer: engine
overlays leave outside to the ordered dismissable stack (see the module
comment).
The dialog-with-an-inner-select case is NOT what the aspect list protects.
That one is covered by a modal never registering AT ALL, whatever aspects it
would have named.
outside— {@link watchInteractOutside} does not treat interactions inside the layer as outside interactions.focus— {@link pushFocusTrap} includes the layer as an extra focusable container, so Tab/Shift+Tab can reach it.hide— {@link setAriaHiddenOutside} hides AROUND the layer rather than hiding it.
export type NestedLayerAspect = 'outside' | 'focus' | 'hide';
NestedLayerScope from @llui/components/utils/index
The asking layer's own boundary — what "nested inside ME" is measured against. Omit it for the flat, layer-agnostic answer.
export type NestedLayerScope = ElementSource;
Placement from @llui/components/utils/index
export declare type Placement = Prettify<Side | AlignedPlacement>;
RovingMove from @llui/components/utils/index
The navigation a key implies on a roving tablist.
export type RovingMove =
/** An arrow / Home / End resolved to a (different, enabled) tab value. */
{
type: 'focus';
value: string;
}
/** Enter or Space — activate the currently focused tab (manual mode). */
| {
type: 'activate';
};
RovingOrientation from @llui/components/utils/index
Headless roving-tablist navigation — the keyboard logic of a WAI-ARIA tablist, decoupled from any particular DOM contract.
components/tabs.ts builds its reactive part-bags on top of this; a
consumer that wants its OWN markup (different classes, ids, no
data-scope/data-part) can drive the same keyboard behaviour by
calling resolveRovingMove from its trigger's onKeyDown and
focusRovingTab to move DOM focus — without adopting the component's
markup or its connect() state machine.
The resolver is pure (key + current value + items → a move); the only
shared DOM assumption lives in focusRovingTab, and it is the minimal
one both surfaces already satisfy: triggers carry role="tab" and
data-value="<value>".
The list walk itself lives in list-navigation.ts — this module is the
keyboard + DOM-focus surface over it, nothing more.
export type RovingOrientation = 'horizontal' | 'vertical';
SyncEngineFocusBodyRequired from @llui/components/utils/index
The type an ASYNC body collapses to in {@link runEngineFocus}'s parameter
position. Nothing is assignable to it, so runEngineFocus(async () => …) is a
compile error naming the contract rather than a silently inert call.
export type SyncEngineFocusBodyRequired = {
readonly [SYNC_BODY_REQUIRED]: 'runEngineFocus requires a SYNCHRONOUS body — the guard is released the moment body returns';
};
TextDirection from @llui/components/utils/index
Text reading direction. The single shared RTL vocabulary for the package.
export type TextDirection = 'ltr' | 'rtl';
Interfaces
Anatomy from @llui/components/utils/index
export interface Anatomy<P extends string> {
readonly name: string
readonly parts: readonly P[]
/** Create a new scope instance. Pass an explicit id to force a value (SSR). */
scope(id?: string): AnatomyScope<P>
}
AnatomyScope from @llui/components/utils/index
export interface AnatomyScope<P extends string> {
/** Instance id — unique across all anatomy scopes. */
readonly id: string
/** Resolve the id for a specific part (for ARIA wiring). */
idFor(part: P): string
/** Build the common data-attrs + id for a part. */
attrs(part: P): { id: string; 'data-scope': string; 'data-part': P }
}
DismissableOptions from @llui/components/utils/index
export interface DismissableOptions {
/** The layer element (e.g. a dialog content or popover). */
element: ElementSource;
/** Trigger / anchor elements that should not count as outside interactions. */
ignore?: ElementSource;
/** Called when the user dismisses the layer. */
onDismiss: (source: DismissSource, event: Event) => void;
/**
* Custom Escape router. When provided it runs for the Escape key INSTEAD of
* `onDismiss('escape', …)`, letting the layer unwind an internal level first
* (e.g. a menu closes its open submenu before closing the whole menu). Return
* `false` to decline — the event is not claimed and propagates as if this
* layer had `disableEscape`. Any other return (incl. `undefined`) claims it.
*/
onEscape?: (event: KeyboardEvent) => boolean | void;
/** Disable outside-click dismissal (default: false). */
disableOutside?: boolean;
/** Disable Escape-key dismissal (default: false). */
disableEscape?: boolean;
}
FloatingOptions from @llui/components/utils/index
export interface FloatingOptions {
/** The reference element (trigger/anchor). */
anchor: Element;
/** The floating element (content). */
floating: HTMLElement;
/** Preferred placement (default: 'bottom'). */
placement?: Placement;
/** Gap between anchor and floating, in px (default: 0). */
offset?: number;
/** Flip to opposite side when there isn't enough room (default: true). */
flip?: boolean;
/** Shift along axis to stay in view (default: padding 8 unless false). */
shift?: boolean | {
padding?: number;
};
/**
* Reading direction. Under `'rtl'`, logical `*-start`/`*-end` placements
* track the inline-start/inline-end edges. When given it is AUTHORITATIVE —
* it overrides the direction the floating element happens to compute to,
* which for a portaled overlay is the direction of wherever it landed.
* Omit it to leave that decision to the page, as floating-ui does by default.
*/
dir?: TextDirection;
/** Optional arrow element to position. */
arrow?: HTMLElement;
/** Notify after each position computation. */
onUpdate?: (data: {
x: number;
y: number;
placement: Placement;
arrow?: {
x?: number;
y?: number;
};
}) => void;
}
FocusRestoreQuery from @llui/components/utils/index
The one rule for "should this layer pull focus back to its anchor?".
Restoring focus to the trigger is right when the layer was closed with focus still resting inside it (Escape, a close button, a programmatic close) — the user's focus would otherwise be left on a detached node. It is WRONG when the dismissal was caused by focus moving somewhere the user chose: yanking it back to the trigger takes focus away from the control they just reached, and can leave a still-open layer with focus outside it (#173).
document.body (and a null activeElement) counts as "inside" because that
is where focus lands when the focused element is removed — nobody chose it, so
the anchor is a better home than the body.
Both callers must ask BEFORE tearing anything down: the focus trap's release
and the aria-hidden/inert sweep both move or invalidate activeElement,
so a decision taken after them is a decision about the engine's own cleanup.
export interface FocusRestoreQuery {
/** The region that counts as "inside" this layer. */
boundary: Element
/** The element focus would be restored TO. */
anchor?: Element | null
/**
* Also treat the anchor itself being focused as "inside" (`select`, which
* focuses its own trigger on open — without this its restore reads as "the
* user moved focus to the trigger" and never runs).
*/
allowAnchorActive?: boolean
}
FocusTrapOptions from @llui/components/utils/index
export interface FocusTrapOptions {
/** The container whose focusable descendants form the trap. */
container: ElementSource;
/** Element to focus when the trap activates. Defaults to first focusable. */
initialFocus?: Element | (() => Element | null);
/** Restore focus to the previously active element on release (default: true). */
restoreFocus?: boolean;
}
InteractOutsideOptions from @llui/components/utils/index
export interface InteractOutsideOptions {
/** Element(s) that define the "inside" region. */
element: ElementSource;
/** Additional elements whose interactions should not count as outside (e.g. triggers). */
ignore?: ElementSource;
/** Called on pointerdown or focus outside the inside region. */
onInteractOutside: (event: Event) => void;
/**
* If provided, called first with the event. Return `false` to suppress the
* outside callback (for an in-flight layer to claim the event).
*/
shouldDispatch?: (event: Event) => boolean;
}
NestedLayerOptions from @llui/components/utils/index
export interface NestedLayerOptions {
/**
* Consumers this registration participates in. Defaults to all of them, which
* is what a surface with no dismissable layer of its own needs. Narrow it when
* another mechanism already covers an aspect — see the module comment.
*/
aspects?: readonly NestedLayerAspect[];
/**
* The element this layer is logically nested INSIDE — its trigger/anchor, or
* the host element it belongs to. This is what makes the registry per-layer:
* an asking layer exempts this registration only when the owner is inside the
* asker's own boundary (transitively through other nested layers).
*
* The owner is NOT the layer's portal root — that is the `source` argument.
* It is the thing in the main document tree that the portal speaks for.
*
* Resolver form is supported and re-read on every lookup, so an owner that
* mounts and unmounts with its component can be named once.
*
* A missing or unresolved owner grants no scoped exemption and emits a
* development warning. Even a registration used only through the unscoped
* registry-wide view should name its logical owner to keep the contract
* explicit.
*/
owner?: ElementSource;
}
NumericGrid from @llui/components/utils/index
A stepped numeric range. min/max default to unbounded and step to 0
(= continuous). Component states name their fields the same way, so a state
object can be passed straight in.
export interface NumericGrid {
min?: number
max?: number
step?: number
}
ParsedDateValue from @llui/components/utils/index
export interface ParsedDateValue {
date: Date
/**
* True when the input was a bare calendar date. Formatters MUST then render
* in UTC (where the anchor was taken), otherwise the ambient zone shifts the
* rendered day.
*/
dateOnly: boolean
}
RovingItem from @llui/components/utils/index
export interface RovingItem {
value: string;
/** Disabled items are skipped by arrow/Home/End navigation. */
disabled?: boolean;
}
RovingOptions from @llui/components/utils/index
export interface RovingOptions {
/** Arrow axis — 'horizontal' uses Left/Right, 'vertical' uses Up/Down. Default 'horizontal'. */
orientation?: RovingOrientation;
/** Whether arrow navigation wraps at the ends. Default true. */
loop?: boolean;
/**
* An element used to resolve text direction for RTL arrow flipping
* (typically the event's `currentTarget`). When it resolves to
* `dir="rtl"`, ArrowLeft/ArrowRight swap. Optional.
*/
element?: Element | null;
}
TreeNode from @llui/components/utils/index
TreeCollection — helper for building tree-view payloads from a nested data structure. Tree-view's state machine only knows about flat lists (visibleItems, visibleLabels) and opaque ids; the collection owns the structure and derives those flat arrays on demand.
Typical flow:
const col = new TreeCollection(data) state.visibleItems = col.visibleItems(state.expanded) state.visibleLabels = col.visibleLabels(state.expanded)
After every expand / collapse message, the consumer dispatches
setVisibleItems with the updated arrays. The collection itself is
immutable — build a new one when the tree structure changes.
export interface TreeNode {
id: string
label?: string
disabled?: boolean
children?: TreeNode[]
}
Classes
TreeCollection from @llui/components/utils/index
class TreeCollection {
roots: TreeNode[]
info: Map<string, NodeInfo>
constructor(roots: TreeNode | TreeNode[])
index(): void
getNode(id: string): TreeNode | null
getLabel(id: string): string
getParent(id: string): string | null
getDepth(id: string): number
getChildren(id: string): string[]
getDescendants(id: string): string[]
isBranch(id: string): boolean
isDisabled(id: string): boolean
visibleItems(expanded: string[]): string[]
visibleLabels(expanded: string[]): string[]
computeIndeterminate(checked: Set<string>): string[]
}
Constants
ALL_NESTED_LAYER_ASPECTS from @llui/components/utils/index
Every aspect — the default for a registration that names none.
const ALL_NESTED_LAYER_ASPECTS: readonly NestedLayerAspect[]
TYPEAHEAD_TIMEOUT_MS from @llui/components/utils/index
Typeahead search — accumulates keystrokes into a query while the user types rapidly, then matches the first item whose label starts with the query. Used by listbox, menu, select, combobox, tree-view to support WAI-ARIA keyboard navigation patterns.
Behavior:
- If a keystroke arrives within
TYPEAHEAD_TIMEOUT_MSof the previous one, append to the existing query (so typing "sa" finds "Saturn" even if the highlight is currently on "Jupiter"). - Otherwise, start a fresh single-character query.
- Single-character queries advance past the current position (jump to the next item starting with that letter), which is the standard WAI-ARIA behavior — rapid repeated presses of "s" cycle through items beginning with "s".
- Multi-character queries search from the current cursor position (inclusive) so if the cursor is already on a matching item, it stays — typing "ap" while on "apricot" keeps focus on "apricot".
const TYPEAHEAD_TIMEOUT_MS
@llui/components/utils/interact-outside
Functions
watchInteractOutside() from @llui/components/utils/interact-outside
Watch for pointer or focus events outside a given element. Returns a
cleanup function. Uses the capture phase so upstream stopPropagation
calls cannot hide events.
- pointerdown (or mousedown/touchstart fallback) triggers "outside" if the
target is not contained by
elementorignore. - focusin triggers "outside" when focus moves outside the element, except
when the new target is in
ignore.
export declare function watchInteractOutside(opts: InteractOutsideOptions): () => void;
Interfaces
InteractOutsideOptions from @llui/components/utils/interact-outside
export interface InteractOutsideOptions {
/** Element(s) that define the "inside" region. */
element: ElementSource;
/** Additional elements whose interactions should not count as outside (e.g. triggers). */
ignore?: ElementSource;
/** Called on pointerdown or focus outside the inside region. */
onInteractOutside: (event: Event) => void;
/**
* If provided, called first with the event. Return `false` to suppress the
* outside callback (for an in-flight layer to claim the event).
*/
shouldDispatch?: (event: Event) => boolean;
}
@llui/components/utils/lifecycle
Functions
onScopeTeardown() from @llui/components/utils/lifecycle
Run fn when the enclosing build's scope tears down — but only if there is a
build to hook.
connect() in this package is a pure part-bag builder: it must stay callable
from a unit test with no build context, so a bare onTeardown (which throws
outside a build) cannot be used directly. __currentBuildInfo() returns
null outside a build, giving a non-throwing predicate.
Best-effort by design. Outside a build this is a no-op, so anything registered here must be a CLEANUP for something that is already safe on its own — a pending hover timer whose message is dropped by a detached-element guard, say. Cancelling it is the tidy-up; the guard is the correctness.
function onScopeTeardown(fn: () => void): void
@llui/components/utils/list-navigation
Functions
applySelection() from @llui/components/utils/list-navigation
Apply a click/Enter on value to the current selection. Single mode
replaces, multiple toggles, and a disabled item changes nothing — returning
the SAME array reference so the reducer's no-op stays a no-op for the
reference-equality reconciler.
function applySelection(current: string[], value: string, opts: { mode: SelectionMode; disabled?: readonly string[] }): string[]
firstEnabled() from @llui/components/utils/list-navigation
Internal value navigation used by the public roving-focus primitive.
export declare function firstEnabled(items: readonly string[], disabled: readonly string[]): string | null;
firstEnabledIndex() from @llui/components/utils/list-navigation
function firstEnabledIndex(items: readonly string[], disabled: readonly string[]): number | null
isEnabledItem() from @llui/components/utils/list-navigation
An item counts as navigable only while it is in the list AND not disabled.
function isEnabledItem(items: readonly string[], disabled: readonly string[], value: string): boolean
lastEnabled() from @llui/components/utils/list-navigation
export declare function lastEnabled(items: readonly string[], disabled: readonly string[]): string | null;
lastEnabledIndex() from @llui/components/utils/list-navigation
function lastEnabledIndex(items: readonly string[], disabled: readonly string[]): number | null
nextEnabled() from @llui/components/utils/list-navigation
export declare function nextEnabled(items: readonly string[], disabled: readonly string[], from: string, delta: 1 | -1, loop: boolean): string | null;
nextEnabledIndex() from @llui/components/utils/list-navigation
The index of the next enabled item delta steps from from, wrapping.
from === null starts before the first item (delta 1) or after the last
(delta -1), so the first/last enabled index comes back.
function nextEnabledIndex(items: readonly string[], disabled: readonly string[], from: number | null, delta: 1 | -1): number | null
pruneToEnabled() from @llui/components/utils/list-navigation
Keep value only while it still names an enabled item, else null. Every
reducer that replaces the item list owes this to whatever it holds as
focused/selected — a dangling reference is the tab-stop bug.
function pruneToEnabled(items: readonly string[], disabled: readonly string[], value: string | null): string | null
rovingTabStop() from @llui/components/utils/list-navigation
The single item that carries tabindex="0".
WAI-ARIA's roving-tabindex pattern requires EXACTLY ONE tab stop in a
composite widget: with none, Tab skips the widget entirely and it becomes
keyboard-unreachable. So a preferred candidate (the focused item, the
checked radio, …) is honoured only while it is still an enabled member, and
the first enabled item answers otherwise. Null only when nothing is enabled.
Every roving-tabindex widget in the package routes through here (#145 closed
the last three: menubar, navigation-menu and tags-input). Keep it that way —
an inline focused === x ? 0 : -1 has no fallback, and since nothing prunes
focused against the current list, removing or disabling the focused item
leaves EVERY item at -1 and the widget disappears from the Tab order.
tags-input is index-keyed and passes String(i) as the item identity (its
data-index, and the only identity that survives duplicate tag values);
navigation-menu passes either the membership list its consumer maintains or
the ids handed to its own item(), filtered first to the ones not sealed
inside a closed submenu — membership alone would seat the stop on an element
inside a hidden panel, which is present, unique and untabbable.
Null when nothing is enabled is deliberate and is a caller's problem to
notice: a widget whose items are ALL disabled ends up with no tab stop at
all. That is right for radio-group/toggle-group/toolbar/tree-view,
whose items are genuinely disabled and therefore unfocusable anyway, and it
is a 1 -> 0 change for menubar, whose triggers carry only aria-disabled
and stay focusable. Any revision belongs here, applying to every caller at
once — not in one component.
function rovingTabStop(items: readonly string[], disabled: readonly string[], ...preferred: readonly (string | null | undefined)[]): string | null
Types
SelectionMode from @llui/components/utils/list-navigation
export type SelectionMode = 'single' | 'multiple'
@llui/components/utils/nested-layer
Functions
_nestedLayerCount() from @llui/components/utils/nested-layer
@internal — tests only
export declare function _nestedLayerCount(): number;
getNestedLayers() from @llui/components/utils/nested-layer
Currently-registered nested-layer elements (resolvers re-read live).
With an aspect, only registrations that participate in it; without one, all
of them. With a within boundary, only registrations nested inside it (see
the module comment); without one, the flat, layer-agnostic answer.
export declare function getNestedLayers(aspect?: NestedLayerAspect, within?: NestedLayerScope): Element[];
isInNestedLayer() from @llui/components/utils/nested-layer
Whether target is inside (or equal to) a registered nested layer that
participates in aspect (any layer when aspect is omitted) and is nested
inside within (any layer when within is omitted).
export declare function isInNestedLayer(target: Node | null, aspect?: NestedLayerAspect, within?: NestedLayerScope): boolean;
registerNestedLayer() from @llui/components/utils/nested-layer
Register source (an element, array of elements, or a resolver returning
either) as a nested layer. Returns a cleanup that removes the registration.
Prefer the resolver form for a portaled overlay: register once on mount and
return the live root only while open ([] when closed), so a single
registration tracks the overlay's open/closed lifecycle without churn.
Pass opts.owner when scoped consumers must exempt the layer. Missing or
unresolved ownership fails closed and warns in development.
export declare function registerNestedLayer(source: ElementSource, opts?: NestedLayerOptions): () => void;
Types
NestedLayerAspect from @llui/components/utils/nested-layer
A consumer of the registry. A registration participates only in the aspects it
names, because a single answer is wrong for at least one consumer: engine
overlays leave outside to the ordered dismissable stack (see the module
comment).
The dialog-with-an-inner-select case is NOT what the aspect list protects.
That one is covered by a modal never registering AT ALL, whatever aspects it
would have named.
outside— {@link watchInteractOutside} does not treat interactions inside the layer as outside interactions.focus— {@link pushFocusTrap} includes the layer as an extra focusable container, so Tab/Shift+Tab can reach it.hide— {@link setAriaHiddenOutside} hides AROUND the layer rather than hiding it.
export type NestedLayerAspect = 'outside' | 'focus' | 'hide';
NestedLayerScope from @llui/components/utils/nested-layer
The asking layer's own boundary — what "nested inside ME" is measured against. Omit it for the flat, layer-agnostic answer.
export type NestedLayerScope = ElementSource;
Interfaces
NestedLayerOptions from @llui/components/utils/nested-layer
export interface NestedLayerOptions {
/**
* Consumers this registration participates in. Defaults to all of them, which
* is what a surface with no dismissable layer of its own needs. Narrow it when
* another mechanism already covers an aspect — see the module comment.
*/
aspects?: readonly NestedLayerAspect[];
/**
* The element this layer is logically nested INSIDE — its trigger/anchor, or
* the host element it belongs to. This is what makes the registry per-layer:
* an asking layer exempts this registration only when the owner is inside the
* asker's own boundary (transitively through other nested layers).
*
* The owner is NOT the layer's portal root — that is the `source` argument.
* It is the thing in the main document tree that the portal speaks for.
*
* Resolver form is supported and re-read on every lookup, so an owner that
* mounts and unmounts with its component can be named once.
*
* A missing or unresolved owner grants no scoped exemption and emits a
* development warning. Even a registration used only through the unscoped
* registry-wide view should name its logical owner to keep the contract
* explicit.
*/
owner?: ElementSource;
}
Constants
ALL_NESTED_LAYER_ASPECTS from @llui/components/utils/nested-layer
Every aspect — the default for a registration that names none.
const ALL_NESTED_LAYER_ASPECTS: readonly NestedLayerAspect[]
@llui/components/utils/number
Functions
allFiniteNumbers() from @llui/components/utils/number
Whether every number nested in one atomic runtime payload is usable. Non-numeric leaves are ignored; arrays and plain payload objects are walked so callers cannot accidentally validate one coordinate while committing a bad sibling. Cycles are harmless because an already-seen object contains no new numeric leaves.
function allFiniteNumbers(...values: readonly unknown[]): boolean
clamp() from @llui/components/utils/number
Bound n into [min, max]. The result is always FINITE: a non-finite input
maps to a defined legal value instead of being stored verbatim.
Every comparison against NaN is false, so NaN used to fall straight
through to return n and land in state — package-wide, since this is the one
clamp every mutation path routes through (#152). It is not merely a wrong
number: JSON.stringify(NaN) (and Infinity) is null, so a non-finite
value breaks the State-is-JSON-serializable invariant and with it devtools
time-travel, @llui/test replay, agent state snapshots and SSR rehydration.
Rejecting at this boundary is what lets every caller state its own
postcondition — e.g. slider's withThumb — without a finiteness caveat.
function clamp(n: number, min: number, max: number): number
clampToStep() from @llui/components/utils/number
Clamp into the range AND snap onto the grid. The result is always within
[min, max]: snapping can leave the range when an endpoint is not itself on
the grid (min 0, max 10, step 4 → 10 snaps up to 12), and the answer there is
the last grid value INSIDE the range, not an out-of-range or off-grid one.
It is always FINITE too — the clamp rejects a non-finite input first (#152).
function clampToStep(value: number, grid: NumericGrid): number
decimalPlaces() from @llui/components/utils/number
Fraction digits n is written with, INCLUDING exponential notation —
String(1e-7) is '1e-7', which a scan for '.' reads as zero decimals.
function decimalPlaces(n: number): number
finiteBound() from @llui/components/utils/number
A bound as STATE may hold it: the finite number itself, or undefined for
"no bound on this side". THE ONE normalizer for a bound, mirroring clamp's
role for a value (#177).
±Infinity and an ABSENT bound already mean the same thing to every clamp in
the package — clampToStep expands grid.min ?? -Infinity — but only one of
the two spellings survives JSON.stringify, which writes null for both
Infinity and NaN. State must be JSON-serializable (devtools time-travel,
@llui/test replay, agent state snapshots, SSR rehydration all compare
serialized state), so the infinite spelling belongs to the RUNTIME expansion
and never to state: normalize at every write, let the grid expand the absence
again. An unbounded number-input used to store min: -Infinity and
rehydrate as min: null — a number field holding null, on the DEFAULT
configuration.
NaN collapses here too, and that is the half a ?? cannot rescue: NaN is
not nullish, so a NaN bound reached clamp, every comparison against it
was false, and THAT SIDE OF THE RANGE STOPPED CLAMPING — angle-slider after
setMin: NaN stored -9999 for setValue(-9999).
Callers decide what an absent bound means for them, and there are exactly two idioms:
- UNBOUNDED-CAPABLE (
number-input): store theundefinedby OMITTING the key, so the state shape IS aNumericGridand round-trips identically. - INTRINSICALLY BOUNDED (
angle-slider,slider,splitter, …): a requiredmin: numbercannot spell "unbounded", so?? DEFAULTatinitand REJECT the write in asetMin/setMaxreducer — dropping a meaningless bound keeps the range the component already had, which is the only answer that cannot silently disable clamping.
function finiteBound(raw: number | null | undefined): number | undefined
finiteOrDefault() from @llui/components/utils/number
A component-owned number that has no range to clamp into. Initialization
replaces an unusable input with the field's ordinary default; runtime
reducers use {@link allFiniteNumbers} to refuse the whole message instead.
Keeping those two policies here prevents a free position or timestamp from
accidentally inheriting either the grid-value policy (clamp) or the
optional-bound policy (finiteBound).
function finiteOrDefault(raw: number | null | undefined, fallback: number): number
positiveFinite() from @llui/components/utils/number
A finite number strictly greater than zero, or undefined when unusable.
function positiveFinite(raw: number | null | undefined): number | undefined
positiveFiniteOrDefault() from @llui/components/utils/number
A positive finite number, or the field's ordinary initialization default.
function positiveFiniteOrDefault(raw: number | null | undefined, fallback: number): number
snapToStep() from @llui/components/utils/number
Nearest multiple of step from origin. A non-positive step is a no-op.
A non-finite value names no position on the grid, so it snaps to the grid's
own anchor — the same policy clamp applies to the range (#152). This is
unreachable from clampToStep, which clamps first; it keeps the util's
direct consumers on the same rule.
function snapToStep(value: number, step: number, origin = 0): number
stepBy() from @llui/components/utils/number
Move count whole steps (negative to step down), then clamp+snap.
From an OFF-GRID value one call moves to the nearest grid value in the
direction of travel and stops there — that jump is the whole change, however
large count is. This is HTML's stepUp/stepDown (step 3 of the
value-stepping algorithm) and it is what makes increment land on the grid
instead of dragging an off-grid value along forever.
ONE DELIBERATE DIVERGENCE from the spec: HTML's step base falls back min ->
the value CONTENT ATTRIBUTE -> 0; gridOrigin goes min -> 0. A headless
machine has no content attributes — the seed value is just the initial state,
and anchoring the grid on it would make two components with the same
min/max/step disagree about which values are legal depending on where they
happened to start.
function stepBy(value: number, count: number, grid: NumericGrid): number
Interfaces
NumericGrid from @llui/components/utils/number
A stepped numeric range. min/max default to unbounded and step to 0
(= continuous). Component states name their fields the same way, so a state
object can be passed straight in.
export interface NumericGrid {
min?: number
max?: number
step?: number
}
@llui/components/utils/overlay-engine
Functions
createOverlay() from @llui/components/utils/overlay-engine
function createOverlay<S>(opts: OverlayEngineOptions<S>): Mountable
positionerProps() from @llui/components/utils/overlay-engine
Merge a consumer-supplied class into a positioner part bag.
createOverlay BUILDS the positioner div itself (div(opts.positioner, opts.content())), so a consumer styling an overlay had no way to reach it —
the one node in the tree they could not class. That is fine while the opt-in
baseline stylesheet is doing the work (it targets
[data-scope][data-part='positioner'] directly), and a real gap for anyone
styling with utilities instead, who could not put a z-index on the floating
wrapper at all.
Returns base UNCHANGED when no class is supplied, so every existing call
site keeps its exact props object and allocates nothing extra on the overlay
mount path.
function positionerProps(base: ElProps, className: string | undefined): ElProps
Types
OverlayElementReference from @llui/components/utils/overlay-engine
A live DOM relationship resolved when an overlay's interaction phase mounts.
export type OverlayElementReference = { id: string } | { resolve: () => Element | null }
Interfaces
OverlayDismissConfig from @llui/components/utils/overlay-engine
export interface OverlayDismissConfig {
disableEscape?: boolean
disableOutside?: boolean
/** Dismiss boundary element (default: `'content'`). `'floating'` extends the
* boundary to the whole popup (searchable-select's filter input is a sibling
* of content inside the popup). */
boundary?: 'content' | 'floating'
/** Extra side effect after the standard `onDismiss()` — popover refocuses the
* trigger on dismiss. */
extra?: (els: OverlayElements) => void
/**
* Custom Escape router. When provided it runs for the Escape key instead of
* the standard `onDismiss()`, letting the component unwind an internal level
* first — e.g. a menu closes its open submenu before closing the whole menu.
* Return `false` to let Escape propagate (decline); any other return claims it.
*/
onEscape?: (els: OverlayElements, event: KeyboardEvent) => boolean | void
}
OverlayElements from @llui/components/utils/overlay-engine
The live elements resolved for the interaction phase.
export interface OverlayElements {
/** The overlay content element (resolved by `contentId`). */
content: HTMLElement
/** Element used only for floating placement. */
placementAnchor: HTMLElement | null
/** Elements ignored only by outside-dismissal detection. */
dismissIgnore: Element[]
/** Element used only as the explicit focus-return target. */
focusReturnTarget: HTMLElement | null
/** The floating element — the nearest `[data-part="positioner"]` ancestor of
* `content`, or `content` itself when there is no positioner. */
floating: HTMLElement
}
OverlayEngineOptions from @llui/components/utils/overlay-engine
export interface OverlayEngineOptions<S> {
state: Signal<S>
/** Resolved portal host (see `resolvePortalTarget`). */
host: Element | undefined
/** The positioner part props spread onto the wrapping `div`. */
positioner: ElProps
content: () => Renderable
contentId: string
/** Explicit, independent DOM relationships used by this overlay. */
relationships: OverlayRelationships
/** Id resolution strategy. `'scope'` (default) resolves within the node's root
* (shadow-DOM safe); `'document'` uses the global `document` (dialog family). */
idScope?: 'scope' | 'document'
/** Keep the node mounted while this holds (through the exit animation). */
mountWhen: (s: S) => boolean
/** When provided, the interaction phase is wrapped in an inner `show` gated on
* this so it unwinds at the close request while the node lingers. */
visibleWhen?: (s: S) => boolean
/** Fired when the overlay is dismissed (Escape / outside click). */
onDismiss: () => void
/** Fired after the interaction phase has fully unwound, including on dispose. */
onInteractionEnd?: () => void
floating?: OverlayFloatingConfig
dismiss?: OverlayDismissConfig
focusTrap?: OverlayFocusTrapConfig
lockScroll?: boolean
hideSiblings?: boolean
/**
* Whether this overlay registers its live content as a NESTED LAYER while the
* interaction phase is up (see `registerNestedLayer`). Defaults to "this
* overlay is not modal" — `!(focusTrap || hideSiblings)`.
*
* WHY non-modal only. Every one of these overlays portals to a body-level
* sibling, so an overlay opened from inside an open dialog lands OUTSIDE the
* dialog's focus trap and its `inert` sweep. Registering makes Tab reach it and
* keeps it out of the sweep. A MODAL surface must NOT register: it is the layer
* everything else is nested in, and registering it would let a trap on the
* layer beneath Tab into it and would make its own CONTENT read as "inside a
* nested layer" for an overlay open on top of it — so a click anywhere in the
* dialog's panel would stop dismissing an inner `select`. (`content` is the
* element registered below, and it is only the panel: `dialog` renders
* `backdrop`, `positioner` and `content` as three separate parts, so a click
* on the dialog's BACKGROUND is outside the registered element either way.)
*
* The aspects are narrowed further (see below): outside-click cooperation
* between engine overlays comes from the dismissable STACK, not the registry.
*/
nestedLayer?: boolean
/** Element id to focus once the overlay opens. */
focusOnOpenId?: string
/** Select the focused input's existing value (searchable-select prefill). */
focusOnOpenSelect?: boolean
/**
* Optional element-level enter/leave transition (from `@llui/transitions` —
* e.g. `fade({ duration: 150 })`). Threaded onto the OUTER `show(mountWhen)`
* gate — the single show that keeps the popup content in the DOM — so `enter`
* animates the content in when the overlay opens and `leave` defers the final
* unmount until its promise resolves (giving raw-open overlays an exit
* animation for free).
*
* Coordination with the presence (`data-state`) machinery: the JS transition
* and the CSS presence exit are two mechanisms for the SAME job (defer unmount
* for the exit animation), gated on mutually-exclusive status transitions — the
* CSS exit plays on `status: 'closing'`, the JS `leave` fires only when the
* outer gate finally goes false (`status: 'closed'`). With the components'
* default `skipAnimations: true` there is no `'closing'` phase, so a supplied
* transition is the SOLE exit driver — no double-animation, no hang. Supplying
* BOTH a JS transition AND `skipAnimations: false` would run them in sequence
* (CSS then JS); keep `skipAnimations` at its default when driving exits with a
* JS transition.
*/
transition?: TransitionOptions
}
OverlayFloatingConfig from @llui/components/utils/overlay-engine
export interface OverlayFloatingConfig {
placement: Placement
offset: number
flip: boolean
shift: boolean
/** CSS selector (within content) for the arrow element to position. */
arrowSelector?: string
/** Match the floating element's min-width to the anchor's width. */
sameWidth?: boolean
/** Reading direction — a function so it can be peeked at mount time (menu). */
dir?: TextDirection | (() => TextDirection | undefined)
/** Attach positioning in the MOUNT phase (survives the exit animation) rather
* than the interaction phase. Used by popover, whose content stays anchored
* while the close transition plays. */
persistent?: boolean
}
OverlayFocusReturnConfig from @llui/components/utils/overlay-engine
export interface OverlayFocusReturnConfig {
target: OverlayElementReference
/** Boundary used to decide whether focus is still "inside" the overlay at
* teardown (default: `'content'`). */
boundary?: 'content' | 'floating'
/** Also treat the return target itself being focused as "inside" (select). */
allowTargetActive?: boolean
/** Restore during interaction teardown (default true). Popover opts out and
* performs its conditional dismissal-time return in `dismiss.extra`. */
restoreOnTeardown?: boolean
}
OverlayFocusTrapConfig from @llui/components/utils/overlay-engine
export interface OverlayFocusTrapConfig {
initialFocus?: Element | (() => Element | null)
restoreFocus?: boolean
}
OverlayRelationships from @llui/components/utils/overlay-engine
Independent DOM relationships for an overlay. A declaration must opt into each behavior separately: naming a placement anchor never also changes layer ownership, dismissal, or focus return.
export interface OverlayRelationships {
placementAnchor?: OverlayElementReference
nestedLayerOwner?: OverlayElementReference
dismissIgnore?: readonly OverlayElementReference[]
focusReturn?: OverlayFocusReturnConfig
}
@llui/components/utils/portal-target
Functions
resolvePortalTarget() from @llui/components/utils/portal-target
Resolve an overlay's portal target SSR-safely.
An overlay's host is resolved at overlay() build time, which runs on the
SERVER too (SSR renders the whole view). Touching document there throws
ReferenceError: document is not defined. A string selector is therefore
resolved against document ONLY in a browser; on the server we return
undefined and let portal() fall back to the env's doc.body. Overlays are
gated behind show(state.open), so a closed overlay never mounts its portal on
the server anyway — the guard just keeps the eager host resolution from
crashing the SSR render.
function resolvePortalTarget(target: string | Element): Element | undefined
@llui/components/utils/presence-end
Functions
presenceEndHandler() from @llui/components/utils/presence-end
Guard a presence "animation/transition ended" handler so it only advances the
presence machine when the event fired on the element the listener is bound to
(e.target === e.currentTarget) — never on a bubbling descendant.
Overlay content (dialog, popover, menu, toast) reflects its exit phase via
data-state="closing" and stays mounted until an animationend/transitionend
dispatches animationEnd/transitionEnd. Without this guard, ANY descendant
animation or transition ending during the exit — a spinner, a ripple, a child
fade — bubbles up and prematurely unmounts the overlay before its own exit
animation completes.
Mirrors the e.target === el guard the transitions runtime applies in
waitForEnd (@llui/transitions).
function presenceEndHandler<E extends AnimationEvent | TransitionEvent>(handler: (e: E) => void): (e: E) => void
presenceEndProps() from @llui/components/utils/presence-end
Build BOTH end handlers for a part, guarded and tagSend-tagged.
Wiring them one at a time is how four surfaces ended up guarded and five did
not (#126) — dialog/popover/menu/toast remembered the guard while
drawer/hover-card/tooltip/context-menu and presence itself forgot it, and a
descendant animation ending mid-exit unmounted those overlays early. Taking
the pair from one factory makes "guarded" the only thing a caller can build.
transitionMsg defaults to animationMsg for the components that treat the
two events as one message.
function presenceEndProps<M extends { type: string }>(send: (msg: M) => void, animationMsg: M, transitionMsg: M = animationMsg): PresenceEndProps
Interfaces
PresenceEndProps from @llui/components/utils/presence-end
The pair of end handlers a presence-bearing part spreads.
export interface PresenceEndProps {
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
@llui/components/utils/remove-scroll
Functions
_scrollLockCount() from @llui/components/utils/remove-scroll
@internal — tests only
export declare function _scrollLockCount(): number;
lockBodyScroll() from @llui/components/utils/remove-scroll
Lock body scroll while an overlay is open, preserving scrollbar width to avoid layout shift. Reference-counted so nested locks compose cleanly.
export declare function lockBodyScroll(): () => void;
@llui/components/utils/root-scope
Functions
getElementByIdInScope() from @llui/components/utils/root-scope
Resolve an element by id within the DOM tree that ref belongs to.
ref.getRootNode() returns the enclosing Document in light DOM, or the
ShadowRoot when ref lives inside a shadow tree — both expose
getElementById. Overlays resolve their trigger/content parts through this
(passing the onMount root, which shares the parts' tree) so floating
positioning still anchors when the component is mounted inside a shadow root
(isolate mode): the global document.getElementById cannot see into shadow
trees and silently returns null, which no-ops the anchor.
Light-DOM behavior is identical to document.getElementById: a node attached
to the main document roots to that Document. A detached ref (its root is a
bare element/fragment without getElementById) falls back to the global
document so callers keep working.
function getElementByIdInScope(ref: Node, id: string): HTMLElement | null
@llui/components/utils/roving
Functions
firstEnabled() from @llui/components/utils/roving
Internal value navigation used by the public roving-focus primitive.
export declare function firstEnabled(items: readonly string[], disabled: readonly string[]): string | null;
focusRovingItem() from @llui/components/utils/roving
Move DOM focus to the roving item identified by value within the same
widget instance as origin.
Roving-tabindex widgets track the active index in STATE, but assistive tech
follows real DOM focus — so after a keyboard move the handler MUST also move
focus, or arrow keys are silent for AT. origin is the event's
currentTarget (the item that received the key); its closest
[data-scope][data-part="root"] ancestor scopes the search so sibling
widgets of the same scope never cross-focus. No-op if nothing matches.
send() is synchronous and items already exist in the DOM, so this can be
called immediately after the navigation send.
export declare function focusRovingItem(origin: Element | null, scope: string, value: string, opts?: {
itemPart?: string;
attr?: string;
}): void;
focusRovingTab() from @llui/components/utils/roving
Move DOM focus to the trigger whose data-value matches, within
container. Relies only on the role="tab" + data-value contract
(shared by components/tabs and any hand-rolled tablist). No-op when no
trigger matches. Call after the DOM reflects the new active tab (e.g. in
a microtask if activation triggers a re-render).
export declare function focusRovingTab(container: Element, value: string): void;
lastEnabled() from @llui/components/utils/roving
export declare function lastEnabled(items: readonly string[], disabled: readonly string[]): string | null;
nextEnabled() from @llui/components/utils/roving
export declare function nextEnabled(items: readonly string[], disabled: readonly string[], from: string, delta: 1 | -1, loop: boolean): string | null;
resolveRovingMove() from @llui/components/utils/roving
Map a keyboard key + the current tab value to a roving-tablist move,
or null when the key isn't a navigation/activation key or the move is
a no-op (empty list, no enabled sibling). Pure — does not touch the DOM
or call preventDefault; the caller decides (typically: prevent default
iff the result is non-null).
export declare function resolveRovingMove(key: string, current: string, items: readonly RovingItem[], opts?: RovingOptions): RovingMove | null;
Types
RovingMove from @llui/components/utils/roving
The navigation a key implies on a roving tablist.
export type RovingMove =
/** An arrow / Home / End resolved to a (different, enabled) tab value. */
{
type: 'focus';
value: string;
}
/** Enter or Space — activate the currently focused tab (manual mode). */
| {
type: 'activate';
};
RovingOrientation from @llui/components/utils/roving
Headless roving-tablist navigation — the keyboard logic of a WAI-ARIA tablist, decoupled from any particular DOM contract.
components/tabs.ts builds its reactive part-bags on top of this; a
consumer that wants its OWN markup (different classes, ids, no
data-scope/data-part) can drive the same keyboard behaviour by
calling resolveRovingMove from its trigger's onKeyDown and
focusRovingTab to move DOM focus — without adopting the component's
markup or its connect() state machine.
The resolver is pure (key + current value + items → a move); the only
shared DOM assumption lives in focusRovingTab, and it is the minimal
one both surfaces already satisfy: triggers carry role="tab" and
data-value="<value>".
The list walk itself lives in list-navigation.ts — this module is the
keyboard + DOM-focus surface over it, nothing more.
export type RovingOrientation = 'horizontal' | 'vertical';
Interfaces
RovingItem from @llui/components/utils/roving
export interface RovingItem {
value: string;
/** Disabled items are skipped by arrow/Home/End navigation. */
disabled?: boolean;
}
RovingOptions from @llui/components/utils/roving
export interface RovingOptions {
/** Arrow axis — 'horizontal' uses Left/Right, 'vertical' uses Up/Down. Default 'horizontal'. */
orientation?: RovingOrientation;
/** Whether arrow navigation wraps at the ends. Default true. */
loop?: boolean;
/**
* An element used to resolve text direction for RTL arrow flipping
* (typically the event's `currentTarget`). When it resolves to
* `dir="rtl"`, ArrowLeft/ArrowRight swap. Optional.
*/
element?: Element | null;
}
@llui/components/utils/tree-collection
Interfaces
TreeNode from @llui/components/utils/tree-collection
TreeCollection — helper for building tree-view payloads from a nested data structure. Tree-view's state machine only knows about flat lists (visibleItems, visibleLabels) and opaque ids; the collection owns the structure and derives those flat arrays on demand.
Typical flow:
const col = new TreeCollection(data) state.visibleItems = col.visibleItems(state.expanded) state.visibleLabels = col.visibleLabels(state.expanded)
After every expand / collapse message, the consumer dispatches
setVisibleItems with the updated arrays. The collection itself is
immutable — build a new one when the tree structure changes.
export interface TreeNode {
id: string
label?: string
disabled?: boolean
children?: TreeNode[]
}
Classes
TreeCollection from @llui/components/utils/tree-collection
class TreeCollection {
roots: TreeNode[]
info: Map<string, NodeInfo>
constructor(roots: TreeNode | TreeNode[])
index(): void
getNode(id: string): TreeNode | null
getLabel(id: string): string
getParent(id: string): string | null
getDepth(id: string): number
getChildren(id: string): string[]
getDescendants(id: string): string[]
isBranch(id: string): boolean
isDisabled(id: string): boolean
visibleItems(expanded: string[]): string[]
visibleLabels(expanded: string[]): string[]
computeIndeterminate(checked: Set<string>): string[]
}
@llui/components/utils/typeahead
Functions
isTypeaheadKey() from @llui/components/utils/typeahead
Returns true if the key event should trigger a typeahead query — i.e., a
single printable character that isn't a modified keyboard shortcut. Use
this in onKeyDown handlers to decide whether to dispatch a typeahead
message.
function isTypeaheadKey(e: KeyboardEvent): boolean
typeaheadAccumulate() from @llui/components/utils/typeahead
Advance the typeahead query based on a new keystroke and the previous
expiration time. Returns the new query string; callers combine this with
typeaheadMatch() to produce a new highlight index.
function typeaheadAccumulate(prev: string, char: string, now: number, expiresAt: number): string
typeaheadMatch() from @llui/components/utils/typeahead
Find the first enabled item whose label starts with the query
(case-insensitive). labels and disabledMask are parallel arrays.
startFrom is the current highlighted index; for single-character
queries the search begins at startFrom + 1 (so repeated "s" keys
cycle), for multi-character queries it begins at startFrom (inclusive).
Returns the matching index, or null if no enabled item matches.
function typeaheadMatch(labels: string[], disabledMask: boolean[], query: string, startFrom: number | null): number | null
typeaheadMatchByItems() from @llui/components/utils/typeahead
Convenience: pass a disabled list of values instead of a boolean mask.
Builds the mask by checking membership via === on the raw string values.
function typeaheadMatchByItems(items: string[], disabled: readonly string[], query: string, startFrom: number | null): number | null
Constants
TYPEAHEAD_TIMEOUT_MS from @llui/components/utils/typeahead
Typeahead search — accumulates keystrokes into a query while the user types rapidly, then matches the first item whose label starts with the query. Used by listbox, menu, select, combobox, tree-view to support WAI-ARIA keyboard navigation patterns.
Behavior:
- If a keystroke arrives within
TYPEAHEAD_TIMEOUT_MSof the previous one, append to the existing query (so typing "sa" finds "Saturn" even if the highlight is currently on "Jupiter"). - Otherwise, start a fresh single-character query.
- Single-character queries advance past the current position (jump to the next item starting with that letter), which is the standard WAI-ARIA behavior — rapid repeated presses of "s" cycle through items beginning with "s".
- Multi-character queries search from the current cursor position (inclusive) so if the cursor is already on a matching item, it stays — typing "ap" while on "apricot" keeps focus on "apricot".
const TYPEAHEAD_TIMEOUT_MS
@llui/components/format/cache
Functions
cached() from @llui/components/format/cache
function cached<T>(key: string, create: () => T): T
cacheKey() from @llui/components/format/cache
function cacheKey(prefix: string, locale: string, opts: Record<string, unknown>): string
@llui/components/format/defaults
Functions
defaultLocale() from @llui/components/format/defaults
Default locale: navigator.language in browsers, 'en' in SSR/tests.
function defaultLocale(): string
@llui/components/format/format-date
Functions
formatDate() from @llui/components/format/format-date
function formatDate(value: DateValue, opts: FormatDateOptions = {}): string
formatDateTime() from @llui/components/format/format-date
function formatDateTime(value: DateValue, opts: FormatDateTimeOptions = {}): string
formatTime() from @llui/components/format/format-date
function formatTime(value: DateValue, opts: FormatTimeOptions = {}): string
Types
DateValue from @llui/components/format/format-date
Date-only handling — the ONE place that decides whether a value denotes an INSTANT or a bare CALENDAR DATE.
new Date('2026-01-15') parses a date-only string as UTC midnight (ES spec),
so formatting it against the ambient zone renders the PREVIOUS day everywhere
west of UTC: formatDate('2026-01-15') printed "January 14, 2026" under
America/New_York and the right answer under Europe/Rome, which is why it
survived (#125 defect 4).
A calendar date carries no instant and therefore no zone, so it is anchored
at UTC midnight and its consumers must render it in UTC — see dateOnly.
export type DateValue = Date | string | number
Interfaces
FormatDateOptions from @llui/components/format/format-date
export interface FormatDateOptions {
locale?: string
dateStyle?: DateStyle
calendar?: string
numberingSystem?: string
timeZone?: string
weekday?: 'long' | 'short' | 'narrow'
year?: 'numeric' | '2-digit'
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'
day?: 'numeric' | '2-digit'
era?: 'long' | 'short' | 'narrow'
}
FormatDateTimeOptions from @llui/components/format/format-date
export interface FormatDateTimeOptions {
locale?: string
dateStyle?: DateStyle
timeStyle?: DateStyle
timeZone?: string
calendar?: string
hour12?: boolean
hourCycle?: 'h11' | 'h12' | 'h23' | 'h24'
}
FormatTimeOptions from @llui/components/format/format-date
export interface FormatTimeOptions {
locale?: string
timeStyle?: DateStyle
timeZone?: string
hour12?: boolean
hourCycle?: 'h11' | 'h12' | 'h23' | 'h24'
hour?: 'numeric' | '2-digit'
minute?: 'numeric' | '2-digit'
second?: 'numeric' | '2-digit'
fractionalSecondDigits?: 0 | 1 | 2 | 3
timeZoneName?: 'long' | 'short' | 'shortOffset' | 'longOffset' | 'shortGeneric' | 'longGeneric'
dayPeriod?: 'narrow' | 'short' | 'long'
}
@llui/components/format/format-display-name
Functions
formatDisplayName() from @llui/components/format/format-display-name
function formatDisplayName(value: string, type: DisplayNameType, opts: FormatDisplayNameOptions = {}): string | undefined
Types
DisplayNameType from @llui/components/format/format-display-name
export type DisplayNameType =
| 'language'
| 'region'
| 'script'
| 'currency'
| 'calendar'
| 'dateTimeField'
Interfaces
FormatDisplayNameOptions from @llui/components/format/format-display-name
export interface FormatDisplayNameOptions {
locale?: string
style?: 'long' | 'short' | 'narrow'
languageDisplay?: 'dialect' | 'standard'
fallback?: 'code' | 'none'
}
@llui/components/format/format-file-size
Functions
formatFileSize() from @llui/components/format/format-file-size
function formatFileSize(value: number | bigint, opts: FormatFileSizeOptions = {}): string
Interfaces
FormatFileSizeOptions from @llui/components/format/format-file-size
export interface FormatFileSizeOptions {
locale?: string
units?: string[]
decimalPlaces?: number
}
@llui/components/format/format-list
Functions
formatList() from @llui/components/format/format-list
function formatList(value: string[], opts: FormatListOptions = {}): string
Interfaces
FormatListOptions from @llui/components/format/format-list
export interface FormatListOptions {
locale?: string
type?: 'conjunction' | 'disjunction' | 'unit'
style?: 'long' | 'short' | 'narrow'
}
@llui/components/format/format-number
Functions
formatNumber() from @llui/components/format/format-number
function formatNumber(value: number, opts: FormatNumberOptions = {}): string
Interfaces
FormatNumberOptions from @llui/components/format/format-number
export interface FormatNumberOptions {
locale?: string
style?: 'decimal' | 'currency' | 'percent' | 'unit'
currency?: string
currencyDisplay?: 'symbol' | 'narrowSymbol' | 'code' | 'name'
signDisplay?: 'auto' | 'never' | 'always' | 'exceptZero'
notation?: 'standard' | 'scientific' | 'engineering' | 'compact'
compactDisplay?: 'short' | 'long'
unit?: string
unitDisplay?: 'short' | 'long' | 'narrow'
useGrouping?: boolean
minimumIntegerDigits?: number
minimumFractionDigits?: number
maximumFractionDigits?: number
minimumSignificantDigits?: number
maximumSignificantDigits?: number
}
@llui/components/format/format-plural
Functions
formatPlural() from @llui/components/format/format-plural
function formatPlural(value: number, messages: PluralMessages, opts: FormatPluralOptions = {}): string
resolvePluralCategory() from @llui/components/format/format-plural
function resolvePluralCategory(value: number, opts: FormatPluralOptions = {}): PluralCategory
Types
PluralCategory from @llui/components/format/format-plural
export type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other'
PluralMessages from @llui/components/format/format-plural
export type PluralMessages = Partial<Record<PluralCategory, string>> & { other: string }
Interfaces
FormatPluralOptions from @llui/components/format/format-plural
export interface FormatPluralOptions {
locale?: string
type?: 'cardinal' | 'ordinal'
minimumIntegerDigits?: number
minimumFractionDigits?: number
maximumFractionDigits?: number
minimumSignificantDigits?: number
maximumSignificantDigits?: number
}
@llui/components/format/format-relative-time
Functions
formatRelativeTime() from @llui/components/format/format-relative-time
function formatRelativeTime(value: number, unit: RelativeTimeUnit, opts: FormatRelativeTimeOptions = {}): string
Types
RelativeTimeUnit from @llui/components/format/format-relative-time
export type RelativeTimeUnit =
| 'year'
| 'quarter'
| 'month'
| 'week'
| 'day'
| 'hour'
| 'minute'
| 'second'
Interfaces
FormatRelativeTimeOptions from @llui/components/format/format-relative-time
export interface FormatRelativeTimeOptions {
locale?: string
numeric?: 'always' | 'auto'
style?: 'long' | 'short' | 'narrow'
}
@llui/components/format/index
Functions
formatDate() from @llui/components/format/index
function formatDate(value: DateValue, opts: FormatDateOptions = {}): string
formatDateTime() from @llui/components/format/index
function formatDateTime(value: DateValue, opts: FormatDateTimeOptions = {}): string
formatDisplayName() from @llui/components/format/index
function formatDisplayName(value: string, type: DisplayNameType, opts: FormatDisplayNameOptions = {}): string | undefined
formatFileSize() from @llui/components/format/index
function formatFileSize(value: number | bigint, opts: FormatFileSizeOptions = {}): string
formatList() from @llui/components/format/index
function formatList(value: string[], opts: FormatListOptions = {}): string
formatNumber() from @llui/components/format/index
function formatNumber(value: number, opts: FormatNumberOptions = {}): string
formatPlural() from @llui/components/format/index
function formatPlural(value: number, messages: PluralMessages, opts: FormatPluralOptions = {}): string
formatRelativeTime() from @llui/components/format/index
function formatRelativeTime(value: number, unit: RelativeTimeUnit, opts: FormatRelativeTimeOptions = {}): string
formatTime() from @llui/components/format/index
function formatTime(value: DateValue, opts: FormatTimeOptions = {}): string
resolvePluralCategory() from @llui/components/format/index
function resolvePluralCategory(value: number, opts: FormatPluralOptions = {}): PluralCategory
Types
DateValue from @llui/components/format/index
Date-only handling — the ONE place that decides whether a value denotes an INSTANT or a bare CALENDAR DATE.
new Date('2026-01-15') parses a date-only string as UTC midnight (ES spec),
so formatting it against the ambient zone renders the PREVIOUS day everywhere
west of UTC: formatDate('2026-01-15') printed "January 14, 2026" under
America/New_York and the right answer under Europe/Rome, which is why it
survived (#125 defect 4).
A calendar date carries no instant and therefore no zone, so it is anchored
at UTC midnight and its consumers must render it in UTC — see dateOnly.
export type DateValue = Date | string | number
DisplayNameType from @llui/components/format/index
export type DisplayNameType =
| 'language'
| 'region'
| 'script'
| 'currency'
| 'calendar'
| 'dateTimeField'
PluralCategory from @llui/components/format/index
export type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other'
PluralMessages from @llui/components/format/index
export type PluralMessages = Partial<Record<PluralCategory, string>> & { other: string }
RelativeTimeUnit from @llui/components/format/index
export type RelativeTimeUnit =
| 'year'
| 'quarter'
| 'month'
| 'week'
| 'day'
| 'hour'
| 'minute'
| 'second'
Interfaces
FormatDateOptions from @llui/components/format/index
export interface FormatDateOptions {
locale?: string
dateStyle?: DateStyle
calendar?: string
numberingSystem?: string
timeZone?: string
weekday?: 'long' | 'short' | 'narrow'
year?: 'numeric' | '2-digit'
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'
day?: 'numeric' | '2-digit'
era?: 'long' | 'short' | 'narrow'
}
FormatDateTimeOptions from @llui/components/format/index
export interface FormatDateTimeOptions {
locale?: string
dateStyle?: DateStyle
timeStyle?: DateStyle
timeZone?: string
calendar?: string
hour12?: boolean
hourCycle?: 'h11' | 'h12' | 'h23' | 'h24'
}
FormatDisplayNameOptions from @llui/components/format/index
export interface FormatDisplayNameOptions {
locale?: string
style?: 'long' | 'short' | 'narrow'
languageDisplay?: 'dialect' | 'standard'
fallback?: 'code' | 'none'
}
FormatFileSizeOptions from @llui/components/format/index
export interface FormatFileSizeOptions {
locale?: string
units?: string[]
decimalPlaces?: number
}
FormatListOptions from @llui/components/format/index
export interface FormatListOptions {
locale?: string
type?: 'conjunction' | 'disjunction' | 'unit'
style?: 'long' | 'short' | 'narrow'
}
FormatNumberOptions from @llui/components/format/index
export interface FormatNumberOptions {
locale?: string
style?: 'decimal' | 'currency' | 'percent' | 'unit'
currency?: string
currencyDisplay?: 'symbol' | 'narrowSymbol' | 'code' | 'name'
signDisplay?: 'auto' | 'never' | 'always' | 'exceptZero'
notation?: 'standard' | 'scientific' | 'engineering' | 'compact'
compactDisplay?: 'short' | 'long'
unit?: string
unitDisplay?: 'short' | 'long' | 'narrow'
useGrouping?: boolean
minimumIntegerDigits?: number
minimumFractionDigits?: number
maximumFractionDigits?: number
minimumSignificantDigits?: number
maximumSignificantDigits?: number
}
FormatPluralOptions from @llui/components/format/index
export interface FormatPluralOptions {
locale?: string
type?: 'cardinal' | 'ordinal'
minimumIntegerDigits?: number
minimumFractionDigits?: number
maximumFractionDigits?: number
minimumSignificantDigits?: number
maximumSignificantDigits?: number
}
FormatRelativeTimeOptions from @llui/components/format/index
export interface FormatRelativeTimeOptions {
locale?: string
numeric?: 'always' | 'auto'
style?: 'long' | 'short' | 'narrow'
}
FormatTimeOptions from @llui/components/format/index
export interface FormatTimeOptions {
locale?: string
timeStyle?: DateStyle
timeZone?: string
hour12?: boolean
hourCycle?: 'h11' | 'h12' | 'h23' | 'h24'
hour?: 'numeric' | '2-digit'
minute?: 'numeric' | '2-digit'
second?: 'numeric' | '2-digit'
fractionalSecondDigits?: 0 | 1 | 2 | 3
timeZoneName?: 'long' | 'short' | 'shortOffset' | 'longOffset' | 'shortGeneric' | 'longGeneric'
dayPeriod?: 'narrow' | 'short' | 'long'
}
@llui/components/toggle
Functions
connect() from @llui/components/toggle
function connect(state: Signal<ToggleState>, send: Send<ToggleMsg>): ToggleParts
init() from @llui/components/toggle
function init(opts: ToggleInit = {}): ToggleState
update() from @llui/components/toggle
function update(state: ToggleState, msg: ToggleMsg): [ToggleState, never[]]
Types
ToggleMsg from @llui/components/toggle
export type ToggleMsg =
/** @intent("Flip the toggle button's pressed state") */
| { type: 'toggle' }
/** @intent("Set the toggle's pressed state to a specific value") */
| { type: 'setPressed'; pressed: boolean }
/** @humanOnly */
| { type: 'setDisabled'; disabled: boolean }
Interfaces
ToggleInit from @llui/components/toggle
export interface ToggleInit {
pressed?: boolean
disabled?: boolean
}
ToggleParts from @llui/components/toggle
export interface ToggleParts {
root: {
type: 'button'
role: 'button'
'aria-pressed': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-state': Signal<'on' | 'off'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'toggle'
'data-part': 'root'
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
}
ToggleState from @llui/components/toggle
Toggle button — a button that can be pressed or not. Unlike a checkbox, a toggle represents an action that is applied immediately (e.g. "bold" in a text editor toolbar).
export interface ToggleState {
pressed: boolean
disabled: boolean
}
Constants
toggle from @llui/components/toggle
const toggle
@llui/components/checkbox
Functions
connect() from @llui/components/checkbox
function connect(state: Signal<CheckboxState>, send: Send<CheckboxMsg>): CheckboxParts
init() from @llui/components/checkbox
function init(opts: CheckboxInit = {}): CheckboxState
update() from @llui/components/checkbox
function update(state: CheckboxState, msg: CheckboxMsg): [CheckboxState, never[]]
Types
CheckboxMsg from @llui/components/checkbox
export type CheckboxMsg =
/** @intent("Toggle the checkbox between checked and unchecked") */
| { type: 'toggle' }
/** @intent("Set the checkbox state to checked, unchecked, or indeterminate") */
| { type: 'setChecked'; checked: CheckedState }
/** @humanOnly */
| { type: 'setDisabled'; disabled: boolean }
CheckedState from @llui/components/checkbox
Checkbox — a tri-state form control (checked / unchecked / indeterminate).
The indeterminate state is a visual-only state used to represent "partial"
selection (e.g. a parent whose children are mixed checked).
Rendering typically uses two elements: a visual indicator (the styled box)
and a hidden native <input type="checkbox"> for form participation +
accessibility. connect() returns props for both.
export type CheckedState = boolean | 'indeterminate'
Interfaces
CheckboxInit from @llui/components/checkbox
export interface CheckboxInit {
checked?: CheckedState
disabled?: boolean
required?: boolean
}
CheckboxParts from @llui/components/checkbox
export interface CheckboxParts {
/** The visual box/container — `role="checkbox"` for accessibility. */
root: {
role: 'checkbox'
'aria-checked': Signal<'true' | 'false' | 'mixed'>
'aria-disabled': Signal<'true' | undefined>
'aria-required': Signal<'true' | undefined>
'data-state': Signal<'checked' | 'unchecked' | 'indeterminate'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'checkbox'
'data-part': 'root'
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
/** A native hidden input for form participation. */
hiddenInput: {
type: 'checkbox'
'aria-hidden': 'true'
tabindex: -1
style: string
checked: Signal<boolean>
indeterminate: Signal<boolean>
disabled: Signal<boolean>
required: Signal<boolean>
'data-scope': 'checkbox'
'data-part': 'hidden-input'
}
/** Optional indicator child (the checkmark). */
indicator: {
'data-state': Signal<'checked' | 'unchecked' | 'indeterminate'>
'data-scope': 'checkbox'
'data-part': 'indicator'
}
}
CheckboxState from @llui/components/checkbox
export interface CheckboxState {
checked: CheckedState
disabled: boolean
required: boolean
}
Constants
checkbox from @llui/components/checkbox
const checkbox
@llui/components/accordion
Functions
connect() from @llui/components/accordion
function connect(state: Signal<AccordionState>, send: Send<AccordionMsg>, opts: ConnectOptions): AccordionParts
focusTarget() from @llui/components/accordion
Helper: compute the next/prev item value given a focus message + current state. Users' view/onMount can use this to move DOM focus to the correct trigger.
function focusTarget(state: AccordionState, msg: Extract<AccordionMsg, { type: `focus${string}` }>): string | null
init() from @llui/components/accordion
function init(opts: AccordionInit = {}): AccordionState
update() from @llui/components/accordion
function update(state: AccordionState, msg: AccordionMsg): [AccordionState, never[]]
Types
AccordionMsg from @llui/components/accordion
export type AccordionMsg =
/** @intent("Toggle the named accordion item open/closed") */
| { type: 'toggle'; value: string }
/** @intent("Open the named accordion item") */
| { type: 'open'; value: string }
/** @intent("Close the named accordion item") */
| { type: 'close'; value: string }
/** @intent("Replace the set of currently-open items with the provided values") */
| { type: 'setValue'; value: string[] }
/** @humanOnly */
| { type: 'setItems'; items: string[] }
/** @humanOnly */
| { type: 'focusNext'; value: string }
/** @humanOnly */
| { type: 'focusPrev'; value: string }
/** @humanOnly */
| { type: 'focusFirst' }
/** @humanOnly */
| { type: 'focusLast' }
Interfaces
AccordionInit from @llui/components/accordion
export interface AccordionInit {
value?: string[]
multiple?: boolean
collapsible?: boolean
disabled?: boolean
items?: string[]
}
AccordionItemParts from @llui/components/accordion
export interface AccordionItemParts {
trigger: {
type: 'button'
'aria-expanded': Signal<boolean>
'aria-controls': string
id: string
'data-state': Signal<'open' | 'closed'>
'data-disabled': Signal<'' | undefined>
disabled: Signal<boolean>
'data-scope': 'accordion'
'data-part': 'trigger'
'data-value': string
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
content: {
role: 'region'
id: string
'aria-labelledby': string
'data-state': Signal<'open' | 'closed'>
'data-scope': 'accordion'
'data-part': 'content'
hidden: Signal<boolean>
}
item: {
'data-state': Signal<'open' | 'closed'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'accordion'
'data-part': 'item'
'data-value': string
}
}
AccordionParts from @llui/components/accordion
export interface AccordionParts {
root: {
// No role on the root: an accordion is a set of disclosure buttons, and
// labelling the whole container `region` (without an accessible name) just
// adds an unlabeled landmark. The meaningful regions are the per-item
// panels, each a `role="region"` with `aria-labelledby` its trigger.
'data-scope': 'accordion'
'data-part': 'root'
'data-orientation': 'vertical'
}
item: (value: string) => AccordionItemParts
}
AccordionState from @llui/components/accordion
Accordion — a stack of expandable panels. Items are identified by a string
value. Either a single item is expandable at a time (default) or many
(multiple: true). collapsible: false prevents closing the only open
item in single mode.
Items themselves are provided by the user's view (accordion is agnostic to
item data). The connect() API returns a root prop set and an item(value)
factory that produces trigger and content prop sets scoped to that item.
export interface AccordionState {
/** Values of currently-expanded items. */
value: string[]
multiple: boolean
collapsible: boolean
disabled: boolean
/** Ordered list of item values (for keyboard navigation). */
items: string[]
}
ConnectOptions from @llui/components/accordion
export interface ConnectOptions {
/** Namespace prefix for part ids (for ARIA wiring). Should be unique per instance. */
id: string
}
Constants
accordion from @llui/components/accordion
const accordion
@llui/components/tabs
Functions
connect() from @llui/components/tabs
function connect(state: Signal<TabsState>, send: Send<TabsMsg>, opts: ConnectOptions): TabsParts
init() from @llui/components/tabs
function init(opts: TabsInit = {}): TabsState
update() from @llui/components/tabs
function update(state: TabsState, msg: TabsMsg): [TabsState, never[]]
watchTabIndicator() from @llui/components/tabs
Track the active tab trigger and update CSS custom properties on the
indicator element so it can be animated into position. Call from
onMount with the tabs root element; the returned function removes
the observers.
Sets --indicator-left, --indicator-top, --indicator-width,
--indicator-height on the indicator element every time the active
trigger changes or the list resizes. Style the indicator with:
transform: translate(var(--indicator-left), var(--indicator-top));
width: var(--indicator-width);
height: var(--indicator-height);
function watchTabIndicator(root: HTMLElement): () => void
Types
Activation from @llui/components/tabs
export type Activation = 'automatic' | 'manual'
Orientation from @llui/components/tabs
Tabs — tabbed interface with keyboard navigation. Each tab has a value (string) that identifies both the trigger and the associated panel.
Two activation modes:
'automatic'(default): focusing a trigger also activates it.'manual': arrow keys move focus without activating; Enter/Space activates.
export type Orientation = 'horizontal' | 'vertical'
TabsMsg from @llui/components/tabs
export type TabsMsg =
/** @intent("Switch to the tab with the given value") */
| { type: 'setValue'; value: string }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @humanOnly */
| { type: 'focusTab'; value: string }
/**
* @intent("Activate the tab with the given value — the click/press action (deselects it when `deselectable` and it is already active)")
*/
| { type: 'activateTab'; value: string }
/** @humanOnly */
| { type: 'focusNext'; from: string }
/** @humanOnly */
| { type: 'focusPrev'; from: string }
/** @humanOnly */
| { type: 'focusFirst' }
/** @humanOnly */
| { type: 'focusLast' }
/** @intent("Activate the currently-focused tab (for manual activation mode)") */
| { type: 'activateFocused' }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
Interfaces
ConnectOptions from @llui/components/tabs
export interface ConnectOptions {
id: string
/**
* Called whenever a tab is clicked/activated. Useful for anchor-style
* navigation where the tab's value is a URL path and you want to push
* to the history or router.
*/
onNavigate?: (value: string) => void
}
TabsInit from @llui/components/tabs
export interface TabsInit {
value?: string
items?: string[]
disabledItems?: string[]
orientation?: Orientation
activation?: Activation
loopFocus?: boolean
deselectable?: boolean
dir?: 'ltr' | 'rtl'
}
TabsItemParts from @llui/components/tabs
export interface TabsItemParts {
trigger: {
type: 'button'
role: 'tab'
'aria-selected': Signal<boolean>
'aria-controls': string
'aria-disabled': Signal<'true' | undefined>
id: string
'data-state': Signal<'active' | 'inactive'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'tabs'
'data-part': 'trigger'
'data-value': string
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
}
panel: {
role: 'tabpanel'
id: string
'aria-labelledby': string
tabindex: 0
hidden: Signal<boolean>
'data-state': Signal<'active' | 'inactive'>
'data-scope': 'tabs'
'data-part': 'panel'
'data-value': string
}
}
TabsParts from @llui/components/tabs
export interface TabsParts {
root: {
'data-scope': 'tabs'
'data-part': 'root'
'data-orientation': Signal<Orientation>
}
/**
* A movable underline/highlight element. Position tracks the active
* trigger via CSS custom properties written by `watchTabIndicator()`:
* `--indicator-left`, `--indicator-top`, `--indicator-width`,
* `--indicator-height` — all in pixels.
* The consumer styles the indicator using these properties (e.g.
* `transform: translateX(var(--indicator-left))`).
*/
indicator: {
'data-scope': 'tabs'
'data-part': 'indicator'
'data-orientation': Signal<Orientation>
}
list: {
role: 'tablist'
'aria-orientation': Signal<Orientation>
'data-scope': 'tabs'
'data-part': 'list'
}
item: (value: string) => TabsItemParts
}
TabsState from @llui/components/tabs
export interface TabsState {
value: string
items: string[]
disabledItems: string[]
orientation: Orientation
activation: Activation
/** The currently focused (but not necessarily active) tab. For manual mode. */
focused: string | null
/** Whether Arrow navigation wraps at the ends of the tab list. Default: true. */
loopFocus: boolean
/** Whether clicking the active tab deselects it (empty value). Default: false. */
deselectable: boolean
/** Reading direction. Under 'rtl', ArrowLeft/ArrowRight swap meaning. */
dir: 'ltr' | 'rtl'
}
Constants
tabs from @llui/components/tabs
const tabs
@llui/components/slider
Functions
closestThumbIndex() from @llui/components/slider
Determine which thumb index is closest to a given raw value.
function closestThumbIndex(state: SliderState, raw: number): number
connect() from @llui/components/slider
function connect(state: Signal<SliderState>, send: Send<SliderMsg>): SliderParts
init() from @llui/components/slider
function init(opts: SliderInit = {}): SliderState
update() from @llui/components/slider
function update(state: SliderState, msg: SliderMsg): [SliderState, never[]]
valueFromPoint() from @llui/components/slider
Compute the slider value at a given pointer position within the control's bounding rect. Returns null if the pointer is outside the track.
function valueFromPoint(state: SliderState, rect: DOMRect, clientX: number, clientY: number): number
Types
Orientation from @llui/components/slider
Slider — numeric input controlled by drag or keyboard. Supports multiple
thumbs (range slider) and horizontal/vertical orientations. The machine is
pure; pointer drag handling (pointermove listeners during a drag) is done
by the consumer via startThumbDrag() helper which returns a cleanup.
export type Orientation = 'horizontal' | 'vertical'
SliderMsg from @llui/components/slider
export type SliderMsg =
/** @intent("Replace all thumb values at once") */
| { type: 'setValue'; value: number[] }
/** @intent("Set the value of the thumb at the given index. Ignored while disabled") */
| { type: 'setThumb'; index: number; value: number }
/** @intent("Move the thumb at the given index up by one step (or step × multiplier). Ignored while disabled") */
| { type: 'increment'; index: number; multiplier?: number }
/** @intent("Move the thumb at the given index down by one step (or step × multiplier). Ignored while disabled") */
| { type: 'decrement'; index: number; multiplier?: number }
/** @intent("Snap the thumb at the given index to the slider's minimum. Ignored while disabled") */
| { type: 'toMin'; index: number }
/** @intent("Snap the thumb at the given index to the slider's maximum. Ignored while disabled") */
| { type: 'toMax'; index: number }
/** @intent("Enable or disable the slider — a host/agent write, never gated") */
| { type: 'setDisabled'; disabled: boolean }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
Interfaces
SliderInit from @llui/components/slider
export interface SliderInit {
value?: number[]
min?: number
max?: number
step?: number
disabled?: boolean
orientation?: Orientation
minStepsBetweenThumbs?: number
dir?: 'ltr' | 'rtl'
}
SliderParts from @llui/components/slider
export interface SliderParts {
root: {
'data-scope': 'slider'
'data-part': 'root'
'data-orientation': Signal<Orientation>
'data-disabled': Signal<'' | undefined>
}
control: {
'data-scope': 'slider'
'data-part': 'control'
'data-orientation': Signal<Orientation>
onPointerDown: (e: PointerEvent) => void
}
track: {
'data-scope': 'slider'
'data-part': 'track'
'data-orientation': Signal<Orientation>
}
range: {
'data-scope': 'slider'
'data-part': 'range'
'data-orientation': Signal<Orientation>
style: Signal<string>
}
thumb: (index: number) => SliderThumbParts
/** Current raw values — reactive convenience. */
value: Signal<number[]>
}
SliderState from @llui/components/slider
export interface SliderState {
/** One value per thumb. For a single-value slider, a one-element array. */
value: number[]
min: number
max: number
step: number
disabled: boolean
orientation: Orientation
/** Minimum gap enforced between adjacent thumbs (range slider). */
minStepsBetweenThumbs: number
/** Reading direction. Under 'rtl' horizontal arrow keys are flipped. */
dir: 'ltr' | 'rtl'
}
SliderThumbParts from @llui/components/slider
export interface SliderThumbParts {
thumb: {
role: 'slider'
'aria-valuemin': Signal<number>
'aria-valuemax': Signal<number>
'aria-valuenow': Signal<number>
'aria-orientation': Signal<Orientation>
'aria-disabled': Signal<'true' | undefined>
'data-orientation': Signal<Orientation>
'data-disabled': Signal<'' | undefined>
'data-scope': 'slider'
'data-part': 'thumb'
'data-index': string
tabindex: Signal<number>
onKeyDown: (e: KeyboardEvent) => void
style: Signal<string>
}
}
Constants
slider from @llui/components/slider
const slider
@llui/components/dialog
Functions
connect() from @llui/components/dialog
function connect(state: Signal<DialogState>, send: Send<DialogMsg>, opts: ConnectOptions): DialogParts
init() from @llui/components/dialog
function init(opts: DialogInit = {}): DialogState
isMounted() from @llui/components/dialog
Whether the dialog node should be in the DOM — true through the exit animation.
Tolerates a partial slice without status (e.g. the { open } bridge a pattern
passes to overlay): it falls back to open for instant, backward-compatible unmount.
function isMounted(state: DialogState): boolean
isPresent() from @llui/components/dialog
Alias of {@link isMounted} — whether the dialog is currently present in the DOM.
function isPresent(state: DialogState): boolean
overlay() from @llui/components/dialog
Build the dialog's DOM tree and wire up all accessibility utilities.
Returns a show() structural block gated on isMounted(state) so the node
stays mounted through an exit animation (status 'closing') and is removed at
animation end; with skipAnimations (the default) close unmounts synchronously.
function overlay(opts: OverlayOptions): Mountable
update() from @llui/components/dialog
function update(state: DialogState, msg: DialogMsg): [DialogState, never[]]
Types
DialogMsg from @llui/components/dialog
export type DialogMsg =
/** @intent("Open the dialog") */
| { type: 'open' }
/** @intent("Close the dialog") */
| { type: 'close' }
/** @intent("Toggle the dialog open/closed") */
| { type: 'toggle' }
/** @intent("Set the dialog's open state to a specific value") */
| { type: 'setOpen'; open: boolean }
/** @humanOnly */
| { type: 'animationEnd' }
/** @humanOnly */
| { type: 'transitionEnd' }
Interfaces
ConnectOptions from @llui/components/dialog
export interface ConnectOptions {
/** Unique id per dialog instance (used for ARIA wiring). */
id: string
/** ARIA role (default: 'dialog'). Use 'alertdialog' for destructive confirmations. */
role?: 'dialog' | 'alertdialog'
/** Modal dialogs trap focus and lock scroll (default: true). */
modal?: boolean
/** Accessible label for the close button (default: 'Close'). */
closeLabel?: string
}
DialogInit from @llui/components/dialog
export interface DialogInit {
open?: boolean
/** Skip enter/exit animations — close unmounts synchronously (default: true). */
skipAnimations?: boolean
}
DialogParts from @llui/components/dialog
export interface DialogParts {
trigger: {
type: 'button'
'aria-haspopup': 'dialog'
'aria-expanded': Signal<boolean>
'aria-controls': string
id: string
'data-state': Signal<'open' | 'closed'>
'data-scope': 'dialog'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
}
backdrop: {
'data-state': Signal<PresenceStatus>
'data-scope': 'dialog'
'data-part': 'backdrop'
'aria-hidden': 'true'
}
positioner: {
'data-scope': 'dialog'
'data-part': 'positioner'
}
content: {
role: 'dialog' | 'alertdialog'
id: string
'aria-modal': 'true' | undefined
'aria-labelledby': string
'aria-describedby': string
tabindex: -1
'data-state': Signal<PresenceStatus>
'data-scope': 'dialog'
'data-part': 'content'
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
title: {
id: string
'data-scope': 'dialog'
'data-part': 'title'
}
description: {
id: string
'data-scope': 'dialog'
'data-part': 'description'
}
closeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'dialog'
'data-part': 'close-trigger'
onClick: (e: MouseEvent) => void
}
}
DialogState from @llui/components/dialog
Dialog — modal / non-modal overlay. Ties together focus-trap, dismissable, body scroll lock, sibling aria-hidden, and portal-to-body rendering into a single view helper.
Two layers:
- state machine (
init,update,connect) — pure, minimal. overlay()view helper — opens the dialog's DOM tree inside a body portal, wires up all accessibility utilities on mount, tears them down on close, restores focus to the trigger.
view: ({ state, send }) => {
const dialogState = state.at('dialog')
const dialogSend = mapSend<Msg, dialog.DialogMsg>(send, (msg) => ({
type: 'dialog',
msg,
}))
const parts = dialog.connect(dialogState, dialogSend, { id: 'dialog' })
return [
button({ ...parts.trigger, class: 'btn' }, [text('Delete')]),
dialog.overlay({
state: dialogState,
send: dialogSend,
parts,
content: () => [
div({ ...parts.content, class: 'dialog' }, [
h2({ ...parts.title }, [text('Are you sure?')]),
button({ ...parts.closeTrigger, class: 'btn' }, [text('Cancel')]),
]),
],
}),
]
}
export interface DialogState {
open: boolean
/** Presence lifecycle — drives data-state and keeps the node mounted through exit
* animations. Optional: a partial `{ open }` bridge (e.g. a pattern passing a
* slice to `overlay`) omits it, and the runtime falls back to `open` for instant,
* backward-compatible mount/visibility. `init` always sets it. */
status?: PresenceStatus
/** When true, close transitions go straight to 'closed' (no exit-animation wait).
* Optional for the same partial-slice reason as `status`; `init` always sets it. */
skipAnimations?: boolean
}
OverlayOptions from @llui/components/dialog
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
/** Dialog state slice as a Signal. */
state: Signal<DialogState>
/** Send dispatcher for dialog messages. */
send: Send<DialogMsg>
/** Parts from `connect()` — used to locate the content element by id. */
parts: DialogParts
/** Content rendering. */
content: () => Renderable
/**
* Optional enter/leave transition for the dialog content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example dialog.overlay({ state, send, parts, content, transition: fade({ duration: 150 }) })
*/
transition?: TransitionOptions
/** Close on Escape key (default: true). */
closeOnEscape?: boolean
/** Close on click outside content (default: true). */
closeOnOutsideClick?: boolean
/** Trap focus inside the dialog while open (default: true for modal). */
trapFocus?: boolean
/** Lock body scroll while open (default: true for modal). */
lockScroll?: boolean
/** Apply aria-hidden to sibling trees (default: true for modal). */
hideSiblings?: boolean
/** Target element / selector for the portal (default: 'body'). */
target?: string | HTMLElement
/** Element to focus initially (default: first focusable inside content). */
initialFocus?: Element | (() => Element | null)
/** Restore focus on close (default: true). */
restoreFocus?: boolean
}
Constants
dialog from @llui/components/dialog
const dialog
@llui/components/popover
Functions
connect() from @llui/components/popover
function connect(state: Signal<PopoverState>, send: Send<PopoverMsg>, opts: ConnectOptions): PopoverParts
init() from @llui/components/popover
function init(opts: PopoverInit = {}): PopoverState
isMounted() from @llui/components/popover
Whether the popover node should be in the DOM — true through the exit animation.
function isMounted(state: PopoverState): boolean
isPresent() from @llui/components/popover
Alias of {@link isMounted} — whether the popover is currently present in the DOM.
function isPresent(state: PopoverState): boolean
overlay() from @llui/components/popover
function overlay(opts: OverlayOptions): Mountable
update() from @llui/components/popover
function update(state: PopoverState, msg: PopoverMsg): [PopoverState, never[]]
Types
PopoverMsg from @llui/components/popover
export type PopoverMsg =
/** @intent("Open the popover") */
| { type: 'open' }
/** @intent("Close the popover") */
| { type: 'close' }
/** @intent("Toggle the popover open/closed") */
| { type: 'toggle' }
/** @intent("Set the popover's open state to a specific value") */
| { type: 'setOpen'; open: boolean }
/** @humanOnly */
| { type: 'animationEnd' }
/** @humanOnly */
| { type: 'transitionEnd' }
Interfaces
ConnectOptions from @llui/components/popover
export interface ConnectOptions {
id: string
closeLabel?: string
}
OverlayOptions from @llui/components/popover
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<PopoverState>
send: Send<PopoverMsg>
parts: PopoverParts
content: () => Renderable
/**
* Optional enter/leave transition for the popover content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example popover.overlay({ state, send, parts, content, transition: fade({ duration: 150 }) })
*/
transition?: TransitionOptions
/** Placement preference — bottom | top | right | left with -start/-end variants. */
placement?: Placement
/** Offset between trigger and content, px (default: 8). */
offset?: number
/** Auto-flip to opposite side (default: true). */
flip?: boolean
/** Shift to keep in viewport (default: true). */
shift?: boolean
/** Close on Escape (default: true). */
closeOnEscape?: boolean
/** Close on outside click (default: true). */
closeOnOutsideClick?: boolean
/** Trap focus inside popover while open (default: false — non-modal). */
trapFocus?: boolean
/** Restore focus to trigger on close (default: true). */
restoreFocus?: boolean
/** Portal target (default: 'body'). */
target?: string | HTMLElement
/** Arrow element selector within content (optional). */
arrowSelector?: string
}
PopoverInit from @llui/components/popover
export interface PopoverInit {
open?: boolean
/** Skip enter/exit animations — close unmounts synchronously (default: true). */
skipAnimations?: boolean
}
PopoverParts from @llui/components/popover
export interface PopoverParts {
trigger: {
type: 'button'
'aria-haspopup': 'dialog'
'aria-expanded': Signal<boolean>
'aria-controls': string
id: string
'data-state': Signal<'open' | 'closed'>
'data-scope': 'popover'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
}
positioner: {
'data-scope': 'popover'
'data-part': 'positioner'
style: string
}
content: {
role: 'dialog'
id: string
'aria-labelledby': string
tabindex: -1
'data-state': Signal<PresenceStatus>
'data-scope': 'popover'
'data-part': 'content'
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
title: {
id: string
'data-scope': 'popover'
'data-part': 'title'
}
description: {
id: string
'data-scope': 'popover'
'data-part': 'description'
}
arrow: {
'data-scope': 'popover'
'data-part': 'arrow'
}
closeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'popover'
'data-part': 'close-trigger'
onClick: (e: MouseEvent) => void
}
}
PopoverState from @llui/components/popover
Popover — click-triggered, non-modal floating overlay anchored to its trigger. Use for menus, date pickers, color pickers, filters, etc.
Like dialog, has a pure state machine + a view helper (overlay()) that
wires floating-ui positioning, dismissable, and optional focus trapping.
export interface PopoverState {
open: boolean
/** Presence lifecycle — drives data-state and keeps the node mounted through exit animations. */
status: PresenceStatus
/** When true, close transitions go straight to 'closed' (no exit-animation wait). */
skipAnimations: boolean
}
Constants
popover from @llui/components/popover
const popover
@llui/components/tooltip
Functions
connect() from @llui/components/tooltip
function connect(state: Signal<TooltipState>, send: Send<TooltipMsg>, opts: ConnectOptions): TooltipParts
init() from @llui/components/tooltip
function init(opts: TooltipInit = {}): TooltipState
isMounted() from @llui/components/tooltip
Whether the tooltip should be in the DOM (mounted through the exit animation).
function isMounted(state: TooltipState): boolean
overlay() from @llui/components/tooltip
function overlay(opts: OverlayOptions): Mountable
update() from @llui/components/tooltip
function update(state: TooltipState, msg: TooltipMsg): [TooltipState, never[]]
Types
TooltipMsg from @llui/components/tooltip
export type TooltipMsg =
/** @intent("Show the tooltip") */
| { type: 'show' }
/** @intent("Hide the tooltip") */
| { type: 'hide' }
/** @intent("Toggle the tooltip's visibility") */
| { type: 'toggle' }
/** @intent("Set the tooltip's open state to a specific value") */
| { type: 'setOpen'; open: boolean }
/** @humanOnly */
| { type: 'animationEnd' }
Interfaces
ConnectOptions from @llui/components/tooltip
export interface ConnectOptions {
id: string
/** ms to wait before opening (default: 300). */
delayOpen?: number
/** ms to wait before closing after pointer leaves (default: 100). */
delayClose?: number
/** Open immediately on focus without delay (default: true). */
openOnFocus?: boolean
}
OverlayOptions from @llui/components/tooltip
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<TooltipState>
send: Send<TooltipMsg>
parts: TooltipParts
content: () => Renderable
/**
* Optional enter/leave transition for the tooltip content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Opt-in only — supplying this does not turn animation on automatically.
*
* @example tooltip.overlay({ state, send, parts, content, transition: fade({ duration: 100 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
target?: string | HTMLElement
arrowSelector?: string
/** Dismiss on Escape regardless of where focus is (default: true). */
closeOnEscape?: boolean
}
TooltipInit from @llui/components/tooltip
export interface TooltipInit {
open?: boolean
/**
* Enable the exit-animation lifecycle: a close enters `closing` and stays
* mounted until `animationEnd`. Default false (instant unmount). The
* `overlay()` helper turns this on automatically when given a `transition`.
*/
animated?: boolean
}
TooltipParts from @llui/components/tooltip
export interface TooltipParts {
trigger: {
id: string
'aria-describedby': Signal<string | undefined>
'data-state': Signal<'open' | 'closed'>
'data-scope': 'tooltip'
'data-part': 'trigger'
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onFocus: (e: FocusEvent) => void
onBlur: (e: FocusEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
positioner: {
'data-scope': 'tooltip'
'data-part': 'positioner'
style: string
}
content: {
role: 'tooltip'
id: string
style: string
'data-state': Signal<PresenceStatus>
'data-scope': 'tooltip'
'data-part': 'content'
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onKeyDown: (e: KeyboardEvent) => void
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
arrow: {
'data-scope': 'tooltip'
'data-part': 'arrow'
}
}
TooltipState from @llui/components/tooltip
Tooltip — hover / focus-triggered, positioned. Opens after a short delay to avoid flicker from passing pointers, closes immediately on blur or after a grace period on pointer leave.
Pure reducer handles only the boolean open state; timing (delays,
debouncing) lives in the event handlers returned from connect(), which
close over per-instance timers.
export interface TooltipState {
/**
* Whether the tooltip is intended to be visible (true while `opening`/`open`,
* false once a close is requested). Flips to false at close-request time —
* exactly as today — so existing consumers reading `open` are unaffected.
* The DOM node is kept mounted through an exit animation via `status`, not
* `open`. Backed by the presence lifecycle in `status`.
*/
open: boolean
/**
* Full presence lifecycle. `closed → opening → open → closing → closed`.
* When `animated` is false (the default) a close skips `closing` and lands
* on `closed` synchronously, matching today's instant unmount.
*/
status: PresenceStatus
/**
* Whether an exit animation is configured. When false, closing is
* synchronous (no `closing` state, no waiting for `animationEnd`).
*/
animated: boolean
}
Constants
tooltip from @llui/components/tooltip
const tooltip
@llui/components/menu
Functions
connect() from @llui/components/menu
function connect(state: Signal<MenuState>, send: Send<MenuMsg>, opts: ConnectOptions): MenuParts
floatingDir() from @llui/components/menu
The direction to hand attachFloating. undefined means "do not declare
one" — floating-ui then reads the floating element's own computed direction,
which is what an RTL page wants. Anything else overrides the page, so it is
only produced when the host actually asked for it (#138 review, blocking 4).
function floatingDir(state: MenuState): TextDirection | undefined
init() from @llui/components/menu
function init(opts: MenuInit = {}): MenuState
isPresent() from @llui/components/menu
Whether the root menu content should be in the DOM. True for every status except 'closed' — so the content stays mounted through the exit animation.
function isPresent(state: MenuState): boolean
overlay() from @llui/components/menu
function overlay(opts: OverlayOptions): Mountable
update() from @llui/components/menu
function update(state: MenuState, msg: MenuMsg): [MenuState, never[]]
Types
MenuCheckItemParts from @llui/components/menu
export type MenuCheckItemParts = MenuCheckItemPartsOf<'menu'>
MenuGroupParts from @llui/components/menu
export type MenuGroupParts = MenuGroupPartsOf<'menu'>
MenuItem from @llui/components/menu
A single node in the menu item tree (JSON-serializable). Shared with
context-menu (and menubar) via the {@link MenuNode} machine type.
export type MenuItem = MenuNode
MenuItemKind from @llui/components/menu
Kind of a menu item.
export type MenuItemKind = MenuNodeKind
MenuItemParts from @llui/components/menu
export type MenuItemParts = MenuItemPartsOf<'menu'>
MenuMsg from @llui/components/menu
export type MenuMsg =
/** @intent("Open the menu") */
| { type: 'open' }
/** @intent("Close the menu") */
| { type: 'close' }
/** @intent("Toggle the menu open/closed") */
| { type: 'toggle' }
/** @humanOnly */
| { type: 'highlight'; level: string; value: string | null }
/** @humanOnly */
| { type: 'highlightNext'; level: string }
/** @humanOnly */
| { type: 'highlightPrev'; level: string }
/** @humanOnly */
| { type: 'highlightFirst'; level: string }
/** @humanOnly */
| { type: 'highlightLast'; level: string }
/** @intent("Activate the currently-highlighted item at the given level") */
| { type: 'selectHighlighted'; level: string }
/** @intent("Activate the menu item with the given value") */
| { type: 'select'; value: string }
/** @intent("Open the submenu for the given parent item") */
| { type: 'openSub'; value: string }
/** @intent("Close the deepest open submenu") */
| { type: 'closeSub' }
/** @humanOnly */
| { type: 'setItems'; items: MenuItem[] }
/** @humanOnly */
| { type: 'typeahead'; level: string; char: string; now: number }
/** @intent("Set the reading direction — 'ltr'/'rtl', or null to follow the page") */
| { type: 'setDir'; dir: TextDirection | null }
/** @humanOnly */
| { type: 'animationEnd' }
MenuSeparatorParts from @llui/components/menu
export type MenuSeparatorParts = MenuSeparatorPartsOf<'menu'>
MenuSubContentParts from @llui/components/menu
export type MenuSubContentParts = MenuSubContentPartsOf<'menu'>
MenuSubPositionerParts from @llui/components/menu
export type MenuSubPositionerParts = MenuSubPositionerPartsOf<'menu'>
MenuSubTriggerParts from @llui/components/menu
export type MenuSubTriggerParts = MenuSubTriggerPartsOf<'menu'>
Interfaces
ConnectOptions from @llui/components/menu
export interface ConnectOptions {
id: string
/** Called when an item is activated (Enter/Space/click). */
onSelect?: (value: string) => void
/** ms to wait before opening a submenu on hover (default: 200). */
hoverDelay?: number
/** ms to wait before closing a submenu after the pointer leaves (default: 300). */
hoverCloseDelay?: number
}
MenuInit from @llui/components/menu
export interface MenuInit {
open?: boolean
items?: MenuItem[]
highlighted?: string | null
checked?: string[]
closeOnSelect?: boolean
/** Omit to follow the page's own direction (see {@link MenuState.dir}). */
dir?: TextDirection | null
/** When false, closing the menu plays an exit animation and the content stays
* mounted (status 'closing') until an `animationEnd`. Default true: instant. */
skipAnimations?: boolean
}
MenuParts from @llui/components/menu
export interface MenuParts {
trigger: {
type: 'button'
'aria-haspopup': 'menu'
'aria-expanded': Signal<boolean>
'aria-controls': string
id: string
'data-state': Signal<'open' | 'closed'>
'data-scope': 'menu'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
positioner: {
'data-scope': 'menu'
'data-part': 'positioner'
style: string
}
content: {
role: 'menu'
id: string
'aria-labelledby': string
/** The id of the virtually-focused (highlighted) item at the root level, so
* assistive tech announces it while DOM focus stays on the container. */
'aria-activedescendant': Signal<string | undefined>
tabindex: -1
/** Reflects the presence lifecycle: 'opening' | 'open' | 'closing' | 'closed'.
* Stays mounted while 'closing' so the exit animation can run. */
'data-state': Signal<PresenceStatus>
'data-scope': 'menu'
'data-part': 'content'
onKeyDown: (e: KeyboardEvent) => void
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
item: (value: string) => MenuItemParts
checkboxItem: (value: string) => MenuCheckItemParts
radioItem: (value: string) => MenuCheckItemParts
group: (id: string) => MenuGroupParts
separator: () => MenuSeparatorParts
subTrigger: (value: string) => MenuSubTriggerParts
subPositioner: (value: string) => MenuSubPositionerParts
subContent: (value: string) => MenuSubContentParts
}
MenuState from @llui/components/menu
export interface MenuState extends MenuTreeState {
open: boolean
/**
* Presence lifecycle of the root content, layered over `open` for exit
* animations. `open` stays the logical "should be visible/interactive" flag;
* `status` tracks 'opening'/'open'/'closing'/'closed' so the content can stay
* mounted while its exit animation runs (status 'closing'). When
* `skipAnimations` is true (the default) a close jumps straight to 'closed'.
*/
status: PresenceStatus
/** When true (default), a close goes straight to 'closed' synchronously — no
* exit animation, no waiting for an `animationEnd` that may never fire. */
skipAnimations: boolean
items: MenuItem[]
/** Highlighted value per open level. Key `''` is the root; otherwise the parent subTrigger value. */
highlights: Record<string, string | null>
/** Chain of subTrigger values whose submenus are open (deepest last). */
openPath: string[]
/** Checked checkbox / radio values. */
checked: string[]
/** When true, selecting a checkbox/radio also closes the menu (default false). */
closeOnSelect: boolean
/** Accumulator for typeahead search (scoped to the deepest matching level). */
typeahead: string
typeaheadExpiresAt: number
/**
* Reading direction, or `null` for "the host never said — let the page
* decide". Under 'rtl', ArrowLeft/ArrowRight swap meaning, and the overlay's
* `*-start`/`*-end` alignment tracks the inline-start/inline-end edge.
*
* `null` rather than an `'ltr'` default because the value is AUTHORITATIVE
* once it reaches `attachFloating`: a concrete default overrode the page, so
* a menu on `<html dir="rtl">` was laid out LTR (#138 review, blocking 4).
* See {@link floatingDir}.
*/
dir: TextDirection | null
}
OverlayOptions from @llui/components/menu
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<MenuState>
send: Send<MenuMsg>
parts: MenuParts
content: () => Renderable
/**
* Optional enter/leave transition for the menu content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example menu.overlay({ state, send, parts, content, transition: fade({ duration: 120 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
target?: string | HTMLElement
}
Constants
isMounted from @llui/components/menu
Alias of {@link isPresent} for parity with the presence-convention naming.
const isMounted
menu from @llui/components/menu
const menu
@llui/components/switch
Functions
connect() from @llui/components/switch
function connect(state: Signal<SwitchState>, send: Send<SwitchMsg>): SwitchParts
init() from @llui/components/switch
function init(opts: SwitchInit = {}): SwitchState
update() from @llui/components/switch
function update(state: SwitchState, msg: SwitchMsg): [SwitchState, never[]]
Types
SwitchMsg from @llui/components/switch
export type SwitchMsg =
/** @intent("Flip the switch on/off") */
| { type: 'toggle' }
/** @intent("Set the switch's checked state to a specific value") */
| { type: 'setChecked'; checked: boolean }
/** @humanOnly */
| { type: 'setDisabled'; disabled: boolean }
Interfaces
SwitchInit from @llui/components/switch
export interface SwitchInit {
checked?: boolean
disabled?: boolean
}
SwitchParts from @llui/components/switch
export interface SwitchParts {
root: {
role: 'switch'
'aria-checked': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
'data-state': Signal<'checked' | 'unchecked'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'switch'
'data-part': 'root'
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
track: {
'data-state': Signal<'checked' | 'unchecked'>
'data-scope': 'switch'
'data-part': 'track'
}
thumb: {
'data-state': Signal<'checked' | 'unchecked'>
'data-scope': 'switch'
'data-part': 'thumb'
}
hiddenInput: {
type: 'checkbox'
role: 'switch'
'aria-hidden': 'true'
tabindex: -1
style: string
checked: Signal<boolean>
disabled: Signal<boolean>
'data-scope': 'switch'
'data-part': 'hidden-input'
}
}
SwitchState from @llui/components/switch
Switch — two-state on/off control. Semantically like a checkbox but
visually a toggle track + thumb. Uses role="switch" for ARIA.
export interface SwitchState {
checked: boolean
disabled: boolean
}
Constants
switchMachine from @llui/components/switch
const switchMachine
@llui/components/radio-group
Functions
connect() from @llui/components/radio-group
function connect(state: Signal<RadioGroupState>, send: Send<RadioGroupMsg>, opts: ConnectOptions): RadioGroupParts
init() from @llui/components/radio-group
function init(opts: RadioGroupInit = {}): RadioGroupState
update() from @llui/components/radio-group
function update(state: RadioGroupState, msg: RadioGroupMsg): [RadioGroupState, never[]]
Types
Orientation from @llui/components/radio-group
Radio group — a set of mutually-exclusive options. Users select one value at a time. Supports keyboard arrow navigation and disabled items.
export type Orientation = 'horizontal' | 'vertical'
RadioGroupMsg from @llui/components/radio-group
export type RadioGroupMsg =
/** @intent("Pick the radio option with the given value") */
| { type: 'setValue'; value: string }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @intent("Move selection to the next enabled option after the given value") */
| { type: 'selectNext'; from: string }
/** @intent("Move selection to the previous enabled option before the given value") */
| { type: 'selectPrev'; from: string }
/** @intent("Select the first enabled option") */
| { type: 'selectFirst' }
/** @intent("Select the last enabled option") */
| { type: 'selectLast' }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
Interfaces
ConnectOptions from @llui/components/radio-group
export interface ConnectOptions {
id: string
}
RadioGroupInit from @llui/components/radio-group
export interface RadioGroupInit {
value?: string | null
items?: string[]
disabledItems?: string[]
disabled?: boolean
orientation?: Orientation
loopFocus?: boolean
dir?: 'ltr' | 'rtl'
}
RadioGroupParts from @llui/components/radio-group
export interface RadioGroupParts {
root: {
role: 'radiogroup'
'aria-orientation': Signal<Orientation>
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'radio-group'
'data-part': 'root'
'data-orientation': Signal<Orientation>
'data-disabled': Signal<'' | undefined>
}
item: (value: string) => RadioItemParts
}
RadioGroupState from @llui/components/radio-group
export interface RadioGroupState {
value: string | null
items: string[]
disabledItems: string[]
disabled: boolean
orientation: Orientation
/** Whether arrow navigation wraps at the ends. Default true (WAI-ARIA radio
* groups wrap). Present on every roving widget in the package — its absence
* here was pure drift between the copies of the navigation code (#126). */
loopFocus: boolean
/** Reading direction. Under 'rtl', ArrowLeft/ArrowRight swap meaning. */
dir: 'ltr' | 'rtl'
}
RadioItemParts from @llui/components/radio-group
export interface RadioItemParts {
root: {
role: 'radio'
id: string
'aria-checked': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
'data-state': Signal<'checked' | 'unchecked'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'radio-group'
'data-part': 'item'
'data-value': string
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
label: {
'data-scope': 'radio-group'
'data-part': 'label'
'data-value': string
for: string
}
indicator: {
'data-state': Signal<'checked' | 'unchecked'>
'data-scope': 'radio-group'
'data-part': 'indicator'
}
}
Constants
radioGroup from @llui/components/radio-group
const radioGroup
@llui/components/collapsible
Functions
connect() from @llui/components/collapsible
function connect(state: Signal<CollapsibleState>, send: Send<CollapsibleMsg>, opts: ConnectOptions): CollapsibleParts
init() from @llui/components/collapsible
function init(opts: CollapsibleInit = {}): CollapsibleState
update() from @llui/components/collapsible
function update(state: CollapsibleState, msg: CollapsibleMsg): [CollapsibleState, never[]]
Types
CollapsibleMsg from @llui/components/collapsible
export type CollapsibleMsg =
/** @intent("Toggle the collapsible panel open/closed") */
| { type: 'toggle' }
/** @intent("Expand the collapsible panel") */
| { type: 'open' }
/** @intent("Collapse the panel") */
| { type: 'close' }
/** @intent("Set the panel's open state to a specific value") */
| { type: 'setOpen'; open: boolean }
Interfaces
CollapsibleInit from @llui/components/collapsible
export interface CollapsibleInit {
open?: boolean
disabled?: boolean
}
CollapsibleParts from @llui/components/collapsible
export interface CollapsibleParts {
root: {
'data-state': Signal<'open' | 'closed'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'collapsible'
'data-part': 'root'
}
trigger: {
type: 'button'
'aria-expanded': Signal<boolean>
'aria-controls': string
id: string
disabled: Signal<boolean>
'data-state': Signal<'open' | 'closed'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'collapsible'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
}
content: {
role: 'region'
id: string
'aria-labelledby': string
hidden: Signal<boolean>
'data-state': Signal<'open' | 'closed'>
'data-scope': 'collapsible'
'data-part': 'content'
}
}
CollapsibleState from @llui/components/collapsible
Collapsible — a single expandable/collapsible section. Simpler than accordion (no grouping, no keyboard navigation between siblings).
export interface CollapsibleState {
open: boolean
disabled: boolean
}
ConnectOptions from @llui/components/collapsible
export interface ConnectOptions {
id: string
}
Constants
collapsible from @llui/components/collapsible
const collapsible
@llui/components/toggle-group
Functions
connect() from @llui/components/toggle-group
function connect(state: Signal<ToggleGroupState>, send: Send<ToggleGroupMsg>): ToggleGroupParts
init() from @llui/components/toggle-group
function init(opts: ToggleGroupInit = {}): ToggleGroupState
update() from @llui/components/toggle-group
function update(state: ToggleGroupState, msg: ToggleGroupMsg): [ToggleGroupState, never[]]
Types
Orientation from @llui/components/toggle-group
Toggle group — a set of toggle buttons. type: 'single' enforces
one-active-at-a-time (like a radio group but visually toggles).
type: 'multiple' allows any subset to be pressed.
export type Orientation = 'horizontal' | 'vertical'
ToggleGroupMsg from @llui/components/toggle-group
export type ToggleGroupMsg =
/** @intent("Toggle the button with the given value (in single mode, replaces selection)") */
| { type: 'toggle'; value: string }
/** @intent("Replace the pressed-value set with the provided list") */
| { type: 'setValue'; value: string[] }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @humanOnly */
| { type: 'focusNext'; from: string }
/** @humanOnly */
| { type: 'focusPrev'; from: string }
/** @humanOnly */
| { type: 'focusItem'; value: string }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
Interfaces
ToggleGroupInit from @llui/components/toggle-group
export interface ToggleGroupInit {
value?: string[]
type?: 'single' | 'multiple'
items?: string[]
disabledItems?: string[]
disabled?: boolean
orientation?: Orientation
deselectable?: boolean
focused?: string | null
loopFocus?: boolean
dir?: 'ltr' | 'rtl'
}
ToggleGroupItemParts from @llui/components/toggle-group
export interface ToggleGroupItemParts {
root: {
type: 'button'
role: 'button'
'aria-pressed': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-state': Signal<'on' | 'off'>
'data-disabled': Signal<'' | undefined>
'data-scope': 'toggle-group'
'data-part': 'item'
'data-value': string
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
}
}
ToggleGroupParts from @llui/components/toggle-group
export interface ToggleGroupParts {
root: {
role: 'group'
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'toggle-group'
'data-part': 'root'
'data-orientation': Signal<Orientation>
'data-disabled': Signal<'' | undefined>
}
item: (value: string) => ToggleGroupItemParts
}
ToggleGroupState from @llui/components/toggle-group
export interface ToggleGroupState {
value: string[]
type: 'single' | 'multiple'
items: string[]
disabledItems: string[]
disabled: boolean
orientation: Orientation
/** In single mode, whether the active item can be deselected. */
deselectable: boolean
/** The currently roving-focused item (independent of the pressed value). */
focused: string | null
/** Whether Arrow navigation wraps at the ends of the group. Default: true. */
loopFocus: boolean
/** Reading direction. Under 'rtl', ArrowLeft/ArrowRight swap meaning. */
dir: 'ltr' | 'rtl'
}
Constants
toggleGroup from @llui/components/toggle-group
const toggleGroup
@llui/components/number-input
Functions
connect() from @llui/components/number-input
function connect(state: Signal<NumberInputState>, send: Send<NumberInputMsg>, opts: ConnectOptions = {}): NumberInputParts
init() from @llui/components/number-input
function init(opts: NumberInputInit = {}): NumberInputState
update() from @llui/components/number-input
function update(state: NumberInputState, msg: NumberInputMsg): [NumberInputState, never[]]
Types
NumberInputMsg from @llui/components/number-input
export type NumberInputMsg =
/** @intent("Set the numeric value (clamped to min/max, snapped to step)") */
| { type: 'setValue'; value: number | null }
/** @humanOnly */
| { type: 'setRawText'; text: string }
/** @intent("Commit the in-progress text input — parse, clamp, snap, and update value. Ignored while disabled or readonly") */
| { type: 'commit' }
/** @intent("Increase value by step (or step × multiplier). Ignored while disabled or readonly") */
| { type: 'increment'; multiplier?: number }
/** @intent("Decrease value by step (or step × multiplier). Ignored while disabled or readonly") */
| { type: 'decrement'; multiplier?: number }
/** @intent("Snap value to the configured minimum. Ignored while disabled or readonly") */
| { type: 'toMin' }
/** @intent("Snap value to the configured maximum. Ignored while disabled or readonly") */
| { type: 'toMax' }
/** @intent("Enable or disable the input — a host/agent write, never gated") */
| { type: 'setDisabled'; disabled: boolean }
Interfaces
ConnectOptions from @llui/components/number-input
export interface ConnectOptions {
incrementLabel?: string
decrementLabel?: string
/** Validate the numeric value before committing. Non-empty array blocks setValue. */
validate?: (value: number) => string[] | null
}
NumberInputInit from @llui/components/number-input
export interface NumberInputInit {
value?: number | null
min?: number
max?: number
step?: number
disabled?: boolean
readonly?: boolean
}
NumberInputParts from @llui/components/number-input
export interface NumberInputParts {
root: {
'data-scope': 'number-input'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
input: {
type: 'text'
role: 'spinbutton'
inputmode: 'decimal'
'aria-valuemin': Signal<number | undefined>
'aria-valuemax': Signal<number | undefined>
'aria-valuenow': Signal<number | undefined>
'aria-disabled': Signal<'true' | undefined>
'aria-readonly': Signal<'true' | undefined>
disabled: Signal<boolean>
readonly: Signal<boolean>
value: Signal<string>
'data-scope': 'number-input'
'data-part': 'input'
onInput: (e: Event) => void
onBlur: (e: FocusEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
increment: {
type: 'button'
'aria-label': string
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-scope': 'number-input'
'data-part': 'increment'
tabindex: -1
onClick: (e: MouseEvent) => void
}
decrement: {
type: 'button'
'aria-label': string
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-scope': 'number-input'
'data-part': 'decrement'
tabindex: -1
onClick: (e: MouseEvent) => void
}
}
NumberInputState from @llui/components/number-input
Number input — numeric field with increment/decrement buttons. Clamps to min/max and snaps to step. Keyboard: Arrow Up/Down, PageUp/PageDown, Home/End.
export interface NumberInputState {
value: number | null
/**
* The bounds, ABSENT when that side is unbounded — the state's `min`/`max`/
* `step` ARE a `NumericGrid`, which is what lets `clampToStep(value, state)`
* take the state object straight in.
*
* A bound is never `±Infinity` and never `NaN`: this is the ONE component in
* the package whose DEFAULT range is unbounded, and it used to spell that
* `min: -Infinity` / `max: Infinity` in state, which `JSON.stringify` writes
* as `null` — so its default state did not survive a round trip and the
* rehydrated object held `null` in a `number` field (#177). Absence is the
* serializable spelling of the same fact; `finiteBound` is the one place it
* is decided.
*/
min?: number
max?: number
step: number
disabled: boolean
readonly: boolean
/** Allow a free-text input value while the user is typing. */
rawText: string
}
Constants
numberInput from @llui/components/number-input
const numberInput
@llui/components/pin-input
Functions
acceptedChars() from @llui/components/pin-input
Accepted characters of a pasted sequence, in order and WITHOUT the holes a rejected character used to leave. Sanitizing per SLOT dropped '123-456' into ['1','2','3','','4','5'] — the separator ate a slot and the last digit fell off the end (#125). Entries may hold more than one character — the paste handler passes the clipboard text WHOLE, which is what makes the code-point iteration below reachable: a surrogate pair is judged (and rejected) as ONE character rather than as two lone halves. Splitting by UTF-16 code unit before the call throws that away, which is what the caller used to do.
function acceptedChars(values: readonly string[], type: PinType): string[]
connect() from @llui/components/pin-input
function connect(state: Signal<PinInputState>, send: Send<PinInputMsg>, opts: ConnectOptions): PinInputParts
getValue() from @llui/components/pin-input
function getValue(state: PinInputState): string
init() from @llui/components/pin-input
function init(opts: PinInputInit = {}): PinInputState
isComplete() from @llui/components/pin-input
function isComplete(state: PinInputState): boolean
update() from @llui/components/pin-input
function update(state: PinInputState, msg: PinInputMsg): [PinInputState, never[]]
Types
PinInputMsg from @llui/components/pin-input
export type PinInputMsg =
/** @intent("Set the character at a given field index (auto-advances focus on accept)") */
| { type: 'setValue'; index: number; value: string }
/** @intent("Replace every field at once (typically from paste)") */
| { type: 'setAll'; values: string[] }
/** @humanOnly */
| { type: 'focus'; index: number }
/** @intent("Clear every field") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'backspace'; index: number }
/** @intent("Enable or disable the pin-input — a host/agent write, never gated") */
| { type: 'setDisabled'; disabled: boolean }
PinType from @llui/components/pin-input
Pin input — a sequence of single-character fields for OTP codes, etc. Auto-advances on input, handles backspace to previous field, supports paste-to-fill across multiple fields.
export type PinType = 'numeric' | 'alphanumeric' | 'alphabetic'
Interfaces
ConnectOptions from @llui/components/pin-input
export interface ConnectOptions {
id: string
inputLabel?: (index: number) => string
/** Validate each character before setting. Non-empty array blocks setDigit. */
validate?: (value: string) => string[] | null
}
PinInputInit from @llui/components/pin-input
export interface PinInputInit {
length?: number
type?: PinType
mask?: boolean
disabled?: boolean
values?: string[]
}
PinInputParts from @llui/components/pin-input
export interface PinInputParts {
root: {
role: 'group'
'aria-labelledby': string
'data-scope': 'pin-input'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
label: {
id: string
'data-scope': 'pin-input'
'data-part': 'label'
}
/** Props for the input at a given index. */
input: (index: number) => {
type: Signal<'text' | 'password'>
inputmode: Signal<'numeric' | 'text'>
pattern: Signal<string>
maxlength: 1
autocomplete: 'off'
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'pin-input'
'data-part': 'input'
'data-index': string
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
onPaste: (e: ClipboardEvent) => void
}
}
PinInputState from @llui/components/pin-input
export interface PinInputState {
values: string[]
length: number
type: PinType
mask: boolean
disabled: boolean
focusedIndex: number
}
Constants
pinInput from @llui/components/pin-input
const pinInput
@llui/components/progress
Functions
connect() from @llui/components/progress
function connect(state: Signal<ProgressState>, _send: Send<ProgressMsg>, opts: ConnectOptions = {}): ProgressParts
init() from @llui/components/progress
function init(opts: ProgressInit = {}): ProgressState
percent() from @llui/components/progress
function percent(state: ProgressState): number | null
update() from @llui/components/progress
function update(state: ProgressState, msg: ProgressMsg): [ProgressState, never[]]
valueState() from @llui/components/progress
function valueState(state: ProgressState): 'indeterminate' | 'complete' | 'loading'
Types
ProgressMsg from @llui/components/progress
export type ProgressMsg =
/** @humanOnly */
| { type: 'setValue'; value: number | null }
/** @humanOnly */
| { type: 'setMax'; max: number }
ProgressOrientation from @llui/components/progress
Progress — linear or circular progress indicator. Determinate (0..max) or
indeterminate (value: null).
export type ProgressOrientation = 'horizontal' | 'vertical'
Interfaces
ConnectOptions from @llui/components/progress
export interface ConnectOptions {
label?: string
/** Custom formatter for value text. */
format?: (value: number | null, max: number) => string
}
ProgressInit from @llui/components/progress
export interface ProgressInit {
value?: number | null
min?: number
max?: number
orientation?: ProgressOrientation
}
ProgressParts from @llui/components/progress
export interface ProgressParts {
root: {
role: 'progressbar'
'aria-valuemin': Signal<number>
'aria-valuemax': Signal<number>
'aria-valuenow': Signal<number | undefined>
'aria-label': string | undefined
'data-state': Signal<'indeterminate' | 'complete' | 'loading'>
'data-orientation': Signal<ProgressOrientation>
'data-scope': 'progress'
'data-part': 'root'
}
track: {
'data-state': Signal<'indeterminate' | 'complete' | 'loading'>
'data-orientation': Signal<ProgressOrientation>
'data-scope': 'progress'
'data-part': 'track'
}
range: {
'data-state': Signal<'indeterminate' | 'complete' | 'loading'>
'data-orientation': Signal<ProgressOrientation>
'data-scope': 'progress'
'data-part': 'range'
style: Signal<string>
}
label: {
'data-scope': 'progress'
'data-part': 'label'
}
valueText: Signal<string>
}
ProgressState from @llui/components/progress
export interface ProgressState {
value: number | null
min: number
max: number
orientation: ProgressOrientation
}
Constants
progress from @llui/components/progress
const progress
@llui/components/rating-group
Functions
connect() from @llui/components/rating-group
function connect(state: Signal<RatingGroupState>, send: Send<RatingGroupMsg>, opts: ConnectOptions = {}): RatingGroupParts
init() from @llui/components/rating-group
function init(opts: RatingGroupInit = {}): RatingGroupState
itemFill() from @llui/components/rating-group
function itemFill(state: RatingGroupState, index: number): ItemFill
update() from @llui/components/rating-group
function update(state: RatingGroupState, msg: RatingGroupMsg): [RatingGroupState, never[]]
Types
ItemFill from @llui/components/rating-group
export type ItemFill = 'full' | 'half' | 'empty'
RatingGroupMsg from @llui/components/rating-group
export type RatingGroupMsg =
/** @intent("Set the rating value directly (clamped to 0..count, snapped to 0.5 if allowHalf)") */
| { type: 'setValue'; value: number }
/** @humanOnly */
| { type: 'hover'; value: number | null }
/** @humanOnly */
| { type: 'clickItem'; index: number; isLeftHalf: boolean }
/** @humanOnly */
| { type: 'hoverItem'; index: number; isLeftHalf: boolean }
/** @intent("Increase the rating by step (default: 0.5 if allowHalf, else 1)") */
| { type: 'incrementValue'; step?: number }
/** @intent("Decrease the rating by step (default: 0.5 if allowHalf, else 1)") */
| { type: 'decrementValue'; step?: number }
/** @intent("Snap the rating to its maximum (count)") */
| { type: 'toEnd' }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
Interfaces
ConnectOptions from @llui/components/rating-group
export interface ConnectOptions {
label?: string
}
RatingGroupInit from @llui/components/rating-group
export interface RatingGroupInit {
value?: number
count?: number
allowHalf?: boolean
disabled?: boolean
readonly?: boolean
dir?: 'ltr' | 'rtl'
}
RatingGroupParts from @llui/components/rating-group
export interface RatingGroupParts {
root: {
role: 'radiogroup'
'aria-label': string | undefined
'aria-disabled': Signal<'true' | undefined>
'aria-readonly': Signal<'true' | undefined>
'data-scope': 'rating-group'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
'data-readonly': Signal<'' | undefined>
}
item: (index: number) => RatingItemParts
}
RatingGroupState from @llui/components/rating-group
Rating group — a sequence of clickable items (stars) representing a discrete rating. Supports half-step ratings and keyboard navigation.
export interface RatingGroupState {
value: number
count: number
/** If true, allows values like 1.5 (half-stars). */
allowHalf: boolean
disabled: boolean
readonly: boolean
hoveredValue: number | null
/** Reading direction. Under 'rtl', ArrowLeft/ArrowRight swap meaning. */
dir: 'ltr' | 'rtl'
}
RatingItemParts from @llui/components/rating-group
export interface RatingItemParts {
root: {
role: 'radio'
'aria-checked': Signal<boolean>
'data-fill': Signal<ItemFill>
'data-scope': 'rating-group'
'data-part': 'item'
'data-value': string
'data-disabled': Signal<'' | undefined>
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onPointerMove: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
}
Constants
ratingGroup from @llui/components/rating-group
const ratingGroup
@llui/components/pagination
Functions
connect() from @llui/components/pagination
function connect(state: Signal<PaginationState>, send: Send<PaginationMsg>, opts: ConnectOptions = {}): PaginationParts
init() from @llui/components/pagination
function init(opts: PaginationInit = {}): PaginationState
onControlKeyDown() from @llui/components/pagination
onKeyDown for every focusable pagination control. Moves DOM focus across
the controls (ArrowLeft/ArrowRight, Home/End), skipping ellipsis + disabled
prev/next. Page triggers stay real <button>s, so Enter/Space activate
natively — this handler deliberately ignores them.
dir is the explicit reading direction (from State); omit it to resolve the
direction from the DOM instead.
function onControlKeyDown(e: KeyboardEvent, dir?: TextDirection): void
pageItems() from @llui/components/pagination
Compute the visible page buttons with ellipses:
[first ..boundaries] … [siblings around current] … [boundaries ..last].
function pageItems(state: PaginationState): PageItem[]
totalPages() from @llui/components/pagination
function totalPages(state: PaginationState): number
update() from @llui/components/pagination
function update(state: PaginationState, msg: PaginationMsg): [PaginationState, never[]]
Types
PageItem from @llui/components/pagination
export type PageItem =
| { type: 'page'; page: number }
| { type: 'ellipsis'; position: 'start' | 'end' }
PaginationMsg from @llui/components/pagination
export type PaginationMsg =
/** @intent("Jump to a specific 1-based page number") */
| { type: 'goTo'; page: number }
/** @intent("Advance to the next page") */
| { type: 'next' }
/** @intent("Go back to the previous page") */
| { type: 'prev' }
/** @intent("Jump to the first page") */
| { type: 'first' }
/** @intent("Jump to the last page") */
| { type: 'last' }
/** @intent("Change how many items each page contains") */
| { type: 'setPageSize'; pageSize: number }
/** @humanOnly */
| { type: 'setTotal'; total: number }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: TextDirection }
Interfaces
ConnectOptions from @llui/components/pagination
export interface ConnectOptions {
label?: string
prevLabel?: string
nextLabel?: string
pageLabel?: (page: number) => string
}
PaginationInit from @llui/components/pagination
export interface PaginationInit {
page?: number
pageSize?: number
total?: number
siblings?: number
boundaries?: number
disabled?: boolean
dir?: TextDirection
}
PaginationParts from @llui/components/pagination
export interface PaginationParts {
root: {
role: 'navigation'
'aria-label': string
'data-scope': 'pagination'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
prevTrigger: {
type: 'button'
'aria-label': string
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-scope': 'pagination'
'data-part': 'prev-trigger'
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
nextTrigger: {
type: 'button'
'aria-label': string
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-scope': 'pagination'
'data-part': 'next-trigger'
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
item: (page: number) => {
type: 'button'
'aria-label': string
'aria-current': Signal<'page' | undefined>
'data-selected': Signal<'' | undefined>
'data-scope': 'pagination'
'data-part': 'item'
'data-value': string
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
ellipsis: (position: 'start' | 'end') => {
'aria-hidden': 'true'
'data-scope': 'pagination'
'data-part': 'ellipsis'
'data-position': 'start' | 'end'
}
}
PaginationState from @llui/components/pagination
Pagination — page navigation with ellipses for large ranges.
page is 1-based. Siblings are the count of pages shown on each side
of the current page. Boundaries are shown at the start/end.
export interface PaginationState {
page: number
pageSize: number
total: number
siblings: number
boundaries: number
disabled: boolean
dir: TextDirection
}
Constants
pagination from @llui/components/pagination
const pagination
@llui/components/alert-dialog
Functions
connect() from @llui/components/alert-dialog
function connect(state: Signal<DialogState>, send: Send<DialogMsg>, opts: AlertDialogConnectOptions): AlertDialogParts
init() from @llui/components/alert-dialog
function init(opts: DialogInit = {}): DialogState
isMounted() from @llui/components/alert-dialog
Whether the dialog node should be in the DOM — true through the exit animation.
Tolerates a partial slice without status (e.g. the { open } bridge a pattern
passes to overlay): it falls back to open for instant, backward-compatible unmount.
function isMounted(state: DialogState): boolean
isPresent() from @llui/components/alert-dialog
Alias of {@link isMounted} — whether the dialog is currently present in the DOM.
function isPresent(state: DialogState): boolean
overlay() from @llui/components/alert-dialog
function overlay(opts: AlertDialogOverlayOptions): Mountable
update() from @llui/components/alert-dialog
function update(state: DialogState, msg: DialogMsg): [DialogState, never[]]
Types
AlertDialogConnectOptions from @llui/components/alert-dialog
Connect options — the dialog options minus role (fixed to alertdialog).
export type AlertDialogConnectOptions = Omit<DialogConnectOptions, 'role'>
AlertDialogMsg from @llui/components/alert-dialog
export type DialogMsg =
/** @intent("Open the dialog") */
| { type: 'open' }
/** @intent("Close the dialog") */
| { type: 'close' }
/** @intent("Toggle the dialog open/closed") */
| { type: 'toggle' }
/** @intent("Set the dialog's open state to a specific value") */
| { type: 'setOpen'; open: boolean }
/** @humanOnly */
| { type: 'animationEnd' }
/** @humanOnly */
| { type: 'transitionEnd' }
AlertDialogParts from @llui/components/alert-dialog
export type AlertDialogParts = DialogParts
Interfaces
AlertDialogOverlayOptions from @llui/components/alert-dialog
export interface AlertDialogOverlayOptions {
/**
* Class applied to the positioner — the wrapper `div` `dialog.overlay` builds
* around the content. Forwarded verbatim; the consumer supplies the layer's
* `fixed inset-0` and `z-index`, since the part bag carries only `data-*`.
*
* This interface does not spread `DialogOverlayOptions`, so a new dialog
* option has to be restated here to reach `dialogOverlay` through the `...opts`
* below — which is exactly how this one was missed the first time.
*/
positionerClass?: string
state: Signal<DialogState>
send: Send<DialogMsg>
parts: AlertDialogParts
content: () => Renderable
/**
* Optional enter/leave transition for the alert-dialog content (from
* `@llui/transitions`), forwarded to `dialog.overlay`. `enter` animates it in
* on open; `leave` defers the unmount until its promise resolves, so the close
* plays an exit animation. Keep `skipAnimations` at its default (true) here.
*
* @example alertDialog.overlay({ state, send, parts, content, transition: fade({ duration: 150 }) })
*/
transition?: TransitionOptions
closeOnEscape?: boolean
/** Whether outside-click should dismiss (default: false for alert dialogs). */
closeOnOutsideClick?: boolean
trapFocus?: boolean
lockScroll?: boolean
hideSiblings?: boolean
target?: string | HTMLElement
initialFocus?: Element | (() => Element | null)
restoreFocus?: boolean
}
AlertDialogState from @llui/components/alert-dialog
Dialog — modal / non-modal overlay. Ties together focus-trap, dismissable, body scroll lock, sibling aria-hidden, and portal-to-body rendering into a single view helper.
Two layers:
- state machine (
init,update,connect) — pure, minimal. overlay()view helper — opens the dialog's DOM tree inside a body portal, wires up all accessibility utilities on mount, tears them down on close, restores focus to the trigger.
view: ({ state, send }) => {
const dialogState = state.at('dialog')
const dialogSend = mapSend<Msg, dialog.DialogMsg>(send, (msg) => ({
type: 'dialog',
msg,
}))
const parts = dialog.connect(dialogState, dialogSend, { id: 'dialog' })
return [
button({ ...parts.trigger, class: 'btn' }, [text('Delete')]),
dialog.overlay({
state: dialogState,
send: dialogSend,
parts,
content: () => [
div({ ...parts.content, class: 'dialog' }, [
h2({ ...parts.title }, [text('Are you sure?')]),
button({ ...parts.closeTrigger, class: 'btn' }, [text('Cancel')]),
]),
],
}),
]
}
export interface DialogState {
open: boolean
/** Presence lifecycle — drives data-state and keeps the node mounted through exit
* animations. Optional: a partial `{ open }` bridge (e.g. a pattern passing a
* slice to `overlay`) omits it, and the runtime falls back to `open` for instant,
* backward-compatible mount/visibility. `init` always sets it. */
status?: PresenceStatus
/** When true, close transitions go straight to 'closed' (no exit-animation wait).
* Optional for the same partial-slice reason as `status`; `init` always sets it. */
skipAnimations?: boolean
}
Constants
alertDialog from @llui/components/alert-dialog
const alertDialog
@llui/components/drawer
Functions
connect() from @llui/components/drawer
function connect(state: Signal<DrawerState>, send: Send<DrawerMsg>, opts: ConnectOptions): DrawerParts
init() from @llui/components/drawer
function init(opts: DrawerInit = {}): DrawerState
isMounted() from @llui/components/drawer
Whether the drawer node should be in the DOM — true through the exit animation.
Tolerates a partial slice without status by falling back to open (instant unmount).
function isMounted(state: DrawerState): boolean
isPresent() from @llui/components/drawer
Alias of {@link isMounted} — whether the drawer is currently present in the DOM.
function isPresent(state: DrawerState): boolean
overlay() from @llui/components/drawer
function overlay(opts: OverlayOptions): Mountable
update() from @llui/components/drawer
function update(state: DrawerState, msg: DrawerMsg): [DrawerState, never[]]
Types
DrawerMsg from @llui/components/drawer
export type DrawerMsg =
/** @intent("Open the drawer") */
| { type: 'open' }
/** @intent("Close the drawer") */
| { type: 'close' }
/** @intent("Toggle the drawer open/closed") */
| { type: 'toggle' }
/** @intent("Set the drawer's open state to a specific value") */
| { type: 'setOpen'; open: boolean }
/** @humanOnly */
| { type: 'animationEnd' }
/** @humanOnly */
| { type: 'transitionEnd' }
DrawerSide from @llui/components/drawer
Drawer — a panel that slides in from a screen edge. Structurally
identical to dialog (portal + focus trap + dismissable + aria-hidden +
scroll lock), but adds a side so styling can animate from that edge.
export type DrawerSide = 'left' | 'right' | 'top' | 'bottom'
Interfaces
ConnectOptions from @llui/components/drawer
export interface ConnectOptions {
id: string
side?: DrawerSide
closeLabel?: string
}
DrawerInit from @llui/components/drawer
export interface DrawerInit {
open?: boolean
/** Skip enter/exit animations — close unmounts synchronously (default: true). */
skipAnimations?: boolean
}
DrawerParts from @llui/components/drawer
export interface DrawerParts {
trigger: {
type: 'button'
'aria-haspopup': 'dialog'
'aria-expanded': Signal<boolean>
'aria-controls': string
id: string
'data-state': Signal<'open' | 'closed'>
'data-scope': 'drawer'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
}
backdrop: {
'data-state': Signal<PresenceStatus>
'data-scope': 'drawer'
'data-part': 'backdrop'
'aria-hidden': 'true'
}
positioner: {
'data-scope': 'drawer'
'data-part': 'positioner'
'data-side': DrawerSide
}
content: {
role: 'dialog'
id: string
'aria-modal': 'true'
'aria-labelledby': string
tabindex: -1
'data-state': Signal<PresenceStatus>
'data-scope': 'drawer'
'data-part': 'content'
'data-side': DrawerSide
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
title: {
id: string
'data-scope': 'drawer'
'data-part': 'title'
}
description: {
id: string
'data-scope': 'drawer'
'data-part': 'description'
}
closeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'drawer'
'data-part': 'close-trigger'
onClick: (e: MouseEvent) => void
}
}
DrawerState from @llui/components/drawer
export interface DrawerState {
open: boolean
/** Presence lifecycle — drives data-state and keeps the node mounted through exit animations. */
status: PresenceStatus
/** When true, close transitions go straight to 'closed' (no exit-animation wait). */
skipAnimations: boolean
}
OverlayOptions from @llui/components/drawer
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<DrawerState>
send: Send<DrawerMsg>
parts: DrawerParts
content: () => Renderable
/**
* Optional enter/leave transition for the drawer content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example drawer.overlay({ state, send, parts, content, transition: slide({ duration: 200 }) })
*/
transition?: TransitionOptions
closeOnEscape?: boolean
closeOnOutsideClick?: boolean
trapFocus?: boolean
lockScroll?: boolean
hideSiblings?: boolean
target?: string | HTMLElement
initialFocus?: Element | (() => Element | null)
restoreFocus?: boolean
}
Constants
drawer from @llui/components/drawer
const drawer
@llui/components/toast
Functions
connect() from @llui/components/toast
function connect(state: Signal<ToasterState>, send: Send<ToasterMsg>, opts: ConnectOptions = {}): ToasterParts
init() from @llui/components/toast
function init(opts: ToasterInit = {}): ToasterState
isPresent() from @llui/components/toast
Whether a toast with the given id is in the queue (mounted). Stays true
through 'closing'; false once animationEnd removes it.
function isPresent(state: ToasterState, id: string): boolean
nextToastId() from @llui/components/toast
function nextToastId(): string
politeness() from @llui/components/toast
Resolve the announcement politeness for a toast: explicit override, else
error → assertive, everything else → polite.
function politeness(toast: Pick<Toast, 'type' | 'ariaLive'>): ToastPoliteness
progress() from @llui/components/toast
Fraction of the countdown remaining for a given toast, in [0,1]. Sticky toasts report 1; a missing toast reports 0.
function progress(state: ToasterState, id: string): number
update() from @llui/components/toast
function update(state: ToasterState, msg: ToasterMsg): [ToasterState, never[]]
Types
ToasterMsg from @llui/components/toast
export type ToasterMsg =
/** @intent("Show a new toast notification") */
| { type: 'create'; toast: ToastInput }
/** @intent("Dismiss the toast with the given id") */
| { type: 'dismiss'; id: string }
/** @intent("Dismiss every toast currently visible") */
| { type: 'dismissAll' }
/** @intent("Patch fields on the toast with the given id (title, description, type, etc.)") */
| { type: 'update'; id: string; patch: Partial<Toast> }
/** @humanOnly Advance the countdown for one toast by `elapsedMs` since the last tick. */
| { type: 'tick'; id: string; elapsedMs: number }
/** @intent("Pause auto-dismiss countdown for the toast with the given id") */
| { type: 'pause'; id: string }
/** @intent("Resume auto-dismiss countdown for the toast with the given id") */
| { type: 'resume'; id: string }
/** @intent("Pause auto-dismiss for every visible toast") */
| { type: 'pauseAll' }
/** @intent("Resume auto-dismiss for every visible toast") */
| { type: 'resumeAll' }
/** @humanOnly Exit animation finished for the toast with the given id — remove it from the queue. */
| { type: 'animationEnd'; id: string }
ToastInput from @llui/components/toast
A new toast as supplied to create. remainingMs/paused/status are
optional — seeded from duration/false/'open' when omitted.
export type ToastInput = Omit<Toast, 'remainingMs' | 'paused' | 'status'> & {
remainingMs?: number
paused?: boolean
status?: PresenceStatus
}
ToastPlacement from @llui/components/toast
export type ToastPlacement =
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
ToastPoliteness from @llui/components/toast
aria-live politeness for a toast's announcement region.
export type ToastPoliteness = 'polite' | 'assertive'
ToastType from @llui/components/toast
Toast — ephemeral non-modal notifications rendered in a fixed region. Multiple toasts can be active at once. Each has a duration after which it auto-dismisses (unless paused or sticky).
Architecture (timer-free, tick-driven — same division of labor as timer.ts):
toast.toasterstate manages a collection of toasts. Each toast carries its own countdown in state:duration(null = sticky),remainingMs, andpaused.- The machine owns NO interval. The consumer drives the countdown with a
tick(id, elapsedMs)message (e.g. via @llui/effectsinterval), subtracting the elapsed wall time since the last tick. Apausedtoast freezes itsremainingMs(ticks are ignored). - When
remainingMshits 0 the REDUCER dismisses that toast itself, so there is no consumer/runtime race over who removes it.
Presence (exit animation) — additive, opt-in via init({ animated: true }):
- Each toast carries a presence
status(closed/opening/open/closing). A freshly created toast is born'open'(no enter-animation gate — toasts appear immediately). - Dismissing a toast (manually or when its countdown reaches 0) moves it to
'closing'and KEEPS IT MOUNTED so it can play an exit animation; ananimationEnd(id)message then removes it from the queue. - When the toaster is NOT animated, dismiss removes the toast SYNCHRONOUSLY (today's behavior) — never waiting for an animationend that won't fire.
export type ToastType = 'info' | 'success' | 'warning' | 'error' | 'loading' | 'custom'
Interfaces
ConnectOptions from @llui/components/toast
export interface ConnectOptions {
regionLabel?: string
closeLabel?: string
}
Toast from @llui/components/toast
export interface Toast {
id: string
type: ToastType
title?: string
description?: string
/** ms until auto-dismiss. `null` = sticky (never auto-dismisses). */
duration: number | null
/** ms left before auto-dismiss. Counts down via `tick`. */
remainingMs: number
/** Whether the toast can be manually dismissed. */
dismissable: boolean
/** Pause flag — frozen countdown while set (consumer sets on hover/focus). */
paused: boolean
/** Optional per-toast politeness override; otherwise derived from `type`. */
ariaLive?: ToastPoliteness
/**
* Presence lifecycle for this toast (closed/opening/open/closing). Born
* `'open'`; a dismiss moves it to `'closing'` (when the toaster is animated)
* so it can play an exit animation before `animationEnd` removes it.
*/
status: PresenceStatus
}
ToasterInit from @llui/components/toast
export interface ToasterInit {
max?: number
placement?: ToastPlacement
/** Play an exit animation on dismiss (toasts go to `'closing'` and stay
* mounted until `animationEnd`). Default false — instant removal. */
animated?: boolean
}
ToasterParts from @llui/components/toast
export interface ToasterParts {
region: {
role: 'region'
'aria-label': string
tabindex: -1
'data-scope': 'toast'
'data-part': 'region'
'data-placement': Signal<ToastPlacement>
}
/**
* Build the per-row part descriptors for one toast. Takes the row's
* `Signal<Toast>` (e.g. the `item` from `each`) rather than a snapshot, so
* consumers don't `.peek()` in a reactive slot (which the signal compiler
* rejects). A toast's `id`/`type`/`ariaLive` are immutable for its lifetime —
* created then dismissed, never structurally replaced — so this reads the
* value once internally to build the id/role wiring; the keyed `each`
* rebuilds the row if `id` changes.
*/
toast: (toast: Signal<Toast>) => ToastItemParts
/**
* Reactive fraction (in [0,1]) of the countdown remaining for the toast with
* `id` — for a countdown progress bar. Sticky toasts report 1; a dismissed /
* missing toast reports 0.
*/
progress: (id: string) => Signal<number>
/**
* Reactive presence: whether the toast with `id` is still in the queue (i.e.
* should be mounted). Stays true through `'closing'` so the exit animation can
* play; flips false once `animationEnd` removes it. The keyed `each` over
* `toasts` already handles the actual mount/unmount — this is for consumers
* coordinating other elements off a single toast's lifecycle.
*/
isPresent: (id: string) => Signal<boolean>
}
ToasterState from @llui/components/toast
export interface ToasterState {
toasts: Toast[]
max: number
placement: ToastPlacement
/**
* Whether dismissed toasts play an exit animation. When true a dismiss moves
* the toast to `'closing'` (kept mounted) until `animationEnd` removes it;
* when false (default) dismiss removes synchronously — today's behavior, no
* wait for an animationend that won't fire.
*/
animated: boolean
}
ToastItemParts from @llui/components/toast
export interface ToastItemParts {
root: {
role: 'status' | 'alert'
'aria-atomic': 'true'
'aria-live': ToastPoliteness
id: string
'data-scope': 'toast'
'data-part': 'root'
'data-type': ToastType
'data-id': string
/** Reactive presence status (closed/opening/open/closing) for CSS-driven
* enter/exit animations. */
'data-state': Signal<PresenceStatus>
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onFocus: (e: FocusEvent) => void
onBlur: (e: FocusEvent) => void
/** Advance past the exit animation: a `'closing'` toast is removed from the
* queue once its animation/transition ends. */
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
title: {
id: string
'data-scope': 'toast'
'data-part': 'title'
}
description: {
id: string
'data-scope': 'toast'
'data-part': 'description'
}
closeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'toast'
'data-part': 'close-trigger'
onClick: (e: MouseEvent) => void
}
}
Constants
toast from @llui/components/toast
const toast
@llui/components/listbox
Functions
connect() from @llui/components/listbox
function connect(state: Signal<ListboxState>, send: Send<ListboxMsg>, opts: ConnectOptions): ListboxParts
init() from @llui/components/listbox
function init(opts: ListboxInit = {}): ListboxState
update() from @llui/components/listbox
function update(state: ListboxState, msg: ListboxMsg): [ListboxState, never[]]
Types
ListboxMsg from @llui/components/listbox
export type ListboxMsg =
/** @intent("Pick the option with the given value (toggles in multi-select)") */
| { type: 'select'; value: string }
/** @intent("Replace the selected values with the provided list") */
| { type: 'setValue'; value: string[] }
/** @intent("Clear all selected values") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'highlight'; index: number | null }
/** @humanOnly */
| { type: 'highlightNext' }
/** @humanOnly */
| { type: 'highlightPrev' }
/** @humanOnly */
| { type: 'highlightFirst' }
/** @humanOnly */
| { type: 'highlightLast' }
/** @intent("Pick the currently-highlighted option") */
| { type: 'selectHighlighted' }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @humanOnly */
| { type: 'typeahead'; char: string; now: number }
SelectionMode from @llui/components/listbox
Listbox — a list of selectable options. Supports single and multiple
selection, keyboard navigation (arrows, Home, End), typeahead, and
disabled items. Renders as role="listbox" with role="option" items.
export type SelectionMode = 'single' | 'multiple'
Interfaces
ConnectOptions from @llui/components/listbox
export interface ConnectOptions {
id: string
}
ListboxInit from @llui/components/listbox
export interface ListboxInit {
value?: string[]
items?: string[]
disabledItems?: string[]
disabled?: boolean
selectionMode?: SelectionMode
}
ListboxItemParts from @llui/components/listbox
export interface ListboxItemParts {
root: {
role: 'option'
id: string
'aria-selected': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
'data-state': Signal<'selected' | undefined>
'data-highlighted': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
'data-scope': 'listbox'
'data-part': 'item'
'data-value': string
'data-index': string
onClick: (e: MouseEvent) => void
onPointerMove: (e: PointerEvent) => void
}
}
ListboxParts from @llui/components/listbox
export interface ListboxParts {
root: {
role: 'listbox'
'aria-multiselectable': Signal<'true' | undefined>
'aria-disabled': Signal<'true' | undefined>
'aria-activedescendant': Signal<string | undefined>
tabindex: Signal<number>
id: string
'data-scope': 'listbox'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
onKeyDown: (e: KeyboardEvent) => void
}
item: (value: string, index: number) => ListboxItemParts
}
ListboxState from @llui/components/listbox
export interface ListboxState {
value: string[]
items: string[]
disabledItems: string[]
disabled: boolean
selectionMode: SelectionMode
highlightedIndex: number | null
typeahead: string
typeaheadExpiresAt: number
}
Constants
listbox from @llui/components/listbox
const listbox
@llui/components/select
Functions
connect() from @llui/components/select
function connect(state: Signal<SelectState>, send: Send<SelectMsg>, opts: ConnectOptions): SelectParts
init() from @llui/components/select
function init(opts: SelectInit = {}): SelectState
overlay() from @llui/components/select
function overlay(opts: OverlayOptions): Mountable
update() from @llui/components/select
function update(state: SelectState, msg: SelectMsg): [SelectState, never[]]
Types
SelectionMode from @llui/components/select
Select — a trigger button that opens a listbox dropdown. Value(s) are
visible on the trigger. Supports single or multiple selection.
Positioned relative to the trigger via @floating-ui/dom.
export type SelectionMode = 'single' | 'multiple'
SelectMsg from @llui/components/select
export type SelectMsg =
/** @intent("Open the select dropdown") */
| { type: 'open' }
/** @intent("Close the select dropdown") */
| { type: 'close' }
/** @intent("Toggle the select dropdown open/closed") */
| { type: 'toggle' }
/** @intent("Pick the option with the given value (toggles in multi-select)") */
| { type: 'selectOption'; value: string }
/** @intent("Replace the selected values with the provided list") */
| { type: 'setValue'; value: string[] }
/** @intent("Clear all selected values") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'highlight'; value: string | null }
/** @humanOnly */
| { type: 'highlightNext' }
/** @humanOnly */
| { type: 'highlightPrev' }
/** @humanOnly */
| { type: 'highlightFirst' }
/** @humanOnly */
| { type: 'highlightLast' }
/** @intent("Pick the currently-highlighted option") */
| { type: 'selectHighlighted' }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @humanOnly */
| { type: 'typeahead'; char: string; now: number }
Interfaces
ConnectOptions from @llui/components/select
export interface ConnectOptions {
id: string
/** Text to show in trigger when empty. */
placeholder?: string
/** Join multi-value labels with this separator. */
separator?: string
/**
* Native form-field name. When set, the `hiddenSelect` part becomes a real
* form control that submits the current selection under this name — render
* `<select {...parts.hiddenSelect}>` with one `<option {...parts.hiddenOption(v)}>`
* per item. Without a `name`, the hidden select carries no value into a form.
*/
name?: string
}
OverlayOptions from @llui/components/select
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<SelectState>
send: Send<SelectMsg>
parts: SelectParts
content: () => Renderable
/**
* Optional enter/leave transition for the select listbox (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, giving the raw-`open` select an exit
* animation for free. Omitted ⇒ the listbox closes synchronously as before.
*
* @example select.overlay({ state, send, parts, content, transition: fade({ duration: 120 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
/** Match content width to trigger width (default: true). */
sameWidth?: boolean
target?: string | HTMLElement
}
SelectGroup from @llui/components/select
A labelled section of options (rendered like <optgroup>). items are the
option VALUES belonging to the group, in visual order. Groups are an
additive, parallel structure: the flat items list always remains the
source of truth for navigation/highlight indices and item ids — when
groups is provided without an explicit items list, init derives the
flat list by concatenating each group's items in order. A plain flat
string[] (no groups) keeps working unchanged. Group LABELS are never
options, so highlight/typeahead/arrow navigation skips over them for free.
export interface SelectGroup {
id: string
label: string
items: string[]
}
SelectGroupParts from @llui/components/select
export interface SelectGroupParts {
group: {
role: 'group'
'aria-labelledby': string
'data-scope': 'select'
'data-part': 'group'
'data-group': string
}
groupLabel: {
id: string
'aria-hidden': 'true'
'data-scope': 'select'
'data-part': 'group-label'
'data-group': string
}
}
SelectInit from @llui/components/select
export interface SelectInit {
value?: string[]
items?: string[]
/** Optional labelled sections. When provided without `items`, the flat
* `items` list is derived by concatenating each group's `items` in order. */
groups?: SelectGroup[]
disabledItems?: string[]
selectionMode?: SelectionMode
disabled?: boolean
required?: boolean
}
SelectItemParts from @llui/components/select
export interface SelectItemParts {
item: {
role: 'option'
id: string
'aria-selected': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
'data-state': Signal<'selected' | undefined>
'data-highlighted': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
'data-scope': 'select'
'data-part': 'item'
'data-value': string
/** The option's live position in the flat item list (reactive — reused rows
* never report a stale index). */
'data-index': Signal<string>
onClick: (e: MouseEvent) => void
onPointerMove: (e: PointerEvent) => void
}
}
SelectParts from @llui/components/select
export interface SelectParts {
trigger: {
type: 'button'
role: 'combobox'
'aria-haspopup': 'listbox'
'aria-expanded': Signal<boolean>
'aria-controls': string
'aria-activedescendant': Signal<string | undefined>
'aria-disabled': Signal<'true' | undefined>
'aria-required': Signal<'true' | undefined>
id: string
disabled: Signal<boolean>
'data-state': Signal<'open' | 'closed'>
/** Present while the trigger is showing the PLACEHOLDER rather than a
* value. `valueText` already falls back to the placeholder string, but a
* string is not something CSS can branch on, so without this the
* placeholder renders at full foreground weight and reads as a real
* selection. This is the attribute every shadcn Select greys it from. */
'data-placeholder': Signal<'' | undefined>
'data-scope': 'select'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
positioner: {
'data-scope': 'select'
'data-part': 'positioner'
style: string
}
content: {
role: 'listbox'
id: string
'aria-multiselectable': Signal<'true' | undefined>
'aria-labelledby': string
tabindex: -1
'data-state': Signal<'open' | 'closed'>
'data-scope': 'select'
'data-part': 'content'
onKeyDown: (e: KeyboardEvent) => void
}
hiddenSelect: {
'aria-hidden': 'true'
tabindex: -1
style: string
/** Native form field name, or `undefined` when `name` was not supplied. */
name: string | undefined
disabled: Signal<boolean>
multiple: Signal<boolean>
required: Signal<boolean>
'data-scope': 'select'
'data-part': 'hidden-select'
}
/** An `<option>` for the hidden native `<select>`. Render one per item inside
* `hiddenSelect` so the browser submits the selection under the form `name`. */
hiddenOption: (value: string) => {
value: string
selected: Signal<boolean>
'data-scope': 'select'
'data-part': 'hidden-option'
}
/** Build the parts for an option by VALUE. The optional `index` is accepted
* for call-site convenience only — it is NOT used for identity (highlight,
* selection and ids are all value-keyed), so a reused row is never stale. */
item: (value: string, index?: number) => SelectItemParts
/** Parts for a labelled option group (`<optgroup>`-style section). Pass the
* group id; render the section element with `group` and its label element
* (referenced by `aria-labelledby`) with `groupLabel`. Group labels are not
* options, so navigation skips them automatically. */
group: (id: string) => SelectGroupParts
/** Selected value(s) — use for rendering the trigger label. */
valueText: Signal<string>
}
SelectState from @llui/components/select
export interface SelectState {
open: boolean
value: string[]
items: string[]
groups: SelectGroup[]
disabledItems: string[]
selectionMode: SelectionMode
/** The highlighted option's VALUE (not its index). Value-based identity keeps
* the highlight pinned to the right option when the list is filtered or
* reordered and rows are reused (value-keyed `each`). */
highlightedValue: string | null
disabled: boolean
required: boolean
typeahead: string
typeaheadExpiresAt: number
}
Constants
select from @llui/components/select
const select
@llui/components/combobox
Functions
connect() from @llui/components/combobox
function connect(state: Signal<ComboboxState>, send: Send<ComboboxMsg>, opts: ConnectOptions): ComboboxParts
init() from @llui/components/combobox
function init(opts: ComboboxInit = {}): ComboboxState
isCreateOption() from @llui/components/combobox
function isCreateOption(value: string): boolean
overlay() from @llui/components/combobox
function overlay(opts: OverlayOptions): Mountable
update() from @llui/components/combobox
function update(state: ComboboxState, msg: ComboboxMsg): [ComboboxState, ComboboxEffect[]]
Types
AsyncStatus from @llui/components/combobox
export type AsyncStatus = 'idle' | 'loading' | 'loaded' | 'error'
ComboboxEffect from @llui/components/combobox
Effects emitted by the combobox machine. Creation is owned by the consumer:
when a create sentinel is selected the machine surfaces the typed text as a
createOption effect rather than mutating its own value.
export type ComboboxEffect =
/** @intent("The user asked to create a brand-new option from the typed text") */
{ type: 'createOption'; value: string }
ComboboxMsg from @llui/components/combobox
export type ComboboxMsg =
/** @intent("Open the combobox dropdown") */
| { type: 'open' }
/** @intent("Close the combobox dropdown") */
| { type: 'close' }
/** @intent("Set the text input contents (re-runs the filter)") */
| { type: 'setInputValue'; value: string }
/** @intent("Pick the option with the given value (toggles in multi-select)") */
| { type: 'selectOption'; value: string }
/** @intent("Replace the selected values with the provided list") */
| { type: 'setValue'; value: string[] }
/** @intent("Clear all selected values and the input text") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'highlightNext' }
/** @humanOnly */
| { type: 'highlightPrev' }
/** @humanOnly */
| { type: 'highlightFirst' }
/** @humanOnly */
| { type: 'highlightLast' }
/** @humanOnly */
| { type: 'highlight'; value: string | null }
/** @intent("Pick the currently-highlighted option in the filtered list") */
| { type: 'selectHighlighted' }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @intent("Mark an async option fetch as started; pass the request's id") */
| { type: 'loadStart'; requestId: number }
/** @humanOnly */
| { type: 'loadSuccess'; requestId: number; items: string[] }
/** @humanOnly */
| { type: 'loadError'; requestId: number; error: string }
SelectionMode from @llui/components/combobox
Combobox — text input paired with a filtered listbox dropdown. User types to filter items, arrow keys navigate the filtered set, Enter selects. Supports single and multiple selection.
Beyond the sync filtered listbox the machine owns three additive surfaces:
- Async option loading —
status/requestId/errortrack an in-flight fetch. The consumer debounces (e.g.@llui/effectsdebounce) and runs the fetch itself, dispatchingloadStart/loadSuccess/loadErrortagged with a monotonically-increasingrequestId. The reducer DROPS anyloadSuccess/loadErrorwhoserequestIdis not the current one, so a late response from a superseded request can never clobber fresh state. The machine owns no timers. - Option groups —
groupsmirrorselect'sSelectGroupshape exactly. The flatitemslist stays the source of truth for navigation/highlight indices; group LABELS are never options, so arrow navigation skips them. - Creatable — opt-in
allowCreate. WheninputValueis non-empty and matches no item, a synthetic create sentinel is appended tofilteredItems. Selecting it emits acreateOptionEFFECT (carrying the typed text) so the consumer owns creation; the machine never mutatesvaluefor it.
export type SelectionMode = 'single' | 'multiple'
Interfaces
ComboboxGroup from @llui/components/combobox
A labelled section of options (rendered like <optgroup>). items are the
option VALUES belonging to the group, in visual order. Groups are an
additive, parallel structure: the flat items list always remains the
source of truth for navigation/highlight indices and item ids — when
groups is provided without an explicit items list, init derives the
flat list by concatenating each group's items in order. A plain flat
string[] (no groups) keeps working unchanged. Group LABELS are never
options, so highlight/arrow navigation skips over them for free.
Mirrors select's SelectGroup shape exactly.
export interface ComboboxGroup {
id: string
label: string
items: string[]
}
ComboboxGroupParts from @llui/components/combobox
export interface ComboboxGroupParts {
group: {
role: 'group'
'aria-labelledby': string
'data-scope': 'combobox'
'data-part': 'group'
'data-group': string
}
groupLabel: {
id: string
'aria-hidden': 'true'
'data-scope': 'combobox'
'data-part': 'group-label'
'data-group': string
}
}
ComboboxInit from @llui/components/combobox
export interface ComboboxInit {
value?: string[]
inputValue?: string
items?: string[]
/** Optional labelled sections. When provided without `items`, the flat
* `items` list is derived by concatenating each group's `items` in order. */
groups?: ComboboxGroup[]
disabledItems?: string[]
selectionMode?: SelectionMode
disabled?: boolean
/** Enable creatable mode: a synthetic create option is offered when the
* typed text matches no existing item. */
allowCreate?: boolean
}
ComboboxItemParts from @llui/components/combobox
export interface ComboboxItemParts {
item: {
role: 'option'
id: string
'aria-selected': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
'data-state': Signal<'selected' | undefined>
'data-highlighted': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
'data-create': '' | undefined
'data-scope': 'combobox'
'data-part': 'item'
'data-value': string
/** The option's live position in the FILTERED list (reactive — reused rows
* never report a stale index). */
'data-index': Signal<string>
onClick: (e: MouseEvent) => void
onPointerMove: (e: PointerEvent) => void
}
}
ComboboxParts from @llui/components/combobox
export interface ComboboxParts {
root: {
'data-scope': 'combobox'
'data-part': 'root'
'data-state': Signal<'open' | 'closed'>
}
input: {
type: 'text'
role: 'combobox'
autocomplete: 'off'
'aria-autocomplete': 'list'
'aria-expanded': Signal<boolean>
'aria-controls': string
'aria-activedescendant': Signal<string | undefined>
'aria-disabled': Signal<'true' | undefined>
id: string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'combobox'
'data-part': 'input'
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
}
trigger: {
type: 'button'
'aria-label': string
'aria-expanded': Signal<boolean>
'aria-controls': string
tabindex: -1
'data-scope': 'combobox'
'data-part': 'trigger'
onClick: (e: MouseEvent) => void
}
positioner: {
'data-scope': 'combobox'
'data-part': 'positioner'
style: string
}
content: {
role: 'listbox'
id: string
'aria-labelledby': string
'aria-busy': Signal<'true' | undefined>
tabindex: -1
'data-state': Signal<'open' | 'closed'>
'data-status': Signal<AsyncStatus>
'data-scope': 'combobox'
'data-part': 'content'
}
/** Build the parts for an option by VALUE. The optional `index` is accepted
* for call-site convenience only — it is NOT used for identity (highlight,
* selection and ids are all value-keyed), so a reused row is never stale. */
item: (value: string, index?: number) => ComboboxItemParts
/** Parts for a labelled option group (`<optgroup>`-style section). Pass the
* group id; render the section element with `group` and its label element
* (referenced by `aria-labelledby`) with `groupLabel`. Group labels are not
* options, so navigation skips them automatically. Mirrors `select`. */
group: (id: string) => ComboboxGroupParts
/** Polite live region announcing the result count / error to screen readers
* as the async filter resolves. Render a visually-hidden element with these
* attributes and the `text` signal as its content. */
liveRegion: {
role: 'status'
'aria-live': 'polite'
'aria-atomic': 'true'
'data-scope': 'combobox'
'data-part': 'live-region'
text: Signal<string>
}
empty: {
'data-scope': 'combobox'
'data-part': 'empty'
}
}
ComboboxState from @llui/components/combobox
export interface ComboboxState {
open: boolean
value: string[]
inputValue: string
items: string[]
groups: ComboboxGroup[]
disabledItems: string[]
filteredItems: string[]
/** The highlighted option's VALUE (not its index) in the FILTERED list.
* Value-based identity keeps the highlight pinned to the right option as the
* list is filtered/reordered and value-keyed rows are reused. May hold the
* create sentinel ({@link CREATE_OPTION_VALUE}) in creatable mode. */
highlightedValue: string | null
selectionMode: SelectionMode
disabled: boolean
allowCreate: boolean
status: AsyncStatus
requestId: number
error: string | null
}
ConnectOptions from @llui/components/combobox
export interface ConnectOptions {
id: string
triggerLabel?: string
}
OverlayOptions from @llui/components/combobox
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<ComboboxState>
send: Send<ComboboxMsg>
parts: ComboboxParts
content: () => Renderable
/**
* Optional enter/leave transition for the combobox listbox (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, giving the raw-`open` combobox an exit
* animation for free. Omitted ⇒ the listbox closes synchronously as before.
*
* @example combobox.overlay({ state, send, parts, content, transition: fade({ duration: 120 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
sameWidth?: boolean
target?: string | HTMLElement
}
Constants
combobox from @llui/components/combobox
const combobox
CREATE_OPTION_VALUE from @llui/components/combobox
Sentinel value used for the synthetic "create" option appended to
filteredItems in creatable mode. It is intentionally a value that no real
option will ever carry. Render it specially (the data-create part flag),
and treat a selection of it as a create request, not a normal pick.
const CREATE_OPTION_VALUE
@llui/components/hover-card
Functions
connect() from @llui/components/hover-card
function connect(state: Signal<HoverCardState>, send: Send<HoverCardMsg>, opts: ConnectOptions): HoverCardParts
init() from @llui/components/hover-card
function init(opts: HoverCardInit = {}): HoverCardState
isMounted() from @llui/components/hover-card
Whether the hover-card node should be in the DOM — true through the exit animation.
function isMounted(state: HoverCardState): boolean
isPresent() from @llui/components/hover-card
Alias of {@link isMounted} — whether the hover-card is currently present in the DOM.
function isPresent(state: HoverCardState): boolean
overlay() from @llui/components/hover-card
function overlay(opts: OverlayOptions): Mountable
update() from @llui/components/hover-card
function update(state: HoverCardState, msg: HoverCardMsg): [HoverCardState, never[]]
Types
HoverCardMsg from @llui/components/hover-card
export type HoverCardMsg =
| { type: 'show' }
| { type: 'hide' }
| { type: 'setOpen'; open: boolean }
/** @humanOnly */
| { type: 'animationEnd' }
/** @humanOnly */
| { type: 'transitionEnd' }
Interfaces
ConnectOptions from @llui/components/hover-card
export interface ConnectOptions {
id: string
/** ms before showing on hover (default: 700). */
openDelay?: number
/** ms before hiding after pointer leaves (default: 300). */
closeDelay?: number
}
HoverCardInit from @llui/components/hover-card
export interface HoverCardInit {
open?: boolean
/** Skip enter/exit animations — hide unmounts synchronously (default: true). */
skipAnimations?: boolean
}
HoverCardParts from @llui/components/hover-card
export interface HoverCardParts {
trigger: {
id: string
'aria-controls': string
'aria-expanded': Signal<boolean>
'data-state': Signal<'open' | 'closed'>
'data-scope': 'hover-card'
'data-part': 'trigger'
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onFocus: (e: FocusEvent) => void
onBlur: (e: FocusEvent) => void
}
positioner: {
'data-scope': 'hover-card'
'data-part': 'positioner'
style: string
}
content: {
id: string
'data-state': Signal<PresenceStatus>
'data-scope': 'hover-card'
'data-part': 'content'
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
arrow: {
'data-scope': 'hover-card'
'data-part': 'arrow'
}
}
HoverCardState from @llui/components/hover-card
Hover card — richer tooltip-like popup triggered by hover or focus.
Unlike tooltip, it uses role="dialog" (not role="tooltip") and
allows interactive content. Content can be hovered without closing.
export interface HoverCardState {
open: boolean
/** Presence lifecycle — drives data-state and keeps the node mounted through exit animations. */
status: PresenceStatus
/** When true, hide transitions go straight to 'closed' (no exit-animation wait). */
skipAnimations: boolean
}
OverlayOptions from @llui/components/hover-card
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<HoverCardState>
send: Send<HoverCardMsg>
parts: HoverCardParts
content: () => Renderable
/**
* Optional enter/leave transition for the hover-card content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example hoverCard.overlay({ state, send, parts, content, transition: fade({ duration: 150 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
target?: string | HTMLElement
arrowSelector?: string
}
Constants
hoverCard from @llui/components/hover-card
const hoverCard
@llui/components/avatar
Functions
connect() from @llui/components/avatar
function connect(state: Signal<AvatarState>, send: Send<AvatarMsg>, opts: ConnectOptions = {}): AvatarParts
init() from @llui/components/avatar
function init(opts: AvatarInit = {}): AvatarState
update() from @llui/components/avatar
function update(state: AvatarState, msg: AvatarMsg): [AvatarState, never[]]
Types
AvatarMsg from @llui/components/avatar
export type AvatarMsg =
/** @humanOnly */
| { type: 'loadStart' }
/** @humanOnly */
| { type: 'loaded' }
/** @humanOnly */
| { type: 'error' }
/** @intent("Reset the avatar's load status back to idle") */
| { type: 'reset' }
ImageStatus from @llui/components/avatar
Avatar — image with automatic fallback. Tracks image load status so consumers can render the image, a fallback (initials, icon), or a loading placeholder.
export type ImageStatus = 'idle' | 'loading' | 'loaded' | 'error'
Interfaces
AvatarInit from @llui/components/avatar
export interface AvatarInit {
status?: ImageStatus
}
AvatarParts from @llui/components/avatar
export interface AvatarParts {
root: {
'data-scope': 'avatar'
'data-part': 'root'
'data-status': Signal<ImageStatus>
}
image: {
'data-scope': 'avatar'
'data-part': 'image'
'data-status': Signal<ImageStatus>
hidden: Signal<boolean>
alt: string
onLoad: (e: Event) => void
onError: (e: Event) => void
onLoadStart: (e: Event) => void
}
fallback: {
'data-scope': 'avatar'
'data-part': 'fallback'
'data-status': Signal<ImageStatus>
hidden: Signal<boolean>
'aria-hidden': Signal<'true' | undefined>
}
}
AvatarState from @llui/components/avatar
export interface AvatarState {
status: ImageStatus
}
ConnectOptions from @llui/components/avatar
export interface ConnectOptions {
alt?: string
}
Constants
avatar from @llui/components/avatar
const avatar
@llui/components/clipboard
Functions
connect() from @llui/components/clipboard
function connect(state: Signal<ClipboardState>, send: Send<ClipboardMsg>, opts: ConnectOptions = {}): ClipboardParts
copyToClipboard() from @llui/components/clipboard
Attempt to copy the value to the clipboard. Returns a Promise that resolves
on success. Consumer dispatches copied or reset based on the result.
function copyToClipboard(value: string): Promise<void>
init() from @llui/components/clipboard
function init(opts: ClipboardInit = {}): ClipboardState
update() from @llui/components/clipboard
function update(state: ClipboardState, msg: ClipboardMsg): [ClipboardState, never[]]
Types
ClipboardMsg from @llui/components/clipboard
export type ClipboardMsg =
/** @intent("Update the value to be copied") */
| { type: 'setValue'; value: string }
/** @intent("Initiate a clipboard copy of the current value") */
| { type: 'copy' }
/** @humanOnly */
| { type: 'copied' }
/** @intent("Clear the transient \"copied\" feedback state") */
| { type: 'reset' }
Interfaces
ClipboardInit from @llui/components/clipboard
export interface ClipboardInit {
value?: string
}
ClipboardParts from @llui/components/clipboard
export interface ClipboardParts {
root: {
'data-scope': 'clipboard'
'data-part': 'root'
'data-copied': Signal<'' | undefined>
}
trigger: {
type: 'button'
'aria-label': string
'data-scope': 'clipboard'
'data-part': 'trigger'
'data-copied': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
}
input: {
type: 'text'
readonly: true
value: Signal<string>
'data-scope': 'clipboard'
'data-part': 'input'
onFocus: (e: FocusEvent) => void
}
indicator: {
'data-scope': 'clipboard'
'data-part': 'indicator'
'data-copied': Signal<'' | undefined>
'aria-live': 'polite'
}
}
ClipboardState from @llui/components/clipboard
Clipboard — copy-to-clipboard with transient "copied" feedback. The actual clipboard write is performed by the consumer via an effect (or inline in the trigger's onClick handler). Reducer tracks the success state flag and an auto-reset timestamp.
export interface ClipboardState {
value: string
copied: boolean
}
ConnectOptions from @llui/components/clipboard
export interface ConnectOptions {
copyLabel?: string
onCopy?: (value: string) => void
}
Constants
clipboard from @llui/components/clipboard
const clipboard
@llui/components/editable
Functions
connect() from @llui/components/editable
function connect(state: Signal<EditableState>, send: Send<EditableMsg>, opts: ConnectOptions = {}): EditableParts
init() from @llui/components/editable
function init(opts: EditableInit = {}): EditableState
update() from @llui/components/editable
function update(state: EditableState, msg: EditableMsg): [EditableState, never[]]
Types
EditableMsg from @llui/components/editable
export type EditableMsg =
/** @intent("Enter edit mode (seeds the draft from the current value)") */
| { type: 'edit' }
/** @intent("Update the in-progress draft as the user types") */
| { type: 'setDraft'; draft: string }
/** @intent("Commit the draft as the new value and exit edit mode") */
| { type: 'submit' }
/** @intent("Discard the draft and exit edit mode without changing the value") */
| { type: 'cancel' }
/** @intent("Set the value directly without going through edit mode") */
| { type: 'setValue'; value: string }
Interfaces
ConnectOptions from @llui/components/editable
export interface ConnectOptions {
/** Activate edit mode on preview focus (default: false — requires click). */
activateOnFocus?: boolean
/** Submit on blur (default: true). False = blur cancels. */
submitOnBlur?: boolean
/** Validate the draft text before committing. Non-empty array blocks submit. */
validate?: (value: string) => string[] | null
}
EditableInit from @llui/components/editable
export interface EditableInit {
value?: string
editing?: boolean
disabled?: boolean
}
EditableParts from @llui/components/editable
export interface EditableParts {
root: {
'data-scope': 'editable'
'data-part': 'root'
'data-editing': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
}
preview: {
tabindex: Signal<number>
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'editable'
'data-part': 'preview'
hidden: Signal<boolean>
onClick: (e: MouseEvent) => void
onFocus: (e: FocusEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
input: {
'data-scope': 'editable'
'data-part': 'input'
hidden: Signal<boolean>
value: Signal<string>
disabled: Signal<boolean>
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
onBlur: (e: FocusEvent) => void
}
submitTrigger: {
type: 'button'
'data-scope': 'editable'
'data-part': 'submit-trigger'
onClick: (e: MouseEvent) => void
}
cancelTrigger: {
type: 'button'
'data-scope': 'editable'
'data-part': 'cancel-trigger'
onClick: (e: MouseEvent) => void
}
editTrigger: {
type: 'button'
'data-scope': 'editable'
'data-part': 'edit-trigger'
onClick: (e: MouseEvent) => void
}
}
EditableState from @llui/components/editable
Editable — inline text editor. Click preview to enter edit mode, Enter
to commit, Escape to cancel. Reports the committed value via onSubmit.
export interface EditableState {
value: string
editing: boolean
draft: string
disabled: boolean
}
Constants
editable from @llui/components/editable
const editable
@llui/components/tags-input
Functions
connect() from @llui/components/tags-input
function connect(state: Signal<TagsInputState>, send: Send<TagsInputMsg>, opts: ConnectOptions = {}): TagsInputParts
init() from @llui/components/tags-input
function init(opts: TagsInputInit = {}): TagsInputState
update() from @llui/components/tags-input
function update(state: TagsInputState, msg: TagsInputMsg): [TagsInputState, never[]]
Types
TagsInputMsg from @llui/components/tags-input
export type TagsInputMsg =
/** @intent("Update the in-progress text in the input field") */
| { type: 'setInput'; value: string }
/** @intent("Commit a new tag (defaults to the current input value)") */
| { type: 'addTag'; value?: string }
/** @intent("Remove the tag at the given index") */
| { type: 'removeTag'; index: number }
/** @intent("Remove the last tag (typically backspace on empty input)") */
| { type: 'removeLast' }
/** @intent("Replace the full tag list with the provided values") */
| { type: 'setValue'; value: string[] }
/** @humanOnly */
| { type: 'focusTag'; index: number | null }
/** @humanOnly */
| { type: 'focusTagNext' }
/** @humanOnly */
| { type: 'focusTagPrev' }
/** @intent("Remove every tag and reset the input") */
| { type: 'clearAll' }
Interfaces
ConnectOptions from @llui/components/tags-input
export interface ConnectOptions {
inputLabel?: string
removeLabel?: string
clearLabel?: string
/** Characters that commit the current input as a tag (default: [',']). */
delimiters?: string[]
/** Commit on blur (default: true). */
commitOnBlur?: boolean
/** Validate a tag value before adding. Non-empty array blocks addTag. */
validate?: (value: string) => string[] | null
}
TagItemParts from @llui/components/tags-input
export interface TagItemParts {
root: {
tabindex: Signal<number>
'data-scope': 'tags-input'
'data-part': 'tag'
'data-value': string
'data-index': string
'data-focused': Signal<'' | undefined>
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
}
remove: {
type: 'button'
'aria-label': string
tabindex: -1
'data-scope': 'tags-input'
'data-part': 'tag-remove'
onClick: (e: MouseEvent) => void
}
}
TagsInputInit from @llui/components/tags-input
export interface TagsInputInit {
value?: string[]
inputValue?: string
disabled?: boolean
max?: number
unique?: boolean
}
TagsInputParts from @llui/components/tags-input
export interface TagsInputParts {
root: {
role: 'group'
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'tags-input'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
input: {
type: 'text'
autocomplete: 'off'
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'tags-input'
'data-part': 'input'
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
onBlur: (e: FocusEvent) => void
}
tag: (value: string, index: number) => TagItemParts
clearTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'tags-input'
'data-part': 'clear-trigger'
onClick: (e: MouseEvent) => void
}
}
TagsInputState from @llui/components/tags-input
Tags input — text input that creates chips (tags) on commit keys (Enter, comma, blur). Backspace on empty input removes the last tag. Each tag is focusable via arrow keys.
export interface TagsInputState {
value: string[]
inputValue: string
disabled: boolean
/** Maximum tag count. 0 = unlimited. */
max: number
/** Only allow unique values. */
unique: boolean
/** Currently-focused tag index, or null. */
focusedIndex: number | null
}
Constants
tagsInput from @llui/components/tags-input
const tagsInput
@llui/components/splitter
Functions
connect() from @llui/components/splitter
function connect(state: Signal<SplitterState>, send: Send<SplitterMsg>): SplitterParts
init() from @llui/components/splitter
function init(opts: SplitterInit = {}): SplitterState
positionFromPoint() from @llui/components/splitter
Compute position percentage from a pointer event within a container rect.
function positionFromPoint(state: SplitterState, rect: DOMRect, clientX: number, clientY: number): number
update() from @llui/components/splitter
function update(state: SplitterState, msg: SplitterMsg): [SplitterState, never[]]
Types
Orientation from @llui/components/splitter
Splitter — resizable panes with a draggable handle. The handle's position is expressed as a percentage of the container, stored as a number 0..100. Supports keyboard arrow resize with a configurable step.
export type Orientation = 'horizontal' | 'vertical'
SplitterMsg from @llui/components/splitter
export type SplitterMsg =
/** @intent("Set the splitter handle position (0–100, clamped to min/max)") */
| { type: 'setPosition'; position: number }
/** @intent("Move the handle by step (or step × multiplier) toward max") */
| { type: 'increment'; multiplier?: number }
/** @intent("Move the handle by step (or step × multiplier) toward min") */
| { type: 'decrement'; multiplier?: number }
/** @intent("Snap the handle to its minimum position") */
| { type: 'toMin' }
/** @intent("Snap the handle to its maximum position") */
| { type: 'toMax' }
/** @humanOnly */
| { type: 'startDrag' }
/** @humanOnly */
| { type: 'endDrag' }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
Interfaces
SplitterInit from @llui/components/splitter
export interface SplitterInit {
position?: number
min?: number
max?: number
step?: number
orientation?: Orientation
disabled?: boolean
dir?: 'ltr' | 'rtl'
}
SplitterParts from @llui/components/splitter
export interface SplitterParts {
root: {
'data-scope': 'splitter'
'data-part': 'root'
'data-orientation': Signal<Orientation>
'data-disabled': Signal<'' | undefined>
'data-dragging': Signal<'' | undefined>
}
primaryPanel: {
'data-scope': 'splitter'
'data-part': 'primary-panel'
style: Signal<string>
}
secondaryPanel: {
'data-scope': 'splitter'
'data-part': 'secondary-panel'
style: Signal<string>
}
resizeTrigger: {
role: 'separator'
'aria-orientation': Signal<Orientation>
'aria-valuemin': Signal<number>
'aria-valuemax': Signal<number>
'aria-valuenow': Signal<number>
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'splitter'
'data-part': 'resize-trigger'
'data-orientation': Signal<Orientation>
tabindex: Signal<number>
onKeyDown: (e: KeyboardEvent) => void
onPointerDown: (e: PointerEvent) => void
}
}
SplitterState from @llui/components/splitter
export interface SplitterState {
position: number
min: number
max: number
step: number
orientation: Orientation
disabled: boolean
dragging: boolean
/** Reading direction. Under 'rtl' horizontal arrow keys are flipped. */
dir: 'ltr' | 'rtl'
}
Constants
splitter from @llui/components/splitter
const splitter
@llui/components/file-upload
Functions
acceptToString() from @llui/components/file-upload
Serialize an AcceptValue into a comma-joined string suitable for the
HTML accept attribute. Both MIME types and extensions are emitted.
function acceptToString(accept: AcceptValue): string
connect() from @llui/components/file-upload
function connect(state: Signal<FileUploadState>, send: Send<FileUploadMsg>, opts: ConnectOptions): FileUploadParts
effectiveMaxFiles() from @llui/components/file-upload
The count limit actually in force: a single-select uploader accepts exactly
one file whatever maxFiles says. Without this, dropping three files on a
multiple: false zone accepted all three with zero rejections (#119).
function effectiveMaxFiles(state: FileUploadState): number
fileMatchesAccept() from @llui/components/file-upload
Check whether a file matches the accept configuration. Raw-string accept is passed through to the browser picker so we always return true here; MIME-object accept is validated by checking MIME type (with wildcards) and extension membership.
function fileMatchesAccept(file: FileLike, accept: AcceptValue): boolean
getFile() from @llui/components/file-upload
The live handle for a tracked file, or undefined once released.
function getFile(ref: FileMeta | string): File | undefined
init() from @llui/components/file-upload
function init(opts: FileUploadInit = {}): FileUploadState
preventDocumentDrop() from @llui/components/file-upload
Install a document-level dragover/drop blocker. Without this, dragging a file outside the dropzone causes the browser to navigate away from the page. Call from onMount and invoke the returned disposer on unmount.
function preventDocumentDrop(): () => void
releaseAllFiles() from @llui/components/file-upload
Release every handle state references. What a component owes at unmount.
function releaseAllFiles(state: FileUploadState): void
releaseDropped() from @llui/components/file-upload
Release the handles prev referenced and next does not. Pure bookkeeping
over two states, so it covers every transition that drops a file without
naming it (single-select replacement, a rejected list overwritten by the next
selection).
It CANNOT cover a message whose handles never reached state at all: it only
sees ids referenced by prev, and a gated addFiles was tracked before the
send. Those are releaseUnlanded's job — read it before assuming this one
function is enough (#138 review).
function releaseDropped(prev: FileUploadState, next: FileUploadState): void
releaseFile() from @llui/components/file-upload
function releaseFile(ref: FileMeta | string): void
releaseFiles() from @llui/components/file-upload
function releaseFiles(refs: readonly (FileMeta | string)[]): void
releaseUnlanded() from @llui/components/file-upload
Release the handles msg carried that next does not reference. A disabled
or readonly gate returns the state unchanged, so the files a drop tracked
are unreachable from any state the moment update returns — releaseDropped
cannot see them, and they leaked live File objects (and the blobs behind
them) for the lifetime of the page (#138 review, blocking 1).
function releaseUnlanded(msg: FileUploadMsg, next: FileUploadState): void
totalSize() from @llui/components/file-upload
function totalSize(state: FileUploadState): number
trackedFileCount() from @llui/components/file-upload
How many live handles the registry currently holds. A leak probe: a handle no State references any more is invisible from every other angle, which is exactly the shape a leak takes here.
function trackedFileCount(): number
trackFile() from @llui/components/file-upload
Register a live File and return the serializable record that goes in State.
function trackFile(file: File): FileMeta
trackFiles() from @llui/components/file-upload
function trackFiles(files: readonly File[]): FileMeta[]
update() from @llui/components/file-upload
function update(state: FileUploadState, msg: FileUploadMsg): [FileUploadState, never[]]
validateFiles() from @llui/components/file-upload
Partition incoming files into accepted and rejected based on state's accept/size/count constraints. The current accepted-file count is used to enforce the count limit — the caller is responsible for passing the post-combine accepted total when appending.
function validateFiles(incoming: FileMeta[], state: FileUploadState, existingAcceptedCount: number): { accepted: FileMeta[]; rejected: RejectedFile[] }
Types
AcceptValue from @llui/components/file-upload
File upload — input element + drag-and-drop zone. Tracks selected files, drag state, accept filters, validation errors. Multiple or single selection.
accept can be either a raw HTML-accept string ("image/*,.pdf") or a
MIME-object ({ 'image/*': ['.png', '.jpg'], 'application/pdf': [] }).
The object form is validated client-side per file; the raw string form
only drives the browser's native picker filter.
Files that fail validation (too large, too small, wrong type, over the
count limit) flow into rejectedFiles with a list of FileError codes
attached. The view can render them alongside accepted files.
State holds FileMeta records — plain JSON — never the live File
objects: State must be JSON-serializable (CLAUDE.md), and a File came
back from a round-trip as {}, wiping every name and turning totalSize
into NaN (#119). The handles live in a module-scoped registry keyed by
FileMeta.id; see trackFile/getFile/releaseDropped.
A restored State has no handles. Serializability is exactly what makes
that so: SSR hydration, replayTrace and an agent state snapshot carry the
FileMeta records and nothing else, so after a restore getFile() returns
undefined for every one of them. That is correct and unavoidable — a File
cannot cross the wire — but it is invisible unless you are told, so a view
must treat a missing handle as normal (render the metadata, skip the object-URL
preview) and a re-upload needs a fresh selection from the user.
export type AcceptValue = string | Record<string, string[]>
FileError from @llui/components/file-upload
export type FileError =
| { code: 'TOO_LARGE'; max: number }
| { code: 'TOO_SMALL'; min: number }
| { code: 'INVALID_TYPE' }
| { code: 'TOO_MANY'; max: number }
| { code: 'CUSTOM'; message: string }
FileUploadMsg from @llui/components/file-upload
export type FileUploadMsg =
/** @humanOnly */
| { type: 'setFiles'; files: FileMeta[]; customRejected?: RejectedFile[] }
/** @humanOnly */
| { type: 'addFiles'; files: FileMeta[]; customRejected?: RejectedFile[] }
/** @intent("Remove the accepted file at the given index") */
| { type: 'removeFile'; index: number }
/** @intent("Remove the rejected file at the given index") */
| { type: 'removeRejected'; index: number }
/** @intent("Clear all accepted files") */
| { type: 'clear' }
/** @intent("Clear the rejected-files list") */
| { type: 'clearRejected' }
/** @humanOnly */
| { type: 'dragEnter' }
/** @humanOnly */
| { type: 'dragLeave' }
/** @humanOnly */
| { type: 'drop' }
/** @humanOnly */
| { type: 'setInvalid'; invalid: boolean }
Interfaces
ConnectOptions from @llui/components/file-upload
export interface ConnectOptions {
id: string
removeLabel?: string
clearLabel?: string
/**
* Hints the browser to use the device camera/microphone for capture. Only
* applies to mobile. Pass `'user'` for the front camera, `'environment'`
* for the back, or `true` to accept either.
*/
capture?: 'user' | 'environment' | boolean
/** Show a directory-picker instead of a file-picker (webkit only). */
directory?: boolean
/**
* Per-file synchronous validator. Return a non-empty array of `FileError`
* codes to reject the file, or null/empty to accept. Runs in addition to
* the state-driven accept/size/count checks — its errors accumulate into
* `rejectedFiles` alongside the built-in errors.
*/
validate?: (file: File) => FileError[] | null
/**
* Optional transform pipeline. Runs before validation. Can return a
* Promise; onChange awaits it before dispatching. Use for image resizing,
* format conversion, etc.
*/
transformFiles?: (files: File[]) => File[] | Promise<File[]>
}
FileLike from @llui/components/file-upload
Everything fileMatchesAccept needs — satisfied by both File and FileMeta.
export interface FileLike {
name: string
type: string
}
FileMeta from @llui/components/file-upload
The serializable half of a selected file. id is the registry key for the
live handle; the rest mirrors the File fields a view needs.
export interface FileMeta {
id: string
name: string
size: number
type: string
lastModified: number
}
FileUploadInit from @llui/components/file-upload
export interface FileUploadInit {
files?: FileMeta[]
disabled?: boolean
multiple?: boolean
accept?: AcceptValue
maxFiles?: number
maxSize?: number
minFileSize?: number
required?: boolean
readonly?: boolean
invalid?: boolean
}
FileUploadItemParts from @llui/components/file-upload
export interface FileUploadItemParts {
item: {
'data-scope': 'file-upload'
'data-part': 'item'
'data-index': string
}
itemName: {
'data-scope': 'file-upload'
'data-part': 'item-name'
}
itemSizeText: {
'data-scope': 'file-upload'
'data-part': 'item-size-text'
}
itemPreview: {
'data-scope': 'file-upload'
'data-part': 'item-preview'
}
removeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'file-upload'
'data-part': 'item-remove'
onClick: (e: MouseEvent) => void
}
/** Zag-aligned alias for removeTrigger. Same wiring. */
itemDeleteTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'file-upload'
'data-part': 'item-delete-trigger'
onClick: (e: MouseEvent) => void
}
}
FileUploadParts from @llui/components/file-upload
export interface FileUploadParts {
root: {
'data-scope': 'file-upload'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
'data-dragging': Signal<'' | undefined>
'data-invalid': Signal<'' | undefined>
'data-readonly': Signal<'' | undefined>
}
dropzone: {
'data-scope': 'file-upload'
'data-part': 'dropzone'
'data-dragging': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
onDragEnter: (e: DragEvent) => void
onDragOver: (e: DragEvent) => void
onDragLeave: (e: DragEvent) => void
onDrop: (e: DragEvent) => void
}
trigger: {
type: 'button'
'data-scope': 'file-upload'
'data-part': 'trigger'
disabled: Signal<boolean>
onClick: (e: MouseEvent) => void
}
hiddenInput: {
type: 'file'
tabindex: -1
'aria-hidden': 'true'
style: string
disabled: Signal<boolean>
multiple: Signal<boolean>
accept: Signal<string>
required: Signal<boolean>
'aria-invalid': Signal<'true' | undefined>
capture?: string | boolean
webkitdirectory?: '' | undefined
'data-scope': 'file-upload'
'data-part': 'hidden-input'
id: string
onChange: (e: Event) => void
}
label: {
for: string
'data-scope': 'file-upload'
'data-part': 'label'
}
clearTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'file-upload'
'data-part': 'clear-trigger'
onClick: (e: MouseEvent) => void
}
itemGroup: {
'data-scope': 'file-upload'
'data-part': 'item-group'
}
item: (index: number) => FileUploadItemParts
}
FileUploadState from @llui/components/file-upload
export interface FileUploadState {
files: FileMeta[]
rejectedFiles: RejectedFile[]
disabled: boolean
multiple: boolean
accept: AcceptValue
maxFiles: number
maxSize: number
minFileSize: number
required: boolean
readonly: boolean
invalid: boolean
/** `dragDepth > 0`, materialized so views bind one boolean. */
dragging: boolean
/**
* Nesting depth of the in-flight drag. `dragenter`/`dragleave` both bubble
* and fire in the order enter@child → leave@parent, so a plain boolean flips
* off while the pointer is still inside the dropzone (#119).
*/
dragDepth: number
}
RejectedFile from @llui/components/file-upload
export interface RejectedFile {
file: FileMeta
errors: FileError[]
}
Constants
fileUpload from @llui/components/file-upload
const fileUpload
@llui/components/tree-view
Functions
connect() from @llui/components/tree-view
function connect(state: Signal<TreeViewState>, send: Send<TreeViewMsg>, opts: ConnectOptions): TreeViewParts
init() from @llui/components/tree-view
function init(opts: TreeViewInit = {}): TreeViewState
isChecked() from @llui/components/tree-view
function isChecked(state: TreeViewState, id: string): boolean
isExpanded() from @llui/components/tree-view
function isExpanded(state: TreeViewState, id: string): boolean
isIndeterminate() from @llui/components/tree-view
function isIndeterminate(state: TreeViewState, id: string): boolean
isLoaded() from @llui/components/tree-view
function isLoaded(state: TreeViewState, id: string): boolean
isLoadFailed() from @llui/components/tree-view
function isLoadFailed(state: TreeViewState, id: string): boolean
isLoading() from @llui/components/tree-view
function isLoading(state: TreeViewState, id: string): boolean
isRenaming() from @llui/components/tree-view
function isRenaming(state: TreeViewState, id: string): boolean
isSelected() from @llui/components/tree-view
function isSelected(state: TreeViewState, id: string): boolean
update() from @llui/components/tree-view
function update(state: TreeViewState, msg: TreeViewMsg): [TreeViewState, TreeViewEffect[]]
Types
SelectionMode from @llui/components/tree-view
Tree view — hierarchical list with expand/collapse. Items are identified by opaque string ids; the tree structure (children relationship) is provided externally. The machine tracks which branches are expanded, which items are selected, and which item has keyboard focus.
export type SelectionMode = 'single' | 'multiple' | 'checkbox'
TreeViewEffect from @llui/components/tree-view
Effects emitted by the tree-view machine for the consumer's onEffect.
export type TreeViewEffect =
/** Fetch the children of `id` lazily, then reply with `childrenLoaded`/`childrenLoadFailed`. */
{ type: 'loadChildren'; id: string }
TreeViewMsg from @llui/components/tree-view
export type TreeViewMsg =
/** @intent("Toggle the branch with the given id expanded/collapsed") */
| { type: 'toggleBranch'; id: string }
/** @intent("Expand the branch with the given id") */
| { type: 'expand'; id: string }
/** @intent("Collapse the branch with the given id") */
| { type: 'collapse'; id: string }
/** @intent("Expand every branch in the provided id list") */
| { type: 'expandAll'; ids: string[] }
/** @intent("Collapse every expanded branch") */
| { type: 'collapseAll' }
/** @intent("Select the item with the given id (additive=true extends multi-selection)") */
| { type: 'select'; id: string; additive?: boolean }
/** @intent("Replace the selected-id set with the provided list") */
| { type: 'setSelected'; ids: string[] }
/** @humanOnly */
| { type: 'focus'; id: string | null }
/** @humanOnly */
| { type: 'focusNext' }
/** @humanOnly */
| { type: 'focusPrev' }
/** @humanOnly */
| { type: 'focusFirst' }
/** @humanOnly */
| { type: 'focusLast' }
/** @humanOnly */
| { type: 'setVisibleItems'; ids: string[]; labels?: string[] }
/** @humanOnly */
| { type: 'typeahead'; char: string; now: number }
/** @humanOnly */
| { type: 'arrowLeftFrom'; id: string; isBranch: boolean; parentId: string | null }
/** @humanOnly */
| { type: 'arrowRightFrom'; id: string }
/** @intent("Toggle the checkbox on the item with the given id (descendantIds drives recursive check)") */
| { type: 'toggleChecked'; id: string; descendantIds?: string[] }
/** @intent("Replace the checked-id set with the provided list") */
| { type: 'setChecked'; ids: string[] }
/** @humanOnly */
| { type: 'setIndeterminate'; ids: string[] }
/** @intent("Begin renaming the item with the given id (seeds the rename input with `initial`)") */
| { type: 'renameStart'; id: string; initial: string }
/** @intent("Update the rename draft as the user types") */
| { type: 'renameChange'; value: string }
/** @intent("Commit the in-progress rename (clears the rename state)") */
| { type: 'renameCommit' }
/** @intent("Cancel the in-progress rename without applying changes") */
| { type: 'renameCancel' }
/** @intent("Mark the branch with the given id as loading children (typically before an async fetch)") */
| { type: 'loadingStart'; id: string }
/** @intent("Clear the loading state for the given branch id (after async fetch completes)") */
| { type: 'loadingEnd'; id: string }
/** @intent("Replace the whole tree structure (adjacency record + root ids)") */
| { type: 'setNodes'; nodes: Record<string, TreeNodeMeta>; roots: string[] }
/** @intent("Supply the lazily-loaded children of branch `id` (clears loading, marks loaded)") */
| { type: 'childrenLoaded'; id: string; items: TreeNodeInput[] }
/** @intent("Report that the lazy load of branch `id` failed (allows retry on re-expand)") */
| { type: 'childrenLoadFailed'; id: string }
Interfaces
ConnectOptions from @llui/components/tree-view
export interface ConnectOptions {
id: string
/**
* If true, clicking anywhere on a branch item (not just the disclosure
* caret) toggles its expanded state. Default: false — clicks on the row
* select it without toggling, consistent with most file-tree UIs.
*/
expandOnClick?: boolean
}
TreeItemParts from @llui/components/tree-view
export interface TreeItemParts {
item: {
role: 'treeitem'
id: string
'aria-expanded': Signal<boolean | undefined>
'aria-selected': Signal<boolean | undefined>
'aria-level': number
'aria-busy': Signal<'true' | undefined>
tabindex: Signal<number>
'data-scope': 'tree-view'
'data-part': 'item'
'data-value': string
'data-depth': string
'data-selected': Signal<'' | undefined>
'data-focused': Signal<'' | undefined>
'data-loading': Signal<'' | undefined>
'data-load-failed': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
}
/** For branch items — expand/collapse disclosure trigger. */
branchTrigger: {
'data-scope': 'tree-view'
'data-part': 'branch-trigger'
'data-state': Signal<'open' | 'closed'>
onClick: (e: MouseEvent) => void
}
/**
* Checkbox element (only meaningful when `selectionMode === 'checkbox'`).
* `aria-checked` is the tri-state string ('true' | 'false' | 'mixed').
* The consumer must render a checkbox input or a visual proxy and
* dispatch `toggleChecked` via the `onClick` binding. For branches,
* pass the branch's descendant ids via `descendantIds` on the message
* so children are propagated in a single reducer step.
*/
checkbox: {
role: 'checkbox'
'aria-checked': Signal<'true' | 'false' | 'mixed'>
'data-scope': 'tree-view'
'data-part': 'checkbox'
'data-state': Signal<'checked' | 'unchecked' | 'indeterminate'>
}
}
TreeNodeInput from @llui/components/tree-view
Shape of a lazily-loaded child handed back via childrenLoaded.
export interface TreeNodeInput {
id: string
/** Eagerly-known children of this freshly-loaded node, if any. */
children?: string[]
disabled?: boolean
/** Mark this freshly-loaded node as itself lazily-loadable. */
hasChildren?: boolean
}
TreeNodeMeta from @llui/components/tree-view
JSON-serializable adjacency entry for one tree node. The reducer owns the
tree structure (as a flat record) so it can traverse descendants/ancestors
for automatic indeterminate derivation and lazy-load bookkeeping without
any external collection. Build the record from a {@link TreeCollection} or
by hand; seed it via init({ nodes, roots }) or the setNodes message.
export interface TreeNodeMeta {
/** Ordered ids of this node's loaded children (empty until loaded). */
children: string[]
/** Parent id, or null for a root. */
parentId: string | null
/** When true, descendant cascade and checked-derivation skip this node. */
disabled?: boolean
/**
* Declares the node as a branch whose children are loaded lazily. Expanding
* a `hasChildren` node that has not yet been loaded emits a `loadChildren`
* effect; the consumer fetches and replies with `childrenLoaded`.
*/
hasChildren?: boolean
}
TreeViewInit from @llui/components/tree-view
export interface TreeViewInit {
expanded?: string[]
selected?: string[]
checked?: string[]
indeterminate?: string[]
selectionMode?: SelectionMode
disabled?: boolean
visibleItems?: string[]
visibleLabels?: string[]
nodes?: Record<string, TreeNodeMeta>
roots?: string[]
loaded?: string[]
loadFailed?: string[]
}
TreeViewParts from @llui/components/tree-view
export interface TreeViewParts {
root: {
role: 'tree'
'aria-multiselectable': Signal<'true' | undefined>
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'tree-view'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
item: (id: string, depth: number, isBranch: boolean, parentId?: string | null) => TreeItemParts
}
TreeViewState from @llui/components/tree-view
export interface TreeViewState {
/** Ids of expanded branches. */
expanded: string[]
/** Ids of selected items. */
selected: string[]
/** Ids of checked items (checkbox selection mode). */
checked: string[]
/** Ids known to be in the indeterminate tri-state (some-but-not-all
* descendants checked). Consumer-computed via propagation logic or the
* `toggleChecked` message's `descendantIds` parameter. */
indeterminate: string[]
/** Currently focused item id. */
focused: string | null
selectionMode: SelectionMode
/** Ordered list of currently-visible item ids (updated by consumer via setVisible). */
visibleItems: string[]
/** Parallel array of visible-item labels for typeahead. If empty, typeahead
* matches against ids directly. Updated alongside visibleItems via the
* optional `labels` field on `setVisibleItems`. */
visibleLabels: string[]
disabled: boolean
/** Typeahead accumulator buffer. */
typeahead: string
typeaheadExpiresAt: number
/** Id of item currently being renamed, or null. */
renaming: string | null
/** Draft value during rename. */
renameDraft: string
/**
* Ids of branches currently loading their children asynchronously. Item
* parts expose `aria-busy` while loading so assistive tech announces the
* in-progress state. This is now driven by the machine itself: expanding a
* `hasChildren` node sets `loading` and emits a `loadChildren` effect; the
* `childrenLoaded` / `childrenLoadFailed` replies clear it. The legacy
* `loadingStart` / `loadingEnd` messages remain for manual control.
*/
loading: string[]
/**
* Flat tree structure (adjacency record). Owned by the reducer for
* descendant/ancestor traversal. JSON-serializable.
*/
nodes: Record<string, TreeNodeMeta>
/** Ids of the top-level (root) nodes, in order. */
roots: string[]
/**
* Ids of branches whose children have been loaded (distinguishes a
* loaded-but-empty branch from a not-yet-fetched one so we never refetch).
*/
loaded: string[]
/**
* Ids of branches whose last lazy load failed. Re-expanding such a branch
* retries the load (clears the flag and re-emits `loadChildren`).
*/
loadFailed: string[]
}
Constants
treeView from @llui/components/tree-view
const treeView
@llui/components/context-menu
Functions
connect() from @llui/components/context-menu
function connect(state: Signal<ContextMenuState>, send: Send<ContextMenuMsg>, opts: ConnectOptions): ContextMenuParts
init() from @llui/components/context-menu
function init(opts: ContextMenuInit = {}): ContextMenuState
isPresent() from @llui/components/context-menu
Whether the root content should be in the DOM. True for every status except
'closed' — so the content stays mounted through the exit animation. Falls back
to open when a consumer drives open directly without advancing status
(backward-compatible with open-driven callers that predate the presence
lifecycle): an open menu is present even if status still reads 'closed'.
function isPresent(state: ContextMenuState): boolean
overlay() from @llui/components/context-menu
function overlay(opts: OverlayOptions): Mountable
update() from @llui/components/context-menu
function update(state: ContextMenuState, msg: ContextMenuMsg): [ContextMenuState, never[]]
Types
ContextMenuCheckItemParts from @llui/components/context-menu
export type ContextMenuCheckItemParts = MenuCheckItemPartsOf<'context-menu'>
ContextMenuGroupParts from @llui/components/context-menu
export type ContextMenuGroupParts = MenuGroupPartsOf<'context-menu'>
ContextMenuItem from @llui/components/context-menu
A single node in the context-menu item tree (JSON-serializable). Shared with
menu via the {@link MenuNode} machine type.
export type ContextMenuItem = MenuNode
ContextMenuItemKind from @llui/components/context-menu
Kind of a context-menu item.
export type ContextMenuItemKind = MenuNodeKind
ContextMenuItemParts from @llui/components/context-menu
export type ContextMenuItemParts = MenuItemPartsOf<'context-menu'>
ContextMenuMsg from @llui/components/context-menu
export type ContextMenuMsg =
/** @humanOnly */
| { type: 'openAt'; x: number; y: number }
/** @intent("Close the context menu") */
| { type: 'close' }
/** @humanOnly */
| { type: 'highlight'; level: string; value: string | null }
/** @humanOnly */
| { type: 'highlightNext'; level: string }
/** @humanOnly */
| { type: 'highlightPrev'; level: string }
/** @humanOnly */
| { type: 'highlightFirst'; level: string }
/** @humanOnly */
| { type: 'highlightLast'; level: string }
/** @intent("Activate the currently-highlighted item at the given level") */
| { type: 'selectHighlighted'; level: string }
/** @intent("Activate the menu item with the given value") */
| { type: 'select'; value: string }
/** @intent("Open the submenu for the given parent item") */
| { type: 'openSub'; value: string }
/** @intent("Close the deepest open submenu") */
| { type: 'closeSub' }
/** @humanOnly */
| { type: 'setItems'; items: ContextMenuItem[] }
/** @humanOnly */
| { type: 'typeahead'; level: string; char: string; now: number }
/** @intent("Set the reading direction — 'ltr'/'rtl', or null to follow the page") */
| { type: 'setDir'; dir: TextDirection | null }
/** @humanOnly */
| { type: 'animationEnd' }
ContextMenuSeparatorParts from @llui/components/context-menu
export type ContextMenuSeparatorParts = MenuSeparatorPartsOf<'context-menu'>
ContextMenuSubContentParts from @llui/components/context-menu
export type ContextMenuSubContentParts = MenuSubContentPartsOf<'context-menu'>
ContextMenuSubPositionerParts from @llui/components/context-menu
export type ContextMenuSubPositionerParts = MenuSubPositionerPartsOf<'context-menu'>
ContextMenuSubTriggerParts from @llui/components/context-menu
export type ContextMenuSubTriggerParts = MenuSubTriggerPartsOf<'context-menu'>
Interfaces
ConnectOptions from @llui/components/context-menu
export interface ConnectOptions {
id: string
onSelect?: (value: string) => void
/** ms to wait before opening a submenu on hover (default: 200). */
hoverDelay?: number
/** ms to wait before closing a submenu after the pointer leaves (default: 300). */
hoverCloseDelay?: number
}
ContextMenuInit from @llui/components/context-menu
export interface ContextMenuInit {
items?: ContextMenuItem[]
checked?: string[]
closeOnSelect?: boolean
/** Omit to follow the page's own direction (see `MenuState.dir`). */
dir?: TextDirection | null
/** When false, closing the menu plays an exit animation and the content stays
* mounted (status 'closing') until an `animationEnd`. Default true: instant. */
skipAnimations?: boolean
}
ContextMenuParts from @llui/components/context-menu
export interface ContextMenuParts {
/** The element users right-click to open the menu. */
trigger: {
'data-scope': 'context-menu'
'data-part': 'trigger'
onContextMenu: (e: MouseEvent) => void
}
positioner: {
'data-scope': 'context-menu'
'data-part': 'positioner'
style: Signal<string>
}
content: {
role: 'menu'
id: string
/** Virtually-focused (highlighted) item id at the root level. */
'aria-activedescendant': Signal<string | undefined>
tabindex: -1
/** Reflects the presence lifecycle: 'opening' | 'open' | 'closing' | 'closed'.
* Stays mounted while 'closing' so the exit animation can run. */
'data-state': Signal<PresenceStatus>
'data-scope': 'context-menu'
'data-part': 'content'
onKeyDown: (e: KeyboardEvent) => void
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
item: (value: string) => ContextMenuItemParts
checkboxItem: (value: string) => ContextMenuCheckItemParts
radioItem: (value: string) => ContextMenuCheckItemParts
group: (id: string) => ContextMenuGroupParts
separator: () => ContextMenuSeparatorParts
subTrigger: (value: string) => ContextMenuSubTriggerParts
subPositioner: (value: string) => ContextMenuSubPositionerParts
subContent: (value: string) => ContextMenuSubContentParts
}
ContextMenuState from @llui/components/context-menu
Context-menu state — the shared menu-tree state plus the pointer (x, y) the root content is positioned at.
export interface ContextMenuState extends MenuTreeState {
x: number
y: number
}
OverlayOptions from @llui/components/context-menu
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<ContextMenuState>
send: Send<ContextMenuMsg>
parts: ContextMenuParts
content: () => Renderable
/**
* Optional enter/leave transition for the context-menu content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example contextMenu.overlay({ state, send, parts, content, transition: fade({ duration: 120 }) })
*/
transition?: TransitionOptions
target?: string | HTMLElement
}
Constants
contextMenu from @llui/components/context-menu
const contextMenu
isMounted from @llui/components/context-menu
Alias of {@link isPresent} for parity with the presence-convention naming.
const isMounted
@llui/components/password-input
Functions
connect() from @llui/components/password-input
function connect(state: Signal<PasswordInputState>, send: Send<PasswordInputMsg>, opts: ConnectOptions = {}): PasswordInputParts
init() from @llui/components/password-input
function init(opts: PasswordInputInit = {}): PasswordInputState
update() from @llui/components/password-input
function update(state: PasswordInputState, msg: PasswordInputMsg): [PasswordInputState, never[]]
Types
PasswordInputMsg from @llui/components/password-input
export type PasswordInputMsg =
/** @intent("Update the password value as the user types") */
| { type: 'setValue'; value: string }
/** @intent("Toggle the show/hide-password state") */
| { type: 'toggleVisibility' }
/** @intent("Set the show/hide-password state to a specific value") */
| { type: 'setVisible'; visible: boolean }
Interfaces
ConnectOptions from @llui/components/password-input
export interface ConnectOptions {
autocomplete?: string
showLabel?: string
hideLabel?: string
}
PasswordInputInit from @llui/components/password-input
export interface PasswordInputInit {
value?: string
visible?: boolean
disabled?: boolean
}
PasswordInputParts from @llui/components/password-input
export interface PasswordInputParts {
root: {
'data-scope': 'password-input'
'data-part': 'root'
'data-visible': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
}
input: {
type: Signal<'text' | 'password'>
autocomplete: string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'password-input'
'data-part': 'input'
onInput: (e: Event) => void
}
visibilityTrigger: {
type: 'button'
'aria-label': Signal<string>
'aria-pressed': Signal<boolean>
disabled: Signal<boolean>
tabindex: Signal<number>
'data-scope': 'password-input'
'data-part': 'visibility-trigger'
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
}
PasswordInputState from @llui/components/password-input
Password input — text input with show/hide visibility toggle.
export interface PasswordInputState {
value: string
visible: boolean
disabled: boolean
}
Constants
passwordInput from @llui/components/password-input
const passwordInput
@llui/components/steps
Functions
connect() from @llui/components/steps
function connect(state: Signal<StepsState>, send: Send<StepsMsg>, opts: ConnectOptions = {}): StepsParts
init() from @llui/components/steps
function init(opts: StepsInit = {}): StepsState
stepStatus() from @llui/components/steps
function stepStatus(state: StepsState, step: number): StepStatus
update() from @llui/components/steps
function update(state: StepsState, msg: StepsMsg): [StepsState, never[]]
Types
StepsMsg from @llui/components/steps
export type StepsMsg =
/** @intent("Jump to a specific step by zero-based index") */
| { type: 'goTo'; step: number }
/** @intent("Advance to the next step") */
| { type: 'next' }
/** @intent("Go back to the previous step") */
| { type: 'prev' }
/** @intent("Mark the given step as completed") */
| { type: 'complete'; step: number }
/** @intent("Mark the given step as having an error") */
| { type: 'markError'; step: number }
/** @intent("Clear the error flag on the given step") */
| { type: 'clearError'; step: number }
/** @intent("Reset progress back to the first step (clears completed and errors)") */
| { type: 'reset' }
StepStatus from @llui/components/steps
Steps — progress indicator for multi-step flows (wizards, checkouts). Tracks current step and completed steps; supports linear and non-linear navigation.
export type StepStatus = 'pending' | 'current' | 'completed' | 'error'
Interfaces
ConnectOptions from @llui/components/steps
export interface ConnectOptions {
label?: string
}
StepsInit from @llui/components/steps
export interface StepsInit {
current?: number
completed?: number[]
steps?: string[]
linear?: boolean
disabled?: boolean
}
StepsItemParts from @llui/components/steps
export interface StepsItemParts {
item: {
'data-scope': 'steps'
'data-part': 'item'
'data-status': Signal<StepStatus>
'data-index': string
'aria-current': Signal<'step' | undefined>
}
trigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'steps'
'data-part': 'trigger'
'data-status': Signal<StepStatus>
onClick: (e: MouseEvent) => void
}
separator: {
'data-scope': 'steps'
'data-part': 'separator'
'data-status': Signal<StepStatus>
'aria-hidden': 'true'
}
}
StepsParts from @llui/components/steps
export interface StepsParts {
root: {
role: 'group'
'aria-label': string
'data-scope': 'steps'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
nextTrigger: {
type: 'button'
disabled: Signal<boolean>
'data-scope': 'steps'
'data-part': 'next-trigger'
onClick: (e: MouseEvent) => void
}
prevTrigger: {
type: 'button'
disabled: Signal<boolean>
'data-scope': 'steps'
'data-part': 'prev-trigger'
onClick: (e: MouseEvent) => void
}
item: (index: number) => StepsItemParts
}
StepsState from @llui/components/steps
export interface StepsState {
current: number
completed: number[]
errors: number[]
steps: string[]
/** If linear, users cannot skip steps. */
linear: boolean
disabled: boolean
}
Constants
steps from @llui/components/steps
const steps
@llui/components/time-picker
Functions
connect() from @llui/components/time-picker
function connect(state: Signal<TimePickerState>, send: Send<TimePickerMsg>, opts: ConnectOptions = {}): TimePickerParts
displayHours() from @llui/components/time-picker
Hours formatted for display (12-hr: 1..12, 24-hr: 0..23).
function displayHours(state: TimePickerState): number
formatTime() from @llui/components/time-picker
Format the full time string (HH:MM or HH:MM:SS).
function formatTime(state: TimePickerState): string
hoursFromDisplay() from @llui/components/time-picker
Inverse of displayHours: turn a value from the hours FIELD into the stored
24-hour value. In 12-hour format the field reads 1..12, so it only says which
hour of the half-day — the meridiem has to come from the current value. The
single mutation path used mod(hours, 24) instead, so typing "3" at 15:00
stored 03:00 and silently flipped PM to AM (#125).
function hoursFromDisplay(hours: number, state: TimePickerState): number
init() from @llui/components/time-picker
function init(opts: TimePickerInit = {}): TimePickerState
period() from @llui/components/time-picker
AM or PM for 12-hour format.
function period(state: TimePickerState): 'AM' | 'PM'
update() from @llui/components/time-picker
function update(state: TimePickerState, msg: TimePickerMsg): [TimePickerState, never[]]
Types
TimeFormat from @llui/components/time-picker
Time picker — hours and minutes input with increment/decrement buttons. 12 or 24-hour format; optional seconds; step for minutes/seconds.
export type TimeFormat = '12' | '24'
TimePickerMsg from @llui/components/time-picker
export type TimePickerMsg =
/** @intent("Set the full time value (hours/minutes/seconds)") */
| { type: 'setValue'; value: TimeValue }
/** @intent("Set the hours field directly — 0-23 in 24-hour format, 1-12 in 12-hour format where the current AM/PM is kept") */
| { type: 'setHours'; hours: number }
/** @intent("Set the minutes field directly") */
| { type: 'setMinutes'; minutes: number }
/** @intent("Set the seconds field directly") */
| { type: 'setSeconds'; seconds: number }
/** @intent("Bump hours up by 1 (wraps at 24/12). Ignored while disabled") */
| { type: 'incrementHours' }
/** @intent("Bump hours down by 1. Ignored while disabled") */
| { type: 'decrementHours' }
/** @intent("Bump minutes up by minuteStep. Ignored while disabled") */
| { type: 'incrementMinutes' }
/** @intent("Bump minutes down by minuteStep. Ignored while disabled") */
| { type: 'decrementMinutes' }
/** @intent("Flip between AM and PM (12-hour format only). Ignored while disabled") */
| { type: 'toggleAmPm' }
/** @intent("Enable or disable the time-picker — a host/agent write, never gated") */
| { type: 'setDisabled'; disabled: boolean }
Interfaces
ConnectOptions from @llui/components/time-picker
export interface ConnectOptions {
label?: string
hoursLabel?: string
minutesLabel?: string
periodLabel?: string
}
TimePickerInit from @llui/components/time-picker
export interface TimePickerInit {
value?: TimeValue
format?: TimeFormat
minuteStep?: number
secondStep?: number
showSeconds?: boolean
disabled?: boolean
}
TimePickerParts from @llui/components/time-picker
export interface TimePickerParts {
root: {
role: 'group'
'aria-label': string
'data-scope': 'time-picker'
'data-part': 'root'
'data-format': Signal<TimeFormat>
}
hoursInput: {
type: 'number'
role: 'spinbutton'
'aria-label': string
'aria-valuemin': Signal<number>
'aria-valuemax': Signal<number>
'aria-valuenow': Signal<number>
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'time-picker'
'data-part': 'hours-input'
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
}
minutesInput: {
type: 'number'
role: 'spinbutton'
'aria-label': string
'aria-valuemin': 0
'aria-valuemax': 59
'aria-valuenow': Signal<number>
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'time-picker'
'data-part': 'minutes-input'
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
}
periodTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'time-picker'
'data-part': 'period-trigger'
'data-period': Signal<'AM' | 'PM'>
onClick: (e: MouseEvent) => void
hidden: Signal<boolean>
}
}
TimePickerState from @llui/components/time-picker
export interface TimePickerState {
value: TimeValue
format: TimeFormat
minuteStep: number
secondStep: number
showSeconds: boolean
disabled: boolean
}
TimeValue from @llui/components/time-picker
export interface TimeValue {
hours: number
minutes: number
seconds: number
}
Constants
timePicker from @llui/components/time-picker
const timePicker
@llui/components/date-picker
Functions
connect() from @llui/components/date-picker
function connect(state: Signal<DatePickerState>, send: Send<DatePickerMsg>, opts: ConnectOptions = {}): DatePickerParts
init() from @llui/components/date-picker
function init(opts: DatePickerInit = {}): DatePickerState
monthGrid() from @llui/components/date-picker
Compute the grid of days for a visible month. Always returns full weeks:
leading days from the previous month and trailing from the next month fill
the grid. offset shifts the rendered month forward by N months from the
state's visibleMonth/visibleYear (used by the multi-month view).
function monthGrid(state: DatePickerState, offset = 0): DayCell[]
monthLabel() from @llui/components/date-picker
Localized "Month YYYY" label for a calendar header, backed by
Intl.DateTimeFormat via the package's formatDate wrapper.
function monthLabel(year: number, month: number, locale?: string): string
update() from @llui/components/date-picker
function update(state: DatePickerState, msg: DatePickerMsg): [DatePickerState, never[]]
weekdayLabels() from @llui/components/date-picker
Localized weekday header labels (length 7), rotated so the array begins on
weekStartsOn (0=Sunday, 1=Monday). Uses a known reference week so the Intl
formatter yields the correct day names.
function weekdayLabels(weekStartsOn: 0 | 1, locale?: string): string[]
weekRows() from @llui/components/date-picker
Group a flat DayCell[] (from monthGrid) into rows of 7 — one row
per week — so the view can wrap each in a role="row" element as
required by the WAI-ARIA grid pattern.
function weekRows(cells: DayCell[]): DayCell[][]
Types
DatePickerMode from @llui/components/date-picker
Selection mode: a single date or a start/end range.
export type DatePickerMode = 'single' | 'range'
DatePickerMsg from @llui/components/date-picker
export type DatePickerMsg =
/** @intent("Set the selected date (YYYY-MM-DD), or null to clear") */
| { type: 'setValue'; value: string | null }
/** @intent("Set the selected date range (YYYY-MM-DD start/end); endpoints are normalized so start <= end") */
| { type: 'setRange'; start: string | null; end: string | null }
/** @humanOnly */
| { type: 'setFocused'; date: string }
/** @humanOnly */
| { type: 'setHover'; date: string }
/** @humanOnly */
| { type: 'clearHover' }
/** @intent("Show the previous month in the calendar") */
| { type: 'prevMonth' }
/** @intent("Show the next month in the calendar") */
| { type: 'nextMonth' }
/** @intent("Show the previous year (same month)") */
| { type: 'prevYear' }
/** @intent("Show the next year (same month)") */
| { type: 'nextYear' }
/** @intent("Select the currently-focused date (anchors or completes a range in range mode)") */
| { type: 'selectFocused' }
/** @humanOnly */
| { type: 'moveFocus'; days: number }
/** @humanOnly */
| { type: 'focusStartOfWeek' }
/** @humanOnly */
| { type: 'focusEndOfWeek' }
/** @humanOnly */
| { type: 'focusToday' }
/** @intent("Clear the current selection") */
| { type: 'clear' }
Interfaces
ConnectOptions from @llui/components/date-picker
export interface ConnectOptions {
/** Selection mode — affects pointer-hover preview wiring. Defaults to 'single'. */
mode?: DatePickerMode
/** BCP-47 locale tag for month/grid labels. Defaults to the runtime default. */
locale?: string
prevLabel?: string
nextLabel?: string
gridLabel?: (year: number, month: number) => string
}
DatePickerInit from @llui/components/date-picker
export interface DatePickerInit {
mode?: DatePickerMode
value?: string | null
start?: string | null
end?: string | null
visibleMonth?: number
visibleYear?: number
months?: number
min?: string | null
max?: string | null
weekStartsOn?: 0 | 1
disabled?: boolean
}
DatePickerParts from @llui/components/date-picker
export interface DatePickerParts {
root: {
'data-scope': 'date-picker'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
/**
* Grid part factory. `offset` (default 0) selects which month this grid
* renders in a multi-month view — the `aria-label` is the localized
* "Month YYYY" of `visibleMonth + offset`.
*/
grid: (offset?: number) => {
role: 'grid'
'aria-label': Signal<string>
'data-scope': 'date-picker'
'data-part': 'grid'
'data-month-offset': number
}
row: {
role: 'row'
'data-scope': 'date-picker'
'data-part': 'row'
}
prevMonthTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'date-picker'
'data-part': 'prev-month-trigger'
onClick: (e: MouseEvent) => void
}
nextMonthTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'date-picker'
'data-part': 'next-month-trigger'
onClick: (e: MouseEvent) => void
}
dayCell: (cell: DayCell) => DayCellParts
/** Preset part factory — clicking dispatches a single `setRange`. */
preset: (range: PresetRange) => PresetParts
}
DatePickerState from @llui/components/date-picker
export interface DatePickerState {
/** Selection mode. Defaults to 'single'. */
mode: DatePickerMode
/** Selected date as YYYY-MM-DD, or null. Used in 'single' mode. */
value: string | null
/** Range start as YYYY-MM-DD, or null. Used in 'range' mode. */
start: string | null
/** Range end as YYYY-MM-DD, or null. Used in 'range' mode. */
end: string | null
/** Date currently hovered/previewed while a range is being completed. */
hoverDate: string | null
/** The month currently visible (1-indexed, 1-12) — the first/leftmost month. */
visibleMonth: number
/** The year currently visible. */
visibleYear: number
/** Number of months rendered side-by-side. Defaults to 1. */
months: number
/** The date currently focused by the keyboard (YYYY-MM-DD). */
focused: string
/** Minimum selectable date, inclusive. */
min: string | null
/** Maximum selectable date, inclusive. */
max: string | null
/** 0=Sunday, 1=Monday. */
weekStartsOn: 0 | 1
disabled: boolean
}
DayCell from @llui/components/date-picker
export interface DayCell {
iso: string
day: number
inMonth: boolean
isToday: boolean
isSelected: boolean
isFocused: boolean
isDisabled: boolean
/** True for the start endpoint of a (committed or previewed) range. */
isRangeStart: boolean
/** True for the end endpoint of a (committed or previewed) range. */
isRangeEnd: boolean
/** True for dates strictly between the range endpoints. */
isInRange: boolean
}
DayCellParts from @llui/components/date-picker
export interface DayCellParts {
cell: {
role: 'gridcell'
// Signals, not plain values: `view()` runs once, so a snapshot here freezes
// every flag at build time and no selection, focus move or range preview
// ever reaches the DOM. See `live` in `connect`.
'aria-selected': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
tabindex: Signal<number>
'data-scope': 'date-picker'
'data-part': 'day-cell'
/** The cell's identity — the one genuinely static attribute. */
'data-date': string
'data-in-month': Signal<'' | undefined>
'data-today': Signal<'' | undefined>
'data-selected': Signal<'' | undefined>
'data-focused': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
'data-range-start': Signal<'' | undefined>
'data-range-end': Signal<'' | undefined>
'data-in-range': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
onFocus: (e: FocusEvent) => void
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
}
}
PresetParts from @llui/components/date-picker
export interface PresetParts {
type: 'button'
'data-scope': 'date-picker'
'data-part': 'preset'
onClick: (e: MouseEvent) => void
}
PresetRange from @llui/components/date-picker
A named preset range a consumer can render as a quick-select button.
export interface PresetRange {
start: string | null
end: string | null
}
Constants
datePicker from @llui/components/date-picker
const datePicker
@llui/components/color-picker
Functions
colorFromPoint() from @llui/components/color-picker
Map a pointer position over the 2D saturation/value area to HSV S/V (0..100). X axis is saturation (left 0 → right 100); Y axis is value (top 100 → bottom 0). The point is clamped to the rect, so out-of-bounds drags saturate cleanly.
function colorFromPoint(rect: DOMRect, x: number, y: number): { s: number; v: number }
connect() from @llui/components/color-picker
function connect(state: Signal<ColorPickerState>, send: Send<ColorPickerMsg>, opts: ConnectOptions = {}): ColorPickerParts
hexToHsl() from @llui/components/color-picker
function hexToHsl(hex: string): Hsl | null
hslToHsv() from @llui/components/color-picker
Convert HSL (h 0-360, s/l 0-100) to HSV (h 0-360, s/v 0-100).
function hslToHsv(hsl: Hsl): Hsv
hslToRgb() from @llui/components/color-picker
Convert HSL (h 0-360, s/l 0-100) to RGB (0-255 each).
function hslToRgb(hsl: Hsl): { r: number; g: number; b: number }
hsvToHsl() from @llui/components/color-picker
Convert HSV (h 0-360, s/v 0-100) to HSL (h 0-360, s/l 0-100).
function hsvToHsl(hsv: Hsv): Hsl
init() from @llui/components/color-picker
function init(opts: ColorPickerInit = {}): ColorPickerState
parseColor() from @llui/components/color-picker
Parse #RGB, #RRGGBB, or #RRGGBBAA into HSL (+ optional alpha).
function parseColor(color: string): { hsl: Hsl; alpha?: number } | null
stateHsl() from @llui/components/color-picker
Derive the HSL projection of the current state (for hex output + HSL sliders).
function stateHsl(state: ColorPickerState): Hsl
toHex() from @llui/components/color-picker
function toHex(hsl: Hsl): string
toHex8() from @llui/components/color-picker
8-digit hex (#RRGGBBAA) including the alpha channel (0..1).
function toHex8(hsl: Hsl, alpha: number): string
update() from @llui/components/color-picker
function update(state: ColorPickerState, msg: ColorPickerMsg): [ColorPickerState, never[]]
Types
ColorPickerMsg from @llui/components/color-picker
export type ColorPickerMsg =
/** @intent("Set the full HSL color at once") */
| { type: 'setHsl'; hsl: Hsl }
/** @intent("Set the hue channel (0–360)") */
| { type: 'setHue'; h: number }
/** @intent("Set the saturation channel (0–100)") */
| { type: 'setSaturation'; s: number }
/** @intent("Set the lightness channel (0–100)") */
| { type: 'setLightness'; l: number }
/** @intent("Set the alpha channel (0–1)") */
| { type: 'setAlpha'; alpha: number }
/** @intent("Set the color from a hex string (#RRGGBB or #RGB)") */
| { type: 'setHex'; hex: string }
/** @intent("Set saturation and value (HSV, 0–100 each) from the 2D area") */
| { type: 'setSv'; s: number; v: number }
/** @intent("Nudge saturation/value (HSV) by signed deltas — used by area arrow keys") */
| { type: 'nudgeSv'; ds: number; dv: number }
/** @intent("Set the color from a swatch or hex string (#RGB, #RRGGBB, or #RRGGBBAA)") */
| { type: 'setColor'; color: string }
Interfaces
ColorPickerInit from @llui/components/color-picker
export interface ColorPickerInit {
/** Initial color as HSL (converted to the canonical HSV store). */
hsl?: Hsl
/** Initial color as HSV (takes precedence over `hsl`). */
hsv?: Hsv
alpha?: number
disabled?: boolean
}
ColorPickerParts from @llui/components/color-picker
export interface ColorPickerParts {
root: {
'data-scope': 'color-picker'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
hueSlider: {
type: 'range'
min: 0
max: 360
step: 1
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'color-picker'
'data-part': 'hue-slider'
onInput: (e: Event) => void
}
saturationSlider: {
type: 'range'
min: 0
max: 100
step: 1
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
style: Signal<string>
'data-scope': 'color-picker'
'data-part': 'saturation-slider'
onInput: (e: Event) => void
}
lightnessSlider: {
type: 'range'
min: 0
max: 100
step: 1
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
style: Signal<string>
'data-scope': 'color-picker'
'data-part': 'lightness-slider'
onInput: (e: Event) => void
}
hexInput: {
type: 'text'
autocomplete: 'off'
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'color-picker'
'data-part': 'hex-input'
onInput: (e: Event) => void
}
/** Static preview swatch showing the currently-selected color. */
preview: {
'data-scope': 'color-picker'
'data-part': 'preview'
'aria-hidden': 'true'
style: Signal<string>
}
/** The 2D saturation/value area track. The view owns pointer events and
* calls `colorFromPoint(track.getBoundingClientRect(), x, y)` to derive S/V. */
area: {
'data-scope': 'color-picker'
'data-part': 'area'
style: Signal<string>
}
/** The draggable thumb inside the 2D area. Keyboard-operable (arrows move
* S/V; Shift = coarse) with role="slider" and a 2D aria-valuetext. */
areaThumb: {
role: 'slider'
'aria-label': string
/** ARIA 1.2 lists this as a REQUIRED property of `slider`, and the
* `aria-valuetext` definition says authors must also specify it. The area
* is 2D, so it reports the horizontal axis (saturation) numerically and
* leaves both axes to `aria-valuetext`. */
'aria-valuenow': Signal<number>
'aria-valuetext': Signal<string>
'aria-disabled': Signal<'true' | undefined>
tabindex: Signal<number>
'data-scope': 'color-picker'
'data-part': 'area-thumb'
style: Signal<string>
onKeyDown: (e: KeyboardEvent) => void
}
/** Alpha (opacity) range input, 0..1. Wired to the existing alpha state. */
alphaSlider: {
type: 'range'
min: 0
max: 1
step: number
'aria-label': string
disabled: Signal<boolean>
value: Signal<string>
style: Signal<string>
'data-scope': 'color-picker'
'data-part': 'alpha-slider'
onInput: (e: Event) => void
}
/** Container for the preset swatch buttons. */
swatchGroup: {
role: 'group'
'aria-label': string
'data-scope': 'color-picker'
'data-part': 'swatch-group'
}
/** Factory for a preset swatch button dispatching a single `setColor`. */
swatch: (color: string) => SwatchParts
}
ColorPickerState from @llui/components/color-picker
export interface ColorPickerState {
/**
* Canonical color, stored in HSV so the 2D saturation/value area preserves
* S and V independently (HSL collapses both at the black/white axis). HSL is
* derived on demand via `stateHsl()` / `hsvToHsl()` for hex output + sliders.
*/
hsv: Hsv
/** Alpha channel 0..1. */
alpha: number
disabled: boolean
}
ConnectOptions from @llui/components/color-picker
export interface ConnectOptions {
hueLabel?: string
saturationLabel?: string
lightnessLabel?: string
hexLabel?: string
/** aria-label for the 2D saturation/value area thumb. */
areaLabel?: string
/** aria-label for the alpha slider. */
alphaLabel?: string
/** aria-label for the swatch group container. */
swatchGroupLabel?: string
/** Fine keyboard step for the area thumb (S/V units). Default 1. */
step?: number
/** Coarse keyboard step for the area thumb when Shift is held. Default 10. */
coarseStep?: number
}
Hsl from @llui/components/color-picker
Color picker — HSL/HSV color selection. Tracks hue (0-360), saturation (0-100), and lightness (0-100). Emits hex strings for convenience.
export interface Hsl {
h: number
s: number
l: number
}
Hsv from @llui/components/color-picker
HSV color (h 0-360, s/v 0-100). The 2D area picker operates in HSV space.
export interface Hsv {
h: number
s: number
v: number
}
SwatchParts from @llui/components/color-picker
export interface SwatchParts {
type: 'button'
'aria-label': string
'aria-pressed': Signal<boolean>
'data-scope': 'color-picker'
'data-part': 'swatch'
'data-value': string
'data-state': Signal<'selected' | undefined>
style: string
onClick: (e: MouseEvent) => void
}
Constants
colorPicker from @llui/components/color-picker
const colorPicker
@llui/components/timer
Functions
connect() from @llui/components/timer
function connect(state: Signal<TimerState>, send: Send<TimerMsg>, opts: ConnectOptions = {}): TimerParts
display() from @llui/components/timer
Returns the display value in ms (elapsed for count-up, remaining for count-down).
function display(state: TimerState): number
formatMs() from @llui/components/timer
Format a ms value using a simple template. Supported tokens: HH / H — hours (2-digit / unpadded) mm / m — minutes ss / s — seconds SSS / S — milliseconds (3-digit / unpadded)
Example: formatMs(125_500, 'mm:ss.SSS') → "02:05.500"
function formatMs(ms: number, template: string): string
init() from @llui/components/timer
function init(opts: TimerInit = {}): TimerState
isComplete() from @llui/components/timer
function isComplete(state: TimerState): boolean
parts() from @llui/components/timer
Breaks a ms value into { hours, minutes, seconds, ms } parts for rendering.
function parts(ms: number): { hours: number; minutes: number; seconds: number; ms: number }
update() from @llui/components/timer
function update(state: TimerState, msg: TimerMsg): [TimerState, never[]]
Types
Direction from @llui/components/timer
Timer — counts elapsed time up from zero, or down from a configured
target. The machine is pure: it doesn't own the ticking interval.
The consumer runs setInterval(() => send({type:'tick', now: Date.now()}), 100)
(or whatever granularity) while the timer is running, and dispatches
start / pause / reset in response to user input.
// `div`, `button`, `text`, `mapSend` are imports from '@llui/dom'.
view: ({ state, send }) => {
const timerState = state.at('timer')
const timerSend = mapSend<Msg, timer.TimerMsg>(send, (msg) => ({
type: 'timer',
msg,
}))
const t = timer.connect(timerState, timerSend)
return [
div({ ...t.root }, [
div({ ...t.display }, [
text(timerState.map((s) => timer.formatMs(timer.display(s), 'mm:ss'))),
]),
button({ ...t.startTrigger }, [text('Start')]),
button({ ...t.pauseTrigger }, [text('Pause')]),
button({ ...t.resetTrigger }, [text('Reset')]),
]),
]
}
export type Direction = 'up' | 'down'
TimerMsg from @llui/components/timer
export type TimerMsg =
/** @intent("Start (or resume) the timer running") */
| { type: 'start'; now: number }
/** @intent("Pause the timer (preserves accumulated elapsed time)") */
| { type: 'pause'; now: number }
/** @intent("Reset the timer back to zero elapsed and pause it") */
| { type: 'reset' }
/** @humanOnly */
| { type: 'tick'; now: number }
/** @intent("Set the countdown target (in milliseconds; 0 disables countdown)") */
| { type: 'setTarget'; targetMs: number }
Interfaces
ConnectOptions from @llui/components/timer
export interface ConnectOptions {
startLabel?: string
pauseLabel?: string
resetLabel?: string
/**
* aria-live politeness for the display element. `'polite'` announces
* updates to assistive tech; `'off'` (default) keeps it silent — use
* 'polite' sparingly to avoid spamming screen reader users with
* every tick.
*/
ariaLive?: 'off' | 'polite'
}
TimerInit from @llui/components/timer
export interface TimerInit {
direction?: Direction
targetMs?: number
elapsedMs?: number
}
TimerParts from @llui/components/timer
export interface TimerParts {
root: {
'data-scope': 'timer'
'data-part': 'root'
'data-running': Signal<'' | undefined>
'data-direction': Signal<Direction>
}
display: {
role: 'timer'
'aria-live': 'off' | 'polite'
'data-scope': 'timer'
'data-part': 'display'
}
startTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'timer'
'data-part': 'start-trigger'
disabled: Signal<boolean>
onClick: (e: MouseEvent) => void
}
pauseTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'timer'
'data-part': 'pause-trigger'
disabled: Signal<boolean>
onClick: (e: MouseEvent) => void
}
resetTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'timer'
'data-part': 'reset-trigger'
onClick: (e: MouseEvent) => void
}
}
TimerState from @llui/components/timer
export interface TimerState {
running: boolean
direction: Direction
/** Target in milliseconds for countdown (0 = no target, runs indefinitely). */
targetMs: number
/** Accumulated elapsed time, excluding the current running interval. */
elapsedMs: number
/** Timestamp when the current running interval started (null when paused). */
startedAt: number | null
}
Constants
timer from @llui/components/timer
const timer
@llui/components/angle-slider
Functions
angleFromPoint() from @llui/components/angle-slider
Compute the angle in degrees from the center of a rect to a point. 0° = up (12 o'clock), increases clockwise. Result is in 0..360.
Useful inside a pointermove handler: const rect = control.getBoundingClientRect() const angle = angleFromPoint(rect, e.clientX, e.clientY) send({ type: 'setValue', value: angle })
function angleFromPoint(rect: DOMRect, x: number, y: number): number
connect() from @llui/components/angle-slider
function connect(state: Signal<AngleSliderState>, send: Send<AngleSliderMsg>, opts: ConnectOptions = {}): AngleSliderParts
init() from @llui/components/angle-slider
function init(opts: AngleSliderInit = {}): AngleSliderState
pointFromAngle() from @llui/components/angle-slider
Convert an angle to (x, y) on a unit circle (radius 1 at origin).
function pointFromAngle(angleDeg: number): { x: number; y: number }
update() from @llui/components/angle-slider
function update(state: AngleSliderState, msg: AngleSliderMsg): [AngleSliderState, never[]]
Types
AngleSliderMsg from @llui/components/angle-slider
export type AngleSliderMsg =
/** @intent("Set the angle in degrees (clamped to min/max, snapped to step)") */
| { type: 'setValue'; value: number }
/** @intent("Increase the angle by `steps` × step (default: 1 step)") */
| { type: 'increment'; steps?: number }
/** @intent("Decrease the angle by `steps` × step (default: 1 step)") */
| { type: 'decrement'; steps?: number }
/** @humanOnly */
| { type: 'setMin'; min: number }
/** @humanOnly */
| { type: 'setMax'; max: number }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
Interfaces
AngleSliderInit from @llui/components/angle-slider
export interface AngleSliderInit {
value?: number
min?: number
max?: number
step?: number
disabled?: boolean
readonly?: boolean
dir?: 'ltr' | 'rtl'
}
AngleSliderParts from @llui/components/angle-slider
export interface AngleSliderParts {
root: {
role: 'slider'
'aria-valuemin': Signal<number>
'aria-valuemax': Signal<number>
'aria-valuenow': Signal<number>
'aria-valuetext': Signal<string>
'aria-orientation': 'horizontal'
'aria-disabled': Signal<'true' | undefined>
'aria-readonly': Signal<'true' | undefined>
tabindex: Signal<number>
'data-scope': 'angle-slider'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
onKeyDown: (e: KeyboardEvent) => void
}
control: {
'data-scope': 'angle-slider'
'data-part': 'control'
}
/**
* The draggable thumb element. Its position is typically computed via
* CSS custom properties `--angle` (0..360) that the consumer sets from
* `state.value` using pointFromAngle() or a CSS `transform: rotate()`.
*/
thumb: {
'data-scope': 'angle-slider'
'data-part': 'thumb'
'data-value': Signal<string>
}
valueText: {
'data-scope': 'angle-slider'
'data-part': 'value-text'
}
/** A hidden input for form participation. */
hiddenInput: {
type: 'hidden'
value: Signal<string>
name?: string
'data-scope': 'angle-slider'
'data-part': 'hidden-input'
}
}
AngleSliderState from @llui/components/angle-slider
Angle slider — a circular input that selects a value in 0..360 degrees by dragging a thumb around a control. The state machine tracks the current angle; the view layer computes angles from pointer positions (helpers exported for that purpose).
Typical view wiring: on pointerdown/pointermove, read the control
element's bounding rect, compute the angle from (pointerX, pointerY)
to the rect center via angleFromPoint(), and dispatch setValue.
Keyboard: Arrow keys adjust by step; Home/End jump to min/max;
PageUp/PageDown adjust by step * 10.
export interface AngleSliderState {
value: number
min: number
max: number
step: number
disabled: boolean
readonly: boolean
/** Reading direction. Under 'rtl' horizontal arrow keys are flipped. */
dir: 'ltr' | 'rtl'
}
ConnectOptions from @llui/components/angle-slider
export interface ConnectOptions {
/** Name for the hidden input (form integration). */
name?: string
/** Formatter for aria-valuetext (default: "{value}°"). */
format?: (value: number) => string
}
Constants
angleSlider from @llui/components/angle-slider
const angleSlider
@llui/components/marquee
Functions
axis() from @llui/components/marquee
Returns 'horizontal' or 'vertical' based on direction.
function axis(direction: MarqueeDirection): 'horizontal' | 'vertical'
connect() from @llui/components/marquee
function connect(state: Signal<MarqueeState>, send: Send<MarqueeMsg>): MarqueeParts
cssAnimationDirection() from @llui/components/marquee
Returns 'normal' (left/up) or 'reverse' (right/down) for CSS animation-direction.
function cssAnimationDirection(direction: MarqueeDirection): 'normal' | 'reverse'
init() from @llui/components/marquee
function init(opts: MarqueeInit = {}): MarqueeState
isRunning() from @llui/components/marquee
Derived: whether the marquee is currently animating.
function isRunning(state: MarqueeState): boolean
update() from @llui/components/marquee
function update(state: MarqueeState, msg: MarqueeMsg): [MarqueeState, never[]]
Types
MarqueeDirection from @llui/components/marquee
Marquee — continuously-scrolling content. The state machine tracks play/pause + direction + speed; the scrolling itself is driven by CSS animations or JS requestAnimationFrame (the consumer owns that).
Expose the active state via CSS custom properties the consumer reads in their stylesheet: --marquee-duration: {N}s --marquee-direction: 'normal' | 'reverse' --marquee-playstate: 'running' | 'paused'
export type MarqueeDirection = 'left' | 'right' | 'up' | 'down'
MarqueeMsg from @llui/components/marquee
export type MarqueeMsg =
/** @intent("Resume the marquee scrolling") */
| { type: 'play' }
/** @intent("Pause the marquee scrolling") */
| { type: 'pause' }
/** @intent("Toggle the marquee between playing and paused") */
| { type: 'toggle' }
/** @humanOnly */
| { type: 'hoverPause' }
/** @humanOnly */
| { type: 'hoverResume' }
/** @intent("Change the scroll direction (left/right/up/down)") */
| { type: 'setDirection'; direction: MarqueeDirection }
/** @intent("Change the loop duration in seconds (larger = slower)") */
| { type: 'setDuration'; durationSec: number }
Interfaces
MarqueeInit from @llui/components/marquee
export interface MarqueeInit {
running?: boolean
direction?: MarqueeDirection
durationSec?: number
pauseOnHover?: boolean
disabled?: boolean
}
MarqueeParts from @llui/components/marquee
export interface MarqueeParts {
root: {
'data-scope': 'marquee'
'data-part': 'root'
'data-running': Signal<'' | undefined>
'data-direction': Signal<MarqueeDirection>
'data-axis': Signal<'horizontal' | 'vertical'>
'data-disabled': Signal<'' | undefined>
style: Signal<string>
onMouseEnter: (e: MouseEvent) => void
onMouseLeave: (e: MouseEvent) => void
}
content: {
'data-scope': 'marquee'
'data-part': 'content'
}
}
MarqueeState from @llui/components/marquee
export interface MarqueeState {
/** User-intended running state (what play/pause/toggle set). The actual
* effective state is derived via `isRunning()` — it combines this with
* `hovered` + `pauseOnHover`. */
running: boolean
direction: MarqueeDirection
/** Duration of one full loop in seconds. Larger = slower. */
durationSec: number
pauseOnHover: boolean
hovered: boolean
disabled: boolean
}
Constants
marquee from @llui/components/marquee
const marquee
@llui/components/presence
Functions
connect() from @llui/components/presence
Signal-surface connect: takes the component's presence state slice as a
Signal and returns reactive (handle-based) props for spreading into a view.
function connect(state: Signal<PresenceState>, send: Send<PresenceMsg>): PresenceParts
init() from @llui/components/presence
function init(opts: PresenceInit = {}): PresenceState
isAnimating() from @llui/components/presence
function isAnimating(state: PresenceState): boolean
isMounted() from @llui/components/presence
Whether the element should be in the DOM (mounted).
function isMounted(state: PresenceState): boolean
isVisible() from @llui/components/presence
Whether the element is visible (not running an exit animation).
function isVisible(state: PresenceState): boolean
presenceClose() from @llui/components/presence
Move toward closed — the mirror of {@link presenceOpen}.
function presenceClose<S extends PresenceOverlay>(state: S, skipAnimations: boolean): S
presenceEnd() from @llui/components/presence
Advance past the enter/exit animation. Only 'opening'/'closing' move; any other status is returned unchanged, so a stray end event cannot reopen or unmount anything.
SEMANTIC ADDITION over the four private copies this collapsed (#126): the
closing->closed transition also writes open: false. It is unreachable
today, but the reason is narrower than "nothing else writes 'closing'":
other code does — update's own 'close' case above and toast.ts's
dismiss both write it — and neither carries an open field or reduces
through this function. The claim is scoped to {@link PresenceOverlay}: among
the states that DO reach here, 'closing' is only ever written together
with open: false, by {@link presenceClose}. So it changes no behaviour,
and it is kept because "finished closing" implying "not open" is the
invariant a caller reducing through this function is entitled to, and a
future writer of 'closing' on an overlay should not be able to leave the
pair inconsistent.
function presenceEnd<S extends PresenceOverlay>(state: S): S
presenceOpen() from @llui/components/presence
Move toward open. skipAnimations lands on 'open' immediately; otherwise the
overlay sits in 'opening' until an end event calls {@link presenceEnd}.
Already-open state comes back by REFERENCE so a redundant open is a no-op for
the reference-equality reconciler.
function presenceOpen<S extends PresenceOverlay>(state: S, skipAnimations: boolean): S
update() from @llui/components/presence
function update(state: PresenceState, msg: PresenceMsg): [PresenceState, never[]]
Types
PresenceMsg from @llui/components/presence
export type PresenceMsg =
/** @intent("Begin opening the element (closed → opening, plays enter animation)") */
| { type: 'open' }
/** @intent("Begin closing the element (open → closing, plays exit animation)") */
| { type: 'close' }
/** @intent("Toggle between open and closed states") */
| { type: 'toggle' }
/** @humanOnly */
| { type: 'animationEnd' }
/** @intent("Set the desired presence directly (true = open, false = closed)") */
| { type: 'setPresent'; present: boolean }
PresenceStatus from @llui/components/presence
Presence — track mount/unmount lifecycle with exit-delay support.
In many components (dialogs, tooltips, menus) the consumer wants to:
- close the overlay (fire exit animation)
- keep it mounted long enough for the animation to finish
- unmount it
LLui already provides @llui/transitions for most of this, but a
presence machine is useful when you want to coordinate multiple
elements or expose state outside the transition primitive.
State flow: closed → (open) → opening → open open → (close) → closing → closed
The consumer fires animationEnd to advance past opening/closing.
If unmountOnExit is true, closed means "safe to remove from DOM";
otherwise the element stays mounted even when closed (display:none).
export type PresenceStatus = 'closed' | 'opening' | 'open' | 'closing'
Interfaces
PresenceInit from @llui/components/presence
export interface PresenceInit {
/** Initial presence — true starts in 'open', false starts in 'closed'. */
present?: boolean
/** Whether 'closed' means "unmount" (true) or "hidden but mounted" (false). Default: true. */
unmountOnExit?: boolean
}
PresenceOverlay from @llui/components/presence
The presence slice an OVERLAY carries alongside its own state: the logical
open flag plus the animation phase layered over it.
Dialog, drawer, popover, hover-card and tooltip each had a byte-identical private copy of the three transitions below (#126). The machines agreed — their HANDLERS did not — but five copies is five chances to drift, so the transitions live here and every overlay reduces through them.
export interface PresenceOverlay {
open: boolean
/** Optional because a partial `{ open }` bridge slice may omit it; an absent
* status is neither opening nor closing, so it only ever gets WRITTEN. */
status?: PresenceStatus
}
PresenceParts from @llui/components/presence
export interface PresenceParts {
root: {
'data-scope': 'presence'
'data-part': 'root'
'data-state': Signal<PresenceStatus>
hidden: Signal<boolean>
onAnimationEnd: (e: AnimationEvent) => void
onTransitionEnd: (e: TransitionEvent) => void
}
}
PresenceState from @llui/components/presence
export interface PresenceState {
status: PresenceStatus
unmountOnExit: boolean
}
Constants
presence from @llui/components/presence
const presence
@llui/components/signature-pad
Functions
connect() from @llui/components/signature-pad
function connect(state: Signal<SignaturePadState>, send: Send<SignaturePadMsg>, opts: ConnectOptions = {}): SignaturePadParts
getBounds() from @llui/components/signature-pad
Compute the axis-aligned bounding box of all strokes, or null if empty. Useful for cropping the exported signature tightly.
function getBounds(state: SignaturePadState): { x: number; y: number; width: number; height: number } | null
init() from @llui/components/signature-pad
function init(opts: SignaturePadInit = {}): SignaturePadState
isEmpty() from @llui/components/signature-pad
function isEmpty(state: SignaturePadState): boolean
pointCount() from @llui/components/signature-pad
Total number of points across all strokes + current.
function pointCount(state: SignaturePadState): number
update() from @llui/components/signature-pad
function update(state: SignaturePadState, msg: SignaturePadMsg): [SignaturePadState, never[]]
Types
SignaturePadMsg from @llui/components/signature-pad
export type SignaturePadMsg =
/** @humanOnly */
| { type: 'strokeStart'; x: number; y: number; pressure?: number }
/** @humanOnly */
| { type: 'strokePoint'; x: number; y: number; pressure?: number }
/** @humanOnly */
| { type: 'strokeEnd' }
/** @humanOnly */
| { type: 'strokeCancel' }
/** @intent("Undo the last completed stroke") */
| { type: 'undo' }
/** @humanOnly */
| { type: 'redo'; stroke: Stroke }
/** @intent("Erase the entire signature") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'setStrokes'; strokes: Stroke[] }
Stroke from @llui/components/signature-pad
export type Stroke = Point[]
Interfaces
ConnectOptions from @llui/components/signature-pad
export interface ConnectOptions {
label?: string
clearLabel?: string
undoLabel?: string
name?: string
}
Point from @llui/components/signature-pad
Signature pad — capture free-form strokes on a canvas. The state machine tracks strokes as arrays of points; the view renders them onto a
Pointer event wiring in the view layer:
onPointerDown: (e) => { canvas.setPointerCapture(e.pointerId) send({ type: 'strokeStart', x: e.offsetX, y: e.offsetY }) } onPointerMove: (e) => { if (state.drawing) send({ type: 'strokePoint', x: e.offsetX, y: e.offsetY }) } onPointerUp: () => send({ type: 'strokeEnd' })
export interface Point {
x: number
y: number
/** Pressure 0..1 (optional; from PointerEvent.pressure). */
pressure?: number
}
SignaturePadInit from @llui/components/signature-pad
export interface SignaturePadInit {
strokes?: Stroke[]
disabled?: boolean
readonly?: boolean
}
SignaturePadParts from @llui/components/signature-pad
export interface SignaturePadParts {
root: {
role: 'application'
'aria-label': string
'data-scope': 'signature-pad'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
'data-readonly': Signal<'' | undefined>
'data-drawing': Signal<'' | undefined>
}
control: {
'data-scope': 'signature-pad'
'data-part': 'control'
}
clearTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'signature-pad'
'data-part': 'clear-trigger'
onClick: (e: MouseEvent) => void
}
undoTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'signature-pad'
'data-part': 'undo-trigger'
onClick: (e: MouseEvent) => void
}
guide: {
'data-scope': 'signature-pad'
'data-part': 'guide'
'aria-hidden': 'true'
}
hiddenInput: {
type: 'hidden'
value: Signal<string>
name?: string
'data-scope': 'signature-pad'
'data-part': 'hidden-input'
}
}
SignaturePadState from @llui/components/signature-pad
export interface SignaturePadState {
strokes: Stroke[]
/** Stroke currently being drawn, or null. */
current: Stroke | null
drawing: boolean
disabled: boolean
readonly: boolean
}
Constants
signaturePad from @llui/components/signature-pad
const signaturePad
@llui/components/toc
Functions
connect() from @llui/components/toc
function connect(state: Signal<TocState>, send: Send<TocMsg>, opts: ConnectOptions = {}): TocParts
init() from @llui/components/toc
function init(opts: TocInit = {}): TocState
isActive() from @llui/components/toc
function isActive(state: TocState, id: string): boolean
isExpanded() from @llui/components/toc
function isExpanded(state: TocState, id: string): boolean
update() from @llui/components/toc
function update(state: TocState, msg: TocMsg): [TocState, never[]]
watchActiveHeading() from @llui/components/toc
Install an IntersectionObserver that watches heading elements and
dispatches setActive as the user scrolls. Call from onMount and
invoke the returned function on unmount.
rootMargin defaults to '0px 0px -80% 0px' — a heading is considered
active once its top edge enters the top 20% of the viewport.
function watchActiveHeading(send: Send<TocMsg>, selector: string = '[id][data-toc]', rootMargin: string = '0px 0px -80% 0px'): () => void
Types
TocMsg from @llui/components/toc
export type TocMsg =
/** @humanOnly */
| { type: 'setItems'; items: TocEntry[] }
/** @humanOnly */
| { type: 'setActive'; id: string | null }
/** @intent("Toggle the expanded state of the entry with the given id") */
| { type: 'toggleExpanded'; id: string }
/** @intent("Expand every collapsible entry") */
| { type: 'expandAll' }
/** @intent("Collapse every expanded entry") */
| { type: 'collapseAll' }
Interfaces
ConnectOptions from @llui/components/toc
export interface ConnectOptions {
label?: string
/** Prefix for href targets (default: '#'). */
hrefPrefix?: string
expandLabel?: string
}
TocEntry from @llui/components/toc
Table of contents — a navigation list that tracks which heading is
currently visible in the main scroll area and highlights it. The
state machine tracks the flat list of heading ids and the currently
active one; the view layer installs an IntersectionObserver in
onMount to detect which heading is on screen and dispatches
setActive.
Typical setup in onMount:
const headings = document.querySelectorAll('h2[id], h3[id]') const io = new IntersectionObserver((entries) => { for (const e of entries) { if (e.isIntersecting) send({ type: 'setActive', id: e.target.id }) } }, { rootMargin: '0px 0px -80% 0px' }) headings.forEach((h) => io.observe(h)) return () => io.disconnect()
export interface TocEntry {
id: string
label: string
/** Nesting level (1 = top-level). */
level: number
}
TocInit from @llui/components/toc
export interface TocInit {
items?: TocEntry[]
activeId?: string | null
expanded?: string[]
}
TocItemParts from @llui/components/toc
export interface TocItemParts {
item: {
'data-scope': 'toc'
'data-part': 'item'
'data-level': string
'data-active': Signal<'' | undefined>
'data-value': string
}
link: {
href: string
'aria-current': Signal<'location' | undefined>
'data-scope': 'toc'
'data-part': 'link'
'data-active': Signal<'' | undefined>
}
expandTrigger: {
type: 'button'
'aria-expanded': Signal<boolean>
'aria-label': string
'data-scope': 'toc'
'data-part': 'expand-trigger'
'data-state': Signal<'open' | 'closed'>
onClick: (e: MouseEvent) => void
}
}
TocParts from @llui/components/toc
export interface TocParts {
root: {
role: 'navigation'
'aria-label': string
'data-scope': 'toc'
'data-part': 'root'
}
list: {
role: 'list'
'data-scope': 'toc'
'data-part': 'list'
}
item: (entry: TocEntry) => TocItemParts
}
TocState from @llui/components/toc
export interface TocState {
items: TocEntry[]
activeId: string | null
/** Ids of entries the user has manually expanded (for collapsible sub-levels). */
expanded: string[]
}
Constants
toc from @llui/components/toc
const toc
@llui/components/tour
Functions
connect() from @llui/components/tour
function connect(state: Signal<TourState>, send: Send<TourMsg>, opts: ConnectOptions): TourParts
currentStep() from @llui/components/tour
function currentStep(state: TourState): TourStep | null
init() from @llui/components/tour
function init(opts: TourInit = {}): TourState
isFirst() from @llui/components/tour
function isFirst(state: TourState): boolean
isLast() from @llui/components/tour
function isLast(state: TourState): boolean
progress() from @llui/components/tour
function progress(state: TourState): { current: number; total: number }
update() from @llui/components/tour
function update(state: TourState, msg: TourMsg): [TourState, never[]]
Types
TourMsg from @llui/components/tour
export type TourMsg =
/** @intent("Begin the tour at the first step (or current index if resuming)") */
| { type: 'start' }
/** @intent("Close the tour without finishing (does not reset progress)") */
| { type: 'stop' }
/** @intent("Advance to the next step (closes the tour after the last step)") */
| { type: 'next' }
/** @intent("Go back to the previous step") */
| { type: 'prev' }
/** @intent("Jump to a specific step by zero-based index") */
| { type: 'goto'; index: number }
/** @humanOnly */
| { type: 'setSteps'; steps: TourStep[] }
Interfaces
ConnectOptions from @llui/components/tour
export interface ConnectOptions {
id: string
closeLabel?: string
/** Whether clicking the backdrop stops the tour. Default: false — tours
* typically require an explicit dismiss. */
closeOnBackdropClick?: boolean
}
TourInit from @llui/components/tour
export interface TourInit {
steps?: TourStep[]
open?: boolean
index?: number
}
TourParts from @llui/components/tour
export interface TourParts {
root: {
role: 'dialog'
'aria-modal': 'false'
'aria-labelledby': string
'aria-describedby': string
'data-scope': 'tour'
'data-part': 'root'
hidden: Signal<boolean>
}
backdrop: {
'data-scope': 'tour'
'data-part': 'backdrop'
'aria-hidden': 'true'
onClick: (e: MouseEvent) => void
}
spotlight: {
'data-scope': 'tour'
'data-part': 'spotlight'
'aria-hidden': 'true'
}
title: {
id: string
'data-scope': 'tour'
'data-part': 'title'
}
description: {
id: string
'data-scope': 'tour'
'data-part': 'description'
}
progressText: {
'data-scope': 'tour'
'data-part': 'progress-text'
}
prevTrigger: {
type: 'button'
disabled: Signal<boolean>
'data-scope': 'tour'
'data-part': 'prev-trigger'
onClick: (e: MouseEvent) => void
}
nextTrigger: {
type: 'button'
'data-scope': 'tour'
'data-part': 'next-trigger'
'data-last': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
}
closeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'tour'
'data-part': 'close-trigger'
onClick: (e: MouseEvent) => void
}
}
TourState from @llui/components/tour
export interface TourState {
steps: TourStep[]
open: boolean
index: number
/** Ids of steps already visited. */
visited: string[]
}
TourStep from @llui/components/tour
Tour — guided walkthrough over a sequence of steps, each targeting an element on the page with a pop-up explanation. The state machine tracks the current step index and open/closed; positioning of the pop-up relative to the target selector is done in the view layer (typically via onMount + attachFloating).
There is no overlay() helper: connect() returns the part bags and you
render/position the pop-up yourself (typically onMount + attachFloating
against the current step's target).
view: ({ state, send }) => {
const t = tour.connect(state.at('tour'), send, { id: 'tour' })
const step = tour.currentStep(state.peek().tour)
return [
div({ ...t.root }, [
h3({ ...t.title }, [text(step.title)]),
p({ ...t.description }, [text(step.description)]),
button({ ...t.prevTrigger }, [text('Back')]),
button({ ...t.nextTrigger }, [text('Next')]),
]),
]
}
export interface TourStep {
id: string
title: string
description: string
/** CSS selector or element ref for the tour target. */
target: string
/** Placement hint for the pop-up. */
placement?: 'top' | 'bottom' | 'left' | 'right'
/** Whether to show the highlight ring around the target. */
spotlight?: boolean
}
Constants
tour from @llui/components/tour
const tour
@llui/components/date-input
Functions
connect() from @llui/components/date-input
function connect(state: Signal<DateInputState>, send: Send<DateInputMsg>, opts: ConnectOptions = {}): DateInputParts
formatDate() from @llui/components/date-input
Format a Date as 'YYYY-MM-DD'.
function formatDate(d: Date): string
init() from @llui/components/date-input
function init(opts: DateInputInit = {}): DateInputState
parseDate() from @llui/components/date-input
Parse an ISO-ish date string. Accepts:
- YYYY-MM-DD
- YYYY/MM/DD
- MM/DD/YYYY (US)
- DD/MM/YYYY (EU) Returns null for anything else.
function parseDate(input: string, format: 'iso' | 'us' | 'eu' = 'iso'): Date | null
toIsoDate() from @llui/components/date-input
Parse into the canonical YYYY-MM-DD form, or null when unparseable.
Everything that reaches state goes through here, so the comparisons in
validate can rely on zero-padded lexicographic order.
function toIsoDate(input: string, format: 'iso' | 'us' | 'eu' = 'iso'): IsoDate | null
update() from @llui/components/date-input
function update(state: DateInputState, msg: DateInputMsg, format: 'iso' | 'us' | 'eu' = 'iso'): [DateInputState, never[]]
Types
DateError from @llui/components/date-input
Date input — keyboard-only date field with masked parsing. Unlike date-picker, this is a plain that parses ISO-ish date strings as the user types. Separate from date-picker to keep each focused.
The machine holds the raw input string + the parsed date as an ISO
YYYY-MM-DD string (null until a complete/valid value is entered) —
never a Date, so the state stays JSON-serializable like date-picker's
(#119). Min/max bounds are validated on every change, populating error
when out of range, and an unparseable value — from init or setValue
alike — sets error: 'invalid' rather than vanishing.
export type DateError = 'invalid' | 'before-min' | 'after-max' | null
DateInputMsg from @llui/components/date-input
export type DateInputMsg =
/** @intent("Update the raw text the user has typed (re-parses to a date)") */
| { type: 'setInput'; value: string }
/** @intent("Set the parsed date directly as YYYY-MM-DD (also updates the displayed text)") */
| { type: 'setValue'; value: IsoDate | null }
/** @intent("Clear the input and the parsed date") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'setMin'; min: IsoDate | null }
/** @humanOnly */
| { type: 'setMax'; max: IsoDate | null }
/** @humanOnly */
| { type: 'setDisabled'; disabled: boolean }
IsoDate from @llui/components/date-input
A calendar date as YYYY-MM-DD. Zero-padded, so plain </> order it.
export type IsoDate = string
Interfaces
ConnectOptions from @llui/components/date-input
export interface ConnectOptions {
placeholder?: string
clearLabel?: string
}
DateInputInit from @llui/components/date-input
export interface DateInputInit {
input?: string
value?: IsoDate | null
min?: IsoDate | null
max?: IsoDate | null
disabled?: boolean
readonly?: boolean
required?: boolean
}
DateInputParts from @llui/components/date-input
export interface DateInputParts {
root: {
'data-scope': 'date-input'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
'data-invalid': Signal<'' | undefined>
}
input: {
type: 'text'
inputmode: 'numeric'
autocomplete: 'off'
spellcheck: false
value: Signal<string>
disabled: Signal<boolean>
readonly: Signal<boolean>
required: Signal<boolean>
'aria-invalid': Signal<'true' | undefined>
placeholder?: string
'data-scope': 'date-input'
'data-part': 'input'
onInput: (e: Event) => void
onBlur: (e: FocusEvent) => void
}
clearTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'date-input'
'data-part': 'clear-trigger'
onClick: (e: MouseEvent) => void
}
errorText: {
role: 'alert'
'aria-live': 'polite'
'data-scope': 'date-input'
'data-part': 'error-text'
hidden: Signal<boolean>
}
}
DateInputState from @llui/components/date-input
export interface DateInputState {
/** Raw string as typed by the user. */
input: string
/** Parsed date as `YYYY-MM-DD`, or null if empty/invalid. */
value: IsoDate | null
/** Optional lower bound (inclusive), `YYYY-MM-DD`. */
min: IsoDate | null
/** Optional upper bound (inclusive), `YYYY-MM-DD`. */
max: IsoDate | null
error: DateError
disabled: boolean
readonly: boolean
required: boolean
}
Constants
dateInput from @llui/components/date-input
const dateInput
@llui/components/async-list
Functions
connect() from @llui/components/async-list
function connect<T>(state: Signal<AsyncListState<T>>, send: Send<AsyncListMsg<T>>): AsyncListParts
init() from @llui/components/async-list
function init<T = unknown>(opts: AsyncListInit<T> = {}): AsyncListState<T>
isEmpty() from @llui/components/async-list
function isEmpty<T>(state: AsyncListState<T>): boolean
isError() from @llui/components/async-list
function isError<T>(state: AsyncListState<T>): boolean
isLoading() from @llui/components/async-list
function isLoading<T>(state: AsyncListState<T>): boolean
update() from @llui/components/async-list
function update<T>(state: AsyncListState<T>, msg: AsyncListMsg<T>): [AsyncListState<T>, never[]]
watchSentinel() from @llui/components/async-list
Install an IntersectionObserver on the sentinel element that auto-dispatches
loadMore whenever the sentinel scrolls into view. Call from onMount.
function watchSentinel<T>(send: Send<AsyncListMsg<T>>, sentinel: Element, rootMargin: string = '200px'): () => void
Types
AsyncListMsg from @llui/components/async-list
export type AsyncListMsg<T = unknown> =
/** @intent("Request the next page of items") */
| { type: 'loadMore' }
/** @humanOnly */
| { type: 'pageLoaded'; items: T[]; hasMore: boolean }
/** @humanOnly */
| { type: 'pageFailed'; error: string }
/** @intent("Discard the loaded items and reset back to page 0") */
| { type: 'reset' }
/** @humanOnly */
| { type: 'setItems'; items: T[]; hasMore?: boolean }
/** @intent("Retry the last failed page request") */
| { type: 'retry' }
AsyncStatus from @llui/components/async-list
Async list — paginated/infinite-scroll list that accumulates pages.
The machine is generic over the item type; the consumer runs the
actual fetch in response to loadMore (via a custom handler or
effect) and dispatches pageLoaded/pageFailed when the request
completes.
Typical flow in consumer's update handler:
(state, msg) => {
if (msg.type === 'loadMore') {
fetch(/api/items?page=${state.list.page + 1})
.then(r => r.json())
.then(items => send({type: 'pageLoaded', items, hasMore: items.length === PAGE_SIZE}))
.catch(e => send({type: 'pageFailed', error: String(e)}))
}
}
export type AsyncStatus = 'idle' | 'loading' | 'loaded' | 'error'
Interfaces
AsyncListInit from @llui/components/async-list
export interface AsyncListInit<T = unknown> {
items?: T[]
page?: number
hasMore?: boolean
}
AsyncListParts from @llui/components/async-list
export interface AsyncListParts {
root: {
'data-scope': 'async-list'
'data-part': 'root'
'data-status': Signal<AsyncStatus>
}
sentinel: {
'data-scope': 'async-list'
'data-part': 'sentinel'
'aria-hidden': 'true'
}
loadMoreTrigger: {
type: 'button'
disabled: Signal<boolean>
'data-scope': 'async-list'
'data-part': 'load-more-trigger'
onClick: (e: MouseEvent) => void
}
retryTrigger: {
type: 'button'
'data-scope': 'async-list'
'data-part': 'retry-trigger'
hidden: Signal<boolean>
onClick: (e: MouseEvent) => void
}
errorText: {
role: 'alert'
'aria-live': 'polite'
'data-scope': 'async-list'
'data-part': 'error-text'
hidden: Signal<boolean>
}
}
AsyncListState from @llui/components/async-list
export interface AsyncListState<T = unknown> {
items: T[]
page: number
hasMore: boolean
status: AsyncStatus
error: string | null
}
Constants
asyncList from @llui/components/async-list
const asyncList
@llui/components/cascade-select
Functions
completeValues() from @llui/components/cascade-select
function completeValues(state: CascadeSelectState): string[] | null
connect() from @llui/components/cascade-select
function connect(state: Signal<CascadeSelectState>, send: Send<CascadeSelectMsg>, opts: ConnectOptions): CascadeSelectParts
init() from @llui/components/cascade-select
function init(opts: CascadeSelectInit = {}): CascadeSelectState
isComplete() from @llui/components/cascade-select
function isComplete(state: CascadeSelectState): boolean
isLevelReady() from @llui/components/cascade-select
function isLevelReady(state: CascadeSelectState, levelIndex: number): boolean
update() from @llui/components/cascade-select
function update(state: CascadeSelectState, msg: CascadeSelectMsg): [CascadeSelectState, never[]]
Types
CascadeSelectMsg from @llui/components/cascade-select
export type CascadeSelectMsg =
/** @humanOnly */
| { type: 'setLevels'; levels: CascadeLevel[] }
/** @intent("Pick a value at the given level (clears selections at deeper levels)") */
| { type: 'setValue'; levelIndex: number; value: string | null }
/** @intent("Clear every level's selection") */
| { type: 'clear' }
Interfaces
CascadeLevel from @llui/components/cascade-select
Cascade select — a series of dependent selects where each level's choice filters the options of the next. Classic example: country → state → city. The machine stores a flat list of selections (one per level, or null) and the options at each level; filtering logic is left to the view/consumer.
Level shape: the consumer passes an array of Level descriptors on setLevels, each with its own options. Selecting at level N clears selections at levels > N.
export interface CascadeLevel {
id: string
label: string
options: Array<{ value: string; label: string; disabled?: boolean }>
}
CascadeLevelParts from @llui/components/cascade-select
export interface CascadeLevelParts {
label: {
for: string
'data-scope': 'cascade-select'
'data-part': 'level-label'
}
select: {
id: string
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'cascade-select'
'data-part': 'level-select'
'data-level': string
'data-ready': Signal<'' | undefined>
onChange: (e: Event) => void
}
}
CascadeSelectInit from @llui/components/cascade-select
export interface CascadeSelectInit {
levels?: CascadeLevel[]
values?: (string | null)[]
disabled?: boolean
}
CascadeSelectParts from @llui/components/cascade-select
export interface CascadeSelectParts {
root: {
'data-scope': 'cascade-select'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
'data-complete': Signal<'' | undefined>
}
clearTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'cascade-select'
'data-part': 'clear-trigger'
onClick: (e: MouseEvent) => void
}
level: (index: number) => CascadeLevelParts
}
CascadeSelectState from @llui/components/cascade-select
export interface CascadeSelectState {
levels: CascadeLevel[]
/** Parallel to levels: one value per level, or null. */
values: (string | null)[]
disabled: boolean
}
ConnectOptions from @llui/components/cascade-select
export interface ConnectOptions {
id: string
clearLabel?: string
}
Constants
cascadeSelect from @llui/components/cascade-select
const cascadeSelect
@llui/components/scroll-area
Functions
connect() from @llui/components/scroll-area
function connect(state: Signal<ScrollAreaState>, send: Send<ScrollAreaMsg>): ScrollAreaParts
init() from @llui/components/scroll-area
function init(opts: ScrollAreaInit = {}): ScrollAreaState
showScrollbars() from @llui/components/scroll-area
Whether the scrollbars should be visible given the state.
function showScrollbars(state: ScrollAreaState, axis: 'x' | 'y'): boolean
thumbPosition() from @llui/components/scroll-area
Thumb position as a proportion (0..1) along the track.
function thumbPosition(state: ScrollAreaState, axis: 'x' | 'y'): number
thumbSize() from @llui/components/scroll-area
Thumb size as a proportion (0..1) of the track.
function thumbSize(state: ScrollAreaState, axis: 'x' | 'y'): number
update() from @llui/components/scroll-area
function update(state: ScrollAreaState, msg: ScrollAreaMsg): [ScrollAreaState, never[]]
Types
ScrollAreaMsg from @llui/components/scroll-area
export type ScrollAreaMsg =
/** @humanOnly */
| {
type: 'setScroll'
scrollTop: number
scrollLeft: number
scrollWidth: number
scrollHeight: number
clientWidth: number
clientHeight: number
}
/** @humanOnly */
| { type: 'setScrolling'; scrolling: boolean }
/** @humanOnly */
| { type: 'setHovered'; hovered: boolean }
ScrollbarVisibility from @llui/components/scroll-area
Scroll area — custom-styled scroll container with scrollbars that
can be hidden/shown based on scroll activity or hover. The state
machine tracks scroll position + overflow flags (whether content
actually overflows each axis); the view layer installs listeners
that populate these via setScroll / setOverflow.
This component is primarily a structural shell — the real scrolling is done by the browser (overflow: auto) on the viewport element. The machine just tracks position so the view can render custom thumbs positioned proportionally.
Typical onMount wiring:
const viewport = root.querySelector('[data-part="viewport"]') const sync = () => send({type:'setScroll', ...dimsOf(viewport)}) viewport.addEventListener('scroll', sync) const ro = new ResizeObserver(sync); ro.observe(viewport); ro.observe(content) sync() // initial
export type ScrollbarVisibility = 'auto' | 'always' | 'hover' | 'scroll'
Interfaces
ScrollAreaInit from @llui/components/scroll-area
export interface ScrollAreaInit {
visibility?: ScrollbarVisibility
}
ScrollAreaParts from @llui/components/scroll-area
export interface ScrollAreaParts {
root: {
'data-scope': 'scroll-area'
'data-part': 'root'
'data-scrolling': Signal<'' | undefined>
'data-hovered': Signal<'' | undefined>
onMouseEnter: (e: MouseEvent) => void
onMouseLeave: (e: MouseEvent) => void
}
viewport: {
tabindex: 0
'data-scope': 'scroll-area'
'data-part': 'viewport'
onScroll: (e: Event) => void
}
content: {
'data-scope': 'scroll-area'
'data-part': 'content'
}
scrollbarX: {
'data-scope': 'scroll-area'
'data-part': 'scrollbar'
'data-axis': 'x'
'data-visible': Signal<'' | undefined>
}
scrollbarY: {
'data-scope': 'scroll-area'
'data-part': 'scrollbar'
'data-axis': 'y'
'data-visible': Signal<'' | undefined>
}
thumbX: {
'data-scope': 'scroll-area'
'data-part': 'thumb'
'data-axis': 'x'
style: Signal<string>
}
thumbY: {
'data-scope': 'scroll-area'
'data-part': 'thumb'
'data-axis': 'y'
style: Signal<string>
}
corner: {
'data-scope': 'scroll-area'
'data-part': 'corner'
'data-visible': Signal<'' | undefined>
}
}
ScrollAreaState from @llui/components/scroll-area
export interface ScrollAreaState extends ScrollDims {
overflowX: boolean
overflowY: boolean
/** Whether the user is currently scrolling (set/cleared by the consumer
* via a debounced scroll handler). */
scrolling: boolean
/** Whether the pointer is over the scroll area. */
hovered: boolean
visibility: ScrollbarVisibility
}
ScrollDims from @llui/components/scroll-area
export interface ScrollDims {
scrollTop: number
scrollLeft: number
scrollWidth: number
scrollHeight: number
clientWidth: number
clientHeight: number
}
Constants
scrollArea from @llui/components/scroll-area
const scrollArea
@llui/components/floating-panel
Functions
connect() from @llui/components/floating-panel
function connect(state: Signal<FloatingPanelState>, send: Send<FloatingPanelMsg>, opts: ConnectOptions = {}): FloatingPanelParts
init() from @llui/components/floating-panel
function init(opts: FloatingPanelInit = {}): FloatingPanelState
update() from @llui/components/floating-panel
function update(state: FloatingPanelState, msg: FloatingPanelMsg): [FloatingPanelState, never[]]
Types
FloatingPanelMsg from @llui/components/floating-panel
export type FloatingPanelMsg =
/** @intent("Open the floating panel") */
| { type: 'open' }
/** @intent("Close the floating panel") */
| { type: 'close' }
/** @intent("Minimize the panel (collapses to title bar)") */
| { type: 'minimize' }
/** @intent("Restore the panel from its minimized state") */
| { type: 'restoreFromMinimized' }
/** @intent("Maximize the panel (fills the viewport)") */
| { type: 'maximize' }
/** @intent("Restore the panel to its pre-maximize geometry") */
| { type: 'restoreFromMaximized' }
/** @intent("Toggle between minimized and normal") */
| { type: 'toggleMinimize' }
/** @intent("Toggle between maximized and normal") */
| { type: 'toggleMaximize' }
/** @humanOnly */
| { type: 'dragStart' }
/** @humanOnly */
| { type: 'dragMove'; dx: number; dy: number }
/** @humanOnly */
| { type: 'dragEnd' }
/** @humanOnly */
| { type: 'resizeStart'; handle: ResizeHandle }
/** @humanOnly */
| { type: 'resizeMove'; dx: number; dy: number }
/** @humanOnly */
| { type: 'resizeEnd' }
/** @intent("Set the panel's top-left position in pixels") */
| { type: 'setPosition'; x: number; y: number }
/** @intent("Set the panel's size in pixels (clamped to min/max)") */
| { type: 'setSize'; width: number; height: number }
ResizeHandle from @llui/components/floating-panel
Floating panel — a draggable + resizable window-like surface, useful for dev tools overlays, pop-out inspectors, preview panels, etc. The state machine tracks position and size; the view layer wires pointer events on the drag handle and resize grips and dispatches the corresponding messages.
Coordinates are in pixels relative to the positioning container
(typically position: fixed relative to the viewport).
export type ResizeHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
Interfaces
ConnectOptions from @llui/components/floating-panel
export interface ConnectOptions {
label?: string
minimizeLabel?: string
maximizeLabel?: string
closeLabel?: string
}
FloatingPanelInit from @llui/components/floating-panel
export interface FloatingPanelInit {
position?: { x: number; y: number }
size?: { width: number; height: number }
minSize?: { width?: number; height?: number }
maxSize?: { width?: number; height?: number } | null
open?: boolean
disabled?: boolean
}
FloatingPanelParts from @llui/components/floating-panel
export interface FloatingPanelParts {
root: {
role: 'dialog'
'aria-label': string
'data-scope': 'floating-panel'
'data-part': 'root'
'data-dragging': Signal<'' | undefined>
'data-resizing': Signal<'' | undefined>
'data-minimized': Signal<'' | undefined>
'data-maximized': Signal<'' | undefined>
hidden: Signal<boolean>
style: Signal<string>
}
dragHandle: {
'data-scope': 'floating-panel'
'data-part': 'drag-handle'
onPointerDown: (e: PointerEvent) => void
}
content: {
'data-scope': 'floating-panel'
'data-part': 'content'
hidden: Signal<boolean>
}
minimizeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'floating-panel'
'data-part': 'minimize-trigger'
onClick: (e: MouseEvent) => void
}
maximizeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'floating-panel'
'data-part': 'maximize-trigger'
onClick: (e: MouseEvent) => void
}
closeTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'floating-panel'
'data-part': 'close-trigger'
onClick: (e: MouseEvent) => void
}
resizeHandle: (handle: ResizeHandle) => {
'data-scope': 'floating-panel'
'data-part': 'resize-handle'
'data-handle': ResizeHandle
onPointerDown: (e: PointerEvent) => void
}
}
FloatingPanelState from @llui/components/floating-panel
export interface FloatingPanelState {
position: { x: number; y: number }
size: { width: number; height: number }
minSize: { width: number; height: number }
/**
* The upper size bound. `null` — or an absent dimension — is unbounded on
* that axis, which is what `clampSize` already spelled `?? Infinity` at the
* point of use. A bound in state is finite or absent, never an infinity
* (`JSON.stringify` writes `null` for one) and never `NaN` (which is not
* nullish, so it survives the `??` and switches that axis's clamp off) —
* #177.
*/
maxSize: { width?: number; height?: number } | null
open: boolean
minimized: boolean
maximized: boolean
dragging: boolean
resizing: ResizeHandle | null
/** Snapshot of the pre-maximize geometry (for restore). */
restoreBounds: { x: number; y: number; width: number; height: number } | null
disabled: boolean
}
Constants
floatingPanel from @llui/components/floating-panel
const floatingPanel
@llui/components/image-cropper
Functions
centerFill() from @llui/components/image-cropper
Compute the largest centered crop that fits image while respecting the
aspect ratio (if any).
function centerFill(image: { width: number; height: number }, aspectRatio: number | null): CropRect
connect() from @llui/components/image-cropper
function connect(state: Signal<ImageCropperState>, send: Send<ImageCropperMsg>, opts: ConnectOptions = {}): ImageCropperParts
init() from @llui/components/image-cropper
function init(opts: ImageCropperInit = {}): ImageCropperState
update() from @llui/components/image-cropper
function update(state: ImageCropperState, msg: ImageCropperMsg): [ImageCropperState, never[]]
Types
ImageCropperMsg from @llui/components/image-cropper
export type ImageCropperMsg =
/** @humanOnly */
| { type: 'setImage'; width: number; height: number }
/** @intent("Set the crop rectangle (x/y/width/height in image-native pixels)") */
| { type: 'setCrop'; crop: CropRect }
/** @intent("Lock the crop to a specific aspect ratio (width/height), or null for free-form") */
| { type: 'setAspectRatio'; ratio: number | null }
/** @humanOnly */
| { type: 'dragStart' }
/** @humanOnly */
| { type: 'dragMove'; dx: number; dy: number }
/** @humanOnly */
| { type: 'dragEnd' }
/** @humanOnly */
| { type: 'resizeStart'; handle: ResizeHandle }
/** @humanOnly */
| { type: 'resizeMove'; dx: number; dy: number }
/** @humanOnly */
| { type: 'resizeEnd' }
/** @intent("Reset the crop to a default selection (full image or aspect-fit)") */
| { type: 'reset' }
/** @intent("Set the crop to a maximum-area centered selection") */
| { type: 'centerFill' }
ResizeHandle from @llui/components/image-cropper
Image cropper — select a rectangular crop region over an image, optionally constrained to an aspect ratio. The machine tracks the image's natural dimensions, the crop rectangle, and in-progress drag/resize state. The view layer wires pointer events on the crop box and its resize handles.
Coordinates are in image-native pixels (0..naturalWidth, 0..naturalHeight). The consumer converts to display pixels using the image's rendered size.
export type ResizeHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
Interfaces
ConnectOptions from @llui/components/image-cropper
export interface ConnectOptions {
resetLabel?: string
}
CropRect from @llui/components/image-cropper
export interface CropRect {
x: number
y: number
width: number
height: number
}
ImageCropperInit from @llui/components/image-cropper
export interface ImageCropperInit {
image?: { width: number; height: number }
crop?: CropRect
aspectRatio?: number | null
minSize?: number
disabled?: boolean
}
ImageCropperParts from @llui/components/image-cropper
export interface ImageCropperParts {
root: {
'data-scope': 'image-cropper'
'data-part': 'root'
'data-dragging': Signal<'' | undefined>
'data-resizing': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
}
image: {
'data-scope': 'image-cropper'
'data-part': 'image'
onLoad: (e: Event) => void
draggable: false
}
cropBox: {
'data-scope': 'image-cropper'
'data-part': 'crop-box'
style: Signal<string>
onPointerDown: (e: PointerEvent) => void
}
resizeHandle: (handle: ResizeHandle) => {
'data-scope': 'image-cropper'
'data-part': 'resize-handle'
'data-handle': ResizeHandle
onPointerDown: (e: PointerEvent) => void
}
resetTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'image-cropper'
'data-part': 'reset-trigger'
onClick: (e: MouseEvent) => void
}
}
ImageCropperState from @llui/components/image-cropper
export interface ImageCropperState {
/** Natural dimensions of the source image. */
image: { width: number; height: number }
crop: CropRect
/** Constrain the crop to this aspect ratio (width / height), or null to free-form. */
aspectRatio: number | null
minSize: number
dragging: boolean
resizing: ResizeHandle | null
disabled: boolean
}
Constants
imageCropper from @llui/components/image-cropper
const imageCropper
@llui/components/navigation-menu
Functions
connect() from @llui/components/navigation-menu
function connect(state: Signal<NavMenuState>, send: Send<NavMenuMsg>, opts: ConnectOptions): NavMenuParts
init() from @llui/components/navigation-menu
function init(opts: NavMenuInit = {}): NavMenuState
isOpen() from @llui/components/navigation-menu
function isOpen(state: NavMenuState, id: string): boolean
update() from @llui/components/navigation-menu
function update(state: NavMenuState, msg: NavMenuMsg): [NavMenuState, never[]]
Types
NavMenuMsg from @llui/components/navigation-menu
export type NavMenuMsg =
/** @intent("Open the submenu identified by id, closing any open siblings") */
| { type: 'openBranch'; id: string; ancestorIds: string[] }
/** @intent("Close the submenu identified by id (also closes its descendants)") */
| { type: 'closeBranch'; id: string }
/** @intent("Toggle the submenu identified by id open/closed") */
| { type: 'toggleBranch'; id: string; ancestorIds: string[] }
/** @intent("Close every open submenu") */
| { type: 'closeAll' }
/** @humanOnly */
| { type: 'focus'; id: string | null }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
/** @intent("Replace the list of ids eligible for the roving tab stop, in document order") */
| { type: 'setItems'; items: string[] }
Interfaces
ConnectOptions from @llui/components/navigation-menu
export interface ConnectOptions {
id: string
label?: string
/**
* Whether pointer-leaving the whole menu closes everything. Default: true.
* The consumer can inject their own close delay by intercepting
* onPointerLeave + calling setTimeout + dispatching closeAll.
*/
closeOnLeave?: boolean
}
NavItemParts from @llui/components/navigation-menu
export interface NavItemParts {
trigger: {
type: 'button'
id: string
/** For a branch item this is the disclosure button controlling its panel;
* `undefined` for a plain link trigger. */
'aria-controls': string | undefined
'aria-expanded': Signal<boolean | undefined>
'data-scope': 'navigation-menu'
'data-part': 'trigger'
'data-state': Signal<'open' | 'closed'>
'data-value': string
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onPointerEnter: (e: PointerEvent) => void
onFocus: (e: FocusEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
content: {
id: string
'aria-labelledby': string
'data-scope': 'navigation-menu'
'data-part': 'content'
'data-state': Signal<'open' | 'closed'>
hidden: Signal<boolean>
onPointerEnter: (e: PointerEvent) => void
}
}
NavMenuInit from @llui/components/navigation-menu
export interface NavMenuInit {
open?: string[]
focused?: string | null
/** Ids eligible for the roving tab stop, in document order — see
* `NavMenuState.items`. */
items?: string[]
disabled?: boolean
dir?: 'ltr' | 'rtl'
}
NavMenuParts from @llui/components/navigation-menu
export interface NavMenuParts {
root: {
// Site navigation is NOT an application menu: it uses a `nav` landmark with
// disclosure buttons, not menubar/menu/menuitem roles. Render the root as a
// `<nav>` element; `aria-label` names the landmark.
'aria-label': string
'data-scope': 'navigation-menu'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
onPointerLeave: (e: PointerEvent) => void
onPointerEnter: (e: PointerEvent) => void
}
/**
* Parts for one trigger (+ its panel when it is a branch).
*
* `ancestorIds` is the open-path this item lives under, root-first. It drives
* sibling-closing in `openBranch` AND the roving tab stop: an item whose
* ancestors are not all open is inside a `hidden` panel and is skipped when
* the stop is resolved.
*
* REQUIRED on every NESTED item, leaf ones included. It used to be read only
* inside `isBranch` guards, so passing it on a leaf was optional in practice;
* since #145 it is what makes a leaf's tabbability knowable. Omitting it
* reads as "top level", which lets the tab stop sit inside a closed submenu
* where no Tab press can reach it.
*/
item: (id: string, options: { isBranch: boolean; ancestorIds?: string[] }) => NavItemParts
}
NavMenuState from @llui/components/navigation-menu
Navigation menu — multi-level menu bar with hover/focus-triggered
submenus. Unlike menu (a single dropdown), navigation-menu supports
nested submenus arbitrarily deep and is typically used for primary
site navigation.
State tracks the currently focused item id and the ids of all currently-open branches. The consumer provides the tree structure (items with optional children); the machine doesn't index the hierarchy itself — it just maintains open-paths and lets the view handle traversal.
Typical interaction model (delay-based):
- Pointer enter on a branch → openBranch after openDelay
- Pointer leave of the whole tree → closeAll after closeDelay
- Click/keyboard activation → toggleBranch immediately
The consumer is responsible for debouncing via setTimeout; the machine just responds to the dispatched messages.
export interface NavMenuState {
/** Ids of open branches, in open order (root-first). Closing an
* ancestor automatically closes its descendants. */
open: string[]
focused: string | null
/**
* Ids of the items ELIGIBLE for the roving tab stop, in document order, when
* the consumer renders a DYNAMIC list — the top-level items in the usual
* case; add deeper ids if a submenu item should be able to own the stop.
*
* It is both the fallback and the membership list: while nothing is focused
* the first entry owns the nav's single tab stop, and a `focused` id that is
* not one of these entries has been removed, so the stop falls back rather
* than vanishing (#145).
*
* Leave it empty for a static menu — `connect` then uses the ids handed to
* `item()`, in call order, as the membership list instead, which is document
* order for any depth-first view. Same escape hatch as `radio-group`/`tabs`,
* which keep their `items` list in state for exactly this reason.
*
* Either way the candidates are filtered to the ones currently TABBABLE: an
* id whose `ancestorIds` are not all in `open` sits inside a `hidden`
* submenu panel and cannot carry the stop.
*/
items: string[]
disabled: boolean
/** Reading direction. Under 'rtl', ArrowLeft/ArrowRight swap meaning. */
dir: 'ltr' | 'rtl'
}
Constants
navigationMenu from @llui/components/navigation-menu
const navigationMenu
@llui/components/qr-code
Functions
connect() from @llui/components/qr-code
function connect(state: Signal<QrCodeState>, send: Send<QrCodeMsg>, opts: ConnectOptions = {}): QrCodeParts
init() from @llui/components/qr-code
function init(opts: QrCodeInit = {}): QrCodeState
size() from @llui/components/qr-code
Matrix side length (in modules). Returns 0 for empty matrix.
function size(state: QrCodeState): number
toDataUrl() from @llui/components/qr-code
Encode the matrix as a monochrome 1-bit-per-pixel PNG-ish URL. This
is a helper for consumption — it generates a
data:image/svg+xml
URL (SVG is simpler and scales losslessly).
function toDataUrl(matrix: boolean[][], foreground: string = '#000', background: string = '#fff'): string
toSvgPath() from @llui/components/qr-code
Compute an SVG path string that fills every dark module. Each dark
module becomes a unit-sized square at (col, row) coordinates in module
space; the caller scales via viewBox or CSS. Using a single path
is vastly more performant than rendering N² individual
function toSvgPath(matrix: boolean[][]): string
update() from @llui/components/qr-code
function update(state: QrCodeState, msg: QrCodeMsg): [QrCodeState, never[]]
Types
ErrorCorrectionLevel from @llui/components/qr-code
QR code — renders a QR matrix as SVG. llui does not bundle a QR
encoder (encoders are sizable and consumer apps typically already
have one); instead, the consumer provides the encoded matrix via
setMatrix (or through the optional encode callback on
ConnectOptions, invoked when the value changes).
Minimum usage with a BYOE (bring-your-own-encoder) library — the
consumer dispatches setMatrix with the encoded bits from their
update handler:
import QRCode from 'qrcode-generator'
update: (state, msg) => { if (msg.type === 'updateQr') { const q = QRCode(0, state.qr.errorCorrection) q.addData(msg.value); q.make() const n = q.getModuleCount() const matrix: boolean[][] = [] for (let y = 0; y < n; y++) { const row: boolean[] = [] for (let x = 0; x < n; x++) row.push(q.isDark(y, x)) matrix.push(row) } return [{ ...state, qr: { ...state.qr, value: msg.value, matrix } }, []] } }
export type ErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H'
QrCodeMsg from @llui/components/qr-code
export type QrCodeMsg =
/** @intent("Set the encoded value (consumer encodes externally and dispatches setMatrix)") */
| { type: 'setValue'; value: string }
/** @humanOnly */
| { type: 'setMatrix'; matrix: boolean[][] }
/** @intent("Change the QR error-correction level (L/M/Q/H)") */
| { type: 'setErrorCorrection'; level: ErrorCorrectionLevel }
Interfaces
ConnectOptions from @llui/components/qr-code
export interface ConnectOptions {
label?: string
downloadLabel?: string
/** Filename for the downloaded SVG. */
downloadFilename?: string
}
QrCodeInit from @llui/components/qr-code
export interface QrCodeInit {
value?: string
matrix?: boolean[][]
errorCorrection?: ErrorCorrectionLevel
}
QrCodeParts from @llui/components/qr-code
export interface QrCodeParts {
root: {
'data-scope': 'qr-code'
'data-part': 'root'
'aria-label': string
}
svg: {
'data-scope': 'qr-code'
'data-part': 'svg'
role: 'img'
viewBox: Signal<string>
'shape-rendering': 'crispEdges'
}
background: {
'data-scope': 'qr-code'
'data-part': 'background'
}
foreground: {
'data-scope': 'qr-code'
'data-part': 'foreground'
d: Signal<string>
}
downloadTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'qr-code'
'data-part': 'download-trigger'
onClick: (e: MouseEvent) => void
}
}
QrCodeState from @llui/components/qr-code
export interface QrCodeState {
value: string
/** NxN boolean matrix — true means dark (filled) module. */
matrix: boolean[][]
errorCorrection: ErrorCorrectionLevel
}
Constants
qrCode from @llui/components/qr-code
const qrCode
@llui/components/carousel
Functions
autoplayEffects() from @llui/components/carousel
The effects that bring the timer in line with state from a cold start.
init() returns state only, so a consumer seeds the timer with this from
its own init().
function autoplayEffects(state: CarouselState): CarouselEffect[]
canGoNext() from @llui/components/carousel
function canGoNext(state: CarouselState): boolean
canGoPrev() from @llui/components/carousel
function canGoPrev(state: CarouselState): boolean
connect() from @llui/components/carousel
function connect(state: Signal<CarouselState>, send: Send<CarouselMsg>, opts: ConnectOptions): CarouselParts
init() from @llui/components/carousel
function init(opts: CarouselInit = {}): CarouselState
isAutoplayRunning() from @llui/components/carousel
Whether the autoplay timer should be running. A single slide has nowhere to
advance to, and a swipe in flight suspends autoplay so the slide doesn't move
out from under the user's finger — the same condition data-paused exposes.
function isAutoplayRunning(state: CarouselState): boolean
swipeDecision() from @llui/components/carousel
Pure swipe resolver. Given a state with an active drag, decide whether the gesture commits to the previous/next slide or snaps back to the current one.
- A leftward swipe (deltaX < 0) that crosses
swipeThresholdtargets the NEXT slide; a rightward swipe (deltaX > 0) targets the PREVIOUS. - Below the threshold, or with no active drag, it snaps back.
- At a non-loop boundary the target direction is unavailable, so it snaps back. With loop enabled the move always commits (wraps).
function swipeDecision(state: CarouselState): 'prev' | 'next' | 'snap'
update() from @llui/components/carousel
function update(state: CarouselState, msg: CarouselMsg): [CarouselState, CarouselEffect[]]
Types
CarouselEffect from @llui/components/carousel
Effects emitted by the carousel machine. Running the timer is the consumer's job — the machine only says when it should run (see the module header).
export type CarouselEffect =
/**
* Run the autoplay timer: dispatch `autoplayTick` every `interval` ms.
* Re-emitted to RESTART a timer already running (manual navigation, an
* `interval` change), so the handler must replace rather than add.
*/
| { type: 'startAutoplay'; interval: number }
/** Retire the autoplay timer. */
| { type: 'stopAutoplay' }
CarouselMsg from @llui/components/carousel
export type CarouselMsg =
/** @intent("Jump to a specific slide by zero-based index") */
| { type: 'goTo'; index: number }
/** @intent("Advance to the next slide (wraps if loop is enabled)") */
| { type: 'next' }
/** @intent("Go back to the previous slide (wraps if loop is enabled)") */
| { type: 'prev' }
/** @humanOnly */
| { type: 'setCount'; count: number }
/** @intent("Pause autoplay (typically while user hovers or focuses the carousel)") */
| { type: 'pause' }
/** @intent("Resume autoplay after a pause") */
| { type: 'resume' }
/** @intent("Turn autoplay on or off") */
| { type: 'setAutoplay'; autoplay: boolean }
/**
* The autoplay timer fired. Advances exactly like `next`, but does NOT
* restart the timer — it IS the timer. Manual navigation restarts it; a tick
* must not, or the period would be re-armed on every fire.
*
* `@humanOnly` because it is the TIMER's message, not a user intent: with no
* tag it defaulted to dispatchMode `'shared'` and an agent could fire the
* timer directly, advancing the carousel without re-arming the period
* (#138 review, item 8). An agent that wants the next slide sends `next`.
*
* @humanOnly
*/
| { type: 'autoplayTick' }
/** @humanOnly */
| { type: 'dragStart'; x: number }
/** @humanOnly */
| { type: 'dragMove'; x: number }
/** @humanOnly */
| { type: 'dragEnd' }
/** @intent("Set the reading direction (ltr/rtl)") */
| { type: 'setDir'; dir: 'ltr' | 'rtl' }
Interfaces
CarouselDrag from @llui/components/carousel
Live pointer-swipe state. JSON-serializable: just the start X and the accumulated horizontal delta (positive = dragged right, negative = left). The view supplies pointer coordinates; the machine does pure math.
export interface CarouselDrag {
startX: number
deltaX: number
}
CarouselInit from @llui/components/carousel
export interface CarouselInit {
current?: number
count?: number
loop?: boolean
autoplay?: boolean
interval?: number
swipeThreshold?: number
dir?: 'ltr' | 'rtl'
}
CarouselParts from @llui/components/carousel
export interface CarouselParts {
root: {
role: 'region'
'aria-roledescription': 'carousel'
'aria-label': string
'data-scope': 'carousel'
'data-part': 'root'
'data-paused': Signal<'' | undefined>
onPointerEnter: (e: PointerEvent) => void
onPointerLeave: (e: PointerEvent) => void
onFocus: (e: FocusEvent) => void
onBlur: (e: FocusEvent) => void
}
viewport: {
'data-scope': 'carousel'
'data-part': 'viewport'
/**
* Set while a pointer swipe is in flight — consumers gate the slide-track
* transition off (`[data-dragging] { transition: none }`) so the track
* follows the finger 1:1 instead of easing.
*/
'data-dragging': Signal<'' | undefined>
/** Live track offset (px) to follow the finger: `translateX(var)`. */
'data-drag-offset': Signal<string | undefined>
onPointerDown: (e: PointerEvent) => void
onPointerMove: (e: PointerEvent) => void
onPointerUp: (e: PointerEvent) => void
onPointerCancel: (e: PointerEvent) => void
}
indicatorGroup: {
role: 'tablist'
'aria-label': string
'data-scope': 'carousel'
'data-part': 'indicator-group'
}
nextTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'carousel'
'data-part': 'next-trigger'
onClick: (e: MouseEvent) => void
}
prevTrigger: {
type: 'button'
'aria-label': string
disabled: Signal<boolean>
'data-scope': 'carousel'
'data-part': 'prev-trigger'
onClick: (e: MouseEvent) => void
}
slide: (index: number) => CarouselSlideParts
}
CarouselSlideParts from @llui/components/carousel
export interface CarouselSlideParts {
slide: {
role: 'tabpanel'
id: string
'aria-roledescription': 'slide'
'aria-label': string
'data-scope': 'carousel'
'data-part': 'slide'
'data-index': string
'data-active': Signal<'' | undefined>
hidden: Signal<boolean>
}
indicator: {
type: 'button'
role: 'tab'
'aria-label': string
'aria-selected': Signal<boolean>
'aria-controls': string
'data-scope': 'carousel'
'data-part': 'indicator'
'data-index': string
'data-active': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
}
CarouselState from @llui/components/carousel
export interface CarouselState {
current: number
count: number
loop: boolean
autoplay: boolean
interval: number
paused: boolean
/** Direction of the last transition — useful for entry animations. */
direction: 'forward' | 'backward'
/**
* Minimum absolute horizontal distance (px) a swipe must cross to commit
* to the previous/next slide. Below this the drag snaps back.
*/
swipeThreshold: number
/** Active pointer swipe, or null when idle. */
dragging: CarouselDrag | null
/** Reading direction. Under 'rtl' indicator horizontal arrow keys are flipped. */
dir: 'ltr' | 'rtl'
}
ConnectOptions from @llui/components/carousel
export interface ConnectOptions {
id: string
label?: string
indicatorLabel?: string
nextLabel?: string
prevLabel?: string
/** Builder for each slide's aria-label. Receives index + known count. */
slideLabel?: (index: number, count: number) => string
}
Constants
carousel from @llui/components/carousel
const carousel
@llui/components/field
Functions
connect() from @llui/components/field
function connect(state: Signal<FieldState>, send: Send<FieldMsg>, opts: FieldConnectOptions = {}): FieldParts
init() from @llui/components/field
function init(opts: FieldInit): FieldState
update() from @llui/components/field
function update(state: FieldState, msg: FieldMsg): [FieldState, never[]]
Types
FieldMsg from @llui/components/field
export type FieldMsg =
/** @intent("Mark the field as valid or invalid (drives aria-invalid + the error region)") */
| { type: 'setInvalid'; invalid: boolean }
/** @intent("Mark the field as required or optional") */
| { type: 'setRequired'; required: boolean }
/** @intent("Enable or disable the field's control") */
| { type: 'setDisabled'; disabled: boolean }
/** @intent("Make the field's control read-only or editable") */
| { type: 'setReadonly'; readonly: boolean }
/** @intent("Mark the field as touched (typically after first blur)") */
| { type: 'setTouched'; touched: boolean }
Interfaces
FieldConnectOptions from @llui/components/field
export interface FieldConnectOptions {
/** Base id; if omitted, falls back to the id stored in state. */
id?: string
/**
* Whether a description element is rendered. When true, the description id is
* always present in `aria-describedby`; when false it is omitted entirely.
*/
hasDescription?: boolean
}
FieldInit from @llui/components/field
export interface FieldInit {
id: string
invalid?: boolean
required?: boolean
disabled?: boolean
readonly?: boolean
touched?: boolean
}
FieldParts from @llui/components/field
export interface FieldParts {
/** The field wrapper. */
root: {
'data-scope': 'field'
'data-part': 'root'
'data-invalid': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
}
/** The `<label>`. `htmlFor` focuses the control on click. */
label: {
id: string
htmlFor: string
'data-scope': 'field'
'data-part': 'label'
}
/** Spread onto the input/select/textarea (or a custom control via aria-labelledby). */
control: {
id: string
'aria-labelledby': string
'aria-describedby': Signal<string | undefined>
'aria-invalid': Signal<'true' | undefined>
'aria-required': Signal<'true' | undefined>
disabled: Signal<boolean>
readOnly: Signal<boolean>
'data-scope': 'field'
'data-part': 'control'
onBlur: (e: FocusEvent) => void
}
/** The description / hint text. Render it only when there is a description to show. */
description: {
id: string
'data-scope': 'field'
'data-part': 'description'
}
/** The error message — a polite live region, intended to be rendered only while invalid. */
errorText: {
id: string
role: 'alert'
'aria-live': 'polite'
'data-scope': 'field'
'data-part': 'error'
}
}
FieldState from @llui/components/field
Field — label / description / error ARIA wiring for a single form control.
Generates a stable family of ids from one base id and wires them together
so the consumer never hand-writes for / aria-describedby / aria-invalid:
label.htmlFor→ the control id (clicking the label focuses the control natively)label.id→ exposed ascontrol['aria-labelledby']for CUSTOM controls (combobox, listbox, etc.) that aren't a native labellable elementcontrol['aria-describedby']references the description id whenever a description is rendered, and ADDS the error id only whileinvaliderrorTextis a polite live region intended to be rendered only while invalid
const f = field.connect(state.at('field'), send, { id: 'email', hasDescription: true })
el('div', f.root, [
el('label', f.label, [text('Email')]),
el('input', { ...f.control, type: 'email' }),
el('p', f.description, [text('We never share it.')]),
show(state.map((s) => s.field.invalid),
() => el('p', f.errorText, [text('Enter a valid email.')])),
])
export interface FieldState {
/** Base id from which the control / label / description / error ids derive. */
id: string
invalid: boolean
required: boolean
disabled: boolean
readonly: boolean
touched: boolean
}
Constants
field from @llui/components/field
const field
@llui/components/fieldset
Functions
connect() from @llui/components/fieldset
function connect(state: Signal<FieldsetState>, _send: Send<FieldsetMsg>, opts: FieldsetConnectOptions = {}): FieldsetParts
init() from @llui/components/fieldset
function init(opts: FieldsetInit): FieldsetState
update() from @llui/components/fieldset
function update(state: FieldsetState, msg: FieldsetMsg): [FieldsetState, never[]]
Types
FieldsetMsg from @llui/components/fieldset
export type FieldsetMsg =
/** @intent("Enable or disable the whole group (propagates to every contained control)") */
| { type: 'setDisabled'; disabled: boolean }
/** @intent("Mark the group as valid or invalid (drives the group-level error region)") */
| { type: 'setInvalid'; invalid: boolean }
Interfaces
FieldsetConnectOptions from @llui/components/fieldset
export interface FieldsetConnectOptions {
/** Base id; if omitted, falls back to the id stored in state. */
id?: string
}
FieldsetInit from @llui/components/fieldset
export interface FieldsetInit {
id: string
disabled?: boolean
invalid?: boolean
}
FieldsetParts from @llui/components/fieldset
export interface FieldsetParts {
/** Spread onto a native `<fieldset>` element (role `group`). */
root: {
role: 'group'
'aria-labelledby': string
'aria-disabled': Signal<'true' | undefined>
disabled: Signal<boolean>
'data-scope': 'fieldset'
'data-part': 'root'
'data-invalid': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
}
/** The `<legend>` naming the group. */
legend: {
id: string
'data-scope': 'fieldset'
'data-part': 'legend'
}
/** Group-level error message — a polite live region, rendered only while invalid. */
errorText: {
id: string
role: 'alert'
'aria-live': 'polite'
'data-scope': 'fieldset'
'data-part': 'error'
}
}
FieldsetState from @llui/components/fieldset
Fieldset — group wiring for a set of related controls (e.g. an address block).
The root is a native <fieldset> (role group) labelled by a <legend>.
Setting the group disabled disables every contained control natively (the
native disabled attribute on <fieldset> propagates to descendants), and is
mirrored to aria-disabled for assistive tech. An optional group-level error
region is exposed for cross-field validation messages.
const g = fieldset.connect(state.at('billing'), send, { id: 'billing' })
el('fieldset', g.root, [
el('legend', g.legend, [text('Billing address')]),
// ...fields...
show(state.map((s) => s.billing.invalid),
() => el('p', g.errorText, [text('Address is incomplete.')])),
])
export interface FieldsetState {
/** Base id from which the legend / error ids derive. */
id: string
disabled: boolean
invalid: boolean
}
Constants
fieldset from @llui/components/fieldset
const fieldset
@llui/components/toolbar
Functions
connect() from @llui/components/toolbar
function connect(state: Signal<ToolbarState>, send: Send<ToolbarMsg>, opts: ConnectOptions): ToolbarParts
init() from @llui/components/toolbar
function init(opts: ToolbarInit = {}): ToolbarState
update() from @llui/components/toolbar
function update(state: ToolbarState, msg: ToolbarMsg): [ToolbarState, never[]]
Types
Orientation from @llui/components/toolbar
Toolbar — a roving-tabindex container for a set of controls (buttons, toggles, menu triggers). The toolbar is a single tab stop: Tab moves focus into the active item and a subsequent Tab leaves the toolbar entirely. Arrow keys rove focus between enabled items, skipping separators and disabled items; Home/End jump to the first/last enabled item.
Toolbar is interaction-agnostic: an item may itself be a toggle or a menu trigger. The toolbar only manages which item holds the single tab stop.
export type Orientation = 'horizontal' | 'vertical'
ToolbarMsg from @llui/components/toolbar
export type ToolbarMsg =
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
/** @humanOnly */
| { type: 'setFocused'; value: string }
/** @humanOnly */
| { type: 'focusNext'; from: string }
/** @humanOnly */
| { type: 'focusPrev'; from: string }
/** @humanOnly */
| { type: 'focusFirst' }
/** @humanOnly */
| { type: 'focusLast' }
Interfaces
ConnectOptions from @llui/components/toolbar
export interface ConnectOptions {
id: string
label?: string
}
ToolbarGroupParts from @llui/components/toolbar
export interface ToolbarGroupParts {
root: {
role: 'group'
'data-scope': 'toolbar'
'data-part': 'group'
'aria-labelledby': string
}
label: {
id: string
'data-scope': 'toolbar'
'data-part': 'group-label'
}
}
ToolbarInit from @llui/components/toolbar
export interface ToolbarInit {
items?: string[]
disabledItems?: string[]
focused?: string | null
orientation?: Orientation
loopFocus?: boolean
disabled?: boolean
}
ToolbarItemParts from @llui/components/toolbar
export interface ToolbarItemParts {
root: {
'data-scope': 'toolbar'
'data-part': 'item'
'data-value': string
'data-disabled': Signal<'' | undefined>
'aria-disabled': Signal<'true' | undefined>
tabindex: Signal<number>
onKeyDown: (e: KeyboardEvent) => void
onFocus: () => void
}
}
ToolbarParts from @llui/components/toolbar
export interface ToolbarParts {
root: {
role: 'toolbar'
'aria-orientation': Signal<Orientation>
'aria-label': string | undefined
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'toolbar'
'data-part': 'root'
'data-orientation': Signal<Orientation>
'data-disabled': Signal<'' | undefined>
}
separator: {
role: 'separator'
'aria-orientation': Signal<Orientation>
'data-scope': 'toolbar'
'data-part': 'separator'
}
item: (value: string) => ToolbarItemParts
group: (label: string) => ToolbarGroupParts
}
ToolbarState from @llui/components/toolbar
export interface ToolbarState {
items: string[]
disabledItems: string[]
focused: string | null
orientation: Orientation
loopFocus: boolean
disabled: boolean
}
Constants
toolbar from @llui/components/toolbar
const toolbar
@llui/components/meter
Functions
connect() from @llui/components/meter
function connect(state: Signal<MeterState>, _send: Send<MeterMsg>, opts: ConnectOptions = {}): MeterParts
init() from @llui/components/meter
function init(opts: MeterInit = {}): MeterState
percent() from @llui/components/meter
function percent(state: MeterState): number
thresholdState() from @llui/components/meter
Derives the threshold band the current value falls into, following the
native
- below
low→ the value is in the lower segment; - above
high→ the value is in the upper segment; - otherwise → the value is in the middle segment.
Whether a segment is "good" depends on
optimum: the segment containingoptimumis 'optimal'; the segment adjacent to it is the lesser-preferred band; and the far segment is the worst. We map the worst → 'low', the preferred → 'optimal', and the in-between → 'high'. Whenlow/highare missing the value is considered to be in the middle segment; whenoptimumis missing every band reads as 'optimal'.
function thresholdState(state: MeterState): MeterThreshold
update() from @llui/components/meter
function update(state: MeterState, msg: MeterMsg): [MeterState, never[]]
Types
MeterMsg from @llui/components/meter
export type MeterMsg =
/** @humanOnly */
| { type: 'setValue'; value: number }
/** @humanOnly */
| { type: 'setMax'; max: number }
MeterThreshold from @llui/components/meter
export type MeterThreshold = 'low' | 'optimal' | 'high'
Interfaces
ConnectOptions from @llui/components/meter
export interface ConnectOptions {
label?: string
/** Custom formatter for value text. */
format?: (value: number, max: number) => string
}
MeterInit from @llui/components/meter
export interface MeterInit {
value?: number
min?: number
max?: number
low?: number
high?: number
optimum?: number
}
MeterParts from @llui/components/meter
export interface MeterParts {
root: {
role: 'meter'
'aria-valuemin': Signal<number>
'aria-valuemax': Signal<number>
'aria-valuenow': Signal<number>
'aria-valuetext': Signal<string>
'aria-label': string | undefined
'data-state': Signal<MeterThreshold>
'data-scope': 'meter'
'data-part': 'root'
}
track: {
'data-state': Signal<MeterThreshold>
'data-scope': 'meter'
'data-part': 'track'
}
range: {
'data-state': Signal<MeterThreshold>
'data-scope': 'meter'
'data-part': 'range'
style: Signal<string>
}
label: {
'data-scope': 'meter'
'data-part': 'label'
}
valueText: Signal<string>
}
MeterState from @llui/components/meter
Meter — role="meter" gauge for a scalar measurement within a known range
(e.g. disk usage, battery level). Distinct from progressbar: a meter is never
indeterminate and represents a static measurement rather than task progress.
low/high/optimum mirror the native data-state.
export interface MeterState {
value: number
min: number
max: number
low?: number
high?: number
optimum?: number
}
Constants
meter from @llui/components/meter
const meter
@llui/components/breadcrumbs
Functions
connect() from @llui/components/breadcrumbs
function connect(state: Signal<BreadcrumbsState>, send: Send<BreadcrumbsMsg>, opts: ConnectOptions = {}): BreadcrumbsParts
init() from @llui/components/breadcrumbs
function init(opts: BreadcrumbsInit = {}): BreadcrumbsState
update() from @llui/components/breadcrumbs
function update(state: BreadcrumbsState, msg: BreadcrumbsMsg): [BreadcrumbsState, never[]]
visibleItems() from @llui/components/breadcrumbs
Compute the visible breadcrumb trail. When maxVisible is set and exceeded
and the trail is not expanded, collapse the middle to:
[first] … [last (maxVisible - 1) items]. The final item is always current.
function visibleItems(state: BreadcrumbsState): VisibleBreadcrumb[]
Types
BreadcrumbsMsg from @llui/components/breadcrumbs
export type BreadcrumbsMsg =
/** @intent("Replace the breadcrumb trail with a new list of items") */
| { type: 'setItems'; items: BreadcrumbItem[] }
/** @intent("Expand the collapsed middle of the trail to reveal all items") */
| { type: 'expand' }
/** @intent("Collapse the trail back to its truncated form") */
| { type: 'collapse' }
VisibleBreadcrumb from @llui/components/breadcrumbs
export type VisibleBreadcrumb =
| { type: 'item'; id: string; label: string; current: boolean }
| { type: 'ellipsis' }
Interfaces
BreadcrumbItem from @llui/components/breadcrumbs
Breadcrumbs — a hierarchical trail of links to ancestor pages.
The last item is the current page. When maxVisible is set and the trail
is longer, the middle collapses to: first item + ellipsis + last N items,
until the user expands it.
export interface BreadcrumbItem {
id: string
label: string
}
BreadcrumbsInit from @llui/components/breadcrumbs
export interface BreadcrumbsInit {
items?: BreadcrumbItem[]
maxVisible?: number | null
expanded?: boolean
}
BreadcrumbsParts from @llui/components/breadcrumbs
export interface BreadcrumbsParts {
root: {
'aria-label': string
'data-scope': 'breadcrumbs'
'data-part': 'root'
}
list: {
'data-scope': 'breadcrumbs'
'data-part': 'list'
}
item: (id: string) => {
'data-scope': 'breadcrumbs'
'data-part': 'item'
'data-value': string
}
link: (id: string) => {
'aria-current': Signal<'page' | undefined>
'data-scope': 'breadcrumbs'
'data-part': 'link'
'data-value': string
'data-current': Signal<'' | undefined>
}
separator: {
'aria-hidden': 'true'
'data-scope': 'breadcrumbs'
'data-part': 'separator'
}
ellipsisTrigger: {
type: 'button'
'aria-label': string
'data-scope': 'breadcrumbs'
'data-part': 'ellipsis-trigger'
onClick: (e: MouseEvent) => void
}
}
BreadcrumbsState from @llui/components/breadcrumbs
export interface BreadcrumbsState {
items: BreadcrumbItem[]
maxVisible: number | null
expanded: boolean
}
ConnectOptions from @llui/components/breadcrumbs
export interface ConnectOptions {
label?: string
expandLabel?: string
}
Constants
breadcrumbs from @llui/components/breadcrumbs
const breadcrumbs
@llui/components/search-field
Functions
connect() from @llui/components/search-field
function connect(state: Signal<SearchFieldState>, send: Send<SearchFieldMsg>, opts: ConnectOptions = {}): SearchFieldParts
init() from @llui/components/search-field
function init(opts: SearchFieldInit = {}): SearchFieldState
update() from @llui/components/search-field
function update(state: SearchFieldState, msg: SearchFieldMsg): [SearchFieldState, never[]]
Types
SearchFieldMsg from @llui/components/search-field
export type SearchFieldMsg =
/** @humanOnly */
| { type: 'setValue'; value: string }
/** @intent("Clear the search field") */
| { type: 'clear' }
/** @intent("Submit the current search query") */
| { type: 'submit'; value: string }
Interfaces
ConnectOptions from @llui/components/search-field
export interface ConnectOptions {
/** Accessible label for the clear button. */
clearLabel?: string
}
SearchFieldInit from @llui/components/search-field
export interface SearchFieldInit {
value?: string
disabled?: boolean
}
SearchFieldParts from @llui/components/search-field
export interface SearchFieldParts {
root: {
role: 'search'
'data-scope': 'search-field'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
label: {
'data-scope': 'search-field'
'data-part': 'label'
}
input: {
type: 'search'
disabled: Signal<boolean>
value: Signal<string>
'data-scope': 'search-field'
'data-part': 'input'
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
}
clearTrigger: {
type: 'button'
'aria-label': string
hidden: Signal<boolean>
tabindex: -1
'data-scope': 'search-field'
'data-part': 'clear-trigger'
onClick: (e: MouseEvent) => void
}
}
SearchFieldState from @llui/components/search-field
Search field — a role="search" landmark wrapping a type="search" input
with a clear button. Escape clears the field (when non-empty), Enter submits
the current value.
Debounced live search is intentionally NOT built into this machine. Keep it
consumer-side: debounce the setValue message (or a derived "search" effect)
with debounce from @llui/effects so the search trigger fires once the user
pauses typing, rather than on every keystroke.
export interface SearchFieldState {
value: string
disabled: boolean
}
Constants
searchField from @llui/components/search-field
const searchField
@llui/components/table
Functions
connect() from @llui/components/table
function connect(state: Signal<TableState>, send: Send<TableMsg>, opts: ConnectOptions): TableParts
init() from @llui/components/table
function init(opts: TableInit = {}): TableState
isAllSelected() from @llui/components/table
function isAllSelected(state: TableState): boolean
isRowSelected() from @llui/components/table
function isRowSelected(state: TableState, id: string): boolean
isSomeSelected() from @llui/components/table
function isSomeSelected(state: TableState): boolean
sortDirectionFor() from @llui/components/table
function sortDirectionFor(state: TableState, columnId: string): SortDirection | null
update() from @llui/components/table
function update(state: TableState, msg: TableMsg): [TableState, never[]]
Types
SortDirection from @llui/components/table
Table / data grid — a headless machine for sortable columns, row
selection, and APG grid keyboard navigation. It is NOT a rendering
engine: row DATA stays in the consumer; the machine tracks only row
IDs (in display order), sort state, the selected-id set, and the
focused cell coordinate. The consumer renders the grid (via each or
virtualEach) and performs the actual data sort — so server-side sort
works by feeding pre-sorted rows back in. focusedCell is addressed
by index, robust to virtualization.
export type SortDirection = 'asc' | 'desc'
TableMsg from @llui/components/table
export type TableMsg =
/** @intent("Cycle the sort on the given column (asc → desc → none, or desc → asc → none when descFirst)") */
| { type: 'toggleSort'; columnId: string }
/** @intent("Set an explicit sort, or null to clear sorting") */
| { type: 'setSort'; sort: TableSort | null }
/** @intent("Toggle selection of the row with the given id at the given display index") */
| { type: 'toggleRow'; id: string; index: number }
/** @intent("Select every row (multiple mode only)") */
| { type: 'selectAll' }
/** @intent("Clear the entire selection") */
| { type: 'clearSelection' }
/** @intent("Toggle between select-all and clear, based on whether every row is selected") */
| { type: 'toggleAll' }
/** @intent("Replace the selected-id set with the provided list") */
| { type: 'setSelection'; ids: string[] }
/** @intent("Select the inclusive range from the current anchor to the given index (Shift+click)") */
| { type: 'selectRange'; index: number }
/** @intent("Activate (open/confirm) the row with the given id at the given index") */
| { type: 'activateRow'; id: string; index: number }
/** @intent("Replace the row-id list (display order); drops selection for ids no longer present") */
| { type: 'setRows'; rows: string[] }
/** @intent("Replace the column descriptors") */
| { type: 'setColumns'; columns: TableColumn[] }
/** @humanOnly */
| { type: 'focusCell'; rowIndex: number; colIndex: number }
/** @humanOnly */
| { type: 'moveCell'; dRow: number; dCol: number }
/** @humanOnly */
| { type: 'rowStart' }
/** @humanOnly */
| { type: 'rowEnd' }
/** @humanOnly */
| { type: 'gridStart' }
/** @humanOnly */
| { type: 'gridEnd' }
/** @humanOnly */
| { type: 'pageDown' }
/** @humanOnly */
| { type: 'pageUp' }
TableSelectionMode from @llui/components/table
export type TableSelectionMode = 'none' | 'single' | 'multiple'
Interfaces
ConnectOptions from @llui/components/table
export interface ConnectOptions {
id: string
}
TableCellCoord from @llui/components/table
export interface TableCellCoord {
/** Row index into `rows`, or {@link HEADER_ROW_INDEX} for the header row. */
rowIndex: number
colIndex: number
}
TableCellParts from @llui/components/table
export interface TableCellParts {
role: 'gridcell'
'aria-colindex': number
tabindex: Signal<number>
'data-scope': 'table'
'data-part': 'cell'
/** 0-based row index — addresses the cell for roving DOM focus. */
'data-row-index': number
/** 0-based column index — addresses the cell for roving DOM focus. */
'data-col-index': number
'data-focused': Signal<'' | undefined>
onFocus: (e: FocusEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
TableCheckboxParts from @llui/components/table
export interface TableCheckboxParts {
role: 'checkbox'
'aria-checked': Signal<'true' | 'false' | 'mixed'>
'data-scope': 'table'
'data-part': 'select-all' | 'row-checkbox'
'data-state': Signal<'checked' | 'unchecked' | 'indeterminate'>
/** Always `-1`: a `role="grid"` has exactly ONE tab stop, the roving cell. */
tabindex: -1
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
TableColumn from @llui/components/table
export interface TableColumn {
/** Opaque column id. */
id: string
/** Whether this column participates in sorting. Defaults to false. */
sortable?: boolean
}
TableColumnHeaderParts from @llui/components/table
export interface TableColumnHeaderParts {
role: 'columnheader'
id: string
'aria-sort': Signal<'ascending' | 'descending' | 'none' | undefined>
/**
* Roving tab stop. The header row participates in the grid's single-tab-stop
* sequence, because it hosts controls — the sort toggle on every sortable
* column, and the select-all checkbox — that are otherwise unreachable by
* keyboard.
*/
tabindex: Signal<number>
'data-scope': 'table'
'data-part': 'column-header'
'data-column': string
/** Always {@link HEADER_ROW_INDEX} — addresses the header for roving DOM focus. */
'data-row-index': typeof HEADER_ROW_INDEX
/** 0-based column index (`-1` for a column not in `columns`). */
'data-col-index': Signal<number>
'data-focused': Signal<'' | undefined>
'data-sortable': Signal<'' | undefined>
'data-sort': Signal<SortDirection | undefined>
onFocus: (e: FocusEvent) => void
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
TableInit from @llui/components/table
export interface TableInit {
columns?: TableColumn[]
rows?: string[]
sort?: TableSort | null
selection?: string[]
selectionMode?: TableSelectionMode
focusedCell?: TableCellCoord | null
pageSize?: number
descFirst?: boolean
disabled?: boolean
}
TableParts from @llui/components/table
export interface TableParts {
root: {
role: 'grid'
id: string
'aria-multiselectable': Signal<'true' | undefined>
'aria-rowcount': Signal<number>
'aria-colcount': Signal<number>
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'table'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
columnHeader: (columnId: string) => TableColumnHeaderParts
row: (id: string, index: number) => TableRowParts
cell: (rowIndex: number, colIndex: number) => TableCellParts
/**
* The select-all checkbox, for the `columnheader` of `columnId`.
*
* The column id is a PARAMETER rather than a `connect()` option because the
* checkbox is unreachable by keyboard without it: it has no gridcell of its
* own, and every part inside a `role="grid"` except the one roving stop is
* `tabindex="-1"`, so its only keyboard route is Enter/Space on the roving
* header that hosts it — and the machine can only route that key if it knows
* which header. As an option it was forgettable, and forgetting it failed
* SILENTLY (no warning, no error; the key sorted the column or did nothing).
* As a required argument you cannot render the checkbox without answering the
* question, so the failure mode is gone at compile time.
*
* `columnId` must be a column in `state.columns` — that is what gives the
* header a colIndex to rove to. A column not in the list can never take the
* roving stop, and its header will not send `toggleAll` either.
*/
selectAllCheckbox: (columnId: string) => TableCheckboxParts
rowCheckbox: (id: string, index: number) => TableCheckboxParts
}
TableRowParts from @llui/components/table
export interface TableRowParts {
role: 'row'
'aria-selected': Signal<boolean | undefined>
'aria-rowindex': number
'data-scope': 'table'
'data-part': 'row'
'data-row': string
'data-selected': Signal<'' | undefined>
onClick: (e: MouseEvent) => void
}
TableSort from @llui/components/table
export interface TableSort {
columnId: string
direction: SortDirection
}
TableState from @llui/components/table
export interface TableState {
/** Column descriptors in display order. */
columns: TableColumn[]
/** Row IDs in display order. Row DATA stays in the consumer. */
rows: string[]
/** Active sort, or null when unsorted. */
sort: TableSort | null
/** Selected row IDs. */
selection: string[]
selectionMode: TableSelectionMode
/** Focused cell coordinate (header row excluded; rowIndex addresses `rows`). */
focusedCell: TableCellCoord | null
/** Index of the last row toggled — the anchor for shift-range selection. */
rangeAnchor: number | null
/** Rows moved per PageUp/PageDown. */
pageSize: number
/** When true, the sort cycle starts at desc instead of asc. */
descFirst: boolean
disabled: boolean
}
Constants
HEADER_ROW_INDEX from @llui/components/table
The row index of the HEADER row. The header is part of the grid's roving
sequence — APG's data-grid examples make column headers focusable exactly
because they carry controls (sort here, plus the select-all checkbox) — so it
needs a coordinate. -1 is the natural one: aria-rowindex already models
the header as row 1 with data row i at i + 2, so the header sits one row
above data row 0.
const HEADER_ROW_INDEX
table from @llui/components/table
The namespace object. {@link HEADER_ROW_INDEX} is a member again now that
issue #151 is fixed — site/src/generate-api.ts classifies namespace members
by kind instead of rendering every one with a hard-coded (). It remains a
module-level export too, re-exported from the barrel as
TABLE_HEADER_ROW_INDEX.
const table
@llui/components/menubar
Functions
connect() from @llui/components/menubar
function connect(state: Signal<MenubarState>, send: Send<MenubarMsg>, opts: ConnectOptions): MenubarParts
init() from @llui/components/menubar
function init(opts: MenubarInit): MenubarState
overlay() from @llui/components/menubar
Render one top-level menu's dropdown. Mirrors menu.overlay but is gated on
state.open === menuId and dismisses by closing the menubar (returning
focus to the top-level trigger). Submenu unwinding goes through the same
dismissable stack the menu machine uses.
function overlay(opts: MenubarOverlayOptions): Mountable
update() from @llui/components/menubar
function update(state: MenubarState, msg: MenubarMsg): [MenubarState, never[]]
Types
MenubarMsg from @llui/components/menubar
export type MenubarMsg =
/** @intent("Open the menu with the given id and focus its first item") */
| { type: 'openMenu'; id: string }
/** @intent("Close the currently-open menu") */
| { type: 'closeMenu' }
/** @intent("Move roving focus to the menu with the given id (switches the open menu in open mode)") */
| { type: 'focusMenu'; id: string }
/** @humanOnly */
| { type: 'focusNext' }
/** @humanOnly */
| { type: 'focusPrev' }
/** @humanOnly */
| { type: 'menuMsg'; id: string; msg: MenuMsg }
Interfaces
ConnectOptions from @llui/components/menubar
export interface ConnectOptions {
id: string
label?: string
/** Called when an item in any menu is activated (Enter/Space/click). */
onSelect?: (menuId: string, value: string) => void
}
MenubarInit from @llui/components/menubar
export interface MenubarInit {
menus: MenubarMenu[]
/** Initially-focused menu id (defaults to the first enabled menu). */
focused?: string | null
}
MenubarMenu from @llui/components/menubar
Declarative description of one top-level menu in the bar.
export interface MenubarMenu {
id: string
items: MenuItem[]
disabled?: boolean
/** When true, selecting a checkbox/radio also closes this menu. */
closeOnSelect?: boolean
}
MenubarOverlayOptions from @llui/components/menubar
export interface MenubarOverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*/
positionerClass?: string
state: Signal<MenubarState>
send: Send<MenubarMsg>
/** The menu id this overlay renders. */
menuId: string
/** The delegated per-menu bag — `connect(...).menu(menuId)`. Its
* `trigger.id` is the id the bar's `menuTrigger(menuId)` renders, so it
* doubles as the overlay's anchor. */
parts: MenuParts
content: () => Renderable
/**
* Optional enter/leave transition for the menubar menu content (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, so the close plays an exit animation.
* Keep `skipAnimations` at its default (true) when driving exits this way.
*
* @example menubar.overlay({ state, send, parts, content, transition: fade({ duration: 120 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
target?: string | HTMLElement
}
MenubarParts from @llui/components/menubar
export interface MenubarParts {
root: {
role: 'menubar'
'aria-label': string
'data-scope': 'menubar'
'data-part': 'root'
}
menuTrigger: (id: string) => MenubarTriggerParts
/** Delegated per-menu part bag (content/item/checkboxItem/submenu/…). */
menu: (id: string) => MenuParts
}
MenubarState from @llui/components/menubar
export interface MenubarState {
/** Top-level menu ids, in bar order. */
menus: string[]
/** The id of the currently-open menu, or null. */
open: string | null
/** The id of the top-level trigger that holds roving focus. */
focused: string | null
/** Ids of disabled menus (cannot be opened/focused). */
disabledMenus: string[]
/** Embedded per-menu machine states, keyed by menu id. */
menuStates: Record<string, MenuState>
}
MenubarTriggerParts from @llui/components/menubar
export interface MenubarTriggerParts {
role: 'menuitem'
id: string
'aria-haspopup': 'menu'
'aria-expanded': Signal<boolean>
'aria-controls': string
'aria-disabled': Signal<'true' | undefined>
'data-scope': 'menubar'
'data-part': 'trigger'
'data-state': Signal<'open' | 'closed'>
'data-value': string
tabindex: Signal<number>
onClick: (e: MouseEvent) => void
onPointerEnter: (e: PointerEvent) => void
onFocus: (e: FocusEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
Constants
menubar from @llui/components/menubar
const menubar
@llui/components/in-view
Functions
connect() from @llui/components/in-view
function connect(state: Signal<InViewState>, _send: Send<InViewMsg>, _opts: ConnectOptions): InViewParts
createObserver() from @llui/components/in-view
Create an IntersectionObserver for the given element. Returns a cleanup function that disconnects the observer.
Call this inside onMount:
onMount((el) => inView.createObserver(el, send, { once: true }))
function createObserver(el: Element, send: Send<InViewMsg>, opts: ObserverOptions = {}): () => void
init() from @llui/components/in-view
function init(): InViewState
update() from @llui/components/in-view
function update(state: InViewState, msg: InViewMsg): [InViewState, never[]]
Types
InViewMsg from @llui/components/in-view
export type InViewMsg = { type: 'enter' } | { type: 'leave' }
Interfaces
ConnectOptions from @llui/components/in-view
export interface ConnectOptions {
id: string
}
InViewParts from @llui/components/in-view
export interface InViewParts {
root: {
'data-scope': 'in-view'
'data-part': 'root'
'data-state': Signal<'visible' | 'hidden'>
}
}
InViewState from @llui/components/in-view
In View — tracks whether an element is visible in the viewport using IntersectionObserver.
State machine: { visible: false } → enter → { visible: true } → leave → …
With once: true, the observer disconnects after the first enter,
keeping visible: true permanently. Useful for lazy-load and
scroll-triggered animations.
view: ({ state, send }) => {
const inViewState = state.at('inView')
const inViewSend = mapSend<Msg, inView.InViewMsg>(send, (msg) => ({
type: 'inView',
msg,
}))
const parts = inView.connect(inViewState, inViewSend, { id: 'hero' })
return [
div({ ...parts.root, class: inViewState.at('visible').map((v) => (v ? 'fade-in' : '')) }, [
onMount((el) =>
inView.createObserver(el, inViewSend, { threshold: 0.5, once: true }),
),
]),
]
}
export interface InViewState {
visible: boolean
}
ObserverOptions from @llui/components/in-view
export interface ObserverOptions {
threshold?: number
rootMargin?: string
once?: boolean
}
Constants
inView from @llui/components/in-view
const inView
@llui/components/theme-switch
Functions
applyTheme() from @llui/components/theme-switch
Set data-theme="light" or data-theme="dark" on <html>. CSS selectors
like [data-theme='dark'] { ... } will then take effect.
function applyTheme(resolved: ResolvedTheme): void
connect() from @llui/components/theme-switch
function connect(state: Signal<ThemeSwitchState>, send: Send<ThemeSwitchMsg>, opts: ConnectOptions): ThemeSwitchParts
init() from @llui/components/theme-switch
function init(theme: Theme = 'system'): ThemeSwitchState
resolveTheme() from @llui/components/theme-switch
Resolve a theme preference to the actual theme to apply. Returns 'dark' or
'light' based on the user's setting, consulting prefers-color-scheme for
'system'.
function resolveTheme(theme: Theme): ResolvedTheme
update() from @llui/components/theme-switch
function update(state: ThemeSwitchState, msg: ThemeSwitchMsg): [ThemeSwitchState, never[]]
watchSystemTheme() from @llui/components/theme-switch
Listen for system theme changes (when user has selected 'system'). Returns
a cleanup function. Call this in onMount and dispatch setTheme on
change if you want the UI to auto-follow OS settings.
function watchSystemTheme(callback: (theme: ResolvedTheme) => void): () => void
Types
ResolvedTheme from @llui/components/theme-switch
export type ResolvedTheme = 'light' | 'dark'
Theme from @llui/components/theme-switch
Theme Switch — light/dark/system theme toggle.
State machine tracks the user's explicit preference (light, dark, or
system). Use resolveTheme() to compute the effective theme (reading
prefers-color-scheme when system), and applyTheme() to set
data-theme on <html> so CSS selectors like [data-theme='dark'] work.
Typically wired via onMount or in app init:
onMount(() => {
applyTheme(resolveTheme(state.theme.theme))
})
For persistence, the app reducer reads/writes localStorage.theme in its
init/update — the state machine itself is storage-agnostic.
export type Theme = 'light' | 'dark' | 'system'
ThemeSwitchMsg from @llui/components/theme-switch
export type ThemeSwitchMsg = { type: 'setTheme'; theme: Theme } | { type: 'toggle' }
Interfaces
ConnectOptions from @llui/components/theme-switch
export interface ConnectOptions {
id: string
/** Accessible label for the theme group (default: 'Theme'). */
label?: string
}
ThemeSwitchParts from @llui/components/theme-switch
export interface ThemeSwitchParts {
root: {
'data-scope': 'theme-switch'
'data-part': 'root'
role: 'group'
'aria-label': string
}
option: (theme: Theme) => {
type: 'button'
'data-scope': 'theme-switch'
'data-part': 'option'
'data-theme': Theme
'aria-pressed': Signal<boolean>
'aria-label': string
onClick: (e: MouseEvent) => void
}
toggle: {
type: 'button'
'data-scope': 'theme-switch'
'data-part': 'toggle'
'data-theme': Signal<Theme>
'aria-label': string
onClick: (e: MouseEvent) => void
}
}
ThemeSwitchState from @llui/components/theme-switch
export interface ThemeSwitchState {
theme: Theme
}
Constants
themeSwitch from @llui/components/theme-switch
const themeSwitch
@llui/components/sortable
Functions
connect() from @llui/components/sortable
function connect(state: Signal<SortableState>, send: Send<SortableMsg>, opts: ConnectOptions): SortableParts
init() from @llui/components/sortable
function init(): SortableState
reorder() from @llui/components/sortable
Move an item in an array from one index to another, returning a new array. Out-of-range indices are clamped to array bounds.
function reorder<T>(arr: readonly T[], from: number, to: number): T[]
update() from @llui/components/sortable
function update(state: SortableState, msg: SortableMsg): [SortableState, never[]]
Types
SortableMsg from @llui/components/sortable
export type SortableMsg =
/** @humanOnly */
| { type: 'start'; id: string; index: number; container: string; x: number; y: number }
/** @humanOnly */
| { type: 'move'; index: number; container: string; x: number; y: number }
/** @humanOnly */
| { type: 'drop' }
/** @humanOnly */
| { type: 'cancel' }
/** @humanOnly */
| { type: 'toggleGrab'; id: string; index: number; container: string }
/** @humanOnly */
| { type: 'moveBy'; delta: number }
Interfaces
ConnectOptions from @llui/components/sortable
export interface ConnectOptions {
id: string
/**
* Drag-target selection + render strategy.
*
* - `'1d'` (default) — single-axis, Y-only. `findTargetAt` picks
* by vertical distance; `style.transform` on the dragged item
* is `translateY(deltaY)`; non-dragged items between source
* and target emit `data-shift: 'up' | 'down'` so CSS can
* animate them via `translateY(±var(--sortable-shift))`.
* Correct for vertical lists; fails for 2D layouts (flex-wrap,
* grid) because same-row items collapse to the same midpoint
* distance.
*
* - `'2d'` — Euclidean target selection against 2D midpoints;
* dragged item follows both X and Y (`translate(dx, dy)`);
* non-dragged items between source and target get a per-item
* `style.transform = translate(deltaFromSnapshot)` that opens
* the correct gap regardless of row boundaries. `data-shift`
* is always `undefined` in 2D so CSS `translateY(var(--...))`
* rules don't conflict with the per-item transform.
*
* Keyboard navigation (`moveBy`) stays linear-array in both modes —
* arrow keys step through the array indices regardless of visual
* row, because that's what screen readers announce and what the
* underlying data order actually is.
*/
layout?: '1d' | '2d'
}
DragState from @llui/components/sortable
Sortable — pointer-based reorderable list.
State machine tracks the currently-dragged item and where it's hovering.
The app owns the actual array; listen for drop and use reorder(arr, from, to)
to compute the new order, or watch currentIndex during drag for live preview.
type State = { items: string[]; sort: SortableState }
update: (state, msg) => {
switch (msg.type) {
case 'sort':
return [{ ...state, sort: sortable.update(state.sort, msg.msg)[0] }, []]
case 'drop': {
const d = state.sort.dragging
if (!d) return [state, []]
return [{ ...state, items: reorder(state.items, d.startIndex, d.currentIndex) }, []]
}
}
}
// `each`, `ul`, `li`, `div`, `text`, `mapSend` are imports from '@llui/dom';
// the view bag provides only `state` (a Signal) and `send`.
view: ({ state, send }) => {
const sortableState = state.at('sort')
const sortableSend = mapSend<Msg, sortable.SortableMsg>(send, (msg) => ({
type: 'sort',
msg,
}))
const s = sortable.connect(sortableState, sortableSend, { id: 'list' })
return [
ul({ ...s.root, class: 'list' }, [
...each({
items: (st) => st.items,
key: (x) => x,
render: ({ item, index }) => [
li({ ...s.item(item(), index()), class: 'item' }, [
div({ ...s.handle(item(), index()), class: 'handle' }, [text('⋮⋮')]),
text(item),
]),
],
}),
]),
]
}
Hook up pointermove/pointerup at the root (attachPointerHandlers) — or
wire them directly via onPointerMove / onPointerUp on the root part.
export interface DragState {
id: string
startIndex: number
currentIndex: number
/**
* Container the drag originated from. Defaults to the connect's `id` for
* single-container sortables. Set when multiple sortables share state.
*/
fromContainer: string
/**
* Container the pointer is currently over. Same as `fromContainer` for
* single-container sortables. Differs when dragging across containers.
*/
toContainer: string
/**
* Pointer X at drag start (viewport coordinates). Used by 2D layouts
* to compute `deltaX = currentX - startX` alongside the Y axis. In 1D
* layouts X is tracked but ignored by the renderer.
*/
startX: number
/**
* Pointer Y at drag start (viewport coordinates). Used by CSS / the
* library's `style.transform` binding to make the dragged item follow
* the pointer.
*/
startY: number
/**
* Current pointer X (viewport coordinates). `deltaX = currentX - startX`.
*/
currentX: number
/**
* Current pointer Y (viewport coordinates). `deltaY = currentY - startY`.
*/
currentY: number
}
SortableParts from @llui/components/sortable
export interface SortableParts {
root: {
'data-scope': 'sortable'
'data-part': 'root'
'data-container-id': string
'data-dragging': Signal<'' | undefined>
onPointerMove: (e: PointerEvent) => void
onPointerUp: (e: PointerEvent) => void
onPointerCancel: (e: PointerEvent) => void
}
item: (
id: string,
index: number,
) => {
'data-scope': 'sortable'
'data-part': 'item'
'data-index': string
'data-id': string
'data-dragging': Signal<'' | undefined>
'data-over': Signal<'' | undefined>
'data-shift': Signal<'up' | 'down' | undefined>
'style.transform': Signal<string | undefined>
'style.zIndex': Signal<string | undefined>
}
handle: (
id: string,
index: number,
) => {
'data-scope': 'sortable'
'data-part': 'handle'
role: 'button'
tabindex: 0
'aria-grabbed': Signal<boolean>
'aria-label': string
onPointerDown: (e: PointerEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
}
SortableState from @llui/components/sortable
export interface SortableState {
dragging: DragState | null
}
Constants
sortable from @llui/components/sortable
const sortable
@llui/components/form
Functions
connect() from @llui/components/form
function connect(state: Signal<FormState>, send: Send<FormMsg>, _opts: ConnectOptions): FormParts
init() from @llui/components/form
function init(): FormState
update() from @llui/components/form
function update(state: FormState, msg: FormMsg): [FormState, never[]]
validateSchema() from @llui/components/form
Run a Standard Schema synchronously against a values object. Throws if the schema returns a Promise — use sync validation only for form submit.
Works with any library implementing the Standard Schema spec: Zod (v3.24+), Valibot (v1+), ArkType, etc.
function validateSchema<T>(schema: StandardSchemaV1<T>, values: unknown): ValidateResult<T>
validateSchemaAsync() from @llui/components/form
Async variant — returns a Promise. Use when the schema performs async validation (e.g. uniqueness checks against a backend).
function validateSchemaAsync<T>(schema: StandardSchemaV1<T>, values: unknown): Promise<ValidateResult<T>>
Types
FormMsg from @llui/components/form
export type FormMsg =
/** @intent("Mark a single field as touched (typically on blur)") */
| { type: 'touch'; field: string }
/** @intent("Mark every named field as touched (typically on submit attempt)") */
| { type: 'touchAll'; fields: string[] }
/** @intent("Begin form submission — validate and dispatch the save effect") */
| { type: 'submit' }
/** @intent("Mark the in-flight submission as successful") */
| { type: 'submitSuccess' }
/** @intent("Mark the in-flight submission as failed with the given error message") */
| { type: 'submitError'; error: string }
/** @intent("Reset the form to its initial state (clears touched flags and submit status)") */
| { type: 'reset' }
FormStatus from @llui/components/form
Form — submit lifecycle + touched tracking + Standard Schema validation.
Values live in the parent component's state; form tracks submit status
and which fields have been interacted with (blur), so errors are shown
only after touch instead of immediately.
Bring your own validation library — any Standard Schema-compatible schema works (Zod, Valibot, ArkType, etc.). See https://standardschema.dev.
import { z } from 'zod'
import { form, validateSchema } from '@llui/components/form'
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
type Values = z.infer<typeof schema>
type State = { values: Values; form: FormState }
update: (state, msg) => {
switch (msg.type) {
case 'submit': {
const result = validateSchema(schema, state.values)
if (!result.isValid) {
return [{ ...state, form: { ...state.form, touched: { email: true, password: true } } }, []]
}
return [{ ...state, form: { ...state.form, status: 'submitting' } }, [saveUserEffect]]
}
}
}
export type FormStatus = 'idle' | 'submitting' | 'submitted' | 'error'
Interfaces
ConnectOptions from @llui/components/form
export interface ConnectOptions {
id: string
}
FormParts from @llui/components/form
export interface FormParts {
root: {
'data-scope': 'form'
'data-part': 'root'
'data-state': Signal<FormStatus>
'aria-busy': Signal<'true' | undefined>
}
field: (name: string) => {
'data-scope': 'form'
'data-part': 'field'
'data-touched': Signal<'' | undefined>
touched: Signal<boolean>
onBlur: (e: FocusEvent) => void
}
submit: {
type: 'submit'
'data-scope': 'form'
'data-part': 'submit'
'data-state': Signal<FormStatus>
disabled: Signal<boolean>
}
}
FormState from @llui/components/form
export interface FormState {
status: FormStatus
touched: Record<string, boolean>
submitError: string | null
}
ValidateResult from @llui/components/form
export interface ValidateResult<T> {
isValid: boolean
/** Field name → first error message. Field name is derived from the issue's path. */
errors: Partial<Record<keyof T, string>>
/** All issues from the schema validator, unaltered. */
issues: readonly StandardSchemaV1.Issue[]
}
Constants
form from @llui/components/form
const form
@llui/components/patterns
Functions
commandMenuConnect() from @llui/components/patterns
Project the composed slice into dialog + combobox part bags plus a
shortcutHint accessor and an empty-state part. The dialog/combobox sends
are adapted to command-menu messages: the consumer spreads these onto
elements exactly like the base components.
function commandMenuConnect(state: Signal<CommandMenuState>, send: Send<CommandMenuMsg>, opts: ConnectOptions): CommandMenuParts
commandMenuInit() from @llui/components/patterns
function commandMenuInit(opts: CommandMenuInit = {}): CommandMenuState
commandMenuUpdate() from @llui/components/patterns
function commandMenuUpdate(state: CommandMenuState, msg: CommandMenuMsg): [CommandMenuState, CommandMenuEffect[]]
commandMenuView() from @llui/components/patterns
Default palette view: a combobox (search input + grouped command list) inside
the dialog overlay. Selecting a command dispatches execute; Escape clears
the query then closes. Consumers wanting a custom row template should drive
the part bags from connect() directly.
function commandMenuView(opts: CommandMenuViewOptions): Mountable
dataTableConnect() from @llui/components/patterns
function dataTableConnect(state: Signal<DataTableState>, send: Send<DataTableMsg>, opts: ConnectOptions): DataTableParts
dataTableInit() from @llui/components/patterns
function dataTableInit(opts: DataTableInit = {}): DataTableState
dataTableUpdate() from @llui/components/patterns
function dataTableUpdate(state: DataTableState, msg: DataTableMsg): [DataTableState, DataTableEffect[]]
isAllSelected() from @llui/components/patterns
function isAllSelected(state: DataTableState): boolean
isEmpty() from @llui/components/patterns
Empty == a settled (not-loading) request that yielded zero rows.
function isEmpty(state: DataTableState): boolean
isError() from @llui/components/patterns
function isError(state: DataTableState): boolean
isLoading() from @llui/components/patterns
function isLoading(state: DataTableState): boolean
pathToFieldName() from @llui/components/patterns
Map a Standard Schema issue path to a flat field name. Object keys join with
. and array indices append as .<n> — so ['address', 'street'] becomes
address.street and ['tags', 0] becomes tags.0. These match the keys a
consumer passes in fields.
function pathToFieldName(path: StandardSchemaV1.Issue['path']): string
searchableSelectConnect() from @llui/components/patterns
function searchableSelectConnect(state: Signal<SearchableSelectState>, send: Send<SearchableSelectMsg>, opts: ConnectOptions): SearchableSelectParts
searchableSelectInit() from @llui/components/patterns
function searchableSelectInit(opts: SearchableSelectInit = {}): SearchableSelectState
searchableSelectOverlay() from @llui/components/patterns
function searchableSelectOverlay(opts: OverlayOptions): Mountable
searchableSelectUpdate() from @llui/components/patterns
function searchableSelectUpdate(state: SearchableSelectState, msg: SearchableSelectMsg): [SearchableSelectState, never[]]
stepStatus() from @llui/components/patterns
function stepStatus(state: WizardState, step: number): StepStatus
totalPages() from @llui/components/patterns
function totalPages(state: DataTableState): number
watchHotkey() from @llui/components/patterns
Listen for the global command-palette hotkey. Returns a cleanup function.
Call from onMount; the DOM listener never lives inside the machine.
combo is a +-joined chord, e.g. 'mod+k' (default). mod matches the
platform-conventional accelerator (⌘ on macOS, Ctrl elsewhere); since both
map to either metaKey or ctrlKey here, mod accepts either modifier.
function watchHotkey(send: Send<CommandMenuMsg>, combo: string = 'mod+k'): () => void
Types
CommandMenuEffect from @llui/components/patterns
Effects emitted by the command-menu machine. execute is the single
agent-resolvable surface: the consumer's onEffect runs the side effect for
the picked command id (the machine never performs IO).
export type CommandMenuEffect =
/** @intent("Run the side effect for the executed command id") */
{ type: 'execute'; commandId: string }
CommandMenuMsg from @llui/components/patterns
export type CommandMenuMsg =
/** @intent("Open the command palette") */
| { type: 'open' }
/** @intent("Close the command palette") */
| { type: 'close' }
/** @intent("Set the search query (re-runs the filter)") */
| { type: 'setQuery'; query: string }
/** @intent("Run the command with the given id, then close the palette") */
| { type: 'execute'; commandId: string }
/** @humanOnly */
| { type: 'escape' }
/** @humanOnly */
| { type: 'setCommands'; commands: Command[] }
ConfirmDialogMsg from @llui/components/patterns
export type ConfirmDialogMsg =
| {
type: 'openWith'
tag: string
title: string
description?: string
confirmLabel?: string
cancelLabel?: string
destructive?: boolean
}
| { type: 'confirm' }
| { type: 'cancel' }
| { type: 'setOpen'; open: boolean }
DataTableEffect from @llui/components/patterns
export type DataTableEffect = LoadPageEffect
DataTableMsg from @llui/components/patterns
export type DataTableMsg =
/** @intent("Cycle the sort on the given column; resets to page 1 and reloads") */
| { type: 'toggleSort'; columnId: string }
/** @intent("Set an explicit sort (or null to clear); resets to page 1 and reloads") */
| { type: 'setSort'; sort: TableSort | null }
/** @intent("Jump to a specific 1-based page and reload") */
| { type: 'setPage'; page: number }
/** @intent("Advance to the next page and reload") */
| { type: 'nextPage' }
/** @intent("Go back to the previous page and reload") */
| { type: 'prevPage' }
/** @intent("Change the page size; preserves the first visible item and reloads") */
| { type: 'setPageSize'; pageSize: number }
/** @intent("Re-request the current page (e.g. after a failure)") */
| { type: 'reload' }
/** @humanOnly */
| { type: 'pageLoaded'; queryId: number; rows: string[]; total: number }
/** @humanOnly */
| { type: 'pageFailed'; queryId: number; error: string }
/** @intent("Toggle selection of the row with the given id at the given display index") */
| { type: 'toggleRow'; id: string; index: number }
/** @intent("Toggle between select-all and clear for the current scope") */
| { type: 'toggleAll' }
/** @intent("Clear the entire selection") */
| { type: 'clearSelection' }
/** @intent("Select the inclusive range from the current anchor to the given index (Shift+click)") */
| { type: 'selectRange'; index: number }
/** @intent("Activate (open/confirm) the row with the given id at the given index") */
| { type: 'activateRow'; id: string; index: number }
/** @humanOnly */
| { type: 'focusCell'; rowIndex: number; colIndex: number }
/** @humanOnly */
| { type: 'tableKey'; msg: TableMsg }
FormFieldMsg from @llui/components/patterns
export type FormFieldMsg =
/** @intent("Validate the given values against a Standard Schema synchronously and update field validity") */
| { type: 'validate'; schema: StandardSchemaV1<unknown>; values: unknown }
/** @intent("Begin an async validation — marks every field pending until validateResult arrives") */
| { type: 'validateAsync'; schema: StandardSchemaV1<unknown>; values: unknown; requestId: number }
/** @intent("Apply the issues from a resolved async validation, clearing the pending state") */
| { type: 'validateResult'; issues: StandardSchemaV1.Issue[]; requestId: number }
/** @intent("Mark a single field as touched (typically on blur)") */
| { type: 'touch'; field: string }
/** @intent("Mark every field as touched (typically on a failed submit attempt)") */
| { type: 'touchAll' }
/** @intent("Begin form submission — transitions status to submitting") */
| { type: 'submit' }
/** @intent("Mark the in-flight submission as successful") */
| { type: 'submitSuccess' }
/** @intent("Mark the in-flight submission as failed with the given error message") */
| { type: 'submitError'; error: string }
/** @intent("Reset the form and every field slice to their initial state") */
| { type: 'reset' }
SearchableSelectAsyncStatus from @llui/components/patterns
export type AsyncStatus = 'idle' | 'loading' | 'loaded' | 'error'
SearchableSelectMsg from @llui/components/patterns
export type SearchableSelectMsg =
/** @intent("Open the searchable select popup") */
| { type: 'open' }
/** @intent("Close the popup (resets the filter)") */
| { type: 'close' }
/** @intent("Set the filter text (re-runs the item filter; never commits a value)") */
| { type: 'setFilter'; value: string }
/** @intent("Select the option with the given value (toggles in multi-select)") */
| { type: 'selectValue'; value: string }
/** @intent("Replace the selected values with the provided list") */
| { type: 'setValue'; value: string[] }
/** @intent("Clear the current selection") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'highlightNext' }
/** @humanOnly */
| { type: 'highlightPrev' }
/** @humanOnly */
| { type: 'highlightFirst' }
/** @humanOnly */
| { type: 'highlightLast' }
/** @humanOnly */
| { type: 'highlight'; value: string | null }
/** @intent("Select the currently-highlighted option (the only commit path from the keyboard)") */
| { type: 'selectHighlighted' }
/** @humanOnly */
| { type: 'triggerType'; char: string }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
SearchableSelectSelectionMode from @llui/components/patterns
Combobox — text input paired with a filtered listbox dropdown. User types to filter items, arrow keys navigate the filtered set, Enter selects. Supports single and multiple selection.
Beyond the sync filtered listbox the machine owns three additive surfaces:
- Async option loading —
status/requestId/errortrack an in-flight fetch. The consumer debounces (e.g.@llui/effectsdebounce) and runs the fetch itself, dispatchingloadStart/loadSuccess/loadErrortagged with a monotonically-increasingrequestId. The reducer DROPS anyloadSuccess/loadErrorwhoserequestIdis not the current one, so a late response from a superseded request can never clobber fresh state. The machine owns no timers. - Option groups —
groupsmirrorselect'sSelectGroupshape exactly. The flatitemslist stays the source of truth for navigation/highlight indices; group LABELS are never options, so arrow navigation skips them. - Creatable — opt-in
allowCreate. WheninputValueis non-empty and matches no item, a synthetic create sentinel is appended tofilteredItems. Selecting it emits acreateOptionEFFECT (carrying the typed text) so the consumer owns creation; the machine never mutatesvaluefor it.
export type SelectionMode = 'single' | 'multiple'
StepValidator from @llui/components/patterns
A step validator: a predicate or a Standard Schema, sync or async.
export type StepValidator = ((values?: unknown) => boolean | Promise<boolean>) | StandardSchemaV1
WizardEffect from @llui/components/patterns
export type WizardEffect =
/** Run the async validator for `step`; dispatch stepValid/stepInvalid. */
{ type: 'validateStep'; step: number }
WizardInit from @llui/components/patterns
export type WizardInit = StepsInit
WizardMsg from @llui/components/patterns
export type WizardMsg =
/** @intent("Validate the current step and, if it passes, advance to the next step") */
| { type: 'next' }
/** @intent("Go back to the previous step (never gated by validation)") */
| { type: 'prev' }
/** @intent("Jump to a specific step by zero-based index (respects linear + completion gating)") */
| { type: 'goTo'; step: number }
/** @humanOnly */
| { type: 'stepValid'; step: number }
/** @humanOnly */
| { type: 'stepInvalid'; step: number }
/** @intent("Reset the wizard back to the first step (clears completed, errors and pending validation)") */
| { type: 'reset' }
WizardValidators from @llui/components/patterns
Map of zero-based step index → validator. Steps without an entry pass freely.
export type WizardValidators = Record<number, StepValidator>
Interfaces
Command from @llui/components/patterns
A single palette command. JSON-serializable (no functions): execution is
surfaced as an execute effect keyed by id, never a callback in state.
export interface Command {
id: string
label: string
/** Optional group/section label. Commands without a group fall into ''. */
group?: string
/** Extra terms matched by the filter in addition to `label`. */
keywords?: string[]
/** Pre-rendered keybinding hint, e.g. 'mod+s'. Surfaced via `shortcutHint`. */
shortcut?: string
disabled?: boolean
}
CommandGroup from @llui/components/patterns
A labelled section of filtered commands, in visual order.
export interface CommandGroup {
label: string
commands: Command[]
}
CommandMenuConnectOptions from @llui/components/patterns
export interface ConnectOptions {
/** Unique id per palette instance (used for ARIA wiring). */
id: string
}
CommandMenuInit from @llui/components/patterns
export interface CommandMenuInit {
commands?: Command[]
recents?: string[]
maxRecents?: number
open?: boolean
}
CommandMenuParts from @llui/components/patterns
export interface CommandMenuParts {
/** Dialog parts (content/title/positioner/backdrop) for the modal shell. */
dialog: DialogParts
/** Combobox parts (root/input/content/item/group/...) for the search + list. */
combobox: ComboboxParts
/** Accessor for a command's keybinding hint (empty string when none). */
shortcutHint: (commandId: string) => Signal<string>
/** Empty-state part: `data-empty` is set when the filtered list is empty. */
empty: {
'data-scope': 'command-menu'
'data-part': 'empty'
role: 'status'
'data-empty': Signal<'' | undefined>
}
}
CommandMenuState from @llui/components/patterns
export interface CommandMenuState {
open: boolean
query: string
commands: Command[]
/** Filtered (and recents-ranked) flat command list. */
filtered: Command[]
/** Filtered commands bucketed into groups (group order preserved). */
filteredGroups: CommandGroup[]
/** Most-recently-executed command ids, most-recent first (deduped). */
recents: string[]
/** Max recents retained for ranking. */
maxRecents: number
}
CommandMenuViewOptions from @llui/components/patterns
export interface CommandMenuViewOptions {
state: Signal<CommandMenuState>
send: Send<CommandMenuMsg>
id: string
inputLabel?: string
/** Custom class for the content root. */
contentClass?: string
/** Accessible title for the palette dialog (default: 'Command palette'). */
title?: string
/** Empty-state text (default: 'No matching commands'). */
emptyText?: string
}
ConfirmDialogInit from @llui/components/patterns
export interface ConfirmDialogInit {
tag?: string
title?: string
description?: string
confirmLabel?: string
cancelLabel?: string
destructive?: boolean
}
ConfirmDialogState from @llui/components/patterns
ConfirmDialog — a pre-wired dialog pattern for confirmations.
Composes dialog with conventional content: title, description, cancel,
confirm. Carries an opaque tag so the consumer's update handler can
recognize which confirmation resolved.
The MACHINE (init/update/openWith) is styling-agnostic and is what most
consumers want. The bundled view() is a convenience for baseline-stylesheet
users only — see the note on ConfirmDialogViewOptions.
Usage in consumer's update:
case 'confirm': {
const [s, fx] = confirmDialog.update(state.confirm, msg.msg)
// When the user clicks confirm, branch on the tag:
if (msg.msg.type === 'confirm') {
switch (state.confirm.tag) {
case 'delete-user': return [{ ...state, confirm: s, users: ... }, fx]
case 'logout': return [{ ...state, confirm: s }, [...fx, logoutEffect]]
}
}
return [{ ...state, confirm: s }, fx]
}
export interface ConfirmDialogState {
open: boolean
tag: string
title: string
description: string
confirmLabel: string
cancelLabel: string
destructive: boolean
}
ConfirmDialogViewOptions from @llui/components/patterns
Options for the convenience view.
view() targets the BASELINE STYLESHEET, not the component registry. It
hardcodes btn btn-secondary, btn btn-danger and confirm-dialog__actions
— class names that only exist in @llui/components/styles/theme.css — and
contentClass / destructiveClass reach only two of them. A consumer
styling with utilities (the registry path) imports tokens.css and NOT
theme.css, so calling this renders unstyled buttons.
That path should wire the machine directly instead: dialogConnect +
dialogOverlay with its own parts, translating the dialog's close into
cancel, which is all this function does minus the class names.
examples/registry-demo's patterns section is the worked example.
export interface ConfirmDialogViewOptions {
state: Signal<ConfirmDialogState>
send: Send<ConfirmDialogMsg>
id: string
/** Custom class for content root. */
contentClass?: string
/** Custom class for destructive confirm button. */
destructiveClass?: string
}
DataTableConnectOptions from @llui/components/patterns
export interface ConnectOptions {
/** Element id base for the table (`grid`) root. */
id: string
/** Accessible label for the pagination nav. */
paginationLabel?: string
}
DataTableInit from @llui/components/patterns
export interface DataTableInit {
columns?: TableColumn[]
selectionMode?: TableSelectionMode
sort?: TableSort | null
page?: number
pageSize?: number
total?: number
siblings?: number
boundaries?: number
descFirst?: boolean
clearOnPageChange?: boolean
}
DataTableParts from @llui/components/patterns
export interface DataTableParts extends DataTableStatusParts {
table: TableParts
pagination: PaginationParts
}
DataTableState from @llui/components/patterns
DataTable — a pre-wired pattern for server-paginated, sortable, selectable
lists. It COMPOSES the headless table machine (sort / selection /
grid-keyboard), pagination (page / pageSize / total), and the async-list
status vocabulary (idle | loading | loaded | error) into a single slice.
The value of the pattern is the GLUE:
- changing the sort resets to page 1 and reloads,
- changing the page or page size reloads,
- every reload bumps a
queryIdversion counter and emits aloadPageeffect carrying{ page, pageSize, sort, queryId }; the consumer fetches and repliespageLoaded { queryId, rows, total }. The reducer DROPS anypageLoadedwhosequeryIdis stale (an older in-flight request), so a slow response can never clobber a newer one.
Row DATA stays in the consumer (the table machine only tracks row IDs in
display order). Server-side sort works for free: feed the pre-sorted IDs back
in via pageLoaded.
Usage in the consumer's onEffect:
onEffect: (effect, send) => {
if (effect.type === 'data-table:loadPage') {
const { page, pageSize, sort, queryId } = effect
fetchRows({ page, pageSize, sort })
.then(({ ids, total, data }) => {
setRowData(data)
send({ type: 'dt', msg: { type: 'pageLoaded', queryId, rows: ids, total } })
})
.catch((e) => send({ type: 'dt', msg: { type: 'pageFailed', queryId, error: String(e) } }))
}
}
export interface DataTableState {
table: TableState
pagination: PaginationState
/** Async status of the current page request. */
status: AsyncStatus
/** Error message from the last failed request, or null. */
error: string | null
/**
* Version counter for the in-flight request. Bumped on every reload; a
* `pageLoaded`/`pageFailed` whose `queryId` differs from this is stale and
* dropped.
*/
queryId: number
/**
* Selection policy. When true (default), the selection is cleared on every
* page change and select-all scopes to the current page. When false, the
* selection persists across pages (cross-page selection).
*/
clearOnPageChange: boolean
}
DataTableStatusParts from @llui/components/patterns
ARIA live-region overlay parts derived from async-list conventions.
export interface DataTableStatusParts {
/** Spinner / overlay shown while loading. `aria-busy` mirrors loading. */
loadingOverlay: {
'data-scope': 'data-table'
'data-part': 'loading-overlay'
'aria-busy': Signal<'true' | undefined>
'aria-live': 'polite'
hidden: Signal<boolean>
}
/** Empty-state region — shown when a settled request has zero rows. */
emptyState: {
role: 'status'
'aria-live': 'polite'
'data-scope': 'data-table'
'data-part': 'empty-state'
hidden: Signal<boolean>
}
/** Error-state region — shown when the last request failed. */
errorState: {
role: 'alert'
'aria-live': 'polite'
'data-scope': 'data-table'
'data-part': 'error-state'
hidden: Signal<boolean>
}
}
FormFieldConnectOptions from @llui/components/patterns
export interface FormFieldConnectOptions {
/** Base id; field slice ids derive as `${id}:${name}`. */
id: string
/** The field names this form manages. */
fields: readonly string[]
}
FormFieldFieldParts from @llui/components/patterns
The composed part bag for a single field.
export interface FormFieldFieldParts {
root: {
'data-scope': 'form-field'
'data-part': 'field'
'data-invalid': Signal<'' | undefined>
'data-touched': Signal<'' | undefined>
}
label: {
id: string
htmlFor: string
'data-scope': 'form-field'
'data-part': 'label'
}
control: {
id: string
'aria-labelledby': string
'aria-describedby': Signal<string | undefined>
'aria-invalid': Signal<'true' | undefined>
'aria-required': Signal<'true' | undefined>
'aria-busy': Signal<'true' | undefined>
disabled: Signal<boolean>
readOnly: Signal<boolean>
'data-scope': 'form-field'
'data-part': 'control'
onBlur: (e: FocusEvent) => void
}
description: {
id: string
'data-scope': 'form-field'
'data-part': 'description'
}
errorText: {
id: string
role: 'alert'
'aria-live': 'polite'
'data-scope': 'form-field'
'data-part': 'error'
/** First visible issue message for this field, or '' when no error is shown. */
message: Signal<string>
/** Every issue mapped to this field (for custom rendering). */
issues: Signal<StandardSchemaV1.Issue[]>
}
/** True only when the field is invalid AND its error should be visible
* (`touched || status === 'submitted'`). Use to gate `show(...)`. */
errorVisible: Signal<boolean>
}
FormFieldInit from @llui/components/patterns
export interface FormFieldInit {
/** Base id; field slice ids derive as `${id}:${name}`. */
id: string
/** The field names this form manages. */
fields: readonly string[]
}
FormFieldParts from @llui/components/patterns
export interface FormFieldParts {
root: {
'data-scope': 'form-field'
'data-part': 'root'
'data-state': Signal<FormStatus>
'aria-busy': Signal<'true' | undefined>
}
submit: {
type: 'submit'
'data-scope': 'form-field'
'data-part': 'submit'
'data-state': Signal<FormStatus>
disabled: Signal<boolean>
}
/** Build the full part bag for the named field, with the form blur-to-touch
* handler already merged into `control`. */
formField: (name: string, opts?: FieldConnectOptions) => FormFieldFieldParts
}
FormFieldSlice from @llui/components/patterns
Per-field slice: the field component's state plus an async-validation flag.
export interface FormFieldSlice extends FieldState {
/** True while an async validation for this field is in flight. */
pending: boolean
}
FormFieldState from @llui/components/patterns
export interface FormFieldState {
/** The composed `form` lifecycle slice (status, touched, submitError). */
form: FormState
/** Per-field slices keyed by field name. */
fields: Record<string, FormFieldSlice>
/** The issues from the last validation, unaltered. */
issues: StandardSchemaV1.Issue[]
/**
* Id of the most recent async validation. `validateAsync` stamps its
* `requestId` here; a `validateResult` whose `requestId` no longer matches is
* DROPPED (stale-response protection — a slow earlier validation must never
* overwrite a newer result). `reset` bumps it so any in-flight validation is
* invalidated.
*/
validationId: number
}
LoadPageEffect from @llui/components/patterns
The loadPage effect — carried out of update, fulfilled by the consumer.
export interface LoadPageEffect {
type: 'data-table:loadPage'
page: number
pageSize: number
sort: TableSort | null
queryId: number
}
SearchableSelectComboboxGroup from @llui/components/patterns
A labelled section of options (rendered like <optgroup>). items are the
option VALUES belonging to the group, in visual order. Groups are an
additive, parallel structure: the flat items list always remains the
source of truth for navigation/highlight indices and item ids — when
groups is provided without an explicit items list, init derives the
flat list by concatenating each group's items in order. A plain flat
string[] (no groups) keeps working unchanged. Group LABELS are never
options, so highlight/arrow navigation skips over them for free.
Mirrors select's SelectGroup shape exactly.
export interface ComboboxGroup {
id: string
label: string
items: string[]
}
SearchableSelectConnectOptions from @llui/components/patterns
export interface ConnectOptions {
id: string
/** aria-label for the clear button. */
clearLabel?: string
/** Text shown in the empty-state live region when no items match. */
emptyText?: string
}
SearchableSelectGroupParts from @llui/components/patterns
export interface SearchableSelectGroupParts {
group: {
role: 'group'
'aria-labelledby': string
'data-scope': 'searchable-select'
'data-part': 'group'
'data-group': string
}
groupLabel: {
id: string
'aria-hidden': 'true'
'data-scope': 'searchable-select'
'data-part': 'group-label'
'data-group': string
}
}
SearchableSelectInit from @llui/components/patterns
export interface SearchableSelectInit {
value?: string[]
items?: string[]
/** Optional labelled sections (passthrough to `combobox`). */
groups?: ComboboxGroup[]
disabledItems?: string[]
selectionMode?: SelectionMode
disabled?: boolean
/** Seed the filter with the selected label on open (default: false → empty). */
prefillFilter?: boolean
/** Trigger text when nothing is selected. */
placeholder?: string
/** Join multiple selected labels with this separator in the trigger. */
separator?: string
}
SearchableSelectItemParts from @llui/components/patterns
export interface SearchableSelectItemParts {
item: {
role: 'option'
id: string
'aria-selected': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
'data-state': Signal<'selected' | undefined>
'data-highlighted': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
'data-scope': 'searchable-select'
'data-part': 'item'
'data-value': string
/** The option's live position in the filtered list (reactive — reused rows
* never report a stale index). */
'data-index': Signal<string>
onClick: (e: MouseEvent) => void
onPointerMove: (e: PointerEvent) => void
}
}
SearchableSelectOverlayOptions from @llui/components/patterns
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*
* Every `overlay()` in `components/` takes this; the pattern overlays were
* missed when it was added, so a utility-styled consumer had no way to give
* this popup a stacking context at all.
*/
positionerClass?: string
state: Signal<SearchableSelectState>
send: Send<SearchableSelectMsg>
parts: SearchableSelectParts
/** Renders the popup body (filter input + listbox). */
content: () => Renderable
/**
* Optional enter/leave transition for the searchable-select popup (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, giving the raw-`open` popup an exit
* animation for free. Omitted ⇒ the popup closes synchronously as before.
*
* @example searchableSelect.overlay({ state, send, parts, content, transition: fade({ duration: 120 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
sameWidth?: boolean
target?: string | HTMLElement
}
SearchableSelectParts from @llui/components/patterns
export interface SearchableSelectParts {
root: {
'data-scope': 'searchable-select'
'data-part': 'root'
'data-state': Signal<'open' | 'closed'>
}
/** The closed-state trigger button. Displays the selection (via
* `triggerLabel`) and opens the popup. Handles closed-trigger typeahead. */
trigger: {
type: 'button'
role: 'combobox'
'aria-haspopup': 'listbox'
'aria-expanded': Signal<boolean>
'aria-controls': string
'aria-disabled': Signal<'true' | undefined>
id: string
disabled: Signal<boolean>
'data-scope': 'searchable-select'
'data-part': 'trigger'
'data-state': Signal<'open' | 'closed'>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
/** Text to render inside the trigger: placeholder, single label, or a joined
* multi-select summary. */
triggerLabel: Signal<string>
/** Whether a selection exists (drive showing/hiding the clear button). */
hasValue: Signal<boolean>
/** The filter input rendered inside the popup, above the listbox. */
input: {
type: 'text'
role: 'combobox'
autocomplete: 'off'
'aria-autocomplete': 'list'
'aria-expanded': Signal<boolean>
'aria-controls': string
'aria-activedescendant': Signal<string | undefined>
id: string
value: Signal<string>
'data-scope': 'searchable-select'
'data-part': 'input'
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
}
positioner: {
'data-scope': 'searchable-select'
'data-part': 'positioner'
style: string
}
content: {
role: 'listbox'
id: string
'aria-labelledby': string
'aria-busy': Signal<'true' | undefined>
'aria-multiselectable': Signal<'true' | undefined>
tabindex: -1
'data-state': Signal<'open' | 'closed'>
'data-status': Signal<AsyncStatus>
'data-scope': 'searchable-select'
'data-part': 'content'
}
/** Build the parts for an option by VALUE. The optional `index` is accepted
* for call-site convenience only — identity is value-keyed, so a reused row is
* never stale. */
item: (value: string, index?: number) => SearchableSelectItemParts
group: (id: string) => SearchableSelectGroupParts
/** Clear-selection trigger. Render only when `hasValue` is true. */
clear: {
type: 'button'
'aria-label': string
tabindex: -1
'data-scope': 'searchable-select'
'data-part': 'clear'
onClick: (e: MouseEvent) => void
}
/** Polite live region announcing the no-results / result count. */
liveRegion: {
role: 'status'
'aria-live': 'polite'
'aria-atomic': 'true'
'data-scope': 'searchable-select'
'data-part': 'live-region'
text: Signal<string>
}
/** Empty-state container (render when the filtered list is empty). */
empty: {
'data-scope': 'searchable-select'
'data-part': 'empty'
hidden: Signal<boolean>
}
}
SearchableSelectState from @llui/components/patterns
export interface SearchableSelectState {
/** Whether the popup is open. Mirrors `combobox.open`; kept at the top level
* so consumers can read it without reaching into the nested machine. */
open: boolean
/** The underlying combobox machine state. `value` is the source of truth for
* the selection; `inputValue` is the filter (never the committed value). */
combobox: ComboboxState
/** When opening, seed the filter with the selected label (and the consumer
* should select-all). When false (default) the filter opens empty. */
prefillFilter: boolean
/** Trigger placeholder shown when nothing is selected. */
placeholder: string
/** Separator used to join multiple selected labels in the trigger. */
separator: string
}
WizardConnectOptions from @llui/components/patterns
export interface WizardConnectOptions {
label?: string
}
WizardParts from @llui/components/patterns
export interface WizardParts {
root: {
role: 'group'
'aria-label': string
'data-scope': 'steps'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
nextTrigger: {
type: 'button'
disabled: Signal<boolean>
'aria-busy': Signal<'true' | undefined>
'data-scope': 'steps'
'data-part': 'next-trigger'
onClick: (e: MouseEvent) => void
}
prevTrigger: {
type: 'button'
disabled: Signal<boolean>
'data-scope': 'steps'
'data-part': 'prev-trigger'
onClick: (e: MouseEvent) => void
}
/** Per-step trigger parts (item / trigger / separator), gated like raw steps. */
item: (index: number) => StepsItemParts
/** The trigger sub-part for a step index — for keyboard-complete step lists. */
stepTrigger: (index: number) => StepsItemParts['trigger']
}
WizardState from @llui/components/patterns
export interface WizardState {
/** Underlying steps machine state. */
steps: StepsState
/** Index of the step whose async validation is pending, or null when idle. */
validating: number | null
}
Constants
commandMenu from @llui/components/patterns
const commandMenu: typeof import('./command-menu.js')
commandMenuPattern from @llui/components/patterns
const commandMenuPattern
confirmDialog from @llui/components/patterns
const confirmDialog: typeof import('./confirm-dialog.js')
dataTable from @llui/components/patterns
const dataTable: typeof import('./data-table.js')
dataTablePattern from @llui/components/patterns
const dataTablePattern
formField from @llui/components/patterns
const formField: typeof import('./form-field.js')
searchableSelect from @llui/components/patterns
const searchableSelect: typeof import('./searchable-select.js')
searchableSelectPattern from @llui/components/patterns
const searchableSelectPattern
wizard from @llui/components/patterns
const wizard: typeof import('./wizard.js')
wizardFlow from @llui/components/patterns
const wizardFlow
@llui/components/patterns/confirm-dialog
Functions
dialogInit() from @llui/components/patterns/confirm-dialog
function dialogInit(opts: DialogInit = {}): DialogState
dialogUpdate() from @llui/components/patterns/confirm-dialog
function dialogUpdate(state: DialogState, msg: DialogMsg): [DialogState, never[]]
init() from @llui/components/patterns/confirm-dialog
function init(opts: ConfirmDialogInit = {}): ConfirmDialogState
openWith() from @llui/components/patterns/confirm-dialog
Helper to create an openWith message builder.
function openWith(tag: string, opts: {
title: string
description?: string
confirmLabel?: string
cancelLabel?: string
destructive?: boolean
}): ConfirmDialogMsg
update() from @llui/components/patterns/confirm-dialog
function update(state: ConfirmDialogState, msg: ConfirmDialogMsg): [ConfirmDialogState, never[]]
view() from @llui/components/patterns/confirm-dialog
function view(opts: ConfirmDialogViewOptions): Mountable
Types
ConfirmDialogMsg from @llui/components/patterns/confirm-dialog
export type ConfirmDialogMsg =
| {
type: 'openWith'
tag: string
title: string
description?: string
confirmLabel?: string
cancelLabel?: string
destructive?: boolean
}
| { type: 'confirm' }
| { type: 'cancel' }
| { type: 'setOpen'; open: boolean }
Interfaces
ConfirmDialogInit from @llui/components/patterns/confirm-dialog
export interface ConfirmDialogInit {
tag?: string
title?: string
description?: string
confirmLabel?: string
cancelLabel?: string
destructive?: boolean
}
ConfirmDialogState from @llui/components/patterns/confirm-dialog
ConfirmDialog — a pre-wired dialog pattern for confirmations.
Composes dialog with conventional content: title, description, cancel,
confirm. Carries an opaque tag so the consumer's update handler can
recognize which confirmation resolved.
The MACHINE (init/update/openWith) is styling-agnostic and is what most
consumers want. The bundled view() is a convenience for baseline-stylesheet
users only — see the note on ConfirmDialogViewOptions.
Usage in consumer's update:
case 'confirm': {
const [s, fx] = confirmDialog.update(state.confirm, msg.msg)
// When the user clicks confirm, branch on the tag:
if (msg.msg.type === 'confirm') {
switch (state.confirm.tag) {
case 'delete-user': return [{ ...state, confirm: s, users: ... }, fx]
case 'logout': return [{ ...state, confirm: s }, [...fx, logoutEffect]]
}
}
return [{ ...state, confirm: s }, fx]
}
export interface ConfirmDialogState {
open: boolean
tag: string
title: string
description: string
confirmLabel: string
cancelLabel: string
destructive: boolean
}
ConfirmDialogViewOptions from @llui/components/patterns/confirm-dialog
Options for the convenience view.
view() targets the BASELINE STYLESHEET, not the component registry. It
hardcodes btn btn-secondary, btn btn-danger and confirm-dialog__actions
— class names that only exist in @llui/components/styles/theme.css — and
contentClass / destructiveClass reach only two of them. A consumer
styling with utilities (the registry path) imports tokens.css and NOT
theme.css, so calling this renders unstyled buttons.
That path should wire the machine directly instead: dialogConnect +
dialogOverlay with its own parts, translating the dialog's close into
cancel, which is all this function does minus the class names.
examples/registry-demo's patterns section is the worked example.
export interface ConfirmDialogViewOptions {
state: Signal<ConfirmDialogState>
send: Send<ConfirmDialogMsg>
id: string
/** Custom class for content root. */
contentClass?: string
/** Custom class for destructive confirm button. */
destructiveClass?: string
}
Constants
confirmDialog from @llui/components/patterns/confirm-dialog
const confirmDialog
@llui/components/patterns/form-field
Functions
connect() from @llui/components/patterns/form-field
function connect(state: Signal<FormFieldState>, send: Send<FormFieldMsg>, opts: FormFieldConnectOptions): FormFieldParts
init() from @llui/components/patterns/form-field
function init(opts: FormFieldInit): FormFieldState
pathToFieldName() from @llui/components/patterns/form-field
Map a Standard Schema issue path to a flat field name. Object keys join with
. and array indices append as .<n> — so ['address', 'street'] becomes
address.street and ['tags', 0] becomes tags.0. These match the keys a
consumer passes in fields.
function pathToFieldName(path: StandardSchemaV1.Issue['path']): string
update() from @llui/components/patterns/form-field
function update(state: FormFieldState, msg: FormFieldMsg): [FormFieldState, never[]]
Types
FormFieldMsg from @llui/components/patterns/form-field
export type FormFieldMsg =
/** @intent("Validate the given values against a Standard Schema synchronously and update field validity") */
| { type: 'validate'; schema: StandardSchemaV1<unknown>; values: unknown }
/** @intent("Begin an async validation — marks every field pending until validateResult arrives") */
| { type: 'validateAsync'; schema: StandardSchemaV1<unknown>; values: unknown; requestId: number }
/** @intent("Apply the issues from a resolved async validation, clearing the pending state") */
| { type: 'validateResult'; issues: StandardSchemaV1.Issue[]; requestId: number }
/** @intent("Mark a single field as touched (typically on blur)") */
| { type: 'touch'; field: string }
/** @intent("Mark every field as touched (typically on a failed submit attempt)") */
| { type: 'touchAll' }
/** @intent("Begin form submission — transitions status to submitting") */
| { type: 'submit' }
/** @intent("Mark the in-flight submission as successful") */
| { type: 'submitSuccess' }
/** @intent("Mark the in-flight submission as failed with the given error message") */
| { type: 'submitError'; error: string }
/** @intent("Reset the form and every field slice to their initial state") */
| { type: 'reset' }
Interfaces
FormFieldConnectOptions from @llui/components/patterns/form-field
export interface FormFieldConnectOptions {
/** Base id; field slice ids derive as `${id}:${name}`. */
id: string
/** The field names this form manages. */
fields: readonly string[]
}
FormFieldFieldParts from @llui/components/patterns/form-field
The composed part bag for a single field.
export interface FormFieldFieldParts {
root: {
'data-scope': 'form-field'
'data-part': 'field'
'data-invalid': Signal<'' | undefined>
'data-touched': Signal<'' | undefined>
}
label: {
id: string
htmlFor: string
'data-scope': 'form-field'
'data-part': 'label'
}
control: {
id: string
'aria-labelledby': string
'aria-describedby': Signal<string | undefined>
'aria-invalid': Signal<'true' | undefined>
'aria-required': Signal<'true' | undefined>
'aria-busy': Signal<'true' | undefined>
disabled: Signal<boolean>
readOnly: Signal<boolean>
'data-scope': 'form-field'
'data-part': 'control'
onBlur: (e: FocusEvent) => void
}
description: {
id: string
'data-scope': 'form-field'
'data-part': 'description'
}
errorText: {
id: string
role: 'alert'
'aria-live': 'polite'
'data-scope': 'form-field'
'data-part': 'error'
/** First visible issue message for this field, or '' when no error is shown. */
message: Signal<string>
/** Every issue mapped to this field (for custom rendering). */
issues: Signal<StandardSchemaV1.Issue[]>
}
/** True only when the field is invalid AND its error should be visible
* (`touched || status === 'submitted'`). Use to gate `show(...)`. */
errorVisible: Signal<boolean>
}
FormFieldInit from @llui/components/patterns/form-field
export interface FormFieldInit {
/** Base id; field slice ids derive as `${id}:${name}`. */
id: string
/** The field names this form manages. */
fields: readonly string[]
}
FormFieldParts from @llui/components/patterns/form-field
export interface FormFieldParts {
root: {
'data-scope': 'form-field'
'data-part': 'root'
'data-state': Signal<FormStatus>
'aria-busy': Signal<'true' | undefined>
}
submit: {
type: 'submit'
'data-scope': 'form-field'
'data-part': 'submit'
'data-state': Signal<FormStatus>
disabled: Signal<boolean>
}
/** Build the full part bag for the named field, with the form blur-to-touch
* handler already merged into `control`. */
formField: (name: string, opts?: FieldConnectOptions) => FormFieldFieldParts
}
FormFieldSlice from @llui/components/patterns/form-field
Per-field slice: the field component's state plus an async-validation flag.
export interface FormFieldSlice extends FieldState {
/** True while an async validation for this field is in flight. */
pending: boolean
}
FormFieldState from @llui/components/patterns/form-field
export interface FormFieldState {
/** The composed `form` lifecycle slice (status, touched, submitError). */
form: FormState
/** Per-field slices keyed by field name. */
fields: Record<string, FormFieldSlice>
/** The issues from the last validation, unaltered. */
issues: StandardSchemaV1.Issue[]
/**
* Id of the most recent async validation. `validateAsync` stamps its
* `requestId` here; a `validateResult` whose `requestId` no longer matches is
* DROPPED (stale-response protection — a slow earlier validation must never
* overwrite a newer result). `reset` bumps it so any in-flight validation is
* invalidated.
*/
validationId: number
}
Constants
formField from @llui/components/patterns/form-field
const formField
@llui/components/patterns/wizard
Functions
connect() from @llui/components/patterns/wizard
function connect(state: Signal<WizardState>, send: Send<WizardMsg>, opts: WizardConnectOptions = {}): WizardParts
init() from @llui/components/patterns/wizard
function init(opts: WizardInit = {}): WizardState
stepStatus() from @llui/components/patterns/wizard
function stepStatus(state: WizardState, step: number): StepStatus
update() from @llui/components/patterns/wizard
function update(state: WizardState, msg: WizardMsg, validators: WizardValidators = {}): [WizardState, WizardEffect[]]
Types
StepValidator from @llui/components/patterns/wizard
A step validator: a predicate or a Standard Schema, sync or async.
export type StepValidator = ((values?: unknown) => boolean | Promise<boolean>) | StandardSchemaV1
WizardEffect from @llui/components/patterns/wizard
export type WizardEffect =
/** Run the async validator for `step`; dispatch stepValid/stepInvalid. */
{ type: 'validateStep'; step: number }
WizardInit from @llui/components/patterns/wizard
export type WizardInit = StepsInit
WizardMsg from @llui/components/patterns/wizard
export type WizardMsg =
/** @intent("Validate the current step and, if it passes, advance to the next step") */
| { type: 'next' }
/** @intent("Go back to the previous step (never gated by validation)") */
| { type: 'prev' }
/** @intent("Jump to a specific step by zero-based index (respects linear + completion gating)") */
| { type: 'goTo'; step: number }
/** @humanOnly */
| { type: 'stepValid'; step: number }
/** @humanOnly */
| { type: 'stepInvalid'; step: number }
/** @intent("Reset the wizard back to the first step (clears completed, errors and pending validation)") */
| { type: 'reset' }
WizardValidators from @llui/components/patterns/wizard
Map of zero-based step index → validator. Steps without an entry pass freely.
export type WizardValidators = Record<number, StepValidator>
Interfaces
WizardConnectOptions from @llui/components/patterns/wizard
export interface WizardConnectOptions {
label?: string
}
WizardParts from @llui/components/patterns/wizard
export interface WizardParts {
root: {
role: 'group'
'aria-label': string
'data-scope': 'steps'
'data-part': 'root'
'data-disabled': Signal<'' | undefined>
}
nextTrigger: {
type: 'button'
disabled: Signal<boolean>
'aria-busy': Signal<'true' | undefined>
'data-scope': 'steps'
'data-part': 'next-trigger'
onClick: (e: MouseEvent) => void
}
prevTrigger: {
type: 'button'
disabled: Signal<boolean>
'data-scope': 'steps'
'data-part': 'prev-trigger'
onClick: (e: MouseEvent) => void
}
/** Per-step trigger parts (item / trigger / separator), gated like raw steps. */
item: (index: number) => StepsItemParts
/** The trigger sub-part for a step index — for keyboard-complete step lists. */
stepTrigger: (index: number) => StepsItemParts['trigger']
}
WizardState from @llui/components/patterns/wizard
export interface WizardState {
/** Underlying steps machine state. */
steps: StepsState
/** Index of the step whose async validation is pending, or null when idle. */
validating: number | null
}
Constants
wizard from @llui/components/patterns/wizard
const wizard
@llui/components/patterns/command-menu
Functions
connect() from @llui/components/patterns/command-menu
Project the composed slice into dialog + combobox part bags plus a
shortcutHint accessor and an empty-state part. The dialog/combobox sends
are adapted to command-menu messages: the consumer spreads these onto
elements exactly like the base components.
function connect(state: Signal<CommandMenuState>, send: Send<CommandMenuMsg>, opts: ConnectOptions): CommandMenuParts
init() from @llui/components/patterns/command-menu
function init(opts: CommandMenuInit = {}): CommandMenuState
update() from @llui/components/patterns/command-menu
function update(state: CommandMenuState, msg: CommandMenuMsg): [CommandMenuState, CommandMenuEffect[]]
view() from @llui/components/patterns/command-menu
Default palette view: a combobox (search input + grouped command list) inside
the dialog overlay. Selecting a command dispatches execute; Escape clears
the query then closes. Consumers wanting a custom row template should drive
the part bags from connect() directly.
function view(opts: CommandMenuViewOptions): Mountable
watchHotkey() from @llui/components/patterns/command-menu
Listen for the global command-palette hotkey. Returns a cleanup function.
Call from onMount; the DOM listener never lives inside the machine.
combo is a +-joined chord, e.g. 'mod+k' (default). mod matches the
platform-conventional accelerator (⌘ on macOS, Ctrl elsewhere); since both
map to either metaKey or ctrlKey here, mod accepts either modifier.
function watchHotkey(send: Send<CommandMenuMsg>, combo: string = 'mod+k'): () => void
Types
CommandMenuEffect from @llui/components/patterns/command-menu
Effects emitted by the command-menu machine. execute is the single
agent-resolvable surface: the consumer's onEffect runs the side effect for
the picked command id (the machine never performs IO).
export type CommandMenuEffect =
/** @intent("Run the side effect for the executed command id") */
{ type: 'execute'; commandId: string }
CommandMenuMsg from @llui/components/patterns/command-menu
export type CommandMenuMsg =
/** @intent("Open the command palette") */
| { type: 'open' }
/** @intent("Close the command palette") */
| { type: 'close' }
/** @intent("Set the search query (re-runs the filter)") */
| { type: 'setQuery'; query: string }
/** @intent("Run the command with the given id, then close the palette") */
| { type: 'execute'; commandId: string }
/** @humanOnly */
| { type: 'escape' }
/** @humanOnly */
| { type: 'setCommands'; commands: Command[] }
Interfaces
Command from @llui/components/patterns/command-menu
A single palette command. JSON-serializable (no functions): execution is
surfaced as an execute effect keyed by id, never a callback in state.
export interface Command {
id: string
label: string
/** Optional group/section label. Commands without a group fall into ''. */
group?: string
/** Extra terms matched by the filter in addition to `label`. */
keywords?: string[]
/** Pre-rendered keybinding hint, e.g. 'mod+s'. Surfaced via `shortcutHint`. */
shortcut?: string
disabled?: boolean
}
CommandGroup from @llui/components/patterns/command-menu
A labelled section of filtered commands, in visual order.
export interface CommandGroup {
label: string
commands: Command[]
}
CommandMenuInit from @llui/components/patterns/command-menu
export interface CommandMenuInit {
commands?: Command[]
recents?: string[]
maxRecents?: number
open?: boolean
}
CommandMenuParts from @llui/components/patterns/command-menu
export interface CommandMenuParts {
/** Dialog parts (content/title/positioner/backdrop) for the modal shell. */
dialog: DialogParts
/** Combobox parts (root/input/content/item/group/...) for the search + list. */
combobox: ComboboxParts
/** Accessor for a command's keybinding hint (empty string when none). */
shortcutHint: (commandId: string) => Signal<string>
/** Empty-state part: `data-empty` is set when the filtered list is empty. */
empty: {
'data-scope': 'command-menu'
'data-part': 'empty'
role: 'status'
'data-empty': Signal<'' | undefined>
}
}
CommandMenuState from @llui/components/patterns/command-menu
export interface CommandMenuState {
open: boolean
query: string
commands: Command[]
/** Filtered (and recents-ranked) flat command list. */
filtered: Command[]
/** Filtered commands bucketed into groups (group order preserved). */
filteredGroups: CommandGroup[]
/** Most-recently-executed command ids, most-recent first (deduped). */
recents: string[]
/** Max recents retained for ranking. */
maxRecents: number
}
CommandMenuViewOptions from @llui/components/patterns/command-menu
export interface CommandMenuViewOptions {
state: Signal<CommandMenuState>
send: Send<CommandMenuMsg>
id: string
inputLabel?: string
/** Custom class for the content root. */
contentClass?: string
/** Accessible title for the palette dialog (default: 'Command palette'). */
title?: string
/** Empty-state text (default: 'No matching commands'). */
emptyText?: string
}
ConnectOptions from @llui/components/patterns/command-menu
export interface ConnectOptions {
/** Unique id per palette instance (used for ARIA wiring). */
id: string
}
Constants
commandMenu from @llui/components/patterns/command-menu
const commandMenu
@llui/components/patterns/data-table
Functions
connect() from @llui/components/patterns/data-table
function connect(state: Signal<DataTableState>, send: Send<DataTableMsg>, opts: ConnectOptions): DataTableParts
init() from @llui/components/patterns/data-table
function init(opts: DataTableInit = {}): DataTableState
isAllSelected() from @llui/components/patterns/data-table
function isAllSelected(state: DataTableState): boolean
isEmpty() from @llui/components/patterns/data-table
Empty == a settled (not-loading) request that yielded zero rows.
function isEmpty(state: DataTableState): boolean
isError() from @llui/components/patterns/data-table
function isError(state: DataTableState): boolean
isLoading() from @llui/components/patterns/data-table
function isLoading(state: DataTableState): boolean
totalPages() from @llui/components/patterns/data-table
function totalPages(state: DataTableState): number
update() from @llui/components/patterns/data-table
function update(state: DataTableState, msg: DataTableMsg): [DataTableState, DataTableEffect[]]
Types
DataTableEffect from @llui/components/patterns/data-table
export type DataTableEffect = LoadPageEffect
DataTableMsg from @llui/components/patterns/data-table
export type DataTableMsg =
/** @intent("Cycle the sort on the given column; resets to page 1 and reloads") */
| { type: 'toggleSort'; columnId: string }
/** @intent("Set an explicit sort (or null to clear); resets to page 1 and reloads") */
| { type: 'setSort'; sort: TableSort | null }
/** @intent("Jump to a specific 1-based page and reload") */
| { type: 'setPage'; page: number }
/** @intent("Advance to the next page and reload") */
| { type: 'nextPage' }
/** @intent("Go back to the previous page and reload") */
| { type: 'prevPage' }
/** @intent("Change the page size; preserves the first visible item and reloads") */
| { type: 'setPageSize'; pageSize: number }
/** @intent("Re-request the current page (e.g. after a failure)") */
| { type: 'reload' }
/** @humanOnly */
| { type: 'pageLoaded'; queryId: number; rows: string[]; total: number }
/** @humanOnly */
| { type: 'pageFailed'; queryId: number; error: string }
/** @intent("Toggle selection of the row with the given id at the given display index") */
| { type: 'toggleRow'; id: string; index: number }
/** @intent("Toggle between select-all and clear for the current scope") */
| { type: 'toggleAll' }
/** @intent("Clear the entire selection") */
| { type: 'clearSelection' }
/** @intent("Select the inclusive range from the current anchor to the given index (Shift+click)") */
| { type: 'selectRange'; index: number }
/** @intent("Activate (open/confirm) the row with the given id at the given index") */
| { type: 'activateRow'; id: string; index: number }
/** @humanOnly */
| { type: 'focusCell'; rowIndex: number; colIndex: number }
/** @humanOnly */
| { type: 'tableKey'; msg: TableMsg }
Interfaces
ConnectOptions from @llui/components/patterns/data-table
export interface ConnectOptions {
/** Element id base for the table (`grid`) root. */
id: string
/** Accessible label for the pagination nav. */
paginationLabel?: string
}
DataTableInit from @llui/components/patterns/data-table
export interface DataTableInit {
columns?: TableColumn[]
selectionMode?: TableSelectionMode
sort?: TableSort | null
page?: number
pageSize?: number
total?: number
siblings?: number
boundaries?: number
descFirst?: boolean
clearOnPageChange?: boolean
}
DataTableParts from @llui/components/patterns/data-table
export interface DataTableParts extends DataTableStatusParts {
table: TableParts
pagination: PaginationParts
}
DataTableState from @llui/components/patterns/data-table
DataTable — a pre-wired pattern for server-paginated, sortable, selectable
lists. It COMPOSES the headless table machine (sort / selection /
grid-keyboard), pagination (page / pageSize / total), and the async-list
status vocabulary (idle | loading | loaded | error) into a single slice.
The value of the pattern is the GLUE:
- changing the sort resets to page 1 and reloads,
- changing the page or page size reloads,
- every reload bumps a
queryIdversion counter and emits aloadPageeffect carrying{ page, pageSize, sort, queryId }; the consumer fetches and repliespageLoaded { queryId, rows, total }. The reducer DROPS anypageLoadedwhosequeryIdis stale (an older in-flight request), so a slow response can never clobber a newer one.
Row DATA stays in the consumer (the table machine only tracks row IDs in
display order). Server-side sort works for free: feed the pre-sorted IDs back
in via pageLoaded.
Usage in the consumer's onEffect:
onEffect: (effect, send) => {
if (effect.type === 'data-table:loadPage') {
const { page, pageSize, sort, queryId } = effect
fetchRows({ page, pageSize, sort })
.then(({ ids, total, data }) => {
setRowData(data)
send({ type: 'dt', msg: { type: 'pageLoaded', queryId, rows: ids, total } })
})
.catch((e) => send({ type: 'dt', msg: { type: 'pageFailed', queryId, error: String(e) } }))
}
}
export interface DataTableState {
table: TableState
pagination: PaginationState
/** Async status of the current page request. */
status: AsyncStatus
/** Error message from the last failed request, or null. */
error: string | null
/**
* Version counter for the in-flight request. Bumped on every reload; a
* `pageLoaded`/`pageFailed` whose `queryId` differs from this is stale and
* dropped.
*/
queryId: number
/**
* Selection policy. When true (default), the selection is cleared on every
* page change and select-all scopes to the current page. When false, the
* selection persists across pages (cross-page selection).
*/
clearOnPageChange: boolean
}
DataTableStatusParts from @llui/components/patterns/data-table
ARIA live-region overlay parts derived from async-list conventions.
export interface DataTableStatusParts {
/** Spinner / overlay shown while loading. `aria-busy` mirrors loading. */
loadingOverlay: {
'data-scope': 'data-table'
'data-part': 'loading-overlay'
'aria-busy': Signal<'true' | undefined>
'aria-live': 'polite'
hidden: Signal<boolean>
}
/** Empty-state region — shown when a settled request has zero rows. */
emptyState: {
role: 'status'
'aria-live': 'polite'
'data-scope': 'data-table'
'data-part': 'empty-state'
hidden: Signal<boolean>
}
/** Error-state region — shown when the last request failed. */
errorState: {
role: 'alert'
'aria-live': 'polite'
'data-scope': 'data-table'
'data-part': 'error-state'
hidden: Signal<boolean>
}
}
LoadPageEffect from @llui/components/patterns/data-table
The loadPage effect — carried out of update, fulfilled by the consumer.
export interface LoadPageEffect {
type: 'data-table:loadPage'
page: number
pageSize: number
sort: TableSort | null
queryId: number
}
Constants
dataTable from @llui/components/patterns/data-table
const dataTable
@llui/components/patterns/searchable-select
Functions
connect() from @llui/components/patterns/searchable-select
function connect(state: Signal<SearchableSelectState>, send: Send<SearchableSelectMsg>, opts: ConnectOptions): SearchableSelectParts
init() from @llui/components/patterns/searchable-select
function init(opts: SearchableSelectInit = {}): SearchableSelectState
overlay() from @llui/components/patterns/searchable-select
function overlay(opts: OverlayOptions): Mountable
update() from @llui/components/patterns/searchable-select
function update(state: SearchableSelectState, msg: SearchableSelectMsg): [SearchableSelectState, never[]]
Types
AsyncStatus from @llui/components/patterns/searchable-select
export type AsyncStatus = 'idle' | 'loading' | 'loaded' | 'error'
SearchableSelectMsg from @llui/components/patterns/searchable-select
export type SearchableSelectMsg =
/** @intent("Open the searchable select popup") */
| { type: 'open' }
/** @intent("Close the popup (resets the filter)") */
| { type: 'close' }
/** @intent("Set the filter text (re-runs the item filter; never commits a value)") */
| { type: 'setFilter'; value: string }
/** @intent("Select the option with the given value (toggles in multi-select)") */
| { type: 'selectValue'; value: string }
/** @intent("Replace the selected values with the provided list") */
| { type: 'setValue'; value: string[] }
/** @intent("Clear the current selection") */
| { type: 'clear' }
/** @humanOnly */
| { type: 'highlightNext' }
/** @humanOnly */
| { type: 'highlightPrev' }
/** @humanOnly */
| { type: 'highlightFirst' }
/** @humanOnly */
| { type: 'highlightLast' }
/** @humanOnly */
| { type: 'highlight'; value: string | null }
/** @intent("Select the currently-highlighted option (the only commit path from the keyboard)") */
| { type: 'selectHighlighted' }
/** @humanOnly */
| { type: 'triggerType'; char: string }
/** @humanOnly */
| { type: 'setItems'; items: string[]; disabled?: string[] }
SelectionMode from @llui/components/patterns/searchable-select
Combobox — text input paired with a filtered listbox dropdown. User types to filter items, arrow keys navigate the filtered set, Enter selects. Supports single and multiple selection.
Beyond the sync filtered listbox the machine owns three additive surfaces:
- Async option loading —
status/requestId/errortrack an in-flight fetch. The consumer debounces (e.g.@llui/effectsdebounce) and runs the fetch itself, dispatchingloadStart/loadSuccess/loadErrortagged with a monotonically-increasingrequestId. The reducer DROPS anyloadSuccess/loadErrorwhoserequestIdis not the current one, so a late response from a superseded request can never clobber fresh state. The machine owns no timers. - Option groups —
groupsmirrorselect'sSelectGroupshape exactly. The flatitemslist stays the source of truth for navigation/highlight indices; group LABELS are never options, so arrow navigation skips them. - Creatable — opt-in
allowCreate. WheninputValueis non-empty and matches no item, a synthetic create sentinel is appended tofilteredItems. Selecting it emits acreateOptionEFFECT (carrying the typed text) so the consumer owns creation; the machine never mutatesvaluefor it.
export type SelectionMode = 'single' | 'multiple'
Interfaces
ComboboxGroup from @llui/components/patterns/searchable-select
A labelled section of options (rendered like <optgroup>). items are the
option VALUES belonging to the group, in visual order. Groups are an
additive, parallel structure: the flat items list always remains the
source of truth for navigation/highlight indices and item ids — when
groups is provided without an explicit items list, init derives the
flat list by concatenating each group's items in order. A plain flat
string[] (no groups) keeps working unchanged. Group LABELS are never
options, so highlight/arrow navigation skips over them for free.
Mirrors select's SelectGroup shape exactly.
export interface ComboboxGroup {
id: string
label: string
items: string[]
}
ConnectOptions from @llui/components/patterns/searchable-select
export interface ConnectOptions {
id: string
/** aria-label for the clear button. */
clearLabel?: string
/** Text shown in the empty-state live region when no items match. */
emptyText?: string
}
OverlayOptions from @llui/components/patterns/searchable-select
export interface OverlayOptions {
/**
* Class applied to the positioner — the floating wrapper `div` this helper
* builds around the content. Needed when styling with utilities rather than
* the opt-in baseline stylesheet: it is the element that carries the
* `z-index` for the floating layer.
*
* Every `overlay()` in `components/` takes this; the pattern overlays were
* missed when it was added, so a utility-styled consumer had no way to give
* this popup a stacking context at all.
*/
positionerClass?: string
state: Signal<SearchableSelectState>
send: Send<SearchableSelectMsg>
parts: SearchableSelectParts
/** Renders the popup body (filter input + listbox). */
content: () => Renderable
/**
* Optional enter/leave transition for the searchable-select popup (from
* `@llui/transitions`). `enter` animates it in on open; `leave` defers the
* unmount until its promise resolves, giving the raw-`open` popup an exit
* animation for free. Omitted ⇒ the popup closes synchronously as before.
*
* @example searchableSelect.overlay({ state, send, parts, content, transition: fade({ duration: 120 }) })
*/
transition?: TransitionOptions
placement?: Placement
offset?: number
flip?: boolean
shift?: boolean
sameWidth?: boolean
target?: string | HTMLElement
}
SearchableSelectGroupParts from @llui/components/patterns/searchable-select
export interface SearchableSelectGroupParts {
group: {
role: 'group'
'aria-labelledby': string
'data-scope': 'searchable-select'
'data-part': 'group'
'data-group': string
}
groupLabel: {
id: string
'aria-hidden': 'true'
'data-scope': 'searchable-select'
'data-part': 'group-label'
'data-group': string
}
}
SearchableSelectInit from @llui/components/patterns/searchable-select
export interface SearchableSelectInit {
value?: string[]
items?: string[]
/** Optional labelled sections (passthrough to `combobox`). */
groups?: ComboboxGroup[]
disabledItems?: string[]
selectionMode?: SelectionMode
disabled?: boolean
/** Seed the filter with the selected label on open (default: false → empty). */
prefillFilter?: boolean
/** Trigger text when nothing is selected. */
placeholder?: string
/** Join multiple selected labels with this separator in the trigger. */
separator?: string
}
SearchableSelectItemParts from @llui/components/patterns/searchable-select
export interface SearchableSelectItemParts {
item: {
role: 'option'
id: string
'aria-selected': Signal<boolean>
'aria-disabled': Signal<'true' | undefined>
'data-state': Signal<'selected' | undefined>
'data-highlighted': Signal<'' | undefined>
'data-disabled': Signal<'' | undefined>
'data-scope': 'searchable-select'
'data-part': 'item'
'data-value': string
/** The option's live position in the filtered list (reactive — reused rows
* never report a stale index). */
'data-index': Signal<string>
onClick: (e: MouseEvent) => void
onPointerMove: (e: PointerEvent) => void
}
}
SearchableSelectParts from @llui/components/patterns/searchable-select
export interface SearchableSelectParts {
root: {
'data-scope': 'searchable-select'
'data-part': 'root'
'data-state': Signal<'open' | 'closed'>
}
/** The closed-state trigger button. Displays the selection (via
* `triggerLabel`) and opens the popup. Handles closed-trigger typeahead. */
trigger: {
type: 'button'
role: 'combobox'
'aria-haspopup': 'listbox'
'aria-expanded': Signal<boolean>
'aria-controls': string
'aria-disabled': Signal<'true' | undefined>
id: string
disabled: Signal<boolean>
'data-scope': 'searchable-select'
'data-part': 'trigger'
'data-state': Signal<'open' | 'closed'>
onClick: (e: MouseEvent) => void
onKeyDown: (e: KeyboardEvent) => void
}
/** Text to render inside the trigger: placeholder, single label, or a joined
* multi-select summary. */
triggerLabel: Signal<string>
/** Whether a selection exists (drive showing/hiding the clear button). */
hasValue: Signal<boolean>
/** The filter input rendered inside the popup, above the listbox. */
input: {
type: 'text'
role: 'combobox'
autocomplete: 'off'
'aria-autocomplete': 'list'
'aria-expanded': Signal<boolean>
'aria-controls': string
'aria-activedescendant': Signal<string | undefined>
id: string
value: Signal<string>
'data-scope': 'searchable-select'
'data-part': 'input'
onInput: (e: Event) => void
onKeyDown: (e: KeyboardEvent) => void
}
positioner: {
'data-scope': 'searchable-select'
'data-part': 'positioner'
style: string
}
content: {
role: 'listbox'
id: string
'aria-labelledby': string
'aria-busy': Signal<'true' | undefined>
'aria-multiselectable': Signal<'true' | undefined>
tabindex: -1
'data-state': Signal<'open' | 'closed'>
'data-status': Signal<AsyncStatus>
'data-scope': 'searchable-select'
'data-part': 'content'
}
/** Build the parts for an option by VALUE. The optional `index` is accepted
* for call-site convenience only — identity is value-keyed, so a reused row is
* never stale. */
item: (value: string, index?: number) => SearchableSelectItemParts
group: (id: string) => SearchableSelectGroupParts
/** Clear-selection trigger. Render only when `hasValue` is true. */
clear: {
type: 'button'
'aria-label': string
tabindex: -1
'data-scope': 'searchable-select'
'data-part': 'clear'
onClick: (e: MouseEvent) => void
}
/** Polite live region announcing the no-results / result count. */
liveRegion: {
role: 'status'
'aria-live': 'polite'
'aria-atomic': 'true'
'data-scope': 'searchable-select'
'data-part': 'live-region'
text: Signal<string>
}
/** Empty-state container (render when the filtered list is empty). */
empty: {
'data-scope': 'searchable-select'
'data-part': 'empty'
hidden: Signal<boolean>
}
}
SearchableSelectState from @llui/components/patterns/searchable-select
export interface SearchableSelectState {
/** Whether the popup is open. Mirrors `combobox.open`; kept at the top level
* so consumers can read it without reaching into the nested machine. */
open: boolean
/** The underlying combobox machine state. `value` is the source of truth for
* the selection; `inputValue` is the filter (never the committed value). */
combobox: ComboboxState
/** When opening, seed the filter with the selected label (and the consumer
* should select-all). When false (default) the filter opens empty. */
prefillFilter: boolean
/** Trigger placeholder shown when nothing is selected. */
placeholder: string
/** Separator used to join multiple selected labels in the trigger. */
separator: string
}
Constants
searchableSelect from @llui/components/patterns/searchable-select
const searchableSelect
@llui/components/styles
Functions
createVariants() from @llui/components/styles
function createVariants<V extends VariantRecord>(config: VariantConfig<V>): (props?: VariantProps<V>) => string
cx() from @llui/components/styles
Concatenate class strings, filtering falsy values.
function cx(...classes: ClassValue[]): string
Types
ClassValue from @llui/components/styles
export type ClassValue = string | false | null | undefined
ThemeToken from @llui/components/styles
export type ThemeToken = keyof ThemeTokens
VariantProps from @llui/components/styles
export type VariantProps<V extends VariantRecord> = {
[K in keyof V]?: keyof V[K]
}
VariantRecord from @llui/components/styles
export type VariantRecord = Record<string, Record<string, string>>
Interfaces
ThemeTokens from @llui/components/styles
export interface ThemeTokens extends ThemeBaseTokens, ThemeDerivedTokens, ThemeScaleTokens {}
VariantConfig from @llui/components/styles
export interface VariantConfig<V extends VariantRecord> {
base: string
variants: V
defaultVariants?: { [K in keyof V]?: keyof V[K] }
compoundVariants?: Array<{ [K in keyof V]?: keyof V[K] } & { class: string }>
}