Initial import of NODEDC BIM Viewer
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
export function createSliderControl({ label = "", value = 0, min = 0, max = 100, step = 1, onChange }) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "slider-row";
|
||||
|
||||
const lbl = document.createElement("label");
|
||||
lbl.textContent = label;
|
||||
lbl.className = "slider-label";
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "range";
|
||||
input.className = "slider-input";
|
||||
input.min = min;
|
||||
input.max = max;
|
||||
input.step = step;
|
||||
input.value = value;
|
||||
|
||||
const updateFill = () => {
|
||||
const percent = ((input.value - input.min) / (input.max - input.min)) * 100;
|
||||
input.style.setProperty("--slider-percent", `${percent}%`);
|
||||
};
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
updateFill();
|
||||
const v = Number(input.value);
|
||||
onChange?.(v);
|
||||
});
|
||||
|
||||
updateFill();
|
||||
|
||||
row.appendChild(lbl);
|
||||
row.appendChild(input);
|
||||
return row;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export function createLogo({
|
||||
size = 160, // ширина контейнера (px)
|
||||
aspect = 4, // соотношение width / height контейнера
|
||||
offsetY = 0,
|
||||
paddingX = 0,
|
||||
imageUrl = "./assets/logo.svg",
|
||||
} = {}) {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "ue-logo";
|
||||
wrapper.style.width = `${size}px`;
|
||||
wrapper.style.height = `${size / aspect}px`;
|
||||
wrapper.style.marginLeft = `${paddingX}px`;
|
||||
wrapper.style.marginRight = `${paddingX}px`;
|
||||
wrapper.style.transform = `translateY(${offsetY}px)`;
|
||||
const img = document.createElement("img");
|
||||
img.src = imageUrl;
|
||||
img.alt = "logo";
|
||||
img.style.width = "100%";
|
||||
img.style.height = "100%";
|
||||
img.style.objectFit = "contain";
|
||||
img.style.display = "block";
|
||||
wrapper.appendChild(img);
|
||||
return wrapper;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
export function createMeasurementModal({ panel, backdrop, onDelete, onToggleVisibility, onClose }) {
|
||||
let activeMeasurementId = null;
|
||||
let currentVisible = true;
|
||||
|
||||
const hide = () => {
|
||||
if (panel) {
|
||||
panel.classList.add("hidden");
|
||||
panel.style.display = "";
|
||||
}
|
||||
if (backdrop) {
|
||||
backdrop.classList.add("hidden");
|
||||
backdrop.style.display = "";
|
||||
}
|
||||
activeMeasurementId = null;
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
if (!panel) return;
|
||||
panel.innerHTML = "";
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "templates-header";
|
||||
header.innerHTML = `<span class="templates-title">Размер</span>`;
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.className = "templates-close";
|
||||
closeBtn.textContent = "×";
|
||||
closeBtn.addEventListener("click", hide);
|
||||
header.appendChild(closeBtn);
|
||||
panel.appendChild(header);
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "template-list";
|
||||
panel.appendChild(list);
|
||||
|
||||
const deleteBtn = document.createElement("button");
|
||||
deleteBtn.className = "example-card";
|
||||
deleteBtn.textContent = "Удалить размер";
|
||||
deleteBtn.addEventListener("click", () => {
|
||||
if (!activeMeasurementId) return;
|
||||
onDelete?.(activeMeasurementId);
|
||||
});
|
||||
list.appendChild(deleteBtn);
|
||||
|
||||
const toggleBtn = document.createElement("button");
|
||||
toggleBtn.className = "example-card";
|
||||
toggleBtn.textContent = currentVisible ? "Скрыть размер" : "Показать размер";
|
||||
toggleBtn.addEventListener("click", () => {
|
||||
if (!activeMeasurementId) return;
|
||||
const nextVisible = !currentVisible;
|
||||
onToggleVisibility?.(activeMeasurementId, nextVisible);
|
||||
currentVisible = nextVisible;
|
||||
toggleBtn.textContent = currentVisible ? "Скрыть размер" : "Показать размер";
|
||||
});
|
||||
list.appendChild(toggleBtn);
|
||||
};
|
||||
|
||||
const show = ({ measurementId, visible = true } = {}) => {
|
||||
activeMeasurementId = measurementId || null;
|
||||
currentVisible = visible !== false;
|
||||
render();
|
||||
if (panel) {
|
||||
panel.classList.remove("hidden");
|
||||
panel.style.display = "grid";
|
||||
}
|
||||
if (backdrop) {
|
||||
backdrop.classList.remove("hidden");
|
||||
backdrop.style.display = "block";
|
||||
}
|
||||
};
|
||||
|
||||
const setVisibility = (visible) => {
|
||||
currentVisible = !!visible;
|
||||
render();
|
||||
if (panel && !panel.classList.contains("hidden")) {
|
||||
panel.style.display = "grid";
|
||||
}
|
||||
};
|
||||
|
||||
if (backdrop) {
|
||||
backdrop.addEventListener("click", hide);
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
hide,
|
||||
setVisibility,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
export function createObjectModal({ panel, backdrop, onDelete, onClose }) {
|
||||
let activeTarget = null;
|
||||
|
||||
const hide = () => {
|
||||
if (panel) {
|
||||
panel.classList.add("hidden");
|
||||
panel.style.display = "";
|
||||
}
|
||||
if (backdrop) {
|
||||
backdrop.classList.add("hidden");
|
||||
backdrop.style.display = "";
|
||||
}
|
||||
activeTarget = null;
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
if (!panel) return;
|
||||
panel.innerHTML = "";
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "templates-header";
|
||||
header.innerHTML = `<span class="templates-title">Объект</span>`;
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.className = "templates-close";
|
||||
closeBtn.textContent = "×";
|
||||
closeBtn.addEventListener("click", hide);
|
||||
header.appendChild(closeBtn);
|
||||
panel.appendChild(header);
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "template-list";
|
||||
panel.appendChild(list);
|
||||
|
||||
if (activeTarget?.label) {
|
||||
const label = document.createElement("div");
|
||||
label.className = "muted";
|
||||
label.style.margin = "0 0 8px";
|
||||
label.textContent = activeTarget.label;
|
||||
list.appendChild(label);
|
||||
}
|
||||
|
||||
const deleteBtn = document.createElement("button");
|
||||
deleteBtn.className = "example-card danger-btn";
|
||||
deleteBtn.textContent = "Удалить объект";
|
||||
deleteBtn.addEventListener("click", () => {
|
||||
if (!activeTarget?.modelId) return;
|
||||
onDelete?.(activeTarget);
|
||||
});
|
||||
list.appendChild(deleteBtn);
|
||||
};
|
||||
|
||||
const show = ({ modelId, label } = {}) => {
|
||||
activeTarget = { modelId, label };
|
||||
render();
|
||||
if (panel) {
|
||||
panel.classList.remove("hidden");
|
||||
panel.style.display = "grid";
|
||||
}
|
||||
if (backdrop) {
|
||||
backdrop.classList.remove("hidden");
|
||||
backdrop.style.display = "block";
|
||||
}
|
||||
};
|
||||
|
||||
if (backdrop) {
|
||||
backdrop.addEventListener("click", hide);
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
hide,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
import { createSliderControl } from "./controls/slider.js";
|
||||
|
||||
const defaultSettings = {
|
||||
themeVersion: "v3-yellow",
|
||||
bgOuter: "#0f0f0f",
|
||||
canvasTop: "#1c1c1c",
|
||||
canvasBottom: "#1c1c1c",
|
||||
loadingTop: "#1c1c1c",
|
||||
loadingBottom: "#1c1c1c",
|
||||
accent: "#ffea00",
|
||||
accent2: "#ffea00",
|
||||
templateFill: "#1c1c1c",
|
||||
templateOutline: "#233044",
|
||||
templateOutlineHover: "rgba(255,234,0,0.7)",
|
||||
projectBtnFill: "#1c1c1c",
|
||||
projectBtnBorder: "#233044",
|
||||
projectBtnHover: "rgba(255,234,0,0.7)",
|
||||
modalBtnPrimary: "#ffea00",
|
||||
modalBtnSecondary: "#292929",
|
||||
loaderColor: "#ffea00",
|
||||
templateGlow: 80,
|
||||
toolbarBg: "#1c1c1c",
|
||||
toolbarBgActive: "#ffea00",
|
||||
toolbarBorder: "#233044",
|
||||
toolbarOutline: "#ffea00",
|
||||
modalBg: "#1c1c1c",
|
||||
modalBgAlpha: 1,
|
||||
modalFocus: "#292929",
|
||||
modalFocusAlpha: 1,
|
||||
modalBorder: "#292929",
|
||||
modalElement: "#292929",
|
||||
modalElementOutline: "#2b2b2b",
|
||||
modalRadius: "14px",
|
||||
border: "#233044",
|
||||
text: "#e6edf3",
|
||||
panel: "#1c1c1c",
|
||||
panel2: "#292929",
|
||||
highlight: "#ffea00",
|
||||
cubeText: "#242424",
|
||||
cubeEdges: "#e0e7ff",
|
||||
plus: "#ffea00",
|
||||
navCubeBase: "#ffea00",
|
||||
navCubeHover: "#ffffff",
|
||||
navCubeText: "#242424",
|
||||
navCubeEdge: "#e0e7ff",
|
||||
navCubeEdgeAlpha: 0.6,
|
||||
measureLine: "#ffea00",
|
||||
measureLabel: "#ffea00",
|
||||
measureLabelText: "#292929",
|
||||
measureDot: "#ffea00",
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "bimdc-settings-v3";
|
||||
const LEGACY_KEYS = ["bimdc-settings", "bimdc-settings-v2"];
|
||||
const projectSettings = typeof window !== "undefined" && window.__BIMDC_THEME__
|
||||
? { ...defaultSettings, ...window.__BIMDC_THEME__ }
|
||||
: { ...defaultSettings };
|
||||
|
||||
const applySettings = (s) => {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty("--bg-outer", s.bgOuter);
|
||||
root.style.setProperty("--canvas-top", s.canvasTop);
|
||||
root.style.setProperty("--canvas-bottom", s.canvasBottom);
|
||||
root.style.setProperty("--loading-top", s.loadingTop);
|
||||
root.style.setProperty("--loading-bottom", s.loadingBottom);
|
||||
root.style.setProperty("--accent", s.accent);
|
||||
root.style.setProperty("--accent-2", s.accent2);
|
||||
root.style.setProperty("--template-fill", s.templateFill);
|
||||
root.style.setProperty("--template-outline", s.templateOutline);
|
||||
root.style.setProperty("--template-outline-hover", s.templateOutlineHover);
|
||||
root.style.setProperty("--project-btn-fill", s.projectBtnFill);
|
||||
root.style.setProperty("--project-btn-border", s.projectBtnBorder);
|
||||
root.style.setProperty("--project-btn-hover", s.projectBtnHover);
|
||||
root.style.setProperty("--modal-btn-primary", s.modalBtnPrimary);
|
||||
root.style.setProperty("--modal-btn-secondary", s.modalBtnSecondary);
|
||||
root.style.setProperty("--loader-color", s.loaderColor);
|
||||
root.style.setProperty("--template-glow-strength", `${s.templateGlow}%`);
|
||||
root.style.setProperty("--toolbar-bg", s.toolbarBg);
|
||||
root.style.setProperty("--toolbar-bg-active", s.toolbarBgActive);
|
||||
root.style.setProperty("--toolbar-border", s.toolbarBorder);
|
||||
root.style.setProperty("--toolbar-outline", s.toolbarOutline);
|
||||
const bgAlphaPct = `${(s.modalBgAlpha ?? 1) * 100}%`;
|
||||
const focusAlphaPct = `${(s.modalFocusAlpha ?? 1) * 100}%`;
|
||||
root.style.setProperty("--modal-bg", s.modalBg);
|
||||
root.style.setProperty("--modal-bg-alpha", bgAlphaPct);
|
||||
root.style.setProperty("--modal-focus", s.modalFocus);
|
||||
root.style.setProperty("--modal-focus-alpha", focusAlphaPct);
|
||||
root.style.setProperty("--modal-border", s.modalBorder);
|
||||
root.style.setProperty("--modal-element", s.modalElement);
|
||||
root.style.setProperty("--modal-element-outline", s.modalElementOutline);
|
||||
root.style.setProperty("--modal-radius", s.modalRadius);
|
||||
root.style.setProperty("--border", s.border);
|
||||
root.style.setProperty("--text", s.text);
|
||||
root.style.setProperty("--panel", s.panel);
|
||||
root.style.setProperty("--panel-2", s.panel2);
|
||||
root.style.setProperty("--highlight-color", s.highlight);
|
||||
root.style.setProperty("--cube-text", s.cubeText);
|
||||
root.style.setProperty("--cube-edges", s.navCubeBase || s.cubeEdges);
|
||||
root.style.setProperty("--cube-base", s.navCubeBase || s.cubeEdges);
|
||||
root.style.setProperty("--cube-hover", s.navCubeHover);
|
||||
root.style.setProperty("--cube-text", s.navCubeText || s.cubeText);
|
||||
root.style.setProperty("--cube-edge-color", s.navCubeEdge);
|
||||
root.style.setProperty("--cube-edge-alpha", s.navCubeEdgeAlpha);
|
||||
root.style.setProperty("--plus-color", s.plus);
|
||||
root.style.setProperty("--template-fill", s.templateFill);
|
||||
root.style.setProperty("--template-outline", s.templateOutline);
|
||||
root.style.setProperty("--measure-line", s.measureLine);
|
||||
root.style.setProperty("--measure-label", s.measureLabel);
|
||||
root.style.setProperty("--measure-label-text", s.measureLabelText);
|
||||
root.style.setProperty("--measure-dot", s.measureDot);
|
||||
// подсветка объектов
|
||||
root.style.setProperty("--highlight-color", s.highlight);
|
||||
};
|
||||
|
||||
const saveSettings = (s) => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const loadSettings = () => {
|
||||
try {
|
||||
// purge legacy keys so старые палитры не мешали
|
||||
LEGACY_KEYS.forEach((k) => localStorage.removeItem(k));
|
||||
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) {
|
||||
// зашиваем проектные настройки обратно в storage, чтобы при чистом кэше они сразу подхватывались
|
||||
saveSettings(projectSettings);
|
||||
return { ...projectSettings };
|
||||
}
|
||||
const stored = JSON.parse(raw);
|
||||
if (stored.themeVersion && stored.themeVersion !== projectSettings.themeVersion) {
|
||||
saveSettings(projectSettings);
|
||||
return { ...projectSettings };
|
||||
}
|
||||
return { ...projectSettings, ...stored };
|
||||
} catch (e) {
|
||||
saveSettings(projectSettings);
|
||||
return { ...projectSettings };
|
||||
}
|
||||
};
|
||||
|
||||
export function createSettingsMenu({ panel, controls = {}, onChange = null, onShow, onHide }) {
|
||||
let state = loadSettings();
|
||||
applySettings(state);
|
||||
|
||||
const getState = () => ({ ...state });
|
||||
|
||||
const applyStateExternal = (nextState = {}) => {
|
||||
state = { ...state, ...nextState };
|
||||
applySettings(state);
|
||||
saveSettings(state);
|
||||
if (typeof onChange === "function") {
|
||||
onChange({ key: null, state });
|
||||
}
|
||||
if (panel && !panel.classList.contains("hidden")) {
|
||||
render();
|
||||
}
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
if (panel) {
|
||||
panel.classList.add("hidden");
|
||||
panel.style.display = "";
|
||||
}
|
||||
onHide?.();
|
||||
};
|
||||
|
||||
const setVal = (key, val) => {
|
||||
state = { ...state, [key]: val };
|
||||
applySettings(state);
|
||||
saveSettings(state);
|
||||
if (typeof onChange === "function") {
|
||||
onChange({ key, value: val, state });
|
||||
}
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
if (!panel) return;
|
||||
panel.innerHTML = "";
|
||||
const header = document.createElement("div");
|
||||
header.className = "settings-header";
|
||||
header.innerHTML = `<span class="settings-title">Настройки</span>`;
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.className = "settings-close";
|
||||
closeBtn.textContent = "—";
|
||||
closeBtn.addEventListener("click", hide);
|
||||
header.appendChild(closeBtn);
|
||||
panel.appendChild(header);
|
||||
|
||||
const section = (title) => {
|
||||
const box = document.createElement("div");
|
||||
box.className = "settings-section";
|
||||
const t = document.createElement("div");
|
||||
t.className = "section-title";
|
||||
t.textContent = title;
|
||||
box.appendChild(t);
|
||||
return box;
|
||||
};
|
||||
|
||||
const addColor = (parent, label, key) => {
|
||||
const isHex = (val) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test((val || "").trim());
|
||||
const row = document.createElement("div");
|
||||
row.className = "color-row";
|
||||
const lbl = document.createElement("label");
|
||||
lbl.textContent = label;
|
||||
const input = document.createElement("input");
|
||||
input.type = "color";
|
||||
input.value = isHex(state[key]) ? state[key] : "#ffffff";
|
||||
const hex = document.createElement("input");
|
||||
hex.type = "text";
|
||||
hex.value = state[key];
|
||||
input.addEventListener("change", (e) => {
|
||||
hex.value = e.target.value;
|
||||
setVal(key, e.target.value);
|
||||
});
|
||||
hex.addEventListener("change", (e) => {
|
||||
input.value = e.target.value;
|
||||
setVal(key, e.target.value);
|
||||
});
|
||||
row.appendChild(lbl);
|
||||
row.appendChild(input);
|
||||
row.appendChild(hex);
|
||||
parent.appendChild(row);
|
||||
};
|
||||
|
||||
const font = section("Фон");
|
||||
addColor(font, "Фон внешний", "bgOuter");
|
||||
addColor(font, "Фон холста верх", "canvasTop");
|
||||
addColor(font, "Фон холста низ", "canvasBottom");
|
||||
addColor(font, "Фон загрузки верх", "loadingTop");
|
||||
addColor(font, "Фон загрузки низ", "loadingBottom");
|
||||
panel.appendChild(font);
|
||||
|
||||
const buttons = section("Кнопки");
|
||||
addColor(buttons, "Плюс", "plus");
|
||||
addColor(buttons, "Кнопки (заливка)", "templateFill");
|
||||
addColor(buttons, "Кнопки (бордер)", "templateOutline");
|
||||
addColor(buttons, "Кнопки (outline hover)", "templateOutlineHover");
|
||||
const brightnessSlider = createSliderControl({
|
||||
label: "Яркость подсветки",
|
||||
value: Number(state.templateGlow ?? 80),
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
onChange: (val) => setVal("templateGlow", val),
|
||||
});
|
||||
buttons.appendChild(brightnessSlider);
|
||||
panel.appendChild(buttons);
|
||||
|
||||
const projectButtons = section("Кнопки проектов");
|
||||
addColor(projectButtons, "Проекты (заливка)", "projectBtnFill");
|
||||
addColor(projectButtons, "Проекты (бордер)", "projectBtnBorder");
|
||||
addColor(projectButtons, "Проекты (hover)", "projectBtnHover");
|
||||
panel.appendChild(projectButtons);
|
||||
|
||||
const modalButtons = section("Кнопки модалок");
|
||||
addColor(modalButtons, "Модальная primary", "modalBtnPrimary");
|
||||
addColor(modalButtons, "Модальная secondary", "modalBtnSecondary");
|
||||
addColor(modalButtons, "Лоудер", "loaderColor");
|
||||
panel.appendChild(modalButtons);
|
||||
|
||||
const toolbar = section("Toolbar");
|
||||
addColor(toolbar, "Фон", "toolbarBg");
|
||||
addColor(toolbar, "Актив", "toolbarBgActive");
|
||||
addColor(toolbar, "Бордер", "toolbarBorder");
|
||||
addColor(toolbar, "Outline", "toolbarOutline");
|
||||
panel.appendChild(toolbar);
|
||||
|
||||
const navCube = section("NavCube");
|
||||
addColor(navCube, "Грани", "navCubeBase");
|
||||
addColor(navCube, "Hover", "navCubeHover");
|
||||
addColor(navCube, "Текст", "navCubeText");
|
||||
addColor(navCube, "Рёбра", "navCubeEdge");
|
||||
const edgeAlphaRow = document.createElement("div");
|
||||
edgeAlphaRow.className = "color-row";
|
||||
const edgeAlphaLbl = document.createElement("label");
|
||||
edgeAlphaLbl.textContent = "Прозрачность рёбер (0-1)";
|
||||
const edgeAlphaInput = document.createElement("input");
|
||||
edgeAlphaInput.type = "number";
|
||||
edgeAlphaInput.step = "0.05";
|
||||
edgeAlphaInput.min = "0";
|
||||
edgeAlphaInput.max = "1";
|
||||
edgeAlphaInput.value = state.navCubeEdgeAlpha;
|
||||
edgeAlphaInput.addEventListener("change", () => {
|
||||
const v = Math.min(1, Math.max(0, parseFloat(edgeAlphaInput.value) || 0));
|
||||
edgeAlphaInput.value = v;
|
||||
setVal("navCubeEdgeAlpha", v);
|
||||
});
|
||||
edgeAlphaRow.appendChild(edgeAlphaLbl);
|
||||
edgeAlphaRow.appendChild(edgeAlphaInput);
|
||||
edgeAlphaRow.appendChild(document.createElement("div"));
|
||||
navCube.appendChild(edgeAlphaRow);
|
||||
panel.appendChild(navCube);
|
||||
|
||||
const measure = section("Линейка");
|
||||
addColor(measure, "Линия/бордер", "measureLine");
|
||||
addColor(measure, "Плашка", "measureLabel");
|
||||
addColor(measure, "Текст плашки", "measureLabelText");
|
||||
addColor(measure, "Кружок", "measureDot");
|
||||
panel.appendChild(measure);
|
||||
|
||||
const viewport = section("Viewport");
|
||||
const addCheck = (label, checked, handler) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "checkbox-row";
|
||||
const chk = document.createElement("input");
|
||||
chk.type = "checkbox";
|
||||
chk.checked = checked;
|
||||
chk.addEventListener("change", () => handler(chk.checked));
|
||||
const lbl = document.createElement("label");
|
||||
lbl.textContent = label;
|
||||
row.appendChild(chk);
|
||||
row.appendChild(lbl);
|
||||
viewport.appendChild(row);
|
||||
};
|
||||
if (controls.toggleEdges) addCheck("Рёбра", controls.toggleEdges.checked, (v) => { controls.toggleEdges.checked = v; controls.toggleEdges.dispatchEvent(new Event("change")); });
|
||||
if (controls.toggleTransparent) addCheck("Прозрачный фон", controls.toggleTransparent.checked, (v) => { controls.toggleTransparent.checked = v; controls.toggleTransparent.dispatchEvent(new Event("change")); });
|
||||
if (controls.addMode) addCheck("Добавлять в сцену", controls.addMode.checked, (v) => { controls.addMode.checked = v; });
|
||||
panel.appendChild(viewport);
|
||||
|
||||
const modals = section("Модальные окна");
|
||||
addColor(modals, "Основной фон", "modalBg");
|
||||
addColor(modals, "Фокусный фон", "modalFocus");
|
||||
const modalBgAlphaRow = createSliderControl({
|
||||
label: "Прозрачность основного фона",
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
value: parseFloat(state.modalBgAlpha ?? 1),
|
||||
onChange: (val) => {
|
||||
const num = Math.max(0, Math.min(1, parseFloat(val)));
|
||||
setVal("modalBgAlpha", num);
|
||||
document.documentElement.style.setProperty("--modal-bg-alpha", `${num * 100}%`);
|
||||
},
|
||||
});
|
||||
modals.appendChild(modalBgAlphaRow);
|
||||
|
||||
const modalFocusAlphaRow = createSliderControl({
|
||||
label: "Прозрачность фокусного фона",
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
value: parseFloat(state.modalFocusAlpha ?? 1),
|
||||
onChange: (val) => {
|
||||
const num = Math.max(0, Math.min(1, parseFloat(val)));
|
||||
setVal("modalFocusAlpha", num);
|
||||
document.documentElement.style.setProperty("--modal-focus-alpha", `${num * 100}%`);
|
||||
},
|
||||
});
|
||||
modals.appendChild(modalFocusAlphaRow);
|
||||
|
||||
addColor(modals, "Аутлайн", "modalBorder");
|
||||
addColor(modals, "Фон элемента", "modalElement");
|
||||
addColor(modals, "Аутлайн элемента", "modalElementOutline");
|
||||
const radiusRow = document.createElement("div");
|
||||
radiusRow.className = "color-row";
|
||||
const radiusLbl = document.createElement("label");
|
||||
radiusLbl.textContent = "Радиус";
|
||||
const radiusInput = document.createElement("input");
|
||||
radiusInput.type = "number";
|
||||
radiusInput.value = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--modal-radius")) || 14;
|
||||
radiusInput.addEventListener("change", () => {
|
||||
const val = `${radiusInput.value}px`;
|
||||
document.documentElement.style.setProperty("--modal-radius", val);
|
||||
setVal("modalRadius", val);
|
||||
});
|
||||
radiusRow.appendChild(radiusLbl);
|
||||
radiusRow.appendChild(radiusInput);
|
||||
radiusRow.appendChild(document.createElement("div"));
|
||||
modals.appendChild(radiusRow);
|
||||
panel.appendChild(modals);
|
||||
|
||||
const cube = section("Выделение объектов");
|
||||
addColor(cube, "Подсветка объектов", "highlight");
|
||||
panel.appendChild(cube);
|
||||
};
|
||||
|
||||
const show = () => {
|
||||
render();
|
||||
if (panel) {
|
||||
panel.classList.remove("hidden");
|
||||
panel.style.display = "grid";
|
||||
}
|
||||
onShow?.();
|
||||
};
|
||||
|
||||
const toggle = () => {
|
||||
if (!panel) return;
|
||||
if (panel.classList.contains("hidden")) {
|
||||
show();
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
};
|
||||
|
||||
return { render, show, toggle, hide, getState, applyState: applyStateExternal };
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
export function createTemplatesMenu({ panel, fetchProjects, openProject, onShow, onHide }) {
|
||||
let state = { templates: [], projects: [], loading: false, error: null };
|
||||
|
||||
const setState = (next) => {
|
||||
state = { ...state, ...next };
|
||||
render();
|
||||
};
|
||||
|
||||
const renderEmpty = (parent, text) => {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "templates-empty";
|
||||
empty.textContent = text;
|
||||
parent.appendChild(empty);
|
||||
};
|
||||
|
||||
const renderSection = (title, items, emptyText) => {
|
||||
const section = document.createElement("div");
|
||||
section.className = "templates-section";
|
||||
const subtitle = document.createElement("div");
|
||||
subtitle.className = "templates-subtitle";
|
||||
subtitle.textContent = title;
|
||||
section.appendChild(subtitle);
|
||||
if (!items || !items.length) {
|
||||
renderEmpty(section, emptyText);
|
||||
} else {
|
||||
items.forEach((item) => {
|
||||
const card = document.createElement("button");
|
||||
card.className = "example-card template-card";
|
||||
card.type = "button";
|
||||
card.addEventListener("click", () => openProject?.(item.id, item.type));
|
||||
|
||||
const titleEl = document.createElement("div");
|
||||
titleEl.className = "card-title";
|
||||
titleEl.textContent = item.name || item.id;
|
||||
card.appendChild(titleEl);
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "card-meta";
|
||||
const typePill = document.createElement("span");
|
||||
typePill.className = "card-pill";
|
||||
typePill.dataset.kind = item.type || "project";
|
||||
typePill.textContent = item.type === "template" ? "Шаблон" : "Проект";
|
||||
meta.appendChild(typePill);
|
||||
if (item.updatedAt) {
|
||||
const updated = document.createElement("span");
|
||||
updated.textContent = new Date(item.updatedAt).toLocaleDateString();
|
||||
meta.appendChild(updated);
|
||||
}
|
||||
card.appendChild(meta);
|
||||
|
||||
section.appendChild(card);
|
||||
});
|
||||
}
|
||||
return section;
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
if (!panel) return;
|
||||
panel.innerHTML = "";
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "templates-header";
|
||||
header.innerHTML = `<span class="templates-title">Проекты и шаблоны</span>`;
|
||||
const actions = document.createElement("div");
|
||||
actions.style.display = "flex";
|
||||
actions.style.gap = "6px";
|
||||
const refresh = document.createElement("button");
|
||||
refresh.className = "templates-close";
|
||||
refresh.title = "Обновить список";
|
||||
refresh.textContent = "↻";
|
||||
refresh.addEventListener("click", () => load());
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.className = "templates-close";
|
||||
closeBtn.textContent = "×";
|
||||
closeBtn.addEventListener("click", hide);
|
||||
actions.appendChild(refresh);
|
||||
actions.appendChild(closeBtn);
|
||||
header.appendChild(actions);
|
||||
panel.appendChild(header);
|
||||
|
||||
if (state.loading) {
|
||||
const loadingEl = document.createElement("div");
|
||||
loadingEl.className = "templates-empty";
|
||||
loadingEl.textContent = "Загрузка списка проектов...";
|
||||
panel.appendChild(loadingEl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.error) {
|
||||
const errorEl = document.createElement("div");
|
||||
errorEl.className = "templates-empty";
|
||||
errorEl.style.color = "var(--error)";
|
||||
errorEl.textContent = state.error;
|
||||
panel.appendChild(errorEl);
|
||||
}
|
||||
|
||||
panel.appendChild(renderSection("Шаблоны", state.templates, "Нет шаблонов"));
|
||||
panel.appendChild(renderSection("Мои проекты", state.projects, "Нет сохранённых проектов"));
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
if (!fetchProjects) return;
|
||||
setState({ loading: true, error: null });
|
||||
try {
|
||||
const data = await fetchProjects();
|
||||
setState({
|
||||
loading: false,
|
||||
templates: data?.templates || [],
|
||||
projects: data?.projects || [],
|
||||
error: null
|
||||
});
|
||||
} catch (err) {
|
||||
setState({ loading: false, error: err?.message || "Не удалось загрузить проекты" });
|
||||
}
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
if (panel) {
|
||||
panel.classList.add("hidden");
|
||||
panel.style.display = "";
|
||||
}
|
||||
onHide?.();
|
||||
};
|
||||
|
||||
const show = () => {
|
||||
render();
|
||||
if (panel) {
|
||||
panel.classList.remove("hidden");
|
||||
panel.style.display = "grid";
|
||||
}
|
||||
load();
|
||||
onShow?.();
|
||||
};
|
||||
|
||||
const toggle = () => {
|
||||
if (!panel) return;
|
||||
if (panel.classList.contains("hidden")) {
|
||||
show();
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
};
|
||||
|
||||
const setData = (data = {}) => {
|
||||
setState({
|
||||
templates: data.templates || [],
|
||||
projects: data.projects || [],
|
||||
loading: false,
|
||||
error: data.error || null
|
||||
});
|
||||
};
|
||||
|
||||
return { render, show, hide, toggle, reload: load, setData };
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
const toolbarConfig = {
|
||||
size: 25,
|
||||
gap: 10,
|
||||
radius: 7,
|
||||
paddingY: 6,
|
||||
iconPadding: 5,
|
||||
iconSize: 13,
|
||||
};
|
||||
|
||||
const applyToolbarVars = () => {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty("--tb-size", `${toolbarConfig.size}px`);
|
||||
root.style.setProperty("--tb-gap", `${toolbarConfig.gap}px`);
|
||||
root.style.setProperty("--tb-radius", `${toolbarConfig.radius}px`);
|
||||
root.style.setProperty("--tb-pad-y", `${toolbarConfig.paddingY}px`);
|
||||
root.style.setProperty("--tb-icon-pad", `${toolbarConfig.iconPadding}px`);
|
||||
root.style.setProperty("--tb-icon-size", `${toolbarConfig.iconSize}px`);
|
||||
};
|
||||
|
||||
export { toolbarConfig, applyToolbarVars };
|
||||
Reference in New Issue
Block a user