Add core-powered web sculpting studio with tablet controls
Add pressure-aware worker sculpting, bounded shared input, project files and history, and compact icon navigation with an XYZ orbit gizmo. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83 Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
// Panels overlay the canvas: revealing tools never resizes or replaces it.
|
||||
export class Menus {
|
||||
constructor(canOpen, ignoreOutsidePointer) {
|
||||
this.canOpen = canOpen;
|
||||
this.items = [
|
||||
["tools-toggle", "tools-panel"],
|
||||
["settings-toggle", "inspector"],
|
||||
].map(([button, panel]) => ({
|
||||
button: document.getElementById(button),
|
||||
panel: document.getElementById(panel),
|
||||
}));
|
||||
for (const item of this.items) {
|
||||
item.button.addEventListener("pointerenter", (e) => {
|
||||
if (["pen", "mouse"].includes(e.pointerType) && e.buttons === 0)
|
||||
this.open(item);
|
||||
});
|
||||
item.button.addEventListener("click", () => {
|
||||
if (this.current === item && this.pinned) this.close();
|
||||
else this.open(item, true);
|
||||
});
|
||||
item.button.addEventListener("keydown", (e) => {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
this.open(item, true, true);
|
||||
}
|
||||
});
|
||||
for (const el of [item.button, item.panel]) {
|
||||
el.addEventListener("pointerenter", () => clearTimeout(this.timer));
|
||||
el.addEventListener("pointerleave", () => {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
if (!this.pinned) this.close(false);
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
item.panel.addEventListener("pointerdown", () => {
|
||||
this.pinned = true;
|
||||
clearTimeout(this.timer);
|
||||
});
|
||||
item.panel.addEventListener("focusin", () => {
|
||||
this.pinned = true;
|
||||
clearTimeout(this.timer);
|
||||
});
|
||||
item.panel.querySelector("[data-close]").onclick = () => this.close();
|
||||
}
|
||||
document.addEventListener(
|
||||
"pointerdown",
|
||||
(e) => {
|
||||
if (
|
||||
this.current &&
|
||||
!ignoreOutsidePointer(e) &&
|
||||
!this.current.panel.contains(e.target) &&
|
||||
!this.current.button.contains(e.target)
|
||||
)
|
||||
this.close(false);
|
||||
},
|
||||
true,
|
||||
);
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
(e) => {
|
||||
if (
|
||||
e.key === "Escape" &&
|
||||
this.current &&
|
||||
!document.querySelector("dialog[open]")
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
window.addEventListener("resize", () => this.close(false));
|
||||
}
|
||||
open(item, pin = false, focus = false) {
|
||||
if (!this.canOpen() || document.querySelector("dialog[open]")) return;
|
||||
clearTimeout(this.timer);
|
||||
if (this.current !== item) {
|
||||
this.close(false);
|
||||
this.current = item;
|
||||
this.pinned = false;
|
||||
}
|
||||
this.pinned ||= pin;
|
||||
item.panel.hidden = false;
|
||||
const top = Math.max(
|
||||
12,
|
||||
Math.min(
|
||||
item.button.getBoundingClientRect().top,
|
||||
window.innerHeight - item.panel.offsetHeight - 12,
|
||||
),
|
||||
);
|
||||
item.panel.style.top = `${top}px`;
|
||||
item.panel.style.maxHeight = `calc(100dvh - ${top + 12}px)`;
|
||||
item.button.setAttribute("aria-expanded", "true");
|
||||
if (focus)
|
||||
item.panel.querySelector("button:not(:disabled),input,select")?.focus();
|
||||
}
|
||||
close(restore = true) {
|
||||
clearTimeout(this.timer);
|
||||
if (!this.current) return;
|
||||
const item = this.current;
|
||||
this.current = null;
|
||||
this.pinned = false;
|
||||
item.panel.hidden = true;
|
||||
item.button.setAttribute("aria-expanded", "false");
|
||||
if (restore) item.button.focus();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user