refactor(floating): relocate the pure engine to the `$floating` art

The positioning engine is pure collision geometry over `$adom` — a runtime
artifact, not soma-specific. Moves it out of `soma/layers/floating/engine/` into
a new `arts/floating` art (`$floating`), exactly parallel to `$motion`: both soma
(the JS positioning path) and eidos (the CSS-anchor path, future) build on it,
so the shared pure core belongs in arts, not buried in one consumer.

Moved to `$floating` (git renames, history preserved): geometry, rects, clipping,
overflow, supports, compute, auto-update, flag, types, the 7 middleware, and the
two parity test suites. `placement.ts` and the contract types `Measurable` /
`Middleware` / `MiddlewareData` move too — `$floating` is now self-contained
(depends only on `$adom`, never back on soma). New `arts/floating/index.ts` barrel
is the public surface. Alias `$floating` added to vite.config.ts + svelte.config.js.

Stays in `soma/layers/floating/` (reactive composition): use-floating.svelte.ts
(runes), floating.svelte.ts (providers/context), shell.ts, safe-polygon.ts,
utils.ts, the reactive types, index.ts. These now import `$floating`; soma's
types.ts/index.ts + soma/types/index.ts re-export the placement/contract types.

Two latent type gaps the typed `$floating` surface exposed (the soma loose-factory
shim had hidden them) are fixed: `DetectOverflowOptions` now declares `boundary`
(the middleware genuinely accept it; compute's read phase extracts it), and a
`size` test's `apply` returns void.

`@floating-ui` is UNTOUCHED — P4 deliberately NOT executed: the dep stays installed,
the fui imports + the `USE_OWN_ENGINE` flag (still OFF) remain in the wrappers, the
runtime still positions via floating-ui. Verified: 350 synthetic + 137 real-DOM
parity cases green from the new location, soma overlay providers green, check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent f204b55741
commit 58683b0306

@ -7,7 +7,7 @@
import { isElement } from '$adom' import { isElement } from '$adom'
import type { ActiveDom } from '$adom' import type { ActiveDom } from '$adom'
import type { Measurable } from '../types' import type { Measurable } from './types'
import { getOverflowAncestors } from './clipping' import { getOverflowAncestors } from './clipping'
export type AutoUpdateOptions = { export type AutoUpdateOptions = {

@ -5,7 +5,7 @@
// cross-iframe traversal — the rare case; the ancestor-walk is the option-2 core). // cross-iframe traversal — the rare case; the ancestor-walk is the option-2 core).
import { getDocumentElement, getNodeName, getParentNode, isElement, isHTMLElement } from '$adom' import { getDocumentElement, getNodeName, getParentNode, isElement, isHTMLElement } from '$adom'
import type { Strategy } from '../placement' import type { Strategy } from './placement'
import type { Rect } from './types' import type { Rect } from './types'
import { import {
createCoords, createCoords,

@ -6,8 +6,8 @@
// Reimplemented from floating-ui's `computePosition` (spec), our idioms. // Reimplemented from floating-ui's `computePosition` (spec), our idioms.
import { getDocumentElement, isElement } from '$adom' import { getDocumentElement, isElement } from '$adom'
import type { Placement, Strategy } from '../placement' import type { Placement, Strategy } from './placement'
import type { Measurable, MiddlewareData } from '../types' import type { Measurable, MiddlewareData } from './types'
import { getClippingRect } from './clipping' import { getClippingRect } from './clipping'
import { computeCoordsFromPlacement } from './geometry' import { computeCoordsFromPlacement } from './geometry'
import { import {

@ -31,8 +31,8 @@ import {
} from '@floating-ui/dom' } from '@floating-ui/dom'
import { hide as ownHide } from './middleware' import { hide as ownHide } from './middleware'
import type { Middleware } from './types' import type { Middleware } from './types'
import type { Measurable } from '../types' import type { Measurable } from './types'
import type { Placement, Strategy } from '../placement' import type { Placement, Strategy } from './placement'
const dom = createActiveDom() const dom = createActiveDom()
const trash: HTMLElement[] = [] const trash: HTMLElement[] = []
@ -1300,11 +1300,21 @@ describe('real-DOM parity — arrow + size in real layout', () => {
{ {
own: [ own: [
ownOffset({ mainAxis: 8 }), ownOffset({ mainAxis: 8 }),
ownSize({ padding: 0, apply: ({ availableHeight }) => (ownAH = availableHeight) }) ownSize({
padding: 0,
apply: ({ availableHeight }) => {
ownAH = availableHeight
}
})
], ],
fui: [ fui: [
fuiOffset({ mainAxis: 8 }), fuiOffset({ mainAxis: 8 }),
fuiSize({ padding: 0, apply: ({ availableHeight }) => (fuiAH = availableHeight) }) fuiSize({
padding: 0,
apply: ({ availableHeight }) => {
fuiAH = availableHeight
}
})
] ]
}, },
'bottom' 'bottom'

@ -29,7 +29,7 @@ import {
size as ownSize size as ownSize
} from './middleware' } from './middleware'
import type { Coords, Dimensions, Middleware, Rect } from './types' import type { Coords, Dimensions, Middleware, Rect } from './types'
import type { Placement } from '../placement' import type { Placement } from './placement'
type Scenario = { type Scenario = {
reference: Rect reference: Rect

@ -1,8 +1,8 @@
// Pure placement / coordinate math. No DOM, no reads — all inputs are rects. // Pure placement / coordinate math. No DOM, no reads — all inputs are rects.
// Reimplemented from the floating-ui algorithm (spec), our idioms + types. // Reimplemented from the floating-ui algorithm (spec), our idioms + types.
import type { Align, Placement, Side } from '../placement' import type { Align, Placement, Side } from './placement'
import { OPPOSITE_SIDE } from '../placement' import { OPPOSITE_SIDE } from './placement'
import type { Axis, Coords, ElementRects, Length, Padding, SideObject } from './types' import type { Axis, Coords, ElementRects, Length, Padding, SideObject } from './types'
const OPPOSITE_ALIGN: Record<'start' | 'end', 'start' | 'end'> = { start: 'end', end: 'start' } const OPPOSITE_ALIGN: Record<'start' | 'end', 'start' | 'end'> = { start: 'end', end: 'start' }

@ -0,0 +1,82 @@
// `$floating` — the in-house positioning engine, an art (parallel to `$motion`).
//
// Pure collision/positioning geometry over `$adom`'s DOM reads. It has NO
// reactive / component dependency — the Svelte wrappers (`use-floating`,
// `FloatingContent` providers) live in `soma/layers/floating` and consume this.
// Both soma (the JS positioning path) and eidos (the CSS-anchor path) build on
// it, exactly as both build on `$motion` / `$color`.
//
// floating-ui was read as the spec (MIT); nothing is vendored — these are our
// own idioms + types. Verified pixel-identical to `@floating-ui` by the parity
// suites (`engine.test.ts` + `engine-dom.svelte.test.ts`).
export { computePosition, runMiddleware, type ReadSnapshot } from './compute'
export { autoUpdate, type AutoUpdateOptions } from './auto-update'
export { supportsCssAnchor } from './supports'
export { USE_OWN_ENGINE } from './flag'
export {
offset,
shift,
flip,
arrow,
size,
hide,
limitShift,
type OffsetOptions,
type ShiftOptions,
type FlipOptions,
type ArrowOptions,
type SizeOptions,
type SizeApplyArgs,
type HideOptions,
type LimitShiftOptions,
type ShiftLimiter
} from './middleware'
export {
SIDE_OPTIONS,
ALIGN_OPTIONS,
OPPOSITE_SIDE,
type Side,
type Align,
type Boundary,
type Placement,
type Strategy
} from './placement'
export type {
Measurable,
Middleware,
MiddlewareData,
MiddlewareState,
MiddlewareReturn,
ComputePositionConfig,
ComputePositionReturn,
Coords,
Dimensions,
Rect,
SideObject,
Padding,
Axis,
Length,
ElementRects,
FloatingElements,
ClippingContext
} from './types'
export {
getSide,
getAlignment,
getSideAxis,
getAlignmentAxis,
getAxisLength,
getOppositeAxis,
getOppositePlacement,
getOppositeAlignmentPlacement,
getAlignmentSides,
getExpandedPlacements,
getOppositeAxisPlacements,
getPaddingObject,
computeCoordsFromPlacement
} from './geometry'

@ -3,7 +3,7 @@
// pre-walked clipping). Returns `{ reset: { placement } }` to restart the chain. // pre-walked clipping). Returns `{ reset: { placement } }` to restart the chain.
// Reimplemented from floating-ui's `flip` (spec). // Reimplemented from floating-ui's `flip` (spec).
import type { Placement } from '../../placement' import type { Placement } from '../placement'
import { import {
getAlignmentSides, getAlignmentSides,
getExpandedPlacements, getExpandedPlacements,

@ -1,7 +1,7 @@
// offset — shifts the floating element along the main / cross / alignment axes. // offset — shifts the floating element along the main / cross / alignment axes.
// PURE. Reimplemented from floating-ui's `offset` (spec). // PURE. Reimplemented from floating-ui's `offset` (spec).
import type { Placement } from '../../placement' import type { Placement } from '../placement'
import { getAlignment, getSide, getSideAxis } from '../geometry' import { getAlignment, getSide, getSideAxis } from '../geometry'
import type { Coords, Middleware, MiddlewareState } from '../types' import type { Coords, Middleware, MiddlewareState } from '../types'

@ -10,6 +10,11 @@ import type { MiddlewareState, Padding, SideObject } from './types'
export type DetectOverflowOptions = { export type DetectOverflowOptions = {
padding?: Padding padding?: Padding
/** The clipping boundary the read phase walks — `'clippingAncestors'` (the
* overflow-ancestor walk) or an explicit `Element[]`. Carried on the middleware
* options so `compute`'s read phase can extract it; `detectOverflow` itself reads
* the pre-walked `state.clipping` and ignores this. @default 'clippingAncestors' */
boundary?: 'clippingAncestors' | Element[]
/** Which element's rect to test — `'floating'` at `state.x/y`, or the /** Which element's rect to test — `'floating'` at `state.x/y`, or the
* reference rect. @default 'floating' */ * reference rect. @default 'floating' */
elementContext?: 'floating' | 'reference' elementContext?: 'floating' | 'reference'

@ -6,7 +6,7 @@
// (single-document fidelity) — the option-2 ancestor-walk lives in clipping.ts. // (single-document fidelity) — the option-2 ancestor-walk lives in clipping.ts.
import { getDocumentElement, getNodeName, getParentNode, isElement, isHTMLElement } from '$adom' import { getDocumentElement, getNodeName, getParentNode, isElement, isHTMLElement } from '$adom'
import type { Strategy } from '../placement' import type { Strategy } from './placement'
import type { Coords, ElementRects, FloatingElements, Rect } from './types' import type { Coords, ElementRects, FloatingElements, Rect } from './types'
const round = Math.round const round = Math.round

@ -1,10 +1,27 @@
// Engine-internal positioning types. Reimplemented to our idioms (floating-ui as // Positioning types for the `$floating` art — math types the engine threads
// spec). The public `Middleware`/`MiddlewareData` in ../types are the consumer // internally, plus the public consumer contract (`Measurable`, `Middleware`,
// surface; these are the math types the engine threads internally. // `MiddlewareData`). Reimplemented to our idioms (floating-ui read as spec).
import type { ActiveDom } from '$adom' import type { ActiveDom } from '$adom'
import type { Placement, Strategy } from '../placement' import type { Placement, Strategy } from './placement'
import type { MiddlewareData, Measurable } from '../types'
/** The anchor/reference — a real element or a virtual element exposing only a
* `getBoundingClientRect` (the context-menu / `customAnchor` pattern). */
export type Measurable = {
getBoundingClientRect: () => DOMRect
}
/**
* Per-middleware results read off `computePosition`. Open-ended (`[key]` index)
* for forward-compat + the custom `transformOrigin` middleware. Only the keys
* the consumers actually read are typed.
*/
export interface MiddlewareData {
arrow?: { x?: number; y?: number; centerOffset?: number; alignmentOffset?: number }
hide?: { referenceHidden?: boolean; escaped?: boolean }
transformOrigin?: { x: number | string; y: number | string }
[key: string]: unknown
}
export type Axis = 'x' | 'y' export type Axis = 'x' | 'y'
export type Length = 'width' | 'height' export type Length = 'width' | 'height'

@ -1,5 +1,16 @@
# CONTINUE — Removing `@floating-ui`, building our own positioning layer # CONTINUE — Removing `@floating-ui`, building our own positioning layer
> **RELOCATION DONE (2026-07-01): the pure engine is now the `$floating` art**
> (`src/arts/floating/`), parallel to `$motion` — consumed by soma's reactive
> wrappers (still here in `soma/layers/floating/`: `use-floating.svelte.ts`,
> `floating.svelte.ts`, `shell.ts`, `safe-polygon.ts`, `utils.ts`, `types.ts`,
> `index.ts`). Why: the engine is pure collision geometry over `$adom`; soma (JS
> path) AND eidos (CSS-anchor path, future) both build on it. `Measurable` /
> `Middleware` / `MiddlewareData` / placement types now live in `$floating`; soma
> `types.ts`/`index.ts` re-export them. Alias `$floating` added to vite + svelte
> config. The paths below that say `engine/…` now mean `$floating/…`. **`@floating-ui`
> is UNTOUCHED — P4 NOT executed** (dep installed, fui imports + flag intact, flag OFF).
>
> **Status: P1 COMPLETE — flag-gated, math-verified.** The whole engine is built > **Status: P1 COMPLETE — flag-gated, math-verified.** The whole engine is built
> and compiling: DOM-read + clipping ancestor-walk + `detectOverflow` + all 7 > and compiling: DOM-read + clipping ancestor-walk + `detectOverflow` + all 7
> middleware + `compute` (read phase + pure `runMiddleware` loop) + `autoUpdate` + > middleware + `compute` (read phase + pure `runMiddleware` loop) + `autoUpdate` +

@ -10,15 +10,15 @@ import {
} from '@floating-ui/dom'; } from '@floating-ui/dom';
import { import {
arrow as ownArrow, arrow as ownArrow,
autoUpdate as ownAutoUpdate,
flip as ownFlip, flip as ownFlip,
hide as ownHide, hide as ownHide,
limitShift as ownLimitShift, limitShift as ownLimitShift,
offset as ownOffset, offset as ownOffset,
shift as ownShift, shift as ownShift,
size as ownSize size as ownSize,
} from './engine/middleware'; USE_OWN_ENGINE
import { autoUpdate as ownAutoUpdate } from './engine/auto-update'; } from '$floating';
import { USE_OWN_ENGINE } from './engine/flag';
import type { Middleware } from './types'; import type { Middleware } from './types';
import { attachRef, type RefAttachment } from '$libs/reactive'; import { attachRef, type RefAttachment } from '$libs/reactive';
import { cssToStyleObj, styleToString } from '../../css'; import { cssToStyleObj, styleToString } from '../../css';
@ -33,7 +33,7 @@ import { ElementSize, watch } from 'runed';
import { useFloating } from './use-floating.svelte'; import { useFloating } from './use-floating.svelte';
import type { Measurable, UseFloatingReturn } from './types'; import type { Measurable, UseFloatingReturn } from './types';
import { OPPOSITE_SIDE, type Align, type Boundary, type Placement, type Side } from './placement'; import { OPPOSITE_SIDE, type Align, type Boundary, type Placement, type Side } from '$floating';
// A/B middleware factory set — own engine or @floating-ui, selected once via the // A/B middleware factory set — own engine or @floating-ui, selected once via the
// flag. Typed loosely so the single `middleware` array expression below serves // flag. Typed loosely so the single `middleware` array expression below serves

@ -14,7 +14,7 @@ export {
type Side, type Side,
type Align, type Align,
type Boundary type Boundary
} from './placement'; } from '$floating';
// ── Floating engine ────────────────────────────────────────────────────────── // ── Floating engine ──────────────────────────────────────────────────────────
export type { export type {

@ -1,5 +1,5 @@
import { watch } from 'runed'; import { watch } from 'runed';
import type { Side } from './placement'; import type { Side } from '$floating';
import { isElement, type ActiveDom } from '$adom'; import { isElement, type ActiveDom } from '$adom';
import type { TimerHandle, TimerScheduler } from '$timer'; import type { TimerHandle, TimerScheduler } from '$timer';

@ -1,4 +1,3 @@
import type { Middleware } from './engine/types';
import type { ActiveDom } from '$adom'; import type { ActiveDom } from '$adom';
import type { import type {
Active, Active,
@ -6,17 +5,14 @@ import type {
} from '$libs/reactive'; } from '$libs/reactive';
import type { Arrayable, Direction, StyleProperties } from '../../types'; import type { Arrayable, Direction, StyleProperties } from '../../types';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import type { Align, Boundary, Placement, Side, Strategy } from './placement'; import type { Align, Boundary, Measurable, Middleware, MiddlewareData, Placement, Side, Strategy } from '$floating';
// ─── Shared ─────────────────────────────────────────────────────────────────── // ─── Shared ───────────────────────────────────────────────────────────────────
// The middleware contract now lives in `engine/types` (de-vendored from // The positioning contract (Measurable / Middleware / MiddlewareData) now lives
// `@floating-ui/dom`). Re-exported here so `./types` stays the public surface. // in the `$floating` art. Re-exported here so `./types` stays soma's public
export type { Middleware } from './engine/types'; // surface for these.
export type { Measurable, Middleware, MiddlewareData } from '$floating';
export type Measurable = {
getBoundingClientRect: () => DOMRect;
};
/** The positioned (floating) element — always a real DOM element. */ /** The positioned (floating) element — always a real DOM element. */
export type FloatingElement = HTMLElement; export type FloatingElement = HTMLElement;
@ -24,18 +20,6 @@ export type FloatingElement = HTMLElement;
/** The anchor/reference — a real element or a virtual `Measurable`. */ /** The anchor/reference — a real element or a virtual `Measurable`. */
export type ReferenceElement = Measurable | HTMLElement; export type ReferenceElement = Measurable | HTMLElement;
/**
* Per-middleware results read off `computePosition`. Open-ended (`[key]` index)
* for forward-compat + our custom `transformOrigin` middleware. Replaces the
* `@floating-ui/dom` `MiddlewareData` — only the keys we actually read are typed.
*/
export interface MiddlewareData {
arrow?: { x?: number; y?: number; centerOffset?: number; alignmentOffset?: number };
hide?: { referenceHidden?: boolean; escaped?: boolean };
transformOrigin?: { x: number | string; y: number | string };
[key: string]: unknown;
}
// ─── useFloating Options ───────────────────────────────────────────────────── // ─── useFloating Options ─────────────────────────────────────────────────────
export type UseFloatingOptions = { export type UseFloatingOptions = {

@ -6,9 +6,12 @@ import {
} from '$libs/reactive'; } from '$libs/reactive';
import { getDPR, roundByDPR, isReferenceHidden } from './utils'; import { getDPR, roundByDPR, isReferenceHidden } from './utils';
import type { MiddlewareData, UseFloatingOptions, UseFloatingReturn } from './types'; import type { MiddlewareData, UseFloatingOptions, UseFloatingReturn } from './types';
import type { Placement, Strategy } from './placement'; import {
import { computePosition as ownComputePosition } from './engine/compute'; computePosition as ownComputePosition,
import { USE_OWN_ENGINE } from './engine/flag'; USE_OWN_ENGINE,
type Placement,
type Strategy
} from '$floating';
export function useFloating(options: UseFloatingOptions): UseFloatingReturn { export function useFloating(options: UseFloatingOptions): UseFloatingReturn {
const whileElementsMountedOption = options.whileElementsMounted; const whileElementsMountedOption = options.whileElementsMounted;

@ -22,7 +22,7 @@ export type {
export { isFunction, isNull, isNotNull, isNumberString } from './guards'; export { isFunction, isNull, isNotNull, isNumberString } from './guards';
export type { Side, Align, Boundary } from '../layers/floating/placement'; export type { Side, Align, Boundary } from '$floating';
export type { export type {
PrimitiveDivAttributes, PrimitiveDivAttributes,
@ -44,5 +44,7 @@ export type {
PrimitiveTHAttributes, PrimitiveTHAttributes,
PrimitiveTDAttributes, PrimitiveTDAttributes,
PrimitiveCaptionAttributes, PrimitiveCaptionAttributes,
PrimitiveVideoAttributes,
PrimitiveAudioAttributes,
PassthroughProps PassthroughProps
} from './html'; } from './html';

@ -24,6 +24,7 @@ const config = {
'$clipboard': resolve(__dirname, 'src/arts/clipboard'), '$clipboard': resolve(__dirname, 'src/arts/clipboard'),
'$color': resolve(__dirname, 'src/arts/color'), '$color': resolve(__dirname, 'src/arts/color'),
'$connection': resolve(__dirname, 'src/arts/connection'), '$connection': resolve(__dirname, 'src/arts/connection'),
'$floating': resolve(__dirname, 'src/arts/floating'),
'$format': resolve(__dirname, 'src/arts/format'), '$format': resolve(__dirname, 'src/arts/format'),
'$http': resolve(__dirname, 'src/arts/http'), '$http': resolve(__dirname, 'src/arts/http'),
'$langs': resolve(__dirname, 'src/arts/langs'), '$langs': resolve(__dirname, 'src/arts/langs'),

@ -18,6 +18,7 @@ const aliases = {
'$clipboard': resolve(__dirname, 'src/arts/clipboard'), '$clipboard': resolve(__dirname, 'src/arts/clipboard'),
'$color': resolve(__dirname, 'src/arts/color'), '$color': resolve(__dirname, 'src/arts/color'),
'$connection': resolve(__dirname, 'src/arts/connection'), '$connection': resolve(__dirname, 'src/arts/connection'),
'$floating': resolve(__dirname, 'src/arts/floating'),
'$format': resolve(__dirname, 'src/arts/format'), '$format': resolve(__dirname, 'src/arts/format'),
'$http': resolve(__dirname, 'src/arts/http'), '$http': resolve(__dirname, 'src/arts/http'),
'$langs': resolve(__dirname, 'src/arts/langs'), '$langs': resolve(__dirname, 'src/arts/langs'),

Loading…
Cancel
Save

Powered by TurnKey Linux.