From 45066631aee4997ac1f033dbe75918b5b2d439d4 Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Mon, 24 Aug 2026 21:53:54 +0300 Subject: [PATCH 1/9] docs(spec): define resize wall carrier contract Issue: #298 User-Visible: no --- .../298-resize-wall-thickness-carrier.md | 352 ++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 353 insertions(+) create mode 100644 docs/specs/298-resize-wall-thickness-carrier.md diff --git a/docs/specs/298-resize-wall-thickness-carrier.md b/docs/specs/298-resize-wall-thickness-carrier.md new file mode 100644 index 00000000..668da6a8 --- /dev/null +++ b/docs/specs/298-resize-wall-thickness-carrier.md @@ -0,0 +1,352 @@ +# Issue #298 — Resize сохраняет wall records на решётке и на carrier + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/298 +- **Статус:** первая редакция для внешнего ревью; канонический статус задаётся + метками issue +- **Тип / приоритет:** bug / P1 +- **Оценка:** пользовательская ценность 10/10; ценность для разработки 10/10; + сложность 7/10; риск 8/10 +- **Область:** fixed-topology Resize, exact wall records, live preview/commit, + grid barrier, real-plan invariant smoke и mutation coverage +- **Модель данных:** schema и model version не меняются; исправляются только + записи, которые переносит новый Resize-жест +- **Связано:** #253, #277, #289, #291, #293, #297, + `docs/RESIZE.md`, `docs/WALL-THICKNESS.md`, `docs/CANVAS.md` + +## 1. Сценарий и подтверждённая причина + +Пользователь безопасно сдвигает одну стену комнаты. Moving wall едет +параллельно себе, а две соседние стены меняют длину. После жеста визуально всё +может выглядеть правдоподобно, но запись толщины другой стены получает endpoint, +которого нет ни среди вершин room polygons, ни на границе wall carrier. На +следующем редактировании это проявляется потерей кладки, ошибкой Optimize или +сломавшейся ручкой Resize. + +На обеих приложенных к issue реальных fixtures причина подтверждена в +`rekeyWallsAfterMove()`. Для точки `p` на старом ребре helper вычисляет +относительную долю `t`, затем возвращает точку с той же долей на новом ребре. +Когда fixed-topology Resize двигает только один endpoint бокового ребра, +внутренний endpoint wall record тоже пропорционально уезжает. Так `-85` +становится `-82.457`, хотя такой geometry boundary в новом polygon нет. + +Текущий commit guard проверяет только мультимножество `cm` и число open spans. +Он не доказывает, что все exact wall records лежат на carriers нового плана, +поэтому повреждённая запись сохраняется. + +## 2. Пользовательский результат + +Resize продолжает выглядеть и управляться как после #277/#293. Отличие в +сохранённых данных: перемещаются только wall endpoints, для которых существует +однозначное соответствие старой и новой topology vertex. Остальные endpoints +не интерполируются и остаются на своей физической границе. После commit все +записи толщины остаются на grid и на room-wall carriers; последующие Resize, +Optimize и рендер не получают скрытую повреждённую геометрию. + +Если lossless correspondence доказать нельзя, кандидат не сохраняется. Live +preview остаётся на последней валидной позиции, а при отсутствии валидной +позиции жест завершается существующей локализованной ошибкой Resize без config +write и Undo entry. Автоматического снапа настоящей off-grid координаты или +догадки по ближайшей стене нет. + +## 3. Fixed-topology correspondence + +### 3.1 Источник истины + +Для каждого затронутого room edge Resize уже имеет параллельную пару +`old edge → new edge` из immutable pre-drag snapshot и exact candidate. +Topology signature гарантирует одинаковое число и циклическую identity +вершин. На этой основе строится таблица соответствия: + +`old room vertex → new room vertex`. + +В таблицу входят только действительно изменившиеся vertices. Одинаковая +старая точка, принадлежащая двум room copies общей стены, обязана иметь ровно +одну новую destination. Несколько разных destinations означают конфликт и +отменяют кандидат; порядок rooms или wall records не выбирает победителя. + +### 3.2 Exact wall records + +Пара `a/b` является identity exact wall record; compatibility `key` только +пересчитывается из итогового span. + +Каждая исходная exact запись обрабатывается из immutable snapshot: + +1. запись делится на атомы в endpoints перекрывающихся затронутых old edges; +2. endpoint атома заменяется только если он равен old topology vertex из + таблицы correspondence в пределах canonical coordinate epsilon; +3. interior endpoint без vertex correspondence остаётся byte-equivalent — + относительная доля длины ребра для него не вычисляется; +4. нулевые атомы удаляются, совместимые соседние атомы с одинаковым `cm` + склеиваются только когда их endpoints точно совпали и они коллинеарны; +5. для каждого изменённого span заново строится compatibility key, а `cm` и + известные совместимые поля сохраняются. + +Длинная запись, пересекающая затронутый и незатронутый пролёты, обязана +разделиться на границе old edge: только endpoint затронутого атома следует за +вершиной. Нельзя affine-масштабировать целую запись или её внутреннюю точку. + +### 3.3 Legacy key-only records + +Legacy запись без валидных `a/b` не имеет длины и endpoints, поэтому ей нельзя +изобретать атомы. Разрешён только однозначный whole-edge fallback: старый key +точно соответствует целому изменённому edge и переносится на его новый key. +Projected-midpoint перенос по относительной доле запрещён в production Safe +Resize. Если legacy key затронут, но whole-edge соответствие неоднозначно, +кандидат fail closed и не сохраняется. Незатронутые legacy records остаются +byte-equivalent. + +Исторический generic scale/rotate helper может остаться для изолированных +pure-тестов старых преобразований, но production Safe Resize обязан вызывать +fixed-topology API. Переиспользование proportional `t` в этом path запрещено. + +## 4. Carrier и lattice preflight + +После wall rekey, но до принятия live preview, product code проверяет exact +candidate целиком. + +Для каждой exact wall entry: + +- `a` и `b` конечны и лежат на canonical grid либо отличаются не больше + действующего near-node storage epsilon; +- весь открытый span между `a` и `b` покрывается непрерывным объединением + коллинеарных room edges; недостаточно проверить только midpoint; +- нет зазора между carrier atoms и нет участка, принадлежащего только + продолжению оси за пределами стены; +- compatibility key согласован с итоговыми `a/b`; +- `cm` остаётся в допустимом диапазоне, а мультимножество исходных физических + значений толщины не теряется. + +Проверка допускает одну compact record через несколько смежных коллинеарных +room edges, но не допускает запись через разрыв. Independent partitions не +являются carrier для `space.walls`: их толщина хранится своей partition +geometry. Open spans по-прежнему проходят существующий отдельный rekey и +carrier validation. + +`LATTICE_NOISE_STEPS` из измерительного гейта отличает форматный near-node шум +от настоящего off-grid значения. Исправление не округляет авторскую +координату, удалённую от grid: такой candidate отклоняется. Уже сохранённые +старые off-grid records не мигрируют при загрузке и не меняются без жеста. + +## 5. Preview, commit и failure semantics + +1. Preview каждый раз строится из immutable pre-drag rooms/walls/open spans, + а не из предыдущего кадра. +2. Fixed-topology mapping и carrier/lattice preflight являются частью одного + candidate builder, которым пользуются preview и pointerup. +3. `_serverCfg`, history и queued save не меняются до успешного pointerup. +4. Commit принимает только уже показанный exact preview и повторно проверяет + snapshot/plan signature. Отдельного второго rekey нет. +5. Конфликт correspondence, неоднозначный legacy record либо carrier/lattice + failure отклоняет кандидат целиком. Частичная запись запрещена. +6. После runtime reject сохраняется последний валидный preview; если валидного + ненулевого preview не было, config/history byte-equivalent исходному. +7. Undo/Redo восстанавливают rooms, openings, walls и open spans целиком. + +Предсказуемый конфликт, который можно доказать в eligibility, должен сделать +handle disabled до pointer capture. Непредсказуемый runtime reject использует +уже существующее сообщение `resize.commit_failed`; новый persisted reason или +новый UX в этой задаче не вводится. + +## 6. Scope + +### Входит + +- fixed-topology rekey exact и legacy wall records; +- endpoint correspondence без proportional interpolation; +- product carrier/lattice preflight до preview/commit; +- точные регрессии обеих fixtures из #298; +- обновление `smoke_edit_walk`/`KNOWN`, mutation и performance evidence; +- canonical Resize/wall-thickness/testing docs и оба changelog. + +### Не входит + +- изменение eligibility, safe range, pointer UX или topology #277/#293; +- автоматический ремонт уже сохранённого bad plan через Optimize; +- schema migration, integer-coordinate storage либо общий ADR #282; +- смешанные shared/exterior роли #289/#299; +- изменение толщины пользователем, renderer wall union или opening geometry; +- touch parity Plan editor сверх общего safety floor. + +## 7. Acceptance criteria + +### AC1. Exact repro второго этажа + +На `real-plan-second-floor.json` production Resize выполняет указанную в issue +последовательность `room-a`, edge 2, `x=96 → 100` grid steps. Запись исходного +горизонтального span `y=304` сохраняет endpoint на существующей topology +boundary; значение `-85` не превращается в `-82.457` или другую +пропорциональную точку. + +После preview и commit нет `off_lattice_coordinate`, `wall_carrier`, wall-key, +reference и physical-geometry нарушений. Moving/shared rooms и openings +сохраняют контракты #277/#293. + +**Доказательство:** fixture-backed pure/unit regression и production-bundle +pointer smoke; assert проверяет конкретную identity записи до/после, а не +только отсутствие исключения. + +### AC2. Exact repro первого этажа + +На `real-plan-first-floor.json` последовательность `room-b`, edge 0, +`x=49 → 52` не создаёт endpoint `59.538` на wall record линии `y=155`. +Итоговые endpoints принадлежат реальным vertices/carrier boundaries +`17/52/57/101` согласно новому candidate. + +**Доказательство:** отдельный fixture-backed regression с exact endpoint, +carrier/lattice и unchanged-record assertions. + +### AC3. Untouched records действительно не меняются + +Table-driven unit покрывает non-shared wall, exact shared pair, длинную запись, +пересекающую moved и untouched spans, reversed orientation и несколько +одинаковых `cm`. Запись без overlap и без moved endpoint остаётся deep/byte +equivalent, включая key и порядок. Затронутые записи сохраняют все значения +`cm`; коллизия destinations отклоняет весь candidate. + +### AC4. Legacy compatibility не угадывается + +- unambiguous whole-edge key переезжает на новый whole-edge key; +- untouched key-only record не меняется; +- key-only midpoint на части изменившего длину edge не переносится + пропорционально и приводит к fail-closed candidate; +- exact record всегда использует `a/b`, даже если старый compatibility key + неверен. + +**Доказательство:** pure unit и production-preview reject с нулём config/history +writes. + +### AC5. Carrier preflight проверяет весь span + +Positive cases: одна room edge и непрерывная цепочка нескольких коллинеарных +room edges. Negative cases: оба endpoints на carriers, но между ними разрыв; +midpoint на продолжении за пределами edge; endpoint на independent partition; +настоящая off-grid coordinate дальше `LATTICE_NOISE_STEPS`; conflicting shared +destinations. Каждый negative candidate отклоняется до сохранения. + +### AC6. Шесть edit-walk запусков больше не несут этот долг + +Проходят: + +```text +node demo/smoke_edit_walk.mjs --seed 1 --plan real-plan-second-floor.json +node demo/smoke_edit_walk.mjs --seed 2 --plan real-plan-second-floor.json +node demo/smoke_edit_walk.mjs --seed 3 --plan real-plan-second-floor.json +node demo/smoke_edit_walk.mjs --seed 1 --plan real-plan-first-floor.json +node demo/smoke_edit_walk.mjs --seed 2 --plan real-plan-first-floor.json +node demo/smoke_edit_walk.mjs --seed 3 --plan real-plan-first-floor.json +``` + +Для результатов Resize нет `off_lattice_coordinate` и `wall_carrier`. +`KNOWN` обновляется в том же implementation-коммите только для исправленных +kinds. Независимый долг `mixed_role_record` #299 не скрывается и не считается +регрессией #298. + +### AC7. Preview/commit/Undo атомарны + +Valid pointer drag показывает exact candidate до release, затем создаёт ровно +один config write и один Undo entry. Undo возвращает byte-equivalent исходные +rooms/walls/open spans; Redo возвращает тот же exact candidate. Forced carrier +failure даёт ноль writes/history и существующую локализованную ошибку один раз. + +### AC8. Мутационный страж + +Mutation возвращает proportional `t` mapping для interior endpoint либо +отключает carrier preflight. AC1/AC2 или AC5 обязаны падать. Blanket-disable +Resize не проходит positive pointer scenario AC7 и существующие #293 smokes. + +### AC9. Производительность и локальные гейты + +Mapping строится один раз на candidate из уже ограниченного набора затронутых +edges. Нельзя добавлять глобальное pairwise сравнение всех records со всеми +room edges на каждый `pointermove` без подготовленного carrier index. p95 budget +Resize из `docs/RESIZE.md` сохраняется; если hot path меняется, targeted +benchmark сравнивается с baseline. + +Обязательны: + +- `npm run typecheck`; +- `npm test`; +- `npm run build` и bundle parity; +- `node scripts/check-docs.mjs`; +- targeted Resize pointer smoke, шесть edit-walk запусков и mutation gate. + +Полные golden, smoke, performance и Linux HA harness выполняются перед beta. + +## 8. Совместимость, touch и security + +Schema/storage/model version не меняются. Старые планы читаются без фоновой +перезаписи; исторический off-grid долг остаётся видимым до явной правки или +отдельного Optimize repair. Новый commit только запрещает Safe Resize создавать +новый долг. + +Plan editor остаётся desktop-first. Touch — best effort, но safety floor общий: +single-pointer drag не пишет invalid candidate, pinch/pan, pointercancel и lost +capture не создают config/history entries. Новых HA actions, сетевых запросов, +HTML/CSS input или security boundaries нет. + +## 9. Риски и меры + +- **Слишком широкий vertex match** может двигать соседний interior endpoint. + Мера: canonical epsilon, explicit correspondence и exact AC1–AC3. +- **Слишком узкий match** потеряет толщину moved wall. Мера: positive + non-shared/shared/long-record matrix и `checkWallRecordsPreserved`. +- **Проверка только endpoints/midpoint** пропустит разрыв carrier. Мера: full + interval coverage AC5. +- **Legacy fallback снова введёт интерполяцию.** Мера: explicit fail-closed AC4 + и mutant AC8. +- **Соседняя #299 меняет тот же `KNOWN`.** Мера: #298 удаляет только два своих + kinds и при rebase сохраняет независимые mixed-role строки. +- **Новый global validation замедлит pointermove.** Мера: подготовленный index + и benchmark AC9. + +## 10. Откат + +Откат — полный revert implementation-коммита вместе с tests/docs/`KNOWN`. +Миграция или восстановление schema не нужны. Конфиги, уже сохранённые новой +версией, используют прежнюю схему и читаются старой версией. + +## 11. Ожидаемые файлы и release artifacts + +Product code: + +- `src/wall-thickness.ts` — fixed-topology rekey; +- `src/houseplan-card.ts` — candidate integration и fail-closed preflight; +- отдельный pure carrier helper допускается, если не дублирует invariant model. + +Tests/evidence: + +- `test/wall-thickness.test.mjs` и/или targeted Resize unit; +- `demo/smoke_edit_walk.mjs`, включая точное обновление `KNOWN`; +- production-bundle Resize pointer smoke; +- mutation registry и targeted benchmark при изменении hot path. + +Документация: + +- `docs/RESIZE.md`, `docs/WALL-THICKNESS.md`, `docs/TESTING.md`; +- при изменении архитектурной границы — `docs/ARCHITECTURE.md`; +- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`. + +Визуальный дизайн не меняется, поэтому новый golden baseline не ожидается. +Если штатный docs screenshot всё же изменится, принимается только artifact +Linux workflow после визуального review и bundle sync. + +Implementation-коммит имеет terminal trailers: + +```text +Issue: #298 +User-Visible: yes +``` + +Issue не закрывается вручную: она закрывается пакетно при выпуске beta. + +## 12. Принятые технические предположения + +1. Зафиксированные в issue gestures и tracked real-plan fixtures являются + достаточным privacy-safe regression input; новые приватные данные не нужны. +2. `space.walls` относится только к room-wall carriers. Independent partitions + хранят толщину в собственном объекте и не оправдывают wall record вне room + boundary. +3. Existing `resize.commit_failed` достаточно для редкого runtime reject; + новый user-facing reason не требуется, пока eligibility не меняется. +4. Near-node storage noise из #291 отличается от настоящего repro #298; + исправление не расширяет snap tolerance. diff --git a/docs/specs/README.md b/docs/specs/README.md index 598dbac7..f8cc208e 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -77,6 +77,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#280](https://github.com/Matysh/houseplan-card/issues/280) Backend принимает доказанный Optimize rehost | [280-optimize-rehost-validation.md](280-optimize-rehost-validation.md) | | [#281](https://github.com/Matysh/houseplan-card/issues/281) Честный Resize после outer-partition reconciliation | [281-resize-zero-range.md](281-resize-zero-range.md) | | [#293](https://github.com/Matysh/houseplan-card/issues/293) Активная рукоятка Resize выполняет pointer-жест | [293-resize-pointer-noop.md](293-resize-pointer-noop.md) | +| [#298](https://github.com/Matysh/houseplan-card/issues/298) Resize сохраняет wall records на решётке и на carrier | [298-resize-wall-thickness-carrier.md](298-resize-wall-thickness-carrier.md) | ## P2 From 8221ad9b84be1ebdf33a1cb2d139ec2ef00df54a Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:01:30 +0000 Subject: [PATCH 2/9] docs: review document for #298 Issue: #298 User-Visible: no --- docs/reviews/SPEC-REVIEW-298-r1.md | 198 +++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 docs/reviews/SPEC-REVIEW-298-r1.md diff --git a/docs/reviews/SPEC-REVIEW-298-r1.md b/docs/reviews/SPEC-REVIEW-298-r1.md new file mode 100644 index 00000000..096d3e42 --- /dev/null +++ b/docs/reviews/SPEC-REVIEW-298-r1.md @@ -0,0 +1,198 @@ +# SPEC-REVIEW-298-r1 + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/298 +- **ТЗ:** `docs/specs/298-resize-wall-thickness-carrier.md` @ commit `45066631aee4997ac1f033dbe75918b5b2d439d4` +- **Этап:** ТЗ на ревью (PROCESS.md §2.4) +- **Заход:** r1 · блокирующих циклов израсходовано 0 из 4 (лимит лёгкого трека не действует — issue без метки `small`) +- **Ревьюер:** Claude (сессия ревью ТЗ), автор ≠ ревьюер + +## Скоуп ревью + +Полный разбор — это первый заход, дельты нет. Читаю ТЗ состязательно: ищу, где +оно невыполнимо, непроверяемо, либо выдаёт догадку за решение продукта. + +## Как проверялось + +1. `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` — целиком, перед тем как судить. +2. Тело issue #298 и три комментария (аналитика, занятие, передача ТЗ) — через + `gh issue view 298 --json ...` (MCP-инструменты GitHub были недоступны без + подтверждения, поэтому использован `gh` CLI). +3. Полный текст `docs/specs/298-resize-wall-thickness-carrier.md`. +4. Канонические документы затронутой подсистемы: `docs/RESIZE.md`, + `docs/WALL-THICKNESS.md`, разделы `docs/TOUCH-SUPPORT.md` про safety floor + редакторов. +5. Существование артефактов, на которые ссылается ТЗ, проверено в дереве + репозитория, а не на слово автора: + - `test/fixtures/real-plan-second-floor.json`, + `test/fixtures/real-plan-first-floor.json` — существуют; + - `LATTICE_NOISE_STEPS` — существует, `src/coordinate-canonicalization.ts:11`, + значение `1e-4`, используется в `scripts/model-invariants.mjs` и + мутационном гейте; + - `rekeyWallsAfterMove()` — существует, `src/wall-thickness.ts:533`; прочитан + код: помеченная в issue пропорциональная проекция (`mapPoint`, строки + 574–582 — `t` считается от старого ребра и переносится на новое без учёта + реального соответствия вершин) реально в проде — корневая причина + подтверждена чтением, а не поверена на слово; + - `off_lattice_coordinate` / `wall_carrier` — существуют как виды нарушений + в `scripts/model-invariants.mjs` и таблице `KNOWN` `demo/smoke_edit_walk.mjs`; + текущая таблица `KNOWN` (строки 73–87) уже регистрирует ровно те находки, + которые AC6 требует убрать (`off_lattice_coordinate`, `wall_carrier` для + обеих фикстур), и отдельно `mixed_role_record` — долг #299, который ТЗ + прямо исключает из скоупа и просит не трогать в `KNOWN`. Согласуется. +6. Проверено существование и состояние всех связанных issue, на которые ссылается + ТЗ: #253, #277, #289, #291, #293, #297 — закрыты; #299 — открыт, как и + утверждает ТЗ (не дубликат, отдельный долг). Ни одна ссылка не битая и не + искажена. +7. Сверены формулировки с канонoм: раздел 3.3 (legacy fallback, запрет + proportional-midpoint) согласуется с `WALL-THICKNESS.md` («Legacy entries + without endpoints keep the unambiguous whole-key/midpoint fallback and are + never split by inventing a length»); раздел 8 (touch safety floor) согласуется + с разделом `TOUCH-SUPPORT.md` «Safety floor that still applies to touch + editors»; §5 (disabled handle до pointer capture) согласуется с существующим + текстом `RESIZE.md` про eligibility и disabled-handle. Ни одного места, где + автор выдаёт непроверяемую догадку за факт, не найдено — все нетривиальные + технические решения (§12 «Принятые предположения») либо выводятся из уже + принятого контракта #277, либо явно помечены как предположение. + +**Не проверялось** (стадия ТЗ этого не требует): выполнение гейтов +`typecheck`/`test`/`build` — кода к задаче ещё нет; сам код ещё не написан. + +## Находки + +### Medium (в скоупе задачи) — отсутствуют обязательные продуктовые разделы §7.1 + +`PROCESS.md` §7.1 требует в ТЗ два продуктовых раздела первыми: «какая персона +встречает изменение, на какой поверхности, в какой момент» и «что человек +увидит до и после — одной фразой, без терминов реализации». `AGENTS.md` +повторяет это как обязательное дополнение к §7.1. Ни то, ни другое в тексте +ТЗ не выделено. + +- §1 («Сценарий и подтверждённая причина») называет только техническую цепочку + (`rekeyWallsAfterMove()`, endpoint, topology boundary) — персона, поверхность + и момент («администратор дома, десктоп, Plan editor / Resize, после + нескольких правок за дни») были прямо названы в комментарии аналитики к + issue, но не перенесены в сам документ ТЗ. +- §2 («Пользовательский результат») — единственный кандидат на «одну фразу без + терминов реализации», но весь абзац написан в терминах реализации: «wall + endpoints», «topology vertex», «lossless correspondence», «config write», + «Undo entry». Читатель без контекста кода не поймёт по нему, что видит + пользователь. + +Дефект не про содержание решения — оно верное и подтверждено кодом — а про +форму ТЗ, которая обязана доказывать, что это изменение продукта, а не работа +над кодом (PROCESS.md §7.1: «ТЗ, которое не может ответить на эти два вопроса, +описывает работу, а не изменение продукта»). Правка дешёвая: одна-две фразы в +начале документа, без пересмотра контракта. + +**Предлагаемая правка** (пример, автор волен сформулировать иначе): +> Персона — администратор дома (`docs/SCOPE.md`), поверхность — desktop Plan +> editor, инструмент Resize; момент — любой ресайз стены, эффект копится +> незаметно и проявляется через дни на другой стене. +> Видимо: сегодня после серии обычных ресайзов случайная стена в другом месте +> плана вдруг рисуется другой толщиной или теряет рабочую ручку ресайза, хотя +> её никто не трогал. После исправления обычный ресайз либо проходит как +> раньше, либо (в редком неоднозначном случае) заканчивается тем же самым +> сообщением об ошибке без изменения плана — но никогда не портит стену, которую +> пользователь не двигал. + +Это Medium: без High-находок вердикт жёлтый, правка делается автором в этом же +issue, повторного полного разбора не требует — фактическая архитектура решения +ревью не оспаривает. + +### Low (снято ревьюером с записью) — способ доказательства не назван явно у двух AC + +DoR-чеклист (`PROCESS.md` §2.5) требует у каждого AC явного указания, чем он +доказывается (`unit`/`backend`/`smoke`/`golden`/«ревью кода»). У AC1, AC2, AC4, +AC6, AC8, AC9 это явно есть (или очевидно из перечисленных команд). У **AC5** +(carrier preflight) и **AC7** (preview/commit/Undo атомарны) отсутствует строка +«Доказательство: …» — способ проверки не назван словом, хотя из контекста +понятен: AC5 — чистая функция преflight из §4, доказывается unit-тестом; AC7 — +поведение pointer-жеста, доказывается production-bundle smoke по аналогии с уже +существующим `demo/smoke_room_resize.mjs` (см. `RESIZE.md` «Verification»). + +Снимаю без возврата на цикл: неоднозначности в том, *что* проверяется, нет — их +формулировки уже настолько конкретны (перечислены positive/negative кейсы, +атомарность записи/Undo), что тип теста восстанавливается однозначно. Автору +стоит добавить явную строку при реализации ради единообразия с остальными AC, +но это не блокирует переход в «Готово к разработке». + +### Low (снято ревьюером с записью) — разделы «UX» и «i18n» не выделены явно + +`PROCESS.md` §7.1 перечисляет UX и i18n как обязательные разделы. В документе +нет разделов с этими заголовками; содержание по факту размазано — UX-семантика +(disabled handle, единственное существующее сообщение об ошибке, отсутствие +нового UX) описана в §5 и подтверждена явным предположением №3 в §12; i18n +покрыт тем же предположением №3 («новый persisted reason или новый UX в этой +задаче не вводится» ⇒ новых ключей нет). + +Снимаю: по существу оба вопроса закрыты (нет нового текста, нет новых ключей), +и это прямо написано, просто не под ожидаемым заголовком. Chisto формальный +момент, не влияющий на проверяемость AC. + +## Что проверено и корректно + +- Корневая причина в §1 подтверждена чтением `rekeyWallsAfterMove()` — + реальный код делает ровно ту пропорциональную проекцию (`mapPoint`), о которой + говорит issue и ТЗ; это не пересказ чужих слов. +- Оба воспроизведения (AC1/AC2) содержат точные числа из issue + (`-85 → -82.457`, `59.538`, вершины `17/52/57/101`) и совпадают с текстом + issue буквально — не пересказаны с искажением. +- Скоуп «не входит» (§6) корректно исключает #289/#299 (смешанные роли), + ADR #282 (integer storage), eligibility/#277/#293 (topology) — все со ссылками + на реально существующие issue в правильном статусе. +- AC6 корректно ссылается на реальную структуру `KNOWN` в + `demo/smoke_edit_walk.mjs` и требует убрать только те два вида находок, которые + там сейчас действительно зарегистрированы для этой пары фикстур, не трогая + независимый долг #299 (`mixed_role_record`) — проверено построчно. +- Ни одна ссылка на связанные issue (#253, #277, #289, #291, #293, #297, #299) + не оказалась битой, дублирующей или искажающей состояние (закрыт/открыт). +- Технические решения раздела 3 (correspondence-таблица, конфликт нескольких + destinations, атомарное разбиение длинной записи) и раздела 4 (carrier + preflight по всему span, не только по концам/midpoint) не изобретают новое + поведение — они последовательно продолжают уже принятый контракт #277 и модель + данных `WALL-THICKNESS.md` («exact endpoints… independent of whichever room + topology later happens to split the same straight line»), а не выдают догадку + за факт. +- Раздел 3.3 (legacy fallback, fail-closed при неоднозначном whole-edge + соответствии) прямо согласован с уже задокументированным в `WALL-THICKNESS.md` + запретом «изобретать длину» для legacy-записей без `a/b`. +- Touch/safety floor в §8 корректно ссылается на существующий раздел + `TOUCH-SUPPORT.md` «Safety floor that still applies to touch editors», а не + придумывает новое правило. +- Откат (§10) реалистичен: чистый revert коммита, миграции нет, потому что схема + не меняется — согласуется с §8. +- Продуктовых вопросов владельцу в этом ТЗ нет, и по факту разбора это + оправданно: спорные места (§5 п.7 про предсказуемый конфликт до pointer + capture) читаются как следствие уже принятого принципа fail-closed + (`RESIZE.md`: «If neither step is safe, the handle remains… disabled»), а не + как новое расширение eligibility — само ТЗ явно исключает изменение eligibility + из скоупа (§6 «Не входит»), и текст §5 п.7 с этим не расходится при точном + чтении («если конфликт можно доказать в eligibility» — то есть уже + существующими проверками, а не новыми). +- Ни один найденный ранее класс дефектов (#253, #258/#259, #287/#289) не + игнорируется задним числом — ТЗ явно проговаривает отличие от каждого в + разделе «Почему это дорого» issue и в §6/§12 ТЗ. + +## Чего не проверял + +- Само исправление (кода ещё нет — задача в `S4-spec-review`, реализация не + началась). AC1–AC9 разбирались на выполнимость и проверяемость формулировки, + а не на то, будущий код им действительно удовлетворит — это работа + код-ревью. +- Полный текст `docs/CANVAS.md` — раздел на который ссылается ТЗ, но задача не + меняет canvas-рендер напрямую (только данные, которые он потребляет); беглая + проверка показала, что ссылка не противоречит модели данных `walls[*]`. +- Все шесть комбинаций `smoke_edit_walk` не прогонялись — на этой стадии нет + кода для прогона; сверка ограничилась статическим соответствием таблицы + `KNOWN` тому, что требует AC6. +- Производительность (AC9) — числовые бюджеты `RESIZE.md` не пересчитывались, + только сверено само требование «сохраняется p95 budget» с текстом канона. + +## Вывод + +Единственная содержательная находка — Medium, отсутствие обязательных +продуктовых разделов §7.1 (персона/поверхность/момент + «одна фраза без +терминов реализации»). Остальное — два Low, снятых с запиской. High-находок +нет: контракт технически выполним, каждый AC проверяем, ни одна ссылка не +искажена, ни одна догадка не выдана за решённый факт. Вердикт жёлтый — +формально из-за Medium, а не из-за сомнений в самом решении. From 55db16df6e51027a81aaf56de6cd08bbf19c3b77 Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Mon, 24 Aug 2026 22:02:46 +0300 Subject: [PATCH 3/9] docs(spec): add resize user contract Issue: #298 User-Visible: no --- .../298-resize-wall-thickness-carrier.md | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/specs/298-resize-wall-thickness-carrier.md b/docs/specs/298-resize-wall-thickness-carrier.md index 668da6a8..bb48e62e 100644 --- a/docs/specs/298-resize-wall-thickness-carrier.md +++ b/docs/specs/298-resize-wall-thickness-carrier.md @@ -13,7 +13,19 @@ - **Связано:** #253, #277, #289, #291, #293, #297, `docs/RESIZE.md`, `docs/WALL-THICKNESS.md`, `docs/CANVAS.md` -## 1. Сценарий и подтверждённая причина +## 1. Персона, поверхность, момент и видимый результат + +Персона — администратор дома. Поверхность — desktop Plan editor, инструмент +Resize; момент — обычный ресайз стены готового плана. Повреждение копится +незаметно и может проявиться лишь через несколько следующих правок или дней. + +**До:** после серии обычных ресайзов случайная стена в другом месте плана вдруг +рисуется другой толщиной или теряет рабочую ручку, хотя её никто не трогал. +**После:** ресайз либо проходит внешне как раньше, либо в редком неоднозначном +случае заканчивается прежним сообщением об ошибке без изменения плана, но +никогда не портит другую стену. + +### Подтверждённая причина Пользователь безопасно сдвигает одну стену комнаты. Moving wall едет параллельно себе, а две соседние стены меняют длину. После жеста визуально всё @@ -147,6 +159,12 @@ handle disabled до pointer capture. Непредсказуемый runtime rej уже существующее сообщение `resize.commit_failed`; новый persisted reason или новый UX в этой задаче не вводится. +### UX и i18n + +Новых контролов, состояний и текстов нет. Enabled/disabled handle, toast и +доступные имена сохраняют существующий контракт #277/#293; новых RU/EN ключей +не добавляется. Меняется только атомарность данных за прежним жестом. + ## 6. Scope ### Входит @@ -223,6 +241,9 @@ midpoint на продолжении за пределами edge; endpoint на настоящая off-grid coordinate дальше `LATTICE_NOISE_STEPS`; conflicting shared destinations. Каждый negative candidate отклоняется до сохранения. +**Доказательство:** table-driven unit pure carrier preflight плюс integration +test отказа candidate builder до мутации preview/config. + ### AC6. Шесть edit-walk запусков больше не несут этот долг Проходят: @@ -248,6 +269,9 @@ Valid pointer drag показывает exact candidate до release, затем rooms/walls/open spans; Redo возвращает тот же exact candidate. Forced carrier failure даёт ноль writes/history и существующую локализованную ошибку один раз. +**Доказательство:** production-bundle pointer smoke с чтением DOM preview, +persisted config и history до release, после commit, Undo/Redo и forced reject. + ### AC8. Мутационный страж Mutation возвращает proportional `t` mapping для interior endpoint либо From 451932c8e9e155998f10cb9836f82fb6aa5a26ea Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:08:31 +0000 Subject: [PATCH 4/9] docs: review document for #298 Issue: #298 User-Visible: no --- docs/reviews/SPEC-REVIEW-298-r2.md | 126 +++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/reviews/SPEC-REVIEW-298-r2.md diff --git a/docs/reviews/SPEC-REVIEW-298-r2.md b/docs/reviews/SPEC-REVIEW-298-r2.md new file mode 100644 index 00000000..8844986b --- /dev/null +++ b/docs/reviews/SPEC-REVIEW-298-r2.md @@ -0,0 +1,126 @@ +# SPEC-REVIEW-298-r2 + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/298 +- **ТЗ:** `docs/specs/298-resize-wall-thickness-carrier.md` @ commit `55db16df6e51027a81aaf56de6cd08bbf19c3b77` +- **Этап:** ТЗ на ревью (PROCESS.md §2.4) +- **Заход:** r2 · блокирующих циклов израсходовано 1 из 4 (потрачено r1, жёлтый) +- **Ревьюер:** Claude (сессия ревью ТЗ), автор ≠ ревьюер + +## Скоуп ревью + +Не первый заход. По PROCESS.md §2.9 / issue #214 разбор идёт по дельте: + +- предыдущий вердикт — комментарий `claude`, 2026-08-24T19:01:20Z, в issue #298: + жёлтый, r1, блокирующих циклов 0/4, High 0 / Medium 1 (в скоупе), документ + `docs/reviews/SPEC-REVIEW-298-r1.md`, ТЗ проверялось на SHA + `45066631aee4997ac1f033dbe75918b5b2d439d4` (SHA не был назван в самом + вердикте — восстановлен из ссылки автора на коммит ТЗ в комментарии за 18:54:17Z, + который текстуально совпадает с состоянием, разобранным в r1-документе); +- правка автора — коммит `55db16df6e51027a81aaf56de6cd08bbf19c3b77` + (`docs(spec): add resize user contract`, единственный файл + `docs/specs/298-resize-wall-thickness-carrier.md`, +25/-1); +- дельта строго локальна: добавлен раздел «Персона, поверхность, момент и + видимый результат», добавлена подсекция «UX и i18n», добавлены две строки + «Доказательство:» под AC5 и AC7. Ни один AC, ни контракт поведения (§§3–5), + ни scope (§6) не менялись. Рёбейза на ушедший вперёд `dev` не было (родитель + коммита — `8221ad9b`, публикация r1-документа, что и ожидается между + раундами). Подсистема не сменилась, новых открытых вопросов правка не внесла. + Объём дельты (одна секция + два предложения) несопоставим с объёмом задачи → + разбор сокращён до дельты, остальное наследуется из r1. + +## Как проверялось + +1. `git diff 45066631..55db16df -- docs/specs/298-resize-wall-thickness-carrier.md` + — получен полный текст дельты, показан ниже в разделе «Закрытие раунда r1». +2. Дельта прочитана в контексте всего текущего файла (`Read` целиком, 377 + строк), чтобы исключить рассинхронизацию новых фраз с остальным ТЗ. +3. Новый раздел 1 сверен с: + - `docs/SCOPE.md` — персона «Home admin (primary) … Desktop browser», job + J6 «Keep the plan true as the home evolves … drag/resize» — совпадает + с «администратор дома / desktop Plan editor / инструмент Resize»; + - комментарием-аналитикой автора (18:52:14Z), где та же персона и та же + поверхность были названы изначально, но не попадали в сам документ — + ровно то несоответствие, которое r1 отметил как находку. +4. Термин «ручка» и написание «Resize» сверены с `docs/USER-GUIDE.ru.md` + (строки 441, 480–481: «Resize», «ручка объясняет…», «Ручка остаётся + видимой, но приглушена») — терминология не изобретена, взята из канона. +5. Новая подсекция «UX и i18n» сверена с §6 «Не входит» («изменение + eligibility, safe range, pointer UX … не входит») и §12 п.3 («Existing + `resize.commit_failed` достаточно … пока eligibility не меняется») — + утверждение «новых контролов/текстов нет» не противоречит остальному + документу. +6. Новые строки «Доказательство:» под AC5/AC7 сверены с разделом 11 + («Ожидаемые файлы и release artifacts»): методы («table-driven unit pure + carrier preflight», «production-bundle Resize pointer smoke») там же + названы как ожидаемые файлы/evidence — не расходятся. +7. Коммит `55db16df` проверен `git show --stat`: правда только один файл, + +25/-1, что совпадает с диффом, разобранным построчно (не поверено на + слово автора). +8. Трейлеры коммита: `Issue: #298`, `User-Visible: no` — для docs-only + spec-коммита корректно (видимого поведения нет, меняется только текст ТЗ). + +**Не проверялось повторно** (наследуется из r1, ниже отдельным разделом): +корневая причина по коду, существование фикстур/констант/связанных issue, +формулировки §§3–5, §6, §7 (AC1–AC9 по содержанию), §8–§12 — дельта их не +касается, к этим утверждениям автор не притрагивался. + +## Закрытие раунда r1 + +| Находка r1 | Чем закрыта | Где это видно | +|---|---|---| +| **Medium (в скоупе):** ТЗ не содержит обязательных продуктовых разделов §7.1 — «персона/поверхность/момент» и «что человек увидит до/после» без терминов реализации; §1 и §2 были на языке реализации (`rekeyWallsAfterMove`, topology vertex, config write) | Добавлен раздел 1 «Персона, поверхность, момент и видимый результат»: явно назван «администратор дома», «desktop Plan editor, инструмент Resize», «обычный ресайз стены готового плана»; добавлены два предложения «До:»/«После:» без единого термина реализации (стена рисуется другой толщиной / теряет ручку → ресайз либо проходит как раньше, либо fail-closed с прежней ошибкой) | `docs/specs/298-resize-wall-thickness-carrier.md`, новые строки 16–26 (коммит `55db16df`) | +| **Low (снят с записью, не блокировал):** AC5 и AC7 не называют явно способ доказательства | Под AC5 и AC7 добавлены явные строки «Доказательство: …» | строки 244–245 (AC5) и 272–273 (AC7) | +| **Low (снят с записью, не блокировал):** разделы «UX» и «i18n» не выделены явными заголовками | Добавлена подсекция `### UX и i18n` внутри §5 с прямым утверждением «новых контролов, состояний и текстов нет» | строки 162–166 | + +Все три пункта r1 закрыты правкой; новых расхождений между добавленным текстом +и остальным документом не найдено (см. «Как проверялось», пп. 3–6). + +## Унаследовано из r1 + +Без повторной проверки в этом раунде принято по документу +`docs/reviews/SPEC-REVIEW-298-r1.md` (ТЗ на SHA `45066631aee4997ac1f033dbe75918b5b2d439d4`), +поскольку дельта r1→r2 их не касается: + +- корневая причина подтверждена чтением `rekeyWallsAfterMove()` + (`src/wall-thickness.ts:533`, пропорциональная проекция `mapPoint`, + строки 574–582) — совпадает с числами из issue; +- обе fixture-репродукции (`real-plan-second-floor.json`, + `real-plan-first-floor.json`) существуют и соответствуют описанным в AC1/AC2 + жестам; +- `LATTICE_NOISE_STEPS` существует (`src/coordinate-canonicalization.ts:11`, + `1e-4`), используется в инвариантах и мутационном гейте; +- все связанные issue (#253, #277, #289, #291, #293, #297, #299) существуют в + заявленном состоянии (закрыты/открыт), ссылки не искажены; +- ссылки на канон (`WALL-THICKNESS.md`, `RESIZE.md`, `TOUCH-SUPPORT.md`) + точны, не изобретены; +- AC1–AC9 по содержанию проверяемы, скоуп «не входит» корректно исключает + #299 (`mixed_role_record`); +- §12 «Принятые технические предположения» — явные, не выданы за факт. + +## Находки + +Нет. Единственная находка r1 (Medium, в скоупе) закрыта дельтой; новых находок +дельта не создала. + +## Что проверено и корректно + +- Дельта r1→r2 (раздел 1, подсекция UX/i18n, две строки «Доказательство:») + внутренне согласована с остальным документом и с каноном (`SCOPE.md`, + `USER-GUIDE.ru.md`). +- Персона/поверхность/момент и фраза «до/после» присутствуют, без терминов + реализации — оба обязательных продуктовых раздела §7.1 закрыты. +- Коммит содержит ровно заявленное изменение, трейлеры корректны. + +## Чего не проверял + +- Повторно не перечитывал и не оспаривал §§2–12 по содержанию — дельта их не + меняла, см. «Унаследовано из r1». +- Гейты `typecheck`/`test`/`build`/`check-docs` не запускал — стадия ТЗ, кода к + задаче ещё нет; это ожидается на этапе code-review. +- Не проверял снова состояние связанных issue (#253/#277/#289/#291/#293/#297/#299) + — наследуется из r1, ссылки дельта не меняла. + +## Вердикт + +Зелёный. Единственная Medium-находка r1 закрыта точечной и корректной правкой, +новых находок нет. From 3fb2edc90469d44fe628f1c096bbf2f50df8a928 Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Mon, 24 Aug 2026 22:27:05 +0300 Subject: [PATCH 5/9] fix: preserve resize wall thickness carriers Issue: #298 User-Visible: yes --- .../houseplan/frontend/houseplan-card.js | 232 +++++++++--------- demo/smoke_edit_walk.mjs | 27 +- dist/houseplan-card.js | 232 +++++++++--------- docs/ARCHITECTURE.md | 6 + docs/CHANGELOG.md | 5 + docs/CHANGELOG.ru.md | 5 + docs/RESIZE.md | 11 + docs/TESTING.md | 6 + docs/USER-GUIDE.md | 3 + docs/USER-GUIDE.ru.md | 3 + docs/WALL-THICKNESS.md | 10 + docs/images/01-view-desktop.png | Bin 203954 -> 197073 bytes docs/images/02-view-touch.png | Bin 63336 -> 63254 bytes docs/images/03-space-create.png | Bin 61028 -> 45556 bytes docs/images/04-room-contour-close.png | Bin 358799 -> 348698 bytes docs/images/05-plan-context-tray.png | Bin 345709 -> 336804 bytes docs/images/06-device-display-preview.png | Bin 327899 -> 296201 bytes docs/images/06-device-editor.png | Bin 326261 -> 297823 bytes docs/images/07-background-editor.png | Bin 304173 -> 297295 bytes docs/images/08-room-card.png | Bin 33134 -> 32778 bytes docs/images/09-device-info.png | Bin 146170 -> 136931 bytes docs/images/screenshots.json | 42 ++-- scripts/mutation-gate.mjs | 12 + src/houseplan-card.ts | 47 +++- src/wall-thickness.ts | 120 ++++++++- test/wall-thickness.test.mjs | 71 ++++++ 26 files changed, 563 insertions(+), 269 deletions(-) diff --git a/custom_components/houseplan/frontend/houseplan-card.js b/custom_components/houseplan/frontend/houseplan-card.js index 996b18ed..364944be 100755 --- a/custom_components/houseplan/frontend/houseplan-card.js +++ b/custom_components/houseplan/frontend/houseplan-card.js @@ -1,4 +1,4 @@ -globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0b4484d42e0e4f0b284f34749967b05877c9a111ac82061aeb9e71293b7c1051";const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),s=new WeakMap;let n=class{constructor(t,e,s){if(this._$cssResult$=!0,s!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const i=this.t;if(e&&void 0===t){const e=void 0!==i&&1===i.length;e&&(t=s.get(i)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&s.set(i,t))}return t}toString(){return this.cssText}};const o=(t,...e)=>{const s=1===t.length?t[0]:e.reduce((e,i,s)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[s+1],t[0]);return new n(s,t,i)},a=e?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new n("string"==typeof t?t:t+"",void 0,i))(e)})(t):t,{is:r,defineProperty:l,getOwnPropertyDescriptor:c,getOwnPropertyNames:h,getOwnPropertySymbols:d,getPrototypeOf:p}=Object,u=globalThis,_=u.trustedTypes,m=_?_.emptyScript:"",g=u.reactiveElementPolyfillSupport,f=(t,e)=>t,v={toAttribute(t,e){switch(e){case Boolean:t=t?m:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},y=(t,e)=>!r(t,e),b={attribute:!0,type:String,converter:v,reflect:!1,useDefault:!1,hasChanged:y};Symbol.metadata??=Symbol("metadata"),u.litPropertyMetadata??=new WeakMap;let w=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=b){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const i=Symbol(),s=this.getPropertyDescriptor(t,i,e);void 0!==s&&l(this.prototype,t,s)}}static getPropertyDescriptor(t,e,i){const{get:s,set:n}=c(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get:s,set(e){const o=s?.call(this);n?.call(this,e),this.requestUpdate(t,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(f("elementProperties")))return;const t=p(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(f("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(f("properties"))){const t=this.properties,e=[...h(t),...d(t)];for(const i of e)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,e]of this.elementProperties){const i=this._$Eu(t,e);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(a(t))}else void 0!==t&&e.push(a(t));return e}static _$Eu(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const i of e.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const i=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((i,s)=>{if(e)i.adoptedStyleSheets=s.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const e of s){const s=document.createElement("style"),n=t.litNonce;void 0!==n&&s.setAttribute("nonce",n),s.textContent=e.cssText,i.appendChild(s)}})(i,this.constructor.elementStyles),i}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ET(t,e){const i=this.constructor.elementProperties.get(t),s=this.constructor._$Eu(t,i);if(void 0!==s&&!0===i.reflect){const n=(void 0!==i.converter?.toAttribute?i.converter:v).toAttribute(e,i.type);this._$Em=t,null==n?this.removeAttribute(s):this.setAttribute(s,n),this._$Em=null}}_$AK(t,e){const i=this.constructor,s=i._$Eh.get(t);if(void 0!==s&&this._$Em!==s){const t=i.getPropertyOptions(s),n="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:v;this._$Em=s;const o=n.fromAttribute(e,t.type);this[s]=o??this._$Ej?.get(s)??o,this._$Em=null}}requestUpdate(t,e,i,s=!1,n){if(void 0!==t){const o=this.constructor;if(!1===s&&(n=this[t]),i??=o.getPropertyOptions(t),!((i.hasChanged??y)(n,e)||i.useDefault&&i.reflect&&n===this._$Ej?.get(t)&&!this.hasAttribute(o._$Eu(t,i))))return;this.C(t,e,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,e,{useDefault:i,reflect:s,wrapped:n},o){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,o??e??this[t]),!0!==n||void 0!==o)||(this._$AL.has(t)||(this.hasUpdated||i||(e=void 0),this._$AL.set(t,e)),!0===s&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,i]of t){const{wrapped:t}=i,s=this[e];!0!==t||this._$AL.has(e)||void 0===s||this.C(e,void 0,i,s)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(e){throw t=!1,this._$EM(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}};w.elementStyles=[],w.shadowRootOptions={mode:"open"},w[f("elementProperties")]=new Map,w[f("finalized")]=new Map,g?.({ReactiveElement:w}),(u.reactiveElementVersions??=[]).push("2.1.2");const k=globalThis,x=t=>t,$=k.trustedTypes,S=$?$.createPolicy("lit-html",{createHTML:t=>t}):void 0,M="$lit$",C=`lit$${Math.random().toFixed(9).slice(2)}$`,D="?"+C,P=`<${D}>`,T=document,R=()=>T.createComment(""),I=t=>null===t||"object"!=typeof t&&"function"!=typeof t,A=Array.isArray,z="[ \t\n\f\r]",F=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,E=/-->/g,N=/>/g,O=RegExp(`>|${z}(?:([^\\s"'>=/]+)(${z}*=${z}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),H=/'/g,L=/"/g,B=/^(?:script|style|textarea|title)$/i,q=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),W=q(1),j=q(2),G=Symbol.for("lit-noChange"),U=Symbol.for("lit-nothing"),V=new WeakMap,K=T.createTreeWalker(T,129);function Y(t,e){if(!A(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==S?S.createHTML(e):e}const J=(t,e)=>{const i=t.length-1,s=[];let n,o=2===e?"":3===e?"":"",a=F;for(let e=0;e"===l[0]?(a=n??F,c=-1):void 0===l[1]?c=-2:(c=a.lastIndex-l[2].length,r=l[1],a=void 0===l[3]?O:'"'===l[3]?L:H):a===L||a===H?a=O:a===E||a===N?a=F:(a=O,n=void 0);const d=a===O&&t[e+1].startsWith("/>")?" ":"";o+=a===F?i+P:c>=0?(s.push(r),i.slice(0,c)+M+i.slice(c)+C+d):i+C+(-2===c?e:d)}return[Y(t,o+(t[i]||"")+(2===e?"":3===e?"":"")),s]};class X{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let n=0,o=0;const a=t.length-1,r=this.parts,[l,c]=J(t,e);if(this.el=X.createElement(l,i),K.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(s=K.nextNode())&&r.length0){s.textContent=$?$.emptyScript:"";for(let i=0;iA(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==U&&I(this._$AH)?this._$AA.nextSibling.data=t:this.T(T.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:i}=t,s="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=X.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===s)this._$AH.p(e);else{const t=new Q(s,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=V.get(t.strings);return void 0===e&&V.set(t.strings,e=new X(t)),e}k(t){A(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,s=0;for(const n of t)s===e.length?e.push(i=new tt(this.O(R()),this.O(R()),this,this.options)):i=e[s],i._$AI(n),s++;s2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=U}_$AI(t,e=this,i,s){const n=this.strings;let o=!1;if(void 0===n)t=Z(this,t,e,0),o=!I(t)||t!==this._$AH&&t!==G,o&&(this._$AH=t);else{const s=t;let a,r;for(t=n[0],a=0;a{const s=i?.renderBefore??e;let n=s._$litPart$;if(void 0===n){const t=i?.renderBefore??null;s._$litPart$=n=new tt(e.insertBefore(R(),t),t,void 0,i??{})}return n._$AI(t),n},ct=globalThis;let ht=class extends w{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=lt(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return G}};ht._$litElement$=!0,ht.finalized=!0,ct.litElementHydrateSupport?.({LitElement:ht});const dt=ct.litElementPolyfillSupport;dt?.({LitElement:ht}),(ct.litElementVersions??=[]).push("4.2.2");const pt=2,ut=t=>(...e)=>({_$litDirective$:t,values:e});let _t=class{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}};const mt={},gt=ut(class extends _t{constructor(){super(...arguments),this.ot=mt}render(t,e){return e()}update(t,[e,i]){if(Array.isArray(e)){if(Array.isArray(this.ot)&&this.ot.length===e.length&&e.every((t,e)=>t===this.ot[e]))return G}else if(this.ot===e)return G;return this.ot=Array.isArray(e)?Array.from(e):e,this.render(e,i)}}),{I:ft}=at,vt=t=>t,yt=()=>document.createComment(""),bt=(t,e,i)=>{const s=t._$AA.parentNode,n=void 0===e?t._$AB:e._$AA;if(void 0===i){const e=s.insertBefore(yt(),n),o=s.insertBefore(yt(),n);i=new ft(e,o,t,t.options)}else{const e=i._$AB.nextSibling,o=i._$AM,a=o!==t;if(a){let e;i._$AQ?.(t),i._$AM=t,void 0!==i._$AP&&(e=t._$AU)!==o._$AU&&i._$AP(e)}if(e!==n||a){let t=i._$AA;for(;t!==e;){const e=vt(t).nextSibling;vt(s).insertBefore(t,n),t=e}}}return i},wt=(t,e,i=t)=>(t._$AI(e,i),t),kt={},xt=(t,e=kt)=>t._$AH=e,$t=t=>{t._$AR(),t._$AA.remove()},St=(t,e,i)=>{const s=new Map;for(let n=e;n<=i;n++)s.set(t[n],n);return s},Mt=ut(class extends _t{constructor(t){if(super(t),t.type!==pt)throw Error("repeat() can only be used in text expressions")}dt(t,e,i){let s;void 0===i?i=e:void 0!==e&&(s=e);const n=[],o=[];let a=0;for(const e of t)n[a]=s?s(e,a):a,o[a]=i(e,a),a++;return{values:o,keys:n}}render(t,e,i){return this.dt(t,e,i).values}update(t,[e,i,s]){const n=(t=>t._$AH)(t),{values:o,keys:a}=this.dt(e,i,s);if(!Array.isArray(n))return this.ut=a,o;const r=this.ut??=[],l=[];let c,h,d=0,p=n.length-1,u=0,_=o.length-1;for(;d<=p&&u<=_;)if(null===n[d])d++;else if(null===n[p])p--;else if(r[d]===a[u])l[u]=wt(n[d],o[u]),d++,u++;else if(r[p]===a[_])l[_]=wt(n[p],o[_]),p--,_--;else if(r[d]===a[_])l[_]=wt(n[d],o[_]),bt(t,l[_+1],n[d]),d++,_--;else if(r[p]===a[u])l[u]=wt(n[p],o[u]),bt(t,n[d],n[p]),p--,u++;else if(void 0===c&&(c=St(a,u,_),h=St(r,d,p)),c.has(r[d]))if(c.has(r[p])){const e=h.get(a[u]),i=void 0!==e?n[e]:null;if(null===i){const e=bt(t,n[d]);wt(e,o[u]),l[u]=e}else l[u]=wt(i,o[u]),bt(t,n[d],i),n[e]=null;u++}else $t(n[p]),p--;else $t(n[d]),d++;for(;u<=_;){const e=bt(t,l[_+1]);wt(e,o[u]),l[u++]=e}for(;d<=p;){const t=n[d++];null!==t&&$t(t)}return this.ut=a,xt(t,l),G}}),Ct=new WeakMap;let Dt=0;class Pt extends ht{constructor(){super(...arguments),this.title="",this.icon="",this.wide=!1,this.dismissOnScrim=!1,this.hass=null,this._opener=null,this._focusRoot=null,this._useHaDialog=!1,this._closing=!1,this._overlays=[],this._titleId="hp-dialog-title-"+ ++Dt,this._focusInitial=()=>{const t=this._focusableElements(),e=t.find(t=>t.hasAttribute("autofocus"))||t[0]||(this._useHaDialog?null:this.renderRoot.querySelector(".close"))||this.renderRoot.querySelector(".surface")||this.renderRoot.querySelector("ha-dialog");e?.focus({preventScroll:!0})},this._requestClose=()=>{this._closing||(this._closing=!0,this.dispatchEvent(new CustomEvent("hp-close",{bubbles:!0,composed:!0})))},this._onKeyDown=t=>{if("Escape"===t.key){t.preventDefault(),t.stopImmediatePropagation(),this._pruneOverlays();const e=this._overlays[this._overlays.length-1];return e?void this._closeOverlay(e,"escape"):void this._requestClose()}if("Tab"!==t.key||this._useHaDialog)return;const e=this.renderRoot.querySelector(".close"),i=e?[e,...this._focusableElements()]:this._focusableElements();if(!i.length)return t.preventDefault(),void this.renderRoot.querySelector(".surface")?.focus({preventScroll:!0});const s=this._deepActiveElement(),n=i[0],o=i[i.length-1];!t.shiftKey||s!==n&&i.includes(s)?t.shiftKey||s!==o||(t.preventDefault(),n.focus()):(t.preventDefault(),o.focus())},this._onFallbackCancel=t=>{t.preventDefault(),this._requestClose()},this._onFallbackClick=t=>{this.dismissOnScrim&&t.target===t.currentTarget&&this._requestClose()}}connectedCallback(){super.connectedCallback(),this._opener=this._deepActiveElement();const t=this.getRootNode();this._focusRoot=t;const e=Ct.get(t)||{dialogs:new Set,opener:this._opener};e.dialogs.add(this),Ct.set(t,e),this._useHaDialog=!!customElements.get("ha-dialog"),this.addEventListener("keydown",this._onKeyDown,!0)}disconnectedCallback(){this.removeEventListener("keydown",this._onKeyDown,!0);const t=[...this._overlays];this._overlays=[];for(const e of t.reverse())e.close("disconnect");const e=this._focusRoot,i=this._opener;this._opener=null,this._focusRoot=null;const s=e?Ct.get(e):void 0;s?.dialogs.delete(this),super.disconnectedCallback(),e&&s&&requestAnimationFrame(()=>{const t=Ct.get(e);if(!t)return;if(!t.dialogs.size)return t.opener?.isConnected&&t.opener.focus({preventScroll:!0}),void Ct.delete(e);const s=i?.closest("hp-dialog");i?.isConnected&&s&&t.dialogs.has(s)&&i.focus({preventScroll:!0})})}firstUpdated(t){if(super.firstUpdated(t),!this._useHaDialog){const t=this.renderRoot.querySelector("dialog");t&&!t.open&&t.showModal()}queueMicrotask(()=>this._focusInitial())}_deepActiveElement(){let t=document.activeElement;for(;t?.shadowRoot?.activeElement;)t=t.shadowRoot.activeElement;return t&&t!==document.body?t:null}_focusableElements(){const t=["[autofocus]","a[href]","button:not([disabled])",'input:not([disabled]):not([type="hidden"])',"select:not([disabled])","textarea:not([disabled])",'[contenteditable="true"]','[tabindex]:not([tabindex="-1"])'].join(","),e=[],i=new Set,s=n=>{if(!i.has(n)){if(i.add(n),n instanceof HTMLElement){if(n.matches(t)&&e.push(n),n instanceof HTMLSlotElement){for(const t of n.assignedNodes({flatten:!0}))s(t);return}if(n.shadowRoot){for(const t of n.shadowRoot.childNodes)s(t);return}}for(const t of n.childNodes)s(t)}};for(const t of this.childNodes)s(t);const n=this.overlayPortal();return n&&s(n),e.filter(t=>{let e=t;for(;e;){const t=getComputedStyle(e);if(e.hidden||e.inert||"true"===e.getAttribute("aria-hidden")||"none"===t.display||"hidden"===t.visibility)return!1;if(e=e.assignedSlot||e.parentElement||(e.getRootNode()instanceof ShadowRoot?e.getRootNode().host:null),e===this)break}return!0})}_pruneOverlays(){this._overlays=this._overlays.filter(t=>t.owner.isConnected)}_closeOverlay(t,e){const i=this._overlays.findIndex(e=>e.token===t.token);i>=0&&this._overlays.splice(i,1),t.close(e)}registerOverlay(t){this._pruneOverlays();const e=this._overlays.find(e=>e.owner===t.owner);e&&this._overlays.splice(this._overlays.indexOf(e),1);const i=t.group||"transient";for(const t of[...this._overlays].reverse())t.group===i&&this._closeOverlay(t,"exclusive");const s={...t,group:i,token:Symbol("hp-overlay")};this._overlays.push(s);let n=!1;return()=>{if(n)return;n=!0;const t=this._overlays.findIndex(t=>t.token===s.token);t>=0&&this._overlays.splice(t,1)}}closeTransientOverlays(t="outside"){this._pruneOverlays();const e=[...this._overlays].filter(t=>"transient"===(t.group||"transient"));for(const i of e.reverse())this._closeOverlay(i,t);return e.length>0}overlayPortal(){return this.renderRoot.querySelector(".overlay-portal")}render(){const t=W` +globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="fd2331dc100999f197f251c7e817576d859911d68a046dc4ed1e6dc6cceb5a96";const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),s=new WeakMap;let n=class{constructor(t,e,s){if(this._$cssResult$=!0,s!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const i=this.t;if(e&&void 0===t){const e=void 0!==i&&1===i.length;e&&(t=s.get(i)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&s.set(i,t))}return t}toString(){return this.cssText}};const o=(t,...e)=>{const s=1===t.length?t[0]:e.reduce((e,i,s)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[s+1],t[0]);return new n(s,t,i)},a=e?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new n("string"==typeof t?t:t+"",void 0,i))(e)})(t):t,{is:r,defineProperty:l,getOwnPropertyDescriptor:c,getOwnPropertyNames:h,getOwnPropertySymbols:d,getPrototypeOf:p}=Object,u=globalThis,_=u.trustedTypes,m=_?_.emptyScript:"",g=u.reactiveElementPolyfillSupport,f=(t,e)=>t,v={toAttribute(t,e){switch(e){case Boolean:t=t?m:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},y=(t,e)=>!r(t,e),b={attribute:!0,type:String,converter:v,reflect:!1,useDefault:!1,hasChanged:y};Symbol.metadata??=Symbol("metadata"),u.litPropertyMetadata??=new WeakMap;let w=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=b){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const i=Symbol(),s=this.getPropertyDescriptor(t,i,e);void 0!==s&&l(this.prototype,t,s)}}static getPropertyDescriptor(t,e,i){const{get:s,set:n}=c(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get:s,set(e){const o=s?.call(this);n?.call(this,e),this.requestUpdate(t,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(f("elementProperties")))return;const t=p(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(f("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(f("properties"))){const t=this.properties,e=[...h(t),...d(t)];for(const i of e)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,e]of this.elementProperties){const i=this._$Eu(t,e);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(a(t))}else void 0!==t&&e.push(a(t));return e}static _$Eu(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const i of e.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const i=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((i,s)=>{if(e)i.adoptedStyleSheets=s.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const e of s){const s=document.createElement("style"),n=t.litNonce;void 0!==n&&s.setAttribute("nonce",n),s.textContent=e.cssText,i.appendChild(s)}})(i,this.constructor.elementStyles),i}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ET(t,e){const i=this.constructor.elementProperties.get(t),s=this.constructor._$Eu(t,i);if(void 0!==s&&!0===i.reflect){const n=(void 0!==i.converter?.toAttribute?i.converter:v).toAttribute(e,i.type);this._$Em=t,null==n?this.removeAttribute(s):this.setAttribute(s,n),this._$Em=null}}_$AK(t,e){const i=this.constructor,s=i._$Eh.get(t);if(void 0!==s&&this._$Em!==s){const t=i.getPropertyOptions(s),n="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:v;this._$Em=s;const o=n.fromAttribute(e,t.type);this[s]=o??this._$Ej?.get(s)??o,this._$Em=null}}requestUpdate(t,e,i,s=!1,n){if(void 0!==t){const o=this.constructor;if(!1===s&&(n=this[t]),i??=o.getPropertyOptions(t),!((i.hasChanged??y)(n,e)||i.useDefault&&i.reflect&&n===this._$Ej?.get(t)&&!this.hasAttribute(o._$Eu(t,i))))return;this.C(t,e,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,e,{useDefault:i,reflect:s,wrapped:n},o){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,o??e??this[t]),!0!==n||void 0!==o)||(this._$AL.has(t)||(this.hasUpdated||i||(e=void 0),this._$AL.set(t,e)),!0===s&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,i]of t){const{wrapped:t}=i,s=this[e];!0!==t||this._$AL.has(e)||void 0===s||this.C(e,void 0,i,s)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(e){throw t=!1,this._$EM(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}};w.elementStyles=[],w.shadowRootOptions={mode:"open"},w[f("elementProperties")]=new Map,w[f("finalized")]=new Map,g?.({ReactiveElement:w}),(u.reactiveElementVersions??=[]).push("2.1.2");const k=globalThis,x=t=>t,$=k.trustedTypes,S=$?$.createPolicy("lit-html",{createHTML:t=>t}):void 0,M="$lit$",C=`lit$${Math.random().toFixed(9).slice(2)}$`,D="?"+C,P=`<${D}>`,T=document,R=()=>T.createComment(""),A=t=>null===t||"object"!=typeof t&&"function"!=typeof t,I=Array.isArray,z="[ \t\n\f\r]",F=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,E=/-->/g,N=/>/g,O=RegExp(`>|${z}(?:([^\\s"'>=/]+)(${z}*=${z}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),H=/'/g,L=/"/g,B=/^(?:script|style|textarea|title)$/i,q=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),W=q(1),j=q(2),G=Symbol.for("lit-noChange"),U=Symbol.for("lit-nothing"),V=new WeakMap,K=T.createTreeWalker(T,129);function Y(t,e){if(!I(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==S?S.createHTML(e):e}const J=(t,e)=>{const i=t.length-1,s=[];let n,o=2===e?"":3===e?"":"",a=F;for(let e=0;e"===l[0]?(a=n??F,c=-1):void 0===l[1]?c=-2:(c=a.lastIndex-l[2].length,r=l[1],a=void 0===l[3]?O:'"'===l[3]?L:H):a===L||a===H?a=O:a===E||a===N?a=F:(a=O,n=void 0);const d=a===O&&t[e+1].startsWith("/>")?" ":"";o+=a===F?i+P:c>=0?(s.push(r),i.slice(0,c)+M+i.slice(c)+C+d):i+C+(-2===c?e:d)}return[Y(t,o+(t[i]||"")+(2===e?"":3===e?"":"")),s]};class X{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let n=0,o=0;const a=t.length-1,r=this.parts,[l,c]=J(t,e);if(this.el=X.createElement(l,i),K.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(s=K.nextNode())&&r.length0){s.textContent=$?$.emptyScript:"";for(let i=0;iI(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==U&&A(this._$AH)?this._$AA.nextSibling.data=t:this.T(T.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:i}=t,s="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=X.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===s)this._$AH.p(e);else{const t=new Q(s,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=V.get(t.strings);return void 0===e&&V.set(t.strings,e=new X(t)),e}k(t){I(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,s=0;for(const n of t)s===e.length?e.push(i=new tt(this.O(R()),this.O(R()),this,this.options)):i=e[s],i._$AI(n),s++;s2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=U}_$AI(t,e=this,i,s){const n=this.strings;let o=!1;if(void 0===n)t=Z(this,t,e,0),o=!A(t)||t!==this._$AH&&t!==G,o&&(this._$AH=t);else{const s=t;let a,r;for(t=n[0],a=0;a{const s=i?.renderBefore??e;let n=s._$litPart$;if(void 0===n){const t=i?.renderBefore??null;s._$litPart$=n=new tt(e.insertBefore(R(),t),t,void 0,i??{})}return n._$AI(t),n},ct=globalThis;let ht=class extends w{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=lt(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return G}};ht._$litElement$=!0,ht.finalized=!0,ct.litElementHydrateSupport?.({LitElement:ht});const dt=ct.litElementPolyfillSupport;dt?.({LitElement:ht}),(ct.litElementVersions??=[]).push("4.2.2");const pt=2,ut=t=>(...e)=>({_$litDirective$:t,values:e});let _t=class{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}};const mt={},gt=ut(class extends _t{constructor(){super(...arguments),this.ot=mt}render(t,e){return e()}update(t,[e,i]){if(Array.isArray(e)){if(Array.isArray(this.ot)&&this.ot.length===e.length&&e.every((t,e)=>t===this.ot[e]))return G}else if(this.ot===e)return G;return this.ot=Array.isArray(e)?Array.from(e):e,this.render(e,i)}}),{I:ft}=at,vt=t=>t,yt=()=>document.createComment(""),bt=(t,e,i)=>{const s=t._$AA.parentNode,n=void 0===e?t._$AB:e._$AA;if(void 0===i){const e=s.insertBefore(yt(),n),o=s.insertBefore(yt(),n);i=new ft(e,o,t,t.options)}else{const e=i._$AB.nextSibling,o=i._$AM,a=o!==t;if(a){let e;i._$AQ?.(t),i._$AM=t,void 0!==i._$AP&&(e=t._$AU)!==o._$AU&&i._$AP(e)}if(e!==n||a){let t=i._$AA;for(;t!==e;){const e=vt(t).nextSibling;vt(s).insertBefore(t,n),t=e}}}return i},wt=(t,e,i=t)=>(t._$AI(e,i),t),kt={},xt=(t,e=kt)=>t._$AH=e,$t=t=>{t._$AR(),t._$AA.remove()},St=(t,e,i)=>{const s=new Map;for(let n=e;n<=i;n++)s.set(t[n],n);return s},Mt=ut(class extends _t{constructor(t){if(super(t),t.type!==pt)throw Error("repeat() can only be used in text expressions")}dt(t,e,i){let s;void 0===i?i=e:void 0!==e&&(s=e);const n=[],o=[];let a=0;for(const e of t)n[a]=s?s(e,a):a,o[a]=i(e,a),a++;return{values:o,keys:n}}render(t,e,i){return this.dt(t,e,i).values}update(t,[e,i,s]){const n=(t=>t._$AH)(t),{values:o,keys:a}=this.dt(e,i,s);if(!Array.isArray(n))return this.ut=a,o;const r=this.ut??=[],l=[];let c,h,d=0,p=n.length-1,u=0,_=o.length-1;for(;d<=p&&u<=_;)if(null===n[d])d++;else if(null===n[p])p--;else if(r[d]===a[u])l[u]=wt(n[d],o[u]),d++,u++;else if(r[p]===a[_])l[_]=wt(n[p],o[_]),p--,_--;else if(r[d]===a[_])l[_]=wt(n[d],o[_]),bt(t,l[_+1],n[d]),d++,_--;else if(r[p]===a[u])l[u]=wt(n[p],o[u]),bt(t,n[d],n[p]),p--,u++;else if(void 0===c&&(c=St(a,u,_),h=St(r,d,p)),c.has(r[d]))if(c.has(r[p])){const e=h.get(a[u]),i=void 0!==e?n[e]:null;if(null===i){const e=bt(t,n[d]);wt(e,o[u]),l[u]=e}else l[u]=wt(i,o[u]),bt(t,n[d],i),n[e]=null;u++}else $t(n[p]),p--;else $t(n[d]),d++;for(;u<=_;){const e=bt(t,l[_+1]);wt(e,o[u]),l[u++]=e}for(;d<=p;){const t=n[d++];null!==t&&$t(t)}return this.ut=a,xt(t,l),G}}),Ct=new WeakMap;let Dt=0;class Pt extends ht{constructor(){super(...arguments),this.title="",this.icon="",this.wide=!1,this.dismissOnScrim=!1,this.hass=null,this._opener=null,this._focusRoot=null,this._useHaDialog=!1,this._closing=!1,this._overlays=[],this._titleId="hp-dialog-title-"+ ++Dt,this._focusInitial=()=>{const t=this._focusableElements(),e=t.find(t=>t.hasAttribute("autofocus"))||t[0]||(this._useHaDialog?null:this.renderRoot.querySelector(".close"))||this.renderRoot.querySelector(".surface")||this.renderRoot.querySelector("ha-dialog");e?.focus({preventScroll:!0})},this._requestClose=()=>{this._closing||(this._closing=!0,this.dispatchEvent(new CustomEvent("hp-close",{bubbles:!0,composed:!0})))},this._onKeyDown=t=>{if("Escape"===t.key){t.preventDefault(),t.stopImmediatePropagation(),this._pruneOverlays();const e=this._overlays[this._overlays.length-1];return e?void this._closeOverlay(e,"escape"):void this._requestClose()}if("Tab"!==t.key||this._useHaDialog)return;const e=this.renderRoot.querySelector(".close"),i=e?[e,...this._focusableElements()]:this._focusableElements();if(!i.length)return t.preventDefault(),void this.renderRoot.querySelector(".surface")?.focus({preventScroll:!0});const s=this._deepActiveElement(),n=i[0],o=i[i.length-1];!t.shiftKey||s!==n&&i.includes(s)?t.shiftKey||s!==o||(t.preventDefault(),n.focus()):(t.preventDefault(),o.focus())},this._onFallbackCancel=t=>{t.preventDefault(),this._requestClose()},this._onFallbackClick=t=>{this.dismissOnScrim&&t.target===t.currentTarget&&this._requestClose()}}connectedCallback(){super.connectedCallback(),this._opener=this._deepActiveElement();const t=this.getRootNode();this._focusRoot=t;const e=Ct.get(t)||{dialogs:new Set,opener:this._opener};e.dialogs.add(this),Ct.set(t,e),this._useHaDialog=!!customElements.get("ha-dialog"),this.addEventListener("keydown",this._onKeyDown,!0)}disconnectedCallback(){this.removeEventListener("keydown",this._onKeyDown,!0);const t=[...this._overlays];this._overlays=[];for(const e of t.reverse())e.close("disconnect");const e=this._focusRoot,i=this._opener;this._opener=null,this._focusRoot=null;const s=e?Ct.get(e):void 0;s?.dialogs.delete(this),super.disconnectedCallback(),e&&s&&requestAnimationFrame(()=>{const t=Ct.get(e);if(!t)return;if(!t.dialogs.size)return t.opener?.isConnected&&t.opener.focus({preventScroll:!0}),void Ct.delete(e);const s=i?.closest("hp-dialog");i?.isConnected&&s&&t.dialogs.has(s)&&i.focus({preventScroll:!0})})}firstUpdated(t){if(super.firstUpdated(t),!this._useHaDialog){const t=this.renderRoot.querySelector("dialog");t&&!t.open&&t.showModal()}queueMicrotask(()=>this._focusInitial())}_deepActiveElement(){let t=document.activeElement;for(;t?.shadowRoot?.activeElement;)t=t.shadowRoot.activeElement;return t&&t!==document.body?t:null}_focusableElements(){const t=["[autofocus]","a[href]","button:not([disabled])",'input:not([disabled]):not([type="hidden"])',"select:not([disabled])","textarea:not([disabled])",'[contenteditable="true"]','[tabindex]:not([tabindex="-1"])'].join(","),e=[],i=new Set,s=n=>{if(!i.has(n)){if(i.add(n),n instanceof HTMLElement){if(n.matches(t)&&e.push(n),n instanceof HTMLSlotElement){for(const t of n.assignedNodes({flatten:!0}))s(t);return}if(n.shadowRoot){for(const t of n.shadowRoot.childNodes)s(t);return}}for(const t of n.childNodes)s(t)}};for(const t of this.childNodes)s(t);const n=this.overlayPortal();return n&&s(n),e.filter(t=>{let e=t;for(;e;){const t=getComputedStyle(e);if(e.hidden||e.inert||"true"===e.getAttribute("aria-hidden")||"none"===t.display||"hidden"===t.visibility)return!1;if(e=e.assignedSlot||e.parentElement||(e.getRootNode()instanceof ShadowRoot?e.getRootNode().host:null),e===this)break}return!0})}_pruneOverlays(){this._overlays=this._overlays.filter(t=>t.owner.isConnected)}_closeOverlay(t,e){const i=this._overlays.findIndex(e=>e.token===t.token);i>=0&&this._overlays.splice(i,1),t.close(e)}registerOverlay(t){this._pruneOverlays();const e=this._overlays.find(e=>e.owner===t.owner);e&&this._overlays.splice(this._overlays.indexOf(e),1);const i=t.group||"transient";for(const t of[...this._overlays].reverse())t.group===i&&this._closeOverlay(t,"exclusive");const s={...t,group:i,token:Symbol("hp-overlay")};this._overlays.push(s);let n=!1;return()=>{if(n)return;n=!0;const t=this._overlays.findIndex(t=>t.token===s.token);t>=0&&this._overlays.splice(t,1)}}closeTransientOverlays(t="outside"){this._pruneOverlays();const e=[...this._overlays].filter(t=>"transient"===(t.group||"transient"));for(const i of e.reverse())this._closeOverlay(i,t);return e.length>0}overlayPortal(){return this.renderRoot.querySelector(".overlay-portal")}render(){const t=W` ${this.icon?W``:U} ${this.title} `;return this._useHaDialog?W`"number"==typeof t&&Number.isFinite(t)))return null;const[i,s,n]=e.map(t=>Math.round(Math.min(255,Math.max(0,t))));return`rgb(${i}, ${s}, ${n})`}const Ft=(t,e,i)=>Math.min(i,Math.max(e,Number.isFinite(t)?t:e));function Et(t){return Number.isFinite(t)?(t%360+360)%360:0}function Nt(t){if("string"!=typeof t)return null;const e=t.trim().replace(/^#/,""),i=/^[0-9a-fA-F]{3}$/.test(e)?e.split("").map(t=>`${t}${t}`).join(""):e;return/^[0-9a-fA-F]{6}$/.test(i)?`#${i.toLowerCase()}`:null}function Ot(t){const e=Nt(t);return e?{r:Number.parseInt(e.slice(1,3),16),g:Number.parseInt(e.slice(3,5),16),b:Number.parseInt(e.slice(5,7),16)}:null}function Ht(t){const e=Ft(t.r,0,255)/255,i=Ft(t.g,0,255)/255,s=Ft(t.b,0,255)/255,n=Math.max(e,i,s),o=n-Math.min(e,i,s);let a=0;return o>0&&(a=n===e?(i-s)/o%6*60:n===i?60*((s-e)/o+2):60*((e-i)/o+4)),{h:Et(a),s:0===n?0:o/n*100,v:100*n}}function Lt(t){return function(t){const e=t=>Math.round(Ft(t,0,255)).toString(16).padStart(2,"0");return`#${e(t.r)}${e(t.g)}${e(t.b)}`}(function(t){const e=Et(t.h),i=Ft(t.s,0,100)/100,s=Ft(t.v,0,100)/100,n=s*i,o=n*(1-Math.abs(e/60%2-1)),a=s-n;let r;return r=e<60?[n,o,0]:e<120?[o,n,0]:e<180?[0,n,o]:e<240?[0,o,n]:e<300?[o,0,n]:[n,0,o],{r:Math.round(255*(r[0]+a)),g:Math.round(255*(r[1]+a)),b:Math.round(255*(r[2]+a))}}(t))}function Bt(t){const e=t.visualViewport;return e&&Number.isFinite(e.width)&&Number.isFinite(e.height)?{left:Number(e.offsetLeft)||0,top:Number(e.offsetTop)||0,width:Math.max(0,Number(e.width)||0),height:Math.max(0,Number(e.height)||0)}:{left:0,top:0,width:t.innerWidth,height:t.innerHeight}}function qt(t,e,i,s=7,n=8){const o=i.left+n,a=i.top+n,r=Math.max(o,i.left+i.width-n),l=Math.max(a,i.top+i.height-n),c=Math.max(0,r-o),h=Math.max(0,l-a),d=Math.min(Math.max(0,e.width),c),p=Math.min(Math.max(0,e.height),h);let u=t.left;u+d>r&&(u=t.right-d),u=Math.min(Math.max(o,u),Math.max(o,r-d));const _=t.bottom+s,m=t.top-s-p;let g="bottom",f=_;return _+p>l&&m>=a?(g="top",f=Math.min(Math.max(a,m),Math.max(a,l-p))):f=Math.min(Math.max(a,_),Math.max(a,l-p)),{left:Math.round(u),top:Math.round(f),side:g,maxWidth:Math.round(c),maxHeight:Math.round(h)}}class Wt{constructor(t,e,i){this.owner=t,this.name=e,this.keydown=i,this.host=null,this.root=null}window(){return this.owner.ownerDocument.defaultView}dialog(){return this.owner.closest("hp-dialog")}usesPopover(t=!1){const e=this.window();return!t&&!!e&&function(t){const e=t.HTMLElement?.prototype;return"function"==typeof e?.showPopover&&"function"==typeof e?.hidePopover}(e)}get hasFallback(){return!!this.host}containsPath(t){return t.includes(this.owner)||!!this.host&&t.includes(this.host)}ownsActiveElement(){let t=this.owner.ownerDocument.activeElement;for(;t?.shadowRoot?.activeElement;)t=t.shadowRoot.activeElement;return!(!t||t!==this.owner&&!this.owner.shadowRoot?.contains(t)&&!this.root?.contains(t))}renderFallback(t,e){const i=this.ensureFallback();return i?(lt(W`${t}`,i),i):null}surface(t,e){return e?this.owner.shadowRoot?.querySelector(t)||null:this.root?.querySelector(t)||null}destroy(){this.root&<(U,this.root),this.host?.remove(),this.host=null,this.root=null}ensureFallback(){if(this.root?.isConnected)return this.root;const t=this.dialog()?.overlayPortal()||this.owner.ownerDocument.body;if(!t)return null;const e=this.owner.ownerDocument.createElement("div");return e.dataset.hpOverlay=this.name,e.style.display="contents",this.keydown&&e.addEventListener("keydown",this.keydown,!0),t.append(e),this.host=e,this.root=e.attachShadow({mode:"open"}),this.root}}const jt={title:"Color picker",hue:"Hue",saturation:"Saturation",value:"Brightness",hex:"Hex color",invalidHex:"Enter a 3- or 6-digit hex color"};class Gt extends ht{constructor(){super(...arguments),this.label="",this.opacityLabel="Opacity",this.color="#607d8b",this.opacity=1,this.disabled=!1,this.showOpacity=!0,this.pickerLabels=jt,this._open=!1,this._pickerRaf=0,this._emitRaf=0,this._forceFallback=!1,this._overlayDispose=null,this._hue=0,this._saturation=0,this._value=0,this._hexDraft="#607d8b",this._hexInvalid=!1,this._lastValidColor="#607d8b",this._activePointerId=null,this._outsidePointerDown=t=>{if(!this._open)return;const e=t.composedPath();this._floating.containsPath(e)||this._closePicker(!1,"outside")},this._keyDown=t=>{this._open&&"Escape"===t.key&&(t.preventDefault(),t.stopImmediatePropagation(),this._closePicker(this._floating.ownsActiveElement(),"escape"))},this._floating=new Wt(this,"color-opacity",this._keyDown),this._queuePickerPosition=()=>{if(!this._open)return;this._pickerRaf&&cancelAnimationFrame(this._pickerRaf);const t=this._window();t&&(this._pickerRaf=t.requestAnimationFrame(()=>{this._pickerRaf=0,this._positionPicker()||this._closePicker()}))}}connectedCallback(){super.connectedCallback(),this._syncFromColor(this.color,!1),this.ownerDocument.addEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.addEventListener("scroll",this._queuePickerPosition,!0);const t=this.ownerDocument.defaultView;t?.addEventListener("resize",this._queuePickerPosition),t?.addEventListener("orientationchange",this._queuePickerPosition),t?.visualViewport?.addEventListener("resize",this._queuePickerPosition),t?.visualViewport?.addEventListener("scroll",this._queuePickerPosition),this.addEventListener("keydown",this._keyDown,!0)}disconnectedCallback(){this.ownerDocument.removeEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.removeEventListener("scroll",this._queuePickerPosition,!0);const t=this.ownerDocument.defaultView;t?.removeEventListener("resize",this._queuePickerPosition),t?.removeEventListener("orientationchange",this._queuePickerPosition),t?.visualViewport?.removeEventListener("resize",this._queuePickerPosition),t?.visualViewport?.removeEventListener("scroll",this._queuePickerPosition),this.removeEventListener("keydown",this._keyDown,!0),this._pickerRaf&&cancelAnimationFrame(this._pickerRaf),this._emitRaf&&cancelAnimationFrame(this._emitRaf),this._pickerRaf=0,this._emitRaf=0,this._closePicker(!1,"disconnect"),super.disconnectedCallback()}updated(t){if(t.has("color")){const t=Nt(this.color);t&&t!==this._lastValidColor&&this._syncFromColor(t,this._open)}this.disabled&&this._open?this._closePicker():this._open&&(this._supportsPopover()||this._renderFallback(),(t.has("color")||t.has("opacity")||t.has("showOpacity")||t.has("label")||t.has("opacityLabel")||t.has("pickerLabels"))&&this._queuePickerPosition())}_window(){return this._floating.window()}_supportsPopover(){return this._floating.usesPopover(this._forceFallback)}_dialog(){return this._floating.dialog()}async _toggle(){this.disabled||(this._open?this._closePicker():(this._forceFallback=!1,this._syncFromColor(this.color,!1),this._hexInvalid=!1,this._activePointerId=null,this._open=!0,await this.updateComplete,this._open&&(this._supportsPopover()||this._renderFallback(),this._positionPicker()?this._overlayDispose=this._dialog()?.registerOverlay({owner:this,group:"transient",close:t=>this._closePicker("escape"===t&&this._floating.ownsActiveElement(),t)})||null:this._closePicker())))}_closePicker(t=!1,e="exclusive"){if(!this._open&&!this._floating.hasFallback)return;const i=this._overlayDispose;this._overlayDispose=null,i?.();const s=this.renderRoot.querySelector(".picker");if(s?.hidePopover)try{s.matches(":popover-open")&&s.hidePopover()}catch{}this._open=!1,this._cancelQueuedEmit(),this._activePointerId=null,this._floating.destroy(),t&&this.updateComplete.then(()=>this.renderRoot.querySelector(".trigger")?.focus())}_surface(){return this._floating.surface(".picker",this._supportsPopover())}_renderFallback(){const t=Gt.styles.cssText;this._floating.renderFallback(this._pickerTemplate(!1),t)}_positionPicker(){if(!this._open)return!1;const t=this._window(),e=this.renderRoot.querySelector(".trigger");let i=this._surface();if(!t||!e?.isConnected||!i?.isConnected)return!1;const s=Bt(t);if(i.style.maxWidth=`${Math.max(0,s.width-16)}px`,i.style.maxHeight=`${Math.max(0,s.height-16)}px`,i.style.visibility="hidden",this._supportsPopover()&&i.showPopover)try{i.matches(":popover-open")||i.showPopover()}catch{if(this._forceFallback=!0,this._renderFallback(),i=this._surface(),!i?.isConnected)return!1;i.style.maxWidth=`${Math.max(0,s.width-16)}px`,i.style.maxHeight=`${Math.max(0,s.height-16)}px`,i.style.visibility="hidden"}const n=e.getBoundingClientRect(),o=i.getBoundingClientRect();if(!(n.width&&n.height&&o.width&&o.height))return!1;const a=qt(n,o,s);return i.style.left=`${a.left}px`,i.style.top=`${a.top}px`,i.dataset.side=a.side,i.style.visibility="",!0}_labels(){const t=this.pickerLabels||jt;return{title:t.title||jt.title,hue:t.hue||jt.hue,saturation:t.saturation||jt.saturation,value:t.value||jt.value,hex:t.hex||jt.hex,invalidHex:t.invalidHex||jt.invalidHex}}_syncFromColor(t,e){const i=Nt(Rt(t,"#607d8b"))||"#607d8b",s=Ot(i);if(!s)return;const n=Ht(s);(!e||n.s>1e-4)&&(this._hue=n.h),this._saturation=n.s,this._value=n.v,this._hexDraft=i,this._hexInvalid=!1,this._lastValidColor=i}_cancelQueuedEmit(t=!1){this._emitRaf&&cancelAnimationFrame(this._emitRaf),this._emitRaf=0,t&&this._syncFromColor(this._lastValidColor,!0)}_queueHsvEmit(){if(this._emitRaf)return;const t=this._window();t?this._emitRaf=t.requestAnimationFrame(()=>{this._emitRaf=0,this._emitHsv()}):this._emitHsv()}_emitHsv(){this._emit(Lt({h:this._hue,s:this._saturation,v:this._value}),this.opacity)}_setHue(t){this._hue=Math.min(359,Math.max(0,Number.isFinite(t)?t:0)),this._emitHsv()}_setSaturation(t){this._saturation=Math.min(100,Math.max(0,Number.isFinite(t)?t:0)),this._emitHsv()}_setValue(t){this._value=Math.min(100,Math.max(0,Number.isFinite(t)?t:0)),this._emitHsv()}_setOpacity(t){this._emit(this._lastValidColor,t)}_shiftRangeKey(t,e,i,s,n){if(!t.shiftKey)return;const o="ArrowRight"===t.key||"ArrowUp"===t.key?10:"ArrowLeft"===t.key||"ArrowDown"===t.key?-10:0;o&&(t.preventDefault(),t.stopPropagation(),n(Math.min(s,Math.max(i,e+o))))}_setSvFromPointer(t,e){const i=t.currentTarget.getBoundingClientRect();i.width&&i.height&&(this._saturation=Math.min(100,Math.max(0,(t.clientX-i.left)/i.width*100)),this._value=Math.min(100,Math.max(0,100*(1-(t.clientY-i.top)/i.height))),e?(this._cancelQueuedEmit(),this._emitHsv()):this._queueHsvEmit())}_svPointerDown(t){if(t.preventDefault(),t.stopPropagation(),null===this._activePointerId){this._activePointerId=t.pointerId;try{t.currentTarget.setPointerCapture(t.pointerId)}catch{}this._setSvFromPointer(t,!0)}}_svPointerMove(t){t.pointerId===this._activePointerId&&(t.preventDefault(),t.stopPropagation(),this._setSvFromPointer(t,!1))}_svPointerUp(t){if(t.pointerId===this._activePointerId){t.preventDefault(),t.stopPropagation(),this._setSvFromPointer(t,!0),this._activePointerId=null;try{t.currentTarget.releasePointerCapture(t.pointerId)}catch{}}}_svPointerCancel(t){t.pointerId===this._activePointerId&&(t.preventDefault(),t.stopPropagation(),this._activePointerId=null,this._cancelQueuedEmit(!0))}_svLostPointerCapture(t){t.pointerId===this._activePointerId&&(this._activePointerId=null,this._cancelQueuedEmit(!0))}_hexInput(t){const e=t.target.value;this._hexDraft=e,this._hexInvalid=!1;const i=Nt(e),s=i?Ot(i):null;if(!i||!s)return;const n=Ht(s);n.s>1e-4&&(this._hue=n.h),this._saturation=n.s,this._value=n.v,this._emit(i,this.opacity,!0)}_commitHex(){const t=Nt(this._hexDraft);if(!t)return this._hexDraft=this._lastValidColor,void(this._hexInvalid=!0);this._hexDraft=t,this._hexInvalid=!1,t!==this._lastValidColor&&this._emit(t,this.opacity)}_hexKeyDown(t){"Enter"===t.key&&(t.preventDefault(),t.stopPropagation(),this._commitHex())}_emit(t,e,i=!1){const s=Nt(t)||this._lastValidColor,n=Number.isFinite(e)?e:Number(this.opacity)||0,o=Math.min(1,Math.max(0,n));this.color=s,this.opacity=o,this._lastValidColor=s,i||(this._hexDraft=s),this._hexInvalid=!1,this.dispatchEvent(new CustomEvent("hp-color-opacity-change",{detail:{color:s,opacity:o},bubbles:!0,composed:!0}))}_pickerTemplate(t){const e=Math.round(100*Math.min(1,Math.max(0,Number(this.opacity)||0))),i=Lt({h:this._hue,s:this._saturation,v:this._value}),s=Lt({h:this._hue,s:100,v:100}),n=this._labels(),o=Math.round(this._hue),a=Math.round(this._saturation),r=Math.round(this._value);return W` + `,customElements.get("hp-dialog")||customElements.define("hp-dialog",Pt);const Tt=/^#[0-9a-fA-F]{6}$/;function Rt(t,e){return"string"==typeof t&&Tt.test(t)?t:e}const At="(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])",It=new RegExp(`^rgb\\(${At}, ${At}, ${At}\\)$`);function zt(t){if(!Array.isArray(t)||t.length<3)return null;const e=t.slice(0,3);if(!e.every(t=>"number"==typeof t&&Number.isFinite(t)))return null;const[i,s,n]=e.map(t=>Math.round(Math.min(255,Math.max(0,t))));return`rgb(${i}, ${s}, ${n})`}const Ft=(t,e,i)=>Math.min(i,Math.max(e,Number.isFinite(t)?t:e));function Et(t){return Number.isFinite(t)?(t%360+360)%360:0}function Nt(t){if("string"!=typeof t)return null;const e=t.trim().replace(/^#/,""),i=/^[0-9a-fA-F]{3}$/.test(e)?e.split("").map(t=>`${t}${t}`).join(""):e;return/^[0-9a-fA-F]{6}$/.test(i)?`#${i.toLowerCase()}`:null}function Ot(t){const e=Nt(t);return e?{r:Number.parseInt(e.slice(1,3),16),g:Number.parseInt(e.slice(3,5),16),b:Number.parseInt(e.slice(5,7),16)}:null}function Ht(t){const e=Ft(t.r,0,255)/255,i=Ft(t.g,0,255)/255,s=Ft(t.b,0,255)/255,n=Math.max(e,i,s),o=n-Math.min(e,i,s);let a=0;return o>0&&(a=n===e?(i-s)/o%6*60:n===i?60*((s-e)/o+2):60*((e-i)/o+4)),{h:Et(a),s:0===n?0:o/n*100,v:100*n}}function Lt(t){return function(t){const e=t=>Math.round(Ft(t,0,255)).toString(16).padStart(2,"0");return`#${e(t.r)}${e(t.g)}${e(t.b)}`}(function(t){const e=Et(t.h),i=Ft(t.s,0,100)/100,s=Ft(t.v,0,100)/100,n=s*i,o=n*(1-Math.abs(e/60%2-1)),a=s-n;let r;return r=e<60?[n,o,0]:e<120?[o,n,0]:e<180?[0,n,o]:e<240?[0,o,n]:e<300?[o,0,n]:[n,0,o],{r:Math.round(255*(r[0]+a)),g:Math.round(255*(r[1]+a)),b:Math.round(255*(r[2]+a))}}(t))}function Bt(t){const e=t.visualViewport;return e&&Number.isFinite(e.width)&&Number.isFinite(e.height)?{left:Number(e.offsetLeft)||0,top:Number(e.offsetTop)||0,width:Math.max(0,Number(e.width)||0),height:Math.max(0,Number(e.height)||0)}:{left:0,top:0,width:t.innerWidth,height:t.innerHeight}}function qt(t,e,i,s=7,n=8){const o=i.left+n,a=i.top+n,r=Math.max(o,i.left+i.width-n),l=Math.max(a,i.top+i.height-n),c=Math.max(0,r-o),h=Math.max(0,l-a),d=Math.min(Math.max(0,e.width),c),p=Math.min(Math.max(0,e.height),h);let u=t.left;u+d>r&&(u=t.right-d),u=Math.min(Math.max(o,u),Math.max(o,r-d));const _=t.bottom+s,m=t.top-s-p;let g="bottom",f=_;return _+p>l&&m>=a?(g="top",f=Math.min(Math.max(a,m),Math.max(a,l-p))):f=Math.min(Math.max(a,_),Math.max(a,l-p)),{left:Math.round(u),top:Math.round(f),side:g,maxWidth:Math.round(c),maxHeight:Math.round(h)}}class Wt{constructor(t,e,i){this.owner=t,this.name=e,this.keydown=i,this.host=null,this.root=null}window(){return this.owner.ownerDocument.defaultView}dialog(){return this.owner.closest("hp-dialog")}usesPopover(t=!1){const e=this.window();return!t&&!!e&&function(t){const e=t.HTMLElement?.prototype;return"function"==typeof e?.showPopover&&"function"==typeof e?.hidePopover}(e)}get hasFallback(){return!!this.host}containsPath(t){return t.includes(this.owner)||!!this.host&&t.includes(this.host)}ownsActiveElement(){let t=this.owner.ownerDocument.activeElement;for(;t?.shadowRoot?.activeElement;)t=t.shadowRoot.activeElement;return!(!t||t!==this.owner&&!this.owner.shadowRoot?.contains(t)&&!this.root?.contains(t))}renderFallback(t,e){const i=this.ensureFallback();return i?(lt(W`${t}`,i),i):null}surface(t,e){return e?this.owner.shadowRoot?.querySelector(t)||null:this.root?.querySelector(t)||null}destroy(){this.root&<(U,this.root),this.host?.remove(),this.host=null,this.root=null}ensureFallback(){if(this.root?.isConnected)return this.root;const t=this.dialog()?.overlayPortal()||this.owner.ownerDocument.body;if(!t)return null;const e=this.owner.ownerDocument.createElement("div");return e.dataset.hpOverlay=this.name,e.style.display="contents",this.keydown&&e.addEventListener("keydown",this.keydown,!0),t.append(e),this.host=e,this.root=e.attachShadow({mode:"open"}),this.root}}const jt={title:"Color picker",hue:"Hue",saturation:"Saturation",value:"Brightness",hex:"Hex color",invalidHex:"Enter a 3- or 6-digit hex color"};class Gt extends ht{constructor(){super(...arguments),this.label="",this.opacityLabel="Opacity",this.color="#607d8b",this.opacity=1,this.disabled=!1,this.showOpacity=!0,this.pickerLabels=jt,this._open=!1,this._pickerRaf=0,this._emitRaf=0,this._forceFallback=!1,this._overlayDispose=null,this._hue=0,this._saturation=0,this._value=0,this._hexDraft="#607d8b",this._hexInvalid=!1,this._lastValidColor="#607d8b",this._activePointerId=null,this._outsidePointerDown=t=>{if(!this._open)return;const e=t.composedPath();this._floating.containsPath(e)||this._closePicker(!1,"outside")},this._keyDown=t=>{this._open&&"Escape"===t.key&&(t.preventDefault(),t.stopImmediatePropagation(),this._closePicker(this._floating.ownsActiveElement(),"escape"))},this._floating=new Wt(this,"color-opacity",this._keyDown),this._queuePickerPosition=()=>{if(!this._open)return;this._pickerRaf&&cancelAnimationFrame(this._pickerRaf);const t=this._window();t&&(this._pickerRaf=t.requestAnimationFrame(()=>{this._pickerRaf=0,this._positionPicker()||this._closePicker()}))}}connectedCallback(){super.connectedCallback(),this._syncFromColor(this.color,!1),this.ownerDocument.addEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.addEventListener("scroll",this._queuePickerPosition,!0);const t=this.ownerDocument.defaultView;t?.addEventListener("resize",this._queuePickerPosition),t?.addEventListener("orientationchange",this._queuePickerPosition),t?.visualViewport?.addEventListener("resize",this._queuePickerPosition),t?.visualViewport?.addEventListener("scroll",this._queuePickerPosition),this.addEventListener("keydown",this._keyDown,!0)}disconnectedCallback(){this.ownerDocument.removeEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.removeEventListener("scroll",this._queuePickerPosition,!0);const t=this.ownerDocument.defaultView;t?.removeEventListener("resize",this._queuePickerPosition),t?.removeEventListener("orientationchange",this._queuePickerPosition),t?.visualViewport?.removeEventListener("resize",this._queuePickerPosition),t?.visualViewport?.removeEventListener("scroll",this._queuePickerPosition),this.removeEventListener("keydown",this._keyDown,!0),this._pickerRaf&&cancelAnimationFrame(this._pickerRaf),this._emitRaf&&cancelAnimationFrame(this._emitRaf),this._pickerRaf=0,this._emitRaf=0,this._closePicker(!1,"disconnect"),super.disconnectedCallback()}updated(t){if(t.has("color")){const t=Nt(this.color);t&&t!==this._lastValidColor&&this._syncFromColor(t,this._open)}this.disabled&&this._open?this._closePicker():this._open&&(this._supportsPopover()||this._renderFallback(),(t.has("color")||t.has("opacity")||t.has("showOpacity")||t.has("label")||t.has("opacityLabel")||t.has("pickerLabels"))&&this._queuePickerPosition())}_window(){return this._floating.window()}_supportsPopover(){return this._floating.usesPopover(this._forceFallback)}_dialog(){return this._floating.dialog()}async _toggle(){this.disabled||(this._open?this._closePicker():(this._forceFallback=!1,this._syncFromColor(this.color,!1),this._hexInvalid=!1,this._activePointerId=null,this._open=!0,await this.updateComplete,this._open&&(this._supportsPopover()||this._renderFallback(),this._positionPicker()?this._overlayDispose=this._dialog()?.registerOverlay({owner:this,group:"transient",close:t=>this._closePicker("escape"===t&&this._floating.ownsActiveElement(),t)})||null:this._closePicker())))}_closePicker(t=!1,e="exclusive"){if(!this._open&&!this._floating.hasFallback)return;const i=this._overlayDispose;this._overlayDispose=null,i?.();const s=this.renderRoot.querySelector(".picker");if(s?.hidePopover)try{s.matches(":popover-open")&&s.hidePopover()}catch{}this._open=!1,this._cancelQueuedEmit(),this._activePointerId=null,this._floating.destroy(),t&&this.updateComplete.then(()=>this.renderRoot.querySelector(".trigger")?.focus())}_surface(){return this._floating.surface(".picker",this._supportsPopover())}_renderFallback(){const t=Gt.styles.cssText;this._floating.renderFallback(this._pickerTemplate(!1),t)}_positionPicker(){if(!this._open)return!1;const t=this._window(),e=this.renderRoot.querySelector(".trigger");let i=this._surface();if(!t||!e?.isConnected||!i?.isConnected)return!1;const s=Bt(t);if(i.style.maxWidth=`${Math.max(0,s.width-16)}px`,i.style.maxHeight=`${Math.max(0,s.height-16)}px`,i.style.visibility="hidden",this._supportsPopover()&&i.showPopover)try{i.matches(":popover-open")||i.showPopover()}catch{if(this._forceFallback=!0,this._renderFallback(),i=this._surface(),!i?.isConnected)return!1;i.style.maxWidth=`${Math.max(0,s.width-16)}px`,i.style.maxHeight=`${Math.max(0,s.height-16)}px`,i.style.visibility="hidden"}const n=e.getBoundingClientRect(),o=i.getBoundingClientRect();if(!(n.width&&n.height&&o.width&&o.height))return!1;const a=qt(n,o,s);return i.style.left=`${a.left}px`,i.style.top=`${a.top}px`,i.dataset.side=a.side,i.style.visibility="",!0}_labels(){const t=this.pickerLabels||jt;return{title:t.title||jt.title,hue:t.hue||jt.hue,saturation:t.saturation||jt.saturation,value:t.value||jt.value,hex:t.hex||jt.hex,invalidHex:t.invalidHex||jt.invalidHex}}_syncFromColor(t,e){const i=Nt(Rt(t,"#607d8b"))||"#607d8b",s=Ot(i);if(!s)return;const n=Ht(s);(!e||n.s>1e-4)&&(this._hue=n.h),this._saturation=n.s,this._value=n.v,this._hexDraft=i,this._hexInvalid=!1,this._lastValidColor=i}_cancelQueuedEmit(t=!1){this._emitRaf&&cancelAnimationFrame(this._emitRaf),this._emitRaf=0,t&&this._syncFromColor(this._lastValidColor,!0)}_queueHsvEmit(){if(this._emitRaf)return;const t=this._window();t?this._emitRaf=t.requestAnimationFrame(()=>{this._emitRaf=0,this._emitHsv()}):this._emitHsv()}_emitHsv(){this._emit(Lt({h:this._hue,s:this._saturation,v:this._value}),this.opacity)}_setHue(t){this._hue=Math.min(359,Math.max(0,Number.isFinite(t)?t:0)),this._emitHsv()}_setSaturation(t){this._saturation=Math.min(100,Math.max(0,Number.isFinite(t)?t:0)),this._emitHsv()}_setValue(t){this._value=Math.min(100,Math.max(0,Number.isFinite(t)?t:0)),this._emitHsv()}_setOpacity(t){this._emit(this._lastValidColor,t)}_shiftRangeKey(t,e,i,s,n){if(!t.shiftKey)return;const o="ArrowRight"===t.key||"ArrowUp"===t.key?10:"ArrowLeft"===t.key||"ArrowDown"===t.key?-10:0;o&&(t.preventDefault(),t.stopPropagation(),n(Math.min(s,Math.max(i,e+o))))}_setSvFromPointer(t,e){const i=t.currentTarget.getBoundingClientRect();i.width&&i.height&&(this._saturation=Math.min(100,Math.max(0,(t.clientX-i.left)/i.width*100)),this._value=Math.min(100,Math.max(0,100*(1-(t.clientY-i.top)/i.height))),e?(this._cancelQueuedEmit(),this._emitHsv()):this._queueHsvEmit())}_svPointerDown(t){if(t.preventDefault(),t.stopPropagation(),null===this._activePointerId){this._activePointerId=t.pointerId;try{t.currentTarget.setPointerCapture(t.pointerId)}catch{}this._setSvFromPointer(t,!0)}}_svPointerMove(t){t.pointerId===this._activePointerId&&(t.preventDefault(),t.stopPropagation(),this._setSvFromPointer(t,!1))}_svPointerUp(t){if(t.pointerId===this._activePointerId){t.preventDefault(),t.stopPropagation(),this._setSvFromPointer(t,!0),this._activePointerId=null;try{t.currentTarget.releasePointerCapture(t.pointerId)}catch{}}}_svPointerCancel(t){t.pointerId===this._activePointerId&&(t.preventDefault(),t.stopPropagation(),this._activePointerId=null,this._cancelQueuedEmit(!0))}_svLostPointerCapture(t){t.pointerId===this._activePointerId&&(this._activePointerId=null,this._cancelQueuedEmit(!0))}_hexInput(t){const e=t.target.value;this._hexDraft=e,this._hexInvalid=!1;const i=Nt(e),s=i?Ot(i):null;if(!i||!s)return;const n=Ht(s);n.s>1e-4&&(this._hue=n.h),this._saturation=n.s,this._value=n.v,this._emit(i,this.opacity,!0)}_commitHex(){const t=Nt(this._hexDraft);if(!t)return this._hexDraft=this._lastValidColor,void(this._hexInvalid=!0);this._hexDraft=t,this._hexInvalid=!1,t!==this._lastValidColor&&this._emit(t,this.opacity)}_hexKeyDown(t){"Enter"===t.key&&(t.preventDefault(),t.stopPropagation(),this._commitHex())}_emit(t,e,i=!1){const s=Nt(t)||this._lastValidColor,n=Number.isFinite(e)?e:Number(this.opacity)||0,o=Math.min(1,Math.max(0,n));this.color=s,this.opacity=o,this._lastValidColor=s,i||(this._hexDraft=s),this._hexInvalid=!1,this.dispatchEvent(new CustomEvent("hp-color-opacity-change",{detail:{color:s,opacity:o},bubbles:!0,composed:!0}))}_pickerTemplate(t){const e=Math.round(100*Math.min(1,Math.max(0,Number(this.opacity)||0))),i=Lt({h:this._hue,s:this._saturation,v:this._value}),s=Lt({h:this._hue,s:100,v:100}),n=this._labels(),o=Math.round(this._hue),a=Math.round(this._saturation),r=Math.round(this._value);return W`