@llui/components

Current package version: 0.21.0

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

The product contract lists 95 components. @llui/components/<name> imports a component's headless machine (behaviour, state, ARIA) when it has one; llui add <name> copies styled source into your app. They are different artifacts, and a component may have either, both, or names that differ between the two.

Kind Components Headless import Copied by llui add
machine 69 69 64
pattern 6 6 3
presentational 19 none, by design 19
skin, app-owned state 1 none, by design 1
all 95 75 89 add names, 6 aliases among them

Every component, with its import, add names, styling support and a stable Component Gallery link, is in the component catalog.

75 headless machine and pattern subpaths, each imported as @llui/components/<subpath>, grouped by category:

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. Two independent styling paths are available; choose one per app. Styling is the full guide.

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

This is ordinary published CSS. It requires no Tailwind dependency, plugin, configuration, or preprocessing step, and it includes dark mode, reduced-motion and forced-colors handling. Override semantic tokens in plain CSS:

:root {
  --primary: oklch(0.55 0.21 258);
  --llui-radius-lg: 1rem;
}

For a smaller bundle, import the modules individually — Styling → entry points lists every stylesheet in dependency order, generated from this package's exports. Upgrading from an older release? See the migration guide.

Registry skins — copied source, Tailwind v4

Class recipes do not ship in this package. They are registry source you own and edit, copied into your project by @llui/cli and verified against a real Tailwind v4 build:

npx @llui/cli add tabs

llui add <name> copies a skin, pattern adapter or presentational component; this package's @llui/components/<name> is the headless machine the skin spreads. The two are different artifacts and their names can differ — the component catalog lists both for every component. Import tokens.css and tokens-dark.css in your Tailwind stylesheet — not theme.css, whose unlayered baseline rules would override every recipe.

Variant engine

createVariants builds a typed variant-to-class function for your own recipes (the registry's copied recipes use it too):

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 69 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[]
closing string[]
exitGenerations RetainedExitGeneration<string>[]
exitSequence number
animated boolean

Messages: setItems, focusNext, focusPrev, focusFirst, focusLast, exitComplete

Init options: value?: string[], multiple?: boolean, collapsible?: boolean, disabled?: boolean, items?: string[], animated?: boolean

Connect options: ConnectOptions

Parts: root, exitCompletion, close, 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
hovered boolean
focusWithin boolean
direction 'forward' | 'backward'
swipeThreshold number
dragging CarouselDrag | null
dir 'ltr' | 'rtl'
dirSource DirectionSource

Messages: goTo, next, prev, setCount, pause, resume, setHovered, setFocusWithin, setAutoplay, autoplayTick, dragStart, dragMove, dragEnd, setDir, syncDomDir

Init options: current?: number, count?: number, loop?: boolean, autoplay?: boolean, interval?: number, swipeThreshold?: number, dir?: 'ltr' | 'rtl'

Connect options: ConnectOptions

Parts: root, viewport, track, indicatorGroup, nextTrigger, prevTrigger, slide, directionSync

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


Chart

State (ChartState):

Field Type
coord ChartCoord
domain ChartDomain
series ChartSeries[]
rows ChartRow[]
label string
description string
width number
height number
padding ChartPadding
min number
max number
tickCount number
stacked boolean
activeIndex number | null
activeSeries string | null
innerRadius number
horizontal boolean

Messages: setCoord, setDomain, setActive, moveActive, firstActive, lastActive, setActiveSeries, setRows, setMin, setMax, setStacked, setSize, setInnerRadius, setHorizontal

Init options: series: readonly ChartSeries[], rows?: readonly ChartRow[], coord?: ChartCoord, domain?: ChartDomain, label?: string, description?: string, width?: number, height?: number, padding?: Partial<ChartPadding>, min?: number, max?: number, tickCount?: number, stacked?: boolean, innerRadius?: number, horizontal?: boolean

Connect options: ChartConnectOptions

Parts: root, svg, title, desc, table, tooltip, layer, grid, axisLabel, dotProps, legendItem, legendSwatch, markProps, marks, vertices, gridLines, categoryTicks, tooltipRows, activeLabel, rows, series

Utilities: geometry(), chartForcedColorPatterns()


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
failed boolean

Messages: setValue, copy, copied, copyFailed, reset

Init options: value?: string

Connect options: ConnectOptions

Parts: root, trigger, input, indicator

Utilities: copyToClipboard()


Collapsible

State (CollapsibleState):

Field Type
open boolean
disabled boolean
closing boolean
exitGeneration number
animated boolean

Messages: exitComplete

Init options: open?: boolean, disabled?: boolean, animated?: boolean

Connect options: ConnectOptions

Parts: root, trigger, content, exitCompletion, close


Color Picker

State (ColorPickerState):

Field Type
color `| { model: 'hsv'; h: number; s: number; v: number }
\| { model: 'oklch'; l: number; c: number; h: number }` |

| alpha | number | | disabled | boolean | | maxChroma | number | | eyeDropperSupported | boolean |

Messages: setModel, setHsl, setHue, setSaturation, setLightness, setAlpha, setHex, setSv, nudgeSv, setColor, setOklch, setChroma, setOklchLightness, setLc, nudgeLc, setEyeDropperSupported, eyeDropperFailed

Init options: model?: ColorModel, color?: string, hsl?: Hsl, hsv?: Hsv, oklch?: Oklch, alpha?: number, disabled?: boolean, maxChroma?: number

Connect options: ConnectOptions

Parts: root, modelToggle, hueSlider, saturationSlider, lightnessSlider, chromaSlider, oklchLightnessSlider, hexInput, preview, area, areaCanvas, areaThumb, alphaSlider, eyeDropperTrigger, swatchGroup, swatch

Utilities: stateHsl(), stateHsv(), stateOklch(), isOutOfGamut(), toHex(), toHex8(), toCss(), colorFromPoint(), lcFromPoint(), hsvToOklchPreserving(), oklchToHsvPreserving(), supportsEyeDropper(), openEyeDropper(), oklchPlanePixels(), paintOklchPlane(), oklchHueRampGradient(), hslToHsv(), hsvToHsl(), sanitizeHsv(), sanitizeOklch(), pickerColorToCssColor(), cssColorToPickerColor(), pickerColorToCss(), eyeDropperSupportMount(), areaCanvasBinding()

Constants: DEFAULT_MAX_CHROMA


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, loadState, item, group, liveRegion, empty

Utilities: overlay(), isCreateOption(), loadProjection()

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, syncDomDir, animationEnd

Init options: items?: ContextMenuItem[], checked?: string[], closeOnSelect?: boolean, dir?: TextDirection, skipAnimations?: boolean

Connect options: ConnectOptions

Parts: trigger, positioner, content, item, checkboxItem, radioItem, group, separator, subTrigger, subPositioner, subContent, directionSync

Utilities: overlay(), subOverlay(), 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
today string | null
unavailable string[]

Messages: setValue, setRange, setFocused, setHover, clearHover, prevMonth, nextMonth, prevYear, nextYear, selectFocused, moveFocus, focusStartOfWeek, focusEndOfWeek, focusToday, moveFocusMonths, clear, setToday, setUnavailable

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, today?: string | null, unavailable?: string[]

Connect options: ConnectOptions

Parts: root, grid, row, prevMonthTrigger, nextMonthTrigger, dayCell, preset

Utilities: monthGrid(), weekRows(), monthLabel(), weekdayLabels(), todayInTimeZone()


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
uploads Record<string, FileUploadProgress>

Messages: setFiles, addFiles, removeFile, removeRejected, clear, clearRejected, dragEnter, dragLeave, drop, setInvalid, uploadProgress, uploadSucceeded, uploadFailed, retryUpload

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(), uploadOf(), 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, moveBy, resizeBy

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


Gradient Picker

State (GradientPickerState):

Field Type
kind GradientKind
repeating boolean
direction GradientDirection
conicAngle number
center GradientCenter
shape RadialShape
size RadialSize
interpolation GradientInterpolation
stops GradientStop[]
selectedId string
nextId number
defaultModel ColorModel
maxChroma number
minStops number
maxStops number
disabled boolean
dir 'ltr' | 'rtl'
eyeDropperSupported boolean
cssDraft string | null
cssError string | null

Messages: addStop, removeStop, selectStop, moveStop, nudgeStop, setKind, setRepeating, setAngle, setCenter, setShape, setSize, setInterpolation, reverse, distribute, setGradientDraft, setGradient, picker, selectNextStop, selectPrevStop, setDir

Init options: css?: string, stops?: GradientStopInit[], kind?: GradientKind, repeating?: boolean, angle?: number, center?: Partial<GradientCenter>, shape?: RadialShape, size?: RadialSize, interpolation?: Partial<GradientInterpolation>, defaultModel?: ColorModel, maxChroma?: number, minStops?: number, maxStops?: number, disabled?: boolean, dir?: 'ltr' | 'rtl'

Connect options: ConnectOptions

Parts: root, preview, track, stop, addStopButton, removeStopButton, kindToggle, repeatingToggle, angleInput, centerArea, centerThumb, shapeOption, sizeOption, interpolationSpaceSelect, interpolationHueSelect, reverseButton, distributeButton, cssInput, cssError, picker

Utilities: toCss(), colorAt(), parseGradient(), pickerStateOf()


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, nudge, zoom

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

Messages: open, close, toggle, highlight, highlightNext, highlightPrev, highlightFirst, highlightLast, selectHighlighted, select, openSub, closeSub, setItems, typeahead, setDir, syncDomDir, animationEnd

Init options: open?: boolean, items?: MenuItem[], highlighted?: string | null, checked?: string[], closeOnSelect?: boolean, dir?: TextDirection, skipAnimations?: boolean

Connect options: ConnectOptions

Parts: trigger, positioner, content, item, checkboxItem, radioItem, group, separator, subTrigger, subPositioner, subContent, directionSync

Utilities: overlay(), subOverlay(), isPresent(), isMounted()


State (MenubarState):

Field Type
menus string[]
open string | null
focused string | null
disabledMenus string[]
menuStates Record<string, MenuState>
dir TextDirection
dirSource DirectionSource

Messages: openMenu, closeMenu, focusMenu, syncTriggerFocus, focusNext, focusPrev, menuMsg, setDir, syncDomDir

Init options: menus: MenubarMenu[], focused?: string | null, dir?: TextDirection

Connect options: ConnectOptions

Parts: root, menuTrigger, menu, directionSync

Utilities: overlay(), subOverlay()


Meter

State (MeterState):

Field Type
value number
min number
max number
bands MeterBand[]

Messages: setValue, setMax, setBands

Init options: value?: number, min?: number, max?: number, low?: number, high?: number, optimum?: number, bands?: readonly MeterBandInit[]

Connect options: ConnectOptions

Parts: root, track, range, marker, label, bands, bandProps, band, valueText, bandLabel

Utilities: percent(), bandAt(), toneAt()


State (NavMenuState):

Field Type
open string[]
focused string | null
items string[]
disabled boolean
dir 'ltr' | 'rtl'
dirSource DirectionSource

Messages: openBranch, closeBranch, toggleBranch, closeAll, focus, setDir, syncDomDir, setItems

Init options: open?: string[], focused?: string | null, items?: string[], disabled?: boolean, dir?: 'ltr' | 'rtl'

Connect options: ConnectOptions

Parts: root, indicator, directionSync, item

Utilities: isOpen(), watchNavMenuIndicator()


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
dirSource DirectionSource

Messages: goTo, next, prev, first, last, setPageSize, setTotal, setDir, syncDomDir

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, directionSync

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
cleared Stroke[] | null

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


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
count number
dragging DragState | null
announcement SortableAnnouncement | null

Messages: start, move, drop, cancel, toggleGrab, moveBy

Connect options: ConnectOptions

Parts: liveRegion, instructions, root, item, handle

Utilities: reorder(), droppedMove()


Sparkline

State (SparklineState):

Field Type
points SparklinePoint[]
band SparklineBand
now number
min number
max number
width number
height number
padding SparklinePadding
curve Curve
trim SparklineTrim | null
track SparklineTrack | null
calendar SparklineCalendar
activeIndex number | null

Messages: setActive, moveActive, firstActive, lastActive, setPoints, setNow, setBand, setSize

Init options: points?: readonly SparklinePoint[], band?: SparklineBand, now?: number, min?: number, max?: number, width?: number, height?: number, padding?: Partial<SparklinePadding>, curve?: Curve, trim?: boolean | Partial<SparklineTrim>, track?: SparklineTrack | null, calendar?: Partial<SparklineCalendar>

Connect options: SparklineConnectOptions

Parts: root, svg, title, desc, table, band, line, now, layer, tickProps, dotProps, spanProps, tooltip, ticks, dots, spans, rows, activeDot, label, summary

Utilities: geometry(), sparklineGeometry(), createSparklineGeometry(), locateIndex(), trimLeadingOutliers(), sparklineLabel(), isoDay()


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: viewport, 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'
dirSource DirectionSource

Messages: setValue, setItems, focusTab, activateTab, focusNext, focusPrev, focusFirst, focusLast, activateFocused, setDir, syncDomDir

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, directionSync

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, setPlacement, 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(), isPaused()


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

addUnits() from @llui/components

Advance an instant by n whole units, in the offset's calendar.

function addUnits(at: number, unit: CalendarUnit, n: number, opts: CalendarOptions = {}): number
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>
annularSectorPath() from @llui/components

An annular sector — the polar answer to a bar. r0 is the inner radius (0 gives a pie wedge, > 0 a donut segment); angles follow {@link polarPoint}.

A sweep of a full turn or more is drawn as a complete ring, because the arc endpoints would otherwise coincide and SVG would draw nothing at all.

function annularSectorPath(cx: number, cy: number, r0: number, r1: number, a0: number, a1: number): string
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

Publish the user's PREFERENCE on <html>: data-theme="light" / "dark" for an explicit choice, and the attribute REMOVED for 'system'.

Removing it is not "doing nothing" — it is how 'system' is spelled, and tokens-dark.css answers all three states from that one attribute:

@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme='light'])):where(:not(.light)) { … }  // absent -> OS
}
.dark,
[data-theme='dark'] { … }                             // "dark"   -> dark

with :root in tokens.css as the light default. So an absent attribute follows the OS in CSS, and data-theme="light" opts a subtree back out of the media query.

This takes a {@link Theme}, not a {@link ResolvedTheme}, deliberately (#233). Three costs to resolving 'system' in JS and pinning the answer, none of which the CSS route pays:

  • watchSystemTheme becomes MANDATORY to track OS changes the media query would have followed for free;
  • SSR has no correct attribute to render, since the server cannot know the client's prefers-color-scheme;
  • the attribute stops naming the PREFERENCE. data-theme="dark" then means either "the user chose dark" or "the user chose system and the OS is dark", and nothing on the page can tell them apart.

It does NOT cost a first-paint flash — a claim worth not repeating, because it is false and easy to check: resolveTheme('system') reads the same media query the first paint used, so the attribute it writes agrees with what is already on screen.

Use resolveTheme() only when you genuinely need the resolved value IN JS — canvas theming, a <meta name="theme-color"> — never merely to honour the OS setting.

THIS FUNCTION DELIBERATELY WRITES NO .dark CLASS (#242), and that is the decision, not an omission. tokens-dark.css still ships a .dark selector for consumers whose own tooling writes it (next-themes, an SSR-rendered class), but maintaining it HERE was rejected on measurement: .dark names a RESOLVED theme, so a 'system' preference would have to resolve prefers-color-scheme in JS, and the class then goes stale the moment the OS flips. A stale class does not merely fail to help — it SUPPRESSES the media query that is correct with no JS at all. Measured in Chromium against the shipped stylesheets, with the preference on 'system' throughout:

  os=light class=(none) -> LIGHT ok        os=dark class=(none) -> DARK  ok
  os=light class=dark   -> DARK  !!WRONG!! os=dark class=dark   -> DARK  ok
  os=light class=light  -> LIGHT ok        os=dark class=light  -> LIGHT !!WRONG!!

So the class buys one cell (a consumer's .dark-scoped override under 'system' on a dark OS) at the price of making watchSystemTheme mandatory, leaving SSR a class it cannot render, and turning a zero-JS-correct cell into one that is wrong until JS runs. The supported answer for that cell is a CSS twin block under the media guard — see site/content/styling.md.

The media guard quoted above is :where()-wrapped so it is specificity (0,1,0), not (0,3,0) (#241). At (0,3,0) it outranked EVERY consumer override of a base token on a dark OS, including [data-theme='dark'] under an explicit 'dark' preference, where the consumer's selector matches and still lost. Do not "simplify" the :where()s away, and do not wrap :root itself — that is (0,0,0) and loses to tokens.css's own :root, breaking dark mode outright.

function applyTheme(theme: Theme): void
areaPath() from @llui/components

A filled region between an upper run and a lower run. The lower run is walked BACKWARDS and appended, then the path is closed — one subpath, so a fill-rule cannot punch a hole in it the way two separate subpaths can.

function areaPath(upper: readonly Point[], lower: readonly Point[], curve: Curve, axis: CurveAxis = 'x'): string
attachFloating() from @llui/components

Position floating relative to anchor with live updates on scroll/resize. Owns position, top, left, transform and the two available-size custom properties ({@link FLOATING_AVAILABLE_HEIGHT}) on floating; position and all four physical inset properties on an optional arrow; and placement attributes on stateTarget. The arrow's static-side inset is half its untransformed layout size, so a square arrow straddles the resolved edge. Cleanup is idempotent, suppresses pending writes/callbacks, and restores the exact prior values (including priority) or absence of those properties.

export declare function attachFloating(opts: FloatingOptions): () => void;
bandCenter() from @llui/components

The centre of band i in normalized u.

function bandCenter(i: number, band: Band): number
bandExtent() from @llui/components

The [start, end] extent of band i in normalized u.

function bandExtent(i: number, band: Band): [number, number]
calendarTicks() from @llui/components

Calendar boundaries inside [start, end], ascending.

An EMPTY array for a non-finite or reversed range, and for a request that would exceed {@link MAX_CALENDAR_TICKS} — a chart with no data is a normal state, and a truncated axis is a lie.

function calendarTicks(start: number, end: number, opts: CalendarStepOptions = {}): CalendarTick[]
cartesianProjection() from @llui/components
function cartesianProjection(frame: Frame, opts: CartesianOptions = {}): Projection
chooseCalendarStep() from @llui/components

The finest rung whose boundaries fit inside maxTicks.

A FORCED unit is honoured as given (stride 1), except on year, where the stride still grows to fit — "yearly" over four centuries has to mean something, and 400 gridlines is not it.

function chooseCalendarStep(start: number, end: number, opts: CalendarStepOptions = {}): CalendarStep
circlePath() from @llui/components

A full circle, as two arcs — one arc of 360° is degenerate and draws nothing, which is the classic way a polar gridline disappears.

function circlePath(cx: number, cy: number, r: number): string
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
countCalendarTicks() from @llui/components

How many boundaries of step fall inside [start, end] — WITHOUT building them. Constant time at every rung, which is what lets the ladder reject a finer unit for a century-long span for free.

One informational edge, in the safe direction: within a month of the extremes of the representable range (year ±271821), flooring to a period start falls outside what a Date can hold and {@link utcAt} answers NaN, so the count is 0 and {@link calendarTicks} draws no gridlines. It neither hangs nor throws; the axis is simply unlabelled at the end of time.

function countCalendarTicks(start: number, end: number, step: CalendarStep, opts: CalendarOptions = {}): number
cssColorAlpha() from @llui/components

Resolved alpha (none -> 1, CSS Color 4's used value for a missing alpha OUTSIDE interpolation). Inside {@link interpolateColor}, a missing alpha instead carries the OTHER endpoint's alpha — see its doc comment.

function cssColorAlpha(c: CssColor): number
cssColorToHex() from @llui/components

Gamut-map (if needed) and convert to a displayable hex string. #rrggbb when alpha is (resolved to) 1, #rrggbbaa otherwise.

function cssColorToHex(c: CssColor): string
cssColorToSrgb() from @llui/components

Convert any {@link CssColor} into fully-resolved (non-null) Srgb, treating none as 0 and NOT gamut-mapping (an oklch/oklab input outside sRGB stays outside [0,1]).

function cssColorToSrgb(c: CssColor): Srgb
curvePath() from @llui/components

Join a run of points with the named curve. closed applies to linear only — the other two are defined on an open, x-increasing run.

function curvePath(points: readonly Point[], curve: Curve, closed = false, axis: CurveAxis = 'x'): string
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
denormalize() from @llui/components

Inverse of {@link normalize}.

function denormalize(u: number, domain: Domain): 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;
floorToUnit() from @llui/components

Floor an instant to the start of its unit, in the offset's calendar.

function floorToUnit(at: number, unit: CalendarUnit, opts: CalendarOptions = {}): number
fmt() from @llui/components

Format a coordinate. A non-finite input becomes 0 — a NaN in a path silently voids the WHOLE path element, taking every later command with it.

function fmt(n: number): 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
formatHex() from @llui/components
function formatHex(rgb: Rgb255): string
formatHex8() from @llui/components
function formatHex8(rgb: Rgb255, alpha: number): string
formatList() from @llui/components
function formatList(value: string[], opts: FormatListOptions = {}): string
formatNumber() from @llui/components
function formatNumber(value: number, opts: FormatNumberOptions = {}): string
formatOklch() from @llui/components

oklch(L C H) / oklch(L C H / A). Fixed precision: L as a 0–1 fraction at 4 dp, C at 4 dp, H at 2 dp — chosen so the string is stable and round-trips through {@link parseCssColor} without drifting.

function formatOklch(ok: Oklch, alpha = 1): 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
formatRgb() from @llui/components
function formatRgb(rgb: Rgb255, alpha = 1): string
formatTime() from @llui/components
function formatTime(value: DateValue, opts: FormatTimeOptions = {}): string
gamutMapOklchToSrgb() from @llui/components

CSS Color 4 §13.2 gamut mapping: reduce OKLCH chroma by binary search until simple-clipping the result into sRGB is perceptually indistinguishable (deltaEOK < the 0.02 JND) from the reduced color, then clip. Given an already-in-gamut color this returns it converted (and defensively clipped for float noise) rather than a no-op, so the result is always safe to hand straight to {@link srgbToRgb255}. test/utils/color.test.ts pins the converged chroma for a known out-of-gamut color as a regression value (this file's own binary search, not an independent oracle — see that test's comment for what it does and does not prove).

function gamutMapOklchToSrgb(ok: Oklch): Srgb
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[];
hslToHsv() from @llui/components

Convert HSL (h 0-360, s/l 0-100) to HSV (h 0-360, s/v 0-100), float.

function hslToHsv(hsl: Hsl): Hsv
hslToRgb255() from @llui/components

Convert HSL (h 0-360, s/l 0-100) to RGB (0-255 each, rounded).

function hslToRgb255(hsl: Hsl): Rgb255
hsvToHsl() from @llui/components

Convert HSV (h 0-360, s/v 0-100) to HSL (h 0-360, s/l 0-100), float.

function hsvToHsl(hsv: Hsv): Hsl
hsvToOklch() from @llui/components
function hsvToOklch(hsv: Hsv): Oklch
hsvToRgb255() from @llui/components

HSV -> RGB255, via the DIRECT (not HSL-routed) float conversion.

function hsvToRgb255(hsv: Hsv): Rgb255
icon() from @llui/components

Render one Iconify glyph as a real <svg>.

name is prefix:name ('lucide:star', 'simple-icons:github'; a bare name is a Lucide glyph) or a ReadSignal<string> of one, in which case the glyph follows the signal. props spread onto the <svg>; class is passed through as given, so a caller sizes and colours the glyph from there.

function icon(name: string | ReadSignal<string>, props?: ElProps): Mountable
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>
inSrgbGamut() from @llui/components

Whether ok, converted to sRGB, falls inside [0,1] on every channel (within epsilon — floating-point round-trip noise).

function inSrgbGamut(ok: Oklch, epsilon = 1e-4): boolean
interpolateColor() from @llui/components

Interpolate between two colors per CSS Color 4 §12: convert both into space, resolve missing (none, or a cross-space-converted achromatic color's undefined) components — including alpha itself, which carries the OTHER endpoint's alpha when missing rather than defaulting to 1 (CSS Color 4 §12.2's analogous-component rule applies to alpha too — rgb(255 0 0 / none) mixed with rgb(0 0 255 / 0.2) has alpha exactly 0.2 throughout, not a lerp toward 1; pinned in test/utils/color.test.ts) — premultiply non-hue channels by alpha, lerp, then un-premultiply. hueMethod controls which way a hue-bearing space (hsl, oklch) rotates, normalizing both hues to [0,360) FIRST (see {@link adjustHue}); ignored for spaces with no hue.

function interpolateColor(a: CssColor, b: CssColor, t: number, space: InterpolationSpace, hueMethod: HueInterpolationMethod = 'shorter'): CssColor
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
linearPath() from @llui/components

Straight segments through every point.

function linearPath(points: readonly Point[], closed = false): string
linearSrgbToOklab() from @llui/components
function linearSrgbToOklab(s: Srgb): Oklab
linearToSrgb() from @llui/components
function linearToSrgb(s: Srgb): Srgb
loadIcon() from @llui/components

Resolve one glyph, batched: one HTTP request per PREFIX per tick, not one per icon. A page rendering a dozen chevrons asks Iconify once; the batch is flushed on a macrotask so every icon mounted during the same view build joins it. Exposed so a consumer can warm the cache ahead of a mount.

function loadIcon(name: string): Promise<IconData | 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: ReadSignal<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[]]
monotonePath() from @llui/components

Monotone cubic interpolation (Fritsch–Carlson). Its defining property is that the curve NEVER OVERSHOOTS the data: between two samples it stays within their values, so a series that only rises cannot dip below a point on the way. A plain Catmull-Rom or cardinal spline does overshoot, and on a chart that means drawing a number nobody measured.

Defined on a function y = f(x) with strictly increasing x. It is therefore meaningless on a closed angular loop, which is why polarProjection declines it — see projection.ts.

function monotonePath(points: readonly Point[], axis: CurveAxis = 'x'): string
nearestBand() from @llui/components

The band index nearest a normalized position — the hit test behind hover and pointer tracking. Returns null for an empty axis.

Nearest-CENTRE, not containment: the gaps between bands belong to whichever band is closer, so a pointer never falls into a dead zone between two bars.

function nearestBand(u: number, band: Band): number | null
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
niceDomain() from @llui/components

Extend [min, max] outward to the nearest round tick boundaries, so the axis ends on a labelled value instead of mid-air. Iterates because widening the domain can change the chosen step, which can change the boundaries again; d3's nice does the same, and it converges in at most a few rounds.

function niceDomain(min: number, max: number, count: number): Domain
normalize() from @llui/components

Map a value into [0, 1] across domain. A DEGENERATE domain (min === max) maps everything to 0.5 rather than dividing by zero — a single-valued series draws as a flat line through the middle, which is the only reading that is not a crash or a NaN in a path string.

function normalize(value: number, domain: Domain): number
oklabToLinearSrgb() from @llui/components
function oklabToLinearSrgb(lab: Oklab): Srgb
oklabToOklch() from @llui/components
function oklabToOklch(lab: Oklab): Oklch
oklabToSrgb() from @llui/components

NOT gamut-mapped — may return channels outside [0,1] for an OKLab/OKLCH color outside sRGB. Use {@link gamutMapOklchToSrgb} for a displayable result.

function oklabToSrgb(lab: Oklab): Srgb
oklchToHsv() from @llui/components

Gamut-maps into sRGB first — HSV cannot represent an out-of-gamut color.

function oklchToHsv(ok: Oklch): Hsv
oklchToOklab() from @llui/components
function oklchToOklab(ok: Oklch): Oklab
oklchToSrgb() from @llui/components

NOT gamut-mapped. See {@link oklabToSrgb}.

function oklchToSrgb(ok: Oklch): Srgb
parseCssColor() from @llui/components

Parse a CSS color string into its typed model. Supports #rgb/#rgba/ #rrggbb/#rrggbbaa, rgb()/rgba(), hsl()/hsla() (both legacy comma and modern space/slash syntax, with the legacy grammar's none-forbidden and uniform-percentage-or-number rules enforced), hwb(), oklch(), oklab(), lab(), lch(), color() (predefined spaces srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020, xyz, xyz-d50, xyz-d65), none components, transparent, and the 148 CSS Color 4 named colors. Returns null on anything else — never throws.

function parseCssColor(input: string): CssColor | null
parseDateValue() from @llui/components
function parseDateValue(value: DateValue): ParsedDateValue
parseHexColor() from @llui/components

Parse #rgb, #rgba, #rrggbb, #rrggbbaa. Returns null on anything else, never throws.

function parseHexColor(hex: string): { rgb: Rgb255; alpha: number } | null
pointerDragHandlers() from @llui/components

Build the five pointer handlers for one drag track. One call per live connect() instance — dragging/activePointerId are closure-scoped per instance, the same shape as this package's other per-instance mutable state (e.g. color-picker's eyedropper pendingEyeDropper AbortController).

  • Primary button only on pointerdown (e.button !== 0 is ignored), AND only when no drag is already live — a second finger/pointer touching down mid-drag is ignored rather than hijacking activePointerId.
  • Every subsequent event (pointermove/pointerup/pointercancel/ onLostPointerCapture) is checked against activePointerId: a second pointer's events never affect a drag it didn't start.
  • setPointerCapture on pointerdown's currentTarget, so pointermove keeps firing even once the pointer leaves the element's bounds — a plain drag with no capture stalls out at the track's edge instead of saturating like a native <input type="range">.
  • Released on pointerup/pointercancel, guarded by hasPointerCapture so releasing twice (or without ever capturing) is a harmless no-op.
  • dragging is unconditionally reset on onLostPointerCapture for the active pointer, even though that handler never calls release() itself (capture is already gone by definition when this fires).
function pointerDragHandlers(callbacks: PointerDragCallbacks): PointerDragHandlers
polarPoint() from @llui/components

A point on a circle. Angles are RADIANS CLOCKWISE FROM 12 O'CLOCK, which is where every reader expects a chart to start; SVG's own 0 is 3 o'clock.

function polarPoint(cx: number, cy: number, r: number, angle: number): Point
polarProjection() from @llui/components
function polarProjection(frame: Frame, opts: PolarOptions = {}): Projection
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
projectionFor() from @llui/components

Build the projection a chart's coordinate setting names.

function projectionFor(coord: ChartCoord, frame: Frame, opts: CartesianOptions & PolarOptions = {}): Projection
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;
rectPath() from @llui/components

An axis-aligned rectangle, corners in any order.

function rectPath(x0: number, y0: number, x1: number, y1: number): string
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).

The walk crosses SHADOW boundaries: Element.closest() stops at the nearest shadow root and cannot see a dir set on an ancestor of the host, so an element inside a shadow tree whose host (or the host's own ancestors) declares dir would otherwise silently read as ltr. Continuing from root.host after getRootNode() returns a ShadowRoot walks out to the light-DOM ancestor and keeps going, arbitrarily many shadow levels deep. The final fallback reads dir off the element's OWN document (ownerDocument), never the global document — the global binding names a DIFFERENT document inside an iframe or any other multi-document context.

export declare function resolveDir(el: Element): TextDirection;
resolveNone() from @llui/components

A null (CSS none) component behaves as 0 outside interpolation.

function resolveNone(n: number | null): number
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
rgb255ToHsl() from @llui/components

Convert RGB (0-255 each) to HSL (h 0-360, s/l 0-100), float.

function rgb255ToHsl(rgb: Rgb255): Hsl
rgb255ToHsv() from @llui/components

RGB255 -> HSV, via the DIRECT (not HSL-routed) float conversion.

function rgb255ToHsv(rgb: Rgb255): Hsv
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
srgb255ToSrgb() from @llui/components
function srgb255ToSrgb(rgb: Rgb255): Srgb
srgbToLinear() from @llui/components
function srgbToLinear(s: Srgb): Srgb
srgbToOklab() from @llui/components
function srgbToOklab(s: Srgb): Oklab
srgbToOklch() from @llui/components
function srgbToOklch(s: Srgb): Oklch
srgbToRgb255() from @llui/components

Rounds AND clamps into 0-255 — the boundary where an out-of-gamut float (an un-mapped OKLCH conversion) must finally become a displayable byte.

function srgbToRgb255(s: Srgb): Rgb255
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
stepPath() from @llui/components

Axis-aligned staircase: hold each value until halfway along the independent axis, then step.

function stepPath(points: readonly Point[], axis: CurveAxis = 'x'): string
tickIncrement() from @llui/components

The step size a nice axis would use to fit about count ticks into [start, stop]. A negative return means "one tick every 1/-step units" — d3's encoding, kept because the reciprocal form avoids the float drift that 10 ** -power introduces for small steps.

function tickIncrement(start: number, stop: number, count: number): number
ticks() from @llui/components

Round, human-readable tick values covering [start, stop]. Returns an EMPTY array for a non-finite or degenerate range rather than looping forever — a chart with no data is a normal state, not an error.

function ticks(start: number, stop: number, count: number): 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>>
valueDomain() from @llui/components

The value-axis domain for a set of series.

min / max are UNBOUNDED-CAPABLE bounds in the sense of finiteBound (#177): an absent one means "derive it", never ±Infinity. A derived domain always INCLUDES ZERO, because a bar whose baseline is not zero misrepresents its own length — the one axis default worth being opinionated about. A caller who wants otherwise passes both bounds.

function valueDomain(values: readonly number[], opts: { min?: number; max?: number; tickCount: number }): Domain
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 } & Retain)
  /** @intent("Open the named accordion item") */
  | ({ type: 'open'; value: string } & Retain)
  /** @intent("Close the named accordion item") */
  | ({ type: 'close'; value: string } & Retain)
  /** @intent("Replace the set of currently-open items with the provided values") */
  | ({ type: 'setValue'; value: string[] } & Retain)
  /** @humanOnly */
  | { type: 'setItems'; items: string[] }
  /** @humanOnly */
  | { type: 'focusNext'; value: string }
  /** @humanOnly */
  | { type: 'focusPrev'; value: string }
  /** @humanOnly */
  | { type: 'focusFirst' }
  /** @humanOnly */
  | { type: 'focusLast' }
  /** @humanOnly — sent by the retained content's own animation end/cancel event,
   * or by `exitCompletion`'s own cleanup settling every still-closing item once
   * the last watcher for this `id` detaches. */
  | { type: 'exitComplete'; value: string; generation: number }
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' }
CalendarUnit from @llui/components

Rungs of the ladder, finest first.

export type CalendarUnit = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'
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' }
  /** @humanOnly */
  | { type: 'setHovered'; hovered: boolean }
  /** @humanOnly */
  | { type: 'setFocusWithin'; focusWithin: boolean }
  /** @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' }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; 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' }
ChartCoord from @llui/components
export type ChartCoord = 'cartesian' | 'polar'
ChartMsg from @llui/components
export type ChartMsg =
  /** @intent("Switch between cartesian and polar projection") */
  | { type: 'setCoord'; coord: ChartCoord }
  /** @intent("Allocate the independent axis by equal slots or by share of the total") */
  | { type: 'setDomain'; domain: ChartDomain }
  /** @intent("Set the row under the cursor, or clear it with null") */
  | { type: 'setActive'; index: number | null }
  /** @intent("Move the keyboard cursor along the rows by delta, wrapping") */
  | { type: 'moveActive'; delta: number }
  /** @intent("Move the keyboard cursor to the first row") */
  | { type: 'firstActive' }
  /** @intent("Move the keyboard cursor to the last row") */
  | { type: 'lastActive' }
  /** @intent("Isolate one series, or show them all again with null") */
  | { type: 'setActiveSeries'; key: string | null }
  /** @intent("Replace the plotted rows") */
  | { type: 'setRows'; rows: ChartRow[] }
  /** @intent("Pin the low end of the value axis, or derive it again with null") */
  | { type: 'setMin'; value: number | null }
  /** @intent("Pin the high end of the value axis, or derive it again with null") */
  | { type: 'setMax'; value: number | null }
  /** @intent("Stack the series instead of overlaying them") */
  | { type: 'setStacked'; stacked: boolean }
  /** @intent("Set the viewBox size in user units") */
  | { type: 'setSize'; width: number; height: number }
  /** @intent("Set the polar inner radius as a fraction of the outer") */
  | { type: 'setInnerRadius'; value: number }
  /** @intent("Swap the cartesian axes for a horizontal bar chart") */
  | { type: 'setHorizontal'; horizontal: boolean }
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' }
  /** @humanOnly */
  | { type: 'copyFailed' }
  /** @intent("Clear the transient \"copied\" / \"failed\" feedback state") */
  | { type: 'reset' }
CollapsibleMsg from @llui/components
export type CollapsibleMsg =
  /** @intent("Toggle the collapsible panel open/closed") */
  | ({ type: 'toggle' } & Retain)
  /** @intent("Expand the collapsible panel") */
  | ({ type: 'open' } & Retain)
  /** @intent("Collapse the panel") */
  | ({ type: 'close' } & Retain)
  /** @intent("Set the panel's open state to a specific value") */
  | ({ type: 'setOpen'; open: boolean } & Retain)
  /** @humanOnly — sent by the retained content's own animation end/cancel event,
   * or by `exitCompletion`'s own cleanup settling a still-closing panel once
   * the last watcher for this `id` detaches. */
  | { type: 'exitComplete'; generation: number }
ColorModel from @llui/components

Color picker — HSV and OKLCH color selection.

The canonical color lives in whichever model is ACTIVE (state.color.model) and is lossless within that model. Every message that edits the color goes through a PROJECTION into the model it naturally operates in (HSV, HSL, or OKLCH), applies the edit there, then converts the result into the active model — so setSaturation (an HSL concept) and setChroma (an OKLCH concept) both work regardless of which model is currently active, and setHue sets the hue of whichever model IS active (HSV hue and OKLCH hue are numerically different rulers for the same wheel).

HUE STABILITY ON ACHROMATIC COLORS is the reason HSV was chosen as the original canonical store (S or V at 0 loses no information about hue, unlike HSL's black/white axis) and it is preserved across the HSV<->OKLCH boundary too: {@link hsvToOklchPreserving}/{@link oklchToHsvPreserving} carry the stored hue straight across whenever the color is achromatic in the SOURCE model, rather than recomputing an arbitrary hue-of-zero-chroma. setModel and every cross-model projection route through these two.

export type ColorModel = 'hsv' | 'oklch'
ColorPickerMsg from @llui/components
export type ColorPickerMsg =
  /** @intent("Switch the active color model, converting the stored color and keeping its hue stable if it is achromatic") */
  | { type: 'setModel'; model: ColorModel }
  /** @intent("Set the full HSL color at once") */
  | { type: 'setHsl'; hsl: Hsl }
  /** @intent("Set the hue of the ACTIVE model (0–360)") */
  | { type: 'setHue'; h: number }
  /** @intent("Set the HSL saturation channel (0–100)") */
  | { type: 'setSaturation'; s: number }
  /** @intent("Set the HSL lightness channel (0–100)") */
  | { type: 'setLightness'; l: number }
  /** @intent("Set the alpha channel (0–1)") */
  | { type: 'setAlpha'; alpha: number }
  /** @intent("Set the color from any CSS color string (hex, rgb(), hsl(), oklch(), oklab(), a named color)") */
  | { 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, an eyedropper pick, or any CSS color string") */
  | { type: 'setColor'; color: string }
  /** @intent("Set the full OKLCH color at once (L 0–1, C >= 0, H degrees)") */
  | { type: 'setOklch'; l: number; c: number; h: number }
  /** @intent("Set the OKLCH chroma channel") */
  | { type: 'setChroma'; c: number }
  /** @intent("Set the OKLCH lightness channel (0–1)") */
  | { type: 'setOklchLightness'; l: number }
  /** @intent("Set chroma and lightness (OKLCH, x/y of the 2D area) at once") */
  | { type: 'setLc'; c: number; l: number }
  /** @intent("Nudge chroma/lightness (OKLCH) by signed deltas — used by area arrow keys") */
  | { type: 'nudgeLc'; dc: number; dl: number }
  /** @humanOnly */
  | { type: 'setEyeDropperSupported'; supported: boolean }
  /** @humanOnly */
  | { type: 'eyeDropperFailed'; message: 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
   *
   * Replace `items` (and optionally `disabled`) synchronously — the same
   * highlight-resolution policy as `loadSuccess` applies: a highlight that no
   * longer survives the fresh list moves to the first enabled match WHILE
   * OPEN, or to `null` WHILE CLOSED (#265 G3), since a background refresh
   * must not manufacture a highlight before the control is ever opened.
   */
  | { type: 'setItems'; items: string[]; disabled?: string[] }
  /** @intent("Mark an async option fetch as started; pass the request's id") */
  | { type: 'loadStart'; requestId: number }
  /**
   * @humanOnly
   *
   * Atomic replacement: `items` is required. `disabled` is an OPTIONAL
   * companion that replaces `disabledItems` when present (omitted ⇒
   * unchanged). `groups` is different: omitting it RESETS to no groups
   * (`[]`), the same as `init()` with no `groups` option — a fresh
   * replacement with no `groups` describes a flat result, and carrying a
   * PREVIOUS load's groups forward would keep describing options this
   * replacement never mentioned as belonging to a group that may no longer
   * apply (#265 A3). Every field the fresh `items`/`disabled`/`groups` makes
   * inconsistent is reconciled in this SAME reducer step, never in a
   * follow-up message. `value` (selection) is dropped when it no longer
   * names a value in the new `items` (after the new `disabled` is applied).
   * `highlightedValue` is kept only when it is BOTH still in the fresh
   * filtered list AND not newly disabled. Otherwise the fallback depends on
   * whether the listbox is open: WHILE OPEN it moves to the first enabled
   * match (or `null` when none is enabled) — never left dangling for a
   * render in between, and never left naming an option that is now
   * disabled. WHILE CLOSED it always resolves to `null` instead, even when
   * the fresh list has enabled options: the listbox content is unmounted
   * while closed, so there is no option `aria-activedescendant` could
   * correctly name, and a background load (a prefetch, a poll) must not
   * manufacture a highlight before the control is ever opened — re-opening
   * always reseeds the highlight itself (#265 G3).
   */
  | {
      type: 'loadSuccess'
      requestId: number
      items: string[]
      groups?: ComboboxGroup[]
      disabled?: 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' or 'rtl'") */
  | { type: 'setDir'; dir: TextDirection }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; dir: TextDirection }
  /** @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'>
CssColor from @llui/components

A color as CSS Color 4 sees it. lab()/lch()/wide-gamut color() are NOT separate tags here — see the module doc — they resolve to 'oklch' (or 'srgb' for color(srgb ...)/color(srgb-linear ...)) at parse time.

export type CssColor =
  | { space: 'srgb'; r: number | null; g: number | null; b: number | null; alpha: number | null }
  | {
      space: 'srgb-linear'
      r: number | null
      g: number | null
      b: number | null
      alpha: number | null
    }
  | { space: 'hsl'; h: number | null; s: number | null; l: number | null; alpha: number | null }
  | { space: 'oklab'; l: number | null; a: number | null; b: number | null; alpha: number | null }
  | { space: 'oklch'; l: number | null; c: number | null; h: number | null; alpha: number | null }
Curve from @llui/components

How a run of points is joined.

export type Curve = 'linear' | 'monotone' | 'step'
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' }
  /** @humanOnly */
  | { type: 'moveFocusMonths'; months: number }
  /** @intent("Clear the current selection") */
  | { type: 'clear' }
  /** @humanOnly */
  | { type: 'setToday'; today: string | null }
  /** @intent("Replace the set of individually unavailable dates (YYYY-MM-DD)") */
  | { type: 'setUnavailable'; dates: string[] }
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 }
  /** @humanOnly */
  | { type: 'uploadProgress'; id: string; progress: number }
  /** @humanOnly */
  | { type: 'uploadSucceeded'; id: string }
  /** @humanOnly */
  | { type: 'uploadFailed'; id: string; error: string }
  /** @intent("Retry the failed upload of the file with the given id") */
  | { type: 'retryUpload'; id: string }
FileUploadStatus from @llui/components
export type FileUploadStatus = 'uploading' | 'done' | 'error'
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 }
  /** @intent("Move the panel by a pixel offset (x right, y down)") */
  | { type: 'moveBy'; dx: number; dy: number }
  /** @intent("Resize the panel from one edge or corner by a pixel offset (clamped to min/max)") */
  | { type: 'resizeBy'; handle: ResizeHandle; dx: number; dy: 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'
GradientKind from @llui/components

Gradient picker — CSS linear-gradient()/radial-gradient()/ conic-gradient() (and their repeating- forms) authoring: a horizontal stop ramp plus per-kind controls, with a full color-picker embedded for whichever stop is selected.

The embedded picker edits the SELECTED STOP'S OWN model

There is exactly one color store per stop (GradientStop.color, the same PickerColor union color-picker itself uses) and exactly one place that projects it into a live ColorPickerState: {@link pickerStateOf}. It hands the stop's color through UNCHANGED — no HSV<->OKLCH re-projection — so a picker message that only touches one channel (setAlpha, setHue, …) leaves every OTHER channel of an out-of-gamut OKLCH stop bit-identical. update()'s 'picker' case runs colorPicker.update on that DERIVED state and writes color/alpha straight back onto the SELECTED stop; a stop's MODEL only ever changes via an explicit picker: { msg: { type: 'setModel' } } }, which is itself just color-picker's own reducer producing a same-shape next.color in the new model — there is no separate "gradient-level model" to keep in sync. defaultModel is a DIFFERENT, narrower thing: which model a freshly-added stop with no other way to pick one starts in (see addStop/buildStopsFromOptions).

The stop-position domain is always one cycle, 0–100

repeating only changes the CSS repeating- prefix (how the SAME 0–100 ramp tiles across the painted box) — it never changes what a stop's position number means. colorAt/addStop always read position as a percentage of one cycle; colorAt WRAPS a query outside [first, last] by the stop range's own period when repeating is true (matching a real repeating-linear-gradient's paint), and clamps to the end stops otherwise. toCss's stop list is always the one un-repeated cycle, matching what {@link parseGradient} reads back.

export type GradientKind = 'linear' | 'radial' | 'conic'
GradientPickerMsg from @llui/components

@intent group

export type GradientPickerMsg =
  /** @intent("Add a stop at `position` (0-100), colored EXACTLY what the gradient already renders there") */
  | { type: 'addStop'; position: number }
  /** @intent("Remove the stop with the given id. Refused below `minStops`; a removed selection reselects a neighbour") */
  | { type: 'removeStop'; id: string }
  /** @humanOnly */
  | { type: 'selectStop'; id: string }
  /** @intent("Move the stop with the given id to `position` (0-100, clamped), keeping sort order and selection") */
  | { type: 'moveStop'; id: string; position: number }
  /** @intent("Move the stop with the given id by a signed `delta` (0-100 units)") */
  | { type: 'nudgeStop'; id: string; delta: number }
  /** @intent("Switch the gradient kind (linear/radial/conic)") */
  | { type: 'setKind'; kind: GradientKind }
  /** @intent("Toggle the `repeating-` form of the current gradient kind") */
  | { type: 'setRepeating'; repeating: boolean }
  /** @intent("Set an EXPLICIT angle in degrees — linear's direction (converting away from a to-keyword direction) or conic's `from` angle. No-op for radial. Normalized to [0,360)") */
  | { type: 'setAngle'; angle: number }
  /** @intent("Set the center position (percent 0-100 each axis) — radial and conic only") */
  | { type: 'setCenter'; x: number; y: number }
  /** @intent("Set the radial gradient's shape (circle/ellipse)") */
  | { type: 'setShape'; shape: RadialShape }
  /** @intent("Set the radial gradient's extent keyword") */
  | { type: 'setSize'; size: RadialSize }
  /** @intent("Set the color-interpolation-method: the space, and (polar spaces only) the hue method") */
  | { type: 'setInterpolation'; space: InterpolationSpace; hue?: HueInterpolationMethod }
  /** @intent("Reverse the stop order (mirrors every stop's position about the middle)") */
  | { type: 'reverse' }
  /** @intent("Space every stop evenly across 0-100, preserving relative order") */
  | { type: 'distribute' }
  /** @humanOnly */
  | { type: 'setGradientDraft'; value: string }
  /** @intent("Parse `css` and, if valid AND within min/maxStops, replace the whole gradient. Invalid input (or a stop count outside min/maxStops) sets a describable error and leaves the gradient unchanged") */
  | { type: 'setGradient'; css: string }
  /** @intent("Edit the color/alpha of the SELECTED stop through the embedded color-picker's own message set") */
  | { type: 'picker'; msg: ColorPickerMsg }
  /** @humanOnly */
  | { type: 'selectNextStop' }
  /** @humanOnly */
  | { type: 'selectPrevStop' }
  /** @intent("Set the reading direction (ltr/rtl)") */
  | { type: 'setDir'; dir: 'ltr' | 'rtl' }
HoverCardMsg from @llui/components
export type HoverCardMsg =
  | { type: 'show' }
  | { type: 'hide' }
  | { type: 'setOpen'; open: boolean }
  /** @humanOnly */
  | { type: 'animationEnd' }
  /** @humanOnly */
  | { type: 'transitionEnd' }
HueInterpolationMethod from @llui/components
export type HueInterpolationMethod = 'shorter' | 'longer' | 'increasing' | 'decreasing'
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' }
  /** @intent("Move the crop area by a percentage of the image size (x right, y down)") */
  | { type: 'nudge'; x: number; y: number }
  /** @intent("Zoom the crop area about its centre: factor > 1 zooms in, < 1 zooms out") */
  | { type: 'zoom'; factor: number }
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'
InterpolationSpace from @llui/components

Interpolation / generic-conversion target spaces (CSS Color 4 §12).

export type InterpolationSpace = 'srgb' | 'srgb-linear' | 'oklab' | 'oklch' | 'hsl'
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 }
MarkType from @llui/components
export type MarkType = 'line' | 'area' | 'bar'
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: 'syncTriggerFocus'; id: string }
  /** @humanOnly */
  | { type: 'focusNext' }
  /** @humanOnly */
  | { type: 'focusPrev' }
  /** @humanOnly */
  | { type: 'menuMsg'; id: string; msg: MenuMsg }
  /** @intent("Set the reading direction") */
  | { type: 'setDir'; dir: TextDirection }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; dir: TextDirection }
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' or 'rtl'") */
  | { type: 'setDir'; dir: TextDirection }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; dir: TextDirection }
  /** @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 }
  /** @humanOnly */
  | { type: 'setBands'; bands: readonly MeterBandInit[] }
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' }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; 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 }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; dir: TextDirection }
ParseGradientResult from @llui/components
export type ParseGradientResult =
  | { ok: true; value: ParsedGradient }
  | { ok: false; reason: string }
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 }
PickerColor from @llui/components

The canonical stored color union — HSV or OKLCH, tagged by model. Exported as a named type for gradient-picker, whose stops each store one of these (never a hex string) so a gray stop keeps its hue and an out-of-gamut OKLCH stop stays exact.

export type PickerColor = ColorPickerState['color']
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 }
RadialShape from @llui/components
export type RadialShape = 'circle' | 'ellipse'
RadialSize from @llui/components
export type RadialSize = 'closest-side' | 'closest-corner' | 'farthest-side' | 'farthest-corner'
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; isStartHalf: boolean }
  /** @humanOnly */
  | { type: 'hoverItem'; index: number; isStartHalf: 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
      /** Items in the origin container (for "item X of N"). */
      count: 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; count: 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'
SparklineBandKind from @llui/components
export type SparklineBandKind = 'between' | 'below' | 'above'
SparklineMsg from @llui/components
export type SparklineMsg =
  /** @intent("Set the point under the cursor, or clear it with null") */
  | { type: 'setActive'; index: number | null }
  /** @intent("Move the cursor along the points by delta, clamping at the ends") */
  | { type: 'moveActive'; delta: number }
  /** @intent("Move the cursor to the first point") */
  | { type: 'firstActive' }
  /** @intent("Move the cursor to the last point") */
  | { type: 'lastActive' }
  /** @intent("Replace the plotted points") */
  | { type: 'setPoints'; points: SparklinePoint[] }
  /** @intent("Set the instant the right edge denotes, or derive it with null") */
  | { type: 'setNow'; at: number | null }
  /** @intent("Set the reference band; null on a side leaves it unbounded") */
  | { type: 'setBand'; low: number | null; high: number | null }
  /** @intent("Set the viewBox size in user units") */
  | { type: 'setSize'; width: number; height: number }
SparklineTone from @llui/components
export type SparklineTone = 'below' | 'in' | 'above' | 'none'
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' }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; 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). applyTheme() publishes that PREFERENCE on <html> — writing data-theme for an explicit choice and REMOVING it for 'system', which is the state tokens-dark.css's prefers-color-scheme block answers in pure CSS. Do not resolve 'system' in JS just to honour the OS setting: that makes watchSystemTheme mandatory, gives SSR nothing correct to render, and overwrites the preference with its answer.

Typically wired via onMount or in app init:

onMount(() => {
  applyTheme(state.theme.theme)
})

resolveTheme() and watchSystemTheme() remain for consumers who need the resolved value IN JS — canvas theming, a <meta name="theme-color"> — but they are no longer on the critical path for merely following the OS.

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("Move the toast region to a different corner/edge of the viewport") */
  | { type: 'setPlacement'; placement: ToastPlacement }
  /** @intent("Patch mutable presentation fields on the toast with the given id (title, description, type, etc.); `id` cannot be patched") */
  | { type: 'update'; id: string; patch: ToastPatch }
  /** @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; reason?: ToastPauseReason }
  /** @intent("Resume auto-dismiss countdown for the toast with the given id (releases only the given reason, default manual)") */
  | { type: 'resume'; id: string; reason?: ToastPauseReason }
  /** @intent("Pause auto-dismiss for every visible toast") */
  | { type: 'pauseAll'; reason?: ToastPauseReason }
  /** @intent("Resume auto-dismiss for every visible toast (releases only the given reason, default manual)") */
  | { type: 'resumeAll'; reason?: ToastPauseReason }
  /** @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/pausedBy/status are optional — seeded from duration/[]/'open' when omitted.

export type ToastInput = Omit<Toast, 'remainingMs' | 'pausedBy' | 'status'> & {
  remainingMs?: number
  pausedBy?: readonly ToastPauseReason[]
  status?: PresenceStatus
}
ToastPatch from @llui/components

Fields an update message may patch on a mounted toast. id is the ONE truly immutable field — a toast is created once and dismissed once, never structurally replaced with a different id for the same row — so it is excluded here rather than silently ignored by the reducer. type and ariaLive (among others) ARE genuine mutable presentation fields (the toast.promise-style loading→success/error flow patches type, title and description on the same mounted toast) and connect()'s toast() builder binds every one of these reactively (never via a one-shot peek()), so a patch here is visible wherever it renders.

status/remainingMs/pausedBy are excluded: they are LIFECYCLE fields the reducer itself owns (presence transitions, the tick-driven countdown, pause/resume) and a patch is the wrong channel for them — dismiss/tick/ pause/resume already exist and a caller patching remainingMs directly would race the reducer's own countdown math. duration stays patchable (it IS presentation — the toast.promise flow moves a sticky loading toast to a finite success/error duration), so update re-seeds remainingMs from the new duration whenever duration is part of the patch (#265 A2) — otherwise a toast created sticky (duration: null, remainingMs frozen at 0) that is later patched to a finite duration would inherit that frozen 0 and dismiss on the very next tick instead of lasting its new duration.

undefined on a REQUIRED field (type, duration, dismissable) means "not patched", the same as an absent key (#265 G6): { duration: undefined } leaves the countdown alone rather than re-seeding it to 0 — patch duration: null to make a toast sticky. On an OPTIONAL field (title, description, ariaLive) undefined clears it, so a loading -> success patch can drop a description the success toast should not keep.

export type ToastPatch = Partial<Omit<Toast, 'id' | 'status' | 'remainingMs' | 'pausedBy'>>
ToastPauseReason from @llui/components

Why a toast's countdown is paused. The row's own handlers use 'hover' and 'focus'; a pause/resume message without a reason uses 'manual'.

export type ToastPauseReason = (typeof PAUSE_REASONS)[number]
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 pausedBy (the set of reasons currently holding it 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 (any reason held) freezes its remainingMs (ticks are ignored).
  • Pause REASONS are independent (#265 G6): the row's hover pauses/resumes 'hover', focus entering/leaving the row pauses/resumes 'focus', and pause/resume with no reason use 'manual'. Pointer-leave therefore never resumes a toast whose close button still has keyboard focus.
  • 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' }
ToggleItemPart from @llui/components
export type ToggleItemPart = 'kind-toggle' | 'shape-option' | 'size-option'
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[]
  /**
   * Retain closing content for an exit animation. Off by default so a missing
   * stylesheet/event cannot leave hidden semantic content mounted forever.
   */
  animated?: boolean
}
AccordionItemParts from @llui/components
export interface AccordionItemParts {
  trigger: {
    type: 'button'
    'aria-expanded': ReadSignal<boolean>
    'aria-controls': string
    id: string
    'data-state': ReadSignal<'open' | 'closed'>
    'data-disabled': ReadSignal<'' | undefined>
    disabled: ReadSignal<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': ReadSignal<'open' | 'closing' | 'closed'>
    'data-scope': 'accordion'
    'data-part': 'content'
    'data-value': string
    hidden: ReadSignal<boolean>
    'aria-hidden': ReadSignal<'true' | undefined>
    inert: ReadSignal<boolean>
    onAnimationStart: (e: AnimationEvent) => void
    onAnimationEnd: (e: AnimationEvent) => void
    onAnimationCancel: (e: AnimationEvent) => void
    onTransitionStart: (e: TransitionEvent) => void
    onTransitionEnd: (e: TransitionEvent) => void
    onTransitionCancel: (e: TransitionEvent) => void
  }
  item: {
    'data-state': ReadSignal<'open' | 'closed'>
    'data-disabled': ReadSignal<'' | 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
  /**
   * Settles a retained `closing` item once its content's own exit
   * animation/transition ends — or immediately, if the skin runs no exit
   * motion at all. This only ever has anything to settle for a
   * PROGRAMMATIC close/toggle/setValue when that message carried
   * `retain: true`, which `parts.close(value)` stamps for you; a raw
   * `send({ type: 'close', value })` with no `retain` closes instantly and
   * never enters `closing` at all, so there is nothing here to settle for
   * it. Its mount ALSO reports whether it is placed at all (#264 item F1):
   * `animated: true` only ever retains `closing` content while this is
   * mounted — forgetting to place it degrades gracefully to an instant
   * close (with a one-time dev warning) rather than hanging `closing` +
   * `inert` forever, so placing it is no longer required for SAFETY, only
   * for the requested exit animation to actually run on a retained close.
   * A click-driven close is still safety-netted synchronously inside the
   * trigger regardless of whether this is placed.
   */
  exitCompletion: Mountable
  /**
   * An animated-aware programmatic close (#264 review-264j): a raw
   * `send({ type: 'close', value })` from app/agent code carries no
   * `retain` and therefore closes INSTANTLY, even with `animated: true` and
   * `exitCompletion` placed — documented, fail-safe behavior, since a bare
   * message has no way to know whether a watcher happens to be mounted.
   * `parts.close(value)` is the correct way for a host to close an item
   * programmatically and still get the animated exit: it stamps `retain`
   * from the SAME runtime registry the trigger handlers read.
   */
  close: (value: string) => void
}
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[]
  /** Presentation-only items retained while their opted-in exit animation runs. */
  closing: string[]
  /** Monotonic exit generation per item, used to reject stale end events. */
  exitGenerations: RetainedExitGeneration<string>[]
  /** Monotonic source for collision-free exit generations; never grows by id. */
  exitSequence: number
  /** Whether closed content is retained until its own animation end/cancel event. */
  animated: boolean
}
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: ReadSignal<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': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number>
    'aria-valuetext': ReadSignal<string>
    'aria-orientation': 'horizontal'
    'aria-disabled': ReadSignal<'true' | undefined>
    'aria-readonly': ReadSignal<'true' | undefined>
    tabindex: ReadSignal<number>
    'data-scope': 'angle-slider'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | 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': ReadSignal<string>
  }
  valueText: {
    'data-scope': 'angle-slider'
    'data-part': 'value-text'
  }
  /** A hidden input for form participation. */
  hiddenInput: {
    type: 'hidden'
    value: ReadSignal<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': ReadSignal<AsyncStatus>
    /** `'true'` while a page request is in flight — the list's content is changing. */
    'aria-busy': ReadSignal<'true' | 'false'>
    /** Present once a load has SETTLED with zero items: the empty-state hook. */
    'data-empty': ReadSignal<'' | undefined>
    /** Present when the source reports no further pages. */
    'data-exhausted': ReadSignal<'' | undefined>
  }
  sentinel: {
    'data-scope': 'async-list'
    'data-part': 'sentinel'
    'aria-hidden': 'true'
  }
  loadMoreTrigger: {
    type: 'button'
    disabled: ReadSignal<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: ReadSignal<boolean>
    onClick: (e: MouseEvent) => void
  }
  errorText: {
    role: 'alert'
    'aria-live': 'polite'
    'data-scope': 'async-list'
    'data-part': 'error-text'
    hidden: ReadSignal<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': ReadSignal<ImageStatus>
    'data-density': AvatarDensity | undefined
  }
  image: {
    'data-scope': 'avatar'
    'data-part': 'image'
    'data-status': ReadSignal<ImageStatus>
    hidden: ReadSignal<boolean>
    alt: string
    onLoad: (e: Event) => void
    onError: (e: Event) => void
    onLoadStart: (e: Event) => void
  }
  fallback: {
    'data-scope': 'avatar'
    'data-part': 'fallback'
    'data-status': ReadSignal<ImageStatus>
    hidden: ReadSignal<boolean>
    'aria-hidden': ReadSignal<'true' | undefined>
  }
}
AvatarState from @llui/components
export interface AvatarState {
  status: ImageStatus
}
Band from @llui/components

A discrete axis of count slots laid out across [0, 1].

export interface Band {
  count: number
  /** Gap between adjacent bands, as a fraction of a slot. */
  paddingInner: number
  /** Gap before the first and after the last band, as a fraction of a slot. */
  paddingOuter: number
}

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': ReadSignal<'page' | undefined>
    'data-scope': 'breadcrumbs'
    'data-part': 'link'
    'data-value': string
    'data-current': ReadSignal<'' | 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
}
CalendarOptions from @llui/components
export interface CalendarOptions {
  /**
   * Minutes east of UTC that boundaries are measured in. Default 0 (UTC). A
   * FIXED offset — see the module note; `-300` is US Eastern standard time and
   * stays at −5 across a DST transition rather than following it.
   */
  offsetMinutes?: number
  /** First day of the week for the `week` rung: 0 = Sunday … 6 = Saturday.
   *  Default 1 (Monday), matching ISO 8601. */
  weekStartsOn?: number
}
CalendarStep from @llui/components

A chosen rung, plus how many of it each boundary advances. stride is always 1 except on year.

export interface CalendarStep {
  unit: CalendarUnit
  stride: number
}
CalendarStepOptions from @llui/components
export interface CalendarStepOptions extends CalendarOptions {
  /** Upper bound on the number of boundaries. Default 6. */
  maxTicks?: number
  /** Force a rung instead of choosing one from the span. */
  unit?: CalendarUnit
}
CalendarTick from @llui/components
export interface CalendarTick {
  /** The boundary instant, in epoch milliseconds. */
  at: number
  unit: CalendarUnit
}
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: {
    id: string
    role: 'region'
    'aria-roledescription': 'carousel'
    'aria-label': string
    'data-scope': 'carousel'
    'data-part': 'root'
    'data-paused': ReadSignal<'' | undefined>
    onPointerEnter: (e: PointerEvent) => void
    onPointerLeave: (e: PointerEvent) => void
    onFocusIn: (e: FocusEvent) => void
    onFocusOut: (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': ReadSignal<'' | undefined>
    /** Live track offset (px) to follow the finger: `translateX(var)`. */
    'data-drag-offset': ReadSignal<string | undefined>
    /** Physical pointer delta consumed by either skin's track transform. */
    'style.--carousel-drag-offset': ReadSignal<string | undefined>
    onPointerDown: (e: PointerEvent) => void
    onPointerMove: (e: PointerEvent) => void
    onPointerUp: (e: PointerEvent) => void
    onPointerCancel: (e: PointerEvent) => void
  }
  /** Place all slides directly inside this transform-bearing track. */
  track: {
    'data-scope': 'carousel'
    'data-part': 'track'
  }
  indicatorGroup: {
    role: 'tablist'
    'aria-label': string
    'data-scope': 'carousel'
    'data-part': 'indicator-group'
  }
  nextTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'carousel'
    'data-part': 'next-trigger'
    onClick: (e: MouseEvent) => void
  }
  prevTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'carousel'
    'data-part': 'prev-trigger'
    onClick: (e: MouseEvent) => void
  }
  slide: (index: number) => CarouselSlideParts
  /** Place once anywhere in the same build to keep automatic direction live. */
  directionSync: Mountable
}
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': ReadSignal<'' | undefined>
    hidden: ReadSignal<boolean>
  }
  indicator: {
    type: 'button'
    role: 'tab'
    /** APG roving tab stop: only the selected indicator participates in Tab. */
    tabindex: ReadSignal<0 | -1>
    'aria-label': string
    'aria-selected': ReadSignal<boolean>
    'aria-controls': string
    'data-scope': 'carousel'
    'data-part': 'indicator'
    'data-index': string
    'data-active': ReadSignal<'' | 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
  /** Explicit application pause, independent from hover/focus interaction. */
  paused: boolean
  /** Pointer is currently over the carousel root. */
  hovered: boolean
  /** DOM focus is currently contained by the carousel root. */
  focusWithin: 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'
  /** Whether direction follows the mounted DOM or explicit init/setDir configuration. */
  dirSource: DirectionSource
}
CartesianOptions from @llui/components
export interface CartesianOptions {
  /** Swap the axes: `u` runs top-to-bottom and `v` left-to-right. This is what
   *  turns a column chart into a horizontal bar chart, and it is a PROJECTION
   *  concern — no mark, scale or hit test changes. */
  horizontal?: boolean
}
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: ReadSignal<boolean>
    value: ReadSignal<string>
    'data-scope': 'cascade-select'
    'data-part': 'level-select'
    'data-level': string
    'data-ready': ReadSignal<'' | 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': ReadSignal<'' | undefined>
    'data-complete': ReadSignal<'' | undefined>
  }
  clearTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<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
}
ChartCategoryTick from @llui/components
export interface ChartCategoryTick {
  index: number
  label: string
  x: number
  y: number
  anchor: 'start' | 'middle' | 'end'
  baseline: 'auto' | 'middle' | 'hanging'
  active: boolean
}
ChartConnectOptions from @llui/components
export interface ChartConnectOptions {
  /** Base id; the title and description ids derive from it. */
  id: string
  /**
   * Whether the consumer renders the `desc` part (default: true). When false,
   * the svg's `aria-labelledby` names only the title, never an element that
   * does not exist (the `dialog` `hasDescription` rule, #268). The TITLE is
   * the image's name — required for `role="img"` — and is always referenced.
   */
  hasDescription?: boolean
}
ChartGeometry from @llui/components
export interface ChartGeometry {
  frame: Frame
  domain: Domain
  band: Band
  /**
   * The proportional allocation of the independent axis under
   * `ChartState.domain === 'share'`, and `null` otherwise. It is the axis
   * itself, not a decoration: the pointer hit test reads it instead of `band`.
   *
   * With more than one bar series each ring gets its OWN allocation from its
   * own values — that is what makes a nested donut's rings each proportional —
   * so a single `u` names different rows on different rings and the pointer
   * has to follow one of them. It follows the FIRST bar series, which is the
   * whole story for the single-series pie that is the common case.
   */
  slices: ShareSlice[] | null
  projection: Projection
  marks: ChartMark[]
  vertices: ChartVertex[]
  gridLines: ChartGridLine[]
  categoryTicks: ChartCategoryTick[]
  /** Anchor for the tooltip, in user units. `null` when nothing is active. */
  tooltipAt: { x: number; y: number } | null
  tooltipRows: ChartTooltipRow[]
}
ChartGridLine from @llui/components
export interface ChartGridLine {
  value: number
  label: string
  d: string
  x: number
  y: number
  anchor: 'start' | 'middle' | 'end'
  baseline: 'auto' | 'middle' | 'hanging'
}
ChartInit from @llui/components
export interface ChartInit {
  series: readonly ChartSeries[]
  rows?: readonly ChartRow[]
  coord?: ChartCoord
  domain?: ChartDomain
  label?: string
  description?: string
  width?: number
  height?: number
  padding?: Partial<ChartPadding>
  min?: number
  max?: number
  tickCount?: number
  stacked?: boolean
  innerRadius?: number
  horizontal?: boolean
}
ChartMark from @llui/components

A drawn mark: one series, one path.

export interface ChartMark {
  seriesKey: string
  seriesCue: ChartSeriesCue
  label: string
  mark: MarkType
  /** The SVG path `d`. */
  d: string
  /** Row index, for a `bar`/wedge — `null` for a whole-series line or area. */
  index: number | null
  active: boolean
  dimmed: boolean
}
ChartPadding from @llui/components
export interface ChartPadding {
  top: number
  right: number
  bottom: number
  left: number
}
ChartParts from @llui/components
export interface ChartParts {
  root: {
    'data-scope': 'chart'
    'data-part': 'root'
    // Spelled out rather than aliased: `scripts/test/registry-attrs.test.ts`
    // reads part-bag VALUES syntactically, and an imported alias reads as an
    // open type it declines to give a verdict on.
    'data-coord': ReadSignal<'cartesian' | 'polar'>
    'data-domain': ReadSignal<'value' | 'share'>
    'data-active': ReadSignal<'' | undefined>
    /** Per-instance forced-colors fill custom properties — see `connect()`'s
     * own doc for why these must be per-chart rather than a shared global. */
    style: string
  }
  /**
   * The `<svg>`. `role="img"` with a name and description is what a screen
   * reader announces; the real data path is {@link ChartParts.table}, because
   * AT support for the WAI-ARIA graphics roles is still thin enough that a
   * chart relying on them alone is unreadable.
   *
   * `tabindex="0"` makes the plot itself the keyboard target: arrows walk the
   * rows, Home/End jump to the ends, Escape clears. There is no
   * `aria-activedescendant` — it is not valid on `role="img"`, and the tooltip
   * live region is what actually announces the cursor.
   */
  svg: {
    'data-scope': 'chart'
    'data-part': 'svg'
    role: 'img'
    'aria-labelledby': string
    viewBox: ReadSignal<string>
    tabindex: 0
    onKeyDown: (e: KeyboardEvent) => void
    onPointerMove: (e: PointerEvent) => void
    onPointerLeave: (e: PointerEvent) => void
    onBlur: (e: FocusEvent) => void
  }
  title: { id: string; 'data-scope': 'chart'; 'data-part': 'title' }
  desc: { id: string; 'data-scope': 'chart'; 'data-part': 'desc' }
  /** The visually-hidden `<table>` fallback. Render it with the rows below. */
  table: {
    'data-scope': 'chart'
    'data-part': 'table'
    'aria-label': ReadSignal<string>
  }
  /** Tooltip ATTRIBUTES — spreadable, with its own reactive `hidden`. */
  tooltip: {
    'data-scope': 'chart'
    'data-part': 'tooltip'
    role: 'status'
    'aria-live': 'polite'
    hidden: ReadSignal<boolean>
    style: ReadSignal<string>
  }
  /** A `<g>` stacking layer. Static — spread it on each layer group. */
  layer: { 'data-scope': 'chart'; 'data-part': 'layer' }
  /** A value gridline. Pass `d` from the {@link ChartGridLine}. */
  grid: { 'data-scope': 'chart'; 'data-part': 'grid' }
  /** An axis label — value ticks and category names. */
  axisLabel: { 'data-scope': 'chart'; 'data-part': 'axis-label' }
  /** Attributes for one vertex dot on a line or area series. */
  dotProps: (vertex: ChartVertex) => {
    'data-scope': 'chart'
    'data-part': 'dot'
    'data-series': string
    'data-series-cue': ChartSeriesCue
    'data-active': '' | undefined
    cx: number
    cy: number
  }
  legendItem: (key: string) => {
    type: 'button'
    'data-scope': 'chart'
    'data-part': 'legend-item'
    'data-series': string
    'data-series-cue': ReadSignal<ChartSeriesCue>
    'data-dimmed': ReadSignal<'' | undefined>
    'aria-pressed': ReadSignal<boolean>
    onClick: (e: MouseEvent) => void
  }
  /** The legend's colour chip. Spreadable onto its own element (a `<span>` in
   * both skins) so a forced-colors rule can key off `data-series-cue` the
   * SAME way a mark does — a legend swatch that only carries `--mark-color`
   * paints identically for every series once forced colors flattens author
   * colour, which is the accessibility gap #264 review item 7 names. */
  legendSwatch: (key: string) => {
    'data-scope': 'chart'
    'data-part': 'legend-swatch'
    'data-series-cue': ReadSignal<ChartSeriesCue>
  }
  /** Attributes for one drawn mark. Spread onto a `<path>` and pass `d`. */
  markProps: (mark: ChartMark) => {
    'data-scope': 'chart'
    'data-part': 'mark'
    'data-mark': 'line' | 'area' | 'bar'
    'data-series': string
    'data-series-cue': ChartSeriesCue
    'data-active': '' | undefined
    'data-dimmed': '' | undefined
    d: string
    onPointerEnter: (e: PointerEvent) => void
  }
  // Derived geometry, as signals the view renders with `each`.
  marks: ReadSignal<ChartMark[]>
  vertices: ReadSignal<ChartVertex[]>
  gridLines: ReadSignal<ChartGridLine[]>
  categoryTicks: ReadSignal<ChartCategoryTick[]>
  tooltipRows: ReadSignal<ChartTooltipRow[]>
  activeLabel: ReadSignal<string>
  rows: ReadSignal<ChartRow[]>
  series: ReadSignal<ChartSeries[]>
}
ChartRow from @llui/components

One row of the independent axis: a category and its value per series.

export interface ChartRow {
  label: string
  values: Record<string, number>
}
ChartSeries from @llui/components
export interface ChartSeries {
  /** Stable key. Also the `--color-<key>` variable a skin reads. */
  key: string
  /** Human label, used by the legend, the tooltip and the a11y table. */
  label: string
  mark: MarkType
  /** Ignored by a polar projection, which supports `linear` only. */
  curve?: Curve
}
ChartState from @llui/components
export interface ChartState {
  coord: ChartCoord
  /**
   * Equal slots per category (`value`), or slots proportional to the value
   * (`share`). See {@link ChartDomain}; `share` + `coord: 'polar'` is a pie.
   *
   * Under `share` only `bar` series are drawn: a line or an area along an axis
   * whose spacing already encodes the magnitude would plot each point at a
   * position that means something other than where it sits, so they are
   * DECLINED rather than approximated — the same call `polarProjection` makes
   * about `monotone`. `stacked` is ignored for the same reason, each category
   * being its own slice already.
   */
  domain: ChartDomain
  series: ChartSeries[]
  rows: ChartRow[]
  /** Accessible name for the whole chart. */
  label: string
  /** Longer description, announced with the name. */
  description: string
  width: number
  height: number
  padding: ChartPadding
  /**
   * Explicit value-axis bounds. UNBOUNDED-CAPABLE in the sense of `finiteBound`
   * (#177): "derive it" is spelled by OMITTING the key, never by `undefined`,
   * `null` or `±Infinity` — only the omission survives a JSON round trip
   * unchanged.
   */
  min?: number
  max?: number
  tickCount: number
  /** Stack series instead of overlaying them. */
  stacked: boolean
  /** Row index under the pointer or the keyboard cursor. */
  activeIndex: number | null
  /** Series key the legend has isolated, or `null` for all. */
  activeSeries: string | null
  /** Polar only: inner radius as a fraction of the outer (0 = pie, .5 = donut). */
  innerRadius: number
  /** Cartesian only: swap the axes for a horizontal bar chart. */
  horizontal: boolean
}
ChartTooltipRow from @llui/components
export interface ChartTooltipRow {
  seriesKey: string
  label: string
  value: number
  /**
   * The row's fraction of its series' total under a share domain — what a pie
   * tooltip shows as a percentage — and `null` under a value domain, where a
   * share of an axis that may cross zero is not defined.
   */
  share: number | null
}
ChartVertex from @llui/components

A vertex on a line or area series, for the dot layer and hit feedback.

export interface ChartVertex {
  seriesKey: string
  seriesCue: ChartSeriesCue
  index: number
  x: number
  y: number
  active: 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': ReadSignal<'true' | 'false' | 'mixed'>
    'aria-disabled': ReadSignal<'true' | undefined>
    'aria-required': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'checked' | 'unchecked' | 'indeterminate'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'checkbox'
    'data-part': 'root'
    tabindex: ReadSignal<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: ReadSignal<boolean>
    indeterminate: ReadSignal<boolean>
    disabled: ReadSignal<boolean>
    required: ReadSignal<boolean>
    'data-scope': 'checkbox'
    'data-part': 'hidden-input'
  }
  /** Optional indicator child (the checkmark). */
  indicator: {
    'data-state': ReadSignal<'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': ReadSignal<'' | undefined>
    'data-failed': ReadSignal<'' | undefined>
  }
  trigger: {
    type: 'button'
    'aria-label': string
    'data-scope': 'clipboard'
    'data-part': 'trigger'
    'data-copied': ReadSignal<'' | undefined>
    'data-failed': ReadSignal<'' | undefined>
    onClick: (e: MouseEvent) => void
  }
  input: {
    type: 'text'
    readonly: true
    value: ReadSignal<string>
    'data-scope': 'clipboard'
    'data-part': 'input'
    onFocus: (e: FocusEvent) => void
  }
  indicator: {
    'data-scope': 'clipboard'
    'data-part': 'indicator'
    'data-copied': ReadSignal<'' | undefined>
    'data-failed': ReadSignal<'' | 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.

copy is a REQUEST and does not set copied: the write can fail (permission denied, insecure context, browser policy) and indicator carries aria-live="polite", so a flag set before the promise resolves ANNOUNCES a success that never happened (#232). Dispatch copied from the resolved write, copyFailed from the rejected one, and reset to clear the feedback:

onEffect(effect, send) {
  copyToClipboard(effect.value).then(
    () => send({ type: 'copied' }),
    () => send({ type: 'copyFailed' }), // refused — say so, never "copied"
  )
}

copyFailed publishes data-failed (root, trigger, indicator); the skin tells the user the write was refused and that the value is still selectable in the read-only input (#266).

export interface ClipboardState {
  value: string
  /** The last write RESOLVED. Exclusive with `failed`. */
  copied: boolean
  /**
   * The last write was REFUSED (permission denied, insecure context, browser
   * policy). Exclusive with `copied`. The read-only `input` is the fallback: the
   * user can still select the value and copy it by hand, which is what a skin
   * should say when this is set (#266).
   */
  failed: boolean
}
CollapsibleInit from @llui/components
export interface CollapsibleInit {
  open?: boolean
  disabled?: boolean
  /** Opt into retained exit motion. Defaults off to guarantee no event/no hang. */
  animated?: boolean
}
CollapsibleParts from @llui/components
export interface CollapsibleParts {
  root: {
    'data-state': ReadSignal<'open' | 'closed'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'collapsible'
    'data-part': 'root'
  }
  trigger: {
    type: 'button'
    'aria-expanded': ReadSignal<boolean>
    'aria-controls': string
    id: string
    disabled: ReadSignal<boolean>
    'data-state': ReadSignal<'open' | 'closed'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'collapsible'
    'data-part': 'trigger'
    onClick: (e: MouseEvent) => void
  }
  content: {
    role: 'region'
    id: string
    'aria-labelledby': string
    hidden: ReadSignal<boolean>
    'data-state': ReadSignal<'open' | 'closing' | 'closed'>
    'data-scope': 'collapsible'
    'data-part': 'content'
    'aria-hidden': ReadSignal<'true' | undefined>
    inert: ReadSignal<boolean>
    onAnimationStart: (e: AnimationEvent) => void
    onAnimationEnd: (e: AnimationEvent) => void
    onAnimationCancel: (e: AnimationEvent) => void
    onTransitionStart: (e: TransitionEvent) => void
    onTransitionEnd: (e: TransitionEvent) => void
    onTransitionCancel: (e: TransitionEvent) => void
  }
  /**
   * Settles a retained `closing` panel once the content's own exit
   * animation/transition ends — or immediately, if the skin runs no exit
   * motion at all. This only ever has anything to settle for a
   * PROGRAMMATIC close/toggle/setOpen when that message carried
   * `retain: true`, which `parts.close()` stamps for you; a raw
   * `send({ type: 'close' })` with no `retain` closes instantly and never
   * enters `closing` at all. Its mount ALSO reports whether it is placed at
   * all: `animated: true` only ever retains `closing` while this is mounted
   * (#264 item F1) — forgetting to place it degrades gracefully to an
   * instant close (with a one-time dev warning) rather than hanging
   * `closing` + `inert` forever, so placing it is no longer required for
   * SAFETY, only for the requested exit animation to actually run on a
   * retained close. A click-driven close is still safety-netted
   * synchronously inside the trigger regardless of whether this is placed.
   */
  exitCompletion: Mountable
  /**
   * An animated-aware programmatic close (#264 review-264j) — see
   * `accordion.ts`'s identical part for the full rationale. A raw
   * `send({ type: 'close' })` carries no `retain` and closes INSTANTLY;
   * `parts.close()` stamps `retain` from the runtime registry.
   */
  close: () => void
}
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
  /** Presentation-only retention while an opted-in exit animation runs. */
  closing: boolean
  /** Monotonic generation used to reject stale animation completion events. */
  exitGeneration: number
  /** Whether close waits for the content's own animation end/cancel event. */
  animated: boolean
}
ColorPickerConnectOptions from @llui/components
export interface ConnectOptions {
  hueLabel?: string
  saturationLabel?: string
  lightnessLabel?: string
  chromaLabel?: string
  oklchLightnessLabel?: string
  hexLabel?: string
  eyeDropperLabel?: string
  /** aria-label for the 2D 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, or the OKLCH
   * equivalent scaled by `state.maxChroma`). Default 1. */
  step?: number
  /** Coarse keyboard step for the area thumb when Shift is held. Default 10. */
  coarseStep?: number
}
ColorPickerInit from @llui/components
export interface ColorPickerInit {
  /** Which model the canonical color is stored in. Default `'hsv'`. */
  model?: ColorModel
  /** Initial color as any CSS color string the shared parser accepts (hex,
   * `rgb()`, `hsl()`, `oklch()`, `oklab()`, a named color). Takes precedence
   * over `hsl`/`hsv`/`oklch` below, and over `alpha` for the color's own
   * alpha component (an explicit `alpha` option still wins). */
  color?: string
  /** Initial color as HSL (converted into the canonical store). */
  hsl?: Hsl
  /** Initial color as HSV. Takes precedence over `hsl`. */
  hsv?: Hsv
  /** Initial color as OKLCH. Takes precedence over `hsl`/`hsv`. */
  oklch?: Oklch
  alpha?: number
  disabled?: boolean
  /** Upper bound for OKLCH chroma. Must be finite and > 0 — anything else
   * (including omission) falls back to {@link DEFAULT_MAX_CHROMA}. */
  maxChroma?: number
}
ColorPickerParts from @llui/components
export interface ColorPickerParts {
  root: {
    'data-scope': 'color-picker'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
    'data-model': ReadSignal<ColorModel>
    /** Bare boolean (package convention): present when the current OKLCH
     * color falls outside sRGB. Always absent in HSV mode (an HSV color is
     * an sRGB parameterization by construction). */
    'data-out-of-gamut': ReadSignal<'' | undefined>
  }
  /** Cycles the active model between `'hsv'` and `'oklch'`. */
  modelToggle: {
    type: 'button'
    'aria-label': ReadSignal<string>
    disabled: ReadSignal<boolean>
    'data-scope': 'color-picker'
    'data-part': 'model-toggle'
    'data-model': ReadSignal<ColorModel>
    onClick: (e: MouseEvent) => void
  }
  hueSlider: {
    type: 'range'
    min: 0
    max: 360
    step: 1
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<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: ReadSignal<boolean>
    value: ReadSignal<string>
    style: ReadSignal<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: ReadSignal<boolean>
    value: ReadSignal<string>
    style: ReadSignal<string>
    'data-scope': 'color-picker'
    'data-part': 'lightness-slider'
    onInput: (e: Event) => void
  }
  /** OKLCH chroma, `0..state.maxChroma`. */
  chromaSlider: {
    type: 'range'
    min: 0
    max: ReadSignal<number>
    step: ReadSignal<number>
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    style: ReadSignal<string>
    'data-scope': 'color-picker'
    'data-part': 'chroma-slider'
    onInput: (e: Event) => void
  }
  /** OKLCH perceptual lightness, `0..1`. */
  oklchLightnessSlider: {
    type: 'range'
    min: 0
    max: 1
    step: number
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    style: ReadSignal<string>
    'data-scope': 'color-picker'
    'data-part': 'oklch-lightness-slider'
    onInput: (e: Event) => void
  }
  hexInput: {
    type: 'text'
    autocomplete: 'off'
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<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: ReadSignal<string>
  }
  /** The 2D area track. The machine owns the pointer-drag lifecycle
   * (capture on down, released on up/cancel, primary button only, ignored
   * while disabled): it computes from `currentTarget.getBoundingClientRect()`
   * and dispatches `setSv` (HSV mode, via `colorFromPoint`) or `setLc`
   * (OKLCH mode, via `lcFromPoint`) — see `utils/pointer-drag.ts`. Keyboard
   * continues from wherever a drag left off because `pointerdown` focuses
   * `areaThumb`. */
  area: {
    'data-scope': 'color-picker'
    'data-part': 'area'
    'data-model': ReadSignal<ColorModel>
    // HSV-mode hue backdrop only; empty in OKLCH mode, where `areaCanvas`
    // paints the plane instead (the sRGB gamut boundary is not expressible
    // as a CSS gradient).
    style: ReadSignal<string>
    onPointerDown: (e: PointerEvent) => void
    onPointerMove: (e: PointerEvent) => void
    onPointerUp: (e: PointerEvent) => void
    onPointerCancel: (e: PointerEvent) => void
    onLostPointerCapture: (e: PointerEvent) => void
  }
  /** Decorative `<canvas>` seam for the OKLCH area — headless, so the
   * consumer owns creating and sizing the real `<canvas>` element (give it
   * an id and spread these props), but never its own repaint wiring: place
   * {@link areaCanvasBinding}`(state, thatSameId)` anywhere in the view and
   * it stays painted, on mount and on every hue/`maxChroma` change. Absent/
   * inert in HSV mode. */
  areaCanvas: {
    'data-scope': 'color-picker'
    'data-part': 'area-canvas'
    'aria-hidden': 'true'
  }
  /** The draggable thumb inside the 2D area. Keyboard-operable (arrows move
   * S/V or C/L depending on the active model; Shift = coarse) with
   * role="slider" and a 2D aria-valuetext. `aria-label` follows the ACTIVE
   * model ("Saturation / Value" in HSV mode, "Chroma / Lightness" in OKLCH),
   * not a fixed string — the axes it labels are literally different. */
  areaThumb: {
    role: 'slider'
    'aria-label': ReadSignal<string>
    'aria-valuemin': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number>
    'aria-valuetext': ReadSignal<string>
    'aria-disabled': ReadSignal<'true' | undefined>
    tabindex: ReadSignal<number>
    'data-scope': 'color-picker'
    'data-part': 'area-thumb'
    style: ReadSignal<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: ReadSignal<boolean>
    value: ReadSignal<string>
    style: ReadSignal<string>
    'data-scope': 'color-picker'
    'data-part': 'alpha-slider'
    onInput: (e: Event) => void
  }
  /** EyeDropper API trigger. Always PLACED, but `hidden`/`disabled`/
   * `data-unsupported` all derive from `state.eyeDropperSupported` — a
   * consumer writes no feature-detect code of its own; see
   * {@link eyeDropperSupportMount}. */
  eyeDropperTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    hidden: ReadSignal<boolean>
    /** Bare boolean (package convention). */
    'data-unsupported': ReadSignal<'' | undefined>
    'data-scope': 'color-picker'
    'data-part': 'eyedropper-trigger'
    onClick: (e: MouseEvent) => 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 {
  color:
    | { model: 'hsv'; h: number; s: number; v: number }
    | { model: 'oklch'; l: number; c: number; h: number }
  /** Alpha channel 0..1. */
  alpha: number
  disabled: boolean
  /** Upper bound for OKLCH chroma — the ONE source of truth for the reducer's
   * clamp (`setOklch`/`setChroma`/`setLc`/`nudgeLc`) AND for `connect()`'s
   * chroma slider/area rendering range, so the two can no longer disagree
   * the way a `ConnectOptions.maxChroma` rendering-only override could.
   * Always finite and > 0 (`init` validates it; there is no setter — it is
   * fixed for the component's lifetime, like `slider`'s `min`/`max`). */
  maxChroma: number
  /** Whether the browser's EyeDropper API is available. ALWAYS `false` at
   * `init()` — this is what keeps SSR and the client's first paint identical
   * (see the module doc on {@link eyeDropperSupportMount}, the ONLY thing
   * that ever flips it, from a real mount-time feature check). A consumer
   * never feature-detects on its own: placing that one Mountable is the
   * whole contract. */
  eyeDropperSupported: 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'
    /** The group label's id, or absent when `hasLabel: false`. */
    'aria-labelledby': string | undefined
    '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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'selected' | undefined>
    'data-highlighted': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | 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': ReadSignal<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: ReadSignal<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': ReadSignal<'open' | 'closed'>
  }
  input: {
    type: 'text'
    role: 'combobox'
    autocomplete: 'off'
    'aria-autocomplete': 'list'
    'aria-expanded': ReadSignal<boolean>
    'aria-controls': string
    'aria-activedescendant': ReadSignal<string | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    id: string
    disabled: ReadSignal<boolean>
    value: ReadSignal<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': ReadSignal<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': ReadSignal<'true' | undefined>
    tabindex: -1
    'data-state': ReadSignal<'open' | 'closed'>
    'data-status': ReadSignal<AsyncStatus>
    /** The mutually-exclusive load projection (#265 finding 11) — see
     * {@link LoadProjection}. Mirrors the top-level `loadState` signal. */
    'data-load-state': ReadSignal<LoadProjection>
    'data-scope': 'combobox'
    'data-part': 'content'
  }
  /** The mutually-exclusive async load projection: `'initial-empty'` |
   * `'loading'` | `'stale-results'` | `'success'` | `'error'`. A single
   * signal instead of independent `isLoading`/`isEmpty`/`hasError` booleans,
   * so it can never contradict itself. See {@link LoadProjection}. */
  loadState: ReadSignal<LoadProjection>
  /** 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`. */
  /**
   * A labelled group's parts. The group names its `label` part through
   * `aria-labelledby`; a consumer that renders the group WITHOUT that label
   * passes `{ hasLabel: false }` so it never names an element that does not
   * exist (an unlabelled `role="group"` is valid; a broken idref is not — the
   * `dialog` `hasDescription` rule, #268). Default: true.
   */
  group: (id: string, options?: { readonly hasLabel?: boolean }) => 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: ReadSignal<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 {@link ContextMenuState}'s
   * `dir`/`dirSource`, resolved from the mounted trigger by `directionSync`). */
  dir?: TextDirection
  /** When false, opening and closing play enter/exit animations and the content
   * stays mounted (status 'opening' or '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: ReadSignal<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. `id` is REQUIRED — it is
   * the scope `directionSync` (below) observes for live ancestor `dir`
   * changes, since it (unlike `content`) is always mounted (#265 finding 6). */
  trigger: {
    id: string
    'data-scope': 'context-menu'
    'data-part': 'trigger'
    onContextMenu: (e: MouseEvent) => void
  }
  positioner: {
    'data-scope': 'context-menu'
    'data-part': 'positioner'
    style: ReadSignal<string>
  }
  content: {
    role: 'menu'
    id: string
    /** Virtually-focused (highlighted) item id at the root level. */
    'aria-activedescendant': ReadSignal<string | undefined>
    tabindex: -1
    /** Reflects the presence lifecycle: 'opening' | 'open' | 'closing' | 'closed'.
     * Stays mounted while 'closing' so the exit animation can run. */
    'data-state': ReadSignal<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
  /**
   * A labelled group's parts. The group names its `label` part through
   * `aria-labelledby`; a consumer that renders the group WITHOUT that label
   * passes `{ hasLabel: false }` so it never names an element that does not
   * exist (an unlabelled `role="group"` is valid; a broken idref is not — the
   * `dialog` `hasDescription` rule, #268). Default: true.
   */
  group: (id: string, options?: { readonly hasLabel?: boolean }) => ContextMenuGroupParts
  separator: () => ContextMenuSeparatorParts
  subTrigger: (value: string) => ContextMenuSubTriggerParts
  subPositioner: (value: string) => ContextMenuSubPositionerParts
  subContent: (value: string) => ContextMenuSubContentParts
  /** Place once anywhere in the same build to keep automatic direction live —
   * the shared `@llui/interactions` direction-sync seam (#265 finding 6),
   * observing the TRIGGER (always mounted, unlike the content) as this
   * instance's root. A discarded `Mountable` is inert, so this must be placed
   * in the view. */
  directionSync: Mountable
}
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': ReadSignal<'' | undefined>
    'data-invalid': ReadSignal<'' | undefined>
  }
  input: {
    type: 'text'
    inputmode: 'numeric'
    autocomplete: 'off'
    spellcheck: false
    value: ReadSignal<string>
    disabled: ReadSignal<boolean>
    readonly: ReadSignal<boolean>
    required: ReadSignal<boolean>
    'aria-invalid': ReadSignal<'true' | undefined>
    /**
     * The error text's id while the value is invalid — so the field names its
     * own error rather than relying on the one-shot `role="alert"`
     * announcement (#268). Absent without a `ConnectOptions.id`.
     */
    'aria-describedby': ReadSignal<string | 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: ReadSignal<boolean>
    'data-scope': 'date-input'
    'data-part': 'clear-trigger'
    onClick: (e: MouseEvent) => void
  }
  errorText: {
    /** `<id>:error` when `ConnectOptions.id` is given. */
    id: string | undefined
    role: 'alert'
    'aria-live': 'polite'
    'data-scope': 'date-input'
    'data-part': 'error-text'
    hidden: ReadSignal<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
  /** Pin "today" (YYYY-MM-DD); omit or `null` to read the runtime clock. */
  today?: string | null
  /** Individually unavailable dates (YYYY-MM-DD). */
  unavailable?: string[]
}
DatePickerParts from @llui/components
export interface DatePickerParts {
  root: {
    'data-scope': 'date-picker'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | 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': ReadSignal<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: ReadSignal<boolean>
    'data-scope': 'date-picker'
    'data-part': 'prev-month-trigger'
    onClick: (e: MouseEvent) => void
  }
  nextMonthTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<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
  /**
   * A pinned "today" (YYYY-MM-DD), or `null` to read the runtime clock. Pin it
   * for deterministic rendering (SSR + hydration, galleries, tests) or to show
   * today in a specific time zone — see `todayInTimeZone` (#266).
   */
  today: string | null
  /**
   * Individually unavailable dates (YYYY-MM-DD) — bookings, holidays. Disabled
   * like an out-of-bounds date, published separately as `data-unavailable`, and
   * a range may not be completed across one (#266).
   */
  unavailable: string[]
}
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
  /** True for a date listed in `unavailable` (also `isDisabled`). */
  isUnavailable: 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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    tabindex: ReadSignal<number>
    'data-scope': 'date-picker'
    'data-part': 'day-cell'
    /** The cell's identity — the one genuinely static attribute. */
    'data-date': string
    'data-in-month': ReadSignal<'' | undefined>
    'data-today': ReadSignal<'' | undefined>
    'data-selected': ReadSignal<'' | undefined>
    'data-focused': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | undefined>
    'data-range-start': ReadSignal<'' | undefined>
    'data-range-end': ReadSignal<'' | undefined>
    'data-in-range': ReadSignal<'' | undefined>
    /** An individually unavailable date (also disabled) — distinct from out-of-bounds. */
    'data-unavailable': ReadSignal<'' | 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: ReadSignal<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': ReadSignal<boolean>
    'aria-controls': string
    id: string
    'data-state': ReadSignal<'open' | 'closed'>
    'data-scope': 'dialog'
    'data-part': 'trigger'
    onClick: (e: MouseEvent) => void
  }
  backdrop: {
    'data-state': ReadSignal<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
    /** The description part's id, or absent when `hasDescription: false`. */
    'aria-describedby': string | undefined
    tabindex: -1
    'data-state': ReadSignal<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;
}
Domain from @llui/components

A closed numeric interval. Both ends are finite by construction.

export interface Domain {
  min: number
  max: number
}
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: droppedMove(prev, msg) names the move a message COMPLETES — a pointer drop, or a keyboard toggleGrab while an item is grabbed — and reorder(arr, from, to) applies it. Watch currentIndex during a drag for a live preview.

type State = { items: string[]; sort: SortableState }

update: (state, msg) => {
  switch (msg.type) {
    case 'sort': {
      const moved = sortable.droppedMove(state.sort, msg.msg)
      const [sort] = sortable.update(state.sort, msg.msg)
      const items = moved ? reorder(state.items, moved.from, moved.to) : state.items
      return [{ items, sort }, []]
    }
  }
}

// `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.

Screen readers. A keyboard user who grabs, moves, drops or cancels needs to HEAR it — nothing on screen tells them where the item is. The machine therefore owns two more parts, and both must be rendered:

  • liveRegion — a polite, atomic role="status" region whose text (a Signal, rendered as the region's CHILD, never spread as an attribute) announces "Picked up Apple, item 2 of 5.", "Apple moved to position 3 of 5.", the drop and a cancel. Render it visually hidden (sr-only), never display: none — a hidden live region is never announced. Pass itemLabel to name the item; without it the announcements say "item".
  • instructions — the how-to text, hidden, which every handle references through aria-describedby (a directly referenced hidden element still provides a description, and stays out of the reading order). A consumer that supplies its own keyboard help and does not render this part passes hasInstructions: false, so no handle names an element that does not exist (the dialog hasDescription precedent, #268).
const { text: live, ...liveAttrs } = s.liveRegion
const { text: howTo, ...howToAttrs } = s.instructions
div({ ...liveAttrs, class: 'sr-only' }, [text(live)])
div({ ...howToAttrs }, [text(howTo)])

The grab is aria-pressed on the handle (a toggle button: pressed while the item is carried). aria-grabbed is NOT used: ARIA 1.1 deprecated it and aria-dropeffect with no replacement, and screen readers never broadly exposed either — the live region is how every mature implementation (dnd-kit, React Aria) conveys a drag. aria-roledescription is deliberately not set: it would replace "toggle button", the one cue that Space operates the handle. All text comes from LocaleContext (Locale['sortable']).

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
  /**
   * How many items the origin container held when the drag started — the "N"
   * in "item 2 of N", and the last slot a keyboard `moveBy` may reach.
   */
  count: 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: ReadSignal<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': ReadSignal<boolean>
    'aria-controls': string
    id: string
    'data-state': ReadSignal<'open' | 'closed'>
    'data-scope': 'drawer'
    'data-part': 'trigger'
    onClick: (e: MouseEvent) => void
  }
  backdrop: {
    'data-state': ReadSignal<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': ReadSignal<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': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | undefined>
  }
  preview: {
    tabindex: ReadSignal<number>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-scope': 'editable'
    'data-part': 'preview'
    hidden: ReadSignal<boolean>
    onClick: (e: MouseEvent) => void
    onFocus: (e: FocusEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  input: {
    'data-scope': 'editable'
    'data-part': 'input'
    hidden: ReadSignal<boolean>
    value: ReadSignal<string>
    disabled: ReadSignal<boolean>
    onInput: (e: Event) => void
    onKeyDown: (e: KeyboardEvent) => void
    onBlur: (e: FocusEvent) => void
  }
  submitTrigger: {
    type: 'button'
    /** Disabled with the editable: a disabled instance ignores the message
     *  this trigger sends, so an enabled trigger would be a dead control
     *  (#268 audit). */
    disabled: ReadSignal<boolean>
    'data-scope': 'editable'
    'data-part': 'submit-trigger'
    onClick: (e: MouseEvent) => void
  }
  cancelTrigger: {
    type: 'button'
    /** Disabled with the editable: a disabled instance ignores the message
     *  this trigger sends, so an enabled trigger would be a dead control
     *  (#268 audit). */
    disabled: ReadSignal<boolean>
    'data-scope': 'editable'
    'data-part': 'cancel-trigger'
    onClick: (e: MouseEvent) => void
  }
  editTrigger: {
    type: 'button'
    /** Disabled with the editable: a disabled instance ignores the message
     *  this trigger sends, so an enabled trigger would be a dead control
     *  (#268 audit). */
    disabled: ReadSignal<boolean>
    '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
}
EyeDropperOpenOptions from @llui/components
export interface EyeDropperOpenOptions {
  signal?: AbortSignal
}
EyeDropperResult from @llui/components
export interface EyeDropperResult {
  sRGBHex: string
}
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': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | undefined>
    'aria-disabled': ReadSignal<'true' | 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': ReadSignal<string | undefined>
    'aria-invalid': ReadSignal<'true' | undefined>
    'aria-required': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    readOnly: ReadSignal<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
  /**
   * Whether the consumer renders the `legend` part (default: true). When
   * false the group omits `aria-labelledby` rather than naming an element
   * that does not exist (the `dialog` `hasDescription` rule, #268). A
   * `<fieldset>` without a legend is valid HTML; a broken idref is not.
   */
  hasLegend?: boolean
}
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'
    /** The legend's id, or absent when `hasLegend: false`. */
    'aria-labelledby': string | undefined
    'aria-disabled': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    'data-scope': 'fieldset'
    'data-part': 'root'
    'data-invalid': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | 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
    /** The file's upload status; absent until an upload is reported (#266). */
    'data-upload-status': ReadSignal<FileUploadStatus | undefined>
  }
  /** A labelled progressbar, shown only while the file is uploading (#266). */
  itemProgress: {
    role: 'progressbar'
    'aria-label': string
    'aria-valuemin': 0
    'aria-valuemax': 100
    'aria-valuenow': ReadSignal<number | undefined>
    hidden: ReadSignal<boolean>
    'data-scope': 'file-upload'
    'data-part': 'item-progress'
  }
  /** The progress fill; its width is written inline as a percentage. */
  itemProgressRange: {
    'data-scope': 'file-upload'
    'data-part': 'item-progress-range'
    style: ReadSignal<string>
  }
  /**
   * Live region for the file's failure message, shown only on error. Its text
   * is `uploadOf(state, id)?.error` — the view renders it as a child.
   */
  itemErrorText: {
    role: 'alert'
    hidden: ReadSignal<boolean>
    'data-scope': 'file-upload'
    'data-part': 'item-error-text'
  }
  /** Shown only for a failed upload; dispatches `retryUpload` for this file. */
  itemRetryTrigger: {
    type: 'button'
    'aria-label': string
    hidden: ReadSignal<boolean>
    'data-scope': 'file-upload'
    'data-part': 'item-retry-trigger'
    onClick: (e: MouseEvent) => void
  }
  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': ReadSignal<'' | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-dragging': ReadSignal<'' | undefined>
    'data-invalid': ReadSignal<'' | undefined>
    'data-readonly': ReadSignal<'' | undefined>
    /** Present while any accepted file is uploading (#266). */
    'data-uploading': ReadSignal<'' | undefined>
  }
  dropzone: {
    'data-scope': 'file-upload'
    'data-part': 'dropzone'
    'data-dragging': ReadSignal<'' | 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: ReadSignal<boolean>
    onClick: (e: MouseEvent) => void
  }
  hiddenInput: {
    type: 'file'
    tabindex: -1
    'aria-hidden': 'true'
    style: string
    disabled: ReadSignal<boolean>
    multiple: ReadSignal<boolean>
    accept: ReadSignal<string>
    required: ReadSignal<boolean>
    'aria-invalid': ReadSignal<'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
}
FileUploadProgress from @llui/components
export interface FileUploadProgress {
  status: FileUploadStatus
  /** 0..1 */
  progress: number
  /** The failure message while `status === 'error'`, else `null`. */
  error: string | null
}
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
  /**
   * Upload lifecycle per accepted file, keyed by `FileMeta.id` — never by
   * index, which shifts when an earlier file is removed. A file with no entry
   * has not started uploading. The machine does not upload: the consumer's
   * effect does, and reports back with `uploadProgress` / `uploadSucceeded` /
   * `uploadFailed`; `retryUpload` is the request to try again (#266).
   */
  uploads: Record<string, FileUploadProgress>
}
FloatingOptions from @llui/components
export interface FloatingOptions {
    /** The reference element (trigger/anchor). */
    anchor: Element;
    /** The floating element (content). */
    floating: HTMLElement;
    /**
     * Element that receives the resolved full `data-placement` and physical
     * `data-side`. Defaults to `floating`. Use a separate content element when
     * `floating` is a geometry-only positioner wrapper.
     */
    stateTarget?: 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': ReadSignal<'' | undefined>
    'data-resizing': ReadSignal<'' | undefined>
    'data-minimized': ReadSignal<'' | undefined>
    'data-maximized': ReadSignal<'' | undefined>
    hidden: ReadSignal<boolean>
    style: ReadSignal<string>
  }
  /**
   * Pointer drag starts here, and it is also a keyboard stop (#266): arrows
   * move the panel 10px (50px with Shift). Physical under RTL — the panel is
   * positioned with physical `left`/`top`.
   */
  dragHandle: {
    /**
     * A focusable, NAMED group: without a role an `aria-label` is prohibited
     * on a generic element and assistive tech announced nothing (#268 audit).
     * `group`, not `button`: the handle is the title bar and CONTAINS the
     * minimize/maximize/close buttons (a button may not), and its action is
     * the arrow keys `aria-keyshortcuts` names, not an activation.
     */
    role: 'group'
    tabindex: 0
    'aria-label': string
    'aria-keyshortcuts': string
    'data-scope': 'floating-panel'
    'data-part': 'drag-handle'
    onPointerDown: (e: PointerEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  content: {
    'data-scope': 'floating-panel'
    'data-part': 'content'
    hidden: ReadSignal<boolean>
  }
  minimizeTrigger: {
    type: 'button'
    'aria-label': string
    /** A toggle: `'true'` while minimized. */
    'aria-pressed': ReadSignal<'true' | 'false'>
    'data-scope': 'floating-panel'
    'data-part': 'minimize-trigger'
    onClick: (e: MouseEvent) => void
  }
  maximizeTrigger: {
    type: 'button'
    'aria-label': string
    /** A toggle: `'true'` while maximized. */
    'aria-pressed': ReadSignal<'true' | 'false'>
    '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
  }
  /** A resize grip; also a keyboard stop whose arrows resize from this grip (#266). */
  resizeHandle: (handle: ResizeHandle) => {
    /** A focusable, named group (see `dragHandle`): its action is the arrow
     *  keys, and a thin edge grip is a window-chrome affordance, not a
     *  pointer button. */
    role: 'group'
    tabindex: 0
    'aria-label': string
    'aria-keyshortcuts': string
    'data-scope': 'floating-panel'
    'data-part': 'resize-handle'
    'data-handle': ResizeHandle
    onPointerDown: (e: PointerEvent) => void
    onKeyDown: (e: KeyboardEvent) => 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 the first
     * tab-reachable descendant, else the container itself (given a temporary
     * `tabindex="-1"` when it has none). */
    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': ReadSignal<FormStatus>
    'aria-busy': ReadSignal<'true' | undefined>
  }
  field: (name: string) => {
    'data-scope': 'form'
    'data-part': 'field'
    'data-touched': ReadSignal<'' | undefined>
    touched: ReadSignal<boolean>
    onBlur: (e: FocusEvent) => void
  }
  submit: {
    type: 'submit'
    'data-scope': 'form'
    'data-part': 'submit'
    'data-state': ReadSignal<FormStatus>
    disabled: ReadSignal<boolean>
  }
}
FormState from @llui/components
export interface FormState {
  status: FormStatus
  touched: Record<string, boolean>
  submitError: string | null
}
Frame from @llui/components

The plot area in user units — the box marks are drawn inside, already inset by whatever padding the axes need.

export interface Frame {
  x: number
  y: number
  width: number
  height: number
}
GradientCenter from @llui/components
export interface GradientCenter {
  x: number
  y: number
}
GradientInterpolation from @llui/components
export interface GradientInterpolation {
  space: InterpolationSpace
  hue: HueInterpolationMethod
}
GradientPickerConnectOptions from @llui/components
export interface ConnectOptions {
  id: string
  trackLabel?: string
  addStopLabel?: string
  removeStopLabel?: string
  kindLabels?: Partial<Record<GradientKind, string>>
  repeatingLabel?: string
  angleLabel?: string
  centerLabel?: string
  shapeLabels?: Partial<Record<RadialShape, string>>
  sizeLabels?: Partial<Record<RadialSize, string>>
  interpolationSpaceLabel?: string
  interpolationHueLabel?: string
  reverseLabel?: string
  distributeLabel?: string
  cssLabel?: string
  /** Fine keyboard step for a stop / the center thumb (percent units).
   * Default 1. */
  step?: number
  /** Coarse keyboard step when Shift is held. Default 10. */
  coarseStep?: number
  /** Forwarded to the embedded `color-picker`'s own `connect()` (labels,
   * area-thumb step/coarseStep). */
  pickerOptions?: ColorPickerConnectOptions
}
GradientPickerInit from @llui/components
export interface GradientPickerInit {
  /** Parse an initial gradient from a CSS string (via {@link parseGradient}).
   * Takes precedence over `stops`/`kind`/… below. Invalid CSS (or a stop
   * count outside `minStops`/`maxStops`) falls back to the `stops`/defaults
   * path below, same as `color-picker`'s `color` option falling back on an
   * unparsable string. */
  css?: string
  /** Explicit initial stops (ignored if `css` parses). An EMPTY array is
   * treated the same as omitting the option (never an empty gradient — see
   * finding #7); fewer than `minStops` are PADDED. Sorted by `position` on
   * init; ids are assigned sequentially in the SORTED order. */
  stops?: GradientStopInit[]
  kind?: GradientKind
  repeating?: boolean
  /** Sets the initial direction as an explicit angle (linear) or `from`
   * angle (conic). For a `to <side>` initial direction, use `css` instead. */
  angle?: number
  center?: Partial<GradientCenter>
  shape?: RadialShape
  size?: RadialSize
  interpolation?: Partial<GradientInterpolation>
  /** Which model a stop with no explicit `PickerColor`/parseable string
   * starts in. Default `'hsv'`. */
  defaultModel?: ColorModel
  maxChroma?: number
  minStops?: number
  maxStops?: number
  disabled?: boolean
  dir?: 'ltr' | 'rtl'
}
GradientPickerParts from @llui/components
export interface GradientPickerParts {
  root: {
    'data-scope': 'gradient-picker'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
    'data-kind': ReadSignal<GradientKind>
    'data-repeating': ReadSignal<'' | undefined>
  }
  preview: {
    'data-scope': 'gradient-picker'
    'data-part': 'preview'
    'aria-hidden': 'true'
    style: ReadSignal<string>
  }
  /** The horizontal stop ramp: a plain `in <space>` linear ramp of the SAME
   * stop list `toCss` serializes (finding #3 — one shared builder, never two
   * strings that can drift), including the hue method and the
   * `repeating-`/rtl-mirrored angle so what the track PAINTS matches
   * `colorAt` at every position, regardless of `state.kind`. Pointerdown on
   * the bare track (not on an existing stop, which calls
   * `stopPropagation()`) adds a new stop at that position and starts
   * dragging it immediately. */
  track: {
    'data-scope': 'gradient-picker'
    'data-part': 'track'
    style: ReadSignal<string>
    onPointerDown: (e: PointerEvent) => void
    onPointerMove: (e: PointerEvent) => void
    onPointerUp: (e: PointerEvent) => void
    onPointerCancel: (e: PointerEvent) => void
    onLostPointerCapture: (e: PointerEvent) => void
  }
  stop: (id: string) => GradientStopParts
  addStopButton: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'gradient-picker'
    'data-part': 'add-stop-button'
    onClick: (e: MouseEvent) => void
  }
  removeStopButton: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'gradient-picker'
    'data-part': 'remove-stop-button'
    onClick: (e: MouseEvent) => void
  }
  kindToggle: (kind: GradientKind) => ToggleItemParts
  repeatingToggle: {
    type: 'button'
    'aria-label': string
    'aria-pressed': ReadSignal<boolean>
    disabled: ReadSignal<boolean>
    'data-scope': 'gradient-picker'
    'data-part': 'repeating-toggle'
    'data-state': ReadSignal<'on' | 'off'>
    onClick: (e: MouseEvent) => void
  }
  angleInput: {
    type: 'range'
    min: 0
    max: 360
    step: 1
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    'data-scope': 'gradient-picker'
    'data-part': 'angle-input'
    onInput: (e: Event) => void
  }
  centerArea: {
    'data-scope': 'gradient-picker'
    'data-part': 'center-area'
    onPointerDown: (e: PointerEvent) => void
    onPointerMove: (e: PointerEvent) => void
    onPointerUp: (e: PointerEvent) => void
    onPointerCancel: (e: PointerEvent) => void
    onLostPointerCapture: (e: PointerEvent) => void
  }
  centerThumb: {
    role: 'slider'
    'aria-label': string
    'aria-valuemin': 0
    'aria-valuemax': 100
    'aria-valuenow': ReadSignal<number>
    'aria-valuetext': ReadSignal<string>
    'aria-disabled': ReadSignal<'true' | undefined>
    tabindex: ReadSignal<number>
    'data-scope': 'gradient-picker'
    'data-part': 'center-thumb'
    style: ReadSignal<string>
    onKeyDown: (e: KeyboardEvent) => void
  }
  shapeOption: (shape: RadialShape) => ToggleItemParts
  sizeOption: (size: RadialSize) => ToggleItemParts
  interpolationSpaceSelect: {
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<InterpolationSpace>
    'data-scope': 'gradient-picker'
    'data-part': 'interpolation-space-select'
    onInput: (e: Event) => void
  }
  interpolationHueSelect: {
    'aria-label': string
    /** Disabled when the whole picker is disabled, OR the current space has
     * no hue (only `hsl`/`oklch` do). */
    disabled: ReadSignal<boolean>
    value: ReadSignal<HueInterpolationMethod>
    'data-scope': 'gradient-picker'
    'data-part': 'interpolation-hue-select'
    onInput: (e: Event) => void
  }
  reverseButton: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'gradient-picker'
    'data-part': 'reverse-button'
    onClick: (e: MouseEvent) => void
  }
  distributeButton: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'gradient-picker'
    'data-part': 'distribute-button'
    onClick: (e: MouseEvent) => void
  }
  /** The draft, NOT the committed value, while editing — see
   * `GradientPickerState.cssDraft`. Commits on `change`/Enter, never on
   * every keystroke, so the caret never jumps mid-edit; `aria-invalid` +
   * `aria-describedby` (pointing at `cssError`'s `id`) publish validity. */
  cssInput: {
    type: 'text'
    autocomplete: 'off'
    spellcheck: 'false'
    'aria-label': string
    'aria-invalid': ReadSignal<'true' | undefined>
    'aria-describedby': ReadSignal<string | undefined>
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    'data-scope': 'gradient-picker'
    'data-part': 'css-input'
    onInput: (e: Event) => void
    onChange: (e: Event) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  /** The describable error region `cssInput`'s `aria-describedby` points at
   * — empty/hidden text when there is no error, matching `form-field`'s
   * `errorText` convention (attributes + a sibling `visible`/`message`
   * rather than folding non-attribute content into the same bag). */
  cssError: {
    id: string
    role: 'alert'
    'data-scope': 'gradient-picker'
    'data-part': 'css-error'
    visible: ReadSignal<boolean>
    message: ReadSignal<string>
  }
  /**
   * The embedded `color-picker`'s FULL part bag, connected over the DERIVED
   * `pickerStateOf(state)` — eyedropper, OKLCH canvas, model toggle, every
   * slider, all included with zero glue. Messages are wrapped as
   * `{ type: 'picker'; msg }` via `wrapChildSend` (which also tags the
   * dispatcher `__lluiVariants: ['picker']`, so every handler `color-picker`
   * builds from it reports the truthful PARENT-visible type) and unwrapped
   * by this component's `update()` — see the module doc comment.
   */
  picker: ColorPickerParts
}
GradientPickerState from @llui/components
export interface GradientPickerState {
  kind: GradientKind
  repeating: boolean
  /** Linear only. Ignored (but still stored — the machine always publishes
   * both per-kind clusters; the view branches on `kind`) for radial/conic. */
  direction: GradientDirection
  /** Degrees, conic's `from` angle only. Conic has no `to <side>` form in
   * CSS at all, so — unlike linear — it never needs the `GradientDirection`
   * union; it is always a plain angle. */
  conicAngle: number
  /** Percent 0–100. Radial + conic only. */
  center: GradientCenter
  /** Radial only. */
  shape: RadialShape
  /** Radial only. */
  size: RadialSize
  interpolation: GradientInterpolation
  /** Kept sorted by `position` ascending; ties keep insertion order. */
  stops: GradientStop[]
  selectedId: string
  /** The next deterministic stop id counter (`` `s${nextId}` ``) — the reducer
   * stays pure, so ids can never come from `Math.random()`/`crypto`. MONOTONE
   * across a `setGradient`: a successful replace continues the counter
   * rather than restarting at `s1`, so an id a consumer captured earlier
   * (a DOM id, a test assertion, an undo/redo log) is never reissued to a
   * DIFFERENT stop. */
  nextId: number
  /** Which model a freshly-added stop starts in when nothing else determines
   * one (`buildStopsFromOptions`'s fallback color). Distinct from any
   * EXISTING stop's own stored model, which `picker`/`setModel` edits
   * in-place — see the module doc comment. */
  defaultModel: ColorModel
  /** Upper bound for OKLCH chroma — shared with the embedded picker. NEVER
   * clamps an author-specified chroma down (CLAUDE.md's "never clamp/drop"
   * rule, review finding #2c): `init`/`setGradient` RAISE this to fit
   * whatever chroma the input actually contains instead. */
  maxChroma: number
  /** `removeStop` refuses to go below this many stops; `init` PADS up to it
   * if given fewer; `setGradient` REJECTS a parse that would drop below it
   * (never truncates/pads a user-authored CSS gradient — see the module doc
   * on `setGradient`). Default 2. Always a positive integer. */
  minStops: number
  /**
   * Upper bound on stop count, or ABSENT for unbounded. Follows the
   * package's UNBOUNDED-CAPABLE idiom (CLAUDE.md #177, `breadcrumbs.
   * maxVisible`): the key is OMITTED rather than holding `null`, so a
   * `JSON.parse(JSON.stringify(state))` round trip is a key-for-key
   * identity. Always a positive integer `>= minStops` when present — `init`
   * normalizes a non-integer or too-low value rather than producing a
   * config that could never be satisfied.
   */
  maxStops?: number
  disabled: boolean
  /** Reading direction. Under `rtl`, the track's physical left/right (both
   * the stop ramp's own background angle and each stop's `left%`) mirrors —
   * see `mirrorForDir`'s doc comment on why this differs from
   * `color-picker`'s (native-input) hue slider. */
  dir: 'ltr' | 'rtl'
  /** Whether the browser's EyeDropper API is available — mirrors
   * `color-picker`'s own `eyeDropperSupported` field (never `true` at
   * `init()`; only `eyeDropperSupportMount`, wired through the `picker`
   * wrapper in `connect()`, ever flips it). Lives at the GRADIENT level, not
   * per-stop — it is a mount-time browser capability, not a color. */
  eyeDropperSupported: boolean
  /** The uncommitted `cssInput` text while the user is actively editing, or
   * `null` when not editing (display falls back to the live `toCss(state)`
   * value). Kept OUT of the committed gradient so the input's own caret
   * never jumps mid-keystroke — see `setGradientDraft`/`setGradient`. */
  cssDraft: string | null
  /** The reason the last `setGradient` attempt failed, or `null`. Cleared by
   * the NEXT successful `setGradient`; typing (`setGradientDraft`) does NOT
   * clear it, so the error stays visible while the user is mid-correction. */
  cssError: string | null
}
GradientStop from @llui/components
export interface GradientStop {
  id: string
  color: PickerColor
  alpha: number
  /** 0–100, one cycle — see the module doc comment. */
  position: number
}
GradientStopInit from @llui/components
export interface GradientStopInit {
  color?: PickerColor | string
  alpha?: number
  position: number
}
GradientStopParts from @llui/components
export interface GradientStopParts {
  id: string
  role: 'slider'
  'aria-label': string
  'aria-valuemin': 0
  'aria-valuemax': 100
  'aria-valuenow': ReadSignal<number>
  'aria-valuetext': ReadSignal<string>
  'aria-disabled': ReadSignal<'true' | undefined>
  /** Every stop is independently tabbable (never -1 while enabled) — the
   * package's existing multi-thumb precedent (`slider.ts`'s `SliderThumbParts`)
   * rather than a roving single-tab-stop composite: APG's roving-tabindex
   * pattern is for ONE composite control with ONE conceptual focus (a
   * toolbar, a listbox); a multi-stop gradient ramp is closer to several
   * independent sliders, which is exactly the case `slider.ts` already
   * chose this way for. Tab therefore visits stops in DOM order, which
   * tracks position order since `each()` renders the reducer's own
   * position-sorted `stops` array. */
  tabindex: ReadSignal<number>
  'data-scope': 'gradient-picker'
  'data-part': 'stop'
  'data-value': string
  'data-selected': ReadSignal<'' | undefined>
  style: ReadSignal<string>
  onPointerDown: (e: PointerEvent) => void
  onPointerMove: (e: PointerEvent) => void
  onPointerUp: (e: PointerEvent) => void
  onPointerCancel: (e: PointerEvent) => void
  onLostPointerCapture: (e: PointerEvent) => void
  onFocus: (e: FocusEvent) => void
  onKeyDown: (e: KeyboardEvent) => void
}
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: ReadSignal<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
  /** Selector for an arrow rendered inside the content element. When present,
   * floating positioning owns its absolute edge geometry transactionally. */
  arrowSelector?: string
}
HoverCardParts from @llui/components
export interface HoverCardParts {
  trigger: {
    id: string
    'aria-controls': string
    'aria-expanded': ReadSignal<boolean>
    'data-state': ReadSignal<'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': ReadSignal<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

HSL color. h 0–360 degrees, s/l 0–100. Float — see the module doc.

export interface Hsl {
  h: number
  s: number
  l: number
}
Hsv from @llui/components

HSV color. h 0–360 degrees, s/v 0–100. Float — see the module doc.

export interface Hsv {
  h: number
  s: number
  v: number
}
IconData from @llui/components

One resolved glyph: its body markup and the box it was drawn in.

export interface IconData {
  body: string
  width: number
  height: 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': ReadSignal<'' | undefined>
    'data-resizing': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | undefined>
  }
  image: {
    'data-scope': 'image-cropper'
    'data-part': 'image'
    onLoad: (e: Event) => void
    draggable: false
  }
  /**
   * The crop area: a focusable, named `group` (#266). Arrow keys move it 1% of
   * the image (10% with Shift), `+`/`-` zoom it. Keys stay PHYSICAL under
   * `dir="rtl"`: the image is never mirrored, so neither is the box on it.
   * Geometry is written as an inline `style` in PERCENT of the image — a skin
   * must not set position or size.
   */
  cropBox: {
    role: 'group'
    tabindex: 0
    'aria-label': ReadSignal<string>
    'aria-keyshortcuts': string
    'data-scope': 'image-cropper'
    'data-part': 'crop-box'
    style: ReadSignal<string>
    onPointerDown: (e: PointerEvent) => void
    onKeyDown: (e: KeyboardEvent) => 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': ReadSignal<'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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'selected' | undefined>
    'data-highlighted': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | 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': ReadSignal<'true' | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    'aria-activedescendant': ReadSignal<string | undefined>
    tabindex: ReadSignal<number>
    id: string
    'data-scope': 'listbox'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | 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
    value: string
    chroma: string
    oklchLightness: string
    hex: string
    alpha: string
    eyeDropper: string
    switchToOklch: string
    switchToHsv: string
    swatchGroup: 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; retry: string; progress: string }
  floatingPanel: {
    label: string
    minimize: string
    maximize: string
    close: string
    move: string
    resize: string
  }
  gradientPicker: {
    track: string
    stop: (index: number, count: number, color: string, position: number) => string
    addStop: string
    removeStop: string
    linear: string
    radial: string
    conic: string
    repeating: string
    angle: string
    center: string
    shapeCircle: string
    shapeEllipse: string
    sizeClosestSide: string
    sizeClosestCorner: string
    sizeFarthestSide: string
    sizeFarthestCorner: string
    interpolationSpace: string
    interpolationHue: string
    reverse: string
    distribute: string
    css: string
    cssError: (reason: string) => string
  }
  imageCropper: { reset: string; cropArea: 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 }
  /**
   * The sortable's accessible text. `item` is the consumer's `itemLabel(id)` (or `undefined`
   * without one); positions and counts are 1-based, as a listener counts them. The four
   * announcements are read by the machine's polite live region; `instructions` is the hidden
   * description every handle references through `aria-describedby`.
   */
  sortable: {
    handle: (item: string | undefined) => string
    instructions: string
    grabbed: (item: string | undefined, position: number, count: number) => string
    moved: (item: string | undefined, position: number, count: number) => string
    dropped: (item: string | undefined, from: number, to: number, count: number) => string
    cancelled: (item: string | undefined, position: number, count: number) => string
  }
  /** The composed accessible name of a sparkline. `from`/`to` arrive as
   *  `YYYY-MM-DD` in the sparkline's own calendar offset. */
  sparkline: { empty: string; range: (count: number, from: string, to: string) => string }
  steps: { label: string }
  /** Accessible names of the table's selection checkboxes (#268). */
  table: { selectAll: string; selectRow: 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': ReadSignal<'' | undefined>
    'data-direction': ReadSignal<MarqueeDirection>
    'data-axis': ReadSignal<'horizontal' | 'vertical'>
    'data-disabled': ReadSignal<'' | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    style: ReadSignal<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
  /** Reading direction for horizontal keys and delegated menus. Omit to
   * follow the page's own direction (see {@link MenubarState}'s `dir`/
   * `dirSource`, resolved from the mounted root by `directionSync`). */
  dir?: TextDirection
}

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: ReadSignal<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: {
    // `id` is REQUIRED — it is the scope `directionSync` (below) looks the
    // live root up by, the same contract `navigation-menu`'s own `root.id`
    // already honours (#265 finding 6). A consumer that overrides it with a
    // DIFFERENT id breaks the direction sync silently.
    id: string
    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
  /** Place once anywhere in the same build to keep automatic direction live —
   * the shared `@llui/interactions` direction-sync seam (#265 finding 6). A
   * discarded `Mountable` is inert, so this must be placed in the view. */
  directionSync: Mountable
}
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>
  /** Reading direction for both the bar and its delegated menu trees. Routed
   * through the shared `@llui/interactions` direction-sync seam
   * (`../utils/direction.js`) rather than a second resolver — `dirSource`
   * tracks whether `dir` came from explicit config/`setDir` or from the
   * mounted root's live ancestor `dir` attribute (#265 finding 6). Every
   * embedded `MenuState`'s own `dir` is kept explicitly in sync with this one
   * (see `syncEmbeddedDir` below), so a delegated menu never disagrees with
   * the bar that owns it. */
  dir: TextDirection
  dirSource: DirectionSource
}
export interface MenubarTriggerParts {
  role: 'menuitem'
  id: string
  'aria-haspopup': 'menu'
  'aria-expanded': ReadSignal<boolean>
  'aria-controls': string
  'aria-disabled': ReadSignal<'true' | undefined>
  'data-scope': 'menubar'
  'data-part': 'trigger'
  'data-state': ReadSignal<'open' | 'closed'>
  'data-value': string
  tabindex: ReadSignal<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}'s `dir`/
   * `dirSource`, resolved from the mounted root by `directionSync`). */
  dir?: TextDirection
  /** When false, opening and closing play enter/exit animations and the content
   * stays mounted (status 'opening' or '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: ReadSignal<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': ReadSignal<boolean>
    'aria-controls': string
    id: string
    'data-state': ReadSignal<'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': ReadSignal<string | undefined>
    tabindex: -1
    /** Reflects the presence lifecycle: 'opening' | 'open' | 'closing' | 'closed'.
     * Stays mounted while 'closing' so the exit animation can run. */
    'data-state': ReadSignal<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
  /**
   * A labelled group's parts. The group names its `label` part through
   * `aria-labelledby`; a consumer that renders the group WITHOUT that label
   * passes `{ hasLabel: false }` so it never names an element that does not
   * exist (an unlabelled `role="group"` is valid; a broken idref is not — the
   * `dialog` `hasDescription` rule, #268). Default: true.
   */
  group: (id: string, options?: { readonly hasLabel?: boolean }) => MenuGroupParts
  separator: () => MenuSeparatorParts
  subTrigger: (value: string) => MenuSubTriggerParts
  subPositioner: (value: string) => MenuSubPositionerParts
  subContent: (value: string) => MenuSubContentParts
  /** Place once anywhere in the same build to keep automatic direction live —
   * the shared `@llui/interactions` direction-sync seam (#265 finding 6),
   * observing the TRIGGER (always mounted, unlike the content) as this
   * instance's root. A discarded `Mountable` is inert, so this must be placed
   * in the view. */
  directionSync: Mountable
}
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
  // `dir` + `dirSource` come from `MenuTreeState` (extends `DirectionState`) —
  // the shared `@llui/interactions` direction-sync seam (#265 finding 6).
  // Under 'rtl', ArrowLeft/ArrowRight swap meaning, and the overlay's
  // `*-start`/`*-end` alignment tracks the inline-start/inline-end edge. An
  // EXPLICIT direction is AUTHORITATIVE once it reaches `attachFloating`: a
  // silently-overridden page direction laid a menu on `<html dir="rtl">` out
  // LTR (#138 review, blocking 4) — `dirSource: 'explicit'` is what keeps
  // that override from being overwritten by the next DOM observation.
}
MeterInit from @llui/components
export interface MeterInit {
  value?: number
  min?: number
  max?: number
  /**
   * Native `<meter>` spelling for the three-segment case, compiled into
   * {@link MeterState.bands} as `low` / `middle` / `high`. Ignored when
   * `bands` is given and yields at least one usable band — which is what makes
   * `bands: []` keep the single-optimum behaviour.
   */
  low?: number
  high?: number
  optimum?: number
  bands?: readonly MeterBandInit[]
}
MeterParts from @llui/components
export interface MeterParts {
  root: {
    role: 'meter'
    'aria-valuemin': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number>
    'aria-valuetext': ReadSignal<string>
    'aria-label': string | undefined
    // Spelled out rather than aliased: `scripts/test/registry-attrs.test.ts`
    // reads part-bag VALUES syntactically and gives an ALIAS no verdict, which
    // is how the shipped skin came to style `data-[state=critical]` against a
    // machine emitting `low`/`high` — two rules of dead CSS, for a release.
    'data-state': ReadSignal<'optimal' | 'suboptimal' | 'critical' | 'neutral'>
    /** The current band's id — absent when the reading is in no band. */
    'data-band': ReadSignal<string | undefined>
    'data-scope': 'meter'
    'data-part': 'root'
  }
  track: {
    'data-state': ReadSignal<'optimal' | 'suboptimal' | 'critical' | 'neutral'>
    'data-scope': 'meter'
    'data-part': 'track'
  }
  /** The filled bar of a classic gauge: `inline-size` up to the reading. */
  range: {
    'data-state': ReadSignal<'optimal' | 'suboptimal' | 'critical' | 'neutral'>
    'data-scope': 'meter'
    'data-part': 'range'
    style: ReadSignal<string>
  }
  /** The reading itself, for a BANDED track: positioned, not filled. */
  marker: {
    'data-state': ReadSignal<'optimal' | 'suboptimal' | 'critical' | 'neutral'>
    'data-band': ReadSignal<string | undefined>
    'data-scope': 'meter'
    'data-part': 'marker'
    style: ReadSignal<string>
  }
  label: {
    'data-scope': 'meter'
    'data-part': 'label'
  }
  /** The laid-out bands, for `each` — key rows on `band.id`. */
  bands: ReadSignal<MeterBandGeometry[]>
  /**
   * Attributes for one laid-out band, taking the ROW HANDLE `each` hands the
   * render function. It returns signals rather than plain values so the bag is
   * spreadable AS IS — `chart`'s `markProps` takes a snapshot instead and every
   * call site has to `peek()` the row and then re-bind the fields that move.
   */
  bandProps: (band: Signal<MeterBandGeometry>) => {
    'data-scope': 'meter'
    'data-part': 'band'
    'data-band': ReadSignal<string>
    'data-state': ReadSignal<'optimal' | 'suboptimal' | 'critical' | 'neutral'>
    'data-current': ReadSignal<'' | undefined>
    style: ReadSignal<string>
  }
  /**
   * One band by id, for a view that names its bands statically. An id no band
   * carries yields an INERT bag — `hidden`, zero-width, `neutral` — rather than
   * throwing: `connect` runs while the view is being built, so a typo would
   * take the whole page down instead of leaving one stripe undrawn.
   */
  band: (id: string) => {
    'data-scope': 'meter'
    'data-part': 'band'
    'data-band': string
    'data-state': ReadSignal<'optimal' | 'suboptimal' | 'critical' | 'neutral'>
    'data-current': ReadSignal<'' | undefined>
    hidden: ReadSignal<boolean>
    style: ReadSignal<string>
  }
  /** The formatted reading, WITHOUT the band name. */
  valueText: ReadSignal<string>
  /** The current band's announced name, or `''`. */
  bandLabel: ReadSignal<string>
}
MeterState from @llui/components
export interface MeterState {
  value: number
  min: number
  max: number
  /**
   * The reference range, in AUTHOR ORDER. Nothing is sorted or merged: order
   * decides which band wins an overlap (the first match) and the order the
   * view draws them in, and re-ordering the author's ids would silently change
   * both.
   */
  bands: MeterBand[]
}
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': ReadSignal<boolean | undefined>
    'data-scope': 'navigation-menu'
    'data-part': 'trigger'
    'data-state': ReadSignal<'open' | 'closed'>
    'data-value': string
    tabindex: ReadSignal<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': ReadSignal<'open' | 'closed'>
    hidden: ReadSignal<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: {
    // `id` is REQUIRED — it is the scope `directionSyncMount` (below) looks
    // the live root up by, the same contract `tabs`/`carousel`/`pagination`'s
    // own `root.id` already honours. A consumer that overrides it with a
    // DIFFERENT id breaks the direction sync silently (#265 finding 6): the
    // watcher would observe nothing, since `getElementByIdInScope` would
    // never find this element under the id it was given.
    id: string
    // 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': ReadSignal<'' | undefined>
    onPointerLeave: (e: PointerEvent) => void
    onPointerEnter: (e: PointerEvent) => void
  }
  /**
   * The arrow that tracks the open top-level trigger — Radix's
   * `NavigationMenuIndicator`, whose `data-state="visible" | "hidden"` drives
   * the enter/exit animation. Render it as a sibling of the trigger list,
   * inside the positioned root.
   *
   * It stays MOUNTED in both states: the recipe animates it out with
   * `data-[state=hidden]:animate-out fade-out`, and a `show(...)` would unmount
   * the node the exit animation is running on. Same reason `formField`'s live
   * region is hidden rather than gated.
   *
   * `data-state` is all the MACHINE can answer — where the arrow sits is a
   * layout fact, not state (`NavMenuState` is JSON-serializable and holds no
   * pixels), so the position arrives through `watchNavMenuIndicator`, which
   * writes `--indicator-left`/`--indicator-width` onto this element. That is
   * the same split `tabs` uses for its own indicator.
   */
  indicator: {
    // Pure decoration: it duplicates no information a screen reader cannot get
    // from the trigger's own `aria-expanded`, and it is always in the tree.
    'aria-hidden': 'true'
    'data-scope': 'navigation-menu'
    'data-part': 'indicator'
    'data-state': ReadSignal<'visible' | 'hidden'>
  }
  /** Place once anywhere in the same build to keep automatic direction live —
   * the shared `@llui/interactions` direction-sync seam (#265 finding 6),
   * same as `tabs`/`carousel`/`pagination`'s own `directionSync` part. A
   * discarded `Mountable` is inert, so this must be placed in the view. */
  directionSync: Mountable
  /**
   * 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.
   * Routed through the shared `@llui/interactions` direction-sync seam
   * (`../utils/direction.js`) rather than a second resolver — `dirSource`
   * tracks whether `dir` came from explicit config/`setDir` or from the
   * mounted root's live ancestor `dir` attribute (#265 finding 6). */
  dir: 'ltr' | 'rtl'
  dirSource: DirectionSource
}
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': ReadSignal<'' | undefined>
  }
  input: {
    type: 'text'
    role: 'spinbutton'
    inputmode: 'decimal'
    'aria-valuemin': ReadSignal<number | undefined>
    'aria-valuemax': ReadSignal<number | undefined>
    'aria-valuenow': ReadSignal<number | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    'aria-readonly': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    readonly: ReadSignal<boolean>
    value: ReadSignal<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': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    'data-scope': 'number-input'
    'data-part': 'increment'
    tabindex: -1
    onClick: (e: MouseEvent) => void
  }
  decrement: {
    type: 'button'
    'aria-label': string
    'aria-disabled': ReadSignal<'true' | undefined>
    disabled: ReadSignal<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
}
Oklab from @llui/components

Oklab. l is 0–1 lightness; a/b are unbounded (±0.4 roughly spans sRGB).

export interface Oklab {
  l: number
  a: number
  b: number
}
Oklch from @llui/components

OKLCH. l 0–1, c >= 0 (unbounded — maxChroma in the color picker is a UI choice, not a property of the color space), h in degrees.

export interface Oklch {
  l: number
  c: number
  h: 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: {
    id: string
    role: 'navigation'
    'aria-label': string
    'data-scope': 'pagination'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
  }
  prevTrigger: {
    type: 'button'
    'aria-label': string
    'aria-disabled': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    'data-scope': 'pagination'
    'data-part': 'prev-trigger'
    tabindex: ReadSignal<number>
    onClick: (e: MouseEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  nextTrigger: {
    type: 'button'
    'aria-label': string
    'aria-disabled': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    'data-scope': 'pagination'
    'data-part': 'next-trigger'
    tabindex: ReadSignal<number>
    onClick: (e: MouseEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  item: (page: number) => {
    type: 'button'
    'aria-label': string
    'aria-current': ReadSignal<'page' | undefined>
    'data-selected': ReadSignal<'' | undefined>
    'data-scope': 'pagination'
    'data-part': 'item'
    'data-value': string
    tabindex: ReadSignal<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'
  }
  /** Place once anywhere in the same build to keep automatic direction live. */
  directionSync: Mountable
}
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
  dirSource: DirectionSource
}
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
}
ParsedGradient from @llui/components
export interface ParsedGradient {
  kind: GradientKind
  repeating: boolean
  direction: GradientDirection
  conicAngle: number
  center: GradientCenter
  shape: RadialShape
  size: RadialSize
  interpolation: GradientInterpolation
  stops: Array<{ color: PickerColor; alpha: number; position: number }>
}
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': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | undefined>
  }
  input: {
    type: ReadSignal<'text' | 'password'>
    autocomplete: string
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    'data-scope': 'password-input'
    'data-part': 'input'
    onInput: (e: Event) => void
  }
  visibilityTrigger: {
    type: 'button'
    'aria-label': ReadSignal<string>
    'aria-pressed': ReadSignal<boolean>
    disabled: ReadSignal<boolean>
    tabindex: ReadSignal<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'
    /** The label part's id, or absent when `hasLabel: false`. */
    'aria-labelledby': string | undefined
    'data-scope': 'pin-input'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
  }
  label: {
    id: string
    'data-scope': 'pin-input'
    'data-part': 'label'
  }
  /** Props for the input at a given index. */
  input: (index: number) => {
    type: ReadSignal<'text' | 'password'>
    inputmode: ReadSignal<'numeric' | 'text'>
    pattern: ReadSignal<string>
    maxlength: 1
    autocomplete: 'off'
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<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
}
Point from @llui/components
export interface Point {
  x: number
  y: number
}
PointerDragCallbacks from @llui/components

One call site's drag callbacks.

export interface PointerDragCallbacks {
  /** Checked on `pointerdown` only — a drag that is already underway keeps
   * running even if this flips true mid-drag (the reducer's own `disabled`
   * guard is the actual safety net for a message dispatched from `onDrag`
   * after that point; this only decides whether a NEW drag may start). */
  isDisabled: () => boolean
  /** Fires once, on `pointerdown`, after capture is acquired and BEFORE the
   * first `onDrag` call — the hook for a one-time side effect like moving
   * focus onto a thumb element. */
  onDragStart?: (e: PointerEvent) => void
  /** Fires on `pointerdown` (immediately after `onDragStart`) and again on
   * every `pointermove` while the drag is live. */
  onDrag: (e: PointerEvent) => void
  /** Fires once on `pointerup`/`pointercancel`, but ONLY when a drag was
   * actually live (never on a stray up/cancel with no matching down) — the
   * counterpart to `onDragStart`, for a caller that tracks per-drag identity
   * across the lifecycle (`gradient-picker`'s track drag creates a new stop
   * on `pointerdown` and must stop targeting it once the drag ends). Runs
   * AFTER pointer capture is released. */
  onDragEnd?: (e: PointerEvent) => void
}
PointerDragHandlers from @llui/components
export interface PointerDragHandlers {
  onPointerDown: (e: PointerEvent) => void
  onPointerMove: (e: PointerEvent) => void
  onPointerUp: (e: PointerEvent) => void
  onPointerCancel: (e: PointerEvent) => void
  /** The browser can revoke pointer capture without ever firing `pointerup`/
   * `pointercancel` (an OS-level interruption, a nested capture request, …).
   * Left unwired, `dragging` would stay `true` forever and a later, UNRELATED
   * pointer's `pointermove` would be misread as a continuation of this drag.
   * Spread onto the same element as the other four. */
  onLostPointerCapture: (e: PointerEvent) => void
}
PolarOptions from @llui/components
export interface PolarOptions {
  /** Inner radius as a fraction of the outer. `0` is a pie, `0.5` a donut, and
   *  a radar usually wants `0`. */
  innerRadius?: number
  /** Where `u = 0` sits, in radians clockwise from 12 o'clock. */
  startAngle?: number
  /** Total sweep in radians. Defaults to a full turn, which is what makes the
   *  projection `closed`. */
  sweep?: number
  /**
   * Gridline shape. `ring` is a circle at the given magnitude — right for a
   * pie or a rose. `web` joins the `spokes` sample positions with straight
   * segments — right for a radar, where a circular grid reads as a different
   * chart from the polygonal data on top of it.
   */
  grid?: 'ring' | 'web'
  /** How many spokes a `web` gridline has. Ignored for `ring`. */
  spokes?: number
  /**
   * Swap the axes, exactly as {@link CartesianOptions.horizontal} does: the
   * INDEPENDENT axis moves off the angle and onto the RADIUS, so each category
   * becomes its own concentric ring and magnitude is read as arc length. That
   * is a radial bar chart, and it is the polar reading of the same flag that
   * turns a column chart on its side — one field, the same meaning in both
   * coordinate systems, so a consumer flipping `coord` keeps whatever
   * orientation they asked for.
   *
   * The independent axis is then the radius, which is NOT a loop, so the
   * projection reports `closed: false` however wide the sweep is.
   */
  horizontal?: boolean
}
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: ReadSignal<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
  /** Selector for an arrow rendered inside the content element. When present,
   * floating positioning owns its absolute edge geometry transactionally. */
  arrowSelector?: string
}
PopoverParts from @llui/components
export interface PopoverParts {
  trigger: {
    type: 'button'
    'aria-haspopup': 'dialog'
    'aria-expanded': ReadSignal<boolean>
    'aria-controls': string
    id: string
    'data-state': ReadSignal<'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': ReadSignal<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': ReadSignal<PresenceStatus>
    hidden: ReadSignal<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': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number | undefined>
    'aria-label': string | undefined
    'data-state': ReadSignal<'indeterminate' | 'complete' | 'loading'>
    'data-orientation': ReadSignal<ProgressOrientation>
    'data-scope': 'progress'
    'data-part': 'root'
  }
  track: {
    'data-state': ReadSignal<'indeterminate' | 'complete' | 'loading'>
    'data-orientation': ReadSignal<ProgressOrientation>
    'data-scope': 'progress'
    'data-part': 'track'
  }
  range: {
    'data-state': ReadSignal<'indeterminate' | 'complete' | 'loading'>
    'data-orientation': ReadSignal<ProgressOrientation>
    'data-scope': 'progress'
    'data-part': 'range'
    style: ReadSignal<string>
  }
  label: {
    'data-scope': 'progress'
    'data-part': 'label'
  }
  valueText: ReadSignal<string>
}
ProgressState from @llui/components
export interface ProgressState {
  value: number | null
  min: number
  max: number
  orientation: ProgressOrientation
}
Projection from @llui/components
export interface Projection {
  readonly kind: ChartCoord
  /**
   * Whether the independent axis is a CLOSED loop. `true` for a polar
   * projection covering a full turn, which is what makes a radar outline join
   * back to its first vertex instead of leaving a gap.
   */
  readonly closed: boolean
  /**
   * Curves this projection can honour. A caller asking for one outside the set
   * gets `linear` — see the polar note below for why this is the honest answer
   * rather than a silent approximation.
   */
  readonly curves: readonly Curve[]
  /** Normalized (u, v) → a point in user units. */
  point(u: number, v: number): Point
  /** A run of samples joined along increasing `u`. */
  line(samples: readonly Sample[], curve: Curve): string
  /** The region between an upper run and a lower run of the same length. */
  area(upper: readonly Sample[], lower: readonly Sample[], curve: Curve): string
  /** A band spanning `u0..u1` and `v0..v1`: a bar, or a wedge. */
  band(u0: number, u1: number, v0: number, v1: number): string
  /** The iso-magnitude line at `v` — a value gridline. */
  gridline(v: number): string
  /** Where the independent-axis tick for `u` belongs. */
  tick(u: number): TickPlacement
  /** Where the DEPENDENT-axis tick for `v` belongs. */
  valueTick(v: number): TickPlacement
  /**
   * Invert a point in user units back to `u`, for pointer hit testing. Returns
   * `null` when the point carries no meaningful `u` — the exact centre of a
   * polar chart, where every angle is equally close.
   */
  locate(x: number, y: number): number | null
}
QrCodeInit from @llui/components
export interface QrCodeInit {
  value?: string
  matrix?: boolean[][]
  errorCorrection?: ErrorCorrectionLevel
}
QrCodeParts from @llui/components
export interface QrCodeParts {
  /**
   * A labelled `group` around the code and its download action. `aria-label` on
   * a bare `<div>` (role `generic`) is prohibited ARIA and ignored by most
   * assistive technology, which is why the role is stated.
   */
  root: {
    role: 'group'
    'data-scope': 'qr-code'
    'data-part': 'root'
    'aria-label': string
    /** Present while no matrix has been supplied — the empty state hook. */
    'data-empty': ReadSignal<'' | undefined>
  }
  /**
   * The `role="img"` graphic carries its OWN accessible name. A screen-reader
   * user cannot scan the modules, so the name includes the encoded value.
   */
  svg: {
    'data-scope': 'qr-code'
    'data-part': 'svg'
    role: 'img'
    'aria-label': ReadSignal<string>
    viewBox: ReadSignal<string>
    'shape-rendering': 'crispEdges'
  }
  /** Spread onto a `<rect>`: sized to the module grid so it covers the quiet background. */
  background: {
    'data-scope': 'qr-code'
    'data-part': 'background'
    width: ReadSignal<string>
    height: ReadSignal<string>
  }
  /** Spread onto a `<path>`: one sub-path per dark module. */
  foreground: {
    'data-scope': 'qr-code'
    'data-part': 'foreground'
    d: ReadSignal<string>
  }
  downloadTrigger: {
    type: 'button'
    'aria-label': string
    'data-scope': 'qr-code'
    'data-part': 'download-trigger'
    /** Nothing to download until a matrix exists. */
    disabled: ReadSignal<boolean>
    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': ReadSignal<Orientation>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-scope': 'radio-group'
    'data-part': 'root'
    'data-orientation': ReadSignal<Orientation>
    'data-disabled': ReadSignal<'' | 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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'checked' | 'unchecked'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'radio-group'
    'data-part': 'item'
    'data-value': string
    tabindex: ReadSignal<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': ReadSignal<'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': ReadSignal<'true' | undefined>
    'aria-readonly': ReadSignal<'true' | undefined>
    'data-scope': 'rating-group'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
    'data-readonly': ReadSignal<'' | 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': ReadSignal<boolean>
    'data-fill': ReadSignal<ItemFill>
    'data-scope': 'rating-group'
    'data-part': 'item'
    'data-value': string
    'data-disabled': ReadSignal<'' | undefined>
    tabindex: ReadSignal<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[]
}
Rgb255 from @llui/components

8-bit-per-channel sRGB, 0–255 (integers once produced by this module).

export interface Rgb255 {
  r: number
  g: number
  b: number
}
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;
}
Sample from @llui/components

A normalized sample: position along the independent axis, and magnitude.

export interface Sample {
  u: number
  v: number
}
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': ReadSignal<'' | undefined>
    'data-hovered': ReadSignal<'' | 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': ReadSignal<'' | undefined>
  }
  scrollbarY: {
    'data-scope': 'scroll-area'
    'data-part': 'scrollbar'
    'data-axis': 'y'
    'data-visible': ReadSignal<'' | undefined>
  }
  thumbX: {
    'data-scope': 'scroll-area'
    'data-part': 'thumb'
    'data-axis': 'x'
    style: ReadSignal<string>
  }
  thumbY: {
    'data-scope': 'scroll-area'
    'data-part': 'thumb'
    'data-axis': 'y'
    style: ReadSignal<string>
  }
  corner: {
    'data-scope': 'scroll-area'
    'data-part': 'corner'
    'data-visible': ReadSignal<'' | 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': ReadSignal<'' | undefined>
  }
  label: {
    'data-scope': 'search-field'
    'data-part': 'label'
  }
  input: {
    type: 'search'
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    'data-scope': 'search-field'
    'data-part': 'input'
    onInput: (e: Event) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  clearTrigger: {
    type: 'button'
    'aria-label': string
    hidden: ReadSignal<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'
    /** The group label's id, or absent when `hasLabel: false`. */
    'aria-labelledby': string | undefined
    '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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'selected' | undefined>
    'data-highlighted': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | 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': ReadSignal<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: ReadSignal<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': ReadSignal<boolean>
    'aria-controls': string
    'aria-activedescendant': ReadSignal<string | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    'aria-required': ReadSignal<'true' | undefined>
    id: string
    disabled: ReadSignal<boolean>
    'data-state': ReadSignal<'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': ReadSignal<'' | 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': ReadSignal<'true' | undefined>
    'aria-labelledby': string
    tabindex: -1
    'data-state': ReadSignal<'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: ReadSignal<boolean>
    multiple: ReadSignal<boolean>
    required: ReadSignal<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: ReadSignal<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. */
  /**
   * A labelled group's parts. The group names its `label` part through
   * `aria-labelledby`; a consumer that renders the group WITHOUT that label
   * passes `{ hasLabel: false }` so it never names an element that does not
   * exist (an unlabelled `role="group"` is valid; a broken idref is not — the
   * `dialog` `hasDescription` rule, #268). Default: true.
   */
  group: (id: string, options?: { readonly hasLabel?: boolean }) => SelectGroupParts
  /** Selected value(s) — use for rendering the trigger label. */
  valueText: ReadSignal<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': ReadSignal<'' | undefined>
    'data-readonly': ReadSignal<'' | undefined>
    'data-drawing': ReadSignal<'' | undefined>
    /** Present while nothing has been drawn — the placeholder hook. */
    'data-empty': ReadSignal<'' | undefined>
  }
  control: {
    'data-scope': 'signature-pad'
    'data-part': 'control'
  }
  clearTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'signature-pad'
    'data-part': 'clear-trigger'
    onClick: (e: MouseEvent) => void
  }
  undoTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<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: ReadSignal<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
  /**
   * The strokes the last `clear` removed, until the next edit — what makes a
   * destructive clear undoable (`undo` right after `clear` restores them).
   * `null` when there is nothing to restore (#266).
   */
  cleared: Stroke[] | null
}
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': ReadSignal<Orientation>
    'data-disabled': ReadSignal<'' | undefined>
  }
  control: {
    'data-scope': 'slider'
    'data-part': 'control'
    'data-orientation': ReadSignal<Orientation>
    onPointerDown: (e: PointerEvent) => void
  }
  track: {
    'data-scope': 'slider'
    'data-part': 'track'
    'data-orientation': ReadSignal<Orientation>
  }
  range: {
    'data-scope': 'slider'
    'data-part': 'range'
    'data-orientation': ReadSignal<Orientation>
    style: ReadSignal<string>
  }
  thumb: (index: number) => SliderThumbParts
  /** Current raw values — reactive convenience. */
  value: ReadSignal<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': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number>
    'aria-orientation': ReadSignal<Orientation>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-orientation': ReadSignal<Orientation>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'slider'
    'data-part': 'thumb'
    'data-index': string
    tabindex: ReadSignal<number>
    onKeyDown: (e: KeyboardEvent) => void
    style: ReadSignal<string>
  }
}
SortableParts from @llui/components
export interface SortableParts {
  root: {
    'data-scope': 'sortable'
    'data-part': 'root'
    'data-container-id': string
    'data-dragging': ReadSignal<'' | 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': ReadSignal<'' | undefined>
    'data-over': ReadSignal<'' | undefined>
    'data-shift': ReadSignal<'up' | 'down' | undefined>
    'style.transform': ReadSignal<string | undefined>
    'style.zIndex': ReadSignal<string | undefined>
  }
  handle: (
    id: string,
    index: number,
  ) => {
    'data-scope': 'sortable'
    'data-part': 'handle'
    role: 'button'
    tabindex: 0
    /** A toggle button: pressed while this handle's item is carried. */
    'aria-pressed': ReadSignal<boolean>
    'aria-label': string
    /** The `instructions` part's id, or absent when `hasInstructions: false`. */
    'aria-describedby': string | undefined
    onPointerDown: (e: PointerEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  /**
   * The polite live region announcing grab / move / drop / cancel. `text` is
   * the region's CHILD (a Signal), not an attribute: spread the rest and render
   * `text(text)` inside. Keep it visually hidden but rendered (`sr-only`) — a
   * `display: none` region is never announced. Only the ORIGIN container's
   * region speaks, so connects that share one state never announce twice.
   */
  liveRegion: {
    role: 'status'
    'aria-live': 'polite'
    'aria-atomic': 'true'
    'data-scope': 'sortable'
    'data-part': 'live-region'
    text: ReadSignal<string>
  }
  /**
   * The keyboard instructions every handle's `aria-describedby` points at.
   * `hidden`: a directly referenced hidden element still supplies the
   * description, and stays out of the reading order. `text` is its CHILD.
   */
  instructions: {
    id: string
    hidden: true
    'data-scope': 'sortable'
    'data-part': 'instructions'
    text: string
  }
}
SortableState from @llui/components
export interface SortableState {
  dragging: DragState | null
  /** The latest announcement; `null` before any drag and after a cross-container drop. */
  announcement: SortableAnnouncement | null
}
SparklineBand from @llui/components

A reference band. Both bounds are UNBOUNDED-CAPABLE in the sense of finiteBound (#177): "no bound on this side" is spelled by OMITTING the key, never by undefined, null or ±Infinity — only the omission survives a JSON round trip key-for-key, and this lives in state.

The three readings are not a convenience, they are the whole point: both bounds shade BETWEEN them, a high alone shades everything BELOW it, and a low alone shades everything ABOVE it. "Acceptable is under 140" and "acceptable is between 90 and 120" are different statements and must not both come out as a stripe.

export interface SparklineBand {
  low?: number
  high?: number
}
SparklineBandGeometry from @llui/components
export interface SparklineBandGeometry {
  kind: SparklineBandKind
  low: number | null
  high: number | null
  d: string
}
SparklineCalendar from @llui/components

The serializable subset of {@link CalendarStepOptions} this state carries.

export interface SparklineCalendar {
  offsetMinutes: number
  weekStartsOn: number
  maxTicks: number
  /** Force a rung instead of choosing one from the span. */
  unit?: CalendarUnit
}
SparklineConnectOptions from @llui/components
export interface SparklineConnectOptions {
  /** Base id; the title and description ids derive from it. */
  id: string
  /** Override the composed accessible name entirely. */
  label?: string
  /** Longer description, announced with the name. Defaults to empty. */
  description?: string
  /**
   * Whether the consumer renders the `desc` part (default: true). When false,
   * the svg's `aria-labelledby` names only the title, never an element that
   * does not exist (the `dialog` `hasDescription` rule, #268). The TITLE is
   * the image's name — required for `role="img"` — and is always referenced.
   */
  hasDescription?: boolean
}
SparklineDot from @llui/components
export interface SparklineDot {
  index: number
  at: number
  value: number
  x: number
  y: number
  tone: SparklineTone
  /** True for the most recent drawn point — the reading the row is about. */
  last: boolean
  /** True for the point under the cursor. Follows `chart`: the cursor is part
   *  of the derived picture, so one signal per row carries everything a dot
   *  renders from and nothing has to combine two signals at the call site. */
  active: boolean
  key: string
}
SparklineGeometry from @llui/components
export interface SparklineGeometry {
  frame: Frame
  time: Domain
  value: Domain
  /** The points actually drawn: finite, sorted, and after trimming. */
  points: SparklinePoint[]
  trimmed: number
  /** The trend line. Empty string when there is nothing to draw. */
  path: string
  band: SparklineBandGeometry | null
  ticks: SparklineTick[]
  dots: SparklineDot[]
  spans: SparklineSpan[]
  now: SparklineNow
  summary: SparklineSummary
  /** Built-in English composition of {@link summary}. `connect` replaces it
   *  with the locale's phrasing. */
  label: string
  rows: SparklineRow[]
}
SparklineGeometryOptions from @llui/components
export interface SparklineGeometryOptions {
  band?: SparklineBand
  now?: number
  min?: number
  max?: number
  width?: number
  height?: number
  padding?: Partial<SparklinePadding>
  curve?: Curve
  trim?: SparklineTrim | null
  track?: SparklineTrack | null
  calendar?: Partial<SparklineCalendar>
  /** Which drawn point the cursor is on. Indexes the DRAWN list — after
   *  non-finite samples are dropped and after trimming — so it is the same
   *  index `locateIndex` returns. */
  activeIndex?: number | null
}
SparklineInit from @llui/components
export interface SparklineInit {
  points?: readonly SparklinePoint[]
  band?: SparklineBand
  now?: number
  min?: number
  max?: number
  width?: number
  height?: number
  padding?: Partial<SparklinePadding>
  curve?: Curve
  /** `true` for the documented defaults, an object to tune them, absent for
   *  OFF — trimming discards data, so it is never on by accident. */
  trim?: boolean | Partial<SparklineTrim>
  track?: SparklineTrack | null
  calendar?: Partial<SparklineCalendar>
}
SparklineNow from @llui/components
export interface SparklineNow {
  at: number
  x: number
  d: string
  /** True when the right edge is later than the last reading — the series
   *  trails off. */
  stale: boolean
}
SparklinePadding from @llui/components
export interface SparklinePadding {
  top: number
  right: number
  bottom: number
  left: number
}
SparklineParts from @llui/components
export interface SparklineParts {
  root: {
    'data-scope': 'sparkline'
    'data-part': 'root'
    'data-stale': ReadSignal<'' | undefined>
    'data-active': ReadSignal<'' | undefined>
  }
  /**
   * The `<svg>`. `role="img"` named through its own `<title>`/`<desc>`; the
   * numbers live in {@link SparklineParts.table}, because an SVG polyline is
   * otherwise silent and the WAI-ARIA graphics roles are not carried well
   * enough to be the only route to the data.
   */
  svg: {
    'data-scope': 'sparkline'
    'data-part': 'svg'
    role: 'img'
    'aria-labelledby': string
    viewBox: ReadSignal<string>
    tabindex: 0
    onKeyDown: (e: KeyboardEvent) => void
    onPointerMove: (e: PointerEvent) => void
    onPointerLeave: (e: PointerEvent) => void
    onBlur: (e: FocusEvent) => void
  }
  title: { id: string; 'data-scope': 'sparkline'; 'data-part': 'title' }
  desc: { id: string; 'data-scope': 'sparkline'; 'data-part': 'desc' }
  /** The visually-hidden `<table>` fallback. Render it with {@link rows}. */
  table: {
    'data-scope': 'sparkline'
    'data-part': 'table'
    'aria-label': ReadSignal<string>
  }
  /** The reference band. Stays MOUNTED and hides itself, so a band appearing or
   *  disappearing does not rebuild the layer. */
  band: {
    'data-scope': 'sparkline'
    'data-part': 'band'
    'data-band': ReadSignal<'between' | 'below' | 'above' | undefined>
    d: ReadSignal<string>
    hidden: ReadSignal<boolean>
  }
  line: { 'data-scope': 'sparkline'; 'data-part': 'line'; d: ReadSignal<string> }
  /** The right edge. `data-stale` is set when it is later than the last
   *  reading. */
  now: {
    'data-scope': 'sparkline'
    'data-part': 'now'
    'data-stale': ReadSignal<'' | undefined>
    d: ReadSignal<string>
  }
  /** A `<g>` stacking layer. Static — spread it on each layer group. */
  layer: { 'data-scope': 'sparkline'; 'data-part': 'layer' }
  /**
   * Per-row attribute bags. Each takes the ROW SIGNAL an `each` hands its
   * render function, not a snapshot value, and every geometry-derived
   * attribute comes back as a Signal.
   *
   * That is not decoration. A keyed `each` REUSES a row whose key is unchanged,
   * so a dot whose key is `index:instant` survives a change of time domain —
   * and a static `cx` read at build time would then be frozen at the position
   * the point had under the OLD domain, with the key giving no hint that
   * anything moved. Taking the row signal is what makes the reuse safe.
   */
  tickProps: (tick: Signal<SparklineTick>) => {
    'data-scope': 'sparkline'
    'data-part': 'grid'
    'data-unit': ReadSignal<'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'>
    d: ReadSignal<string>
  }
  dotProps: (dot: Signal<SparklineDot>) => {
    'data-scope': 'sparkline'
    'data-part': 'dot'
    'data-tone': ReadSignal<'below' | 'in' | 'above' | 'none'>
    'data-last': ReadSignal<'' | undefined>
    'data-active': ReadSignal<'' | undefined>
    cx: ReadSignal<number>
    cy: ReadSignal<number>
  }
  spanProps: (span: Signal<SparklineSpan>) => {
    'data-scope': 'sparkline'
    'data-part': 'span'
    'data-grain': ReadSignal<string>
    d: ReadSignal<string>
  }
  /** Tooltip ATTRIBUTES — spreadable, with its own reactive `hidden`. */
  tooltip: {
    'data-scope': 'sparkline'
    'data-part': 'tooltip'
    role: 'status'
    'aria-live': 'polite'
    hidden: ReadSignal<boolean>
    style: ReadSignal<string>
  }
  ticks: ReadSignal<SparklineTick[]>
  dots: ReadSignal<SparklineDot[]>
  spans: ReadSignal<SparklineSpan[]>
  rows: ReadSignal<SparklineRow[]>
  /** The dot under the cursor, or `null`. */
  activeDot: ReadSignal<SparklineDot | null>
  /** The composed accessible name — the locale's phrasing of
   *  {@link SparklineGeometry.summary}, or `opts.label` when given. */
  label: ReadSignal<string>
  summary: ReadSignal<SparklineSummary>
}
SparklinePoint from @llui/components

Sparkline — "this value, over time, against a reference band", at the size of a table cell.

Why this is not chart

chart is a plotting machine: ChartState, a projection seam, a legend, a keyboard cursor over CATEGORIES, and numeric-nice value ticks. A sparkline is a pure function of its points — dozens appear in one table, and the only interaction any of them has is a hover readout. So the load-bearing export here is {@link sparklineGeometry}, which takes points and options and returns path strings; it needs no signal, no send, no mount and no state.

init/update/connect exist BESIDE it, not instead of it, and add exactly one fact: which point the cursor is on. A consumer rendering fifty trends in a table calls the pure function fifty times and pays for nothing else; one that wants a tooltip opts into the machine for that column. The stateless middle ground works too — connect(constant(state), noSend, { id }) gives the full part bag with the cursor pinned wherever the state says.

What it reuses

The geometry is utils/scale.ts + utils/path.ts + utils/projection.ts, the same three files chart is built from, so every coordinate goes through fmt and a path recomputed from unchanged data is BYTE-IDENTICAL — the reconciler commits on output-equality, and a sparkline in a table is exactly the place where a spuriously-changing path string costs the most.

Two things it does NOT reuse, each for a stated reason:

  • valueDomain, because it always includes zero. That is right for a bar, whose LENGTH states the magnitude and therefore lies if the baseline is not zero; it is wrong for a trend line, where the shape is the message. A series reading 118–142 against a 90–120 band has to fill the box, not sit as a flat smear across the top of a 0–142 axis.
  • ticks/niceDomain, because they are NUMERIC. See utils/calendar-ticks.ts: rounding an epoch to a nice number never lands on the 1st of March.

There is no coord and no polar projection. A sparkline is inline text furniture; a round one is a different picture, and chart already draws it.

export interface SparklinePoint {
  /** Instant, in epoch milliseconds. */
  at: number
  value: number
  /**
   * Sampling resolution of this reading — "spot", "session", "daily", whatever
   * the domain calls it. Free-form and never interpreted: consecutive points
   * sharing a tag become one segment of the granularity track, so a line whose
   * sampling changes SAYS so instead of implying uniform data.
   */
  grain?: string
}
SparklineRow from @llui/components

One row of the visually-hidden <table> fallback.

export interface SparklineRow {
  at: number
  /** `YYYY-MM-DD` in the configured offset's calendar. Locale-neutral by
   *  design: the table is a data fallback, not a formatted report. */
  day: string
  value: number
  tone: SparklineTone
  grain: string | null
}
SparklineSpan from @llui/components
export interface SparklineSpan {
  grain: string
  /** First and last instant the segment covers. */
  from: number
  to: number
  x0: number
  x1: number
  d: string
  key: string
}
SparklineState from @llui/components
export interface SparklineState {
  points: SparklinePoint[]
  /** Omit a side to leave it unbounded — see {@link SparklineBand}. */
  band: SparklineBand
  /**
   * The instant the RIGHT EDGE denotes. A stale series then visibly trails off
   * instead of looking current, which is the difference between "the last
   * reading was fine" and "the last reading was fine, eight months ago".
   *
   * UNBOUNDED-CAPABLE: omit it and the edge is the last reading's own instant.
   * It is deliberately NOT defaulted to `Date.now()` anywhere in this module —
   * geometry has to be a pure function of state or an SSR render and the first
   * client render disagree, and `Date.now()` would additionally make the path
   * string change on every recomputation, which is the one thing `fmt` exists
   * to prevent. A consumer that wants "now" puts a clock in its own state.
   */
  now?: number
  /** Value-axis bounds. UNBOUNDED-CAPABLE (#177): omit to derive. */
  min?: number
  max?: number
  width: number
  height: number
  padding: SparklinePadding
  curve: Curve
  /** `null` = no trimming. See {@link SparklineTrim}. */
  trim: SparklineTrim | null
  /** `null` = no granularity track. */
  track: SparklineTrack | null
  /** Fixed offset from UTC that calendar boundaries are measured in, and the
   *  ladder's ceiling. See `utils/calendar-ticks.ts` on why it is an offset and
   *  not a zone. */
  calendar: SparklineCalendar
  /** Index of the point under the pointer or keyboard cursor. */
  activeIndex: number | null
}
SparklineSummary from @llui/components

The facts a label is composed from. Separated from the label itself so the same numbers can be phrased by the locale in connect and by the built-in English template in the pure path.

export interface SparklineSummary {
  count: number
  /** `YYYY-MM-DD` of the first and last drawn reading; empty strings when there
   *  is no data. */
  from: string
  to: string
  trimmed: number
  stale: boolean
}
SparklineTick from @llui/components
export interface SparklineTick {
  at: number
  unit: CalendarUnit
  x: number
  /** The vertical gridline, in user units. */
  d: string
  /** Stable key for a keyed `each`. */
  key: string
}
SparklineTrack from @llui/components

The granularity track: a thin bar under the plot, in the bottom padding.

export interface SparklineTrack {
  height: number
  /** Distance between the bottom of the plot frame and the top of the track. */
  gap: number
}
SparklineTrim from @llui/components

Leading-outlier trimming. A HEURISTIC that DISCARDS REAL DATA, so it is off unless asked for: trim: null is the default, and a chart that silently drops a reading is worse than a squashed one.

The rule: while more than floor points remain, compare the FIRST gap against the MEDIAN of the gaps after it, and drop the leading point when the first gap is more than factor times that median. Median rather than mean so a second ancient reading cannot inflate the yardstick it is being measured against; factor > 1 is required, and a median of zero (every remaining reading at the same instant) stops the walk rather than eating the series.

trimmed in the geometry reports how many points went, so a consumer can say so on the page.

export interface SparklineTrim {
  /** How many times the median gap the leading gap must exceed. Default 4. */
  factor: number
  /** Never trim below this many points. Default 3, floored at 2 — one point is
   *  not a trend and cannot be drawn as a line. */
  floor: number
}
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 {
  /**
   * A layout container: its panels hold the CONSUMER's content. It carries no
   * disabled state — the resize trigger is the control and publishes its own
   * (`aria-disabled`, out of the tab order). A root `data-disabled` made every
   * disabled-state stylesheet dim the panels with it, taking ordinary text to
   * 3.69:1 (#268 audit).
   */
  root: {
    'data-scope': 'splitter'
    'data-part': 'root'
    'data-orientation': ReadSignal<Orientation>
    'data-dragging': ReadSignal<'' | undefined>
  }
  primaryPanel: {
    'data-scope': 'splitter'
    'data-part': 'primary-panel'
    style: ReadSignal<string>
  }
  secondaryPanel: {
    'data-scope': 'splitter'
    'data-part': 'secondary-panel'
    style: ReadSignal<string>
  }
  resizeTrigger: {
    role: 'separator'
    'aria-orientation': ReadSignal<Orientation>
    'aria-valuemin': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-scope': 'splitter'
    'data-part': 'resize-trigger'
    'data-orientation': ReadSignal<Orientation>
    tabindex: ReadSignal<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'
}
Srgb from @llui/components

sRGB, gamma-encoded, 0–1 per channel. May fall outside [0,1] as an intermediate value (e.g. converted from an out-of-gamut OKLCH) — callers that need an in-gamut result go through {@link gamutMapOklchToSrgb}.

export interface Srgb {
  r: number
  g: number
  b: number
}
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': ReadSignal<StepStatus>
    'data-index': string
    'aria-current': ReadSignal<'step' | undefined>
  }
  trigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'steps'
    'data-part': 'trigger'
    'data-status': ReadSignal<StepStatus>
    onClick: (e: MouseEvent) => void
  }
  separator: {
    'data-scope': 'steps'
    'data-part': 'separator'
    'data-status': ReadSignal<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': ReadSignal<'' | undefined>
  }
  nextTrigger: {
    type: 'button'
    disabled: ReadSignal<boolean>
    'data-scope': 'steps'
    'data-part': 'next-trigger'
    onClick: (e: MouseEvent) => void
  }
  prevTrigger: {
    type: 'button'
    disabled: ReadSignal<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': ReadSignal<boolean>
  'data-scope': 'color-picker'
  'data-part': 'swatch'
  'data-value': string
  'data-state': ReadSignal<'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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'checked' | 'unchecked'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'switch'
    'data-part': 'root'
    tabindex: ReadSignal<number>
    onClick: (e: MouseEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  track: {
    'data-state': ReadSignal<'checked' | 'unchecked'>
    'data-scope': 'switch'
    'data-part': 'track'
  }
  thumb: {
    'data-state': ReadSignal<'checked' | 'unchecked'>
    'data-scope': 'switch'
    'data-part': 'thumb'
  }
  hiddenInput: {
    type: 'checkbox'
    role: 'switch'
    'aria-hidden': 'true'
    tabindex: -1
    style: string
    checked: ReadSignal<boolean>
    disabled: ReadSignal<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: ReadSignal<number>
  'data-scope': 'table'
  'data-part': 'cell'
  /** 0-based row index — addresses the cell for roving DOM focus. Reactive
   * for the same reason `TableRowParts`'s `aria-rowindex` is. */
  'data-row-index': ReadSignal<number>
  /** 0-based column index — addresses the cell for roving DOM focus. Columns
   * do not reorder, so this stays a plain number. */
  'data-col-index': number
  'data-focused': ReadSignal<'' | undefined>
  onFocus: (e: FocusEvent) => void
  onKeyDown: (e: KeyboardEvent) => void
}
TableCheckboxParts from @llui/components
export interface TableCheckboxParts {
  role: 'checkbox'
  /**
   * The checkbox's accessible name (localized; `Locale['table']`). A
   * `role="checkbox"` span has no content to be named by, so without it
   * assistive tech announced an unnamed checkbox in every row (#268).
   */
  'aria-label': string
  'aria-checked': ReadSignal<'true' | 'false' | 'mixed'>
  'data-scope': 'table'
  'data-part': 'select-all' | 'row-checkbox'
  'data-state': ReadSignal<'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': ReadSignal<'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: ReadSignal<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': ReadSignal<number>
  'data-focused': ReadSignal<'' | undefined>
  'data-sortable': ReadSignal<'' | undefined>
  'data-sort': ReadSignal<SortDirection | undefined>
  onFocus: (e: FocusEvent) => void
  onClick: (e: MouseEvent) => void
  onKeyDown: (e: KeyboardEvent) => void
}
TableConnectOptions from @llui/components
export interface ConnectOptions {
  id: string
  /** Presentation density published on the grid root for either skin to consume. */
  density?: TableDensity
}
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 {
  /**
   * Place the native table carrying {@link root} directly inside this owned
   * scroll viewport. Wide grids then scroll locally without changing the
   * table's native layout/ARIA semantics or overflowing the page.
   */
  viewport: {
    'data-scope': 'table'
    'data-part': 'viewport'
  }
  root: {
    role: 'grid'
    id: string
    'aria-multiselectable': ReadSignal<'true' | undefined>
    'aria-rowcount': ReadSignal<number>
    'aria-colcount': ReadSignal<number>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-scope': 'table'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
    'data-density': TableDensity | undefined
  }
  columnHeader: (columnId: string) => TableColumnHeaderParts
  /**
   * `index` accepts a plain `number` OR a `ReadSignal<number>` (the row handle
   * `each`/`virtualEach` passes its render callback) — a keyed row is REUSED
   * (moved, not rebuilt) on reorder, so a plain number captured at build time
   * would freeze `aria-rowindex` and the row's own `toggleRow`/`selectRange`
   * dispatch at the row's ORIGINAL position forever. Pass the row's reactive
   * index handle whenever rows can reorder (sorting, filtering); a plain
   * number is still accepted for a table that never reorders.
   */
  row: (id: string, index: Reactive<number>) => TableRowParts
  /** `rowIndex` has the same `Reactive<number>` contract as {@link row}'s
   * `index` — the column index does not reorder and stays a plain `number`. */
  cell: (rowIndex: Reactive<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
  /** Same `Reactive<number>` contract as {@link row}'s `index`. */
  rowCheckbox: (id: string, index: Reactive<number>) => TableCheckboxParts
}
TableRowParts from @llui/components
export interface TableRowParts {
  role: 'row'
  'aria-selected': ReadSignal<boolean | undefined>
  /** Reactive: a row's DISPLAY position can change after sort/reorder without
   * this row being rebuilt (`each` reuses rows by key), so the index this
   * addresses must follow the row's live position rather than freeze at
   * whatever it was when the row was first built. */
  'aria-rowindex': ReadSignal<number>
  'data-scope': 'table'
  'data-part': 'row'
  'data-row': string
  'data-selected': ReadSignal<'' | 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': ReadSignal<boolean>
    'aria-controls': string
    'aria-disabled': ReadSignal<'true' | undefined>
    id: string
    'data-state': ReadSignal<'active' | 'inactive'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'tabs'
    'data-part': 'trigger'
    'data-value': string
    tabindex: ReadSignal<number>
    onClick: (e: MouseEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
    onFocus: (e: FocusEvent) => void
  }
  panel: {
    role: 'tabpanel'
    id: string
    'aria-labelledby': string
    tabindex: 0
    hidden: ReadSignal<boolean>
    'data-state': ReadSignal<'active' | 'inactive'>
    'data-scope': 'tabs'
    'data-part': 'panel'
    'data-value': string
  }
}
TabsParts from @llui/components
export interface TabsParts {
  root: {
    id: string
    'data-scope': 'tabs'
    'data-part': 'root'
    'data-orientation': ReadSignal<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': ReadSignal<Orientation>
  }
  list: {
    role: 'tablist'
    'aria-orientation': ReadSignal<Orientation>
    'data-scope': 'tabs'
    'data-part': 'list'
  }
  item: (value: string) => TabsItemParts
  /** Place once anywhere in the same build to keep automatic direction live. */
  directionSync: Mountable
}
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'
  dirSource: DirectionSource
}
TagItemParts from @llui/components
export interface TagItemParts {
  root: {
    tabindex: ReadSignal<number>
    'data-scope': 'tags-input'
    'data-part': 'tag'
    'data-value': string
    'data-index': string
    'data-focused': ReadSignal<'' | 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': ReadSignal<'true' | undefined>
    'data-scope': 'tags-input'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
  }
  input: {
    type: 'text'
    autocomplete: 'off'
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<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': ReadSignal<boolean>
    'aria-label': string
    onClick: (e: MouseEvent) => void
  }
  toggle: {
    type: 'button'
    'data-scope': 'theme-switch'
    'data-part': 'toggle'
    'data-theme': ReadSignal<Theme>
    'aria-label': string
    onClick: (e: MouseEvent) => void
  }
}
ThemeSwitchState from @llui/components
export interface ThemeSwitchState {
  theme: Theme
}
TickPlacement from @llui/components

Where an independent-axis tick label sits, and how to align it there.

export interface TickPlacement {
  x: number
  y: number
  /** SVG `text-anchor`. */
  anchor: 'start' | 'middle' | 'end'
  /** SVG `dominant-baseline`. */
  baseline: 'auto' | 'middle' | 'hanging'
}
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': ReadSignal<TimeFormat>
    'data-disabled': ReadSignal<'' | undefined>
  }
  hoursInput: {
    type: 'number'
    role: 'spinbutton'
    'aria-label': string
    'aria-valuemin': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number>
    disabled: ReadSignal<boolean>
    value: ReadSignal<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': ReadSignal<number>
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    'data-scope': 'time-picker'
    'data-part': 'minutes-input'
    onInput: (e: Event) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  periodTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'time-picker'
    'data-part': 'period-trigger'
    'data-period': ReadSignal<'AM' | 'PM'>
    onClick: (e: MouseEvent) => void
    hidden: ReadSignal<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': ReadSignal<'' | undefined>
    'data-direction': ReadSignal<Direction>
    /** Present once a countdown has reached its target (see `isComplete`). */
    'data-complete': ReadSignal<'' | undefined>
  }
  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: ReadSignal<boolean>
    onClick: (e: MouseEvent) => void
  }
  pauseTrigger: {
    type: 'button'
    'aria-label': string
    'data-scope': 'timer'
    'data-part': 'pause-trigger'
    disabled: ReadSignal<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 USER can dismiss the toast: `false` hides the close trigger
   * (`closeTrigger.hidden`) and ignores its click. A `dismiss` message from
   * code still removes it. Patchable. */
  dismissable: boolean
  /** The reasons currently holding the countdown paused, as a canonical set
   * (unique, in {@link ToastPauseReason} order). Empty = running. Owned by the
   * reducer: change it with `pause`/`resume`, read it with {@link isPaused}. */
  pausedBy: ToastPauseReason[]
  /** 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': ReadSignal<ToastPlacement>
  }
  /**
   * Build the per-row part descriptors for one toast. Takes the row's
   * `ReadSignal<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). Only `id` is immutable for a toast's lifetime — created then
   * dismissed, never structurally replaced — so this reads `id` once
   * internally to build id-derived wiring (the keyed `each` rebuilds the row
   * if `id` changes); every other field (`type`, `ariaLive`, `status`, …) is
   * bound reactively so an `update` patch renders wherever it appears.
   */
  toast: (toast: ReadSignal<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) => ReadSignal<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) => ReadSignal<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: {
    /**
     * Reactive: derived from the toast's current `type`/`ariaLive` (see
     * {@link politeness}), never frozen at mount — an `update` patching
     * either is visible here.
     */
    role: ReadSignal<'status' | 'alert'>
    'aria-atomic': 'true'
    /** Reactive — see `role` above. */
    'aria-live': ReadSignal<ToastPoliteness>
    id: string
    'data-scope': 'toast'
    'data-part': 'root'
    /** Reactive: an `update` patching `type` (e.g. a promise toast moving
     * loading → success) is visible here, not frozen at mount. */
    'data-type': ReadSignal<ToastType>
    'data-id': string
    /** Reactive presence status (closed/opening/open/closing) for CSS-driven
     * enter/exit animations. */
    'data-state': ReadSignal<PresenceStatus>
    onPointerEnter: (e: PointerEvent) => void
    onPointerLeave: (e: PointerEvent) => void
    /** Bubbling — a plain (non-bubbling) `onFocus`/`onBlur` here would never
     * fire for the only naturally focusable descendant, the close button,
     * so pause-on-focus would be dead: nothing but the row itself receives
     * `focus`/`blur` directly, and it carries no `tabindex`. */
    onFocusIn: (e: FocusEvent) => void
    onFocusOut: (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'
    /** Reactive: true while the toast is not `dismissable` — the button leaves
     * the accessibility tree and the tab order. Hiding it while it has focus
     * is safe for the `'focus'` pause reason: the browser's focus fixup fires
     * `blur`/`focusout` on the row (verified in Chromium), which releases it. */
    hidden: ReadSignal<boolean>
    /** Dismisses the toast; ignored while it is not `dismissable`. */
    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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    'data-state': ReadSignal<'on' | 'off'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'toggle-group'
    'data-part': 'item'
    'data-value': string
    tabindex: ReadSignal<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': ReadSignal<'true' | undefined>
    'data-scope': 'toggle-group'
    'data-part': 'root'
    'data-orientation': ReadSignal<Orientation>
    'data-disabled': ReadSignal<'' | 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
}
ToggleItemParts from @llui/components
export interface ToggleItemParts {
  type: 'button'
  'aria-label': string
  'aria-pressed': ReadSignal<boolean>
  'data-scope': 'gradient-picker'
  'data-part': ToggleItemPart
  'data-value': string
  'data-state': ReadSignal<'on' | 'off'>
  disabled: ReadSignal<boolean>
  onClick: (e: MouseEvent) => void
}
ToggleParts from @llui/components
export interface ToggleParts {
  root: {
    type: 'button'
    role: 'button'
    'aria-pressed': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    'data-state': ReadSignal<'on' | 'off'>
    'data-disabled': ReadSignal<'' | 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'
    /** The label part's id, or absent when `hasLabel: false`. */
    'aria-labelledby': string | undefined
  }
  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': ReadSignal<'' | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    tabindex: ReadSignal<number>
    onKeyDown: (e: KeyboardEvent) => void
    onFocus: () => void
  }
}
ToolbarParts from @llui/components
export interface ToolbarParts {
  root: {
    role: 'toolbar'
    'aria-orientation': ReadSignal<Orientation>
    'aria-label': string | undefined
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-scope': 'toolbar'
    'data-part': 'root'
    'data-orientation': ReadSignal<Orientation>
    'data-disabled': ReadSignal<'' | undefined>
  }
  separator: {
    role: 'separator'
    'aria-orientation': ReadSignal<Orientation>
    'data-scope': 'toolbar'
    'data-part': 'separator'
  }
  item: (value: string) => ToolbarItemParts
  /**
   * A labelled group's parts. The group names its `label` part through
   * `aria-labelledby`; a consumer that renders the group WITHOUT that label
   * passes `{ hasLabel: false }` so it never names an element that does not
   * exist (an unlabelled `role="group"` is valid; a broken idref is not — the
   * `dialog` `hasDescription` rule, #268). Default: true.
   */
  group: (label: string, options?: { readonly hasLabel?: boolean }) => 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: ReadSignal<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
  /** Selector for an arrow rendered inside the content element. When present,
   * floating positioning owns its absolute edge geometry transactionally. */
  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': ReadSignal<string | undefined>
    'data-state': ReadSignal<'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': ReadSignal<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
    /** The description part's id, or absent when `hasDescription: false`. */
    'aria-describedby': string | undefined
    'data-scope': 'tour'
    'data-part': 'root'
    hidden: ReadSignal<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: ReadSignal<boolean>
    'data-scope': 'tour'
    'data-part': 'prev-trigger'
    onClick: (e: MouseEvent) => void
  }
  nextTrigger: {
    type: 'button'
    'data-scope': 'tour'
    'data-part': 'next-trigger'
    'data-last': ReadSignal<'' | 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': ReadSignal<boolean | undefined>
    'aria-selected': ReadSignal<boolean | undefined>
    'aria-level': number
    'aria-busy': ReadSignal<'true' | undefined>
    tabindex: ReadSignal<number>
    'data-scope': 'tree-view'
    'data-part': 'item'
    'data-value': string
    'data-depth': string
    'data-selected': ReadSignal<'' | undefined>
    'data-focused': ReadSignal<'' | undefined>
    'data-loading': ReadSignal<'' | undefined>
    'data-load-failed': ReadSignal<'' | 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': ReadSignal<'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': ReadSignal<'true' | 'false' | 'mixed'>
    'data-scope': 'tree-view'
    'data-part': 'checkbox'
    'data-state': ReadSignal<'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': ReadSignal<'true' | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-scope': 'tree-view'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | 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
chart from @llui/components
const chart
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
gradientPicker from @llui/components
const gradientPicker
hoverCard from @llui/components
const hoverCard
iconConfig from @llui/components

Where glyphs are fetched from. Point this at a self-hosted Iconify to drop the third-party CDN; the path shape is identical.

const iconConfig
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
MAX_CALENDAR_TICKS from @llui/components

A hard ceiling on the boundaries one call will produce. Reached only by FORCING a fine unit across an enormous span; the automatic ladder can never exceed maxTicks. Over the ceiling the answer is an EMPTY array — no gridlines — rather than a truncated one, because a truncated axis silently claims the data ends where the array does.

const MAX_CALENDAR_TICKS
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
sparkline from @llui/components
const sparkline
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

addUnits() from @llui/components/utils

Advance an instant by n whole units, in the offset's calendar.

function addUnits(at: number, unit: CalendarUnit, n: number, opts: CalendarOptions = {}): number
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>
annularSectorPath() from @llui/components/utils

An annular sector — the polar answer to a bar. r0 is the inner radius (0 gives a pie wedge, > 0 a donut segment); angles follow {@link polarPoint}.

A sweep of a full turn or more is drawn as a complete ring, because the arc endpoints would otherwise coincide and SVG would draw nothing at all.

function annularSectorPath(cx: number, cy: number, r0: number, r1: number, a0: number, a1: number): string
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[]
areaPath() from @llui/components/utils

A filled region between an upper run and a lower run. The lower run is walked BACKWARDS and appended, then the path is closed — one subpath, so a fill-rule cannot punch a hole in it the way two separate subpaths can.

function areaPath(upper: readonly Point[], lower: readonly Point[], curve: Curve, axis: CurveAxis = 'x'): string
attachFloating() from @llui/components/utils

Position floating relative to anchor with live updates on scroll/resize. Owns position, top, left, transform and the two available-size custom properties ({@link FLOATING_AVAILABLE_HEIGHT}) on floating; position and all four physical inset properties on an optional arrow; and placement attributes on stateTarget. The arrow's static-side inset is half its untransformed layout size, so a square arrow straddles the resolved edge. Cleanup is idempotent, suppresses pending writes/callbacks, and restores the exact prior values (including priority) or absence of those properties.

export declare function attachFloating(opts: FloatingOptions): () => void;
bandCenter() from @llui/components/utils

The centre of band i in normalized u.

function bandCenter(i: number, band: Band): number
bandExtent() from @llui/components/utils

The [start, end] extent of band i in normalized u.

function bandExtent(i: number, band: Band): [number, number]
calendarTicks() from @llui/components/utils

Calendar boundaries inside [start, end], ascending.

An EMPTY array for a non-finite or reversed range, and for a request that would exceed {@link MAX_CALENDAR_TICKS} — a chart with no data is a normal state, and a truncated axis is a lie.

function calendarTicks(start: number, end: number, opts: CalendarStepOptions = {}): CalendarTick[]
cartesianProjection() from @llui/components/utils
function cartesianProjection(frame: Frame, opts: CartesianOptions = {}): Projection
chooseCalendarStep() from @llui/components/utils

The finest rung whose boundaries fit inside maxTicks.

A FORCED unit is honoured as given (stride 1), except on year, where the stride still grows to fit — "yearly" over four centuries has to mean something, and 400 gridlines is not it.

function chooseCalendarStep(start: number, end: number, opts: CalendarStepOptions = {}): CalendarStep
circlePath() from @llui/components/utils

A full circle, as two arcs — one arc of 360° is degenerate and draws nothing, which is the classic way a polar gridline disappears.

function circlePath(cx: number, cy: number, r: number): string
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
countCalendarTicks() from @llui/components/utils

How many boundaries of step fall inside [start, end] — WITHOUT building them. Constant time at every rung, which is what lets the ladder reject a finer unit for a century-long span for free.

One informational edge, in the safe direction: within a month of the extremes of the representable range (year ±271821), flooring to a period start falls outside what a Date can hold and {@link utcAt} answers NaN, so the count is 0 and {@link calendarTicks} draws no gridlines. It neither hangs nor throws; the axis is simply unlabelled at the end of time.

function countCalendarTicks(start: number, end: number, step: CalendarStep, opts: CalendarOptions = {}): number
cssColorAlpha() from @llui/components/utils

Resolved alpha (none -> 1, CSS Color 4's used value for a missing alpha OUTSIDE interpolation). Inside {@link interpolateColor}, a missing alpha instead carries the OTHER endpoint's alpha — see its doc comment.

function cssColorAlpha(c: CssColor): number
cssColorToHex() from @llui/components/utils

Gamut-map (if needed) and convert to a displayable hex string. #rrggbb when alpha is (resolved to) 1, #rrggbbaa otherwise.

function cssColorToHex(c: CssColor): string
cssColorToSrgb() from @llui/components/utils

Convert any {@link CssColor} into fully-resolved (non-null) Srgb, treating none as 0 and NOT gamut-mapping (an oklch/oklab input outside sRGB stays outside [0,1]).

function cssColorToSrgb(c: CssColor): Srgb
curvePath() from @llui/components/utils

Join a run of points with the named curve. closed applies to linear only — the other two are defined on an open, x-increasing run.

function curvePath(points: readonly Point[], curve: Curve, closed = false, axis: CurveAxis = 'x'): string
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
denormalize() from @llui/components/utils

Inverse of {@link normalize}.

function denormalize(u: number, domain: Domain): 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;
floorToUnit() from @llui/components/utils

Floor an instant to the start of its unit, in the offset's calendar.

function floorToUnit(at: number, unit: CalendarUnit, opts: CalendarOptions = {}): number
fmt() from @llui/components/utils

Format a coordinate. A non-finite input becomes 0 — a NaN in a path silently voids the WHOLE path element, taking every later command with it.

function fmt(n: number): 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;
formatHex() from @llui/components/utils
function formatHex(rgb: Rgb255): string
formatHex8() from @llui/components/utils
function formatHex8(rgb: Rgb255, alpha: number): string
formatOklch() from @llui/components/utils

oklch(L C H) / oklch(L C H / A). Fixed precision: L as a 0–1 fraction at 4 dp, C at 4 dp, H at 2 dp — chosen so the string is stable and round-trips through {@link parseCssColor} without drifting.

function formatOklch(ok: Oklch, alpha = 1): string
formatRgb() from @llui/components/utils
function formatRgb(rgb: Rgb255, alpha = 1): string
gamutMapOklchToSrgb() from @llui/components/utils

CSS Color 4 §13.2 gamut mapping: reduce OKLCH chroma by binary search until simple-clipping the result into sRGB is perceptually indistinguishable (deltaEOK < the 0.02 JND) from the reduced color, then clip. Given an already-in-gamut color this returns it converted (and defensively clipped for float noise) rather than a no-op, so the result is always safe to hand straight to {@link srgbToRgb255}. test/utils/color.test.ts pins the converged chroma for a known out-of-gamut color as a regression value (this file's own binary search, not an independent oracle — see that test's comment for what it does and does not prove).

function gamutMapOklchToSrgb(ok: Oklch): Srgb
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[];
hslToHsv() from @llui/components/utils

Convert HSL (h 0-360, s/l 0-100) to HSV (h 0-360, s/v 0-100), float.

function hslToHsv(hsl: Hsl): Hsv
hslToRgb255() from @llui/components/utils

Convert HSL (h 0-360, s/l 0-100) to RGB (0-255 each, rounded).

function hslToRgb255(hsl: Hsl): Rgb255
hsvToHsl() from @llui/components/utils

Convert HSV (h 0-360, s/v 0-100) to HSL (h 0-360, s/l 0-100), float.

function hsvToHsl(hsv: Hsv): Hsl
hsvToOklch() from @llui/components/utils
function hsvToOklch(hsv: Hsv): Oklch
hsvToRgb255() from @llui/components/utils

HSV -> RGB255, via the DIRECT (not HSL-routed) float conversion.

function hsvToRgb255(hsv: Hsv): Rgb255
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>
inSrgbGamut() from @llui/components/utils

Whether ok, converted to sRGB, falls inside [0,1] on every channel (within epsilon — floating-point round-trip noise).

function inSrgbGamut(ok: Oklch, epsilon = 1e-4): boolean
interpolateColor() from @llui/components/utils

Interpolate between two colors per CSS Color 4 §12: convert both into space, resolve missing (none, or a cross-space-converted achromatic color's undefined) components — including alpha itself, which carries the OTHER endpoint's alpha when missing rather than defaulting to 1 (CSS Color 4 §12.2's analogous-component rule applies to alpha too — rgb(255 0 0 / none) mixed with rgb(0 0 255 / 0.2) has alpha exactly 0.2 throughout, not a lerp toward 1; pinned in test/utils/color.test.ts) — premultiply non-hue channels by alpha, lerp, then un-premultiply. hueMethod controls which way a hue-bearing space (hsl, oklch) rotates, normalizing both hues to [0,360) FIRST (see {@link adjustHue}); ignored for spaces with no hue.

function interpolateColor(a: CssColor, b: CssColor, t: number, space: InterpolationSpace, hueMethod: HueInterpolationMethod = 'shorter'): CssColor
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
linearPath() from @llui/components/utils

Straight segments through every point.

function linearPath(points: readonly Point[], closed = false): string
linearSrgbToOklab() from @llui/components/utils
function linearSrgbToOklab(s: Srgb): Oklab
linearToSrgb() from @llui/components/utils
function linearToSrgb(s: Srgb): Srgb
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>
monotonePath() from @llui/components/utils

Monotone cubic interpolation (Fritsch–Carlson). Its defining property is that the curve NEVER OVERSHOOTS the data: between two samples it stays within their values, so a series that only rises cannot dip below a point on the way. A plain Catmull-Rom or cardinal spline does overshoot, and on a chart that means drawing a number nobody measured.

Defined on a function y = f(x) with strictly increasing x. It is therefore meaningless on a closed angular loop, which is why polarProjection declines it — see projection.ts.

function monotonePath(points: readonly Point[], axis: CurveAxis = 'x'): string
nearestBand() from @llui/components/utils

The band index nearest a normalized position — the hit test behind hover and pointer tracking. Returns null for an empty axis.

Nearest-CENTRE, not containment: the gaps between bands belong to whichever band is closer, so a pointer never falls into a dead zone between two bars.

function nearestBand(u: number, band: Band): number | null
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
niceDomain() from @llui/components/utils

Extend [min, max] outward to the nearest round tick boundaries, so the axis ends on a labelled value instead of mid-air. Iterates because widening the domain can change the chosen step, which can change the boundaries again; d3's nice does the same, and it converges in at most a few rounds.

function niceDomain(min: number, max: number, count: number): Domain
normalize() from @llui/components/utils

Map a value into [0, 1] across domain. A DEGENERATE domain (min === max) maps everything to 0.5 rather than dividing by zero — a single-valued series draws as a flat line through the middle, which is the only reading that is not a crash or a NaN in a path string.

function normalize(value: number, domain: Domain): number
oklabToLinearSrgb() from @llui/components/utils
function oklabToLinearSrgb(lab: Oklab): Srgb
oklabToOklch() from @llui/components/utils
function oklabToOklch(lab: Oklab): Oklch
oklabToSrgb() from @llui/components/utils

NOT gamut-mapped — may return channels outside [0,1] for an OKLab/OKLCH color outside sRGB. Use {@link gamutMapOklchToSrgb} for a displayable result.

function oklabToSrgb(lab: Oklab): Srgb
oklchToHsv() from @llui/components/utils

Gamut-maps into sRGB first — HSV cannot represent an out-of-gamut color.

function oklchToHsv(ok: Oklch): Hsv
oklchToOklab() from @llui/components/utils
function oklchToOklab(ok: Oklch): Oklab
oklchToSrgb() from @llui/components/utils

NOT gamut-mapped. See {@link oklabToSrgb}.

function oklchToSrgb(ok: Oklch): Srgb
parseCssColor() from @llui/components/utils

Parse a CSS color string into its typed model. Supports #rgb/#rgba/ #rrggbb/#rrggbbaa, rgb()/rgba(), hsl()/hsla() (both legacy comma and modern space/slash syntax, with the legacy grammar's none-forbidden and uniform-percentage-or-number rules enforced), hwb(), oklch(), oklab(), lab(), lch(), color() (predefined spaces srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020, xyz, xyz-d50, xyz-d65), none components, transparent, and the 148 CSS Color 4 named colors. Returns null on anything else — never throws.

function parseCssColor(input: string): CssColor | null
parseDateValue() from @llui/components/utils
function parseDateValue(value: DateValue): ParsedDateValue
parseHexColor() from @llui/components/utils

Parse #rgb, #rgba, #rrggbb, #rrggbbaa. Returns null on anything else, never throws.

function parseHexColor(hex: string): { rgb: Rgb255; alpha: number } | null
pointerDragHandlers() from @llui/components/utils

Build the five pointer handlers for one drag track. One call per live connect() instance — dragging/activePointerId are closure-scoped per instance, the same shape as this package's other per-instance mutable state (e.g. color-picker's eyedropper pendingEyeDropper AbortController).

  • Primary button only on pointerdown (e.button !== 0 is ignored), AND only when no drag is already live — a second finger/pointer touching down mid-drag is ignored rather than hijacking activePointerId.
  • Every subsequent event (pointermove/pointerup/pointercancel/ onLostPointerCapture) is checked against activePointerId: a second pointer's events never affect a drag it didn't start.
  • setPointerCapture on pointerdown's currentTarget, so pointermove keeps firing even once the pointer leaves the element's bounds — a plain drag with no capture stalls out at the track's edge instead of saturating like a native <input type="range">.
  • Released on pointerup/pointercancel, guarded by hasPointerCapture so releasing twice (or without ever capturing) is a harmless no-op.
  • dragging is unconditionally reset on onLostPointerCapture for the active pointer, even though that handler never calls release() itself (capture is already gone by definition when this fires).
function pointerDragHandlers(callbacks: PointerDragCallbacks): PointerDragHandlers
polarPoint() from @llui/components/utils

A point on a circle. Angles are RADIANS CLOCKWISE FROM 12 O'CLOCK, which is where every reader expects a chart to start; SVG's own 0 is 3 o'clock.

function polarPoint(cx: number, cy: number, r: number, angle: number): Point
polarProjection() from @llui/components/utils
function polarProjection(frame: Frame, opts: PolarOptions = {}): Projection
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
projectionFor() from @llui/components/utils

Build the projection a chart's coordinate setting names.

function projectionFor(coord: ChartCoord, frame: Frame, opts: CartesianOptions & PolarOptions = {}): Projection
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;
rectPath() from @llui/components/utils

An axis-aligned rectangle, corners in any order.

function rectPath(x0: number, y0: number, x1: number, y1: number): string
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).

The walk crosses SHADOW boundaries: Element.closest() stops at the nearest shadow root and cannot see a dir set on an ancestor of the host, so an element inside a shadow tree whose host (or the host's own ancestors) declares dir would otherwise silently read as ltr. Continuing from root.host after getRootNode() returns a ShadowRoot walks out to the light-DOM ancestor and keeps going, arbitrarily many shadow levels deep. The final fallback reads dir off the element's OWN document (ownerDocument), never the global document — the global binding names a DIFFERENT document inside an iframe or any other multi-document context.

export declare function resolveDir(el: Element): TextDirection;
resolveNone() from @llui/components/utils

A null (CSS none) component behaves as 0 outside interpolation.

function resolveNone(n: number | null): number
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;
rgb255ToHsl() from @llui/components/utils

Convert RGB (0-255 each) to HSL (h 0-360, s/l 0-100), float.

function rgb255ToHsl(rgb: Rgb255): Hsl
rgb255ToHsv() from @llui/components/utils

RGB255 -> HSV, via the DIRECT (not HSL-routed) float conversion.

function rgb255ToHsv(rgb: Rgb255): Hsv
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
srgb255ToSrgb() from @llui/components/utils
function srgb255ToSrgb(rgb: Rgb255): Srgb
srgbToLinear() from @llui/components/utils
function srgbToLinear(s: Srgb): Srgb
srgbToOklab() from @llui/components/utils
function srgbToOklab(s: Srgb): Oklab
srgbToOklch() from @llui/components/utils
function srgbToOklch(s: Srgb): Oklch
srgbToRgb255() from @llui/components/utils

Rounds AND clamps into 0-255 — the boundary where an out-of-gamut float (an un-mapped OKLCH conversion) must finally become a displayable byte.

function srgbToRgb255(s: Srgb): Rgb255
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
stepPath() from @llui/components/utils

Axis-aligned staircase: hold each value until halfway along the independent axis, then step.

function stepPath(points: readonly Point[], axis: CurveAxis = 'x'): string
tickIncrement() from @llui/components/utils

The step size a nice axis would use to fit about count ticks into [start, stop]. A negative return means "one tick every 1/-step units" — d3's encoding, kept because the reciprocal form avoids the float drift that 10 ** -power introduces for small steps.

function tickIncrement(start: number, stop: number, count: number): number
ticks() from @llui/components/utils

Round, human-readable tick values covering [start, stop]. Returns an EMPTY array for a non-finite or degenerate range rather than looping forever — a chart with no data is a normal state, not an error.

function ticks(start: number, stop: number, count: number): 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
valueDomain() from @llui/components/utils

The value-axis domain for a set of series.

min / max are UNBOUNDED-CAPABLE bounds in the sense of finiteBound (#177): an absent one means "derive it", never ±Infinity. A derived domain always INCLUDES ZERO, because a bar whose baseline is not zero misrepresents its own length — the one axis default worth being opinionated about. A caller who wants otherwise passes both bounds.

function valueDomain(values: readonly number[], opts: { min?: number; max?: number; tickCount: number }): Domain
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

CalendarUnit from @llui/components/utils

Rungs of the ladder, finest first.

export type CalendarUnit = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'
CssColor from @llui/components/utils

A color as CSS Color 4 sees it. lab()/lch()/wide-gamut color() are NOT separate tags here — see the module doc — they resolve to 'oklch' (or 'srgb' for color(srgb ...)/color(srgb-linear ...)) at parse time.

export type CssColor =
  | { space: 'srgb'; r: number | null; g: number | null; b: number | null; alpha: number | null }
  | {
      space: 'srgb-linear'
      r: number | null
      g: number | null
      b: number | null
      alpha: number | null
    }
  | { space: 'hsl'; h: number | null; s: number | null; l: number | null; alpha: number | null }
  | { space: 'oklab'; l: number | null; a: number | null; b: number | null; alpha: number | null }
  | { space: 'oklch'; l: number | null; c: number | null; h: number | null; alpha: number | null }
Curve from @llui/components/utils

How a run of points is joined.

export type Curve = 'linear' | 'monotone' | 'step'
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)
HueInterpolationMethod from @llui/components/utils
export type HueInterpolationMethod = 'shorter' | 'longer' | 'increasing' | 'decreasing'
InterpolationSpace from @llui/components/utils

Interpolation / generic-conversion target spaces (CSS Color 4 §12).

export type InterpolationSpace = 'srgb' | 'srgb-linear' | 'oklab' | 'oklch' | 'hsl'
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 }
}
Band from @llui/components/utils

A discrete axis of count slots laid out across [0, 1].

export interface Band {
  count: number
  /** Gap between adjacent bands, as a fraction of a slot. */
  paddingInner: number
  /** Gap before the first and after the last band, as a fraction of a slot. */
  paddingOuter: number
}
CalendarOptions from @llui/components/utils
export interface CalendarOptions {
  /**
   * Minutes east of UTC that boundaries are measured in. Default 0 (UTC). A
   * FIXED offset — see the module note; `-300` is US Eastern standard time and
   * stays at −5 across a DST transition rather than following it.
   */
  offsetMinutes?: number
  /** First day of the week for the `week` rung: 0 = Sunday … 6 = Saturday.
   *  Default 1 (Monday), matching ISO 8601. */
  weekStartsOn?: number
}
CalendarStep from @llui/components/utils

A chosen rung, plus how many of it each boundary advances. stride is always 1 except on year.

export interface CalendarStep {
  unit: CalendarUnit
  stride: number
}
CalendarStepOptions from @llui/components/utils
export interface CalendarStepOptions extends CalendarOptions {
  /** Upper bound on the number of boundaries. Default 6. */
  maxTicks?: number
  /** Force a rung instead of choosing one from the span. */
  unit?: CalendarUnit
}
CalendarTick from @llui/components/utils
export interface CalendarTick {
  /** The boundary instant, in epoch milliseconds. */
  at: number
  unit: CalendarUnit
}
CartesianOptions from @llui/components/utils
export interface CartesianOptions {
  /** Swap the axes: `u` runs top-to-bottom and `v` left-to-right. This is what
   *  turns a column chart into a horizontal bar chart, and it is a PROJECTION
   *  concern — no mark, scale or hit test changes. */
  horizontal?: boolean
}
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;
}
Domain from @llui/components/utils

A closed numeric interval. Both ends are finite by construction.

export interface Domain {
  min: number
  max: number
}
FloatingOptions from @llui/components/utils
export interface FloatingOptions {
    /** The reference element (trigger/anchor). */
    anchor: Element;
    /** The floating element (content). */
    floating: HTMLElement;
    /**
     * Element that receives the resolved full `data-placement` and physical
     * `data-side`. Defaults to `floating`. Use a separate content element when
     * `floating` is a geometry-only positioner wrapper.
     */
    stateTarget?: 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 the first
     * tab-reachable descendant, else the container itself (given a temporary
     * `tabindex="-1"` when it has none). */
    initialFocus?: Element | (() => Element | null);
    /** Restore focus to the previously active element on release (default: true). */
    restoreFocus?: boolean;
}
Frame from @llui/components/utils

The plot area in user units — the box marks are drawn inside, already inset by whatever padding the axes need.

export interface Frame {
  x: number
  y: number
  width: number
  height: number
}
Hsl from @llui/components/utils

HSL color. h 0–360 degrees, s/l 0–100. Float — see the module doc.

export interface Hsl {
  h: number
  s: number
  l: number
}
Hsv from @llui/components/utils

HSV color. h 0–360 degrees, s/v 0–100. Float — see the module doc.

export interface Hsv {
  h: number
  s: number
  v: number
}
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
}
Oklab from @llui/components/utils

Oklab. l is 0–1 lightness; a/b are unbounded (±0.4 roughly spans sRGB).

export interface Oklab {
  l: number
  a: number
  b: number
}
Oklch from @llui/components/utils

OKLCH. l 0–1, c >= 0 (unbounded — maxChroma in the color picker is a UI choice, not a property of the color space), h in degrees.

export interface Oklch {
  l: number
  c: number
  h: 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
}
Point from @llui/components/utils
export interface Point {
  x: number
  y: number
}
PointerDragCallbacks from @llui/components/utils

One call site's drag callbacks.

export interface PointerDragCallbacks {
  /** Checked on `pointerdown` only — a drag that is already underway keeps
   * running even if this flips true mid-drag (the reducer's own `disabled`
   * guard is the actual safety net for a message dispatched from `onDrag`
   * after that point; this only decides whether a NEW drag may start). */
  isDisabled: () => boolean
  /** Fires once, on `pointerdown`, after capture is acquired and BEFORE the
   * first `onDrag` call — the hook for a one-time side effect like moving
   * focus onto a thumb element. */
  onDragStart?: (e: PointerEvent) => void
  /** Fires on `pointerdown` (immediately after `onDragStart`) and again on
   * every `pointermove` while the drag is live. */
  onDrag: (e: PointerEvent) => void
  /** Fires once on `pointerup`/`pointercancel`, but ONLY when a drag was
   * actually live (never on a stray up/cancel with no matching down) — the
   * counterpart to `onDragStart`, for a caller that tracks per-drag identity
   * across the lifecycle (`gradient-picker`'s track drag creates a new stop
   * on `pointerdown` and must stop targeting it once the drag ends). Runs
   * AFTER pointer capture is released. */
  onDragEnd?: (e: PointerEvent) => void
}
PointerDragHandlers from @llui/components/utils
export interface PointerDragHandlers {
  onPointerDown: (e: PointerEvent) => void
  onPointerMove: (e: PointerEvent) => void
  onPointerUp: (e: PointerEvent) => void
  onPointerCancel: (e: PointerEvent) => void
  /** The browser can revoke pointer capture without ever firing `pointerup`/
   * `pointercancel` (an OS-level interruption, a nested capture request, …).
   * Left unwired, `dragging` would stay `true` forever and a later, UNRELATED
   * pointer's `pointermove` would be misread as a continuation of this drag.
   * Spread onto the same element as the other four. */
  onLostPointerCapture: (e: PointerEvent) => void
}
PolarOptions from @llui/components/utils
export interface PolarOptions {
  /** Inner radius as a fraction of the outer. `0` is a pie, `0.5` a donut, and
   *  a radar usually wants `0`. */
  innerRadius?: number
  /** Where `u = 0` sits, in radians clockwise from 12 o'clock. */
  startAngle?: number
  /** Total sweep in radians. Defaults to a full turn, which is what makes the
   *  projection `closed`. */
  sweep?: number
  /**
   * Gridline shape. `ring` is a circle at the given magnitude — right for a
   * pie or a rose. `web` joins the `spokes` sample positions with straight
   * segments — right for a radar, where a circular grid reads as a different
   * chart from the polygonal data on top of it.
   */
  grid?: 'ring' | 'web'
  /** How many spokes a `web` gridline has. Ignored for `ring`. */
  spokes?: number
  /**
   * Swap the axes, exactly as {@link CartesianOptions.horizontal} does: the
   * INDEPENDENT axis moves off the angle and onto the RADIUS, so each category
   * becomes its own concentric ring and magnitude is read as arc length. That
   * is a radial bar chart, and it is the polar reading of the same flag that
   * turns a column chart on its side — one field, the same meaning in both
   * coordinate systems, so a consumer flipping `coord` keeps whatever
   * orientation they asked for.
   *
   * The independent axis is then the radius, which is NOT a loop, so the
   * projection reports `closed: false` however wide the sweep is.
   */
  horizontal?: boolean
}
Projection from @llui/components/utils
export interface Projection {
  readonly kind: ChartCoord
  /**
   * Whether the independent axis is a CLOSED loop. `true` for a polar
   * projection covering a full turn, which is what makes a radar outline join
   * back to its first vertex instead of leaving a gap.
   */
  readonly closed: boolean
  /**
   * Curves this projection can honour. A caller asking for one outside the set
   * gets `linear` — see the polar note below for why this is the honest answer
   * rather than a silent approximation.
   */
  readonly curves: readonly Curve[]
  /** Normalized (u, v) → a point in user units. */
  point(u: number, v: number): Point
  /** A run of samples joined along increasing `u`. */
  line(samples: readonly Sample[], curve: Curve): string
  /** The region between an upper run and a lower run of the same length. */
  area(upper: readonly Sample[], lower: readonly Sample[], curve: Curve): string
  /** A band spanning `u0..u1` and `v0..v1`: a bar, or a wedge. */
  band(u0: number, u1: number, v0: number, v1: number): string
  /** The iso-magnitude line at `v` — a value gridline. */
  gridline(v: number): string
  /** Where the independent-axis tick for `u` belongs. */
  tick(u: number): TickPlacement
  /** Where the DEPENDENT-axis tick for `v` belongs. */
  valueTick(v: number): TickPlacement
  /**
   * Invert a point in user units back to `u`, for pointer hit testing. Returns
   * `null` when the point carries no meaningful `u` — the exact centre of a
   * polar chart, where every angle is equally close.
   */
  locate(x: number, y: number): number | null
}
Rgb255 from @llui/components/utils

8-bit-per-channel sRGB, 0–255 (integers once produced by this module).

export interface Rgb255 {
  r: number
  g: number
  b: number
}
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;
}
Sample from @llui/components/utils

A normalized sample: position along the independent axis, and magnitude.

export interface Sample {
  u: number
  v: number
}
Srgb from @llui/components/utils

sRGB, gamma-encoded, 0–1 per channel. May fall outside [0,1] as an intermediate value (e.g. converted from an out-of-gamut OKLCH) — callers that need an in-gamut result go through {@link gamutMapOklchToSrgb}.

export interface Srgb {
  r: number
  g: number
  b: number
}
TickPlacement from @llui/components/utils

Where an independent-axis tick label sits, and how to align it there.

export interface TickPlacement {
  x: number
  y: number
  /** SVG `text-anchor`. */
  anchor: 'start' | 'middle' | 'end'
  /** SVG `dominant-baseline`. */
  baseline: 'auto' | 'middle' | 'hanging'
}
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[]
MAX_CALENDAR_TICKS from @llui/components/utils

A hard ceiling on the boundaries one call will produce. Reached only by FORCING a fine unit across an enormous span; the automatic ladder can never exceed maxTicks. Over the ceiling the answer is an EMPTY array — no gridlines — rather than a truncated one, because a truncated axis silently claims the data ends where the array does.

const MAX_CALENDAR_TICKS
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/calendar-ticks

Functions

addUnits() from @llui/components/utils/calendar-ticks

Advance an instant by n whole units, in the offset's calendar.

function addUnits(at: number, unit: CalendarUnit, n: number, opts: CalendarOptions = {}): number
calendarTicks() from @llui/components/utils/calendar-ticks

Calendar boundaries inside [start, end], ascending.

An EMPTY array for a non-finite or reversed range, and for a request that would exceed {@link MAX_CALENDAR_TICKS} — a chart with no data is a normal state, and a truncated axis is a lie.

function calendarTicks(start: number, end: number, opts: CalendarStepOptions = {}): CalendarTick[]
chooseCalendarStep() from @llui/components/utils/calendar-ticks

The finest rung whose boundaries fit inside maxTicks.

A FORCED unit is honoured as given (stride 1), except on year, where the stride still grows to fit — "yearly" over four centuries has to mean something, and 400 gridlines is not it.

function chooseCalendarStep(start: number, end: number, opts: CalendarStepOptions = {}): CalendarStep
countCalendarTicks() from @llui/components/utils/calendar-ticks

How many boundaries of step fall inside [start, end] — WITHOUT building them. Constant time at every rung, which is what lets the ladder reject a finer unit for a century-long span for free.

One informational edge, in the safe direction: within a month of the extremes of the representable range (year ±271821), flooring to a period start falls outside what a Date can hold and {@link utcAt} answers NaN, so the count is 0 and {@link calendarTicks} draws no gridlines. It neither hangs nor throws; the axis is simply unlabelled at the end of time.

function countCalendarTicks(start: number, end: number, step: CalendarStep, opts: CalendarOptions = {}): number
floorToUnit() from @llui/components/utils/calendar-ticks

Floor an instant to the start of its unit, in the offset's calendar.

function floorToUnit(at: number, unit: CalendarUnit, opts: CalendarOptions = {}): number

Types

CalendarUnit from @llui/components/utils/calendar-ticks

Rungs of the ladder, finest first.

export type CalendarUnit = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'

Interfaces

CalendarOptions from @llui/components/utils/calendar-ticks
export interface CalendarOptions {
  /**
   * Minutes east of UTC that boundaries are measured in. Default 0 (UTC). A
   * FIXED offset — see the module note; `-300` is US Eastern standard time and
   * stays at −5 across a DST transition rather than following it.
   */
  offsetMinutes?: number
  /** First day of the week for the `week` rung: 0 = Sunday … 6 = Saturday.
   *  Default 1 (Monday), matching ISO 8601. */
  weekStartsOn?: number
}
CalendarStep from @llui/components/utils/calendar-ticks

A chosen rung, plus how many of it each boundary advances. stride is always 1 except on year.

export interface CalendarStep {
  unit: CalendarUnit
  stride: number
}
CalendarStepOptions from @llui/components/utils/calendar-ticks
export interface CalendarStepOptions extends CalendarOptions {
  /** Upper bound on the number of boundaries. Default 6. */
  maxTicks?: number
  /** Force a rung instead of choosing one from the span. */
  unit?: CalendarUnit
}
CalendarTick from @llui/components/utils/calendar-ticks
export interface CalendarTick {
  /** The boundary instant, in epoch milliseconds. */
  at: number
  unit: CalendarUnit
}

Constants

MAX_CALENDAR_TICKS from @llui/components/utils/calendar-ticks

A hard ceiling on the boundaries one call will produce. Reached only by FORCING a fine unit across an enormous span; the automatic ladder can never exceed maxTicks. Over the ceiling the answer is an EMPTY array — no gridlines — rather than a truncated one, because a truncated axis silently claims the data ends where the array does.

const MAX_CALENDAR_TICKS

@llui/components/utils/child-send

Functions

wrapChildSend() from @llui/components/utils/child-send

Wrap a child machine's send into the PARENT's own message type, the way menubar.ts's per-submenu menuSend and gradient-picker.ts's embedded pickerSend both do — and additionally mark the returned function's __lluiVariants, which plain mapSend (@llui/dom) does not.

That tag is what makes every tagSend(...) call built INSIDE the child's own connect() report the PARENT's wrapper variant(s) instead of the child's own specific ones (binding-descriptors.ts's tagSend: a send.__lluiVariants tag on the dispatcher itself always wins over the libraryVariants array a given tagSend call names). That is the TRUTHFUL external view: an agent driving the parent only ever sees the wrapper type dispatched ('picker', 'menuMsg', …), with the child's real action nested inside msg — not menu.ts's internal 'highlight'/ 'selectItem'/… vocabulary leaking out as if the parent could dispatch those directly. Left unset (the untagged mapSend), a submenu's own tagSend(send, ['highlightNext'], …) call reports 'highlightNext' to anything introspecting the PARENT's variants — a message type the parent's own Msg union does not even have a case for.

function wrapChildSend<Outer extends { type: string }, Inner>(send: Send<Outer>, wrap: (inner: Inner) => Outer, variants: readonly Outer['type'][]): Send<Inner>

@llui/components/utils/color

Functions

cssColorAlpha() from @llui/components/utils/color

Resolved alpha (none -> 1, CSS Color 4's used value for a missing alpha OUTSIDE interpolation). Inside {@link interpolateColor}, a missing alpha instead carries the OTHER endpoint's alpha — see its doc comment.

function cssColorAlpha(c: CssColor): number
cssColorToHex() from @llui/components/utils/color

Gamut-map (if needed) and convert to a displayable hex string. #rrggbb when alpha is (resolved to) 1, #rrggbbaa otherwise.

function cssColorToHex(c: CssColor): string
cssColorToSrgb() from @llui/components/utils/color

Convert any {@link CssColor} into fully-resolved (non-null) Srgb, treating none as 0 and NOT gamut-mapping (an oklch/oklab input outside sRGB stays outside [0,1]).

function cssColorToSrgb(c: CssColor): Srgb
formatHex() from @llui/components/utils/color
function formatHex(rgb: Rgb255): string
formatHex8() from @llui/components/utils/color
function formatHex8(rgb: Rgb255, alpha: number): string
formatHsl() from @llui/components/utils/color

hsl(H S% L%) / hsl(H S% L% / A). gradient-picker's colorAt needs this so an in hsl interpolation result can be serialized without an extra (lossy) round trip through sRGB bytes.

function formatHsl(hsl: Hsl, alpha = 1): string
formatOklab() from @llui/components/utils/color

oklab(L A B) / oklab(L A B / A), same precision convention as {@link formatOklch} — gradient-picker's colorAt needs this to stay exact when the interpolation space itself is oklab (interpolating in oklch and in oklab are different color spaces, and only one of them has a serializer here otherwise).

function formatOklab(lab: Oklab, alpha = 1): string
formatOklch() from @llui/components/utils/color

oklch(L C H) / oklch(L C H / A). Fixed precision: L as a 0–1 fraction at 4 dp, C at 4 dp, H at 2 dp — chosen so the string is stable and round-trips through {@link parseCssColor} without drifting.

function formatOklch(ok: Oklch, alpha = 1): string
formatRgb() from @llui/components/utils/color
function formatRgb(rgb: Rgb255, alpha = 1): string
gamutMapOklchToSrgb() from @llui/components/utils/color

CSS Color 4 §13.2 gamut mapping: reduce OKLCH chroma by binary search until simple-clipping the result into sRGB is perceptually indistinguishable (deltaEOK < the 0.02 JND) from the reduced color, then clip. Given an already-in-gamut color this returns it converted (and defensively clipped for float noise) rather than a no-op, so the result is always safe to hand straight to {@link srgbToRgb255}. test/utils/color.test.ts pins the converged chroma for a known out-of-gamut color as a regression value (this file's own binary search, not an independent oracle — see that test's comment for what it does and does not prove).

function gamutMapOklchToSrgb(ok: Oklch): Srgb
hslToHsv() from @llui/components/utils/color

Convert HSL (h 0-360, s/l 0-100) to HSV (h 0-360, s/v 0-100), float.

function hslToHsv(hsl: Hsl): Hsv
hslToRgb255() from @llui/components/utils/color

Convert HSL (h 0-360, s/l 0-100) to RGB (0-255 each, rounded).

function hslToRgb255(hsl: Hsl): Rgb255
hslToSrgb() from @llui/components/utils/color

HSL -> sRGB, float, lossless.

function hslToSrgb(hsl: Hsl): Srgb
hsvToHsl() from @llui/components/utils/color

Convert HSV (h 0-360, s/v 0-100) to HSL (h 0-360, s/l 0-100), float.

function hsvToHsl(hsv: Hsv): Hsl
hsvToOklch() from @llui/components/utils/color
function hsvToOklch(hsv: Hsv): Oklch
hsvToRgb255() from @llui/components/utils/color

HSV -> RGB255, via the DIRECT (not HSL-routed) float conversion.

function hsvToRgb255(hsv: Hsv): Rgb255
hsvToSrgb() from @llui/components/utils/color

HSV -> sRGB, DIRECT (not routed through HSL), float, lossless.

function hsvToSrgb(hsv: Hsv): Srgb
hwbToSrgb() from @llui/components/utils/color

HWB -> sRGB (CSS Color 4 §8.4).

function hwbToSrgb(hwb: Hwb): Srgb
inSrgbGamut() from @llui/components/utils/color

Whether ok, converted to sRGB, falls inside [0,1] on every channel (within epsilon — floating-point round-trip noise).

function inSrgbGamut(ok: Oklch, epsilon = 1e-4): boolean
interpolateColor() from @llui/components/utils/color

Interpolate between two colors per CSS Color 4 §12: convert both into space, resolve missing (none, or a cross-space-converted achromatic color's undefined) components — including alpha itself, which carries the OTHER endpoint's alpha when missing rather than defaulting to 1 (CSS Color 4 §12.2's analogous-component rule applies to alpha too — rgb(255 0 0 / none) mixed with rgb(0 0 255 / 0.2) has alpha exactly 0.2 throughout, not a lerp toward 1; pinned in test/utils/color.test.ts) — premultiply non-hue channels by alpha, lerp, then un-premultiply. hueMethod controls which way a hue-bearing space (hsl, oklch) rotates, normalizing both hues to [0,360) FIRST (see {@link adjustHue}); ignored for spaces with no hue.

function interpolateColor(a: CssColor, b: CssColor, t: number, space: InterpolationSpace, hueMethod: HueInterpolationMethod = 'shorter'): CssColor
labToLch() from @llui/components/utils/color
function labToLch(lab: Lab): Lch
labToXyzD50() from @llui/components/utils/color

Lab (D50) -> XYZ (D50).

function labToXyzD50(lab: Lab): Xyz
lchToLab() from @llui/components/utils/color
function lchToLab(lch: Lch): Lab
linearSrgbToOklab() from @llui/components/utils/color
function linearSrgbToOklab(s: Srgb): Oklab
linearToSrgb() from @llui/components/utils/color
function linearToSrgb(s: Srgb): Srgb
oklabToLinearSrgb() from @llui/components/utils/color
function oklabToLinearSrgb(lab: Oklab): Srgb
oklabToOklch() from @llui/components/utils/color
function oklabToOklch(lab: Oklab): Oklch
oklabToSrgb() from @llui/components/utils/color

NOT gamut-mapped — may return channels outside [0,1] for an OKLab/OKLCH color outside sRGB. Use {@link gamutMapOklchToSrgb} for a displayable result.

function oklabToSrgb(lab: Oklab): Srgb
oklchToHsv() from @llui/components/utils/color

Gamut-maps into sRGB first — HSV cannot represent an out-of-gamut color.

function oklchToHsv(ok: Oklch): Hsv
oklchToOklab() from @llui/components/utils/color
function oklchToOklab(ok: Oklch): Oklab
oklchToSrgb() from @llui/components/utils/color

NOT gamut-mapped. See {@link oklabToSrgb}.

function oklchToSrgb(ok: Oklch): Srgb
parseCssColor() from @llui/components/utils/color

Parse a CSS color string into its typed model. Supports #rgb/#rgba/ #rrggbb/#rrggbbaa, rgb()/rgba(), hsl()/hsla() (both legacy comma and modern space/slash syntax, with the legacy grammar's none-forbidden and uniform-percentage-or-number rules enforced), hwb(), oklch(), oklab(), lab(), lch(), color() (predefined spaces srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020, xyz, xyz-d50, xyz-d65), none components, transparent, and the 148 CSS Color 4 named colors. Returns null on anything else — never throws.

function parseCssColor(input: string): CssColor | null
parseHexColor() from @llui/components/utils/color

Parse #rgb, #rgba, #rrggbb, #rrggbbaa. Returns null on anything else, never throws.

function parseHexColor(hex: string): { rgb: Rgb255; alpha: number } | null
resolveNone() from @llui/components/utils/color

A null (CSS none) component behaves as 0 outside interpolation.

function resolveNone(n: number | null): number
rgb255ToHsl() from @llui/components/utils/color

Convert RGB (0-255 each) to HSL (h 0-360, s/l 0-100), float.

function rgb255ToHsl(rgb: Rgb255): Hsl
rgb255ToHsv() from @llui/components/utils/color

RGB255 -> HSV, via the DIRECT (not HSL-routed) float conversion.

function rgb255ToHsv(rgb: Rgb255): Hsv
srgb255ToSrgb() from @llui/components/utils/color
function srgb255ToSrgb(rgb: Rgb255): Srgb
srgbToHsl() from @llui/components/utils/color

sRGB -> HSL, float, lossless (no byte quantization).

function srgbToHsl(s: Srgb): Hsl
srgbToHsv() from @llui/components/utils/color

sRGB -> HSV, DIRECT (not routed through HSL), float, lossless.

function srgbToHsv(s: Srgb): Hsv
srgbToHwb() from @llui/components/utils/color

sRGB -> HWB.

function srgbToHwb(s: Srgb): Hwb
srgbToLinear() from @llui/components/utils/color
function srgbToLinear(s: Srgb): Srgb
srgbToOklab() from @llui/components/utils/color
function srgbToOklab(s: Srgb): Oklab
srgbToOklch() from @llui/components/utils/color
function srgbToOklch(s: Srgb): Oklch
srgbToRgb255() from @llui/components/utils/color

Rounds AND clamps into 0-255 — the boundary where an out-of-gamut float (an un-mapped OKLCH conversion) must finally become a displayable byte.

function srgbToRgb255(s: Srgb): Rgb255
xyzD50ToLab() from @llui/components/utils/color

XYZ (D50) -> Lab (D50).

function xyzD50ToLab(xyz: Xyz): Lab
xyzD65ToD50() from @llui/components/utils/color

XYZ (D65) -> XYZ (D50), Bradford chromatic adaptation. The inverse of {@link xyzD50ToD65} — not on this module's own parse/serialize path (every D50-native input, lab()/lch()/color(xyz-d50 ...), converts TO D65 to reach OKLab), but exported as the natural symmetric counterpart for a caller that needs to go the other way (e.g. producing a lab()/lch() string from an OKLCH color).

function xyzD65ToD50(xyz: Xyz): Xyz

Types

CssColor from @llui/components/utils/color

A color as CSS Color 4 sees it. lab()/lch()/wide-gamut color() are NOT separate tags here — see the module doc — they resolve to 'oklch' (or 'srgb' for color(srgb ...)/color(srgb-linear ...)) at parse time.

export type CssColor =
  | { space: 'srgb'; r: number | null; g: number | null; b: number | null; alpha: number | null }
  | {
      space: 'srgb-linear'
      r: number | null
      g: number | null
      b: number | null
      alpha: number | null
    }
  | { space: 'hsl'; h: number | null; s: number | null; l: number | null; alpha: number | null }
  | { space: 'oklab'; l: number | null; a: number | null; b: number | null; alpha: number | null }
  | { space: 'oklch'; l: number | null; c: number | null; h: number | null; alpha: number | null }
HueInterpolationMethod from @llui/components/utils/color
export type HueInterpolationMethod = 'shorter' | 'longer' | 'increasing' | 'decreasing'
InterpolationSpace from @llui/components/utils/color

Interpolation / generic-conversion target spaces (CSS Color 4 §12).

export type InterpolationSpace = 'srgb' | 'srgb-linear' | 'oklab' | 'oklch' | 'hsl'
PredefinedRgbSpace from @llui/components/utils/color
export type PredefinedRgbSpace =
  | 'srgb'
  | 'srgb-linear'
  | 'display-p3'
  | 'a98-rgb'
  | 'prophoto-rgb'
  | 'rec2020'

Interfaces

Hsl from @llui/components/utils/color

HSL color. h 0–360 degrees, s/l 0–100. Float — see the module doc.

export interface Hsl {
  h: number
  s: number
  l: number
}
Hsv from @llui/components/utils/color

HSV color. h 0–360 degrees, s/v 0–100. Float — see the module doc.

export interface Hsv {
  h: number
  s: number
  v: number
}
Hwb from @llui/components/utils/color

HWB color. h 0–360 degrees, w (whiteness)/bk (blackness) 0–100.

export interface Hwb {
  h: number
  w: number
  bk: number
}
Lab from @llui/components/utils/color

CIE Lab, D50-relative (CSS Color 4 lab()). l 0–100; a/b unbounded (CSS's reference range is ±125, but the value itself is not clamped).

export interface Lab {
  l: number
  a: number
  b: number
}
Lch from @llui/components/utils/color

CIE LCh, the polar form of {@link Lab}. l 0–100, c >= 0, h degrees.

export interface Lch {
  l: number
  c: number
  h: number
}
Oklab from @llui/components/utils/color

Oklab. l is 0–1 lightness; a/b are unbounded (±0.4 roughly spans sRGB).

export interface Oklab {
  l: number
  a: number
  b: number
}
Oklch from @llui/components/utils/color

OKLCH. l 0–1, c >= 0 (unbounded — maxChroma in the color picker is a UI choice, not a property of the color space), h in degrees.

export interface Oklch {
  l: number
  c: number
  h: number
}
Rgb255 from @llui/components/utils/color

8-bit-per-channel sRGB, 0–255 (integers once produced by this module).

export interface Rgb255 {
  r: number
  g: number
  b: number
}
Srgb from @llui/components/utils/color

sRGB, gamma-encoded, 0–1 per channel. May fall outside [0,1] as an intermediate value (e.g. converted from an out-of-gamut OKLCH) — callers that need an in-gamut result go through {@link gamutMapOklchToSrgb}.

export interface Srgb {
  r: number
  g: number
  b: number
}
Xyz from @llui/components/utils/color

CIE XYZ tristimulus values, relative to whichever white point the function producing them documents (D65 unless named otherwise).

export interface Xyz {
  x: number
  y: number
  z: number
}

@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

directionSyncMount() from @llui/components/utils/direction

Observe the exact component root and its live ancestor chain after mount. Child-list observation covers relocation between differently directed ancestors; the Mountable cleanup disconnects all observation.

function directionSyncMount(rootId: string, sync: (dir: 'ltr' | 'rtl') => void): Mountable
eventDirection() from @llui/components/utils/direction

Resolve direction at event time so same-tick ancestor changes are correct. Routes through @llui/interactions' resolveDir — the package's documented single source of truth for DOM-derived direction — rather than a second, independently-maintained ancestor walk.

function eventDirection(state: DirectionState, origin: Element | null): 'ltr' | 'rtl'
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;
floatingDir() from @llui/components/utils/direction

The EXPLICIT direction to hand an overlay's floating.dir, or undefined while direction is still automatic (dirSource === 'dom'). undefined is not "unknown": the overlay engine then resolves the direction from the overlay's placement ANCHOR — the trigger, in the app's own container — and writes it on the portaled floating element, so geometry, CSS and key handlers inside the portal all agree with the trigger (#265 finding 6). Passing the state's dir unconditionally (#265 A1) mirrored every floating surface on an RTL page as LTR: the untouched state default said 'ltr' while the DOM said 'rtl'. Once a consumer calls setDir/passes dir, dirSource is 'explicit' and that value wins over the DOM.

function floatingDir(state: DirectionState): 'ltr' | 'rtl' | undefined
initDirection() from @llui/components/utils/direction

Deterministic on the server; omitted direction is resolved only after mount.

function initDirection(dir: 'ltr' | 'rtl' | undefined): DirectionState
resolveDir() from @llui/components/utils/direction

Resolve the text direction for an element by walking up the DOM tree. Returns 'rtl' or 'ltr' (default).

The walk crosses SHADOW boundaries: Element.closest() stops at the nearest shadow root and cannot see a dir set on an ancestor of the host, so an element inside a shadow tree whose host (or the host's own ancestors) declares dir would otherwise silently read as ltr. Continuing from root.host after getRootNode() returns a ShadowRoot walks out to the light-DOM ancestor and keeps going, arbitrarily many shadow levels deep. The final fallback reads dir off the element's OWN document (ownerDocument), never the global document — the global binding names a DIFFERENT document inside an iframe or any other multi-document context.

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;
setDirection() from @llui/components/utils/direction

Public configuration becomes authoritative over future ancestor observation.

function setDirection<T extends DirectionState>(state: T, dir: 'ltr' | 'rtl'): T
syncDomDirection() from @llui/components/utils/direction

Apply an internal DOM observation only while direction remains automatic.

function syncDomDirection<T extends DirectionState>(state: T, dir: 'ltr' | 'rtl'): T
watchDirection() from @llui/components/utils/direction

Watch every dir that can decide target()'s resolved direction: dir attributes on the element and its live ancestor chain (across shadow roots), and child-list changes that relocate it between differently directed ancestors. Calls onChange after any such mutation — the caller re-resolves — then re-observes the chain target() names NOW, so a relocated or replaced element keeps being watched. Returns a disconnect. A no-op where MutationObserver does not exist (SSR).

function watchDirection(target: () => Element | null, onChange: () => void): () => void

Types

DirectionSource from @llui/components/utils/direction
export type DirectionSource = 'dom' | 'explicit'
TextDirection from @llui/components/utils/direction

Text reading direction. The single shared RTL vocabulary for the package.

export type TextDirection = 'ltr' | 'rtl';

Interfaces

DirectionState from @llui/components/utils/direction
export interface DirectionState {
  readonly dir: 'ltr' | 'rtl'
  readonly dirSource: DirectionSource
}

@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. Owns position, top, left, transform and the two available-size custom properties ({@link FLOATING_AVAILABLE_HEIGHT}) on floating; position and all four physical inset properties on an optional arrow; and placement attributes on stateTarget. The arrow's static-side inset is half its untransformed layout size, so a square arrow straddles the resolved edge. Cleanup is idempotent, suppresses pending writes/callbacks, and restores the exact prior values (including priority) or absence of those properties.

export declare function attachFloating(opts: FloatingOptions): () => void;
restoreInlineStyles() from @llui/components/utils/floating
export declare function restoreInlineStyles(element: HTMLElement, snapshots: readonly InlineStyleSnapshot[], hadStyleAttribute: boolean): void;
snapshotInlineStyle() from @llui/components/utils/floating
export declare function snapshotInlineStyle(element: HTMLElement, property: string): InlineStyleSnapshot;

Types

InlineStyleSnapshot from @llui/components/utils/floating

One inline style property's exact prior state (present or absent, value and priority), so restoreInlineStyles can put it back byte-for-byte rather than merely clearing whatever this attachment wrote. Exported so a caller that imperatively sets a SINGLE inline style outside attachFloating itself (overlay-engine.ts's sameWidth handling) shares this snapshot discipline instead of re-implementing it (#265 LOW).

export type InlineStyleSnapshot = {
    property: string;
    present: boolean;
    value: string;
    priority: string;
};
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;
    /**
     * Element that receives the resolved full `data-placement` and physical
     * `data-side`. Defaults to `floating`. Use a separate content element when
     * `floating` is a geometry-only positioner wrapper.
     */
    stateTarget?: 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 the first
     * tab-reachable descendant, else the container itself (given a temporary
     * `tabindex="-1"` when it has none). */
    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/follow-active-descendant

Functions

followActiveDescendant() from @llui/components/utils/follow-active-descendant

Keep the item an aria-activedescendant names scrolled into view (#268).

Every LLui list-like overlay — menu, select, combobox, searchable-select, context menu, menubar — keeps DOM focus on its content (or input) and moves a HIGHLIGHT through aria-activedescendant. Nothing scrolled the highlighted item into view, so in a list taller than its popup the keyboard highlight walked off the visible area: a keyboard user pressing End saw nothing happen, and with a pointer out of the way there was no other cue. (Native focus scrolls itself; an active descendant is not focus.)

One observer per open overlay, filtered to the one attribute and disconnected on teardown. It watches the whole document because the owner of the attribute is not always inside the popup — a combobox's lives on its INPUT — and acts only when the named item IS inside container, so a sibling widget's highlight never scrolls this one.

function followActiveDescendant(container: HTMLElement): () => void

@llui/components/utils/index

Functions

addUnits() from @llui/components/utils/index

Advance an instant by n whole units, in the offset's calendar.

function addUnits(at: number, unit: CalendarUnit, n: number, opts: CalendarOptions = {}): number
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>
annularSectorPath() from @llui/components/utils/index

An annular sector — the polar answer to a bar. r0 is the inner radius (0 gives a pie wedge, > 0 a donut segment); angles follow {@link polarPoint}.

A sweep of a full turn or more is drawn as a complete ring, because the arc endpoints would otherwise coincide and SVG would draw nothing at all.

function annularSectorPath(cx: number, cy: number, r0: number, r1: number, a0: number, a1: number): string
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[]
areaPath() from @llui/components/utils/index

A filled region between an upper run and a lower run. The lower run is walked BACKWARDS and appended, then the path is closed — one subpath, so a fill-rule cannot punch a hole in it the way two separate subpaths can.

function areaPath(upper: readonly Point[], lower: readonly Point[], curve: Curve, axis: CurveAxis = 'x'): string
attachFloating() from @llui/components/utils/index

Position floating relative to anchor with live updates on scroll/resize. Owns position, top, left, transform and the two available-size custom properties ({@link FLOATING_AVAILABLE_HEIGHT}) on floating; position and all four physical inset properties on an optional arrow; and placement attributes on stateTarget. The arrow's static-side inset is half its untransformed layout size, so a square arrow straddles the resolved edge. Cleanup is idempotent, suppresses pending writes/callbacks, and restores the exact prior values (including priority) or absence of those properties.

export declare function attachFloating(opts: FloatingOptions): () => void;
bandCenter() from @llui/components/utils/index

The centre of band i in normalized u.

function bandCenter(i: number, band: Band): number
bandExtent() from @llui/components/utils/index

The [start, end] extent of band i in normalized u.

function bandExtent(i: number, band: Band): [number, number]
calendarTicks() from @llui/components/utils/index

Calendar boundaries inside [start, end], ascending.

An EMPTY array for a non-finite or reversed range, and for a request that would exceed {@link MAX_CALENDAR_TICKS} — a chart with no data is a normal state, and a truncated axis is a lie.

function calendarTicks(start: number, end: number, opts: CalendarStepOptions = {}): CalendarTick[]
cartesianProjection() from @llui/components/utils/index
function cartesianProjection(frame: Frame, opts: CartesianOptions = {}): Projection
chooseCalendarStep() from @llui/components/utils/index

The finest rung whose boundaries fit inside maxTicks.

A FORCED unit is honoured as given (stride 1), except on year, where the stride still grows to fit — "yearly" over four centuries has to mean something, and 400 gridlines is not it.

function chooseCalendarStep(start: number, end: number, opts: CalendarStepOptions = {}): CalendarStep
circlePath() from @llui/components/utils/index

A full circle, as two arcs — one arc of 360° is degenerate and draws nothing, which is the classic way a polar gridline disappears.

function circlePath(cx: number, cy: number, r: number): string
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
countCalendarTicks() from @llui/components/utils/index

How many boundaries of step fall inside [start, end] — WITHOUT building them. Constant time at every rung, which is what lets the ladder reject a finer unit for a century-long span for free.

One informational edge, in the safe direction: within a month of the extremes of the representable range (year ±271821), flooring to a period start falls outside what a Date can hold and {@link utcAt} answers NaN, so the count is 0 and {@link calendarTicks} draws no gridlines. It neither hangs nor throws; the axis is simply unlabelled at the end of time.

function countCalendarTicks(start: number, end: number, step: CalendarStep, opts: CalendarOptions = {}): number
cssColorAlpha() from @llui/components/utils/index

Resolved alpha (none -> 1, CSS Color 4's used value for a missing alpha OUTSIDE interpolation). Inside {@link interpolateColor}, a missing alpha instead carries the OTHER endpoint's alpha — see its doc comment.

function cssColorAlpha(c: CssColor): number
cssColorToHex() from @llui/components/utils/index

Gamut-map (if needed) and convert to a displayable hex string. #rrggbb when alpha is (resolved to) 1, #rrggbbaa otherwise.

function cssColorToHex(c: CssColor): string
cssColorToSrgb() from @llui/components/utils/index

Convert any {@link CssColor} into fully-resolved (non-null) Srgb, treating none as 0 and NOT gamut-mapping (an oklch/oklab input outside sRGB stays outside [0,1]).

function cssColorToSrgb(c: CssColor): Srgb
curvePath() from @llui/components/utils/index

Join a run of points with the named curve. closed applies to linear only — the other two are defined on an open, x-increasing run.

function curvePath(points: readonly Point[], curve: Curve, closed = false, axis: CurveAxis = 'x'): string
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
denormalize() from @llui/components/utils/index

Inverse of {@link normalize}.

function denormalize(u: number, domain: Domain): 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;
floorToUnit() from @llui/components/utils/index

Floor an instant to the start of its unit, in the offset's calendar.

function floorToUnit(at: number, unit: CalendarUnit, opts: CalendarOptions = {}): number
fmt() from @llui/components/utils/index

Format a coordinate. A non-finite input becomes 0 — a NaN in a path silently voids the WHOLE path element, taking every later command with it.

function fmt(n: number): 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;
formatHex() from @llui/components/utils/index
function formatHex(rgb: Rgb255): string
formatHex8() from @llui/components/utils/index
function formatHex8(rgb: Rgb255, alpha: number): string
formatOklch() from @llui/components/utils/index

oklch(L C H) / oklch(L C H / A). Fixed precision: L as a 0–1 fraction at 4 dp, C at 4 dp, H at 2 dp — chosen so the string is stable and round-trips through {@link parseCssColor} without drifting.

function formatOklch(ok: Oklch, alpha = 1): string
formatRgb() from @llui/components/utils/index
function formatRgb(rgb: Rgb255, alpha = 1): string
gamutMapOklchToSrgb() from @llui/components/utils/index

CSS Color 4 §13.2 gamut mapping: reduce OKLCH chroma by binary search until simple-clipping the result into sRGB is perceptually indistinguishable (deltaEOK < the 0.02 JND) from the reduced color, then clip. Given an already-in-gamut color this returns it converted (and defensively clipped for float noise) rather than a no-op, so the result is always safe to hand straight to {@link srgbToRgb255}. test/utils/color.test.ts pins the converged chroma for a known out-of-gamut color as a regression value (this file's own binary search, not an independent oracle — see that test's comment for what it does and does not prove).

function gamutMapOklchToSrgb(ok: Oklch): Srgb
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[];
hslToHsv() from @llui/components/utils/index

Convert HSL (h 0-360, s/l 0-100) to HSV (h 0-360, s/v 0-100), float.

function hslToHsv(hsl: Hsl): Hsv
hslToRgb255() from @llui/components/utils/index

Convert HSL (h 0-360, s/l 0-100) to RGB (0-255 each, rounded).

function hslToRgb255(hsl: Hsl): Rgb255
hsvToHsl() from @llui/components/utils/index

Convert HSV (h 0-360, s/v 0-100) to HSL (h 0-360, s/l 0-100), float.

function hsvToHsl(hsv: Hsv): Hsl
hsvToOklch() from @llui/components/utils/index
function hsvToOklch(hsv: Hsv): Oklch
hsvToRgb255() from @llui/components/utils/index

HSV -> RGB255, via the DIRECT (not HSL-routed) float conversion.

function hsvToRgb255(hsv: Hsv): Rgb255
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>
inSrgbGamut() from @llui/components/utils/index

Whether ok, converted to sRGB, falls inside [0,1] on every channel (within epsilon — floating-point round-trip noise).

function inSrgbGamut(ok: Oklch, epsilon = 1e-4): boolean
interpolateColor() from @llui/components/utils/index

Interpolate between two colors per CSS Color 4 §12: convert both into space, resolve missing (none, or a cross-space-converted achromatic color's undefined) components — including alpha itself, which carries the OTHER endpoint's alpha when missing rather than defaulting to 1 (CSS Color 4 §12.2's analogous-component rule applies to alpha too — rgb(255 0 0 / none) mixed with rgb(0 0 255 / 0.2) has alpha exactly 0.2 throughout, not a lerp toward 1; pinned in test/utils/color.test.ts) — premultiply non-hue channels by alpha, lerp, then un-premultiply. hueMethod controls which way a hue-bearing space (hsl, oklch) rotates, normalizing both hues to [0,360) FIRST (see {@link adjustHue}); ignored for spaces with no hue.

function interpolateColor(a: CssColor, b: CssColor, t: number, space: InterpolationSpace, hueMethod: HueInterpolationMethod = 'shorter'): CssColor
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
linearPath() from @llui/components/utils/index

Straight segments through every point.

function linearPath(points: readonly Point[], closed = false): string
linearSrgbToOklab() from @llui/components/utils/index
function linearSrgbToOklab(s: Srgb): Oklab
linearToSrgb() from @llui/components/utils/index
function linearToSrgb(s: Srgb): Srgb
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>
monotonePath() from @llui/components/utils/index

Monotone cubic interpolation (Fritsch–Carlson). Its defining property is that the curve NEVER OVERSHOOTS the data: between two samples it stays within their values, so a series that only rises cannot dip below a point on the way. A plain Catmull-Rom or cardinal spline does overshoot, and on a chart that means drawing a number nobody measured.

Defined on a function y = f(x) with strictly increasing x. It is therefore meaningless on a closed angular loop, which is why polarProjection declines it — see projection.ts.

function monotonePath(points: readonly Point[], axis: CurveAxis = 'x'): string
nearestBand() from @llui/components/utils/index

The band index nearest a normalized position — the hit test behind hover and pointer tracking. Returns null for an empty axis.

Nearest-CENTRE, not containment: the gaps between bands belong to whichever band is closer, so a pointer never falls into a dead zone between two bars.

function nearestBand(u: number, band: Band): number | null
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
niceDomain() from @llui/components/utils/index

Extend [min, max] outward to the nearest round tick boundaries, so the axis ends on a labelled value instead of mid-air. Iterates because widening the domain can change the chosen step, which can change the boundaries again; d3's nice does the same, and it converges in at most a few rounds.

function niceDomain(min: number, max: number, count: number): Domain
normalize() from @llui/components/utils/index

Map a value into [0, 1] across domain. A DEGENERATE domain (min === max) maps everything to 0.5 rather than dividing by zero — a single-valued series draws as a flat line through the middle, which is the only reading that is not a crash or a NaN in a path string.

function normalize(value: number, domain: Domain): number
oklabToLinearSrgb() from @llui/components/utils/index
function oklabToLinearSrgb(lab: Oklab): Srgb
oklabToOklch() from @llui/components/utils/index
function oklabToOklch(lab: Oklab): Oklch
oklabToSrgb() from @llui/components/utils/index

NOT gamut-mapped — may return channels outside [0,1] for an OKLab/OKLCH color outside sRGB. Use {@link gamutMapOklchToSrgb} for a displayable result.

function oklabToSrgb(lab: Oklab): Srgb
oklchToHsv() from @llui/components/utils/index

Gamut-maps into sRGB first — HSV cannot represent an out-of-gamut color.

function oklchToHsv(ok: Oklch): Hsv
oklchToOklab() from @llui/components/utils/index
function oklchToOklab(ok: Oklch): Oklab
oklchToSrgb() from @llui/components/utils/index

NOT gamut-mapped. See {@link oklabToSrgb}.

function oklchToSrgb(ok: Oklch): Srgb
parseCssColor() from @llui/components/utils/index

Parse a CSS color string into its typed model. Supports #rgb/#rgba/ #rrggbb/#rrggbbaa, rgb()/rgba(), hsl()/hsla() (both legacy comma and modern space/slash syntax, with the legacy grammar's none-forbidden and uniform-percentage-or-number rules enforced), hwb(), oklch(), oklab(), lab(), lch(), color() (predefined spaces srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020, xyz, xyz-d50, xyz-d65), none components, transparent, and the 148 CSS Color 4 named colors. Returns null on anything else — never throws.

function parseCssColor(input: string): CssColor | null
parseDateValue() from @llui/components/utils/index
function parseDateValue(value: DateValue): ParsedDateValue
parseHexColor() from @llui/components/utils/index

Parse #rgb, #rgba, #rrggbb, #rrggbbaa. Returns null on anything else, never throws.

function parseHexColor(hex: string): { rgb: Rgb255; alpha: number } | null
pointerDragHandlers() from @llui/components/utils/index

Build the five pointer handlers for one drag track. One call per live connect() instance — dragging/activePointerId are closure-scoped per instance, the same shape as this package's other per-instance mutable state (e.g. color-picker's eyedropper pendingEyeDropper AbortController).

  • Primary button only on pointerdown (e.button !== 0 is ignored), AND only when no drag is already live — a second finger/pointer touching down mid-drag is ignored rather than hijacking activePointerId.
  • Every subsequent event (pointermove/pointerup/pointercancel/ onLostPointerCapture) is checked against activePointerId: a second pointer's events never affect a drag it didn't start.
  • setPointerCapture on pointerdown's currentTarget, so pointermove keeps firing even once the pointer leaves the element's bounds — a plain drag with no capture stalls out at the track's edge instead of saturating like a native <input type="range">.
  • Released on pointerup/pointercancel, guarded by hasPointerCapture so releasing twice (or without ever capturing) is a harmless no-op.
  • dragging is unconditionally reset on onLostPointerCapture for the active pointer, even though that handler never calls release() itself (capture is already gone by definition when this fires).
function pointerDragHandlers(callbacks: PointerDragCallbacks): PointerDragHandlers
polarPoint() from @llui/components/utils/index

A point on a circle. Angles are RADIANS CLOCKWISE FROM 12 O'CLOCK, which is where every reader expects a chart to start; SVG's own 0 is 3 o'clock.

function polarPoint(cx: number, cy: number, r: number, angle: number): Point
polarProjection() from @llui/components/utils/index
function polarProjection(frame: Frame, opts: PolarOptions = {}): Projection
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
projectionFor() from @llui/components/utils/index

Build the projection a chart's coordinate setting names.

function projectionFor(coord: ChartCoord, frame: Frame, opts: CartesianOptions & PolarOptions = {}): Projection
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;
rectPath() from @llui/components/utils/index

An axis-aligned rectangle, corners in any order.

function rectPath(x0: number, y0: number, x1: number, y1: number): string
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).

The walk crosses SHADOW boundaries: Element.closest() stops at the nearest shadow root and cannot see a dir set on an ancestor of the host, so an element inside a shadow tree whose host (or the host's own ancestors) declares dir would otherwise silently read as ltr. Continuing from root.host after getRootNode() returns a ShadowRoot walks out to the light-DOM ancestor and keeps going, arbitrarily many shadow levels deep. The final fallback reads dir off the element's OWN document (ownerDocument), never the global document — the global binding names a DIFFERENT document inside an iframe or any other multi-document context.

export declare function resolveDir(el: Element): TextDirection;
resolveNone() from @llui/components/utils/index

A null (CSS none) component behaves as 0 outside interpolation.

function resolveNone(n: number | null): number
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;
rgb255ToHsl() from @llui/components/utils/index

Convert RGB (0-255 each) to HSL (h 0-360, s/l 0-100), float.

function rgb255ToHsl(rgb: Rgb255): Hsl
rgb255ToHsv() from @llui/components/utils/index

RGB255 -> HSV, via the DIRECT (not HSL-routed) float conversion.

function rgb255ToHsv(rgb: Rgb255): Hsv
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
srgb255ToSrgb() from @llui/components/utils/index
function srgb255ToSrgb(rgb: Rgb255): Srgb
srgbToLinear() from @llui/components/utils/index
function srgbToLinear(s: Srgb): Srgb
srgbToOklab() from @llui/components/utils/index
function srgbToOklab(s: Srgb): Oklab
srgbToOklch() from @llui/components/utils/index
function srgbToOklch(s: Srgb): Oklch
srgbToRgb255() from @llui/components/utils/index

Rounds AND clamps into 0-255 — the boundary where an out-of-gamut float (an un-mapped OKLCH conversion) must finally become a displayable byte.

function srgbToRgb255(s: Srgb): Rgb255
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
stepPath() from @llui/components/utils/index

Axis-aligned staircase: hold each value until halfway along the independent axis, then step.

function stepPath(points: readonly Point[], axis: CurveAxis = 'x'): string
tickIncrement() from @llui/components/utils/index

The step size a nice axis would use to fit about count ticks into [start, stop]. A negative return means "one tick every 1/-step units" — d3's encoding, kept because the reciprocal form avoids the float drift that 10 ** -power introduces for small steps.

function tickIncrement(start: number, stop: number, count: number): number
ticks() from @llui/components/utils/index

Round, human-readable tick values covering [start, stop]. Returns an EMPTY array for a non-finite or degenerate range rather than looping forever — a chart with no data is a normal state, not an error.

function ticks(start: number, stop: number, count: number): 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
valueDomain() from @llui/components/utils/index

The value-axis domain for a set of series.

min / max are UNBOUNDED-CAPABLE bounds in the sense of finiteBound (#177): an absent one means "derive it", never ±Infinity. A derived domain always INCLUDES ZERO, because a bar whose baseline is not zero misrepresents its own length — the one axis default worth being opinionated about. A caller who wants otherwise passes both bounds.

function valueDomain(values: readonly number[], opts: { min?: number; max?: number; tickCount: number }): Domain
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

CalendarUnit from @llui/components/utils/index

Rungs of the ladder, finest first.

export type CalendarUnit = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'
CssColor from @llui/components/utils/index

A color as CSS Color 4 sees it. lab()/lch()/wide-gamut color() are NOT separate tags here — see the module doc — they resolve to 'oklch' (or 'srgb' for color(srgb ...)/color(srgb-linear ...)) at parse time.

export type CssColor =
  | { space: 'srgb'; r: number | null; g: number | null; b: number | null; alpha: number | null }
  | {
      space: 'srgb-linear'
      r: number | null
      g: number | null
      b: number | null
      alpha: number | null
    }
  | { space: 'hsl'; h: number | null; s: number | null; l: number | null; alpha: number | null }
  | { space: 'oklab'; l: number | null; a: number | null; b: number | null; alpha: number | null }
  | { space: 'oklch'; l: number | null; c: number | null; h: number | null; alpha: number | null }
Curve from @llui/components/utils/index

How a run of points is joined.

export type Curve = 'linear' | 'monotone' | 'step'
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)
HueInterpolationMethod from @llui/components/utils/index
export type HueInterpolationMethod = 'shorter' | 'longer' | 'increasing' | 'decreasing'
InterpolationSpace from @llui/components/utils/index

Interpolation / generic-conversion target spaces (CSS Color 4 §12).

export type InterpolationSpace = 'srgb' | 'srgb-linear' | 'oklab' | 'oklch' | 'hsl'
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 }
}
Band from @llui/components/utils/index

A discrete axis of count slots laid out across [0, 1].

export interface Band {
  count: number
  /** Gap between adjacent bands, as a fraction of a slot. */
  paddingInner: number
  /** Gap before the first and after the last band, as a fraction of a slot. */
  paddingOuter: number
}
CalendarOptions from @llui/components/utils/index
export interface CalendarOptions {
  /**
   * Minutes east of UTC that boundaries are measured in. Default 0 (UTC). A
   * FIXED offset — see the module note; `-300` is US Eastern standard time and
   * stays at −5 across a DST transition rather than following it.
   */
  offsetMinutes?: number
  /** First day of the week for the `week` rung: 0 = Sunday … 6 = Saturday.
   *  Default 1 (Monday), matching ISO 8601. */
  weekStartsOn?: number
}
CalendarStep from @llui/components/utils/index

A chosen rung, plus how many of it each boundary advances. stride is always 1 except on year.

export interface CalendarStep {
  unit: CalendarUnit
  stride: number
}
CalendarStepOptions from @llui/components/utils/index
export interface CalendarStepOptions extends CalendarOptions {
  /** Upper bound on the number of boundaries. Default 6. */
  maxTicks?: number
  /** Force a rung instead of choosing one from the span. */
  unit?: CalendarUnit
}
CalendarTick from @llui/components/utils/index
export interface CalendarTick {
  /** The boundary instant, in epoch milliseconds. */
  at: number
  unit: CalendarUnit
}
CartesianOptions from @llui/components/utils/index
export interface CartesianOptions {
  /** Swap the axes: `u` runs top-to-bottom and `v` left-to-right. This is what
   *  turns a column chart into a horizontal bar chart, and it is a PROJECTION
   *  concern — no mark, scale or hit test changes. */
  horizontal?: boolean
}
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;
}
Domain from @llui/components/utils/index

A closed numeric interval. Both ends are finite by construction.

export interface Domain {
  min: number
  max: number
}
FloatingOptions from @llui/components/utils/index
export interface FloatingOptions {
    /** The reference element (trigger/anchor). */
    anchor: Element;
    /** The floating element (content). */
    floating: HTMLElement;
    /**
     * Element that receives the resolved full `data-placement` and physical
     * `data-side`. Defaults to `floating`. Use a separate content element when
     * `floating` is a geometry-only positioner wrapper.
     */
    stateTarget?: 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 the first
     * tab-reachable descendant, else the container itself (given a temporary
     * `tabindex="-1"` when it has none). */
    initialFocus?: Element | (() => Element | null);
    /** Restore focus to the previously active element on release (default: true). */
    restoreFocus?: boolean;
}
Frame from @llui/components/utils/index

The plot area in user units — the box marks are drawn inside, already inset by whatever padding the axes need.

export interface Frame {
  x: number
  y: number
  width: number
  height: number
}
Hsl from @llui/components/utils/index

HSL color. h 0–360 degrees, s/l 0–100. Float — see the module doc.

export interface Hsl {
  h: number
  s: number
  l: number
}
Hsv from @llui/components/utils/index

HSV color. h 0–360 degrees, s/v 0–100. Float — see the module doc.

export interface Hsv {
  h: number
  s: number
  v: number
}
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
}
Oklab from @llui/components/utils/index

Oklab. l is 0–1 lightness; a/b are unbounded (±0.4 roughly spans sRGB).

export interface Oklab {
  l: number
  a: number
  b: number
}
Oklch from @llui/components/utils/index

OKLCH. l 0–1, c >= 0 (unbounded — maxChroma in the color picker is a UI choice, not a property of the color space), h in degrees.

export interface Oklch {
  l: number
  c: number
  h: 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
}
Point from @llui/components/utils/index
export interface Point {
  x: number
  y: number
}
PointerDragCallbacks from @llui/components/utils/index

One call site's drag callbacks.

export interface PointerDragCallbacks {
  /** Checked on `pointerdown` only — a drag that is already underway keeps
   * running even if this flips true mid-drag (the reducer's own `disabled`
   * guard is the actual safety net for a message dispatched from `onDrag`
   * after that point; this only decides whether a NEW drag may start). */
  isDisabled: () => boolean
  /** Fires once, on `pointerdown`, after capture is acquired and BEFORE the
   * first `onDrag` call — the hook for a one-time side effect like moving
   * focus onto a thumb element. */
  onDragStart?: (e: PointerEvent) => void
  /** Fires on `pointerdown` (immediately after `onDragStart`) and again on
   * every `pointermove` while the drag is live. */
  onDrag: (e: PointerEvent) => void
  /** Fires once on `pointerup`/`pointercancel`, but ONLY when a drag was
   * actually live (never on a stray up/cancel with no matching down) — the
   * counterpart to `onDragStart`, for a caller that tracks per-drag identity
   * across the lifecycle (`gradient-picker`'s track drag creates a new stop
   * on `pointerdown` and must stop targeting it once the drag ends). Runs
   * AFTER pointer capture is released. */
  onDragEnd?: (e: PointerEvent) => void
}
PointerDragHandlers from @llui/components/utils/index
export interface PointerDragHandlers {
  onPointerDown: (e: PointerEvent) => void
  onPointerMove: (e: PointerEvent) => void
  onPointerUp: (e: PointerEvent) => void
  onPointerCancel: (e: PointerEvent) => void
  /** The browser can revoke pointer capture without ever firing `pointerup`/
   * `pointercancel` (an OS-level interruption, a nested capture request, …).
   * Left unwired, `dragging` would stay `true` forever and a later, UNRELATED
   * pointer's `pointermove` would be misread as a continuation of this drag.
   * Spread onto the same element as the other four. */
  onLostPointerCapture: (e: PointerEvent) => void
}
PolarOptions from @llui/components/utils/index
export interface PolarOptions {
  /** Inner radius as a fraction of the outer. `0` is a pie, `0.5` a donut, and
   *  a radar usually wants `0`. */
  innerRadius?: number
  /** Where `u = 0` sits, in radians clockwise from 12 o'clock. */
  startAngle?: number
  /** Total sweep in radians. Defaults to a full turn, which is what makes the
   *  projection `closed`. */
  sweep?: number
  /**
   * Gridline shape. `ring` is a circle at the given magnitude — right for a
   * pie or a rose. `web` joins the `spokes` sample positions with straight
   * segments — right for a radar, where a circular grid reads as a different
   * chart from the polygonal data on top of it.
   */
  grid?: 'ring' | 'web'
  /** How many spokes a `web` gridline has. Ignored for `ring`. */
  spokes?: number
  /**
   * Swap the axes, exactly as {@link CartesianOptions.horizontal} does: the
   * INDEPENDENT axis moves off the angle and onto the RADIUS, so each category
   * becomes its own concentric ring and magnitude is read as arc length. That
   * is a radial bar chart, and it is the polar reading of the same flag that
   * turns a column chart on its side — one field, the same meaning in both
   * coordinate systems, so a consumer flipping `coord` keeps whatever
   * orientation they asked for.
   *
   * The independent axis is then the radius, which is NOT a loop, so the
   * projection reports `closed: false` however wide the sweep is.
   */
  horizontal?: boolean
}
Projection from @llui/components/utils/index
export interface Projection {
  readonly kind: ChartCoord
  /**
   * Whether the independent axis is a CLOSED loop. `true` for a polar
   * projection covering a full turn, which is what makes a radar outline join
   * back to its first vertex instead of leaving a gap.
   */
  readonly closed: boolean
  /**
   * Curves this projection can honour. A caller asking for one outside the set
   * gets `linear` — see the polar note below for why this is the honest answer
   * rather than a silent approximation.
   */
  readonly curves: readonly Curve[]
  /** Normalized (u, v) → a point in user units. */
  point(u: number, v: number): Point
  /** A run of samples joined along increasing `u`. */
  line(samples: readonly Sample[], curve: Curve): string
  /** The region between an upper run and a lower run of the same length. */
  area(upper: readonly Sample[], lower: readonly Sample[], curve: Curve): string
  /** A band spanning `u0..u1` and `v0..v1`: a bar, or a wedge. */
  band(u0: number, u1: number, v0: number, v1: number): string
  /** The iso-magnitude line at `v` — a value gridline. */
  gridline(v: number): string
  /** Where the independent-axis tick for `u` belongs. */
  tick(u: number): TickPlacement
  /** Where the DEPENDENT-axis tick for `v` belongs. */
  valueTick(v: number): TickPlacement
  /**
   * Invert a point in user units back to `u`, for pointer hit testing. Returns
   * `null` when the point carries no meaningful `u` — the exact centre of a
   * polar chart, where every angle is equally close.
   */
  locate(x: number, y: number): number | null
}
Rgb255 from @llui/components/utils/index

8-bit-per-channel sRGB, 0–255 (integers once produced by this module).

export interface Rgb255 {
  r: number
  g: number
  b: number
}
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;
}
Sample from @llui/components/utils/index

A normalized sample: position along the independent axis, and magnitude.

export interface Sample {
  u: number
  v: number
}
Srgb from @llui/components/utils/index

sRGB, gamma-encoded, 0–1 per channel. May fall outside [0,1] as an intermediate value (e.g. converted from an out-of-gamut OKLCH) — callers that need an in-gamut result go through {@link gamutMapOklchToSrgb}.

export interface Srgb {
  r: number
  g: number
  b: number
}
TickPlacement from @llui/components/utils/index

Where an independent-axis tick label sits, and how to align it there.

export interface TickPlacement {
  x: number
  y: number
  /** SVG `text-anchor`. */
  anchor: 'start' | 'middle' | 'end'
  /** SVG `dominant-baseline`. */
  baseline: 'auto' | 'middle' | 'hanging'
}
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[]
MAX_CALENDAR_TICKS from @llui/components/utils/index

A hard ceiling on the boundaries one call will produce. Reached only by FORCING a fine unit across an enormous span; the automatic ladder can never exceed maxTicks. Over the ceiling the answer is an EMPTY array — no gridlines — rather than a truncated one, because a truncated axis silently claims the data ends where the array does.

const MAX_CALENDAR_TICKS
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: ReadSignal<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 {
  /**
   * Preferred placement. A function so it can be resolved AT ATTACH TIME
   * (#265 A4) — a per-level submenu chooses its physical side (`right-start`
   * under 'ltr', `left-start` under 'rtl') from the reading direction in
   * effect when the level opens, the same way `dir` below is already
   * resolved lazily rather than captured at declaration time.
   */
  placement: 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
  /**
   * An EXPLICIT reading direction — a function so it can be peeked at attach
   * time (menu). Return `undefined` (or omit it) while direction is automatic:
   * the engine then resolves it from the placement ANCHOR, never from where
   * the portal landed (#265 finding 6). Whichever wins is handed to
   * `attachFloating` and written as `dir` on the floating element, so the
   * whole portaled subtree — CSS logical properties, key handlers resolving
   * from `e.currentTarget`, and nested overlays anchored inside it — reads
   * the same direction as the trigger it belongs to.
   */
  dir?: TextDirection | (() => TextDirection | undefined)
  /** Attach positioning in the MOUNT phase (survives the exit animation) rather
   * than the interaction phase. Required when `visibleWhen` unwinds interactions
   * before `mountWhen` releases retained exit content; the engine rejects that
   * two-phase lifetime unless placement persists with the mounted node. */
  persistent?: boolean
  /**
   * Re-run floating attachment (detach then reattach, re-evaluating the
   * `placement`/`dir` thunks fresh) whenever this key's value CHANGES while
   * mounted. Needed because `placement`/`dir` are otherwise resolved ONCE at
   * attach and `autoUpdate` never re-polls them — a physical `placement`
   * string (`'right-start'`) encodes a reading-direction decision that a
   * later `computePosition` pass with the same closed-over string cannot
   * correct (#265 A4: a submenu whose menu tree flips direction while the
   * level stays open must re-place, not just re-run the same geometry). A DOM
   * direction change needs no key — the engine watches the anchor's
   * direction itself; this is for STATE the DOM does not show (an explicit
   * `setDir`).
   *
   * There is no public imperative signal-subscribe seam for framework-internal
   * code running inside a mount callback (`@llui/dom`'s reactivity is
   * binding-driven, not subscription-driven) — a caller declares this key by
   * binding it reactively as a `data-llui-reattach-key` attribute on the
   * content element or an ancestor it controls (`subOverlay` puts it on the
   * positioner it builds), and the engine watches that ATTRIBUTE with a
   * `MutationObserver`, the same declarative-binding-to-DOM-observation idiom
   * `direction.ts:watchDirection` uses in the other direction. The marker is
   * found with `content.closest('[data-llui-reattach-key]')`; supplying
   * `reattachKey` without one is an authoring error (`LluiFrameworkError`).
   */
  reattachKey?: () => string | number
}
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/path

Functions

annularSectorPath() from @llui/components/utils/path

An annular sector — the polar answer to a bar. r0 is the inner radius (0 gives a pie wedge, > 0 a donut segment); angles follow {@link polarPoint}.

A sweep of a full turn or more is drawn as a complete ring, because the arc endpoints would otherwise coincide and SVG would draw nothing at all.

function annularSectorPath(cx: number, cy: number, r0: number, r1: number, a0: number, a1: number): string
areaPath() from @llui/components/utils/path

A filled region between an upper run and a lower run. The lower run is walked BACKWARDS and appended, then the path is closed — one subpath, so a fill-rule cannot punch a hole in it the way two separate subpaths can.

function areaPath(upper: readonly Point[], lower: readonly Point[], curve: Curve, axis: CurveAxis = 'x'): string
circlePath() from @llui/components/utils/path

A full circle, as two arcs — one arc of 360° is degenerate and draws nothing, which is the classic way a polar gridline disappears.

function circlePath(cx: number, cy: number, r: number): string
curvePath() from @llui/components/utils/path

Join a run of points with the named curve. closed applies to linear only — the other two are defined on an open, x-increasing run.

function curvePath(points: readonly Point[], curve: Curve, closed = false, axis: CurveAxis = 'x'): string
fmt() from @llui/components/utils/path

Format a coordinate. A non-finite input becomes 0 — a NaN in a path silently voids the WHOLE path element, taking every later command with it.

function fmt(n: number): string
linearPath() from @llui/components/utils/path

Straight segments through every point.

function linearPath(points: readonly Point[], closed = false): string
monotonePath() from @llui/components/utils/path

Monotone cubic interpolation (Fritsch–Carlson). Its defining property is that the curve NEVER OVERSHOOTS the data: between two samples it stays within their values, so a series that only rises cannot dip below a point on the way. A plain Catmull-Rom or cardinal spline does overshoot, and on a chart that means drawing a number nobody measured.

Defined on a function y = f(x) with strictly increasing x. It is therefore meaningless on a closed angular loop, which is why polarProjection declines it — see projection.ts.

function monotonePath(points: readonly Point[], axis: CurveAxis = 'x'): string
polarPoint() from @llui/components/utils/path

A point on a circle. Angles are RADIANS CLOCKWISE FROM 12 O'CLOCK, which is where every reader expects a chart to start; SVG's own 0 is 3 o'clock.

function polarPoint(cx: number, cy: number, r: number, angle: number): Point
rectPath() from @llui/components/utils/path

An axis-aligned rectangle, corners in any order.

function rectPath(x0: number, y0: number, x1: number, y1: number): string
stepPath() from @llui/components/utils/path

Axis-aligned staircase: hold each value until halfway along the independent axis, then step.

function stepPath(points: readonly Point[], axis: CurveAxis = 'x'): string

Types

Curve from @llui/components/utils/path

How a run of points is joined.

export type Curve = 'linear' | 'monotone' | 'step'
CurveAxis from @llui/components/utils/path

Which coordinate a curve is a FUNCTION OF. monotone and step are both defined on w = f(t) with increasing t; this names which of x/y plays t.

It is not a formatting detail. A horizontal bar/line chart puts the independent axis on y, and running the curve along x regardless offsets every control point along the wrong axis: measured, a four-point series emitted M150,0C190,25 230,75 270,75, where the second control already sits at the segment's end instead of two thirds along it. The endpoints stay correct in both orientations, which is why it reads as a smoothing artefact rather than a bug — and why asserting the path advances monotonically catches nothing.

export type CurveAxis = 'x' | 'y'

Interfaces

Point from @llui/components/utils/path
export interface Point {
  x: number
  y: number
}

@llui/components/utils/pointer-drag

Functions

pointerDragHandlers() from @llui/components/utils/pointer-drag

Build the five pointer handlers for one drag track. One call per live connect() instance — dragging/activePointerId are closure-scoped per instance, the same shape as this package's other per-instance mutable state (e.g. color-picker's eyedropper pendingEyeDropper AbortController).

  • Primary button only on pointerdown (e.button !== 0 is ignored), AND only when no drag is already live — a second finger/pointer touching down mid-drag is ignored rather than hijacking activePointerId.
  • Every subsequent event (pointermove/pointerup/pointercancel/ onLostPointerCapture) is checked against activePointerId: a second pointer's events never affect a drag it didn't start.
  • setPointerCapture on pointerdown's currentTarget, so pointermove keeps firing even once the pointer leaves the element's bounds — a plain drag with no capture stalls out at the track's edge instead of saturating like a native <input type="range">.
  • Released on pointerup/pointercancel, guarded by hasPointerCapture so releasing twice (or without ever capturing) is a harmless no-op.
  • dragging is unconditionally reset on onLostPointerCapture for the active pointer, even though that handler never calls release() itself (capture is already gone by definition when this fires).
function pointerDragHandlers(callbacks: PointerDragCallbacks): PointerDragHandlers

Interfaces

PointerDragCallbacks from @llui/components/utils/pointer-drag

One call site's drag callbacks.

export interface PointerDragCallbacks {
  /** Checked on `pointerdown` only — a drag that is already underway keeps
   * running even if this flips true mid-drag (the reducer's own `disabled`
   * guard is the actual safety net for a message dispatched from `onDrag`
   * after that point; this only decides whether a NEW drag may start). */
  isDisabled: () => boolean
  /** Fires once, on `pointerdown`, after capture is acquired and BEFORE the
   * first `onDrag` call — the hook for a one-time side effect like moving
   * focus onto a thumb element. */
  onDragStart?: (e: PointerEvent) => void
  /** Fires on `pointerdown` (immediately after `onDragStart`) and again on
   * every `pointermove` while the drag is live. */
  onDrag: (e: PointerEvent) => void
  /** Fires once on `pointerup`/`pointercancel`, but ONLY when a drag was
   * actually live (never on a stray up/cancel with no matching down) — the
   * counterpart to `onDragStart`, for a caller that tracks per-drag identity
   * across the lifecycle (`gradient-picker`'s track drag creates a new stop
   * on `pointerdown` and must stop targeting it once the drag ends). Runs
   * AFTER pointer capture is released. */
  onDragEnd?: (e: PointerEvent) => void
}
PointerDragHandlers from @llui/components/utils/pointer-drag
export interface PointerDragHandlers {
  onPointerDown: (e: PointerEvent) => void
  onPointerMove: (e: PointerEvent) => void
  onPointerUp: (e: PointerEvent) => void
  onPointerCancel: (e: PointerEvent) => void
  /** The browser can revoke pointer capture without ever firing `pointerup`/
   * `pointercancel` (an OS-level interruption, a nested capture request, …).
   * Left unwired, `dragging` would stay `true` forever and a later, UNRELATED
   * pointer's `pointermove` would be misread as a continuation of this drag.
   * Spread onto the same element as the other four. */
  onLostPointerCapture: (e: PointerEvent) => void
}

@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/projection

Functions

cartesianProjection() from @llui/components/utils/projection
function cartesianProjection(frame: Frame, opts: CartesianOptions = {}): Projection
polarProjection() from @llui/components/utils/projection
function polarProjection(frame: Frame, opts: PolarOptions = {}): Projection
projectionFor() from @llui/components/utils/projection

Build the projection a chart's coordinate setting names.

function projectionFor(coord: ChartCoord, frame: Frame, opts: CartesianOptions & PolarOptions = {}): Projection

Types

ChartCoord from @llui/components/utils/projection
export type ChartCoord = 'cartesian' | 'polar'

Interfaces

CartesianOptions from @llui/components/utils/projection
export interface CartesianOptions {
  /** Swap the axes: `u` runs top-to-bottom and `v` left-to-right. This is what
   *  turns a column chart into a horizontal bar chart, and it is a PROJECTION
   *  concern — no mark, scale or hit test changes. */
  horizontal?: boolean
}
Frame from @llui/components/utils/projection

The plot area in user units — the box marks are drawn inside, already inset by whatever padding the axes need.

export interface Frame {
  x: number
  y: number
  width: number
  height: number
}
PolarOptions from @llui/components/utils/projection
export interface PolarOptions {
  /** Inner radius as a fraction of the outer. `0` is a pie, `0.5` a donut, and
   *  a radar usually wants `0`. */
  innerRadius?: number
  /** Where `u = 0` sits, in radians clockwise from 12 o'clock. */
  startAngle?: number
  /** Total sweep in radians. Defaults to a full turn, which is what makes the
   *  projection `closed`. */
  sweep?: number
  /**
   * Gridline shape. `ring` is a circle at the given magnitude — right for a
   * pie or a rose. `web` joins the `spokes` sample positions with straight
   * segments — right for a radar, where a circular grid reads as a different
   * chart from the polygonal data on top of it.
   */
  grid?: 'ring' | 'web'
  /** How many spokes a `web` gridline has. Ignored for `ring`. */
  spokes?: number
  /**
   * Swap the axes, exactly as {@link CartesianOptions.horizontal} does: the
   * INDEPENDENT axis moves off the angle and onto the RADIUS, so each category
   * becomes its own concentric ring and magnitude is read as arc length. That
   * is a radial bar chart, and it is the polar reading of the same flag that
   * turns a column chart on its side — one field, the same meaning in both
   * coordinate systems, so a consumer flipping `coord` keeps whatever
   * orientation they asked for.
   *
   * The independent axis is then the radius, which is NOT a loop, so the
   * projection reports `closed: false` however wide the sweep is.
   */
  horizontal?: boolean
}
Projection from @llui/components/utils/projection
export interface Projection {
  readonly kind: ChartCoord
  /**
   * Whether the independent axis is a CLOSED loop. `true` for a polar
   * projection covering a full turn, which is what makes a radar outline join
   * back to its first vertex instead of leaving a gap.
   */
  readonly closed: boolean
  /**
   * Curves this projection can honour. A caller asking for one outside the set
   * gets `linear` — see the polar note below for why this is the honest answer
   * rather than a silent approximation.
   */
  readonly curves: readonly Curve[]
  /** Normalized (u, v) → a point in user units. */
  point(u: number, v: number): Point
  /** A run of samples joined along increasing `u`. */
  line(samples: readonly Sample[], curve: Curve): string
  /** The region between an upper run and a lower run of the same length. */
  area(upper: readonly Sample[], lower: readonly Sample[], curve: Curve): string
  /** A band spanning `u0..u1` and `v0..v1`: a bar, or a wedge. */
  band(u0: number, u1: number, v0: number, v1: number): string
  /** The iso-magnitude line at `v` — a value gridline. */
  gridline(v: number): string
  /** Where the independent-axis tick for `u` belongs. */
  tick(u: number): TickPlacement
  /** Where the DEPENDENT-axis tick for `v` belongs. */
  valueTick(v: number): TickPlacement
  /**
   * Invert a point in user units back to `u`, for pointer hit testing. Returns
   * `null` when the point carries no meaningful `u` — the exact centre of a
   * polar chart, where every angle is equally close.
   */
  locate(x: number, y: number): number | null
}
TickPlacement from @llui/components/utils/projection

Where an independent-axis tick label sits, and how to align it there.

export interface TickPlacement {
  x: number
  y: number
  /** SVG `text-anchor`. */
  anchor: 'start' | 'middle' | 'end'
  /** SVG `dominant-baseline`. */
  baseline: 'auto' | 'middle' | 'hanging'
}

@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/scale

Functions

bandCenter() from @llui/components/utils/scale

The centre of band i in normalized u.

function bandCenter(i: number, band: Band): number
bandExtent() from @llui/components/utils/scale

The [start, end] extent of band i in normalized u.

function bandExtent(i: number, band: Band): [number, number]
denormalize() from @llui/components/utils/scale

Inverse of {@link normalize}.

function denormalize(u: number, domain: Domain): number
nearestBand() from @llui/components/utils/scale

The band index nearest a normalized position — the hit test behind hover and pointer tracking. Returns null for an empty axis.

Nearest-CENTRE, not containment: the gaps between bands belong to whichever band is closer, so a pointer never falls into a dead zone between two bars.

function nearestBand(u: number, band: Band): number | null
nearestShare() from @llui/components/utils/scale

The slice containing a normalized position — the hit test behind hover on a pie.

CONTAINMENT, not the nearest centre that {@link nearestBand} uses. Bands have gaps, so a pointer between two bars has to be given to one of them; slices tile the axis with no gaps, so containment is exact and nearest-centre would be actively wrong — a thin slice beside a wide one would lose its own interior to the wide one's centre.

Zero-width slices are unhittable by construction. u outside [0, 1] and an axis with no positive total both answer null.

function nearestShare(u: number, slices: readonly ShareSlice[]): number | null
niceDomain() from @llui/components/utils/scale

Extend [min, max] outward to the nearest round tick boundaries, so the axis ends on a labelled value instead of mid-air. Iterates because widening the domain can change the chosen step, which can change the boundaries again; d3's nice does the same, and it converges in at most a few rounds.

function niceDomain(min: number, max: number, count: number): Domain
normalize() from @llui/components/utils/scale

Map a value into [0, 1] across domain. A DEGENERATE domain (min === max) maps everything to 0.5 rather than dividing by zero — a single-valued series draws as a flat line through the middle, which is the only reading that is not a crash or a NaN in a path string.

function normalize(value: number, domain: Domain): number
shareExtents() from @llui/components/utils/scale

Allocate the independent axis [0, 1] in PROPORTION to each value, instead of giving every category an equal slot the way {@link bandExtent} does.

This is the whole of what a pie chart is. Under a polar projection each slice is a wedge whose angle states its share; under a cartesian one the same slices are the segments of a single full-width 100%-share bar. Neither the projection nor any mark needs to know which it is drawing — the magnitude has simply moved from v (a bar's height) to u (a slice's extent), which is why no new mark type is involved.

Three rules, each a correctness statement rather than a default:

  • There is NO padding, and there cannot be. bandExtent takes paddingInner/paddingOuter because a bar's slot is arbitrary and the gaps only cost whitespace. Here the extent IS the datum: gaps would make the slices sum to less than the whole, so every slice would overstate or understate its share by however much padding was chosen, and a full turn would no longer be 100%. Separate slices with a stroke in the skin (what shadcn's own pie does), never with a gap in the scale.
  • A NEGATIVE value takes no arc. A share of a negative quantity is not defined, and the two silent readings are both wrong: using the magnitude draws a slice for a number nobody measured, and letting it subtract makes the remaining slices sum past 1. Contributing zero is the only reading that neither invents data nor breaks the total. Same argument as polarProjection declining monotone rather than approximating it.
  • A total of zero yields zero-width slices, not NaN. No data is a normal state; a NaN in a u becomes a NaN in a path string, and one of those voids the whole path element.

The final slice is closed at exactly 1 rather than at the accumulated sum, so float drift cannot leave a hairline wedge of background at the end of the axis — visible on a full-turn pie as a seam at 12 o'clock.

function shareExtents(values: readonly number[]): ShareSlice[]
tickIncrement() from @llui/components/utils/scale

The step size a nice axis would use to fit about count ticks into [start, stop]. A negative return means "one tick every 1/-step units" — d3's encoding, kept because the reciprocal form avoids the float drift that 10 ** -power introduces for small steps.

function tickIncrement(start: number, stop: number, count: number): number
ticks() from @llui/components/utils/scale

Round, human-readable tick values covering [start, stop]. Returns an EMPTY array for a non-finite or degenerate range rather than looping forever — a chart with no data is a normal state, not an error.

function ticks(start: number, stop: number, count: number): number[]
valueDomain() from @llui/components/utils/scale

The value-axis domain for a set of series.

min / max are UNBOUNDED-CAPABLE bounds in the sense of finiteBound (#177): an absent one means "derive it", never ±Infinity. A derived domain always INCLUDES ZERO, because a bar whose baseline is not zero misrepresents its own length — the one axis default worth being opinionated about. A caller who wants otherwise passes both bounds.

function valueDomain(values: readonly number[], opts: { min?: number; max?: number; tickCount: number }): Domain

Interfaces

Band from @llui/components/utils/scale

A discrete axis of count slots laid out across [0, 1].

export interface Band {
  count: number
  /** Gap between adjacent bands, as a fraction of a slot. */
  paddingInner: number
  /** Gap before the first and after the last band, as a fraction of a slot. */
  paddingOuter: number
}
Domain from @llui/components/utils/scale

A closed numeric interval. Both ends are finite by construction.

export interface Domain {
  min: number
  max: number
}
Sample from @llui/components/utils/scale

A normalized sample: position along the independent axis, and magnitude.

export interface Sample {
  u: number
  v: number
}
ShareSlice from @llui/components/utils/scale

One slice of a proportionally-allocated independent axis.

export interface ShareSlice {
  /** Start of the slice in normalized u. */
  start: number
  /** End of the slice in normalized u. */
  end: number
  /** The slice's fraction of the whole — `end - start`, named for readability. */
  share: number
}

@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

A cache key for a formatter built from opts (any Intl.*Options object): its defined entries in key order, so equal options give equal keys whatever order they were written in.

function cacheKey(prefix: string, locale: string, opts: object): 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: ReadSignal<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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    'data-state': ReadSignal<'on' | 'off'>
    'data-disabled': ReadSignal<'' | 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: ReadSignal<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': ReadSignal<'true' | 'false' | 'mixed'>
    'aria-disabled': ReadSignal<'true' | undefined>
    'aria-required': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'checked' | 'unchecked' | 'indeterminate'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'checkbox'
    'data-part': 'root'
    tabindex: ReadSignal<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: ReadSignal<boolean>
    indeterminate: ReadSignal<boolean>
    disabled: ReadSignal<boolean>
    required: ReadSignal<boolean>
    'data-scope': 'checkbox'
    'data-part': 'hidden-input'
  }
  /** Optional indicator child (the checkmark). */
  indicator: {
    'data-state': ReadSignal<'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: ReadSignal<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 } & Retain)
  /** @intent("Open the named accordion item") */
  | ({ type: 'open'; value: string } & Retain)
  /** @intent("Close the named accordion item") */
  | ({ type: 'close'; value: string } & Retain)
  /** @intent("Replace the set of currently-open items with the provided values") */
  | ({ type: 'setValue'; value: string[] } & Retain)
  /** @humanOnly */
  | { type: 'setItems'; items: string[] }
  /** @humanOnly */
  | { type: 'focusNext'; value: string }
  /** @humanOnly */
  | { type: 'focusPrev'; value: string }
  /** @humanOnly */
  | { type: 'focusFirst' }
  /** @humanOnly */
  | { type: 'focusLast' }
  /** @humanOnly — sent by the retained content's own animation end/cancel event,
   * or by `exitCompletion`'s own cleanup settling every still-closing item once
   * the last watcher for this `id` detaches. */
  | { type: 'exitComplete'; value: string; generation: number }

Interfaces

AccordionInit from @llui/components/accordion
export interface AccordionInit {
  value?: string[]
  multiple?: boolean
  collapsible?: boolean
  disabled?: boolean
  items?: string[]
  /**
   * Retain closing content for an exit animation. Off by default so a missing
   * stylesheet/event cannot leave hidden semantic content mounted forever.
   */
  animated?: boolean
}
AccordionItemParts from @llui/components/accordion
export interface AccordionItemParts {
  trigger: {
    type: 'button'
    'aria-expanded': ReadSignal<boolean>
    'aria-controls': string
    id: string
    'data-state': ReadSignal<'open' | 'closed'>
    'data-disabled': ReadSignal<'' | undefined>
    disabled: ReadSignal<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': ReadSignal<'open' | 'closing' | 'closed'>
    'data-scope': 'accordion'
    'data-part': 'content'
    'data-value': string
    hidden: ReadSignal<boolean>
    'aria-hidden': ReadSignal<'true' | undefined>
    inert: ReadSignal<boolean>
    onAnimationStart: (e: AnimationEvent) => void
    onAnimationEnd: (e: AnimationEvent) => void
    onAnimationCancel: (e: AnimationEvent) => void
    onTransitionStart: (e: TransitionEvent) => void
    onTransitionEnd: (e: TransitionEvent) => void
    onTransitionCancel: (e: TransitionEvent) => void
  }
  item: {
    'data-state': ReadSignal<'open' | 'closed'>
    'data-disabled': ReadSignal<'' | 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
  /**
   * Settles a retained `closing` item once its content's own exit
   * animation/transition ends — or immediately, if the skin runs no exit
   * motion at all. This only ever has anything to settle for a
   * PROGRAMMATIC close/toggle/setValue when that message carried
   * `retain: true`, which `parts.close(value)` stamps for you; a raw
   * `send({ type: 'close', value })` with no `retain` closes instantly and
   * never enters `closing` at all, so there is nothing here to settle for
   * it. Its mount ALSO reports whether it is placed at all (#264 item F1):
   * `animated: true` only ever retains `closing` content while this is
   * mounted — forgetting to place it degrades gracefully to an instant
   * close (with a one-time dev warning) rather than hanging `closing` +
   * `inert` forever, so placing it is no longer required for SAFETY, only
   * for the requested exit animation to actually run on a retained close.
   * A click-driven close is still safety-netted synchronously inside the
   * trigger regardless of whether this is placed.
   */
  exitCompletion: Mountable
  /**
   * An animated-aware programmatic close (#264 review-264j): a raw
   * `send({ type: 'close', value })` from app/agent code carries no
   * `retain` and therefore closes INSTANTLY, even with `animated: true` and
   * `exitCompletion` placed — documented, fail-safe behavior, since a bare
   * message has no way to know whether a watcher happens to be mounted.
   * `parts.close(value)` is the correct way for a host to close an item
   * programmatically and still get the animated exit: it stamps `retain`
   * from the SAME runtime registry the trigger handlers read.
   */
  close: (value: string) => void
}
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[]
  /** Presentation-only items retained while their opted-in exit animation runs. */
  closing: string[]
  /** Monotonic exit generation per item, used to reject stale end events. */
  exitGenerations: RetainedExitGeneration<string>[]
  /** Monotonic source for collision-free exit generations; never grows by id. */
  exitSequence: number
  /** Whether closed content is retained until its own animation end/cancel event. */
  animated: boolean
}
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: ReadSignal<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.

PASS THE TABS ROOT, NOT WHATEVER onMount HANDED YOU. onMount receives the BUILD's root container, not the element the call sits inside, so forwarding it resolves the lookups below in document order across the whole build — and a page with two tab sets has both tracking the first one's indicator.

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' }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; 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': ReadSignal<boolean>
    'aria-controls': string
    'aria-disabled': ReadSignal<'true' | undefined>
    id: string
    'data-state': ReadSignal<'active' | 'inactive'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'tabs'
    'data-part': 'trigger'
    'data-value': string
    tabindex: ReadSignal<number>
    onClick: (e: MouseEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
    onFocus: (e: FocusEvent) => void
  }
  panel: {
    role: 'tabpanel'
    id: string
    'aria-labelledby': string
    tabindex: 0
    hidden: ReadSignal<boolean>
    'data-state': ReadSignal<'active' | 'inactive'>
    'data-scope': 'tabs'
    'data-part': 'panel'
    'data-value': string
  }
}
TabsParts from @llui/components/tabs
export interface TabsParts {
  root: {
    id: string
    'data-scope': 'tabs'
    'data-part': 'root'
    'data-orientation': ReadSignal<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': ReadSignal<Orientation>
  }
  list: {
    role: 'tablist'
    'aria-orientation': ReadSignal<Orientation>
    'data-scope': 'tabs'
    'data-part': 'list'
  }
  item: (value: string) => TabsItemParts
  /** Place once anywhere in the same build to keep automatic direction live. */
  directionSync: Mountable
}
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'
  dirSource: DirectionSource
}

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: ReadSignal<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': ReadSignal<Orientation>
    'data-disabled': ReadSignal<'' | undefined>
  }
  control: {
    'data-scope': 'slider'
    'data-part': 'control'
    'data-orientation': ReadSignal<Orientation>
    onPointerDown: (e: PointerEvent) => void
  }
  track: {
    'data-scope': 'slider'
    'data-part': 'track'
    'data-orientation': ReadSignal<Orientation>
  }
  range: {
    'data-scope': 'slider'
    'data-part': 'range'
    'data-orientation': ReadSignal<Orientation>
    style: ReadSignal<string>
  }
  thumb: (index: number) => SliderThumbParts
  /** Current raw values — reactive convenience. */
  value: ReadSignal<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': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number>
    'aria-orientation': ReadSignal<Orientation>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-orientation': ReadSignal<Orientation>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'slider'
    'data-part': 'thumb'
    'data-index': string
    tabindex: ReadSignal<number>
    onKeyDown: (e: KeyboardEvent) => void
    style: ReadSignal<string>
  }
}

Constants

slider from @llui/components/slider
const slider

@llui/components/dialog

Functions

connect() from @llui/components/dialog
function connect(state: ReadSignal<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
  /**
   * Whether the consumer renders the `description` part (default: true).
   * When false, the content omits `aria-describedby` — which would otherwise
   * name an element that does not exist, a broken reference assistive tech
   * reports as a missing description (#268). The title stays mandatory: a
   * dialog without a name is not accessible.
   */
  hasDescription?: boolean
}
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': ReadSignal<boolean>
    'aria-controls': string
    id: string
    'data-state': ReadSignal<'open' | 'closed'>
    'data-scope': 'dialog'
    'data-part': 'trigger'
    onClick: (e: MouseEvent) => void
  }
  backdrop: {
    'data-state': ReadSignal<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
    /** The description part's id, or absent when `hasDescription: false`. */
    'aria-describedby': string | undefined
    tabindex: -1
    'data-state': ReadSignal<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: ReadSignal<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: ReadSignal<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: ReadSignal<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
  /** Selector for an arrow rendered inside the content element. When present,
   * floating positioning owns its absolute edge geometry transactionally. */
  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': ReadSignal<boolean>
    'aria-controls': string
    id: string
    'data-state': ReadSignal<'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': ReadSignal<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: ReadSignal<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: ReadSignal<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
  /** Selector for an arrow rendered inside the content element. When present,
   * floating positioning owns its absolute edge geometry transactionally. */
  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': ReadSignal<string | undefined>
    'data-state': ReadSignal<'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': ReadSignal<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: ReadSignal<MenuState>, send: Send<MenuMsg>, opts: ConnectOptions): MenuParts
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
subOverlay() from @llui/components/menu

Engine-owned floating overlay for one submenu level (#265 A4). Call once per children-bearing item, alongside its subTrigger, in place of the old hand-rolled show(isOpen, () => [div(subPositioner, [div(subContent, …)])]): subOverlay builds both wrapper divs itself and owns mount/floating/ nested-layer-ownership. See menu-machine.ts:subOverlay's doc comment for the full contract (direction resolution, no-dismiss design, runtime direction-change re-placement).

function subOverlay(opts: SubOverlayOptions): 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' or 'rtl'") */
  | { type: 'setDir'; dir: TextDirection }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; dir: TextDirection }
  /** @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}'s `dir`/
   * `dirSource`, resolved from the mounted root by `directionSync`). */
  dir?: TextDirection
  /** When false, opening and closing play enter/exit animations and the content
   * stays mounted (status 'opening' or 'closing') until an `animationEnd`.
   * Default true: instant. */
  skipAnimations?: boolean
}
export interface MenuParts {
  trigger: {
    type: 'button'
    'aria-haspopup': 'menu'
    'aria-expanded': ReadSignal<boolean>
    'aria-controls': string
    id: string
    'data-state': ReadSignal<'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': ReadSignal<string | undefined>
    tabindex: -1
    /** Reflects the presence lifecycle: 'opening' | 'open' | 'closing' | 'closed'.
     * Stays mounted while 'closing' so the exit animation can run. */
    'data-state': ReadSignal<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
  /**
   * A labelled group's parts. The group names its `label` part through
   * `aria-labelledby`; a consumer that renders the group WITHOUT that label
   * passes `{ hasLabel: false }` so it never names an element that does not
   * exist (an unlabelled `role="group"` is valid; a broken idref is not — the
   * `dialog` `hasDescription` rule, #268). Default: true.
   */
  group: (id: string, options?: { readonly hasLabel?: boolean }) => MenuGroupParts
  separator: () => MenuSeparatorParts
  subTrigger: (value: string) => MenuSubTriggerParts
  subPositioner: (value: string) => MenuSubPositionerParts
  subContent: (value: string) => MenuSubContentParts
  /** Place once anywhere in the same build to keep automatic direction live —
   * the shared `@llui/interactions` direction-sync seam (#265 finding 6),
   * observing the TRIGGER (always mounted, unlike the content) as this
   * instance's root. A discarded `Mountable` is inert, so this must be placed
   * in the view. */
  directionSync: Mountable
}
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
  // `dir` + `dirSource` come from `MenuTreeState` (extends `DirectionState`) —
  // the shared `@llui/interactions` direction-sync seam (#265 finding 6).
  // Under 'rtl', ArrowLeft/ArrowRight swap meaning, and the overlay's
  // `*-start`/`*-end` alignment tracks the inline-start/inline-end edge. An
  // EXPLICIT direction is AUTHORITATIVE once it reaches `attachFloating`: a
  // silently-overridden page direction laid a menu on `<html dir="rtl">` out
  // LTR (#138 review, blocking 4) — `dirSource: 'explicit'` is what keeps
  // that override from being overwritten by the next DOM observation.
}
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: ReadSignal<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
}
SubOverlayOptions from @llui/components/menu
export interface SubOverlayOptions {
  /** The subTrigger value this level opens under. */
  value: string
  state: ReadSignal<MenuState>
  parts: Pick<MenuParts, 'subTrigger' | 'subPositioner' | 'subContent'>
  content: () => Renderable
  target?: string | HTMLElement
  positionerClass?: string
  align?: 'start' | 'end'
  offset?: number
  flip?: boolean
  shift?: boolean
}

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: ReadSignal<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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'checked' | 'unchecked'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'switch'
    'data-part': 'root'
    tabindex: ReadSignal<number>
    onClick: (e: MouseEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  track: {
    'data-state': ReadSignal<'checked' | 'unchecked'>
    'data-scope': 'switch'
    'data-part': 'track'
  }
  thumb: {
    'data-state': ReadSignal<'checked' | 'unchecked'>
    'data-scope': 'switch'
    'data-part': 'thumb'
  }
  hiddenInput: {
    type: 'checkbox'
    role: 'switch'
    'aria-hidden': 'true'
    tabindex: -1
    style: string
    checked: ReadSignal<boolean>
    disabled: ReadSignal<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: ReadSignal<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': ReadSignal<Orientation>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-scope': 'radio-group'
    'data-part': 'root'
    'data-orientation': ReadSignal<Orientation>
    'data-disabled': ReadSignal<'' | 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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'checked' | 'unchecked'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'radio-group'
    'data-part': 'item'
    'data-value': string
    tabindex: ReadSignal<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': ReadSignal<'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: ReadSignal<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' } & Retain)
  /** @intent("Expand the collapsible panel") */
  | ({ type: 'open' } & Retain)
  /** @intent("Collapse the panel") */
  | ({ type: 'close' } & Retain)
  /** @intent("Set the panel's open state to a specific value") */
  | ({ type: 'setOpen'; open: boolean } & Retain)
  /** @humanOnly — sent by the retained content's own animation end/cancel event,
   * or by `exitCompletion`'s own cleanup settling a still-closing panel once
   * the last watcher for this `id` detaches. */
  | { type: 'exitComplete'; generation: number }

Interfaces

CollapsibleInit from @llui/components/collapsible
export interface CollapsibleInit {
  open?: boolean
  disabled?: boolean
  /** Opt into retained exit motion. Defaults off to guarantee no event/no hang. */
  animated?: boolean
}
CollapsibleParts from @llui/components/collapsible
export interface CollapsibleParts {
  root: {
    'data-state': ReadSignal<'open' | 'closed'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'collapsible'
    'data-part': 'root'
  }
  trigger: {
    type: 'button'
    'aria-expanded': ReadSignal<boolean>
    'aria-controls': string
    id: string
    disabled: ReadSignal<boolean>
    'data-state': ReadSignal<'open' | 'closed'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'collapsible'
    'data-part': 'trigger'
    onClick: (e: MouseEvent) => void
  }
  content: {
    role: 'region'
    id: string
    'aria-labelledby': string
    hidden: ReadSignal<boolean>
    'data-state': ReadSignal<'open' | 'closing' | 'closed'>
    'data-scope': 'collapsible'
    'data-part': 'content'
    'aria-hidden': ReadSignal<'true' | undefined>
    inert: ReadSignal<boolean>
    onAnimationStart: (e: AnimationEvent) => void
    onAnimationEnd: (e: AnimationEvent) => void
    onAnimationCancel: (e: AnimationEvent) => void
    onTransitionStart: (e: TransitionEvent) => void
    onTransitionEnd: (e: TransitionEvent) => void
    onTransitionCancel: (e: TransitionEvent) => void
  }
  /**
   * Settles a retained `closing` panel once the content's own exit
   * animation/transition ends — or immediately, if the skin runs no exit
   * motion at all. This only ever has anything to settle for a
   * PROGRAMMATIC close/toggle/setOpen when that message carried
   * `retain: true`, which `parts.close()` stamps for you; a raw
   * `send({ type: 'close' })` with no `retain` closes instantly and never
   * enters `closing` at all. Its mount ALSO reports whether it is placed at
   * all: `animated: true` only ever retains `closing` while this is mounted
   * (#264 item F1) — forgetting to place it degrades gracefully to an
   * instant close (with a one-time dev warning) rather than hanging
   * `closing` + `inert` forever, so placing it is no longer required for
   * SAFETY, only for the requested exit animation to actually run on a
   * retained close. A click-driven close is still safety-netted
   * synchronously inside the trigger regardless of whether this is placed.
   */
  exitCompletion: Mountable
  /**
   * An animated-aware programmatic close (#264 review-264j) — see
   * `accordion.ts`'s identical part for the full rationale. A raw
   * `send({ type: 'close' })` carries no `retain` and closes INSTANTLY;
   * `parts.close()` stamps `retain` from the runtime registry.
   */
  close: () => void
}
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
  /** Presentation-only retention while an opted-in exit animation runs. */
  closing: boolean
  /** Monotonic generation used to reject stale animation completion events. */
  exitGeneration: number
  /** Whether close waits for the content's own animation end/cancel event. */
  animated: 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: ReadSignal<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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    'data-state': ReadSignal<'on' | 'off'>
    'data-disabled': ReadSignal<'' | undefined>
    'data-scope': 'toggle-group'
    'data-part': 'item'
    'data-value': string
    tabindex: ReadSignal<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': ReadSignal<'true' | undefined>
    'data-scope': 'toggle-group'
    'data-part': 'root'
    'data-orientation': ReadSignal<Orientation>
    'data-disabled': ReadSignal<'' | 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: ReadSignal<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': ReadSignal<'' | undefined>
  }
  input: {
    type: 'text'
    role: 'spinbutton'
    inputmode: 'decimal'
    'aria-valuemin': ReadSignal<number | undefined>
    'aria-valuemax': ReadSignal<number | undefined>
    'aria-valuenow': ReadSignal<number | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    'aria-readonly': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    readonly: ReadSignal<boolean>
    value: ReadSignal<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': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    'data-scope': 'number-input'
    'data-part': 'increment'
    tabindex: -1
    onClick: (e: MouseEvent) => void
  }
  decrement: {
    type: 'button'
    'aria-label': string
    'aria-disabled': ReadSignal<'true' | undefined>
    disabled: ReadSignal<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: ReadSignal<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
  /**
   * Whether the consumer renders the `label` part (default: true). When false
   * — the group is named some other way, or not at all, which `role="group"`
   * allows — the root omits `aria-labelledby` rather than naming an element
   * that does not exist (the `dialog` `hasDescription` rule, #268).
   */
  hasLabel?: boolean
}
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'
    /** The label part's id, or absent when `hasLabel: false`. */
    'aria-labelledby': string | undefined
    'data-scope': 'pin-input'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
  }
  label: {
    id: string
    'data-scope': 'pin-input'
    'data-part': 'label'
  }
  /** Props for the input at a given index. */
  input: (index: number) => {
    type: ReadSignal<'text' | 'password'>
    inputmode: ReadSignal<'numeric' | 'text'>
    pattern: ReadSignal<string>
    maxlength: 1
    autocomplete: 'off'
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<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: ReadSignal<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': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number | undefined>
    'aria-label': string | undefined
    'data-state': ReadSignal<'indeterminate' | 'complete' | 'loading'>
    'data-orientation': ReadSignal<ProgressOrientation>
    'data-scope': 'progress'
    'data-part': 'root'
  }
  track: {
    'data-state': ReadSignal<'indeterminate' | 'complete' | 'loading'>
    'data-orientation': ReadSignal<ProgressOrientation>
    'data-scope': 'progress'
    'data-part': 'track'
  }
  range: {
    'data-state': ReadSignal<'indeterminate' | 'complete' | 'loading'>
    'data-orientation': ReadSignal<ProgressOrientation>
    'data-scope': 'progress'
    'data-part': 'range'
    style: ReadSignal<string>
  }
  label: {
    'data-scope': 'progress'
    'data-part': 'label'
  }
  valueText: ReadSignal<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: ReadSignal<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; isStartHalf: boolean }
  /** @humanOnly */
  | { type: 'hoverItem'; index: number; isStartHalf: 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': ReadSignal<'true' | undefined>
    'aria-readonly': ReadSignal<'true' | undefined>
    'data-scope': 'rating-group'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
    'data-readonly': ReadSignal<'' | 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': ReadSignal<boolean>
    'data-fill': ReadSignal<ItemFill>
    'data-scope': 'rating-group'
    'data-part': 'item'
    'data-value': string
    'data-disabled': ReadSignal<'' | undefined>
    tabindex: ReadSignal<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: ReadSignal<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 }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; dir: TextDirection }

Interfaces

ConnectOptions from @llui/components/pagination
export interface ConnectOptions {
  id: string
  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: {
    id: string
    role: 'navigation'
    'aria-label': string
    'data-scope': 'pagination'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
  }
  prevTrigger: {
    type: 'button'
    'aria-label': string
    'aria-disabled': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    'data-scope': 'pagination'
    'data-part': 'prev-trigger'
    tabindex: ReadSignal<number>
    onClick: (e: MouseEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  nextTrigger: {
    type: 'button'
    'aria-label': string
    'aria-disabled': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    'data-scope': 'pagination'
    'data-part': 'next-trigger'
    tabindex: ReadSignal<number>
    onClick: (e: MouseEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  item: (page: number) => {
    type: 'button'
    'aria-label': string
    'aria-current': ReadSignal<'page' | undefined>
    'data-selected': ReadSignal<'' | undefined>
    'data-scope': 'pagination'
    'data-part': 'item'
    'data-value': string
    tabindex: ReadSignal<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'
  }
  /** Place once anywhere in the same build to keep automatic direction live. */
  directionSync: Mountable
}
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
  dirSource: DirectionSource
}

Constants

pagination from @llui/components/pagination
const pagination

@llui/components/alert-dialog

Functions

connect() from @llui/components/alert-dialog
function connect(state: ReadSignal<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: ReadSignal<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: ReadSignal<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': ReadSignal<boolean>
    'aria-controls': string
    id: string
    'data-state': ReadSignal<'open' | 'closed'>
    'data-scope': 'drawer'
    'data-part': 'trigger'
    onClick: (e: MouseEvent) => void
  }
  backdrop: {
    'data-state': ReadSignal<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': ReadSignal<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: ReadSignal<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: ReadSignal<ToasterState>, send: Send<ToasterMsg>, opts: ConnectOptions = {}): ToasterParts
init() from @llui/components/toast
function init(opts: ToasterInit = {}): ToasterState
isPaused() from @llui/components/toast

Whether any reason currently holds the toast's countdown paused.

function isPaused(t: Pick<Toast, 'pausedBy'>): boolean
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("Move the toast region to a different corner/edge of the viewport") */
  | { type: 'setPlacement'; placement: ToastPlacement }
  /** @intent("Patch mutable presentation fields on the toast with the given id (title, description, type, etc.); `id` cannot be patched") */
  | { type: 'update'; id: string; patch: ToastPatch }
  /** @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; reason?: ToastPauseReason }
  /** @intent("Resume auto-dismiss countdown for the toast with the given id (releases only the given reason, default manual)") */
  | { type: 'resume'; id: string; reason?: ToastPauseReason }
  /** @intent("Pause auto-dismiss for every visible toast") */
  | { type: 'pauseAll'; reason?: ToastPauseReason }
  /** @intent("Resume auto-dismiss for every visible toast (releases only the given reason, default manual)") */
  | { type: 'resumeAll'; reason?: ToastPauseReason }
  /** @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/pausedBy/status are optional — seeded from duration/[]/'open' when omitted.

export type ToastInput = Omit<Toast, 'remainingMs' | 'pausedBy' | 'status'> & {
  remainingMs?: number
  pausedBy?: readonly ToastPauseReason[]
  status?: PresenceStatus
}
ToastPatch from @llui/components/toast

Fields an update message may patch on a mounted toast. id is the ONE truly immutable field — a toast is created once and dismissed once, never structurally replaced with a different id for the same row — so it is excluded here rather than silently ignored by the reducer. type and ariaLive (among others) ARE genuine mutable presentation fields (the toast.promise-style loading→success/error flow patches type, title and description on the same mounted toast) and connect()'s toast() builder binds every one of these reactively (never via a one-shot peek()), so a patch here is visible wherever it renders.

status/remainingMs/pausedBy are excluded: they are LIFECYCLE fields the reducer itself owns (presence transitions, the tick-driven countdown, pause/resume) and a patch is the wrong channel for them — dismiss/tick/ pause/resume already exist and a caller patching remainingMs directly would race the reducer's own countdown math. duration stays patchable (it IS presentation — the toast.promise flow moves a sticky loading toast to a finite success/error duration), so update re-seeds remainingMs from the new duration whenever duration is part of the patch (#265 A2) — otherwise a toast created sticky (duration: null, remainingMs frozen at 0) that is later patched to a finite duration would inherit that frozen 0 and dismiss on the very next tick instead of lasting its new duration.

undefined on a REQUIRED field (type, duration, dismissable) means "not patched", the same as an absent key (#265 G6): { duration: undefined } leaves the countdown alone rather than re-seeding it to 0 — patch duration: null to make a toast sticky. On an OPTIONAL field (title, description, ariaLive) undefined clears it, so a loading -> success patch can drop a description the success toast should not keep.

export type ToastPatch = Partial<Omit<Toast, 'id' | 'status' | 'remainingMs' | 'pausedBy'>>
ToastPauseReason from @llui/components/toast

Why a toast's countdown is paused. The row's own handlers use 'hover' and 'focus'; a pause/resume message without a reason uses 'manual'.

export type ToastPauseReason = (typeof PAUSE_REASONS)[number]
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 pausedBy (the set of reasons currently holding it 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 (any reason held) freezes its remainingMs (ticks are ignored).
  • Pause REASONS are independent (#265 G6): the row's hover pauses/resumes 'hover', focus entering/leaving the row pauses/resumes 'focus', and pause/resume with no reason use 'manual'. Pointer-leave therefore never resumes a toast whose close button still has keyboard focus.
  • 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 USER can dismiss the toast: `false` hides the close trigger
   * (`closeTrigger.hidden`) and ignores its click. A `dismiss` message from
   * code still removes it. Patchable. */
  dismissable: boolean
  /** The reasons currently holding the countdown paused, as a canonical set
   * (unique, in {@link ToastPauseReason} order). Empty = running. Owned by the
   * reducer: change it with `pause`/`resume`, read it with {@link isPaused}. */
  pausedBy: ToastPauseReason[]
  /** 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': ReadSignal<ToastPlacement>
  }
  /**
   * Build the per-row part descriptors for one toast. Takes the row's
   * `ReadSignal<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). Only `id` is immutable for a toast's lifetime — created then
   * dismissed, never structurally replaced — so this reads `id` once
   * internally to build id-derived wiring (the keyed `each` rebuilds the row
   * if `id` changes); every other field (`type`, `ariaLive`, `status`, …) is
   * bound reactively so an `update` patch renders wherever it appears.
   */
  toast: (toast: ReadSignal<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) => ReadSignal<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) => ReadSignal<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: {
    /**
     * Reactive: derived from the toast's current `type`/`ariaLive` (see
     * {@link politeness}), never frozen at mount — an `update` patching
     * either is visible here.
     */
    role: ReadSignal<'status' | 'alert'>
    'aria-atomic': 'true'
    /** Reactive — see `role` above. */
    'aria-live': ReadSignal<ToastPoliteness>
    id: string
    'data-scope': 'toast'
    'data-part': 'root'
    /** Reactive: an `update` patching `type` (e.g. a promise toast moving
     * loading → success) is visible here, not frozen at mount. */
    'data-type': ReadSignal<ToastType>
    'data-id': string
    /** Reactive presence status (closed/opening/open/closing) for CSS-driven
     * enter/exit animations. */
    'data-state': ReadSignal<PresenceStatus>
    onPointerEnter: (e: PointerEvent) => void
    onPointerLeave: (e: PointerEvent) => void
    /** Bubbling — a plain (non-bubbling) `onFocus`/`onBlur` here would never
     * fire for the only naturally focusable descendant, the close button,
     * so pause-on-focus would be dead: nothing but the row itself receives
     * `focus`/`blur` directly, and it carries no `tabindex`. */
    onFocusIn: (e: FocusEvent) => void
    onFocusOut: (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'
    /** Reactive: true while the toast is not `dismissable` — the button leaves
     * the accessibility tree and the tab order. Hiding it while it has focus
     * is safe for the `'focus'` pause reason: the browser's focus fixup fires
     * `blur`/`focusout` on the row (verified in Chromium), which releases it. */
    hidden: ReadSignal<boolean>
    /** Dismisses the toast; ignored while it is not `dismissable`. */
    onClick: (e: MouseEvent) => void
  }
}

Constants

toast from @llui/components/toast
const toast

@llui/components/listbox

Functions

connect() from @llui/components/listbox
function connect(state: ReadSignal<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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'selected' | undefined>
    'data-highlighted': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | 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': ReadSignal<'true' | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    'aria-activedescendant': ReadSignal<string | undefined>
    tabindex: ReadSignal<number>
    id: string
    'data-scope': 'listbox'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | 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: ReadSignal<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: ReadSignal<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'
    /** The group label's id, or absent when `hasLabel: false`. */
    'aria-labelledby': string | undefined
    '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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'selected' | undefined>
    'data-highlighted': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | 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': ReadSignal<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': ReadSignal<boolean>
    'aria-controls': string
    'aria-activedescendant': ReadSignal<string | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    'aria-required': ReadSignal<'true' | undefined>
    id: string
    disabled: ReadSignal<boolean>
    'data-state': ReadSignal<'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': ReadSignal<'' | 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': ReadSignal<'true' | undefined>
    'aria-labelledby': string
    tabindex: -1
    'data-state': ReadSignal<'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: ReadSignal<boolean>
    multiple: ReadSignal<boolean>
    required: ReadSignal<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: ReadSignal<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. */
  /**
   * A labelled group's parts. The group names its `label` part through
   * `aria-labelledby`; a consumer that renders the group WITHOUT that label
   * passes `{ hasLabel: false }` so it never names an element that does not
   * exist (an unlabelled `role="group"` is valid; a broken idref is not — the
   * `dialog` `hasDescription` rule, #268). Default: true.
   */
  group: (id: string, options?: { readonly hasLabel?: boolean }) => SelectGroupParts
  /** Selected value(s) — use for rendering the trigger label. */
  valueText: ReadSignal<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: ReadSignal<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
loadProjection() from @llui/components/combobox
function loadProjection(state: Pick<ComboboxState, 'status' | 'items'>): LoadProjection
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
   *
   * Replace `items` (and optionally `disabled`) synchronously — the same
   * highlight-resolution policy as `loadSuccess` applies: a highlight that no
   * longer survives the fresh list moves to the first enabled match WHILE
   * OPEN, or to `null` WHILE CLOSED (#265 G3), since a background refresh
   * must not manufacture a highlight before the control is ever opened.
   */
  | { type: 'setItems'; items: string[]; disabled?: string[] }
  /** @intent("Mark an async option fetch as started; pass the request's id") */
  | { type: 'loadStart'; requestId: number }
  /**
   * @humanOnly
   *
   * Atomic replacement: `items` is required. `disabled` is an OPTIONAL
   * companion that replaces `disabledItems` when present (omitted ⇒
   * unchanged). `groups` is different: omitting it RESETS to no groups
   * (`[]`), the same as `init()` with no `groups` option — a fresh
   * replacement with no `groups` describes a flat result, and carrying a
   * PREVIOUS load's groups forward would keep describing options this
   * replacement never mentioned as belonging to a group that may no longer
   * apply (#265 A3). Every field the fresh `items`/`disabled`/`groups` makes
   * inconsistent is reconciled in this SAME reducer step, never in a
   * follow-up message. `value` (selection) is dropped when it no longer
   * names a value in the new `items` (after the new `disabled` is applied).
   * `highlightedValue` is kept only when it is BOTH still in the fresh
   * filtered list AND not newly disabled. Otherwise the fallback depends on
   * whether the listbox is open: WHILE OPEN it moves to the first enabled
   * match (or `null` when none is enabled) — never left dangling for a
   * render in between, and never left naming an option that is now
   * disabled. WHILE CLOSED it always resolves to `null` instead, even when
   * the fresh list has enabled options: the listbox content is unmounted
   * while closed, so there is no option `aria-activedescendant` could
   * correctly name, and a background load (a prefetch, a poll) must not
   * manufacture a highlight before the control is ever opened — re-opening
   * always reseeds the highlight itself (#265 G3).
   */
  | {
      type: 'loadSuccess'
      requestId: number
      items: string[]
      groups?: ComboboxGroup[]
      disabled?: string[]
    }
  /** @humanOnly */
  | { type: 'loadError'; requestId: number; error: string }
LoadProjection from @llui/components/combobox

A single, mutually-exclusive summary of the async load lifecycle, derived from status and whether any items are currently on hand. This exists so a consumer never has to reconcile independent booleans (isLoading, isEmpty, hasError) that can read true at the same time — exactly the defect #265 finding 11 named test-first. There are five states partitioning every reachable (status, items.length) pair:

  • 'initial-empty' — nothing has ever loaded and none were given synchronously (status === 'idle', no items).
  • 'loading' — a fetch is in flight and there is nothing yet to show (a first-ever load).
  • 'stale-results' — the STALE-WHILE-REVALIDATE state: a fetch is in flight while a previous list is still on screen. loadStart never clears items, so the previous results keep rendering, filterable and selectable, until the matching loadSuccess/loadError lands.
  • 'success' — the current items are the result of a completed load, or were given synchronously and never superseded by a failed fetch.
  • 'error' — the most recent fetch failed. Per the same policy, items from an earlier successful load are left mounted and selectable; only the live region / a consumer's own error slot communicate the failure, so 'error' is reported the same whether or not stale items remain.
export type LoadProjection = 'initial-empty' | 'loading' | 'stale-results' | 'success' | 'error'
SelectionMode from @llui/components/combobox

Combobox — text input paired with a filtered listbox dropdown. User types to filter items, arrow keys navigate the filtered set, Enter selects. Supports single and multiple selection.

Beyond the sync filtered listbox the machine owns three additive surfaces:

  • Async option loading — status/requestId/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 groups — groups 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'
    /** The group label's id, or absent when `hasLabel: false`. */
    'aria-labelledby': string | undefined
    '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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'selected' | undefined>
    'data-highlighted': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | 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': ReadSignal<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': ReadSignal<'open' | 'closed'>
  }
  input: {
    type: 'text'
    role: 'combobox'
    autocomplete: 'off'
    'aria-autocomplete': 'list'
    'aria-expanded': ReadSignal<boolean>
    'aria-controls': string
    'aria-activedescendant': ReadSignal<string | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    id: string
    disabled: ReadSignal<boolean>
    value: ReadSignal<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': ReadSignal<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': ReadSignal<'true' | undefined>
    tabindex: -1
    'data-state': ReadSignal<'open' | 'closed'>
    'data-status': ReadSignal<AsyncStatus>
    /** The mutually-exclusive load projection (#265 finding 11) — see
     * {@link LoadProjection}. Mirrors the top-level `loadState` signal. */
    'data-load-state': ReadSignal<LoadProjection>
    'data-scope': 'combobox'
    'data-part': 'content'
  }
  /** The mutually-exclusive async load projection: `'initial-empty'` |
   * `'loading'` | `'stale-results'` | `'success'` | `'error'`. A single
   * signal instead of independent `isLoading`/`isEmpty`/`hasError` booleans,
   * so it can never contradict itself. See {@link LoadProjection}. */
  loadState: ReadSignal<LoadProjection>
  /** 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`. */
  /**
   * A labelled group's parts. The group names its `label` part through
   * `aria-labelledby`; a consumer that renders the group WITHOUT that label
   * passes `{ hasLabel: false }` so it never names an element that does not
   * exist (an unlabelled `role="group"` is valid; a broken idref is not — the
   * `dialog` `hasDescription` rule, #268). Default: true.
   */
  group: (id: string, options?: { readonly hasLabel?: boolean }) => 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: ReadSignal<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: ReadSignal<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: ReadSignal<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': ReadSignal<boolean>
    'data-state': ReadSignal<'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': ReadSignal<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: ReadSignal<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
  /** Selector for an arrow rendered inside the content element. When present,
   * floating positioning owns its absolute edge geometry transactionally. */
  arrowSelector?: string
}

Constants

hoverCard from @llui/components/hover-card
const hoverCard

@llui/components/avatar

Functions

connect() from @llui/components/avatar
function connect(state: ReadSignal<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

AvatarDensity from @llui/components/avatar

Optional density of the styled avatar root; behavior remains unchanged.

export type AvatarDensity = 'comfortable' | 'compact'
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': ReadSignal<ImageStatus>
    'data-density': AvatarDensity | undefined
  }
  image: {
    'data-scope': 'avatar'
    'data-part': 'image'
    'data-status': ReadSignal<ImageStatus>
    hidden: ReadSignal<boolean>
    alt: string
    onLoad: (e: Event) => void
    onError: (e: Event) => void
    onLoadStart: (e: Event) => void
  }
  fallback: {
    'data-scope': 'avatar'
    'data-part': 'fallback'
    'data-status': ReadSignal<ImageStatus>
    hidden: ReadSignal<boolean>
    'aria-hidden': ReadSignal<'true' | undefined>
  }
}
AvatarState from @llui/components/avatar
export interface AvatarState {
  status: ImageStatus
}
ConnectOptions from @llui/components/avatar
export interface ConnectOptions {
  alt?: string
  /** Presentation density published on the root for either skin to consume. */
  density?: AvatarDensity
}

Constants

avatar from @llui/components/avatar
const avatar

@llui/components/clipboard

Functions

connect() from @llui/components/clipboard
function connect(state: ReadSignal<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 and REJECTS when the write is refused. Dispatch copied from the resolved branch only, and copyFailed from the rejected one — never copied followed by reset, which would announce and then retract a success that never happened.

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' }
  /** @humanOnly */
  | { type: 'copyFailed' }
  /** @intent("Clear the transient \"copied\" / \"failed\" 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': ReadSignal<'' | undefined>
    'data-failed': ReadSignal<'' | undefined>
  }
  trigger: {
    type: 'button'
    'aria-label': string
    'data-scope': 'clipboard'
    'data-part': 'trigger'
    'data-copied': ReadSignal<'' | undefined>
    'data-failed': ReadSignal<'' | undefined>
    onClick: (e: MouseEvent) => void
  }
  input: {
    type: 'text'
    readonly: true
    value: ReadSignal<string>
    'data-scope': 'clipboard'
    'data-part': 'input'
    onFocus: (e: FocusEvent) => void
  }
  indicator: {
    'data-scope': 'clipboard'
    'data-part': 'indicator'
    'data-copied': ReadSignal<'' | undefined>
    'data-failed': ReadSignal<'' | 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.

copy is a REQUEST and does not set copied: the write can fail (permission denied, insecure context, browser policy) and indicator carries aria-live="polite", so a flag set before the promise resolves ANNOUNCES a success that never happened (#232). Dispatch copied from the resolved write, copyFailed from the rejected one, and reset to clear the feedback:

onEffect(effect, send) {
  copyToClipboard(effect.value).then(
    () => send({ type: 'copied' }),
    () => send({ type: 'copyFailed' }), // refused — say so, never "copied"
  )
}

copyFailed publishes data-failed (root, trigger, indicator); the skin tells the user the write was refused and that the value is still selectable in the read-only input (#266).

export interface ClipboardState {
  value: string
  /** The last write RESOLVED. Exclusive with `failed`. */
  copied: boolean
  /**
   * The last write was REFUSED (permission denied, insecure context, browser
   * policy). Exclusive with `copied`. The read-only `input` is the fallback: the
   * user can still select the value and copy it by hand, which is what a skin
   * should say when this is set (#266).
   */
  failed: boolean
}
ConnectOptions from @llui/components/clipboard
export interface ConnectOptions {
  copyLabel?: string
  /**
   * Called from the trigger with the value to write, alongside the `copy`
   * message. This is the seam for performing the write when the consumer does
   * not route it through an effect; dispatch `copied` from its resolved
   * promise, never from the call itself.
   */
  onCopy?: (value: string) => void
}

Constants

clipboard from @llui/components/clipboard
const clipboard

@llui/components/editable

Functions

connect() from @llui/components/editable
function connect(state: ReadSignal<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': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | undefined>
  }
  preview: {
    tabindex: ReadSignal<number>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-scope': 'editable'
    'data-part': 'preview'
    hidden: ReadSignal<boolean>
    onClick: (e: MouseEvent) => void
    onFocus: (e: FocusEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  input: {
    'data-scope': 'editable'
    'data-part': 'input'
    hidden: ReadSignal<boolean>
    value: ReadSignal<string>
    disabled: ReadSignal<boolean>
    onInput: (e: Event) => void
    onKeyDown: (e: KeyboardEvent) => void
    onBlur: (e: FocusEvent) => void
  }
  submitTrigger: {
    type: 'button'
    /** Disabled with the editable: a disabled instance ignores the message
     *  this trigger sends, so an enabled trigger would be a dead control
     *  (#268 audit). */
    disabled: ReadSignal<boolean>
    'data-scope': 'editable'
    'data-part': 'submit-trigger'
    onClick: (e: MouseEvent) => void
  }
  cancelTrigger: {
    type: 'button'
    /** Disabled with the editable: a disabled instance ignores the message
     *  this trigger sends, so an enabled trigger would be a dead control
     *  (#268 audit). */
    disabled: ReadSignal<boolean>
    'data-scope': 'editable'
    'data-part': 'cancel-trigger'
    onClick: (e: MouseEvent) => void
  }
  editTrigger: {
    type: 'button'
    /** Disabled with the editable: a disabled instance ignores the message
     *  this trigger sends, so an enabled trigger would be a dead control
     *  (#268 audit). */
    disabled: ReadSignal<boolean>
    '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: ReadSignal<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: ReadSignal<number>
    'data-scope': 'tags-input'
    'data-part': 'tag'
    'data-value': string
    'data-index': string
    'data-focused': ReadSignal<'' | 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': ReadSignal<'true' | undefined>
    'data-scope': 'tags-input'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
  }
  input: {
    type: 'text'
    autocomplete: 'off'
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<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: ReadSignal<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 {
  /**
   * A layout container: its panels hold the CONSUMER's content. It carries no
   * disabled state — the resize trigger is the control and publishes its own
   * (`aria-disabled`, out of the tab order). A root `data-disabled` made every
   * disabled-state stylesheet dim the panels with it, taking ordinary text to
   * 3.69:1 (#268 audit).
   */
  root: {
    'data-scope': 'splitter'
    'data-part': 'root'
    'data-orientation': ReadSignal<Orientation>
    'data-dragging': ReadSignal<'' | undefined>
  }
  primaryPanel: {
    'data-scope': 'splitter'
    'data-part': 'primary-panel'
    style: ReadSignal<string>
  }
  secondaryPanel: {
    'data-scope': 'splitter'
    'data-part': 'secondary-panel'
    style: ReadSignal<string>
  }
  resizeTrigger: {
    role: 'separator'
    'aria-orientation': ReadSignal<Orientation>
    'aria-valuemin': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-scope': 'splitter'
    'data-part': 'resize-trigger'
    'data-orientation': ReadSignal<Orientation>
    tabindex: ReadSignal<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: ReadSignal<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[]]
uploadOf() from @llui/components/file-upload

The upload entry for a file id, or undefined when it has not started.

function uploadOf(state: FileUploadState, id: string): FileUploadProgress | undefined
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 }
  /** @humanOnly */
  | { type: 'uploadProgress'; id: string; progress: number }
  /** @humanOnly */
  | { type: 'uploadSucceeded'; id: string }
  /** @humanOnly */
  | { type: 'uploadFailed'; id: string; error: string }
  /** @intent("Retry the failed upload of the file with the given id") */
  | { type: 'retryUpload'; id: string }
FileUploadStatus from @llui/components/file-upload
export type FileUploadStatus = 'uploading' | 'done' | 'error'

Interfaces

ConnectOptions from @llui/components/file-upload
export interface ConnectOptions {
  id: string
  removeLabel?: string
  retryLabel?: string
  progressLabel?: 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
    /** The file's upload status; absent until an upload is reported (#266). */
    'data-upload-status': ReadSignal<FileUploadStatus | undefined>
  }
  /** A labelled progressbar, shown only while the file is uploading (#266). */
  itemProgress: {
    role: 'progressbar'
    'aria-label': string
    'aria-valuemin': 0
    'aria-valuemax': 100
    'aria-valuenow': ReadSignal<number | undefined>
    hidden: ReadSignal<boolean>
    'data-scope': 'file-upload'
    'data-part': 'item-progress'
  }
  /** The progress fill; its width is written inline as a percentage. */
  itemProgressRange: {
    'data-scope': 'file-upload'
    'data-part': 'item-progress-range'
    style: ReadSignal<string>
  }
  /**
   * Live region for the file's failure message, shown only on error. Its text
   * is `uploadOf(state, id)?.error` — the view renders it as a child.
   */
  itemErrorText: {
    role: 'alert'
    hidden: ReadSignal<boolean>
    'data-scope': 'file-upload'
    'data-part': 'item-error-text'
  }
  /** Shown only for a failed upload; dispatches `retryUpload` for this file. */
  itemRetryTrigger: {
    type: 'button'
    'aria-label': string
    hidden: ReadSignal<boolean>
    'data-scope': 'file-upload'
    'data-part': 'item-retry-trigger'
    onClick: (e: MouseEvent) => void
  }
  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': ReadSignal<'' | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-dragging': ReadSignal<'' | undefined>
    'data-invalid': ReadSignal<'' | undefined>
    'data-readonly': ReadSignal<'' | undefined>
    /** Present while any accepted file is uploading (#266). */
    'data-uploading': ReadSignal<'' | undefined>
  }
  dropzone: {
    'data-scope': 'file-upload'
    'data-part': 'dropzone'
    'data-dragging': ReadSignal<'' | 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: ReadSignal<boolean>
    onClick: (e: MouseEvent) => void
  }
  hiddenInput: {
    type: 'file'
    tabindex: -1
    'aria-hidden': 'true'
    style: string
    disabled: ReadSignal<boolean>
    multiple: ReadSignal<boolean>
    accept: ReadSignal<string>
    required: ReadSignal<boolean>
    'aria-invalid': ReadSignal<'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
}
FileUploadProgress from @llui/components/file-upload
export interface FileUploadProgress {
  status: FileUploadStatus
  /** 0..1 */
  progress: number
  /** The failure message while `status === 'error'`, else `null`. */
  error: string | null
}
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
  /**
   * Upload lifecycle per accepted file, keyed by `FileMeta.id` — never by
   * index, which shifts when an earlier file is removed. A file with no entry
   * has not started uploading. The machine does not upload: the consumer's
   * effect does, and reports back with `uploadProgress` / `uploadSucceeded` /
   * `uploadFailed`; `retryUpload` is the request to try again (#266).
   */
  uploads: Record<string, FileUploadProgress>
}
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: ReadSignal<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': ReadSignal<boolean | undefined>
    'aria-selected': ReadSignal<boolean | undefined>
    'aria-level': number
    'aria-busy': ReadSignal<'true' | undefined>
    tabindex: ReadSignal<number>
    'data-scope': 'tree-view'
    'data-part': 'item'
    'data-value': string
    'data-depth': string
    'data-selected': ReadSignal<'' | undefined>
    'data-focused': ReadSignal<'' | undefined>
    'data-loading': ReadSignal<'' | undefined>
    'data-load-failed': ReadSignal<'' | 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': ReadSignal<'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': ReadSignal<'true' | 'false' | 'mixed'>
    'data-scope': 'tree-view'
    'data-part': 'checkbox'
    'data-state': ReadSignal<'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': ReadSignal<'true' | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-scope': 'tree-view'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | 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: ReadSignal<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
subOverlay() from @llui/components/context-menu

Engine-owned floating overlay for one submenu level (#265 A4), positioned beside its subTrigger. See menu-machine.ts:subOverlay's doc comment for the full contract.

function subOverlay(opts: SubOverlayOptions): 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' or 'rtl'") */
  | { type: 'setDir'; dir: TextDirection }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; dir: TextDirection }
  /** @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 {@link ContextMenuState}'s
   * `dir`/`dirSource`, resolved from the mounted trigger by `directionSync`). */
  dir?: TextDirection
  /** When false, opening and closing play enter/exit animations and the content
   * stays mounted (status 'opening' or '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. `id` is REQUIRED — it is
   * the scope `directionSync` (below) observes for live ancestor `dir`
   * changes, since it (unlike `content`) is always mounted (#265 finding 6). */
  trigger: {
    id: string
    'data-scope': 'context-menu'
    'data-part': 'trigger'
    onContextMenu: (e: MouseEvent) => void
  }
  positioner: {
    'data-scope': 'context-menu'
    'data-part': 'positioner'
    style: ReadSignal<string>
  }
  content: {
    role: 'menu'
    id: string
    /** Virtually-focused (highlighted) item id at the root level. */
    'aria-activedescendant': ReadSignal<string | undefined>
    tabindex: -1
    /** Reflects the presence lifecycle: 'opening' | 'open' | 'closing' | 'closed'.
     * Stays mounted while 'closing' so the exit animation can run. */
    'data-state': ReadSignal<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
  /**
   * A labelled group's parts. The group names its `label` part through
   * `aria-labelledby`; a consumer that renders the group WITHOUT that label
   * passes `{ hasLabel: false }` so it never names an element that does not
   * exist (an unlabelled `role="group"` is valid; a broken idref is not — the
   * `dialog` `hasDescription` rule, #268). Default: true.
   */
  group: (id: string, options?: { readonly hasLabel?: boolean }) => ContextMenuGroupParts
  separator: () => ContextMenuSeparatorParts
  subTrigger: (value: string) => ContextMenuSubTriggerParts
  subPositioner: (value: string) => ContextMenuSubPositionerParts
  subContent: (value: string) => ContextMenuSubContentParts
  /** Place once anywhere in the same build to keep automatic direction live —
   * the shared `@llui/interactions` direction-sync seam (#265 finding 6),
   * observing the TRIGGER (always mounted, unlike the content) as this
   * instance's root. A discarded `Mountable` is inert, so this must be placed
   * in the view. */
  directionSync: Mountable
}
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: ReadSignal<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
}
SubOverlayOptions from @llui/components/context-menu
export interface SubOverlayOptions {
  /** The subTrigger value this level opens under. */
  value: string
  state: ReadSignal<ContextMenuState>
  parts: Pick<ContextMenuParts, 'subTrigger' | 'subPositioner' | 'subContent'>
  content: () => Renderable
  target?: string | HTMLElement
  positionerClass?: string
  align?: 'start' | 'end'
  offset?: number
  flip?: boolean
  shift?: boolean
}

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: ReadSignal<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': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | undefined>
  }
  input: {
    type: ReadSignal<'text' | 'password'>
    autocomplete: string
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    'data-scope': 'password-input'
    'data-part': 'input'
    onInput: (e: Event) => void
  }
  visibilityTrigger: {
    type: 'button'
    'aria-label': ReadSignal<string>
    'aria-pressed': ReadSignal<boolean>
    disabled: ReadSignal<boolean>
    tabindex: ReadSignal<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: ReadSignal<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': ReadSignal<StepStatus>
    'data-index': string
    'aria-current': ReadSignal<'step' | undefined>
  }
  trigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'steps'
    'data-part': 'trigger'
    'data-status': ReadSignal<StepStatus>
    onClick: (e: MouseEvent) => void
  }
  separator: {
    'data-scope': 'steps'
    'data-part': 'separator'
    'data-status': ReadSignal<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': ReadSignal<'' | undefined>
  }
  nextTrigger: {
    type: 'button'
    disabled: ReadSignal<boolean>
    'data-scope': 'steps'
    'data-part': 'next-trigger'
    onClick: (e: MouseEvent) => void
  }
  prevTrigger: {
    type: 'button'
    disabled: ReadSignal<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: ReadSignal<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': ReadSignal<TimeFormat>
    'data-disabled': ReadSignal<'' | undefined>
  }
  hoursInput: {
    type: 'number'
    role: 'spinbutton'
    'aria-label': string
    'aria-valuemin': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number>
    disabled: ReadSignal<boolean>
    value: ReadSignal<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': ReadSignal<number>
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    'data-scope': 'time-picker'
    'data-part': 'minutes-input'
    onInput: (e: Event) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  periodTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'time-picker'
    'data-part': 'period-trigger'
    'data-period': ReadSignal<'AM' | 'PM'>
    onClick: (e: MouseEvent) => void
    hidden: ReadSignal<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: ReadSignal<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
todayInTimeZone() from @llui/components/date-picker

The calendar date (YYYY-MM-DD) of an instant in an IANA time zone. "Today" is a property of a PLACE, not of an instant: at 23:30 UTC it is still the 14th in Los Angeles and already the 15th in Auckland. Pin the result as today to show the calendar in a zone other than the runtime's (#266).

function todayInTimeZone(timeZone: string, now: number = Date.now()): 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' }
  /** @humanOnly */
  | { type: 'moveFocusMonths'; months: number }
  /** @intent("Clear the current selection") */
  | { type: 'clear' }
  /** @humanOnly */
  | { type: 'setToday'; today: string | null }
  /** @intent("Replace the set of individually unavailable dates (YYYY-MM-DD)") */
  | { type: 'setUnavailable'; dates: string[] }

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
  /** Pin "today" (YYYY-MM-DD); omit or `null` to read the runtime clock. */
  today?: string | null
  /** Individually unavailable dates (YYYY-MM-DD). */
  unavailable?: string[]
}
DatePickerParts from @llui/components/date-picker
export interface DatePickerParts {
  root: {
    'data-scope': 'date-picker'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | 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': ReadSignal<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: ReadSignal<boolean>
    'data-scope': 'date-picker'
    'data-part': 'prev-month-trigger'
    onClick: (e: MouseEvent) => void
  }
  nextMonthTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<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
  /**
   * A pinned "today" (YYYY-MM-DD), or `null` to read the runtime clock. Pin it
   * for deterministic rendering (SSR + hydration, galleries, tests) or to show
   * today in a specific time zone — see `todayInTimeZone` (#266).
   */
  today: string | null
  /**
   * Individually unavailable dates (YYYY-MM-DD) — bookings, holidays. Disabled
   * like an out-of-bounds date, published separately as `data-unavailable`, and
   * a range may not be completed across one (#266).
   */
  unavailable: string[]
}
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
  /** True for a date listed in `unavailable` (also `isDisabled`). */
  isUnavailable: 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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    tabindex: ReadSignal<number>
    'data-scope': 'date-picker'
    'data-part': 'day-cell'
    /** The cell's identity — the one genuinely static attribute. */
    'data-date': string
    'data-in-month': ReadSignal<'' | undefined>
    'data-today': ReadSignal<'' | undefined>
    'data-selected': ReadSignal<'' | undefined>
    'data-focused': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | undefined>
    'data-range-start': ReadSignal<'' | undefined>
    'data-range-end': ReadSignal<'' | undefined>
    'data-in-range': ReadSignal<'' | undefined>
    /** An individually unavailable date (also disabled) — distinct from out-of-bounds. */
    'data-unavailable': ReadSignal<'' | 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

areaCanvasBinding() from @llui/components/color-picker

The component-owned repaint seam for the OKLCH area's <canvas> (finding E: a consumer writes ZERO registerBinding/isSignalHandle/currentDoc of its own). Renders a canvas element with id={canvasId} — carrying areaCanvas's spread props — and PLACE this function's result anywhere in the same view; it paints once on mount and again whenever the hue or maxChroma changes, via {@link paintOklchPlane}.

Mirrors icon.ts's nameBinding + onMount pair: registerBinding's commit can fire before the canvas element exists in the DOM (binding commits run before runMounts), so the FIRST paint happens from onMount (which hands back the real node), and only LATER hue/maxChroma changes repaint directly through the cached reference.

function areaCanvasBinding(state: ReadSignal<ColorPickerState>, canvasId: string): Renderable
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: ReadSignal<ColorPickerState>, send: Send<ColorPickerMsg>, opts: ConnectOptions = {}): ColorPickerParts
cssColorToPickerColor() from @llui/components/color-picker

Convert an already-parsed {@link CssColor} into the canonical store for a given active model, exactly the way setColor/setHex project a typed color string — but for a caller (gradient-picker) that already has a CssColor, never a string, and must not round-trip through one.

function cssColorToPickerColor(parsed: CssColor, model: ColorModel, maxChroma: number): { color: PickerColor; alpha: number }
eyeDropperSupportMount() from @llui/components/color-picker

Detect EyeDropper support ONCE, client-side, after mount, and dispatch the answer into state — the ONLY piece of environment detection a consumer wires up; place the returned Mountable anywhere in the view (it renders nothing) and eyeDropperTrigger's hidden/disabled/data-unsupported follow automatically. Never runs during a render (SSR or the client's first paint), which is what keeps eyeDropperSupported's init() default of false identical on both — a check made INLINE during render would make SSR and a supporting browser's client render disagree.

function eyeDropperSupportMount(send: Send<ColorPickerMsg>): Mountable
hsvToOklchPreserving() from @llui/components/color-picker

HSV -> OKLCH, keeping the HSV hue verbatim when the color is achromatic (S or V is 0) instead of the arbitrary hue an achromatic OKLab conversion would otherwise produce.

function hsvToOklchPreserving(hsv: Hsv): Oklch
init() from @llui/components/color-picker
function init(opts: ColorPickerInit = {}): ColorPickerState
isOutOfGamut() from @llui/components/color-picker

Whether the OKLCH color falls outside the sRGB gamut (always false in HSV mode — an HSV color is an sRGB parameterization by construction).

function isOutOfGamut(state: ColorPickerState): boolean
lcFromPoint() from @llui/components/color-picker

Map a pointer position over the 2D chroma/lightness area to OKLCH C/L. X axis is chroma (left 0 → right maxChroma); Y axis is lightness (top 1 → bottom 0), mirroring {@link colorFromPoint}'s value axis.

function lcFromPoint(rect: DOMRect, x: number, y: number, maxChroma: number = DEFAULT_MAX_CHROMA): { c: number; l: number }
oklchHueRampGradient() from @llui/components/color-picker

A full-circle OKLCH hue ramp at a fixed lightness/chroma, using CSS Color 4's in oklch longer hue interpolation method — the ONLY way to express a hue sweep as a plain gradient, since hue 0 and hue 360 are the SAME point in oklab/srgb (the default interpolation space), so a naive linear-gradient(oklch(L C 0), oklch(L C 360)) would render as a solid color instead of a rainbow.

function oklchHueRampGradient(l = 0.75, c = 0.15): string
oklchPlanePixels() from @llui/components/color-picker

Render one OKLCH hue's chroma/lightness plane as RGBA pixels — chroma increasing left to right (0..maxChroma), lightness increasing bottom to top (0..1), matching {@link lcFromPoint}'s axes. Out-of-gamut pixels are fully transparent (alpha 0) rather than gamut-mapped, so a consumer can paint them hatched/checkered underneath if it wants a visible "no color here" cue instead of a silently-wrong one.

PER-ROW BINARY SEARCH, not a full per-pixel gamut-mapping search: for a fixed lightness/hue, {@link inSrgbGamut} is true on a PREFIX of chroma values and false beyond it (moving out from the achromatic point along one ray only ever crosses the gamut boundary once — color.test.ts's "gamut boundary is monotone per row" test sweeps a grid of hues/lightnesses and fails loudly if that stops holding for some hue this file didn't anticipate). Finding the edge costs O(log width) inSrgbGamut calls instead of running the full chroma-reduction search for every pixel; pixels before the edge need only the CHEAP unmapped conversion (already proven in-gamut), and pixels at/after it are skipped entirely (transparent, r/g/b left at the buffer's zero-fill). Measured on a 256x256 plane (median of 15 runs, warmed up) against the OLD full-gamut-map-every-pixel approach: hue 145 (the review's own reproduction case) went from 45.1ms to 1.8ms (24.5x); hue 30 from 49.2ms to 1.3ms (37.9x); hue 265 from 49.2ms to 2.0ms (24.8x) — see the mutation table / final report for the exact reproduction script.

function oklchPlanePixels(hue: number, width: number, height: number, maxChroma: number = DEFAULT_MAX_CHROMA): Uint8ClampedArray<ArrayBuffer>
oklchToHsvPreserving() from @llui/components/color-picker

OKLCH -> HSV (gamut-mapped), keeping the OKLCH hue verbatim when the color is achromatic (chroma ~0).

function oklchToHsvPreserving(ok: Oklch): Hsv
openEyeDropper() from @llui/components/color-picker

Open the browser's native EyeDropper UI and resolve to the picked color as #rrggbb. Resolves null — never rejects — when the API is unsupported or the user cancels (AbortError, including a deliberate signal.abort()). Rejects only for a genuine unexpected failure.

function openEyeDropper(signal?: AbortSignal): Promise<string | null>
paintOklchPlane() from @llui/components/color-picker

Paint {@link oklchPlanePixels} onto a canvas sized to its current width/height, using the hue/maxChroma from state (not separate arguments — the ONE source of truth for both, matching how connect() itself reads them). A no-op on a zero-sized canvas or a context-less environment (jsdom without a 2D context polyfill).

function paintOklchPlane(canvas: HTMLCanvasElement, state: ColorPickerState): void
pickerColorToCss() from @llui/components/color-picker

toCss's rule (hex/hex8 for hsv, exact oklch() for oklch — see {@link toCss}'s own doc comment) for just a color + alpha, with none of ColorPickerState's other fields needed. The seam a caller with a bare PickerColor (gradient-picker's per-stop color, never wrapped in a full picker state) reaches for, instead of building a throwaway state shim just to call toCss.

function pickerColorToCss(color: PickerColor, alpha: number): string
pickerColorToCssColor() from @llui/components/color-picker

The inverse of {@link cssColorToPickerColor}: a canonical stored color plus alpha, as the shared {@link CssColor} model interpolateColor operates on. OKLCH stays OKLCH (exact, not gamut-mapped); HSV becomes srgb.

function pickerColorToCssColor(color: PickerColor, alpha: number): CssColor
sanitizeHsv() from @llui/components/color-picker

Exported for gradient-picker, which sanitizes a stop's color the same way init/setColor do when projecting a parsed CSS color onto HSV.

function sanitizeHsv(raw: Hsv): Hsv
sanitizeOklch() from @llui/components/color-picker

Exported for gradient-picker, same reason as {@link sanitizeHsv}.

function sanitizeOklch(raw: Oklch, maxChroma: number): Oklch
stateHsl() from @llui/components/color-picker

Project the current color onto HSL (via {@link stateHsv}).

function stateHsl(state: ColorPickerState): Hsl
stateHsv() from @llui/components/color-picker

Project the current color onto HSV (gamut-mapped if the active model is OKLCH and out of gamut).

function stateHsv(state: ColorPickerState): Hsv
stateOklch() from @llui/components/color-picker

Project the current color onto OKLCH (exact — not gamut-mapped — if the active model already IS OKLCH; derived from HSV otherwise, which is always in-gamut by construction).

function stateOklch(state: ColorPickerState): Oklch
supportsEyeDropper() from @llui/components/color-picker

Whether the EyeDropper API is available. SSR-safe (false when there is no window). Exported as a plain, synchronously-callable function for whatever else might want it, but connect()'s own eyeDropperTrigger does NOT call this directly — it reads state.eyeDropperSupported, which only {@link eyeDropperSupportMount} ever sets (see that function's doc comment for why: calling this INLINE during a render would make SSR and a supporting browser's client render disagree).

function supportsEyeDropper(): boolean
toCss() from @llui/components/color-picker

The current color as a CSS string. HSV model -> hex (#rrggbb/#rrggbbaa depending on alpha) — cheap, familiar, and what the hex input already shows. OKLCH model -> oklch(), EXACT rather than gamut-mapped, because an OKLCH color may legitimately sit outside sRGB and a hex round trip would silently clip it; the browser gamut-maps oklch() for display on its own terms, same as it would for a value typed directly into CSS.

function toCss(state: ColorPickerState): string
toHex() from @llui/components/color-picker

#rrggbb, gamut-mapped from OKLCH when the active model needs it.

function toHex(state: ColorPickerState): string
toHex8() from @llui/components/color-picker

#rrggbbaa.

function toHex8(state: ColorPickerState): string
update() from @llui/components/color-picker
function update(state: ColorPickerState, msg: ColorPickerMsg): [ColorPickerState, never[]]

Types

ColorModel from @llui/components/color-picker

Color picker — HSV and OKLCH color selection.

The canonical color lives in whichever model is ACTIVE (state.color.model) and is lossless within that model. Every message that edits the color goes through a PROJECTION into the model it naturally operates in (HSV, HSL, or OKLCH), applies the edit there, then converts the result into the active model — so setSaturation (an HSL concept) and setChroma (an OKLCH concept) both work regardless of which model is currently active, and setHue sets the hue of whichever model IS active (HSV hue and OKLCH hue are numerically different rulers for the same wheel).

HUE STABILITY ON ACHROMATIC COLORS is the reason HSV was chosen as the original canonical store (S or V at 0 loses no information about hue, unlike HSL's black/white axis) and it is preserved across the HSV<->OKLCH boundary too: {@link hsvToOklchPreserving}/{@link oklchToHsvPreserving} carry the stored hue straight across whenever the color is achromatic in the SOURCE model, rather than recomputing an arbitrary hue-of-zero-chroma. setModel and every cross-model projection route through these two.

export type ColorModel = 'hsv' | 'oklch'
ColorPickerMsg from @llui/components/color-picker
export type ColorPickerMsg =
  /** @intent("Switch the active color model, converting the stored color and keeping its hue stable if it is achromatic") */
  | { type: 'setModel'; model: ColorModel }
  /** @intent("Set the full HSL color at once") */
  | { type: 'setHsl'; hsl: Hsl }
  /** @intent("Set the hue of the ACTIVE model (0–360)") */
  | { type: 'setHue'; h: number }
  /** @intent("Set the HSL saturation channel (0–100)") */
  | { type: 'setSaturation'; s: number }
  /** @intent("Set the HSL lightness channel (0–100)") */
  | { type: 'setLightness'; l: number }
  /** @intent("Set the alpha channel (0–1)") */
  | { type: 'setAlpha'; alpha: number }
  /** @intent("Set the color from any CSS color string (hex, rgb(), hsl(), oklch(), oklab(), a named color)") */
  | { 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, an eyedropper pick, or any CSS color string") */
  | { type: 'setColor'; color: string }
  /** @intent("Set the full OKLCH color at once (L 0–1, C >= 0, H degrees)") */
  | { type: 'setOklch'; l: number; c: number; h: number }
  /** @intent("Set the OKLCH chroma channel") */
  | { type: 'setChroma'; c: number }
  /** @intent("Set the OKLCH lightness channel (0–1)") */
  | { type: 'setOklchLightness'; l: number }
  /** @intent("Set chroma and lightness (OKLCH, x/y of the 2D area) at once") */
  | { type: 'setLc'; c: number; l: number }
  /** @intent("Nudge chroma/lightness (OKLCH) by signed deltas — used by area arrow keys") */
  | { type: 'nudgeLc'; dc: number; dl: number }
  /** @humanOnly */
  | { type: 'setEyeDropperSupported'; supported: boolean }
  /** @humanOnly */
  | { type: 'eyeDropperFailed'; message: string }
PickerColor from @llui/components/color-picker

The canonical stored color union — HSV or OKLCH, tagged by model. Exported as a named type for gradient-picker, whose stops each store one of these (never a hex string) so a gray stop keeps its hue and an out-of-gamut OKLCH stop stays exact.

export type PickerColor = ColorPickerState['color']

Interfaces

ColorPickerInit from @llui/components/color-picker
export interface ColorPickerInit {
  /** Which model the canonical color is stored in. Default `'hsv'`. */
  model?: ColorModel
  /** Initial color as any CSS color string the shared parser accepts (hex,
   * `rgb()`, `hsl()`, `oklch()`, `oklab()`, a named color). Takes precedence
   * over `hsl`/`hsv`/`oklch` below, and over `alpha` for the color's own
   * alpha component (an explicit `alpha` option still wins). */
  color?: string
  /** Initial color as HSL (converted into the canonical store). */
  hsl?: Hsl
  /** Initial color as HSV. Takes precedence over `hsl`. */
  hsv?: Hsv
  /** Initial color as OKLCH. Takes precedence over `hsl`/`hsv`. */
  oklch?: Oklch
  alpha?: number
  disabled?: boolean
  /** Upper bound for OKLCH chroma. Must be finite and > 0 — anything else
   * (including omission) falls back to {@link DEFAULT_MAX_CHROMA}. */
  maxChroma?: number
}
ColorPickerParts from @llui/components/color-picker
export interface ColorPickerParts {
  root: {
    'data-scope': 'color-picker'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
    'data-model': ReadSignal<ColorModel>
    /** Bare boolean (package convention): present when the current OKLCH
     * color falls outside sRGB. Always absent in HSV mode (an HSV color is
     * an sRGB parameterization by construction). */
    'data-out-of-gamut': ReadSignal<'' | undefined>
  }
  /** Cycles the active model between `'hsv'` and `'oklch'`. */
  modelToggle: {
    type: 'button'
    'aria-label': ReadSignal<string>
    disabled: ReadSignal<boolean>
    'data-scope': 'color-picker'
    'data-part': 'model-toggle'
    'data-model': ReadSignal<ColorModel>
    onClick: (e: MouseEvent) => void
  }
  hueSlider: {
    type: 'range'
    min: 0
    max: 360
    step: 1
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<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: ReadSignal<boolean>
    value: ReadSignal<string>
    style: ReadSignal<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: ReadSignal<boolean>
    value: ReadSignal<string>
    style: ReadSignal<string>
    'data-scope': 'color-picker'
    'data-part': 'lightness-slider'
    onInput: (e: Event) => void
  }
  /** OKLCH chroma, `0..state.maxChroma`. */
  chromaSlider: {
    type: 'range'
    min: 0
    max: ReadSignal<number>
    step: ReadSignal<number>
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    style: ReadSignal<string>
    'data-scope': 'color-picker'
    'data-part': 'chroma-slider'
    onInput: (e: Event) => void
  }
  /** OKLCH perceptual lightness, `0..1`. */
  oklchLightnessSlider: {
    type: 'range'
    min: 0
    max: 1
    step: number
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    style: ReadSignal<string>
    'data-scope': 'color-picker'
    'data-part': 'oklch-lightness-slider'
    onInput: (e: Event) => void
  }
  hexInput: {
    type: 'text'
    autocomplete: 'off'
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<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: ReadSignal<string>
  }
  /** The 2D area track. The machine owns the pointer-drag lifecycle
   * (capture on down, released on up/cancel, primary button only, ignored
   * while disabled): it computes from `currentTarget.getBoundingClientRect()`
   * and dispatches `setSv` (HSV mode, via `colorFromPoint`) or `setLc`
   * (OKLCH mode, via `lcFromPoint`) — see `utils/pointer-drag.ts`. Keyboard
   * continues from wherever a drag left off because `pointerdown` focuses
   * `areaThumb`. */
  area: {
    'data-scope': 'color-picker'
    'data-part': 'area'
    'data-model': ReadSignal<ColorModel>
    // HSV-mode hue backdrop only; empty in OKLCH mode, where `areaCanvas`
    // paints the plane instead (the sRGB gamut boundary is not expressible
    // as a CSS gradient).
    style: ReadSignal<string>
    onPointerDown: (e: PointerEvent) => void
    onPointerMove: (e: PointerEvent) => void
    onPointerUp: (e: PointerEvent) => void
    onPointerCancel: (e: PointerEvent) => void
    onLostPointerCapture: (e: PointerEvent) => void
  }
  /** Decorative `<canvas>` seam for the OKLCH area — headless, so the
   * consumer owns creating and sizing the real `<canvas>` element (give it
   * an id and spread these props), but never its own repaint wiring: place
   * {@link areaCanvasBinding}`(state, thatSameId)` anywhere in the view and
   * it stays painted, on mount and on every hue/`maxChroma` change. Absent/
   * inert in HSV mode. */
  areaCanvas: {
    'data-scope': 'color-picker'
    'data-part': 'area-canvas'
    'aria-hidden': 'true'
  }
  /** The draggable thumb inside the 2D area. Keyboard-operable (arrows move
   * S/V or C/L depending on the active model; Shift = coarse) with
   * role="slider" and a 2D aria-valuetext. `aria-label` follows the ACTIVE
   * model ("Saturation / Value" in HSV mode, "Chroma / Lightness" in OKLCH),
   * not a fixed string — the axes it labels are literally different. */
  areaThumb: {
    role: 'slider'
    'aria-label': ReadSignal<string>
    'aria-valuemin': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number>
    'aria-valuetext': ReadSignal<string>
    'aria-disabled': ReadSignal<'true' | undefined>
    tabindex: ReadSignal<number>
    'data-scope': 'color-picker'
    'data-part': 'area-thumb'
    style: ReadSignal<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: ReadSignal<boolean>
    value: ReadSignal<string>
    style: ReadSignal<string>
    'data-scope': 'color-picker'
    'data-part': 'alpha-slider'
    onInput: (e: Event) => void
  }
  /** EyeDropper API trigger. Always PLACED, but `hidden`/`disabled`/
   * `data-unsupported` all derive from `state.eyeDropperSupported` — a
   * consumer writes no feature-detect code of its own; see
   * {@link eyeDropperSupportMount}. */
  eyeDropperTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    hidden: ReadSignal<boolean>
    /** Bare boolean (package convention). */
    'data-unsupported': ReadSignal<'' | undefined>
    'data-scope': 'color-picker'
    'data-part': 'eyedropper-trigger'
    onClick: (e: MouseEvent) => 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 {
  color:
    | { model: 'hsv'; h: number; s: number; v: number }
    | { model: 'oklch'; l: number; c: number; h: number }
  /** Alpha channel 0..1. */
  alpha: number
  disabled: boolean
  /** Upper bound for OKLCH chroma — the ONE source of truth for the reducer's
   * clamp (`setOklch`/`setChroma`/`setLc`/`nudgeLc`) AND for `connect()`'s
   * chroma slider/area rendering range, so the two can no longer disagree
   * the way a `ConnectOptions.maxChroma` rendering-only override could.
   * Always finite and > 0 (`init` validates it; there is no setter — it is
   * fixed for the component's lifetime, like `slider`'s `min`/`max`). */
  maxChroma: number
  /** Whether the browser's EyeDropper API is available. ALWAYS `false` at
   * `init()` — this is what keeps SSR and the client's first paint identical
   * (see the module doc on {@link eyeDropperSupportMount}, the ONLY thing
   * that ever flips it, from a real mount-time feature check). A consumer
   * never feature-detects on its own: placing that one Mountable is the
   * whole contract. */
  eyeDropperSupported: boolean
}
ConnectOptions from @llui/components/color-picker
export interface ConnectOptions {
  hueLabel?: string
  saturationLabel?: string
  lightnessLabel?: string
  chromaLabel?: string
  oklchLightnessLabel?: string
  hexLabel?: string
  eyeDropperLabel?: string
  /** aria-label for the 2D 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, or the OKLCH
   * equivalent scaled by `state.maxChroma`). Default 1. */
  step?: number
  /** Coarse keyboard step for the area thumb when Shift is held. Default 10. */
  coarseStep?: number
}
EyeDropperOpenOptions from @llui/components/color-picker
export interface EyeDropperOpenOptions {
  signal?: AbortSignal
}
EyeDropperResult from @llui/components/color-picker
export interface EyeDropperResult {
  sRGBHex: string
}
Hsl from @llui/components/color-picker

HSL color. h 0–360 degrees, s/l 0–100. Float — see the module doc.

export interface Hsl {
  h: number
  s: number
  l: number
}
Hsv from @llui/components/color-picker

HSV color. h 0–360 degrees, s/v 0–100. Float — see the module doc.

export interface Hsv {
  h: number
  s: number
  v: number
}
Oklch from @llui/components/color-picker

OKLCH. l 0–1, c >= 0 (unbounded — maxChroma in the color picker is a UI choice, not a property of the color space), h in degrees.

export interface Oklch {
  l: number
  c: number
  h: number
}
SwatchParts from @llui/components/color-picker
export interface SwatchParts {
  type: 'button'
  'aria-label': string
  'aria-pressed': ReadSignal<boolean>
  'data-scope': 'color-picker'
  'data-part': 'swatch'
  'data-value': string
  'data-state': ReadSignal<'selected' | undefined>
  style: string
  onClick: (e: MouseEvent) => void
}

Constants

colorPicker from @llui/components/color-picker
const colorPicker
DEFAULT_MAX_CHROMA from @llui/components/color-picker

0.37 covers the whole of sRGB and most of Display P3 while leaving the 2D area / chroma slider a sensible fixed range (an unbounded chroma axis has no natural "full scale" to show) — the default for {@link ColorPickerState.maxChroma} when init() gets no maxChroma option.

const DEFAULT_MAX_CHROMA

@llui/components/gradient-picker

Functions

colorAt() from @llui/components/gradient-picker

The color the gradient renders at position (0–100, one cycle — see the module doc comment), computed the same way the browser would: when repeating is on, position is first WRAPPED by the stop range's own period [first.position, last.position) — exactly how a real repeating-linear-gradient paints past its last stop — before anything else runs. An exact stop match then returns that stop's own color; otherwise the two bracketing stops are interpolated in state.interpolation. addStop stores EXACTLY this string (round-tripped through parseCssColor), so a new stop is indistinguishable from the gradient it was picked off.

function colorAt(state: GradientPickerState, position: number): string
connect() from @llui/components/gradient-picker
function connect(state: ReadSignal<GradientPickerState>, send: Send<GradientPickerMsg>, opts: ConnectOptions): GradientPickerParts
init() from @llui/components/gradient-picker
function init(opts: GradientPickerInit = {}): GradientPickerState
parseGradient() from @llui/components/gradient-picker

Parse a CSS <gradient> (linear-gradient()/radial-gradient()/ conic-gradient(), repeating- included) into this component's shape. Accepts: angles in deg/grad/rad/turn, to <side-or-corner> (stored as a keyword direction — see GradientDirection), at <position> (percentages + left/center/right/top/bottom), radial shape/size keywords, in <space> [<hue> hue] (omitted resolves to srgb when every stop is a legacy color, oklab otherwise — CSS Images 4), 0/1/2 stop positions (a 2-position stop expands to two stops) with missing positions filled per the CSS auto-positioning algorithm, and any color parseCssColor accepts.

Explicitly REJECTS (with a reason, never silently dropping/clamping data): length-based stop positions (only %), a stop position outside 0%-100%, an explicit radial size LENGTH (circle 40px — only the four keyword extents), a length-based at position, a color hint (a bare percentage in the stop list with no color), and any color with a none component (PickerColor cannot store one — see noneComponentName). Chroma beyond a live instance's maxChroma is NOT rejected here (this function has no live instance to compare against) — init/setGradient raise their own maxChroma to fit instead.

parseGradient(toCss(s)) reproduces s modulo stop ids — see the property test.

function parseGradient(css: string): ParseGradientResult
pickerStateOf() from @llui/components/gradient-picker

Build the ColorPickerState the embedded picker edits: the SELECTED stop's OWN color+alpha, verbatim — no HSV<->OKLCH projection. This is what keeps a picker edit from destroying an out-of-gamut OKLCH stop's other channels (review finding #1: oklch(0.7 0.3 150) + setAlpha 0.5 used to come back hsv #00c24780 because the OLD version re-projected onto a shared "active model" on every edit). update()'s 'picker' case is what writes the result back onto the stop; a stop's model changes ONLY via an explicit setModel picker message, which is color-picker's own reducer — nothing here decides it.

function pickerStateOf(state: GradientPickerState): ColorPickerState
toCss() from @llui/components/gradient-picker

Serialize to a real CSS <gradient> — always with an explicit in <space> (plus the hue method, only for a polar space and only when it is not the shorter default) so the string never depends on a browser's own default. Stop colors go through color-picker's own toCss rule ({@link formatStopColor}), so an out-of-gamut OKLCH stop stays exact instead of being silently gamut-mapped through a hex round trip.

function toCss(state: GradientPickerState): string
update() from @llui/components/gradient-picker
function update(state: GradientPickerState, msg: GradientPickerMsg): [GradientPickerState, never[]]

Types

GradientDirection from @llui/components/gradient-picker

A linear gradient's direction, exactly as CSS Images 4 lets it be spelled: an explicit angle, or to <side-or-corner>. Parsing a to right gradient and immediately serializing it back must reproduce to right, not a numerically-equal 90deg — only an EXPLICIT angle edit (the setAngle message, e.g. from angleInput or the angle-slider composition) converts a keyword direction into an angle. x/y are independently optional so a single-axis direction (to right, to top) round-trips without a fabricated opposite axis; both set means a corner.

export type GradientDirection =
  | { type: 'angle'; deg: number }
  | { type: 'to'; x?: 'left' | 'right'; y?: 'top' | 'bottom' }
GradientKind from @llui/components/gradient-picker

Gradient picker — CSS linear-gradient()/radial-gradient()/ conic-gradient() (and their repeating- forms) authoring: a horizontal stop ramp plus per-kind controls, with a full color-picker embedded for whichever stop is selected.

The embedded picker edits the SELECTED STOP'S OWN model

There is exactly one color store per stop (GradientStop.color, the same PickerColor union color-picker itself uses) and exactly one place that projects it into a live ColorPickerState: {@link pickerStateOf}. It hands the stop's color through UNCHANGED — no HSV<->OKLCH re-projection — so a picker message that only touches one channel (setAlpha, setHue, …) leaves every OTHER channel of an out-of-gamut OKLCH stop bit-identical. update()'s 'picker' case runs colorPicker.update on that DERIVED state and writes color/alpha straight back onto the SELECTED stop; a stop's MODEL only ever changes via an explicit picker: { msg: { type: 'setModel' } } }, which is itself just color-picker's own reducer producing a same-shape next.color in the new model — there is no separate "gradient-level model" to keep in sync. defaultModel is a DIFFERENT, narrower thing: which model a freshly-added stop with no other way to pick one starts in (see addStop/buildStopsFromOptions).

The stop-position domain is always one cycle, 0–100

repeating only changes the CSS repeating- prefix (how the SAME 0–100 ramp tiles across the painted box) — it never changes what a stop's position number means. colorAt/addStop always read position as a percentage of one cycle; colorAt WRAPS a query outside [first, last] by the stop range's own period when repeating is true (matching a real repeating-linear-gradient's paint), and clamps to the end stops otherwise. toCss's stop list is always the one un-repeated cycle, matching what {@link parseGradient} reads back.

export type GradientKind = 'linear' | 'radial' | 'conic'
GradientPickerMsg from @llui/components/gradient-picker

@intent group

export type GradientPickerMsg =
  /** @intent("Add a stop at `position` (0-100), colored EXACTLY what the gradient already renders there") */
  | { type: 'addStop'; position: number }
  /** @intent("Remove the stop with the given id. Refused below `minStops`; a removed selection reselects a neighbour") */
  | { type: 'removeStop'; id: string }
  /** @humanOnly */
  | { type: 'selectStop'; id: string }
  /** @intent("Move the stop with the given id to `position` (0-100, clamped), keeping sort order and selection") */
  | { type: 'moveStop'; id: string; position: number }
  /** @intent("Move the stop with the given id by a signed `delta` (0-100 units)") */
  | { type: 'nudgeStop'; id: string; delta: number }
  /** @intent("Switch the gradient kind (linear/radial/conic)") */
  | { type: 'setKind'; kind: GradientKind }
  /** @intent("Toggle the `repeating-` form of the current gradient kind") */
  | { type: 'setRepeating'; repeating: boolean }
  /** @intent("Set an EXPLICIT angle in degrees — linear's direction (converting away from a to-keyword direction) or conic's `from` angle. No-op for radial. Normalized to [0,360)") */
  | { type: 'setAngle'; angle: number }
  /** @intent("Set the center position (percent 0-100 each axis) — radial and conic only") */
  | { type: 'setCenter'; x: number; y: number }
  /** @intent("Set the radial gradient's shape (circle/ellipse)") */
  | { type: 'setShape'; shape: RadialShape }
  /** @intent("Set the radial gradient's extent keyword") */
  | { type: 'setSize'; size: RadialSize }
  /** @intent("Set the color-interpolation-method: the space, and (polar spaces only) the hue method") */
  | { type: 'setInterpolation'; space: InterpolationSpace; hue?: HueInterpolationMethod }
  /** @intent("Reverse the stop order (mirrors every stop's position about the middle)") */
  | { type: 'reverse' }
  /** @intent("Space every stop evenly across 0-100, preserving relative order") */
  | { type: 'distribute' }
  /** @humanOnly */
  | { type: 'setGradientDraft'; value: string }
  /** @intent("Parse `css` and, if valid AND within min/maxStops, replace the whole gradient. Invalid input (or a stop count outside min/maxStops) sets a describable error and leaves the gradient unchanged") */
  | { type: 'setGradient'; css: string }
  /** @intent("Edit the color/alpha of the SELECTED stop through the embedded color-picker's own message set") */
  | { type: 'picker'; msg: ColorPickerMsg }
  /** @humanOnly */
  | { type: 'selectNextStop' }
  /** @humanOnly */
  | { type: 'selectPrevStop' }
  /** @intent("Set the reading direction (ltr/rtl)") */
  | { type: 'setDir'; dir: 'ltr' | 'rtl' }
ParseGradientResult from @llui/components/gradient-picker
export type ParseGradientResult =
  | { ok: true; value: ParsedGradient }
  | { ok: false; reason: string }
RadialShape from @llui/components/gradient-picker
export type RadialShape = 'circle' | 'ellipse'
RadialSize from @llui/components/gradient-picker
export type RadialSize = 'closest-side' | 'closest-corner' | 'farthest-side' | 'farthest-corner'
ToggleItemPart from @llui/components/gradient-picker
export type ToggleItemPart = 'kind-toggle' | 'shape-option' | 'size-option'

Interfaces

ConnectOptions from @llui/components/gradient-picker
export interface ConnectOptions {
  id: string
  trackLabel?: string
  addStopLabel?: string
  removeStopLabel?: string
  kindLabels?: Partial<Record<GradientKind, string>>
  repeatingLabel?: string
  angleLabel?: string
  centerLabel?: string
  shapeLabels?: Partial<Record<RadialShape, string>>
  sizeLabels?: Partial<Record<RadialSize, string>>
  interpolationSpaceLabel?: string
  interpolationHueLabel?: string
  reverseLabel?: string
  distributeLabel?: string
  cssLabel?: string
  /** Fine keyboard step for a stop / the center thumb (percent units).
   * Default 1. */
  step?: number
  /** Coarse keyboard step when Shift is held. Default 10. */
  coarseStep?: number
  /** Forwarded to the embedded `color-picker`'s own `connect()` (labels,
   * area-thumb step/coarseStep). */
  pickerOptions?: ColorPickerConnectOptions
}
GradientCenter from @llui/components/gradient-picker
export interface GradientCenter {
  x: number
  y: number
}
GradientInterpolation from @llui/components/gradient-picker
export interface GradientInterpolation {
  space: InterpolationSpace
  hue: HueInterpolationMethod
}
GradientPickerInit from @llui/components/gradient-picker
export interface GradientPickerInit {
  /** Parse an initial gradient from a CSS string (via {@link parseGradient}).
   * Takes precedence over `stops`/`kind`/… below. Invalid CSS (or a stop
   * count outside `minStops`/`maxStops`) falls back to the `stops`/defaults
   * path below, same as `color-picker`'s `color` option falling back on an
   * unparsable string. */
  css?: string
  /** Explicit initial stops (ignored if `css` parses). An EMPTY array is
   * treated the same as omitting the option (never an empty gradient — see
   * finding #7); fewer than `minStops` are PADDED. Sorted by `position` on
   * init; ids are assigned sequentially in the SORTED order. */
  stops?: GradientStopInit[]
  kind?: GradientKind
  repeating?: boolean
  /** Sets the initial direction as an explicit angle (linear) or `from`
   * angle (conic). For a `to <side>` initial direction, use `css` instead. */
  angle?: number
  center?: Partial<GradientCenter>
  shape?: RadialShape
  size?: RadialSize
  interpolation?: Partial<GradientInterpolation>
  /** Which model a stop with no explicit `PickerColor`/parseable string
   * starts in. Default `'hsv'`. */
  defaultModel?: ColorModel
  maxChroma?: number
  minStops?: number
  maxStops?: number
  disabled?: boolean
  dir?: 'ltr' | 'rtl'
}
GradientPickerParts from @llui/components/gradient-picker
export interface GradientPickerParts {
  root: {
    'data-scope': 'gradient-picker'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
    'data-kind': ReadSignal<GradientKind>
    'data-repeating': ReadSignal<'' | undefined>
  }
  preview: {
    'data-scope': 'gradient-picker'
    'data-part': 'preview'
    'aria-hidden': 'true'
    style: ReadSignal<string>
  }
  /** The horizontal stop ramp: a plain `in <space>` linear ramp of the SAME
   * stop list `toCss` serializes (finding #3 — one shared builder, never two
   * strings that can drift), including the hue method and the
   * `repeating-`/rtl-mirrored angle so what the track PAINTS matches
   * `colorAt` at every position, regardless of `state.kind`. Pointerdown on
   * the bare track (not on an existing stop, which calls
   * `stopPropagation()`) adds a new stop at that position and starts
   * dragging it immediately. */
  track: {
    'data-scope': 'gradient-picker'
    'data-part': 'track'
    style: ReadSignal<string>
    onPointerDown: (e: PointerEvent) => void
    onPointerMove: (e: PointerEvent) => void
    onPointerUp: (e: PointerEvent) => void
    onPointerCancel: (e: PointerEvent) => void
    onLostPointerCapture: (e: PointerEvent) => void
  }
  stop: (id: string) => GradientStopParts
  addStopButton: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'gradient-picker'
    'data-part': 'add-stop-button'
    onClick: (e: MouseEvent) => void
  }
  removeStopButton: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'gradient-picker'
    'data-part': 'remove-stop-button'
    onClick: (e: MouseEvent) => void
  }
  kindToggle: (kind: GradientKind) => ToggleItemParts
  repeatingToggle: {
    type: 'button'
    'aria-label': string
    'aria-pressed': ReadSignal<boolean>
    disabled: ReadSignal<boolean>
    'data-scope': 'gradient-picker'
    'data-part': 'repeating-toggle'
    'data-state': ReadSignal<'on' | 'off'>
    onClick: (e: MouseEvent) => void
  }
  angleInput: {
    type: 'range'
    min: 0
    max: 360
    step: 1
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    'data-scope': 'gradient-picker'
    'data-part': 'angle-input'
    onInput: (e: Event) => void
  }
  centerArea: {
    'data-scope': 'gradient-picker'
    'data-part': 'center-area'
    onPointerDown: (e: PointerEvent) => void
    onPointerMove: (e: PointerEvent) => void
    onPointerUp: (e: PointerEvent) => void
    onPointerCancel: (e: PointerEvent) => void
    onLostPointerCapture: (e: PointerEvent) => void
  }
  centerThumb: {
    role: 'slider'
    'aria-label': string
    'aria-valuemin': 0
    'aria-valuemax': 100
    'aria-valuenow': ReadSignal<number>
    'aria-valuetext': ReadSignal<string>
    'aria-disabled': ReadSignal<'true' | undefined>
    tabindex: ReadSignal<number>
    'data-scope': 'gradient-picker'
    'data-part': 'center-thumb'
    style: ReadSignal<string>
    onKeyDown: (e: KeyboardEvent) => void
  }
  shapeOption: (shape: RadialShape) => ToggleItemParts
  sizeOption: (size: RadialSize) => ToggleItemParts
  interpolationSpaceSelect: {
    'aria-label': string
    disabled: ReadSignal<boolean>
    value: ReadSignal<InterpolationSpace>
    'data-scope': 'gradient-picker'
    'data-part': 'interpolation-space-select'
    onInput: (e: Event) => void
  }
  interpolationHueSelect: {
    'aria-label': string
    /** Disabled when the whole picker is disabled, OR the current space has
     * no hue (only `hsl`/`oklch` do). */
    disabled: ReadSignal<boolean>
    value: ReadSignal<HueInterpolationMethod>
    'data-scope': 'gradient-picker'
    'data-part': 'interpolation-hue-select'
    onInput: (e: Event) => void
  }
  reverseButton: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'gradient-picker'
    'data-part': 'reverse-button'
    onClick: (e: MouseEvent) => void
  }
  distributeButton: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'gradient-picker'
    'data-part': 'distribute-button'
    onClick: (e: MouseEvent) => void
  }
  /** The draft, NOT the committed value, while editing — see
   * `GradientPickerState.cssDraft`. Commits on `change`/Enter, never on
   * every keystroke, so the caret never jumps mid-edit; `aria-invalid` +
   * `aria-describedby` (pointing at `cssError`'s `id`) publish validity. */
  cssInput: {
    type: 'text'
    autocomplete: 'off'
    spellcheck: 'false'
    'aria-label': string
    'aria-invalid': ReadSignal<'true' | undefined>
    'aria-describedby': ReadSignal<string | undefined>
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    'data-scope': 'gradient-picker'
    'data-part': 'css-input'
    onInput: (e: Event) => void
    onChange: (e: Event) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  /** The describable error region `cssInput`'s `aria-describedby` points at
   * — empty/hidden text when there is no error, matching `form-field`'s
   * `errorText` convention (attributes + a sibling `visible`/`message`
   * rather than folding non-attribute content into the same bag). */
  cssError: {
    id: string
    role: 'alert'
    'data-scope': 'gradient-picker'
    'data-part': 'css-error'
    visible: ReadSignal<boolean>
    message: ReadSignal<string>
  }
  /**
   * The embedded `color-picker`'s FULL part bag, connected over the DERIVED
   * `pickerStateOf(state)` — eyedropper, OKLCH canvas, model toggle, every
   * slider, all included with zero glue. Messages are wrapped as
   * `{ type: 'picker'; msg }` via `wrapChildSend` (which also tags the
   * dispatcher `__lluiVariants: ['picker']`, so every handler `color-picker`
   * builds from it reports the truthful PARENT-visible type) and unwrapped
   * by this component's `update()` — see the module doc comment.
   */
  picker: ColorPickerParts
}
GradientPickerState from @llui/components/gradient-picker
export interface GradientPickerState {
  kind: GradientKind
  repeating: boolean
  /** Linear only. Ignored (but still stored — the machine always publishes
   * both per-kind clusters; the view branches on `kind`) for radial/conic. */
  direction: GradientDirection
  /** Degrees, conic's `from` angle only. Conic has no `to <side>` form in
   * CSS at all, so — unlike linear — it never needs the `GradientDirection`
   * union; it is always a plain angle. */
  conicAngle: number
  /** Percent 0–100. Radial + conic only. */
  center: GradientCenter
  /** Radial only. */
  shape: RadialShape
  /** Radial only. */
  size: RadialSize
  interpolation: GradientInterpolation
  /** Kept sorted by `position` ascending; ties keep insertion order. */
  stops: GradientStop[]
  selectedId: string
  /** The next deterministic stop id counter (`` `s${nextId}` ``) — the reducer
   * stays pure, so ids can never come from `Math.random()`/`crypto`. MONOTONE
   * across a `setGradient`: a successful replace continues the counter
   * rather than restarting at `s1`, so an id a consumer captured earlier
   * (a DOM id, a test assertion, an undo/redo log) is never reissued to a
   * DIFFERENT stop. */
  nextId: number
  /** Which model a freshly-added stop starts in when nothing else determines
   * one (`buildStopsFromOptions`'s fallback color). Distinct from any
   * EXISTING stop's own stored model, which `picker`/`setModel` edits
   * in-place — see the module doc comment. */
  defaultModel: ColorModel
  /** Upper bound for OKLCH chroma — shared with the embedded picker. NEVER
   * clamps an author-specified chroma down (CLAUDE.md's "never clamp/drop"
   * rule, review finding #2c): `init`/`setGradient` RAISE this to fit
   * whatever chroma the input actually contains instead. */
  maxChroma: number
  /** `removeStop` refuses to go below this many stops; `init` PADS up to it
   * if given fewer; `setGradient` REJECTS a parse that would drop below it
   * (never truncates/pads a user-authored CSS gradient — see the module doc
   * on `setGradient`). Default 2. Always a positive integer. */
  minStops: number
  /**
   * Upper bound on stop count, or ABSENT for unbounded. Follows the
   * package's UNBOUNDED-CAPABLE idiom (CLAUDE.md #177, `breadcrumbs.
   * maxVisible`): the key is OMITTED rather than holding `null`, so a
   * `JSON.parse(JSON.stringify(state))` round trip is a key-for-key
   * identity. Always a positive integer `>= minStops` when present — `init`
   * normalizes a non-integer or too-low value rather than producing a
   * config that could never be satisfied.
   */
  maxStops?: number
  disabled: boolean
  /** Reading direction. Under `rtl`, the track's physical left/right (both
   * the stop ramp's own background angle and each stop's `left%`) mirrors —
   * see `mirrorForDir`'s doc comment on why this differs from
   * `color-picker`'s (native-input) hue slider. */
  dir: 'ltr' | 'rtl'
  /** Whether the browser's EyeDropper API is available — mirrors
   * `color-picker`'s own `eyeDropperSupported` field (never `true` at
   * `init()`; only `eyeDropperSupportMount`, wired through the `picker`
   * wrapper in `connect()`, ever flips it). Lives at the GRADIENT level, not
   * per-stop — it is a mount-time browser capability, not a color. */
  eyeDropperSupported: boolean
  /** The uncommitted `cssInput` text while the user is actively editing, or
   * `null` when not editing (display falls back to the live `toCss(state)`
   * value). Kept OUT of the committed gradient so the input's own caret
   * never jumps mid-keystroke — see `setGradientDraft`/`setGradient`. */
  cssDraft: string | null
  /** The reason the last `setGradient` attempt failed, or `null`. Cleared by
   * the NEXT successful `setGradient`; typing (`setGradientDraft`) does NOT
   * clear it, so the error stays visible while the user is mid-correction. */
  cssError: string | null
}
GradientStop from @llui/components/gradient-picker
export interface GradientStop {
  id: string
  color: PickerColor
  alpha: number
  /** 0–100, one cycle — see the module doc comment. */
  position: number
}
GradientStopInit from @llui/components/gradient-picker
export interface GradientStopInit {
  color?: PickerColor | string
  alpha?: number
  position: number
}
GradientStopParts from @llui/components/gradient-picker
export interface GradientStopParts {
  id: string
  role: 'slider'
  'aria-label': string
  'aria-valuemin': 0
  'aria-valuemax': 100
  'aria-valuenow': ReadSignal<number>
  'aria-valuetext': ReadSignal<string>
  'aria-disabled': ReadSignal<'true' | undefined>
  /** Every stop is independently tabbable (never -1 while enabled) — the
   * package's existing multi-thumb precedent (`slider.ts`'s `SliderThumbParts`)
   * rather than a roving single-tab-stop composite: APG's roving-tabindex
   * pattern is for ONE composite control with ONE conceptual focus (a
   * toolbar, a listbox); a multi-stop gradient ramp is closer to several
   * independent sliders, which is exactly the case `slider.ts` already
   * chose this way for. Tab therefore visits stops in DOM order, which
   * tracks position order since `each()` renders the reducer's own
   * position-sorted `stops` array. */
  tabindex: ReadSignal<number>
  'data-scope': 'gradient-picker'
  'data-part': 'stop'
  'data-value': string
  'data-selected': ReadSignal<'' | undefined>
  style: ReadSignal<string>
  onPointerDown: (e: PointerEvent) => void
  onPointerMove: (e: PointerEvent) => void
  onPointerUp: (e: PointerEvent) => void
  onPointerCancel: (e: PointerEvent) => void
  onLostPointerCapture: (e: PointerEvent) => void
  onFocus: (e: FocusEvent) => void
  onKeyDown: (e: KeyboardEvent) => void
}
ParsedGradient from @llui/components/gradient-picker
export interface ParsedGradient {
  kind: GradientKind
  repeating: boolean
  direction: GradientDirection
  conicAngle: number
  center: GradientCenter
  shape: RadialShape
  size: RadialSize
  interpolation: GradientInterpolation
  stops: Array<{ color: PickerColor; alpha: number; position: number }>
}
ToggleItemParts from @llui/components/gradient-picker
export interface ToggleItemParts {
  type: 'button'
  'aria-label': string
  'aria-pressed': ReadSignal<boolean>
  'data-scope': 'gradient-picker'
  'data-part': ToggleItemPart
  'data-value': string
  'data-state': ReadSignal<'on' | 'off'>
  disabled: ReadSignal<boolean>
  onClick: (e: MouseEvent) => void
}

Constants

gradientPicker from @llui/components/gradient-picker
const gradientPicker

@llui/components/timer

Functions

connect() from @llui/components/timer
function connect(state: ReadSignal<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': ReadSignal<'' | undefined>
    'data-direction': ReadSignal<Direction>
    /** Present once a countdown has reached its target (see `isComplete`). */
    'data-complete': ReadSignal<'' | undefined>
  }
  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: ReadSignal<boolean>
    onClick: (e: MouseEvent) => void
  }
  pauseTrigger: {
    type: 'button'
    'aria-label': string
    'data-scope': 'timer'
    'data-part': 'pause-trigger'
    disabled: ReadSignal<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: ReadSignal<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': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number>
    'aria-valuetext': ReadSignal<string>
    'aria-orientation': 'horizontal'
    'aria-disabled': ReadSignal<'true' | undefined>
    'aria-readonly': ReadSignal<'true' | undefined>
    tabindex: ReadSignal<number>
    'data-scope': 'angle-slider'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | 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': ReadSignal<string>
  }
  valueText: {
    'data-scope': 'angle-slider'
    'data-part': 'value-text'
  }
  /** A hidden input for form participation. */
  hiddenInput: {
    type: 'hidden'
    value: ReadSignal<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: ReadSignal<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': ReadSignal<'' | undefined>
    'data-direction': ReadSignal<MarqueeDirection>
    'data-axis': ReadSignal<'horizontal' | 'vertical'>
    'data-disabled': ReadSignal<'' | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    style: ReadSignal<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: ReadSignal<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': ReadSignal<PresenceStatus>
    hidden: ReadSignal<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

canUndo() from @llui/components/signature-pad

True when undo would change something: a stroke to remove or a clear to restore.

function canUndo(state: SignaturePadState): boolean
connect() from @llui/components/signature-pad
function connect(state: ReadSignal<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': ReadSignal<'' | undefined>
    'data-readonly': ReadSignal<'' | undefined>
    'data-drawing': ReadSignal<'' | undefined>
    /** Present while nothing has been drawn — the placeholder hook. */
    'data-empty': ReadSignal<'' | undefined>
  }
  control: {
    'data-scope': 'signature-pad'
    'data-part': 'control'
  }
  clearTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'signature-pad'
    'data-part': 'clear-trigger'
    onClick: (e: MouseEvent) => void
  }
  undoTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<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: ReadSignal<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
  /**
   * The strokes the last `clear` removed, until the next edit — what makes a
   * destructive clear undoable (`undo` right after `clear` restores them).
   * `null` when there is nothing to restore (#266).
   */
  cleared: Stroke[] | null
}

Constants

signaturePad from @llui/components/signature-pad
const signaturePad

@llui/components/toc

Functions

connect() from @llui/components/toc
function connect(state: ReadSignal<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': ReadSignal<'' | undefined>
    'data-value': string
  }
  link: {
    href: string
    'aria-current': ReadSignal<'location' | undefined>
    'data-scope': 'toc'
    'data-part': 'link'
    'data-active': ReadSignal<'' | undefined>
  }
  expandTrigger: {
    type: 'button'
    'aria-expanded': ReadSignal<boolean>
    'aria-label': string
    'data-scope': 'toc'
    'data-part': 'expand-trigger'
    'data-state': ReadSignal<'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: ReadSignal<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
  /**
   * Whether the consumer renders the `description` part (default: true — a
   * `TourStep` always carries one). When false, the root omits
   * `aria-describedby`, which would otherwise name an element that does not
   * exist (the `dialog` `hasDescription` rule, #268). The title names the
   * dialog and stays mandatory.
   */
  hasDescription?: 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
    /** The description part's id, or absent when `hasDescription: false`. */
    'aria-describedby': string | undefined
    'data-scope': 'tour'
    'data-part': 'root'
    hidden: ReadSignal<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: ReadSignal<boolean>
    'data-scope': 'tour'
    'data-part': 'prev-trigger'
    onClick: (e: MouseEvent) => void
  }
  nextTrigger: {
    type: 'button'
    'data-scope': 'tour'
    'data-part': 'next-trigger'
    'data-last': ReadSignal<'' | 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: ReadSignal<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
  /**
   * Base id. With it, the error text gets `<id>:error` and the input's
   * `aria-describedby` names it while the value is invalid; without it the
   * error is announced once (`role="alert"`) but not associated with the field.
   */
  id?: 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': ReadSignal<'' | undefined>
    'data-invalid': ReadSignal<'' | undefined>
  }
  input: {
    type: 'text'
    inputmode: 'numeric'
    autocomplete: 'off'
    spellcheck: false
    value: ReadSignal<string>
    disabled: ReadSignal<boolean>
    readonly: ReadSignal<boolean>
    required: ReadSignal<boolean>
    'aria-invalid': ReadSignal<'true' | undefined>
    /**
     * The error text's id while the value is invalid — so the field names its
     * own error rather than relying on the one-shot `role="alert"`
     * announcement (#268). Absent without a `ConnectOptions.id`.
     */
    'aria-describedby': ReadSignal<string | 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: ReadSignal<boolean>
    'data-scope': 'date-input'
    'data-part': 'clear-trigger'
    onClick: (e: MouseEvent) => void
  }
  errorText: {
    /** `<id>:error` when `ConnectOptions.id` is given. */
    id: string | undefined
    role: 'alert'
    'aria-live': 'polite'
    'data-scope': 'date-input'
    'data-part': 'error-text'
    hidden: ReadSignal<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: ReadSignal<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': ReadSignal<AsyncStatus>
    /** `'true'` while a page request is in flight — the list's content is changing. */
    'aria-busy': ReadSignal<'true' | 'false'>
    /** Present once a load has SETTLED with zero items: the empty-state hook. */
    'data-empty': ReadSignal<'' | undefined>
    /** Present when the source reports no further pages. */
    'data-exhausted': ReadSignal<'' | undefined>
  }
  sentinel: {
    'data-scope': 'async-list'
    'data-part': 'sentinel'
    'aria-hidden': 'true'
  }
  loadMoreTrigger: {
    type: 'button'
    disabled: ReadSignal<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: ReadSignal<boolean>
    onClick: (e: MouseEvent) => void
  }
  errorText: {
    role: 'alert'
    'aria-live': 'polite'
    'data-scope': 'async-list'
    'data-part': 'error-text'
    hidden: ReadSignal<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: ReadSignal<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: ReadSignal<boolean>
    value: ReadSignal<string>
    'data-scope': 'cascade-select'
    'data-part': 'level-select'
    'data-level': string
    'data-ready': ReadSignal<'' | 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': ReadSignal<'' | undefined>
    'data-complete': ReadSignal<'' | undefined>
  }
  clearTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<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: ReadSignal<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': ReadSignal<'' | undefined>
    'data-hovered': ReadSignal<'' | 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': ReadSignal<'' | undefined>
  }
  scrollbarY: {
    'data-scope': 'scroll-area'
    'data-part': 'scrollbar'
    'data-axis': 'y'
    'data-visible': ReadSignal<'' | undefined>
  }
  thumbX: {
    'data-scope': 'scroll-area'
    'data-part': 'thumb'
    'data-axis': 'x'
    style: ReadSignal<string>
  }
  thumbY: {
    'data-scope': 'scroll-area'
    'data-part': 'thumb'
    'data-axis': 'y'
    style: ReadSignal<string>
  }
  corner: {
    'data-scope': 'scroll-area'
    'data-part': 'corner'
    'data-visible': ReadSignal<'' | 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: ReadSignal<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 }
  /** @intent("Move the panel by a pixel offset (x right, y down)") */
  | { type: 'moveBy'; dx: number; dy: number }
  /** @intent("Resize the panel from one edge or corner by a pixel offset (clamped to min/max)") */
  | { type: 'resizeBy'; handle: ResizeHandle; dx: number; dy: 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
  moveLabel?: string
  resizeLabel?: 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': ReadSignal<'' | undefined>
    'data-resizing': ReadSignal<'' | undefined>
    'data-minimized': ReadSignal<'' | undefined>
    'data-maximized': ReadSignal<'' | undefined>
    hidden: ReadSignal<boolean>
    style: ReadSignal<string>
  }
  /**
   * Pointer drag starts here, and it is also a keyboard stop (#266): arrows
   * move the panel 10px (50px with Shift). Physical under RTL — the panel is
   * positioned with physical `left`/`top`.
   */
  dragHandle: {
    /**
     * A focusable, NAMED group: without a role an `aria-label` is prohibited
     * on a generic element and assistive tech announced nothing (#268 audit).
     * `group`, not `button`: the handle is the title bar and CONTAINS the
     * minimize/maximize/close buttons (a button may not), and its action is
     * the arrow keys `aria-keyshortcuts` names, not an activation.
     */
    role: 'group'
    tabindex: 0
    'aria-label': string
    'aria-keyshortcuts': string
    'data-scope': 'floating-panel'
    'data-part': 'drag-handle'
    onPointerDown: (e: PointerEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  content: {
    'data-scope': 'floating-panel'
    'data-part': 'content'
    hidden: ReadSignal<boolean>
  }
  minimizeTrigger: {
    type: 'button'
    'aria-label': string
    /** A toggle: `'true'` while minimized. */
    'aria-pressed': ReadSignal<'true' | 'false'>
    'data-scope': 'floating-panel'
    'data-part': 'minimize-trigger'
    onClick: (e: MouseEvent) => void
  }
  maximizeTrigger: {
    type: 'button'
    'aria-label': string
    /** A toggle: `'true'` while maximized. */
    'aria-pressed': ReadSignal<'true' | 'false'>
    '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
  }
  /** A resize grip; also a keyboard stop whose arrows resize from this grip (#266). */
  resizeHandle: (handle: ResizeHandle) => {
    /** A focusable, named group (see `dragHandle`): its action is the arrow
     *  keys, and a thin edge grip is a window-chrome affordance, not a
     *  pointer button. */
    role: 'group'
    tabindex: 0
    'aria-label': string
    'aria-keyshortcuts': string
    'data-scope': 'floating-panel'
    'data-part': 'resize-handle'
    'data-handle': ResizeHandle
    onPointerDown: (e: PointerEvent) => void
    onKeyDown: (e: KeyboardEvent) => 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: ReadSignal<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' }
  /** @intent("Move the crop area by a percentage of the image size (x right, y down)") */
  | { type: 'nudge'; x: number; y: number }
  /** @intent("Zoom the crop area about its centre: factor > 1 zooms in, < 1 zooms out") */
  | { type: 'zoom'; factor: number }
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
  /** Name of the crop area; its live geometry is appended. */
  cropAreaLabel?: 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': ReadSignal<'' | undefined>
    'data-resizing': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | undefined>
  }
  image: {
    'data-scope': 'image-cropper'
    'data-part': 'image'
    onLoad: (e: Event) => void
    draggable: false
  }
  /**
   * The crop area: a focusable, named `group` (#266). Arrow keys move it 1% of
   * the image (10% with Shift), `+`/`-` zoom it. Keys stay PHYSICAL under
   * `dir="rtl"`: the image is never mirrored, so neither is the box on it.
   * Geometry is written as an inline `style` in PERCENT of the image — a skin
   * must not set position or size.
   */
  cropBox: {
    role: 'group'
    tabindex: 0
    'aria-label': ReadSignal<string>
    'aria-keyshortcuts': string
    'data-scope': 'image-cropper'
    'data-part': 'crop-box'
    style: ReadSignal<string>
    onPointerDown: (e: PointerEvent) => void
    onKeyDown: (e: KeyboardEvent) => 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: ReadSignal<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[]]
watchNavMenuIndicator() from @llui/components/navigation-menu

Track the open top-level trigger and write CSS custom properties onto the indicator part so the arrow can be animated into position. Call from onMount with the navigation-menu ROOT element; the returned function removes the observers.

PASS THE NAV, NOT WHATEVER onMount HANDED YOU. onMount receives the BUILD's root container — the component's mount container, or an arm's or a row's — not the element the call sits inside. Forwarding that resolves the lookups below in document order across everything in the build, so a page with two navigation menus has both of them tracking the first one's arrow. That is #123's shape exactly, and it is invisible until there are two. Scope it yourself: give the nav an id and root.querySelector('#…').

Sets --indicator-left, --indicator-top, --indicator-width and --indicator-height on the indicator element every time a trigger's data-state flips or the root resizes. Style the indicator with: left: 0; width: var(--indicator-width); transform: translateX(var(--indicator-left));

This is the same state/layout split tabs uses (watchTabIndicator), and for the same reason: NavMenuState is JSON-serializable and holds no pixels, so a measured position cannot live in it without breaking time-travel, @llui/test replay and SSR. The machine answers data-state; the DOM answers where.

TWO deliberate differences from watchTabIndicator, both forced by this component's DOM:

  1. It resolves the position against the INDICATOR'S OWN offsetParent rather than reading trigger.offsetLeft. A navigation menu wraps each trigger in a positioned li (upstream's own markup does — the inline panel is absolutely positioned against it), which makes the trigger its own offset parent and offsetLeft 0 for every item. tabs can use offsetLeft because its trigger list is flat.
  2. It takes the FIRST open trigger in document order, not the only one. Nested branches publish data-state="open" too, and a child trigger lives inside its parent's panel — so document order puts the outermost open branch first, which is exactly the one the arrow tracks.
function watchNavMenuIndicator(root: HTMLElement): () => void

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' }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; 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': ReadSignal<boolean | undefined>
    'data-scope': 'navigation-menu'
    'data-part': 'trigger'
    'data-state': ReadSignal<'open' | 'closed'>
    'data-value': string
    tabindex: ReadSignal<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': ReadSignal<'open' | 'closed'>
    hidden: ReadSignal<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: {
    // `id` is REQUIRED — it is the scope `directionSyncMount` (below) looks
    // the live root up by, the same contract `tabs`/`carousel`/`pagination`'s
    // own `root.id` already honours. A consumer that overrides it with a
    // DIFFERENT id breaks the direction sync silently (#265 finding 6): the
    // watcher would observe nothing, since `getElementByIdInScope` would
    // never find this element under the id it was given.
    id: string
    // 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': ReadSignal<'' | undefined>
    onPointerLeave: (e: PointerEvent) => void
    onPointerEnter: (e: PointerEvent) => void
  }
  /**
   * The arrow that tracks the open top-level trigger — Radix's
   * `NavigationMenuIndicator`, whose `data-state="visible" | "hidden"` drives
   * the enter/exit animation. Render it as a sibling of the trigger list,
   * inside the positioned root.
   *
   * It stays MOUNTED in both states: the recipe animates it out with
   * `data-[state=hidden]:animate-out fade-out`, and a `show(...)` would unmount
   * the node the exit animation is running on. Same reason `formField`'s live
   * region is hidden rather than gated.
   *
   * `data-state` is all the MACHINE can answer — where the arrow sits is a
   * layout fact, not state (`NavMenuState` is JSON-serializable and holds no
   * pixels), so the position arrives through `watchNavMenuIndicator`, which
   * writes `--indicator-left`/`--indicator-width` onto this element. That is
   * the same split `tabs` uses for its own indicator.
   */
  indicator: {
    // Pure decoration: it duplicates no information a screen reader cannot get
    // from the trigger's own `aria-expanded`, and it is always in the tree.
    'aria-hidden': 'true'
    'data-scope': 'navigation-menu'
    'data-part': 'indicator'
    'data-state': ReadSignal<'visible' | 'hidden'>
  }
  /** Place once anywhere in the same build to keep automatic direction live —
   * the shared `@llui/interactions` direction-sync seam (#265 finding 6),
   * same as `tabs`/`carousel`/`pagination`'s own `directionSync` part. A
   * discarded `Mountable` is inert, so this must be placed in the view. */
  directionSync: Mountable
  /**
   * 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.
   * Routed through the shared `@llui/interactions` direction-sync seam
   * (`../utils/direction.js`) rather than a second resolver — `dirSource`
   * tracks whether `dir` came from explicit config/`setDir` or from the
   * mounted root's live ancestor `dir` attribute (#265 finding 6). */
  dir: 'ltr' | 'rtl'
  dirSource: DirectionSource
}

Constants

const navigationMenu

@llui/components/qr-code

Functions

connect() from @llui/components/qr-code
function connect(state: ReadSignal<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 {
  /**
   * A labelled `group` around the code and its download action. `aria-label` on
   * a bare `<div>` (role `generic`) is prohibited ARIA and ignored by most
   * assistive technology, which is why the role is stated.
   */
  root: {
    role: 'group'
    'data-scope': 'qr-code'
    'data-part': 'root'
    'aria-label': string
    /** Present while no matrix has been supplied — the empty state hook. */
    'data-empty': ReadSignal<'' | undefined>
  }
  /**
   * The `role="img"` graphic carries its OWN accessible name. A screen-reader
   * user cannot scan the modules, so the name includes the encoded value.
   */
  svg: {
    'data-scope': 'qr-code'
    'data-part': 'svg'
    role: 'img'
    'aria-label': ReadSignal<string>
    viewBox: ReadSignal<string>
    'shape-rendering': 'crispEdges'
  }
  /** Spread onto a `<rect>`: sized to the module grid so it covers the quiet background. */
  background: {
    'data-scope': 'qr-code'
    'data-part': 'background'
    width: ReadSignal<string>
    height: ReadSignal<string>
  }
  /** Spread onto a `<path>`: one sub-path per dark module. */
  foreground: {
    'data-scope': 'qr-code'
    'data-part': 'foreground'
    d: ReadSignal<string>
  }
  downloadTrigger: {
    type: 'button'
    'aria-label': string
    'data-scope': 'qr-code'
    'data-part': 'download-trigger'
    /** Nothing to download until a matrix exists. */
    disabled: ReadSignal<boolean>
    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: ReadSignal<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' }
  /** @humanOnly */
  | { type: 'setHovered'; hovered: boolean }
  /** @humanOnly */
  | { type: 'setFocusWithin'; focusWithin: boolean }
  /** @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' }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; 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: {
    id: string
    role: 'region'
    'aria-roledescription': 'carousel'
    'aria-label': string
    'data-scope': 'carousel'
    'data-part': 'root'
    'data-paused': ReadSignal<'' | undefined>
    onPointerEnter: (e: PointerEvent) => void
    onPointerLeave: (e: PointerEvent) => void
    onFocusIn: (e: FocusEvent) => void
    onFocusOut: (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': ReadSignal<'' | undefined>
    /** Live track offset (px) to follow the finger: `translateX(var)`. */
    'data-drag-offset': ReadSignal<string | undefined>
    /** Physical pointer delta consumed by either skin's track transform. */
    'style.--carousel-drag-offset': ReadSignal<string | undefined>
    onPointerDown: (e: PointerEvent) => void
    onPointerMove: (e: PointerEvent) => void
    onPointerUp: (e: PointerEvent) => void
    onPointerCancel: (e: PointerEvent) => void
  }
  /** Place all slides directly inside this transform-bearing track. */
  track: {
    'data-scope': 'carousel'
    'data-part': 'track'
  }
  indicatorGroup: {
    role: 'tablist'
    'aria-label': string
    'data-scope': 'carousel'
    'data-part': 'indicator-group'
  }
  nextTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'carousel'
    'data-part': 'next-trigger'
    onClick: (e: MouseEvent) => void
  }
  prevTrigger: {
    type: 'button'
    'aria-label': string
    disabled: ReadSignal<boolean>
    'data-scope': 'carousel'
    'data-part': 'prev-trigger'
    onClick: (e: MouseEvent) => void
  }
  slide: (index: number) => CarouselSlideParts
  /** Place once anywhere in the same build to keep automatic direction live. */
  directionSync: Mountable
}
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': ReadSignal<'' | undefined>
    hidden: ReadSignal<boolean>
  }
  indicator: {
    type: 'button'
    role: 'tab'
    /** APG roving tab stop: only the selected indicator participates in Tab. */
    tabindex: ReadSignal<0 | -1>
    'aria-label': string
    'aria-selected': ReadSignal<boolean>
    'aria-controls': string
    'data-scope': 'carousel'
    'data-part': 'indicator'
    'data-index': string
    'data-active': ReadSignal<'' | 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
  /** Explicit application pause, independent from hover/focus interaction. */
  paused: boolean
  /** Pointer is currently over the carousel root. */
  hovered: boolean
  /** DOM focus is currently contained by the carousel root. */
  focusWithin: 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'
  /** Whether direction follows the mounted DOM or explicit init/setDir configuration. */
  dirSource: DirectionSource
}
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: ReadSignal<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': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | undefined>
    'aria-disabled': ReadSignal<'true' | 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': ReadSignal<string | undefined>
    'aria-invalid': ReadSignal<'true' | undefined>
    'aria-required': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    readOnly: ReadSignal<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: ReadSignal<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
  /**
   * Whether the consumer renders the `legend` part (default: true). When
   * false the group omits `aria-labelledby` rather than naming an element
   * that does not exist (the `dialog` `hasDescription` rule, #268). A
   * `<fieldset>` without a legend is valid HTML; a broken idref is not.
   */
  hasLegend?: boolean
}
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'
    /** The legend's id, or absent when `hasLegend: false`. */
    'aria-labelledby': string | undefined
    'aria-disabled': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    'data-scope': 'fieldset'
    'data-part': 'root'
    'data-invalid': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | 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: ReadSignal<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'
    /** The label part's id, or absent when `hasLabel: false`. */
    'aria-labelledby': string | undefined
  }
  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': ReadSignal<'' | undefined>
    'aria-disabled': ReadSignal<'true' | undefined>
    tabindex: ReadSignal<number>
    onKeyDown: (e: KeyboardEvent) => void
    onFocus: () => void
  }
}
ToolbarParts from @llui/components/toolbar
export interface ToolbarParts {
  root: {
    role: 'toolbar'
    'aria-orientation': ReadSignal<Orientation>
    'aria-label': string | undefined
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-scope': 'toolbar'
    'data-part': 'root'
    'data-orientation': ReadSignal<Orientation>
    'data-disabled': ReadSignal<'' | undefined>
  }
  separator: {
    role: 'separator'
    'aria-orientation': ReadSignal<Orientation>
    'data-scope': 'toolbar'
    'data-part': 'separator'
  }
  item: (value: string) => ToolbarItemParts
  /**
   * A labelled group's parts. The group names its `label` part through
   * `aria-labelledby`; a consumer that renders the group WITHOUT that label
   * passes `{ hasLabel: false }` so it never names an element that does not
   * exist (an unlabelled `role="group"` is valid; a broken idref is not — the
   * `dialog` `hasDescription` rule, #268). Default: true.
   */
  group: (label: string, options?: { readonly hasLabel?: boolean }) => 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

bandAt() from @llui/components/meter

The band the reading falls in, or null. See {@link matchIndex} for the rule.

function bandAt(state: MeterState): MeterBand | null
connect() from @llui/components/meter
function connect(state: ReadSignal<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
toneAt() from @llui/components/meter

The current band's tone — the data-state every part carries.

function toneAt(state: MeterState): MeterTone
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 }
  /** @humanOnly */
  | { type: 'setBands'; bands: readonly MeterBandInit[] }
MeterTone from @llui/components/meter

A band's quality, and the whole data-state vocabulary. neutral is the absence of an opinion — a plain gauge, or a band the author gave no tone — and is deliberately not a synonym for optimal: a disk-usage meter with no thresholds declared is not claiming the reading is good.

export type MeterTone = 'optimal' | 'suboptimal' | 'critical' | 'neutral'

Interfaces

ConnectOptions from @llui/components/meter
export interface ConnectOptions {
  label?: string
  /** Custom formatter for the numeric half of the value text. */
  format?: (value: number, max: number) => string
}
MeterBand from @llui/components/meter

A band as STATE holds it.

tone is always present — init/setBands fill the default, so every read is total. The EDGES follow #177's UNBOUNDED-CAPABLE idiom: an absent key means the band is open on that side, and the key is OMITTED rather than set to undefined, so the JSON round trip is an identity key for key.

label is what aria-valuetext announces ("2.1 mIU/L, optimal") — the accessibility win the band model exists for, since a bare aria-valuenow of 47 says nothing about whether the reading is in range. An author-written band defaults its label to its id; write label: '' to announce nothing.

export interface MeterBand {
  id: string
  tone: MeterTone
  /** Lower edge, INCLUSIVE. Absent → open below. */
  from?: number
  /** Upper edge, EXCLUSIVE (inclusive only where it reaches `max`). Absent → open above. */
  to?: number
  label?: string
}
MeterBandGeometry from @llui/components/meter

One band, laid out. Derived from state and stored nowhere: from/to are the resolved VALUE edges (an open side takes the track bound), start/size are the drawn box in percent, clamped into the track so a band reaching past min/max paints inside it.

export interface MeterBandGeometry {
  id: string
  tone: MeterTone
  label: string | undefined
  from: number
  to: number
  start: number
  size: number
  current: boolean
}
MeterBandInit from @llui/components/meter

A band as a consumer WRITES it; init/setBands normalise it into a {@link MeterBand}.

export interface MeterBandInit {
  id: string
  from?: number
  to?: number
  tone?: MeterTone
  label?: string
}
MeterInit from @llui/components/meter
export interface MeterInit {
  value?: number
  min?: number
  max?: number
  /**
   * Native `<meter>` spelling for the three-segment case, compiled into
   * {@link MeterState.bands} as `low` / `middle` / `high`. Ignored when
   * `bands` is given and yields at least one usable band — which is what makes
   * `bands: []` keep the single-optimum behaviour.
   */
  low?: number
  high?: number
  optimum?: number
  bands?: readonly MeterBandInit[]
}
MeterParts from @llui/components/meter
export interface MeterParts {
  root: {
    role: 'meter'
    'aria-valuemin': ReadSignal<number>
    'aria-valuemax': ReadSignal<number>
    'aria-valuenow': ReadSignal<number>
    'aria-valuetext': ReadSignal<string>
    'aria-label': string | undefined
    // Spelled out rather than aliased: `scripts/test/registry-attrs.test.ts`
    // reads part-bag VALUES syntactically and gives an ALIAS no verdict, which
    // is how the shipped skin came to style `data-[state=critical]` against a
    // machine emitting `low`/`high` — two rules of dead CSS, for a release.
    'data-state': ReadSignal<'optimal' | 'suboptimal' | 'critical' | 'neutral'>
    /** The current band's id — absent when the reading is in no band. */
    'data-band': ReadSignal<string | undefined>
    'data-scope': 'meter'
    'data-part': 'root'
  }
  track: {
    'data-state': ReadSignal<'optimal' | 'suboptimal' | 'critical' | 'neutral'>
    'data-scope': 'meter'
    'data-part': 'track'
  }
  /** The filled bar of a classic gauge: `inline-size` up to the reading. */
  range: {
    'data-state': ReadSignal<'optimal' | 'suboptimal' | 'critical' | 'neutral'>
    'data-scope': 'meter'
    'data-part': 'range'
    style: ReadSignal<string>
  }
  /** The reading itself, for a BANDED track: positioned, not filled. */
  marker: {
    'data-state': ReadSignal<'optimal' | 'suboptimal' | 'critical' | 'neutral'>
    'data-band': ReadSignal<string | undefined>
    'data-scope': 'meter'
    'data-part': 'marker'
    style: ReadSignal<string>
  }
  label: {
    'data-scope': 'meter'
    'data-part': 'label'
  }
  /** The laid-out bands, for `each` — key rows on `band.id`. */
  bands: ReadSignal<MeterBandGeometry[]>
  /**
   * Attributes for one laid-out band, taking the ROW HANDLE `each` hands the
   * render function. It returns signals rather than plain values so the bag is
   * spreadable AS IS — `chart`'s `markProps` takes a snapshot instead and every
   * call site has to `peek()` the row and then re-bind the fields that move.
   */
  bandProps: (band: Signal<MeterBandGeometry>) => {
    'data-scope': 'meter'
    'data-part': 'band'
    'data-band': ReadSignal<string>
    'data-state': ReadSignal<'optimal' | 'suboptimal' | 'critical' | 'neutral'>
    'data-current': ReadSignal<'' | undefined>
    style: ReadSignal<string>
  }
  /**
   * One band by id, for a view that names its bands statically. An id no band
   * carries yields an INERT bag — `hidden`, zero-width, `neutral` — rather than
   * throwing: `connect` runs while the view is being built, so a typo would
   * take the whole page down instead of leaving one stripe undrawn.
   */
  band: (id: string) => {
    'data-scope': 'meter'
    'data-part': 'band'
    'data-band': string
    'data-state': ReadSignal<'optimal' | 'suboptimal' | 'critical' | 'neutral'>
    'data-current': ReadSignal<'' | undefined>
    hidden: ReadSignal<boolean>
    style: ReadSignal<string>
  }
  /** The formatted reading, WITHOUT the band name. */
  valueText: ReadSignal<string>
  /** The current band's announced name, or `''`. */
  bandLabel: ReadSignal<string>
}
MeterState from @llui/components/meter
export interface MeterState {
  value: number
  min: number
  max: number
  /**
   * The reference range, in AUTHOR ORDER. Nothing is sorted or merged: order
   * decides which band wins an overlap (the first match) and the order the
   * view draws them in, and re-ordering the author's ids would silently change
   * both.
   */
  bands: MeterBand[]
}

Constants

meter from @llui/components/meter
const meter

@llui/components/breadcrumbs

Functions

connect() from @llui/components/breadcrumbs
function connect(state: ReadSignal<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': ReadSignal<'page' | undefined>
    'data-scope': 'breadcrumbs'
    'data-part': 'link'
    'data-value': string
    'data-current': ReadSignal<'' | 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: ReadSignal<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': ReadSignal<'' | undefined>
  }
  label: {
    'data-scope': 'search-field'
    'data-part': 'label'
  }
  input: {
    type: 'search'
    disabled: ReadSignal<boolean>
    value: ReadSignal<string>
    'data-scope': 'search-field'
    'data-part': 'input'
    onInput: (e: Event) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  clearTrigger: {
    type: 'button'
    'aria-label': string
    hidden: ReadSignal<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: ReadSignal<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'
TableDensity from @llui/components/table

Optional density of the styled grid root; table behavior remains unchanged.

export type TableDensity = 'comfortable' | 'compact'
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
  /** Presentation density published on the grid root for either skin to consume. */
  density?: TableDensity
}
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: ReadSignal<number>
  'data-scope': 'table'
  'data-part': 'cell'
  /** 0-based row index — addresses the cell for roving DOM focus. Reactive
   * for the same reason `TableRowParts`'s `aria-rowindex` is. */
  'data-row-index': ReadSignal<number>
  /** 0-based column index — addresses the cell for roving DOM focus. Columns
   * do not reorder, so this stays a plain number. */
  'data-col-index': number
  'data-focused': ReadSignal<'' | undefined>
  onFocus: (e: FocusEvent) => void
  onKeyDown: (e: KeyboardEvent) => void
}
TableCheckboxParts from @llui/components/table
export interface TableCheckboxParts {
  role: 'checkbox'
  /**
   * The checkbox's accessible name (localized; `Locale['table']`). A
   * `role="checkbox"` span has no content to be named by, so without it
   * assistive tech announced an unnamed checkbox in every row (#268).
   */
  'aria-label': string
  'aria-checked': ReadSignal<'true' | 'false' | 'mixed'>
  'data-scope': 'table'
  'data-part': 'select-all' | 'row-checkbox'
  'data-state': ReadSignal<'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': ReadSignal<'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: ReadSignal<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': ReadSignal<number>
  'data-focused': ReadSignal<'' | undefined>
  'data-sortable': ReadSignal<'' | undefined>
  'data-sort': ReadSignal<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 {
  /**
   * Place the native table carrying {@link root} directly inside this owned
   * scroll viewport. Wide grids then scroll locally without changing the
   * table's native layout/ARIA semantics or overflowing the page.
   */
  viewport: {
    'data-scope': 'table'
    'data-part': 'viewport'
  }
  root: {
    role: 'grid'
    id: string
    'aria-multiselectable': ReadSignal<'true' | undefined>
    'aria-rowcount': ReadSignal<number>
    'aria-colcount': ReadSignal<number>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-scope': 'table'
    'data-part': 'root'
    'data-disabled': ReadSignal<'' | undefined>
    'data-density': TableDensity | undefined
  }
  columnHeader: (columnId: string) => TableColumnHeaderParts
  /**
   * `index` accepts a plain `number` OR a `ReadSignal<number>` (the row handle
   * `each`/`virtualEach` passes its render callback) — a keyed row is REUSED
   * (moved, not rebuilt) on reorder, so a plain number captured at build time
   * would freeze `aria-rowindex` and the row's own `toggleRow`/`selectRange`
   * dispatch at the row's ORIGINAL position forever. Pass the row's reactive
   * index handle whenever rows can reorder (sorting, filtering); a plain
   * number is still accepted for a table that never reorders.
   */
  row: (id: string, index: Reactive<number>) => TableRowParts
  /** `rowIndex` has the same `Reactive<number>` contract as {@link row}'s
   * `index` — the column index does not reorder and stays a plain `number`. */
  cell: (rowIndex: Reactive<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
  /** Same `Reactive<number>` contract as {@link row}'s `index`. */
  rowCheckbox: (id: string, index: Reactive<number>) => TableCheckboxParts
}
TableRowParts from @llui/components/table
export interface TableRowParts {
  role: 'row'
  'aria-selected': ReadSignal<boolean | undefined>
  /** Reactive: a row's DISPLAY position can change after sort/reorder without
   * this row being rebuilt (`each` reuses rows by key), so the index this
   * addresses must follow the row's live position rather than freeze at
   * whatever it was when the row was first built. */
  'aria-rowindex': ReadSignal<number>
  'data-scope': 'table'
  'data-part': 'row'
  'data-row': string
  'data-selected': ReadSignal<'' | 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: ReadSignal<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
subOverlay() from @llui/components/menubar

Engine-owned floating overlay for one submenu level of an embedded menu (#265 A4). Unlike menu/context-menu's own subOverlay, this one reads the ROOT MenubarState and reaches into the one open menu's embedded MenuState (s.menuStates[menuId]) for both open-membership and direction, because a menubar's overlay() is likewise keyed on root state (s.open === menuId) rather than on the embedded menu's own state. See menu-machine.ts:subOverlay's doc comment for the shared contract.

function subOverlay(opts: SubOverlayOptions): 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: 'syncTriggerFocus'; id: string }
  /** @humanOnly */
  | { type: 'focusNext' }
  /** @humanOnly */
  | { type: 'focusPrev' }
  /** @humanOnly */
  | { type: 'menuMsg'; id: string; msg: MenuMsg }
  /** @intent("Set the reading direction") */
  | { type: 'setDir'; dir: TextDirection }
  /** @humanOnly — synchronized from the mounted root's live ancestor direction. */
  | { type: 'syncDomDir'; dir: TextDirection }

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
  /** Reading direction for horizontal keys and delegated menus. Omit to
   * follow the page's own direction (see {@link MenubarState}'s `dir`/
   * `dirSource`, resolved from the mounted root by `directionSync`). */
  dir?: TextDirection
}

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: ReadSignal<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: {
    // `id` is REQUIRED — it is the scope `directionSync` (below) looks the
    // live root up by, the same contract `navigation-menu`'s own `root.id`
    // already honours (#265 finding 6). A consumer that overrides it with a
    // DIFFERENT id breaks the direction sync silently.
    id: string
    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
  /** Place once anywhere in the same build to keep automatic direction live —
   * the shared `@llui/interactions` direction-sync seam (#265 finding 6). A
   * discarded `Mountable` is inert, so this must be placed in the view. */
  directionSync: Mountable
}
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>
  /** Reading direction for both the bar and its delegated menu trees. Routed
   * through the shared `@llui/interactions` direction-sync seam
   * (`../utils/direction.js`) rather than a second resolver — `dirSource`
   * tracks whether `dir` came from explicit config/`setDir` or from the
   * mounted root's live ancestor `dir` attribute (#265 finding 6). Every
   * embedded `MenuState`'s own `dir` is kept explicitly in sync with this one
   * (see `syncEmbeddedDir` below), so a delegated menu never disagrees with
   * the bar that owns it. */
  dir: TextDirection
  dirSource: DirectionSource
}
export interface MenubarTriggerParts {
  role: 'menuitem'
  id: string
  'aria-haspopup': 'menu'
  'aria-expanded': ReadSignal<boolean>
  'aria-controls': string
  'aria-disabled': ReadSignal<'true' | undefined>
  'data-scope': 'menubar'
  'data-part': 'trigger'
  'data-state': ReadSignal<'open' | 'closed'>
  'data-value': string
  tabindex: ReadSignal<number>
  onClick: (e: MouseEvent) => void
  onPointerEnter: (e: PointerEvent) => void
  onFocus: (e: FocusEvent) => void
  onKeyDown: (e: KeyboardEvent) => void
}
SubOverlayOptions from @llui/components/menubar
export interface SubOverlayOptions {
  /** The open menu's id (`opts.menuId` of the enclosing `overlay()`). */
  menuId: string
  /** The subTrigger value this level opens under. */
  value: string
  /** The ROOT `ReadSignal<MenubarState>` — the same one passed to `connect()`. */
  state: ReadSignal<MenubarState>
  parts: Pick<MenuParts, 'subTrigger' | 'subPositioner' | 'subContent'>
  content: () => Renderable
  target?: string | HTMLElement
  positionerClass?: string
  align?: 'start' | 'end'
  offset?: number
  flip?: boolean
  shift?: boolean
}

Constants

const menubar

@llui/components/in-view

Functions

connect() from @llui/components/in-view
function connect(state: ReadSignal<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': ReadSignal<'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

Publish the user's PREFERENCE on <html>: data-theme="light" / "dark" for an explicit choice, and the attribute REMOVED for 'system'.

Removing it is not "doing nothing" — it is how 'system' is spelled, and tokens-dark.css answers all three states from that one attribute:

@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme='light'])):where(:not(.light)) { … }  // absent -> OS
}
.dark,
[data-theme='dark'] { … }                             // "dark"   -> dark

with :root in tokens.css as the light default. So an absent attribute follows the OS in CSS, and data-theme="light" opts a subtree back out of the media query.

This takes a {@link Theme}, not a {@link ResolvedTheme}, deliberately (#233). Three costs to resolving 'system' in JS and pinning the answer, none of which the CSS route pays:

  • watchSystemTheme becomes MANDATORY to track OS changes the media query would have followed for free;
  • SSR has no correct attribute to render, since the server cannot know the client's prefers-color-scheme;
  • the attribute stops naming the PREFERENCE. data-theme="dark" then means either "the user chose dark" or "the user chose system and the OS is dark", and nothing on the page can tell them apart.

It does NOT cost a first-paint flash — a claim worth not repeating, because it is false and easy to check: resolveTheme('system') reads the same media query the first paint used, so the attribute it writes agrees with what is already on screen.

Use resolveTheme() only when you genuinely need the resolved value IN JS — canvas theming, a <meta name="theme-color"> — never merely to honour the OS setting.

THIS FUNCTION DELIBERATELY WRITES NO .dark CLASS (#242), and that is the decision, not an omission. tokens-dark.css still ships a .dark selector for consumers whose own tooling writes it (next-themes, an SSR-rendered class), but maintaining it HERE was rejected on measurement: .dark names a RESOLVED theme, so a 'system' preference would have to resolve prefers-color-scheme in JS, and the class then goes stale the moment the OS flips. A stale class does not merely fail to help — it SUPPRESSES the media query that is correct with no JS at all. Measured in Chromium against the shipped stylesheets, with the preference on 'system' throughout:

  os=light class=(none) -> LIGHT ok        os=dark class=(none) -> DARK  ok
  os=light class=dark   -> DARK  !!WRONG!! os=dark class=dark   -> DARK  ok
  os=light class=light  -> LIGHT ok        os=dark class=light  -> LIGHT !!WRONG!!

So the class buys one cell (a consumer's .dark-scoped override under 'system' on a dark OS) at the price of making watchSystemTheme mandatory, leaving SSR a class it cannot render, and turning a zero-JS-correct cell into one that is wrong until JS runs. The supported answer for that cell is a CSS twin block under the media guard — see site/content/styling.md.

The media guard quoted above is :where()-wrapped so it is specificity (0,1,0), not (0,3,0) (#241). At (0,3,0) it outranked EVERY consumer override of a base token on a dark OS, including [data-theme='dark'] under an explicit 'dark' preference, where the consumer's selector matches and still lost. Do not "simplify" the :where()s away, and do not wrap :root itself — that is (0,0,0) and loses to tokens.css's own :root, breaking dark mode outright.

function applyTheme(theme: Theme): void
connect() from @llui/components/theme-switch
function connect(state: ReadSignal<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). applyTheme() publishes that PREFERENCE on <html> — writing data-theme for an explicit choice and REMOVING it for 'system', which is the state tokens-dark.css's prefers-color-scheme block answers in pure CSS. Do not resolve 'system' in JS just to honour the OS setting: that makes watchSystemTheme mandatory, gives SSR nothing correct to render, and overwrites the preference with its answer.

Typically wired via onMount or in app init:

onMount(() => {
  applyTheme(state.theme.theme)
})

resolveTheme() and watchSystemTheme() remain for consumers who need the resolved value IN JS — canvas theming, a <meta name="theme-color"> — but they are no longer on the critical path for merely following the OS.

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': ReadSignal<boolean>
    'aria-label': string
    onClick: (e: MouseEvent) => void
  }
  toggle: {
    type: 'button'
    'data-scope': 'theme-switch'
    'data-part': 'toggle'
    'data-theme': ReadSignal<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: ReadSignal<SortableState>, send: Send<SortableMsg>, opts: ConnectOptions): SortableParts
droppedMove() from @llui/components/sortable

The reorder a message COMPLETES, or null. A pointer drop and a keyboard toggleGrab while an item is grabbed both end a drag at currentIndex; cancel (Escape, pointercancel) ends one WITHOUT a move. Only a drop inside the container the drag started from is a reorder of that list — a drop onto another container is the consumer's transfer to handle.

Call it with the state BEFORE update runs.

function droppedMove(prev: SortableState, msg: SortableMsg): { from: number; to: number } | null
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

SortableAnnouncement from @llui/components/sortable

What the live region says about the latest drag event, as DATA (the text is rendered by connect through the locale). Positions are 0-based here; container is the origin container, the only one whose region speaks.

export type SortableAnnouncement =
  | { kind: 'grabbed'; container: string; id: string; position: number; count: number }
  | { kind: 'moved'; container: string; id: string; position: number; count: number }
  | { kind: 'dropped'; container: string; id: string; from: number; to: number; count: number }
  | { kind: 'cancelled'; container: string; id: string; position: number; count: number }
SortableMsg from @llui/components/sortable
export type SortableMsg =
  /** @humanOnly */
  | {
      type: 'start'
      id: string
      index: number
      /** Items in the origin container (for "item X of N"). */
      count: 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; count: 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'
  /**
   * The name a listener hears for an item, from its `id` ("Picked up Apple,
   * item 2 of 5."; "Drag handle for Apple"). Without it the announcements say
   * "item" and every handle has the same label.
   */
  itemLabel?: (id: string) => string
  /**
   * Whether the consumer renders the `instructions` part (default: true).
   * When false, no handle carries `aria-describedby` — it would otherwise name
   * an element that does not exist, a broken reference assistive technology
   * reports as a missing description (the `dialog` `hasDescription` rule,
   * #268). The default is ON, as for `dialog`, because the instructions are
   * the ONLY place the keyboard model is described (the handle's name is just
   * "Drag handle"): leaving them out must be a deliberate, visible choice, not
   * the quiet default. `command-menu` defaults its description OFF only
   * because its own view renders none; here the consumer renders the part.
   */
  hasInstructions?: boolean
}
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: droppedMove(prev, msg) names the move a message COMPLETES — a pointer drop, or a keyboard toggleGrab while an item is grabbed — and reorder(arr, from, to) applies it. Watch currentIndex during a drag for a live preview.

type State = { items: string[]; sort: SortableState }

update: (state, msg) => {
  switch (msg.type) {
    case 'sort': {
      const moved = sortable.droppedMove(state.sort, msg.msg)
      const [sort] = sortable.update(state.sort, msg.msg)
      const items = moved ? reorder(state.items, moved.from, moved.to) : state.items
      return [{ items, sort }, []]
    }
  }
}

// `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.

Screen readers. A keyboard user who grabs, moves, drops or cancels needs to HEAR it — nothing on screen tells them where the item is. The machine therefore owns two more parts, and both must be rendered:

  • liveRegion — a polite, atomic role="status" region whose text (a Signal, rendered as the region's CHILD, never spread as an attribute) announces "Picked up Apple, item 2 of 5.", "Apple moved to position 3 of 5.", the drop and a cancel. Render it visually hidden (sr-only), never display: none — a hidden live region is never announced. Pass itemLabel to name the item; without it the announcements say "item".
  • instructions — the how-to text, hidden, which every handle references through aria-describedby (a directly referenced hidden element still provides a description, and stays out of the reading order). A consumer that supplies its own keyboard help and does not render this part passes hasInstructions: false, so no handle names an element that does not exist (the dialog hasDescription precedent, #268).
const { text: live, ...liveAttrs } = s.liveRegion
const { text: howTo, ...howToAttrs } = s.instructions
div({ ...liveAttrs, class: 'sr-only' }, [text(live)])
div({ ...howToAttrs }, [text(howTo)])

The grab is aria-pressed on the handle (a toggle button: pressed while the item is carried). aria-grabbed is NOT used: ARIA 1.1 deprecated it and aria-dropeffect with no replacement, and screen readers never broadly exposed either — the live region is how every mature implementation (dnd-kit, React Aria) conveys a drag. aria-roledescription is deliberately not set: it would replace "toggle button", the one cue that Space operates the handle. All text comes from LocaleContext (Locale['sortable']).

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
  /**
   * How many items the origin container held when the drag started — the "N"
   * in "item 2 of N", and the last slot a keyboard `moveBy` may reach.
   */
  count: number
}
SortableParts from @llui/components/sortable
export interface SortableParts {
  root: {
    'data-scope': 'sortable'
    'data-part': 'root'
    'data-container-id': string
    'data-dragging': ReadSignal<'' | 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': ReadSignal<'' | undefined>
    'data-over': ReadSignal<'' | undefined>
    'data-shift': ReadSignal<'up' | 'down' | undefined>
    'style.transform': ReadSignal<string | undefined>
    'style.zIndex': ReadSignal<string | undefined>
  }
  handle: (
    id: string,
    index: number,
  ) => {
    'data-scope': 'sortable'
    'data-part': 'handle'
    role: 'button'
    tabindex: 0
    /** A toggle button: pressed while this handle's item is carried. */
    'aria-pressed': ReadSignal<boolean>
    'aria-label': string
    /** The `instructions` part's id, or absent when `hasInstructions: false`. */
    'aria-describedby': string | undefined
    onPointerDown: (e: PointerEvent) => void
    onKeyDown: (e: KeyboardEvent) => void
  }
  /**
   * The polite live region announcing grab / move / drop / cancel. `text` is
   * the region's CHILD (a Signal), not an attribute: spread the rest and render
   * `text(text)` inside. Keep it visually hidden but rendered (`sr-only`) — a
   * `display: none` region is never announced. Only the ORIGIN container's
   * region speaks, so connects that share one state never announce twice.
   */
  liveRegion: {
    role: 'status'
    'aria-live': 'polite'
    'aria-atomic': 'true'
    'data-scope': 'sortable'
    'data-part': 'live-region'
    text: ReadSignal<string>
  }
  /**
   * The keyboard instructions every handle's `aria-describedby` points at.
   * `hidden`: a directly referenced hidden element still supplies the
   * description, and stays out of the reading order. `text` is its CHILD.
   */
  instructions: {
    id: string
    hidden: true
    'data-scope': 'sortable'
    'data-part': 'instructions'
    text: string
  }
}
SortableState from @llui/components/sortable
export interface SortableState {
  dragging: DragState | null
  /** The latest announcement; `null` before any drag and after a cross-container drop. */
  announcement: SortableAnnouncement | null
}

Constants

sortable from @llui/components/sortable
const sortable

@llui/components/sparkline

Functions

connect() from @llui/components/sparkline
function connect(state: ReadSignal<SparklineState>, send: Send<SparklineMsg>, opts: SparklineConnectOptions): SparklineParts
createSparklineGeometry() from @llui/components/sparkline

A memoized {@link sparklineGeometry}, one cell per instance.

Hold it in a closure, never at module scope: a table of fifty sparklines sharing one cell would evict on every call and the memo would never hit — the same rule utils/derive.ts states for per-item derivations.

function createSparklineGeometry(): (state: SparklineState) => SparklineGeometry
geometry() from @llui/components/sparkline

Derived geometry for a state. Exported so a consumer can measure or test a sparkline without mounting one.

function geometry(state: SparklineState): SparklineGeometry
init() from @llui/components/sparkline
function init(opts: SparklineInit = {}): SparklineState
isoDay() from @llui/components/sparkline

YYYY-MM-DD for an instant, in the given fixed offset's calendar.

function isoDay(at: number, opts: CalendarOptions = {}): string
locateIndex() from @llui/components/sparkline

The drawn point nearest a position in user units — the pointer hit test. Nearest-x, because dots are a run along the axis with no extents to contain a pointer.

A TIE goes to the EARLIER point (<, not <=). Ties are not hypothetical — two readings can share an instant, which is why a dot's key carries its index — and either rule is defensible; what is not defensible is leaving it unstated, since a silent flip changes which reading a tooltip reports.

function locateIndex(geo: SparklineGeometry, x: number): number | null
sparklineGeometry() from @llui/components/sparkline

The whole picture, as data. A PURE function: same inputs, same output, and the same output on a server as in a browser.

function sparklineGeometry(points: readonly SparklinePoint[], opts: SparklineGeometryOptions = {}): SparklineGeometry
sparklineLabel() from @llui/components/sparkline

Phrasing for a {@link SparklineSummary}. The strings are injected so the locale can supply them in connect and the pure path can default to English without a second template.

function sparklineLabel(summary: SparklineSummary, strings: { empty: string; range: (count: number, from: string, to: string) => string }): string
trimLeadingOutliers() from @llui/components/sparkline

Drop leading points separated from the rest by a disproportionate gap. See {@link SparklineTrim} for the rule and for why this is opt-in.

function trimLeadingOutliers(points: readonly SparklinePoint[], trim: SparklineTrim | null): { points: SparklinePoint[]; trimmed: number }
update() from @llui/components/sparkline
function update(state: SparklineState, msg: SparklineMsg): [SparklineState, never[]]

Types

SparklineBandKind from @llui/components/sparkline
export type SparklineBandKind = 'between' | 'below' | 'above'
SparklineMsg from @llui/components/sparkline
export type SparklineMsg =
  /** @intent("Set the point under the cursor, or clear it with null") */
  | { type: 'setActive'; index: number | null }
  /** @intent("Move the cursor along the points by delta, clamping at the ends") */
  | { type: 'moveActive'; delta: number }
  /** @intent("Move the cursor to the first point") */
  | { type: 'firstActive' }
  /** @intent("Move the cursor to the last point") */
  | { type: 'lastActive' }
  /** @intent("Replace the plotted points") */
  | { type: 'setPoints'; points: SparklinePoint[] }
  /** @intent("Set the instant the right edge denotes, or derive it with null") */
  | { type: 'setNow'; at: number | null }
  /** @intent("Set the reference band; null on a side leaves it unbounded") */
  | { type: 'setBand'; low: number | null; high: number | null }
  /** @intent("Set the viewBox size in user units") */
  | { type: 'setSize'; width: number; height: number }
SparklineTone from @llui/components/sparkline
export type SparklineTone = 'below' | 'in' | 'above' | 'none'

Interfaces

SparklineBand from @llui/components/sparkline

A reference band. Both bounds are UNBOUNDED-CAPABLE in the sense of finiteBound (#177): "no bound on this side" is spelled by OMITTING the key, never by undefined, null or ±Infinity — only the omission survives a JSON round trip key-for-key, and this lives in state.

The three readings are not a convenience, they are the whole point: both bounds shade BETWEEN them, a high alone shades everything BELOW it, and a low alone shades everything ABOVE it. "Acceptable is under 140" and "acceptable is between 90 and 120" are different statements and must not both come out as a stripe.

export interface SparklineBand {
  low?: number
  high?: number
}
SparklineBandGeometry from @llui/components/sparkline
export interface SparklineBandGeometry {
  kind: SparklineBandKind
  low: number | null
  high: number | null
  d: string
}
SparklineCalendar from @llui/components/sparkline

The serializable subset of {@link CalendarStepOptions} this state carries.

export interface SparklineCalendar {
  offsetMinutes: number
  weekStartsOn: number
  maxTicks: number
  /** Force a rung instead of choosing one from the span. */
  unit?: CalendarUnit
}
SparklineConnectOptions from @llui/components/sparkline
export interface SparklineConnectOptions {
  /** Base id; the title and description ids derive from it. */
  id: string
  /** Override the composed accessible name entirely. */
  label?: string
  /** Longer description, announced with the name. Defaults to empty. */
  description?: string
  /**
   * Whether the consumer renders the `desc` part (default: true). When false,
   * the svg's `aria-labelledby` names only the title, never an element that
   * does not exist (the `dialog` `hasDescription` rule, #268). The TITLE is
   * the image's name — required for `role="img"` — and is always referenced.
   */
  hasDescription?: boolean
}
SparklineDot from @llui/components/sparkline
export interface SparklineDot {
  index: number
  at: number
  value: number
  x: number
  y: number
  tone: SparklineTone
  /** True for the most recent drawn point — the reading the row is about. */
  last: boolean
  /** True for the point under the cursor. Follows `chart`: the cursor is part
   *  of the derived picture, so one signal per row carries everything a dot
   *  renders from and nothing has to combine two signals at the call site. */
  active: boolean
  key: string
}
SparklineGeometry from @llui/components/sparkline
export interface SparklineGeometry {
  frame: Frame
  time: Domain
  value: Domain
  /** The points actually drawn: finite, sorted, and after trimming. */
  points: SparklinePoint[]
  trimmed: number
  /** The trend line. Empty string when there is nothing to draw. */
  path: string
  band: SparklineBandGeometry | null
  ticks: SparklineTick[]
  dots: SparklineDot[]
  spans: SparklineSpan[]
  now: SparklineNow
  summary: SparklineSummary
  /** Built-in English composition of {@link summary}. `connect` replaces it
   *  with the locale's phrasing. */
  label: string
  rows: SparklineRow[]
}
SparklineGeometryOptions from @llui/components/sparkline
export interface SparklineGeometryOptions {
  band?: SparklineBand
  now?: number
  min?: number
  max?: number
  width?: number
  height?: number
  padding?: Partial<SparklinePadding>
  curve?: Curve
  trim?: SparklineTrim | null
  track?: SparklineTrack | null
  calendar?: Partial<SparklineCalendar>
  /** Which drawn point the cursor is on. Indexes the DRAWN list — after
   *  non-finite samples are dropped and after trimming — so it is the same
   *  index `locateIndex` returns. */
  activeIndex?: number | null
}
SparklineInit from @llui/components/sparkline
export interface SparklineInit {
  points?: readonly SparklinePoint[]
  band?: SparklineBand
  now?: number
  min?: number
  max?: number
  width?: number
  height?: number
  padding?: Partial<SparklinePadding>
  curve?: Curve
  /** `true` for the documented defaults, an object to tune them, absent for
   *  OFF — trimming discards data, so it is never on by accident. */
  trim?: boolean | Partial<SparklineTrim>
  track?: SparklineTrack | null
  calendar?: Partial<SparklineCalendar>
}
SparklineNow from @llui/components/sparkline
export interface SparklineNow {
  at: number
  x: number
  d: string
  /** True when the right edge is later than the last reading — the series
   *  trails off. */
  stale: boolean
}
SparklinePadding from @llui/components/sparkline
export interface SparklinePadding {
  top: number
  right: number
  bottom: number
  left: number
}
SparklineParts from @llui/components/sparkline
export interface SparklineParts {
  root: {
    'data-scope': 'sparkline'
    'data-part': 'root'
    'data-stale': ReadSignal<'' | undefined>
    'data-active': ReadSignal<'' | undefined>
  }
  /**
   * The `<svg>`. `role="img"` named through its own `<title>`/`<desc>`; the
   * numbers live in {@link SparklineParts.table}, because an SVG polyline is
   * otherwise silent and the WAI-ARIA graphics roles are not carried well
   * enough to be the only route to the data.
   */
  svg: {
    'data-scope': 'sparkline'
    'data-part': 'svg'
    role: 'img'
    'aria-labelledby': string
    viewBox: ReadSignal<string>
    tabindex: 0
    onKeyDown: (e: KeyboardEvent) => void
    onPointerMove: (e: PointerEvent) => void
    onPointerLeave: (e: PointerEvent) => void
    onBlur: (e: FocusEvent) => void
  }
  title: { id: string; 'data-scope': 'sparkline'; 'data-part': 'title' }
  desc: { id: string; 'data-scope': 'sparkline'; 'data-part': 'desc' }
  /** The visually-hidden `<table>` fallback. Render it with {@link rows}. */
  table: {
    'data-scope': 'sparkline'
    'data-part': 'table'
    'aria-label': ReadSignal<string>
  }
  /** The reference band. Stays MOUNTED and hides itself, so a band appearing or
   *  disappearing does not rebuild the layer. */
  band: {
    'data-scope': 'sparkline'
    'data-part': 'band'
    'data-band': ReadSignal<'between' | 'below' | 'above' | undefined>
    d: ReadSignal<string>
    hidden: ReadSignal<boolean>
  }
  line: { 'data-scope': 'sparkline'; 'data-part': 'line'; d: ReadSignal<string> }
  /** The right edge. `data-stale` is set when it is later than the last
   *  reading. */
  now: {
    'data-scope': 'sparkline'
    'data-part': 'now'
    'data-stale': ReadSignal<'' | undefined>
    d: ReadSignal<string>
  }
  /** A `<g>` stacking layer. Static — spread it on each layer group. */
  layer: { 'data-scope': 'sparkline'; 'data-part': 'layer' }
  /**
   * Per-row attribute bags. Each takes the ROW SIGNAL an `each` hands its
   * render function, not a snapshot value, and every geometry-derived
   * attribute comes back as a Signal.
   *
   * That is not decoration. A keyed `each` REUSES a row whose key is unchanged,
   * so a dot whose key is `index:instant` survives a change of time domain —
   * and a static `cx` read at build time would then be frozen at the position
   * the point had under the OLD domain, with the key giving no hint that
   * anything moved. Taking the row signal is what makes the reuse safe.
   */
  tickProps: (tick: Signal<SparklineTick>) => {
    'data-scope': 'sparkline'
    'data-part': 'grid'
    'data-unit': ReadSignal<'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'>
    d: ReadSignal<string>
  }
  dotProps: (dot: Signal<SparklineDot>) => {
    'data-scope': 'sparkline'
    'data-part': 'dot'
    'data-tone': ReadSignal<'below' | 'in' | 'above' | 'none'>
    'data-last': ReadSignal<'' | undefined>
    'data-active': ReadSignal<'' | undefined>
    cx: ReadSignal<number>
    cy: ReadSignal<number>
  }
  spanProps: (span: Signal<SparklineSpan>) => {
    'data-scope': 'sparkline'
    'data-part': 'span'
    'data-grain': ReadSignal<string>
    d: ReadSignal<string>
  }
  /** Tooltip ATTRIBUTES — spreadable, with its own reactive `hidden`. */
  tooltip: {
    'data-scope': 'sparkline'
    'data-part': 'tooltip'
    role: 'status'
    'aria-live': 'polite'
    hidden: ReadSignal<boolean>
    style: ReadSignal<string>
  }
  ticks: ReadSignal<SparklineTick[]>
  dots: ReadSignal<SparklineDot[]>
  spans: ReadSignal<SparklineSpan[]>
  rows: ReadSignal<SparklineRow[]>
  /** The dot under the cursor, or `null`. */
  activeDot: ReadSignal<SparklineDot | null>
  /** The composed accessible name — the locale's phrasing of
   *  {@link SparklineGeometry.summary}, or `opts.label` when given. */
  label: ReadSignal<string>
  summary: ReadSignal<SparklineSummary>
}
SparklinePoint from @llui/components/sparkline

Sparkline — "this value, over time, against a reference band", at the size of a table cell.

Why this is not chart

chart is a plotting machine: ChartState, a projection seam, a legend, a keyboard cursor over CATEGORIES, and numeric-nice value ticks. A sparkline is a pure function of its points — dozens appear in one table, and the only interaction any of them has is a hover readout. So the load-bearing export here is {@link sparklineGeometry}, which takes points and options and returns path strings; it needs no signal, no send, no mount and no state.

init/update/connect exist BESIDE it, not instead of it, and add exactly one fact: which point the cursor is on. A consumer rendering fifty trends in a table calls the pure function fifty times and pays for nothing else; one that wants a tooltip opts into the machine for that column. The stateless middle ground works too — connect(constant(state), noSend, { id }) gives the full part bag with the cursor pinned wherever the state says.

What it reuses

The geometry is utils/scale.ts + utils/path.ts + utils/projection.ts, the same three files chart is built from, so every coordinate goes through fmt and a path recomputed from unchanged data is BYTE-IDENTICAL — the reconciler commits on output-equality, and a sparkline in a table is exactly the place where a spuriously-changing path string costs the most.

Two things it does NOT reuse, each for a stated reason:

  • valueDomain, because it always includes zero. That is right for a bar, whose LENGTH states the magnitude and therefore lies if the baseline is not zero; it is wrong for a trend line, where the shape is the message. A series reading 118–142 against a 90–120 band has to fill the box, not sit as a flat smear across the top of a 0–142 axis.
  • ticks/niceDomain, because they are NUMERIC. See utils/calendar-ticks.ts: rounding an epoch to a nice number never lands on the 1st of March.

There is no coord and no polar projection. A sparkline is inline text furniture; a round one is a different picture, and chart already draws it.

export interface SparklinePoint {
  /** Instant, in epoch milliseconds. */
  at: number
  value: number
  /**
   * Sampling resolution of this reading — "spot", "session", "daily", whatever
   * the domain calls it. Free-form and never interpreted: consecutive points
   * sharing a tag become one segment of the granularity track, so a line whose
   * sampling changes SAYS so instead of implying uniform data.
   */
  grain?: string
}
SparklineRow from @llui/components/sparkline

One row of the visually-hidden <table> fallback.

export interface SparklineRow {
  at: number
  /** `YYYY-MM-DD` in the configured offset's calendar. Locale-neutral by
   *  design: the table is a data fallback, not a formatted report. */
  day: string
  value: number
  tone: SparklineTone
  grain: string | null
}
SparklineSpan from @llui/components/sparkline
export interface SparklineSpan {
  grain: string
  /** First and last instant the segment covers. */
  from: number
  to: number
  x0: number
  x1: number
  d: string
  key: string
}
SparklineState from @llui/components/sparkline
export interface SparklineState {
  points: SparklinePoint[]
  /** Omit a side to leave it unbounded — see {@link SparklineBand}. */
  band: SparklineBand
  /**
   * The instant the RIGHT EDGE denotes. A stale series then visibly trails off
   * instead of looking current, which is the difference between "the last
   * reading was fine" and "the last reading was fine, eight months ago".
   *
   * UNBOUNDED-CAPABLE: omit it and the edge is the last reading's own instant.
   * It is deliberately NOT defaulted to `Date.now()` anywhere in this module —
   * geometry has to be a pure function of state or an SSR render and the first
   * client render disagree, and `Date.now()` would additionally make the path
   * string change on every recomputation, which is the one thing `fmt` exists
   * to prevent. A consumer that wants "now" puts a clock in its own state.
   */
  now?: number
  /** Value-axis bounds. UNBOUNDED-CAPABLE (#177): omit to derive. */
  min?: number
  max?: number
  width: number
  height: number
  padding: SparklinePadding
  curve: Curve
  /** `null` = no trimming. See {@link SparklineTrim}. */
  trim: SparklineTrim | null
  /** `null` = no granularity track. */
  track: SparklineTrack | null
  /** Fixed offset from UTC that calendar boundaries are measured in, and the
   *  ladder's ceiling. See `utils/calendar-ticks.ts` on why it is an offset and
   *  not a zone. */
  calendar: SparklineCalendar
  /** Index of the point under the pointer or keyboard cursor. */
  activeIndex: number | null
}
SparklineSummary from @llui/components/sparkline

The facts a label is composed from. Separated from the label itself so the same numbers can be phrased by the locale in connect and by the built-in English template in the pure path.

export interface SparklineSummary {
  count: number
  /** `YYYY-MM-DD` of the first and last drawn reading; empty strings when there
   *  is no data. */
  from: string
  to: string
  trimmed: number
  stale: boolean
}
SparklineTick from @llui/components/sparkline
export interface SparklineTick {
  at: number
  unit: CalendarUnit
  x: number
  /** The vertical gridline, in user units. */
  d: string
  /** Stable key for a keyed `each`. */
  key: string
}
SparklineTrack from @llui/components/sparkline

The granularity track: a thin bar under the plot, in the bottom padding.

export interface SparklineTrack {
  height: number
  /** Distance between the bottom of the plot frame and the top of the track. */
  gap: number
}
SparklineTrim from @llui/components/sparkline

Leading-outlier trimming. A HEURISTIC that DISCARDS REAL DATA, so it is off unless asked for: trim: null is the default, and a chart that silently drops a reading is worse than a squashed one.

The rule: while more than floor points remain, compare the FIRST gap against the MEDIAN of the gaps after it, and drop the leading point when the first gap is more than factor times that median. Median rather than mean so a second ancient reading cannot inflate the yardstick it is being measured against; factor > 1 is required, and a median of zero (every remaining reading at the same instant) stops the walk rather than eating the series.

trimmed in the geometry reports how many points went, so a consumer can say so on the page.

export interface SparklineTrim {
  /** How many times the median gap the leading gap must exceed. Default 4. */
  factor: number
  /** Never trim below this many points. Default 3, floored at 2 — one point is
   *  not a trend and cannot be drawn as a line. */
  floor: number
}

Constants

sparkline from @llui/components/sparkline
const sparkline

@llui/components/form

Functions

connect() from @llui/components/form
function connect(state: ReadSignal<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': ReadSignal<FormStatus>
    'aria-busy': ReadSignal<'true' | undefined>
  }
  field: (name: string) => {
    'data-scope': 'form'
    'data-part': 'field'
    'data-touched': ReadSignal<'' | undefined>
    touched: ReadSignal<boolean>
    onBlur: (e: FocusEvent) => void
  }
  submit: {
    type: 'submit'
    'data-scope': 'form'
    'data-part': 'submit'
    'data-state': ReadSignal<FormStatus>
    disabled: ReadSignal<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: ReadSignal<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. The listbox owns one option per FILTERED command, bucketed into labelled groups (an ungrouped command sits directly in the listbox, with no group and so no dangling label reference). The combobox input's own key handling drives the machine's highlight — arrows/Home/End move it, Enter runs it, Escape clears the query then closes — and a pointer click runs the clicked command. 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: ReadSignal<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: ReadSignal<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: 'highlight'; commandId: string }
  /** @humanOnly */
  | { type: 'highlightNext' }
  /** @humanOnly */
  | { type: 'highlightPrev' }
  /** @humanOnly */
  | { type: 'highlightFirst' }
  /** @humanOnly */
  | { type: 'highlightLast' }
  /** @humanOnly */
  | { type: 'executeHighlighted' }
  /** @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[] }
  /** @intent("Mark an async option fetch as started; pass the request's id") */
  | { type: 'loadStart'; requestId: number }
  /** @humanOnly — atomic replacement passthrough to `combobox`; see its own
   * `loadSuccess` doc for the reconciliation this performs in one step. */
  | {
      type: 'loadSuccess'
      requestId: number
      items: string[]
      groups?: ComboboxGroup[]
      disabled?: string[]
    }
  /** @humanOnly */
  | { type: 'loadError'; requestId: number; error: string }
SearchableSelectSelectionMode from @llui/components/patterns

Combobox — text input paired with a filtered listbox dropdown. User types to filter items, arrow keys navigate the filtered set, Enter selects. Supports single and multiple selection.

Beyond the sync filtered listbox the machine owns three additive surfaces:

  • Async option loading — status/requestId/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 groups — groups 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
  /**
   * Whether the consumer renders `dialog.description` (default: false — the
   * palette's own `view()` renders a title and no description). Forwarded to
   * the dialog so `aria-describedby` never names a missing element (#268).
   */
  hasDescription?: boolean
}
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) => ReadSignal<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': ReadSignal<'' | 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
  /**
   * The keyboard-highlighted command id (the combobox's active descendant).
   * Always an ENABLED member of `filtered` while open; `null` when closed or
   * when nothing enabled matches.
   */
  highlighted: string | null
}
CommandMenuViewOptions from @llui/components/patterns
export interface CommandMenuViewOptions {
  /** A PATH signal: the view slices `filteredGroups` with `.at()` so the list is
   * gated on that path alone. */
  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: ReadSignal<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
  /** Presentation density forwarded to the composed table root. */
  density?: TableDensity
  /** 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': ReadSignal<'true' | undefined>
    'aria-live': 'polite'
    hidden: ReadSignal<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: ReadSignal<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: ReadSignal<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': ReadSignal<'' | undefined>
    'data-touched': ReadSignal<'' | undefined>
  }
  label: {
    id: string
    htmlFor: string
    'data-scope': 'form-field'
    'data-part': 'label'
  }
  control: {
    id: string
    'aria-labelledby': string
    'aria-describedby': ReadSignal<string | undefined>
    'aria-invalid': ReadSignal<'true' | undefined>
    'aria-required': ReadSignal<'true' | undefined>
    'aria-busy': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    readOnly: ReadSignal<boolean>
    'data-scope': 'form-field'
    'data-part': 'control'
    onBlur: (e: FocusEvent) => void
  }
  description: {
    id: string
    'data-scope': 'form-field'
    'data-part': 'description'
  }
  /**
   * The error live region's ATTRIBUTES — spreadable as-is, like every other
   * `errorText` bag in the package (`field`, `fieldset`, `async-list`,
   * `date-input`). It carries its own reactive `hidden`, so the element stays
   * MOUNTED and `role="alert"` announces when it is revealed; unmounting it
   * with `show(...)` would register no live region and announce nothing.
   *
   * The message and the issues are DATA, not attributes, and live on
   * {@link FormFieldFieldParts.error}. They used to sit in this bag, which made
   * it the one part bag in the package a consumer could not spread — spreading
   * it bound `message` and `issues` as junk DOM attributes, so every call site
   * had to destructure them back out first.
   */
  errorText: {
    id: string
    role: 'alert'
    'aria-live': 'polite'
    'data-scope': 'form-field'
    'data-part': 'error'
    hidden: ReadSignal<boolean>
  }
  /** The error CONTENT, for rendering — never spread onto an element. */
  error: {
    /** First visible issue message for this field, or '' when no error is shown. */
    message: ReadSignal<string>
    /** Every issue mapped to this field (for custom rendering). */
    issues: ReadSignal<StandardSchemaV1.Issue[]>
    /** True only when the field is invalid AND its error should be visible
     * (`touched || status === 'submitted'`). */
    visible: ReadSignal<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': ReadSignal<FormStatus>
    'aria-busy': ReadSignal<'true' | undefined>
  }
  submit: {
    type: 'submit'
    'data-scope': 'form-field'
    'data-part': 'submit'
    'data-state': ReadSignal<FormStatus>
    disabled: ReadSignal<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'
    /** The group label's id, or absent when `hasLabel: false`. */
    'aria-labelledby': string | undefined
    '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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'selected' | undefined>
    'data-highlighted': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | 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': ReadSignal<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: ReadSignal<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': ReadSignal<'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': ReadSignal<boolean>
    'aria-controls': string
    'aria-disabled': ReadSignal<'true' | undefined>
    id: string
    disabled: ReadSignal<boolean>
    'data-scope': 'searchable-select'
    'data-part': 'trigger'
    'data-state': ReadSignal<'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: ReadSignal<string>
  /** Whether a selection exists (drive showing/hiding the clear button). */
  hasValue: ReadSignal<boolean>
  /** The filter input rendered inside the popup, above the listbox. */
  input: {
    type: 'text'
    role: 'combobox'
    autocomplete: 'off'
    'aria-autocomplete': 'list'
    'aria-expanded': ReadSignal<boolean>
    'aria-controls': string
    'aria-activedescendant': ReadSignal<string | undefined>
    id: string
    value: ReadSignal<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': ReadSignal<'true' | undefined>
    'aria-multiselectable': ReadSignal<'true' | undefined>
    tabindex: -1
    'data-state': ReadSignal<'open' | 'closed'>
    'data-status': ReadSignal<AsyncStatus>
    /** The mutually-exclusive load projection (#265 finding 11); mirrors the
     * top-level `loadState` signal. See `combobox`'s `LoadProjection`. */
    'data-load-state': ReadSignal<LoadProjection>
    'data-scope': 'searchable-select'
    'data-part': 'content'
  }
  /** The mutually-exclusive async load projection: `'initial-empty'` |
   * `'loading'` | `'stale-results'` | `'success'` | `'error'` — never
   * independent booleans that can contradict each other. Documents the
   * stale-while-revalidate policy: `'stale-results'` means a fetch is in
   * flight while the previous items are still mounted and selectable;
   * `'error'` is reported the same whether or not stale items remain
   * mounted underneath it. */
  loadState: ReadSignal<LoadProjection>
  /** 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
  /**
   * A labelled group's parts. The group names its `label` part through
   * `aria-labelledby`; a consumer that renders the group WITHOUT that label
   * passes `{ hasLabel: false }` so it never names an element that does not
   * exist (an unlabelled `role="group"` is valid; a broken idref is not — the
   * `dialog` `hasDescription` rule, #268). Default: true.
   */
  group: (id: string, options?: { readonly hasLabel?: boolean }) => 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 once the
   * list is settled (`loadState` is `'success'` or `'initial-empty'`), the
   * error text on `'error'`, and nothing while a fetch is in flight
   * (`'loading'` / `'stale-results'`). */
  liveRegion: {
    role: 'status'
    'aria-live': 'polite'
    'aria-atomic': 'true'
    'data-scope': 'searchable-select'
    'data-part': 'live-region'
    text: ReadSignal<string>
  }
  /** Empty-state container. `hidden` is false only when the SETTLED filtered
   * list is empty — never while loading, revalidating, or after a failed
   * fetch (the live region / an error slot own those). */
  empty: {
    'data-scope': 'searchable-select'
    'data-part': 'empty'
    hidden: ReadSignal<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': ReadSignal<'' | undefined>
  }
  nextTrigger: {
    type: 'button'
    disabled: ReadSignal<boolean>
    'aria-busy': ReadSignal<'true' | undefined>
    'data-scope': 'steps'
    'data-part': 'next-trigger'
    onClick: (e: MouseEvent) => void
  }
  prevTrigger: {
    type: 'button'
    disabled: ReadSignal<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: ReadSignal<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: ReadSignal<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': ReadSignal<'' | undefined>
    'data-touched': ReadSignal<'' | undefined>
  }
  label: {
    id: string
    htmlFor: string
    'data-scope': 'form-field'
    'data-part': 'label'
  }
  control: {
    id: string
    'aria-labelledby': string
    'aria-describedby': ReadSignal<string | undefined>
    'aria-invalid': ReadSignal<'true' | undefined>
    'aria-required': ReadSignal<'true' | undefined>
    'aria-busy': ReadSignal<'true' | undefined>
    disabled: ReadSignal<boolean>
    readOnly: ReadSignal<boolean>
    'data-scope': 'form-field'
    'data-part': 'control'
    onBlur: (e: FocusEvent) => void
  }
  description: {
    id: string
    'data-scope': 'form-field'
    'data-part': 'description'
  }
  /**
   * The error live region's ATTRIBUTES — spreadable as-is, like every other
   * `errorText` bag in the package (`field`, `fieldset`, `async-list`,
   * `date-input`). It carries its own reactive `hidden`, so the element stays
   * MOUNTED and `role="alert"` announces when it is revealed; unmounting it
   * with `show(...)` would register no live region and announce nothing.
   *
   * The message and the issues are DATA, not attributes, and live on
   * {@link FormFieldFieldParts.error}. They used to sit in this bag, which made
   * it the one part bag in the package a consumer could not spread — spreading
   * it bound `message` and `issues` as junk DOM attributes, so every call site
   * had to destructure them back out first.
   */
  errorText: {
    id: string
    role: 'alert'
    'aria-live': 'polite'
    'data-scope': 'form-field'
    'data-part': 'error'
    hidden: ReadSignal<boolean>
  }
  /** The error CONTENT, for rendering — never spread onto an element. */
  error: {
    /** First visible issue message for this field, or '' when no error is shown. */
    message: ReadSignal<string>
    /** Every issue mapped to this field (for custom rendering). */
    issues: ReadSignal<StandardSchemaV1.Issue[]>
    /** True only when the field is invalid AND its error should be visible
     * (`touched || status === 'submitted'`). */
    visible: ReadSignal<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': ReadSignal<FormStatus>
    'aria-busy': ReadSignal<'true' | undefined>
  }
  submit: {
    type: 'submit'
    'data-scope': 'form-field'
    'data-part': 'submit'
    'data-state': ReadSignal<FormStatus>
    disabled: ReadSignal<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: ReadSignal<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': ReadSignal<'' | undefined>
  }
  nextTrigger: {
    type: 'button'
    disabled: ReadSignal<boolean>
    'aria-busy': ReadSignal<'true' | undefined>
    'data-scope': 'steps'
    'data-part': 'next-trigger'
    onClick: (e: MouseEvent) => void
  }
  prevTrigger: {
    type: 'button'
    disabled: ReadSignal<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: ReadSignal<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. The listbox owns one option per FILTERED command, bucketed into labelled groups (an ungrouped command sits directly in the listbox, with no group and so no dangling label reference). The combobox input's own key handling drives the machine's highlight — arrows/Home/End move it, Enter runs it, Escape clears the query then closes — and a pointer click runs the clicked command. 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: 'highlight'; commandId: string }
  /** @humanOnly */
  | { type: 'highlightNext' }
  /** @humanOnly */
  | { type: 'highlightPrev' }
  /** @humanOnly */
  | { type: 'highlightFirst' }
  /** @humanOnly */
  | { type: 'highlightLast' }
  /** @humanOnly */
  | { type: 'executeHighlighted' }
  /** @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) => ReadSignal<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': ReadSignal<'' | 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
  /**
   * The keyboard-highlighted command id (the combobox's active descendant).
   * Always an ENABLED member of `filtered` while open; `null` when closed or
   * when nothing enabled matches.
   */
  highlighted: string | null
}
CommandMenuViewOptions from @llui/components/patterns/command-menu
export interface CommandMenuViewOptions {
  /** A PATH signal: the view slices `filteredGroups` with `.at()` so the list is
   * gated on that path alone. */
  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
  /**
   * Whether the consumer renders `dialog.description` (default: false — the
   * palette's own `view()` renders a title and no description). Forwarded to
   * the dialog so `aria-describedby` never names a missing element (#268).
   */
  hasDescription?: boolean
}

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: ReadSignal<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
  /** Presentation density forwarded to the composed table root. */
  density?: TableDensity
  /** 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': ReadSignal<'true' | undefined>
    'aria-live': 'polite'
    hidden: ReadSignal<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: ReadSignal<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: ReadSignal<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: ReadSignal<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'
LoadProjection from @llui/components/patterns/searchable-select

A single, mutually-exclusive summary of the async load lifecycle, derived from status and whether any items are currently on hand. This exists so a consumer never has to reconcile independent booleans (isLoading, isEmpty, hasError) that can read true at the same time — exactly the defect #265 finding 11 named test-first. There are five states partitioning every reachable (status, items.length) pair:

  • 'initial-empty' — nothing has ever loaded and none were given synchronously (status === 'idle', no items).
  • 'loading' — a fetch is in flight and there is nothing yet to show (a first-ever load).
  • 'stale-results' — the STALE-WHILE-REVALIDATE state: a fetch is in flight while a previous list is still on screen. loadStart never clears items, so the previous results keep rendering, filterable and selectable, until the matching loadSuccess/loadError lands.
  • 'success' — the current items are the result of a completed load, or were given synchronously and never superseded by a failed fetch.
  • 'error' — the most recent fetch failed. Per the same policy, items from an earlier successful load are left mounted and selectable; only the live region / a consumer's own error slot communicate the failure, so 'error' is reported the same whether or not stale items remain.
export type LoadProjection = 'initial-empty' | 'loading' | 'stale-results' | 'success' | '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[] }
  /** @intent("Mark an async option fetch as started; pass the request's id") */
  | { type: 'loadStart'; requestId: number }
  /** @humanOnly — atomic replacement passthrough to `combobox`; see its own
   * `loadSuccess` doc for the reconciliation this performs in one step. */
  | {
      type: 'loadSuccess'
      requestId: number
      items: string[]
      groups?: ComboboxGroup[]
      disabled?: string[]
    }
  /** @humanOnly */
  | { type: 'loadError'; requestId: number; error: string }
SelectionMode from @llui/components/patterns/searchable-select

Combobox — text input paired with a filtered listbox dropdown. User types to filter items, arrow keys navigate the filtered set, Enter selects. Supports single and multiple selection.

Beyond the sync filtered listbox the machine owns three additive surfaces:

  • Async option loading — status/requestId/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 groups — groups 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: ReadSignal<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'
    /** The group label's id, or absent when `hasLabel: false`. */
    'aria-labelledby': string | undefined
    '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': ReadSignal<boolean>
    'aria-disabled': ReadSignal<'true' | undefined>
    'data-state': ReadSignal<'selected' | undefined>
    'data-highlighted': ReadSignal<'' | undefined>
    'data-disabled': ReadSignal<'' | 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': ReadSignal<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': ReadSignal<'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': ReadSignal<boolean>
    'aria-controls': string
    'aria-disabled': ReadSignal<'true' | undefined>
    id: string
    disabled: ReadSignal<boolean>
    'data-scope': 'searchable-select'
    'data-part': 'trigger'
    'data-state': ReadSignal<'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: ReadSignal<string>
  /** Whether a selection exists (drive showing/hiding the clear button). */
  hasValue: ReadSignal<boolean>
  /** The filter input rendered inside the popup, above the listbox. */
  input: {
    type: 'text'
    role: 'combobox'
    autocomplete: 'off'
    'aria-autocomplete': 'list'
    'aria-expanded': ReadSignal<boolean>
    'aria-controls': string
    'aria-activedescendant': ReadSignal<string | undefined>
    id: string
    value: ReadSignal<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': ReadSignal<'true' | undefined>
    'aria-multiselectable': ReadSignal<'true' | undefined>
    tabindex: -1
    'data-state': ReadSignal<'open' | 'closed'>
    'data-status': ReadSignal<AsyncStatus>
    /** The mutually-exclusive load projection (#265 finding 11); mirrors the
     * top-level `loadState` signal. See `combobox`'s `LoadProjection`. */
    'data-load-state': ReadSignal<LoadProjection>
    'data-scope': 'searchable-select'
    'data-part': 'content'
  }
  /** The mutually-exclusive async load projection: `'initial-empty'` |
   * `'loading'` | `'stale-results'` | `'success'` | `'error'` — never
   * independent booleans that can contradict each other. Documents the
   * stale-while-revalidate policy: `'stale-results'` means a fetch is in
   * flight while the previous items are still mounted and selectable;
   * `'error'` is reported the same whether or not stale items remain
   * mounted underneath it. */
  loadState: ReadSignal<LoadProjection>
  /** 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
  /**
   * A labelled group's parts. The group names its `label` part through
   * `aria-labelledby`; a consumer that renders the group WITHOUT that label
   * passes `{ hasLabel: false }` so it never names an element that does not
   * exist (an unlabelled `role="group"` is valid; a broken idref is not — the
   * `dialog` `hasDescription` rule, #268). Default: true.
   */
  group: (id: string, options?: { readonly hasLabel?: boolean }) => 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 once the
   * list is settled (`loadState` is `'success'` or `'initial-empty'`), the
   * error text on `'error'`, and nothing while a fetch is in flight
   * (`'loading'` / `'stale-results'`). */
  liveRegion: {
    role: 'status'
    'aria-live': 'polite'
    'aria-atomic': 'true'
    'data-scope': 'searchable-select'
    'data-part': 'live-region'
    text: ReadSignal<string>
  }
  /** Empty-state container. `hidden` is false only when the SETTLED filtered
   * list is empty — never while loading, revalidating, or after a failed
   * fetch (the live region / an error slot own those). */
  empty: {
    'data-scope': 'searchable-select'
    'data-part': 'empty'
    hidden: ReadSignal<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

chipHue() from @llui/components/styles

The hue for a category value — stable across sessions, machines and releases, never inside a {@link RESERVED_HUE_ARCS} band, and always one of {@link CHIP_HUES}.

Feed it to --chip-hue; the chip's fill and ink are derived from that one number by the recipe.

function chipHue(value: string): number
chipHueAt() from @llui/components/styles

The hue for the index-th member of an ORDERED set, walked by a golden-angle stride so that neighbours in the sequence are far apart on the wheel.

This is where --chart-1..5 and the chip scale agree: a chart with more series than the five chart tokens define needs exactly this — an unbounded, reserved-hue-respecting categorical scale allocated by position rather than by name. Wraps (and repeats) past {@link CHIP_HUE_SLOT_COUNT}; a negative or fractional index is normalised rather than rejected, because the caller is usually an array index and a throw there would be a worse failure than a repeat.

function chipHueAt(index: number): number
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
isReservedHue() from @llui/components/styles

Whether hue falls inside a {@link RESERVED_HUE_ARCS} band.

function isReservedHue(hue: number): boolean

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

ReservedHueArc from @llui/components/styles

A hue arc withheld from the categorical scale because a reader assigns it STATUS meaning. Half-widths are categorical: the band spans the hues a viewer would name as that colour family, which is a coarser and more robust boundary than a perceptual-difference threshold at the chip's low chroma.

export interface ReservedHueArc {
  /** What the band protects. */
  readonly name: 'crit' | 'warn' | 'ok'
  /** Centre hue, degrees. */
  readonly center: number
  /** Degrees either side of {@link center} that are excluded. */
  readonly halfWidth: number
}
ThemeTokens from @llui/components/styles
export interface ThemeTokens
  extends ThemeBaseTokens, ThemeDerivedTokens, ThemeScaleTokens, ThemeChipTokens {}
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 }>
}

Constants

CHIP_HUE_SLOT_COUNT from @llui/components/styles

How many distinct chip colours the scale has.

Twelve is the largest count whose slots stay at least 21 degrees apart on the 252 degrees the reserved arcs leave, and 21 degrees is roughly where two chip FILLS stop being reliably distinguishable (their chroma after the mix is only ~0.062–0.081, so a hue step buys little OKLab distance — the more saturated ink carries the rest of the signal). Raising it makes two categories that "have different colours" look the same, which is worse than an honest collision: at twelve slots, values that collide are IDENTICAL rather than subtly-different-but-not-really.

const CHIP_HUE_SLOT_COUNT
CHIP_HUES from @llui/components/styles

The scale itself: {@link CHIP_HUE_SLOT_COUNT} hues, evenly spaced by ARC LENGTH over the unreserved hues and centred in their slots. Even spacing by arc length is what makes the minimum separation a guarantee — skipping a reserved band only ever ADDS degrees between two neighbours.

"Centred in their slots" is centring in the concatenated ARC space and carries no promise about distance to a reserved edge: 131.5 lands 0.5 degrees off ok's edge. See the reserved-hues note at the top of this file.

const CHIP_HUES: readonly number[]
RESERVED_HUE_ARCS from @llui/components/styles

The three status families, in ascending hue. Non-overlapping by construction and asserted to be so by packages/components/test/styles/chip-hue.test.ts — {@link CHIP_HUES} derives the allowed arcs by walking the gaps between them, which is only correct while they are disjoint.

crit is centred on --destructive (27.325 light, 22.216 dark; the band contains both). warn and ok are the amber and green a traffic light trains readers to expect.

const RESERVED_HUE_ARCS: readonly ReservedHueArc[]

@llui/components/chart

Functions

chartForcedColorPatterns() from @llui/components/chart

Five SVG <pattern> fills, one per {@link ChartSeriesCue} name — the SAME cue vocabulary data-series-cue already carries on every mark. A skin's forced-colors rule reads fill: var(--llui-chart-fill-dot) (say), which connect() below sets to THIS chart's own url('#<id>:pattern-dot') CanvasText, so a bar/area mark gets a REAL redundant cue: fill: CanvasText alone makes every bar/area series under forced-colors: active paint identically, since forced colors flattens author colors uniformly (#264) — a dash pattern (already used for LINE marks) does nothing for a filled shape's fill.

Pure, static, stateless markup — not part of connect()'s REACTIVE parts (it never varies with data or state), but keyed by the SAME id connect() takes, and it MUST be. A fixed, globally-shared id (id="llui-chart-pattern- dot" on every chart instance) resolves a url(#...) reference to WHICHEVER same-named element the browser's id table happens to return — measured in real Chromium: when the first such element in the document sits inside a display:none ancestor (one hidden chart earlier on the page), every OTHER, visible chart's pattern-filled marks paint nothing, because a referenced paint server inside a non-rendered subtree does not paint even for a consumer outside it (#264 review item 3). Per-instance ids close the whole bug class rather than depending on document order: each chart only ever references its OWN copy. Place it once as the first child of parts.svg in either skin, passing the SAME id given to connect().

function chartForcedColorPatterns(id: string): Mountable
connect() from @llui/components/chart
function connect(state: ReadSignal<ChartState>, send: Send<ChartMsg>, opts: ChartConnectOptions): ChartParts
geometry() from @llui/components/chart

The derived geometry for a state. Exported so a consumer can measure or test a chart without mounting one.

function geometry(state: ChartState): ChartGeometry
init() from @llui/components/chart
function init(opts: ChartInit): ChartState
update() from @llui/components/chart
function update(state: ChartState, msg: ChartMsg): [ChartState, never[]]

Types

ChartCoord from @llui/components/chart
export type ChartCoord = 'cartesian' | 'polar'
ChartDomain from @llui/components/chart

How the INDEPENDENT axis is allocated — and therefore which of a chart's two normalized coordinates carries the magnitude.

value gives every category an equal slot and reads magnitude off v: a column, a line, a radar spoke. share allocates each category a slot in PROPORTION to its value and lets v span the whole depth, so the magnitude has moved onto u.

That one move is the whole of a pie chart, which is why neither a 'pie' mark type nor a second projection exists here. Under coord: 'polar' a share-allocated bar IS a pie or donut wedge; under coord: 'cartesian' the SAME state is a single full-width 100%-share bar, which is the honest cartesian reading of the same numbers. Switching coord still re-projects one dataset rather than swapping charts — see docs/adr/0003-charts-project-rather-than-branch.md.

export type ChartDomain = 'value' | 'share'
ChartMsg from @llui/components/chart
export type ChartMsg =
  /** @intent("Switch between cartesian and polar projection") */
  | { type: 'setCoord'; coord: ChartCoord }
  /** @intent("Allocate the independent axis by equal slots or by share of the total") */
  | { type: 'setDomain'; domain: ChartDomain }
  /** @intent("Set the row under the cursor, or clear it with null") */
  | { type: 'setActive'; index: number | null }
  /** @intent("Move the keyboard cursor along the rows by delta, wrapping") */
  | { type: 'moveActive'; delta: number }
  /** @intent("Move the keyboard cursor to the first row") */
  | { type: 'firstActive' }
  /** @intent("Move the keyboard cursor to the last row") */
  | { type: 'lastActive' }
  /** @intent("Isolate one series, or show them all again with null") */
  | { type: 'setActiveSeries'; key: string | null }
  /** @intent("Replace the plotted rows") */
  | { type: 'setRows'; rows: ChartRow[] }
  /** @intent("Pin the low end of the value axis, or derive it again with null") */
  | { type: 'setMin'; value: number | null }
  /** @intent("Pin the high end of the value axis, or derive it again with null") */
  | { type: 'setMax'; value: number | null }
  /** @intent("Stack the series instead of overlaying them") */
  | { type: 'setStacked'; stacked: boolean }
  /** @intent("Set the viewBox size in user units") */
  | { type: 'setSize'; width: number; height: number }
  /** @intent("Set the polar inner radius as a fraction of the outer") */
  | { type: 'setInnerRadius'; value: number }
  /** @intent("Swap the cartesian axes for a horizontal bar chart") */
  | { type: 'setHorizontal'; horizontal: boolean }
ChartSeriesCue from @llui/components/chart
export type ChartSeriesCue =
  | 'solid'
  | 'short-dash'
  | 'dot'
  | 'long-dash'
  | 'dash-dot'
  | 'grid'
  | 'cross-hatch'
MarkType from @llui/components/chart
export type MarkType = 'line' | 'area' | 'bar'

Interfaces

ChartCategoryTick from @llui/components/chart
export interface ChartCategoryTick {
  index: number
  label: string
  x: number
  y: number
  anchor: 'start' | 'middle' | 'end'
  baseline: 'auto' | 'middle' | 'hanging'
  active: boolean
}
ChartConnectOptions from @llui/components/chart
export interface ChartConnectOptions {
  /** Base id; the title and description ids derive from it. */
  id: string
  /**
   * Whether the consumer renders the `desc` part (default: true). When false,
   * the svg's `aria-labelledby` names only the title, never an element that
   * does not exist (the `dialog` `hasDescription` rule, #268). The TITLE is
   * the image's name — required for `role="img"` — and is always referenced.
   */
  hasDescription?: boolean
}
ChartGeometry from @llui/components/chart
export interface ChartGeometry {
  frame: Frame
  domain: Domain
  band: Band
  /**
   * The proportional allocation of the independent axis under
   * `ChartState.domain === 'share'`, and `null` otherwise. It is the axis
   * itself, not a decoration: the pointer hit test reads it instead of `band`.
   *
   * With more than one bar series each ring gets its OWN allocation from its
   * own values — that is what makes a nested donut's rings each proportional —
   * so a single `u` names different rows on different rings and the pointer
   * has to follow one of them. It follows the FIRST bar series, which is the
   * whole story for the single-series pie that is the common case.
   */
  slices: ShareSlice[] | null
  projection: Projection
  marks: ChartMark[]
  vertices: ChartVertex[]
  gridLines: ChartGridLine[]
  categoryTicks: ChartCategoryTick[]
  /** Anchor for the tooltip, in user units. `null` when nothing is active. */
  tooltipAt: { x: number; y: number } | null
  tooltipRows: ChartTooltipRow[]
}
ChartGridLine from @llui/components/chart
export interface ChartGridLine {
  value: number
  label: string
  d: string
  x: number
  y: number
  anchor: 'start' | 'middle' | 'end'
  baseline: 'auto' | 'middle' | 'hanging'
}
ChartInit from @llui/components/chart
export interface ChartInit {
  series: readonly ChartSeries[]
  rows?: readonly ChartRow[]
  coord?: ChartCoord
  domain?: ChartDomain
  label?: string
  description?: string
  width?: number
  height?: number
  padding?: Partial<ChartPadding>
  min?: number
  max?: number
  tickCount?: number
  stacked?: boolean
  innerRadius?: number
  horizontal?: boolean
}
ChartMark from @llui/components/chart

A drawn mark: one series, one path.

export interface ChartMark {
  seriesKey: string
  seriesCue: ChartSeriesCue
  label: string
  mark: MarkType
  /** The SVG path `d`. */
  d: string
  /** Row index, for a `bar`/wedge — `null` for a whole-series line or area. */
  index: number | null
  active: boolean
  dimmed: boolean
}
ChartPadding from @llui/components/chart
export interface ChartPadding {
  top: number
  right: number
  bottom: number
  left: number
}
ChartParts from @llui/components/chart
export interface ChartParts {
  root: {
    'data-scope': 'chart'
    'data-part': 'root'
    // Spelled out rather than aliased: `scripts/test/registry-attrs.test.ts`
    // reads part-bag VALUES syntactically, and an imported alias reads as an
    // open type it declines to give a verdict on.
    'data-coord': ReadSignal<'cartesian' | 'polar'>
    'data-domain': ReadSignal<'value' | 'share'>
    'data-active': ReadSignal<'' | undefined>
    /** Per-instance forced-colors fill custom properties — see `connect()`'s
     * own doc for why these must be per-chart rather than a shared global. */
    style: string
  }
  /**
   * The `<svg>`. `role="img"` with a name and description is what a screen
   * reader announces; the real data path is {@link ChartParts.table}, because
   * AT support for the WAI-ARIA graphics roles is still thin enough that a
   * chart relying on them alone is unreadable.
   *
   * `tabindex="0"` makes the plot itself the keyboard target: arrows walk the
   * rows, Home/End jump to the ends, Escape clears. There is no
   * `aria-activedescendant` — it is not valid on `role="img"`, and the tooltip
   * live region is what actually announces the cursor.
   */
  svg: {
    'data-scope': 'chart'
    'data-part': 'svg'
    role: 'img'
    'aria-labelledby': string
    viewBox: ReadSignal<string>
    tabindex: 0
    onKeyDown: (e: KeyboardEvent) => void
    onPointerMove: (e: PointerEvent) => void
    onPointerLeave: (e: PointerEvent) => void
    onBlur: (e: FocusEvent) => void
  }
  title: { id: string; 'data-scope': 'chart'; 'data-part': 'title' }
  desc: { id: string; 'data-scope': 'chart'; 'data-part': 'desc' }
  /** The visually-hidden `<table>` fallback. Render it with the rows below. */
  table: {
    'data-scope': 'chart'
    'data-part': 'table'
    'aria-label': ReadSignal<string>
  }
  /** Tooltip ATTRIBUTES — spreadable, with its own reactive `hidden`. */
  tooltip: {
    'data-scope': 'chart'
    'data-part': 'tooltip'
    role: 'status'
    'aria-live': 'polite'
    hidden: ReadSignal<boolean>
    style: ReadSignal<string>
  }
  /** A `<g>` stacking layer. Static — spread it on each layer group. */
  layer: { 'data-scope': 'chart'; 'data-part': 'layer' }
  /** A value gridline. Pass `d` from the {@link ChartGridLine}. */
  grid: { 'data-scope': 'chart'; 'data-part': 'grid' }
  /** An axis label — value ticks and category names. */
  axisLabel: { 'data-scope': 'chart'; 'data-part': 'axis-label' }
  /** Attributes for one vertex dot on a line or area series. */
  dotProps: (vertex: ChartVertex) => {
    'data-scope': 'chart'
    'data-part': 'dot'
    'data-series': string
    'data-series-cue': ChartSeriesCue
    'data-active': '' | undefined
    cx: number
    cy: number
  }
  legendItem: (key: string) => {
    type: 'button'
    'data-scope': 'chart'
    'data-part': 'legend-item'
    'data-series': string
    'data-series-cue': ReadSignal<ChartSeriesCue>
    'data-dimmed': ReadSignal<'' | undefined>
    'aria-pressed': ReadSignal<boolean>
    onClick: (e: MouseEvent) => void
  }
  /** The legend's colour chip. Spreadable onto its own element (a `<span>` in
   * both skins) so a forced-colors rule can key off `data-series-cue` the
   * SAME way a mark does — a legend swatch that only carries `--mark-color`
   * paints identically for every series once forced colors flattens author
   * colour, which is the accessibility gap #264 review item 7 names. */
  legendSwatch: (key: string) => {
    'data-scope': 'chart'
    'data-part': 'legend-swatch'
    'data-series-cue': ReadSignal<ChartSeriesCue>
  }
  /** Attributes for one drawn mark. Spread onto a `<path>` and pass `d`. */
  markProps: (mark: ChartMark) => {
    'data-scope': 'chart'
    'data-part': 'mark'
    'data-mark': 'line' | 'area' | 'bar'
    'data-series': string
    'data-series-cue': ChartSeriesCue
    'data-active': '' | undefined
    'data-dimmed': '' | undefined
    d: string
    onPointerEnter: (e: PointerEvent) => void
  }
  // Derived geometry, as signals the view renders with `each`.
  marks: ReadSignal<ChartMark[]>
  vertices: ReadSignal<ChartVertex[]>
  gridLines: ReadSignal<ChartGridLine[]>
  categoryTicks: ReadSignal<ChartCategoryTick[]>
  tooltipRows: ReadSignal<ChartTooltipRow[]>
  activeLabel: ReadSignal<string>
  rows: ReadSignal<ChartRow[]>
  series: ReadSignal<ChartSeries[]>
}
ChartRow from @llui/components/chart

One row of the independent axis: a category and its value per series.

export interface ChartRow {
  label: string
  values: Record<string, number>
}
ChartSeries from @llui/components/chart
export interface ChartSeries {
  /** Stable key. Also the `--color-<key>` variable a skin reads. */
  key: string
  /** Human label, used by the legend, the tooltip and the a11y table. */
  label: string
  mark: MarkType
  /** Ignored by a polar projection, which supports `linear` only. */
  curve?: Curve
}
ChartState from @llui/components/chart
export interface ChartState {
  coord: ChartCoord
  /**
   * Equal slots per category (`value`), or slots proportional to the value
   * (`share`). See {@link ChartDomain}; `share` + `coord: 'polar'` is a pie.
   *
   * Under `share` only `bar` series are drawn: a line or an area along an axis
   * whose spacing already encodes the magnitude would plot each point at a
   * position that means something other than where it sits, so they are
   * DECLINED rather than approximated — the same call `polarProjection` makes
   * about `monotone`. `stacked` is ignored for the same reason, each category
   * being its own slice already.
   */
  domain: ChartDomain
  series: ChartSeries[]
  rows: ChartRow[]
  /** Accessible name for the whole chart. */
  label: string
  /** Longer description, announced with the name. */
  description: string
  width: number
  height: number
  padding: ChartPadding
  /**
   * Explicit value-axis bounds. UNBOUNDED-CAPABLE in the sense of `finiteBound`
   * (#177): "derive it" is spelled by OMITTING the key, never by `undefined`,
   * `null` or `±Infinity` — only the omission survives a JSON round trip
   * unchanged.
   */
  min?: number
  max?: number
  tickCount: number
  /** Stack series instead of overlaying them. */
  stacked: boolean
  /** Row index under the pointer or the keyboard cursor. */
  activeIndex: number | null
  /** Series key the legend has isolated, or `null` for all. */
  activeSeries: string | null
  /** Polar only: inner radius as a fraction of the outer (0 = pie, .5 = donut). */
  innerRadius: number
  /** Cartesian only: swap the axes for a horizontal bar chart. */
  horizontal: boolean
}
ChartTooltipRow from @llui/components/chart
export interface ChartTooltipRow {
  seriesKey: string
  label: string
  value: number
  /**
   * The row's fraction of its series' total under a share domain — what a pie
   * tooltip shows as a percentage — and `null` under a value domain, where a
   * share of an axis that may cross zero is not defined.
   */
  share: number | null
}
ChartVertex from @llui/components/chart

A vertex on a line or area series, for the dot layer and hit feedback.

export interface ChartVertex {
  seriesKey: string
  seriesCue: ChartSeriesCue
  index: number
  x: number
  y: number
  active: boolean
}

Constants

chart from @llui/components/chart
const chart

@llui/components/icon

Functions

icon() from @llui/components/icon

Render one Iconify glyph as a real <svg>.

name is prefix:name ('lucide:star', 'simple-icons:github'; a bare name is a Lucide glyph) or a ReadSignal<string> of one, in which case the glyph follows the signal. props spread onto the <svg>; class is passed through as given, so a caller sizes and colours the glyph from there.

function icon(name: string | ReadSignal<string>, props?: ElProps): Mountable
loadIcon() from @llui/components/icon

Resolve one glyph, batched: one HTTP request per PREFIX per tick, not one per icon. A page rendering a dozen chevrons asks Iconify once; the batch is flushed on a macrotask so every icon mounted during the same view build joins it. Exposed so a consumer can warm the cache ahead of a mount.

function loadIcon(name: string): Promise<IconData | null>

Interfaces

IconData from @llui/components/icon

One resolved glyph: its body markup and the box it was drawn in.

export interface IconData {
  body: string
  width: number
  height: number
}

Constants

iconConfig from @llui/components/icon

Where glyphs are fetched from. Point this at a self-hosted Iconify to drop the third-party CDN; the path shape is identical.

const iconConfig