Primitives / DOM

Mounts for layout

KeepInView({ selector }) keeps what is marked in view: whenever an element in its subtree newly matches selector (the row just selected, the node just inserted), it is scrolled into view the least amount that shows it. It sends no Message and redraws nothing, so a focused row stays focused. Where there is no layout (jsdom), it scrolls nothing.

Measure({ targets: { selected: '[aria-selected="true"]' } }) measures, for each named target, the first element in its subtree that matches, relative to its scroll box, and writes --fk-selected-x, -y, -w, -h (pixels) and --fk-selected-display (block, or none while nothing matches) on the element; measured('selected') names them. A child placed absolutely from them, such as an editor's selection outline, sits over the target and scrolls with it. Where an element is, is presentation: it sends no Message and keeps nothing in the Model. It measures again when the subtree changes, as the records arrive, so the box moves in the frame the change is drawn in; and, at most once a frame, when the element scrolls, when it or a target changes size, when the window resizes, when an image or a font inside loads, and on each frame of a transition or animation inside. Anything else that moves a target without resizing it (a stylesheet added) is seen at the next of these.

FluidHover({ name: 'hover', item: '[data-item]', axis: 'y', gapClick: true }) moves one highlight across a container's items: the item nearest the pointer is the one highlighted, so in a gap between two items the pointer still points at one, and the highlight moves from item to item rather than blinking off between them. axis says how nearness is read: y for a list (the row level with the pointer wins), x for a strip, xy for a grid. A disabled item (aria-disabled="true", data-disabled, :disabled) is never highlighted, an item reached by the keyboard (:focus-visible) is, and a touch highlights nothing. It writes --fk-hover-x, -y, -w, -h (pixels, relative to the scroll box like Measure's), --fk-hover-shown (1 or 0) and --fk-hover-moving (0 for the frame the highlight appears in, so a movement's duration scaled by it puts the highlight on its item rather than sliding it in); fluidHovered('hover') names them. With gapClick, a click in a gap presses the highlighted item: its descendant marked data-fluid-press (FLUID_PRESS_ATTRIBUTE), or the item; gapReach: 16 limits that to a click within 16px of it, as a grid of cards with wide gutters wants. Like Measure, it sends no Message.

follows names the item the highlight sits on while the pointer is not moving it: a menu marks the row its keys reach with data-active, so follows: '[data-active]' lights that row whenever the mark moves, and the highlight fades once no item is marked and neither the pointer nor focus is inside. from names the item a fresh highlight grows out of and returns to as it fades: from: '[aria-selected="true"]' makes a tab's hover leave the chosen tab and rejoin it. A marker on the chosen item itself is Measure's job, on the same element. While it runs, the container carries data-fluid-hover (FLUID_HOVER_ATTRIBUTE) set to the name, so a stylist draws the highlight, and drops each item's own hover, only where it really follows the pointer: before a page hydrates, the items keep theirs. Where there is no layout (jsdom), it starts and highlights nothing; an item with no layout box yet (a popup still opening) keeps the highlight's last place, and is measured again for three frames, rather than drawing it at zero.

The item the highlight is shown on carries data-fluid-lit (FLUID_LIT_ATTRIBUTE), so a style changes what sits beside it: a table clears the lines around its lit row.

Give only what a click reaches a highlight: lighting an item that does nothing promises a press with nowhere to land, so a card group joins only its pressable cards. Fluid Functionalism's list of when to leave it off holds here too: where a wrong click would hurt, where only some items are clickable, where items stand in a lot of empty space, and where rows move as the page scrolls.

InputMask({ pattern }) masks a field against # (digit), A (letter), and * (either) placeholders with literal separators, rewrites the field with approximate caret restore, and emits Input { value, raw } with the masked and unmasked text. The parent owns the value, like any controlled input. applyMask(value, pattern) is the pure rule.

keepScroll: the window's scroll across navigations

An entry, lifted with Subscription.persistentEntry, that sends no Messages: it keeps the window's scroll across same-document navigations, so a pushed entry starts at the top and Back, Forward, or a reload return where that entry was. Without a window the stream is empty. The CMS example lifts it beside its other subscriptions.

A stream of the address of each link the reader points at or tabs to, on the page's own origin, once while they stay on it (crossing the text inside a link is one link). Without a document it is empty. Map each address to a Message and lift it with Subscription.persistentEntry; the Message's branch in update is where a Data.preload reads the page there ahead. The CMS site and studio do (examples/cms/src/content/siteConfig.ts, examples/cms/src/apps/studioApp.ts).

Failure

A missing capability yields the failure Message, never a throw. A Mount reads its args once, on insert; key the element by what the args depend on.