diff --git a/src/uix/soma/layers/floating/engine/auto-update.ts b/src/arts/floating/auto-update.ts similarity index 98% rename from src/uix/soma/layers/floating/engine/auto-update.ts rename to src/arts/floating/auto-update.ts index d29da42b0..c5a151de7 100644 --- a/src/uix/soma/layers/floating/engine/auto-update.ts +++ b/src/arts/floating/auto-update.ts @@ -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 = { diff --git a/src/uix/soma/layers/floating/engine/clipping.ts b/src/arts/floating/clipping.ts similarity index 99% rename from src/uix/soma/layers/floating/engine/clipping.ts rename to src/arts/floating/clipping.ts index f8a53fa60..57aa62280 100644 --- a/src/uix/soma/layers/floating/engine/clipping.ts +++ b/src/arts/floating/clipping.ts @@ -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, diff --git a/src/uix/soma/layers/floating/engine/compute.ts b/src/arts/floating/compute.ts similarity index 98% rename from src/uix/soma/layers/floating/engine/compute.ts rename to src/arts/floating/compute.ts index 024ae7fb6..38ffb3eda 100644 --- a/src/uix/soma/layers/floating/engine/compute.ts +++ b/src/arts/floating/compute.ts @@ -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 { diff --git a/src/uix/soma/layers/floating/engine/engine-dom.svelte.test.ts b/src/arts/floating/engine-dom.svelte.test.ts similarity index 99% rename from src/uix/soma/layers/floating/engine/engine-dom.svelte.test.ts rename to src/arts/floating/engine-dom.svelte.test.ts index 5cab41ebb..f8c8d7c06 100644 --- a/src/uix/soma/layers/floating/engine/engine-dom.svelte.test.ts +++ b/src/arts/floating/engine-dom.svelte.test.ts @@ -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' diff --git a/src/uix/soma/layers/floating/engine/engine.test.ts b/src/arts/floating/engine.test.ts similarity index 99% rename from src/uix/soma/layers/floating/engine/engine.test.ts rename to src/arts/floating/engine.test.ts index d8b4c2a46..39ba26fec 100644 --- a/src/uix/soma/layers/floating/engine/engine.test.ts +++ b/src/arts/floating/engine.test.ts @@ -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 diff --git a/src/uix/soma/layers/floating/engine/flag.ts b/src/arts/floating/flag.ts similarity index 100% rename from src/uix/soma/layers/floating/engine/flag.ts rename to src/arts/floating/flag.ts diff --git a/src/uix/soma/layers/floating/engine/geometry.ts b/src/arts/floating/geometry.ts similarity index 98% rename from src/uix/soma/layers/floating/engine/geometry.ts rename to src/arts/floating/geometry.ts index 8a15984a6..0edfa2b6a 100644 --- a/src/uix/soma/layers/floating/engine/geometry.ts +++ b/src/arts/floating/geometry.ts @@ -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' } diff --git a/src/arts/floating/index.ts b/src/arts/floating/index.ts new file mode 100644 index 000000000..a01f41cc3 --- /dev/null +++ b/src/arts/floating/index.ts @@ -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' diff --git a/src/uix/soma/layers/floating/engine/middleware/arrow.ts b/src/arts/floating/middleware/arrow.ts similarity index 100% rename from src/uix/soma/layers/floating/engine/middleware/arrow.ts rename to src/arts/floating/middleware/arrow.ts diff --git a/src/uix/soma/layers/floating/engine/middleware/flip.ts b/src/arts/floating/middleware/flip.ts similarity index 98% rename from src/uix/soma/layers/floating/engine/middleware/flip.ts rename to src/arts/floating/middleware/flip.ts index 7cfabce94..5a8b6c70d 100644 --- a/src/uix/soma/layers/floating/engine/middleware/flip.ts +++ b/src/arts/floating/middleware/flip.ts @@ -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, diff --git a/src/uix/soma/layers/floating/engine/middleware/hide.ts b/src/arts/floating/middleware/hide.ts similarity index 100% rename from src/uix/soma/layers/floating/engine/middleware/hide.ts rename to src/arts/floating/middleware/hide.ts diff --git a/src/uix/soma/layers/floating/engine/middleware/index.ts b/src/arts/floating/middleware/index.ts similarity index 100% rename from src/uix/soma/layers/floating/engine/middleware/index.ts rename to src/arts/floating/middleware/index.ts diff --git a/src/uix/soma/layers/floating/engine/middleware/limit-shift.ts b/src/arts/floating/middleware/limit-shift.ts similarity index 100% rename from src/uix/soma/layers/floating/engine/middleware/limit-shift.ts rename to src/arts/floating/middleware/limit-shift.ts diff --git a/src/uix/soma/layers/floating/engine/middleware/offset.ts b/src/arts/floating/middleware/offset.ts similarity index 97% rename from src/uix/soma/layers/floating/engine/middleware/offset.ts rename to src/arts/floating/middleware/offset.ts index bdc20bb93..f7b351b03 100644 --- a/src/uix/soma/layers/floating/engine/middleware/offset.ts +++ b/src/arts/floating/middleware/offset.ts @@ -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' diff --git a/src/uix/soma/layers/floating/engine/middleware/shift.ts b/src/arts/floating/middleware/shift.ts similarity index 100% rename from src/uix/soma/layers/floating/engine/middleware/shift.ts rename to src/arts/floating/middleware/shift.ts diff --git a/src/uix/soma/layers/floating/engine/middleware/size.ts b/src/arts/floating/middleware/size.ts similarity index 100% rename from src/uix/soma/layers/floating/engine/middleware/size.ts rename to src/arts/floating/middleware/size.ts diff --git a/src/uix/soma/layers/floating/engine/overflow.ts b/src/arts/floating/overflow.ts similarity index 88% rename from src/uix/soma/layers/floating/engine/overflow.ts rename to src/arts/floating/overflow.ts index 32b08d83c..aa51ea3cf 100644 --- a/src/uix/soma/layers/floating/engine/overflow.ts +++ b/src/arts/floating/overflow.ts @@ -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' diff --git a/src/uix/soma/layers/floating/placement.ts b/src/arts/floating/placement.ts similarity index 100% rename from src/uix/soma/layers/floating/placement.ts rename to src/arts/floating/placement.ts diff --git a/src/uix/soma/layers/floating/engine/rects.ts b/src/arts/floating/rects.ts similarity index 99% rename from src/uix/soma/layers/floating/engine/rects.ts rename to src/arts/floating/rects.ts index f25d10b71..4a7562fb7 100644 --- a/src/uix/soma/layers/floating/engine/rects.ts +++ b/src/arts/floating/rects.ts @@ -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 diff --git a/src/uix/soma/layers/floating/engine/supports.ts b/src/arts/floating/supports.ts similarity index 100% rename from src/uix/soma/layers/floating/engine/supports.ts rename to src/arts/floating/supports.ts diff --git a/src/uix/soma/layers/floating/engine/types.ts b/src/arts/floating/types.ts similarity index 76% rename from src/uix/soma/layers/floating/engine/types.ts rename to src/arts/floating/types.ts index a74157ea1..50d1b9470 100644 --- a/src/uix/soma/layers/floating/engine/types.ts +++ b/src/arts/floating/types.ts @@ -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' diff --git a/src/uix/soma/layers/floating/CONTINUE.md b/src/uix/soma/layers/floating/CONTINUE.md index 4f5688150..407a5b0b1 100644 --- a/src/uix/soma/layers/floating/CONTINUE.md +++ b/src/uix/soma/layers/floating/CONTINUE.md @@ -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` + diff --git a/src/uix/soma/layers/floating/floating.svelte.ts b/src/uix/soma/layers/floating/floating.svelte.ts index 1d46394ba..326cffcec 100644 --- a/src/uix/soma/layers/floating/floating.svelte.ts +++ b/src/uix/soma/layers/floating/floating.svelte.ts @@ -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 diff --git a/src/uix/soma/layers/floating/index.ts b/src/uix/soma/layers/floating/index.ts index dab5baddc..9dd07b67b 100644 --- a/src/uix/soma/layers/floating/index.ts +++ b/src/uix/soma/layers/floating/index.ts @@ -14,7 +14,7 @@ export { type Side, type Align, type Boundary -} from './placement'; +} from '$floating'; // ── Floating engine ────────────────────────────────────────────────────────── export type { diff --git a/src/uix/soma/layers/floating/safe-polygon.ts b/src/uix/soma/layers/floating/safe-polygon.ts index 2443b1516..6cbd013e5 100644 --- a/src/uix/soma/layers/floating/safe-polygon.ts +++ b/src/uix/soma/layers/floating/safe-polygon.ts @@ -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'; diff --git a/src/uix/soma/layers/floating/types.ts b/src/uix/soma/layers/floating/types.ts index 716a16987..fc7e03be5 100644 --- a/src/uix/soma/layers/floating/types.ts +++ b/src/uix/soma/layers/floating/types.ts @@ -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 = { diff --git a/src/uix/soma/layers/floating/use-floating.svelte.ts b/src/uix/soma/layers/floating/use-floating.svelte.ts index f23c88fff..c177ccaf4 100644 --- a/src/uix/soma/layers/floating/use-floating.svelte.ts +++ b/src/uix/soma/layers/floating/use-floating.svelte.ts @@ -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; diff --git a/src/uix/soma/types/index.ts b/src/uix/soma/types/index.ts index 69e7650ba..fd7c2b150 100644 --- a/src/uix/soma/types/index.ts +++ b/src/uix/soma/types/index.ts @@ -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'; diff --git a/svelte.config.js b/svelte.config.js index 35f938a81..3bf3ea1ae 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -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'), diff --git a/vite.config.ts b/vite.config.ts index ef1ec27b9..fd8961fe6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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'),