Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
122 lines
3.8 KiB
JavaScript
122 lines
3.8 KiB
JavaScript
// The only place a z-index is written.
|
|
//
|
|
// v4 ended up with 9000, 9999, 10001, 12000, 99999, 200000 and two cross-file
|
|
// collisions, because every overlay picked its own number in isolation. Here the
|
|
// ladder is ordered once and each rung owns a block of slots, so peers stack
|
|
// within a rung and can never cross into the next one.
|
|
//
|
|
// A rung is an ORDERED LIST and z is derived from position, rather than each
|
|
// claim owning a fixed offset. That is what makes raise() and release() always
|
|
// correct: with fixed offsets, a near-full rung has no free slot above the top,
|
|
// and "move to the front" has nowhere to go.
|
|
|
|
// Order is the contract; the numbers are derived. Autocomplete sits ABOVE modal
|
|
// on purpose: it opens from a field that is often inside one. A modal sits above
|
|
// every floating window: nothing behind it may be used until it is closed.
|
|
export const LADDER = [
|
|
"local",
|
|
"dropline",
|
|
"sticky",
|
|
"menubar",
|
|
"backdrop",
|
|
"wizard",
|
|
"floatingWindow",
|
|
"modal",
|
|
"popover",
|
|
"toast",
|
|
"autocomplete",
|
|
"tour",
|
|
];
|
|
|
|
export const SLOTS = 100; // peers per rung
|
|
|
|
const stacks = new Map(); // rung -> entries, bottom to top
|
|
|
|
export function baseOf(rung) {
|
|
const i = LADDER.indexOf(rung);
|
|
if (i === -1) {
|
|
throw new RangeError(`Unknown z rung "${rung}". Known: ${LADDER.join(", ")}.`);
|
|
}
|
|
return (i + 1) * SLOTS;
|
|
}
|
|
|
|
/**
|
|
* Take a z-index in `rung`. `onChange(z)` fires whenever this claim's z moves --
|
|
* which happens when a PEER raises or releases, not only when this one does.
|
|
* Without it a window that was above becomes stale in the DOM the moment
|
|
* another is brought to the front.
|
|
*/
|
|
export function claim(rung, onChange) {
|
|
const start = baseOf(rung);
|
|
if (!stacks.has(rung)) stacks.set(rung, []);
|
|
const list = stacks.get(rung);
|
|
|
|
if (list.length >= SLOTS) {
|
|
// Running a rung dry means something opens overlays and never releases
|
|
// them; quietly reusing a slot would hide the leak and stack two things at
|
|
// one z.
|
|
throw new RangeError(`z rung "${rung}" is full (${SLOTS} live claims). Something is not releasing.`);
|
|
}
|
|
|
|
const entry = { onChange };
|
|
list.push(entry);
|
|
let live = true;
|
|
let lastZ = start + list.indexOf(entry);
|
|
|
|
const handle = {
|
|
get z() {
|
|
if (!live) return lastZ;
|
|
lastZ = start + list.indexOf(entry);
|
|
return lastZ;
|
|
},
|
|
get rung() { return rung; },
|
|
get live() { return live; },
|
|
raise() {
|
|
if (!live) return handle;
|
|
const i = list.indexOf(entry);
|
|
if (i === -1 || i === list.length - 1) return handle;
|
|
list.splice(i, 1);
|
|
list.push(entry);
|
|
notify(list, start);
|
|
return handle;
|
|
},
|
|
release() {
|
|
if (!live) return;
|
|
lastZ = handle.z;
|
|
const i = list.indexOf(entry);
|
|
if (i !== -1) list.splice(i, 1);
|
|
live = false;
|
|
notify(list, start);
|
|
},
|
|
};
|
|
return handle;
|
|
}
|
|
|
|
function notify(list, start) {
|
|
list.forEach((entry, i) => { if (entry.onChange) entry.onChange(start + i); });
|
|
}
|
|
|
|
/**
|
|
* Claim a rung AND keep `node`'s z-index in sync with it -- the form every
|
|
* overlay should use.
|
|
*
|
|
* `claim()` alone hands back a number and trusts the caller to notice when it
|
|
* changes, and a caller that forgets gets a z-index that was right at open time
|
|
* and silently wrong afterwards: a peer releasing compacts the rung, and the
|
|
* next overlay opened lands on the number this one is still displaying. Six of
|
|
* the seven call sites forgot. Binding the node here means there is nothing
|
|
* left to forget.
|
|
*/
|
|
export function claimFor(rung, node) {
|
|
const handle = claim(rung, (z) => { node.style.zIndex = String(z); });
|
|
node.style.zIndex = String(handle.z);
|
|
return handle;
|
|
}
|
|
|
|
/** Live claims in a rung, bottom to top. Diagnostics and tests. */
|
|
export const liveCount = (rung) => (stacks.get(rung) || []).length;
|
|
|
|
export function _resetLayers() {
|
|
stacks.clear();
|
|
}
|