diff --git a/docs/design/505-summary-panel/README.md b/docs/design/505-summary-panel/README.md new file mode 100644 index 00000000..b50d1cfe --- /dev/null +++ b/docs/design/505-summary-panel/README.md @@ -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. + diff --git a/docs/design/505-summary-panel/reference/DEVELOPER-HANDOFF.md b/docs/design/505-summary-panel/reference/DEVELOPER-HANDOFF.md new file mode 100644 index 00000000..7300488a --- /dev/null +++ b/docs/design/505-summary-panel/reference/DEVELOPER-HANDOFF.md @@ -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. + diff --git a/docs/design/505-summary-panel/reference/IMPLEMENTATION-NOTES.md b/docs/design/505-summary-panel/reference/IMPLEMENTATION-NOTES.md new file mode 100644 index 00000000..049bdfb8 --- /dev/null +++ b/docs/design/505-summary-panel/reference/IMPLEMENTATION-NOTES.md @@ -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`; +- значения строк выровнены по правому краю блока; +- в портретной ориентации панель находится снизу, центрируется и не растягивается на всю ширину без необходимости. + diff --git a/docs/design/505-summary-panel/reference/ISSUE-HANDOFF.md b/docs/design/505-summary-panel/reference/ISSUE-HANDOFF.md new file mode 100644 index 00000000..b2a1fe74 --- /dev/null +++ b/docs/design/505-summary-panel/reference/ISSUE-HANDOFF.md @@ -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. + diff --git a/docs/design/505-summary-panel/reference/README.md b/docs/design/505-summary-panel/reference/README.md new file mode 100644 index 00000000..f10b03c1 --- /dev/null +++ b/docs/design/505-summary-panel/reference/README.md @@ -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`. + diff --git a/docs/design/505-summary-panel/reference/components.css b/docs/design/505-summary-panel/reference/components.css new file mode 100644 index 00000000..e90eccef --- /dev/null +++ b/docs/design/505-summary-panel/reference/components.css @@ -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; } +} + diff --git a/docs/design/505-summary-panel/reference/components.html b/docs/design/505-summary-panel/reference/components.html new file mode 100644 index 00000000..ce2427ac --- /dev/null +++ b/docs/design/505-summary-panel/reference/components.html @@ -0,0 +1,135 @@ + + + + + + Home Assistant — Компоненты панели + + + + +
+
+ ⌗ + ▣ + ▧ +
+ +
+
▥План дома
+ +
+ Ground Floor ⚙ + First Floor ⚙ + Yard ⚙ + + +
+ +
+ ⌗ Редактор плана × + ⌘ Редактор устройств + ⌁ Редактор подложки +
+ + 50 устр. +
+ +
+ + + + + + + + + + + + +
+
+ +
+ + +
+ + +
+ + + diff --git a/docs/design/505-summary-panel/reference/dashboard-5.css b/docs/design/505-summary-panel/reference/dashboard-5.css new file mode 100644 index 00000000..7688c28b --- /dev/null +++ b/docs/design/505-summary-panel/reference/dashboard-5.css @@ -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; } +} + diff --git a/docs/design/505-summary-panel/reference/dashboard-5.js b/docs/design/505-summary-panel/reference/dashboard-5.js new file mode 100644 index 00000000..8d89306d --- /dev/null +++ b/docs/design/505-summary-panel/reference/dashboard-5.js @@ -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 ` +
+
${escapeHtml(value.label)}
+
${escapeHtml(resolved.text)}${statusLabel ? `${escapeHtml(statusLabel)}` : ""}
+
`; + }).join(""); + const card = document.createElement("section"); + card.className = "dashboard3-block"; + card.innerHTML = ` +
+

${escapeHtml(block.title)}

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

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

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

Terrace

+ +
+ +
+
+ Kitchen & Living ↗ + ♟ 20.6°   ● 42%   ◒ Выкл +
+ +
+ +
Sauna ↗
+
Guest WC ↗
+
+ Outdoor Storage ↗◉ Выкл +
+
+ Water · 66% + +
+
Boiler Room ↗
+
+ Hallway ↗◉ Выкл + + +
+
+ Under-Stairs Closet ↗ + +
+
Guest Bedroom ↗
+
+

Porch

+ + +
+ + + +
+ ▣● +
+ ● + ● + ◉ + ● + ● + ● + +
+ ▬▣⌂ +
+
+ ▣▦▦▣ +
+
✣✣
+ + 42% + ♟ + ● + ● + ● + ● + ● + ● + +
+ ↔☷↔ +
+
+ ♜▦ +
+ ◉ + 66% + ⌁ + Rinsing + ⌁ + ● + ● + ● + + ◉ + 43% + ♟ +
▣▬
+
+
+ + + + + + + + + + + +
+
+
+ + + diff --git a/docs/design/505-summary-panel/reference/styles.css b/docs/design/505-summary-panel/reference/styles.css new file mode 100644 index 00000000..593fd193 --- /dev/null +++ b/docs/design/505-summary-panel/reference/styles.css @@ -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; } +} + diff --git a/docs/design/505-summary-panel/reference/svg/chevron-down.svg b/docs/design/505-summary-panel/reference/svg/chevron-down.svg new file mode 100644 index 00000000..37fe8e2d --- /dev/null +++ b/docs/design/505-summary-panel/reference/svg/chevron-down.svg @@ -0,0 +1 @@ + diff --git a/docs/design/505-summary-panel/reference/svg/eye-off.svg b/docs/design/505-summary-panel/reference/svg/eye-off.svg new file mode 100644 index 00000000..000f413e --- /dev/null +++ b/docs/design/505-summary-panel/reference/svg/eye-off.svg @@ -0,0 +1 @@ + diff --git a/docs/design/505-summary-panel/reference/svg/eye.svg b/docs/design/505-summary-panel/reference/svg/eye.svg new file mode 100644 index 00000000..41cf11ca --- /dev/null +++ b/docs/design/505-summary-panel/reference/svg/eye.svg @@ -0,0 +1 @@ + diff --git a/docs/design/505-summary-panel/reference/svg/layout-sidebar-right.svg b/docs/design/505-summary-panel/reference/svg/layout-sidebar-right.svg new file mode 100644 index 00000000..d43c300d --- /dev/null +++ b/docs/design/505-summary-panel/reference/svg/layout-sidebar-right.svg @@ -0,0 +1 @@ + diff --git a/docs/design/505-summary-panel/reference/svg/plus.svg b/docs/design/505-summary-panel/reference/svg/plus.svg new file mode 100644 index 00000000..d6aae34f --- /dev/null +++ b/docs/design/505-summary-panel/reference/svg/plus.svg @@ -0,0 +1 @@ + diff --git a/docs/design/505-summary-panel/reference/svg/settings.svg b/docs/design/505-summary-panel/reference/svg/settings.svg new file mode 100644 index 00000000..34c7acf2 --- /dev/null +++ b/docs/design/505-summary-panel/reference/svg/settings.svg @@ -0,0 +1 @@ + diff --git a/docs/specs/505-summary-panel-design-parity.md b/docs/specs/505-summary-panel-design-parity.md new file mode 100644 index 00000000..61a52163 --- /dev/null +++ b/docs/specs/505-summary-panel-design-parity.md @@ -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). + diff --git a/docs/specs/README.md b/docs/specs/README.md index 2f1e2102..47841ef5 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -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) |