Mixins / foldkit-mixins-ui

Views and components of this package

These views take no @foldkit/ui bundle. Call X.view(options, h). Most own no Model and no Message; the components among them that keep a small Model (Menu, Menubar, Toast, Sheet, TagsInput, TreeView, Carousel, Resizable, ColorPicker, InputCopy, MessageScroller, Questionnaire, AngleSlider, Timer, Marquee, ImageCropper, SignaturePad, Chart, CascadeSelect, FloatingPanel, Tour) say so, with its init and update. Every one takes an optional style of its slots and mixins beside it. Style the slots with the matching recipe. Hide text with U.srOnly from foldkit-mixins/utilities. Name a control with FieldAssociation; Label.view takes the id that derived.

ts
import { Style } from 'foldkit-mixins'
import { AspectRatio, AspectRatioSlots, Icon, IconSlots, Recipes } from 'foldkit-mixins-ui'

const Frame = Style.forSlots(AspectRatioSlots)(Recipes.AspectRatio({ ratio: 'video' }))
const Mark = Style.forSlots(IconSlots)(Recipes.Icon({ size: 'sm' }))

// `h` is the view's builder. `content` is `Html`.
AspectRatio.view({ content: h.span([], ['poster']), style: Frame }, h)
Icon.view({ content: h.span([], ['!']), label: 'Warning', style: Mark }, h)
Icon.view({ content: h.span([], ['!']), style: Mark }, h)

A label makes the icon role="img". Without one the glyph is aria-hidden. Icons.glyph is the other tool: a mask on an element that already exists.

