@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

  1. init(opts?) -- creates the initial state
  2. update(state, msg) -- pure reducer, returns [newState, effects[]]
  3. connect(state, send, opts?) -- takes the sliced Signal handle; returns parts objects with reactive props, ARIA attributes, and event handlers. Spread parts onto your elements: div({ ...parts.root }, [...])
  4. 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


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()


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()


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()


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()


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 the undefined by OMITTING the key, so the state shape IS a NumericGrid and round-trips identically.
  • INTRINSICALLY BOUNDED (angle-slider, slider, splitter, …): a required min: number cannot spell "unbounded", so ?? DEFAULT at init and REJECT the write in a setMin/setMax reducer — 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 stores dir in its own State and passes it in);
  • an Element — direction is resolved by walking up the DOM (dir="rtl" ancestor or document.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>
function menubarConnect(state: Signal<MenubarState>, send: Send<MenubarMsg>, opts: ConnectOptions): MenubarParts
function menubarInit(opts: MenubarInit): MenubarState

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
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:

  1. inert cannot be split from aria-hidden — sparing a region spares its WHOLE SUBTREE from both, so an interactive live region (a role="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.
  2. document.querySelectorAll does not pierce shadow roots, so a live region inside one is not exempt. The sweep itself only ever walks light-DOM ancestors of target, 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 element or ignore.
  • 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' }
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 }
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 }
export type MenuCheckItemParts = MenuCheckItemPartsOf<'menu'>
export type MenuGroupParts = MenuGroupPartsOf<'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

Kind of a menu item.

export type MenuItemKind = MenuNodeKind
export type MenuItemParts = MenuItemPartsOf<'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' }
export type MenuSeparatorParts = MenuSeparatorPartsOf<'menu'>
export type MenuSubContentParts = MenuSubContentPartsOf<'menu'>
export type MenuSubPositionerParts = MenuSubPositionerPartsOf<'menu'>
export type MenuSubTriggerParts = MenuSubTriggerPartsOf<'menu'>
MeterMsg from @llui/components
export type MeterMsg =
  /** @humanOnly */
  | { type: 'setValue'; value: number }
  /** @humanOnly */
  | { type: 'setMax'; max: number }
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:

  1. close the overlay (fire exit animation)
  2. keep it mounted long enough for the animation to finish
  3. 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.toaster state manages a collection of toasts. Each toast carries its own countdown in state: duration (null = sticky), remainingMs, and paused.
  • The machine owns NO interval. The consumer drives the countdown with a tick(id, elapsedMs) message (e.g. via @llui/effects interval), subtracting the elapsed wall time since the last tick. A paused toast freezes its remainingMs (ticks are ignored).
  • When remainingMs hits 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; an animationEnd(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
}

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
}
export interface BreadcrumbsInit {
  items?: BreadcrumbItem[]
  maxVisible?: number | null
  expanded?: boolean
}
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
  }
}
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 as control['aria-labelledby'] for CUSTOM controls (combobox, listbox, etc.) that aren't a native labellable element
  • control['aria-describedby'] references the description id whenever a description is rendered, and ADDS the error id only while invalid
  • errorText is 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
}
export interface MenubarInit {
  menus: MenubarMenu[]
  /** Initially-focused menu id (defaults to the first enabled menu). */
  focused?: string | null
}

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
}
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
}
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
}
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>
}
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
}
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
}
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
}
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
}
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 attributes and drive the threshold styling exposed via data-state.

