(() => { const panel = document.querySelector("#dashboard3-panel"); const blocksContainer = document.querySelector("#dashboard3-blocks"); const settingsToggle = document.querySelector(".dashboard3-settings-toggle"); const globalToggle = document.querySelector(".dashboard3-global-toggle"); const panelTitleHeading = document.querySelector("#dashboard5-panel-title"); const dashboardBody = document.querySelector(".dashboard-body"); const spaceTabs = [...document.querySelectorAll(".dashboard3-space-tab")]; const settingsDialog = document.querySelector("#dashboard3-settings-dialog"); const settingsForm = settingsDialog?.querySelector(".dashboard3-settings-dialog__window"); const settingsClose = settingsDialog?.querySelector(".dashboard3-settings-dialog__close"); const settingsCancel = settingsDialog?.querySelector(".dashboard3-settings-cancel"); const settingsSave = settingsDialog?.querySelector(".dashboard3-settings-save"); const settingsStatus = document.querySelector("#dashboard3-settings-status"); const panelTitleInput = document.querySelector("#dashboard3-panel-title"); const panelTitleError = panelTitleInput?.parentElement.querySelector(".dashboard3-field__error"); const showMobileInput = document.querySelector("#dashboard3-show-mobile"); const showPanelInput = document.querySelector("#dashboard5-show-panel"); const editorList = document.querySelector("#dashboard3-block-editor-list"); const blockCount = document.querySelector("#dashboard3-block-count"); const addBlockButton = document.querySelector(".dashboard3-add-block"); const entityPicker = document.querySelector("#dashboard3-entity-picker"); const entitySearch = document.querySelector("#dashboard3-entity-search"); const entityList = document.querySelector("#dashboard3-entity-list"); const sizeToggle = document.querySelector(".dashboard4-size-toggle"); const themeToggle = document.querySelector(".dashboard4-theme-toggle"); const kioskToggle = document.querySelector(".dashboard4-kiosk-toggle"); const sizeDialog = document.querySelector("#dashboard4-size-dialog"); const sizeForm = sizeDialog?.querySelector(".dashboard4-size-dialog__window"); const sizeClose = sizeDialog?.querySelector(".dashboard4-size-close"); const sizeReset = sizeDialog?.querySelector(".dashboard4-size-reset"); const iconSizeInput = document.querySelector("#dashboard4-icon-size"); const textSizeInput = document.querySelector("#dashboard4-text-size"); const iconSizeOutput = document.querySelector("#dashboard4-icon-size-output"); const textSizeOutput = document.querySelector("#dashboard4-text-size-output"); if (!panel || !blocksContainer || !settingsToggle || !globalToggle || !panelTitleHeading || !dashboardBody || !spaceTabs.length || !settingsDialog || !settingsForm || !settingsClose || !settingsCancel || !settingsSave || !settingsStatus || !panelTitleInput || !panelTitleError || !showMobileInput || !showPanelInput || !editorList || !blockCount || !addBlockButton || !entityPicker || !entitySearch || !entityList || !sizeToggle || !themeToggle || !kioskToggle || !sizeDialog || !sizeForm || !sizeClose || !sizeReset || !iconSizeInput || !textSizeInput || !iconSizeOutput || !textSizeOutput) return; const limits = { blocks: 10, values: 20 }; const storageKeys = { config: "houseplan-dashboard5-config", enabled: "houseplan-dashboard5-enabled", theme: "houseplan-dashboard5-theme", kiosk: "houseplan-dashboard5-kiosk", sizes: "houseplan-dashboard5-local-sizes" }; const spaces = [ { id: "ground-floor", name: "Ground Floor" }, { id: "first-floor", name: "First Floor" }, { id: "yard", name: "Yard" } ]; const entities = [ { entityId: "sensor.kitchen_temperature", name: "Kitchen temperature", state: "20,6 °C" }, { entityId: "sensor.living_room_humidity", name: "Living room humidity", state: "42 %" }, { entityId: "light.wall_lamp_3", name: "Wall Lamp 3", state: "Выключено" }, { entityId: "light.terrace_lights", name: "Terrace lights", state: "Включено" }, { entityId: "climate.ground_floor", name: "Ground floor climate", state: "Нагрев · 21 °C" }, { entityId: "sensor.ground_floor_average_temperature", name: "Средняя температура первого этажа", state: "20,9 °C", template: true }, { entityId: "sensor.outdoor_temperature", name: "Outdoor temperature", state: "12,4 °C" }, { entityId: "sensor.outdoor_humidity", name: "Outdoor humidity", state: "Загрузка…", status: "loading" }, { entityId: "binary_sensor.front_door", name: "Front door", state: "Закрыто" }, { entityId: "binary_sensor.smoke_kitchen", name: "Kitchen smoke detector", state: "Норма" }, { entityId: "switch.sauna_heater", name: "Sauna heater", state: "Выключено" }, { entityId: "sensor.sauna_temperature", name: "Sauna temperature", state: "24,1 °C" }, { entityId: "sensor.boiler_temperature", name: "Boiler temperature", state: "54,0 °C" }, { entityId: "binary_sensor.boiler_water_leak", name: "Boiler water leak", state: "Норма" }, { entityId: "sensor.washing_machine_progress", name: "Washing machine progress", state: "66 %" }, { entityId: "sensor.washing_machine_state", name: "Washing machine state", state: "Rinsing" }, { entityId: "media_player.kitchen_display", name: "Kitchen display", state: "Ожидание" }, { entityId: "camera.entrance", name: "Entrance camera", state: "Недоступно", status: "unavailable" }, { entityId: "fan.kitchen_hood", name: "Kitchen hood", state: "Выключено" }, { entityId: "cover.guest_bedroom_blinds", name: "Guest bedroom blinds", state: "Открыто" }, { entityId: "sensor.house_energy_today", name: "House energy today", state: "8,7 kWh" }, { entityId: "vacuum.ground_floor", name: "Ground floor vacuum", state: "На базе" }, { entityId: "person.demo", name: "Demo", state: "Дома" }, { entityId: "weather.home", name: "Home weather", state: "Облачно · 13 °C", status: "stale" } ]; let blockSequence = 1; let valueSequence = 3; let activeSpaceId = "ground-floor"; let globalEnabled = false; let config = { title: "Сводная информация", showOnMobile: true, blocks: [ { id: "block-1", title: "Общее", visible: true, scope: { type: "all", spaceId: null }, values: [ { id: "value-1", label: "Количество устройств", source: { type: "system", key: "device_count", name: "Количество устройств" } }, { id: "value-2", label: "Общая площадь комнат", source: { type: "system", key: "room_area", name: "Общая площадь комнат" } }, { id: "value-3", label: "Текущие дата и время", source: { type: "system", key: "date_time", name: "Текущие дата и время" } } ] } ] }; let draft = null; let baseline = null; let draftEnabled = false; let baselineEnabled = false; let dragContext = null; let pickerContext = null; let currentTheme = "light"; let kioskEnabled = false; let localSizes = { icons: 100, text: 100 }; try { const storedConfig = window.localStorage.getItem(storageKeys.config); if (storedConfig) config = JSON.parse(storedConfig); globalEnabled = window.localStorage.getItem(storageKeys.enabled) === "true"; currentTheme = window.localStorage.getItem(storageKeys.theme) || "light"; kioskEnabled = window.localStorage.getItem(storageKeys.kiosk) === "true"; const storedSizes = window.localStorage.getItem(storageKeys.sizes); if (storedSizes) localSizes = { ...localSizes, ...JSON.parse(storedSizes) }; } catch (error) { console.warn("Dashboard 5: сохранённая конфигурация недоступна", error); } blockSequence = Math.max(1, config.blocks.length); valueSequence = Math.max(3, ...config.blocks.flatMap((block) => block.values.map((value) => Number(String(value.id).match(/\d+/)?.[0]) || 0))); const persist = (key, value) => { try { window.localStorage.setItem(key, typeof value === "string" ? value : JSON.stringify(value)); } catch (error) { console.warn("Dashboard 5: не удалось сохранить настройку", error); } }; const applyTheme = () => { document.documentElement.dataset.theme = currentTheme; themeToggle.setAttribute("aria-label", currentTheme === "dark" ? "Включить светлую тему" : "Включить тёмную тему"); }; const applyKiosk = () => { document.body.classList.toggle("dashboard5-kiosk", kioskEnabled); kioskToggle.setAttribute("aria-pressed", String(kioskEnabled)); kioskToggle.textContent = kioskEnabled ? "Выйти" : "Киоск"; }; const applyLocalSizes = () => { document.documentElement.style.setProperty("--dashboard4-device-scale", String(localSizes.icons / 100)); document.documentElement.style.setProperty("--dashboard4-room-text-scale", String(localSizes.text / 100)); iconSizeInput.value = String(localSizes.icons); textSizeInput.value = String(localSizes.text); iconSizeOutput.textContent = `${localSizes.icons}%`; textSizeOutput.textContent = `${localSizes.text}%`; }; const clone = (value) => JSON.parse(JSON.stringify(value)); const fingerprint = (value) => JSON.stringify(value); const escapeHtml = (value) => String(value).replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]); const currentSpace = () => spaces.find((space) => space.id === activeSpaceId) || spaces[0]; const findDraftBlock = (blockId) => draft?.blocks.find((block) => block.id === blockId); const findDraftValue = (blockId, valueId) => findDraftBlock(blockId)?.values.find((value) => value.id === valueId); const isNarrowViewport = () => { if (typeof window.hass?.narrow === "boolean") return window.hass.narrow; return document.documentElement.dataset.haNarrow === "true"; }; const formatDateTime = () => new Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }).format(new Date()).replace(".", ""); const entitySnapshot = (entityId) => { const hassState = window.hass?.states?.[entityId]; if (hassState) { const rawState = hassState.state; const status = rawState === "unavailable" || rawState === "unknown" ? "unavailable" : hassState.attributes?.restored ? "stale" : "ready"; let text; try { text = typeof window.hass.formatEntityState === "function" ? window.hass.formatEntityState(hassState) : rawState; } catch { text = rawState; } return { text: text || "Нет данных", status }; } const demoEntity = entities.find((item) => item.entityId === entityId); if (!demoEntity) return { text: "Недоступно", status: "unavailable" }; return { text: demoEntity.state || "Нет данных", status: demoEntity.status || "ready" }; }; const resolveValue = (value) => { if (value.source.type === "system") { if (value.source.key === "device_count") return { text: "50", status: "ready" }; if (value.source.key === "room_area") return { text: "156 м²", status: "ready" }; if (value.source.key === "date_time") return { text: formatDateTime(), status: "ready" }; } return entitySnapshot(value.source.entityId); }; const sourcePresentation = (source) => { if (!source) return { name: "Выберите сущность", detail: "Поиск по всем сущностям HA" }; if (source.type === "system") return { name: source.name, detail: "Системное значение" }; const entity = entities.find((item) => item.entityId === source.entityId); if (!entity) return { name: "Недоступная сущность", detail: `${source.entityId || "Не выбрано"} · Требует внимания` }; const status = entity.status === "unavailable" ? " · Недоступно" : entity.status === "stale" ? " · Данные устарели" : entity.status === "loading" ? " · Загрузка" : ""; return { name: entity.name, detail: `${entity.entityId}${status}` }; }; const blockMatchesSpace = (block) => block.scope.type === "all" || block.scope.spaceId === activeSpaceId; const positionPanel = () => { const bottom = window.matchMedia("(orientation: portrait)").matches; panel.classList.toggle("dashboard5-panel-bottom", bottom); panel.dataset.position = bottom ? "bottom" : "right"; }; const renderGlobalState = () => { const hiddenByMobile = isNarrowViewport() && !config.showOnMobile; const visible = globalEnabled && !hiddenByMobile; panel.hidden = false; panel.classList.toggle("is-visible", visible); panel.setAttribute("aria-hidden", String(!visible)); globalToggle.setAttribute("aria-pressed", String(globalEnabled)); globalToggle.title = globalEnabled ? "Скрыть информационную панель" : "Показать информационную панель"; globalToggle.setAttribute("aria-label", globalToggle.title); }; const renderPanel = () => { const title = config.title.trim() || "Сводная информация"; panelTitleHeading.textContent = title; panel.setAttribute("aria-label", title); positionPanel(); renderGlobalState(); blocksContainer.replaceChildren(); const displayedBlocks = config.blocks.filter((block) => block.visible && blockMatchesSpace(block)); if (!displayedBlocks.length) { const empty = document.createElement("div"); empty.className = "dashboard3-empty"; empty.textContent = `Для пространства «${currentSpace().name}» нет настроенных блоков.`; blocksContainer.append(empty); return; } displayedBlocks.forEach((block) => { const valuesHtml = block.values.map((value) => { const resolved = resolveValue(value); const statusLabel = resolved.status === "loading" ? "Загрузка" : resolved.status === "stale" ? "Данные устарели" : resolved.status === "unavailable" ? "Источник недоступен" : ""; return `
${escapeHtml(value.label)}
${escapeHtml(resolved.text)}${statusLabel ? `${escapeHtml(statusLabel)}` : ""}
`; }).join(""); const card = document.createElement("section"); card.className = "dashboard3-block"; card.innerHTML = `

${escapeHtml(block.title)}

${valuesHtml || '
В блоке пока нет значений.
'}
`; blocksContainer.append(card); }); }; const validateDraft = () => { const errors = []; if (!draft?.title.trim()) errors.push({ type: "panel-title" }); draft?.blocks.forEach((block) => { if (!block.title.trim()) errors.push({ type: "block-title", blockId: block.id }); if (block.scope.type === "space" && !block.scope.spaceId) errors.push({ type: "space", blockId: block.id }); block.values.forEach((value) => { const missing = []; if (!value.label.trim()) missing.push("название"); if (!value.source) missing.push("сущность"); if (missing.length) errors.push({ type: "value", blockId: block.id, valueId: value.id, missing }); }); }); return errors; }; const updateDialogState = () => { if (!draft || !baseline) return; const errors = validateDraft(); const dirty = fingerprint(draft) !== fingerprint(baseline) || draftEnabled !== baselineEnabled; settingsSave.hidden = !dirty; settingsSave.disabled = errors.length > 0; panelTitleInput.classList.toggle("is-invalid", !draft.title.trim()); panelTitleError.textContent = draft.title.trim() ? "" : "Введите название панели"; editorList.querySelectorAll(".dashboard3-editor-block").forEach((element) => { const blockId = element.dataset.blockId; const block = findDraftBlock(blockId); if (!block) return; element.querySelector(".dashboard3-block-title").classList.toggle("is-invalid", !block.title.trim()); const scopeSelect = element.querySelector(".dashboard4-space-select"); const invalidScope = block.scope.type === "space" && !block.scope.spaceId; if (scopeSelect) scopeSelect.classList.toggle("is-invalid", invalidScope); element.querySelectorAll(".dashboard3-editor-value").forEach((rowElement) => { const value = findDraftValue(blockId, rowElement.dataset.valueId); if (!value) return; const missing = []; if (!value.label.trim()) missing.push("название"); if (!value.source) missing.push("сущность"); rowElement.classList.toggle("has-error", missing.length > 0); rowElement.querySelector(".dashboard3-value-label").classList.toggle("is-invalid", !value.label.trim()); rowElement.querySelector(".dashboard3-entity-trigger").classList.toggle("is-invalid", !value.source); rowElement.querySelector(".dashboard3-editor-value__error").textContent = missing.length ? `Заполните: ${missing.join(" и ")}` : ""; }); }); settingsStatus.textContent = errors.length ? `Нельзя сохранить: незаполненных полей — ${errors.length}` : dirty ? "Есть несохранённые изменения" : ""; settingsStatus.classList.toggle("is-ready", dirty && !errors.length); }; const moveItemByOffset = (items, itemId, offset) => { const sourceIndex = items.findIndex((item) => item.id === itemId); const targetIndex = sourceIndex + offset; if (sourceIndex < 0 || targetIndex < 0 || targetIndex >= items.length) return false; [items[sourceIndex], items[targetIndex]] = [items[targetIndex], items[sourceIndex]]; return true; }; const moveItem = (items, sourceId, targetId, placeAfter) => { const sourceIndex = items.findIndex((item) => item.id === sourceId); if (sourceIndex < 0) return; const [moved] = items.splice(sourceIndex, 1); const targetIndex = items.findIndex((item) => item.id === targetId); if (targetIndex < 0) items.push(moved); else items.splice(targetIndex + (placeAfter ? 1 : 0), 0, moved); }; const restoreOrderFocus = (selector) => { window.requestAnimationFrame(() => editorList.querySelector(selector)?.focus()); }; const renderEditor = () => { editorList.replaceChildren(); blockCount.textContent = `${draft.blocks.length} из ${limits.blocks}`; addBlockButton.disabled = draft.blocks.length >= limits.blocks; addBlockButton.title = addBlockButton.disabled ? `Можно добавить не более ${limits.blocks} блоков` : "Добавить блок"; if (!draft.blocks.length) { const empty = document.createElement("div"); empty.className = "dashboard3-empty"; empty.textContent = "Блоков пока нет. Добавьте блок, чтобы вывести данные в панели."; editorList.append(empty); } draft.blocks.forEach((block, blockIndex) => { const article = document.createElement("article"); article.className = "dashboard3-editor-block"; article.dataset.blockId = block.id; const knownSpace = spaces.some((space) => space.id === block.scope.spaceId); const spaceOptions = [ '', block.scope.spaceId && !knownSpace ? `` : "", ...spaces.map((space) => ``) ].join(""); const valuesHtml = block.values.map((value, index) => { const source = sourcePresentation(value.source); return `
⠿
`; }).join(""); article.innerHTML = `
⠿
${block.scope.type === "space" && block.scope.spaceId && !knownSpace ? '

Выбранное пространство недоступно. Привязка сохранена и требует внимания.

' : ""}
${valuesHtml || '
В блоке пока нет значений.
'}
`; const blockTitle = article.querySelector(".dashboard3-block-title"); const blockDrag = article.querySelector(".dashboard4-block-drag"); const blockMoveUp = article.querySelector(".dashboard3-block-up"); const blockMoveDown = article.querySelector(".dashboard3-block-down"); const visibility = article.querySelector(".dashboard3-visibility"); const deleteBlock = article.querySelector(".dashboard3-delete-block"); const scopeSelect = article.querySelector(".dashboard4-scope-type"); const spaceSelect = article.querySelector(".dashboard4-space-select"); const addValue = article.querySelector(".dashboard3-add-value"); blockTitle.addEventListener("input", (event) => { block.title = event.target.value; updateDialogState(); }); blockMoveUp.addEventListener("click", () => { if (!moveItemByOffset(draft.blocks, block.id, -1)) return; renderEditor(); restoreOrderFocus(`[data-block-id="${block.id}"] .dashboard3-block-up`); }); blockMoveDown.addEventListener("click", () => { if (!moveItemByOffset(draft.blocks, block.id, 1)) return; renderEditor(); restoreOrderFocus(`[data-block-id="${block.id}"] .dashboard3-block-down`); }); visibility.addEventListener("click", () => { block.visible = !block.visible; renderEditor(); }); deleteBlock.addEventListener("click", () => { if (block.values.length && !window.confirm(`Удалить блок «${block.title || "Без названия"}» и все его значения?`)) return; draft.blocks = draft.blocks.filter((item) => item.id !== block.id); closeEntityPicker(); renderEditor(); }); scopeSelect.addEventListener("change", (event) => { block.scope.type = event.target.value; if (block.scope.type === "all") block.scope.spaceId = null; else block.scope.spaceId = null; renderEditor(); }); spaceSelect.addEventListener("change", (event) => { block.scope.spaceId = event.target.value || null; renderEditor(); }); addValue.addEventListener("click", () => { if (block.values.length >= limits.values) return; block.values.push({ id: `value-${++valueSequence}`, label: "", source: null }); renderEditor(); article.ownerDocument.querySelector(`[data-block-id="${block.id}"] .dashboard3-editor-value:last-of-type .dashboard3-value-label`)?.focus(); }); blockDrag.addEventListener("dragstart", (event) => { dragContext = { type: "block", id: block.id }; article.classList.add("is-dragging"); event.dataTransfer.effectAllowed = "move"; }); article.addEventListener("dragover", (event) => { if (dragContext?.type !== "block" || dragContext.id === block.id) return; event.preventDefault(); article.classList.add("is-drop-target"); }); article.addEventListener("dragleave", () => article.classList.remove("is-drop-target")); article.addEventListener("drop", (event) => { if (dragContext?.type !== "block" || dragContext.id === block.id) return; event.preventDefault(); const rect = article.getBoundingClientRect(); moveItem(draft.blocks, dragContext.id, block.id, event.clientY > rect.top + rect.height / 2); dragContext = null; renderEditor(); }); blockDrag.addEventListener("dragend", () => { dragContext = null; editorList.querySelectorAll(".is-dragging, .is-drop-target").forEach((element) => element.classList.remove("is-dragging", "is-drop-target")); }); article.querySelectorAll(".dashboard3-editor-value").forEach((rowElement) => { const value = block.values.find((item) => item.id === rowElement.dataset.valueId); if (!value) return; const labelInput = rowElement.querySelector(".dashboard3-value-label"); const entityTrigger = rowElement.querySelector(".dashboard3-entity-trigger"); const removeValue = rowElement.querySelector(".dashboard3-editor-value__remove"); const valueMoveUp = rowElement.querySelector(".dashboard3-value-up"); const valueMoveDown = rowElement.querySelector(".dashboard3-value-down"); const valueDrag = rowElement.querySelector(".dashboard4-value-drag"); labelInput.addEventListener("input", (event) => { value.label = event.target.value; updateDialogState(); }); entityTrigger.addEventListener("click", () => openEntityPicker(block.id, value.id, entityTrigger)); valueMoveUp.addEventListener("click", () => { if (!moveItemByOffset(block.values, value.id, -1)) return; renderEditor(); restoreOrderFocus(`[data-block-id="${block.id}"] [data-value-id="${value.id}"] .dashboard3-value-up`); }); valueMoveDown.addEventListener("click", () => { if (!moveItemByOffset(block.values, value.id, 1)) return; renderEditor(); restoreOrderFocus(`[data-block-id="${block.id}"] [data-value-id="${value.id}"] .dashboard3-value-down`); }); removeValue.addEventListener("click", () => { block.values = block.values.filter((item) => item.id !== value.id); closeEntityPicker(); renderEditor(); }); valueDrag.addEventListener("dragstart", (event) => { dragContext = { type: "value", id: value.id, blockId: block.id }; rowElement.classList.add("is-dragging"); event.stopPropagation(); event.dataTransfer.effectAllowed = "move"; }); rowElement.addEventListener("dragover", (event) => { if (dragContext?.type !== "value" || dragContext.blockId !== block.id || dragContext.id === value.id) return; event.preventDefault(); event.stopPropagation(); rowElement.classList.add("is-drop-target"); }); rowElement.addEventListener("drop", (event) => { if (dragContext?.type !== "value" || dragContext.blockId !== block.id || dragContext.id === value.id) return; event.preventDefault(); event.stopPropagation(); const rect = rowElement.getBoundingClientRect(); moveItem(block.values, dragContext.id, value.id, event.clientY > rect.top + rect.height / 2); dragContext = null; renderEditor(); }); valueDrag.addEventListener("dragend", (event) => { event.stopPropagation(); dragContext = null; editorList.querySelectorAll(".is-dragging, .is-drop-target").forEach((element) => element.classList.remove("is-dragging", "is-drop-target")); }); }); editorList.append(article); }); updateDialogState(); }; const renderEntityOptions = () => { const query = entitySearch.value.trim().toLocaleLowerCase("ru-RU"); const filtered = entities.filter((entity) => `${entity.name} ${entity.entityId}`.toLocaleLowerCase("ru-RU").includes(query)); entityList.replaceChildren(); if (!filtered.length) { const empty = document.createElement("div"); empty.className = "dashboard3-entity-picker__empty"; empty.textContent = "Сущности не найдены"; entityList.append(empty); return; } const selected = pickerContext ? findDraftValue(pickerContext.blockId, pickerContext.valueId)?.source?.entityId : null; filtered.forEach((entity) => { const statusLabel = entity.status === "loading" ? "Загрузка" : entity.status === "stale" ? "Устарело" : entity.status === "unavailable" ? "Недоступно" : ""; const option = document.createElement("button"); option.className = `dashboard3-entity-option dashboard4-entity--${entity.status || "ready"}${selected === entity.entityId ? " is-selected" : ""}`; option.type = "button"; option.setAttribute("role", "option"); option.setAttribute("aria-selected", String(selected === entity.entityId)); option.innerHTML = `${escapeHtml(entity.name)}${entity.template ? " · Template" : ""}${escapeHtml(entity.entityId)}${escapeHtml(entity.state)}${statusLabel ? `${escapeHtml(statusLabel)}` : ""}`; option.addEventListener("click", () => { if (!pickerContext) return; const value = findDraftValue(pickerContext.blockId, pickerContext.valueId); if (!value) return; value.source = { type: "entity", entityId: entity.entityId }; closeEntityPicker(); renderEditor(); }); entityList.append(option); }); }; const positionEntityPicker = (trigger) => { const rect = trigger.getBoundingClientRect(); const width = Math.min(420, window.innerWidth - 24); const estimatedHeight = Math.min(430, window.innerHeight - 24); const left = Math.min(Math.max(12, rect.right - width), window.innerWidth - width - 12); const below = rect.bottom + 5; const top = below + estimatedHeight <= window.innerHeight - 12 ? below : Math.max(12, rect.top - estimatedHeight - 5); entityPicker.style.left = `${left}px`; entityPicker.style.top = `${top}px`; }; const openEntityPicker = (blockId, valueId, trigger) => { pickerContext = { blockId, valueId, trigger }; entitySearch.value = ""; entityPicker.hidden = false; renderEntityOptions(); positionEntityPicker(trigger); window.requestAnimationFrame(() => entitySearch.focus()); }; const closeEntityPicker = () => { entityPicker.hidden = true; pickerContext = null; }; const openSettings = () => { baseline = clone(config); draft = clone(config); baselineEnabled = globalEnabled; draftEnabled = globalEnabled; panelTitleInput.value = draft.title; showPanelInput.checked = draftEnabled; showMobileInput.checked = draft.showOnMobile; settingsDialog.hidden = false; document.body.classList.add("dashboard3-dialog-open"); renderEditor(); window.requestAnimationFrame(() => panelTitleInput.focus()); }; const closeSettings = (restore = true) => { closeEntityPicker(); settingsDialog.hidden = true; document.body.classList.remove("dashboard3-dialog-open"); if (restore) { draft = null; baseline = null; draftEnabled = false; baselineEnabled = false; } }; const saveSettings = () => { const errors = validateDraft(); if (errors.length) { updateDialogState(); editorList.querySelector(".is-invalid")?.focus(); return; } config = clone(draft); globalEnabled = draftEnabled; persist(storageKeys.config, config); persist(storageKeys.enabled, String(globalEnabled)); closeSettings(false); draft = null; baseline = null; draftEnabled = false; baselineEnabled = false; renderPanel(); }; globalToggle.addEventListener("click", () => { globalEnabled = !globalEnabled; persist(storageKeys.enabled, String(globalEnabled)); renderGlobalState(); }); themeToggle.addEventListener("click", () => { currentTheme = currentTheme === "dark" ? "light" : "dark"; persist(storageKeys.theme, currentTheme); applyTheme(); }); kioskToggle.addEventListener("click", () => { kioskEnabled = !kioskEnabled; persist(storageKeys.kiosk, String(kioskEnabled)); applyKiosk(); }); sizeToggle.addEventListener("click", () => { sizeDialog.hidden = false; document.body.classList.add("dashboard3-dialog-open"); applyLocalSizes(); iconSizeInput.focus(); }); sizeClose.addEventListener("click", () => { sizeDialog.hidden = true; document.body.classList.remove("dashboard3-dialog-open"); }); sizeForm.addEventListener("submit", (event) => { event.preventDefault(); sizeDialog.hidden = true; document.body.classList.remove("dashboard3-dialog-open"); }); sizeReset.addEventListener("click", () => { localSizes = { icons: 100, text: 100 }; persist(storageKeys.sizes, localSizes); applyLocalSizes(); }); iconSizeInput.addEventListener("input", (event) => { localSizes.icons = Number(event.target.value); persist(storageKeys.sizes, localSizes); applyLocalSizes(); }); textSizeInput.addEventListener("input", (event) => { localSizes.text = Number(event.target.value); persist(storageKeys.sizes, localSizes); applyLocalSizes(); }); settingsToggle.addEventListener("click", openSettings); settingsClose.addEventListener("click", () => closeSettings()); settingsCancel.addEventListener("click", () => closeSettings()); settingsDialog.addEventListener("click", (event) => { if (event.target === settingsDialog) closeSettings(); }); settingsForm.addEventListener("submit", (event) => { event.preventDefault(); saveSettings(); }); panelTitleInput.addEventListener("input", (event) => { draft.title = event.target.value; updateDialogState(); }); showPanelInput.addEventListener("change", (event) => { draftEnabled = event.target.checked; updateDialogState(); }); showMobileInput.addEventListener("change", (event) => { draft.showOnMobile = event.target.checked; updateDialogState(); }); addBlockButton.addEventListener("click", () => { if (draft.blocks.length >= limits.blocks) return; const usedNumbers = draft.blocks.map((block) => Number(block.title.match(/\d+/)?.[0]) || 0); const number = Math.max(0, ...usedNumbers, blockSequence) + 1; blockSequence = number; draft.blocks.push({ id: `block-${Date.now()}-${number}`, title: `Блок ${number}`, visible: true, scope: { type: "all", spaceId: null }, values: [{ id: `value-${++valueSequence}`, label: "", source: null }] }); renderEditor(); editorList.lastElementChild?.scrollIntoView({ behavior: "smooth", block: "nearest" }); editorList.lastElementChild?.querySelector(".dashboard3-block-title")?.focus(); }); entitySearch.addEventListener("input", renderEntityOptions); document.addEventListener("pointerdown", (event) => { if (entityPicker.hidden || entityPicker.contains(event.target) || event.target.closest(".dashboard3-entity-trigger")) return; closeEntityPicker(); }); document.addEventListener("keydown", (event) => { if (event.key !== "Escape") return; if (!entityPicker.hidden) closeEntityPicker(); else if (!sizeDialog.hidden) { sizeDialog.hidden = true; document.body.classList.remove("dashboard3-dialog-open"); } else if (!settingsDialog.hidden) closeSettings(); }); window.addEventListener("resize", () => { if (!entityPicker.hidden && pickerContext?.trigger) positionEntityPicker(pickerContext.trigger); positionPanel(); renderGlobalState(); }); spaceTabs.forEach((tab) => { tab.addEventListener("click", () => { activeSpaceId = tab.dataset.spaceId; spaceTabs.forEach((item) => item.classList.toggle("active", item === tab)); renderPanel(); }); }); applyTheme(); applyKiosk(); applyLocalSizes(); renderPanel(); positionPanel(); if (typeof ResizeObserver === "function") new ResizeObserver(positionPanel).observe(dashboardBody); window.addEventListener("resize", positionPanel, { passive: true }); window.setInterval(renderPanel, 60000); })();