aweft

@aweftjs/ui

Components and theming on top of @aweftjs/dom. h is dom's h plus the handful of things that need to know where the element sits: its theme, its state cells, its listeners. A theme is data you define once and match against, not a stylesheet you write. Everything one page owns is one object made per render, so a build can render two pages at the same time and neither can see the other's classes.

Quickstart

import { h, mount } from '@aweftjs/ui';
import { mutable } from '@aweftjs/core';

const Counter = () => {
	const clicks = mutable(0);
	const hovered = mutable(false);
	return (
		<button theme={['button', hovered.bool('hovered', null)]} isHovered={hovered}
			onClick={() => clicks.set(clicks.get() + 1)}>
			clicked {clicks} times
		</button>
	);
};

mount(document.body, <Counter />);

recipes/ui/ is the whole of this README as a running page. npx vite recipes/ui serves it.

Five rules

Each is stated again where it belongs. They are here together because each is easy to get wrong and the symptom does not name the cause.

  1. A theme value is a list of segments. theme={['button', 'quiet']} reaches button and button_quiet. theme="button_quiet" is one token naming one entry, which is how a part is reached: the element gets the variant's colours and none of button's box. (The theme.)

  2. each builds one shape per list. The row component under each renders the same tags in the same order on every row; a button on some rows and nothing on others is wrong, with no message. Vary a value, not the shape. Table's cell is not under that rule: it may answer a button on one row and null on the next. (Navigation and data.)

  3. The root entry paints nothing. Your own page entry sets background, color and a minHeight of the viewport, and the page's HTML carries <style>body { margin: 0 }</style>, because a theme entry cannot reach body and the host's white margin shows as a band around a dark page. (The look.)

  4. validate is handed the cell, not its value. Read it with cell.get(); a formatter writes back with cell.set(). (Composites, under Validate.)

  5. theme appends, on every component. <Card theme="tight"> is the card entry plus card_tight; the segments you pass never replace the component's own. (What every component takes.)

Source is .tsx

This package's own source is .tsx, compiled by this stack's own transform. JSX becomes plain h calls resolved by ordinary scope, so h has to be in scope in every file that writes JSX. Node cannot load .tsx on its own, so a suite that imports one runs with node --import @aweftjs/build/loader; the root gate does that for you.

An application bundles with aweft() from @aweftjs/build, which is one line in a vite config and is what recipes/ui/vite.config.ts shows. Pass aweft({ defaultH: '@aweftjs/ui' }) if your pages are ui pages: a file that writes JSX and imports no h of its own is given one, and without that setting it is dom's, which writes theme out as an attribute nothing reads.