export interface MeterState {
  value: number
  min: number
  max: number
  low?: number
  high?: number
  optimum?: number
}
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
  }
}
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'
}
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
}

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 element (consumer owns the canvas drawing, typically by redrawing all strokes in an onMount effect whenever state.strokes changes, or by drawing incrementally on each addPoint message).

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
const breadcrumbs
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
const menu
const menubar
const menubarMachine
meter from @llui/components
const meter
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_MS of 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 the undefined by OMITTING the key, so the state shape IS a NumericGrid and round-trips identically.
  • INTRINSICALLY BOUNDED (angle-slider, slider, splitter, …): a required min: number cannot spell "unbounded", so ?? DEFAULT at init and REJECT the write in a setMin/setMax reducer — 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 stores dir in its own State and passes it in);
  • an Element — direction is resolved by walking up the DOM (dir="rtl" ancestor or document.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:

  1. inert cannot be split from aria-hidden — sparing a region spares its WHOLE SUBTREE from both, so an interactive live region (a role="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.
  2. document.querySelectorAll does not pierce shadow roots, so a live region inside one is not exempt. The sweep itself only ever walks light-DOM ancestors of target, 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 element or ignore.
  • 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_MS of 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:

  1. inert cannot be split from aria-hidden — sparing a region spares its WHOLE SUBTREE from both, so an interactive live region (a role="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.
  2. document.querySelectorAll does not pierce shadow roots, so a live region inside one is not exempt. The sweep itself only ever walks light-DOM ancestors of target, 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 stores dir in its own State and passes it in);
  • an Element — direction is resolved by walking up the DOM (dir="rtl" ancestor or document.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 the undefined by OMITTING the key, so the state shape IS a NumericGrid and round-trips identically.
  • INTRINSICALLY BOUNDED (angle-slider, slider, splitter, …): a required min: number cannot spell "unbounded", so ?? DEFAULT at init and REJECT the write in a setMin/setMax reducer — 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 stores dir in its own State and passes it in);
  • an Element — direction is resolved by walking up the DOM (dir="rtl" ancestor or document.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:

  1. inert cannot be split from aria-hidden — sparing a region spares its WHOLE SUBTREE from both, so an interactive live region (a role="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.
  2. document.querySelectorAll does not pierce shadow roots, so a live region inside one is not exempt. The sweep itself only ever walks light-DOM ancestors of target, 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 element or ignore.
  • 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_MS of 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 element or ignore.
  • 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 the undefined by OMITTING the key, so the state shape IS a NumericGrid and round-trips identically.
  • INTRINSICALLY BOUNDED (angle-slider, slider, splitter, …): a required min: number cannot spell "unbounded", so ?? DEFAULT at init and REJECT the write in a setMin/setMax reducer — 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_MS of 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

export type MenuCheckItemParts = MenuCheckItemPartsOf<'menu'>
export type MenuGroupParts = MenuGroupPartsOf<'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

Kind of a menu item.

export type MenuItemKind = MenuNodeKind
export type MenuItemParts = MenuItemPartsOf<'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' }
export type MenuSeparatorParts = MenuSeparatorPartsOf<'menu'>
export type MenuSubContentParts = MenuSubContentPartsOf<'menu'>
export type MenuSubPositionerParts = MenuSubPositionerPartsOf<'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
}
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
}
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
}
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
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.toaster state manages a collection of toasts. Each toast carries its own countdown in state: duration (null = sticky), remainingMs, and paused.
  • The machine owns NO interval. The consumer drives the countdown with a tick(id, elapsedMs) message (e.g. via @llui/effects interval), subtracting the elapsed wall time since the last tick. A paused toast freezes its remainingMs (ticks are ignored).
  • When remainingMs hits 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; an animationEnd(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 loadingstatus/requestId/error track an in-flight fetch. The consumer debounces (e.g. @llui/effects debounce) and runs the fetch itself, dispatching loadStart/loadSuccess/loadError tagged with a monotonically-increasing requestId. The reducer DROPS any loadSuccess/loadError whose requestId is not the current one, so a late response from a superseded request can never clobber fresh state. The machine owns no timers.
  • Option groupsgroups mirror select's SelectGroup shape exactly. The flat items list stays the source of truth for navigation/highlight indices; group LABELS are never options, so arrow navigation skips them.
  • Creatable — opt-in allowCreate. When inputValue is non-empty and matches no item, a synthetic create sentinel is appended to filteredItems. Selecting it emits a createOption EFFECT (carrying the typed text) so the consumer owns creation; the machine never mutates value for 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:

  1. close the overlay (fire exit animation)
  2. keep it mounted long enough for the animation to finish
  3. 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 element (consumer owns the canvas drawing, typically by redrawing all strokes in an onMount effect whenever state.strokes changes, or by drawing incrementally on each addPoint message).

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

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
}
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
  }
}
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'
}
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
}

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

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 s.

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 swipeThreshold targets 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

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 as control['aria-labelledby'] for CUSTOM controls (combobox, listbox, etc.) that aren't a native labellable element
  • control['aria-describedby'] references the description id whenever a description is rendered, and ADDS the error id only while invalid
  • errorText is 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 semantics:

  • 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 containing optimum is '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'. When low/high are missing the value is considered to be in the middle segment; when optimum is 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 attributes and drive the threshold styling exposed via 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

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

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
}
export interface BreadcrumbsInit {
  items?: BreadcrumbItem[]
  maxVisible?: number | null
  expanded?: boolean
}
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
  }
}
export interface BreadcrumbsState {
  items: BreadcrumbItem[]
  maxVisible: number | null
  expanded: boolean
}
ConnectOptions from @llui/components/breadcrumbs
export interface ConnectOptions {
  label?: string
  expandLabel?: string
}

Constants

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

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
}
export interface MenubarInit {
  menus: MenubarMenu[]
  /** Initially-focused menu id (defaults to the first enabled menu). */
  focused?: string | null
}

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
}
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
}
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
}
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>
}
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

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 loadingstatus/requestId/error track an in-flight fetch. The consumer debounces (e.g. @llui/effects debounce) and runs the fetch itself, dispatching loadStart/loadSuccess/loadError tagged with a monotonically-increasing requestId. The reducer DROPS any loadSuccess/loadError whose requestId is not the current one, so a late response from a superseded request can never clobber fresh state. The machine owns no timers.
  • Option groupsgroups mirror select's SelectGroup shape exactly. The flat items list stays the source of truth for navigation/highlight indices; group LABELS are never options, so arrow navigation skips them.
  • Creatable — opt-in allowCreate. When inputValue is non-empty and matches no item, a synthetic create sentinel is appended to filteredItems. Selecting it emits a createOption EFFECT (carrying the typed text) so the consumer owns creation; the machine never mutates value for 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 queryId version counter and emits a loadPage effect carrying { page, pageSize, sort, queryId }; the consumer fetches and replies pageLoaded { queryId, rows, total }. The reducer DROPS any pageLoaded whose queryId is 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 queryId version counter and emits a loadPage effect carrying { page, pageSize, sort, queryId }; the consumer fetches and replies pageLoaded { queryId, rows, total }. The reducer DROPS any pageLoaded whose queryId is 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 loadingstatus/requestId/error track an in-flight fetch. The consumer debounces (e.g. @llui/effects debounce) and runs the fetch itself, dispatching loadStart/loadSuccess/loadError tagged with a monotonically-increasing requestId. The reducer DROPS any loadSuccess/loadError whose requestId is not the current one, so a late response from a superseded request can never clobber fresh state. The machine owns no timers.
  • Option groupsgroups mirror select's SelectGroup shape exactly. The flat items list stays the source of truth for navigation/highlight indices; group LABELS are never options, so arrow navigation skips them.
  • Creatable — opt-in allowCreate. When inputValue is non-empty and matches no item, a synthetic create sentinel is appended to filteredItems. Selecting it emits a createOption EFFECT (carrying the typed text) so the consumer owns creation; the machine never mutates value for 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 }>
}