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.
pointedLinks: where the reader may go next
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.