Getting the packages. Nothing is published to a registry yet. An application takes this repo as a git submodule and its own package manager resolves @aweftjs/* through the workspace, which is how the five applications that use the stack do it. A program written outside a checkout has nothing to resolve those specifiers to, so recipes/ and the tests live inside the repo.

h, svg and html

h(tag, props, ...children) is dom's, with eight prop names taken off it:

propwhat happens
themea string, a list, a cell, or lists of those; flattened at mount into a class list and matched against the theme
classkept, and joined in front of the classes the theme generated. Without a theme it is an ordinary attribute
stylean object (or a string); a bare number in a size property gets px, and $var and $fn() resolve against the element's own theme chain
isHovered, isFocused, isClicked, isToucheda cell this package writes from real events. isFocused is the element's own focus, and isTouched is a pointer event that says it was a finger
onClick, onInput, onKeyDown, …on and an uppercase letter: a handler, handed to dom as $on<type> so a hydration replays it, and gone when its element goes. Your own $onclick beside it runs first

Everything else goes to dom unchanged: a bare name is an attribute, $name is a property.

An element with none of those is dom's h exactly, node and all, so const box = h('div', { class: 'box' }) is an element. An element with any of them is a mounter, not a node: the theme it gets depends on where it is mounted, and dom hands the mount context to a mounter and to nothing else. Hand it to mount as you would anything else.

svg is the same in the SVG namespace, with no theme: class and style stay plain attributes. html is dom's template-literal tag bound to this h.

On a component, each:name renames the loop item: <Row each:row={rows} /> hands each item to the component as props.row rather than props.each.

The theme

A theme is a flat object whose keys are _-joined selector paths.

Theme.define({
	'*': { $brand: '#1b6ef3' },
	tile: { padding: '$space4', borderRadius: '$radius', background: '$brand' },
	tile_flat: { boxShadow: 'none' },
});

An element's theme prop flattens to a class list with * in front of it. An entry matches when its segments appear in that list in order, with gaps allowed. So card_hovered matches an element themed card primary hovered, and hovered_card does not match anything themed card hovered. Entries are ordered by where their last segment matched, ties going to the longer entry, and later in that order wins.

A class token holding _ names one entry: that is how you reach a part. A part is a different element from its component, and it must not wear the component's own box, so theme="card_title" reaches card_title and not the bare card. A modifier is a state or a variant of the same element and stays a segment of its own, so theme={['card', 'tight']} reaches card and card_tight. Both spellings mix: theme={['card_title', 'muted']} reaches card_title and card_title_muted, and an element that wants a part and its component says both tokens.

Values. $name is a variable, $fn(a, b) is a call, $$ is a literal $, and $size$px is the variable followed by the text px. A bare number in one of sizeProperties gets px. What a variable holds is text, and that text is not read again: $a: '$b' writes the four characters $b into the CSS rather than following them. Point a declaration at the variable you mean.

Variables and functions are the same namespace. A $name whose value is a function is a function; anything else is a variable. Both are found by walking the matched chain from the most specific entry down, so a generic entry writes background: '$hover' once and every component supplies its own $hover. And because a function is theme data, an application defines its own and a nested theme replaces one:

Theme.define({ '*': { $em: (args) => `${Number(args[0]) * 16}px` } });

The colour functions ($shiftBrightness, $brightness, $saturate, $hue, $alpha, $invert, $contrast_text, $luminance) and the arithmetic ones ($add, $sub, $mul, $div, $mod, $min, $max, $floor, $ceil, $round, $if) ship in the default theme, so every one of them can be replaced the same way. A $name( nothing defines is written back out as it was read, so calc() and rgb() survive untouched.

Directives live inside an entry, keyed _name_rest:

directivecompiles to
extends: 'other' or ['a', 'b']what it extends applies first, so this entry wins over it; a list starting * replaces the inherited one
_elem_buttonbutton .awN { … }
_children_span.awN > span { … }
_cssProp_focus.awN:focus { … }; a pseudo-element gets ::, and a key written with its own colons is used as written
_media_(min-width: 40em)that entry's body inside the query
_container_(min-width: 28rem)that entry's body inside a container query
_starting_that entry's body inside @starting-style, the style a transition starts from. It takes nothing after the name
_keyframes_spin@keyframes spin-<id>, with $spin bound to the generated name
_fontFace_body@font-face { … }
_import_fonts@import url(…) layer(aweft);

@font-face, @keyframes and @import are emitted once per definition per render rather than with the entry's own rules, so two class chains reaching one entry emit the font once.

A directive block holds declarations and one more directive. A _media_ or a _container_ may hold a _cssProp_ and a _starting_; a _cssProp_ may hold a _starting_. That is what lets a dialog's ::backdrop fade: its transition has to reach .awN::backdrop and has to stay inside the reduced-motion query. A third level compiles to nothing, and an enclosing rule is dropped wherever its own body came out empty. _keyframes_, _fontFace_ and _import_ leave the entry body rather than wrapping the rule, so they are read at the entry's own level only.

A declaration whose value is a list is emitted once per item, in order. appearance: ['none', 'base-select'] writes appearance: none; appearance: base-select;, and a host that cannot read the second keeps the first. That is the only way an entry, which is an object, can say one property twice.

Every rule is inside @layer aweft, and this package ships zero !important. Unlayered styles beat every layer, so an application's own stylesheet overrides the library with a one-class selector and no specificity fight. packages/ui/tests/browser.test.ts asserts that in Chromium.

Two themes on one page. <Theme value={partial}> merges a partial theme onto whatever is above it for its subtree, and that subtree generates its own classes. <ThemeContext value="brand"> is different: it sets a prefix that ThemeContext.use(h => component) puts in front of every theme an element below asks for. An element with no theme prop stays plain; write theme="" to take the cascade and nothing else.

Values in a generated class are literals, not custom properties. The element's class attribute holds one minted name per distinct list, not the segments themselves, and that name's rules are written one block per matched entry, so a stylesheet has as many blocks for .aw1 as entries matched it.

The per-render object

const ui = context();
const body = await render(<App />, { context: ui });
const page = `<!doctype html><html><head><style data-aweft>${ui.theme.markup()}</style></head><body>${body}</body></html>`;

mount, render and hydrate each make one and thread it through dom's context. It carries theme (this render's class cache and stylesheet), ids (the counter behind an aria-labelledby, counting from zero per render so a server and a browser agree), popups, head (the page's head tags, and head.markup()), stage (one entry per live StageContext), and the language the page shows: context({ locale, catalog }), which the text tokens below look up. usedText(ui) answers the keys a render's tokens looked up, which is what a static walk reads to say what a catalog lacks.

render holds the head list and the stage list for the length of the call, so a caller can read both after the page has been taken down. A render object is therefore for one page.

mount puts the stylesheet in the document head and takes it back out when it unmounts. hydrate adopts the one the server wrote rather than making a second. render returns the item's markup only; the CSS is context.theme.markup(), which the page puts in its own head.

The element the mount puts there carries the sheet as it stood then, and nothing writes it again. A class compiled later (a hover state the pointer reaches, a theme cell that moves) arrives as a <style> of its own holding only what that compile added, and every one of them comes out with the mount. So reading the sheet back out of the DOM means every style[data-aweft], each carrying its own @layer aweft wrapper, while theme.markup() is the same rules as one stylesheet without reading the DOM at all. theme.watch(fn) hears each compile's CSS as it is added.

The reason is webfonts. A browser registers an @font-face by name when it parses the sheet that declares it. Change a stylesheet the document already holds and Chromium drops every face on the page and registers them again, whether the change is a rewrite, a rule put in through the CSSOM, or a write to a different element entirely; Firefox does the same when a sheet's text changes. Until the data is back, the page's text falls back to a system font. Nothing in this package declares a face, so nothing here showed it; an application that does hits it on the first hover (design 257).

Two mount calls into one page share that page's render, so two widgets on one page cannot mint the same class name for two different themes. Passing a context() by name asks for a render of your own instead, which is what a static render wants; two named renders in one page each count their classes from zero.

A component hydrates by identity, and the elements it makes on the way are dropped. hydrate builds the client's tree and pairs it against the nodes the server wrote, keeping the server's; the fresh elements it made to compare against are thrown away. So a hydration is not free of createElement, and counting those calls counts the client's tree, not a defect. packages/ui/tests/controls.test.ts pins both halves: every server element is kept by identity, and nothing the client made ends up in the page.

A hydrated page is live. Nothing this package computes is written onto an element: a handler is a $on<type> property and the class and the style are cells handed to dom as attributes, and dom carries all three onto the node the hydration adopted (design 133). So a click, a keystroke, a focus, a hover, a theme cell and an inline style all reach the page a server sent. browser.test.ts clicks, types, focuses and hovers for real in Chromium and reads the hovered tint back off the button the server wrote.

A theme mismatch across a hydration is not reported. dom catches a structural mismatch loudly, but if the client's theme registry says something different from the server's, the client rewrites the adopted <style> and says nothing. Define your theme in a module both sides import.

Reaching a ui system from a mount that has none is an assert naming what to call instead.

Contexts

const Tone = createContext('plain');
const Label = Tone.use((tone) => (props) => <span theme={['label', tone]}>{props.children}</span>);

mount(document.body, <Tone value="accent"><Label>hi</Label></Tone>);

createContext(def, transform) returns the provider, with def, read(context), node(context) and use(build) on it. transform(raw, parentValue, children) runs on first read and again after raw changes; raw arrives exactly as written, so a cell arrives as a cell.

use(build) calls build once per mount of the component it hands back, with the value read at that mount's own place in the tree. So two mounts under two different providers each get their own component, and a mount already on the page does not call build again when the value above it moves. A value that moves is read with read(context) or held in a cell the built component follows, not through use.

A node has id, parent, children and value(). children is an observable array in mount order, so something above can watch what appears below it. node(context) answering null is how a component asks whether there is a provider above it at all.

node(context) reads the tree at that point, not from the root: the node it answers is the nearest provider above the mount whose context you handed it, and parent walks up from there. There is no way in from outside, and that is the design: to introspect from elsewhere, capture a context inside the tree (a component's fourth argument, or a mounter's) and start from that.

Marks

A component that takes more than one slot of children reads them with mark and categories.

<Shown value={open}>
	<p>shown</p>
	<mark.else><p>hidden</p></mark.else>
</Shown>

A mark is a value, never a node, and never reaches mount. Inside a component:

const [popup, anchor] = categories(props.children, ['popup', 'anchor'], 'anchor');

Each category has items and a props merged from every mark of that name. An unknown slot, or a bare child with no default slot, is an assert naming the slots the component knows.

mark.name is declared for the eight slots this package reads (then, else, case, default, popup, anchor, tabs, panels). Any other name works at run time; in a TypeScript file write it as mark('name', props, ...children).

Control flow

Shown takes a condition, a then (the default slot) and a <mark.else>, with invert to flip it. Switch takes a value matched against <mark.case value=…> with a <mark.default>, or a cases object of cells where the first truthy key wins. Neither builds an element or keeps state.

Loading

const Article = suspend(Spinner, async ({ id }) => <Body article={await fetchArticle(id)} />, ErrorPanel);

The fallback shows, the loader runs, and what it resolves to replaces the fallback. The promise is declared pending, so a static render waits for it.

A rejection never leaves the fallback on screen. The call's own failed component shows it, or the LoaderContext's, and with neither the slot goes empty and the rejection is rethrown so the host reports it. LoaderContext carries { loading, failed } and inherits them one field at a time.

Every wait in this package shows the same loader (design 219). suspend while its loader runs, a Button while a promise its onClick returned is pending, and a FileDrop entry an application moved to status: 'loading' all show LoaderContext.loading, and LoadingDots when nothing named one. So naming a loader once names it for all three:

<LoaderContext value={{ loading: MySpinner }}><App /></LoaderContext>

No drawing ships (design 144). LoadingDots is three spans and a keyframes block; for an animated spinner, @aweftjs/icons says which set to install and shows the one line that puts it here.

Popups

<PopupContext>
	<App />
</PopupContext>

<Detached enabled={open}>
	<button onClick={() => open.set(!open.get())}>details</button>
	<mark.popup><Card>what floats</Card></mark.popup>
</Detached>

Everything that floats is a popup: a Tooltip, a Menu, a Select, and Popup itself. Every one of them has somewhere to go without being told. A popup's sink is the nearest <dialog> above where it was written, then the sink a PopupContext gave it, then the element the page was mounted into. So a page needs no wrapper to open one, and a menu opened inside a modal draws inside that dialog, which is what makes its rows clickable: a dialog's top layer swallows every pointer event aimed at anything outside it.

PopupContext is how a page chooses a sink on purpose. It renders its children and then the popups, so a popup is after the page in DOM order. The first one on a page takes the render's own sink; a second one, nested or beside it, makes its own, so a nested PopupContext renders its popups at the end of its own subtree rather than at the end of the page. Popup renders nothing where it is written and puts its element in whichever sink it found. Detached measures its own children, scores twelve placements and picks one, re-measures every animation frame, and closes when the anchor moves, on the reading that the page scrolled.

There is no z-index in this package. A popup asks for the top layer with the popover attribute where the host has one, and falls back to DOM order where it does not. Below the top layer a page with its own stacking context can put something over a popup, and that is the page's decision to make.

Detached mounts its anchor where the anchor was written and reads the anchor's nodes back out of the document, so a page taken over from a server adopts the anchor the server sent (design 153). trackedMount() is the other way of reaching children a component does not own: it returns the array the real nodes appear in and a mounter to render where they belong, and children mounted that way are built fresh rather than adopted.

Detached needs a real browser to open. It places its popup from the anchor's measured rectangle and asks for the next animation frame, and the light tree has neither layout nor frames. So in Node, and in a static render, a Detached renders its anchor, and its popup's children are in the markup inside a display: none container rather than left out of it. That is the right server output, and it is also why the interaction tests for it are in browser.test.ts rather than in the light tree. Popup on its own has no such need: give it a placement and it renders where you put it, anywhere.

Other exports

InputContext is where an input event goes: InputContext.fire(context, 'click', payload) calls the generic on and then on<Type>, with the application's meta merged in last. useAbort(fn) runs fn with a fresh AbortSignal and hands back the abort. sizeProperties is the set of property names a bare number is given px for.

What every component takes

Four props mean the same thing on every component in this package, controls, composites, display pieces and grouping pieces alike.

  • theme appends your own segments to the ones the component writes, so <Card theme="tight"> is the card entry plus card_tight and <Button theme={cell}> follows a cell.

  • class appends a plain class name, leaving the generated one alone.

  • element hands in the node to decorate instead of building one. An element of the wrong tag is an assert naming both tags, because a <div> wearing a checkbox's theme is a checkbox that does nothing.

  • Anything else goes to the element, so id, aria-* and a handler the component does not name land where you would expect them.

Every state prop is a cell, or absent. Given one, the component writes it and follows it; given none, it keeps its own. A display prop takes a value or a cell. There are no imperative handles.

Controls

Every control here is one native element with a theme on it. There is nothing in this package that draws a control out of divs, so the keyboard, the role, the form participation and the autofill are the platform's and not ours (design 128).

import { Button, Select, TextField, Toggle } from '@aweftjs/ui';
import { mutable } from '@aweftjs/core';

const email = mutable('');
const problem = mutable(null);

<TextField label="Email" value={email} error={problem} description="Work address" />
<Toggle label="Email me" value={subscribed} />
<Select label="Owner" value={owner} options={users} display={(user) => user.name} placeholder="Pick someone" />
<Button label="Save" onClick={() => save(email.get())} />
componentthe elementits own propssizeexample
Button<button>, or <a> with an hreflabel, type, icon, iconPosition, disabled, loading, round, inline, href, hrefNewTab, onClick, tracksm, lg, icon, icon-sm, icon-lgbutton
TextField<input>, in a <div> on input_group when it was given an addonvalue, label, description, error, leading, trailing, placeholder, password, onEnter, onKeyDown, disabled, typesm, lgtext-field
TextArea<textarea>the same, plus maxHeightsm, lgtext-area
Checkbox<input type="checkbox">value, label, invert, indeterminate, disabled, onChangesm, lgcheckbox
Radio<input type="radio">value (the group's), option (this one's), label, disabled, onChangesm, lgradio
Toggle<input type="checkbox" role="switch">value, label, disabled, onChange, typesm, lgtoggle
Slider<input type="range">value (a number), min, max, step, disabled, tracksm, lgslider
Selecta <button role="combobox"> in a <span> with its arrow, over a hidden <select>value, options, display, placeholder, open, disabled, name, autocomplete, onChangesm, lgselect
LoadingDotsthree dotstype, size, labela CSS lengthloading-dots
Icon<svg>name, size, label, rota CSS lengthicon

The last column is the file under recipes/ui/examples/, without its .example.tsx ending. Each one renders every state, type and size of its component, in both modes, on a page of its own at catalogue.html#/<Component>, and the recipe's driver reads it there (designs 197 and 226).

size is one theme segment, right after type (design 194): sm is 32px tall, nothing is 36px and lg is 40px, and the entries are button_sm, input_lg, checkbox_sm and so on, the same way quiet and danger are. A small control takes the smaller text step with it. Button also takes icon, icon-sm and icon-lg, which are a square of that height with no padding, for a button whose label is an icon. It is a value or a cell, like every other display prop. LoadingDots and Icon have a size of their own, which is a CSS length and not this axis.

A label is what gives a control a name. Give one and the component renders a <label for> next to the element, both inside one <div>, mints the ids off the render (so a server and its hydration agree), and wires aria-describedby and aria-invalid for you. Give none and you get the bare element, and naming it is yours. packages/ui/tests/controls.test.ts finds every control by its role and its name.

A Button with no name throws where it mounts, in development (design 266). Its name is its label, its text, an aria-label or title on it, or a label on the Icon inside it; a button that is only an unlabelled icon has none, a screen reader says "button" and nothing else, and the mount throws with those three ways to name it. The button is read once it and what is inside it are on the page, so a labelled Icon counts, and a static render() reads it the same way. A release build has no check: the statements are gone.

One document is one render. The ids come off the render's counter, which starts at zero every time (design 109), so two named renders mounted into the same document mint the same ids and their labels point at each other's controls. Two mount calls into one page share that page's render and do not collide; a second context() is a second render and belongs in a second document.

What a control starts with is in the markup. Checkbox, Radio and Toggle write the state they start in as the checked attribute; TextField writes its text as value and TextArea as its content. So a server page shows a ticked box and a filled field before any script runs. The property follows the cell once the page is alive, which is what the platform does with these attributes too: they say what the control started as, not what it holds now.

An error cell is announced when it arrives. While it says something the control carries aria-invalid, its aria-describedby names the message, and the message is a live region. Once the error clears the attribute is removed rather than set to false.

A promise makes a button busy. A promise onClick returns disables the button and shows the LoaderContext loader until it settles, however it settles, so a double click cannot submit twice.

A radio group is a group because its members share a name, minted once per value cell. So the arrow keys, the wrapping and the roving focus are the platform's. browser.test.ts presses the real keys: Space on a checkbox, the arrows in a radio group, Home and End on a slider.

TextField takes leading and trailing, and builds a box only when it was given one (design 210). With neither, the markup is the <input> and nothing else. With either, the input goes inside a <div> on input_group that carries the border, the radius, the fill and the height, and the input carries none of them, so the two read as one control. The focus ring is on the box, so tabbing into the input rings the whole thing. Text is wrapped on input_group_addon for you, and anything else is mounted as it is, which is what lets an Icon or a Button of size="icon" go there.

<TextField label="Price" leading="$" trailing="CAD" value={price} />
<TextField label="Search" leading={<Icon name="search" />} value={query} />

Select draws its own list, on every host (design 224). The closed control is a <button role="combobox">; the open list is a <div role="listbox"> of <div role="option"> rows in a popup placed under the control at the control's width. So it looks the same everywhere, the theme reaches every part of it, and open is a cell you can read and write. The list goes where every popup in this package goes, so it needs no wrapper above it.

It opens on a click, on ArrowDown, ArrowUp, Enter and Space, and on typing a letter, which opens it and jumps to the first row beginning with that letter in the one press. Inside it the arrows move and wrap, Home and End go to the ends, typing keeps searching, Enter and Space choose, and Escape closes and gives the keyboard back to the control. The focus stays on the control the whole time and aria-activedescendant says which row the keys are on.

A hidden <select> sits under it, off the screen and out of the reading order, carrying the same options and the same choice. It takes name and autocomplete, so a form the control is in posts the value and autofill has a real control to find; anything that writes it writes your cell. What autofill cannot do is draw its own highlight over the button, because the element it filled is one pixel square.

Select holds items, not the text a row reads as. options is a list of anything, display says what a person reads, and the cell holds the item you put in the list, so adding, removing or reordering leaves the choice on its own item. An item that is a string or a number carries itself as the hidden option's value; an object carries none, and the platform then posts what the row reads as.

Select draws its own arrow (design 195). Every host draws a different one and no theme can reach any of them, so the entry tells every host to draw none and the component renders the control inside a <span> with an empty box at the right of it. The select_chevron entry draws the arrow the way checkbox draws its tick: a $chevron square with two of its sides in $mutedForeground, turned a quarter turn. So it needs no icon pack, and an application that wants another arrow gives select_chevron its own rules. It is aria-hidden and takes no pointer events, so a screen reader reads the combobox and a click reaches the element under it.

Icon is built from icon data, in the shape the icon sets publish: body, width, height, left, top, and optional rotate, hFlip and vFlip. name is that data, or a name looked up through the Icons context.

This package ships no drawings (design 144). Icons starts empty, and an application supplies a pack, a resolver, or both. This is not only about the Icon you write yourself: DropDown, FileDrop, Modal and Validate each mount one of their own, by name, so a page that mounts any of them and has no Icons provider above it asserts on its first render rather than rendering without the glyph. Validate is the one that surprises people, because its icon is a default nobody asked for. Give the page a provider, or give the component its own icon prop. (Select needs none: its arrow is drawn by the theme, design 195.) @aweftjs/icons turns an installed icon set into exactly that:

import { Icon, Icons } from '@aweftjs/ui';
import standard from '@aweftjs/icons/lucide/+standard';

<Icons value={standard}><App /></Icons>
<Icon name="check" label="done" />

A provider stacks a pack or a resolver in front of what it inherited, newest first, so a pack an application puts up answers before anything under it. A pack is { prefix, icons, aliases?, width?, height? }, where the root width and height are the box every icon in it that states none is drawn in; the sets state it once at the root, so without it a real set renders clipped. A resolver is (name) => data | Promise<data> | null, and a promise is declared pending so a static render waits for it. A resolver that answers null passes the name on to the next source, and so does one that answers a promise of null, so a resolver in front of a pack does not stop the pack behind it being asked. An icon with no label is aria-hidden, because an icon beside the word it means is otherwise read out twice. A name nothing answers is an assert naming the icon, how many sources were asked, and how to answer it: wrap the page in Icons with a pack or resolver that has it.

The components here ask for names, never for drawings. standardIcons is that list, in the spelling the sets publish: chevron-down, chevron-up, chevron-left, chevron-right, check, x, triangle-alert, search, upload. Give one of them a drawing of your own by putting a pack of your own in front, which is what Icons is for.

Laying things out is a theme entry, not a component (design 132): row and column, each with fill, center, start, end, spread, wrap and tight, and divider. center, start and end mean across the page on both.

<div theme={['row', 'fill', 'spread']}><span>left</span><span>right</span></div>

recipes/ui/catalogue.html is every one of them in every state, in both modes, one page per component at #/<Component>, driven in Chromium by recipes/ui/main.ts with axe-core over every page.

Laying a form out

There is no Field component. A form is the theme entries and a bare <label> (design 209): every page that laid a form out reached past the components, for the same reason each time, so the components went and the entries stayed.

import { Checkbox, TextField } from '@aweftjs/ui';

<div theme="field_group">
  <fieldset theme="field_set">
    <legend theme="field_legend">Where to send it</legend>
    <div theme={['field', 'responsive']}>
      <label for="street" theme="field_label">Street</label>
      <TextField id="street" value={street} />
    </div>
    <div theme={['field', 'inline']}>
      <Checkbox id="post" value={post} />
      <label for="post" theme="field_label">Post it rather than email it</label>
    </div>
  </fieldset>
  <TextField label="Notes" value={notes} description="Anything else" />
</div>
entrywhat it lays out
fieldone field: a control, whatever labels it, and whatever is said under it, in a column
field_inlinethe same field as one $control-tall row
field_responsivea column that turns into that row from 28rem of its container
field_groupthe stack a form is, $space6 apart, declaring itself the container above measures
field_setthe same stack on a <fieldset>, with the host's border, padding and minimum width off
field_legendthe <legend> inside one
field_label, field_hint, field_errorthe three things written around a control

A control still labels itself. label, description and error stay on the control, and a control given any of the three wraps itself in its own <div theme="field">. So the last line of the form above needs no box around it: a Field around a control that lays itself out is a column of one thing.

field_inline and field_responsive lay out the box's own children, so they are for a bare control and the <label for> you wrote beside it.

field_responsive measures the nearest ancestor that declares itself a container, which in this package is field_group and nothing else. With no group above it, a query with no container answers false and the field stays a column at every width.

A label under an ancestor carrying data-invalid takes the colour its message has. Nothing writes that attribute for you; write it from the same cell you pass to error if you want it.

Composites

Ten more components, each built out of the controls above and the behaviours underneath them.

import { ColorPicker, Country, Default, DropDown, FileDrop, Menu, Modal, Region, Tooltip, Validate, ValidateContext } from '@aweftjs/ui';
componentwhat it isits own propsexample
Modala stage template: the act inside a native <dialog>label, noEsc, noClickEsc, type (sheet), sidemodal
Defaultthe stage template that adds nothingnonemodal
TooltipDetached plus the hover and focus triggerlabel, enabled, locations, typetooltip
DropDowna <details> whose <summary> wears the button themeopen, label, icon, iconOpen, iconClose, arrow, name, type, disableddrop-down
FileDropa drop zone with a real file input in itfiles, extensions, multiple, limit, clickable, disabled, onDrop, ready, typefile-drop
Validatea check around a control, and the message it showsvalue, validate, signal, valid, error, showError, icon, typevalidate
ValidateContextthe form's answer: every Validate below itvaluevalidate
ColorPickera saturation and brightness square, the hue and the alpha as Sliders, and a swatchvalue, hasAlpha, disabled, typecolor-picker
Countrya button that opens a searched grid of every countryvalue, locale, priority, flags, suggestcountry
Regionthe same, over one country's subdivisionsvalue, countrycountry
Menua button and the list of actions it opensitems, open, label, icon, type, size, disabled, locationsmenu

These ask for chevron-up, chevron-down, x, triangle-alert and upload by name, so a page using them answers those five through Icons; @aweftjs/icons/<set>/+standard does. A Menu asks for none: its rows hold whatever icon you give them.

A Menu is a button and the actions under it. items is a list of { label, icon?, type?, disabled?, onSelect }, and { heading, items } anywhere in that list draws a small heading over its own group. type: 'danger' draws a row in the danger colour, which is what a delete belongs in.

<Menu label="Quick Actions" items={[{
	heading: 'Conversation',
	items: [
		{ label: 'Mute Conversation', onSelect: mute },
		{ label: 'Mark as Read', onSelect: read },
		{ label: 'Delete Conversation', type: 'danger', onSelect: remove },
	],
}]} />

The keys are the ones a Select has, because they are the same behaviour: the arrows move and wrap, Home and End go to the ends, typing moves by what a row reads, Enter and Space choose, Escape and an outside click close it, and Escape and choosing put the focus back on the button. The list is role="menu" and the rows are role="menuitem", named by the button. The list is in the page while the menu is closed, inside the box the sink places, which is what hides it; a script that looks for an open one asks by role with hidden elements left out, not for the first role="menu" it finds.

Opening it moves the focus onto the role="menu" element, which is where aria-activedescendant names the row the keys are on. That is the ARIA menu-button pattern, and it is the only shape ARIA allows: the attribute may not sit on the button that opened the menu. Escape and choosing a row put the focus back on that button.

The anchor is the button this component builds, so the ARIA is in the markup rather than written onto a node it does not own. Children go inside that button, and element hands one in. A group is { heading, items }, and a heading makes it one whether or not it has any items yet.

A modal is a stage template, so back closes it.

stage.open({ name: 'edit', history: true, template: Modal });

A stage calls a template as h(template, props, act), where props is what the open carried past name, template, history and children (design 213). So the dialog is named at the call, and Modal goes in as the template itself:

stage.open({ name: 'edit', history: true, template: Modal, label: 'Edit the thing' });

The act is handed the same props, plus the stage after them. An act reached from the URL carried nothing, so the template the stage was given is called with nothing.

Modal reads the props it names and forwards none of the others. label, noEsc, noClickEsc, type, side, element, theme and class are its own; anything else an open carried goes to the act and no further, so a row handed to the act is not written on the <dialog> as an attribute. A prop the act wants that is spelled like one of the eight is read by Modal too: that is what one prop bag means, and it is visible in the call.

Escape (through the element's own cancel event), a mousedown on the backdrop and the close button all call the stage's close(), so a modal that owns a history entry goes down the same way whichever one you used (design 124). noEsc and noClickEsc turn the first two off. A Modal with no stage above it is an assert naming the call that shows one. A popup opened inside the dialog goes in the dialog, so a Select or a Menu in a modal works with nothing else to write.

A sheet is a Modal with type="sheet", against an edge instead of in the middle, sliding in from it (design 202). side says which edge, right by default, and is read for no other type. Everything else is the same: the same stage, the same three ways to close it, the same backdrop.

stage.open({ name: 'filters', template: Modal, label: 'Filters', type: 'sheet' });

A stack of disclosures that keeps one open is a run of DropDowns sharing one name, which is the platform's own behaviour for a group of <details>. There is no component for it (design 212): give each drop-down the same name and you have one.

A tip is on hover and on focus, and the anchor names it. The children are the anchor, and a <mark.popup> replaces the label with markup. The panel sits inside the box Detached placed and wears no popover of its own: the box is already a popover, and a popover inside a popover is put in the top layer and laid out by the browser, which takes the panel out of the box and lands it in the middle of the screen (design 135). The pause before a hover shows it belongs to the behaviour, so every tip on a page waits the same time and there is no prop for it; focus shows it at once. Each element in the anchor gets aria-describedby naming the panel, written when the page comes alive. A static render leaves it out, because nothing on the client can write it before the pairing walk reaches the anchor, so markup carrying it could not be taken over (design 153). It is taken off again when the component unmounts.

A Tooltip takes over server markup and can sit inside an act a stage swaps away, both since design 153.

A drop down is a disclosure, not a floating menu. The content is the children, in the page's flow. The open cell goes both ways: writing it opens and closes the element, and a person opening it writes the cell. Space, Enter, the button role and the expanded state are the platform's, because the element is a <details>. A floating menu under a button is Detached with a Button anchor, which is one call and no new component.

The country fields are a button and a dialog, and they ship no country data.

import { Countries, Country, Region } from '@aweftjs/ui';
import { countryData } from '@aweftjs/ui/countries';

const data = await countryData();

<Countries value={data}>
  <Country label="Country" value={country} name="country" autocomplete="country"
           priority={['CA', 'US']} placeholder="Pick a country" />
  <Region label="Province" value={region} country={country} name="region" />
</Countries>

Each is a button that opens a modal dialog with a search box over a grid of rows. Country's cell holds a two-letter code, Region's holds a subdivision's short code, and under each is a hidden <select> carrying the same options, so a form posts the code and autofill has a control to find. Nothing is chosen to begin with and nothing ever chooses for the person.

The names come from the host, the codes and the subdivisions from you. Intl.DisplayNames has a country's name in the page's own language, so no list of names ships here and locale is the only thing that decides which language they are in; the flags are the two letters of the code as regional indicator symbols, so no flags ship either, and flags={false} turns them off where a host draws none. What is left is the codes and the subdivisions, which nothing derives: countryData() reads the optional country-region-data peer, and CountryData is a shape you can build yourself instead if the form allows five countries rather than 249. A field with no Countries above it is a loud assert naming both.

A search matches the name, the code and this package's aliases, without accents: uk finds the United Kingdom, usa and america find the United States, cote finds Côte d'Ivoire. It matches anywhere in the word rather than only the start of it, so uk finds Ukraine as well. The aliases are countryAliases on the same subpath, and CountryData.aliases is the whole map rather than an addition to it: to add your own words, spread the shipped map into yours.

const data = await countryData();
<Countries value={{ ...data, aliases: { ...countryAliases, nz: ['aotearoa'] } }}>

A row is chosen with a click on it, or with Enter or Space while the keyboard is on it, which is the listbox map every list in this package shares: the arrows and Home and End move, Escape closes, and the search box keeps every printable character. Writing the cell chooses too, and that is the whole of the control's state: a page that knows the country already writes country.set('CA') and never opens the dialog.

The rows are ordered, and the first one is a suggestion. Intl.Locale(navigator.language).region says which country the browser's language settings point at, so a browser set to en-CA has Canada first, marked; a language with no region in it is maximized by the host, so pt is Brazil. It is the language setting and not the location: nothing is asked for, nothing is fetched, and a person filling a form for somewhere else is ordinary, which is why it suggests and never selects. suggest={false} turns it off. After it comes priority, in the order you wrote it, and then every other country by its name in the page's language.

Region is the same control and not a dropdown, because one country has 217 subdivisions and the middle one has 11 (design 251). Handed no country it is disabled; the country changing does not clear it, because a form clearing a field a person filled is the page's decision. Its names are English, which is what the data has.

Known limits. The grid is drawn the first time the dialog opens, so a static render emits the control, its dialog and the element a form posts, and none of the rows. A country that is already chosen when a page is rendered statically has its name in the markup, and if the server's ICU data and the browser's disagree about that name, the hydration refuses with both names in the message rather than showing the wrong one.

A file drop holds entries, and no upload.

const picked = mutableArray();
<FileDrop files={picked} extensions={['image/png', '.csv']} limit={4_000_000} ready={file} />

An entry is { name, file, status, error, reason }. A file the zone accepted starts as ready and one it refused as error, with error the sentence a person reads and reason the code you branch on: type for a file the extensions do not cover, size for one over limit, and count for a second file while multiple is false (design 214). An accepted entry carries neither. A refused file stays in the list rather than disappearing. Move status to loading while you upload by writing the entry back, files[0] = { ...files[0], status: 'loading' }, which is the edit a list can hear; that row then shows the LoaderContext's loader beside the file's name, and the dots when nothing named one (design 219). ready is written null while anything is loading, and otherwise the file, or the array of files when multiple is true, counting every entry that is not in error. The transport is yours: ui decides nothing about storage, transport or the server, so the entry carries the platform File.

With multiple false, a second pick replaces the entry that is there, in place, so a watch on the list hears a replace and not an add. A page listening for add alone sees the first file and none of the ones after it.

limit is bytes, and every sentence naming it is written for a person: KB, MB or GB, 1024 to a step, so a limit of 4_000_000 reads as 3.8 MB. The prompt names the accepted types the same way, so image/png reads as png, image/* reads as image and .csv reads as csv.

The zone listens for dragenter, dragleave and drop, and reads the dropped files off the event's dataTransfer.files; the input listens for change and reads its own files. Those four are the whole of what reaches this component from the host.

Children replace the prompt line and the listing. The input itself is visually hidden rather than display: none, so it is still focusable, and its label is the zone's prompt whichever chrome is showing. A FileDrop.Button standing on its own names its own input the same way, with an offscreen <label> carrying the button's label or its aria-label, because the button beside it is what shows the words. look.test.ts reads the filedrop_picker rule and fails if it ever becomes display: none.

FileDrop.Button is two things, decided by where it is (design 214). Inside a FileDrop it is a Button that opens that zone's input, and it takes no checking props of its own, because the zone already has them; giving it one is an assert. Outside a zone it is the picker with no chrome: its own hidden input, the same checks, and files, extensions, multiple, limit, onDrop and ready on the button itself.

<FileDrop.Button label="Change photo" extensions={['image/*']} multiple={false} ready={photo} />

A check is given the cell, not the value.

<ValidateContext value={allValid}>
	<Validate value={email} validate="email" signal={submitted}>
		<TextField label="Email" value={email} />
	</Validate>
</ValidateContext>

validate is handed the cell, not what the cell holds. So a check reads it with cell.get(), and one that formats what was typed writes it back with cell.set(...), which is how four of the eight built-ins work. It returns the problem as a string, or '' or null for no problem.

<Validate value={confirm} validate={(cell) => (cell.get() === password.get() ? '' : 'They do not match.')}>

validate is that function, or the name of one of eight built-ins: phone, email, pan, expDate, postalCode, date, number and float. Four of them write a formatted value back into the cell: phone, pan, expDate and postalCode. Each answers nothing for an empty value, so a field is not invalid before anybody has typed in it. They are small on purpose, each doing what its name promises and no more: email is a shape check, date is YYYY-MM-DD, postalCode is the Canadian one, and pan is a Luhn check on thirteen to nineteen digits. Write a function for anything else.

A signal is read, not counted. While that cell holds something falsy nothing is checked, and while it is truthy every change to value is checked (design 208). So a form that clears itself after a successful submit writes submitted back to false and goes quiet again, rather than marking every emptied field. Going quiet clears the message, writes valid true and writes error null. With no signal, checking is live from the start.

Under a ValidateContext, a check runs again when any cell that form is checking changes. That is what confirm-must-match needs: the validator above reads the other password's cell, and nothing else would tell it that the other password moved. A validator that reads a cell no Validate in the same form is checking is not re-run for it; put that control in a Validate too.

The message is rendered once, after the children, as a live region. It also reaches the control: a control of this package that was given no error of its own goes aria-invalid and its aria-describedby names that message. A Validate around a plain <input> still shows and announces the message and leaves the input unmarked, because nothing read it. showError false takes the message off the screen and leaves it announced.

A Validate wraps one control. Every control under it takes the message, so a Validate around two of them marks both invalid and points both at the one message. Two controls want two Validates.

A validator that throws is reported on a microtask the host sees, the way every handler a page wrote is reported here, and the value counts as invalid with the error's message. A form is never quietly valid because its check crashed.

ValidateContext writes its value cell true while every Validate under it is happy. Each one registers when it mounts and leaves when it unmounts, so a field that goes away stops holding the form invalid.

A colour picker is a square and one or two sliders. value is a cell holding CSS colour text, anything readColour reads, and it is written back as rgb() or rgba(); text that is not a colour is an assert naming the text. Saturation and brightness are one place in a square, dragged with a pointer or driven with the arrows; hue and opacity, the last only when hasAlpha is not false, are labelled range inputs with the platform's keyboard on them. The swatch beside them is aria-hidden, because it says what the rest already say. There is no eyedropper and no hex field; a TextField on the same cell is the hex field, because the cell is text.

The square's thumb is the one element in this package with a role written on it, because there is no two-axis role in ARIA (design 222). It is role="slider", focusable, and says both axes: aria-valuenow carries the saturation and aria-valuetext reads saturation 40%, brightness 80%. Left and right move saturation, up and down move brightness, Home and End take saturation to its ends, and Shift makes any of them coarse. A press anywhere in the square moves the thumb there, and a drag that leaves the square keeps working.

A drag, a key or a slider writes the cell, and nothing else does, so mounting a picker on a colour leaves that colour and its notation alone. With hasAlpha false a write keeps the alpha the cell already had: nothing on the screen can change an alpha nobody can see.

The square is $planeSize on each side and there is no size prop: it is a composite with no one height, so an application that wants a bigger square redefines $planeSize. The two gradients over it are the colorpicker_plane entry's, and the hue track's six hues are colorpicker_track_hue's. What the component writes is the colour that is chosen now, which does not exist until it runs: the hue under the square, the thumb's own fill and the opacity track are inline style. The theme check reads source text, and each of those is arithmetic rather than a value anybody typed, so there is nothing there for it to refuse.

A state prop takes a cell. open, enabled, value and files are cells or absent; give one a plain value and it is a loud assert naming the prop and the fix, because a component that quietly kept a cell of its own would look as though it had honoured what you asked for.

The theme entries these add, on top of the ones above: dialog with its ::backdrop, tooltip, disclosure and disclosure_summary, filedrop with dragging, prompt, list and entry, validate, and colorpicker with swatch, plane, plane_thumb, track and hue. The country fields add chooser and its parts: wrap, chosen, panel, head, search, grid, option with selected, active and suggested, flag, lines, note and none. offscreen is one more, and it is yours to use: it takes an element off the screen and leaves it in the reading order.

Each of them has a page on recipes/ui/catalogue.html, in both modes, driven in Chromium by recipes/ui/main.ts with axe-core over it.

Text

import { TextModifiers, Typography } from '@aweftjs/ui';
exportwhat it isits own propsexample
Typographyone run of themed text: one element on the text entrytype, label, element, themetypography
TextModifiersa context holding the list Typography runs over its labelvaluetypography
TypographyPropswhat Typography takes
TextModifierone entry of that list: { check, return }

type is theme segments joined by _, and the first segment also picks the element. <Typography type="h2_bold" label="Today" /> is an <h2> themed text h2 bold, so text_h2 and text_bold both apply and the later segment wins where they disagree. h1 to h6 give that heading, p, p1 and p2 give a <p>, and everything else gives a <span>, so a word this package never defined is a segment like any other and your own text_eyebrow needs nothing from here. With no type it is a <span> on text alone. type may be a cell: the theme follows it, and the element does not, because an element lasts as long as the component.

label and children both render, label first, and neither is required. label may be a cell. element hands in the node to decorate instead of building one, which is how a heading's look goes on a level the document outline wanted instead.

TextModifiers turns parts of a label into something else. A modifier is { check, return }: check is a plain string, matched everywhere and case-insensitively, or a global regex, and return(match) answers whatever that piece becomes. Matches are collected in the order the modifiers are written, sorted by where they start, and an overlap goes to the one that started first, a tie to the one written first. A gap between matches is text.

<TextModifiers value={[{ check: /@\w+/g, return: (name) => <Tooltip label={who(name)}>{name}</Tooltip> }]}>
	<Typography type="p1" label={note} />
</TextModifiers>

Keys beyond those two are ignored, so a list written for something else passes through. A provider replaces the list above it rather than adding to it; a subtree that wants both writes both. A list held in a cell is read when each Typography below it mounts, as Icons is; a later write reaches what mounts after it. The list runs over label only, because a child is already markup and has nothing for a pattern to run over, and a label that is neither a string nor a number renders as given. A regex without the g flag is a loud assert naming the flag, because a pattern that finds one match is almost never what was meant. A cell label runs the pass again when it changes.

The label goes through one resolve step first. A text token resolves to its string in the render's language before the modifiers run, so a modifier matches the translated word; anything else is the label as given (design 278).

What it never does. No editing: nothing swaps an input in on a click and nothing measures text with a span, because editing is TextField's job and a page composes the two. No width cap: a measure is yours, as maxWidth on text_p1. No fonts: the theme engine already emits @font-face and @import from your own theme's directives.

recipes/ui/preview.html shows the whole family in both modes, and the gallery's modifier demo is recipes/ui/page.tsx, both driven by recipes/ui/main.ts.

The text a page shows

import { context, isText, localeOf, mount, text, textOf } from '@aweftjs/ui';

mount(document.body, <App />, undefined, context({ locale: 'fr', catalog: fr }));

text('Save') is a token: what a string a person reads becomes, so it can be looked up where the page mounts. With the build's text option on (packages/build/README.md), every literal a page shows is one already, and a page writes the call itself for a string built in code, a plural, or a sentence with an element inside it. A render with no catalog shows every token's source, so a page written this way runs before it has a second language.

exportwhat it is
text(source, values?)a token: mountable as a child, and resolved by h in a prop, by Typography in a label, and by a head component in a tag
context({ locale, catalog })the language a render shows and its translations, a plain object from key to message
textOf(context, value)the string a token, a string or a number shows in a render, for code that needs characters: a document title set by hand. Takes the mount context or the render context() made, so a server module resolves against a render of its own
localeOf(context)the render's BCP 47 tag, for a date or a number formatted with Intl
isText(value)whether a value is a token
usedText(render)the keys a render's tokens looked up

A token resolves where it mounts. As a child it is a component, so dom mounts it and a hydration pairs it beside a static sibling. An act module's title may be a token too, and the stage announces it in the page's language. In any prop, h claims it the way it claims theme: the resolved string is written into a cell dom binds under the same name, so placeholder, title, alt and aria-label carry the translation on the server's node after a hydration and on a fresh one after a mount. Typography resolves a token label before its modifiers, and a head component resolves a token child or attribute when it declares its tag. A token that mounts under dom's own mount, with no ui systems, shows its source. The key is the source string, or source|context when the call names a context word: text('Close', { context: 'dialog' }) is the key Close|dialog, which is how one word with two meanings gets two entries.

The message syntax is a subset of ICU MessageFormat, read here with no dependency:

writtenmeans
Hello {name}the value under name; a cell is followed
{n, plural, one {# item} other {# items}}the branch Intl.PluralRules(locale) picks for n, =0 and friends matching the exact number first; # is n formatted for the locale; other is required
{kind, select, book {a book} other {a thing}}the branch named by the value of kind, other when none matches; other is required
Read <link>the docs</link>the value under link, a function from the inner content to what to mount; with no function under that name the inner content stands alone
'{', '}', '<', '#', ''that character; an apostrophe quotes up to the next apostrophe, and stands for itself in front of anything else

Branches nest. A message that cannot be read is a loud assert naming the offset and what was expected there; in a release build it shows as written. Every parse is cached by source.

<p>{text('{n, plural, one {# item} other {# items}}', { n: count })}</p>
<p>{text('Read <link>the docs</link>', { link: (inner) => <a href="/docs">{inner}</a> })}</p>
<Button label={text('Save')} />

A cell among the values is followed. A plural over a count (mutable(1) from @aweftjs/core) re-renders as the count moves, through one subscription per cell; a token with plain values costs no subscription at all.

This package's own strings are tokens too, and text.json beside its package.json, reachable as @aweftjs/ui/text.json, lists their keys, so an application's catalog can carry Close, Search, Previous and the rest beside its own words. It ships no translation. The build folds those keys into the source catalog it writes. auth ships the same for its sign-in form.

What it never decides. Which language a visitor gets: a page reads a URL, a header, a cookie or a setting and hands context() the answer. Where a catalog comes from: a file the entry imports, a document a store holds, or an object the page built; this package reads none of them. No fallback chain: fr-CA is one catalog, not fr with overrides, and an application that wants the chain merges the two objects. The language is fixed per render: a switch is a remount or a navigation, never a cell every token follows.

text is also the name of the theme family Typography renders on. A theme entry and an export are different namespaces, and both are the plain word for what they are.

recipes/translated-site is a site written once and launched in three languages, hydrated in Chromium in each.

Markdown

import { Markdown } from '@aweftjs/ui';

<Markdown source={readme} />
componentwhat it isits own propsexample
Markdowna <div> on markdown holding one element per block of a markdown stringsource, modifiers, code, element, thememarkdown

What it renders (design 288). A markdown string, or a cell holding one, as themed blocks whose text runs are Typography, for the subset a README uses:

writtenrendered as
# to ######Typography h1 to h6 on markdown_heading, with an id in GitHub's scheme: lowercased, marks and punctuation dropped, spaces to hyphens, a repeat suffixed -1
a paragraphTypography p1 on markdown_paragraph; lines joined by a space, two trailing spaces a line break
a fenced blocka <pre> on markdown_code with the language on data-language, holding what code answers; focusable, because it scrolls sideways
-, *, + or 1. itemsa <ul> or <ol> on markdown_list (ordered for the second, with start when the first number is not 1), each item an <li> on markdown_item
- [ ] and - [x]an item on markdown_item_task with a Checkbox that follows the source
a table with a delimiter rowa <table> on markdown_tabular in the table_scroll box, with the table_head, table_line, table_heading and table_cell parts; :--: and --: align a column
> linesa <blockquote> on markdown_quote, one paragraph
---, ***, ___an <hr> on markdown_rule

Inline, a code span (one or two backticks, the double form holding a backtick) is <code> on markdown_inline; **bold** and __bold__ are <strong> on markdown_bold; *italic* and _italic_ are <em> on markdown_italic; ***both*** is the two nested; [text](https://github.com/torrinworx/aweft/tree/9a5bb24770dc7555257f8a307d79917efc58df70/packages/ui/href) is <a> on markdown_link with the href as written, unless its scheme is not http, https, mailto or tel, in which case the link is text. Each is a modifier in TextModifiers' shape, listed after modifiers, so an application's own patterns run inside markdown with nothing wired, inside a bold run included:

<Markdown source={note} modifiers={[{ check: /@\w+/g, return: (who) => <Mention name={who} /> }]} />

With no modifiers prop the list is the render's TextModifiers; a cell is read when the component mounts, as TextModifiers reads one. An overlap goes to the match that started first, so a code span holding an asterisk is a code span and a bold run holding a code span is both, and emphasis of mixed length that opens inside another (***a** b*) is read outermost first rather than the way CommonMark reads it.

What is text. A nested list (an indented item joins the item above it, as written), an image, a footnote, an HTML tag, an autolink, a reference link, a setext heading, an indented code block and a backslash escape each stay in the paragraph as the characters written. HTML is never markup: a paragraph is a text node and nothing here sets innerHTML, so an untrusted string renders as text.

The code hook is (text, language) => anything mountable and is called once per fenced block; absent, the block holds a <code> with the text. Highlighting is the application's, and this package ships none.

A cell source re-renders every block when it changes; there is no diff. A task item's box is disabled unless the source is a writable cell, and then a tick rewrites that item's [ ] or [x] in the source and sets the cell, so a document a page shares is what moved.

The theme is the markdown_* entries above, every value a role or a size name, so both modes come from the one set and an application overrides an entry the way it overrides button. Every heading, paragraph, cell and item text is on the text family too, so text_h2 and text_p1 reach them as they reach any Typography.

What it never decides. How code is highlighted. Where a link goes, or whether it opens elsewhere. What HTML means. The width of a column or a block. Whether the source is trusted: HTML in it is text, and a link whose scheme would run something is text, so an untrusted string renders without running anything. CommonMark conformance: the subset above is what is read, and the Markdown example in the catalogue shows every form of it in both modes.

Display

Six things a page shows and nobody operates (design 199). Each is one native element with a theme on it, none takes a handler, and none takes a value the way a control does.

import { Alert, Avatar, Badge, Empty, Progress, Skeleton } from '@aweftjs/ui';
componentthe elementits own propssizeexample
Badge<span> on badgelabel, type (quiet, danger, success, outline), icon, elementsm, lgbadge
Alert<div role="alert"> or <div role="status"> on alerttitle, icon, type (danger, success), element, children as the bodyalert
Avatar<span> on avatar, holding an <img> and a <span>src, alt, fallback, roundsm, lg, or any CSS lengthavatar
Skeleton<div aria-hidden="true"> on skeletonwidth, height, roundskeleton
Progress<progress max="1"> on progressvalue (0 to 1, or nothing), label, elementsm, lgprogress
Empty<div> on emptyicon, title, description, element, children as the actionsempty

An avatar shows its letters until its picture loads, and again if it fails. Both children stay in the tree and whichever is not showing carries hidden, so it is out of the accessibility tree as well as off the screen. round is true unless you set it false.

With a plain src of nothing there is no <img> at all. With a src that is a cell the <img> is in the markup from the first paint whatever the cell holds, and its src follows the cell: a cell that has not resolved yet is not the same as no picture.

size takes a CSS length as well as sm and lg (design 215), the way Icon's does. A step name is a theme segment; a length is written as the element's width and height. The fallback letters are 40% of the box, through a container query on the avatar entry, so they follow it at every size and at any length. Move $avatarLetter to change the proportion.

A progress with no value is indeterminate. value is a fraction of 1, so nothing has to divide; null or nothing leaves the attribute off, which is what the platform reads as waiting and draws as the moving bar. A number outside 0 to 1 is clamped to it, and anything that is not a finite number, NaN and a string of digits included, is indeterminate rather than written through. Give it a label: without one it has no name for a screen reader.

A skeleton announces nothing. It is aria-hidden, because the thing that is loading is what says so and three grey boxes saying it three times is worse than silence. width and height go through style, so a bare number is pixels and any CSS length works.

A badge's size is padding and text, not a height. A badge is not a control, so $control is the wrong number for it: sm tightens the padding and lg widens it and takes the next text step.

An alert's icon is yours. This package ships no drawings, so an Icon by name needs an Icons provider above it and there is no default icon here. The box is one column until you give it one.

danger interrupts and success waits. A danger alert is role="alert", so a screen reader breaks off to read it; every other type, success included, is role="status" and waits its turn (design 216). A thing that went right is not an interruption.

Grouping

One component whose whole job is what it puts around other components (designs 200, 211).

import { Card } from '@aweftjs/ui';
componentthe elementits own propsexample
Card<div> on card, with the stack segment when it has partstitle, description, foot, type, tight, element, children as the bodycard

A Card with none of title, description and foot is the bare block: the card entry, and the children directly inside it (design 211). Given any of the three it takes the stack segment and builds the parts: the head, the body around your children, the foot, and $space4 between them. Each part renders only where it was given something.

<Card><h2 theme={['text', 'lg']}>Today</h2><p theme="text">Nothing yet.</p></Card>
<Card title="Today" description="What is due" foot={<Button label="Add" />}>Nothing yet.</Card>

tight drops the padding, for a card whose children reach the edge. Write <Card theme="stack"> to get the column spacing on a card that has no parts.

A row of buttons joined into one control is yours to write (design 212). There is no component for it: a class list is written by the element that wears it, so a group could never put a segment in its children's lists anyway.

A text field with something beside it inside the same box is TextField's leading and trailing (design 210), not a component of its own. See Controls.

Where a person is, and what they are looking at (designs 201, 203).

import { Breadcrumb, Pagination, Tab, TabPanel, Table, Tabs } from '@aweftjs/ui';
componentthe elementits own propsexample
Tablea <table> on table, inside a <div> on table_scrollcolumns, rows, cell, caption, foot, label, striped, tight, type, elementtable
Breadcrumba <nav> on breadcrumb around an <ol>items of { label, href }, label, elementbreadcrumb
Paginationa <nav> on pagination of quiet Buttonspage (a cell), count, siblings, onChange, size, label, elementpagination
Tabsa <div> on tabs around a <div role="tablist"> on tabs_list and the panelsvalue (a cell), tabs of { value, label, disabled, content }, orientation, type (line), size, label, onChange, elementtabs
Taba <button type="button" role="tab"> on tabvalue, label, disabled, elementtabs
TabPanela <div role="tabpanel" tabindex="0"> on tabs_panelvalue, elementtabs

The table's entries work without the component. Write your own <table theme="table"> with <thead theme="table_head">, <tr theme="table_line">, <th theme="table_heading"> and <td theme="table_cell"> and you get the whole look. Table is the common case: columns is a list of { key, label, align, width } or of plain strings, rows is a list or a cell of one, and cell is (row, column) => anything mountable, defaulting to String(row[column.key]).

Rows go through each, so pushing one inserts one <tr>. A rows cell holding anything but a list, which is what one holds before the first fetch answers, reads as no rows.

A cell may return a different thing on every row. A button on the rows that can be acted on and nothing on the rest, text on some and a Badge on others: cell is called per row and what it returns is mounted per row, so nothing about the one-shape rule under each reaches it.

<Table rows={files} columns={[{ key: 'name', label: 'Name' }, { key: 'act', label: '' }]}
	cell={(row, column) => (column.key !== 'act' ? row.name
		: row.mine ? <Button label="Delete" type="quiet" size="sm" onClick={() => remove(row)} /> : null)} />

What each asks one shape of is a row component you write yourself, which is a different thing; @aweftjs/dom's README says what that costs.

A wide table scrolls in its own box, which carries tabindex="0" so a keyboard can reach the scroll. Give the table a caption or a label: without one it has no name for a screen reader.

A breadcrumb's links are plain anchors with no target. That is what lets a router take the click: createRouter(...).links(root) intercepts same-origin anchors and leaves alone anything with a target. So a breadcrumb inside a routed page navigates with no reload and no handler.

Pagination shows the first page, the last, and siblings each side of the current one, with an aria-hidden ellipsis where a run was left out. page is a cell counted from 1; previous is disabled on page 1 and next on the last. The page showing now carries aria-current="page".

A page outside the count is clamped, and the cell is written with the clamp. count shrinking under the page it was on is what a filter does every time, and the page it leaves behind has no button, no aria-current and a dead Next. So the component moves to the last page, writes page and calls onChange with it (the event argument is null, because nobody pressed anything). A count of 0 renders no page buttons and both arrows off, and writes nothing: there is no page to be on.

A strip of tabs is one stop in the Tab order, and the arrows move inside it (design 203). Tab lands on the tab showing, and the next Tab leaves the strip for that tab's panel rather than walking to the next tab. Right and Left move one tab, wrapping at each end; Down and Up do instead when orientation is vertical; Home and End go to the ends. A disabled tab is stepped over rather than landed on, and it says so with aria-disabled so a screen reader still reads it out.

Arriving on a tab chooses it. The arrows move the selection as well as the focus, so holding Right shows each panel in turn. That is what the roving tabindex is for: the tab showing carries 0 and every other carries -1. All of it is one internal behaviour, tablist.ts, the fifth beside the field wiring, the dismiss, the dialog and the tooltip trigger (design 129); none of the five is exported, and each is tested once on its own.

<Tabs label="Views" value={view} tabs={[
	{ value: 'all', label: 'All', content: <All /> },
	{ value: 'mine', label: 'Mine', content: <Mine /> },
]} />

Every panel stays mounted, and the ones not showing carry hidden, so coming back to a panel finds it as it was left. Wrap a panel's contents in a Shown to have them built again instead.

tabs is the common case; two marks are the long way. The tabs go inside the strip and the panels go outside it, and a component may not read another component's props to tell them apart, so a caller who writes them out says which is which:

<Tabs label="Sections">
	<mark.tabs><Tab value="left" label="Left" /></mark.tabs>
	<mark.panels><TabPanel value="left">the first</TabPanel></mark.panels>
</Tabs>

With no value the component keeps a cell of its own and starts on the first tab, and a cell you passed holding nothing is left holding nothing: choosing for you would be a write you did not ask for. Each tab and its panel name each other with aria-controls and aria-labelledby, off ids minted from the render's counter, so a page rendered on a server and the hydration that adopts it agree.

The tab showing leaving the tabs list moves the selection to the first tab anyone can choose. Otherwise the value names nothing: every tab reads tabindex="-1", so the strip is not in the Tab order at all, and every panel is hidden, so the page shows nothing. The cell is written and onChange is called with the new value (the event argument is null). With every tab disabled, nothing is chosen and the first tab keeps the stop, so the strip is still reachable. Tabs written out in the two marks are your own markup and are not read this way; taking one out is a change you made to your own tree.

A TabPanel whose value no Tab has is refused, because its aria-labelledby would name an id that is not on the page, which no browser reports.

The look

A default theme ships in light and dark. It is what makes theme="button" a button, and it is the contract a component of this package is allowed to use. Designs 115 to 120 are the whole of it.

Four colour scales, neutral, accent, danger and success, twelve steps each, $neutral1 to $neutral12 and so on. The steps follow one job list: 1 and 2 are backgrounds, 3 to 5 component fills by state, 6 to 8 lines, 9 and 10 solids, 11 and 12 text.

Twenty-two roles, each set from one step. A component uses a role, never a step:

rolepairs withwhat it is for
$background$foregroundthe page
$surface$surfaceForegrounda raised block
$muted$mutedForegrounda quiet fill, and quiet text on any background
$accent$accentForegrounda filled control
$accentSubtle$accentSubtleForegrounda tinted control
$danger$dangerForegrounda destructive control
$dangerSubtle$dangerSubtleForegrounda warning block
$success$successForegrounda thing that went right
$successSubtle$successSubtleForegrounda block saying so
$borderthe line around a block
$inputthe edge of a control
$ringthe focus ring
$linktext that goes somewhere

Every pair meets WCAG 2 AA in both modes, 4.5:1 for text and 3:1 for a line, asserted in packages/ui/tests/contrast.test.ts with a ratio the test computes itself.

The default is monochrome. $accent is the neutral scale's text step and $accentForeground is its page step, so a filled button is near-black on near-white in light and the same line read the other way round in dark. $ring and $accentSubtle are neutral too. The accent scale is still defined, all twelve steps of it, and $link is the one role that uses it. Colour is a decision your application makes: one provider at the root redefining $accent and $accentForeground puts it back on every filled control, because no component names a step. $danger is unchanged and still red, and $success is its counterpart in green (design 216): the two tones are for a message about what happened, and no control here is coloured by either.

Type is $textXs, $textSm, $textMd, $textLg, $textXl, $text2xl, $text3xl and $text4xl, in rem, each with $textXsLine and so on beside it. $font and $fontMono are the families.

Sizes are $space (4px) and its six multiples, $space2, $space3, $space4, $space6, $space8 and $space12, with no step between them; $radiusSm, $radius and $radiusLg; $controlSm (32px), $control (36px) and $controlLg (40px); $target (24px, the smallest a pointer target may be, which sizes the things that are not controls); and $borderWidth, $ringWidth and $shadowSm.

Every control is $control tall. A button, a text field, a select, a slider's hit area and the row a checkbox sits in beside its words are all 36px, so a line of controls is a line. A text area is sized by what is in it and keeps a minimum of its own. $controlSm and $controlLg are the same rule at the two other sizes.

The height is the box, so your own padding fits inside it rather than adding to it. Controls declare box-sizing: border-box, which is what makes the declared height the height whatever element wears the entry. A TextField given padding: '12px' through a theme override is still 36px tall, with a 10px content box; padding: '20px' is more than fits, so the control grows to 42px. Before this the padding was added to the height every time. $target is the smallest a pointer target may be and now sizes nothing in this theme: it is the number your own entries reach for.

Motion is $fast (150ms), $slow (240ms), $ease (cubic-bezier(0.4, 0, 0.2, 1)) and $easeOut. The root sets one transition for every themed element, over background-color, background-image, border-color, color, transform, and popup, dialog, its ::backdrop and tooltip each set one more for the opacity and scale they arrive with (design 192). box-shadow is deliberately not on the list: the focus ring is drawn as one, and a ring that fades in is a ring that is not there yet (design 217). Every one of these is written inside @media (prefers-reduced-motion: no-preference), so a person who asked for less motion gets none and no component has to remember the query. The default theme reads neither $slow nor $easeOut; they are there for you.

Three things move that are not a colour. A switch's thumb crosses its pill over $fast, a slider's thumb grows under the pointer over $fast, and a skeleton and the loading dots run their own cycles: a skeleton breathes between full and half opacity over 2s, and the three dots run a 1s cycle a third apart each, so the bright point travels along the row (design 218). The two thumbs are pseudo-elements, which the root's transition on the element does not reach, so each declares its own transition. The skeleton and the dots are not transitions at all: each is an animation on the element itself, with its own keyframes, and the same query around it.

States are one rule. hovered and pressed lay a translucent tint of the element's own foreground over whatever background it has, at two fixed strengths. No component names a hover colour, and there is no ripple. disabled clears the tint.

Focus is one rule. The root gives every themed element, on :focus-visible, a border in $ring and a $ringWidth halo of $ring at half strength, drawn as a box shadow. An outline is drawn outside the border box and cannot be soft, so it reads as a second border; the halo reads as focus. The focus rule is the one place in this package that writes outline: none, and it names $ring twice in the same block, which is what the gate check asks of an entry that turns an outline off.

The border is what meets the contrast target, not the halo. $ring on $background is 6.08:1 in light and 7.39:1 in dark, well past the 3:1 a non-text indicator has to reach. The halo as it is actually painted is $ring at 50% over whatever is behind the control, which measures 2.15:1 in light and 2.74:1 in dark. So the halo is what makes the focus easy to see and the border is what makes it pass; a theme that keeps the halo and drops the border colour has an indicator nobody has measured.

Disabled dims. opacity: 0.5 and cursor: not-allowed, with the state tint off. It does not repaint the control, because a disabled danger button repainted in $muted is no longer the control it is.

Inputs and quiet buttons carry a hairline. $shadowSm is one pixel of offset and two of blur in the element's own foreground at 6%. It is an edge, not elevation: nothing here lifts a block off the page with a shadow.

Overlays arrive. dialog, popup and tooltip transition their opacity and a small scale in over $fast, from an @starting-style, inside the reduced-motion query. The dialog leaves the same way and its ::backdrop fades with it. A popup and a tooltip arrive and do not leave, because what hides either is a display: none written on the box the popup sink places, above the element the theme reaches.

The tick box, the radio and the select's arrow are drawn here (design 195). Each is still the native element: appearance: none takes the host's drawing and leaves the keyboard, the form and the label. A tick box is a $box square with $radiusSm corners and the $input edge, filled $accent when it is ticked, and the tick is two sides of an empty ::before turned 45 degrees in $accentForeground. Indeterminate is a bar. A radio is the same box as a circle with a centred dot. A select's control says appearance: none, so no host draws an arrow on it, and the component puts an empty box at the right of the control for select_chevron to draw one in, the same way: a $chevron square with two of its sides turned a quarter turn. Nothing here is an image, an icon pack or a name asked of one.

The entries it ships. These are the theme names a component of this package, or of yours, can ask for. Everything else is yours to define.

entrywhat it is for
buttona filled control: the accent fill, its foreground, $control tall
button_sm, button_lg, button_squarethe size axis, and a square for a button whose label is an icon. size="icon" is the prop; square is the segment, because icon is an entry of its own
input_sm, input_lg, select_sm, select_lg, checkbox_sm, checkbox_lg, radio_sm, radio_lg, toggle_sm, toggle_lg, slider_sm, slider_lgthe same axis on the rest of the controls
button_quietthe same control with no fill: a border and accent-subtle text
button_dangerthe same control in the danger colours
inputa text field: the surface fill, the $input edge, the hairline, a placeholder in $mutedForeground
input_invalidthat field with a danger edge
selectinput again on a <button>, laid out in a line, with room on the right for its own arrow
carda raised block: the surface fill, a border, the larger radius, $space4 of padding
popupthe same block at popup size: the smaller radius, tighter padding
textbody copy at $textMd, with no margin, and a newline kept as a line break
text_xs, text_sm, text_lg, text_xl, text_2xl, text_3xl, text_4xlthat copy, one size step at a time
text_h1 to text_h6a heading: one step of the scale from $text4xl down to $textMd, at weight 600, with balanced lines
text_p1, text_p2a paragraph at $textMd and at $textSm, with no lone last word and no width cap
text_bold, text_regular, text_italic, text_center, text_inlineone declaration each, to put after any of the above
text_monothat copy in $fontMono
mutedtext in $mutedForeground, readable on any of the three backgrounds
button_round, button_inlinethat control as a circle, and as a run of $link text with no fill or padding at all
textareathe text field again, growing to its content up to $textAreaMax
checkbox, radioa $box square and a circle, drawn here: the box, the tick and the dot
field_label, field_hint, field_errora control's label, the line under it, and its message. Parts, so each is one class token. A label under a [data-invalid] field takes the message's colour
select_wrap, select_chevronthe box a select's own arrow is placed in, and the arrow: a $chevron square with two sides drawn, turned a quarter turn
togglea switch: a pill and a thumb drawn with ::before
slidera range input: the track and the thumb on the vendor pseudo-elements, with $space of room at each end for the thumb
slider_hovered, slider_pressedthe one entry pair that answers the two state segments itself: the root tint goes off the box, the track takes the tint and the thumb scales (design 220)
listboxthe list a select opens: the popup's surface with rows in it, scrolling past $listMax
listbox_itemone row of it, a $control-tall line. listbox_item_selected is the chosen one and listbox_item_active is the one the keys and the pointer are on
menu, menu_itemthe same list and the same row for a Menu's actions
menu_item_danger, menu_heading, menu_groupthe dangerous row in $danger, a group's small heading in $mutedForeground, and the column a group is
card_tightthat card with no padding
field, field_inline, field_responsivewhat a labelled control puts around itself, and what a form is laid out with: a column, a $control-tall row, or a column that turns into one from 28rem of its container
field_group, field_set, field_legendthe stack a form is, a fieldset with the host's frame taken off, and its heading
dots, dotthe three dots of LoadingDots, and one of them: a 1s wave, the second and third a third of a cycle behind
pulsea slow breathe over $pulseCycle, which skeleton extends and any block of yours can
badge and its quiet, danger, success, outline, sm and lga short label on a fill, sized by its padding and its text rather than by $control
alert, alert_lead, alert_danger, alert_success, alert_symbol, alert_title, alert_bodya message about the page: a grid of one column, two when it was given an icon
avatar, avatar_image, avatar_fallback and its sm, lg, rounda picture of a person and the letters shown without one; the part that is not showing carries hidden, and the letters are $avatarLetter of the box
skeleton, skeleton_rounda grey box standing in for something that has not arrived, breathing on pulse
progress and its sm and lga bar filling up, drawn on the three vendor pseudo-elements
empty, empty_symbol, empty_title, empty_description, empty_actionsnothing here yet, and what to do about it
card_stack, card_head, card_title, card_description, card_body, card_footwhat a Card adds when it was given a title, a description or a foot; one with none of the three reaches none of them
button_currentthe one of a run that is showing now: the page a Pagination is on
table, table_scroll, table_head, table_line, table_heading, table_cell, table_caption, table_foot and the striped, right, center, tight modifiersa table: collapsed borders, a hairline under the head and under each row, and a box a wide one scrolls in. The row's part is line and not row, because row is an entry of its own
breadcrumb, breadcrumb_list, breadcrumb_item, breadcrumb_link, breadcrumb_current, breadcrumb_separatora trail of links and the chevron between them, drawn out of the same $chevron box the select's arrow is
pagination, pagination_gapa row of page buttons, and the ellipsis where a run was left out
input_group, input_group_control, input_group_addon and its sm, lg, invalidthe input look on the box a TextField with an addon builds, and the input inside it with none of it
iconan icon, sized in em so it follows the text
row, column, dividerlaying things out in a line, with the seven modifiers above
dialog, dialog_head, dialog_bodya modal dialog, its heading row and its content; the scrim is on ::backdrop
dialog_sheet and its left, right, top, bottomthe same dialog against an edge, anchored by margin and sliding in from it
tooltipa tip: the page's own colours the other way up
disclosure, disclosure_summarya <details> and the summary that wears the button entry
filedrop and its dragging, prompt, input, list and entrya drop zone, its prompt and its listing
validatethe message a Validate shows, beside its icon
colorpicker, colorpicker_swatch, colorpicker_plane, colorpicker_plane_thumb and its hovered and pressed, colorpicker_track and its huethe saturation and brightness square, its thumb, the sliders beside it and the colour they name
offscreenoff the screen and still in the reading order

hovered, pressed and disabled are three more entries, and you put them in a class list yourself: theme={['button', hovered.bool('hovered', null)]}. They match anywhere, so they apply to any entry above. They are meant for the controls, button and its two variants, input and its variant, and select; card, popup, the text sizes and muted have no state to show.

slider is the one entry that answers the first two itself. The tint is a rectangle the width of the row and a slider's thumb is a 16px circle inside it, so slider_hovered and slider_pressed turn the tint off the box and put the state where the control is: the track takes the tint and the thumb scales, on both vendor pseudo-elements (design 220). Write a slider_hovered of your own to have it back. Any entry of yours can do the same: two segments beat one in the chain.

Your root entry sets the page's background and its colour. The root * entry of this theme sets neither, because it is on every themed element: a colour written there lands on an element inside a control as well as on the control, and an Icon inside a filled Button took the page's foreground on the button's own fill, which is an icon nobody can see (design 198). So the page's own entry says both, once, and everything under it inherits:

Theme.define({
	page: {
		// As tall as the viewport, so a short page is painted to the bottom. The host's own body
		// margin still shows as a band of its default colour around the entry, white against a dark
		// page, and a theme entry cannot reach `body`: the page's HTML takes it off with one line,
		// `<style>body { margin: 0 }</style>`.
		minHeight: '100vh',
		background: '$background',
		color: '$foreground',
	},
});

Without one the page reads in the host's own default colours, which is right in light and wrong in dark, so a page that offers dark mode needs this entry.

Every themed element honours hidden (design 207). The root entry carries the rule inside @layer aweft, because an unlayered rule loses to a layered one whatever its specificity and every entry that lays an element out declares a display in there. So hidden on a themed element takes it off the screen and out of the accessibility tree, the way it does on an unthemed one.

Dark is a theme. dark and light are partial themes handed to the provider:

mount(document.body, <Theme value={dark}><App /></Theme>);

Either may nest inside the other, and each subtree resolves its own roles, because a provider's subtree generates its own classes. Following the operating system is your call, in your own theme, with the _media_ directive the engine already has.

A cell in the value is what a switch is. <Theme value={mode}> with mode a cell holding light or dark moves every class below it when the cell moves, on a page that is already up. A plain object subscribes to nothing and costs nothing extra.

No unnamed value. A component of this package writes $name, never a colour, a size or a duration. node packages/testing/scripts/check-theme.ts refuses one that does, over packages/ui/src and recipes/, and packages/ui/tokens.txt is the committed list of every name there is. A value with no name yet gets one in the entry that needs it:

Theme.define({ splash: { $splashHeight: '320px', minHeight: '$splashHeight' } });

No segment that is an entry name. The same check refuses a theme key whose segments after the first name a top-level entry that lays an element out, because a class list is matched segment by segment and that entry is compiled onto the same element: the square button was button_icon and took the icon entry's display: inline-block; width: 1em along with it. The segment is square now, and size="icon" is still what you write.

Point the check at your own source to adopt the same rule; nothing makes you. A path is resolved against the repository root, so an absolute one is taken as it is, and a path with no .ts or .tsx file under it fails instead of passing over nothing.

node packages/testing/scripts/check-theme.ts /home/me/app/src

Overriding it. Theme.define adds to the theme; it refuses one property of one entry given two different values, so it is not how you replace what the default already says. A <Theme value={...}> provider is, and it is also what scopes an override to part of a page. Entry names the default theme does not use are yours to define outright.

A dev-mode warning. When a chain resolves both color and background from named values and the pair is below 4.5:1, this package says so in the console, with the ratio, the target and the role to use. Theme-derived pairs only, and the call is not in a release build.

Two dev-mode throws, on the page itself (design 266). The first mount or hydrate into a browser page throws when <html> has no lang, and then when the document has no title, each with its fix: lang="en" (or the page's language) on the root, and a <title> in the shell or a Title on the page. The page is read once, in the same tick as the mount, so a shell that carries a <title> is what a page should have for the first paint; a Title the page mounts is attached before the check reads it. A light document, a server render and a document inside a frame are not read. A release build has neither check.

What these and the contrast warning cannot see, a test can: audit and walk from @aweftjs/testing/browser run axe and a Tab walk over the page a test drives, and the build refuses the faults the source settles (@aweftjs/build, The access rules).

Routing

import { createRouter } from '@aweftjs/dom/router';
import { Stage, StageContext, mount } from '@aweftjs/ui';

const acts = {
	'': Home,
	'posts/:id': Post,
	docs: Docs,               // renders a StageContext of its own
	about: 'site/About',      // a module, loaded when the URL reaches it
	join: 'auth/SignIn',      // a module from a battery, on the URL you chose
	missing: NotFound,
};

const router = createRouter();
mount(document.body, (
	<StageContext
		router={router}
		sources={[app, authClient]}
		client={client}
		acts={acts}
		template={Layout}
		fallback="missing"
		refused="join"
	>
		<Nav /><Stage />
	</StageContext>
));
router.links(document.body);

StageContext holds the acts and, given a router, the URL; a nested stage under a routed act needs none, because it follows its parent's. Stage renders whichever act is current, inside the template. They are two components because one that did template selection, URL matching, child coordination and the accessibility work at once would be unchangeable.

stage.current names the act the URL chose, from the moment it matches. When that act is refused, the refused act's component is what shows under that name, so a test asking what a page is showing reads the page rather than current.

An act key is a path with no leading slash. '' is the index and matches / only. :name takes one segment, and one trailing *name takes the rest. A trailing bare * takes no segment and no parameter: it matches any path and parks everything from it as the tail for a stage inside the act, which is not rebuilt when that tail moves. A key whose whole text is the path wins outright, and otherwise a literal segment beats :name, :name beats *name and *, and the longer pattern breaks a tie. There are no optional segments and no patterns.

An act is the component, or the name of a module. A component act may carry entries(), an async function returning the parameter sets a static walk should render it at; nothing in this package calls it.

An act module is an ordinary module: deps, defaults, and a factory answering { component, title? }. It is the only form of act that can declare what it needs.

A module an act depends on is built once per page and reused on every later visit, so its factory must hand back live cells and handles rather than an awaited snapshot: the value it returned on the first visit is the value the fifth visit reads.

// A getter over the handle, not `const document = await handle.ready`.
export default ({ client }) => {
	const handle = client.share('board');
	return { ready: handle.ready, get document() { return handle.document; }, stop: handle.stop };
};
// modules/notes/Page.tsx
export const deps = ['auth/Session', 'notes/Current'];
export const entries = async () => (await listNotes()).map((note) => ({ id: note.id }));

export default ({ imports }) => ({
	title: 'Notes',
	component: () => <Notes user={imports.Session.user} notes={imports.Current.document} />,
});
  • component is what the stage renders, handed the same props a component act gets, stage included. An instance with no component function is a loud assert.

  • title is what the live region announces when the act arrives, and nothing else. The browser tab still follows the <Title> the component writes.

  • entries is an export beside deps, so a static walk reads it without running the factory: listing a site's URLs opens no connection and builds no page. A module that exports none answers null, which tells a walk it cannot say what its URLs are.

sources and client. sources is where named acts come from, in precedence order, as createLoader takes them. The stage builds one loader over them for the whole routing tree, and a stage inside an act inherits it and takes no sources of its own. client is the page's connection, handed to every module as its client prop.

That is the page mirroring the server: the platform hands a factory client and nothing else, and everything the application makes is a module that others name in deps. A shared document, a session, a rules table, a gate: each is a module, built in dependency order, and none of them is built in the boot file and threaded around by hand.

When a named act is loaded and unloaded. It is loaded, with its dependencies first, when the stage decides it, and it goes through the same suspend everything slow goes through, with the LoaderContext's loading and failed components. It is unloaded once the next act is showing, so a module both of them depend on is never torn down between them, and the instance's own stop runs there. The modules it depended on stay loaded for the page; when the stage that built the loader is removed, everything it loaded is unloaded in reverse load order.

refused is an act name, shown when loading a named act rejects with a refusal: an error a factory threw that carries a reason. The refused act reads the error as its refusal prop, and gets a retry beside it: call it and the act the URL chose is built again, in place, with the address exactly where it was. auth/SignIn calls it after a successful enter, so a gated page appears once the visitor signs in without anyone navigating. The URL never moves, so the visitor keeps the address they asked for, and refused must name a key with no :name or *name segment, since the refused act renders under the refusing URL's parameters. Anything else that goes wrong (a name no source lists, a cycle, a factory that threw a bare Error) propagates as before.

A static render has no client, so the loader's props carry no client key at all and a module that wants one decides what its absence means. What it must not do is wait for an answer that will never come: render waits on every pending promise, so a factory awaiting one hangs the render. auth/Session is anonymous at once instead, so a static render of a gated page is a gated act refused and the sign-in act in the markup, while a module reading a document with no client renders its waiting state.

{ load } is gone. A name is the lazy form. about: { load: () => import('./about.tsx') } becomes about: 'site/About' in the acts map, a source that lists it, and a small module:

// modules/site/About.tsx
export default () => ({ title: 'About', component: About });

The stage value is StageContext.read(context), or StageContext.use(stage => ...), and every act is handed it as its stage prop, so const Post = (props) => <h1>{props.stage.params.get().id}</h1> needs no context at all. It holds current, params (the :name values), query (a cell: write to it and the URL's query is updated with replace, so a filter leaves one history entry), open and close. A prop named stage in an open does not reach the act; the stage does.

Nesting. An act that renders a StageContext of its own gets what the act above it did not match: /docs/install reaches the docs act, whose child stage sees install. A deep link that arrives before the child has mounted waits for it, and is dropped on the next navigation. One child per stage claims it; a second stage under one act is a content swapper, not a route.

open({ name, template, history, ...props }) shows an act now whatever the URL says, wrapped in the template you name for this open. Props do not accumulate: each open replaces the last one's. history: true pushes a history entry at the URL the page is already on, so back dismisses it and a copied link is the link to the page. It is held in memory against that entry, so a reload lands on the page under it. A stage owns one entry at a time: a second history: true open while one is showing replaces that entry rather than pushing a second, so one back closes whatever is open and lands on the page.

fallback is the 404: an act name, matched last, rendered when nothing matched. initial is what shows when no URL decides: no router, or a parent that took the whole path.

On every act change, in a browser, focus moves to the act's root element (given tabindex="-1" if it cannot take focus), a visually hidden live region announces the act module's title or, with none, the new head title, and the page goes to the top, or to the URL's hash, unless the router has a position saved for this entry. The first act is not a change: a page load should not steal focus. All three are no-ops with no window.

context().stage holds one entry per live StageContext: its declared acts with a loader flag saying which were declared as module names, and each act's entries, its prefix (what its parent actually matched, posts/3 and not posts/:id) and its parent. That is what a static walk reads to know which URLs a site has. The acts come in the order the acts object itself lists them, which puts a whole-number name such as 404 first however it was written.

A page that mounts takes its entry out again when the stage unmounts, so a live page's registry is what is on the page now. render is the exception: it holds the list for the length of the call, because it takes the page down as soon as it has serialized it and a walk reads the list afterwards (design 145). @aweftjs/ssg is what does the walking.

Head tags

const Layout = (props) => <div><Title>My site</Title>{props.children}</div>;

const Post = () => (
	<article>
		<Head>
			<Title>{post.title}</Title>
			<Meta name="description" content={post.summary} />
			<Link rel="canonical" href={`https://example.com/posts/${post.id}`} />
		</Head>
		…
	</article>
);

Head, Title, Meta, Link, Script and Style render nothing where they are written and put a tag in the render's head list. Head opens a deeper scope, and within a group the deepest tag wins, then the latest, so a page beats the layout it is inside without knowing the layout is there.

A group is the tag's own identity, or an explicit key: a title is one per page; a meta by its charset, http-equiv, name or property; a link by rel and href together, with rel="canonical" a singleton; a script by its src and type, or inline plus type; a style by its media. A tag with none of those and no key is additive, so every one of them is emitted. Two inline scripts of one type are one group: give each a key to keep both.

Every value takes a value or a cell, and a cell rewrites the tag in place.

Tags come out in one fixed order however they were written: charset, viewport, other meta, title, links that preload or preconnect, styles, other links, then scripts.

const ui = context();
const body = await render(<App />, { context: ui });
const page = `<!doctype html><html><head>${ui.head.markup()}</head><body>${body}</body></html>`;

head.markup() stamps each tag with data-aweft-head, its group. mount writes the list into document.head as one run at the front of it, because document.title is the first title element there is and one appended after a page shell's would do nothing. A <meta charset> the shell wrote first stays first, because a charset read late is not read at all. hydrate adopts a stamped tag whose group matches, updating it in place rather than removing and re-adding it.

Two renders in one page neither adopt nor remove each other's tags, and each writes its own title into the head. document.title is whichever of them is nearest the front, so the one mounted last takes the tab. Use one render per page and mount the whole page into it, which is what the default shared render already does.

A static render holds its list, because render takes the page down as soon as it has serialized it and markup() is read afterwards. Use one context() per page.

What it never decides

Storage, transport, and anything server-side. Data fetching: suspend takes a promise and does not make one. Upload transport. Auth. Where analytics go: InputContext fires and the application listens. What your application looks like: a default theme ships so a bare theme="button" renders as something readable, and every value in it is yours to replace. What your own components name their own values, and whether you run the theme check. Which acts a site has, what a template looks like, and whether links outside the routed root are intercepted. Writing pages to disk, which is ssg. Your faces, your sizes and your measure, and what a text modifier renders. Whether text is editable: Typography never becomes an input.

Boundaries

Tier 7, client plane. It imports @aweftjs/core and @aweftjs/dom and nothing else, and nothing in the stack imports it.

Nothing at module scope here can be seen through from one page to another. The theme definitions are the one store, and they are data written once at import. The rest is caches keyed on an object the caller already holds: the tag identity behind each <mark.name>, what a theme says as a string, and the merge of a render's own theme with a provider's. A page's own state, its class cache and stylesheet, its id counter and its popup sink, is on the object each render makes. The render every default mount into a page shares is kept on that page's document, not here.

Known limits

A Modal in server markup does not hydrate. Modal builds its <dialog> and hands that element to the control that opens it, so a static render writes the open attribute into the markup, the client has not written it when the pairing walk reaches the element, and dom reports the mismatch; and because a hydration keeps the server's element and drops the client's, the control would be left driving a node nobody can see. You meet it only when a page rendered on a server names Modal as a stage template, since a modal is otherwise opened by an interaction and is not in a page's markup at all. Tooltip and Popup avoid this by reading their element back out of the mount that put it in the document (design 153), which Modal cannot do until dom says which node a mount put in the document and lets a component write an attribute a hydration reconciles rather than compares.

A token is not a string. label.length, '' + label and String(label) on a token answer a function's, because the string only exists in a render. textOf(context, label) is the string, and every place inside this package that reads a label uses it. A token in a prop that is not text is written as its string all the same: in class beside a theme it is lost to the theme's class, and in a handler it is a string where a function was wanted.

Where a token's cells are not followed. A head tag resolves a token once, when it declares its tag, so a <Title> over a plural whose count moves keeps its first string; and a Typography whose label is a cell holding a token follows the cell, not a cell inside the token's values. A token child, a token in a prop, and a token label written directly all follow their cells.

The parse cache grows with distinct sources. A message parses once per source string and the parts are kept for the life of the process, so a token whose source is built at run time (text(`${word} items`)) parses and keeps one entry per distinct string. Write the message once and put the word in a hole.

A Button whose label cell starts empty throws in development. The name check reads the button as it first mounts, and a cell that fills in later is a nameless button at that moment. Give the button a static aria-label for the empty state, which a screen reader wants in any case.

The design notes

A design NNN above is the note of that number in docs/design/, which says what was decided, why, what it costs, and what would reverse it.

API

Every export of @aweftjs/ui, its signature as the compiler resolves it, and its block comment.

@aweftjs/ui

Act

type Act = ActComponent | string;

What an act key maps to: the component itself, or the name of a module that makes one.

ActComponent

type ActComponent = Component<Record<string, unknown>> & { entries?: ActEntries; };

An act that is the component itself, with the static walk's parameter source on it.

ActEntries

type ActEntries = () => Promise<readonly Readonly<Record<string, string>>[] | null>;

The parameter sets an act should be rendered at.

Declared on an act, or exported by an act module beside its deps, and read by a static walk. Nothing in this package calls it. null means the act cannot say what its URLs are, which is what an act module that exports no entries answers (design 242).

ActInstance

interface ActInstance { readonly component: Component<Record<string, unknown>>; readonly title?: unknown; }

What an act module's factory answers (design 242).

Alert

Alert: (props: AlertProps) => unknown

A message about the page.

Params

  • props: title, icon, type, element, and anything else, which goes to the element

  • children: the body

Returns a <div> on the alert entry, role="alert" when type is danger and role="status" otherwise, so a message that matters interrupts a screen reader and a message that does not waits its turn. success is one of the latter: a thing that went right waits (design 216). The title is on alert_title, the body on alert_body, and an icon on alert_symbol in a first column the box grows only when it was given one. The icon is yours. This package ships no drawings (design 144), so an Icon by name needs an Icons provider above it, and there is no default icon here.

Example

<Alert title="Saved" icon={<Icon name="check" />}>Everything went through.</Alert>
<Alert type="danger" title="Nothing was saved">The server refused the write.</Alert>
<Alert type="success" title="Saved">Your changes are live.</Alert>

AlertProps

interface AlertProps { readonly title?: unknown; readonly icon?: unknown; readonly type?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What Alert takes. Everything not named here goes to the element.

Avatar

Avatar: (props: AvatarProps) => unknown

A picture of a person.

Params

  • props: src, alt, fallback, size, round, and anything else, which goes to the outer <span>

Returns a <span> on the avatar entry holding an <img> on avatar_image and a <span> on avatar_fallback. The fallback shows until the image fires load and shows again if it fires error; with no src there is no image at all and the fallback is what renders. Whichever of the two is not showing carries hidden, so it is out of the accessibility tree as well as off the screen. size is sm ($controlSm), nothing ($control), lg ($controlLg), or any CSS length, which is written as the element's width and height (design 215). The fallback's letters are a fraction of the box, so they follow it at every size. A src that is a cell builds the <img> from the first paint, whatever the cell holds, and the image's src follows it: a cell that has not resolved yet is not the same as no picture. A plain src of nothing builds no <img> at all.

Example

<Avatar src={person.photo} alt={person.name} fallback="TL" />
<Avatar fallback="AB" size="sm" round={false} />
<Avatar src={photo} fallback="AB" size="120px" />

AvatarProps

interface AvatarProps { readonly src?: unknown; readonly alt?: unknown; readonly fallback?: unknown; readonly size?: unknown; readonly round?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What Avatar takes. Everything not named here goes to the outer <span>.

Badge

Badge: (props: BadgeProps) => unknown

A short label on a fill.

Params

  • props: label, type, size, icon, element, and anything else, which goes to the element

Returns a <span> on the badge entry: the accent fill at $textXs, or the quiet, danger, success and outline variants of it. Its size axis is padding and text rather than a control height, because a badge is not a control and a row of 36px blocks is not what a caller asked for. It is never interactive. A badge somebody presses is a Button with size="sm".

Example

<Badge label="New" />
<Badge label="3 failed" type="danger" size="sm" />

BadgeProps

interface BadgeProps { readonly label?: unknown; readonly type?: unknown; readonly size?: unknown; readonly icon?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What Badge takes. Everything not named here goes to the element.

Breadcrumb: (props: BreadcrumbProps) => Mounter

Where a page sits, as a trail of links.

Params

  • props: items, label, element, and anything else, which goes to the <nav>

Returns a <nav aria-label> holding an <ol>. Every item but the last is an <a href>; the last is a <span aria-current="page">, because you do not link to the page you are on. A <span aria-hidden="true"> sits between them, drawn by the theme as a chevron, so no icon pack is needed to render one. An item before the last with no href renders as a span too: it reads as a level with nowhere to go rather than as a link that does nothing.

Example

<Breadcrumb items={[{ label: 'Home', href: '/' }, { label: 'Files', href: '/files' },
  { label: 'shot.png' }]} />
interface BreadcrumbItem { readonly label?: unknown; readonly href?: unknown; }

One level of the trail.

interface BreadcrumbProps { readonly items?: unknown; readonly label?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What Breadcrumb takes. Everything not named here goes to the <nav>.

Button

Button: (props: ButtonProps, _cleanup: Cleanup, mounted: Mounted) => Mounter

A button, or a link drawn as one.

Params

  • props: label, type, size, icon, iconPosition, disabled, loading, round, inline, href, hrefNewTab, onClick, track, element, and anything else, which goes to the element

Returns a <button type="button">, or an <a> when href is given. With href and hrefNewTab left alone it also carries target="_blank" and rel="noopener noreferrer", because a new tab that can reach back at the page it came from is a hole nobody meant to open. While it is loading it is disabled and shows the LoaderContext loader in place of its icon, so a form that would submit twice on a double click cannot. Fires the InputContext click event with { component: 'Button', label, href }, unless track is false.

Example

<Button label="Save" onClick={() => save(form)} />
<Button label="Docs" href="https://example.com/docs" type="quiet" />

ButtonProps

interface ButtonProps { readonly label?: unknown; readonly type?: unknown; readonly size?: unknown; readonly icon?: unknown; readonly iconPosition?: string; readonly disabled?: unknown; readonly loading?: unknown; readonly round?: unknown; readonly inline?: unknown; readonly href?: unknown; readonly hrefNewTab?: unknown; readonly onClick?: (event: unknown) => unknown; readonly track?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What Button takes. Everything not named here goes to the element.

Card

Card: (props: CardProps) => unknown

A raised block with a heading, a body and a foot.

Params

  • props: title, description, foot, type, tight, element, and anything else, which goes to the element

  • children: the body

Returns a <div> on the card entry: the surface fill, a border, the larger radius and $space4 of padding, which tight takes away. Given a title, a description or a foot it also takes the stack segment and builds the parts: the title on card_title, the description on card_description in $mutedForeground, the children in a card_body, and the foot as a row, with $space4 between them. Each part renders only where it was given something. Given none of the three it is the bare block and the children are its own children (design 211).

Example

<Card title="Today" description="What is due" foot={<Button label="Add" />}>
  <p theme="text">Nothing yet.</p>
</Card>

<Card><h2 theme={['text', 'lg']}>Today</h2><p theme="text">Nothing yet.</p></Card>

CardProps

interface CardProps { readonly title?: unknown; readonly description?: unknown; readonly foot?: unknown; readonly type?: unknown; readonly tight?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What Card takes. Everything not named here goes to the element.

Catalog

type Catalog = Readonly<Record<string, string>>;

A language's translations: the key a text token looks up, to the message to show for it.

Category

interface Category { readonly items: unknown[]; readonly props: Record<string, unknown>; }

One slot's children, with every mark of that name merged into props.

Checkbox

Checkbox: (props: CheckboxProps) => Mounter

A tick box.

Params

  • props: value, label, description, error, invert, indeterminate, disabled, type, size, onChange, element, and anything else, which goes to the <input>

Returns an <input type="checkbox">, inside a <div> with its label when it was given one. Space toggles it, because it is the platform's own box. invert flips what ticked means: the box is ticked while the cell is false. What the cell holds is unaffected, so a caller reading it reads the same thing either way. indeterminate is a property rather than an attribute in the platform, so it is not in the markup a static render writes and it is set again on the first mount, which is where it can be set at all.

Example

<Checkbox label="Remember me" value={remember} />

CheckboxProps

interface CheckboxProps { readonly value?: unknown; readonly label?: unknown; readonly description?: unknown; readonly error?: unknown; readonly invert?: unknown; readonly indeterminate?: unknown; readonly disabled?: unknown; readonly type?: unknown; readonly size?: unknown; readonly onChange?: (next: boolean, event: unknown) => void; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What Checkbox takes. Everything not named here goes to the element.

ColorPicker

ColorPicker: (props: ColorPickerProps, cleanup: (...fns: (() => void)[]) => void) => Mounter

A colour, picked on a square and two sliders.

Params

  • props: value, hasAlpha, disabled, type, element, and anything else, which goes to the wrapper

Returns a <div> holding a swatch, a saturation and brightness square with a thumb in it, and one or two Sliders: hue, and opacity when hasAlpha is not false. Each slider is labelled, so each is found by name and announces its number; the swatch is aria-hidden, because it says what the rest already say. The square's thumb is role="slider", focusable, and carries both axes: aria-valuenow is the saturation and aria-valuetext reads "saturation 40%, brightness 80%". There is no two-axis role in ARIA and one control that says both beats two a person has to switch between (design 222). Left and right move saturation, up and down move brightness, Home and End take saturation to its ends, and Shift makes any of them coarse. A press anywhere in the square moves the thumb there. The cell holds CSS colour text, anything readColour reads, and is written back as rgb() or rgba(). A drag, a key or a slider writes it, and nothing else does: mounting this on a colour leaves that colour alone. Writing the cell from outside moves the thumb and the sliders. With hasAlpha false there is no opacity slider, and a write keeps the alpha the cell already had rather than making the colour opaque.

Throws an assert, loud in development and stripped in a release build, for text that is not a colour this package can read, naming the text.

Example

<ColorPicker value={picked} hasAlpha={false} />

ColorPickerProps

interface ColorPickerProps { readonly value?: unknown; readonly hasAlpha?: unknown; readonly disabled?: unknown; readonly type?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What ColorPicker takes. Everything not named here goes to the wrapper.

Component

type Component<P = Record<string, unknown>> = (props: P & { children?: unknown[]; each?: unknown; }, cleanup: Cleanup, mounted: Mounted, pending: Pending) => unknown;

A component: called once with its props, and what it returns is mounted.

Context

interface Context<T> { (props: ProviderProps): unknown; readonly def: T; read(context: unknown): T; node(context: unknown): ContextNode<T> | null; use<P extends Record<string, unknown>>(build: (value: T) => Component<P>): Component<P>; }

A context: the provider component, and the four ways to reach what it holds.

ContextNode

interface ContextNode<T> { readonly id: string; readonly parent: ContextNode<T> | null; readonly children: MutableArray<ContextNode<T>>; value(): T; }

One live provider.

ContextOptions

interface ContextOptions { readonly locale?: string | undefined; readonly catalog?: Catalog | undefined; }

What a page may say about the language a render shows. Both take undefined as well as nothing, so an entry writes context({ locale, catalog }) whether or not this page has a catalog.

Countries

Countries: Context<CountryData | null>

The codes, subdivisions and aliases the country fields below read.

Nothing is here by default, because this package ships no country data (design 251). Fill it from @aweftjs/ui/countries, which reads the optional country-region-data peer, or hand over a CountryData of your own: five codes and their regions is a whole value.

Example

const data = await countryData();
<Countries value={data}><Country value={country} /></Countries>

Country

Country: (props: CountryProps, _cleanup: (...fns: (() => void)[]) => void, mounted: (...fns: (() => void)[]) => void) => Mounter

The country field: a button that opens a searchable list of every country the provider has.

Params

  • props: value, locale, priority, flags, suggest, and everything Chooser takes: label, description, error, placeholder, title, search, none, open, disabled, type, size, name, autocomplete, onChange, element, theme

Returns the chooser, over one row per code the provider holds. The cell holds the two-letter code, and a form posts that. The rows are ordered: the suggested country first, then priority in the order it was given, then everything else by its name in the page's own language. Nothing is chosen to begin with, and the suggestion never becomes the value: a person from one country filling a form for another is ordinary, and a guess that fills the field is a mistake nobody notices. A search matches the name, the code and this package's aliases, without accents: uk finds the United Kingdom, cote finds Côte d'Ivoire.

Throws the assert for a page with no Countries provider above it.

Example

<Countries value={data}>
  <Country label="Country" value={country} name="country" autocomplete="country" />
</Countries>

CountryData

interface CountryData { readonly codes: readonly string[]; regions(code: string): readonly Subdivision[]; readonly aliases?: Readonly<Record<string, readonly string[]>>; }

What Countries holds: the codes a form may offer, their subdivisions, and the aliases.

CountryProps

interface CountryProps { readonly value?: unknown; readonly locale?: unknown; readonly priority?: readonly string[]; readonly flags?: unknown; readonly suggest?: unknown; readonly [prop: string]: unknown; }

What Country takes. Everything not named here goes to Chooser and on to the button.

Default

Default: (props: { children?: unknown[] | undefined; }) => unknown

The template that adds nothing: the act, and no element around it.

This is what a stage uses when the page named no template, and it is exported so a page that names one for some acts can name this one for the rest rather than leaving a gap.

Params

  • props: children, the act

Returns the children, unchanged.

Example

stage.open({ name: 'preview', template: Default });

Definitions

type Definitions = Readonly<Record<string, Entry>>;

A whole theme: entries by _-joined selector path.

Detached

Detached: (props: { enabled?: unknown; locations?: readonly Placement[] | undefined; onResize?: ((rect: Rect) => void) | undefined; style?: Record<string, unknown> | undefined; children?: unknown[] | undefined; }, cleanup: (...fns: (() => void)[]) => void) => unknown

A popup placed next to its own children.

Params

  • enabled: the open state, a cell. Setting it false closes the popup, and so does scrolling

  • locations: the placements to consider, in order of preference. Omitted, the eight corner modes

  • onResize: called when the anchor changes size and the popup is re-placed

  • style: merged onto the popup's box

  • children: the anchor, with <mark.popup> for what floats

Returns the anchor where it was written, and the popup at the sink. The anchor is an ordinary mount, so a page taken over from a server adopts the nodes the server sent (design 153).

Throws the assert categories makes for a slot it does not know.

Example

<Detached enabled={open}>
  <button onClick={() => open.set(!open.get())}>menu</button>
  <mark.popup><Card>what floats</Card></mark.popup>
</Detached>
DropDown: (props: DropDownProps) => Mounter

A button and the block it shows.

Params

  • props: open, label, icon, iconOpen, iconClose, arrow, name, type, disabled, element, and anything else, which goes to the <details>

  • children: what shows while it is open, in the page's flow

Returns a <details> with a <summary> wearing the button theme. Space and Enter toggle it, a screen reader reads it as a button and says whether it is expanded, and none of that is written here. The open cell goes both ways: writing it opens and closes the element, and a person opening it writes the cell. name puts it in a group: the platform keeps one <details> of a name open and closes the rest, and the one that closes fires its own toggle, so every cell in the group follows. A stack of disclosures that keeps one open is a run of these sharing a name, and there is no component for it (design 212). A floating menu is not this: that is Detached with a Button anchor (design 136).

Throws the assert elementFor makes for an element that is not a <details>.

Example

<DropDown label="Filters" open={shown}><Filters /></DropDown>
interface DropDownProps { readonly open?: unknown; readonly label?: unknown; readonly icon?: unknown; readonly iconOpen?: unknown; readonly iconClose?: unknown; readonly arrow?: unknown; readonly name?: unknown; readonly type?: unknown; readonly disabled?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What DropDown takes. Everything not named here goes to the <details>.

Empty

Empty: (props: EmptyProps) => unknown

Nothing here yet, and what to do about it.

Params

  • props: icon, title, description, element, and anything else, which goes to the element

  • children: the action row, usually one or two Buttons

Returns a <div> on the empty entry: a centred column with $space6 of padding, holding empty_symbol, empty_title, empty_description and empty_actions. Each part renders only where it was given something.

Example

<Empty icon={<Icon name="inbox" />} title="No messages" description="They will show up here.">
  <Button label="Refresh" onClick={reload} />
</Empty>

EmptyProps

interface EmptyProps { readonly icon?: unknown; readonly title?: unknown; readonly description?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What Empty takes. Everything not named here goes to the element.

Entry

type Entry = Readonly<Record<string, unknown>>;

One theme entry: CSS declarations, $var definitions, extends, and _directive_ blocks.

FileDrop

FileDrop: FileDropComponent

A place to drop files.

Params

  • props: files, extensions, multiple, limit, clickable, disabled, onDrop, ready, type, element, and anything else, which goes to the zone

  • children: given, they replace the prompt line and the listing this draws for itself

Returns a <div> holding a <label>, a visually hidden <input type="file"> and either the default chrome or your children. A drop, a click and the keyboard all reach the same input. The zone listens for dragenter, dragleave and drop (reading dataTransfer.files), and the input for change (reading files). An entry is { name, file, status, error, reason }. A file the zone accepted starts as ready and one it refused as error, with error the sentence a person reads and reason the code a page branches on: type for a file the extensions do not cover, size for one over limit, and count for a second file while multiple is false (design 214). A refused file stays in the list. Move status to loading while you upload by writing the entry back into the list (files[0] = { ...files[0], status: 'loading' }), which is the edit the list can hear. limit is bytes, and every sentence naming it writes it in KB, MB or GB, so a limit of 4_000_000 reads as 3.8 MB. The prompt names the accepted types the way a person says them, so image/png reads as png and image/* reads as image. ready is written null while any entry is loading, and otherwise the file, or the array of files when multiple is true, counting every entry that is not in error. FileDrop.Button is a Button that opens a file dialog. Inside a FileDrop it opens that zone's input and takes no checking props of its own; outside one it is the picker with no chrome, taking files, extensions, multiple, limit, onDrop and ready itself. There is no upload here: the entry carries the platform File and the rest is yours.

Throws the assert elementFor makes for an element that is not a <div>, the assert files makes for a value that is not a cell, and the assert a FileDrop.Button inside a zone makes when it was given a checking prop the zone already owns. All three are loud in development and stripped in a release build.

Example

<FileDrop files={picked} extensions={['image/png', 'image/jpeg']} limit={4_000_000} />
<FileDrop.Button label="Change photo" extensions={['image/*']} multiple={false} ready={photo} />

FileDropButtonProps

interface FileDropButtonProps extends ButtonProps { readonly files?: unknown; readonly extensions?: unknown; readonly multiple?: unknown; readonly limit?: unknown; readonly onDrop?: (files: unknown[]) => void; readonly ready?: unknown; }

What FileDrop.Button takes: the Button props, and the checks when it stands on its own.

FileDropComponent

interface FileDropComponent { (props: FileDropProps): Mounter; Button(props: FileDropButtonProps): Mounter; }

FileDrop, with the button that opens a file dialog.

FileDropEntry

interface FileDropEntry { readonly name: string; readonly file: unknown; readonly status: 'ready' | 'loading' | 'error'; readonly error?: string; readonly reason?: 'type' | 'size' | 'count'; }

One file the zone was given, as the application reads it.

FileDropProps

interface FileDropProps { readonly files?: unknown; readonly extensions?: unknown; readonly multiple?: unknown; readonly limit?: unknown; readonly clickable?: unknown; readonly disabled?: unknown; readonly onDrop?: (files: unknown[]) => void; readonly ready?: unknown; readonly type?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What FileDrop takes. Everything not named here goes to the zone.

Head: (props: { children?: unknown[] | undefined; }) => unknown

A deeper override scope for the head.

Everything inside is one level deeper than everything outside, and within a group the deepest tag wins. A layout writes its defaults outside a Head and a page overrides them from inside one; two pages that both override are decided by which mounted last.

Params

  • children: the subtree whose head tags are one level deeper

Example

<Head><Title>{post.title}</Title></Head>

HeadKind

type HeadKind = 'title' | 'meta' | 'link' | 'script' | 'style';

The elements a head component makes.

HeadList

interface HeadList extends Registry<HeadTag> { markup(): string; title(): string | null; }

The render's head tags: a registry, plus the two questions something else asks of it.

HeadTag

interface HeadTag { readonly kind: HeadKind; readonly group: string | null; readonly depth: number; readonly attrs: Readonly<Record<string, unknown>>; readonly text?: unknown; }

One tag, as a head component declared it.

Hydrated

type Hydrated = Remove & { readonly ready: Promise<void>; }; (from @aweftjs/dom)

What hydrate answers: the remove function, and when the pairing walk finished.

Icon

Icon: (props: IconProps, cleanup: (...fns: (() => void)[]) => void, _mounted: unknown, pending: (promise: Promise<unknown>) => void) => Mounter

One icon, drawn from icon data.

Params

  • props: name, either icon data or a name to look up through Icons; size, a CSS length; label, its name for a screen reader; rot, degrees; and anything else, which goes to the <svg>

Returns one <svg>, filled with currentColor and sized in em so it matches the text beside it. With a label it carries role="img" and that label; without one it is aria-hidden and unfocusable, because an icon beside the word it means is otherwise read out twice. The element lasts as long as the component. A name that is a cell swaps the drawing inside it and never the element itself.

Throws an assert, loud in development and stripped in a release build, for a name no source in the stack answers. A resolver that answers a promise says so late, so that assert is thrown where the host reports it rather than into the promise nobody holds; a resolver that fails reports the same way, naming the reason, and the element stays empty.

Example

<Icon name="check" label="done" />
<Icon name="chevron-down" rot={90} />

IconAlias

interface IconAlias { readonly parent: string; readonly rotate?: number; readonly hFlip?: boolean; readonly vFlip?: boolean; }

One name pointing at another, with its own turns and flips on top.

IconData

interface IconData { readonly body: string; readonly width?: number; readonly height?: number; readonly left?: number; readonly top?: number; readonly rotate?: number; readonly hFlip?: boolean; readonly vFlip?: boolean; }

One icon, in the shape the icon sets publish.

IconPack

interface IconPack { readonly prefix?: string; readonly icons: Readonly<Record<string, IconData>>; readonly aliases?: Readonly<Record<string, IconAlias>>; readonly width?: number; readonly height?: number; }

A set of icons under one prefix.

IconProps

interface IconProps { readonly name?: unknown; readonly size?: unknown; readonly label?: unknown; readonly rot?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What Icon takes. Everything not named here goes to the <svg>.

IconResolver

type IconResolver = (name: string) => IconData | Promise<IconData | null> | null;

A function that finds an icon, or answers null so the next source in the stack is asked.

IconSource

type IconSource = IconPack | IconResolver;

What Icons holds: packs and resolvers, newest first.

Icons

Icons: Context<IconSource[]>

The icon packs and resolvers everything below can look a name up in.

The stack starts empty: this package ships no drawings (design 144). A provider stacks what it names in front of what it inherited, so the nearest one wins. A pack is { prefix, icons, aliases?, width?, height? }, where the root size covers every icon in it that declares none; a resolver is (name) => data | Promise<data> | null, and answering null passes the question on to the next source.

Example

<Icons value={myPack}><App /></Icons>
<Icons value={(name) => fetch(`/icons/${name}.json`).then((r) => r.json())}><App /></Icons>

Ids

interface Ids { next(prefix?: string): string; }

The id source behind an aria-labelledby and its friends.

Input

interface Input { readonly meta?: Readonly<Record<string, unknown>>; readonly on?: (event: Record<string, unknown>) => void; readonly [handler: string]: unknown; }

What an application puts on the context.

InputContext

InputContext: Context<Input> & { fire: (context: unknown, type: string, payload?: Record<string, unknown>) => void; }

The input handlers and the metadata for everything below.

meta merges one level deeper than the rest, so a page adds a tag without replacing the tags above it.

Example

<InputContext value={{ meta: { page: 'home' }, on: (e) => analytics.send(e) }}>{app}</InputContext>
Link: (props: TagProps) => unknown

One <link>.

Params

  • key: a group of your own

Returns nothing where it is written. Its group is rel and href together, except rel="canonical", which is one per page whatever it points at. A <link> with no rel is additive.

Example

<Link rel="canonical" href={`https://example.com${path}`} />

Loader

type Loader<P> = (props: P, cleanup: (...fns: (() => void)[]) => void) => unknown;

What a loader is handed, and what it hands back.

LoaderContext

LoaderContext: Context<Loaders>

The loading and failure components for everything below.

Fields are inherited one at a time, so a provider that names only failed keeps whatever loading was already in effect.

Example

<LoaderContext value={{ loading: Spinner, failed: ErrorPanel }}>{app}</LoaderContext>

Loaders

interface Loaders { readonly loading?: Component | null; readonly failed?: Component<{ error?: unknown; }> | null; }

The loading and failure components an application sets once, for everything below.

LoadingDots

LoadingDots: (props: LoadingDotsProps) => unknown

Three pulsing dots.

Params

  • props: type, size, label, and anything else, which goes to the <span>

Returns a <span> holding three more. With a label it is a polite live region carrying that text; without one it is aria-hidden.

Example

<LoaderContext value={{ loading: LoadingDots }}><App /></LoaderContext>

LoadingDotsProps

interface LoadingDotsProps { readonly type?: unknown; readonly size?: unknown; readonly label?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What LoadingDots takes. Everything not named here goes to the element.

Mark

interface Mark { (name: string, props?: Record<string, unknown> | null, ...children: unknown[]): Marked; readonly then: MarkMaker; readonly else: MarkMaker; readonly case: MarkMaker; readonly default: MarkMaker; readonly popup: MarkMaker; readonly anchor: MarkMaker; readonly tabs: MarkMaker; readonly panels: MarkMaker; readonly [slot: string]: MarkMaker; }

What mark is: callable by name, and a tag under any slot name.

The eight slots this package's own components read are declared one by one rather than left to the index signature, because noUncheckedIndexedAccess widens every read of an index signature with undefined, and a JSX tag whose type may be undefined is not a tag the compiler will call. A slot name outside that list works at run time and is written mark('name', props, ...children) in a TypeScript file.

MarkMaker

type MarkMaker = ((props?: Record<string, unknown>, ...children: unknown[]) => unknown) & { readonly [MAKER]: string; };

What mark.name is: a tag ui's h turns into a mark rather than an element.

Markdown

Markdown: (props: MarkdownProps) => Mounter

A markdown string as themed blocks.

Params

  • props: source, modifiers, code, element, theme, and anything else, which goes to the element

Returns a <div> on the markdown entry holding one element per block. A heading is a Typography h1 to h6 on markdown_heading with an id in GitHub's scheme; a paragraph is p1 on markdown_paragraph; a fenced block is a <pre> on markdown_code with the language on data-language, holding what code answers; a list is a <ul> or <ol> on markdown_list of <li> on markdown_item, a task item with a Checkbox that follows the source and writes it back when the source is a writable cell; a table is a <table> on markdown_tabular in the table_scroll box with the table_* parts; a blockquote is on markdown_quote; a rule is an <hr> on markdown_rule. Inline, a code span is <code> on markdown_inline, bold <strong> on markdown_bold, italic <em> on markdown_italic, a link <a> on markdown_link with the href as written, unless its scheme is not http, https, mailto or tel, in which case the link is text. Each is a modifier in the render's TextModifiers shape, listed after modifiers, so an application's own patterns run inside markdown. A nested list, an image, a footnote, an HTML tag, an autolink and an escape are text, as written.

Throws the assert for a source that is neither a string, a number nor a cell holding one.

Example

<Markdown source={readme} />
<Markdown source={note} modifiers={[{ check: /@\w+/g, return: (who) => <Mention name={who} /> }]} />
<Markdown source={doc} code={(text, language) => <Highlighted text={text} language={language} />} />

MarkdownProps

interface MarkdownProps { readonly source?: unknown; readonly modifiers?: unknown; readonly code?: (text: string, language: string | null) => unknown; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What Markdown takes. Everything not named here goes to the element.

Marked

interface Marked { readonly [MARKED]: string; readonly name: string; readonly props: Record<string, unknown> & { readonly children: unknown[]; }; }

One slot's contents, as the parent component reads it.

Menu: (props: MenuProps, cleanup: (...fns: (() => void)[]) => void, mounted: (...fns: (() => void)[]) => void) => Mounter

A button and the actions it opens.

Params

  • props: items, open, label, icon, type, size, disabled, locations, element, and anything else, which goes to the anchor button

  • children: the anchor's contents, beside or instead of label

Returns a Button where it was written, and a <div role="menu"> of <div role="menuitem"> rows in a popup placed under it, flipping above when there is no room below. An item is { label, icon?, type?, disabled?, onSelect }, and type: 'danger' draws the row in the danger colour. A group is { heading, items } and draws a small heading over its own rows. The keys are design 223's, the same map a Select uses: the arrows move and wrap, Home and End go to the ends, typing moves by what a row reads, Enter and Space choose, Escape and Tab close. Escape and an outside click close it, and Escape and choosing put the focus back on the anchor. Opening moves the focus onto the role="menu" element, which is the ARIA menu-button pattern and where aria-activedescendant names the row the keys are on. The anchor carries none of that: a role="button" may not (design 225). The anchor is the button this component builds, so the ARIA is in the markup rather than written onto somebody else's node. Give it your own contents as children, or hand the element in. The list goes in the nearest <dialog> above it, then a PopupContext, then the page, so a menu inside a modal opens inside that modal and can be clicked.

Example

<Menu label="Actions" items={[
  { label: 'Rename', onSelect: rename },
  { label: 'Delete', type: 'danger', onSelect: remove },
]} />
interface MenuGroup { readonly heading?: unknown; readonly items?: readonly MenuItem[]; }

A run of actions under a heading.

interface MenuItem { readonly label?: unknown; readonly icon?: unknown; readonly type?: unknown; readonly disabled?: unknown; readonly onSelect?: (event: unknown) => void; }

One action in a menu.

interface MenuProps { readonly items?: unknown; readonly open?: unknown; readonly label?: unknown; readonly icon?: unknown; readonly type?: unknown; readonly size?: unknown; readonly disabled?: unknown; readonly locations?: readonly Placement[]; readonly element?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What Menu takes. Everything not named here goes to the anchor button.

Meta

Meta: (props: TagProps) => unknown

One <meta>.

Params

  • key: a group of your own

Returns nothing where it is written. Its group is its charset, http-equiv, name or property, in that order; a <meta> with none of those is additive and every one is emitted.

Example

<Meta name="description" content={summary} />
Modal: (props: ModalProps, cleanup: (...fns: (() => void)[]) => void, mounted: (...fns: (() => void)[]) => void) => Mounter

The act, in a modal dialog.

Params

  • props: label, noEsc, noClickEsc, type, side, element, theme and class. A prop this does not name reaches the act and goes no further here

Returns a <dialog>, opened as this mounts, holding a heading, a close button and the act. type="sheet" is the same dialog against an edge, sliding in from it rather than growing in the middle (design 202). side says which edge, right by default, and is read for no other type. Every close is the stage's close(): Escape through the element's own cancel event, a mousedown on the backdrop, and the close button. So a modal opened with history: true is taken down by the history entry going away, whichever of the three the person used (design 124). A stage calls a template as h(template, props, act), where props is what the open carried past name, history, template and children (design 213). The act is handed the same props, so a prop this component names, label or type or side, is read here as well as there.

Throws an assert, loud in development and stripped in a release build, when there is no stage above it, and the one elementFor makes for an element that is not a <dialog>.

Example

stage.open({ name: 'edit', history: true, template: Modal, label: 'Edit' });
stage.open({ name: 'filters', template: Modal, label: 'Filters', type: 'sheet' });

ModalProps

interface ModalProps { readonly label?: unknown; readonly noEsc?: unknown; readonly noClickEsc?: unknown; readonly type?: unknown; readonly side?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly class?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What Modal takes. A stage hands its template the props the open carried (design 213), so this component reads the ones it names and passes nothing else to the <dialog>: an open carrying a row for the act would otherwise write it on the element as an attribute.

OpenOptions

interface OpenOptions { readonly name: string; readonly template?: Component; readonly history?: boolean; readonly [prop: string]: unknown; }

What open takes. Everything past the three named fields is props for the act.

Pagination

Pagination: (props: PaginationProps, cleanup: (...fns: (() => void)[]) => void) => Mounter

The pages of a long list, as buttons.

Params

  • props: page (a cell, counted from 1), count, siblings, onChange, size, label, element, and anything else, which goes to the <nav>

Returns a <nav aria-label> holding a previous button, the page buttons, and a next button, all type="quiet". The page showing now carries aria-current="page" and the current segment, which is the filled look. Previous is disabled on page 1 and next on the last page. Pages left out are a <span aria-hidden="true"> on pagination_gap, because an ellipsis is not something to read out.

Example

<Pagination page={page} count={12} onChange={(at) => load(at)} />

PaginationProps

interface PaginationProps { readonly page?: unknown; readonly count?: unknown; readonly siblings?: number; readonly onChange?: (page: number, event: unknown) => void; readonly size?: unknown; readonly label?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What Pagination takes. Everything not named here goes to the <nav>.

Placed

interface Placed { readonly mode: Placement; readonly left: number; readonly top: number; readonly maxWidth: number; readonly maxHeight: number; readonly transformOrigin: string; }

Where the popup ends up, and what it may grow to.

Placement

type Placement = 'below-start' | 'below-end' | 'above-start' | 'above-end' | 'right-start' | 'right-end' | 'left-start' | 'left-end' | 'below' | 'above' | 'right' | 'left';

The twelve places a popup can go.

The eight corner modes put one of the popup's corners on one of the anchor's, so the popup hangs off a corner: below-start is under the anchor with their left edges lined up. The four side modes centre the popup on one edge of the anchor.

Popup: (props: { placement?: unknown; canClose?: ((event: unknown) => boolean) | undefined; style?: Record<string, unknown> | undefined; ref?: ((element: unknown) => void) | undefined; children?: unknown[] | undefined; }) => Mounter

A floating box, rendered at the popup sink rather than where it is written.

Params

  • placement: where it goes, a cell so it can move; null hides it

  • canClose: given the event, whether an outside click should close it. Omitted, any outside click closes it by setting placement to null

  • style: merged onto the popup's own box

  • children: what is inside

Returns nothing where it is written. The element is in its sink until this unmounts. Its sink is the nearest <dialog> above where it was written, then the sink a PopupContext gave, then the element the page was mounted into (design 113, amended). So a popup opened inside a modal is inside that dialog and can be clicked, and a page with no PopupContext still works.

Example

<Popup placement={where}><Card>what floats</Card></Popup>

PopupContext

PopupContext: (props: { popups?: Registry<unknown> | undefined; children?: unknown[] | undefined; }) => Mounter

Where every popup below mounts, unless it is inside a <dialog>, which wins.

Renders its children, then the popups, so a popup is after the page in DOM order. Together with the popover attribute that is the whole of the stacking story: no z-index anywhere. A page needs one of these only to choose where its popups go; a page with none still opens them.

Params

  • popups: a registry to share with something else. Omitted, this render's own is used by the first PopupContext to ask for it and a fresh one by every later one

  • children: the page

Example

mount(document.body, h(PopupContext, {}, h(App, {})));

PopupPlacement

type PopupPlacement = Placed | null;

Where a popup sits, as Popup takes it. null hides it.

Progress

Progress: (props: ProgressProps) => unknown

How far along something is.

Params

  • props: value, label, size, element, and anything else, which goes to the <progress>

Returns a <progress max="1"> on the progress entry: a $muted track and an $accent bar, $space2 thick. value is a fraction of 1, so nothing has to divide; null or nothing leaves the attribute off, which is what the platform reads as indeterminate and draws as the moving bar. A number outside 0 to 1 is clamped to it, and anything that is not a finite number, NaN and a string included, is indeterminate.

Throws an assert, loud in development and stripped in a release build, when element is anything but a <progress>.

Example

<Progress value={done} label="Uploading" />
<Progress label="Working" />

ProgressProps

interface ProgressProps { readonly value?: unknown; readonly label?: unknown; readonly size?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What Progress takes. Everything not named here goes to the element.

ProviderProps

interface ProviderProps { readonly value?: unknown; readonly children?: unknown[]; }

What a provider takes: the value for the subtree below it.

Radio

Radio: (props: RadioProps) => Mounter

One choice out of a group.

Params

  • props: value, the cell the group shares; option, this one's value; label, description, error, disabled, type, size, onChange, element, and anything else, which goes to the <input>

Returns an <input type="radio">, inside a <div> with its label when it was given one. Every radio handed the same value cell is one group, so the arrow keys move between them and Tab steps over the group as a whole, both from the platform.

Example

<Radio value={size} option="small" label="Small" />
<Radio value={size} option="large" label="Large" />

RadioProps

interface RadioProps { readonly value?: unknown; readonly option?: unknown; readonly label?: unknown; readonly description?: unknown; readonly error?: unknown; readonly disabled?: unknown; readonly type?: unknown; readonly size?: unknown; readonly onChange?: (next: unknown, event: unknown) => void; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What Radio takes. Everything not named here goes to the element.

Rect

interface Rect { readonly left: number; readonly top: number; readonly width: number; readonly height: number; }

A box on the screen, in the coordinates getBoundingClientRect uses.

Region

Region: (props: RegionProps) => Mounter

The subdivision field: the provinces, states or regions of one country.

Params

  • props: value, country, and everything Chooser takes

Returns the chooser, over the subdivisions the provider has for that country. The cell holds the subdivision's short code. It is the same control as Country and for the same reason: one country has 217 subdivisions and the middle one has 11, and a threshold that opened a dropdown for a small country and a dialog for a large one would change the control under the person as they picked a country. Handed no country, or one the provider has no subdivisions for, it is disabled and says so. It does not clear the cell when the country changes: the page owns that write, and a form that clears a field the person filled has to be the page's decision. The names are the data's, which is English. Hand Countries a CountryData of your own to translate them.

Throws the assert for a page with no Countries provider above it.

Example

<Region label="Province" value={region} country={country} name="region"
        autocomplete="address-level1" />

RegionProps

interface RegionProps { readonly value?: unknown; readonly country?: unknown; readonly [prop: string]: unknown; }

What Region takes. Everything not named here goes to Chooser and on to the button.

Registry

interface Registry<T = unknown> { readonly items: MutableArray<T>; add(item: T): () => void; claim(): boolean; }

A list a component pushes into and something else renders.

Render

interface Render { readonly theme: Sheet; readonly head: HeadList; readonly stage: Registry<StageEntry>; readonly ids: Ids; readonly popups: Registry; readonly locale?: string; readonly catalog?: Catalog; }

Everything one render owns.

Script

Script: (props: TagProps) => unknown

One <script>.

Params

  • children: the inline source, when there is no src

  • key: a group of your own

Returns nothing where it is written. Its group is its src and type, or, with no src, that it is inline and its type. Two inline scripts of one type are therefore one group and only the winner is emitted; give each a key to keep both.

Throws an assert naming the fix when the inline source contains </script, which would end the element early and cannot be escaped inside JavaScript.

Example

<Script src="https://example.com/a.js" async />

Select

Select: (props: SelectProps, cleanup: (...fns: (() => void)[]) => void, mounted: (...fns: (() => void)[]) => void) => Mounter

A choice from a list.

Params

  • props: value, options, display, placeholder, open, label, description, error, disabled, type, size, name, autocomplete, onChange, element, and anything else, which goes to the button

Returns a <button role="combobox"> and a drawn list, inside a <div> with its label when it was given one. The cell holds the item, never the text the person reads, so an object list comes back as objects. The list is a <div role="listbox"> of <div role="option"> rows in a popup placed under the control at the control's width, and it is this package's on every host. It opens on a click, on ArrowDown, ArrowUp, Enter and Space, and on a printable character, which opens it and runs the type-ahead in the one press. The whole keyboard map is design 223's. A hidden <select> carries the same options and the same choice, takes name and autocomplete, and writes the cell when anything writes it. So a form posts the value and autofill reaches the control, and what it cannot do is draw its own highlight over a button. placeholder shows while the cell holds nothing and is not a row in the list. A cell holding an item the list does not have shows the placeholder and selects nothing.

Throws the assert elementFor makes for an element that is not a <button>.

Example

<Select label="Size" value={size} options={['small', 'large']} />
<Select value={user} options={users} display={(u) => u.name} placeholder="Pick someone" />

SelectProps

interface SelectProps { readonly value?: unknown; readonly options?: unknown; readonly display?: unknown; readonly placeholder?: unknown; readonly open?: unknown; readonly label?: unknown; readonly description?: unknown; readonly error?: unknown; readonly disabled?: unknown; readonly type?: unknown; readonly size?: unknown; readonly name?: unknown; readonly autocomplete?: unknown; readonly onChange?: (next: unknown, event: unknown) => void; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What Select takes. Everything not named here goes to the button.

Sheet

interface Sheet { classes(definitions: Definitions, classes: readonly string[]): string; variable(definitions: Definitions, classes: readonly string[], name: string): string | null; call(definitions: Definitions, classes: readonly string[], name: string): ThemeFunction | null; value(definitions: Definitions, classes: readonly string[], text: string): string; base(): Definitions; markup(): string; watch(fn: (css: string) => void): () => void; readonly text: Derived<string>; }

The theme systems for one render: its class cache and its stylesheet.

Shown

Shown: (props: { value?: unknown; invert?: unknown; children?: unknown[] | undefined; }) => unknown

One of two subtrees, by a condition.

Params

  • value: the condition, a value or a cell

  • invert: flip it

  • children: the truthy branch, with <mark.else> for the other

Returns whichever branch applies, following value when it is a cell.

Example

<Shown value={open}>
  <Menu />
  <mark.else><p>nothing open</p></mark.else>
</Shown>

Skeleton

Skeleton: (props: SkeletonProps) => unknown

A grey box standing in for something that has not arrived.

Params

  • props: width, height, round, and anything else, which goes to the <div>

Returns a <div aria-hidden="true"> on the skeleton entry: the $muted fill, pulsing inside prefers-reduced-motion: no-preference and still outside it. width and height go through style, so a bare number is pixels and any CSS length works. It says nothing to a screen reader. The thing that is loading is what announces that, and three boxes announcing it three times is worse than silence.

Example

<Skeleton width="12rem" />
<Skeleton width={40} height={40} round={true} />

SkeletonProps

interface SkeletonProps { readonly width?: unknown; readonly height?: unknown; readonly round?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What Skeleton takes. Everything not named here goes to the element.

Slider

Slider: (props: SliderProps) => Mounter

A number on a line.

Params

  • props: value, label, description, error, min, max, step, disabled, type, size, track, element, and anything else, which goes to the <input>

Returns an <input type="range">, inside a <div> with its label when it was given one. The cell holds a number, not the text the element carries. step: 0 is written out as any, which is what the platform calls a range with no steps in it. An onInput of your own is called with the event, after the cell has been written. Fires the InputContext slide event with { component: 'Slider', label, value } on every change, unless track is false.

Example

<Slider label="Volume" value={volume} min={0} max={11} />

SliderProps

interface SliderProps { readonly value?: unknown; readonly label?: unknown; readonly description?: unknown; readonly error?: unknown; readonly min?: unknown; readonly max?: unknown; readonly step?: unknown; readonly disabled?: unknown; readonly type?: unknown; readonly size?: unknown; readonly track?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What Slider takes. Everything not named here goes to the element.

Source

interface Source { candidates(): Promise<readonly Candidate[]>; } (from @aweftjs/modules)

Somewhere modules come from. Listing evaluates nothing.

Stage

Stage: () => Mounter

Where the current act is rendered.

Returns the act current names, inside the template chosen for it. An act named as a module runs through suspend, so it shows the LoaderContext's loading component while it arrives and its failed component if it never does. Inside a hydration it shows neither: the server's markup stays until the act arrives (design 243).

Throws an assert, loud in development and stripped in a release build, when there is no StageContext above it.

Example

<StageContext router={router} acts={acts}><Nav /><Stage /></StageContext>

StageAct

interface StageAct { readonly name: string; readonly loader: boolean; readonly entries: ActEntries | null; }

One declared act, as the registry reports it.

StageContext

StageContext: StageContextComponent

Hold a set of acts, and the URL when a router is given.

Params

  • acts: the acts, by path. '' is the index, :name takes one segment, one trailing *name takes the rest, and a trailing bare * takes nothing and parks the rest as the tail for a stage inside the act. A value is the component, or the name of a module whose factory answers { component, title? } (design 242)

  • template: what wraps the act. A pass-through when it is left off

  • fallback: the act shown when nothing matched. This is the 404, and it is matched last

  • initial: the act shown when no URL decides: no router, or a parent that took the whole path

  • router: the router this stage reads. Without one the stage is a content swapper driven by open and close, and a stage inside an act takes what its parent did not match

  • sources: where a named act comes from. The stage builds one loader over these for the whole routing tree, so a stage inside an act inherits it and takes no sources

  • loader: the loader the modules are already in, instead of sources, from a platform that built it; the stage loads acts from it and never closes it (design 282)

  • client: the page's connection, handed to every module as its client prop

  • refused: the act shown when loading a named act rejects with a refusal (design 244)

  • children: the page, with a Stage somewhere in it

Returns the subtree, with the stage in scope for everything under it. Reach it with StageContext.read(context) or StageContext.use(stage => ...). Every act is also handed the stage as its stage prop, so an act that only wants params or query takes it as an argument. A named act is loaded, with its dependencies first, when the stage decides it, and unloaded once the next act is showing; the modules it depended on stay loaded for the page, and go when the stage is removed.

Throws an assert, loud in development and stripped in a release build, for an act key that is not relative, has an empty segment, has a : with no name, has a * segment anywhere but last, or cannot be told apart from another key; for a fallback, initial or refused naming an act that is not declared; for a refused naming a key with a :name or *name segment, which would render under another act's parameters; for a nested stage given sources or a loader; for sources beside a loader; for a client with no sources; for a named act with no sources or loader anywhere above it; and for an act module whose instance carries no component.

Example

<StageContext router={router} sources={[app]} client={client}
  acts={{ '': Home, 'posts/:id': 'posts/Page' }} fallback="404" refused="join">
  <Nav /><Stage />
</StageContext>

StageContextComponent

interface StageContextComponent { (props: StageProps): unknown; read(context: unknown): StageValue | null; node(context: unknown): ContextNode<StageValue | null> | null; use<P extends Record<string, unknown>>(build: (stage: StageValue | null) => Component<P>): Component<P>; }

StageContext, with the three ways to reach the stage from outside a consumer.

StageEntry

interface StageEntry { readonly acts: readonly StageAct[]; readonly prefix: string; readonly parent: StageEntry | null; readonly fallback: string | null; readonly current: string | null; }

One live StageContext, read-only, so a static walk can enumerate the pages of a site from one render of it.

StageProps

interface StageProps { readonly acts: Readonly<Record<string, Act>>; readonly template?: Component; readonly fallback?: string; readonly initial?: string; readonly router?: Router; readonly sources?: readonly Source[]; readonly loader?: Loader; readonly client?: unknown; readonly refused?: string; readonly children?: unknown[]; }

What StageContext takes.

StageValue

interface StageValue { readonly current: Derived<string | null>; readonly params: Derived<Readonly<Record<string, string>>>; readonly query: Derived<Record<string, string>>; open(options: OpenOptions): void; close(): void; }

The stage an act is in: what is showing, what the URL said, and the two ways to change it.

Style

Style: (props: TagProps) => unknown

One <style>.

Params

  • children: the CSS

  • key: a group of your own

Returns nothing where it is written. Its group is its media, so a page overrides a layout's styles for one media query and leaves the others alone.

Throws an assert naming the fix when the CSS contains </style.

Example

<Style media="print">{'@page { margin: 2cm; }'}</Style>

Subdivision

interface Subdivision { readonly code: string; readonly name: string; }

One subdivision of one country: a province, a state, a region, a prefecture.

Switch

Switch: (props: { value?: unknown; cases?: Record<string, unknown> | undefined; children?: unknown[] | undefined; }) => unknown

One subtree of several, by a value or by the first truthy cell.

Params

  • value: matched against each <mark.case value=...>, a value or a cell

  • cases: an object of cells, the first truthy key winning; not to be given with value

  • children: <mark.case value=...> for each branch, and <mark.default> for the rest

Returns the branch that matched, following whichever input is reactive.

Throws an assert, loud in development and stripped in a release build, when both value and cases are given, or when a case has no value.

Example

<Switch value={status}>
  <mark.case value="loading"><Spinner /></mark.case>
  <mark.case value="ready"><List /></mark.case>
  <mark.default><p>nothing yet</p></mark.default>
</Switch>

Tab

Tab: (props: TabProps) => Mounter

One of the tabs in a strip.

Params

  • props: value, label, disabled, element, and anything else, which goes to the element

  • children: the label as markup, for anything label cannot say

Returns a <button type="button" role="tab"> on the tab entry, naming its panel with aria-controls and saying whether it is the one showing with aria-selected. Only meaningful inside a Tabs: the variant, the height, the ids and the value cell all come from the group.

Throws an assert, loud in development and stripped in a release build, when there is no Tabs above it.

Example

<Tab value="drafts" label="Drafts" />

TabItem

interface TabItem { readonly value?: unknown; readonly label?: unknown; readonly disabled?: unknown; readonly content?: unknown; }

One tab and the panel under it, for the common case where both are one line to write.

TabPanel

TabPanel: (props: TabPanelProps) => Mounter

What one tab shows.

Params

  • props: value, element, and anything else, which goes to the element

  • children: the panel's contents

Returns a <div role="tabpanel" tabindex="0"> on the tabs_panel entry, named by its tab through aria-labelledby. It is focusable because the second Tab press out of the strip has to land somewhere, and a panel holding nothing focusable would swallow it. A panel that is not showing carries hidden and stays mounted, so coming back to it finds it as it was left. Wrap the contents in a Shown to have them built again instead.

Throws an assert, loud in development and stripped in a release build, when there is no Tabs above it.

Example

<TabPanel value="drafts"><DraftList /></TabPanel>

TabPanelProps

interface TabPanelProps { readonly value?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What TabPanel takes. Everything not named here goes to the element.

TabProps

interface TabProps { readonly value?: unknown; readonly label?: unknown; readonly disabled?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What Tab takes. Everything not named here goes to the <button>.

Table

Table: (props: TableProps) => Mounter

A table of rows and columns.

Params

  • props: columns, rows, cell, caption, foot, label, striped, tight, type, element, and anything else, which goes to the <table>

Returns a <table> inside a <div> on table_scroll, so a table wider than its box scrolls in place rather than widening the page. The box is focusable, because one that scrolls and cannot be focused is unreachable from a keyboard. rows goes through each, so pushing a row inserts one <tr> and moves nothing else. A cell function that returns a different shape for different rows steps outside what a list can clone; packages/dom/README.md says what that costs. Give it a caption or a label: without one the table has no name for a screen reader.

Example

<Table columns={['name', 'size']} rows={files} caption="Everything in this folder" />
<Table
  columns={[{ key: 'name', label: 'Name' }, { key: 'bytes', label: 'Size', align: 'right' }]}
  rows={files}
  cell={(file, column) => (column.key === 'bytes' ? readable(file.bytes) : file.name)}
/>

TableColumn

interface TableColumn { readonly key: string; readonly label?: unknown; readonly align?: string; readonly width?: unknown; }

One column: which value it shows, what it is called, and how it sits.

TableProps

interface TableProps { readonly columns?: readonly (TableColumn | string)[]; readonly rows?: unknown; readonly cell?: (row: unknown, column: TableColumn) => unknown; readonly caption?: unknown; readonly foot?: unknown; readonly label?: unknown; readonly striped?: unknown; readonly tight?: unknown; readonly type?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What Table takes. Everything not named here goes to the <table>.

Tabs

Tabs: (props: TabsProps, cleanup: (...fns: (() => void)[]) => void, mounted: (...fns: (() => void)[]) => void) => Mounter

A set of panels with one showing, and the strip that picks between them.

Params

  • props: value, tabs, orientation, type, size, label, onChange, element, and anything else, which goes to the element

  • children: <mark.tabs> holding Tabs and <mark.panels> holding TabPanels, for anything tabs cannot say

Returns a <div> on the tabs entry holding a <div role="tablist"> on tabs_list and one panel per tab. Each tab and its panel name each other with aria-controls and aria-labelledby, off ids minted from the render's counter, so a page rendered on a server and the hydration that adopts it agree (design 109). The arrows move the selection as well as the focus, so Right steps to the next panel and shows it. Home and End go to the ends, a disabled tab is stepped over, and Tab leaves the strip for the panel rather than for the next tab. type="line" drops the filled strip for an underline under the tab showing. orientation vertical stands the strip beside the panels and turns the arrows to Up and Down.

Throws an assert, loud in development and stripped in a release build, for a bare child: the tabs and the panels go in two different places, so each says which it is.

Example

<Tabs label="Views" value={view} tabs={[
  { value: 'all', label: 'All', content: <All /> },
  { value: 'mine', label: 'Mine', content: <Mine /> },
]} />

TabsProps

interface TabsProps { readonly value?: unknown; readonly tabs?: unknown; readonly orientation?: unknown; readonly type?: unknown; readonly size?: unknown; readonly label?: unknown; readonly onChange?: (next: unknown, event: unknown) => void; readonly element?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What Tabs takes. Everything not named here goes to the element.

TagProps

interface TagProps { readonly key?: string; readonly children?: unknown[]; readonly [attribute: string]: unknown; }

Everything a head component takes beyond its own named props: attributes, and a key.

TailClaim

interface TailClaim { readonly tail: Derived<string>; readonly base: string; readonly router: Router | null; release(): void; }

What claimTail hands a routed child that is not a stage.

TextArea

TextArea: (props: TextAreaProps) => Mounter

Several lines of text, growing to fit them.

Params

  • props: value, label, description, error, placeholder, maxHeight, onEnter, onKeyDown, disabled, type, size, element, and anything else, which goes to the <textarea>

Returns a <textarea> on its own, or the textarea inside a <div> with its label, description and error, when it was given any of the three. It measures itself after every change and takes the height of its content, up to maxHeight, after which it scrolls. On a host with no layout, which is a static render and the light tree, there is nothing to measure and it keeps the height the theme gives it.

Example

<TextArea label="Notes" value={notes} maxHeight="12rem" />

TextAreaProps

interface TextAreaProps { readonly value?: unknown; readonly label?: unknown; readonly description?: unknown; readonly error?: unknown; readonly placeholder?: unknown; readonly maxHeight?: unknown; readonly onEnter?: (event: unknown) => void; readonly onKeyDown?: (event: unknown) => void; readonly disabled?: unknown; readonly type?: unknown; readonly size?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What TextArea takes. Everything not named here goes to the element.

TextField

TextField: (props: TextFieldProps) => Mounter

One line of text.

Params

  • props: leading, trailing, value, label, description, error, placeholder, password, onEnter, onKeyDown, disabled, type, size, element, and anything else, which goes to the <input>

Returns an <input> on its own, or the input inside a <div> with its label, description and error, when it was given any of the three. The cell and the element follow each other: typing writes the cell, and writing the cell writes the element. With a leading or a trailing, the input goes inside a <div> on the input_group entry which carries the border, the radius, the fill and the height, and the input itself carries none of them, so the two read as one control (design 210). The focus ring is on the box. An addon that is text is wrapped on input_group_addon; anything else is mounted as it is, which is what lets an Icon or a Button of size="icon" go there. With neither, none of that is rendered. While error says something the field carries aria-invalid, its aria-describedby names the message, and the message is a live region, so it is read out when it arrives.

Example

<TextField label="Email" value={email} error={emailError} />
<TextField label="Price" leading="$" trailing="CAD" value={price} />

TextFieldProps

interface TextFieldProps { readonly leading?: unknown; readonly trailing?: unknown; readonly value?: unknown; readonly label?: unknown; readonly description?: unknown; readonly error?: unknown; readonly placeholder?: unknown; readonly password?: unknown; readonly onEnter?: (event: unknown) => void; readonly onKeyDown?: (event: unknown) => void; readonly disabled?: unknown; readonly type?: unknown; readonly size?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What TextField takes. Everything not named here goes to the element.

TextModifier

interface TextModifier { readonly check: string | RegExp; readonly return: (match: string) => unknown; readonly [key: string]: unknown; }

One rule for turning part of a label into something else. Keys beyond these two are ignored.

TextModifiers

TextModifiers: Context<TextModifier[]>

The modifiers every Typography below runs over its label.

A provider replaces the list above it rather than adding to it, so a subtree that wants both writes both. The list applies to label only: children are already markup and have nothing for a pattern to run over.

Example

<TextModifiers value={[{ check: 'TODO', return: (word) => <b>{word}</b> }]}>
  <Typography label="TODO: write this" />
</TextModifiers>

TextSource

interface TextSource { readonly source: string; readonly key: string; readonly values: TextValues | undefined; }

What a text token was made from.

TextToken

interface TextToken { (...args: never[]): unknown; readonly [TEXT]: TextSource; }

A text token: mountable as a child, and readable through isText and textOf.

TextValues

interface TextValues { readonly context?: string; readonly [name: string]: unknown; }

The values a message names: a hole's value, a plural's number, a select's word, a tag's function. A cell among them is followed. context is not a value: it disambiguates the key.

Theme

Theme: ThemeApi

A partial theme for everything below, and the registry every render starts from.

Example

Theme.define({ card: { padding: 16 } });
mount(document.body, h(Theme, { value: { card: { padding: 24 } } }, h(App, {})));

ThemeApi

interface ThemeApi extends Context<Definitions> { define(entries: Definitions): void; }

The provider, and the one call that writes definitions.

ThemeCascade

interface ThemeCascade { (props: ProviderProps): unknown; readonly def: string[]; read(context: unknown): string[]; node(context: unknown): ContextNode<string[]> | null; use<P extends Record<string, unknown>>(build: (h: Themed) => Component<P>): Component<P>; }

The theme prefix every element below inherits, and the h that applies it.

ThemeContext

ThemeContext: ThemeCascade

The theme every element below inherits.

Params

  • value: one segment, or a list of them, added to what is already inherited

  • children: the subtree that inherits it

Example

<ThemeContext value="primary"><Panel /></ThemeContext>

ThemeFunction

type ThemeFunction = (args: string[]) => string;

A theme function: the resolved arguments in, the value out.

A function is theme data rather than a table in this package. It sits in an entry beside the variables, under a $name key, and the same precedence walk finds it, so an application's theme can add one and a nested theme can shadow one (design 111). The colour and arithmetic functions ship as entries of the default theme.

Themed

type Themed = (tag: unknown, props?: Record<string, unknown> | null, ...children: unknown[]) => unknown;

An h that carries the inherited theme. Same signature as h.

Title

Title: (props: TagProps) => unknown

The page's title.

Params

  • children: the text, a string or a cell. A cell rewrites the tag in place

  • key: a group of your own, for the rare page that wants two titles in the list

Returns nothing where it is written. The tag goes in the render's head list.

Example

<Head><Title>{post.title}</Title></Head>

Toggle

Toggle: (props: ToggleProps) => Mounter

An on and off switch.

Params

  • props: value, label, description, error, disabled, type, size, onChange, element, and anything else, which goes to the <input>

Returns an <input type="checkbox" role="switch">, inside a <div> with its label when it was given one.

Example

<Toggle label="Email me" value={subscribed} />

ToggleProps

interface ToggleProps { readonly value?: unknown; readonly label?: unknown; readonly description?: unknown; readonly error?: unknown; readonly disabled?: unknown; readonly type?: unknown; readonly size?: unknown; readonly onChange?: (next: boolean, event: unknown) => void; readonly element?: unknown; readonly theme?: unknown; readonly [prop: string]: unknown; }

What Toggle takes. Everything not named here goes to the element.

Tooltip

Tooltip: (props: TooltipProps, cleanup: (...fns: (() => void)[]) => void, mounted: (...fns: (() => void)[]) => void) => Mounter

A tip beside its anchor, on hover and on focus.

Params

  • props: label, enabled, locations, type, and anything else, which goes to the panel

  • children: the anchor, with an optional <mark.popup> holding markup instead of the label

Returns the anchor where it was written, and the panel at the popup sink, inside the box Detached places. The panel is role="tooltip" and every element in the anchor carries aria-describedby naming it. The box is what reaches the top layer, so the panel carries no popover attribute: a popover inside a popover is laid out by the browser and leaves the box the solver placed. The pause before a hover shows it belongs to the behaviour, so every tip on a page waits the same amount of time and there is no prop for it. Focus shows it at once. aria-describedby is written onto the anchor's nodes when the page comes alive, rather than built into the markup, because those nodes are the caller's. A static render leaves it out, so markup a page is taken over from carries the anchor and the panel with no link between them and the link appears on the first live mount. It is taken off again when this unmounts.

Throws the assert categories makes for a slot this component does not know.

Example

<Tooltip label="Delete this for good"><Button label="Delete" type="danger" /></Tooltip>

TooltipProps

interface TooltipProps { readonly label?: unknown; readonly enabled?: unknown; readonly locations?: unknown; readonly type?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What Tooltip takes. Everything not named here goes to the panel.

Transform

type Transform<T> = (raw: unknown, parent: T, children: MutableArray<ContextNode<T>>) => T;

How a provider's raw value becomes the value below it.

raw is what the provider was given, exactly as written: a cell arrives as the cell, because a transform that writes back into one needs it. Read one with .get(). The result is cached until raw changes, so a cell keeps the value live and a plain value costs one call.

Typography

Typography: (props: TypographyProps) => Mounter

One run of themed text.

Params

  • props: type, label, element, theme, and anything else, which goes to the element

Returns one element on the text entry plus a segment per word in type. h1 to h6 give that heading, p, p1 and p2 give a <p>, and everything else gives a <span>. With no type at all it is a <span> on text alone. The later segment wins where two disagree, so h2_bold is a heading at the bold weight. label renders first and children after it, and neither is required. The TextModifiers above it run over label only; a label that is neither a string nor a number renders as given. A type that is a cell moves the theme when it changes. It does not move the element: the element lasts as long as the component, as Icon's does. A page that needs the tag to change puts the two spellings in a Switch.

Throws the assert elementFor makes for an element that is not an element, such as a component or a themed element written as <p theme="card" />, which is a function.

Example

<Typography type="h2_bold" label="Today" />
<Typography type="p2" label={note} />

TypographyProps

interface TypographyProps { readonly type?: unknown; readonly label?: unknown; readonly element?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What Typography takes. Everything not named here goes to the element.

Validate

Validate: (props: ValidateProps, cleanup: (...fns: (() => void)[]) => void) => Mounter

Check what is in a control, and say what is wrong with it.

Params

  • props: value, validate, signal, valid, error, showError, icon, type

  • children: the control

Returns the control, and after it the message while there is one, as a live region in the field_error entry. The message reaches the control too: a control of this package that was given no error of its own goes aria-invalid and its aria-describedby names the message rendered here (design 138). A Validate around a plain <input> still shows and announces the message, and the input itself stays unmarked, because nothing read it. The children are one control. Every control under a Validate takes the message, so one wrapped around two of them marks both invalid and points both at the same message; two controls want two Validates. With a signal, checking follows what that cell holds: nothing is checked while it is falsy, and every change to value is checked while it is truthy (design 208). A form that clears itself and writes its signal back to false is quiet again until the next submit, and going quiet clears the message, writes valid true and writes error null. With no signal, checking is live from the start. Under a ValidateContext, this check runs again when any cell that form is checking changes, so a validator that compares its cell with another field's follows that other field. showError false takes the message off the screen and leaves it announced, because a control that says it is invalid and then says nothing else is a dead end. The validator is given the cell, so one that formats what was typed can write it back. Four of the eight do: phone, pan, expDate and postalCode. A validator that throws is reported the way a handler that throws is reported anywhere in this package, on a microtask the host sees, and the value counts as invalid with the error's message.

Throws an assert, loud in development and stripped in a release build, for a value that is not a cell and for a name that is not one of the eight.

Example

<Validate value={email} validate="email" signal={submitted}>
  <TextField label="Email" value={email} />
</Validate>

ValidateContext

ValidateContext: (props: ValidateContextProps) => Mounter

The form's answer: true while every Validate below it is happy.

Params

  • props: value, a cell this writes

  • children: the form

Returns the children. Each Validate under it registers when it mounts and leaves when it unmounts, so a field that goes away stops holding the form invalid. It also follows every cell those Validates are checking, and runs all their checks again when any one of them changes (design 208). That is what a validator comparing its cell with another field's needs: confirm-must-match reads the other password, and nothing else would tell it that the other password moved.

Example

<ValidateContext value={allValid}><Form /></ValidateContext>

ValidateContextProps

interface ValidateContextProps { readonly value?: unknown; readonly children?: unknown[]; }

What ValidateContext takes.

ValidateProps

interface ValidateProps { readonly value?: unknown; readonly validate?: unknown; readonly signal?: unknown; readonly valid?: unknown; readonly error?: unknown; readonly showError?: unknown; readonly icon?: unknown; readonly type?: unknown; readonly theme?: unknown; readonly children?: unknown[]; readonly [prop: string]: unknown; }

What Validate takes.

categories

categories: (children: readonly unknown[], names: readonly string[], fallback?: string | undefined) => Category[]

Split a component's children into its named slots.

Params

  • children: the component's own props.children

  • names: the slots this component knows, in the order the caller wants them back

  • fallback: the slot a bare child goes in. Omitted, a bare child is refused

Returns one category per name, in that order. A category with nothing in it is empty rather than absent, so a caller can destructure without checking.

Throws an assert, loud in development and stripped in a release build, naming the slots this component knows, for a mark it does not know or a bare child with no slot to go in, and for a name given twice in names.

Example

const [then, otherwise] = categories(props.children, ['then', 'else'], 'then');

claimTail

claimTail: (context: unknown) => TailClaim | null

Claim the parent stage's tail for a component that is a routed child without being a stage (design 282): a frame that routes inside itself, say. It claims exactly what a nested StageContext claims, once, and the tail is released with release when the component unmounts (design 123).

Params

  • context: the mount context the component was handed

Returns the claim, or null when there is no stage above or the tail is already claimed, which is what a second child under one act gets.

Example

const Room = (props): Mounter => (elem, _item, before, context) => {
  const claim = claimTail(context);
  const stop = claim?.tail.effect((tail) => { route.url = `/${tail}`; });
  ...
  return (arg) => { if (arg !== undefined) return remove(arg); stop?.(); claim?.release(); return remove(); };
};

context

context: (options?: ContextOptions) => Render

Make the systems for one render.

Params

  • options: locale, the language the render shows, and catalog, its translations. Both optional; a render with neither shows every text token's source

Returns a fresh object sharing nothing with any other render. Hand it to mount, render or hydrate, or let those make their own.

Example

const ui = context({ locale: 'fr', catalog: fr });
const markup = await render(h(App, {}), { context: ui });
const css = ui.theme.markup();

createContext

createContext: <T>(def: T, transform?: Transform<T>) => Context<T>

Make a context.

Params

  • def: the value where no provider is above

  • transform: how a provider's raw value, the value above it, and its live children become the value below it. Omitted, the raw value wins and a null one inherits

Returns the provider component, carrying def, read, node and use.

Example

const Tone = createContext('plain');
const Button = Tone.use((tone) => (props) => h('button', { theme: ['button', tone] }, props['label']));
mount(document.body, h(Tone, { value: 'accent' }, h(Button, { label: 'Go' })));

dark

dark: Readonly<Record<string, Readonly<Record<string, unknown>>>>

The dark mode.

Returns a partial theme redefining every scale step and every role. Everything else, the type scale, the sizes, the motion, the component entries, is shared with the light mode.

Example

mount(document.body, <Theme value={dark}><App /></Theme>);

// following the operating system is the application's call, in its own theme:
Theme.define({ '*': { '_media_(prefers-color-scheme: dark)': { colorScheme: 'dark' } } });

flagOf

flagOf: (code: string) => string

The flag emoji for a country code: the two letters as regional indicator symbols.

Params

  • code: a two-letter country code, in either case

Returns the two symbols, which a host with flag glyphs draws as a flag and one without draws as the two letters. Anything that is not two letters comes back empty, so a made-up code draws nothing rather than two boxes.

Example

flagOf('ca');  // '🇨🇦'

h

h: (tag: unknown, props?: Record<string, unknown> | null, ...children: unknown[]) => unknown

Make an element, a component's mounter, or a mark.

Params

  • tag: an element name, an existing node, a component, or mark.name

  • props: everything dom's h takes, plus theme, class alongside it, style as an object, isHovered, isFocused, isClicked, isTouched, onXxx, and each:name on a component

  • children: what goes inside, exactly as dom takes it

Returns the element itself when nothing is reactive and ui claimed nothing; a value mount binds when something inside is reactive; a mounter when ui claimed a prop; a mark when the tag is one.

Throws an assert, loud in development and stripped in a release build, for a null tag, a state prop that is not a writable cell, an onXxx that is not a function, and everything dom's own h refuses.

Example

mount(document.body, h('button', { theme: ['button', tone], onClick: go }, 'Go'));

html

html: (strings: TemplateStringsArray, ...values: unknown[]) => unknown

Markup in a template literal, bound to ui's h.

The same tag dom ships, over this package's h, so markup in a ui file is themed the same way JSX in one is.

Example

mount(document.body, html`<button theme="button" onClick=${go}>Go</button>`);

hydrate

hydrate: (target: ParentLike, item: unknown, render?: Render | undefined) => Hydrated

Take over markup render wrote, with the ui systems under it.

Params

  • target: the element whose children are the server's markup

  • item: the same item the server rendered

  • render: the systems to use. Omitted, the target's document is asked for the one every default mount into it shares, as mount does

Returns the remove function with ready on it, as dom does. The <style data-aweft> the server wrote is adopted rather than replaced, so a hydration makes no element for the theme. ready resolves once every act, page or panel the page was still loading has arrived and the markup has been checked (design 243).

Example

const page = hydrate(document.body, h(App, {}));
await page.ready;

isText

isText: (value: unknown) => value is TextToken

Whether a value is a text token.

Params

  • value: anything

Returns true for what text() answered.

Example

if (isText(label)) label = textOf(context, label);

light

light: Readonly<Record<string, Readonly<Record<string, unknown>>>>

The light mode: the values the default theme already starts from.

It exists so a light island can sit inside a dark page, which is the same need read the other way round. A page that is light throughout needs no provider at all.

Example

<Theme value={dark}><App><Theme value={light}><Preview /></Theme></App></Theme>

localeOf

localeOf: (context: unknown) => string | undefined

The language a render shows.

Params

  • context: the opaque context dom handed a mounter, or the render context() made

Returns the BCP 47 tag the page gave context(), or undefined for a page that named none, which is what Intl takes for the host's own language.

Example

new Intl.DateTimeFormat(localeOf(context)).format(when);

localeRegion

localeRegion: () => string | null

The country the host looks like it is in, from the language settings and nothing else.

Returns a two-letter code, or null where there is no host to ask. en-CA is CA; a language with no region in it is maximized, so fr is FR and pt is BR, which is the host's own likely-region data rather than a table of guesses in this file. This is the language setting, not the location: no permission is asked, nothing is fetched, and an American in Toronto whose browser says en-US looks American. It is a suggestion for that reason, and it never becomes the value.

mark

mark: Mark

A named slot of children, for a component that takes more than one.

Written as a tag, <mark.popup>...</mark.popup>, which ui's h reads. Called directly, mark('popup', props, ...children), for a file that does not write JSX. A mark never reaches mount: the component it was written inside reads it with categories.

Example

<Detached enabled={open}>
  <button>menu</button>
  <mark.popup><Menu /></mark.popup>
</Detached>

mount

mount: (target: ParentLike, item: unknown, before?: Remove | undefined, render?: Render | undefined) => Remove

Mount an item, with the ui systems under it.

Params

  • target: an element, or anything dom's mount takes

  • item: anything dom's mount takes

  • before: the anchor, as dom means it

  • render: the systems to use. Omitted, the target's document is asked for the one every default mount into it shares, so two widgets on one page cannot mint the same class name for two different themes

Returns the remove function, as dom does. It also takes the stylesheet back out of the document head, once the last mount sharing it has gone.

Example

const stop = mount(document.body, h(App, {}));

render

render: (item: unknown, options?: { context?: Render | undefined; }) => Promise<string>

Render an item to markup, with the ui systems under it and no browser.

Params

  • item: anything dom's render takes

  • options: context, the systems to use. Omitted, a fresh set is made and the CSS it generated is unreachable, so pass one whenever the page needs its stylesheet

Returns the item's markup. The theme's CSS and the page's head tags are not in it: read context.theme.markup() and context.head.markup() and put both in the page's own head. The head list and the stage list are both held for the whole render, because a static render takes the page down as soon as it has serialized it and both have to still be there afterwards. So a render object that has been through render keeps every tag its page declared and every stage its page mounted; use one per page, as design 109 says. use(context).stage after the call is what a static walk reads to learn which URLs the page declares (designs 126, 145).

Example

const ui = context();
const body = await render(h(App, {}), { context: ui });
const head = `<style data-aweft>${ui.theme.markup()}</style>${ui.head.markup()}`;
const page = `<html><head>${head}</head><body>${body}</body></html>`;

sizeProperties

sizeProperties: ReadonlySet<string>

The property names a bare number is written in pixels for.

A theme or a style prop may say padding: 8, and these are the properties where 8 means eight pixels rather than the number eight. Everything else keeps the number as written, so flexGrow: 1 and zoom: 2 are not broken by the convenience.

standardIcons

standardIcons: readonly string[]

The icon names the components in this package ask for, in the spelling the icon sets publish.

An application whose Icons stack answers all of these has every component covered. @aweftjs/icons/<set>/+standard is this list taken from one installed set.

Example

const missing = standardIcons.filter((name) => myPack.icons[name] === undefined);

suspend

suspend: <P extends Record<string, unknown>>(fallback: Component | null, loader: Loader<P>, failed?: Component<{ error?: unknown; }> | null | undefined) => (props: P & { children?: unknown[] | undefined; }, cleanup: (...fns: (() => void)[]) => void, _mounted: unknown, pending: (promise: Promise<unknown>) => void) => Mounter

A component whose content arrives later.

Params

  • fallback: what to show while the loader runs, or null to use the LoaderContext's loading

  • loader: called once with the component's props and its cleanup; whatever it resolves to is mounted in place of the fallback

  • failed: what to show when the loader rejects, given { error }. Omitted, the LoaderContext's failed is used; with neither, the slot goes empty and the rejection is rethrown on a fresh task so the host reports it

Returns a component. It declares its promise pending, so a static render waits for it and a hydration keeps its pairing walk open for it, and a suspend removed before its loader settles mounts nothing and reports nothing. Inside a hydration no fallback is shown at all: the server's markup stays until the loader answers (design 243).

Example

const Article = suspend(Spinner, async ({ id }) => {
  const article = await fetch(`/articles/${id}`).then((r) => r.json());
  return <Body article={article} />;
});

svg

svg: (tag: unknown, props?: Record<string, unknown> | null, ...children: unknown[]) => unknown

Make an SVG element.

The same as h with two differences: the node is made in the SVG namespace, and there is no theme, so class and style are written as plain attributes.

Params

  • tag: an SVG element name

  • props: as h, without theme

  • children: as h

Returns what h returns.

Example

svg('svg', { viewBox: '0 0 24 24' }, svg('circle', { cx: 12, cy: 12, r: 10 }));

template

template: (spec: TemplateElement, edits: readonly TemplateEdit[]) => Template

The static shape of a ui subtree, made once per document and instanced per use.

The signature is dom's, and so is everything about the shape: build emits a call to this one instead when the file's h came from @aweftjs/ui. What differs is that each instance's properties are split first, so theme, style, onXxx and the state cells do what h would have done with them.

Params

  • spec: the element, its literal attributes and its static children, nested

  • edits: where something varies, in the order the source evaluates the values

Returns a function taking one value per edit. It answers what ui's h would have answered for the same subtree: the instance itself when ui claimed nothing anywhere in it, and otherwise a mounter.

Throws whatever dom's template throws, and the asserts h makes about a claimed prop.

Example

const row = template(['li', null, ['span', null]], [['props', []], ['child', [0], -1]]);
mount(list, row([{ theme: 'row' }, title]));

text

text: (source: string, values?: TextValues | undefined) => TextToken

A piece of text a page shows, to be looked up in the render's catalog where it mounts.

Params

  • source: the message as written, in the subset of ICU MessageFormat the package reads: {name} holes, {n, plural, one {# item} other {# items}}, {kind, select, book {a book} other {a thing}}, and <link>the docs</link> around part of it

  • values: what the message names. A hole takes any value and a cell is followed; a plural takes a number; a select takes a word; a tag takes a function from the inner content to what to mount. context is one word that tells two meanings of the same source apart and becomes part of the key

Returns a token. As a child it mounts the message, translated when the render has a catalog with its key and as written when it has none. In a prop, ui's h writes the resolved string on the element. Typography runs its modifiers over the resolved string, and a head component writes it into its tag. textOf answers the string for code that needs characters.

Throws an assert, loud in development and stripped in a release build, for a source that is not a string. A message that cannot be read asserts where it is first formatted, naming the offset.

Example

<p>{text('Hello {name}', { name })}</p>
<Button label={text('Save')} />
<p>{text('Read <link>the docs</link>', { link: (inner) => <a href="/docs">{inner}</a> })}</p>

textOf

textOf: (context: unknown, value: unknown) => string

The string a value shows, resolved in a render.

Params

  • context: the opaque context dom handed a mounter, or the render context() made, so code outside any mount, a server module building a title, resolves against a render of its own

  • value: a text token, a string, a number, or a cell holding one

Returns for a token, its message in the render's language with its values as they are now written in and a tag reduced to what is inside it; for a string or a number, that; for null or undefined, the empty string. A context with no ui systems shows a token's source.

Example

document.title = textOf(context, text('Inbox'));

trackedMount

trackedMount: () => [MutableArray<NodeLike>, (props: { children?: unknown[] | undefined; }) => unknown]

Mount children into a target that only records them.

Returns the array the real nodes appear in, and a mounter to render where the children belong. The caller renders the mounter and then the array, so the nodes still land in the document and the caller also has them, which is what measuring or listening on children you did not build needs.

Example

const [nodes, virtual] = trackedMount();
return [h(virtual, {}, ...props.children), nodes];

use

use: (value: unknown) => Render

The render a mount belongs to.

Params

  • value: the opaque context dom handed the mounter

Returns the render's systems.

Throws an assert, loud in development and stripped in a release build, when the mount was not started by one of ui's entry points, naming what to call instead.

Example

const sheet = use(context).theme;

useAbort

useAbort: <A extends unknown[]>(fn: (signal: AbortSignal, ...args: A) => void) => (...args: A) => () => void

Turn a function that wants an abort signal into one that hands back its abort.

Params

  • fn: called with a fresh AbortSignal and whatever else the caller passed

Returns a function that runs fn and answers the abort, so it can go straight into cleanup. Every listener registered with { signal } inside comes off in one call.

Example

const listen = useAbort((signal, node) => {
  node.addEventListener('scroll', onScroll, { signal });
  node.addEventListener('resize', onResize, { signal });
});
cleanup(listen(element));

usedText

usedText: (render: Render) => readonly string[]

The keys the text tokens of a render looked up, in the order they were first asked for.

Params

  • render: a render that has mounted or rendered a page

Returns the keys. A key is here whether or not the catalog had an entry for it, which is what lets a static walk report the entries a catalog lacks and the ones nothing uses.

Example

const missing = usedText(ui).filter((key) => catalog[key] === undefined);

@aweftjs/ui/countries

CountryData

interface CountryData { readonly codes: readonly string[]; regions(code: string): readonly Subdivision[]; readonly aliases?: Readonly<Record<string, readonly string[]>>; }

What Countries holds: the codes a form may offer, their subdivisions, and the aliases.

Subdivision

interface Subdivision { readonly code: string; readonly name: string; }

One subdivision of one country: a province, a state, a region, a prefecture.

countryAliases

countryAliases: Readonly<Record<string, readonly string[]>>

The words a person types that no data set carries.

Keyed by the two-letter code, lowercase, and searched with the country's own name. Two kinds of entry: a name the country used to have or is also called (Holland, Burma, Ivory Coast), and the letters a person actually types for it (uk, usa, uae, nz). A formal name is not here: Republic of India is not what anybody types, and the search already matches the name the host gives and the code itself.

countryData

countryData: (load?: (() => Promise<unknown>) | undefined) => Promise<CountryData>

The whole country list, from the package that has it.

Params

  • load: where to get the data from. Omitted, the optional country-region-data peer. Hand one over for a copy of that data you got some other way: a file the page fetched, a module an application vendored, a subset it built. What comes back must have an allCountries

Returns a promise of CountryData over every territory the data has, with this package's aliases on it. Hand it to Countries. The peer is optional and is named here and nowhere else. In a bundler an import it cannot resolve fails the build before this runs, naming the same package; in Node a missing peer is the refusal below.

Throws countries-not-installed when the load fails, carrying the install command, and countries-unreadable when what it answers is not the list this reads. A load of your own that throws refuses the same way, with the same message: the common reason to be here is the peer, and a caller who handed over a loader can see what their own loader did.

Example

const data = await countryData();
<Countries value={data}><Country value={country} /></Countries>

@aweftjs/ui/icon-names

standardIcons

standardIcons: readonly string[]

The icon names the components in this package ask for, in the spelling the icon sets publish.

An application whose Icons stack answers all of these has every component covered. @aweftjs/icons/<set>/+standard is this list taken from one installed set.

Example

const missing = standardIcons.filter((name) => myPack.icons[name] === undefined);

@aweftjs/ui/text.json

Data, not code: the file as the package ships it.

Refusals

Every error this package raises carries a reason to switch on and a fix that says what to do. These are its reasons, from errors.txt.

reasonfix
countries-not-installedInstall the package the message names, or hand your own CountryData to Countries.
countries-unreadableInstall a 3 or 4 release of the package, whose allCountries is a list of countries.

Recipes

The programs in the stack's gate that use this package, each a job someone would have.

  • recipes/full-stack: A page and a server in one directory: the page reaching the server in development through the dev server's proxy, and a sign-in that sets the cookie on that one origin

  • recipes/ui: A page with themes, contexts, control flow, a popup and a suspend, built by vite and driven in a real browser

  • recipes/icons: Icons named three ways, and what each way puts in the bundle

  • recipes/routed-site: A site with real URLs: nested pages, a page with a parameter, a page that arrives later, a dialog the back button dismisses, and a title per page

  • recipes/ssg: A routed site written out as files, served by anything, and taken over in place when the browser gets to it

  • recipes/translated-site: A site written in one language and launched in three: the build finds every string and writes the catalog an agent fills, each language is a tree of pages with the plural rules of its own, a stored act is compiled where it runs, and the Ukrainian page hydrates in place

  • recipes/static: A generated site served by the stack's own server: one process is the whole deployment, and the page still comes alive where it stands

  • recipes/logs: A page recorded end to end in a browser and the visit read back: an error, a rejection, a console line, a failed call on both sides, a commit's shape with a private slot absent, a typed character never stored, sign-in mid-visit

  • recipes/uploads: A page uploads pictures under the gate and they paint from /files/<id>; each refusal reaches the page with its reason, a module makes a file of its own, and the static battery behind it never sees a file

  • recipes/notify: Two pages of one user hear a send live and mark it read for each other, a device registered from the page, email and push against two fakes, a failed mail kept, a forged write refused, a restart, and a server with no store sending a contact form's mail

  • recipes/room: An act module stored on the server, run in a frame on the page: the board it shares reaches the server, its ask carries the page's identity, its own stage navigates on the page's URL under the host act, an error inside reaches the page's logs, and the frame paints but cannot fetch

  • recipes/posts-to-pages: Pages written while the application runs: a post published over a socket becomes a page, and a scheduled full write refreshes the sitemap

  • recipes/accessible-page: A page everyone can use, driven by keyboard and audited in both modes, and each guardrail catching one page written wrong: the build refusing an element no one can read, the mount throwing on a nameless button and a page with no language, audit and walk reading what only the rendered page shows