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 type { ActiveDom } from '$adom'
import type { Measurable } from '../types'
import type { Measurable } from './types'
import { getOverflowAncestors } from './clipping'
export type AutoUpdateOptions = {

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

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

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

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

@ -1,8 +1,8 @@
// Pure placement / coordinate math. No DOM, no reads — all inputs are rects.
// Reimplemented from the floating-ui algorithm (spec), our idioms + types.
import type { Align, Placement, Side } from '../placement'
import { OPPOSITE_SIDE } from '../placement'
import type { Align, Placement, Side } from './placement'
import { OPPOSITE_SIDE } from './placement'
import type { Axis, Coords, ElementRects, Length, Padding, SideObject } from './types'
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.
// Reimplemented from floating-ui's `flip` (spec).
import type { Placement } from '../../placement'
import type { Placement } from '../placement'
import {
getAlignmentSides,
getExpandedPlacements,

@ -1,7 +1,7 @@
// offset — shifts the floating element along the main / cross / alignment axes.
// 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 type { Coords, Middleware, MiddlewareState } from '../types'

@ -10,6 +10,11 @@ import type { MiddlewareState, Padding, SideObject } from './types'
export type DetectOverflowOptions = {
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
* reference rect. @default 'floating' */
elementContext?: 'floating' | 'reference'

@ -6,7 +6,7 @@
// (single-document fidelity) — the option-2 ancestor-walk lives in clipping.ts.
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'
const round = Math.round

@ -1,10 +1,27 @@
// Engine-internal positioning types. Reimplemented to our idioms (floating-ui as
// spec). The public `Middleware`/`MiddlewareData` in ../types are the consumer
// surface; these are the math types the engine threads internally.
// Positioning types for the `$floating` art — math types the engine threads
// internally, plus the public consumer contract (`Measurable`, `Middleware`,
// `MiddlewareData`). Reimplemented to our idioms (floating-ui read as spec).
import type { ActiveDom } from '$adom'
import type { Placement, Strategy } from '../placement'
import type { MiddlewareData, Measurable } from '../types'
import type { Placement, Strategy } from './placement'
/** 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 Length = 'width' | 'height'

@ -1,5 +1,16 @@
# 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
> and compiling: DOM-read + clipping ancestor-walk + `detectOverflow` + all 7
> middleware + `compute` (read phase + pure `runMiddleware` loop) + `autoUpdate` +

@ -10,15 +10,15 @@ import {
} from '@floating-ui/dom';
import {
arrow as ownArrow,
autoUpdate as ownAutoUpdate,
flip as ownFlip,
hide as ownHide,
limitShift as ownLimitShift,
offset as ownOffset,
shift as ownShift,
size as ownSize
} from './engine/middleware';
import { autoUpdate as ownAutoUpdate } from './engine/auto-update';
import { USE_OWN_ENGINE } from './engine/flag';
size as ownSize,
USE_OWN_ENGINE
} from '$floating';
import type { Middleware } from './types';
import { attachRef, type RefAttachment } from '$libs/reactive';
import { cssToStyleObj, styleToString } from '../../css';
@ -33,7 +33,7 @@ import { ElementSize, watch } from 'runed';
import { useFloating } from './use-floating.svelte';
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
// flag. Typed loosely so the single `middleware` array expression below serves

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

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

@ -1,4 +1,3 @@
import type { Middleware } from './engine/types';
import type { ActiveDom } from '$adom';
import type {
Active,
@ -6,17 +5,14 @@ import type {
} from '$libs/reactive';
import type { Arrayable, Direction, StyleProperties } from '../../types';
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 ───────────────────────────────────────────────────────────────────
// The middleware contract now lives in `engine/types` (de-vendored from
// `@floating-ui/dom`). Re-exported here so `./types` stays the public surface.
export type { Middleware } from './engine/types';
export type Measurable = {
getBoundingClientRect: () => DOMRect;
};
// The positioning contract (Measurable / Middleware / MiddlewareData) now lives
// in the `$floating` art. Re-exported here so `./types` stays soma's public
// surface for these.
export type { Measurable, Middleware, MiddlewareData } from '$floating';
/** The positioned (floating) element — always a real DOM element. */
export type FloatingElement = HTMLElement;
@ -24,18 +20,6 @@ export type FloatingElement = HTMLElement;
/** The anchor/reference — a real element or a virtual `Measurable`. */
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 ─────────────────────────────────────────────────────
export type UseFloatingOptions = {

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

@ -22,7 +22,7 @@ export type {
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 {
PrimitiveDivAttributes,
@ -44,5 +44,7 @@ export type {
PrimitiveTHAttributes,
PrimitiveTDAttributes,
PrimitiveCaptionAttributes,
PrimitiveVideoAttributes,
PrimitiveAudioAttributes,
PassthroughProps
} from './html';

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

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

Loading…
Cancel
Save

Powered by TurnKey Linux.