You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/arts/ethereal/README.md

6.7 KiB

ethereal ($ethereal)

The in-house positioning engine — collision-aware placement geometry for floating overlays (popover, tooltip, menu, select…). Pure, deterministic, DOM-read-only: computePosition reads the layout once through $adom and then runs a middleware chain purely over that snapshot. It is an art (parallel to $motion and $color) so both UIX layers consume it with no cross-layer dependency:

  • soma — soma/layers/floating (use-floating, FloatingContent) calls computePosition + autoUpdate for the JS positioning path.
  • eidos — render-css emits the native CSS Anchor Positioning rules for the overlays selectPositioningStrategy clears for the native path.

Nothing is vendored: @floating-ui was read as the spec (MIT); these are our own idioms and types, kept pixel-identical to it by the parity suites.

Status — consumed. @floating-ui is a devDep-only parity baseline; the runtime is this engine (layers/floating + $ethereal). Correctness & performance record: PERF.md — 487 parity cases (350 synthetic, 137 real-DOM), cross-browser on Chromium + WebKit + Firefox (engine.test.ts + engine-dom.svelte.test.ts). Interactive demo: web/routes/demos/ethereal.

Why an art

The positioning math has two consumers — soma's JS path and eidos's CSS-anchor generator — exactly like $motion (soma Presence + eidos CSS) and $color (eidos build + runtime). Living in arts/ (below the UIX layers) lets both consume it with no soma→eidos coupling. Per the arts convention it imports no other art, owns no reactive $state, and touches the DOM only through the injected $adom runtime (iframe / popup safe — every read goes through dom.getWindow(node) / dom.measure, never a bare window).

The read-phase model

The whole point of the sec-dom design: computePosition positions inside a single coalesced dom.measure rAF and never forces a synchronous reflow.

dom.measure(floating) ──▶ read EVERYTHING once
   reference rect · floating dims · offsetParent · clipping ancestor-walk
   · arrow dims · offsetParent→viewport delta · scale · direction
                          │
                          ▼
runMiddleware(...)  ──▶ pure chain over the snapshot (NO DOM access)
   offset → shift → flip → arrow → size → hide …   (reset restarts, cap 50)
                          │
                          ▼
   { x, y, placement, strategy, middlewareData }

runMiddleware is synchronous; @floating-ui's computePosition is async (a Promise per read + per middleware). The honest trade-off is +1 frame of latency (the deliberate rAF), documented in PERF.md.

Dual engine — JS + native CSS Anchor Positioning

selectPositioningStrategy(features) decides, per overlay, between the JS engine and the browser's native anchor positioning (anchor-name / position-anchor / position-area / @position-try). Native wins only when the browser supports it (supportsCssAnchor) and none of the JS-forcing features are present — a continuous shift, an arrow, a virtual anchor, or an explicit boundary. This gate is the behavioural twin of eidos's @supports (anchor-name) and (position-area) block; the two must agree. See strategy.ts + PERF.md §"Dual engine".

API

Export Purpose
computePosition(reference, floating, config) orchestrator → Promise<ComputePositionReturn>; one dom.measure read, then the pure chain
runMiddleware(placement, strategy, middleware, snap) the pure middleware loop over a ReadSnapshot — no DOM; exported so the math is parity-testable
autoUpdate(reference, floating, update, options) re-runs update on ancestor scroll/resize, element resize, and (opt-in) a per-frame loop → cleanup fn
selectPositioningStrategy(features) 'native' | 'js' — the native-vs-JS discriminator
supportsCssAnchor(window) runtime capability probe for CSS Anchor Positioning

Middleware (chain order is caller-controlled): offset, shift, flip, arrow, size, hide, limitShift (a limiter on shift, the sticky behaviour). Each is a Middleware — { name, options?, fn(state) } — pure over the MiddlewareState.

Placement vocabulary (placement.ts): SIDE_OPTIONS (top/right/bottom/left), ALIGN_OPTIONS (start/center/end), OPPOSITE_SIDE; Placement = Side | ${Side}-start|end (center carries no suffix — it is the bare side); Strategy = 'absolute' | 'fixed'.

Types (types.ts): Measurable (a real element or a virtual anchor exposing only getBoundingClientRect — the context-menu / customAnchor pattern), Middleware, MiddlewareData, MiddlewareState, ComputePositionConfig / ComputePositionReturn, Coords / Dimensions / Rect / SideObject / Padding, ClippingContext, Boundary (Element | null). Geometry helpers (getSide, getAlignment, getOppositePlacement, getExpandedPlacements, computeCoordsFromPlacement, …) back the middleware math and are exported for reuse.

config = { placement = 'bottom', strategy = 'absolute', middleware = [], dom } — dom is the required ActiveDom. autoUpdate options default ancestorScroll / ancestorResize / elementResize to true and animationFrame to false; the observeMove IntersectionObserver layout-shift path is intentionally not implemented (scope: scroll + resize + rAF).

Depends on

  • $adom — every DOM read/observe (getWindow, measure, listen, observeResize, requestFrame), so the engine is iframe / popup / test safe.
  • no other art; @floating-ui is a devDep-only parity baseline, never a runtime import.

Consumers

  • src/uix/soma/layers/floating/ — use-floating.svelte.ts (calls computePosition), floating.svelte.ts (the middleware wiring + autoUpdate).
  • src/uix/eidos/lib/render-css.ts — the native CSS-anchor block gated on the same capability set selectPositioningStrategy uses.

Powered by TurnKey Linux.