Skip to content

UI Overview

etaf-ui is the official catalog of reusable components for ETAF text applications. It adds no new framework concepts: every component is an ordinary Component whose render result is a View built from Hosts, styled through the same cascade, provided the same Theme and driven by the same interaction layer. What the catalog contributes is a set of named, keyboard-operable controls with a uniform contract, so an application composes a complete interface instead of rebuilding buttons, lists, dialogs and grids every time. This chapter defines that shared contract — how inputs are owned, how an entry is written, which keyboard behavior every component promises, how components stay inside the standard line height, how they read theme tokens, and when a definition should use :view or :render. The following chapters then define each family: Icons, Containers, Feedback and Status, Buttons and Choice Controls, Text Input Controls, Embedded Native Editors, Pickers and Menus, Overlays, Navigation and Collections, Data Display, Composite Components, Panels and Windows, Desktop Navigation and Tiling, Media and Reactions, Grids and Character Surfaces, and finally Gallery and Acceptance. Component defines the Component model itself, Component Styles and Themes the rule list and token mechanism, and Keyboard the key layer that every component relies on.

Every interactive component in the catalog is a controlled control: the application owns the value, and the component only renders it and reports what the user did. The default interface is a pair of props — a value prop such as :value or :checked plus a callback prop such as :on-change, :on-press, :on-select or :on-toggle. The callback receives the candidate next value or the chosen key, and the application decides whether and how to store it. A component never writes application data and never keeps a second copy of it.

The alternative is :bind: instead of the value/callback pair, the caller passes one existing writable ref. A bound control reads its current value from that ref and writes candidate updates back to it; a later write to the same ref updates every mounted control and every other reader. Because the ref is the caller's, :bind and the explicit pair are mutually exclusive: supplying a value or a callback together with a non-nil bind is a rejected call, including an explicit nil. Unmounting releases the control's own handlers and subscriptions without disposing or clearing the caller's ref, and a rejected publication leaves the previous binding target in place. Reactivity defines the write rules in general.

Only genuinely local presentation state belongs to a component. A dropdown menu owns its open flag and its anchor because they exist only for that retained instance, while etaf-popover keeps :open controlled so the application decides when a surface is visible. Values, selections, expansion sets, window geometry and media playback are always caller-owned; the catalog's callbacks request a change, and its names follow that meaning — :on-close asks to close, it does not close by itself.

Props accept plain values, refs and computed sources. A plain value is what the caller passed; a ref or computed source keeps its identity, so writing it updates the mounted control in place without re-rendering the surrounding View. This is the same reactive input rule Component defines for every Component.

A catalog component is not a Host: it owns no Ebox node of its own. Its render result is a View — nil, one Host, or several Hosts grouped by a fragment — and the component instance is the unit that survives re-renders, matched by Component and :key. An instance keeps its inputs, its Scope with the refs and bindings created during initialization, and its retained children until it stops being rendered or its Runtime is unmounted.

That division decides what a component needs to run. A component that only assembles Hosts and semantic content is stateless and can be rendered by etaf-render as well as mounted. A component that declares initialization, bindings or retained state needs a mounted Runtime, because its Scope must have a lifecycle owner; rendering one without a mount signals etaf-renderer-error. The catalog follows this rule like any other Component, and its documentation marks the components that need a live Runtime — for example a text input's editing session, or a window whose mode lives in retained properties.

Host identity, slots and attribute fallthrough are ordinary Component rules: a catalog component reads its props, projects the caller's default and named slots, and renders whatever the caller supplied, so a :class, :ref or :id passed to a component call reaches the Host that represents it. The catalog does not wrap its roots in extra containers, so a component that produces a fragment joins the enclosing layout directly.

A catalog entry is written in one shape, so a reader can find a fact in the same place every time. After the one-sentence description, the supported facts appear as a props table — each prop's name, accepted type, default, whether it participates in a controlled pair or :bind, and its meaning — followed, when the component has them, by its slots, the events it reports, the keyboard and focus behavior it adds beyond the catalog's baseline, its accessible metadata (:aria-* and the default accessible name), the theme tokens it reads, and its size rules: how many rows it occupies, which dimensions are intrinsic, and whether it can stretch. The Example shows a call in context; See also points at the related entries and chapters.

Props are regular Component props, so the calling rules of Component apply unchanged: keyword arguments on the call, defaults resolved when a prop is missing, explicit nil distinct from an omitted prop, and a documented error when a prop falls outside its contract. An invalid input is reported at the point where the contract is checked — during validation or during rendering — naming the component and the property, so a failure can be traced to the call that caused it.

