mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 13:48:57 +00:00
docs: specify summary panel designer parity
Issue: #505 User-Visible: no
This commit is contained in:
@@ -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 & 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 & 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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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 & Living <sup>↗</sup></strong>
|
||||
<small>♟ 20.6° ● 42% ◒ Выкл</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 & 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">✓ Сохранить</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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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).
|
||||
|
||||
@@ -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) |
|
||||
|
||||
Reference in New Issue
Block a user