Primitives / DOM
PointerPosition: where the pointer is inside an element
For a surface the pointer points into or draws on: a color area, a dial, a
signature pad. A primary press, each move until the release, and the release
reach the parent as Messages, each place in CSS pixels from the top left of
the element's padding box (inside its border, padding included) with the
box's size at that event, so a share across is point.x / width. That is the
box an absolutely positioned thumb or stroke is laid out against, so a place
read here is where such a child sits, whatever padding a stylist adds.
import { Match, Schema } from 'effect'
import type { Html, HtmlBuilder } from 'foldkit/html'
import { defineMessageUnion } from 'foldkit/message'
import * as Mount from 'foldkit/mount'
import { PointerPosition } from 'foldkit-primitives/dom'
const Message = defineMessageUnion({
/** The pointer is `share` of the way across the track, 0 to 1, left to right. */
Pointed: { share: Schema.Number },
Released: {},
})
const shareOf = (x: number, width: number) =>
width === 0 ? 0 : Math.min(1, Math.max(0, x / width))
const track = (h: HtmlBuilder<typeof Message.Type>): Html =>
h.div(
[
h.OnMount(
Mount.mapMessage(PointerPosition({ focus: '[role="slider"]' }), fact =>
Match.valueTags(fact, {
PointerPressed: ({ point, width }) =>
Message.Pointed({ share: shareOf(point.x, width) }),
PointerMoved: ({ point, width }) => Message.Pointed({ share: shareOf(point.x, width) }),
PointerReleased: () => Message.Released(),
}),
),
),
],
[h.div([h.Role('slider'), h.Tabindex(0)], [])],
)PointerPressed { point, width, height, pointerType }: a primary press. It keeps the browser's own focus and text selection away, and focuses the descendantfocusselects, if any, so the keys go on from where the pointer left it.PointerMoved { point, width, height, points }:pointsis every place the browser coalesced into this move, ending withpoint, for a stroke that should keep its curve.PointerReleased { completed }:falsefor a cancel, a lost capture, or the same pointer pressed again after a release nothing heard.
A point is { x, y, pressure }. The pointer is captured from press to
release, so a drag that leaves the element is still heard, and a captured
place may lie outside the box (negative, or past width): clamping is the
parent's. Coordinates are raw, left to right whatever the direction, so a
right-to-left reading is the parent's too. The moves and the release are
listened to only during a press; a second pointer meanwhile is ignored. The
box is measured at each event, never kept. paddingBoxOf is the arithmetic,
and measurePaddingBox(element) the same measure, for a size kept beside the
places (a ResizeObserver reports the content box, which padding makes
smaller).
It is not Move, which reports how far the pointer went from where it went
down and nothing about where in the element that is, nor PointerDrag, which
says which marked element is under the pointer.
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.