Every catalog component is keyboard operable through ETAF's key layer; a component that cannot be activated by keyboard is not accepted into the catalog. The baseline promise is one tab stop per control: Tab reaches an interactive control, and the arrows plus C-n, C-p, C-b and C-f move between the parts of a compound control. Collectors — tabs, segmented and radio selectors, desktop bars, window controls, tables, lists, trees, calendars, breadcrumbs, steps and tile grids — behave as one tab stop entered at their selected or focused item, so a long list does not flood the Tab order. Modal surfaces — dialogs, drawers, popovers, menus and the media viewer — establish a key scope: the keys in scope are the surface's own, Escape requests a close, and closing returns focus to the control that opened it. ? opens read-only key help listing the keys currently in scope. Focus defines tab order, group entry and modal scopes, and Keyboard defines :keys, direction handling and help.

A component's own key behavior is part of its entry: which keys it adds, what they do, and which keys it reserves. A component that cannot bind a key reports it during validation rather than silently dropping the binding.

The catalog's default is one standard line. A label, button, checkbox, badge, tag, keycap or icon occupies exactly one text row; trailing padding is expressed in row units that do not grow the line, so stacking components in a column keeps a predictable grid and never adds unexpected blank rows. Components with a larger fixed shape say so: a search field is a three-row pill with half a row of surface above and below its text, a text input's visible height follows its :rows minimum, and an icon is exactly one row by default and exactly two rows with :lines 2, with its size clamped inside those rows so an oversized glyph can never push the layout. Intrinsic dimensions stay intrinsic: tile grid cells and playing cards keep their authored cell size when the containing viewport changes, and reflow happens in the surrounding layout instead of by stretching the control.

The rule matters for positioned content. A window, resizable panel or any other component that positions text inside a canvas shares that canvas's line height, and Ebox rejects a font whose resolved line height exceeds it before publication; a larger CSS font size does not enlarge the canvas. Boxes and Normal Flow defines how rows and the standard line arise, and the Panels and Windows chapter covers the canvas contract.

Catalog components paint through Theme tokens rather than literal colors. A token names a semantic role — a surface, a muted foreground, an accent fill, a status color, a table selection — and the component reads it at the point where the declaration is lowered, so one provided Theme restyles every component in the subtree by re-lowering static rules and retained properties, without re-rendering the tree. Component Styles and Themes defines token resolution, provision and the base vocabulary; each catalog chapter defines the tokens of its own family, and the Theme token table lists the full vocabulary.

A component always supplies a built-in fallback for every token it reads, so a catalog runs and looks coherent without any Theme; providing a key overrides the fallback for the whole subtree. The fallbacks are the catalog's default light look. A desktop shell additionally reads its own shell tokens, so a window manager can dim or recolor the shell and its pickers independently of the dialogs it keeps crisp. Because tokens resolve per declaration, a token read in a retained Host property or a static rule pins the theme value, not the color: switching the provided Theme repaints the same mounted components.

Both authoring frontends of etaf-define-component are ordinary choices for a catalog component, and the component's entry describes behavior, not which one it uses. As Component defines, :view is the structural language compiled with the definition: each prop name it uses is a tracked read, so a component that only assembles Hosts and semantic content should use it, and a local update can repaint one binding without re-running the whole render. :render is ordinary Elisp evaluated in a retained effect; use it when the output depends on a computed value read in Lisp, when nodes must be built programmatically, or when the component must branch on ordinary data before choosing structure. Both receive the same props, slots and scope, and both can mix static rules with Host properties.

The practical rule for authors of new catalog components: start with :view; move a component to :render only for the reasons above, not because its view is large or its props numerous.

  • A prop outside its contract signals error→ etaf-component-call-error with the component name, the property, the received value and the expectation, raised through etaf-check-prop. Validation and rendering both use this condition, so callers do not need to distinguish the moment a component checked its input.
  • A rejected input does not publish: a mounted instance keeps its previously published props and callbacks, and a bound control keeps its previous binding target. It does not undo a business ref the caller already wrote, because that write belongs to the application.
  • A malformed definition is a different failure: error→ etaf-component-definition-error is raised while a component's definition is expanded or evaluated, and registering a duplicate name outside the intentional redefinition path signals error→ etaf-component-call-error.
  • A stateful component rendered without a mounted Runtime signals error→ etaf-renderer-error, as every Component does. The catalog's entries say when that applies.
  • A component that depends on an external facility reports its absence as a user-error naming the missing program, not as a component input error, because the call itself was well formed.

See also. Component, Component Styles and Themes, Hosts, Keyboard, and Text Input.