Files
oikos/web/node_modules/@surdeddd/wmkit/dist/chunk-B35DANTX.cjs
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

1738 lines
60 KiB
JavaScript

'use strict';
// src/core/emitter.ts
function createEmitter() {
const listeners = /* @__PURE__ */ new Map();
return {
on(event, listener) {
let set = listeners.get(event);
if (!set) {
set = /* @__PURE__ */ new Set();
listeners.set(event, set);
}
set.add(listener);
return () => {
set.delete(listener);
};
},
emit(event, payload) {
const set = listeners.get(event);
if (!set) return;
for (const listener of [...set]) {
listener(payload);
}
},
clear() {
listeners.clear();
}
};
}
// src/core/geometry.ts
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function clampSize(size, min, max) {
return {
width: clamp(size.width, min.width, max ? max.width : Number.POSITIVE_INFINITY),
height: clamp(size.height, min.height, max ? max.height : Number.POSITIVE_INFINITY)
};
}
function clampToViewport(bounds, viewport, minVisible) {
if (viewport.width <= 0 || viewport.height <= 0) return bounds;
const x = clamp(bounds.x, minVisible - bounds.width, viewport.width - minVisible);
const y = clamp(bounds.y, 0, Math.max(0, viewport.height - minVisible));
return x === bounds.x && y === bounds.y ? bounds : { ...bounds, x, y };
}
function boundsEqual(a, b) {
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
}
function zoneBounds(zone, viewport) {
const w = viewport.width;
const h = viewport.height;
const halfW = Math.round(w / 2);
const halfH = Math.round(h / 2);
switch (zone) {
case "left":
return { x: 0, y: 0, width: halfW, height: h };
case "right":
return { x: halfW, y: 0, width: w - halfW, height: h };
case "top":
return { x: 0, y: 0, width: w, height: halfH };
case "bottom":
return { x: 0, y: halfH, width: w, height: h - halfH };
case "top-left":
return { x: 0, y: 0, width: halfW, height: halfH };
case "top-right":
return { x: halfW, y: 0, width: w - halfW, height: halfH };
case "bottom-left":
return { x: 0, y: halfH, width: halfW, height: h - halfH };
case "bottom-right":
return { x: halfW, y: halfH, width: w - halfW, height: h - halfH };
}
}
function detectSnapZone(x, y, viewport, options = {}) {
const threshold = options.threshold ?? 12;
const cornerSize = options.cornerSize ?? 64;
const w = viewport.width;
const h = viewport.height;
if (w <= 0 || h <= 0) return null;
const nearLeft = x <= threshold;
const nearRight = x >= w - threshold;
const nearTop = y <= threshold;
const nearBottom = y >= h - threshold;
if (nearLeft || nearRight) {
const side = nearLeft ? "left" : "right";
if (y <= cornerSize) return `top-${side}`;
if (y >= h - cornerSize) return `bottom-${side}`;
return side;
}
if (nearTop) {
if (x <= cornerSize) return "top-left";
if (x >= w - cornerSize) return "top-right";
return "top";
}
if (nearBottom) {
if (x <= cornerSize) return "bottom-left";
if (x >= w - cornerSize) return "bottom-right";
return "bottom";
}
return null;
}
function nearestEdge(low, high, targetLow, targetHigh) {
return [targetLow - low, targetHigh - low, targetLow - high, targetHigh - high];
}
function magnetize(bounds, targets, threshold) {
const result = { x: bounds.x, y: bounds.y, snappedX: false, snappedY: false };
if (threshold <= 0 || targets.length === 0) return result;
let bestX = threshold + 1;
let bestY = threshold + 1;
for (const target of targets) {
for (const delta of nearestEdge(
bounds.x,
bounds.x + bounds.width,
target.x,
target.x + target.width
)) {
if (Math.abs(delta) <= threshold && Math.abs(delta) < Math.abs(bestX)) bestX = delta;
}
for (const delta of nearestEdge(
bounds.y,
bounds.y + bounds.height,
target.y,
target.y + target.height
)) {
if (Math.abs(delta) <= threshold && Math.abs(delta) < Math.abs(bestY)) bestY = delta;
}
}
if (Math.abs(bestX) <= threshold) {
result.x = bounds.x + bestX;
result.snappedX = true;
}
if (Math.abs(bestY) <= threshold) {
result.y = bounds.y + bestY;
result.snappedY = true;
}
return result;
}
// src/core/manager.ts
var LAYER_RANK = { normal: 0, floating: 1, modal: 2 };
var STAGES = ["normal", "minimized", "maximized", "snapped"];
var LAYERS = ["normal", "floating", "modal"];
var ZONES = [
"left",
"right",
"top",
"bottom",
"top-left",
"top-right",
"bottom-left",
"bottom-right"
];
function createWindowManager(options = {}) {
const emitter = createEmitter();
const keepInViewport = options.keepInViewport ?? true;
const minVisible = options.minVisible ?? 48;
const defaultSize = options.defaultSize ?? { width: 480, height: 320 };
const cascadeOffset = options.cascadeOffset ?? 32;
const cascadeOrigin = options.cascadeOrigin ?? { x: 32, y: 32 };
const idPrefix = options.idPrefix ?? "wm";
const historyLimit = options.historyLimit ?? 50;
let windows = {};
let order = [];
let focusedId = null;
let viewport = options.viewport ?? { width: 0, height: 0 };
let seq = 0;
let idCounter = 0;
let cascadeIndex = 0;
let snapshot = null;
let batchDepth = 0;
let batchDirty = false;
const pendingEvents = [];
const past = [];
let future = [];
let interactionDepth = 0;
let interactionRecorded = false;
let skipNextHistory = false;
let pendingHistory = null;
const layouts = /* @__PURE__ */ new Map();
function getState() {
if (!snapshot) {
snapshot = { windows, order, focusedId, viewport };
}
return snapshot;
}
function commit() {
snapshot = null;
batchDirty = true;
}
function flushEvents() {
while (pendingEvents.length > 0) {
for (const emit of pendingEvents.splice(0)) emit();
}
}
function queueEvent(emit) {
pendingEvents.push(emit);
}
function setWindow(next) {
windows = { ...windows, [next.id]: next };
}
function removeWindow(id) {
const { [id]: _removed, ...rest } = windows;
windows = rest;
order = order.filter((entry) => entry !== id);
}
function layerRankOf(id) {
return LAYER_RANK[windows[id].layer];
}
function sortByLayer(ids) {
return ids.map((id, index) => ({ id, index, rank: layerRankOf(id) })).sort((a, b) => a.rank !== b.rank ? a.rank - b.rank : a.index - b.index).map((entry) => entry.id);
}
function raise(id) {
const win = windows[id];
const without = order.filter((entry) => entry !== id);
const rank = LAYER_RANK[win.layer];
let insertAt = without.length;
for (let i = without.length - 1; i >= 0; i -= 1) {
if (layerRankOf(without[i]) > rank) insertAt = i;
else break;
}
const next = [...without.slice(0, insertAt), id, ...without.slice(insertAt)];
const changed = next.length !== order.length || next.some((entry, i) => entry !== order[i]);
order = next;
return changed;
}
function topModalId() {
for (let i = order.length - 1; i >= 0; i -= 1) {
const win = windows[order[i]];
if (win.layer === "modal" && win.stage !== "minimized") return win.id;
}
return null;
}
function focusTargets() {
const modal = topModalId();
return order.filter((id) => {
const win = windows[id];
if (win.stage === "minimized") return false;
if (modal && win.layer !== "modal") return false;
return true;
});
}
function focusTop() {
const targets = focusTargets();
focusedId = targets.length > 0 ? targets[targets.length - 1] : null;
}
function emitFocus(win, previous) {
queueEvent(() => emitter.emit("focus", { window: win, previous }));
}
function normalizeSize(size, win) {
return clampSize(size, win.minSize, win.maxSize);
}
function positionForOpen(init) {
if (init.x !== void 0 && init.y !== void 0) return { x: init.x, y: init.y };
const offset = cascadeIndex % 10 * cascadeOffset;
cascadeIndex += 1;
const base = { x: cascadeOrigin.x + offset, y: cascadeOrigin.y + offset };
return { x: init.x ?? base.x, y: init.y ?? base.y };
}
function open(init = {}) {
let id = init.id;
if (id === void 0) {
do {
id = `${idPrefix}-${++idCounter}`;
} while (windows[id]);
}
if (windows[id]) throw new Error(`wmkit: window id "${id}" already exists`);
const minSize = { width: init.minWidth ?? 160, height: init.minHeight ?? 100 };
const maxSize = init.maxWidth !== void 0 || init.maxHeight !== void 0 ? {
width: init.maxWidth ?? Number.POSITIVE_INFINITY,
height: init.maxHeight ?? Number.POSITIVE_INFINITY
} : null;
const size = clampSize(
{ width: init.width ?? defaultSize.width, height: init.height ?? defaultSize.height },
minSize,
maxSize
);
const position = positionForOpen(init);
let bounds = { ...position, ...size };
if (keepInViewport) bounds = clampToViewport(bounds, viewport, minVisible);
const requestedStage = init.stage ?? "normal";
const stage = requestedStage === "snapped" ? "normal" : requestedStage;
let win = {
id,
title: init.title ?? "Window",
bounds,
restoreBounds: null,
restoreStage: null,
stage: "normal",
snapZone: null,
layer: init.layer ?? "normal",
minSize,
maxSize,
openedSeq: ++seq,
draggable: init.draggable ?? true,
resizable: init.resizable ?? true,
closable: init.closable ?? true,
minimizable: init.minimizable ?? true,
maximizable: init.maximizable ?? true,
snappable: init.snappable ?? true,
meta: init.meta ?? {}
};
if (stage === "maximized") {
win = { ...win, stage, restoreBounds: bounds, bounds: fullBounds() };
} else if (stage === "minimized") {
win = { ...win, stage, restoreStage: "normal" };
}
setWindow(win);
raise(id);
let focusPayload = null;
if (stage !== "minimized") {
const previous = focusedId;
const modal = topModalId();
if (!modal || win.layer === "modal") {
focusedId = id;
focusPayload = { window: win, previous };
} else {
focusTop();
}
}
queueEvent(() => emitter.emit("open", { window: win }));
if (focusPayload) {
const payload = focusPayload;
queueEvent(() => emitter.emit("focus", payload));
}
queueEvent(() => emitter.emit("order", { order }));
commit();
return win;
}
function close(id) {
const win = windows[id];
if (!win) return false;
removeWindow(id);
queueEvent(() => emitter.emit("close", { window: win }));
if (focusedId === id) {
focusTop();
if (focusedId) emitFocus(windows[focusedId], id);
}
queueEvent(() => emitter.emit("order", { order }));
commit();
return true;
}
function closeAll() {
for (const id of [...order]) close(id);
}
function focus(id) {
const win = windows[id];
if (!win) return false;
const modal = topModalId();
if (modal && modal !== id && win.layer !== "modal") {
const modalWin = windows[modal];
if (modalWin) queueEvent(() => emitter.emit("modalblocked", { window: modalWin }));
commit();
return false;
}
if (win.stage === "minimized") {
restore(id);
return focusedId === id;
}
const previous = focusedId;
const raised = raise(id);
const changed = focusedId !== id || raised;
focusedId = id;
if (changed) {
emitFocus(win, previous);
if (raised) queueEvent(() => emitter.emit("order", { order }));
commit();
}
return true;
}
function blur() {
if (focusedId === null) return;
focusedId = null;
commit();
}
function cycleFocus(direction = 1) {
const targets = focusTargets();
if (targets.length === 0) return null;
const current = focusedId ? targets.indexOf(focusedId) : -1;
const nextIndex = current === -1 ? direction === 1 ? 0 : targets.length - 1 : (current + direction + targets.length) % targets.length;
const id = targets[nextIndex];
focus(id);
return id;
}
function fullBounds() {
return { x: 0, y: 0, width: viewport.width, height: viewport.height };
}
function applyStage(id, build) {
const win = windows[id];
if (!win) return false;
const next = build(win);
if (!next) return false;
setWindow(next);
queueEvent(() => emitter.emit("stage", { window: next, previous: win.stage }));
commit();
return true;
}
function minimize(id) {
const result = applyStage(id, (win) => {
if (win.stage === "minimized") return null;
return { ...win, stage: "minimized", restoreStage: win.stage };
});
if (result && focusedId === id) {
const previous = focusedId;
focusTop();
if (focusedId) emitFocus(windows[focusedId], previous);
commit();
}
return result;
}
function maximize(id) {
const result = applyStage(id, (win) => {
if (win.stage === "maximized") return null;
const restoreBounds = win.stage === "normal" ? win.bounds : win.restoreBounds;
return {
...win,
stage: "maximized",
snapZone: null,
restoreBounds,
restoreStage: null,
bounds: fullBounds()
};
});
if (result) focus(id);
return result;
}
function toggleMaximize(id) {
const win = windows[id];
if (!win) return false;
return win.stage === "maximized" ? restore(id) : maximize(id);
}
function restore(id) {
const result = applyStage(id, (win) => {
if (win.stage === "minimized") {
const target = win.restoreStage ?? "normal";
if (target === "maximized") {
return { ...win, stage: "maximized", restoreStage: null, bounds: fullBounds() };
}
if (target === "snapped" && win.snapZone) {
return {
...win,
stage: "snapped",
restoreStage: null,
bounds: zoneBounds(win.snapZone, viewport)
};
}
return { ...win, stage: "normal", restoreStage: null };
}
if (win.stage === "normal") return null;
const bounds = win.restoreBounds ?? win.bounds;
return {
...win,
stage: "normal",
snapZone: null,
restoreBounds: null,
restoreStage: null,
bounds: keepInViewport ? clampToViewport(bounds, viewport, minVisible) : bounds
};
});
if (result) focus(id);
return result;
}
function restoreTo(id, bounds) {
const result = applyStage(id, (win) => {
const size = normalizeSize(bounds, win);
const next = { x: bounds.x, y: bounds.y, ...size };
return {
...win,
stage: "normal",
snapZone: null,
restoreBounds: null,
restoreStage: null,
bounds: keepInViewport ? clampToViewport(next, viewport, minVisible) : next
};
});
if (result) focus(id);
return result;
}
function snap(id, zone) {
const result = applyStage(id, (win) => {
const restoreBounds = win.stage === "normal" ? win.bounds : win.restoreBounds;
return {
...win,
stage: "snapped",
snapZone: zone,
restoreBounds,
restoreStage: null,
bounds: zoneBounds(zone, viewport)
};
});
if (result) focus(id);
return result;
}
function move(id, x, y) {
const win = windows[id];
if (win?.stage !== "normal") return false;
let bounds = { ...win.bounds, x, y };
if (keepInViewport) bounds = clampToViewport(bounds, viewport, minVisible);
if (boundsEqual(bounds, win.bounds)) return true;
const next = { ...win, bounds };
setWindow(next);
queueEvent(() => emitter.emit("move", { window: next }));
commit();
return true;
}
function moveBy(id, dx, dy) {
const win = windows[id];
if (!win) return false;
return move(id, win.bounds.x + dx, win.bounds.y + dy);
}
function resize(id, patch) {
const win = windows[id];
if (!win || win.stage === "maximized" || win.stage === "minimized") return false;
const merged = { ...win.bounds, ...patch };
const size = normalizeSize(merged, win);
let bounds = { x: merged.x, y: merged.y, ...size };
if (keepInViewport) bounds = clampToViewport(bounds, viewport, minVisible);
const becameNormal = win.stage === "snapped";
if (boundsEqual(bounds, win.bounds) && !becameNormal) return true;
const next = becameNormal ? { ...win, stage: "normal", snapZone: null, restoreBounds: null, bounds } : { ...win, bounds };
setWindow(next);
if (becameNormal) queueEvent(() => emitter.emit("stage", { window: next, previous: "snapped" }));
queueEvent(() => emitter.emit("resize", { window: next }));
commit();
return true;
}
function update(id, patch) {
const win = windows[id];
if (!win) return false;
const next = {
...win,
title: patch.title ?? win.title,
layer: patch.layer ?? win.layer,
minSize: patch.minSize ?? win.minSize,
maxSize: patch.maxSize === void 0 ? win.maxSize : patch.maxSize,
draggable: patch.draggable ?? win.draggable,
resizable: patch.resizable ?? win.resizable,
closable: patch.closable ?? win.closable,
minimizable: patch.minimizable ?? win.minimizable,
maximizable: patch.maximizable ?? win.maximizable,
snappable: patch.snappable ?? win.snappable,
meta: patch.meta ? { ...win.meta, ...patch.meta } : win.meta
};
const size = normalizeSize(next.bounds, next);
const resized = size.width !== next.bounds.width || size.height !== next.bounds.height;
const finalWin = resized ? { ...next, bounds: { ...next.bounds, ...size } } : next;
setWindow(finalWin);
if (patch.layer && patch.layer !== win.layer) {
order = sortByLayer(order);
queueEvent(() => emitter.emit("order", { order }));
if (patch.layer === "modal" && topModalId() === id && focusedId !== id) {
const previous = focusedId;
focusedId = id;
emitFocus(finalWin, previous);
}
}
queueEvent(() => emitter.emit("update", { window: finalWin }));
if (resized) queueEvent(() => emitter.emit("resize", { window: finalWin }));
commit();
return true;
}
function setViewport(next) {
if (next.width === viewport.width && next.height === viewport.height) return;
viewport = { ...next };
reflowViewport();
snapshot = null;
batchDirty = true;
}
function minimized() {
return Object.values(windows).filter((win) => win.stage === "minimized").sort((a, b) => a.openedSeq - b.openedSeq);
}
function captureEntry() {
return { windows, order, focusedId };
}
function recordHistory() {
if (skipNextHistory) {
skipNextHistory = false;
return;
}
if (historyLimit <= 0) return;
if (interactionDepth > 0) {
if (interactionRecorded) return;
interactionRecorded = true;
}
past.push(pendingHistory);
if (past.length > historyLimit) past.shift();
future = [];
}
function transact(run) {
if (batchDepth === 0) pendingHistory = captureEntry();
batchDepth += 1;
try {
return run();
} finally {
batchDepth -= 1;
if (batchDepth === 0) {
if (batchDirty) {
batchDirty = false;
recordHistory();
snapshot = null;
flushEvents();
emitter.emit("change", { state: getState() });
}
pendingHistory = null;
}
}
}
function reflowViewport() {
for (const id of order) {
const win = windows[id];
if (win.stage === "maximized") {
const updated = { ...win, bounds: fullBounds() };
setWindow(updated);
queueEvent(() => emitter.emit("resize", { window: updated }));
} else if (win.stage === "snapped" && win.snapZone) {
const updated = { ...win, bounds: zoneBounds(win.snapZone, viewport) };
setWindow(updated);
queueEvent(() => emitter.emit("resize", { window: updated }));
} else if (win.stage === "normal" && keepInViewport) {
const bounds = clampToViewport(win.bounds, viewport, minVisible);
if (!boundsEqual(bounds, win.bounds)) {
const updated = { ...win, bounds };
setWindow(updated);
queueEvent(() => emitter.emit("move", { window: updated }));
}
}
}
}
function applyEntry(entry) {
windows = entry.windows;
order = [...entry.order];
focusedId = entry.focusedId;
reflowViewport();
commit();
}
function undo() {
const entry = past.pop();
if (!entry) return false;
future.push(captureEntry());
skipNextHistory = true;
applyEntry(entry);
return true;
}
function redo() {
const entry = future.pop();
if (!entry) return false;
past.push(captureEntry());
skipNextHistory = true;
applyEntry(entry);
return true;
}
function beginInteraction() {
interactionDepth += 1;
if (interactionDepth === 1) interactionRecorded = false;
}
function endInteraction() {
if (interactionDepth > 0) interactionDepth -= 1;
}
function clearHistory() {
past.length = 0;
future = [];
}
function saveLayout(name) {
const data = serialize();
layouts.set(name, structuredClone(data));
return data;
}
function loadLayout(name) {
const data = layouts.get(name);
if (!data) return false;
return hydrate(structuredClone(data));
}
function getLayout(name) {
const data = layouts.get(name);
return data ? structuredClone(data) : void 0;
}
function setLayout(name, data) {
if (!isValidSerialized(data)) return false;
layouts.set(name, structuredClone(data));
return true;
}
function deleteLayout(name) {
return layouts.delete(name);
}
function layoutNames() {
return [...layouts.keys()];
}
function arrange(mode) {
const ids = order.filter((id) => windows[id].stage !== "minimized");
if (ids.length === 0) return;
if (mode === "cascade") {
ids.forEach((id, index) => {
const win = windows[id];
const size = win.restoreBounds ?? win.bounds;
restoreTo(id, {
x: cascadeOrigin.x + index % 10 * cascadeOffset,
y: cascadeOrigin.y + index % 10 * cascadeOffset,
width: size.width,
height: size.height
});
});
return;
}
const cols = Math.ceil(Math.sqrt(ids.length));
const rows = Math.ceil(ids.length / cols);
const cellWidth = Math.floor(viewport.width / cols);
const cellHeight = Math.floor(viewport.height / rows);
ids.forEach((id, index) => {
restoreTo(id, {
x: index % cols * cellWidth,
y: Math.floor(index / cols) * cellHeight,
width: cellWidth,
height: cellHeight
});
});
}
function minimizeAll() {
for (const id of [...order]) minimize(id);
}
function restoreAll() {
for (const win of minimized()) restore(win.id);
}
function serialize() {
return {
version: 1,
windows: order.map((id) => windows[id]).map((win) => ({
...win,
bounds: { ...win.bounds },
restoreBounds: win.restoreBounds ? { ...win.restoreBounds } : null,
minSize: { ...win.minSize },
maxSize: win.maxSize ? {
width: Number.isFinite(win.maxSize.width) ? win.maxSize.width : null,
height: Number.isFinite(win.maxSize.height) ? win.maxSize.height : null
} : null,
meta: { ...win.meta }
})),
order: [...order],
focusedId
};
}
function isBounds(value) {
if (typeof value !== "object" || value === null) return false;
const bounds = value;
return typeof bounds.x === "number" && typeof bounds.y === "number" && typeof bounds.width === "number" && typeof bounds.height === "number";
}
function isSize(value) {
if (typeof value !== "object" || value === null) return false;
const size = value;
return typeof size.width === "number" && typeof size.height === "number";
}
function isSizeAxis(value) {
return value === null || typeof value === "number";
}
function readMaxSize(value) {
if (typeof value !== "object" || value === null) return null;
const size = value;
if (!isSizeAxis(size.width) || !isSizeAxis(size.height)) return null;
if (size.width === null && size.height === null) return null;
return {
width: size.width ?? Number.POSITIVE_INFINITY,
height: size.height ?? Number.POSITIVE_INFINITY
};
}
function isSerializedWindow(value) {
if (typeof value !== "object" || value === null) return false;
const win = value;
return typeof win.id === "string" && win.id.length > 0 && isBounds(win.bounds) && STAGES.includes(win.stage) && LAYERS.includes(win.layer);
}
function isValidSerialized(data) {
if (typeof data !== "object" || data === null) return false;
if (data.version !== 1 || !Array.isArray(data.windows)) return false;
if (!data.windows.every(isSerializedWindow)) return false;
const ids = new Set(data.windows.map((win) => win.id));
return ids.size === data.windows.length;
}
function hydrate(data) {
if (!isValidSerialized(data)) return false;
const previousWindows = windows;
const nextWindows = {};
let maxSeq = 0;
for (const raw of data.windows) {
if (nextWindows[raw.id]) return false;
const win = {
id: raw.id,
title: typeof raw.title === "string" ? raw.title : "Window",
bounds: { ...raw.bounds },
restoreBounds: raw.restoreBounds && isBounds(raw.restoreBounds) ? { ...raw.restoreBounds } : null,
restoreStage: STAGES.includes(raw.restoreStage) ? raw.restoreStage : null,
stage: raw.stage,
snapZone: ZONES.includes(raw.snapZone) ? raw.snapZone : null,
layer: raw.layer,
minSize: isSize(raw.minSize) ? { ...raw.minSize } : { width: 160, height: 100 },
maxSize: readMaxSize(raw.maxSize),
openedSeq: typeof raw.openedSeq === "number" ? raw.openedSeq : ++maxSeq,
draggable: raw.draggable !== false,
resizable: raw.resizable !== false,
closable: raw.closable !== false,
minimizable: raw.minimizable !== false,
maximizable: raw.maximizable !== false,
snappable: raw.snappable !== false,
meta: raw.meta && typeof raw.meta === "object" ? raw.meta : {}
};
maxSeq = Math.max(maxSeq, win.openedSeq);
nextWindows[win.id] = win;
}
const requestedOrder = Array.isArray(data.order) ? data.order : [];
const seen = /* @__PURE__ */ new Set();
const nextOrder = [];
for (const id of requestedOrder) {
if (typeof id === "string" && nextWindows[id] && !seen.has(id)) {
seen.add(id);
nextOrder.push(id);
}
}
for (const id of Object.keys(nextWindows)) {
if (!seen.has(id)) nextOrder.push(id);
}
windows = nextWindows;
order = nextOrder;
order = sortByLayer(order);
seq = maxSeq;
focusedId = data.focusedId && windows[data.focusedId] ? data.focusedId : null;
if (focusedId && windows[focusedId]?.stage === "minimized") focusedId = null;
if (!focusedId) focusTop();
const modal = topModalId();
if (modal) {
const focusedWin = focusedId ? windows[focusedId] : null;
if (focusedWin?.layer !== "modal") focusedId = modal;
}
for (const id of Object.keys(previousWindows)) {
if (!windows[id]) {
const closed = previousWindows[id];
queueEvent(() => emitter.emit("close", { window: closed }));
}
}
for (const id of order) {
if (!previousWindows[id]) {
const opened = windows[id];
queueEvent(() => emitter.emit("open", { window: opened }));
}
}
queueEvent(() => emitter.emit("order", { order }));
clearHistory();
skipNextHistory = true;
commit();
return true;
}
function destroy() {
emitter.clear();
}
return {
open: (init) => transact(() => open(init)),
close: (id) => transact(() => close(id)),
closeAll: () => transact(closeAll),
focus: (id) => transact(() => focus(id)),
blur: () => transact(blur),
cycleFocus: (direction) => transact(() => cycleFocus(direction)),
minimize: (id) => transact(() => minimize(id)),
maximize: (id) => transact(() => maximize(id)),
toggleMaximize: (id) => transact(() => toggleMaximize(id)),
restore: (id) => transact(() => restore(id)),
restoreTo: (id, bounds) => transact(() => restoreTo(id, bounds)),
snap: (id, zone) => transact(() => snap(id, zone)),
move: (id, x, y) => transact(() => move(id, x, y)),
moveBy: (id, dx, dy) => transact(() => moveBy(id, dx, dy)),
resize: (id, patch) => transact(() => resize(id, patch)),
update: (id, patch) => transact(() => update(id, patch)),
get: (id) => windows[id],
getState,
minimized,
setViewport: (next) => transact(() => setViewport(next)),
batch: (run) => transact(run),
serialize,
hydrate: (data) => transact(() => hydrate(data)),
undo: () => transact(undo),
redo: () => transact(redo),
canUndo: () => past.length > 0,
canRedo: () => future.length > 0,
beginInteraction,
endInteraction,
saveLayout,
loadLayout: (name) => transact(() => loadLayout(name)),
getLayout,
setLayout,
deleteLayout,
layoutNames,
arrange: (mode) => transact(() => arrange(mode)),
minimizeAll: () => transact(minimizeAll),
restoreAll: () => transact(restoreAll),
subscribe: (listener) => emitter.on("change", ({ state }) => listener(state)),
on: (event, listener) => emitter.on(event, listener),
destroy
};
}
// src/dom/animate.ts
function prefersReducedMotion(win) {
return win.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
function flipFromTarget(source, target, options = {}) {
const view = source.ownerDocument.defaultView;
if (!view || prefersReducedMotion(view)) return;
if (typeof source.animate !== "function") return;
const from = target.getBoundingClientRect();
const to = source.getBoundingClientRect();
if (from.width === 0 || to.width === 0) return;
const ghost = source.ownerDocument.createElement("div");
const style = view.getComputedStyle(source);
ghost.style.cssText = `position:fixed;left:${to.left}px;top:${to.top}px;width:${to.width}px;height:${to.height}px;margin:0;pointer-events:none;z-index:2147483647;border-radius:${style.borderRadius};background:${style.backgroundColor};box-shadow:${style.boxShadow};transform-origin:top left;will-change:transform,opacity`;
source.ownerDocument.body.append(ghost);
const dx = from.left + from.width / 2 - (to.left + to.width / 2);
const dy = from.top + from.height / 2 - (to.top + to.height / 2);
const scaleX = Math.max(from.width / to.width, 0.05);
const scaleY = Math.max(from.height / to.height, 0.05);
const animation = ghost.animate(
[
{ transform: `translate(${dx}px, ${dy}px) scale(${scaleX}, ${scaleY})`, opacity: 0.2 },
{ transform: "translate(0, 0) scale(1, 1)", opacity: 0.9 }
],
{
duration: options.duration ?? 260,
easing: options.easing ?? "cubic-bezier(0.32, 0.72, 0, 1)"
}
);
animation.onfinish = () => ghost.remove();
animation.oncancel = () => ghost.remove();
}
function flipToTarget(source, target, options = {}) {
const view = source.ownerDocument.defaultView;
if (!view || prefersReducedMotion(view)) return;
if (typeof source.animate !== "function") return;
const from = source.getBoundingClientRect();
const to = target.getBoundingClientRect();
if (from.width === 0 || to.width === 0) return;
const ghost = source.ownerDocument.createElement("div");
const style = view.getComputedStyle(source);
ghost.style.cssText = `position:fixed;left:${from.left}px;top:${from.top}px;width:${from.width}px;height:${from.height}px;margin:0;pointer-events:none;z-index:2147483647;border-radius:${style.borderRadius};background:${style.backgroundColor};box-shadow:${style.boxShadow};transform-origin:top left;will-change:transform,opacity`;
source.ownerDocument.body.append(ghost);
const dx = to.left + to.width / 2 - (from.left + from.width / 2);
const dy = to.top + to.height / 2 - (from.top + from.height / 2);
const scaleX = Math.max(to.width / from.width, 0.05);
const scaleY = Math.max(to.height / from.height, 0.05);
const animation = ghost.animate(
[
{ transform: "translate(0, 0) scale(1, 1)", opacity: 0.9 },
{ transform: `translate(${dx}px, ${dy}px) scale(${scaleX}, ${scaleY})`, opacity: 0.2 }
],
{
duration: options.duration ?? 260,
easing: options.easing ?? "cubic-bezier(0.32, 0.72, 0, 1)"
}
);
animation.onfinish = () => ghost.remove();
animation.oncancel = () => ghost.remove();
}
// src/dom/announcer.ts
var defaultMessages = {
opened: (title) => `${title} window opened`,
closed: (title) => `${title} window closed`,
minimized: (title) => `${title} minimized`,
restored: (title) => `${title} restored`,
maximized: (title) => `${title} maximized`,
snapped: (title, zone) => `${title} snapped to ${zone.replace("-", " ")}`,
focused: (title) => `${title} focused`
};
function createAnnouncer(wm, container, messages = {}) {
const dict = { ...defaultMessages, ...messages };
const element = container.ownerDocument.createElement("div");
element.setAttribute("role", "status");
element.setAttribute("aria-live", "polite");
element.dataset.wmAnnouncer = "";
element.style.cssText = "position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0";
container.append(element);
let clearTimer;
function announce(message) {
element.textContent = message;
if (clearTimer !== void 0) clearTimeout(clearTimer);
clearTimer = setTimeout(() => {
element.textContent = "";
}, 2e3);
}
const unsubscribers = [
wm.on("open", ({ window: win }) => announce(dict.opened(win.title))),
wm.on("close", ({ window: win }) => announce(dict.closed(win.title))),
wm.on("stage", ({ window: win, previous }) => {
if (win.stage === "minimized") announce(dict.minimized(win.title));
else if (win.stage === "maximized") announce(dict.maximized(win.title));
else if (win.stage === "snapped" && win.snapZone)
announce(dict.snapped(win.title, win.snapZone));
else if (win.stage === "normal" && previous !== "normal") announce(dict.restored(win.title));
})
];
return {
element,
announce,
destroy() {
for (const unsubscribe of unsubscribers) unsubscribe();
if (clearTimer !== void 0) clearTimeout(clearTimer);
element.remove();
}
};
}
// src/dom/shared.ts
var INTERACTIVE_SELECTOR = "button, input, select, textarea, a[href], [contenteditable], [data-wm-close], [data-wm-minimize], [data-wm-maximize]";
function windowOf(element) {
const view = element.ownerDocument.defaultView;
if (!view) throw new Error("wmkit: desktop element is not attached to a document");
return view;
}
// src/dom/drag.ts
function createDragStarter(ctx) {
const { wm, doc, view } = ctx;
return function startDrag(id, handle, event) {
const win = wm.get(id);
if (!win?.draggable || event.button !== 0 || ctx.currentDrag()) return;
const target = event.target;
if (target?.closest(INTERACTIVE_SELECTOR)) return;
event.preventDefault();
const releaseRect = ctx.trackRect();
const point = ctx.toLocal(event);
const startBounds = win.bounds;
const session = {
id,
pointerId: event.pointerId,
grabDX: point.x - startBounds.x,
grabDY: point.y - startBounds.y,
grabRatio: clamp((point.x - startBounds.x) / startBounds.width, 0.05, 0.95),
startBounds,
startStage: win.stage,
startZone: win.snapZone,
startRestoreBounds: win.restoreBounds,
restored: win.stage === "normal",
moved: false,
zone: null,
raf: 0,
pendingX: 0,
pendingY: 0,
hasPending: false,
finish: () => {
}
};
const el = ctx.windowElement(id);
function flush() {
if (!session.hasPending || ctx.currentDrag() !== session) return;
session.hasPending = false;
session.raf = 0;
const current = wm.get(id);
if (!current) return;
if (!session.restored) {
const source = current.restoreBounds ?? session.startBounds;
const width = source.width;
const height = source.height;
wm.restoreTo(id, {
x: session.pendingX - width * session.grabRatio,
y: session.pendingY - Math.min(session.grabDY, 24),
width,
height
});
session.restored = true;
const restoredWin = wm.get(id);
if (restoredWin) {
session.grabDX = session.pendingX - restoredWin.bounds.x;
session.grabDY = session.pendingY - restoredWin.bounds.y;
}
return;
}
let nextX = session.pendingX - session.grabDX;
let nextY = session.pendingY - session.grabDY;
if (ctx.magnetThreshold > 0) {
const state = wm.getState();
const targets = [
{ x: 0, y: 0, width: state.viewport.width, height: state.viewport.height }
];
for (const otherId of state.order) {
if (otherId === id) continue;
const other = state.windows[otherId];
if (other && other.stage !== "minimized") targets.push(other.bounds);
}
const magnet = magnetize(
{ x: nextX, y: nextY, width: current.bounds.width, height: current.bounds.height },
targets,
ctx.magnetThreshold
);
nextX = magnet.x;
nextY = magnet.y;
}
wm.move(id, nextX, nextY);
if (ctx.snapEnabled && current.snappable) {
const viewport = wm.getState().viewport;
const rawZone = detectSnapZone(session.pendingX, session.pendingY, viewport, ctx.snapDetect);
let zone = rawZone;
if (rawZone === "top") {
if (ctx.topEdge === "maximize") zone = current.maximizable ? "maximize" : null;
else if (ctx.topEdge === "none") zone = null;
}
session.zone = zone;
if (zone) {
ctx.showPreview(
zone === "maximize" ? { x: 0, y: 0, width: viewport.width, height: viewport.height } : zoneBounds(zone, viewport)
);
} else {
ctx.hidePreview();
}
}
}
function onMove(moveEvent) {
if (moveEvent.pointerId !== session.pointerId) return;
const movePoint = ctx.toLocal(moveEvent);
if (!session.moved) {
const travelled = Math.abs(movePoint.x - (session.startBounds.x + session.grabDX)) + Math.abs(movePoint.y - (session.startBounds.y + session.grabDY));
if (travelled < 3 && session.restored) return;
session.moved = true;
if (el) {
el.dataset.wmDragging = "";
el.style.willChange = "transform";
}
}
session.pendingX = movePoint.x;
session.pendingY = movePoint.y;
session.hasPending = true;
if (session.raf === 0) session.raf = view.requestAnimationFrame(flush);
}
function onUp(upEvent) {
if (upEvent.pointerId !== session.pointerId) return;
finish(false);
}
function onCancel(cancelEvent) {
if (cancelEvent.pointerId !== session.pointerId) return;
finish(true);
}
function onKeydown(keyEvent) {
if (keyEvent.key === "Escape") {
keyEvent.preventDefault();
finish(true);
}
}
function finish(cancelled) {
if (ctx.currentDrag() !== session) return;
if (session.raf !== 0) view.cancelAnimationFrame(session.raf);
if (session.hasPending && !cancelled) flush();
ctx.releaseDrag(session);
releaseRect();
handle.removeEventListener("pointermove", onMove);
handle.removeEventListener("pointerup", onUp);
handle.removeEventListener("pointercancel", onCancel);
doc.removeEventListener("keydown", onKeydown, true);
if (handle.hasPointerCapture(session.pointerId)) {
handle.releasePointerCapture(session.pointerId);
}
if (el) {
delete el.dataset.wmDragging;
el.style.willChange = "";
}
ctx.hidePreview();
if (cancelled) {
if (session.moved && session.restored) {
if (session.startStage === "maximized") {
wm.restoreTo(id, session.startRestoreBounds ?? session.startBounds);
wm.maximize(id);
} else if (session.startStage === "snapped" && session.startZone) {
wm.restoreTo(id, session.startRestoreBounds ?? session.startBounds);
wm.snap(id, session.startZone);
} else {
wm.move(id, session.startBounds.x, session.startBounds.y);
}
}
wm.endInteraction();
return;
}
if (session.moved && session.zone) {
if (session.zone === "maximize") wm.maximize(id);
else wm.snap(id, session.zone);
}
wm.endInteraction();
}
session.finish = finish;
wm.beginInteraction();
ctx.claimDrag(session);
handle.setPointerCapture(event.pointerId);
handle.addEventListener("pointermove", onMove);
handle.addEventListener("pointerup", onUp);
handle.addEventListener("pointercancel", onCancel);
doc.addEventListener("keydown", onKeydown, true);
};
}
// src/dom/resize.ts
var RESIZE_DIRECTIONS = ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
var RESIZE_CURSORS = {
n: "ns-resize",
s: "ns-resize",
e: "ew-resize",
w: "ew-resize",
ne: "nesw-resize",
sw: "nesw-resize",
nw: "nwse-resize",
se: "nwse-resize"
};
function createResizeStarter(ctx) {
const { wm, doc, view } = ctx;
return function startResize(id, direction, event) {
const win = wm.get(id);
if (!win?.resizable || event.button !== 0 || ctx.currentDrag()) return;
event.preventDefault();
event.stopPropagation();
const handleEl = event.currentTarget;
wm.beginInteraction();
const releaseRect = ctx.trackRect();
const startPoint = ctx.toLocal(event);
const start = win.bounds;
const minSize = win.minSize;
const maxSize = win.maxSize;
let raf = 0;
let pendingX = 0;
let pendingY = 0;
let hasPending = false;
const el = ctx.windowElement(id);
if (el) el.dataset.wmResizing = direction;
function clampWidth(width) {
return clamp(width, minSize.width, maxSize ? maxSize.width : Number.POSITIVE_INFINITY);
}
function clampHeight(height) {
return clamp(height, minSize.height, maxSize ? maxSize.height : Number.POSITIVE_INFINITY);
}
function flush() {
if (!hasPending) return;
hasPending = false;
raf = 0;
const dx = pendingX - startPoint.x;
const dy = pendingY - startPoint.y;
const next = { ...start };
if (direction.includes("e")) next.width = clampWidth(start.width + dx);
if (direction.includes("s")) next.height = clampHeight(start.height + dy);
if (direction.includes("w")) {
next.width = clampWidth(start.width - dx);
next.x = start.x + (start.width - next.width);
}
if (direction.includes("n")) {
next.height = clampHeight(start.height - dy);
next.y = start.y + (start.height - next.height);
}
wm.resize(id, next);
}
function onMove(moveEvent) {
if (moveEvent.pointerId !== event.pointerId) return;
const movePoint = ctx.toLocal(moveEvent);
pendingX = movePoint.x;
pendingY = movePoint.y;
hasPending = true;
if (raf === 0) raf = view.requestAnimationFrame(flush);
}
function finish(cancelled) {
if (raf !== 0) view.cancelAnimationFrame(raf);
if (hasPending && !cancelled) flush();
releaseRect();
handleEl.removeEventListener("pointermove", onMove);
handleEl.removeEventListener("pointerup", onUp);
handleEl.removeEventListener("pointercancel", onCancelPointer);
doc.removeEventListener("keydown", onKeydown, true);
if (handleEl.hasPointerCapture(event.pointerId)) {
handleEl.releasePointerCapture(event.pointerId);
}
if (el) delete el.dataset.wmResizing;
if (cancelled) wm.resize(id, start);
wm.endInteraction();
}
function onUp(upEvent) {
if (upEvent.pointerId !== event.pointerId) return;
finish(false);
}
function onCancelPointer(cancelEvent) {
if (cancelEvent.pointerId !== event.pointerId) return;
finish(true);
}
function onKeydown(keyEvent) {
if (keyEvent.key === "Escape") {
keyEvent.preventDefault();
finish(true);
}
}
handleEl.setPointerCapture(event.pointerId);
handleEl.addEventListener("pointermove", onMove);
handleEl.addEventListener("pointerup", onUp);
handleEl.addEventListener("pointercancel", onCancelPointer);
doc.addEventListener("keydown", onKeydown, true);
};
}
function createResizeHandles(doc, edge, corner) {
const base = "position:absolute;touch-action:none;user-select:none;-webkit-user-select:none;";
const styles = {
n: `top:${-edge / 2}px;left:${corner}px;right:${corner}px;height:${edge}px`,
s: `bottom:${-edge / 2}px;left:${corner}px;right:${corner}px;height:${edge}px`,
e: `right:${-edge / 2}px;top:${corner}px;bottom:${corner}px;width:${edge}px`,
w: `left:${-edge / 2}px;top:${corner}px;bottom:${corner}px;width:${edge}px`,
ne: `top:${-edge / 2}px;right:${-edge / 2}px;width:${corner}px;height:${corner}px`,
nw: `top:${-edge / 2}px;left:${-edge / 2}px;width:${corner}px;height:${corner}px`,
se: `bottom:${-edge / 2}px;right:${-edge / 2}px;width:${corner}px;height:${corner}px`,
sw: `bottom:${-edge / 2}px;left:${-edge / 2}px;width:${corner}px;height:${corner}px`
};
return RESIZE_DIRECTIONS.map((direction) => {
const element = doc.createElement("div");
element.dataset.wmResize = direction;
element.setAttribute("aria-hidden", "true");
element.style.cssText = `${base}${styles[direction]};cursor:${RESIZE_CURSORS[direction]}`;
return { element, direction };
});
}
// src/dom/controller.ts
function attachDesktop(wm, element, options = {}) {
const doc = element.ownerDocument;
const view = windowOf(element);
const coarsePointer = typeof view.matchMedia === "function" && view.matchMedia("(pointer: coarse)").matches;
const snapEnabled = options.snap !== false;
const snapOptions = typeof options.snap === "object" ? options.snap : {};
const snapPreviewEnabled = snapOptions.preview !== false;
const topEdge = snapOptions.topEdge ?? "maximize";
const keyboardEnabled = options.keyboard !== false;
const keyboardOptions = typeof options.keyboard === "object" ? options.keyboard : {};
const moveStep = keyboardOptions.moveStep ?? 16;
const cycleEnabled = keyboardOptions.cycle !== false;
const hitEdge = options.hitAreas?.edge ?? (coarsePointer ? 16 : 8);
const hitCorner = options.hitAreas?.corner ?? (coarsePointer ? 24 : 12);
const magnetThreshold = options.magnetism === false ? 0 : (typeof options.magnetism === "object" ? options.magnetism.threshold : void 0) ?? (coarsePointer ? 12 : 8);
element.dataset.wmDesktop = "";
if (view.getComputedStyle(element).position === "static") {
element.style.position = "relative";
}
const registry = /* @__PURE__ */ new Map();
const cleanup = [];
let lastOrder = null;
let lastFocused = null;
let drag = null;
let cachedRect = null;
let rectUsers = 0;
let announcer = null;
if (options.announce !== false) {
announcer = createAnnouncer(
wm,
element,
typeof options.announce === "object" ? options.announce : {}
);
cleanup.push(() => announcer?.destroy());
}
let preview = null;
function showPreview(bounds) {
if (!snapPreviewEnabled) return;
if (!preview) {
preview = doc.createElement("div");
preview.dataset.wmSnapPreview = "";
preview.style.cssText = "position:absolute;left:0;top:0;pointer-events:none;display:none;z-index:2147483646";
element.append(preview);
}
preview.style.display = "block";
preview.style.transform = `translate3d(${bounds.x}px, ${bounds.y}px, 0)`;
preview.style.width = `${bounds.width}px`;
preview.style.height = `${bounds.height}px`;
}
function hidePreview() {
if (preview) preview.style.display = "none";
}
const ctx = {
wm,
doc,
view,
toLocal(event) {
const rect = cachedRect ?? element.getBoundingClientRect();
return { x: event.clientX - rect.left, y: event.clientY - rect.top };
},
trackRect() {
if (rectUsers === 0) {
const rect = element.getBoundingClientRect();
cachedRect = { left: rect.left, top: rect.top };
}
rectUsers += 1;
let released = false;
return () => {
if (released) return;
released = true;
rectUsers -= 1;
if (rectUsers === 0) cachedRect = null;
};
},
windowElement: (id) => registry.get(id)?.element,
showPreview,
hidePreview,
snapEnabled,
snapDetect: {
threshold: snapOptions.threshold ?? (coarsePointer ? 20 : 12),
cornerSize: snapOptions.cornerSize ?? (coarsePointer ? 96 : 64)
},
topEdge,
hitEdge,
hitCorner,
magnetThreshold,
currentDrag: () => drag,
claimDrag(session) {
drag = session;
},
releaseDrag(session) {
if (drag === session) drag = null;
}
};
const startDrag = createDragStarter(ctx);
const startResize = createResizeStarter(ctx);
if (options.autoViewport !== false) {
const applyViewport = () => wm.setViewport({ width: element.clientWidth, height: element.clientHeight });
applyViewport();
const observer = new ResizeObserver(applyViewport);
observer.observe(element);
cleanup.push(() => observer.disconnect());
}
function syncWindow(attached, win, zIndex) {
const el = attached.element;
const firstSync = attached.lastState === null;
if (firstSync) el.style.transition = "none";
if (attached.lastState !== win) {
el.style.transform = `translate3d(${win.bounds.x}px, ${win.bounds.y}px, 0)`;
el.style.width = `${win.bounds.width}px`;
el.style.height = `${win.bounds.height}px`;
el.dataset.wmStage = win.stage;
el.dataset.wmLayer = win.layer;
el.hidden = win.stage === "minimized";
el.setAttribute("aria-label", win.title);
if (win.layer === "modal") el.setAttribute("aria-modal", "true");
else el.removeAttribute("aria-modal");
for (const handle of attached.handles) {
handle.style.display = win.resizable && win.stage === "normal" ? "" : "none";
}
attached.lastState = win;
}
if (attached.lastZ !== zIndex) {
el.style.zIndex = String(zIndex + 1);
attached.lastZ = zIndex;
}
if (firstSync) {
void el.offsetWidth;
el.style.transition = "";
}
}
function syncAll() {
const state = wm.getState();
const orderChanged = state.order !== lastOrder;
state.order.forEach((id, index) => {
const attached = registry.get(id);
const win = state.windows[id];
if (!attached || !win) return;
if (orderChanged || attached.lastState !== win) syncWindow(attached, win, index);
});
if (state.focusedId !== lastFocused) {
if (lastFocused) {
const prev = registry.get(lastFocused);
if (prev) delete prev.element.dataset.wmFocused;
}
if (state.focusedId) {
const next = registry.get(state.focusedId);
if (next) next.element.dataset.wmFocused = "";
}
lastFocused = state.focusedId;
}
lastOrder = state.order;
}
cleanup.push(wm.subscribe(syncAll));
cleanup.push(
wm.on("focus", ({ window: win }) => {
const attached = registry.get(win.id);
if (!attached) return;
if (!attached.element.contains(doc.activeElement)) {
attached.element.focus({ preventScroll: true });
}
})
);
cleanup.push(
wm.on("modalblocked", ({ window: win }) => {
const attached = registry.get(win.id);
if (!attached) return;
delete attached.element.dataset.wmFlash;
void attached.element.offsetWidth;
attached.element.dataset.wmFlash = "";
})
);
cleanup.push(
wm.on("stage", ({ window: win, previous }) => {
const attached = registry.get(win.id);
if (!attached) return;
if (win.stage === "minimized" && previous !== "minimized") {
const target = options.minimizeTarget?.(win);
if (target) flipToTarget(attached.element, target);
} else if (previous === "minimized" && win.stage !== "minimized") {
const target = options.minimizeTarget?.(win);
if (target) flipFromTarget(attached.element, target);
}
})
);
if (keyboardEnabled && cycleEnabled) {
const onDesktopKeydown = (event) => {
if (event.key === "F6") {
event.preventDefault();
wm.cycleFocus(event.shiftKey ? -1 : 1);
}
};
element.addEventListener("keydown", onDesktopKeydown);
cleanup.push(() => element.removeEventListener("keydown", onDesktopKeydown));
}
function endDrag(cancelled) {
if (drag) drag.finish(cancelled);
}
function attachWindow(id, windowElement, windowOptions = {}) {
const win = wm.get(id);
if (!win) throw new Error(`wmkit: cannot attach unknown window "${id}"`);
if (registry.has(id)) throw new Error(`wmkit: window "${id}" is already attached`);
const attached = {
element: windowElement,
handle: null,
handles: [],
lastState: null,
lastZ: -1,
cleanup: []
};
windowElement.dataset.wmWindow = id;
windowElement.setAttribute("role", "dialog");
windowElement.tabIndex = -1;
windowElement.style.position = "absolute";
windowElement.style.left = "0";
windowElement.style.top = "0";
const titleEl = windowElement.querySelector("[data-wm-title]");
if (titleEl) {
if (!titleEl.id) titleEl.id = `wmkit-title-${id}`;
windowElement.setAttribute("aria-labelledby", titleEl.id);
}
const handle = typeof windowOptions.handle === "string" ? windowElement.querySelector(windowOptions.handle) : windowOptions.handle ?? windowElement.querySelector("[data-wm-drag]");
attached.handle = handle;
const onPointerDownFocus = () => {
wm.focus(id);
};
windowElement.addEventListener("pointerdown", onPointerDownFocus, true);
attached.cleanup.push(
() => windowElement.removeEventListener("pointerdown", onPointerDownFocus, true)
);
const onClick = (event) => {
const target = event.target;
if (!target) return;
const current = wm.get(id);
if (!current) return;
if (target.closest("[data-wm-close]")) {
if (current.closable) wm.close(id);
} else if (target.closest("[data-wm-minimize]")) {
if (current.minimizable) wm.minimize(id);
} else if (target.closest("[data-wm-maximize]")) {
if (current.maximizable) wm.toggleMaximize(id);
}
};
windowElement.addEventListener("click", onClick);
attached.cleanup.push(() => windowElement.removeEventListener("click", onClick));
if (handle) {
handle.style.touchAction = "none";
const onHandleDown = (event) => startDrag(id, handle, event);
handle.addEventListener("pointerdown", onHandleDown);
attached.cleanup.push(() => handle.removeEventListener("pointerdown", onHandleDown));
const onDoubleClick = (event) => {
const target = event.target;
if (target?.closest(INTERACTIVE_SELECTOR)) return;
const current = wm.get(id);
if (current?.maximizable) wm.toggleMaximize(id);
};
handle.addEventListener("dblclick", onDoubleClick);
attached.cleanup.push(() => handle.removeEventListener("dblclick", onDoubleClick));
if (options.onTitlebarContextMenu) {
const onContextMenu = (event) => {
const current = wm.get(id);
if (!current) return;
event.preventDefault();
options.onTitlebarContextMenu?.(current, event);
};
handle.addEventListener("contextmenu", onContextMenu);
attached.cleanup.push(() => handle.removeEventListener("contextmenu", onContextMenu));
}
}
if (windowOptions.resizeHandles !== false) {
for (const { element: resizeHandle, direction } of createResizeHandles(
doc,
hitEdge,
hitCorner
)) {
const onResizeDown = (event) => startResize(id, direction, event);
resizeHandle.addEventListener("pointerdown", onResizeDown);
attached.cleanup.push(() => resizeHandle.removeEventListener("pointerdown", onResizeDown));
windowElement.append(resizeHandle);
attached.handles.push(resizeHandle);
}
}
const onWindowKeydown = (event) => {
const target = event.target;
if (target?.closest(INTERACTIVE_SELECTOR)) return;
const current = wm.get(id);
if (!current) return;
const arrows = {
ArrowLeft: [-1, 0],
ArrowRight: [1, 0],
ArrowUp: [0, -1],
ArrowDown: [0, 1]
};
const vector = arrows[event.key];
if (!vector || !keyboardEnabled) return;
event.preventDefault();
const step = event.altKey ? 1 : moveStep;
const [dx, dy] = vector;
if (event.shiftKey) {
if (current.resizable) {
wm.resize(id, {
width: current.bounds.width + dx * step,
height: current.bounds.height + dy * step
});
}
} else if (current.draggable && current.stage === "normal") {
wm.moveBy(id, dx * step, dy * step);
}
};
windowElement.addEventListener("keydown", onWindowKeydown);
attached.cleanup.push(() => windowElement.removeEventListener("keydown", onWindowKeydown));
const onModalTrap = (event) => {
if (event.key !== "Tab") return;
const current = wm.get(id);
if (current?.layer !== "modal") return;
const focusables = windowElement.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusables.length === 0) return;
const first = focusables[0];
const last = focusables[focusables.length - 1];
if (!first || !last) return;
if (event.shiftKey && doc.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && doc.activeElement === last) {
event.preventDefault();
first.focus();
}
};
windowElement.addEventListener("keydown", onModalTrap);
attached.cleanup.push(() => windowElement.removeEventListener("keydown", onModalTrap));
const detach = () => {
if (drag?.id === id) endDrag(true);
for (const dispose of attached.cleanup) dispose();
for (const resizeHandle of attached.handles) resizeHandle.remove();
registry.delete(id);
};
if (windowOptions.removeOnClose) {
const stopOnClose = wm.on("close", ({ window: closed }) => {
if (closed.id !== id) return;
detach();
windowElement.remove();
});
attached.cleanup.push(stopOnClose);
}
registry.set(id, attached);
lastOrder = null;
lastFocused = null;
syncAll();
return detach;
}
return {
element,
wm,
attachWindow,
destroy() {
endDrag(true);
for (const [, attached] of registry) {
for (const dispose of attached.cleanup) dispose();
for (const resizeHandle of attached.handles) resizeHandle.remove();
}
registry.clear();
for (const dispose of cleanup) dispose();
preview?.remove();
delete element.dataset.wmDesktop;
}
};
}
// src/dom/binder.ts
function createDesktopBinder(wm, options = {}) {
let controller = null;
const entries = /* @__PURE__ */ new Set();
function attachEntry(entry) {
if (controller && !entry.detach && wm.get(entry.id)) {
entry.detach = controller.attachWindow(entry.id, entry.element, entry.options);
}
}
wm.on("open", ({ window: win }) => {
for (const entry of entries) {
if (entry.id === win.id) attachEntry(entry);
}
});
return {
wm,
controller: () => controller,
bindDesktop(element) {
if (controller) throw new Error("wmkit: desktop is already bound");
controller = attachDesktop(wm, element, options);
for (const entry of entries) attachEntry(entry);
return () => {
for (const entry of entries) entry.detach = null;
controller?.destroy();
controller = null;
};
},
bindWindow(id, element, windowOptions) {
const entry = { id, element, options: windowOptions, detach: null };
entries.add(entry);
attachEntry(entry);
return () => {
entry.detach?.();
entry.detach = null;
entries.delete(entry);
};
}
};
}
exports.attachDesktop = attachDesktop;
exports.boundsEqual = boundsEqual;
exports.clamp = clamp;
exports.clampSize = clampSize;
exports.clampToViewport = clampToViewport;
exports.createAnnouncer = createAnnouncer;
exports.createDesktopBinder = createDesktopBinder;
exports.createEmitter = createEmitter;
exports.createWindowManager = createWindowManager;
exports.defaultMessages = defaultMessages;
exports.detectSnapZone = detectSnapZone;
exports.flipToTarget = flipToTarget;
exports.magnetize = magnetize;
exports.prefersReducedMotion = prefersReducedMotion;
exports.zoneBounds = zoneBounds;
//# sourceMappingURL=chunk-B35DANTX.cjs.map
//# sourceMappingURL=chunk-B35DANTX.cjs.map