beam: add per-model viewer settings

This commit is contained in:
CODEX
2026-06-04 22:36:15 +03:00
parent 09b1173697
commit a95a7d5c38
6 changed files with 1070 additions and 81 deletions
+216
View File
@@ -0,0 +1,216 @@
const closeEventName = "nodedc-select-open";
const makeSelectId = () => `ndc-select-${Math.random().toString(16).slice(2)}`;
export function createGlassSelect({
host,
value,
options = [],
ariaLabel = "Выбрать",
disabled = false,
onChange = null,
}) {
if (!host) return null;
const selectId = makeSelectId();
let currentValue = value;
let currentOptions = [...options];
let open = false;
let menu = null;
let menuRect = { top: 0, left: 0, width: 0 };
host.innerHTML = "";
const root = document.createElement("div");
root.className = "nodedc-select";
const control = document.createElement("div");
control.className = "nodedc-select__control";
const valueEl = document.createElement("div");
valueEl.className = "nodedc-select__value";
const toggle = document.createElement("button");
toggle.type = "button";
toggle.className = "nodedc-select__toggle";
toggle.setAttribute("aria-label", ariaLabel);
toggle.setAttribute("aria-haspopup", "listbox");
toggle.disabled = !!disabled;
const chevron = document.createElement("span");
chevron.className = "nodedc-select__chevron";
chevron.setAttribute("aria-hidden", "true");
toggle.appendChild(chevron);
control.appendChild(valueEl);
control.appendChild(toggle);
root.appendChild(control);
host.appendChild(root);
const selectedOption = () => (
currentOptions.find((option) => option.value === currentValue) ||
currentOptions[0] ||
null
);
const updateValue = () => {
const selected = selectedOption();
valueEl.textContent = selected?.label || "";
toggle.setAttribute("aria-expanded", open ? "true" : "false");
};
const updateMenuRect = () => {
const rect = root.getBoundingClientRect();
const toggleWidth = 42;
const gap = 8;
const menuMaxHeight = 232;
const bottomTop = rect.bottom + 6;
const top = bottomTop + menuMaxHeight > window.innerHeight - 12
? Math.max(12, rect.top - menuMaxHeight - 6)
: bottomTop;
menuRect = {
top,
left: rect.left,
width: Math.max(180, rect.width - toggleWidth - gap),
};
if (menu) {
menu.style.top = `${menuRect.top}px`;
menu.style.left = `${menuRect.left}px`;
menu.style.width = `${menuRect.width}px`;
}
};
const renderMenuOptions = () => {
if (!menu) return;
menu.innerHTML = "";
currentOptions.forEach((option) => {
const item = document.createElement("button");
item.type = "button";
item.className = "nodedc-dropdown-option nodedc-select__option";
item.dataset.value = option.value;
if (option.value === currentValue) {
item.dataset.selected = "true";
}
item.setAttribute("role", "option");
item.setAttribute("aria-selected", option.value === currentValue ? "true" : "false");
item.textContent = option.label;
item.addEventListener("click", () => {
setValue(option.value, { emit: true });
closeMenu();
});
menu.appendChild(item);
});
};
const openMenu = () => {
if (toggle.disabled || open) return;
open = true;
updateValue();
updateMenuRect();
window.dispatchEvent(new CustomEvent(closeEventName, { detail: { id: selectId } }));
menu = document.createElement("div");
menu.className = "nodedc-dropdown-surface nodedc-select__menu";
menu.setAttribute("role", "listbox");
menu.style.position = "fixed";
menu.style.top = `${menuRect.top}px`;
menu.style.left = `${menuRect.left}px`;
menu.style.width = `${menuRect.width}px`;
renderMenuOptions();
document.body.appendChild(menu);
document.addEventListener("mousedown", handleDocumentPointerDown);
document.addEventListener("keydown", handleKeyDown);
window.addEventListener(closeEventName, handleOtherSelectOpen);
window.addEventListener("resize", updateMenuRect);
window.addEventListener("scroll", updateMenuRect, true);
};
function closeMenu() {
if (!open) return;
open = false;
updateValue();
if (menu) {
menu.remove();
menu = null;
}
document.removeEventListener("mousedown", handleDocumentPointerDown);
document.removeEventListener("keydown", handleKeyDown);
window.removeEventListener(closeEventName, handleOtherSelectOpen);
window.removeEventListener("resize", updateMenuRect);
window.removeEventListener("scroll", updateMenuRect, true);
}
function handleDocumentPointerDown(event) {
const target = event.target;
if (
target instanceof Node &&
!root.contains(target) &&
(!menu || !menu.contains(target))
) {
closeMenu();
}
}
function handleKeyDown(event) {
if (event.key === "Escape") closeMenu();
}
function handleOtherSelectOpen(event) {
if (event?.detail?.id !== selectId) closeMenu();
}
function setValue(nextValue, { emit = false } = {}) {
const nextOption = currentOptions.find((option) => option.value === nextValue);
if (!nextOption) return;
const changed = currentValue !== nextValue;
currentValue = nextValue;
updateValue();
renderMenuOptions();
if (emit && changed && typeof onChange === "function") {
onChange(nextValue);
}
}
const setOptions = (nextOptions = [], nextValue = currentValue) => {
currentOptions = [...nextOptions];
currentValue = currentOptions.some((option) => option.value === nextValue)
? nextValue
: currentOptions[0]?.value;
updateValue();
renderMenuOptions();
};
const setDisabled = (nextDisabled) => {
toggle.disabled = !!nextDisabled;
if (toggle.disabled) closeMenu();
};
const destroy = () => {
closeMenu();
root.remove();
};
toggle.addEventListener("click", () => {
if (open) {
closeMenu();
} else {
openMenu();
}
});
valueEl.addEventListener("click", openMenu);
updateValue();
return {
root,
setValue,
setOptions,
setDisabled,
close: closeMenu,
destroy,
getValue: () => currentValue,
};
}
+11 -11
View File
@@ -1,7 +1,7 @@
import { createSliderControl } from "./controls/slider.js";
const defaultSettings = {
themeVersion: "v3-yellow",
themeVersion: "v4-beam-lime",
bgOuter: "#0f0f0f",
canvasTop: "#1c1c1c",
canvasBottom: "#1c1c1c",
@@ -15,14 +15,14 @@ const defaultSettings = {
projectBtnFill: "#1c1c1c",
projectBtnBorder: "#233044",
projectBtnHover: "rgba(255,234,0,0.7)",
modalBtnPrimary: "#ffea00",
modalBtnPrimary: "#c4ff67",
modalBtnSecondary: "#292929",
loaderColor: "#ffea00",
loaderColor: "#c4ff67",
templateGlow: 80,
toolbarBg: "#1c1c1c",
toolbarBgActive: "#ffea00",
toolbarBgActive: "#c4ff67",
toolbarBorder: "#233044",
toolbarOutline: "#ffea00",
toolbarOutline: "#c4ff67",
modalBg: "#1c1c1c",
modalBgAlpha: 1,
modalFocus: "#292929",
@@ -35,19 +35,19 @@ const defaultSettings = {
text: "#e6edf3",
panel: "#1c1c1c",
panel2: "#292929",
highlight: "#ffea00",
highlight: "#c4ff67",
cubeText: "#242424",
cubeEdges: "#e0e7ff",
plus: "#ffea00",
navCubeBase: "#ffea00",
plus: "#c4ff67",
navCubeBase: "#ffffff",
navCubeHover: "#ffffff",
navCubeText: "#242424",
navCubeEdge: "#e0e7ff",
navCubeEdgeAlpha: 0.6,
measureLine: "#ffea00",
measureLabel: "#ffea00",
measureLine: "#c4ff67",
measureLabel: "#c4ff67",
measureLabelText: "#292929",
measureDot: "#ffea00",
measureDot: "#c4ff67",
};
const STORAGE_KEY = "bimdc-settings-v3";