docs: specify summary panel designer parity

Issue: #505
User-Visible: no
This commit is contained in:
Matysh
2026-09-09 09:32:23 +03:00
parent 2e387ac1e0
commit 598e95f4c4
19 changed files with 3467 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
# #505 designer reference
Issue: https://github.com/Matysh/houseplan-card/issues/505
Specification: [505-summary-panel-design-parity.md](../../specs/505-summary-panel-design-parity.md).
Original files supplied by the owner on 2026-09-09 in `макет.zip` are preserved
under [reference](reference/index.html). ZIP SHA-256:
`bc754c16c9d9fc79dbbf093fcf8e14b96f8826fa01d1f0c8eccf68ab95357e15`.
Open index.html and use the rightmost split control (gear, then sidebar toggle).
The demo uses local state; its JS is design reference, not production or process
instructions. No backend dependencies are needed. Demo dates may vary.
Do not bundle these files into the product. The numbered spec documents the
allowed adaptation for HA themes, accessibility and production data ownership.
@@ -0,0 +1,143 @@
# Dashboard 5 — передача в разработку
Дата пакета: 8 сентября 2026 года.
Основание: [houseplan-card issue #437](https://github.com/Matysh/houseplan-card/issues/437) и согласованные уточнения встречи от 8 сентября 2026 года.
## Назначение
Dashboard 5 — интерактивный HTML-прототип конфигурируемой информационной панели рядом с планом House Plan. Пакет предназначен как визуальная и поведенческая основа для реализации внутри существующей Lovelace-карточки.
## Зафиксированное поведение
- Информационная панель плавает поверх плана и не участвует в геометрии, `content bbox` или автоматическом `fit`.
- В горизонтальной ориентации панель размещается справа.
- В портретной ориентации панель размещается снизу, центрируется и подстраивает ширину под содержимое с ограничением шириной экрана.
- Панель имеет собственный пользовательский заголовок.
- В шапке House Plan используется составной контрол: слева настройка, справа показ или скрытие панели.
- Правая кнопка использует `svg/layout-sidebar-right.svg`.
- Отдельного состояния сворачивания всей панели и сворачивания блоков нет.
- Значения в строках выровнены по правому краю.
- Панель и её содержимое ограничены по высоте; при переполнении прокручиваются данные, а не план.
- В режиме киоска панель располагается ниже плавающего блока управления.
## Структура данных
```text
информационная панель
└── блок
└── значение: пользовательское название — источник/состояние
```
Предлагаемая модель:
```js
{
title: "Сводная информация",
showOnMobile: true,
blocks: [
{
id: "stable-block-id",
title: "Общее",
visible: true,
scope: { type: "all" },
values: [
{
id: "stable-value-id",
label: "Количество устройств",
source: { type: "system", key: "device_count" }
}
]
}
]
}
```
Для блока, привязанного к пространству:
```js
scope: { type: "space", spaceId: "stable-space-id" }
```
Для сущности Home Assistant:
```js
source: { type: "entity", entityId: "sensor.example" }
```
## Начальная конфигурация
- Название панели: «Сводная информация».
- Один включённый блок «Общее» для всех пространств.
- Системные строки в порядке:
1. «Количество устройств»;
2. «Общая площадь комнат»;
3. «Текущие дата и время».
## Диалог настроек
Диалог работает с единым черновиком. «Сохранить» применяет все изменения атомарно. «Отмена», крестик и Escape отбрасывают весь черновик.
Поддерживаются:
- название панели;
- общий показ панели;
- показ в нативном узком режиме Home Assistant;
- создание, переименование, включение, выключение и удаление блоков;
- выбор области блока: все пространства или одно определённое;
- создание, переименование, сортировка и удаление строк;
- сортировка блоков и строк перетаскиванием и квадратными кнопками «выше/ниже»;
- выбор сущности по имени и `entity_id`;
- состояния загрузки, устаревания и недоступности;
- проверка обязательных полей и блокировка сохранения при ошибках;
- подтверждение удаления непустого блока.
Ограничения структуры: не более 10 блоков, не более 20 значений в блоке, заголовок панели или блока до 48 символов, название значения до 64 символов.
## Интеграция с Home Assistant
Прототип содержит демонстрационный список сущностей. В продукте необходимо:
1. Получать полный доступный пользователю список из `hass.states`.
2. Использовать стабильный `entity_id` и форматирование состояния средствами Home Assistant.
3. Показывать системные показатели в том же селекторе источников, что и сущности.
4. Автоматически включать в список новые Template-сущности.
5. Сохранять ранее выбранную недоступную сущность и отмечать её как требующую внимания.
6. Получать мобильный/узкий режим из нативного контекста Home Assistant, а не из собственного числового breakpoint.
7. Интегрировать конфигурацию с постоянным хранилищем House Plan, совместной работой и экспортом/импортом, не добавляя данные панели в геометрию плана.
8. Следовать действующей модели прав House Plan для доступа к структурным настройкам.
## Файлы прототипа
- `index.html` — экран House Plan, панель и диалоги.
- `dashboard-5.js` — состояние, черновик настроек, фильтрация по пространству и адаптивная позиция.
- `dashboard-5.css` — компоновка, темы, киоск и анимации.
- `styles.css`, `components.css`, `components.html` — базовая демонстрационная оболочка.
- `svg/` — локальные иконки интерфейса.
## Проверки перед реализацией
- Обычный и нативный узкий режим Home Assistant.
- Альбомная и портретная ориентации телефона и планшета.
- Панель справа и снизу, короткий и максимальный состав.
- Светлая и тёмная темы, 200% zoom и safe area.
- Клавиатурная навигация, Escape, фокус после сортировки.
- Pan/pinch/scroll плана при открытой панели.
- Переход между пространствами без `fit/refit` и скачка масштаба.
- Загрузка, отсутствие данных, `unknown`, `unavailable`, восстановленная и удалённая сущность.
- Удалённое пространство, сохранённая проблемная привязка и смена активного пространства.
- Перезагрузка страницы, совместное редактирование, импорт и экспорт конфигурации.
## Запуск прототипа
Из папки `Dashboard 5`:
```text
python -m http.server 8768
```
Открыть `http://127.0.0.1:8768/`.
## Граница ответственности прототипа
HTML-прототип демонстрирует интерфейс и взаимодействия. Статические системные числа, демонстрационный массив сущностей и `localStorage` не являются готовой продуктовой реализацией. Источники данных, постоянное хранение, права, i18n и интеграционные тесты реализуются в кодовой базе House Plan.
@@ -0,0 +1,32 @@
# Dashboard 5 — заметки по реализации
Версия реализует изменения из документа «ТЗ Dashboard 4 — финальные изменения по итогам встречи 08.09.2026.md» как отдельный прототип и не изменяет Dashboard 4.
## Реализовано
- автоматический выбор позиции по ориентации viewport через `matchMedia("(orientation: portrait)")`;
- `ResizeObserver` и обработчик `resize` для реакции на изменение размеров и поворот устройства;
- плавающее размещение поверх плана;
- компактная ширина по содержимому в правом и нижнем положении с ограничением доступным экраном;
- направленная анимация показа и скрытия;
- составной контрол из двух доступных кнопок;
- единое состояние показа в шапке и диалоге;
- собственный заголовок информационной панели;
- отсутствие элементов и состояния сворачивания блоков;
- внутренний скролл при переполнении;
- безопасное расположение под управлением киоска;
- отключение переходов при `prefers-reduced-motion`.
## Прототипные ограничения
- сущности заданы демонстрационным массивом;
- сохранение основано на `localStorage`;
- реальные контексты и форматирование Home Assistant требуют интеграции;
- touch/pan/pinch, safe area и 200% zoom требуют проверки в реальной карточке.
## Последние визуальные уточнения
- кнопка показа использует `svg/layout-sidebar-right.svg`;
- значения строк выровнены по правому краю блока;
- в портретной ориентации панель находится снизу, центрируется и не растягивается на всю ширину без необходимости.
@@ -0,0 +1,22 @@
# Текст для публикации в issue #437
Подготовлен архив **Dashboard-5-Issue-437-handoff-2026-09-08.zip** с актуальной версией HTML-прототипа информационной панели.
В Dashboard 5 зафиксированы последние согласованные решения:
- плавающая панель поверх плана без изменения масштаба и геометрии;
- автоматическое расположение справа в горизонтальной и снизу в портретной ориентации;
- компактный размер по содержимому с ограничением доступным экраном;
- составной контрол «настройки + показать/скрыть» в шапке House Plan;
- собственный пользовательский заголовок панели;
- единый диалог настройки и атомарные «Сохранить/Отмена»;
- блоки всегда развёрнуты;
- сортировка блоков и строк, включая доступные кнопки «выше/ниже»;
- области отображения блоков по пространствам;
- начальный блок «Общее» с тремя системными значениями;
- состояния сущностей, темы, киоск и локальные размеры экрана.
В архив включены исходники прототипа, локальные SVG, инструкция запуска и документ `DEVELOPER-HANDOFF.md` с моделью данных, интеграционными требованиями и перечнем проверок для продуктовой реализации.
Прототип использует демонстрационные сущности и `localStorage`. В продукте список должен формироваться из `hass.states`, а конфигурация — сохраняться штатными средствами House Plan.
@@ -0,0 +1,46 @@
# Dashboard 5
Интерактивный HTML-прототип финальной компоновки информационной панели House Plan по итогам встречи 8 сентября 2026 года и требованиям [issue #437](https://github.com/Matysh/houseplan-card/issues/437).
## Запуск
Откройте папку через локальный HTTP-сервер:
```text
python -m http.server 8768
```
После запуска страница доступна по адресу `http://127.0.0.1:8768/`.
## Главное отличие версии 5
- панель плавает поверх плана и не изменяет его рабочую область;
- в горизонтальной ориентации экрана располагается справа;
- в вертикальной ориентации экрана автоматически перемещается вниз;
- положение пересчитывается при изменении размера окна или повороте устройства;
- габариты панели зависят от содержимого и ограничиваются доступным экраном;
- в нижнем положении панель центрируется и сохраняет компактную ширину по содержимому;
- собственный заголовок возвращён в панель;
- в шапке House Plan находится компактный составной контрол без текста;
- левая половина контрола открывает общий диалог настройки;
- правая половина показывает и скрывает панель;
- переключатель видимости в диалоге управляет тем же состоянием;
- отдельное состояние сворачивания панели отсутствует;
- блоки не сворачиваются ни в просмотре, ни в настройках;
- появление и скрытие анимируется в направлении текущего края;
- в режиме киоска учитываются плавающие элементы управления.
## Основные файлы
- `index.html` — интерфейс House Plan и разметка панели/диалогов;
- `dashboard-5.js` — модель, адаптивная позиция, настройки и взаимодействия;
- `dashboard-5.css` — компоновка, анимации, темы и киоск;
- `styles.css`, `components.css` — базовое оформление плана;
- `svg/` — локальные иконки.
## Ограничение прототипа
Список сущностей остаётся демонстрационным, а конфигурация сохраняется в `localStorage`. В продуктовой реализации источником сущностей должен быть `hass.states`, а хранение необходимо интегрировать со штатной конфигурацией House Plan.
Полный пакет передачи разработчикам описан в `DEVELOPER-HANDOFF.md`. Текст, который можно использовать при прикреплении архива к issue, находится в `ISSUE-HANDOFF.md`.
@@ -0,0 +1,425 @@
:root {
--ha-blue: #03a4cf;
--ha-blue-dark: #0497bd;
--ha-text: #202124;
--ha-muted: #686b6e;
--ha-border: #dedede;
--ha-soft: #f1f1f1;
--ha-danger: #ff725d;
}
* { box-sizing: border-box; }
html, body { margin: 0; min-width: 1120px; min-height: 720px; font-family: Arial, Helvetica, sans-serif; color: var(--ha-text); }
button, input { font: inherit; }
button { cursor: default; }
.components-page { min-height: 100vh; overflow: auto; background: #dceaf0; }
.ha-component-header { position: relative; z-index: 5; background: #fff; }
.ha-view-tabs {
display: flex;
align-items: flex-end;
height: 82px;
padding-left: 21px;
gap: 25px;
border-bottom: 1px solid var(--ha-border);
}
.ha-view-tab {
position: relative;
display: grid;
place-items: center;
width: 56px;
height: 62px;
color: #48494a;
font-size: 29px;
font-weight: 700;
}
.ha-view-tab.active::after { position: absolute; right: 0; bottom: 0; left: 0; height: 3px; content: ""; background: #222; }
.ha-context-row {
display: flex;
align-items: center;
height: 86px;
padding: 0 20px;
gap: 14px;
border-bottom: 1px solid var(--ha-border);
white-space: nowrap;
}
.ha-plan-label { display: flex; align-items: center; gap: 8px; font-size: 21px; }
.ha-home-icon { color: var(--ha-blue); font-size: 28px; }
.ha-segmented { display: flex; align-items: center; height: 57px; padding: 5px; border-radius: 17px; background: var(--ha-soft); }
.ha-segment { display: flex; align-items: center; height: 47px; padding: 0 21px; gap: 8px; border-radius: 13px; color: #555; font-size: 18px; font-weight: 700; }
.ha-segment small { color: #bbb; font-size: 15px; }
.ha-segment.active { color: #fff; background: var(--ha-blue); }
.ha-segment.active small { color: #8ed5e8; }
.ha-segment b { margin-left: 9px; font-size: 23px; font-weight: 300; }
.ha-add { padding: 0 13px; color: #666; font-size: 24px; }
.ha-editor-switcher { margin-left: 0; }
.ha-editor-switcher .ha-segment { padding: 0 18px; }
.ha-device-count { color: #555; font-size: 17px; }
.ha-tools-row {
display: flex;
align-items: center;
min-height: 85px;
padding: 14px 22px;
gap: 15px;
border-bottom: 1px solid var(--ha-border);
background: #fff;
white-space: nowrap;
}
.ha-tool-mode { color: #ffb52e; font-size: 34px; }
.ha-tool {
display: inline-flex;
align-items: center;
justify-content: center;
height: 47px;
padding: 0 18px;
gap: 11px;
border: 1px solid #d7d7d7;
border-radius: 14px;
color: #111;
background: #fff;
font-size: 19px;
}
.ha-tool.active { border-color: var(--ha-blue); color: #fff; background: var(--ha-blue); }
.ha-tool-icon { font-size: 22px; font-weight: 700; }
.ha-chevron { margin-left: 3px; }
.ha-history-actions { display: flex; margin-left: auto; gap: 21px; }
.ha-history-actions button { border: 0; color: #929292; background: transparent; font-size: 27px; }
.ha-component-stage { position: relative; min-height: 1090px; overflow: hidden; background: #dbeaf0; }
.ha-stage-grid {
position: absolute;
inset: 0;
opacity: .6;
background-image: radial-gradient(#4da2bb 1px, transparent 1.5px);
background-size: 10px 10px;
}
.ha-plan-fragment { position: absolute; inset: 100px 10% 0 15%; color: #383d40; font-weight: 700; opacity: .45; }
.ha-plan-room { position: absolute; display: grid; place-items: center; border: 12px solid #596266; background: #a0a9ae; }
.room-a { left: 0; top: 0; width: 70%; height: 340px; }
.room-b { left: 28%; top: 328px; width: 31%; height: 390px; }
.room-c { left: 58%; top: 328px; width: 28%; height: 390px; }
.ha-modal-backdrop { position: absolute; inset: 0; background: rgba(40, 40, 40, .48); }
.ha-device-modal {
position: absolute;
top: 9px;
left: 50%;
display: flex;
flex-direction: column;
width: 870px;
height: 1080px;
transform: translateX(-50%);
overflow: hidden;
border-radius: 21px;
background: #fff;
box-shadow: 0 7px 24px rgba(0,0,0,.18);
}
.ha-modal-header { display: flex; align-items: center; flex: 0 0 119px; padding: 0 37px; gap: 18px; }
.ha-modal-header h1 { margin: 0; font-size: 31px; line-height: 1; }
.ha-modal-close { display: grid; place-items: center; width: 34px; height: 34px; font-size: 40px; font-weight: 200; line-height: 1; }
.ha-device-shapes { position: relative; display: block; width: 31px; height: 34px; }
.ha-device-shapes i { position: absolute; display: block; background: var(--ha-blue); }
.ha-device-shapes i:nth-child(1) { left: 0; top: 4px; width: 13px; height: 13px; }
.ha-device-shapes i:nth-child(2) { right: 0; top: 4px; width: 13px; height: 13px; border-radius: 50%; }
.ha-device-shapes i:nth-child(3) { left: 1px; bottom: 1px; width: 0; height: 0; border-right: 7px solid transparent; border-bottom: 0; border-left: 7px solid transparent; border-top: 13px solid var(--ha-blue); background: transparent; transform: rotate(180deg); }
.ha-device-shapes i:nth-child(4) { right: 1px; bottom: 4px; width: 13px; height: 3px; }
.ha-device-shapes i:nth-child(4)::after { position: absolute; left: 5px; top: -5px; width: 3px; height: 13px; content: ""; background: var(--ha-blue); }
.ha-modal-body { flex: 1 1 auto; min-height: 0; padding: 8px 46px 32px 37px; overflow-y: scroll; scrollbar-color: #b7b7b7 transparent; scrollbar-width: auto; }
.ha-modal-body::-webkit-scrollbar { width: 14px; }
.ha-modal-body::-webkit-scrollbar-track { background: transparent; }
.ha-modal-body::-webkit-scrollbar-thumb { border: 3px solid #fff; border-radius: 9px; background: #b7b7b7; }
.ha-field { margin-bottom: 20px; }
.ha-field label, .ha-field-label { display: block; margin-bottom: 12px; color: var(--ha-muted); font-size: 18px; }
.ha-field input[type="text"], .ha-light-sources input[type="text"] {
width: 100%;
height: 45px;
padding: 0 14px;
border: 1px solid #d6d6d6;
border-radius: 10px;
color: #242424;
background: #fff;
outline: none;
font-size: 19px;
}
.ha-binding-card { padding: 20px 13px 12px; border: 1px solid #d7d7d7; border-radius: 12px; }
.ha-choice { display: flex !important; align-items: center; margin: 0 0 21px !important; gap: 13px; color: var(--ha-muted); font-size: 18px; }
.ha-choice input { width: 19px; height: 19px; margin: 0; accent-color: #078cff; }
.ha-choice-row { display: flex; align-items: center; justify-content: flex-start; gap: 29px; }
.ha-switch-label { display: flex; align-items: center; gap: 12px; color: #727272; font-size: 18px; }
.ha-switch { position: relative; display: inline-block; width: 71px; height: 37px; border: 1px solid #a9a9a9; border-radius: 20px; background: #f7f7f7; }
.ha-switch i { position: absolute; left: 4px; top: 3px; width: 29px; height: 29px; border-radius: 50%; background: #737373; }
.ha-select-box { display: flex; align-items: center; width: 100%; height: 47px; padding: 0 14px; border: 1px solid #d6d6d6; border-radius: 10px; color: #222; background: #fff; font-size: 19px; }
.strong-select { height: 54px; border-color: #555; }
.ha-entity { margin-left: auto; color: #696969; font-weight: 400; }
.ha-select-arrow { margin-left: 18px; font-size: 23px; font-weight: 700; }
.action-field { margin-bottom: 30px; }
.action-field p { margin: 9px 0 0; color: var(--ha-muted); font-size: 18px; }
.confirmation { margin: 0 0 25px; }
.ha-light-sources h2 { margin: 0 0 18px; color: var(--ha-muted); font-size: 18px; font-weight: 400; }
.ha-light-sources p { margin: 0 0 8px; color: var(--ha-muted); font-size: 17px; line-height: 1.45; }
.ha-light-sources input { margin-top: 14px; }
.ha-light-sources input::placeholder { color: #777; opacity: 1; }
.ha-source-type { margin: 41px 0 0; padding: 26px 18px 13px; border: 1px solid #d8d8d8; border-radius: 12px; }
.ha-source-type legend { padding: 0 8px 0 0; font-size: 20px; font-weight: 700; }
.ha-help { display: inline-grid; place-items: center; width: 23px; height: 23px; margin-left: 13px; border: 2px solid #707070; border-radius: 50%; color: #707070; font-size: 15px; }
.ha-source-type .ha-choice { margin-bottom: 19px !important; }
.ha-modal-spacer { height: 130px; }
.ha-modal-footer { display: flex; align-items: center; justify-content: space-between; flex: 0 0 107px; padding: 0 47px; border-top: 1px solid #e0e0e0; background: #fff; }
.ha-footer-left, .ha-footer-right { display: flex; align-items: center; gap: 13px; }
.ha-action-button, .ha-text-button, .ha-save-button { display: inline-flex; align-items: center; justify-content: center; height: 48px; border-radius: 11px; background: #fff; font-size: 18px; }
.ha-action-button { padding: 0 17px; gap: 10px; border: 1px solid #d5d5d5; }
.ha-action-button.danger { border-color: var(--ha-danger); color: var(--ha-danger); }
.ha-text-button { padding: 0 22px; border: 0; }
.ha-save-button { padding: 0 22px; gap: 12px; border: 1px solid var(--ha-blue); color: #fff; background: var(--ha-blue); font-weight: 700; }
@media (max-width: 1500px) {
.ha-context-row { gap: 8px; padding-inline: 12px; }
.ha-segment { padding-inline: 13px; font-size: 15px; }
.ha-plan-label { font-size: 18px; }
.ha-tools-row { gap: 8px; padding-inline: 12px; }
.ha-tool { padding-inline: 11px; font-size: 15px; }
}
@media (max-height: 950px) {
.ha-device-modal { height: calc(100vh - 18px); }
.ha-component-stage { min-height: calc(100vh - 253px); }
}
/* Reusable home information panel */
.ha-home-panel {
position: relative;
z-index: 4;
display: flex;
flex: 0 0 390px;
width: 390px;
min-width: 0;
height: 100%;
flex-direction: column;
border-left: 1px solid #d9dcde;
background: #f5f7f8;
box-shadow: -5px 0 20px rgba(46, 58, 64, .1);
transition: flex-basis .22s ease, width .22s ease;
}
.ha-home-panel.is-collapsed { flex-basis: 48px; width: 48px; }
.ha-home-panel__header {
position: relative;
display: flex;
align-items: center;
flex: 0 0 62px;
min-height: 62px;
padding: 0 13px 0 9px;
gap: 10px;
border-bottom: 1px solid #dfe2e4;
background: #fff;
}
.ha-icon-button {
display: inline-grid;
place-items: center;
width: 34px;
height: 34px;
flex: 0 0 34px;
padding: 0;
border: 0;
border-radius: 9px;
color: #555b5e;
background: transparent;
font-size: 18px;
}
.ha-icon-button:hover { background: #edf1f2; }
.ha-home-panel__size-toggle { border: 1px solid #dce0e2; font-size: 27px; line-height: 1; }
.ha-home-panel__size-toggle .when-collapsed { display: none; }
.ha-home-panel.is-collapsed .ha-home-panel__size-toggle .when-expanded { display: none; }
.ha-home-panel.is-collapsed .ha-home-panel__size-toggle .when-collapsed { display: inline; }
.ha-home-panel__title-wrap { display: flex; align-items: center; min-width: 0; gap: 9px; }
.ha-home-panel__mark { color: var(--ha-blue); font-size: 21px; }
.ha-home-panel__title-wrap h2 { margin: 0; font-size: 16px; line-height: 1.2; }
.ha-home-panel__title-wrap small { display: block; margin-top: 2px; color: #818588; font-size: 10px; }
.ha-panel-settings-toggle { margin-left: auto; }
.ha-panel-settings-toggle.is-active { color: #fff; background: var(--ha-blue); }
.ha-home-panel__rail-title { display: none; position: absolute; top: 56px; left: 16px; color: #72777a; font-size: 11px; font-weight: 700; letter-spacing: .04em; writing-mode: vertical-rl; }
.ha-home-panel.is-collapsed .ha-home-panel__header { flex: 1 1 auto; align-items: flex-start; padding: 10px 7px; }
.ha-home-panel.is-collapsed .ha-home-panel__title-wrap,
.ha-home-panel.is-collapsed .ha-panel-settings-toggle,
.ha-home-panel.is-collapsed .ha-home-panel__content { display: none; }
.ha-home-panel.is-collapsed .ha-home-panel__rail-title { display: block; }
.ha-home-panel__content { display: flex; flex: 1 1 auto; min-height: 0; flex-direction: column; padding: 10px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #b8bec1 transparent; }
.ha-summary-card,
.ha-info-block,
.ha-panel-manager { margin-bottom: 9px; border: 1px solid #dde1e3; border-radius: 11px; background: #fff; box-shadow: 0 1px 2px rgba(34, 45, 50, .04); }
.ha-summary-card { padding: 13px; }
.ha-panel-manager { order: 0; }
.ha-summary-card { order: 1; }
.ha-panel-blocks { order: 2; }
.ha-summary-card__heading,
.ha-panel-manager__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.ha-summary-actions { display: flex; align-items: center; gap: 5px; }
.ha-eyebrow { display: block; margin-bottom: 3px; color: #878b8e; font-size: 9px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
.ha-summary-card h3,
.ha-panel-manager h3 { margin: 0; font-size: 15px; }
.ha-status-badge,
.room-state { display: inline-flex; align-items: center; min-height: 21px; padding: 0 7px; border-radius: 10px; color: #5e6467; background: #edf0f1; font-size: 9px; font-weight: 700; white-space: nowrap; }
.ha-status-badge.good,
.room-state.good { color: #168848; background: #e1f5e8; }
.room-state.warning { color: #9b6500; background: #fff0ca; }
.ha-temperature-grid { display: grid; grid-template-columns: repeat(2, 1fr); margin-top: 12px; gap: 7px; }
.ha-temperature-metric { display: flex; align-items: center; min-width: 0; padding: 9px; gap: 9px; border-radius: 9px; background: linear-gradient(135deg, #eef8fb, #f6fafb); }
.ha-temperature-metric.outside { background: linear-gradient(135deg, #fff6dc, #fffaf0); }
.ha-temperature-metric .metric-icon { display: grid; place-items: center; width: 29px; height: 29px; flex: 0 0 29px; border-radius: 9px; color: #fff; background: #3995ae; font-size: 13px; }
.ha-temperature-metric.outside .metric-icon { background: #edae2e; }
.ha-temperature-metric span:last-child { min-width: 0; }
.ha-temperature-metric small { display: block; margin-bottom: 2px; color: #777d80; font-size: 8px; }
.ha-temperature-metric strong { display: block; font-size: 14px; white-space: nowrap; }
.ha-summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); margin-top: 7px; gap: 6px; }
.ha-summary-metric { display: flex; align-items: center; min-width: 0; flex-direction: column; padding: 8px 3px; border-radius: 8px; background: #f4f6f7; text-align: center; }
.ha-summary-metric .metric-icon { display: grid; place-items: center; width: 21px; height: 21px; margin-bottom: 4px; border-radius: 50%; color: #fff; background: #768085; font-size: 10px; }
.ha-summary-metric strong { font-size: 13px; }
.ha-summary-metric small { margin-top: 2px; overflow: hidden; max-width: 100%; color: #85898c; font-size: 8px; text-overflow: ellipsis; white-space: nowrap; }
.ha-summary-note { display: flex; align-items: center; margin-top: 10px; padding-top: 9px; gap: 7px; border-top: 1px solid #eceeef; color: #6b7073; font-size: 10px; }
.ha-warning-dot { width: 8px; height: 8px; flex: 0 0 8px; border-radius: 50%; background: #ffae00; box-shadow: 0 0 0 3px #fff1c9; }
.ha-panel-blocks { min-height: 40px; }
.ha-info-block { overflow: clip; transition: opacity .15s ease, border-color .15s ease; }
.ha-info-block.is-dragging { border-color: var(--ha-blue); opacity: .55; }
.ha-info-block__header { display: flex; align-items: center; min-height: 43px; padding: 0 8px; gap: 7px; }
.ha-drag-handle,
.ha-manager-drag { color: #a0a5a8; cursor: grab; font-size: 17px; line-height: 1; user-select: none; }
.ha-drag-handle { visibility: hidden; width: 0; overflow: hidden; opacity: 0; transition: width .15s ease, opacity .15s ease; }
.ha-home-panel.is-editing .ha-drag-handle { visibility: visible; width: auto; overflow: visible; opacity: 1; }
.ha-home-panel.is-editing .ha-info-block { border-style: dashed; }
.ha-home-panel.is-editing .ha-info-block__header { background: #fbfdfe; }
.ha-block-settings-button { display: none; place-items: center; width: 27px; height: 27px; flex: 0 0 27px; padding: 0; border: 1px solid #cfe4ea; border-radius: 7px; color: #087f9d; background: #eef9fb; font-size: 12px; }
.ha-home-panel.is-editing .ha-block-settings-button { display: grid; }
.ha-block-settings-button:hover { color: #fff; background: var(--ha-blue); }
.ha-drag-handle:active,
.ha-manager-drag:active { cursor: grabbing; }
.ha-block-icon { display: grid; place-items: center; width: 24px; height: 24px; flex: 0 0 24px; border-radius: 7px; color: #31798e; background: #e5f3f7; font-size: 11px; font-weight: 800; }
.ha-info-block__toggle { display: grid; grid-template-columns: 1fr auto 16px; align-items: center; width: 100%; height: 42px; padding: 0; gap: 7px; border: 0; color: #292d2f; background: transparent; text-align: left; }
.ha-info-block__toggle > span { font-size: 13px; font-weight: 700; }
.ha-info-block__toggle small { color: #8a8e90; font-size: 9px; font-weight: 400; }
.ha-info-block__toggle b { color: #74797c; font-size: 12px; text-align: center; transition: transform .18s ease; }
.ha-info-block.is-collapsed .ha-info-block__toggle b { transform: rotate(180deg); }
.ha-info-block.is-collapsed .ha-info-block__body { display: none; }
.ha-info-block__body { padding: 0 11px 11px 38px; border-top: 1px solid #eef0f1; }
.ha-status-list,
.ha-compact-list,
.ha-timeline { margin: 0; padding: 0; list-style: none; }
.ha-status-list li { display: flex; align-items: center; justify-content: space-between; min-height: 42px; padding: 7px 0; gap: 8px; border-bottom: 1px solid #eef0f1; }
.ha-status-list li:last-child { border-bottom: 0; }
.ha-status-list li > span:first-child { min-width: 0; }
.ha-status-list strong { display: block; overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.ha-status-list small { display: block; margin-top: 3px; overflow: hidden; color: #858a8c; font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
.ha-block-link { display: flex; align-items: center; justify-content: space-between; width: 100%; margin-top: 6px; padding: 7px 0 0; border: 0; color: #078eb1; background: transparent; font-size: 10px; font-weight: 700; }
.ha-inline-stats { display: grid; grid-template-columns: repeat(3, 1fr); padding: 10px 0; gap: 5px; }
.ha-inline-stats span { display: flex; align-items: center; flex-direction: column; padding: 7px 3px; border-radius: 7px; background: #f5f7f7; }
.ha-inline-stats strong { font-size: 13px; }
.ha-inline-stats small { margin-top: 2px; color: #858a8c; font-size: 8px; }
.ha-compact-list li { display: flex; align-items: center; justify-content: space-between; min-height: 31px; gap: 8px; border-top: 1px solid #eef0f1; font-size: 10px; }
.ha-compact-list li span { color: #73787a; font-size: 9px; white-space: nowrap; }
.good-text { color: #168848 !important; font-weight: 700; }
.ha-timeline { padding-top: 7px; }
.ha-timeline li { display: flex; min-height: 43px; gap: 9px; }
.ha-timeline strong { display: block; font-size: 10px; }
.ha-timeline small { display: block; margin-top: 3px; color: #7d8284; font-size: 9px; line-height: 1.3; }
.event-dot { width: 9px; height: 9px; flex: 0 0 9px; margin-top: 3px; border-radius: 50%; background: #92989b; }
.event-dot.warning { background: #ffae00; }
.event-dot.good { background: #38c86e; }
.ha-panel-manager { padding: 12px; border-color: #b9dce5; background: #fafdfe; }
.ha-panel-manager__heading .ha-icon-button { width: 27px; height: 27px; flex-basis: 27px; }
.ha-panel-manager > p { margin: 8px 0 10px; color: #74797c; font-size: 9px; line-height: 1.4; }
.ha-panel-manager__list { display: flex; flex-direction: column; gap: 5px; }
.ha-panel-manager__item { display: grid; grid-template-columns: 20px 1fr auto; align-items: center; min-height: 36px; padding: 0 6px; border: 1px solid #dfe4e6; border-radius: 8px; background: #fff; }
.ha-panel-manager__item.is-dragging { opacity: .5; }
.ha-panel-manager__item.is-pinned { border-style: dashed; background: #f4f8f9; }
.ha-panel-manager__item.is-pinned > small { color: #8a9092; font-size: 8px; }
.ha-manager-pin { color: var(--ha-blue); font-size: 8px; text-align: center; }
.ha-panel-manager__item label { display: flex; align-items: center; gap: 7px; font-size: 10px; font-weight: 700; }
.ha-panel-manager__item input { width: 15px; height: 15px; margin: 0; accent-color: var(--ha-blue); }
.ha-manager-order { display: flex; gap: 3px; }
.ha-manager-order button { display: grid; place-items: center; width: 25px; height: 25px; padding: 0; border: 1px solid #dce0e2; border-radius: 6px; color: #5f6568; background: #fff; }
.ha-manager-order button:disabled { opacity: .3; }
.ha-reset-layout { width: 100%; height: 30px; margin-top: 9px; border: 1px solid #d4dade; border-radius: 7px; color: #555d60; background: #fff; font-size: 9px; font-weight: 700; }
.ha-device-categories { margin-top: 3px; }
.ha-device-category { border-top: 1px solid #e9edef; }
.ha-device-category__toggle { display: grid; grid-template-columns: 1fr auto 17px; align-items: center; width: 100%; min-height: 37px; padding: 0; gap: 7px; border: 0; color: #303537; background: transparent; text-align: left; }
.ha-device-category__toggle span { font-size: 10px; font-weight: 700; }
.ha-device-category__toggle small { color: #858a8c; font-size: 8px; }
.ha-device-category__toggle b { color: #747a7d; font-size: 12px; text-align: center; transition: transform .16s ease; }
.ha-device-category.is-open .ha-device-category__toggle b { transform: rotate(180deg); }
.ha-device-category__list { margin: 0; padding: 0 0 5px; list-style: none; }
.ha-device-category__list li { display: flex; align-items: center; justify-content: space-between; min-height: 43px; padding: 6px 0 6px 8px; gap: 8px; border-top: 1px solid #eef0f1; }
.ha-device-category__list li > span { min-width: 0; }
.ha-device-category__list strong { display: block; overflow: hidden; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.ha-device-category__list small { display: block; margin-top: 3px; overflow: hidden; color: #858a8c; font-size: 8px; text-overflow: ellipsis; white-space: nowrap; }
.ha-device-category__list li.is-device-hidden { opacity: .48; }
.ha-device-settings-button { display: inline-flex; align-items: center; min-height: 27px; padding: 0 7px; gap: 4px; border: 1px solid #d9dfe1; border-radius: 7px; color: #4e575a; background: #fff; font-size: 9px; white-space: nowrap; }
.ha-device-settings-button:hover { border-color: #8ecddd; color: #087f9d; background: #f1fafc; }
.ha-device-editor { display: flex; flex: 1 1 auto; min-height: 0; flex-direction: column; background: #fff; }
.ha-device-editor[hidden] { display: none; }
.ha-home-panel.is-collapsed .ha-device-editor { display: none; }
.ha-home-panel.is-device-editor-open .ha-home-panel__content { display: none; }
.ha-device-editor__header { display: flex; align-items: center; min-height: 63px; padding: 0 14px; gap: 10px; border-bottom: 1px solid #e2e5e7; }
.ha-device-editor__header h3 { margin: 0; overflow: hidden; max-width: 270px; font-size: 15px; text-overflow: ellipsis; white-space: nowrap; }
.ha-device-editor__body { flex: 1 1 auto; min-height: 0; padding: 16px; overflow-y: auto; }
.ha-device-editor__body .ha-field { margin-bottom: 16px; }
.ha-device-editor__body .ha-field label { margin-bottom: 7px; font-size: 11px; }
.ha-device-editor__body input[type="text"],
.ha-device-editor__body select { width: 100%; height: 39px; padding: 0 10px; border: 1px solid #d4dadd; border-radius: 8px; color: #2c3133; background: #fff; font-size: 12px; outline: none; }
.ha-device-editor__body input:focus,
.ha-device-editor__body select:focus { border-color: var(--ha-blue); box-shadow: 0 0 0 2px rgba(3,164,207,.12); }
.ha-device-editor__body input[readonly] { color: #777e81; background: #f5f7f8; }
.ha-device-enabled { display: flex; align-items: center; margin-top: 22px; gap: 9px; color: #52595c; font-size: 11px; }
.ha-device-enabled input { width: 17px; height: 17px; margin: 0; accent-color: var(--ha-blue); }
.ha-device-editor__footer { display: flex; justify-content: flex-end; min-height: 61px; padding: 12px 14px; gap: 8px; border-top: 1px solid #e2e5e7; }
.ha-device-editor__footer button { height: 35px; padding: 0 13px; border-radius: 8px; font-size: 11px; font-weight: 700; }
.ha-device-editor__cancel { border: 1px solid #d6dcde; color: #555d60; background: #fff; }
.ha-device-editor__save { border: 1px solid var(--ha-blue); color: #fff; background: var(--ha-blue); }
.ha-block-editor { display: flex; flex: 1 1 auto; min-height: 0; flex-direction: column; background: #fff; }
.ha-block-editor[hidden] { display: none; }
.ha-home-panel.is-collapsed .ha-block-editor { display: none; }
.ha-home-panel.is-block-editor-open .ha-home-panel__content { display: none; }
.ha-block-editor__header { display: flex; align-items: center; min-height: 63px; padding: 0 14px; gap: 10px; border-bottom: 1px solid #e2e5e7; }
.ha-block-editor__header h3 { margin: 0; overflow: hidden; max-width: 270px; font-size: 15px; text-overflow: ellipsis; white-space: nowrap; }
.ha-block-editor__body { flex: 1 1 auto; min-height: 0; padding: 15px; overflow-y: auto; }
.ha-block-editor__body > p { margin: 0 0 13px; color: #747a7d; font-size: 10px; line-height: 1.45; }
.ha-block-editor__title-field { margin-bottom: 17px; padding-bottom: 15px; border-bottom: 1px solid #e6e9ea; }
.ha-block-editor__title-field label { display: block; margin-bottom: 7px; color: #5e6568; font-size: 10px; font-weight: 700; }
.ha-block-editor__title-field input { width: 100%; height: 38px; padding: 0 10px; border: 1px solid #d5dbdd; border-radius: 8px; color: #303638; background: #fff; font-size: 12px; outline: none; }
.ha-block-editor__title-field input:focus { border-color: var(--ha-blue); box-shadow: 0 0 0 2px rgba(3,164,207,.12); }
.ha-block-editor__list { display: flex; flex-direction: column; gap: 7px; }
.ha-block-editor__item { display: grid; grid-template-columns: 1fr auto; align-items: center; min-height: 43px; padding: 0 8px 0 10px; gap: 8px; border: 1px solid #dfe4e6; border-radius: 9px; background: #fafcfc; }
.ha-block-editor__item label { display: flex; align-items: center; min-width: 0; gap: 8px; color: #34393b; font-size: 10px; font-weight: 700; }
.ha-block-editor__item label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ha-block-editor__item input { width: 16px; height: 16px; margin: 0; accent-color: var(--ha-blue); }
.ha-block-editor__item button { height: 27px; padding: 0 8px; border: 1px solid #ffb6a9; border-radius: 7px; color: #d75643; background: #fff; font-size: 8px; font-weight: 700; }
.ha-block-editor__empty { margin: 0; padding: 18px 10px; border: 1px dashed #ced5d8; border-radius: 9px; color: #858b8e; background: #fafcfc; font-size: 10px; text-align: center; }
.ha-block-editor__add { margin-top: 20px; padding-top: 14px; border-top: 1px solid #e6e9ea; }
.ha-block-editor__add > label { display: block; margin-bottom: 7px; color: #5e6568; font-size: 10px; font-weight: 700; }
.ha-block-editor__add > div { display: flex; gap: 7px; }
.ha-block-editor__add select { min-width: 0; height: 35px; flex: 1 1 auto; padding: 0 8px; border: 1px solid #d5dbdd; border-radius: 8px; color: #3e4447; background: #fff; font-size: 9px; }
.ha-block-editor__add-button { height: 35px; padding: 0 10px; border: 1px solid var(--ha-blue); border-radius: 8px; color: #fff; background: var(--ha-blue); font-size: 9px; font-weight: 700; white-space: nowrap; }
.ha-block-editor__add-button:disabled { border-color: #d6dcde; color: #969b9d; background: #eef1f2; }
.ha-block-editor__footer { display: flex; justify-content: space-between; min-height: 61px; padding: 12px 14px; gap: 8px; border-top: 1px solid #e2e5e7; }
.ha-block-editor__footer button { height: 35px; padding: 0 13px; border-radius: 8px; font-size: 10px; font-weight: 700; }
.ha-block-editor__reset { border: 1px solid #d6dcde; color: #596063; background: #fff; }
.ha-block-editor__done { border: 1px solid var(--ha-blue); color: #fff; background: var(--ha-blue); }
@media (max-width: 1450px) {
.ha-home-panel { flex-basis: 350px; width: 350px; }
.ha-summary-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 1220px) {
.ha-home-panel { flex-basis: 320px; width: 320px; }
}
@@ -0,0 +1,135 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home Assistant — Компоненты панели</title>
<link rel="stylesheet" href="components.css">
<link rel="stylesheet" href="styles.css">
</head>
<body class="components-page">
<header class="ha-component-header" aria-label="Панели редактора плана">
<div class="ha-view-tabs ha-view-tabs--compact" aria-label="Представление">
<span class="ha-view-tab active" aria-label="План">⌗</span>
<span class="ha-view-tab" aria-label="Панель">▣</span>
<span class="ha-view-tab" aria-label="Изображение">▧</span>
</div>
<div class="ha-context-row ha-context-row--compact">
<div class="ha-plan-label"><span class="ha-home-icon">▥</span><strong>План дома</strong></div>
<div class="ha-segmented ha-floor-switcher">
<span class="ha-segment active">Ground Floor <small>⚙</small></span>
<span class="ha-segment">First Floor <small>⚙</small></span>
<span class="ha-segment">Yard <small>⚙</small></span>
<span class="ha-segment ha-add">+</span>
</div>
<div class="ha-segmented ha-editor-switcher">
<span class="ha-segment active"><span>⌗</span> Редактор плана <b>×</b></span>
<span class="ha-segment"><span>⌘</span> Редактор устройств</span>
<span class="ha-segment"><span>⌁</span> Редактор подложки</span>
</div>
<span class="ha-device-count">50 устр.</span>
</div>
<div class="ha-tools-row ha-tools-row--compact" aria-label="Инструменты редактирования">
<span class="ha-tool-mode" aria-hidden="true">⌗</span>
<button class="ha-tool"><span class="ha-tool-icon">➤</span>Выбрать</button>
<button class="ha-tool active"><span class="ha-tool-icon">⌁</span>Стены</button>
<button class="ha-tool"><span class="ha-tool-icon">⌗</span>Колонна</button>
<button class="ha-tool"><span class="ha-tool-icon">▱</span>Объединить</button>
<button class="ha-tool"><span class="ha-tool-icon">⌁</span>Разделить</button>
<button class="ha-tool"><span class="ha-tool-icon">⛶</span>Размер</button>
<button class="ha-tool"><span class="ha-tool-icon">▯</span>Проём <span class="ha-chevron">⌄</span></button>
<button class="ha-tool"><span class="ha-tool-icon">◲</span>Граница</button>
<button class="ha-tool"><span class="ha-tool-icon">▦</span>Толщина</button>
<button class="ha-tool"><span class="ha-tool-icon">▱</span>Удалить комнату</button>
<span class="ha-history-actions"><button aria-label="Отменить">↶</button><button aria-label="Повторить">↷</button></span>
</div>
</header>
<main class="ha-component-stage">
<div class="ha-stage-grid" aria-hidden="true"></div>
<div class="ha-plan-fragment" aria-hidden="true">
<div class="ha-plan-room room-a">Kitchen &amp; Living</div>
<div class="ha-plan-room room-b">Hallway</div>
<div class="ha-plan-room room-c">Guest Bedroom</div>
</div>
<div class="ha-modal-backdrop"></div>
<section class="ha-device-modal" role="dialog" aria-modal="true" aria-labelledby="device-modal-title">
<header class="ha-modal-header">
<span class="ha-modal-close" aria-hidden="true">×</span>
<span class="ha-device-shapes" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
<h1 id="device-modal-title">Устройство на плане</h1>
</header>
<div class="ha-modal-body">
<div class="ha-field">
<label for="device-name">Имя (отображается на плане)</label>
<input id="device-name" type="text" value="Wall Lamp 3" readonly>
</div>
<section class="ha-field">
<div class="ha-field-label">Привязка к устройству HA</div>
<div class="ha-binding-card">
<label class="ha-choice"><input type="radio" name="binding"><span>Виртуальное устройство (без привязки)</span></label>
<div class="ha-choice-row">
<label class="ha-choice"><input type="radio" name="binding" checked><span>Выбрать из списка HA</span></label>
<label class="ha-switch-label"><span class="ha-switch"><i></i></span><span>Отображать сущности</span></label>
</div>
<div class="ha-select-box strong-select">
<strong>Wall Lamp 3</strong>
<span class="ha-entity">entity:light.wall_lamp_3</span>
<span class="ha-select-arrow">⌄</span>
</div>
</div>
</section>
<div class="ha-field">
<label for="room-select">Комната (переопределить размещение)</label>
<div class="ha-select-box" id="room-select"><span>Ground Floor · Kitchen &amp; Living</span><span class="ha-select-arrow">⌄</span></div>
</div>
<div class="ha-field action-field">
<label for="action-select">Действие по нажатию для этого устройства</label>
<div class="ha-select-box" id="action-select"><span>Переключить состояние</span><span class="ha-select-arrow">⌄</span></div>
<p>Цель: Wall Lamp 3 (light.wall_lamp_3).</p>
<p>Сейчас: выключено → включится.</p>
</div>
<label class="ha-switch-label confirmation"><span class="ha-switch"><i></i></span><span>Спрашивать подтверждение</span></label>
<section class="ha-light-sources">
<h2>Управляет другими источниками света</h2>
<p>При действии «Переключить» клик разом переключает все добавленные здесь источники.</p>
<p>Собственная сущность маркера управляется напрямую и сюда не добавляется; чтобы считать её светом, включите флаг ниже.</p>
<input type="text" placeholder="Поиск ламп и выключателей..." readonly>
</section>
<fieldset class="ha-source-type">
<legend>Является источником света <span class="ha-help">?</span></legend>
<label class="ha-choice"><input type="radio" name="source" checked><span>Автоматически (источник света)</span></label>
<label class="ha-choice"><input type="radio" name="source"><span>Всегда источник света</span></label>
</fieldset>
<div class="ha-modal-spacer" aria-hidden="true"></div>
</div>
<footer class="ha-modal-footer">
<div class="ha-footer-left">
<button class="ha-action-button"><span>◉̸</span>Скрыть</button>
<button class="ha-action-button danger"><span>▱</span>Удалить</button>
</div>
<div class="ha-footer-right">
<button class="ha-text-button">Отмена</button>
<button class="ha-save-button"><span>✓</span>Сохранить</button>
</div>
</footer>
</section>
</main>
</body>
</html>
@@ -0,0 +1,767 @@
.dashboard3-panel[hidden],
#legacy-home-panel[hidden],
.dashboard3-settings-dialog[hidden],
.dashboard3-entity-picker[hidden] { display: none !important; }
/* Dashboard 3 header — compact Home Assistant chrome from the supplied reference. */
.dashboard3-app-header {
padding: 0 14px 0 10px;
}
.dashboard3-view-tabs,
.dashboard3-header-actions,
.dashboard3-context-actions,
.dashboard3-zoom-group {
display: flex;
align-items: center;
}
.dashboard3-view-tabs {
align-self: stretch;
gap: 14px;
}
.dashboard3-header-actions {
gap: 18px;
}
.dashboard3-header-icon {
position: relative;
display: grid;
width: 26px;
height: 100%;
padding: 0;
place-items: center;
border: 0;
color: #242729;
background: transparent;
cursor: pointer;
}
.dashboard3-header-icon svg,
.dashboard3-context-icon svg,
.dashboard3-zoom-group svg {
display: block;
width: 17px;
height: 17px;
fill: none;
stroke: currentColor;
stroke-width: 1.9;
stroke-linecap: round;
stroke-linejoin: round;
}
.dashboard3-view-tab.active::after {
position: absolute;
right: -3px;
bottom: -1px;
left: -3px;
height: 2px;
content: "";
background: #242729;
}
.dashboard3-header-icon:hover,
.dashboard3-context-icon:hover,
.dashboard3-zoom-group button:hover {
color: #059bc1;
background: #f2f7f8;
}
.dashboard3-context-bar {
overflow: visible;
background: #fff;
}
.dashboard3-context-bar .ha-plan-label {
flex: 0 0 auto;
color: #17191a;
}
.ha-plan-symbol {
width: 17px;
height: 17px;
fill: #03a4cf;
stroke: #03a4cf;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.dashboard3-context-bar .ha-segmented {
background: #f0f0f0;
}
.dashboard3-context-bar .ha-floor-switcher {
flex: 0 0 auto;
}
.dashboard3-context-bar .ha-editor-switcher {
flex: 0 1 auto;
min-width: 0;
}
.dashboard3-context-bar .ha-editor-switcher .ha-segment {
background: transparent;
}
.dashboard3-context-actions {
flex: 0 0 auto;
margin-left: auto;
gap: 7px;
}
.dashboard3-zoom-group {
height: 31px;
padding: 0 3px;
overflow: hidden;
border-radius: 9px;
background: #f1f1f1;
}
.dashboard3-zoom-group button,
.dashboard3-context-icon {
display: grid;
width: 30px;
height: 30px;
padding: 0;
place-items: center;
border: 0;
color: #242729;
background: transparent;
font: inherit;
font-size: 17px;
font-weight: 600;
cursor: pointer;
}
.dashboard3-context-icon {
border: 1px solid #e1e4e5;
border-radius: 8px;
background: #fff;
}
.dashboard3-help-icon {
border-radius: 9px;
font-size: 13px;
}
.dashboard3-help-icon::before {
display: grid;
width: 15px;
height: 15px;
place-items: center;
border: 1.5px solid #242729;
border-radius: 50%;
content: "";
}
.dashboard3-help-icon {
position: relative;
color: transparent;
}
.dashboard3-help-icon::after {
position: absolute;
color: #242729;
content: "?";
font-size: 10px;
font-weight: 800;
}
.dashboard3-space-tab {
border: 0;
background: transparent;
font-family: inherit;
cursor: pointer;
}
.dashboard3-space-tab.active { background: var(--ha-blue); }
.dashboard3-summary-switcher {
flex: 0 0 auto;
overflow: hidden;
}
.dashboard3-summary-switcher .dashboard3-global-toggle {
width: auto;
height: 25px;
padding: 0 5px 0 10px;
border: 0;
border-radius: 7px 0 0 7px;
color: #555;
background: transparent;
font: inherit;
font-size: 11px;
font-weight: 700;
cursor: pointer;
}
.dashboard3-settings-toggle {
display: grid;
width: 25px;
height: 25px;
padding: 0 6px 0 2px;
place-items: center;
border: 0;
border-radius: 0 7px 7px 0;
color: #777;
background: transparent;
cursor: pointer;
}
.dashboard3-settings-toggle img { display: block; width: 13px; height: 13px; opacity: .55; }
.dashboard3-summary-switcher .dashboard3-global-toggle[aria-pressed="true"],
.dashboard3-summary-switcher .dashboard3-global-toggle[aria-pressed="true"] + .dashboard3-settings-toggle {
color: #fff;
background: var(--ha-blue);
}
.dashboard3-summary-switcher .dashboard3-global-toggle[aria-pressed="true"] + .dashboard3-settings-toggle img { filter: brightness(0) invert(1); opacity: .72; }
.dashboard3-summary-switcher .dashboard3-global-toggle:hover,
.dashboard3-settings-toggle:hover { background: #e5e5e5; }
.dashboard3-summary-switcher .dashboard3-global-toggle[aria-pressed="true"]:hover,
.dashboard3-summary-switcher .dashboard3-global-toggle[aria-pressed="true"] + .dashboard3-settings-toggle:hover { background: var(--ha-blue-dark); }
@media (max-width: 1500px) {
.dashboard3-header-actions { gap: 14px; }
.dashboard3-context-actions { gap: 5px; }
.dashboard3-context-bar .ha-editor-switcher .ha-segment { padding-inline: 7px; }
}
@media (max-width: 1280px) {
.dashboard3-context-bar .ha-editor-switcher .ha-segment span { display: none; }
.dashboard3-context-bar .ha-editor-switcher .ha-segment { padding-inline: 6px; font-size: 9px; }
.dashboard3-context-icon,
.dashboard3-zoom-group button { width: 27px; }
}
.dashboard-body {
position: relative;
}
.dashboard3-panel {
position: absolute;
z-index: 6;
top: 12px;
right: 12px;
display: block;
height: fit-content;
min-height: 0;
max-height: calc(100vh - var(--global-bar-height) - 68px);
border-left: 0;
background: transparent;
box-shadow: none;
}
.dashboard3-panel__content {
display: flex;
width: 100%;
min-height: 0;
max-height: calc(100vh - var(--global-bar-height) - 68px);
flex: 0 1 auto;
flex-direction: column;
padding: 10px;
overflow: hidden;
border: 1px solid #d9dcde;
border-radius: 12px;
background: #f5f7f8;
box-shadow: -5px 0 20px rgba(46, 58, 64, .1);
}
.dashboard3-space-context {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 29px;
margin-bottom: 8px;
padding: 0 9px;
border: 1px solid #dce7ea;
border-radius: 8px;
color: #7a8285;
background: #f8fbfc;
font-size: 9px;
}
.dashboard3-space-context strong { color: #3f494c; font-size: 9px; }
.dashboard3-blocks { display: flex; min-height: 0; flex: 0 1 auto; flex-direction: column; gap: 9px; overflow-y: auto; padding-right: 2px; scrollbar-color: #b8bec1 transparent; scrollbar-width: thin; }
.dashboard3-block {
flex: 0 0 auto;
overflow: hidden;
border: 1px solid #dde3e5;
border-radius: 11px;
background: #fff;
box-shadow: 0 1px 2px rgba(34, 45, 50, .04);
}
.dashboard3-block__header { display: flex; align-items: center; min-height: 43px; padding: 0 14px; gap: 8px; border-bottom: 1px solid #eef1f2; }
.dashboard3-block__mark { display: grid; width: 25px; height: 25px; flex: 0 0 25px; place-items: center; border-radius: 7px; color: #157d98; background: #e7f5f8; font-size: 11px; font-weight: 800; }
.dashboard3-block__header h3 { margin: 0; overflow: hidden; min-width: 0; flex: 1 1 auto; color: #343b3d; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.dashboard3-block__scope { max-width: 120px; overflow: hidden; padding: 4px 7px; border-radius: 10px; color: #677174; background: #f0f3f4; font-size: 8px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
.dashboard3-block__values { margin: 0; padding: 4px 11px 7px; }
.dashboard3-value {
display: grid;
min-height: 36px;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
align-items: center;
gap: 12px;
border-bottom: 1px solid #edf0f1;
}
.dashboard3-value:last-child { border-bottom: 0; }
.dashboard3-value dt,
.dashboard3-value dd { margin: 0; min-width: 0; }
.dashboard3-value dt { overflow: hidden; color: #697174; font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
.dashboard3-value dd { overflow: hidden; color: #30383a; font-size: 10px; font-weight: 800; text-align: right; text-overflow: ellipsis; white-space: nowrap; }
.dashboard3-empty {
display: grid;
min-height: 120px;
padding: 18px;
place-items: center;
border: 1px dashed #cfd9dc;
border-radius: 11px;
color: #899194;
background: #fafcfc;
font-size: 10px;
line-height: 1.45;
text-align: center;
}
.dashboard3-settings-dialog {
position: fixed;
z-index: 1000;
inset: 0;
display: grid;
padding: 16px;
place-items: center;
background: rgba(42, 49, 52, .42);
backdrop-filter: blur(2px);
}
.dashboard3-settings-dialog__window {
display: flex;
width: min(780px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
flex-direction: column;
overflow: hidden;
border: 1px solid #cfdadd;
border-radius: 15px;
background: #f8fbfc;
box-shadow: 0 18px 60px rgba(25, 34, 38, .26);
}
.dashboard3-settings-dialog__header {
display: flex;
min-height: 70px;
align-items: center;
justify-content: space-between;
padding: 15px 17px 12px;
gap: 12px;
border-bottom: 1px solid #dfe7e9;
background: #fff;
}
.dashboard3-settings-dialog__header h2 { margin: 0; color: #303638; font-size: 22px; line-height: 1.2; }
.dashboard3-settings-dialog__close { width: 31px; height: 31px; flex: 0 0 31px; }
.dashboard3-settings-dialog__body { min-height: 0; padding: 14px 17px 18px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #b8bec1 transparent; }
.dashboard3-general-settings,
.dashboard3-block-editor { border: 1px solid #dce5e7; border-radius: 11px; background: #fff; }
.dashboard3-general-settings { padding: 13px; }
.dashboard3-general-settings > header h3,
.dashboard3-block-editor__heading h3 { margin: 0; color: #343b3d; font-size: 13px; }
.dashboard3-general-settings__grid { display: grid; margin-top: 11px; grid-template-columns: 1fr 1fr; gap: 12px; }
.dashboard3-field > span,
.dashboard3-editor-field > span { display: block; margin-bottom: 5px; color: #667073; font-size: 9px; font-weight: 700; }
.dashboard3-field input,
.dashboard3-editor-field input,
.dashboard3-editor-field select,
.dashboard3-value-label {
width: 100%;
height: 36px;
padding: 0 9px;
border: 1px solid #d5dddf;
border-radius: 7px;
color: #303638;
background: #fff;
font: inherit;
font-size: 10px;
outline: none;
}
.dashboard3-field input:focus,
.dashboard3-editor-field input:focus,
.dashboard3-editor-field select:focus,
.dashboard3-value-label:focus { border-color: var(--ha-blue); box-shadow: 0 0 0 2px rgba(3, 164, 207, .12); }
.dashboard3-field input.is-invalid,
.dashboard3-editor-field input.is-invalid,
.dashboard3-editor-field select.is-invalid,
.dashboard3-value-label.is-invalid,
.dashboard3-entity-trigger.is-invalid { border-color: #dc675a; box-shadow: 0 0 0 2px rgba(220, 103, 90, .10); }
.dashboard3-field__error { display: block; min-height: 12px; margin-top: 3px; color: #c84f43; font-size: 8px; }
.dashboard3-switch-field {
display: flex;
min-height: 54px;
align-items: center;
justify-content: space-between;
padding: 4px 0;
gap: 12px;
border: 0;
border-radius: 0;
cursor: pointer;
}
.dashboard3-switch-field > span:first-child { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
.dashboard3-switch-field strong { color: #434b4e; font-size: 9px; }
.dashboard3-switch-field small { color: #7d8588; font-size: 8px; line-height: 1.35; }
.dashboard3-switch-field input { position: absolute; width: 1px; height: 1px; overflow: hidden; opacity: 0; }
.dashboard3-switch { position: relative; width: 36px; height: 21px; flex: 0 0 36px; border: 1px solid #b7bec1; border-radius: 12px; background: #d8dddf; transition: .16s ease; }
.dashboard3-switch::after { position: absolute; top: 2px; left: 2px; width: 15px; height: 15px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, .2); content: ""; transition: transform .16s ease; }
.dashboard3-switch-field input:checked + .dashboard3-switch { border-color: var(--ha-blue); background: var(--ha-blue); }
.dashboard3-switch-field input:checked + .dashboard3-switch::after { transform: translateX(15px); }
.dashboard3-switch-field input:focus-visible + .dashboard3-switch { outline: 2px solid rgba(3, 164, 207, .28); outline-offset: 2px; }
.dashboard3-block-editor { margin-top: 12px; padding: 13px; }
.dashboard3-block-editor__heading { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 10px; gap: 12px; }
.dashboard3-block-editor__heading p { margin: 3px 0 0; color: #7b8386; font-size: 8px; }
.dashboard3-block-count { padding: 4px 7px; border-radius: 10px; color: #597176; background: #edf6f8; font-size: 8px; font-weight: 700; }
.dashboard3-block-editor__list { display: flex; flex-direction: column; gap: 10px; }
.dashboard3-editor-block { overflow: visible; border: 1px solid #dce3e5; border-radius: 10px; background: #fbfdfd; }
.dashboard3-editor-block__header { display: grid; min-height: 50px; padding: 7px 8px; grid-template-columns: 61px minmax(120px, 1fr) 160px 31px 31px; align-items: center; gap: 7px; border-bottom: 1px solid #e5eaec; background: #fff; border-radius: 10px 10px 0 0; }
.dashboard3-order-buttons { display: grid; grid-template-columns: repeat(2, 29px); align-items: center; gap: 3px; }
.dashboard3-order-button { display: grid; width: 29px; height: 29px; padding: 0; place-items: center; border: 1px solid #d8dcde; border-radius: 6px; color: #586164; background: #fff; font: inherit; font-size: 13px; line-height: 1; cursor: pointer; }
.dashboard3-order-button:hover:not(:disabled) { border-color: #83c9da; color: #087f9d; background: #f2fafc; }
.dashboard3-order-button:disabled { opacity: .32; cursor: default; }
.dashboard3-block-title { width: 100%; height: 34px; min-width: 0; padding: 0 8px; border: 1px solid #d5dddf; border-radius: 7px; color: #343b3d; background: #fff; font: inherit; font-size: 11px; font-weight: 800; outline: none; }
.dashboard3-block-title:hover { border-color: #dce4e6; background: #fafcfc; }
.dashboard3-block-title:focus { border-color: var(--ha-blue); background: #fff; box-shadow: 0 0 0 2px rgba(3, 164, 207, .12); }
.dashboard3-block-title.is-invalid { border-color: #dc675a; background: #fff7f6; }
.dashboard3-scope-select { width: 100%; height: 34px; min-width: 0; padding: 0 26px 0 8px; border: 1px solid #d5dddf; border-radius: 7px; color: #4e575a; background: #fff; font: inherit; font-size: 9px; outline: none; cursor: pointer; }
.dashboard3-scope-select:focus { border-color: var(--ha-blue); box-shadow: 0 0 0 2px rgba(3, 164, 207, .12); }
.dashboard3-scope-select.is-invalid { border-color: #dc675a; box-shadow: 0 0 0 2px rgba(220, 103, 90, .10); }
.dashboard3-visibility { display: grid; width: 31px; height: 31px; padding: 0; place-items: center; border: 1px solid #acd9e4; border-radius: 7px; background: #f1fafc; cursor: pointer; }
.dashboard3-visibility.is-off { border-color: #d8dcde; background: #fff; }
.dashboard3-visibility img { width: 16px; height: 16px; opacity: .72; }
.dashboard3-editor-block__body { padding: 10px; }
.dashboard3-editor-values { display: flex; flex-direction: column; gap: 6px; }
.dashboard3-editor-value { display: grid; min-height: 43px; padding: 4px; grid-template-columns: 61px minmax(135px, .9fr) minmax(190px, 1.25fr) 29px; align-items: start; gap: 6px; border: 1px solid #e2e7e9; border-radius: 8px; background: #fff; }
.dashboard3-value-label { height: 34px; min-width: 0; }
.dashboard3-entity-trigger { display: flex; width: 100%; min-width: 0; min-height: 34px; padding: 5px 8px; align-items: center; justify-content: space-between; gap: 8px; border: 1px solid #d5dddf; border-radius: 7px; color: #3b4345; background: #fff; font: inherit; text-align: left; cursor: pointer; }
.dashboard3-entity-trigger__text { display: flex; min-width: 0; flex-direction: column; gap: 1px; }
.dashboard3-entity-trigger strong,
.dashboard3-entity-trigger small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dashboard3-entity-trigger strong { font-size: 9px; }
.dashboard3-entity-trigger small { color: #828a8d; font-size: 8px; font-weight: 400; }
.dashboard3-entity-trigger > b { color: #7c8588; font-size: 10px; }
.dashboard3-editor-value__remove { width: 29px; height: 29px; margin-top: 2px; padding: 0; border: 0; border-radius: 7px; color: #ca5548; background: #fff3f1; font-size: 15px; cursor: pointer; }
.dashboard3-editor-value__error { display: none; grid-column: 2 / 4; margin: -2px 2px 2px; color: #c84f43; font-size: 8px; }
.dashboard3-editor-value.has-error .dashboard3-editor-value__error { display: block; }
.dashboard3-editor-block__footer { display: flex; justify-content: flex-end; margin-top: 10px; padding-top: 9px; border-top: 1px solid #e8edef; }
.dashboard3-delete-block { min-height: 30px; padding: 0 11px; border: 1px solid #e2968c; border-radius: 7px; color: #c84f43; background: #fff; font: inherit; font-size: 9px; font-weight: 700; cursor: pointer; }
.dashboard3-delete-block:hover { border-color: #d96759; background: #fff5f3; }
.dashboard3-add-value,
.dashboard3-add-block { display: flex; align-items: center; justify-content: center; gap: 6px; border: 1px dashed #78c4d6; color: #087f9d; background: #f2fafc; font: inherit; font-size: 9px; font-weight: 700; cursor: pointer; }
.dashboard3-add-value { width: 100%; min-height: 32px; margin-top: 7px; border-radius: 8px; }
.dashboard3-add-block { display: inline-flex; width: auto; min-height: 35px; margin-top: 10px; padding: 0 12px; border-radius: 9px; }
.dashboard3-add-value img,
.dashboard3-add-block img { width: 14px; height: 14px; opacity: .7; }
.dashboard3-add-value:disabled,
.dashboard3-add-block:disabled { border-color: #d9dfe1; color: #9ba1a4; background: #f7f8f8; cursor: not-allowed; }
.dashboard3-add-value:hover:not(:disabled),
.dashboard3-add-block:hover:not(:disabled) { color: #fff; background: var(--ha-blue); }
.dashboard3-add-value:hover:not(:disabled) img,
.dashboard3-add-block:hover:not(:disabled) img { filter: brightness(0) invert(1); opacity: 1; }
.dashboard3-settings-dialog__footer { display: flex; min-height: 61px; align-items: center; justify-content: flex-end; padding: 11px 17px; gap: 8px; border-top: 1px solid #dfe7e9; background: #fff; }
.dashboard3-settings-status { margin-right: auto; color: #c25246; font-size: 9px; }
.dashboard3-settings-status.is-ready { color: #5f777c; }
.dashboard3-button { height: 35px; padding: 0 14px; border-radius: 8px; font: inherit; font-size: 10px; font-weight: 700; cursor: pointer; }
.dashboard3-button--secondary { border: 1px solid #d5dcde; color: #555e61; background: #fff; }
.dashboard3-button--primary { border: 1px solid var(--ha-blue); color: #fff; background: var(--ha-blue); }
.dashboard3-button--primary:disabled { border-color: #b9d4da; color: #eef7f8; background: #91c7d4; cursor: not-allowed; }
.dashboard3-entity-picker {
position: fixed;
z-index: 1100;
display: flex;
width: min(420px, calc(100vw - 24px));
max-height: min(430px, calc(100vh - 24px));
flex-direction: column;
overflow: hidden;
border: 1px solid #cfdadd;
border-radius: 11px;
background: #fff;
box-shadow: 0 14px 38px rgba(25, 34, 38, .25);
}
.dashboard3-entity-picker__search { display: flex; min-height: 47px; align-items: center; padding: 7px 9px; gap: 7px; border-bottom: 1px solid #e3e8ea; }
.dashboard3-entity-picker__search > span { color: #657074; font-size: 15px; }
.dashboard3-entity-picker__search input { width: 100%; height: 33px; padding: 0 8px; border: 1px solid #d5dddf; border-radius: 7px; color: #303638; font: inherit; font-size: 10px; outline: none; }
.dashboard3-entity-picker__search input:focus { border-color: var(--ha-blue); box-shadow: 0 0 0 2px rgba(3, 164, 207, .12); }
.dashboard3-entity-picker__list { min-height: 80px; overflow-y: auto; padding: 5px; scrollbar-width: thin; }
.dashboard3-entity-option { display: flex; width: 100%; min-height: 43px; padding: 6px 8px; align-items: center; justify-content: space-between; gap: 10px; border: 0; border-radius: 7px; color: #343b3d; background: #fff; font: inherit; text-align: left; cursor: pointer; }
.dashboard3-entity-option:hover,
.dashboard3-entity-option:focus-visible { background: #edf8fa; outline: none; }
.dashboard3-entity-option__name { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
.dashboard3-entity-option__name strong,
.dashboard3-entity-option__name small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dashboard3-entity-option__name strong { font-size: 10px; }
.dashboard3-entity-option__name small { color: #7d8588; font-size: 8px; }
.dashboard3-entity-option__state { flex: 0 0 auto; color: #3e616a; font-size: 9px; font-weight: 700; }
.dashboard3-entity-option.is-selected { background: #e5f5f8; }
.dashboard3-entity-picker__empty { padding: 18px; color: #858d90; font-size: 9px; text-align: center; }
.dashboard3-entity-picker__hint { margin: 0; padding: 8px 10px; border-top: 1px solid #e3e8ea; color: #717a7d; background: #f7fafb; font-size: 8px; line-height: 1.4; }
button:focus-visible,
.dashboard3-space-tab:focus-visible { outline: 2px solid rgba(3, 164, 207, .35); outline-offset: 2px; }
@media (max-width: 1450px) {
.dashboard3-panel { flex-basis: 350px; width: 350px; }
}
@media (max-width: 1220px) {
.dashboard3-panel { flex-basis: 320px; width: 320px; }
.dashboard3-block__scope { display: none; }
}
@media (max-width: 760px) {
.dashboard3-settings-dialog { padding: 8px; }
.dashboard3-settings-dialog__window { width: calc(100vw - 16px); max-height: calc(100vh - 16px); }
.dashboard3-general-settings__grid,
.dashboard3-block-options { grid-template-columns: 1fr; }
.dashboard3-editor-block__header { grid-template-columns: 61px minmax(0, 1fr) 31px 31px; }
.dashboard3-scope-select { grid-column: 1 / -1; grid-row: 2; }
.dashboard3-editor-value { grid-template-columns: 61px minmax(0, 1fr) 29px; }
.dashboard3-entity-trigger { grid-column: 2 / 3; }
.dashboard3-editor-value__remove { grid-column: 3; grid-row: 1; }
.dashboard3-editor-value__error { grid-column: 2 / 4; }
}
/* Dashboard 5: sorting controls, explicit scope, data states and local view settings. */
.dashboard4-size-dialog[hidden] { display: none !important; }
.dashboard3-editor-block.is-dragging,
.dashboard3-editor-value.is-dragging { opacity: .5; }
.dashboard3-editor-block.is-drop-target,
.dashboard3-editor-value.is-drop-target { border-color: var(--ha-blue); box-shadow: 0 0 0 2px rgba(3, 164, 207, .15); }
.dashboard4-drag-handle { display: grid; width: 26px; height: 30px; place-items: center; border: 0; color: #7f8b8f; background: transparent; cursor: grab; font-size: 17px; user-select: none; }
.dashboard4-drag-handle:active { cursor: grabbing; }
.dashboard3-editor-block__header { grid-template-columns: 26px 61px minmax(120px, 1fr) 31px 31px; }
.dashboard4-block-drag { grid-column: 1; grid-row: 1; }
.dashboard3-editor-block__header > .dashboard3-order-buttons { grid-column: 2; grid-row: 1; }
.dashboard3-editor-block__header > .dashboard3-block-title { grid-column: 3; grid-row: 1; }
.dashboard3-editor-block__header > .dashboard3-visibility { grid-column: 4; grid-row: 1; }
.dashboard4-scope-controls { display: grid; min-width: 0; grid-column: 3 / -1; grid-row: 2; grid-template-columns: minmax(150px, .9fr) minmax(160px, 1fr); gap: 5px; }
.dashboard4-scope-controls > select[hidden] { display: none; }
.dashboard4-scope-controls:has(.dashboard4-space-select[hidden]) { grid-template-columns: 1fr; }
.dashboard4-scope-warning { margin: 0 0 9px; padding: 7px 9px; border: 1px solid #e4b569; border-radius: 7px; color: #805514; background: #fff9eb; font-size: 9px; }
.dashboard3-editor-value { grid-template-columns: 27px 61px minmax(125px, .9fr) minmax(180px, 1.2fr) 29px; }
.dashboard3-editor-value__error { grid-column: 3 / 5; }
.dashboard4-value--loading dd { color: #738084; }
.dashboard4-value--stale dd { color: #9a650d; }
.dashboard4-value--unavailable dd { color: #b2443b; }
.dashboard3-value dd small { display: block; margin-top: 1px; color: inherit; font-size: 7px; font-weight: 600; }
.dashboard3-entity-option__state { display: flex; flex-direction: column; align-items: flex-end; }
.dashboard3-entity-option__state small { color: #a55a21; font-size: 7px; }
.dashboard4-entity--unavailable .dashboard3-entity-option__state { color: #b2443b; }
.dashboard4-entity--stale .dashboard3-entity-option__state { color: #9a650d; }
.dashboard4-kiosk-toggle { min-width: 44px; height: 27px; padding: 0 8px; border: 1px solid #d9dfe1; border-radius: 7px; color: #566064; background: #fff; font: inherit; font-size: 9px; font-weight: 700; cursor: pointer; }
.dashboard4-kiosk-toggle[aria-pressed="true"] { color: #fff; border-color: var(--ha-blue); background: var(--ha-blue); }
.dashboard4-size-dialog { position: fixed; z-index: 1200; inset: 0; display: grid; padding: 16px; place-items: center; background: rgba(42, 49, 52, .42); backdrop-filter: blur(2px); }
.dashboard4-size-dialog__window { width: min(430px, calc(100vw - 32px)); overflow: hidden; border: 1px solid #d6dfe1; border-radius: 14px; background: #fff; box-shadow: 0 18px 55px rgba(25, 34, 38, .25); }
.dashboard4-size-dialog__window > header { display: flex; min-height: 58px; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #e3e8ea; }
.dashboard4-size-dialog h2 { margin: 0; font-size: 17px; }
.dashboard4-size-close { width: 31px; height: 31px; border: 0; border-radius: 7px; background: transparent; font-size: 21px; cursor: pointer; }
.dashboard4-size-dialog label { display: grid; padding: 14px 16px 3px; grid-template-columns: 1fr auto; gap: 9px 12px; color: #4e585b; font-size: 12px; }
.dashboard4-size-dialog output { font-weight: 800; }
.dashboard4-size-dialog input[type="range"] { grid-column: 1 / -1; width: 100%; accent-color: var(--ha-blue); }
.dashboard4-size-dialog footer { display: flex; justify-content: flex-end; padding: 14px 16px; gap: 8px; }
.dashboard4-size-reset { min-height: 35px; padding: 0 12px; border: 1px solid #d5dcde; border-radius: 8px; background: #fff; cursor: pointer; }
.floor-plan .device { transform: scale(var(--dashboard4-device-scale, 1)); }
.floor-plan .room-name { font-size: calc(11px * var(--dashboard4-room-text-scale, 1)); }
html[data-theme="dark"] body { color: #e8edef; background: #14191b; }
html[data-theme="dark"] .sidebar,
html[data-theme="dark"] .global-bar,
html[data-theme="dark"] .dashboard3-context-bar,
html[data-theme="dark"] .dashboard3-panel__content,
html[data-theme="dark"] .dashboard3-block,
html[data-theme="dark"] .dashboard3-settings-dialog__window,
html[data-theme="dark"] .dashboard3-settings-dialog__header,
html[data-theme="dark"] .dashboard3-settings-dialog__footer,
html[data-theme="dark"] .dashboard3-general-settings,
html[data-theme="dark"] .dashboard3-block-editor,
html[data-theme="dark"] .dashboard3-editor-block,
html[data-theme="dark"] .dashboard3-editor-block__header,
html[data-theme="dark"] .dashboard3-editor-value,
html[data-theme="dark"] .dashboard4-size-dialog__window { color: #e8edef; border-color: #3a4549; background: #22292c; }
html[data-theme="dark"] input,
html[data-theme="dark"] select,
html[data-theme="dark"] .dashboard3-entity-trigger,
html[data-theme="dark"] .dashboard3-order-button,
html[data-theme="dark"] .dashboard4-kiosk-toggle,
html[data-theme="dark"] .dashboard4-size-reset { color: #e8edef; border-color: #48555a; background: #1b2124; }
html[data-theme="dark"] .dashboard3-block__header h3,
html[data-theme="dark"] .dashboard3-value dd,
html[data-theme="dark"] .dashboard3-settings-dialog h2,
html[data-theme="dark"] .dashboard3-settings-dialog h3 { color: #f2f5f6; }
html[data-theme="dark"] .dashboard3-value { border-color: #333e42; }
html[data-theme="dark"] .dashboard3-value dt,
html[data-theme="dark"] .dashboard3-block-editor__heading p { color: #aeb8bb; }
body.dashboard5-kiosk .app-shell { grid-template-columns: 1fr; }
body.dashboard5-kiosk .sidebar,
body.dashboard5-kiosk .global-bar,
body.dashboard5-kiosk .dashboard3-context-bar > :not(.dashboard3-context-actions) { display: none; }
body.dashboard5-kiosk .workspace { grid-column: 1; }
body.dashboard5-kiosk .dashboard3-context-bar { position: fixed; z-index: 30; top: 10px; right: 10px; width: auto; min-height: 42px; padding: 6px; border: 1px solid #d8dfe1; border-radius: 10px; background: rgba(255,255,255,.94); box-shadow: 0 5px 18px rgba(25,34,38,.16); }
body.dashboard5-kiosk .dashboard3-context-actions { display: flex; }
body.dashboard5-kiosk .dashboard-body { height: 100vh; }
html[data-theme="dark"] body.dashboard5-kiosk .dashboard3-context-bar { border-color: #48555a; background: rgba(34, 41, 44, .96); }
@media (max-width: 900px) {
.dashboard3-editor-block__header { grid-template-columns: 26px 61px minmax(0, 1fr) 31px 31px; }
.dashboard4-scope-controls { grid-column: 3 / -1; grid-row: 2; grid-template-columns: 1fr; }
.dashboard3-editor-value { grid-template-columns: 27px 61px minmax(0, 1fr) 29px; }
.dashboard3-entity-trigger { grid-column: 3 / 4; }
.dashboard3-editor-value__remove { grid-column: 4; grid-row: 1; }
.dashboard3-editor-value__error { grid-column: 3 / 5; }
}
/* Dashboard 5: one floating adaptive information panel. */
.dashboard5-summary-control {
display: flex;
height: 29px;
flex: 0 0 auto;
overflow: hidden;
border: 1px solid #d8dfe1;
border-radius: 8px;
background: #fff;
box-shadow: 0 1px 2px rgba(30, 44, 49, .05);
}
.dashboard5-summary-control .dashboard3-settings-toggle,
.dashboard5-summary-control .dashboard3-global-toggle {
display: grid;
width: 31px;
height: 27px;
padding: 0;
place-items: center;
border: 0;
border-radius: 0;
color: #586367;
background: transparent;
cursor: pointer;
}
.dashboard5-summary-control .dashboard3-settings-toggle {
border-right: 1px solid #d8dfe1;
}
.dashboard5-summary-control .dashboard3-settings-toggle img {
width: 15px;
height: 15px;
opacity: .7;
}
.dashboard5-summary-control .dashboard3-global-toggle img {
display: block;
width: 17px;
height: 17px;
opacity: .74;
}
.dashboard5-summary-control .dashboard3-settings-toggle:hover,
.dashboard5-summary-control .dashboard3-global-toggle:hover { background: #edf2f3; }
.dashboard5-summary-control .dashboard3-global-toggle[aria-pressed="true"] { color: #fff; background: var(--ha-blue); }
.dashboard5-summary-control .dashboard3-global-toggle[aria-pressed="true"] img { filter: brightness(0) invert(1); opacity: .92; }
.dashboard5-summary-control .dashboard3-global-toggle[aria-pressed="true"]:hover { background: var(--ha-blue-dark); }
.dashboard5-summary-control button:focus-visible { position: relative; z-index: 1; outline: 2px solid rgba(3, 164, 207, .42); outline-offset: -2px; }
.dashboard3-panel {
top: 12px;
right: 12px;
bottom: auto;
left: auto;
width: max-content;
min-width: 280px;
max-width: min(420px, calc(100% - 24px));
height: fit-content;
max-height: calc(100% - 24px);
flex-basis: auto;
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: translateX(18px);
transition: opacity .19s ease, transform .19s ease, visibility 0s linear .19s;
}
.dashboard3-panel.is-visible {
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: translateX(0);
transition-delay: 0s;
}
.dashboard3-panel__content {
width: max-content;
min-width: 280px;
max-width: min(420px, calc(100vw - 24px));
max-height: calc(100% - 24px);
padding: 0;
overflow: hidden;
box-shadow: 0 8px 26px rgba(40, 54, 60, .16);
}
.dashboard5-panel-header {
display: flex;
min-height: 48px;
flex: 0 0 auto;
align-items: center;
padding: 0 14px;
border-bottom: 1px solid #e1e7e9;
background: #fff;
}
.dashboard5-panel-header h2 {
max-width: 100%;
margin: 0;
overflow: hidden;
color: #30383b;
font-size: 14px;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.dashboard3-blocks {
width: 100%;
padding: 10px;
}
.dashboard3-value {
grid-template-columns: minmax(0, 1fr) max-content;
}
.dashboard3-panel.dashboard5-panel-bottom {
top: auto;
right: auto;
bottom: 12px;
left: 50%;
width: max-content;
min-width: min(360px, calc(100% - 24px));
max-width: calc(100% - 24px);
max-height: min(60%, calc(100% - 24px));
transform: translate(-50%, 18px);
}
.dashboard3-panel.dashboard5-panel-bottom.is-visible { transform: translate(-50%, 0); }
.dashboard3-panel.dashboard5-panel-bottom .dashboard3-panel__content {
width: max-content;
min-width: min(360px, calc(100vw - 24px));
max-width: calc(100vw - 24px);
max-height: min(60vh, calc(100vh - 24px));
}
/* Blocks are always expanded, including in the settings dialog. */
.dashboard3-editor-block__header { grid-template-columns: 26px 61px minmax(120px, 1fr) 31px; }
.dashboard3-editor-block__header > .dashboard3-visibility { grid-column: 4; grid-row: 1; }
.dashboard4-scope-controls { grid-column: 3 / -1; }
body.dashboard5-kiosk .dashboard3-panel:not(.dashboard5-panel-bottom) {
top: 72px;
max-height: calc(100vh - 84px);
}
body.dashboard5-kiosk .dashboard3-panel:not(.dashboard5-panel-bottom) .dashboard3-panel__content { max-height: calc(100vh - 84px); }
body.dashboard5-kiosk .dashboard3-panel.dashboard5-panel-bottom { top: auto; bottom: 12px; }
html[data-theme="dark"] .dashboard5-summary-control { border-color: #48555a; background: #1b2124; }
html[data-theme="dark"] .dashboard5-summary-control .dashboard3-settings-toggle { border-color: #48555a; }
html[data-theme="dark"] .dashboard5-summary-control .dashboard3-settings-toggle:hover,
html[data-theme="dark"] .dashboard5-summary-control .dashboard3-global-toggle:hover { background: #303b3f; }
html[data-theme="dark"] .dashboard5-panel-header { border-color: #3a4549; background: #22292c; }
html[data-theme="dark"] .dashboard5-panel-header h2 { color: #f2f5f6; }
@media (max-width: 900px) {
.dashboard3-editor-block__header { grid-template-columns: 26px 61px minmax(0, 1fr) 31px; }
.dashboard4-scope-controls { grid-column: 3 / -1; }
}
@media (prefers-reduced-motion: reduce) {
.dashboard3-panel { transition: none; }
}
@@ -0,0 +1,756 @@
(() => {
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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 `
<div class="dashboard3-value dashboard4-value--${resolved.status}">
<dt>${escapeHtml(value.label)}</dt>
<dd>${escapeHtml(resolved.text)}${statusLabel ? `<small>${escapeHtml(statusLabel)}</small>` : ""}</dd>
</div>`;
}).join("");
const card = document.createElement("section");
card.className = "dashboard3-block";
card.innerHTML = `
<header class="dashboard3-block__header">
<h3>${escapeHtml(block.title)}</h3>
</header>
<dl class="dashboard3-block__values">${valuesHtml || '<div class="dashboard3-empty">В блоке пока нет значений.</div>'}</dl>`;
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 = [
'<option value="">Выберите пространство</option>',
block.scope.spaceId && !knownSpace ? `<option value="${escapeHtml(block.scope.spaceId)}" selected>Недоступно · ${escapeHtml(block.scope.spaceId)}</option>` : "",
...spaces.map((space) => `<option value="${space.id}"${block.scope.spaceId === space.id ? " selected" : ""}>${escapeHtml(space.name)}</option>`)
].join("");
const valuesHtml = block.values.map((value, index) => {
const source = sourcePresentation(value.source);
return `
<div class="dashboard3-editor-value" data-value-id="${value.id}">
<span class="dashboard4-drag-handle dashboard4-value-drag" draggable="true" title="Перетащить значение" aria-label="Перетащить значение">⠿</span>
<span class="dashboard3-order-buttons" role="group" aria-label="Изменить порядок значения ${index + 1}">
<button class="dashboard3-order-button dashboard3-value-up" type="button" aria-label="Переместить значение ${index + 1} выше" title="Выше"${index === 0 ? " disabled" : ""}>↑</button>
<button class="dashboard3-order-button dashboard3-value-down" type="button" aria-label="Переместить значение ${index + 1} ниже" title="Ниже"${index === block.values.length - 1 ? " disabled" : ""}>↓</button>
</span>
<input class="dashboard3-value-label" type="text" value="${escapeHtml(value.label)}" maxlength="64" placeholder="Название" aria-label="Название значения ${index + 1}">
<button class="dashboard3-entity-trigger" type="button" aria-label="Выбрать сущность для значения ${index + 1}">
<span class="dashboard3-entity-trigger__text"><strong>${escapeHtml(source.name)}</strong><small>${escapeHtml(source.detail)}</small></span><b>⌄</b>
</button>
<button class="dashboard3-editor-value__remove" type="button" aria-label="Удалить значение ${index + 1}" title="Удалить значение">×</button>
<small class="dashboard3-editor-value__error" aria-live="polite"></small>
</div>`;
}).join("");
article.innerHTML = `
<header class="dashboard3-editor-block__header">
<span class="dashboard4-drag-handle dashboard4-block-drag" draggable="true" title="Перетащить блок" aria-label="Перетащить блок">⠿</span>
<span class="dashboard3-order-buttons" role="group" aria-label="Изменить порядок блока ${escapeHtml(block.title || blockIndex + 1)}">
<button class="dashboard3-order-button dashboard3-block-up" type="button" aria-label="Переместить блок выше" title="Выше"${blockIndex === 0 ? " disabled" : ""}>↑</button>
<button class="dashboard3-order-button dashboard3-block-down" type="button" aria-label="Переместить блок ниже" title="Ниже"${blockIndex === draft.blocks.length - 1 ? " disabled" : ""}>↓</button>
</span>
<input class="dashboard3-block-title" type="text" value="${escapeHtml(block.title)}" maxlength="48" placeholder="Название блока" aria-label="Название блока">
<span class="dashboard4-scope-controls">
<select class="dashboard3-scope-select dashboard4-scope-type" aria-label="Область отображения блока"><option value="all"${block.scope.type === "all" ? " selected" : ""}>Все пространства</option><option value="space"${block.scope.type === "space" ? " selected" : ""}>Определённое пространство</option></select>
<select class="dashboard3-scope-select dashboard4-space-select" aria-label="Выберите пространство"${block.scope.type === "all" ? " hidden" : ""}>${spaceOptions}</select>
</span>
<button class="dashboard3-visibility${block.visible ? "" : " is-off"}" type="button" aria-pressed="${block.visible}" aria-label="${block.visible ? "Скрыть блок" : "Показать блок"}" title="${block.visible ? "Скрыть блок" : "Показать блок"}"><img src="./svg/${block.visible ? "eye.svg" : "eye-off.svg"}" alt="" aria-hidden="true"></button>
</header>
<div class="dashboard3-editor-block__body">
${block.scope.type === "space" && block.scope.spaceId && !knownSpace ? '<p class="dashboard4-scope-warning" role="status">Выбранное пространство недоступно. Привязка сохранена и требует внимания.</p>' : ""}
<div class="dashboard3-editor-values">${valuesHtml || '<div class="dashboard3-empty">В блоке пока нет значений.</div>'}</div>
<button class="dashboard3-add-value" type="button"${block.values.length >= limits.values ? " disabled" : ""}><img src="./svg/plus.svg" alt="" aria-hidden="true">Добавить значение</button>
<footer class="dashboard3-editor-block__footer"><button class="dashboard3-delete-block" type="button">Удалить блок</button></footer>
</div>`;
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 = `<span class="dashboard3-entity-option__name"><strong>${escapeHtml(entity.name)}${entity.template ? " · Template" : ""}</strong><small>${escapeHtml(entity.entityId)}</small></span><span class="dashboard3-entity-option__state">${escapeHtml(entity.state)}${statusLabel ? `<small>${escapeHtml(statusLabel)}</small>` : ""}</span>`;
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);
})();
@@ -0,0 +1,496 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home Assistant — Dashboard 5</title>
<link rel="stylesheet" href="components.css">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="dashboard-5.css?v=5.4">
<script src="dashboard-5.js?v=5.1" defer></script>
</head>
<body>
<div class="app-shell">
<aside class="sidebar" aria-label="Основная навигация">
<div class="brand-row">
<span class="nav-icon hamburger" aria-hidden="true">☰</span>
<span class="brand-name">Home Assistant</span>
</div>
<nav class="side-nav">
<a class="nav-item" href="#"><span class="nav-icon">⌂</span><span>Обзор</span></a>
<a class="nav-item active" href="#"><span class="nav-icon plan-glyph">⌗</span><span>House plan</span></a>
<a class="nav-item" href="#"><span class="nav-icon">◆</span><span>Map</span></a>
<a class="nav-item" href="#"><span class="nav-icon lightning">ϟ</span><span>Энергия</span></a>
<a class="nav-item" href="#"><span class="nav-icon">☷</span><span>Активность</span></a>
<a class="nav-item" href="#"><span class="nav-icon">▥</span><span>История</span></a>
<a class="nav-item" href="#"><span class="nav-icon">▣</span><span>Календарь</span></a>
<a class="nav-item" href="#"><span class="nav-icon">▶</span><span>Мультимедиа</span></a>
<a class="nav-item" href="#"><span class="nav-icon">☷</span><span>Списки дел</span></a>
</nav>
<div class="sidebar-footer">
<a class="nav-item" href="#"><span class="nav-icon">⚙</span><span>Настройки</span><span class="badge">4</span></a>
<a class="nav-item" href="#"><span class="nav-icon">♟</span><span>Уведомления</span><span class="badge">1</span></a>
<a class="nav-item profile" href="#"><span class="avatar">D</span><span>Demo</span></a>
</div>
</aside>
<section class="workspace">
<header class="global-bar dashboard3-app-header">
<div class="dashboard3-view-tabs" aria-label="Варианты представления">
<button class="dashboard3-header-icon dashboard3-view-tab active" type="button" aria-label="План дома" aria-current="page">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 3h7v7H4zM13 3h7v4h-7zM13 9h7v12h-7zM4 12h7v9H4z"/><path d="M7 3v3M17 3v2M4 16h3M17 14h3M13 17h3"/></svg>
</button>
<button class="dashboard3-header-icon dashboard3-view-tab" type="button" aria-label="Карточки">
<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="1.5"/><path d="M7 8h5v4H7zM14 8h3M14 11h3M7 15h10"/></svg>
</button>
<button class="dashboard3-header-icon dashboard3-view-tab" type="button" aria-label="Изображение">
<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="1.5"/><circle cx="9" cy="9" r="1.4"/><path d="m6.5 17 4-4 2.7 2.5 2.1-2.1 2.2 3.6"/></svg>
</button>
</div>
<div class="dashboard3-header-actions" aria-label="Глобальные действия">
<button class="dashboard3-header-icon" type="button" aria-label="Добавить"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
<button class="dashboard3-header-icon" type="button" aria-label="Поиск"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="10.5" cy="10.5" r="5.5"/><path d="m15 15 4.5 4.5"/></svg></button>
<button class="dashboard3-header-icon" type="button" aria-label="Сообщения"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5h16v12H9l-4 3v-3H4z"/><path d="M8 10h1M12 10h1M16 10h1"/></svg></button>
<button class="dashboard3-header-icon" type="button" aria-label="Редактировать"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 17-.7 3 3-.7L18.5 8.1l-2.3-2.3zM14.8 7.2l2.3 2.3"/></svg></button>
</div>
</header>
<div class="ha-context-row ha-context-row--compact dashboard3-context-bar">
<div class="ha-plan-label">
<svg class="ha-plan-symbol" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 10.5 12 3l9 7.5V21h-7v-6h-4v6H3z"/><path d="M4 7V4h4v1.6M16 4h5v5"/></svg>
<strong>План дома</strong>
</div>
<div class="ha-segmented ha-floor-switcher">
<button class="ha-segment dashboard3-space-tab active" type="button" data-space-id="ground-floor">Ground Floor <small>⚙</small></button>
<button class="ha-segment dashboard3-space-tab" type="button" data-space-id="first-floor">First Floor <small>⚙</small></button>
<button class="ha-segment dashboard3-space-tab" type="button" data-space-id="yard">Yard <small>⚙</small></button>
<span class="ha-segment ha-add">+</span>
</div>
<div class="ha-segmented ha-editor-switcher">
<span class="ha-segment"><span>⌗</span> Редактор плана</span>
<span class="ha-segment"><span>⌘</span> Редактор устройств</span>
<span class="ha-segment"><span>⌁</span> Редактор подложки</span>
</div>
<span class="ha-device-count">50 устр.</span>
<div class="dashboard3-context-actions" aria-label="Действия с планом">
<div class="dashboard3-zoom-group" aria-label="Масштаб">
<button type="button" aria-label="Уменьшить">−</button>
<button type="button" aria-label="Вместить план"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4H4v4M16 4h4v4M20 16v4h-4M8 20H4v-4"/><path d="m9 9 6 6M15 9l-6 6"/></svg></button>
<button type="button" aria-label="Увеличить">+</button>
</div>
<button class="dashboard3-context-icon dashboard4-size-toggle" type="button" aria-label="Размеры на этом экране" title="Размеры на этом экране"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h10M18 7h2M4 17h2M10 17h10M14 4v6M7 14v6"/></svg></button>
<button class="dashboard3-context-icon" type="button" aria-label="Печать"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 9V4h10v5M7 17H5a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-2"/><path d="M7 14h10v7H7zM17 12h1"/></svg></button>
<button class="dashboard3-context-icon dashboard3-help-icon" type="button" aria-label="Помощь">?</button>
<button class="dashboard3-context-icon dashboard4-theme-toggle" type="button" aria-label="Включить тёмную тему" title="Тема">◐</button>
<button class="dashboard4-kiosk-toggle" type="button" aria-pressed="false">Киоск</button>
<div class="dashboard5-summary-control" role="group" aria-label="Управление информационной панелью">
<button class="dashboard3-settings-toggle" type="button" aria-controls="dashboard3-settings-dialog" aria-haspopup="dialog" aria-label="Настроить информационную панель" title="Настроить информационную панель">
<img src="./svg/settings.svg" alt="" aria-hidden="true">
</button>
<button class="dashboard3-global-toggle" type="button" aria-controls="dashboard3-panel" aria-pressed="false" aria-label="Показать информационную панель" title="Показать информационную панель">
<img src="./svg/layout-sidebar-right.svg" alt="" aria-hidden="true">
</button>
</div>
</div>
</div>
<div class="dashboard-body">
<main class="canvas" aria-label="План дома">
<div class="ambient-light light-one"></div>
<div class="ambient-light light-two"></div>
<div class="floor-plan">
<section class="room terrace">
<h1>Terrace</h1>
<span class="door terrace-door"></span>
</section>
<section class="room kitchen">
<div class="room-name kitchen-name">
<strong>Kitchen &amp; Living <sup>↗</sup></strong>
<small>♟ 20.6° &nbsp; ● 42% &nbsp; ◒ Выкл</small>
</div>
<span class="door kitchen-door"></span>
</section>
<section class="room sauna"><span class="room-name">Sauna <sup>↗</sup></span></section>
<section class="room guest-wc"><span class="room-name">Guest WC <sup>↗</sup></span><span class="door wc-door"></span></section>
<section class="room storage">
<span class="room-name storage-name">Outdoor Storage <sup>↗</sup><small>◉ Выкл</small></span>
</section>
<section class="room washroom">
<span class="water-value">Water · 66%</span>
<span class="door wash-door"></span>
</section>
<section class="room boiler"><span class="room-name">Boiler Room <sup>↗</sup></span></section>
<section class="room hallway">
<span class="room-name hallway-name">Hallway <sup>↗</sup><small>◉ Выкл</small></span>
<span class="door hall-door-left"></span>
<span class="door hall-door-right"></span>
</section>
<section class="room stairs-closet">
<span class="room-name closet-name">Under-Stairs Closet <sup>↗</sup></span>
<span class="door closet-door"></span>
</section>
<section class="room guest-bedroom"><span class="room-name bedroom-name">Guest Bedroom <sup>↗</sup></span></section>
<section class="room porch">
<h2>Porch</h2>
<span class="door porch-door-one"></span>
<span class="door porch-door-two"></span>
</section>
<div class="dashed-zone" aria-hidden="true"></div>
<div class="device-row top-left">
<span class="device">▣</span><span class="device bulb">●</span>
</div>
<span class="device bulb d1">●</span>
<span class="device bulb d2">●</span>
<span class="device camera d3">◉</span>
<span class="device bulb d4">●</span>
<span class="device bulb d5">●</span>
<span class="device small-indicator online-1">●</span>
<div class="device-column left-devices">
<span class="device">▬</span><span class="device">▣</span><span class="device">⌂</span>
</div>
<div class="device-column right-devices">
<span class="device">▣</span><span class="device">▦</span><span class="device">▦</span><span class="device">▣</span>
</div>
<div class="fan-row"><span class="device">✣</span><span class="device">✣</span></div>
<span class="device sensor temp-main">42%</span>
<span class="device thermometer temp-icon">♟</span>
<span class="device bulb center-bulb-1">●</span>
<span class="device bulb center-bulb-2">●</span>
<span class="device bulb center-bulb-3">●</span>
<span class="device bulb center-bulb-4">●</span>
<span class="device bulb center-bulb-5">●</span>
<span class="device bulb center-bulb-6">●</span>
<div class="device-column utility-devices">
<span class="device">↔</span><span class="device">☷</span><span class="device">↔</span>
</div>
<div class="device-column wash-devices">
<span class="device">♜</span><span class="device">▦</span>
</div>
<span class="device camera wash-camera">◉</span>
<span class="device sensor wash-percent">66%</span>
<span class="device warning wash-warning">⌁</span>
<span class="status-pill rinsing">Rinsing</span>
<span class="device warning boiler-warning">⌁</span>
<span class="device bulb boiler-bulb">●</span>
<span class="device small-indicator online-2">●</span>
<span class="device small-indicator online-3">●</span>
<span class="device camera closet-camera">◉</span>
<span class="device sensor guest-temp">43%</span>
<span class="device thermometer guest-thermo">♟</span>
<div class="device-column guest-devices"><span class="device">▣</span><span class="device">▬</span></div>
</div>
</main>
<aside class="ha-home-panel" id="legacy-home-panel" aria-label="Старая панель дома" hidden>
<header class="ha-home-panel__header">
<button class="ha-icon-button ha-home-panel__size-toggle" type="button" aria-controls="home-panel-content" aria-expanded="true" title="Свернуть панель вправо">
<span class="when-expanded">›</span>
<span class="when-collapsed">‹</span>
</button>
<div class="ha-home-panel__title-wrap">
<span class="ha-home-panel__mark">▥</span>
<div>
<h2>Панель дома</h2>
<small>Ground Floor</small>
</div>
</div>
<button class="ha-icon-button ha-panel-settings-toggle" type="button" aria-controls="panel-manager" aria-expanded="false" title="Настроить блоки">⚙</button>
<span class="ha-home-panel__rail-title">Панель дома</span>
</header>
<section class="ha-device-editor" id="device-editor" aria-labelledby="device-editor-title" hidden>
<header class="ha-device-editor__header">
<button class="ha-icon-button ha-device-editor__back" type="button" aria-label="Вернуться к панели">←</button>
<div><span class="ha-eyebrow">Настройка устройства</span><h3 id="device-editor-title">Устройство</h3></div>
</header>
<div class="ha-device-editor__body">
<div class="ha-field">
<label for="editor-device-name">Имя</label>
<input id="editor-device-name" type="text">
</div>
<div class="ha-field">
<label for="editor-device-entity">Сущность Home Assistant</label>
<input id="editor-device-entity" type="text" readonly>
</div>
<div class="ha-field">
<label for="editor-device-room">Комната</label>
<select id="editor-device-room">
<option>Kitchen &amp; Living</option><option>Hallway</option><option>Guest Bedroom</option><option>Boiler Room</option><option>Sauna</option><option>Outdoor Storage</option><option>Terrace</option><option>Porch</option>
</select>
</div>
<div class="ha-field">
<label for="editor-device-action">Действие по нажатию</label>
<select id="editor-device-action">
<option>Переключить состояние</option><option>Открыть подробнее</option><option>Включить</option><option>Выключить</option><option>Нет действия</option>
</select>
</div>
<label class="ha-device-enabled"><input type="checkbox" id="editor-device-enabled" checked><span>Показывать устройство на панели</span></label>
</div>
<footer class="ha-device-editor__footer">
<button class="ha-device-editor__cancel" type="button">Отмена</button>
<button class="ha-device-editor__save" type="button">✓&nbsp; Сохранить</button>
</footer>
</section>
<section class="ha-block-editor" id="block-editor" aria-labelledby="block-editor-title" hidden>
<header class="ha-block-editor__header">
<button class="ha-icon-button ha-block-editor__back" type="button" aria-label="Вернуться к настройке панели">←</button>
<div><span class="ha-eyebrow">Настройка блока</span><h3 id="block-editor-title">Блок</h3></div>
</header>
<div class="ha-block-editor__body">
<p>Отключайте данные без удаления или удаляйте типы информации из блока.</p>
<div class="ha-block-editor__title-field">
<label for="block-title-input">Заголовок блока</label>
<input id="block-title-input" type="text" maxlength="48">
</div>
<div class="ha-block-editor__list" aria-label="Типы информации блока"></div>
<div class="ha-block-editor__add">
<label for="block-info-add-select">Добавить тип информации</label>
<div><select id="block-info-add-select"></select><button type="button" class="ha-block-editor__add-button">+ Добавить</button></div>
</div>
</div>
<footer class="ha-block-editor__footer">
<button class="ha-block-editor__reset" type="button">Сбросить блок</button>
<button class="ha-block-editor__done" type="button">Готово</button>
</footer>
</section>
<div class="ha-home-panel__content" id="home-panel-content">
<section class="ha-summary-card" id="summary-card" data-panel-id="summary" data-panel-title="Общая сводка" aria-labelledby="summary-title">
<div class="ha-summary-card__heading">
<div>
<span class="ha-eyebrow">Общая сводка</span>
<h3 id="summary-title">Состояние дома</h3>
</div>
<div class="ha-summary-actions"><span class="ha-status-badge good">Всё в порядке</span><button class="ha-block-settings-button" type="button" aria-label="Настроить общую сводку">⚙</button></div>
</div>
<div class="ha-temperature-grid">
<div class="ha-temperature-metric home" data-info-key="home-temperature" data-info-label="Средняя температура дома"><span class="metric-icon">⌂</span><span><small>Средняя дома</small><strong>20,5 °C</strong></span></div>
<div class="ha-temperature-metric outside" data-info-key="outside-temperature" data-info-label="Температура снаружи"><span class="metric-icon">☀</span><span><small>Снаружи</small><strong>18,3 °C</strong></span></div>
</div>
<div class="ha-summary-grid ha-summary-grid--secondary">
<div class="ha-summary-metric" data-info-key="humidity" data-info-label="Средняя влажность"><span class="metric-icon">◉</span><strong>41%</strong><small>Влажность</small></div>
<div class="ha-summary-metric" data-info-key="lighting" data-info-label="Сводка освещения"><span class="metric-icon">◉</span><strong>0 / 9</strong><small>Свет включён</small></div>
<div class="ha-summary-metric" data-info-key="entries" data-info-label="Состояние входов"><span class="metric-icon">⌂</span><strong>3</strong><small>Входа закрыто</small></div>
</div>
<div class="ha-summary-note" data-info-key="warnings" data-info-label="Предупреждения"><span class="ha-warning-dot"></span><span>2 инженерных устройства требуют внимания</span></div>
</section>
<section class="ha-panel-manager" id="panel-manager" aria-labelledby="panel-manager-title" hidden>
<div class="ha-panel-manager__heading">
<div><span class="ha-eyebrow">Режим редактирования</span><h3 id="panel-manager-title">Блоки панели</h3></div>
<button class="ha-icon-button ha-panel-settings-close" type="button" aria-label="Закрыть настройки">×</button>
</div>
<p>Перетаскивайте блоки или используйте стрелки. Переключатель управляет отображением.</p>
<div class="ha-panel-manager__list" aria-label="Порядок и видимость блоков"></div>
<button class="ha-reset-layout" type="button">Сбросить настройки</button>
</section>
<div class="ha-panel-blocks" id="panel-blocks">
<section class="ha-info-block" data-panel-id="rooms" data-panel-title="Комнаты" draggable="false">
<header class="ha-info-block__header">
<span class="ha-drag-handle" title="Перетащить">⠿</span><span class="ha-block-icon">⌂</span>
<button class="ha-info-block__toggle" type="button" aria-expanded="true"><span>Комнаты</span><small>10 зон</small><b>⌃</b></button>
</header>
<div class="ha-info-block__body">
<ul class="ha-status-list room-list" data-info-key="room-statuses" data-info-label="Состояния комнат">
<li><span><strong>Kitchen &amp; Living</strong><small>20,5° · 41% · свет выключен</small></span><span class="room-state">4 света</span></li>
<li><span><strong>Hallway</strong><small>Освещение выключено</small></span><span class="room-state">5 светов</span></li>
<li><span><strong>Guest Bedroom</strong><small>Влажность 44%</small></span><span class="room-state">2 устройства</span></li>
<li><span><strong>Boiler Room</strong><small>Бойлер 54,0° · вода 65%</small></span><span class="room-state warning">Внимание</span></li>
</ul>
<button class="ha-block-link" type="button" data-info-key="all-rooms-link" data-info-label="Переход ко всем зонам">Показать все 10 зон <span>→</span></button>
</div>
</section>
<section class="ha-info-block" data-panel-id="lighting" data-panel-title="Освещение" draggable="false">
<header class="ha-info-block__header">
<span class="ha-drag-handle" title="Перетащить">⠿</span><span class="ha-block-icon">●</span>
<button class="ha-info-block__toggle" type="button" aria-expanded="true"><span>Освещение</span><small>9 источников</small><b>⌃</b></button>
</header>
<div class="ha-info-block__body">
<div class="ha-inline-stats" data-info-key="lighting-summary" data-info-label="Общие показатели"><span><strong>0</strong><small>включено</small></span><span><strong>9</strong><small>выключено</small></span><span><strong>2</strong><small>группы</small></span></div>
<ul class="ha-compact-list" data-info-key="lighting-groups" data-info-label="Группы освещения"><li>Kitchen &amp; Living <span>4 · Выкл</span></li><li>Hallway <span>5 · Выкл</span></li></ul>
</div>
</section>
<section class="ha-info-block" data-panel-id="climate" data-panel-title="Климат" draggable="false">
<header class="ha-info-block__header">
<span class="ha-drag-handle" title="Перетащить">⠿</span><span class="ha-block-icon">♨</span>
<button class="ha-info-block__toggle" type="button" aria-expanded="true"><span>Климат</span><small>4 датчика</small><b>⌃</b></button>
</header>
<div class="ha-info-block__body">
<div class="ha-inline-stats" data-info-key="climate-summary" data-info-label="Средние показатели"><span><strong>20,5°</strong><small>в доме</small></span><span><strong>18,3°</strong><small>снаружи</small></span><span><strong>41%</strong><small>влажность</small></span></div>
<ul class="ha-compact-list" data-info-key="climate-sensors" data-info-label="Датчики климата"><li>Kitchen &amp; Living <span>20,5° · 41%</span></li><li>Guest Bedroom <span>44%</span></li><li>Boiler Room <span>54,0°</span></li><li>Sauna <span>Нагреватель</span></li></ul>
</div>
</section>
<section class="ha-info-block" data-panel-id="water" data-panel-title="Вода" draggable="false">
<header class="ha-info-block__header">
<span class="ha-drag-handle" title="Перетащить">⠿</span><span class="ha-block-icon">◒</span>
<button class="ha-info-block__toggle" type="button" aria-expanded="true"><span>Вода</span><small>4 устройства</small><b>⌃</b></button>
</header>
<div class="ha-info-block__body">
<div class="ha-inline-stats" data-info-key="water-summary" data-info-label="Сводка воды"><span><strong>65%</strong><small>датчик</small></span><span><strong>54,0°</strong><small>бойлер</small></span><span><strong>2</strong><small>клапана</small></span></div>
<ul class="ha-status-list" data-info-key="water-devices" data-info-label="Водяные устройства"><li><span><strong>Water Leak</strong><small>Boiler Room</small></span><span class="room-state warning">65%</span></li><li><span><strong>Бойлер</strong><small>Температура воды</small></span><span class="room-state">54,0°</span></li><li><span><strong>Клапаны</strong><small>2 устройства</small></span><span class="room-state good">Закрыты</span></li></ul>
</div>
</section>
<section class="ha-info-block" data-panel-id="security" data-panel-title="Безопасность" draggable="false">
<header class="ha-info-block__header">
<span class="ha-drag-handle" title="Перетащить">⠿</span><span class="ha-block-icon">▣</span>
<button class="ha-info-block__toggle" type="button" aria-expanded="true"><span>Безопасность</span><small>5 устройств</small><b>⌃</b></button>
</header>
<div class="ha-info-block__body">
<div class="ha-inline-stats" data-info-key="security-summary" data-info-label="Сводка безопасности"><span><strong>3</strong><small>закрыто</small></span><span><strong>2</strong><small>камеры</small></span><span><strong>0</strong><small>тревоги</small></span></div>
<ul class="ha-compact-list" data-info-key="security-devices" data-info-label="Двери и камеры"><li>Terrace <span class="good-text">Закрыто</span></li><li>Porch · 2 двери <span class="good-text">Закрыто</span></li><li>Камеры <span>2 доступны</span></li></ul>
</div>
</section>
<section class="ha-info-block" data-panel-id="devices" data-panel-title="Устройства" draggable="false">
<header class="ha-info-block__header">
<span class="ha-drag-handle" title="Перетащить">⠿</span><span class="ha-block-icon">▦</span>
<button class="ha-info-block__toggle" type="button" aria-expanded="true"><span>Устройства</span><small>14 устройств</small><b>⌃</b></button>
</header>
<div class="ha-info-block__body">
<div class="ha-inline-stats" data-info-key="device-summary" data-info-label="Сводка устройств"><span><strong>12</strong><small>доступно</small></span><span><strong>2</strong><small>внимание</small></span><span><strong>0</strong><small>офлайн</small></span></div>
<div class="ha-device-categories" data-info-key="device-categories" data-info-label="Категории устройств">
<section class="ha-device-category" data-device-category="appliances">
<button class="ha-device-category__toggle" type="button" aria-expanded="false"><span>Бытовая техника</span><small>7 устройств</small><b>⌄</b></button>
<ul class="ha-device-category__list" hidden>
<li><span><strong>Dishwasher</strong><small>Kitchen &amp; Living · Доступно</small></span><button class="ha-device-settings-button" type="button" data-device-name="Dishwasher" data-device-entity="switch.dishwasher" data-device-room="Kitchen & Living">⚙ <span>Настроить</span></button></li>
<li><span><strong>Oven</strong><small>Kitchen &amp; Living · Выключено</small></span><button class="ha-device-settings-button" type="button" data-device-name="Oven" data-device-entity="switch.oven" data-device-room="Kitchen & Living">⚙ <span>Настроить</span></button></li>
<li><span><strong>Fridge</strong><small>Kitchen &amp; Living · Работает</small></span><button class="ha-device-settings-button" type="button" data-device-name="Fridge" data-device-entity="sensor.fridge" data-device-room="Kitchen & Living">⚙ <span>Настроить</span></button></li>
<li><span><strong>Washer</strong><small>Boiler Room · Done</small></span><button class="ha-device-settings-button" type="button" data-device-name="Washer" data-device-entity="sensor.washer" data-device-room="Boiler Room">⚙ <span>Настроить</span></button></li>
<li><span><strong>Dryer</strong><small>Boiler Room · Доступно</small></span><button class="ha-device-settings-button" type="button" data-device-name="Dryer" data-device-entity="switch.dryer" data-device-room="Boiler Room">⚙ <span>Настроить</span></button></li>
<li><span><strong>Sauna Heater</strong><small>Sauna · Выключено</small></span><button class="ha-device-settings-button" type="button" data-device-name="Sauna Heater" data-device-entity="climate.sauna_heater" data-device-room="Sauna">⚙ <span>Настроить</span></button></li>
<li><span><strong>Boiler</strong><small>Boiler Room · 54,0 °C</small></span><button class="ha-device-settings-button" type="button" data-device-name="Boiler" data-device-entity="water_heater.boiler" data-device-room="Boiler Room">⚙ <span>Настроить</span></button></li>
</ul>
</section>
<section class="ha-device-category" data-device-category="media">
<button class="ha-device-category__toggle" type="button" aria-expanded="false"><span>Мультимедиа</span><small>3 устройства</small><b>⌄</b></button>
<ul class="ha-device-category__list" hidden>
<li><span><strong>Living Room TV</strong><small>Kitchen &amp; Living · Выключено</small></span><button class="ha-device-settings-button" type="button" data-device-name="Living Room TV" data-device-entity="media_player.living_room_tv" data-device-room="Kitchen & Living">⚙ <span>Настроить</span></button></li>
<li><span><strong>Media Player</strong><small>Kitchen &amp; Living · Доступно</small></span><button class="ha-device-settings-button" type="button" data-device-name="Media Player" data-device-entity="media_player.main" data-device-room="Kitchen & Living">⚙ <span>Настроить</span></button></li>
<li><span><strong>Bedroom Display</strong><small>Guest Bedroom · Доступно</small></span><button class="ha-device-settings-button" type="button" data-device-name="Bedroom Display" data-device-entity="media_player.bedroom_display" data-device-room="Guest Bedroom">⚙ <span>Настроить</span></button></li>
</ul>
</section>
<section class="ha-device-category" data-device-category="sensors">
<button class="ha-device-category__toggle" type="button" aria-expanded="false"><span>Контроллеры и датчики</span><small>4 устройства</small><b>⌄</b></button>
<ul class="ha-device-category__list" hidden>
<li><span><strong>Kitchen Temperature</strong><small>Kitchen &amp; Living · 20,5 °C</small></span><button class="ha-device-settings-button" type="button" data-device-name="Kitchen Temperature" data-device-entity="sensor.kitchen_temperature" data-device-room="Kitchen & Living">⚙ <span>Настроить</span></button></li>
<li><span><strong>Kitchen Humidity</strong><small>Kitchen &amp; Living · 41%</small></span><button class="ha-device-settings-button" type="button" data-device-name="Kitchen Humidity" data-device-entity="sensor.kitchen_humidity" data-device-room="Kitchen & Living">⚙ <span>Настроить</span></button></li>
<li><span><strong>Water Leak</strong><small>Boiler Room · 65%</small></span><button class="ha-device-settings-button" type="button" data-device-name="Water Leak" data-device-entity="binary_sensor.water_leak" data-device-room="Boiler Room">⚙ <span>Настроить</span></button></li>
<li><span><strong>Terrace Camera</strong><small>Terrace · Доступно</small></span><button class="ha-device-settings-button" type="button" data-device-name="Terrace Camera" data-device-entity="camera.terrace" data-device-room="Terrace">⚙ <span>Настроить</span></button></li>
</ul>
</section>
</div>
</div>
</section>
<section class="ha-info-block" data-panel-id="events" data-panel-title="События" draggable="false">
<header class="ha-info-block__header">
<span class="ha-drag-handle" title="Перетащить">⠿</span><span class="ha-block-icon">!</span>
<button class="ha-info-block__toggle" type="button" aria-expanded="true"><span>События</span><small>2 записи</small><b>⌃</b></button>
</header>
<div class="ha-info-block__body">
<div class="ha-inline-stats" data-info-key="event-summary" data-info-label="Сводка событий"><span><strong>1</strong><small>внимание</small></span><span><strong>1</strong><small>завершено</small></span><span><strong>0</strong><small>критично</small></span></div>
<ul class="ha-timeline" data-info-key="event-history" data-info-label="История событий"><li><span class="event-dot warning"></span><div><strong>Boiler Room</strong><small>Проверьте показатель Water Leak · 65%</small></div></li><li><span class="event-dot good"></span><div><strong>Процесс завершён</strong><small>Статус инженерного устройства: Done</small></div></li></ul>
</div>
</section>
</div>
</div>
</aside>
<aside class="ha-home-panel dashboard3-panel" id="dashboard3-panel" aria-label="Сводная информация" hidden>
<div class="dashboard3-panel__content" id="dashboard3-panel-content">
<header class="dashboard5-panel-header">
<h2 id="dashboard5-panel-title">Сводная информация</h2>
</header>
<div class="dashboard3-blocks" id="dashboard3-blocks" aria-live="polite"></div>
</div>
</aside>
<div class="dashboard3-settings-dialog" id="dashboard3-settings-dialog" role="dialog" aria-modal="true" aria-labelledby="dashboard3-settings-title" hidden>
<form class="dashboard3-settings-dialog__window" novalidate>
<header class="dashboard3-settings-dialog__header">
<h2 id="dashboard3-settings-title">Настройка панели</h2>
<button class="ha-icon-button dashboard3-settings-dialog__close" type="button" aria-label="Закрыть настройки">×</button>
</header>
<div class="dashboard3-settings-dialog__body">
<section class="dashboard3-general-settings" aria-labelledby="dashboard3-general-title">
<header><h3 id="dashboard3-general-title">Основные настройки</h3></header>
<div class="dashboard3-general-settings__grid">
<label class="dashboard3-field">
<span>Название панели</span>
<input id="dashboard3-panel-title" type="text" value="Сводная информация" maxlength="48" required>
<small class="dashboard3-field__error" aria-live="polite"></small>
</label>
<label class="dashboard3-switch-field">
<span><strong>Показывать информационную панель</strong><small>То же состояние, что у кнопки показа в шапке House Plan.</small></span>
<input id="dashboard5-show-panel" type="checkbox">
<span class="dashboard3-switch" aria-hidden="true"></span>
</label>
<label class="dashboard3-switch-field">
<span><strong>Отображать на мобильных устройствах</strong><small>Ограничивает показ в нативном узком режиме Home Assistant.</small></span>
<input id="dashboard3-show-mobile" type="checkbox" checked>
<span class="dashboard3-switch" aria-hidden="true"></span>
</label>
</div>
</section>
<section class="dashboard3-block-editor" aria-labelledby="dashboard3-block-editor-title">
<header class="dashboard3-block-editor__heading">
<div><h3 id="dashboard3-block-editor-title">Блоки</h3><p>Настройте порядок, область отображения и значения.</p></div>
<span class="dashboard3-block-count" id="dashboard3-block-count">1 из 10</span>
</header>
<div class="dashboard3-block-editor__list" id="dashboard3-block-editor-list"></div>
<button class="dashboard3-add-block" type="button"><img src="./svg/plus.svg" alt="" aria-hidden="true">Добавить блок</button>
</section>
</div>
<footer class="dashboard3-settings-dialog__footer">
<span class="dashboard3-settings-status" id="dashboard3-settings-status" aria-live="polite"></span>
<button class="dashboard3-button dashboard3-button--secondary dashboard3-settings-cancel" type="button">Отмена</button>
<button class="dashboard3-button dashboard3-button--primary dashboard3-settings-save" type="submit" hidden>Сохранить</button>
</footer>
</form>
</div>
<div class="dashboard3-entity-picker" id="dashboard3-entity-picker" role="dialog" aria-label="Выбор сущности Home Assistant" hidden>
<label class="dashboard3-entity-picker__search">
<span aria-hidden="true">⌕</span>
<input id="dashboard3-entity-search" type="search" placeholder="Поиск сущности..." autocomplete="off">
</label>
<div class="dashboard3-entity-picker__list" id="dashboard3-entity-list" role="listbox"></div>
<p class="dashboard3-entity-picker__hint">Нужно своё вычисление? Создайте Template-сущность в Home Assistant — она появится в этом списке.</p>
</div>
<div class="dashboard4-size-dialog" id="dashboard4-size-dialog" role="dialog" aria-modal="true" aria-labelledby="dashboard4-size-title" hidden>
<form class="dashboard4-size-dialog__window">
<header><h2 id="dashboard4-size-title">Размеры на этом экране</h2><button class="dashboard4-size-close" type="button" aria-label="Закрыть">×</button></header>
<label><span>Размер значков устройств</span><output id="dashboard4-icon-size-output">100%</output><input id="dashboard4-icon-size" type="range" min="80" max="160" step="10" value="100"></label>
<label><span>Размер текста карточек комнат</span><output id="dashboard4-text-size-output">100%</output><input id="dashboard4-text-size" type="range" min="80" max="160" step="10" value="100"></label>
<footer><button class="dashboard4-size-reset" type="button">Сбросить</button><button class="dashboard3-button dashboard3-button--primary" type="submit">Готово</button></footer>
</form>
</div>
</div>
</section>
</div>
</body>
</html>
@@ -0,0 +1,373 @@
:root {
--sidebar-width: 190px;
--global-bar-height: 41px;
--toolbar-height: 42px;
--blue: #00a5cf;
--ink: #303437;
--muted: #767b7e;
--line: #e7e7e7;
--room-fill: #8f979e;
--wall-dark: #53585b;
}
* { box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
font-family: Arial, Helvetica, sans-serif;
color: var(--ink);
background: #fff;
}
body { min-width: 1160px; min-height: 680px; }
.components-page { min-height: 100vh; overflow: auto; }
.app-shell { display: flex; width: 100vw; height: 100vh; }
.sidebar {
position: relative;
z-index: 10;
flex: 0 0 var(--sidebar-width);
height: 100%;
border-right: 1px solid var(--line);
background: #fff;
}
.brand-row {
display: flex;
align-items: center;
height: var(--global-bar-height);
padding: 0 12px;
border-bottom: 1px solid var(--line);
gap: 12px;
}
.brand-name { font-size: 16px; font-weight: 500; color: #242424; }
.nav-icon { width: 19px; text-align: center; color: #7b7f80; font-size: 17px; font-weight: 700; }
.hamburger { font-size: 15px; transform: scaleX(1.1); }
.plan-glyph, .active .nav-icon { color: var(--blue); }
.lightning { font-size: 23px; }
.side-nav { padding-top: 7px; }
.nav-item {
display: flex;
align-items: center;
height: 34px;
padding: 0 12px;
gap: 11px;
color: #2e3032;
text-decoration: none;
font-size: 12px;
font-weight: 600;
}
.nav-item.active {
margin: 0 3px;
padding-left: 9px;
border-radius: 3px;
color: var(--blue);
background: #d9f1f7;
}
.sidebar-footer { position: absolute; left: 0; right: 0; bottom: 8px; }
.sidebar-footer .nav-item { position: relative; }
.badge {
position: absolute;
right: 11px;
display: grid;
place-items: center;
width: 17px;
height: 17px;
border-radius: 50%;
color: #fff;
background: #ff9900;
font-size: 10px;
}
.profile { height: 38px; }
.avatar {
display: grid;
place-items: center;
width: 31px;
height: 31px;
margin-left: -5px;
border-radius: 50%;
color: #328496;
background: #d1f2fb;
font-weight: 400;
}
.workspace { position: relative; display: flex; flex: 1 1 auto; flex-direction: column; min-width: 0; height: 100%; }
.global-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--global-bar-height);
padding: 0 14px 0 10px;
border-bottom: 1px solid var(--line);
background: #fff;
flex: 0 0 var(--global-bar-height);
}
.view-switcher { display: flex; align-self: stretch; gap: 14px; }
.view-icon {
position: relative;
display: grid;
place-items: center;
width: 28px;
color: #555;
font-size: 16px;
}
.view-icon.selected::after {
position: absolute;
right: 0;
bottom: -1px;
left: 0;
height: 2px;
content: "";
background: #2d2d2d;
}
.global-actions { display: flex; align-items: center; gap: 24px; font-size: 20px; color: #222; }
.ha-view-tabs--compact {
align-items: stretch;
align-self: stretch;
height: var(--global-bar-height);
padding-left: 0;
gap: 14px;
border-bottom: 0;
}
.ha-view-tabs--compact .ha-view-tab { width: 28px; height: var(--global-bar-height); font-size: 16px; }
.ha-view-tabs--compact .ha-view-tab.active::after { height: 2px; }
.ha-context-row--compact {
flex: 0 0 44px;
height: 44px;
min-height: 44px;
padding: 0 10px;
gap: 8px;
overflow: hidden;
font-size: 11px;
}
.ha-context-row--compact .ha-plan-label { gap: 5px; font-size: 12px; }
.ha-context-row--compact .ha-home-icon { font-size: 17px; }
.ha-context-row--compact .ha-segmented { height: 31px; padding: 3px; border-radius: 10px; }
.ha-context-row--compact .ha-segment { height: 25px; padding: 0 11px; gap: 5px; border-radius: 7px; font-size: 11px; }
.ha-context-row--compact .ha-segment small { font-size: 10px; }
.ha-context-row--compact .ha-segment b { margin-left: 4px; font-size: 15px; }
.ha-context-row--compact .ha-add { padding: 0 7px; font-size: 16px; }
.ha-context-row--compact .ha-device-count { font-size: 11px; }
.ha-tools-row--compact {
flex: 0 0 50px;
min-height: 50px;
height: 50px;
padding: 7px 10px;
gap: 7px;
overflow: hidden;
}
.ha-tools-row--compact .ha-tool-mode { font-size: 24px; }
.ha-tools-row--compact .ha-tool { height: 34px; padding: 0 10px; gap: 6px; border-radius: 9px; font-size: 12px; }
.ha-tools-row--compact .ha-tool-icon { font-size: 15px; }
.ha-tools-row--compact .ha-history-actions { gap: 8px; }
.ha-tools-row--compact .ha-history-actions button { font-size: 18px; }
.dashboard-body { display: flex; flex: 1 1 auto; min-height: 0; overflow: hidden; }
.plan-toolbar {
position: relative;
z-index: 4;
display: flex;
align-items: center;
height: var(--toolbar-height);
padding: 0 10px;
gap: 8px;
border-bottom: 1px solid #ededed;
background: #fff;
white-space: nowrap;
font-size: 11px;
}
.plan-title { display: flex; align-items: center; gap: 5px; font-size: 12px; }
.home-mark { color: var(--blue); font-size: 17px; }
.floor-tabs, .editor-tabs, .zoom-tools {
display: flex;
align-items: center;
height: 29px;
border-radius: 10px;
background: #f1f1f1;
}
.floor-tab { height: 29px; padding: 8px 12px; color: #606164; font-weight: 600; }
.floor-tab small { color: #999; }
.floor-tab.active { margin: 2px; height: 25px; padding-top: 6px; border-radius: 6px; color: #fff; background: var(--blue); }
.add-floor { padding: 0 9px; color: #777; font-size: 16px; }
.editor-tabs { gap: 18px; padding: 0 13px; color: #616164; font-weight: 600; }
.device-count { color: #777; }
.zoom-tools { margin-left: auto; border-radius: 8px; }
.zoom-tools span { display: grid; place-items: center; width: 29px; height: 29px; font-size: 16px; }
.settings-button { display: grid; place-items: center; width: 30px; height: 29px; border-radius: 8px; background: #f5f5f5; font-size: 15px; }
.canvas {
position: relative;
flex: 1 1 auto;
min-width: 0;
height: auto;
overflow: hidden;
background: #d9ebf1;
}
.canvas::before {
position: absolute;
inset: 0;
content: "";
background:
radial-gradient(circle at 50% 45%, rgba(255,255,255,.34), transparent 37%),
linear-gradient(90deg, rgba(255,255,255,.08), transparent 12%, transparent 88%, rgba(255,255,255,.08));
}
.ambient-light { position: absolute; border-radius: 50%; filter: blur(20px); opacity: .6; }
.light-one { right: 5%; top: 53%; width: 155px; height: 155px; background: rgba(255, 245, 145, .42); }
.light-two { left: 39%; top: 5%; width: 70px; height: 70px; background: rgba(255, 236, 166, .22); }
.floor-plan {
position: absolute;
top: 50%;
left: 50%;
width: 700px;
height: 850px;
transform: translate(-46%, -49%) scale(var(--plan-scale, .94));
transform-origin: center;
color: #3d4143;
}
.room {
position: absolute;
border: 10px solid transparent;
background:
linear-gradient(var(--room-fill), var(--room-fill)) padding-box,
repeating-linear-gradient(135deg, #4f5355 0 2px, #9da2a4 2px 7px) border-box;
}
.room-name { position: absolute; font-size: 12px; font-weight: 700; color: #414548; white-space: nowrap; }
.room-name sup { color: #666; font-size: 9px; }
.room-name small { display: block; margin-top: 8px; font-size: 9px; color: #555; }
.terrace {
left: 185px; top: 9px; width: 420px; height: 190px;
border: 10px solid #5d7c8a;
border-top-style: dashed;
background: rgba(175, 195, 204, .55);
}
.terrace h1 { margin: 42px 0 0; text-align: center; font-size: 44px; line-height: 1; }
.kitchen { left: 12px; top: 170px; width: 590px; height: 285px; }
.kitchen-name { right: 76px; top: 46px; font-size: 15px; text-align: left; }
.kitchen-name small { margin-top: 8px; font-size: 11px; }
.sauna { left: 12px; top: 445px; width: 122px; height: 150px; }
.sauna .room-name { left: 7px; bottom: 17px; }
.guest-wc { left: 123px; top: 445px; width: 145px; height: 150px; }
.guest-wc .room-name { right: 9px; bottom: 17px; }
.storage { left: 12px; top: 585px; width: 112px; height: 165px; }
.storage-name { top: 7px; left: 6px; font-size: 9px; }
.washroom { left: 113px; top: 585px; width: 155px; height: 165px; }
.water-value { position: absolute; top: 57px; left: -17px; color: #4f879c; font-size: 12px; font-weight: 700; }
.boiler { left: 155px; top: 668px; width: 113px; height: 82px; }
.boiler .room-name { right: 4px; bottom: 17px; }
.hallway { left: 258px; top: 445px; width: 193px; height: 305px; }
.hallway-name { left: 0; top: 163px; }
.stairs-closet { left: 440px; top: 365px; width: 250px; height: 165px; }
.closet-name { right: 10px; bottom: 14px; font-size: 11px; }
.guest-bedroom { left: 440px; top: 520px; width: 160px; height: 230px; }
.bedroom-name { right: 7px; bottom: 17px; font-size: 11px; }
.porch {
left: 12px; top: 740px; width: 440px; height: 102px;
border-color: #5d7c8a;
background: #b8aa9a;
}
.porch h2 { margin: 24px 0 0; text-align: center; color: #595d5f; font-size: 26px; }
.door { position: absolute; width: 56px; height: 56px; border: 2px solid #555; border-left: 0; border-bottom: 0; border-radius: 0 56px 0 0; }
.terrace-door { left: 41px; bottom: -11px; transform: rotate(-90deg); transform-origin: bottom left; }
.kitchen-door { left: 235px; bottom: -11px; transform: rotate(90deg); transform-origin: bottom left; }
.wc-door { right: -10px; top: 48px; transform: rotate(-90deg); }
.wash-door { right: -10px; top: 68px; transform: rotate(-90deg); }
.hall-door-left { left: -10px; top: 23px; transform: rotate(-90deg); }
.hall-door-right { right: -10px; top: 58px; transform: rotate(-90deg); }
.closet-door { left: 70px; bottom: -10px; transform: rotate(180deg); }
.porch-door-one { left: 8px; top: -10px; transform: rotate(90deg); }
.porch-door-two { right: 74px; top: -10px; transform: rotate(90deg); }
.dashed-zone { position: absolute; left: 273px; top: 450px; width: 223px; height: 84px; border: 2px dashed #555; border-bottom: 0; }
.device {
display: grid;
place-items: center;
width: 24px;
height: 24px;
border: 2px solid #9da3a7;
border-radius: 50%;
box-shadow: 0 1px 2px rgba(0,0,0,.22);
color: #111;
background: #fff;
font-size: 11px;
font-weight: 800;
line-height: 1;
}
.bulb { font-size: 10px; }
.camera { font-size: 13px; }
.sensor { width: 29px; font-size: 10px; }
.thermometer { font-size: 13px; }
.small-indicator { width: 14px; height: 14px; border: 0; color: #fff; background: #36c86c; font-size: 7px; box-shadow: 0 0 5px rgba(55,202,108,.6); }
.device-row { position: absolute; display: flex; gap: 4px; }
.device-column { position: absolute; display: flex; flex-direction: column; gap: 4px; }
.top-left { left: 24px; top: 182px; }
.d1 { left: 188px; top: 182px; }
.d2 { left: 305px; top: 182px; }
.d3 { left: 298px; top: 204px; }
.d4 { left: 421px; top: 182px; }
.d5 { left: 540px; top: 182px; }
.online-1 { left: 271px; top: 184px; }
.left-devices { left: 23px; top: 340px; }
.right-devices { left: 565px; top: 202px; }
.fan-row { position: absolute; left: 536px; top: 302px; display: flex; gap: 7px; }
.temp-main { left: 138px; top: 411px; }
.temp-icon { left: 183px; top: 411px; }
.center-bulb-1 { left: 219px; top: 337px; }
.center-bulb-2 { left: 324px; top: 446px; }
.center-bulb-3 { left: 324px; top: 497px; }
.center-bulb-4 { left: 324px; top: 548px; }
.center-bulb-5 { left: 324px; top: 607px; }
.center-bulb-6 { left: 324px; top: 660px; }
.utility-devices { left: 23px; top: 611px; }
.wash-devices { left: 128px; top: 622px; }
.wash-camera { left: 158px; top: 592px; }
.wash-percent { left: 151px; top: 633px; }
.wash-warning { left: 187px; top: 634px; color: #fff; background: #ffae00; }
.status-pill { position: absolute; padding: 5px 7px; border: 2px solid #fff; border-radius: 11px; color: #fff; background: #ffad00; font-size: 9px; font-weight: 700; box-shadow: 0 1px 2px rgba(0,0,0,.2); }
.rinsing { left: 191px; top: 672px; }
.boiler-warning { left: 123px; top: 711px; color: #fff; background: #ffae00; }
.boiler-bulb { left: 143px; top: 692px; }
.online-2 { left: 53px; top: 718px; }
.online-3 { left: 325px; top: 718px; }
.closet-camera { left: 622px; top: 386px; }
.guest-temp { left: 514px; top: 525px; }
.guest-thermo { left: 550px; top: 525px; }
.guest-devices { left: 442px; top: 603px; }
@media (max-height: 820px) {
.floor-plan { --plan-scale: .76; }
}
@media (min-height: 821px) and (max-height: 940px) {
.floor-plan { --plan-scale: .88; }
}
@media (min-height: 941px) {
.floor-plan { --plan-scale: 1; }
}
@media (max-width: 1450px) {
:root { --sidebar-width: 178px; }
.ha-context-row--compact .ha-segment { padding-inline: 7px; font-size: 10px; }
.ha-tools-row--compact .ha-tool { padding-inline: 7px; font-size: 11px; }
.floor-plan { transform: translate(-44%, -49%) scale(var(--plan-scale, .85)); }
}
@media (max-width: 1220px) {
.ha-context-row--compact .ha-editor-switcher .ha-segment { padding-inline: 5px; font-size: 9px; }
.ha-tools-row--compact .ha-tool { padding-inline: 5px; gap: 3px; font-size: 10px; }
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-down"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M6 9l6 6l6 -6" /></svg>

After

Width:  |  Height:  |  Size: 337 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-eye-off"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" /><path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" /><path d="M3 3l18 18" /></svg>

After

Width:  |  Height:  |  Size: 586 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-eye"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /></svg>

After

Width:  |  Height:  |  Size: 444 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-layout-sidebar-right"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12" /><path d="M15 4l0 16" /></svg>

After

Width:  |  Height:  |  Size: 435 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-plus"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>

After

Width:  |  Height:  |  Size: 349 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-settings"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065" /><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" /></svg>

After

Width:  |  Height:  |  Size: 889 B

@@ -0,0 +1,250 @@
# #505 — сводная панель: дизайнерский прототип и UX-исправления
Issue: https://github.com/Matysh/houseplan-card/issues/505
Task branch: `issue/505-summary-panel-polish`
Baseline: `dev@abbca50ce732a87b59c8675d394f691738ea0961` (1.73.0-beta.7).
Status: specification; implementation requires independent S4 green verdict.
Owner decision: 2026-09-09, resume after scope extension with designer archive.
## 1. Problem, value and scope
The working #437 panel differs materially from the supplied Dashboard 5 design.
This task fixes the whole visual composition, not just individual CSS defects:
header controls, floating panel and settings dialog. Original four requests remain
mandatory: animated exit, genuinely wide dialog, removal of screen-size controls,
and disabling the mobile option while local show is off.
Value: user 8/10 (readability, predictable layout and consistency with accepted
design); product/maintenance 7/10 (one scoped implementation instead of competing
UI conventions). Complexity 6/10, risk 5/10, P2, bug/polish.
Full track: fails small's complexity/risk <=3 and introduces coordinated contracts
on three surfaces including supported touch; not trivial.
The summary remains read-only current information, per SCOPE's #437 exception.
No arbitrary dashboard/cards/actions, new data source types, config migrations,
camera/layout changes, new panel-placement setting or modification of HA chrome.
## 2. Design authority and reproducible reference
[Designer reference](../design/505-summary-panel/README.md), including original
[index.html](../design/505-summary-panel/reference/index.html), CSS, JS and SVG.
Archive received from owner: `макет.zip`, SHA-256
`bc754c16c9d9fc79dbbf093fcf8e14b96f8826fa01d1f0c8eccf68ab95357e15`.
Open index.html in a browser; press the last header button, then its adjacent gear.
The final Dashboard 5 CSS rules override older demo rules in the same files.
Priority: explicit owner requirements here > current House Plan data/safety/
accessibility contracts > prototype visual and interaction reference > demo
implementation details. Reference JS is not product code or process instructions.
Do not copy demo HA chrome, hardcoded entities/states, demo storage or external
navigation into House Plan.
The intended visual structure is binding; it is not satisfied by retaining the
old generic settings form and changing its width. Permitted adaptations are:
HA theme colours, current font family, readable font sizes, 44px touch targets,
mobile wrapping and existing hp-dialog close/focus/fullscreen conventions.
No global component restyling. Save remains visible (disabled when unchanged),
even if demo accidentally conceals it. Missing-source/loading/conflict messages
use existing product semantics, not invented demo stale-time rules.
### Visual target map (CSS px at normal text scale)
| Surface | Reference | Product target / bounded adaptation |
|---|---|---|
| Header pair | Gear left, sidebar-right right; divider, radius 8; last group | Same icons, order and far-right placement after other View actions; touch area >=44x44, glyphs 18–22; no new buttons outside this pair |
| Selected state | Only show/hide half filled accent | HA primary colour, theme-appropriate foreground; gear remains neutral; hover/focus/disabled unambiguous |
| Floating shell | Inset 12; radius 12; shadow 0 8px 26px at ~16% | Same silhouette, border/shadow hierarchy; safe-area/kiosk control insets take precedence |
| Panel width | Right intrinsic 280–420; bottom intrinsic min 360 | Right min(280, available) to min(420, available); bottom min(360, available), max available; never stretch merely to fill stage |
| Panel header | Separate full-width surface, >=48 height, padding14 | Same; 14–16 text, can wrap long title rather than hide meaning |
| Block area | Muted container padding10, gap9; white cards radius11 | Same two-level surfaces, header divider, rows with last divider removed; adapt colours for dark HA theme |
| Values | Label left, stronger value right | Distinct columns, right alignment, min-width:0; readable 12–14 text, wrapping instead of horizontal overflow |
| Settings shell | 780 wide, radius15, pale body, white header/footer | Target 920, capped by viewport; increased for >=44 controls/readable 14px fields; native and HA shell both sized, not inner min-width |
| Settings body | General-settings card, then Blocks card; padding14–18 | Same cards and hierarchy, block count, nested editor blocks; no detached legacy size section |
| Block header | Grip, square up/down, name, eye; scope beneath | Same order and grouping; scope all/one plus space selector; narrow rows wrap |
| Value row | Grip, square up/down, label, source, remove | Same desktop reading order, aligned row; narrow stacks label/source without hiding controls |
| Add/delete | Dashed add row; outlined delete-block footer | Plus icon, full-row add-value; add-block below list; delete-block separated from reorder |
| Dialog footer | Separate surface/border, right-aligned Cancel/Save | Existing hp-dialog action slots, wraps within available width, no edge clipping |
Use theme-scoped semantic variables backed by HA card/primary/secondary text,
divider and accent colours. Do not bake the prototype cyan into an orange HA theme.
Scope all selectors under summary classes or `hp-dialog[data-kind="summary"]`.
No changes to other editor toolbars, panels or dialogs.
## 3. Controls and panel behaviour
The compound control is the last item in the normal View header action group.
Settings (left) opens the existing shared/local-only dialog; show/hide (right)
uses the designer sidebar-right outline SVG and changes only local show intent.
It stays available for household users, independently of admin edit controls.
Use matching designer gear, eye/eye-off, plus and chevron; semantic arrow/remove
icons share stroke weight. SVG uses currentColor, aria-hidden, explicit viewBox,
width/height; every button has a localized accessible name and focus indication.
In kiosk the pair stays in the existing floating-controls container; the panel
starts below the measured controls. Neither controls nor panel are in zoomwrap.
No refit, camera updates, stage size changes or document scroll on show/hide.
The existing resolver is authoritative: stage width >= height => right, otherwise
bottom. This is not viewport orientation. Preserve safe insets, minimum usable
height, max60% stage height for bottom and mobile/narrow visibility policy.
A panel that cannot fit remains temporarily hidden without changing stored intent.
Probe/minimum-height measurements must reflect the new title/block/row styles.
All blocks are expanded. Shared visibility and all/one-space filters are unchanged.
Separate panel heading remains outside vertically scrolling blocks. Values update
live through existing lifecycle/cache/clock logic. Scroll position is not reset by
unrelated HA state updates. Empty configured panel/space has an explanatory empty
state within the same shell. Long titles/values/units/IDs and 10x20 rows stay bounded.
## 4. Animation lifecycle
Reference motion: opacity + 18px from/to the anchored edge, 190ms ease, both right
and bottom. Bottom retains horizontal centering throughout; no camera animation.
Separate effective visibility/local intent from transient DOM presentation.
Presentation states: hidden -> entering -> visible -> exiting -> hidden.
Normal local off starts exit from current visual progress, immediately inert and
aria-hidden; removal only after opacity exit completion. Local on during exit
reverses from current rendered progress; never rebuild at full opacity first.
Last intent wins and only one panel DOM subtree exists.
A completion callback is generation-scoped and cannot remove a newer panel.
Use filtered transition completion plus a bounded safety cleanup (<=300ms after
normal exit start) so cancelled transition events cannot leave a ghost.
No timers/listeners survive disconnect, identity/config-host change or route exit.
Special cases:
- Reduced motion: no movement or delayed removal; changes to media preference
during a transition settle to the latest intent.
- Editor entry, route leave/disconnect, unsupported config, or identity change:
synchronously remove presentation and cancel pending work; no stale private data.
- Document hidden: settle latest intent without replaying a transition on return.
- Resize/narrow/fits makes visible panel ineligible: settle hidden immediately
(do not draw beyond available bounds); eligible again may enter normally.
- Right/bottom changes mid-transition: cancel old direction/completion and settle
latest effective state at new anchor; no stale transform or sudden old-location flash.
- Config/space change while remaining eligible updates content in place, no full
panel exit/enter and no exposure of filtered-out blocks.
- No animation modifies or persists `local.show` by itself.
## 5. Settings interactions and persistence
Keep existing shared draft/revision validation and atomic save/recovery contracts
of #437/#490/#493. Name, block CRUD, value CRUD/source picker, reorder, visibility,
scope, confirmations and errors must remain functional after markup changes.
General settings appear before Blocks. Full editor shows panel name plus local
show and mobile controls as caption/helper/switch rows. Show/mobile retain their
current shared/local ownership (mobile is shared; show is per-user/per-card).
Mobile checkbox has native disabled when `!dialog.localShow` (and while saving).
Its value is not cleared. Reenable restores exact draft value. Master off/on,
Save, Cancel/X/Escape, reopen and reload behave consistently for all four pairs.
Do not optimistically persist either draft toggle while editing.
Non-admin/kiosk/unsupported-config local-only view shows the explanatory hint and
local show only; it must not reveal or mutate shared editor fields.
Remove only the summary dialog's Sizes on this screen header, sliders and reset,
in every locale and local-only mode. Do not remove independent View/kiosk scale
settings elsewhere, normalized storage values or scale application. Retain the
storage-unavailable warning where relevant to local show. Opening/unchanged Save,
title-only edit, show/mobile edit and Cancel never rewrite unrelated scale values.
Block visibility is an eye/eye-off button with aria-pressed and localized action
name. Reordering keeps both drag handles and square up/down alternatives; touch
and keyboard need not drag. First/last buttons disabled. Stable block/value IDs
remain authoritative for picker ownership; deleting/reordering a block cannot
retarget an open picker accidentally. Delete nonempty block confirmation remains.
Picker stays lazy, searchable and bounded to existing 100 matches; one active
picker only. Show friendly label and secondary entity ID (or system-source
caption) in the source button with chevron. Do not regress list keyboard/Escape,
focus return, missing old refs, 10k source indexing, validation or broken-space
warnings. Native and HA modal keyboard trap/close/save-busy semantics remain.
## 6. Responsiveness, a11y and localization
Settings shell: native fallback `wide` plus scoped --hp-dialog-wide-width:920px;
real HA width=medium plus scoped --ha-dialog-width-md:920px (verify actual pinned
HA frontend contract). Avoid changing generic hp-dialog widths. Real HA's native
fullscreen policy at <=450px width or <=500px height is permitted and retained.
At <=800 available dialog width stack general grid and source row fields; at
<=480 stack block header fields/scope, keep action groups usable, preserve DOM
reading order. Use shrinkable grid/flex children and border-box; no min-width
forcing content wider than shell. Vertical body scroll, fixed reachable footer.
320px and 390px, landscape short-height, 200% text, long RU/DE/FR content must work.
All interactive targets >=44x44. Functional visual clipping is not an acceptable
substitute for wrapping; horizontal overflow hidden must not mask inaccessible
fields/buttons. Input labels/accessible names remain associated after regrouping.
Use existing summary, btn and kiosk keys where semantics match; new en+ru+de+fr:
`summary.general_settings`, `summary.blocks`, `summary.blocks_hint`,
`summary.show_local_hint`, `summary.show_mobile_hint`,
`summary.hide_block`, `summary.show_block`, `summary.system_source`.
Reuse localized summary.count if available, otherwise add `summary.block_count`
with count/limit formatting. Remove `summary.sizes_title` only if no remaining
consumer. No data IDs, raw labels or untranslated new user-facing UI.
## 7. Implementation boundaries and performance
Expected files: src/summary-panel-style.ts, summary-panel-editor.ts,
summary-panel-runtime-loaded.ts; small icon/presentation helper(s) if needed;
src/houseplan-card.ts only control order; locale dictionaries; summary tests/
runtime tests; demo/smoke_summary_panel.mjs or focused companion smoke.
No backend/schema/type-model changes required. Config namespace stays version1;
compatibility registry/storage shapes do not change.
Keep summary/editor lazily loaded. No framework, remote icon request or permanent
animation RAF loop. Animate compositor opacity/transform, never dimensions.
No baseline increases, new large eager chunks or broad style recalculation.
Normal gate:small, unit, typecheck, bundle parity/budget, no-new-any and docs gates.
Full release/golden/performance capture is not required for this scoped task.
## 8. Acceptance and evidence
| AC | Required result | Evidence |
|---|---|---|
| AC1 | Compound icons/order/placement/states match §2–3 in View and kiosk | smoke DOM/order/geometry + paired reference/product visual review |
| AC2 | Shell/title/cards/rows/right values match design; right/bottom fit and live/empty states | smoke bounds/content updates + visual review |
| AC3 | Show/hide at190ms, retained inert exit then removal, reversals, one subtree | real-browser smoke sampling opacity/transform and DOM over time |
| AC4 | Reduced motion, resize/anchor change, narrow/fits, editor, hidden document, route/identity/disconnect cannot leave callbacks/ghosts | runtime tests + focused lifecycle browser smoke |
| AC5 | Desktop modal genuinely wide in native and real HA, mobile/200% text no horizontal clipping, reachable footer | native smoke geometry + authentic HA-dialog visual/bounds evidence (stub alone insufficient) |
| AC6 | General/Blocks cards, headers, source rows, eye/reorder/add/delete follow reference | paired visual review + smoke CRUD/reorder/visibility/source/confirm |
| AC7 | Size controls absent; old scale values preserved for admin and local-only paths | runtime + smoke persisted storage comparison |
| AC8 | Mobile disabled under master off, preserved values, four combinations save/cancel/reopen/reload | smoke actual controls/focus and persisted config/local intent |
| AC9 | Permissions, unavailable/future schema, conflict/lost ACK, broken refs, stable picker and 100-row cap remain | existing runtime/unit/summary smoke including 10k entities |
| AC10 | Themes/locales/44px targets/long content/keyboard and camera independence | smoke RU/EN/DE/FR + light/dark + 320/390/desktop, review |
| AC11 | Docs/changelogs current, scoped CSS/lazy budgets/build parity preserved | docs gate, code review, gate:small |
Visual evidence is mandatory: matched 1600x1000 reference and product captures of
control+open right panel and settings, dark desktop, bottom portrait, kiosk; plus
narrow native/real HA settings screenshots. Use equivalent default three values
and one custom-source row. Inspect geometry, hierarchy, glyphs, columns and
allowed adaptations separately from irrelevant background-plan pixels.
Record dimensions and accepted differences; no claim of pixel-identical UI.
These are task diagnostic images, not updates of accepted golden/docs baselines.
Keep portable captures/evidence instructions and reference in task docs; if raster
artifact publishing is needed use the existing Linux CI artifact path. Never
accept Windows screenshots as new canonical baselines. Review must be able to
reproduce the paired UI; automation passing alone is not visual acceptance.
At least one targeted negative witness for expensive smoke: demonstrate
AC3 fails if exit DOM retention is removed and AC5 fails if shell-wide sizing is
removed. Run mutation-gate for the relevant smoke if classed costly; do not
weaken gate assertions or update unrelated golden files to make it pass.
## 9. Risks, rollback, release artifacts
Main risks: fake parity from outer-only styling; real HA vs fallback sizing;
stale animation callback; loss of local/shared ownership; unavailable references
or picker retargeting; touch inaccessible controls after density adaptation.
Sections4–8 provide explicit prevention/evidence. No unresolved product question;
any deviation beyond §2's bounded adaptations returns to owner.
Rollback is revert of task commits plus bundle rebuild/sync; data remains fully
compatible with pre-task client because no format or ownership changes.
Release artifacts: both changelogs with #505; USER-GUIDE.ru.md and .md summary
section update control placement, layout, mobile dependency, removed size controls;
spec index and visual acceptance report. No release/tag/public announcement in
this task; stop at pipeline S8 (merged, waiting for next beta).
+1
View File
@@ -101,6 +101,7 @@
| Issue | ТЗ |
|---|---|
| [#505](https://github.com/Matysh/houseplan-card/issues/505) Сводная панель: дизайнерский прототип и UX-исправления | [505-summary-panel-design-parity.md](505-summary-panel-design-parity.md) |
| [#493](https://github.com/Matysh/houseplan-card/issues/493) Ограниченный picker и надёжный lifecycle сводной панели | [493-summary-panel-hardening.md](493-summary-panel-hardening.md) |
| [#437](https://github.com/Matysh/houseplan-card/issues/437) Конфигурируемая сводная панель поверх плана | [437-summary-panel.md](437-summary-panel.md) |
| [#485](https://github.com/Matysh/houseplan-card/issues/485) Радары присутствия: общий контракт и три этапа | [Общий контракт](485-radar-presence.md) · [Этап 1](485-radar-presence-stage1.md) · [Этап 2](485-radar-presence-stage2.md) · [Этап 3](485-radar-presence-stage3.md) |