mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-30 08:46:03 +00:00
House Plan Card v1.0.0: Lovelace card (TS+Lit) + houseplan integration (server-side layout storage), HACS-ready
This commit is contained in:
Executable
+17
@@ -0,0 +1,17 @@
|
||||
name: Release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 22 }
|
||||
- run: npm ci && npm run build
|
||||
- run: cp dist/houseplan-card.js custom_components/houseplan/frontend/
|
||||
- name: Attach card to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: dist/houseplan-card.js
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
tsout/
|
||||
*.log
|
||||
@@ -0,0 +1,68 @@
|
||||
# House Plan Card — интерактивный план дома для Home Assistant
|
||||
|
||||
Нативная Lovelace-карточка «План дома» (дача, Кирилловское) + интеграция для серверного
|
||||
хранения раскладки иконок. Замена прототипа `/local/houseplan/` (iframe + токен + localStorage).
|
||||
|
||||
- Работает **без токена** — использует объект `hass` фронтенда (сессия пользователя).
|
||||
- План этажей (подложки РЕМПЛАННЕР 1489×1053, 1 ед. = 1 px) + двор, комнаты-зоны кликабельны.
|
||||
- Устройства строятся **из реестров HA** (devices/entities/areas) с курированием и
|
||||
группировкой ламп — те же правила, что в прототипе. Новое устройство с area появляется само.
|
||||
- **Живые состояния**: температура у датчиков, подсветка вкл/выкл (свет/розетки/медиа),
|
||||
открыто/закрыто (шторы/ворота/замки/датчики), приглушение при `unavailable`. Всё реактивно.
|
||||
- Клик по иконке → штатное окно **more-info** (группа ламп → меню: вся группа / отдельные).
|
||||
- Клик по комнате → страница зоны (`/config/areas/area/…`) без перезагрузки.
|
||||
- **Правка**: кнопка ✥ — drag иконок + ввод X/Y; позиции сохраняются **на сервере**
|
||||
(WS-команды интеграции `houseplan`) — раскладка одна на все устройства и браузеры.
|
||||
Без интеграции карточка откатывается на localStorage.
|
||||
|
||||
## Установка (HACS)
|
||||
|
||||
1. HACS → меню (⋮) → **Custom repositories** → URL этого репозитория, категория **Integration**.
|
||||
2. Установить «House Plan», перезапустить HA.
|
||||
3. Настройки → Устройства и службы → **Добавить интеграцию → House Plan** (одна запись; опция
|
||||
«правка только администраторами»). Интеграция сама раздаёт и подключает JS карточки —
|
||||
ресурс Lovelace добавлять не нужно.
|
||||
4. В дашборд добавить карточку:
|
||||
|
||||
```yaml
|
||||
type: custom:houseplan-card
|
||||
title: План дома · Кирилловское
|
||||
default_floor: f1 # f1 | f2 | yard
|
||||
icon_size: 22 # px
|
||||
show_temperature: true
|
||||
live_states: true
|
||||
```
|
||||
|
||||
Только карточка (без серверного хранения): категория **Dashboard/Lovelace**, ресурс
|
||||
`/hacsfiles/houseplan-card/houseplan-card.js` — позиции будут в localStorage браузера.
|
||||
|
||||
## WS-API интеграции
|
||||
|
||||
| Команда | Параметры | Действие |
|
||||
|---|---|---|
|
||||
| `houseplan/layout/get` | — | вернуть раскладку `{device_id: {x, y}}` |
|
||||
| `houseplan/layout/set` | `layout` | заменить целиком |
|
||||
| `houseplan/layout/update` | `device_id`, `pos` | обновить одну позицию |
|
||||
|
||||
Хранилище: `.storage/houseplan.layout` (helpers.storage.Store).
|
||||
|
||||
## Почему не ha-floorplan
|
||||
|
||||
Оценивали `ExperienceLovelace/ha-floorplan` (установлен в этом HA): он закрывает SVG-план,
|
||||
привязку сущностей и стили по состоянию, но привязки статичны (entity → элемент SVG прописывается
|
||||
руками в YAML/SVG), нет автопоявления новых устройств из реестра, нет курирования/группировки
|
||||
ламп и главное — нет drag-раскладки с серверным сохранением. Перенос этих требований в
|
||||
ha-floorplan потребовал бы генератор SVG+конфига и всё равно не дал бы редактирование из UI.
|
||||
Своя карточка (~1 файл) оказалась дешевле и полностью повторяет прототип. Решение: **свой card**.
|
||||
|
||||
## Разработка
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build # dist/houseplan-card.js (+ скопировать в custom_components/houseplan/frontend/)
|
||||
```
|
||||
|
||||
Геометрия комнат/viewBox — `src/data/house.ts`; подложки — `src/data/backgrounds.ts`
|
||||
(генерируются из data.js прототипа); правила иконок/курирования — `src/rules.ts`.
|
||||
|
||||
Релиз: тег `vX.Y.Z` → GitHub Release; workflow соберёт и приложит `houseplan-card.js`.
|
||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Executable
+64
@@ -0,0 +1,64 @@
|
||||
"""House Plan: серверное хранилище раскладки + раздача Lovelace-карточки."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from homeassistant.components.frontend import add_extra_js_url
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from . import websocket_api as hp_ws
|
||||
from .const import DOMAIN, FRONTEND_URL, STORAGE_KEY, STORAGE_VERSION
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config) -> bool:
|
||||
"""Регистрируем WS-команды и хранилище на старте."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN]["store"] = Store(hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
hp_ws.async_register(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Config entry: статика фронтенда + авто-подключение JS."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN]["entry"] = entry
|
||||
entry.async_on_unload(entry.add_update_listener(_update_listener))
|
||||
|
||||
card_path = Path(__file__).parent / "frontend" / "houseplan-card.js"
|
||||
if card_path.exists():
|
||||
try:
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
|
||||
await hass.http.async_register_static_paths(
|
||||
[StaticPathConfig(FRONTEND_URL, str(card_path), cache_headers=False)]
|
||||
)
|
||||
except ImportError: # старые версии HA
|
||||
hass.http.register_static_path(FRONTEND_URL, str(card_path), cache_headers=False)
|
||||
add_extra_js_url(hass, f"{FRONTEND_URL}?v={_manifest_version(hass)}")
|
||||
else:
|
||||
_LOGGER.warning("houseplan-card.js не найден рядом с интеграцией: %s", card_path)
|
||||
return True
|
||||
|
||||
|
||||
def _manifest_version(hass: HomeAssistant) -> str:
|
||||
try:
|
||||
import json
|
||||
|
||||
manifest = Path(__file__).parent / "manifest.json"
|
||||
return json.loads(manifest.read_text()).get("version", "0")
|
||||
except Exception: # noqa: BLE001
|
||||
return "0"
|
||||
|
||||
|
||||
async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
hass.data[DOMAIN]["entry"] = entry
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass.data[DOMAIN].pop("entry", None)
|
||||
return True
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+41
@@ -0,0 +1,41 @@
|
||||
"""Config flow: одна запись без параметров."""
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
||||
from .const import CONF_ADMIN_ONLY, DOMAIN
|
||||
|
||||
|
||||
class HouseplanConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Установка в один шаг."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="House Plan", data={}, options=user_input)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Optional(CONF_ADMIN_ONLY, default=False): bool}),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def async_get_options_flow(config_entry):
|
||||
return HouseplanOptionsFlow()
|
||||
|
||||
|
||||
class HouseplanOptionsFlow(config_entries.OptionsFlow):
|
||||
"""Опция: правка раскладки только администраторами."""
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
current = self.config_entry.options.get(CONF_ADMIN_ONLY, False)
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema({vol.Optional(CONF_ADMIN_ONLY, default=current): bool}),
|
||||
)
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
"""Константы интеграции House Plan."""
|
||||
|
||||
DOMAIN = "houseplan"
|
||||
STORAGE_KEY = f"{DOMAIN}.layout"
|
||||
STORAGE_VERSION = 1
|
||||
FRONTEND_URL = "/houseplan_files/houseplan-card.js"
|
||||
CONF_ADMIN_ONLY = "admin_only"
|
||||
+403
File diff suppressed because one or more lines are too long
Executable
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "houseplan",
|
||||
"name": "House Plan",
|
||||
"codeowners": ["@justbusiness"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["http", "frontend", "websocket_api"],
|
||||
"documentation": "https://github.com/justbusiness/houseplan-card",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_push",
|
||||
"issue_tracker": "https://github.com/justbusiness/houseplan-card/issues",
|
||||
"requirements": [],
|
||||
"version": "1.0.0"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "House Plan",
|
||||
"data": { "admin_only": "Правка раскладки только администраторами" }
|
||||
}
|
||||
},
|
||||
"abort": { "single_instance_allowed": "Уже настроено — допускается одна запись." }
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": { "admin_only": "Правка раскладки только администраторами" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "House Plan",
|
||||
"data": { "admin_only": "Правка раскладки только администраторами" }
|
||||
}
|
||||
},
|
||||
"abort": { "single_instance_allowed": "Уже настроено — допускается одна запись." }
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": { "admin_only": "Правка раскладки только администраторами" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
"""WS-команды houseplan/layout/get|set|update — серверное хранение раскладки."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import CONF_ADMIN_ONLY, DOMAIN
|
||||
|
||||
POS_SCHEMA = vol.Schema({vol.Required("x"): vol.Coerce(float), vol.Required("y"): vol.Coerce(float)})
|
||||
LAYOUT_SCHEMA = vol.Schema({str: POS_SCHEMA})
|
||||
|
||||
|
||||
@callback
|
||||
def async_register(hass: HomeAssistant) -> None:
|
||||
"""Регистрация WS-команд."""
|
||||
websocket_api.async_register_command(hass, ws_layout_get)
|
||||
websocket_api.async_register_command(hass, ws_layout_set)
|
||||
websocket_api.async_register_command(hass, ws_layout_update)
|
||||
|
||||
|
||||
def _store(hass: HomeAssistant):
|
||||
return hass.data[DOMAIN]["store"]
|
||||
|
||||
|
||||
def _check_write(hass: HomeAssistant, connection) -> bool:
|
||||
entry = hass.data[DOMAIN].get("entry")
|
||||
admin_only = bool(entry and entry.options.get(CONF_ADMIN_ONLY, False))
|
||||
return connection.user.is_admin if admin_only else True
|
||||
|
||||
|
||||
@websocket_api.websocket_command({vol.Required("type"): "houseplan/layout/get"})
|
||||
@websocket_api.async_response
|
||||
async def ws_layout_get(hass: HomeAssistant, connection, msg: dict[str, Any]) -> None:
|
||||
"""Вернуть сохранённую раскладку."""
|
||||
data = await _store(hass).async_load() or {}
|
||||
connection.send_result(msg["id"], {"layout": data.get("layout", {})})
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{vol.Required("type"): "houseplan/layout/set", vol.Required("layout"): LAYOUT_SCHEMA}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def ws_layout_set(hass: HomeAssistant, connection, msg: dict[str, Any]) -> None:
|
||||
"""Полностью заменить раскладку."""
|
||||
if not _check_write(hass, connection):
|
||||
connection.send_error(msg["id"], "unauthorized", "Правка раскладки разрешена только администраторам")
|
||||
return
|
||||
await _store(hass).async_save({"layout": msg["layout"]})
|
||||
connection.send_result(msg["id"], {"ok": True})
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "houseplan/layout/update",
|
||||
vol.Required("device_id"): str,
|
||||
vol.Required("pos"): POS_SCHEMA,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def ws_layout_update(hass: HomeAssistant, connection, msg: dict[str, Any]) -> None:
|
||||
"""Обновить позицию одного устройства."""
|
||||
if not _check_write(hass, connection):
|
||||
connection.send_error(msg["id"], "unauthorized", "Правка раскладки разрешена только администраторам")
|
||||
return
|
||||
store = _store(hass)
|
||||
data = await store.async_load() or {}
|
||||
layout = data.get("layout", {})
|
||||
layout[msg["device_id"]] = msg["pos"]
|
||||
await store.async_save({"layout": layout})
|
||||
connection.send_result(msg["id"], {"ok": True})
|
||||
+403
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "House Plan (card + storage)",
|
||||
"render_readme": true,
|
||||
"homeassistant": "2024.6.0"
|
||||
}
|
||||
+915
@@ -0,0 +1,915 @@
|
||||
{
|
||||
"name": "houseplan-card",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "houseplan-card",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lit": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"rollup": "^4.18.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lit-labs/ssr-dom-shim": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz",
|
||||
"integrity": "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@lit/reactive-element": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz",
|
||||
"integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
|
||||
"integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.0.1",
|
||||
"@types/resolve": "1.20.2",
|
||||
"deepmerge": "^4.2.2",
|
||||
"is-module": "^1.0.0",
|
||||
"resolve": "^1.22.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.78.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-terser": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
|
||||
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"serialize-javascript": "^6.0.1",
|
||||
"smob": "^1.0.0",
|
||||
"terser": "^5.17.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-typescript": {
|
||||
"version": "11.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz",
|
||||
"integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.1.0",
|
||||
"resolve": "^1.22.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.14.0||^3.0.0||^4.0.0",
|
||||
"tslib": "*",
|
||||
"typescript": ">=3.7.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
},
|
||||
"tslib": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
|
||||
"integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
|
||||
"integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
|
||||
"integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
|
||||
"integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
|
||||
"integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
|
||||
"integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
|
||||
"integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
|
||||
"integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
|
||||
"integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
|
||||
"integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
|
||||
"integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
|
||||
"integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
|
||||
"integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
|
||||
"integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.16.2",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
|
||||
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-module": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
|
||||
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lit": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz",
|
||||
"integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^2.1.0",
|
||||
"lit-element": "^4.2.0",
|
||||
"lit-html": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lit-element": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz",
|
||||
"integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.5.0",
|
||||
"@lit/reactive-element": "^2.1.0",
|
||||
"lit-html": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lit-html": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz",
|
||||
"integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.12",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"is-core-module": "^2.16.1",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
|
||||
"integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.9"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.62.2",
|
||||
"@rollup/rollup-android-arm64": "4.62.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.62.2",
|
||||
"@rollup/rollup-darwin-x64": "4.62.2",
|
||||
"@rollup/rollup-freebsd-arm64": "4.62.2",
|
||||
"@rollup/rollup-freebsd-x64": "4.62.2",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.62.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.62.2",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.62.2",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.62.2",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.62.2",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-x64-musl": "4.62.2",
|
||||
"@rollup/rollup-openbsd-x64": "4.62.2",
|
||||
"@rollup/rollup-openharmony-arm64": "4.62.2",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.62.2",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.62.2",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.62.2",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.62.2",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/smob": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz",
|
||||
"integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.48.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz",
|
||||
"integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "houseplan-card",
|
||||
"version": "1.0.0",
|
||||
"description": "Interactive house plan Lovelace card for Home Assistant (dacha Kirillovskoe)",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"watch": "rollup -c --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"rollup": "^4.18.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"lit": "^3.1.3"
|
||||
}
|
||||
}
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
|
||||
export default {
|
||||
input: 'src/houseplan-card.ts',
|
||||
output: {
|
||||
file: 'dist/houseplan-card.js',
|
||||
format: 'es',
|
||||
sourcemap: false,
|
||||
},
|
||||
plugins: [resolve(), typescript(), terser({ format: { comments: false } })],
|
||||
};
|
||||
Executable
+6
File diff suppressed because one or more lines are too long
Executable
+140
@@ -0,0 +1,140 @@
|
||||
// Auto-generated from prototype data.js — geometry of house plan (1 unit = 1 px of 1489x1053 render)
|
||||
export const IMG_W = 1489;
|
||||
export const IMG_H = 1053;
|
||||
export interface Room { floor: string; name: string; area: string; x: number; y: number; w: number; h: number; }
|
||||
export const ROOMS: Room[] = [
|
||||
{
|
||||
"floor": "f1",
|
||||
"name": "Гостиная",
|
||||
"area": "gostinaia_na_dache",
|
||||
"x": 510,
|
||||
"y": 205,
|
||||
"w": 430,
|
||||
"h": 310
|
||||
},
|
||||
{
|
||||
"floor": "f1",
|
||||
"name": "Гостевой с/у",
|
||||
"area": "gostevoi_s_u",
|
||||
"x": 600,
|
||||
"y": 515,
|
||||
"w": 190,
|
||||
"h": 170
|
||||
},
|
||||
{
|
||||
"floor": "f1",
|
||||
"name": "Котельная",
|
||||
"area": "kotelnaia",
|
||||
"x": 600,
|
||||
"y": 690,
|
||||
"w": 190,
|
||||
"h": 180
|
||||
},
|
||||
{
|
||||
"floor": "f1",
|
||||
"name": "Кладовка уличная",
|
||||
"area": "kladovka",
|
||||
"x": 510,
|
||||
"y": 690,
|
||||
"w": 90,
|
||||
"h": 180
|
||||
},
|
||||
{
|
||||
"floor": "f1",
|
||||
"name": "Прихожая",
|
||||
"area": "prikhozhaia",
|
||||
"x": 790,
|
||||
"y": 600,
|
||||
"w": 150,
|
||||
"h": 270
|
||||
},
|
||||
{
|
||||
"floor": "f1",
|
||||
"name": "Гостевая спальня",
|
||||
"area": "gostevaia_komnata",
|
||||
"x": 945,
|
||||
"y": 600,
|
||||
"w": 255,
|
||||
"h": 270
|
||||
},
|
||||
{
|
||||
"floor": "f2",
|
||||
"name": "Детская Майя",
|
||||
"area": "detskaia_maiia",
|
||||
"x": 365,
|
||||
"y": 40,
|
||||
"w": 435,
|
||||
"h": 315
|
||||
},
|
||||
{
|
||||
"floor": "f2",
|
||||
"name": "Детская Элина",
|
||||
"area": "detskaia_elina",
|
||||
"x": 810,
|
||||
"y": 40,
|
||||
"w": 550,
|
||||
"h": 315
|
||||
},
|
||||
{
|
||||
"floor": "f2",
|
||||
"name": "Детский с/у",
|
||||
"area": "detskii_s_u",
|
||||
"x": 390,
|
||||
"y": 360,
|
||||
"w": 260,
|
||||
"h": 180
|
||||
},
|
||||
{
|
||||
"floor": "f2",
|
||||
"name": "Мастер с/у",
|
||||
"area": "master_s_u",
|
||||
"x": 390,
|
||||
"y": 540,
|
||||
"w": 260,
|
||||
"h": 160
|
||||
},
|
||||
{
|
||||
"floor": "f2",
|
||||
"name": "Мастер-спальня",
|
||||
"area": "spalnia",
|
||||
"x": 650,
|
||||
"y": 600,
|
||||
"w": 330,
|
||||
"h": 320
|
||||
},
|
||||
{
|
||||
"floor": "f2",
|
||||
"name": "Кабинет",
|
||||
"area": "kabinet",
|
||||
"x": 985,
|
||||
"y": 600,
|
||||
"w": 290,
|
||||
"h": 320
|
||||
},
|
||||
{
|
||||
"floor": "yard",
|
||||
"name": "Двор / Участок",
|
||||
"area": "dvor",
|
||||
"x": 380,
|
||||
"y": 120,
|
||||
"w": 640,
|
||||
"h": 760
|
||||
}
|
||||
];
|
||||
export const FLOOR_VB: Record<string, number[]> = {"f1": [480, 30, 760, 950], "f2": [350, 20, 1040, 960], "yard": [340, 90, 720, 800]};
|
||||
export const FLOOR_TITLES: Record<string, string> = {f1: '1 этаж', f2: '2 этаж', yard: 'Двор'};
|
||||
export const AREA_NAMES: Record<string, string> = {
|
||||
"gostinaia_na_dache": "Гостиная",
|
||||
"gostevoi_s_u": "Гостевой с/у",
|
||||
"kotelnaia": "Котельная",
|
||||
"kladovka": "Кладовка уличная",
|
||||
"prikhozhaia": "Прихожая",
|
||||
"gostevaia_komnata": "Гостевая спальня",
|
||||
"detskaia_maiia": "Детская Майя",
|
||||
"detskaia_elina": "Детская Элина",
|
||||
"detskii_s_u": "Детский с/у",
|
||||
"master_s_u": "Мастер с/у",
|
||||
"spalnia": "Мастер-спальня",
|
||||
"kabinet": "Кабинет",
|
||||
"dvor": "Двор / Участок"
|
||||
};
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
/** Редактор конфигурации карточки (GUI в Lovelace). */
|
||||
import { LitElement, html, nothing } from 'lit';
|
||||
|
||||
const SCHEMA = [
|
||||
{ name: 'title', selector: { text: {} } },
|
||||
{
|
||||
name: 'default_floor',
|
||||
selector: {
|
||||
select: {
|
||||
mode: 'dropdown',
|
||||
options: [
|
||||
{ value: 'f1', label: '1 этаж' },
|
||||
{ value: 'f2', label: '2 этаж' },
|
||||
{ value: 'yard', label: 'Двор' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: 'icon_size', selector: { number: { min: 14, max: 48, mode: 'box' } } },
|
||||
{ name: 'show_temperature', selector: { boolean: {} } },
|
||||
{ name: 'live_states', selector: { boolean: {} } },
|
||||
];
|
||||
|
||||
const LABELS: Record<string, string> = {
|
||||
title: 'Заголовок',
|
||||
default_floor: 'Этаж по умолчанию',
|
||||
icon_size: 'Размер иконок, px',
|
||||
show_temperature: 'Показывать температуру',
|
||||
live_states: 'Живые состояния (вкл/выкл, открыто…)',
|
||||
};
|
||||
|
||||
class HouseplanCardEditor extends LitElement {
|
||||
public hass?: any;
|
||||
private _config?: any;
|
||||
|
||||
static properties = {
|
||||
hass: { attribute: false },
|
||||
_config: { state: true },
|
||||
};
|
||||
|
||||
public setConfig(config: any): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) return nothing;
|
||||
return html`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${SCHEMA}
|
||||
.computeLabel=${(s: any) => LABELS[s.name] || s.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
const config = { ...this._config, ...ev.detail.value };
|
||||
const e = new Event('config-changed', { bubbles: true, composed: true }) as any;
|
||||
e.detail = { config };
|
||||
this.dispatchEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('houseplan-card-editor')) {
|
||||
customElements.define('houseplan-card-editor', HouseplanCardEditor);
|
||||
}
|
||||
Executable
+934
@@ -0,0 +1,934 @@
|
||||
/**
|
||||
* House Plan Card — интерактивный план дома (дача, Кирилловское) как нативная Lovelace-карточка.
|
||||
* Работает от объекта `hass` (без токена). Раскладка иконок хранится на сервере
|
||||
* через WS-команды интеграции `houseplan` (fallback — localStorage).
|
||||
*/
|
||||
import { LitElement, html, css, nothing, TemplateResult, PropertyValues } from 'lit';
|
||||
import { ROOMS, FLOOR_VB, FLOOR_TITLES, AREA_NAMES, IMG_W, IMG_H, Room } from './data/house';
|
||||
import { FLOOR_BG } from './data/backgrounds';
|
||||
import { EXCLUDED_DOMAINS, GROUP_TITLES, iconFor, DOMAIN_PRIORITY } from './rules';
|
||||
import './editor';
|
||||
|
||||
const CARD_VERSION = '1.0.0';
|
||||
const LS_KEY = 'houseplan_card_layout_v1';
|
||||
|
||||
interface DevItem {
|
||||
id: string;
|
||||
name: string;
|
||||
model: string;
|
||||
area: string;
|
||||
floor: string;
|
||||
icon: string;
|
||||
entities: string[];
|
||||
primary?: string;
|
||||
temp?: number | null;
|
||||
members?: { id: string; name: string; primary?: string }[];
|
||||
link?: string | null;
|
||||
linkPrimary?: string;
|
||||
}
|
||||
|
||||
interface CardConfig {
|
||||
type: string;
|
||||
title?: string;
|
||||
default_floor?: string;
|
||||
icon_size?: number;
|
||||
show_temperature?: boolean;
|
||||
live_states?: boolean;
|
||||
rooms?: Room[];
|
||||
}
|
||||
|
||||
const fireEvent = (node: EventTarget, type: string, detail?: unknown) => {
|
||||
const ev = new Event(type, { bubbles: true, composed: true }) as any;
|
||||
ev.detail = detail ?? {};
|
||||
node.dispatchEvent(ev);
|
||||
};
|
||||
|
||||
const navigate = (path: string) => {
|
||||
history.pushState(null, '', path);
|
||||
fireEvent(window, 'location-changed', { replace: false });
|
||||
};
|
||||
|
||||
const debounce = <T extends (...a: any[]) => void>(fn: T, ms: number) => {
|
||||
let t: number | undefined;
|
||||
return (...a: Parameters<T>) => {
|
||||
clearTimeout(t);
|
||||
t = window.setTimeout(() => fn(...a), ms);
|
||||
};
|
||||
};
|
||||
|
||||
class HouseplanCard extends LitElement {
|
||||
public hass?: any;
|
||||
private _config?: CardConfig;
|
||||
|
||||
private _floor = 'f1';
|
||||
private _edit = false;
|
||||
private _layout: Record<string, { x: number; y: number }> = {};
|
||||
private _serverStorage = false;
|
||||
private _layoutLoaded = false;
|
||||
private _devices: DevItem[] = [];
|
||||
private _regSignature = '';
|
||||
private _defPos: Record<string, { x: number; y: number }> = {};
|
||||
private _menuDev: DevItem | null = null;
|
||||
private _menuXY = { x: 0, y: 0 };
|
||||
private _tip: { x: number; y: number; title: string; meta: string } | null = null;
|
||||
private _selId: string | null = null;
|
||||
private _toast = '';
|
||||
private _toastTimer?: number;
|
||||
|
||||
// drag state
|
||||
private _drag: { id: string; sx: number; sy: number; ox: number; oy: number; moved: boolean } | null = null;
|
||||
|
||||
static properties = {
|
||||
hass: { attribute: false },
|
||||
_config: { state: true },
|
||||
_floor: { state: true },
|
||||
_edit: { state: true },
|
||||
_layout: { state: true },
|
||||
_devices: { state: true },
|
||||
_menuDev: { state: true },
|
||||
_tip: { state: true },
|
||||
_selId: { state: true },
|
||||
_toast: { state: true },
|
||||
};
|
||||
|
||||
public static getConfigElement() {
|
||||
return document.createElement('houseplan-card-editor');
|
||||
}
|
||||
|
||||
public static getStubConfig(): Partial<CardConfig> {
|
||||
return { type: 'custom:houseplan-card', title: 'План дома · Кирилловское' };
|
||||
}
|
||||
|
||||
public setConfig(config: CardConfig): void {
|
||||
this._config = { icon_size: 22, show_temperature: true, live_states: true, ...config };
|
||||
if (config.default_floor) this._floor = config.default_floor;
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 12;
|
||||
}
|
||||
|
||||
private get _rooms(): Room[] {
|
||||
return this._config?.rooms?.length ? this._config.rooms : ROOMS;
|
||||
}
|
||||
|
||||
private get _areaSet(): Set<string> {
|
||||
return new Set(this._rooms.filter((r) => r.area).map((r) => r.area));
|
||||
}
|
||||
|
||||
private _areaRoom(area: string): Room | undefined {
|
||||
return this._rooms.find((r) => r.area === area);
|
||||
}
|
||||
|
||||
protected willUpdate(changed: PropertyValues): void {
|
||||
if (changed.has('hass') && this.hass) {
|
||||
if (!this._layoutLoaded) {
|
||||
this._layoutLoaded = true;
|
||||
this._loadLayout();
|
||||
}
|
||||
this._maybeRebuildDevices();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- раскладка: сервер (интеграция houseplan) или localStorage ----------
|
||||
private async _loadLayout(): Promise<void> {
|
||||
try {
|
||||
const resp = await this.hass.callWS({ type: 'houseplan/layout/get' });
|
||||
this._layout = (resp && resp.layout) || {};
|
||||
this._serverStorage = true;
|
||||
} catch (e) {
|
||||
this._serverStorage = false;
|
||||
try {
|
||||
this._layout = JSON.parse(localStorage.getItem(LS_KEY) || '{}') || {};
|
||||
} catch {
|
||||
this._layout = {};
|
||||
}
|
||||
this._showToast('Интеграция houseplan не найдена — позиции сохраняются локально');
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _persistLayout = debounce(() => {
|
||||
if (this._serverStorage) {
|
||||
this.hass
|
||||
.callWS({ type: 'houseplan/layout/set', layout: this._layout })
|
||||
.catch((e: any) => this._showToast('Не удалось сохранить на сервере: ' + (e?.message || e)));
|
||||
} else {
|
||||
localStorage.setItem(LS_KEY, JSON.stringify(this._layout));
|
||||
}
|
||||
}, 600);
|
||||
|
||||
// ---------- построение списка устройств из реестров hass ----------
|
||||
private _maybeRebuildDevices(): void {
|
||||
const h = this.hass;
|
||||
if (!h?.devices || !h?.entities || !h?.areas) return;
|
||||
const sig =
|
||||
Object.keys(h.devices).length + ':' + Object.keys(h.entities).length + ':' + Object.keys(h.areas).length;
|
||||
if (sig === this._regSignature && this._devices.length) return;
|
||||
this._regSignature = sig;
|
||||
this._devices = this._buildDevices();
|
||||
this._defPos = this._defaultPositions();
|
||||
}
|
||||
|
||||
private _entitiesByDevice(): Record<string, string[]> {
|
||||
const map: Record<string, string[]> = {};
|
||||
for (const [eid, ent] of Object.entries<any>(this.hass.entities)) {
|
||||
if (ent?.device_id) (map[ent.device_id] = map[ent.device_id] || []).push(eid);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private _domainOfDevice(dev: any, entIds: string[]): string {
|
||||
if (dev.identifiers?.[0]?.[0]) return dev.identifiers[0][0];
|
||||
for (const eid of entIds) {
|
||||
const p = this.hass.entities[eid]?.platform;
|
||||
if (p) return p;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private _primaryEntity(entIds: string[], icon: string): string | undefined {
|
||||
const ents = entIds
|
||||
.map((eid) => ({ eid, reg: this.hass.entities[eid], st: this.hass.states[eid] }))
|
||||
.filter((e) => e.reg && !e.reg.hidden);
|
||||
const usable = ents.filter((e) => !e.reg.entity_category); // без diagnostic/config
|
||||
const pool = usable.length ? usable : ents;
|
||||
// датчик температуры — сразу температурная сущность
|
||||
if (icon === 'mdi:thermometer' || icon === 'mdi:air-filter') {
|
||||
const t = pool.find((e) => this._isTempEntity(e.eid));
|
||||
if (t) return t.eid;
|
||||
}
|
||||
for (const dom of DOMAIN_PRIORITY) {
|
||||
const found = pool.find((e) => e.eid.split('.')[0] === dom);
|
||||
if (found) return found.eid;
|
||||
}
|
||||
return pool[0]?.eid;
|
||||
}
|
||||
|
||||
private _isTempEntity(eid: string): boolean {
|
||||
const st = this.hass.states[eid];
|
||||
if (!st) return /_temperature$/.test(eid);
|
||||
const a = st.attributes || {};
|
||||
return (
|
||||
a.device_class === 'temperature' || /°C|°F/.test(a.unit_of_measurement || '') || /_temperature$/.test(eid)
|
||||
);
|
||||
}
|
||||
|
||||
private _tempFor(entIds: string[]): number | null {
|
||||
for (const eid of entIds) {
|
||||
if (!this._isTempEntity(eid)) continue;
|
||||
const st = this.hass.states[eid];
|
||||
if (!st) continue;
|
||||
const v = parseFloat(st.state);
|
||||
if (!isNaN(v)) return Math.round(v * 10) / 10;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Курирование + группировка — перенос buildDevices() из прототипа. */
|
||||
private _buildDevices(): DevItem[] {
|
||||
const h = this.hass;
|
||||
const areaSet = this._areaSet;
|
||||
const entsBy = this._entitiesByDevice();
|
||||
const groupByArea: Record<string, any> = {};
|
||||
for (const dev of Object.values<any>(h.devices)) {
|
||||
if (dev.model === 'Group' && dev.area_id) groupByArea[dev.area_id] = dev;
|
||||
}
|
||||
const seen: Record<string, 1> = {};
|
||||
const out: DevItem[] = [];
|
||||
for (const dev of Object.values<any>(h.devices)) {
|
||||
const area = dev.area_id;
|
||||
if (!area || !areaSet.has(area)) continue;
|
||||
if (dev.entry_type === 'service') continue;
|
||||
const entIds = entsBy[dev.id] || [];
|
||||
const dom = this._domainOfDevice(dev, entIds);
|
||||
if (EXCLUDED_DOMAINS.has(dom)) continue;
|
||||
if (dev.model === 'Group') continue;
|
||||
if (/scene/i.test(dev.model || '')) continue;
|
||||
if (/bridge/i.test((dev.model || '') + (dev.name || ''))) continue;
|
||||
if (dom === 'myheat' && dev.via_device_id) continue;
|
||||
const name = (dev.name_by_user || dev.name || 'без имени').trim();
|
||||
const key = name + '|' + area;
|
||||
if (seen[key]) continue;
|
||||
seen[key] = 1;
|
||||
const icon = iconFor(name, dev.model);
|
||||
const room = this._areaRoom(area)!;
|
||||
const item: DevItem = {
|
||||
id: dev.id,
|
||||
name,
|
||||
model: dev.model || '',
|
||||
area,
|
||||
floor: room.floor,
|
||||
icon,
|
||||
entities: entIds,
|
||||
};
|
||||
item.primary = this._primaryEntity(entIds, icon);
|
||||
if (icon === 'mdi:thermometer' || icon === 'mdi:air-filter') item.temp = this._tempFor(entIds);
|
||||
out.push(item);
|
||||
}
|
||||
// группировка ламп: ≥2 ламп в комнате → одна групповая иконка
|
||||
const lamps: Record<string, DevItem[]> = {};
|
||||
const rest: DevItem[] = [];
|
||||
for (const d of out) {
|
||||
if (d.icon === 'mdi:lightbulb') (lamps[d.area] = lamps[d.area] || []).push(d);
|
||||
else rest.push(d);
|
||||
}
|
||||
for (const area of Object.keys(lamps)) {
|
||||
const ms = lamps[area];
|
||||
if (ms.length < 2) {
|
||||
rest.push(...ms);
|
||||
continue;
|
||||
}
|
||||
const grpDev = groupByArea[area];
|
||||
const item: DevItem = {
|
||||
id: 'grp_' + area,
|
||||
name: GROUP_TITLES[area] || 'Лампы',
|
||||
model: 'группа · ' + ms.length + ' шт',
|
||||
area,
|
||||
floor: this._areaRoom(area)!.floor,
|
||||
icon: 'mdi:lightbulb-group',
|
||||
entities: ms.flatMap((m) => m.entities),
|
||||
link: grpDev?.id || null,
|
||||
members: ms.map((m) => ({ id: m.id, name: m.name, primary: m.primary })),
|
||||
};
|
||||
if (grpDev) {
|
||||
const grpEnts = this._entitiesByDevice()[grpDev.id] || [];
|
||||
item.linkPrimary = this._primaryEntity(grpEnts, 'mdi:lightbulb');
|
||||
}
|
||||
rest.push(item);
|
||||
}
|
||||
return rest;
|
||||
}
|
||||
|
||||
// ---------- позиции ----------
|
||||
private _defaultPositions(): Record<string, { x: number; y: number }> {
|
||||
const map: Record<string, { x: number; y: number }> = {};
|
||||
for (const r of this._rooms) {
|
||||
const ds = this._devices.filter((d) => d.area === r.area && d.floor === r.floor && r.area);
|
||||
if (!ds.length) continue;
|
||||
const pad = 20;
|
||||
const iw = r.w - pad * 2;
|
||||
const ih = r.h - pad * 2;
|
||||
const cols = Math.max(1, Math.round(Math.sqrt((ds.length * iw) / Math.max(ih, 1))));
|
||||
const rows = Math.ceil(ds.length / cols);
|
||||
const cw = iw / cols;
|
||||
const ch = ih / Math.max(rows, 1);
|
||||
ds.forEach((d, i) => {
|
||||
const c = i % cols;
|
||||
const rw = Math.floor(i / cols);
|
||||
map[d.id] = { x: r.x + pad + cw * (c + 0.5), y: r.y + pad + ch * (rw + 0.5) };
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private _pos(d: DevItem): { x: number; y: number } {
|
||||
const vb = FLOOR_VB[this._floor];
|
||||
return this._layout[d.id] || this._defPos[d.id] || { x: vb[0] + vb[2] / 2, y: vb[1] + vb[3] / 2 };
|
||||
}
|
||||
|
||||
// ---------- живые состояния ----------
|
||||
private _stateClass(d: DevItem): string {
|
||||
if (!this._config?.live_states) return '';
|
||||
const st = (eid?: string) => (eid ? this.hass.states[eid] : undefined);
|
||||
const list = d.members
|
||||
? (d.members.map((m) => st(m.primary)).filter(Boolean) as any[])
|
||||
: ([st(d.primary)].filter(Boolean) as any[]);
|
||||
if (!list.length) return '';
|
||||
if (list.every((s) => s.state === 'unavailable')) return 'unavail';
|
||||
const p = d.members ? list.find((s) => s.state === 'on') || list[0] : list[0];
|
||||
const dom = p.entity_id.split('.')[0];
|
||||
if (['light', 'switch', 'fan', 'humidifier'].includes(dom)) return p.state === 'on' ? 'on' : '';
|
||||
if (dom === 'cover' || dom === 'valve') return ['open', 'opening'].includes(p.state) ? 'open' : '';
|
||||
if (dom === 'lock') return ['unlocked', 'open'].includes(p.state) ? 'open' : '';
|
||||
if (dom === 'binary_sensor') {
|
||||
const dc = p.attributes?.device_class;
|
||||
if (['door', 'window', 'garage_door', 'opening', 'gas', 'smoke', 'moisture', 'problem'].includes(dc))
|
||||
return p.state === 'on' ? 'open' : '';
|
||||
}
|
||||
if (dom === 'media_player') return ['playing', 'on'].includes(p.state) ? 'on' : '';
|
||||
if (dom === 'vacuum') return ['cleaning', 'returning'].includes(p.state) ? 'on' : '';
|
||||
return '';
|
||||
}
|
||||
|
||||
private _liveTemp(d: DevItem): number | null {
|
||||
if (!this._config?.show_temperature) return null;
|
||||
if (d.icon !== 'mdi:thermometer' && d.icon !== 'mdi:air-filter') return null;
|
||||
return this._tempFor(d.entities);
|
||||
}
|
||||
|
||||
// ---------- взаимодействие ----------
|
||||
private _openMoreInfo(entityId?: string): void {
|
||||
if (!entityId) {
|
||||
this._showToast('У устройства нет подходящей сущности');
|
||||
return;
|
||||
}
|
||||
fireEvent(this, 'hass-more-info', { entityId });
|
||||
}
|
||||
|
||||
private _clickDevice(ev: MouseEvent, d: DevItem): void {
|
||||
ev.stopPropagation();
|
||||
if (this._drag?.moved) return;
|
||||
if (this._edit) {
|
||||
this._selId = d.id;
|
||||
return;
|
||||
}
|
||||
if (d.members) {
|
||||
this._menuDev = d;
|
||||
this._menuXY = { x: ev.clientX, y: ev.clientY };
|
||||
this._tip = null;
|
||||
return;
|
||||
}
|
||||
this._openMoreInfo(d.primary);
|
||||
}
|
||||
|
||||
private _clickRoom(r: Room): void {
|
||||
if (this._edit) return;
|
||||
navigate('/config/areas/area/' + r.area);
|
||||
}
|
||||
|
||||
// drag
|
||||
private _pointerDown(ev: PointerEvent, d: DevItem): void {
|
||||
if (!this._edit) return;
|
||||
ev.preventDefault();
|
||||
const p = this._pos(d);
|
||||
this._drag = { id: d.id, sx: ev.clientX, sy: ev.clientY, ox: p.x, oy: p.y, moved: false };
|
||||
(ev.target as HTMLElement).setPointerCapture(ev.pointerId);
|
||||
this._tip = null;
|
||||
}
|
||||
|
||||
private _pointerMove(ev: PointerEvent, d: DevItem): void {
|
||||
if (!this._drag || this._drag.id !== d.id) return;
|
||||
const stage = this.renderRoot.querySelector('.stage') as HTMLElement;
|
||||
if (!stage) return;
|
||||
const vb = FLOOR_VB[this._floor];
|
||||
const rect = stage.getBoundingClientRect();
|
||||
const dx = ((ev.clientX - this._drag.sx) / rect.width) * vb[2];
|
||||
const dy = ((ev.clientY - this._drag.sy) / rect.height) * vb[3];
|
||||
if (Math.abs(ev.clientX - this._drag.sx) + Math.abs(ev.clientY - this._drag.sy) > 3) this._drag.moved = true;
|
||||
let nx = Math.round(this._drag.ox + dx);
|
||||
let ny = Math.round(this._drag.oy + dy);
|
||||
nx = Math.max(vb[0] + 6, Math.min(vb[0] + vb[2] - 6, nx));
|
||||
ny = Math.max(vb[1] + 6, Math.min(vb[1] + vb[3] - 6, ny));
|
||||
this._layout = { ...this._layout, [d.id]: { x: nx, y: ny } };
|
||||
}
|
||||
|
||||
private _pointerUp(_ev: PointerEvent, d: DevItem): void {
|
||||
if (!this._drag || this._drag.id !== d.id) return;
|
||||
const moved = this._drag.moved;
|
||||
this._drag = moved ? this._drag : null; // click-обработчик проверит moved
|
||||
if (moved) {
|
||||
this._persistLayout();
|
||||
this._selId = d.id;
|
||||
window.setTimeout(() => (this._drag = null), 0);
|
||||
}
|
||||
}
|
||||
|
||||
private _applyXY(axis: 'x' | 'y', val: string): void {
|
||||
if (!this._selId) return;
|
||||
const n = parseInt(val, 10);
|
||||
if (isNaN(n)) return;
|
||||
const d = this._devices.find((x) => x.id === this._selId);
|
||||
if (!d) return;
|
||||
const vb = FLOOR_VB[this._floor];
|
||||
const p = { ...this._pos(d) };
|
||||
p[axis] = axis === 'x'
|
||||
? Math.max(vb[0] + 6, Math.min(vb[0] + vb[2] - 6, n))
|
||||
: Math.max(vb[1] + 6, Math.min(vb[1] + vb[3] - 6, n));
|
||||
this._layout = { ...this._layout, [d.id]: p };
|
||||
this._persistLayout();
|
||||
}
|
||||
|
||||
private _resetLayout(): void {
|
||||
if (!confirm('Сбросить позиции всех иконок к авто-раскладке?')) return;
|
||||
this._layout = {};
|
||||
this._persistLayout();
|
||||
}
|
||||
|
||||
private _showToast(msg: string): void {
|
||||
this._toast = msg;
|
||||
clearTimeout(this._toastTimer);
|
||||
this._toastTimer = window.setTimeout(() => {
|
||||
this._toast = '';
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
private _showTip(ev: MouseEvent, title: string, meta: string): void {
|
||||
if (this._drag) return;
|
||||
this._tip = { x: ev.clientX, y: ev.clientY, title, meta };
|
||||
}
|
||||
|
||||
// ---------- рендер ----------
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._config || !this.hass) return nothing;
|
||||
const vb = FLOOR_VB[this._floor];
|
||||
const bg = FLOOR_BG[this._floor];
|
||||
const rooms = this._rooms.filter((r) => r.floor === this._floor && r.area);
|
||||
const devs = this._devices.filter((d) => d.floor === this._floor);
|
||||
const iconSize = this._config.icon_size || 22;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="head">
|
||||
<div class="title">
|
||||
<ha-icon icon="mdi:home-city"></ha-icon>
|
||||
${this._config.title || 'План дома'}
|
||||
</div>
|
||||
<div class="tabs">
|
||||
${Object.keys(FLOOR_VB).map(
|
||||
(f) => html`<button
|
||||
class="tab ${this._floor === f ? 'active' : ''}"
|
||||
@click=${() => {
|
||||
this._floor = f;
|
||||
this._selId = null;
|
||||
}}
|
||||
>
|
||||
${FLOOR_TITLES[f] || f}
|
||||
</button>`,
|
||||
)}
|
||||
</div>
|
||||
<span class="count">${devs.length} устр.</span>
|
||||
<span class="spacer"></span>
|
||||
<button class="btn ${this._edit ? 'on' : ''}" @click=${() => {
|
||||
this._edit = !this._edit;
|
||||
this._selId = null;
|
||||
}} title="Режим правки: перетаскивание иконок">
|
||||
<ha-icon icon="mdi:cursor-move"></ha-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="stage ${this._edit ? 'edit' : ''}" style="aspect-ratio:${vb[2]}/${vb[3]}">
|
||||
<svg viewBox="${vb.join(' ')}" preserveAspectRatio="xMidYMid meet">
|
||||
${bg
|
||||
? html`<image href="${bg}" x="0" y="0" width="${IMG_W}" height="${IMG_H}" preserveAspectRatio="none" />`
|
||||
: nothing}
|
||||
${rooms.map(
|
||||
(r) => html`<rect
|
||||
class="room ${bg ? 'overlay' : 'yard'}"
|
||||
x="${r.x}" y="${r.y}" width="${r.w}" height="${r.h}" rx="8"
|
||||
@click=${() => this._clickRoom(r)}
|
||||
@mousemove=${(e: MouseEvent) => this._showTip(e, r.name, 'комната — открыть зону')}
|
||||
@mouseleave=${() => (this._tip = null)}
|
||||
></rect>
|
||||
${!bg ? html`<text class="rlabel" x="${r.x + r.w / 2}" y="${r.y + 26}">${r.name}</text>` : nothing}`,
|
||||
)}
|
||||
</svg>
|
||||
<div class="devlayer" style="--icon-size:${iconSize}px">
|
||||
${devs.map((d) => this._renderDevice(d, vb))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this._edit ? this._renderEditbar() : nothing}
|
||||
${this._menuDev ? this._renderMenu() : nothing}
|
||||
${this._tip
|
||||
? html`<div class="tip" style="left:${this._tip.x + 12}px;top:${this._tip.y + 12}px">
|
||||
<b>${this._tip.title}</b>${this._tip.meta ? html`<span class="m">${this._tip.meta}</span>` : nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._toast ? html`<div class="toast">${this._toast}</div>` : nothing}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderDevice(d: DevItem, vb: number[]): TemplateResult {
|
||||
const p = this._pos(d);
|
||||
const left = ((p.x - vb[0]) / vb[2]) * 100;
|
||||
const top = ((p.y - vb[1]) / vb[3]) * 100;
|
||||
const cls = this._stateClass(d);
|
||||
const temp = this._liveTemp(d);
|
||||
return html`<div
|
||||
class="dev ${cls} ${this._selId === d.id ? 'sel' : ''}"
|
||||
style="left:${left}%;top:${top}%"
|
||||
@click=${(e: MouseEvent) => this._clickDevice(e, d)}
|
||||
@mousemove=${(e: MouseEvent) =>
|
||||
this._showTip(e, d.name, d.model + (temp != null ? ' · ' + temp + '°' : ''))}
|
||||
@mouseleave=${() => (this._tip = null)}
|
||||
@pointerdown=${(e: PointerEvent) => this._pointerDown(e, d)}
|
||||
@pointermove=${(e: PointerEvent) => this._pointerMove(e, d)}
|
||||
@pointerup=${(e: PointerEvent) => this._pointerUp(e, d)}
|
||||
>
|
||||
<ha-icon icon="${d.icon}"></ha-icon>
|
||||
${temp != null ? html`<span class="tval">${temp}°</span>` : nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderEditbar(): TemplateResult {
|
||||
const sel = this._selId ? this._devices.find((d) => d.id === this._selId) : null;
|
||||
const p = sel ? this._pos(sel) : null;
|
||||
return html`<div class="editbar">
|
||||
<ha-icon icon="mdi:cursor-move" class="warn"></ha-icon>
|
||||
<span class="sname">${sel ? sel.name : 'Режим правки — тащите иконки мышью'}</span>
|
||||
${sel && p
|
||||
? html`<label>X</label><input type="number" .value=${String(Math.round(p.x))}
|
||||
@change=${(e: Event) => this._applyXY('x', (e.target as HTMLInputElement).value)} />
|
||||
<label>Y</label><input type="number" .value=${String(Math.round(p.y))}
|
||||
@change=${(e: Event) => this._applyXY('y', (e.target as HTMLInputElement).value)} />`
|
||||
: nothing}
|
||||
<span class="spacer"></span>
|
||||
<span class="hint">${this._serverStorage ? 'сохранение: сервер (для всех)' : 'сохранение: этот браузер'}</span>
|
||||
<button class="btn ghost" @click=${this._resetLayout} title="Сбросить всё">
|
||||
<ha-icon icon="mdi:backup-restore"></ha-icon>
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderMenu(): TemplateResult {
|
||||
const d = this._menuDev!;
|
||||
return html`<div class="menuwrap" @click=${() => (this._menuDev = null)}>
|
||||
<div class="menu" style="left:${this._menuXY.x}px;top:${this._menuXY.y}px" @click=${(e: Event) =>
|
||||
e.stopPropagation()}>
|
||||
<div class="hd"><ha-icon icon="mdi:lightbulb-group"></ha-icon>${d.name} · ${d.members!.length}</div>
|
||||
${d.linkPrimary
|
||||
? html`<div class="it all" @click=${() => {
|
||||
this._menuDev = null;
|
||||
this._openMoreInfo(d.linkPrimary);
|
||||
}}>
|
||||
<ha-icon icon="mdi:lightbulb-group"></ha-icon>Открыть группу целиком
|
||||
</div>`
|
||||
: nothing}
|
||||
${d.members!.map(
|
||||
(m) => html`<div class="it" @click=${() => {
|
||||
this._menuDev = null;
|
||||
this._openMoreInfo(m.primary);
|
||||
}}>
|
||||
<ha-icon icon="mdi:lightbulb"></ha-icon>${m.name}
|
||||
</div>`,
|
||||
)}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--hp-bg: var(--card-background-color, #16212e);
|
||||
--hp-line: var(--divider-color, #2b3d4f);
|
||||
--hp-txt: var(--primary-text-color, #e6edf3);
|
||||
--hp-muted: var(--secondary-text-color, #8aa0b3);
|
||||
--hp-accent: var(--primary-color, #3ea6ff);
|
||||
--hp-on: #ffd45c;
|
||||
--hp-open: #ff9f43;
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--hp-line);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.title ha-icon {
|
||||
color: var(--hp-accent);
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: rgba(127, 127, 127, 0.12);
|
||||
padding: 3px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.tab {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hp-muted);
|
||||
padding: 6px 13px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: 0.15s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.tab:hover {
|
||||
color: var(--hp-txt);
|
||||
}
|
||||
.tab.active {
|
||||
background: var(--hp-accent);
|
||||
color: var(--text-primary-color, #fff);
|
||||
}
|
||||
.count {
|
||||
font-size: 12px;
|
||||
color: var(--hp-muted);
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 1px solid var(--hp-line);
|
||||
background: transparent;
|
||||
color: var(--hp-txt);
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: 0.15s;
|
||||
}
|
||||
.btn ha-icon {
|
||||
--mdc-icon-size: 17px;
|
||||
}
|
||||
.btn:hover {
|
||||
border-color: var(--hp-accent);
|
||||
}
|
||||
.btn.on {
|
||||
background: var(--hp-accent);
|
||||
color: var(--text-primary-color, #fff);
|
||||
border-color: var(--hp-accent);
|
||||
}
|
||||
.btn.ghost {
|
||||
border: none;
|
||||
}
|
||||
.stage {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.stage svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.room {
|
||||
transition: 0.12s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.room.overlay {
|
||||
fill: transparent;
|
||||
stroke: transparent;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.room.overlay:hover {
|
||||
fill: rgba(62, 166, 255, 0.18);
|
||||
stroke: var(--hp-accent);
|
||||
}
|
||||
.room.yard {
|
||||
fill: rgba(75, 140, 90, 0.14);
|
||||
stroke: #4b8c5a;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.room.yard:hover {
|
||||
fill: rgba(75, 140, 90, 0.24);
|
||||
stroke: #6fbf86;
|
||||
}
|
||||
.rlabel {
|
||||
fill: var(--hp-muted);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
pointer-events: none;
|
||||
text-anchor: middle;
|
||||
}
|
||||
.stage.edit .room {
|
||||
pointer-events: none;
|
||||
}
|
||||
.devlayer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dev {
|
||||
position: absolute;
|
||||
width: var(--icon-size, 22px);
|
||||
height: var(--icon-size, 22px);
|
||||
margin: calc(var(--icon-size, 22px) / -2) 0 0 calc(var(--icon-size, 22px) / -2);
|
||||
border-radius: 5px;
|
||||
background: var(--hp-bg);
|
||||
border: 1px solid var(--hp-line);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--hp-txt);
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: transform 0.08s, background 0.15s, border-color 0.15s, opacity 0.2s;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
|
||||
z-index: 2;
|
||||
}
|
||||
.dev ha-icon {
|
||||
--mdc-icon-size: calc(var(--icon-size, 22px) * 0.62);
|
||||
}
|
||||
.dev:hover {
|
||||
background: var(--hp-accent);
|
||||
color: var(--text-primary-color, #fff);
|
||||
transform: scale(1.35);
|
||||
z-index: 5;
|
||||
}
|
||||
.dev.on {
|
||||
background: var(--hp-on);
|
||||
border-color: var(--hp-on);
|
||||
color: #503c00;
|
||||
box-shadow: 0 0 8px rgba(255, 212, 92, 0.7);
|
||||
}
|
||||
.dev.open {
|
||||
background: var(--hp-open);
|
||||
border-color: var(--hp-open);
|
||||
color: #4a2800;
|
||||
}
|
||||
.dev.unavail {
|
||||
opacity: 0.35;
|
||||
}
|
||||
.dev.sel {
|
||||
border-color: #ffc14d;
|
||||
box-shadow: 0 0 0 3px rgba(255, 193, 77, 0.35);
|
||||
}
|
||||
.stage.edit .dev {
|
||||
cursor: grab;
|
||||
}
|
||||
.dev .tval {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin-left: 2px;
|
||||
background: rgba(4, 18, 31, 0.9);
|
||||
border: 1px solid var(--hp-accent);
|
||||
border-radius: 4px;
|
||||
padding: 0 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 15px;
|
||||
color: #dff1ff;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
.editbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 14px;
|
||||
border-top: 1px solid var(--hp-line);
|
||||
font-size: 13px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.editbar .warn {
|
||||
color: #ffc14d;
|
||||
}
|
||||
.editbar .sname {
|
||||
font-weight: 600;
|
||||
}
|
||||
.editbar input {
|
||||
width: 74px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--hp-line);
|
||||
color: var(--hp-txt);
|
||||
border-radius: 6px;
|
||||
padding: 5px 7px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.editbar label,
|
||||
.editbar .hint {
|
||||
color: var(--hp-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.menuwrap {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 80;
|
||||
}
|
||||
.menu {
|
||||
position: fixed;
|
||||
background: var(--hp-bg);
|
||||
border: 1px solid var(--hp-accent);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.45);
|
||||
min-width: 210px;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
transform: translate(0, 8px);
|
||||
}
|
||||
.menu .hd {
|
||||
padding: 8px 12px;
|
||||
font-weight: 600;
|
||||
font-size: 12.5px;
|
||||
border-bottom: 1px solid var(--hp-line);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.menu .hd ha-icon,
|
||||
.menu .it.all ha-icon {
|
||||
color: var(--hp-accent);
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
.menu .it {
|
||||
padding: 8px 12px;
|
||||
font-size: 12.5px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.menu .it ha-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
color: var(--hp-muted);
|
||||
}
|
||||
.menu .it:hover {
|
||||
background: rgba(127, 127, 127, 0.15);
|
||||
}
|
||||
.menu .it.all {
|
||||
color: var(--hp-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.tip {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
background: var(--hp-bg);
|
||||
border: 1px solid var(--hp-accent);
|
||||
color: var(--hp-txt);
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 12.5px;
|
||||
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.45);
|
||||
z-index: 99;
|
||||
max-width: 260px;
|
||||
}
|
||||
.tip .m {
|
||||
color: var(--hp-muted);
|
||||
font-size: 11px;
|
||||
display: block;
|
||||
}
|
||||
.toast {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 22px;
|
||||
transform: translateX(-50%);
|
||||
background: var(--hp-bg);
|
||||
border: 1px solid var(--hp-accent);
|
||||
color: var(--hp-txt);
|
||||
padding: 9px 16px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.45);
|
||||
z-index: 120;
|
||||
max-width: 90vw;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
if (!customElements.get('houseplan-card')) {
|
||||
customElements.define('houseplan-card', HouseplanCard);
|
||||
}
|
||||
|
||||
(window as any).customCards = (window as any).customCards || [];
|
||||
if (!(window as any).customCards.find((c: any) => c.type === 'houseplan-card')) {
|
||||
(window as any).customCards.push({
|
||||
type: 'houseplan-card',
|
||||
name: 'House Plan Card',
|
||||
description: 'Интерактивный план дома: этажи, комнаты, устройства с живыми состояниями и drag-раскладкой.',
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(`%c HOUSEPLAN-CARD %c v${CARD_VERSION} `, 'background:#3ea6ff;color:#04121f;font-weight:700', '');
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Правила прототипа — перенесены 1-в-1 из index.html/build_data.py (см. DOCUMENTATION.md §4.2–4.5).
|
||||
*/
|
||||
|
||||
/** Интеграции-домены, чьи устройства скрываются (курирование). */
|
||||
export const EXCLUDED_DOMAINS = new Set([
|
||||
'hacs', 'sun', 'backup', 'hassio', 'met', 'telegram_bot', 'mobile_app',
|
||||
'systemmonitor', 'better_thermostat', 'adaptive_lighting', 'yandex_pogoda',
|
||||
'upnp_serial_number',
|
||||
]);
|
||||
|
||||
/** Заголовки групп ламп по area_id. */
|
||||
export const GROUP_TITLES: Record<string, string> = {
|
||||
gostinaia_na_dache: 'Настенные лампы',
|
||||
prikhozhaia: 'Свет в прихожей',
|
||||
detskaia_maiia: 'Свет у Майи',
|
||||
detskaia_elina: 'Свет у Элины',
|
||||
};
|
||||
|
||||
const ICON_RULES: Array<[string, string]> = [
|
||||
['протечк', 'mdi:water-alert'],
|
||||
['клапан', 'mdi:pipe-valve'],
|
||||
['дым', 'mdi:smoke-detector'],
|
||||
['термоголов', 'mdi:radiator'],
|
||||
['температ', 'mdi:thermometer'],
|
||||
['qingping|air monitor|молекул', 'mdi:air-filter'],
|
||||
['штор', 'mdi:roller-shade'],
|
||||
['розетк|plug', 'mdi:power-socket-de'],
|
||||
['выключат|switch', 'mdi:light-switch'],
|
||||
['лампа|лампочк|bulb|gx53|светильник|rgb', 'mdi:lightbulb'],
|
||||
['камер|camera', 'mdi:cctv'],
|
||||
['замок|ttlock|lock', 'mdi:lock-smart'],
|
||||
['ворота|garage', 'mdi:garage-variant'],
|
||||
['калитк|door|открыт', 'mdi:door'],
|
||||
['счётчик|счетчик|kws|meter', 'mdi:meter-electric'],
|
||||
['вводный автомат|breaker|wifimcbn', 'mdi:electric-switch'],
|
||||
['myheat|котёл|котел|boiler|отоплен', 'mdi:water-boiler'],
|
||||
['холодильник|fridge', 'mdi:fridge'],
|
||||
['стиральн|washer', 'mdi:washing-machine'],
|
||||
['сушилк|dryer', 'mdi:tumble-dryer'],
|
||||
['пылесос|vacuum|dreame', 'mdi:robot-vacuum'],
|
||||
['soundbar|колонк|станц', 'mdi:soundbar'],
|
||||
['tv|телевизор|hyundaitv|mitv', 'mdi:television'],
|
||||
['keenetic|роутер|router', 'mdi:router-wireless'],
|
||||
['ибп|ups|kirpich', 'mdi:battery-charging-high'],
|
||||
['slzb|координат|zigbee', 'mdi:zigbee'],
|
||||
];
|
||||
|
||||
/** Подбор MDI-иконки по имени/модели устройства. */
|
||||
export function iconFor(name?: string, model?: string): string {
|
||||
const s = ((name || '') + ' ' + (model || '')).toLowerCase();
|
||||
for (const [pat, icon] of ICON_RULES) {
|
||||
if (new RegExp(pat).test(s)) return icon;
|
||||
}
|
||||
return 'mdi:chip';
|
||||
}
|
||||
|
||||
/** Приоритет доменов для выбора «первичной» сущности устройства (more-info). */
|
||||
export const DOMAIN_PRIORITY = [
|
||||
'light', 'switch', 'cover', 'valve', 'lock', 'climate', 'fan',
|
||||
'media_player', 'camera', 'vacuum', 'humidifier', 'water_heater',
|
||||
'alarm_control_panel', 'sensor', 'binary_sensor', 'event', 'button',
|
||||
'number', 'select', 'update',
|
||||
];
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["es2021", "dom", "dom.iterable"],
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"outDir": "tsout"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user