ViewCallRecipe
Accordion{ id, items: { id, title, content, disabled? }[], open, onToggle, headingLevel? }; Accordion.toggled(open, id, { multiple?, collapsible? }) is the transition update applies, the open ids being the application's. Each trigger is a button in a heading naming its panel; a shut panel stays drawn and inert. The panel opens on a 0fr-1fr grid row, the chevron turns, and the open title turns semibold in its own width. The view takes an icon in place of the recipe's.variant: plain (default), bordered, separated, filled, ghost; separators: on (default, between sections only), off; indicator: chevron (default), plus; indicatorPosition: end (default), start; active: none (default), trigger, item (the open section tinted). Slots root, item, heading, trigger, label, chevron, icon, panel, body, content.
Alert{ layout, title, description?, icon?, actions?, onDismiss?, dismissLabel? }, and by layout: 'inline' takes assertive? and is a live region inside the content, of something that just happened (role="alert" when assertive, role="status" otherwise); 'banner' takes label and is a full-width strip across a page or a section, there on load, so a section landmark named by label rather than a live region, which would say nothing on load. Each action is { label, href } (a link) or { label, onClick } (a button); onDismiss draws a close button named Dismiss <title> inline, Dismiss <label> as a banner. The icon is aria-hidden. Whether it shows is the application's. The root writes data-layout and data-dismissible.tone: info (default), success, warning, error; emphasis: subtle (default, the family's tint) or bold (its fill). Slots root, icon, title, description, actions, action, dismiss.
AngleSliderAfter Zag's: a dial in degrees, 0 at the top and clockwise. AngleSlider.init({ id, value? }) is the angle and the drag, folded with AngleSlider.update(layout), where layout (step, largeStep, marks, restrictToMarks) is the object the view spreads in too: AngleSlider.view({ ...layout, model, onMessage, label, name?, format?, valueText?, showValue?, disabled?, readOnly?, invalid?, style? }, h). A press or drag on the dial sends Pointed and Released; the APG's slider keys reach update as what was pressed, and go round past 0. A name draws a hidden input for a form.tone, track (ring, plain, disc), value (inside, below), size. Slots root, label, control, needle, thumb, markers, marker, markerLabel, value, input.
AspectRatio{ content? }. Slot frame.ratio: square (default), video (16/9), portrait (3/4). The corner reads radius.
Avatar{ name, src?, initials?, status?, statusLabel? }. The root is one role="img" named by the person (and the status, "Ada Lovelace, Online"); initials show until the picture covers them, the picture's alt empty. status (online, away, busy, offline) is a dot on the lower end corner. Avatar.group({ people, max?, label?, moreLabel?, style?, avatarStyle? }) overlaps them, each ringed in the page, and counts the rest as one more avatar. An avatar beside a name and a line is an Item with the avatar as its media.size: xs, sm, md (default), lg, xl; shape: circle (default), rounded, square; color: neutral (default) or name (initials in a hue from the name); ring: none, page, accent. Slots root, fallback, image, status. Recipes.AvatarGroup({ overlap: 'tight' | 'loose' | 'none' }) over AvatarGroupSlots (root).
Badge{ text, tone? } writes data-tone.Recipes.Badge({ attribute, tones, appearance? }) maps that attribute; values outside the map keep the base. appearance: tinted (default), dot, outline, solid. Slot badge.
Breadcrumb{ steps: { label, href?, current?, icon? }[], separator?, collapse? }. current is text with aria-current="page"; the others are links. An icon draws before its label, hidden from assistive technology. separator is drawn between steps in place of the recipe's; collapse: { max, onExpand, label? } folds the middle of a long trail into an ellipsis (Breadcrumb.collapsedOf(steps, max) is the split). collapse: { max, menu: { model, onMessage, mixins } } makes the ellipsis open a Menu of the hidden steps instead: the Menu's Model is the application's, built from Breadcrumb.menuRows(steps, max), and Breadcrumb.chosen(steps, max, value) is the step a row leads to.separator: slash (default), chevron or dot, drawn where the view is given no separator of its own. Slots root, list, item, link, current, icon, separator, ellipsis, dots.
ButtonGroup{ label, items }. role="group". The buttons stay the caller's. Segmented is the control that selects.No axis. Slot root.
CalendarOne date system, inline: Calendar.Selection is Single, Range or Multiple, owned by the application (Calendar.empty(mode), fold each SelectedDate through Calendar.picked, Calendar.isComplete says when a picker may close). Spread Calendar.inputs(selection, mixins, { h }) into upstream Calendar.view's inputs: its toView (Calendar.toView(selection, mixins, { h })) marks each chosen day aria-selected, a range's ends data-selected with data-range-start/data-range-end and every day between data-in-range, and its maybeSelectedDate (Calendar.selectedDate(selection), a Single selection's day) agrees with it. The grid of a Range or Multiple selection is aria-multiselectable. A selection in a popover is the DatePicker's. Options, the fourth argument of toView, inputs and resolve (CalendarOptions): weekNumbers, caption: 'label' or dropdowns: { model, onShow } (month and year selects; answer onShow with Calendar.show), paging with Calendar.alongside(model, other, offset) for two months side by side, and preview: { date, onPreview } for a range under the pointer. ResolvedDays' paging buttons are Options and its heading is a caption (Grid, Label, Dropdowns). Presets: Calendar.lastDays, nextDays, monthOf, day, drawn by Calendar.presetsView. Calendar.localeOf('de-DE') reads upstream's locale from Intl.size (the Calendar's); outside: shown (default), hidden. Slots CalendarSlots.
Card{ title?, description?, content?, image?, media?, action?, footer?, press?, label?, selected?, disabled?, dismiss? }. press is { onClick } or { href, external? }: one button or link stretched over the card, named by label or the title (a pressable card with neither throws); action, footer and the dismiss button stay pressable above it. selected is aria-pressed on the button, aria-current on the link. dismiss: { onDismiss, label?, always? } is revealed on hover and focus unless always, and always on a touch screen. The root carries data-selected, data-disabled, data-pressable, data-dismissible (the header then keeps clear of the button).orientation: stacked (default), inline (image or media leading, footer trailing). surface: outlined (default), elevated, filled, plain. padding: compact, comfortable (default), roomy, flush. elevation: 0 to 3, a step of the surface ladder (lighter in a dark scheme; a contact and an ambient shadow in a light one). lift: none (default) or hover, a lift under the pointer that travel stills. Card.overlay is the stretched press control, shared with Item. Slots root, image, media, header, title, description, action, content, footer, overlay, dismiss.
Card.group{ cards: (CardView & { key })[], columns?, fluidHover?, label?, cardStyle? }. A keyed list of cards in columns (default 1); each item carries data-row and data-column. With fluidHover (default) one highlight follows the pointer to the nearest card through FluidHover from foldkit-primitives/dom, reading both directions when there is more than one column, and a press in a gap presses the highlighted card. cardStyle styles every card that names no style of its own.Recipes.CardGroup: layout: divided (default, hairlines between rows and columns), separated. border: none (default), outlined. Use cards of surface: 'plain' inside. Slots root, list, item, highlight.
CarouselThe APG carousel: Carousel.init({ id, slide?, autoplay?, delay? }), folded with Carousel.update({ count, perView?, loop? }), drawn with Carousel.view({ label, model, slides, onMessage, loop?, perView?, picker?, thumbnails?, slideLabel?, previousLabel?, nextLabel?, pickerLabel?, rotationLabels?, style? }, h). A group with aria-roledescription="carousel", each slide a group named "2 of 5"; previous and next (aria-disabled at the ends unless loop), a dot per place the track can rest (or thumbnails in their place), and a polite status naming the slide shown. The track is a CSS scroll-snap scroller, so swipes move it natively, and ScrollSettle reports where a swipe rests, so the dots follow the finger. Every press, swipe and timer reaches update, which returns the Carousel.ShowSlide Command that scrolls the track: do not keep the slide yourself. With autoplay it moves on every delay (4s), the current dot filling as it waits; it pauses under the pointer, stops when focus arrives by the keys, quiets its status while it rotates, and draws the APG's rotation button first.peek: none (default) or edge. Slots root, track, slide, controls, rotation, previous, next, picker, dot, thumbnail, status.
CascadeSelectAfter Zag's: one value chosen as a path of ids through nested options. CascadeSelect.init({ id }) holds the chosen path and the one the keys are on while open, folded with CascadeSelect.update(config) (config is { nodes, parents?, closeOnSelect?, loop? }, the same object the view takes): CascadeSelect.view({ model, config, onMessage, label, placeholder?, levels?, layout?, expandOn?, clearable?, separator?, name?, disabled?, invalid?, direction?, style? }, h). The trigger is a combobox opening a top-layer APG tree, a column per level or one level at a time (layout: 'drill'; the columns also drill in on their own when they would not fit the window, measured as the popup opens); the arrows move within and between levels (swapped right to left), Home, End and a letter jump, Enter and Space choose, and only an option without children is chosen unless parents allows any.size, trigger (outline, filled, ghost), width (trigger, auto). Slots root, trigger, valueText, chevron, clear, input, layer, backdrop, panel, column, heading, back, item, itemText, branch.
ChartAfter shadcn's Chart: Chart.view({ id, label, kind: 'bar' | 'line' | 'area', categories, series: [{ name, values, color? }], model, onMessage, description?, format?, legend?, table?, categoryLabel?, style? }, h), or kind: 'donut' with slices: [{ name, value, color? }] and totalLabel?. The value axis's ticks are round and reach both ends, zero included; the plot is drawn in percentages, so it fills its container's width with nothing measured and renders the same on a server. Chart.Model (Chart.init(), folded with Chart.update) holds the lit point: the pointer lights a category's band and its tooltip, and the plot is a tab stop whose arrows, Home and End step through the points while a polite status says each. The drawing is hidden from assistive technology, which reads a data table, visually hidden unless table: 'shown'. Series take chart-1 to chart-5 in order, or the color they name; a sixth series throws.height (sm, md, lg), grid (lines, zero), indicator (dot, line). Slots root, title, description, legend, legendItem, swatch, body, yAxis, xAxis, yTick, xTick, plot, canvas, paths, grid, cursor, bar, line, area, dot, slice, band, center, centerValue, centerLabel, tooltip and its parts, status, table and its parts.
ChatMessageOne message of a chat: ChatMessage.view({ from, content, author?, avatar?, time?, status?, onRetry?, actions?, pinActions?, streaming?, style? }, h), from being user, assistant or system. The user's sits at the end in a bubble, an answer as text at the start. status is sending, sent or failed, which draws a retry; actions are named buttons ({ key, label, onClick, icon?, pressed?, disabled? }) shown on hover and focus unless pinned. A streamed answer is the same message drawn again with more content and streaming: true (aria-busy); its body wraps with text-wrap: wrap, so no earlier word moves line. ChatMessage.group({ items, continues?, style?, messageStyle? }, h) draws a run, a sender's following messages continued. Markdown is the application's to render (foldkit-richtext-markdown's parse, foldkit-richtext-dom/view's renderDocument).layout (chat, thread), bubble (muted, accent), content (text, markdown). Recipes.ChatMessageGroup for the group. Slots root, avatar, main, header, author, time, bubble, body, footer, status, retry, actions, action.
CheckboxGroup{ id, legend, options: { value, label, description?, disabled? }[], checked, onToggle, error?, selectAll? }; toggled(checked, value) is one option's transition, coverage(options, checked) is none | some | all (a disabled option counts for neither), and setEnabled(options, checked, on) is every enabled option or none, a disabled one keeping the state checked gives it. selectAll: { label, onSet } draws a first row whose checkbox is checked, mixed or unchecked by coverage and controls every option; a press sends onSet with setEnabled's values, on unless all already are. A fieldset named by its legend, each option the APG's checkbox (role="checkbox") named by a label for it, so a press on the words toggles it. error is shown under the options and the group is aria-invalid, described by it.variant: plain (default), list (checked rows fill, and merge where they stand together; a vertical list has no gap, or the block would split). orientation: vertical (default), horizontal (a wrapping row). The checkbox sits on the label's row, so a description does not pull it down. Slots root, legend, options, option, checkbox, label, description, error.
CodeBlock{ code, tokens?, language?, title?, lineNumbers?, copy? }. tokens are { from, to, kind }, as foldkit-richtext-code and foldkit-richtext-code-shiki tokenizers return them; each is drawn with data-kind, and a token out of order or outside the code throws. The block's text is the code exactly. copy: { onCopy?, copied? }: onCopy gets the code, so run copyText from foldkit-primitives; without it the button has no handler, for a page whose script copies. CodeBlock.linesOf is the cut.No axis. Colors each kind from the theme's syntax group; data-line-numbers numbers lines, data-copied marks a copy done. Slots root, header, title, copy, body, code, line, token.
ColorPickerAfter ReUI's: a saturation and brightness area, hue and alpha Sliders, an eyedropper where the browser has one (ColorPicker.Eyedrop, canEyedrop), text in hex, rgb, hsl or oklch, and presets as a radio group of swatches. The color, its format and a draft being typed are ColorPicker.Model (init({ id, color?, format? })), folded with ColorPicker.update: keys and drags reach update as what was pressed, so fast keys each step from the last color. Drawn with ColorPicker.view({ model, onMessage, label, presets?, alpha?, eyedropper?, labels?, disabled?, style?, slider? }, h); ColorPicker.popover(options, h) is a Popover's toView that draws it behind a swatch. ColorPicker.color('#3b82f6') reads a literal in code (it throws on a typo); format, parse (an Option) and toOklch are pure.size. Its Sliders take Recipes.Slider({ track: 'paint', label: 'hidden' }). Slots root, label, area, areaThumb, controls, eyedropper, channels, channel, swatch, fields, format, input, presets, preset, picker, trigger, value.
Command{ id, label, placeholder?, groups: { heading?, items: { value, label, description?, keywords?, shortcut?, icon?, opensPage?, disabled? }[] }[], query, onQuery, active, onActive, onRun, empty?, loading?, page?, hints?, kbd? }, a shortcut being a tuple of keys (['⌘', 'K']) drawn as Kbd with kbd. page: { trail, onBack } draws the pages stepped into as crumbs before the field, and Backspace in an empty field sends onBack; a row with opensPage has a chevron. loading says it and marks the list aria-busy, its rows still shown; hints are a footer of keys. Command.hotkey(message) is ⌘K or Ctrl+K anywhere, for an application to lift with Subscription.persistentEntry and open its Dialog on; Command.filtered(groups, query) is what a query leaves. shadcn's Command and Fluid's CommandMenu: the field points at the highlighted row through aria-activedescendant, ↑ and ↓ wrap through enabled rows, Enter runs the highlighted one; rows never re-sort, and a filtered-away highlight moves to the first enabled row. The list stands in a frame beside its bar, which Command.overlayBars drives (see Overlay scroll bars). A shortcut reads left to right on a right-to-left page.frame: card (default) or bare, for inside a dialog. Slots root, search, crumbs, crumb, input, list, group, heading, item, icon, text, label, description, shortcut, chevron, status, empty, loading, frame, bar, thumb, footer, hint.
Empty{ title, description?, icon?, action? }. The title takes the subtitle role; the icon is aria-hidden.media: icon (default), illustration; frame: none (default), dashed, filled; tone: neutral (default), danger; size: compact, comfortable (default). Slots root, icon, title, description, action.
FloatingPanelAfter Zag's: a non-modal window. FloatingPanel.init({ id, rect, open? }) holds its rect, stage and a drag, folded with FloatingPanel.update(limits) (min, max, grid, overflow); FloatingPanel.open(model, { opener }) focuses it and close gives focus back. FloatingPanel.view({ model, onMessage, title, content, order?, draggable?, resizable?, closeOnEscape?, step?, direction?, labels?, style? }, h): the drag trigger moves it and eight edges resize it, kept within the viewport; the arrows move it from the drag trigger and resize it from the bottom-end corner; minimize, maximize, restore and close. A press or focus inside returns the Raised OutMessage, which the application folds into the order its panels stack by with FloatingPanel.raise(order, id).header. Slots root, boundary, panel, header, dragTrigger, title, controls, minimize, maximize, restore, close, body, handle.
HoverCard (recipe)Styles @foldkit/ui's HoverIntent (HoverIntentSlots): a trigger read as a link, a raised panel fading in on the fast tier as it is drawn. Place the panel with HoverIntent.toView's anchored, which shows it in the top layer with an arrow; the open and close delays are HoverIntent's (200ms and 300ms by default).No axis. Slots trigger, panel, arrow.
Icon{ content, label? }. A label is role="img"; without one the glyph is aria-hidden.size: sm, md, lg on the type scale; none (the default) stands on the size ladder, 16px or 14px compact. Slot glyph.
ImageCropperAfter Zag's: the image fitted in a viewport, a selection that a drag moves (Move) and eight handles resize, zoom by a Slider, the wheel and +/- with the selection kept where it is on screen, a pan of the zoomed image, quarter turns, an aspect lock and a round crop. ImageCropper.init({ id, aspect?, shape?, crop?, min?, maxZoom? }) holds the crop, in the turned image's pixels, folded with ImageCropper.update; keys (arrows, Shift ×10, Alt to resize) step by screen pixels. Drawn with ImageCropper.view({ model, onMessage, label, src, crossOrigin?, controls?, labels?, disabled?, style?, slider? }, h). ImageCropper.crop(model) is the crop in whole pixels (an Option); RequestedExport({ type, quality }) returns the Export Command, answered by Exported({ url, width, height }) or FailedExport.guides, handles (all, corners), viewport (landscape, wide, square). Its zoom takes Recipes.Slider({ label: 'hidden' }). Slots root, label, viewport, stage, pan, image, selection, guides, handle, controls, zoom, rotate, reset.
InputCopyA read-only value with a copy button, Zag's clipboard: InputCopy.view({ id, label, value, copied, onCopy, copyLabel?, copiedLabel?, style? }, h). The view is stateless: answer onCopy with the InputCopy.Copy({ text }) Command, which yields foldkit-primitives/dom's ClipboardMessage (Copied or CopyFailed, never a throw); copied is the application's, and InputCopy.Model keeps it: InputCopy.init({ resetAfter? }) (default 2000ms), folded with InputCopy.update, where Copied on ClipboardMessage.Copied shows it copied and starts the Reset Command that sets it back unless a later copy came first; pass model.copied. The button is named Copy <label>; copied, it reads Copied and a polite status region says so.No axis. Slots root, label, group, input, button, status.
ItemThe row primitive: { content?, media?, title?, description?, actions?, press?, label?, selected?, disabled? }; content follows the title and description. press is { onClick } or { href }, one control stretched over the row (overlay, as a Card's) and named by label or the title, actions staying pressable above it; selected is aria-pressed or aria-current. Item.group({ items: (ItemView & { key })[], label?, style?, itemStyle? }) is a list of them. Field is for form controls.variant: plain (default), outline, muted; density: comfortable (default), compact; media: plain (default), icon, image. Slots root, media, content, title, description, actions, overlay. Recipes.ItemGroup({ layout: 'divided' | 'separated' }) over ItemGroupSlots (root, row).
Kbd{ key }. Names the key. Kbd.combo({ keys, joiner? }, h) draws several in one kbd, with an optional joiner between them.No axis. Slots key, combo, joiner.
Label{ for, text }. for is the control's id.No axis. Slot label.
MarqueeAfter Zag's: content that scrolls on without a seam. Marquee.init({ id }) holds what it measured and whether it is paused, folded with Marquee.update; Marquee.view({ model, onMessage, label, items, side?, speed?, autoFill?, repeat?, delay?, loopCount?, style? }, h) measures its content (Resize) and draws copies to fill the viewport, inert after the first. A pointer, focus or its pause press (WCAG 2.2.2) holds it; Marquee.Message.Paused() holds it from outside. Its motion is CSS, and under reduced motion the viewport scrolls instead.spacing, edges (fade, none). Slots root, viewport, content, item, pause.
MenuOne menu, opened three ways: Menu.init(id, rows, { isAnimated?, isModal? }) (a row with items hosts a submenu, one level deep; isAnimated gives the menu's and its submenus' panels upstream's transition marks, for the recipe's fade, and isModal makes the rest of the page inert while the menu is open), folded with Menu.update, drawn with Menu.view({ model, rows, buttonContent, onMessage, opensAt?, direction?, isRowDisabled?, mixins, submenuMixins? }, h), the style being one of the mixins ([MenuStyle.mixin]). By a button (opensAt unset); at a point (opensAt: 'point', with Menu.target(onMessage, h) on the area: a right press records its point and the contextmenu after it opens the menu there). A row is an ItemRow ({ label }), a CheckRow (check: 'checkbox' | 'radio', checked: menuitemcheckbox or menuitemradio with aria-checked; choosing it sends Selected and leaves the menu open, the application keeping checked, a radio row's group being the rows of its group), or a SubmenuRow (items). Any row may name a group (rows next to each other with one group stand under its heading, a separator between groups), a shortcut (keys drawn at the row's end as Kbd with the view's kbd, named in aria-keyshortcuts; the menu binds nothing) and tone: 'destructive' (data-tone). A submenu's row is a menuitem with aria-haspopup="menu" and aria-expanded; ArrowRight, Enter or Space opens it on its first row, ArrowLeft or Escape closes it back to the row (under direction: 'rtl', drawn as the root's dir, the submenu opens on the left, its chevron points there, and the arrows swap), a pointer resting 150ms opens it, and choosing a leaf closes everything. Selected names the value and the submenu it came from. Pass Menu.fluid beside the style (in submenuMixins, not mixins, where rows open submenus). Its panels, submenus included, draw in the top layer. A @foldkit/ui Menu the application folds itself, for typed items or groups, is drawn through the panel adapter instead, and opens at a point with Menu.drawAtPoint and Menu.pointTarget (see Why some components needed a fork).No axis. Slots MenuSlots.
MenubarA row of @foldkit/ui Menus, shadcn's and the APG's menubar: Menubar.view({ label, model, menus, onMessage, direction?, style?, menuMixins }, h), each menu's { label, rows, isRowDisabled? } taking a Menu's rows (none opening a submenu), its Model (Menubar.init(ids, { isAnimated?, isModal? })) folded with Menubar.update, whose Selected OutMessage names the menu and the row. One trigger is the tab stop; ArrowLeft/ArrowRight (Home/End while all are shut) move it, swapped under direction: 'rtl' (drawn as the bar's dir), and with a menu open they open the next on its first row. While one is open, pointing at another opens it, and opening any shuts the rest. Recipes.MenubarMenu is the Menu recipe with a bar's flat triggers.No axis. Slots bar, menu; each menu's are MenuSlots.
MessageScrollerA conversation's log that follows the newest message while the reader is at the end and stops when they scroll up. MessageScroller.init({ id }) holds only whether it follows and how many arrived unseen; the scroll position is the browser's, read by its FollowEnd Mount. Fold MessageScroller.update, and send Message.Appended({ count }) when your update appends messages (not when it loads older ones, which it holds in place). MessageScroller.view({ model, label, content, onMessage, jumpLabel?, unseenLabel?, style?, areaStyle?, areaMixins? }, h) draws a named role="log" in a ScrollArea, with a jump button counting what arrived while away; give its parent a height.No axis. Slots root, log, jump, count.
Meter@foldkit/ui's Meter: Meter.view({ id, label, value, min?, max?, low?, high?, optimum?, valueText?, style? }, h) draws a meter named by its label. The root says how good the value is as data-level (good, fair, poor), as the HTML meter judges it; Meter.levelOf is that judgment. A measurement, where Progress is work.No axis; the fill takes success, warning or error by level. Slots root, label, track, fill.
NavTwo layers. Nav.toView(mixins, { h }, label, draw?) styles @foldkit/ui's stateless Nav, drawing nav > ul > li > a with the current link aria-current="page". Nav.view({ model, entries, label, onMessage, orientation?, direction?, mixins? }, h) is a navigation menu: links and triggers whose panels of link columns (groups of { label, href, description?, current? }) show in the top layer, with the APG's disclosure-navigation keys (Escape, ArrowDown into a panel, the arrows, Home and End along the row; a press outside and focus leaving close it); which panel shows is Nav.init(id)'s Model, folded with Nav.update. Nav.fluid beside the style glides a line to the current link and a faint pill to the one the pointer nears.indicator: underline (default), pill; orientation: 'vertical' is a column whose panels open beside it. Slots nav, list, item, link, trigger, chevron, panel, groups, group, heading, links, panelItem, panelLink, title, description, indicator, highlight.
NumberField{ id, label, description?, value, onChange, min?, max?, step?, page?, format?, disabled? }; NumberField.settled(value, bounds) clamps a value onto its step. The APG's editable spinbutton: an input with role="spinbutton" that the arrows, PageUp and PageDown, Home and End step, a typed number committing on change, between steppers out of the tab order that disable at their bound.No axis. Slots root, label, group, input, decrement, increment, description.
OtpField{ label, value, onChange, length?, pattern?, groups?, disabled? }; OtpField.kept(text, length, pattern) filters a code. shadcn's input-otp: one real input (autocomplete="one-time-code", paste and Backspace the browser's) over cells that mirror it, the next cell marked data-next for its caret.No axis. Slots root, input, group, cell, separator.
Pagination{ page, pageCount, onPage, label?, siblings?, boundaries?, numbered?, ends?, range? }. Fewer than two pages draws nothing. Pagination.itemsOf(page, pageCount, window?) is the stops; Pagination.summaryOf(page, { total, perPage, text? }) is "1–10 of 97", drawn before the stops when range is given. range.choose: { sizes, onPerPage, label? } adds a select of rows per page (labelled Rows per page by default) that sends onPerPage with the size; the application sets perPage and a page that keeps its rows in view. numbered: false leaves previous and next alone; ends draws first and last. The current stop holds / 12 in a pageCount span hidden from assistive technology.compact: narrow (default, below a 30rem window), always or never. Compact shows only the current number, with its / 12, and no ellipses. The current stop reads aria-current. Slots root, summary, perPage, perPageSelect, first, prev, next, last, page, pageCount, ellipsis.
PasswordInputPasswordInput.view({ id, label, value, onInput, visible, onToggle, autocomplete?, toggleLabel?, description?, invalid?, disabled? }, h): a password field whose toggle button (aria-pressed, aria-controls, one fixed name) shows it as text. autocomplete is current-password (default) or new-password; spellcheck and autocorrect stay off. strength is a Meter view's options, drawn under the field; requirements are { label, met }[] (PasswordRequirement), listed and each read as met or not (requirementStates); capsLock: { on, onKey } says Caps Lock is on, politely, onKey given capsLockOf(key, modifiers) for each key (an Option: none for a key that says nothing of it). The app owns the text, the flag and the Caps Lock reading.size (sm, md, lg). Input's pieces in InputGroup's box; the eye is Icons.glyph by aria-pressed. Slots root, label, group, input, toggle, description, strength, requirements, requirement, requirementState, capsLock.
Progress@foldkit/ui's Progress: Progress.view({ id, label, value, min?, max?, valueText?, style? }, h) draws a progressbar named by its label. value is an Option; none is indeterminate, and the bar's fill sweeps toward the inline end, leftward right to left. The root carries the share done as --fk-progress. showValue draws the value (valueText, else a percentage) at the end of a bar's label line and inside a ring. A value given as ProgressSections is one stacked bar of toned parts, read aloud as their sum; buffer draws how far the work could go behind the fill; segments cuts the track into steps.shape: bar (default) or ring, Circular Progress; tone: accent, neutral, success, warning, error, info (Meter's tokens); stripes: none (default), still, moving (travel-aware); size: sm, md, lg. Slots root, label, track, indicator.
QrCodeAfter Zag's: QrCode.view({ id, value, label?, level?, quietZone?, overlay?, tooLong?, style? }, h), one img named QR code: <value> (or label), every dark module one SVG path over a background that carries the quiet zone. level is the error correction, M by default and H under an overlay (a logo at the centre). QrCode.encode(text, level) gives the matrix, none when no version holds the text, and pathOf its path; both are pure. QrCode.Download({ id, format, filename, pixels }) saves the drawn code as PNG or SVG in the colors it shows; the overlay is not in the file.scheme (light, dark on light on any page, or page), size. Slots root, frame, background, pattern, overlay, fallback.
QuestionnaireAn assistant's questions answered in the chat. Questionnaire.init({ id, questions, layout? }) with Questionnaire.single, multiple and text questions (Question is a Schema, so a tool's input decodes into it), folded with Questionnaire.update, which says what is missing and focuses it, and returns OutMessage.Answered({ answers }) once every required question is answered; the form then gives way to a summary. Questionnaire.view({ model, onMessage, title?, style?, radioGroup?, checkboxGroup?, input?, textarea?, button?, submitButton? }, h): the controls are the package's RadioGroup, CheckboxGroup, Input and Textarea, each taking its own style.variant (plain, card), actions (end, start). Slots root, title, progress, question, prompt, words, field, other, otherLabel, error, actions, summary, asked, answer.
RatingRating.view({ id, label, value, onChange, max?, starLabel?, readOnly?, disabled?, style? }, h): a radiogroup of native radios named by its label, one per star under one name, so the browser's arrows move the choice and a form submits it. value is an Option; none is unrated. Read-only, it is one img whose name says the rating. precision: 0.5 draws two radios a star; a read-only value of any fraction fills its last star part way. onClear is sent by a press on the star already chosen; showValue shows starLabel of the rating beside the stars, so a word per value is one function. Home and End go to the ends through the Rating.Ends Mount (resolve it in a Scene test with a choice's Message). A read-only rating (readOnly: true) takes no onChange.tone: warning (default, gold), accent or error; glyph: star (default), heart, circle, or --_fk-rating-shape; size: sm, md, lg. Stars are a CSS clip-path on each radio's ::before; pointing previews the rating. Slots root, label, star.
ResizableResizable.Model (init(share)), its Message and update(model, message, bounds), which the application keeps and folds; Resizable.view({ model, onMessage, label, first, second, orientation?, direction?, min?, max?, step? }). shadcn's Resizable and the APG's window splitter: the handle is a separator with its value, stepped by the arrows and sent to the bounds by Home and End; a drag follows the Move Mount from where it began over the group the Resize Mount measured. Under direction: 'rtl', drawn as the root's dir, the first panel is on the right, so ArrowLeft and a drag to the left grow it. `collapsible: 'first''second'(withcollapsedTo) collapses that panel on Enter or a double-click and puts it back where it was, easing there while a drag follows the pointer at once; pass the same bounds to update` and the view. A split fills its panel, so splits nest either way.handle: line (default; a thicker accent line under the pointer) or grip (a pill of dots); data-orientation lays it out. Slots root, first, handle, second.
ScrollArea{ content }. The height that makes it scroll is the caller's mixin, on viewport or root; a width goes on root, which the viewport fills and the bars lie over. The viewport is tabbable. A bar per axis is drawn beside the viewport, and ScrollArea.overlayBars beside the style lays them over its edge, driven by ScrollBars from foldkit-primitives/dom: a thumb that rests 4px wide, widens to 6px under the pointer, and fades once scrolling and the pointer rest; a coarse pointer keeps the browser's bar. See Overlay scroll bars.orientation: vertical (default), horizontal, both; vertical and horizontal fade the content toward an edge with more past it and draw a hairline there. Slots root, viewport, bar, thumb.
Separator{ orientation?, label?, decorative? }, horizontal by default. A label sits between two drawn rules and is read as text; decorative is role="none". A vertical rule needs its container to set the cross size.orientation: horizontal (default), vertical; line: solid, dashed, dotted; thickness: thin, thick; spacing: none, sm, md, lg; inset: none, sm, md. The line is a border in outline.default, which a dark page shows. Slots rule, root, label.
SheetA Dialog its handle drags, vaul's Drawer: Sheet.init({ id, side?, points?, opensAt?, dismissible?, isAnimated? }), folded with Sheet.update and opened with Sheet.open and Sheet.close (each also reporting the Dialog's Opened or Closed), drawn with Sheet.view({ model, onMessage, mixins, hasDescription?, covered?, scalesPage?, draw }, h), draw laying out DialogSlots' bundles with h.div(sheet.handle) in the panel. The panel stands on side's edge (data-side); a drag of the handle follows the pointer and settles at the nearest of points (the share of the panel shown, ascending), or, flicked or dragged far enough past the first, closes it, unless dismissible: false. The Dialog keeps focus, Escape and scroll lock. scalesPage with Recipes.sheetPage on the page beside the sheet, never around it, steps the page back while it is open.Recipes.Sheet: size: sm, md (default), lg, full; handle: bar (default) or none. Slots are DialogSlots.
Sidebarshadcn's sidebar with Fluid's collapse, keeping a Model: Sidebar.init({ id, rows, open?, collapsed?, side? }) (a Tooltip per row id, and the phone's Sheet), folded with Sidebar.update, drawn with Sidebar.view({ model, onMessage, label, groups, header?, footer?, direction?, collapse?, style?, mixins?, tooltip?, sheet? }, h). groups are { id, title?, collapsible?, rows }, a row a SidebarLink ({ id, label, href, current?, icon?, badge? }) or a SidebarBranch of nested links (items). Groups and branches open like an accordion's, several at once; the current page is aria-current and Sidebar.fluid glides a marker to it. collapse: 'offcanvas' (default) takes the whole column off the page; 'icons' leaves a rail of icons named in Tooltips (tooltip, a Tooltip style). With sheet (a Sheet style), an opener shows it in a Sheet on a phone. Shut groups and a collapsed column stay drawn and inert. A resizable width is a Resizable whose first is the sidebar with width: 'fill'.width: 'fill'. Slots root, header, footer, opener, title, toggle, nav, section, heading, trigger, chevron, links, item, branch, link, icon, label, badge, tip, indicator, highlight.
SignaturePadAfter Zag's: strokes drawn by one pointer over a dashed line and "Sign here", undo, clear, and a typed name instead. SignaturePad.init({ id, pen? }) holds the strokes (points with pressure, in the pad's coordinates), folded with SignaturePad.update. Drawn with SignaturePad.view({ model, onMessage, label, fallback?, labels?, disabled?, style? }, h). pathOf(stroke, pen) and toSvg(model, { color?, font? }) are pure; RequestedPng({ scale, background }) returns ExportPng, answered by ExportedPng({ url, width, height }) or FailedExport.surface (plain, muted, dashed), size. Slots root, label, control, segments, stroke, guide, placeholder, typedName, controls, undo, clear, fallback, fallbackLabel, input.
Skeleton{ label? }, role="status", a bar a film of the text. Skeleton.lines({ lines, label? }), avatarRow, card and tableRow({ columns }) draw the shapes content takes, in a group.shape: bar (default), circle; animation: pulse (default), shimmer, none. Slots bar, group.
SliderSlider.view({ id, label, value, onChange, min?, max?, step?, format?, marks?, showValue?, name?, disabled?, style? }, h), where value is a number or a { low, high } range and onChange is handed the same kind. Native input type="range" thumbs, so drag, keys, right to left and the form value are the browser's. One number is one thumb across the track, named by the label; a press anywhere on the track moves it. A range is a group named by its label holding two thumbs named by thumbLabels (default Minimum and Maximum), each bounded by the other (the low one's max is the high value), so the browser stops a drag or a key at the other thumb; Slider.moved(range, 'low' | 'high', value, bounds) snaps, bounds and orders a range, and returns the same range when nothing changed. marks are values dotted on the track (data-filled where the fill covers them) and showValue draws each value, both hidden from assistive technology, which hears aria-valuetext. @foldkit/ui's Slider is not adapted: draw its Submodel with Slots of your own. orientation: 'vertical' stands it upright; ticks dots every step; a mark may be { value, label } (SliderMark), its words under the track.tone: accent (default) or neutral; size: sm, md (default), lg, the thumb's edge; track: thick (default; the fill reaches each thumb's far edge, Fluid's look), thin or paint (a track a holder paints, as the color picker's); value: above (default), inside (widens the thumb into a pill; best at md or lg) or end (at the end of the label's row); label: shown (default) or hidden. Slots root, label, control, track, fill, mark, value, thumb.
Spinner{ label?, showLabel? }. role="status"; showLabel draws the label beside the wheel. A turn is about 720ms times travel.kind: ring (default), dots, bars, pulse, orbit; size: sm, md (default), lg; tone: accent (default), neutral, current. Slots wheel, part, root, text.
StepsSteps.view({ label, steps: { title, description?, icon?, content? }[], current, onSelect?, style? }, h): a stepper, an ordered list named by label. The step at current is aria-current="step", those before it data-state="complete" (a check replaces the number), those after upcoming. An icon stands in the marker for its number; content follows the step, beside the line in a vertical stepper. With onSelect(index), completed steps are buttons for going back; steps ahead never are. The application owns current. progress (0 to 1) fills the connector after the current step when the recipe's connector is a progress bar.orientation: horizontal (default) or vertical; size: xs, sm, md, lg (the Avatar's sizes; unset, 1.75rem); marker: number or dot; connector: line or progress. Slots root, item, step, trigger, marker, text, title, description, content, connector.
TableThe presentational table: { columns: { header, value, numeric?, sortKey? }[], rows, rowKey?, caption?, fluid?, sort?, selection?, expansion?, empty? }. A column with a sortKey heads with a button that sends sort.onSort(column), and the sorted column carries aria-sort; Table.nextSort(sort, column) cycles ascending, descending and none. selection puts a checkbox on every row and one in the heading, mixed while only some are chosen; expansion opens a row's detail in a row below it (aria-expanded, aria-controls); empty stands across the table while there are no rows (an Empty.view). The sort, the chosen and the open rows are the application's. With fluid, one highlight behind the body rows follows the pointer, skipping detail and empty rows. Filtering, column reorder and resize, and virtualized rows stay with foldkit-data-grid.rows: lined, striped, plain; density: compact, regular, comfortable (none set follows the region); header: static, sticky. Slots frame, highlight (with fluid), root, caption, head, headerRow, headerCell, sortButton, sortIcon, body, row, cell, controlCell, checkboxLabel, expandButton, expandIcon, detailRow, detailCell, emptyRow, emptyCell.
TagsInputZag's tags input, a small Model of its own: TagsInput.init({ id, tags?, delimiter?, max? }), folded with TagsInput.update (which returns a focus Command), drawn with TagsInput.view({ model, onMessage, label, placeholder?, disabled? }, h). The tags are a list of chips named by the label, each with a Remove <tag> button, then a text input. Enter or the delimiter (default ,) adds the draft split at the delimiter and trimmed, refusing empty and repeated parts, and parts past max: a refusal (TagsInput.Refusal: Repeat, Empty, Full) is said in a polite message (worded by refusalText) and shakes the field once, the shake travel-aware; Backspace on an empty draft removes the last tag; a removal returns focus to the input. Keys reach update, which decides from its own draft.No axis; data-disabled on control. Slots root, label, control, list, tag, remove, input, message.
ThinkingIndicatorAn assistant at work: ThinkingIndicator.view({ label?, detail?, active?, icon?, style? }, h) draws dots, a pulse or the view's icon beside words a band of light sweeps, and a detail line, in a polite status. Keep it drawn with active: false before work starts, so what it says when it starts is announced. Every movement holds under reduced motion.mark (dots, pulse, none), label (shimmer, plain, hidden), tone (muted, accent). Slots root, mark, dot, icon, label, detail.
ThinkingStepsThe trace of what an assistant did: ThinkingSteps.view({ id, steps, open, onToggle, duration?, summary?, openSteps?, onToggleStep?, stateWords?, style? }, h). Each step is { id, title, state, duration?, detail?, icon?, steps? }, state being pending, running, done or failed, said in words too. The button says the running step's title, or "Thought for 4s". Its openness is yours, an Option<boolean>: none opens it while a step runs and shuts it once all are done, and the press sends the state the reader asked for. openSteps and onToggleStep make each step's detail collapsible (Accordion.toggled).variant (plain, card), connector (line, none). Slots root, trigger, summary, chevron, panel, list, step, head, marker, title, state, duration, body, detail.
TimerAfter Zag's: a countdown (Timer.init({ id, countdown: true, duration })) or a stopwatch (Timer.init({ id, interval? })), folded with Timer.update, whose Commands (Timer.Stamp, Timer.Tick) read Effect's Clock, so TestClock drives a run. Timer.view({ model, onMessage, label, units?, separator?, labels?, style? }, h): a role="timer" with each unit in a Slot, one press that says what comes next, and reset. Timer.phase(model) is idle, running, paused or done; to start at once, return Timer.update(model, Timer.Message.Pressed({ action: 'start' })) from init.tone, look, progress, size. Slots root, label, area, item, separator, controls, action.
ToastA stack of toasts, Sonner's: Toast.init(id, { position?, direction?, duration?, max?, threshold? }), folded with Toast.update, whose Acted OutMessage names a pressed action; Toast.show(model, { type?, title, description?, action?, duration? }) returns the stack and the new toast's id, Toast.settle(model, id, input) turns a toast (a loading one, a promise's) into its outcome, and Toast.dismiss(model, id) takes it away; draw it with Toast.view({ model, onMessage, label?, closeLabel?, expand?, icons?, style? }, h). The newest stand in front, spread out while the pointer or focus holds the stack, at most max (3) showing; a held stack or a hidden page pauses every countdown, keeping the time left; a swipe toward the edge past threshold dismisses one. A toast is spoken in a polite region, an error in an assertive one. A promise toast is show with type: 'loading', the work run as a Command, and settle on the toast's id when its Message comes back. ToastView is the fork for toasts that carry their own payload.tone: plain (default) or rich (each type on its tint). Slots container, entry, list, icon, body, title, description, action, close, status, alert.
TourSteps anchored beside their targets in the top layer, over a dim with a cutout the target's presses reach. Tour.init({ id }), started with Tour.start(model, config, { opener }) and folded with Tour.update(config) (config is { steps: [{ title, description, target?, placement? }] }); Tour.view({ model, config, onMessage, padding?, interactive?, dismissOnOutside?, closeOnEscape?, labels?, direction?, style? }, h). The target scrolls into view; Next, Back and the arrows move on, Escape and Skip end it with focus back to the opener, and update returns Ended { completed }. A step without a target, or whose target is missing, stands in the middle.No axis. Slots layer, blocker, spotlight, panel, arrow, progress, title, description, actions, back, next, skip.
TreeViewThe APG's and Zag's tree view, a small Model of its own: TreeView.init({ id, open?, checked? }), folded with TreeView.update(nodes, { checkable? }) (curried over the tree, flattened once), drawn with TreeView.view({ label, model, nodes, onMessage, direction?, style?, mixins? }, h) over nodes { id, label, children?, disabled?, icon?, openIcon? }. checkable (on the view and update) gives each row a box: the Model's checked holds the leaves, a branch reads checked, mixed or not from them (TreeView.checksOf), and Space checks the current row in update. One tab stop; Up/Down move through the rows showing, stepping over shut and disabled ones; Right opens or steps in, Left shuts or steps out (swapped under direction: 'rtl', drawn as the root's dir); Home/End; a character moves to the next showing row that starts with it; Enter, Space or a click chooses a row and opens or shuts a branch. Keys reach update, which decides from its own Model and sends a focus Command. Each item carries --fk-tree-level; a shut group stays drawn and inert.guides: none (default) or lines, a guide down each open branch; data-state on a branch item and its group. Slots root, item, row, chevron, checkbox, icon, label, group.
Typography{ level, text }. The level chooses the element (h1–h4, lead, body, small, muted, quote, code, and the interface roles display, title, subtitle, caption, micro as paragraphs).level, same set; a role is its size and leading on the size ladder. Defaults to body. Slot text.
VirtualList@foldkit/ui's virtual list: VirtualList.view({ model, items, itemToKey, itemToView, onMessage, overscan?, itemToRowHeightPx? | dynamicRowHeights?, style?, mixins? }, h) draws only the rows in view. Its Model is upstream's (@foldkit/ui/virtualList's init), kept by the application and folded with that module's update. A row's mixins may style it, not handle its events, since it is drawn inside the list's submodel. A bar is drawn beside the list, and VirtualList.overlayBars lays it over the list's edge: root runs ScrollBars for upstream's list, which holds a Mount of its own.No axis. Slots root (the box it scrolls within: 18rem tall by default and as wide as its container, offering 20rem where the container takes its width from it, so the rows drawn as it scrolls never resize it), row, bar, thumb.