Files
oikos/web/node_modules/bits-ui/dist/internal/floating-svelte/use-floating.svelte.js
dtoro d4d99a7473
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
feat: Phase 1 — extract the client (web SPA + desktop) to dtoro/oikos-web
Problem: the hexagonal refactor churns the backend tree for nine more
phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper,
compose/web image) must move to its own repo first so doc/layout
rewrites land once on a backend-only tree.

Change:
- New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/
  (updateURL repointed to oikos-web releases), compose/, own CI (web +
  desktop jobs), own deploy script (CI-green gate, TOCTOU guard,
  version-tagged images, prune-to-3), own webhook receiver on :9798 +
  launchd unit, own compose project publishing the same 8091:80.
- Cutover executed on mac-mini in order: oikos stack's web service
  stopped+removed, oikos-web project brought up on 8091; outer Caddy
  untouched (targets the published port) — serving + Authentik flow +
  /wails 404 quirk verified post-cutover.
- Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI
  workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install
  targets, the compose web service, oikos-web from deploy.sh's fallback
  prune list; wails + go-keyring dropped from go.mod, vendor synced.
- README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now
  point at the new repo; mbse + mascot design docs carry a path note.

Risk: production SPA serving depends on the new pipeline now; rollback
is versioned-image re-up of the old web service from a pre-split
checkout (port 8091). Desktop builds installed before the split still
check dtoro/oikos releases — one manual reinstall, noted in the
oikos-web release notes.

Verification: go vet, make test (race), make generate-check, golangci
(no new findings; baseline down 400→365); post-cutover curls —
localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
2026-08-15 22:27:52 +02:00

193 lines
6.7 KiB
JavaScript

import { computePosition } from "@floating-ui/dom";
import { simpleBox } from "svelte-toolbelt";
import { get, getDPR, roundByDPR } from "./floating-utils.svelte.js";
export function useFloating(options) {
/** Options */
const whileElementsMountedOption = options.whileElementsMounted;
const openOption = $derived(get(options.open) ?? true);
const middlewareOption = $derived(get(options.middleware));
const transformOption = $derived(get(options.transform) ?? true);
const placementOption = $derived(get(options.placement) ?? "bottom");
const strategyOption = $derived(get(options.strategy) ?? "absolute");
const sideOffsetOption = $derived(get(options.sideOffset) ?? 0);
const alignOffsetOption = $derived(get(options.alignOffset) ?? 0);
const reference = options.reference;
/** State */
let x = $state(0);
let y = $state(0);
const floating = simpleBox(null);
// svelte-ignore state_referenced_locally
let strategy = $state(strategyOption);
// svelte-ignore state_referenced_locally
let placement = $state(placementOption);
let middlewareData = $state({});
let isPositioned = $state(false);
let hasWhileMountedPosition = false;
let updateRequestId = 0;
const floatingStyles = $derived.by(() => {
// preserve last known position when floating ref is null (during transitions)
const xVal = floating.current ? roundByDPR(floating.current, x) : x;
const yVal = floating.current ? roundByDPR(floating.current, y) : y;
if (transformOption) {
return {
position: strategy,
left: "0",
top: "0",
transform: `translate(${xVal}px, ${yVal}px)`,
...(floating.current &&
getDPR(floating.current) >= 1.5 && {
willChange: "transform",
}),
};
}
return {
position: strategy,
left: `${xVal}px`,
top: `${yVal}px`,
};
});
/** Effects */
let whileElementsMountedCleanup;
function update() {
if (reference.current === null || floating.current === null)
return;
const referenceNode = reference.current;
const floatingNode = floating.current;
const requestId = ++updateRequestId;
computePosition(referenceNode, floatingNode, {
middleware: middlewareOption,
placement: placementOption,
strategy: strategyOption,
}).then((position) => {
// ignore stale async resolutions when newer updates were requested.
if (requestId !== updateRequestId)
return;
// ignore stale resolutions after ref replacement.
if (reference.current !== referenceNode || floating.current !== floatingNode)
return;
const referenceHidden = isReferenceHidden(referenceNode);
if (referenceHidden) {
// keep last good coordinates when the anchor disappears to avoid
// a transient jump to viewport origin before close propagates.
middlewareData = {
...middlewareData,
hide: {
// oxlint-disable-next-line no-explicit-any
...middlewareData.hide,
referenceHidden: true,
},
};
return;
}
// ignore bad coordinates that cause jumping during close transitions
if (!openOption && x !== 0 && y !== 0) {
// if we had a good position and now getting coordinates near
// the expected offset bounds during close, ignore it
const maxExpectedOffset = Math.max(Math.abs(sideOffsetOption), Math.abs(alignOffsetOption), 15);
if (position.x <= maxExpectedOffset && position.y <= maxExpectedOffset)
return;
}
x = position.x;
y = position.y;
strategy = position.strategy;
placement = position.placement;
middlewareData = position.middlewareData;
isPositioned = true;
});
}
function cleanup() {
if (typeof whileElementsMountedCleanup === "function") {
whileElementsMountedCleanup();
whileElementsMountedCleanup = undefined;
}
updateRequestId++;
}
function attach() {
cleanup();
if (whileElementsMountedOption === undefined) {
update();
return;
}
if (!openOption)
return;
if (reference.current === null || floating.current === null)
return;
whileElementsMountedCleanup = whileElementsMountedOption(reference.current, floating.current, update);
}
function reset() {
if (!openOption && floating.current === null) {
isPositioned = false;
}
}
function trackWhileMountedDeps() {
return [
middlewareOption,
placementOption,
strategyOption,
sideOffsetOption,
alignOffsetOption,
openOption,
];
}
$effect(() => {
if (whileElementsMountedOption !== undefined)
return;
if (!openOption)
return;
update();
});
$effect(attach);
$effect(() => {
if (whileElementsMountedOption === undefined)
return;
trackWhileMountedDeps();
if (!openOption) {
hasWhileMountedPosition = false;
return;
}
if (!isPositioned) {
hasWhileMountedPosition = false;
return;
}
// skip the first post-position run, since autoUpdate already computed it
if (!hasWhileMountedPosition) {
hasWhileMountedPosition = true;
return;
}
update();
});
$effect(reset);
$effect(() => cleanup);
return {
floating,
reference,
get strategy() {
return strategy;
},
get placement() {
return placement;
},
get middlewareData() {
return middlewareData;
},
get isPositioned() {
return isPositioned;
},
get floatingStyles() {
return floatingStyles;
},
get update() {
return update;
},
};
}
function isReferenceHidden(node) {
if (!(node instanceof Element))
return false;
if (!node.isConnected)
return true;
if (node instanceof HTMLElement && node.hidden)
return true;
return node.getClientRects().length === 0;
}