Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6ec494a1a |
@@ -31,16 +31,6 @@ fi
|
||||
|
||||
status=0
|
||||
|
||||
# Локальный набор гейтов (#343). Выключен по умолчанию намеренно: 20-45 секунд на
|
||||
# каждый пуш, включая пуши одной строки документации, — цена, которую стоит
|
||||
# платить осознанно. Документация: docs/TESTING.md.
|
||||
if [ "${HP_PREPUSH_GATE:-}" = "1" ] && [ -f "$repo_root/scripts/pre-push-gate.mjs" ]; then
|
||||
echo "pre-push-gate: HP_PREPUSH_GATE=1, прогоняю локальный набор" >&2
|
||||
if ! node "$repo_root/scripts/pre-push-gate.mjs" >&2; then
|
||||
status=1
|
||||
fi
|
||||
fi
|
||||
|
||||
while read -r local_ref local_sha remote_ref remote_sha; do
|
||||
# Deleting a remote branch pushes nothing to examine.
|
||||
if [ "$local_sha" = "$zero" ]; then
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Анонс релиза
|
||||
name: Announce release
|
||||
# Telegram notifications for t.me/ha_houseplan (owner request, 2026-08-07).
|
||||
# Stable releases are announced; prereleases are deliberately silent.
|
||||
# workflow_dispatch exists purely as a connectivity test button and therefore
|
||||
@@ -36,13 +36,12 @@ permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
telegram:
|
||||
name: Оповещение в Telegram (только стабильные)
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'release' && github.event.release.prerelease == false) || (github.event_name == 'workflow_call' && inputs.prerelease == false) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out release notes for a reusable call
|
||||
if: ${{ inputs.reusable == true }}
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
- name: Send to Telegram
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
# Скриншоты документации снимаются здесь и только здесь (#246).
|
||||
#
|
||||
# Съёмка на машине исполнителя даёт байтово разный PNG при одинаковом кадре:
|
||||
# сглаживание и хинтинг зависят от окружения. Измерено на истории — пересъёмка
|
||||
# в #231 изменила два файла из девяти на 7–8 байт, набор с беты все девять
|
||||
# целиком. Одно окружение убирает этот шум насовсем.
|
||||
#
|
||||
# Джоба ничего не коммитит: она публикует артефакт, который человек принимает
|
||||
# локально через `npm run docs:accept -- --reviewed --from=<распакованный>`.
|
||||
# Та же конструкция, что у golden-эталонов, и по той же причине: картинки
|
||||
# попадают в репозиторий через явное решение, а не через бота.
|
||||
#
|
||||
# Снимать здесь больше не обязанность, а удобство (#401). Приёмка проверяет не
|
||||
# место съёмки, а её воспроизводимость: каждый кадр, не объявленный изменённым,
|
||||
# должен совпасть с закоммиченным байт-в-байт. Эта джоба потому и удобна, что
|
||||
# среда у неё та же, в которой снят закоммиченный набор, — но принять получится
|
||||
# из любой, где кадры воспроизводятся, и не получится ни из одной, где нет.
|
||||
name: Скриншоты документации
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Ветка или SHA, с которого снимать
|
||||
required: false
|
||||
default: dev
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
capture:
|
||||
name: Съёмка и сверка скриншот-индекса
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
# Тот же кэш и тот же отказ от --with-deps, что в smoke/golden (#175, #206):
|
||||
# системные библиотеки Chromium уже в образе раннера.
|
||||
- name: Кэш браузеров Playwright
|
||||
id: pw
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||||
- name: Install pinned Chromium
|
||||
if: steps.pw.outputs.cache-hit != 'true'
|
||||
run: npx playwright install chromium
|
||||
- name: Build the bundle the screenshots must come from
|
||||
run: npm run build
|
||||
# oxipng без потерь снимает с набора ~19% (замер в #345 на pyoxipng 9.1.1;
|
||||
# у 10.2.0 пресеты уровней перебалансированы, точная доля может отличаться,
|
||||
# но кадры остаются пиксельно идентичными в любой версии — это перепаковка).
|
||||
#
|
||||
# Пин версии и контрольной суммы намеренно, а не `apt-get install oxipng`:
|
||||
# пакет из образа раннера может пропасть или переехать, а падение шага
|
||||
# съёмки стоит целого цикла приёмки. Тот же урок, что с azure-зеркалом
|
||||
# Playwright (#175, #206).
|
||||
- name: Установить oxipng
|
||||
env:
|
||||
OXIPNG_VERSION: 10.2.0
|
||||
OXIPNG_SHA256: b33f84c73d42cb592bea5d84c431030b1e97784817693380dfcec7d9575f871e
|
||||
run: |
|
||||
set -euo pipefail
|
||||
asset="oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
curl -fsSL -o "$asset" \
|
||||
"https://github.com/oxipng/oxipng/releases/download/v${OXIPNG_VERSION}/${asset}"
|
||||
echo "${OXIPNG_SHA256} ${asset}" | sha256sum -c -
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
tar -xzf "$asset" --strip-components=1 -C "$HOME/.local/bin" \
|
||||
"oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu/oxipng"
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
"$HOME/.local/bin/oxipng" --version
|
||||
# Съёмка идёт дважды и сравнивается по хешам (#422). Прежний замер
|
||||
# (`--stability`) отвечает на вопрос «плавает ли кадр от времени внутри
|
||||
# страницы» и остаётся ниже; дефект #410 был по другой оси — обрезка
|
||||
# плавала МЕЖДУ прогонами, и три снимка внутри одного процесса совпали бы
|
||||
# всегда. Проверка, объявленная гарантией воспроизводимости, на
|
||||
# собственном инциденте промолчала бы.
|
||||
#
|
||||
# Второй прогон заодно оставляет в `docs/images` кадры, которые и уедут в
|
||||
# артефакт: сравнивать хеши и публиковать разные файлы было бы странно.
|
||||
- name: "Съёмка воспроизводима между прогонами (#410, #422)"
|
||||
run: node scripts/capture-determinism.mjs
|
||||
# Вердикт до всякой приёмки. Само число изменившихся файлов ничего не
|
||||
# говорит: набор, снятый другим браузером, меняет их все, и это нормально
|
||||
# ровно один раз — при переходе на канонический прогон. Сравнивать надо
|
||||
# браузер: тот же Chromium и десять изменившихся картинок означают, что
|
||||
# изменился продукт (или что-то не так), другой Chromium — ожидаемую
|
||||
# разницу рендеринга.
|
||||
# Хеши печатаются в лог, а не только уезжают в артефакт (#410): чтобы
|
||||
# сравнить два прогона одного и того же SHA, нужен текст, который видно с
|
||||
# экрана. Именно так измеряется недетерминированность съёмки — и её
|
||||
# отсутствие после починки.
|
||||
# Съёмка обязана быть воспроизводимой, и это проверяется, а не
|
||||
# предполагается (#410): три снимка на сценарий в одном процессе, без
|
||||
# правки состояния между ними, обязаны совпасть побайтово. Шаг падает,
|
||||
# если кадр снова начнёт зависеть от времени.
|
||||
#
|
||||
# Шаг стоит ПОСЛЕ съёмки и до вердикта: публикацию артефакта он всё равно
|
||||
# блокирует падением job'а, а снимать набор третий раз ради порядка строк
|
||||
# в логе незачем.
|
||||
- name: "Кадр не плавает внутри одного состояния (#410)"
|
||||
run: node demo/docs/capture.mjs --stability=3
|
||||
- name: Хеши кадров
|
||||
run: |
|
||||
for f in docs/images/*.png; do sha256sum "$f"; done
|
||||
- name: Вердикт
|
||||
run: |
|
||||
# Поле манифеста «до» — из закоммиченного состояния, «после» — из
|
||||
# свежего. Читается одинаково для браузера и для упаковщика: оба
|
||||
# переписывают все кадры сразу, и различить их причины обязан вердикт,
|
||||
# а не человек по памяти (#345).
|
||||
field() {
|
||||
node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{console.log(JSON.parse(s).$1||'')}catch{console.log('')}})"
|
||||
}
|
||||
git status --porcelain docs/images
|
||||
changed=$(git diff --name-only docs/images | grep -c png || true)
|
||||
before=$(git show HEAD:docs/images/screenshots.json | field chromium)
|
||||
after=$(node -e "console.log(require('./docs/images/screenshots.json').chromium)")
|
||||
packer_before=$(git show HEAD:docs/images/screenshots.json | field oxipng)
|
||||
packer_after=$(node -e "console.log(require('./docs/images/screenshots.json').oxipng || '')")
|
||||
echo "--- изменившихся PNG: $changed"
|
||||
echo "--- Chromium: было «${before:-не записан}», стало «$after»"
|
||||
echo "--- oxipng: было «${packer_before:-не записан}», стало «${packer_after:-нет}»"
|
||||
if [ "$packer_before" != "$packer_after" ] && [ "$changed" -gt 0 ]; then
|
||||
echo "ВЕРДИКТ: изменился упаковщик, поэтому переписаны все кадры сразу."
|
||||
echo "Это перепаковка без потерь: пиксели те же, размер меньше на ~19%."
|
||||
echo "Ожидаемо один раз — при включении oxipng либо при смене его версии."
|
||||
echo "Проверить можно сравнением декодированных кадров, а не байтов."
|
||||
elif [ "$before" = "$after" ] && [ "$changed" -gt 0 ]; then
|
||||
echo "ВЕРДИКТ: тот же браузер и тот же упаковщик, а картинки изменились —"
|
||||
echo "изменился продукт. Смотрите на кадры: если изменение ожидаемое, принимайте."
|
||||
elif [ "$before" != "$after" ]; then
|
||||
echo "ВЕРДИКТ: браузер другой, поэтому переписаны все кадры сразу."
|
||||
echo "Это ожидаемо один раз — при переходе на канонический прогон."
|
||||
echo "Если Chromium сменился неожиданно, сверьте закреплённую версию в package-lock."
|
||||
else
|
||||
echo "ВЕРДИКТ: ничего не изменилось, принимать нечего."
|
||||
fi
|
||||
- name: Upload candidate
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: docs-screenshots
|
||||
path: |
|
||||
docs/images/*.png
|
||||
docs/images/screenshots.json
|
||||
if-no-files-found: error
|
||||
@@ -1,86 +1,51 @@
|
||||
name: Мутационный гейт
|
||||
name: Mutation gate
|
||||
|
||||
# Реестр известных поломок (issue #85): каждый мутант ломает продуктовый код
|
||||
# известным способом, и объявленный тест ОБЯЗАН на этом покраснеть. Тест,
|
||||
# оставшийся зелёным на сломанном коде, ничего не защищает — он лишь выглядит
|
||||
# защитой, и это хуже его отсутствия.
|
||||
#
|
||||
# Прогон дорогой и проверяет не продукт, а тесты, поэтому он не входит ни в
|
||||
# Validate, ни в цикл разработки, ни в релизный гейт (#513, решение владельца
|
||||
# 09.09): каждую ночь по расписанию, отказ — issue с отчётом (#472).
|
||||
# Мутанты, задетые диффом, конвейер ревью гоняет отдельно на кандидате (#510).
|
||||
# Дешёвая половина — «якоря патчей живы, guard-файлы существуют» — идёт с
|
||||
# обычными юнитами: test/mutation-gate.test.mjs.
|
||||
#
|
||||
# #332: бандл собирается только мутантам с браузерным гвардом (guardNeedsBundle),
|
||||
# компиляция тестов в worktree стартует с тёплого test-build (инкрементальный
|
||||
# tsc), а реестр режется на четыре чересполосных шарда — полный прогон
|
||||
# укладывается в десятки минут вместо часов. Локальный дифф-режим:
|
||||
# node scripts/mutation-gate.mjs --changed origin/dev..HEAD
|
||||
# Прогон дорогой: пересборка бандла на каждого мутанта. Поэтому он не входит в
|
||||
# Validate и не идёт на каждый push. Его место — перед стабильным релизом
|
||||
# (PROCESS.md §8) и раз в неделю по расписанию, чтобы дрейф тестов не копился
|
||||
# до релиза. Дешёвая половина — «якоря патчей живы, guard-файлы существуют» —
|
||||
# идёт с обычными юнитами: test/mutation-gate.test.mjs.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Git ref whose mutation guards must be proved
|
||||
required: false
|
||||
default: dev
|
||||
schedule:
|
||||
# Каждую ночь, 01:00 UTC (04:00 MSK) — после суток правок и до ночного
|
||||
# полного Validate (nightly.yml, 02:30 UTC), чтобы не делить раннеры (#513).
|
||||
- cron: '0 1 * * *'
|
||||
# Понедельник, 05:20 UTC — до начала рабочего дня владельца.
|
||||
- cron: '20 5 * * 1'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Группа зависит от события (#472). Прежде она была одна на всё, и ручной
|
||||
# запуск (отладка гейта) отменял идущий по расписанию — так 24.08 погиб
|
||||
# еженедельный прогон, а отменённый в списке выглядит «не красным». Поймать
|
||||
# отмену изнутри нельзя: вместе с прогоном отменяются и не начавшиеся job,
|
||||
# включая любой репортёр. Значит отмену надо не ловить, а не допускать.
|
||||
concurrency:
|
||||
group: mutation-gate-${{ github.event_name }}
|
||||
group: mutation-gate
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
mutants:
|
||||
name: "Мутанты: каждый обязан красить тесты (шард ${{ matrix.shard }} из 4)"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
# Шард ~64 мутантов × свой guard; бандл собирают только браузерные гварды.
|
||||
# Час — потолок против зависшего Chromium.
|
||||
# Шесть мутантов × (сборка + браузерный смок) — это десятки минут, и это
|
||||
# нормально: гейт предрелизный. Час — потолок против зависшего Chromium.
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || 'dev' }}
|
||||
ref: dev
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Установить backend test dependencies
|
||||
run: |
|
||||
# Точные версии — в tests_backend/requirements.txt (#392): без них
|
||||
# резолвер выбирает набор сам, и «зелёный backend» значит разное
|
||||
# в разные дни.
|
||||
pip install -r tests_backend/requirements.txt
|
||||
pip list --format=columns | grep -Ei 'homeassistant|voluptuous|^pytest '
|
||||
|
||||
- name: Кэш браузеров Playwright
|
||||
id: pw
|
||||
uses: actions/cache@v6
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||||
@@ -92,107 +57,5 @@ jobs:
|
||||
- name: Реестр применим к текущему коду
|
||||
run: node scripts/mutation-gate.mjs --check
|
||||
|
||||
- name: Тёплый test-build для инкрементальной компиляции мутантов
|
||||
run: npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs
|
||||
|
||||
# Вывод шарда сохраняется артефактом (#472): строки
|
||||
# `FAIL <id>: тест остался зелёным…` — единственное место, где названо,
|
||||
# ЧТО сбежало. Без артефакта отказ безымянный. `PIPESTATUS` — чтобы
|
||||
# `tee` не съел код выхода раннера.
|
||||
- name: Каждый тест ловит свою поломку
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
set -o pipefail
|
||||
node scripts/mutation-gate.mjs --shard=${{ matrix.shard }}/4 2>&1 | tee artifacts/mutation-shard-${{ matrix.shard }}.log
|
||||
- name: Сохранить лог шарда
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: mutation-shard-${{ matrix.shard }}
|
||||
path: artifacts/mutation-shard-${{ matrix.shard }}.log
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
# Адресат у отказа (#472). Только по расписанию: ручной dispatch остаётся
|
||||
# для отладки самого гейта, его результат смотрят в прогоне — issue на
|
||||
# каждый такой отказ был бы шумом, который снова перестанут читать.
|
||||
#
|
||||
# Права job-уровня ЗАМЕНЯЮТ права workflow, а не дополняют (прецедент —
|
||||
# validate.yml, job с actions: read): перечислены все три.
|
||||
report:
|
||||
name: "Отказ расписания: issue и Telegram"
|
||||
needs: mutants
|
||||
if: always() && github.event_name == 'schedule' && needs.mutants.result != 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: dev
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Забрать логи шардов
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: mutation-shard-*
|
||||
path: artifacts/mutation-logs
|
||||
- name: Собрать отчёт
|
||||
id: report
|
||||
env:
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
# SHA — того дерева, которое чекаутили и гоняли (dev), а не
|
||||
# github.sha: для расписания это вершина default-ветки main, и отчёт
|
||||
# называл бы «dev @ <SHA main>» (ревью r1). Образец — process.yml.
|
||||
SHA=$(git rev-parse HEAD)
|
||||
mkdir -p artifacts
|
||||
node scripts/mutation-gate-report.mjs \
|
||||
--logs=artifacts/mutation-logs --shards=4 \
|
||||
--run-url="$RUN_URL" --ref=dev --sha="$SHA" \
|
||||
--body-out=artifacts/mutation-report.md \
|
||||
--telegram-out=artifacts/mutation-telegram.txt >> "$GITHUB_OUTPUT"
|
||||
# Одно issue, а не одно на неделю: открытое с тем же маркером в заголовке
|
||||
# получает комментарий, новое заводится только если открытого нет.
|
||||
- name: Issue — создать или дописать
|
||||
id: issue
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
MARKER: ${{ steps.report.outputs.marker }}
|
||||
TITLE: ${{ steps.report.outputs.title }}
|
||||
run: |
|
||||
existing=$(gh issue list --repo "$REPO" --state open --search "\"$MARKER\" in:title" \
|
||||
--json number,title --jq '[.[] | select(.title | startswith(env.MARKER))][0].number // empty')
|
||||
if [ -n "$existing" ]; then
|
||||
gh issue comment "$existing" --repo "$REPO" --body-file artifacts/mutation-report.md
|
||||
url="${{ github.server_url }}/$REPO/issues/$existing"
|
||||
else
|
||||
url=$(gh issue create --repo "$REPO" --title "$TITLE" \
|
||||
--label infra --label process --label tests \
|
||||
--body-file artifacts/mutation-report.md)
|
||||
fi
|
||||
echo "url=$url" >> "$GITHUB_OUTPUT"
|
||||
echo "issue: $url"
|
||||
# Тот же канал, что у релизов (announce.yml). Нет секретов — не отказ:
|
||||
# issue уже заведено, а Telegram — второй адресат, не единственный.
|
||||
- name: Telegram
|
||||
if: always() && steps.issue.outcome == 'success'
|
||||
env:
|
||||
TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
ISSUE_URL: ${{ steps.issue.outputs.url }}
|
||||
run: |
|
||||
if [ -z "$TOKEN" ] || [ -z "$CHAT" ]; then
|
||||
echo "::warning::TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID не заданы — оповещение пропущено, issue заведено"
|
||||
exit 0
|
||||
fi
|
||||
TEXT=$(sed "s|(issue)|$ISSUE_URL|" artifacts/mutation-telegram.txt)
|
||||
curl -sS --fail-with-body -X POST \
|
||||
"https://api.telegram.org/bot$TOKEN/sendMessage" \
|
||||
--data-urlencode "chat_id=$CHAT" \
|
||||
--data-urlencode "text=$TEXT" \
|
||||
-d disable_web_page_preview=true
|
||||
run: node scripts/mutation-gate.mjs
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
# Ночной полный прогон (#479).
|
||||
#
|
||||
# Тяжёлые job Validate — смоки, golden, performance_smoke — на обычном пуше не
|
||||
# идут: они ни разу не ловили дефект в момент ревью и стоили ~6 минут
|
||||
# критического пути на каждую итерацию. Полный набор идёт на кандидате беты
|
||||
# (трейлер `Release:`), по кнопке и здесь — каждую ночь на голове `dev`.
|
||||
#
|
||||
# Почему не `schedule` прямо в validate.yml: расписание исполняется на ветке
|
||||
# по умолчанию (`main`), а проверять надо `dev`. Один dispatch с `--ref dev`
|
||||
# делает это без переписывания checkout во всех job. Reuse (#208) сохраняется:
|
||||
# при неизменённом дереве ночной прогон обойдётся маркерами.
|
||||
#
|
||||
# Красный ночной прогон — сигнал автору последних коммитов на dev, не гейт:
|
||||
# гейт беты по-прежнему требует зелёный Validate на точном SHA кандидата, и
|
||||
# там полный набор идёт заново.
|
||||
#
|
||||
# Сигнал обязан быть настоящим (#492 §7): до этой задачи job завершалась
|
||||
# успехом в момент постановки Validate в очередь, и красный полный прогон не
|
||||
# делал ночной workflow красным. Теперь job находит запущенный прогон и ждёт
|
||||
# его: успешный dispatch — не успешная проверка.
|
||||
name: Ночной полный прогон dev
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 2 * * *'
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dispatch:
|
||||
name: "Запустить Validate на dev с полным набором и дождаться результата"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
since=$(date -u +%FT%TZ)
|
||||
gh workflow run validate.yml --repo "$REPO" --ref dev -f full=true
|
||||
echo "Validate(dev, full=true) поставлен в очередь: $since"
|
||||
# Найти именно этот прогон: workflow_dispatch на dev, созданный не
|
||||
# раньше момента запуска. До трёх минут на появление в списке.
|
||||
run_id=""
|
||||
for _ in $(seq 1 18); do
|
||||
sleep 10
|
||||
run_id=$(gh run list --repo "$REPO" --workflow validate.yml --branch dev \
|
||||
--event workflow_dispatch --json databaseId,createdAt --limit 5 \
|
||||
--jq "[.[] | select(.createdAt >= \"$since\")] | sort_by(.createdAt) | last | .databaseId // empty")
|
||||
[ -n "$run_id" ] && break
|
||||
done
|
||||
if [ -z "$run_id" ]; then
|
||||
echo "::error::прогон Validate не появился за 3 минуты — dispatch не равен проверке"
|
||||
exit 1
|
||||
fi
|
||||
url="${{ github.server_url }}/$REPO/actions/runs/$run_id"
|
||||
echo "дочерний прогон: $url"
|
||||
echo "- Validate(dev, full=true): $url" >> "$GITHUB_STEP_SUMMARY"
|
||||
# Ждём завершения; красный дочерний прогон — красный ночной.
|
||||
gh run watch "$run_id" --repo "$REPO" --exit-status --interval 30
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Полные бенчмарки производительности
|
||||
name: Full Performance
|
||||
|
||||
on:
|
||||
# Every main promotion is a stable-release candidate and must have an
|
||||
@@ -24,28 +24,13 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
performance:
|
||||
name: Бенчмарки рендера и геометрии
|
||||
# Every profile keeps base and candidate sequential on one hosted runner.
|
||||
# Independent profile pairs run in parallel: cross-profile timing is never
|
||||
# compared, while serialising all profile pairs cannot fit the job timeout.
|
||||
# Base and candidate stay sequential on one hosted runner. Splitting them
|
||||
# across runners would turn machine variance into a false regression.
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
profile:
|
||||
- large-house
|
||||
- isometric
|
||||
- isometric-stage3
|
||||
- plan-snap
|
||||
- interaction
|
||||
- blend
|
||||
- overlay
|
||||
- space-default
|
||||
- space-glow
|
||||
steps:
|
||||
- name: Check out candidate
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: candidate
|
||||
fetch-depth: 2
|
||||
@@ -131,12 +116,12 @@ jobs:
|
||||
echo "Comparison base: $sha ($source)" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Check out base SHA
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.base.outputs.sha }}
|
||||
path: baseline
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
@@ -147,127 +132,47 @@ jobs:
|
||||
- name: Install candidate and baseline dependencies
|
||||
run: npm ci --prefix candidate && npm ci --prefix baseline
|
||||
|
||||
# То же, что в validate.yml: кэш браузеров, apt не трогаем (#206).
|
||||
- name: Кэш браузеров Playwright
|
||||
id: pw
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('candidate/package-lock.json') }}
|
||||
- name: Install pinned Chromium
|
||||
if: steps.pw.outputs.cache-hit != 'true'
|
||||
working-directory: candidate
|
||||
run: npx playwright install chromium
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Build both exact source trees
|
||||
run: |
|
||||
npm --prefix candidate run build
|
||||
(cd candidate && node scripts/bundle-sync.mjs)
|
||||
cp candidate/dist/houseplan-card.js candidate/demo/srv/assets/houseplan-card.js
|
||||
npm --prefix baseline run build
|
||||
if [ -f baseline/scripts/bundle-sync.mjs ]; then
|
||||
(cd baseline && node scripts/bundle-sync.mjs)
|
||||
else
|
||||
echo "::notice::The comparison base predates bundle-sync.mjs; materializing its freshly built bundle with the legacy copy path."
|
||||
mkdir -p baseline/custom_components/houseplan/frontend baseline/demo/srv/assets
|
||||
cp baseline/dist/houseplan-card.js baseline/custom_components/houseplan/frontend/houseplan-card.js
|
||||
cp baseline/dist/houseplan-card.js baseline/demo/srv/assets/houseplan-card.js
|
||||
cp baseline/dist/houseplan-card.js baseline/demo/srv/assets/houseplan-card.js
|
||||
|
||||
- name: Capture base and candidate profiles
|
||||
working-directory: candidate
|
||||
run: |
|
||||
npm run benchmark:large-house -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/baseline.json
|
||||
npm run benchmark:large-house -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/candidate.json
|
||||
npm run benchmark:large-house-isometric -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-baseline.json
|
||||
npm run benchmark:large-house-isometric -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-candidate.json
|
||||
npm run benchmark:large-house-plan-snap -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/plan-snap-baseline.json
|
||||
npm run benchmark:large-house-plan-snap -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/plan-snap-candidate.json
|
||||
npm run benchmark:glow -- --profile=large-light-blend-v1 --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/blend-baseline.json
|
||||
npm run benchmark:glow -- --profile=large-light-blend-v1 --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/blend-candidate.json
|
||||
npm run benchmark:glow -- --profile=large-house-glow-overlay-v1 --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/overlay-baseline.json
|
||||
npm run benchmark:glow -- --profile=large-house-glow-overlay-v1 --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/overlay-candidate.json
|
||||
if ! grep -q "glow_enabled" ../baseline/src/logic.ts; then
|
||||
echo "Base predates independent Glow; bootstrap relative overlay baseline, keep absolute gate"
|
||||
cp ../artifacts/performance/overlay-candidate.json ../artifacts/performance/overlay-baseline.json
|
||||
fi
|
||||
|
||||
- name: Capture base and candidate profile
|
||||
- name: Enforce relative and absolute performance budgets
|
||||
working-directory: candidate
|
||||
env:
|
||||
PROFILE: ${{ matrix.profile }}
|
||||
run: |
|
||||
mkdir -p ../artifacts/performance
|
||||
case "$PROFILE" in
|
||||
large-house)
|
||||
npm run benchmark:large-house -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/baseline.json
|
||||
npm run benchmark:large-house -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/candidate.json
|
||||
;;
|
||||
isometric)
|
||||
npm run benchmark:large-house-isometric -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-baseline.json
|
||||
npm run benchmark:large-house-isometric -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-candidate.json
|
||||
;;
|
||||
isometric-stage3)
|
||||
npm run benchmark:isometric-stage3-dense -- --allow-stage2-base --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-stage3-baseline.json
|
||||
npm run benchmark:isometric-stage3-dense -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-stage3-candidate.json
|
||||
;;
|
||||
plan-snap)
|
||||
npm run benchmark:large-house-plan-snap -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/plan-snap-baseline.json
|
||||
npm run benchmark:large-house-plan-snap -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/plan-snap-candidate.json
|
||||
;;
|
||||
interaction)
|
||||
npm run benchmark:large-house-interaction -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/interaction-baseline.json
|
||||
npm run benchmark:large-house-interaction -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/interaction-candidate.json
|
||||
;;
|
||||
blend)
|
||||
npm run benchmark:glow -- --profile=large-light-blend-v1 --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/blend-baseline.json
|
||||
npm run benchmark:glow -- --profile=large-light-blend-v1 --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/blend-candidate.json
|
||||
;;
|
||||
overlay)
|
||||
npm run benchmark:glow -- --profile=large-house-glow-overlay-v1 --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/overlay-baseline.json
|
||||
npm run benchmark:glow -- --profile=large-house-glow-overlay-v1 --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/overlay-candidate.json
|
||||
if ! grep -q "glow_enabled" ../baseline/src/logic.ts; then
|
||||
echo "Base predates independent Glow; bootstrap relative overlay baseline, keep absolute gate"
|
||||
cp ../artifacts/performance/overlay-candidate.json ../artifacts/performance/overlay-baseline.json
|
||||
fi
|
||||
;;
|
||||
space-default)
|
||||
npm run benchmark:glow -- --profile=large-space-card-default-v1 --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/space-default-baseline.json
|
||||
npm run benchmark:glow -- --profile=large-space-card-default-v1 --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/space-default-candidate.json
|
||||
;;
|
||||
space-glow)
|
||||
npm run benchmark:glow -- --profile=large-space-card-glow-v1 --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/space-glow-baseline.json
|
||||
npm run benchmark:glow -- --profile=large-space-card-glow-v1 --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/space-glow-candidate.json
|
||||
if ! grep -q "light_pools" ../baseline/src/space-card.ts; then
|
||||
echo "Base predates opt-in static Glow; bootstrap relative baseline, keep absolute gate"
|
||||
cp ../artifacts/performance/space-glow-candidate.json ../artifacts/performance/space-glow-baseline.json
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "::error::Unknown performance profile: $PROFILE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
npm run benchmark:compare -- --baseline=../artifacts/performance/baseline.json --candidate=../artifacts/performance/candidate.json --output=../artifacts/performance/comparison.json
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-isometric.json --baseline=../artifacts/performance/isometric-baseline.json --candidate=../artifacts/performance/isometric-candidate.json --output=../artifacts/performance/isometric-comparison.json
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-plan-snap.json --baseline=../artifacts/performance/plan-snap-baseline.json --candidate=../artifacts/performance/plan-snap-candidate.json --output=../artifacts/performance/plan-snap-comparison.json
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-light-blend.json --baseline=../artifacts/performance/blend-baseline.json --candidate=../artifacts/performance/blend-candidate.json --output=../artifacts/performance/blend-comparison.json
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-glow-overlay.json --baseline=../artifacts/performance/overlay-baseline.json --candidate=../artifacts/performance/overlay-candidate.json --output=../artifacts/performance/overlay-comparison.json
|
||||
|
||||
- name: Enforce relative and absolute performance budget
|
||||
working-directory: candidate
|
||||
env:
|
||||
PROFILE: ${{ matrix.profile }}
|
||||
run: |
|
||||
case "$PROFILE" in
|
||||
large-house)
|
||||
npm run benchmark:compare -- --baseline=../artifacts/performance/baseline.json --candidate=../artifacts/performance/candidate.json --output=../artifacts/performance/comparison.json
|
||||
;;
|
||||
isometric)
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-isometric.json --baseline=../artifacts/performance/isometric-baseline.json --candidate=../artifacts/performance/isometric-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-comparison.json
|
||||
;;
|
||||
isometric-stage3)
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-isometric-stage3-dense.json --baseline=../artifacts/performance/isometric-stage3-baseline.json --candidate=../artifacts/performance/isometric-stage3-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-stage3-comparison.json
|
||||
;;
|
||||
plan-snap)
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-plan-snap.json --baseline=../artifacts/performance/plan-snap-baseline.json --candidate=../artifacts/performance/plan-snap-candidate.json --output=../artifacts/performance/plan-snap-comparison.json
|
||||
;;
|
||||
interaction)
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-interaction.json --baseline=../artifacts/performance/interaction-baseline.json --candidate=../artifacts/performance/interaction-candidate.json --output=../artifacts/performance/interaction-comparison.json
|
||||
;;
|
||||
blend)
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-light-blend.json --baseline=../artifacts/performance/blend-baseline.json --candidate=../artifacts/performance/blend-candidate.json --output=../artifacts/performance/blend-comparison.json
|
||||
;;
|
||||
overlay)
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-glow-overlay.json --baseline=../artifacts/performance/overlay-baseline.json --candidate=../artifacts/performance/overlay-candidate.json --output=../artifacts/performance/overlay-comparison.json
|
||||
;;
|
||||
space-default)
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-space-card-default.json --baseline=../artifacts/performance/space-default-baseline.json --candidate=../artifacts/performance/space-default-candidate.json --output=../artifacts/performance/space-default-comparison.json
|
||||
;;
|
||||
space-glow)
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-space-card-glow.json --baseline=../artifacts/performance/space-glow-baseline.json --candidate=../artifacts/performance/space-glow-candidate.json --output=../artifacts/performance/space-glow-comparison.json
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Upload full performance report
|
||||
- name: Upload full performance reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: full-performance-${{ matrix.profile }}
|
||||
name: full-performance
|
||||
path: artifacts/performance
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Публикация пре-релиза (ручная)
|
||||
name: Publish prerelease
|
||||
run-name: Publish ${{ inputs.tag }}
|
||||
|
||||
on:
|
||||
@@ -19,17 +19,16 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
name: "Гейт: зелёная Проверка и релизный контракт"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
sha: ${{ steps.candidate.outputs.sha }}
|
||||
tag: ${{ steps.candidate.outputs.tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v7
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 22 }
|
||||
- name: Pin the current dev candidate
|
||||
id: candidate
|
||||
@@ -54,23 +53,6 @@ jobs:
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: node scripts/release-contract.mjs "$TAG" --repo="$GITHUB_REPOSITORY"
|
||||
# #479: тяжёлые job Validate идут только на коммите с трейлером `Release:`.
|
||||
# Зелёный Validate без трейлера означал бы прогон без смоков и golden —
|
||||
# класс тихого пропуска #171/#207, поэтому трейлер проверяется здесь явно.
|
||||
- name: Require the Release trailer on the candidate commit
|
||||
env:
|
||||
SHA: ${{ steps.candidate.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git log -1 --format=%B "$SHA" > /tmp/head-message.txt
|
||||
if ! grep -Eq '^Release:[[:space:]]*v?[0-9]+\.[0-9]+\.[0-9]+' /tmp/head-message.txt; then
|
||||
echo "::error::Candidate $SHA has no Release: trailer — Validate ran without the heavy gates (#479)"
|
||||
exit 1
|
||||
fi
|
||||
# #479: свежесть скриншотов на обычном пуше — предупреждение; на
|
||||
# кандидате она обязана быть доказана строгим режимом.
|
||||
- name: Documentation screenshots are fresh for the candidate
|
||||
run: node scripts/check-docs.mjs --screenshots=strict
|
||||
- name: Require green Validate for this exact SHA
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -79,18 +61,17 @@ jobs:
|
||||
run: node scripts/release-gate.mjs "$SHA"
|
||||
|
||||
publish:
|
||||
name: Публикация тега и релиза
|
||||
needs: gate
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
url: ${{ steps.verify.outputs.url }}
|
||||
newly_published: ${{ steps.release.outputs.newly_published }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.gate.outputs.sha }}
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v7
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 22 }
|
||||
- name: Build and verify both release assets before publication
|
||||
env:
|
||||
@@ -99,15 +80,19 @@ jobs:
|
||||
set -euo pipefail
|
||||
npm ci
|
||||
npm run build
|
||||
node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend
|
||||
npm run bundle:budget
|
||||
cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js
|
||||
cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js
|
||||
VERSION=${TAG#v}
|
||||
grep -RFq "$VERSION" dist
|
||||
grep -Fq "$VERSION" dist/houseplan-card.js
|
||||
(cd custom_components/houseplan && zip -qr ../../houseplan.zip .)
|
||||
node scripts/verify-houseplan-zip.mjs houseplan.zip \
|
||||
custom_components/houseplan/frontend "$VERSION"
|
||||
unzip -l houseplan.zip | grep -q "manifest.json"
|
||||
ZIP_VERSION=$(unzip -p houseplan.zip manifest.json | node -e \
|
||||
"let s='';process.stdin.on('data',d=>s+=d).on('end',()=>process.stdout.write(JSON.parse(s).version))")
|
||||
test "$ZIP_VERSION" = "$VERSION" || {
|
||||
echo "::error::houseplan.zip manifest version $ZIP_VERSION != $VERSION"
|
||||
exit 1
|
||||
}
|
||||
test -s dist/houseplan-card.js
|
||||
test -s dist/houseplan-panel.js
|
||||
test -s houseplan.zip
|
||||
- name: Create or verify the annotated tag
|
||||
env:
|
||||
@@ -188,7 +173,7 @@ jobs:
|
||||
printf '### Published %s\n\n- exact SHA: `%s`\n- [GitHub prerelease](%s)\n- assets: `houseplan-card.js`, `houseplan.zip`\n' \
|
||||
"$TAG" "$SHA" "$URL" >> "$GITHUB_STEP_SUMMARY"
|
||||
- name: Verify HACS prerelease discovery order
|
||||
uses: actions/github-script@v9
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
EXPECTED_TAG: ${{ needs.gate.outputs.tag }}
|
||||
with:
|
||||
@@ -214,7 +199,6 @@ jobs:
|
||||
# A manual step after a successful release is the worst kind: by the time it is
|
||||
# due, the work already looks finished, which is exactly why it gets forgotten.
|
||||
close-merged:
|
||||
name: Закрытие вошедших issue
|
||||
needs: [gate, publish]
|
||||
if: ${{ needs.publish.outputs.newly_published == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
@@ -262,7 +246,6 @@ jobs:
|
||||
}
|
||||
|
||||
announce:
|
||||
name: Комментарий о публикации
|
||||
needs: [gate, publish]
|
||||
if: ${{ needs.publish.outputs.newly_published == 'true' }}
|
||||
uses: ./.github/workflows/announce.yml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: HACS-zip к релизу
|
||||
name: Attach HACS zip to release
|
||||
# hacs.json declares zip_release + filename=houseplan.zip, so every release
|
||||
# (prereleases included) must carry the asset — HACS installs from it and
|
||||
# GitHub's public download counter becomes a free per-version install metric
|
||||
@@ -19,7 +19,6 @@ permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
zip:
|
||||
name: Собрать houseplan.zip и приложить к релизу
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Resolve tag
|
||||
@@ -28,13 +27,13 @@ jobs:
|
||||
EVENT_TAG: ${{ github.event.release.tag_name }}
|
||||
INPUT_TAG: ${{ github.event.inputs.tag }}
|
||||
run: echo "tag=${EVENT_TAG:-$INPUT_TAG}" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.tag.outputs.tag }}
|
||||
- name: Build houseplan.zip (contents of custom_components/houseplan at zip root)
|
||||
run: cd custom_components/houseplan && zip -qr ../../houseplan.zip .
|
||||
- name: Sanity check
|
||||
run: node scripts/verify-houseplan-zip.mjs houseplan.zip custom_components/houseplan/frontend
|
||||
run: unzip -l houseplan.zip | grep -q "manifest.json"
|
||||
- name: Upload asset
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "Релиз: ассеты после зелёной проверки"
|
||||
name: Release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
@@ -14,14 +14,13 @@ jobs:
|
||||
# Needs a push with a token that has the `workflow` scope (the ordinary
|
||||
# Personal Access Token used for `git push` refuses workflow file updates).
|
||||
gate:
|
||||
name: "Гейт: зелёная Проверка точного SHA тега"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v7
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 22 }
|
||||
- name: Require a green Validate for this exact commit
|
||||
env:
|
||||
@@ -34,12 +33,6 @@ jobs:
|
||||
# target_commitish (it may be a branch name) or an event-context SHA.
|
||||
SHA=$(git rev-parse HEAD)
|
||||
echo "release tag: $TAG; exact commit: $SHA"
|
||||
# #479: тяжёлые job Validate идут только на коммите с трейлером
|
||||
# `Release:`; без него зелёный Validate прогона без смоков не доказывает.
|
||||
if ! git log -1 --format=%B "$SHA" | grep -Eq '^Release:[[:space:]]*v?[0-9]+\.[0-9]+\.[0-9]+'; then
|
||||
echo "::error::$SHA has no Release: trailer — Validate ran without the heavy gates (#479)"
|
||||
exit 1
|
||||
fi
|
||||
node scripts/release-gate.mjs "$SHA"
|
||||
- name: Require full performance for a stable release
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
@@ -49,55 +42,35 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SHA=$(git rev-parse HEAD)
|
||||
node scripts/release-gate.mjs "$SHA" --workflow=performance.yml --label="Полные бенчмарки производительности"
|
||||
# #514: the only check on a real Home Assistant. houseplan-e2e installs
|
||||
# the release's houseplan.zip — the bytes HACS ships — into HA in docker
|
||||
# and walks the sidebar page, dashboards, roles, PDF, restart and the
|
||||
# stable→tag upgrade. A red, missing or cancelled run withholds the
|
||||
# assets exactly like Full Performance. Cross-repository dispatch needs a
|
||||
# token with Actions: write on houseplan-e2e; HP_PROCESS_TOKEN (classic,
|
||||
# repo scope) has it, E2E_DISPATCH_TOKEN is the fallback for a
|
||||
# fine-grained token.
|
||||
- name: Require green E2E on a real Home Assistant for a stable release
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.E2E_DISPATCH_TOKEN || secrets.HP_PROCESS_TOKEN }}
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
run: node scripts/e2e-gate.mjs --tag="$TAG"
|
||||
node scripts/release-gate.mjs "$SHA" --workflow=performance.yml --label="Full Performance"
|
||||
build:
|
||||
name: Сборка бандла и загрузка ассетов
|
||||
needs: gate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
- uses: actions/setup-node@v7
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 22 }
|
||||
- run: npm ci && npm run build
|
||||
- name: Verify compositor frame continuity for a stable release
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
run: |
|
||||
npx playwright install --with-deps chromium
|
||||
node scripts/bundle-sync.mjs
|
||||
cp dist/houseplan-card.js demo/srv/assets/houseplan-card.js
|
||||
npm run continuity:screencast
|
||||
- name: Upload failed continuity frames
|
||||
if: ${{ failure() && !github.event.release.prerelease }}
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: continuity-screencast
|
||||
path: artifacts/continuity-screencast
|
||||
- name: Verify the complete committed frontend tree
|
||||
run: |
|
||||
node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend
|
||||
test -s dist/houseplan-card.js
|
||||
test -s dist/houseplan-panel.js
|
||||
- run: cp dist/houseplan-card.js custom_components/houseplan/frontend/
|
||||
- name: Attach card to release
|
||||
uses: softprops/action-gh-release@v3
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: dist/houseplan-card.js
|
||||
hacs-discovery:
|
||||
name: HACS-видимость пре-релиза (порядок бет)
|
||||
# HACS 2.0.x takes the first prerelease in GitHub's response instead of
|
||||
# sorting SemVer. A valid asset can therefore be invisible to beta users
|
||||
# (beta.10 appeared after beta.9). Keep the release asset, but
|
||||
@@ -107,7 +80,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Verify the published tag is the prerelease HACS will discover
|
||||
uses: actions/github-script@v9
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const releases = await github.paginate(github.rest.repos.listReleases, {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
# Без слэша: с ним шаблон не покрывает СИМВОЛИЧЕСКУЮ ССЫЛКУ с этим именем,
|
||||
# и `git add -A` в чужом worktree затягивает её в коммит (#520, прежде #429).
|
||||
node_modules
|
||||
node_modules/
|
||||
tsout/
|
||||
test-build/
|
||||
*.log
|
||||
@@ -9,11 +7,3 @@ __pycache__/
|
||||
.venv-backend/
|
||||
artifacts/
|
||||
.agents/
|
||||
|
||||
# Копия бандла для стенда: её собирает `npm run bundle:sync`, а в репозитории
|
||||
# она только росла — 364 версии по 1.16 МБ за семь недель (#255). Обязательных
|
||||
# копий две: `dist/` (артефакт сборки) и `custom_components/` (её ставит HACS).
|
||||
demo/srv/assets/houseplan-card.js
|
||||
demo/srv/assets/houseplan-panel.js
|
||||
demo/srv/assets/houseplan-assets.json
|
||||
demo/srv/assets/houseplan-assets/
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
3.14
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
House Plan is one HACS package with two parts plus a demo harness:
|
||||
|
||||
- **Lovelace card** (`src/`, TypeScript + Lit) — the primary product, bundled to
|
||||
the entry, manifest and hashed chunks under `dist/`.
|
||||
- **Lovelace card** (`src/`, TypeScript + Lit) — the primary product, bundled to `dist/houseplan-card.js`.
|
||||
- **Storage integration** (`custom_components/houseplan/`, Python) — the Home Assistant backend.
|
||||
- **Demo harness** (`demo/`) — a self-contained Playwright page (`demo/srv/demo.html`) that renders the card against a fake `hass`, used for screenshots and the `smoke_*.mjs` end-to-end suite.
|
||||
|
||||
@@ -41,16 +40,10 @@ task records: problem, scope, acceptance criteria and discussion.
|
||||
of a status and `rejected` on a closed issue. Exactly one `S*` label per open
|
||||
issue. Labels are the whole of it: GitHub Projects is no longer used.
|
||||
|
||||
**The light track is the default, not a shortcut** (owner's decision 2026-08-27,
|
||||
issue #338). `small` — the spec lives in the issue body and its review is a
|
||||
comment. Analysis names the `small` criterion the task *fails* when it takes the
|
||||
full track; "ordinary track" without a named criterion is not a justification.
|
||||
The threshold itself did not move — only which side carries the proof. The full
|
||||
track stays what it was for geometry, config migrations and public contracts,
|
||||
where a criterion is broken plainly and saying which one is easy.
|
||||
|
||||
`trivial` — the short track: no spec stage at all, `S2-analysis` straight to
|
||||
`S5-ready`, with the AC written into the issue body first. `trivial` requires a bug confined to one surface with no new UX
|
||||
Two shortcuts exist for small work. `small` — the light track: the spec lives in
|
||||
the issue body and its review is a comment. `trivial` — the short track: no spec
|
||||
stage at all, `S2-analysis` straight to `S5-ready`, with the AC written into the
|
||||
issue body first. `trivial` requires a bug confined to one surface with no new UX
|
||||
contract, no migration, no i18n, no perf or touch impact, at most three checkable
|
||||
AC, **and expected behaviour already on record** — nothing left to decide. Code
|
||||
review is never skipped on either track; it is what stands in for testing.
|
||||
@@ -94,7 +87,7 @@ Start with the spec?" is the correct answer, not a smaller patch.
|
||||
| **A — product** | `src/**`, `custom_components/houseplan/**/*.py`, `manifest.json`, `hacs.json`, i18n, `custom_components/**/translations/**` | yes |
|
||||
| **B — gates and tooling** | `test/**`, `tests_backend/**`, `demo/**`, `scripts/**`, `.github/workflows/**`, `rollup.config.mjs`, `tsconfig*.json` | yes; may reuse the issue it covers |
|
||||
| **C — documentation** | `docs/**`, `README*`, `CHANGELOG*`, `AGENTS.md` | not if it is part of its issue's DoD |
|
||||
| **D — generated** | `dist/**`, `custom_components/houseplan/frontend/**`, `demo/golden/baselines/**` | never changes on its own. The stand copy `demo/srv/assets/**` is no longer committed (#255): build the complete tree with `npm run bundle:sync` |
|
||||
| **D — generated** | `dist/**`, `custom_components/houseplan/frontend/**`, `demo/srv/assets/houseplan-card.js`, `demo/golden/baselines/**` | never changes on its own |
|
||||
|
||||
The table above is a summary; `PROCESS.md` §1 is the authority and now covers the
|
||||
configuration files this one omits — `package.json`, `package-lock.json`,
|
||||
@@ -153,21 +146,8 @@ in between.
|
||||
|
||||
If the rebase conflicts the pipeline says so in the issue and sends the task back
|
||||
to `S6-in-progress`. The verdict still stands: nothing needs reviewing again, the
|
||||
remaining work is the rebase. When the conflict is only in the committed bundle
|
||||
(`dist/**`, `custom_components/houseplan/frontend/**` — the usual case when two
|
||||
tasks built it in parallel), run `node scripts/rebase-on-dev.mjs` (#479): it takes
|
||||
`dev`'s copy through the rebase, rebuilds with `npm run bundle:sync` and amends
|
||||
the result into your last commit; a conflict anywhere else aborts and leaves the
|
||||
tree as it was. Then push the branch and re-apply `S7-code-review`. When the
|
||||
only difference from the reviewed material is the pipeline's own review-document
|
||||
commit, the next run re-applies the green verdict without calling the model
|
||||
(#499); any other change to the tree — a rebase included — gets a full review.
|
||||
|
||||
The pipeline is an idempotent controller (#499): only `S4-spec-review` and
|
||||
`S7-code-review` start it, it reads the issue's *current* labels rather than the
|
||||
event snapshot, and a label removed before the run starts is treated as a
|
||||
withdrawn request. Push the material **before** applying the label — the reviewer
|
||||
is pinned to the SHA the pipeline captured and must not fetch newer commits. The second review run is not a formality — after a rebase onto a
|
||||
remaining work is the rebase. Resolve it, push the branch, re-apply
|
||||
`S7-code-review`. The second review run is not a formality — after a rebase onto a
|
||||
moved `dev` this is different code, and accepting it unchecked is how regressions
|
||||
arrive. Cycles are counted per stage, so a code review spends its own budget.
|
||||
|
||||
@@ -230,12 +210,7 @@ pipeline, which reviews without anyone asking and takes ten to forty-five minute
|
||||
session.** Reporting "handed over for review" stops a conveyor that could have kept
|
||||
moving on its own. An agent has no clock — it exists only during its own turn — so
|
||||
waiting means polling: every 90 seconds, at most 30 times. A single long sleep hits
|
||||
the command timeout. Do the polling with `node scripts/wait-verdict.mjs --issue NN
|
||||
[--sha <tip>]` (#496): it watches the label, the pipeline's own comments (conflict,
|
||||
cancelled merge, failed run) and optionally Validate on the SHA, prints only when
|
||||
the state changes and exits 0 on a new label, 3 on an event that needs a hand,
|
||||
4 on timeout — the same 90 s × 30 without a model turn per tick. It writes
|
||||
nothing. Watch the **label**, not the comment: the label is the state,
|
||||
the command timeout. Watch the **label**, not the comment: the label is the state,
|
||||
the comment only explains it. Do not wait at all while `blocked` is set — the task
|
||||
is waiting on the owner, not on the reviewer. On exhausting the attempts, stop and
|
||||
tell the owner: a failed run leaves the label where it was, forever.
|
||||
@@ -280,15 +255,10 @@ The exchange happens in **issue comments** — there is no local message bus. Ve
|
||||
format:
|
||||
|
||||
```text
|
||||
Verdict: green/yellow/red · cycle r<N>/4 · High: N · Medium: N → in-task | #… · Document: …
|
||||
Verdict: green/yellow/red · cycle r<N>/4 · High: N · Medium: N → #… · Document: …
|
||||
```
|
||||
|
||||
High blocks. A Medium finding INSIDE the task's scope is fixed within the task:
|
||||
with no High findings the verdict is yellow, the author fixes it and the fix
|
||||
passes another review cycle — no separate issue (owner's decision 2026-08-19,
|
||||
#202: filing and servicing an issue costs far more than fixing in place). Only
|
||||
a Medium finding OUTSIDE the scope becomes its own issue — foreign scope is
|
||||
never patched from this branch. Low is fixed or waived with a note
|
||||
High blocks. Medium must become its own issue. Low is fixed or waived with a note
|
||||
in the review document. A yellow verdict is legitimate even when every acceptance
|
||||
criterion passes, if the change does not solve the stated scenario or degrades a
|
||||
neighbouring one.
|
||||
@@ -299,20 +269,14 @@ the owner splits the task, rejects it, or arbitrates.
|
||||
|
||||
On the light track (`small`: complexity ≤3, one surface, no config migration, no
|
||||
new UX contract, no perf or touch impact — all at once) the spec lives in the issue
|
||||
body and the spec review is a comment. Code review is never skipped. This track is
|
||||
the default: taking the full one means naming the criterion above that the task
|
||||
does not meet.
|
||||
body and the spec review is a comment. Code review is never skipped.
|
||||
|
||||
## Specs
|
||||
|
||||
The spec lives in the **issue body**, under a `## ТЗ` heading (owner decision
|
||||
2026-09-10, #517); `docs/specs/` is an archive of specs written before that date
|
||||
and takes no new files. Required sections are in `PROCESS.md` §7.1, plus two
|
||||
product ones: which persona meets this, on which surface, at what moment; and what
|
||||
the person sees before and after, in one sentence without implementation terms.
|
||||
Proof that a verdict was passed on a given text is the pipeline's job: it writes
|
||||
the `sha256` of the normalised body into the review document's anchor block, and
|
||||
an edit made after a green spec review reaches the code reviewer as a finding.
|
||||
`docs/specs/<NN>-<slug>.md`, linked to its issue in both directions. Required
|
||||
sections are in `PROCESS.md` §7.1, plus two product ones: which persona meets this,
|
||||
on which surface, at what moment; and what the person sees before and after, in one
|
||||
sentence without implementation terms.
|
||||
|
||||
**Ambiguity is asked, not guessed — but only product ambiguity.** A guess written as
|
||||
fact is the worst kind of defect: it passes review because it looks like a decision.
|
||||
@@ -348,41 +312,14 @@ npm run inventory # the only correct way to get test counts
|
||||
|
||||
Never copy test counts into documents by hand; they go stale in days.
|
||||
|
||||
After building, keep the complete manifest-driven bundle trees in sync — CI
|
||||
verifies every listed file byte-for-byte:
|
||||
After building, keep all three bundle snapshots in sync — CI compares them
|
||||
byte-for-byte:
|
||||
|
||||
```
|
||||
npm run bundle:sync # dist → custom_components + demo/srv/assets (#255)
|
||||
npm run bundle:budget # initial View graph <= 256000 B gzip (#337)
|
||||
cp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js
|
||||
cp dist/houseplan-card.js demo/srv/assets/houseplan-card.js
|
||||
```
|
||||
|
||||
`npm run gate:small` runs the mandatory part of PROCESS §8 in one go (#479):
|
||||
unit tests, build with typecheck, `no-new-any` and `smoke-select` in parallel,
|
||||
then the bundle-tree comparison and the bundle budget. It prints the smokes the
|
||||
diff selects but does not run them by default — `npm run gate:small -- --smokes`
|
||||
(#496) adds the browser phase after the artefact preparation: `bundle-sync`, then
|
||||
the directly matched and registered smokes two at a time; "broad" matches stay
|
||||
the reviewer's call. `golden`, `pytest` and `check-docs --screenshots=strict`
|
||||
remain the author's call by diff and AC.
|
||||
|
||||
**Start a task from its packet** (#496): `node scripts/task-packet.mjs --issue NN`
|
||||
prints one derived view — status and track, what the status permits, the owner's
|
||||
recent decisions, the branch against `dev` and Validate on its tip, the previous
|
||||
verdict with its recorded tree, AC → evidence from the last review document and
|
||||
what is still unwitnessed. It reads GitHub and git and writes nothing; the labels
|
||||
remain the only source of status.
|
||||
|
||||
**Heavy CI gates run on the beta candidate, nightly and on demand — not on every
|
||||
push (#479).** `smoke`, `golden` and `performance_smoke` in Validate are gated
|
||||
on the `heavy` output: true for a head commit carrying a `Release:` trailer, for
|
||||
`workflow_dispatch full=true` (which `nightly.yml` issues on `dev` every night)
|
||||
and for pull requests. A plain push to `dev` runs preflight, frontend (types,
|
||||
units, build, bundle sync, no-new-any), backend, hacs and hassfest. Screenshot
|
||||
freshness in `check-docs` is likewise a warning on a plain push and an error on
|
||||
the candidate; `publish-prerelease.yml` and `release.yml` refuse a candidate
|
||||
without the `Release:` trailer, so a green Validate without the heavy jobs can
|
||||
never pass for a release.
|
||||
|
||||
During the implementation cycle the fast gates always run. Since 2026-08-14 the
|
||||
owner's machine also carries Playwright with Chromium (Windows) and a full WSL
|
||||
environment, which changes one thing (#151): **before moving an issue to
|
||||
@@ -391,48 +328,11 @@ demo/smoke_<name>.mjs`. A red smoke that reaches the review costs a cycle; run
|
||||
locally it costs a minute. Precedent: on #89 a fixture error lived through a
|
||||
whole review round that a local run would have caught immediately.
|
||||
|
||||
**One handoff, one push (#510).** Run `node scripts/process-gate.mjs --issues`
|
||||
locally with `gh` available before pushing (without `gh` the hook cannot check the
|
||||
issue status and stays silent). After `S7-code-review` do not push to the branch
|
||||
until the verdict or the return arrives: a push on top of a running review cancels
|
||||
it (10–20 runner minutes) and, after the material is fixed, also the merge (#312).
|
||||
Set `S7` once per round, not after every CI fix: the pipeline now runs Validate
|
||||
with the diff mutants on the material itself and returns a red one to `S6` without
|
||||
spending a review cycle. Mutants by diff no longer run on ordinary pushes — only
|
||||
on the review candidate, the merge candidate, the beta candidate, PRs and the
|
||||
nightly run — so a routine push costs ~3 minutes; 08–09.09 they cost 48 of 56
|
||||
Validate job-hours and were mostly cancelled by the next push.
|
||||
|
||||
The full smoke set, `golden` and `performance_smoke` still belong to the
|
||||
pre-beta run — which is then mandatory and complete. WSL runs of the full HA
|
||||
harness (`~/houseplan-card`, venv) are advisory; **the canon does not move**:
|
||||
the beta gate is CI at the exact SHA.
|
||||
|
||||
**Verifying and capturing are different things (#455).** `golden:verify` is
|
||||
advisory and legal anywhere, Windows included: it reports differences and
|
||||
accepts nothing. **Capturing** frames is refused outside Linux
|
||||
before the browser even starts — `golden:capture` through
|
||||
`demo/golden/policy.mjs`, documentation screenshots through
|
||||
`npm run docs:capture` (use that script, not a bare `node
|
||||
demo/docs/capture.mjs`: the gate sits one step earlier because editing the
|
||||
capture script invalidates the committed screenshot index). The refusal prints
|
||||
the WSL command. The reason is not policy but physics: Windows
|
||||
rasterizes text through DirectWrite with different subpixel and DPI behaviour,
|
||||
so no frame ever matches an accepted baseline byte for byte, no environment
|
||||
witness can exist, and acceptance would refuse anyway (#401 accepts any
|
||||
environment that proves itself with byte-identical undeclared frames — Linux is
|
||||
simply the only one we have). The deliberate override is
|
||||
`HP_ALLOW_FOREIGN_CAPTURE="reason"`; the reason travels into the output and the
|
||||
manifest. Baselines are still accepted only via
|
||||
`npm run golden:accept -- --reviewed` on a complete artefact, and the accepted
|
||||
index records the platform next to the Chromium build. The one local
|
||||
shortcut is `npm run docs:accept -- --identical` (#512): it re-captures on this
|
||||
machine, compares decoded pixels with the committed frames and, only when every
|
||||
frame is identical, refreshes the manifest fingerprints — frames that differ go
|
||||
through the artefact as before. The displayed card version reaches the DOM
|
||||
through `displayVersion()` (`src/card-version.ts`); the global
|
||||
`__HP_VERSION_OVERRIDE__` behind it is for harnesses only and the product never
|
||||
sets it.
|
||||
harness (`~/houseplan-card`, venv) and `golden:verify` are advisory; **the canon
|
||||
does not move**: the beta gate is CI at the exact SHA, and baselines are accepted
|
||||
only via `npm run golden:accept -- --reviewed` on a complete Linux CI artefact.
|
||||
|
||||
**Backend.** A full Home Assistant harness cannot run on native Windows at all:
|
||||
Home Assistant imports the Unix-only `fcntl` module. Its canon is Linux CI or WSL.
|
||||
@@ -453,34 +353,21 @@ complete Linux CI artifact; never accept a partial scenario or images merely to
|
||||
CI green. See `demo/golden/README.md`.
|
||||
|
||||
**Freshness contract**: the embedded fingerprint covers `src/` plus Rollup,
|
||||
TypeScript and package-lock build inputs. Every browser check must verify it
|
||||
before trusting a result — benchmarks, golden runs and documentation captures
|
||||
call `assertFreshDemoBundle` themselves, and smokes get it from `launch()` in
|
||||
`demo/serve.mjs` (#236). A missing or mismatched fingerprint is a hard failure,
|
||||
not a warning; `HP_ALLOW_STALE_BUNDLE=1` skips the check for debugging and says
|
||||
so out loud. A smoke against a stale bundle does not fail cleanly: part of its
|
||||
assertions go red and part stay green, which reads as a logic defect.
|
||||
TypeScript and package-lock build inputs. Benchmark and golden tooling must call
|
||||
`assertFreshDemoBundle` before recording any result; a missing or mismatched
|
||||
fingerprint is a hard failure, not a warning.
|
||||
|
||||
**CI is pinned to an exact SHA.** The release gate accepts only a `completed
|
||||
success` run for the candidate's SHA, not "the last green one"; a new push cancels
|
||||
an unfinished Validate for the same branch. Gate jobs, matching the actual
|
||||
`validate.yml` (#191): `docs`, `provenance`, `process-gate`, `hacs`, `hassfest`,
|
||||
`frontend`, `smoke`, `golden`, `performance_smoke`, `backend`. The `changes` job
|
||||
is a service path-filter, not a gate. `docs` is a real blocker: it checks the
|
||||
screenshots `sourceFingerprint` against current `src/**`, which is exactly what
|
||||
went red after the #113 merge. A stable release additionally waits for Full
|
||||
Performance and for a green E2E run on a real Home Assistant (`houseplan-e2e`,
|
||||
dispatched on the tag by `release.yml`, #514); betas and the development cycle
|
||||
never run E2E.
|
||||
an unfinished Validate for the same branch. Jobs: `provenance`, `hacs`, `hassfest`,
|
||||
`frontend`, `smoke`, `golden`, `performance_smoke`, `backend`.
|
||||
|
||||
**"Verified" without a named command and its result is not evidence.**
|
||||
|
||||
## Environments
|
||||
|
||||
**Local Windows checkout** is the day-to-day environment: Node 22 and Python 3.14
|
||||
as in CI (`npm run toolchain:check` compares the machine with the pins CI actually
|
||||
uses — `.nvmrc` and `.python-version` are derived from the same sources, #496),
|
||||
`gh` authenticated. `.venv-backend` does **not** exist there — it is
|
||||
**Local Windows checkout** is the day-to-day environment: Node 22 as in CI, Python
|
||||
3.13 in a venv, `gh` authenticated. `.venv-backend` does **not** exist there — it is
|
||||
provisioned only by cloud agent startup scripts, which also run `npm ci` and install
|
||||
Playwright Chromium.
|
||||
|
||||
@@ -490,20 +377,21 @@ Chromium — a `1e-6`-tolerance magnet-snap on the opening-*placement* path. It
|
||||
reproduces against the pristine committed bundle, so treat it as
|
||||
pre-existing/pixel-precision, not a regression you introduced.
|
||||
|
||||
## Alpha experiments
|
||||
## Labs flags
|
||||
|
||||
`src/labs.ts` is the single registry and resolver for hidden presentation
|
||||
experiments. One browser-local switch controls the complete set known to the
|
||||
installed build: `hp_alpha=1` in query or the shared hash grammar enables and
|
||||
persists it, while `hp_alpha=0` disables and persists it. Do not add per-feature
|
||||
URL/storage keys or a YAML/config switch. The legacy `hp-labs` grammar and
|
||||
`houseplan_card_labs_v1` storage are ignored and never migrated.
|
||||
experiments. Activate a live flag through `?hp-labs=<id>` or the shared hash
|
||||
grammar, remove it with `-<id>`, and use `off` to clear the set. Do not add a
|
||||
YAML/config switch for a Labs-only experiment. A new entry needs a unique
|
||||
lowercase id, issue, numeric-core `since`, numeric-core `expires`, summary and
|
||||
unit/browser coverage. Invalid or duplicate registry entries fail closed.
|
||||
|
||||
A new registry entry needs a unique lowercase id, issue, summary and
|
||||
unit/browser coverage; it does not get `since` or `expires`. Invalid or duplicate
|
||||
entries fail closed. Alpha capabilities may alter presentation only and must not
|
||||
gate data, migrations, stores, HA actions or network calls. Current renderer
|
||||
details are in `docs/ISOMETRIC.md`.
|
||||
Expiry is exclusive and ignores prerelease suffixes: an entry expiring at
|
||||
`1.65.0` is unavailable in `1.65.0-beta.1`. Before that cycle, either remove the
|
||||
experiment or graduate it through its own reviewed issue; never extend expiry as
|
||||
an incidental change. Labs may alter presentation only and must not gate data,
|
||||
migrations, stores, HA actions or network calls. Current renderer details are in
|
||||
`docs/ISOMETRIC.md`.
|
||||
|
||||
Demo harness render quirk: the fake `hass` in `demo.html` is set once, so opening the
|
||||
page directly in a browser renders the floor plan but **device icons only appear
|
||||
|
||||
@@ -9,44 +9,6 @@ User-visible changes go into **both** changelogs in the same commit:
|
||||
`docs/CHANGELOG.md` (English) and `docs/CHANGELOG.ru.md` (Russian). Entries
|
||||
older than v1.42.0 exist only in the English file — no need to backfill them.
|
||||
|
||||
## Translations
|
||||
|
||||
A shipped UI language has three matching parts:
|
||||
|
||||
1. `src/i18n/<code>.json` for the card;
|
||||
2. `custom_components/houseplan/translations/<code>.json` for the Home Assistant
|
||||
integration;
|
||||
3. one registry entry (code, native label and eager dictionary or lazy loader)
|
||||
in `src/i18n/registry.ts`.
|
||||
|
||||
Use the canonical Home Assistant/BCP 47 language tag as `<code>` (for example,
|
||||
`fr` or `pt-BR`) and use that exact spelling for both JSON filenames. Lookup is
|
||||
case-insensitive and also accepts `_` from legacy locale sources.
|
||||
|
||||
The registry drives language resolution, the visual-editor selector and parity
|
||||
tests. The tests reject missing or extra locale files; frontend dictionaries
|
||||
also fail on mismatched keys, empty values and changed placeholders.
|
||||
Placeholders such as `{name}` and `{n}` are a contract: do not translate, add
|
||||
or remove them.
|
||||
|
||||
English and Russian are synchronous fallback/legacy locales. German is the
|
||||
reference lazy third-locale implementation: its module carries the same build
|
||||
fingerprint as the entry bundle, has one content-hashed retry URL and is listed
|
||||
under `lazyLocaleFiles` in `houseplan-assets.json`. New sizeable locales should
|
||||
follow that path unless a measured initial-bundle budget explicitly justifies
|
||||
an eager import. Extend the runtime, manifest, file/key/placeholder parity and
|
||||
regional-locale tests together; never bypass the registry by importing a locale
|
||||
directly in a component.
|
||||
|
||||
The current `subst()` helper does not implement plural rules. Phrase strings so
|
||||
their grammar does not depend on the numeric value (for example, use a neutral
|
||||
label followed by `{n}` rather than an English singular/plural pair).
|
||||
|
||||
Adding a UI locale does not automatically create another full documentation
|
||||
set; maintain the existing English and Russian documentation according to the
|
||||
project's normal rules. Right-to-left layout is a separate product project,
|
||||
because the plan canvas and editors cannot be mirrored by translations alone.
|
||||
|
||||
## Where to ask
|
||||
|
||||
Not sure whether something is a bug, or just want to discuss an idea before
|
||||
@@ -66,7 +28,7 @@ issue, but they do not replace it or maintain a separate checklist.
|
||||
## Five-minute setup
|
||||
|
||||
```bash
|
||||
git clone --filter=blob:none https://github.com/Matysh/houseplan-card && cd houseplan-card
|
||||
git clone https://github.com/Matysh/houseplan-card && cd houseplan-card
|
||||
npm ci # frontend toolchain
|
||||
npm run typecheck # tsc --noEmit (strict)
|
||||
npm test # node:test — pure logic, i18n parity, tap-action security
|
||||
@@ -75,24 +37,6 @@ pip install pytest voluptuous && python -m pytest tests_backend -q # pure back
|
||||
npm install # also installs .githooks through the prepare script
|
||||
```
|
||||
|
||||
### Why `--filter=blob:none` (#345)
|
||||
|
||||
A full clone is **215 MB of `.git`**; a blobless one is **26 MB** — measured, not
|
||||
estimated. Both carry all 1611 commits and all 182 tags, so ranges, `merge-base`
|
||||
and `git diff` across history work identically; `git diff origin/dev~3..origin/dev`
|
||||
in a blobless clone takes about a second and grows `.git` by one megabyte.
|
||||
|
||||
The difference is that historical *file contents* are fetched only if something
|
||||
actually asks for them. That matters here because 32% of the pack is documentation
|
||||
screenshots — ten PNGs re-captured 196 times — and another sizeable share is the
|
||||
committed bundle, one 1.16 MB file per product change. Almost nobody ever reads an
|
||||
old revision of either.
|
||||
|
||||
Drop the flag if you work offline with history, or need `git log -p` over the whole
|
||||
tree repeatedly. Do **not** replace it with `--depth=1`: a shallow clone is about
|
||||
the same size but has no `merge-base`, so the process gate, `smoke-select` and
|
||||
every `origin/dev..HEAD` range stop working.
|
||||
|
||||
The HA-harness backend tests (`tests_backend/test_ha_*.py`) need Python ≥3.13 and
|
||||
`pytest-homeassistant-custom-component home-assistant-frontend`; CI runs them on
|
||||
every push — locally they are skipped when `homeassistant` is not importable.
|
||||
@@ -101,8 +45,8 @@ every push — locally they are skipped when `homeassistant` is not importable.
|
||||
|
||||
- **Docs in the same commit**: CHANGELOG entry for user-visible changes;
|
||||
`docs/STATUS.md` for state changes; `docs/DEVELOPMENT.md` for new gotchas.
|
||||
- Every UI string goes through `src/i18n/<lang>.json`; follow the
|
||||
[Translations](#translations) flow for registry and backend parity.
|
||||
- Every UI string goes through `src/i18n/<lang>.json` (tests enforce en/ru key parity).
|
||||
Adding a language = adding one JSON file + registering it in `src/i18n.ts`.
|
||||
- The built card must be committed in sync: `cp dist/houseplan-card.js
|
||||
custom_components/houseplan/frontend/` (CI compares them byte-for-byte).
|
||||
- Tap actions have a security model (locks/alarms never toggle from the plan) —
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
| **A. Продукт** | `src/**`, `custom_components/houseplan/**/*.py`, `manifest.json`, `hacs.json`, `src/i18n/*.json`, `custom_components/**/translations/*` | **Да, обязательно.** Только из «Готово к разработке» или дальше |
|
||||
| **B. Гейты и инструменты** | `test/**`, `tests_backend/**`, `demo/**`, `scripts/**`, весь `.github/**`, `.githooks/**`, `rollup.config.mjs`, `tsconfig*.json`, `package.json`, `package-lock.json`, `pytest.ini`, `.gitignore`, `.gitattributes` | **Да.** Может использовать issue того изменения, которое покрывает; самостоятельная работа над гейтом получает свой issue (тип `tech-debt`) |
|
||||
| **C. Документация** | `docs/**`, `README*`, `CHANGELOG*`, `AGENTS.md`, `CONTRIBUTING.md`, `PROCESS*.md`, `LICENSE`, `(CODE\|SPEC)-REVIEW-*.md` | Документирование A/B в том же коммите — часть DoD своего issue. Самостоятельная работа над документацией — свой issue |
|
||||
| **D. Сгенерированное** | `dist/**`, `custom_components/houseplan/frontend/**`, `demo/golden/baselines/**` (копия стенда `demo/srv/assets/houseplan-card.js` с #255 не коммитится вовсе) | Никогда не меняется само по себе. Коммит **только** класса D допустим лишь как релизный промоушен или как принятие эталонов с доказательством ревью |
|
||||
| **D. Сгенерированное** | `dist/**`, `custom_components/houseplan/frontend/**`, `demo/srv/assets/houseplan-card.js`, `demo/golden/baselines/**` | Никогда не меняется само по себе. Коммит **только** класса D допустим лишь как релизный промоушен или как принятие эталонов с доказательством ревью |
|
||||
|
||||
Практический смысл таблицы: «я только поправил тест» и «я только пересобрал
|
||||
бандл» перестают быть лазейками.
|
||||
@@ -101,9 +101,7 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
5. приоритет **P1/P2/P3**;
|
||||
6. тип: баг / фича / техдолг;
|
||||
7. затронутые поверхности (модули, диалоги, бэкенд, i18n);
|
||||
8. трек — **по умолчанию `small`** (§5). Если задача идёт полным треком,
|
||||
называется критерий §5, который она не проходит: «обычный трек» без
|
||||
названного критерия обоснованием не является.
|
||||
8. трек: обычный / `small` / `trivial` по критериям §5 и §5.1.
|
||||
- **Оценки и приоритет ставятся метками сразу, согласие не запрашивается.**
|
||||
Комментарий аналитики — уведомление, а не запрос: **молчание владельца —
|
||||
согласие**, несогласие он выражает правкой меток или комментарием, и это не
|
||||
@@ -122,12 +120,9 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
### 2.3 ТЗ в работе — написание ТЗ
|
||||
|
||||
- **Кто:** автор ТЗ, назначает себя. Статус означает «занято».
|
||||
- **Артефакт:** **тело issue**, раздел `## ТЗ` (решение владельца 2026-09-10,
|
||||
#517). Файл в `docs/specs/` не создаётся ни на одном треке: каталог — архив
|
||||
ТЗ до этой даты, и задачи, у которых файл уже есть, доживают по старой схеме.
|
||||
Доказуемость («вердикт вынесен на этом тексте») держит конвейер: в блок якорей
|
||||
документа ревью пишется `sha256` нормализованного тела, и правка ТЗ после
|
||||
зелёного ревью ТЗ приходит ревьюеру кода находкой, а не тишиной.
|
||||
- **Артефакт:** `docs/specs/<NN>-<slug>.md`, где `NN` — **номер issue**.
|
||||
Многоэтапная задача: `<NN>-<slug>-stage<N>.md`.
|
||||
- **Лёгкий трек:** ТЗ пишется в теле issue, файл не создаётся (§5).
|
||||
- **Выход:** полная первая редакция по §7.
|
||||
|
||||
### 2.4 ТЗ на ревью
|
||||
@@ -136,14 +131,10 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
Его задача — не согласиться, а найти, где ТЗ не выполнимо или не проверяемо.
|
||||
- **Артефакт:** `docs/reviews/SPEC-REVIEW-<NN>-r<N>.md`, вердикт
|
||||
зелёный / жёлтый / красный. Лёгкий трек — комментарий в issue.
|
||||
- **High-находки блокируют.** Medium **в скоупе задачи** чинится в текущем
|
||||
issue: без High это жёлтый вердикт, автор правит ТЗ, фикс проходит повторный
|
||||
цикл. Medium **вне скоупа** — отдельный issue: чужой скоуп в этой задаче не
|
||||
правится. «Оставили в тексте ревью» не считается закрытием ни для одной
|
||||
(решение владельца 2026-08-19, #202: отдельный issue дороже правки на месте).
|
||||
Low либо правится, либо снимается решением ревьюера с записью.
|
||||
- **High-находки блокируют.** Medium/Low — либо правятся, либо становятся
|
||||
отдельными issue со ссылкой; «оставили в тексте ревью» не считается закрытием.
|
||||
- **Выход:** «Готово к разработке» либо возврат в «ТЗ в работе» — не более
|
||||
4 циклов (§4). Второй и последующие циклы разбираются по дельте (§2.10).
|
||||
4 циклов (§4).
|
||||
|
||||
### 2.5 Готово к разработке (DoR)
|
||||
|
||||
@@ -158,8 +149,8 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
- миграция и compatibility-поля решены по `docs/CONFIG-COMPATIBILITY.md`;
|
||||
- влияние на производительность и бюджеты названо (или явно «нет»);
|
||||
- влияние на touch по `docs/TOUCH-SUPPORT.md` (View и киоск — блокирующие);
|
||||
- release-артефакты названы: changelog RU+EN, документация, golden/скриншоты,
|
||||
performance/security — либо явное «нет»;
|
||||
- release-артефакты по правилу `docs/specs/README.md` (changelog RU+EN,
|
||||
документация, golden/скриншоты, performance/security);
|
||||
- **откат**: как выключить или вернуть назад (флаг Labs, обратная миграция);
|
||||
- открытых продуктовых вопросов нет; риски перечислены.
|
||||
|
||||
@@ -196,46 +187,9 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
кода отвечает на вопрос «оно вообще работает»: каждый AC либо доказан
|
||||
автотестом — и ревьюер убедился, что **тест умеет падать**, — либо разобран по
|
||||
коду с явной записью «проверено чтением, не исполнением».
|
||||
- **Защитный AC доказывается таблицей «чем краснеет» (#435).** Для каждого AC,
|
||||
заявляющего защиту — валидация, гард, лимит, отказ, инвариант, — в документе
|
||||
ревью обязательна строка из трёх столбцов: **AC · чем доказан** (точная
|
||||
команда или имя теста) **· чем краснеет** — мутация, снятая защита или
|
||||
отрицательная проба, с результатом прогона. Пустой третий столбец — находка
|
||||
Medium, а не примечание.
|
||||
|
||||
«Тест умеет падать» без названной мутации и её вывода доказательством не
|
||||
является. Аудит v1.71.0-beta.1 нашёл пять контрактов #51 и #423, где тест
|
||||
оставался зелёным на снятой защите; все пять прошли код-ревью как доказанные,
|
||||
а два теста были записаны в закрытие coverage-ratchet под именами, обещавшими
|
||||
то, чего они не проверяли (#430).
|
||||
|
||||
Мутант в `scripts/mutation-gate.mjs` обязателен, когда защита живёт в
|
||||
продуктовом коде и проверяется дорогим гейтом (смок, бэкенд, golden): там
|
||||
ревьюер не воспроизведёт отрицательный прогон второй раз. Для чистых юнитов
|
||||
достаточно прогона со снятой защитой, приведённого в документе.
|
||||
|
||||
Считаются **защитные AC без названного свидетеля**, а не мутанты на
|
||||
подсистему: у #421 мутанты были, и дыра всё равно проехала. «Сколько мутантов
|
||||
принесла задача» остаётся признаком — у #423 их ноль, и именно у #423 нашёлся
|
||||
тест, спрашивавший регулярку, находит ли она подстроку, которую сам же и
|
||||
вырезал.
|
||||
|
||||
Правило не распространяется на AC, не заявляющие защиту (расположение, текст,
|
||||
формат вывода): там свидетель — обычное сравнение ожидаемого с фактическим, и
|
||||
третий столбец превратился бы в ритуал. И не отменяет «проверено чтением»:
|
||||
тогда во втором столбце стоит «чтением», а не имя теста, и читатель ревью
|
||||
видит разницу.
|
||||
- **High блокируют.** Medium **в скоупе задачи** чинится в текущем issue:
|
||||
без High это жёлтый вердикт и возврат автору, фикс проходит повторный цикл.
|
||||
Medium **вне скоупа** — отдельный issue (#202).
|
||||
- **Вердикт привязан к SHA (#312).** Все числа и факты отчёта сверяются с
|
||||
`git rev-parse HEAD` непосредственно перед подведением итогов, а не с SHA,
|
||||
зафиксированным в начале разбора: во время ревью в ветку может прилететь
|
||||
fix-up. Серверный стопор — шаг слияния конвейера сверяет вершину ветки с
|
||||
SHA материала ревью (допустим только собственный doc-коммит публикации
|
||||
поверх) и при расхождении отменяет слияние с возвратом в `S6-in-progress`.
|
||||
- **High блокируют.** Medium **обязаны** превратиться в issue.
|
||||
- **Выход:** очередь на пре-релиз либо возврат в «В разработке», не более
|
||||
4 циклов (§4). Второй и последующие циклы разбираются по дельте (§2.10).
|
||||
4 циклов (§4).
|
||||
|
||||
### 2.8 Закрытие после выпуска беты
|
||||
|
||||
@@ -256,62 +210,6 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
- **Отклонено:** закрытие с записанной причиной (вне скоупа, дубликат, цена не
|
||||
оправдана). Тихое закрытие без причины запрещено.
|
||||
|
||||
### 2.10 Повторный раунд ревью — объём по дельте
|
||||
|
||||
Решение владельца 2026-08-19 (issue #214). Относится и к ревью ТЗ, и к
|
||||
код-ревью, начиная со второго цикла.
|
||||
|
||||
**Предмет повторного раунда — дельта, а не задача целиком.** Раньше объём
|
||||
разбора не был оговорён, промпт ревьюера для всех раундов был одинаковым, и
|
||||
повторный цикл заново выводил продуктовую рамку и перепроверял AC, которых
|
||||
правка не касалась: r2 по #150 стоил полного прогона конвейера ради одной
|
||||
строки в тестовой фикстуре.
|
||||
|
||||
Порядок:
|
||||
|
||||
1. найти вердикт предыдущего раунда и **материал, на котором он получен**.
|
||||
Материал объявлен блоком «Материал раунда» в конце документа предыдущего
|
||||
раунда: конвейер дописывает туда SHA ветки, **дерево** материала и **блоб**
|
||||
каждого ТЗ вместе с командами поиска (issue #416). Блок машинный — править
|
||||
его руками не нужно и не следует;
|
||||
2. объявить дельту: `git diff <тот SHA>..HEAD` для кода, дифф файла ТЗ либо тела
|
||||
issue для этапа ТЗ.
|
||||
|
||||
**Если SHA не резолвится — это не находка, а обычное дело.** Ветку задачи
|
||||
между раундами перебазируют, сквошат или удаляют, и SHA умирает: по корпусу
|
||||
ревью таких объявлений 98 из 804. Материал в этом случае берётся по якорям,
|
||||
которые ребейз не меняет, потому что адресуются содержимым:
|
||||
|
||||
```
|
||||
git log --all --format='%H %T' | grep <дерево>
|
||||
git log --all --find-object=<блоб> -- <путь к ТЗ>
|
||||
```
|
||||
|
||||
Находкой остаётся другое: **SHA, мёртвый уже в момент публикации отчёта** —
|
||||
он означает, что значение сняли до `amend` или `rebase` и не сверили перед
|
||||
выводом, как требует §7.2. Это отличие не теоретическое: на #403 оба
|
||||
источника, автор и ревьюер, независимо назвали один и тот же осиротевший
|
||||
SHA, и следующий раунд восстанавливал коммит по содержимому диффа руками
|
||||
(issue #413). Конвейер теперь такую публикацию останавливает сам;
|
||||
3. по каждой находке предыдущего раунда показать, **чем именно она закрыта** —
|
||||
строкой кода или текста, а не заявлением автора;
|
||||
4. заново проверять только те AC, чьё доказательство дельта задевает;
|
||||
5. **раздел «Унаследовано из r<N−1>»** обязателен: что принято без повторной
|
||||
проверки, со ссылкой на документ того раунда и его материал. Без перечня
|
||||
сокращение превращается в молчаливое доверие.
|
||||
|
||||
Дешёвые гейты (`typecheck`, `test`, `build` со сверкой копий бандла) гоняются в
|
||||
каждом раунде: код изменился, а стоят они минуты. Тяжёлые — по дельте (§10.2).
|
||||
|
||||
**Разбор остаётся полным**, если дельта не локальна: ребейз на ушедший вперёд
|
||||
`dev` (после ребейза это другой код, §7.2), смена контракта поведения, задета
|
||||
новая подсистема, либо объём дельты сопоставим с исходной задачей.
|
||||
|
||||
Сокращается объём **разбора, а не строгость**: правка по замечанию способна
|
||||
сломать AC, который предыдущий раунд признал выполненным — так появилась
|
||||
регрессия #102. Граница не «только находки», а «находки плюс всё, до чего
|
||||
дотягивается дельта».
|
||||
|
||||
---
|
||||
|
||||
## 3. Правила
|
||||
@@ -333,17 +231,13 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
ревью-гейт.
|
||||
7. **Ревью возвращает не более 4 раз.** Пятый заход — решение владельца: разделить,
|
||||
отклонить или арбитраж (§4).
|
||||
8. **High блокирует. Medium в скоупе чинится в текущем issue** (без High —
|
||||
жёлтый вердикт и повторный цикл); Medium вне скоупа становится отдельным
|
||||
issue (#202). Low либо правится, либо снимается решением ревьюера с записью
|
||||
в документе.
|
||||
8. **High блокирует. Medium становится issue.** Low либо правится, либо снимается
|
||||
решением ревьюера с записью в документе.
|
||||
9. **Скоуп не расширяется.** Всё найденное вне ТЗ — новый issue, а не попутная
|
||||
правка. Блокирующая находка отправляет текущий issue в «Заблокировано».
|
||||
10. **Каждый коммит класса A и B несёт трейлер `Issue: #NN`**, ветка называется
|
||||
`issue/NN-slug`, а `User-Visible: yes` требует правок в **обоих** changelog в
|
||||
том же коммите. После `cherry-pick -x` служебная строка `(cherry picked
|
||||
from ...)` должна оставаться выше финального блока трейлеров: перед push
|
||||
проверяем порядок через `git show -s --format=full HEAD`.
|
||||
том же коммите.
|
||||
11. **Документация — в том же коммите, что поведение.** Отдельным «допишу потом»
|
||||
коммитом документация не бывает.
|
||||
12. **Сгенерированное не коммитится само по себе.** Только релизный промоушен или
|
||||
@@ -369,24 +263,11 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
|
||||
## 4. Лимит циклов ревью: 4
|
||||
|
||||
Оба ревью-гейта возвращают задачу на правки не более **4 раз**.
|
||||
Оба ревью-гейта возвращают задачу на правки не более **4 раз**. Счётчик виден в
|
||||
имени документа: `-r1` … `-r4`; на четвёртом заходе ставится метка `review-4`.
|
||||
|
||||
- **Что считается циклом:** отправка на ревью → вердикт с блокирующими находками
|
||||
→ возврат. Уточняющий вопрос без вердикта циклом не считается.
|
||||
- **Зелёный вердикт цикла не образует** и бюджет не тратит (решение владельца
|
||||
2026-08-20, issue #227): он ничего не вернул на правки. Практический случай —
|
||||
зелёное ревью, слияние которого не удалось: конвейер сам предписывает ребейз и
|
||||
возврат метки, и этот заход не должен наказываться. Раньше счётчик считал все
|
||||
вердикты подряд, и на #225 последовательность жёлтый → зелёный → ребейз дала
|
||||
`review-4` на задаче с зелёным ревью и зелёным CI.
|
||||
- **Заход и цикл — разные величины.** Заход — сколько раз ревью отработало; он
|
||||
виден в имени документа (`-r1`, `-r2`, …) и нужен, чтобы два документа не
|
||||
затёрли друг друга. Цикл — единица бюджета §4. Заходов законно бывает больше,
|
||||
чем циклов, поэтому порог проверки 7 в `scripts/process-gate.mjs` выше лимита
|
||||
циклов (шесть документов = четыре цикла плюс два ребейза).
|
||||
- Метка `review-4` ставится, когда исчерпан **бюджет циклов**; конвейер снимать
|
||||
её не вправе — это решение владельца. Если бюджет пересчитан и оказался ниже
|
||||
лимита, конвейер сообщает пересчёт, но метку не трогает.
|
||||
- **Исчерпание лимита — не «пятая попытка», а разбор.** Задача уходит владельцу,
|
||||
решение одно из трёх:
|
||||
1. **разделить** — issue закрывается как «заменён», вместо него 2–3 меньших с
|
||||
@@ -402,26 +283,9 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
|
||||
---
|
||||
|
||||
## 5. Лёгкий трек (метка `small`) — путь по умолчанию
|
||||
## 5. Лёгкий трек (метка `small`)
|
||||
|
||||
Умолчание изменено решением владельца 2026-08-27, issue #338. Прежде полный трек
|
||||
был бесплатен, а выбор лёгкого требовал обоснования. Фактическая цена: **2.9
|
||||
ревью-документа на задачу** в среднем и до шести на одну issue (#329, #316,
|
||||
#290) — при том что Medium-находки всё равно чинятся в той же задаче, без
|
||||
отдельного цикла.
|
||||
|
||||
**Порог не изменился.** Критерии ниже те же и по-прежнему обязательны все
|
||||
одновременно. Изменилась сторона доказательства: теперь обосновывается не выбор
|
||||
лёгкого трека, а отказ от него — в `S2-analysis` называется критерий, который
|
||||
задача не проходит. Полный трек остаётся тем, чем был, для геометрии, миграций
|
||||
конфига и публичных контрактов: там критерии нарушаются сами, и назвать
|
||||
нарушенный несложно.
|
||||
|
||||
Инверсия умолчания не отменяет ничего из §5 ниже и ничего из §4: бюджет четырёх
|
||||
циклов, арбитраж владельца, обязательность ТЗ на полном треке и правило «ревью
|
||||
до мержа» остаются как были. Меняется только стоимость пути по умолчанию.
|
||||
|
||||
**Критерии — все одновременно; нарушенный называется явно:**
|
||||
**Критерии — все одновременно:**
|
||||
|
||||
- сложность и риск ≤ 3;
|
||||
- одна поверхность (один диалог, один модуль, один эндпоинт);
|
||||
@@ -431,8 +295,8 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
|
||||
**Что упрощается:**
|
||||
|
||||
- ТЗ короче: проблема · контракт · AC1…ACn с доказательством · откат
|
||||
(в теле issue, как и на полном треке с 2026-09-10);
|
||||
- ТЗ пишется **в теле issue** по шаблону: проблема · контракт · AC1…ACn с
|
||||
доказательством · откат. Файл в `docs/specs/` не создаётся;
|
||||
- ревью ТЗ — комментарий второго агента, отдельный документ не нужен;
|
||||
- лимит ревью ТЗ — 2 цикла.
|
||||
|
||||
@@ -491,7 +355,7 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
| Роль | Делает | Не имеет права |
|
||||
|---|---|---|
|
||||
| Аналитик | разбор, оценки, поверхности | окончательно ставить приоритет |
|
||||
| Автор ТЗ | раздел `## ТЗ` в теле issue | ревьюить своё ТЗ |
|
||||
| Автор ТЗ | `docs/specs/NN-*.md` или ТЗ в issue | ревьюить своё ТЗ |
|
||||
| Ревьюер ТЗ | `docs/reviews/SPEC-REVIEW-NN-rN.md` | править ТЗ вместо автора |
|
||||
| Разработчик | код, автотесты, документация, changelog | ревьюить свой код, принимать golden |
|
||||
| Ревьюер кода | `docs/reviews/CODE-REVIEW-*-rN.md`, проверка AC | править продуктовый код |
|
||||
@@ -523,7 +387,7 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
|
||||
```
|
||||
issue #NN
|
||||
↔ ТЗ тело issue, раздел `## ТЗ` (хеш тела — в якорях ревью)
|
||||
↔ ТЗ docs/specs/NN-slug.md (или тело issue при `small`)
|
||||
↔ ревью ТЗ docs/reviews/SPEC-REVIEW-NN-rN.md (или комментарий при `small`)
|
||||
↔ ветка issue/NN-slug
|
||||
↔ коммиты трейлеры Issue: #NN · User-Visible: yes|no
|
||||
@@ -580,12 +444,8 @@ issue #NN
|
||||
- **Занятие:** `Взял: <роль> · сессия <id> · ветка issue/NN-slug`
|
||||
- **Хендофф:** `Сделано: … · Файлы: … · Гейты: <команда → результат> ·
|
||||
НЕ сделано: … · Риски: … · Следующий статус: … · Новые issue: #…`
|
||||
- **Вердикт ревью:** `Вердикт: зелёный/жёлтый/красный · заход r<N> ·
|
||||
блокирующих циклов K/<лимит> · High: N · Medium: N → в задаче | #… ·
|
||||
Документ: docs/reviews/…`
|
||||
(«→ #…» — только у Medium вне скоупа; находки в скоупе возвращаются автору.
|
||||
Заход — номер прогона ревью, K — израсходованный бюджет §4: зелёные вердикты
|
||||
его не тратят, поэтому заход и K расходятся, #227)
|
||||
- **Вердикт ревью:** `Вердикт: зелёный/жёлтый/красный · цикл r<N>/<лимит> ·
|
||||
High: N · Medium: N → #… · Документ: docs/reviews/…`
|
||||
- **Закрытие:** `Выпущено в <тег беты> · CI: <ссылка> · Changelog: <ссылка>`
|
||||
|
||||
**Вперёд двигает только зелёный вердикт.** Жёлтый и красный возвращают автору;
|
||||
@@ -596,9 +456,10 @@ issue #NN
|
||||
|
||||
### 7.3 Расхождения с текущим состоянием, которые надо закрыть
|
||||
|
||||
1. ✅ **Статус ТЗ дублировал статус issue.** Закрыто 2026-09-10 (#517): ТЗ живёт
|
||||
в теле issue, `docs/specs/` — архив, индекс с колонкой «Статус ТЗ» удалён
|
||||
вместе с самой таблицей. Статус задачи — только метка `S*`.
|
||||
1. **Статус ТЗ дублирует статус issue.** `docs/specs/README.md` держит колонку
|
||||
«Статус ТЗ» со своим словарём («черновик решения», «в реализации»,
|
||||
«реализовано»). Два источника статуса уже расходятся. Колонку убрать, оставить
|
||||
таблицу «issue ↔ ТЗ».
|
||||
2. **Ревью до релиза 1.62 живут вне репозитория.** Документы `CODE-REVIEW-*.md` и
|
||||
`SPEC-REVIEW-*.md` за прежний период лежат в папке владельца, и переносить их
|
||||
задним числом смысла нет: они описывают код, которого уже нет. Новые документы
|
||||
@@ -615,87 +476,23 @@ issue #NN
|
||||
npx tsc --noEmit
|
||||
npm test
|
||||
npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js \
|
||||
# копия стенда собирается `npm run bundle:sync`, в репозитории её нет (#255)
|
||||
node scripts/smoke-select.mjs --base origin/dev --head HEAD # какие смоки относятся к диффу
|
||||
&& cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js
|
||||
node demo/smoke_<целевые>.mjs
|
||||
node scripts/no-new-any.mjs --base origin/dev --head HEAD # новый код не добавляет any
|
||||
npm run golden:verify # если менялся визуал
|
||||
node scripts/check-docs.mjs # если менялся src/**
|
||||
node scripts/model-invariants.mjs --config <экспорт> # если правилась геометрия или ссылки
|
||||
python -m pytest tests_backend -q # py3.14 как в CI (npm run toolchain:check), если менялся бэкенд
|
||||
python -m pytest tests_backend -q # py3.13, если менялся бэкенд
|
||||
```
|
||||
|
||||
**Новый код не добавляет `any`** (#342). В `src/**` уже 1034 вхождения явного
|
||||
`any` в 49 файлах; перетипизировать это одним заходом — месяц риска ради нуля
|
||||
пользовательской ценности, поэтому долг снимается при плановом извлечении
|
||||
подсистем (#425, прежний #34), а не разовой заменой. Гейт `scripts/no-new-any.mjs` судит
|
||||
**только добавленные строки**: существующий долг на нетронутой строке законен,
|
||||
правка строки со старым `any` — новая ответственность. Исключение объявляется на
|
||||
той же строке, `// any-ok: <конкретная причина>`; голый маркер и причины вида
|
||||
«todo» не проходят. Текст разбирается парсером TypeScript, поэтому слово «any» в
|
||||
комментарии, строке или идентификаторе ложных срабатываний не даёт.
|
||||
|
||||
**Объём гейтов на код-ревью соразмерен задаче** (issue #127). Всегда:
|
||||
`typecheck`, `npm test`, `npm run build` со сверкой трёх копий бандла, а при
|
||||
любом diff'е по `src/**` — ещё и `node scripts/check-docs.mjs`. По
|
||||
необходимости, определяемой diff'ом и AC: браузерные смоки (сколько их —
|
||||
считает `ls demo/smoke_*.mjs | wc -l`, вшитое число здесь трижды отставало от
|
||||
дерева; прогон всех уместен только когда задача задевает всё; какие относятся к
|
||||
диффу, печатает
|
||||
`node scripts/smoke-select.mjs --base origin/dev --head HEAD`, и его вывод
|
||||
прикладывается к ревью вместе с решением по каждой строке), `golden:verify` при изменении видимого
|
||||
`typecheck`, `npm test`, `npm run build` со сверкой трёх копий бандла. По
|
||||
необходимости, определяемой diff'ом и AC: браузерные смоки (их 127 — прогон всех
|
||||
уместен только когда задача задевает всё), `golden:verify` при изменении видимого
|
||||
результата, `pytest tests_backend` при правках в Python, performance-профили при
|
||||
названном в AC влиянии. **Полные наборы — предрелизный гейт, а не гейт ревью.**
|
||||
|
||||
Скриншоты снимаются **только** джобой `Docs screenshots` (`workflow_dispatch`) и
|
||||
принимаются локально: `npm run docs:accept -- --reviewed --from=<распакованный
|
||||
артефакт>` (#246). Съёмка на своей машине даёт байтово другой PNG при том же
|
||||
кадре, и набор из «не того» браузера переписывает все десять файлов без единого
|
||||
содержательного изменения. Приёмка отказывает, если кандидат снят не с этого
|
||||
дерева, не тем капчуром, не называет свой Chromium или неполон; коммит делает
|
||||
человек.
|
||||
|
||||
Когда правка `src/**` кадров не меняет — а это большинство правок — CI-цикл не
|
||||
нужен (#512): `npm run docs:accept -- --identical` снимает кадры локально,
|
||||
декодирует оба набора в Chromium и при нуле отличающихся пикселей во всех
|
||||
кадрах обновляет только отпечаток исходников в `screenshots.json`; байты
|
||||
закоммиченных PNG, их sha, браузер и упаковщик съёмки остаются прежними. Хотя бы
|
||||
один отличающийся пиксель — отказ с перечнем кадров и штатный путь через артефакт.
|
||||
|
||||
`check-docs` стоит в обязательной части не по важности, а по механике: отпечаток
|
||||
скриншотов документации считается по всему `src/**`, поэтому **любая** правка
|
||||
фронтенда делает его устаревшим. Выборка «по diff и AC» здесь не работает — diff
|
||||
всегда попадает, и решать нечего. Цена пропуска измерена: скриншоты не
|
||||
пересняли в #230 и #234, и `dev` стоял с красным job `docs`, пока это не нашли
|
||||
при следующей задаче (#237). Пересъёмка — `npm run build && node
|
||||
demo/docs/capture.mjs`, коммит вместе с задачей.
|
||||
|
||||
**Перф-смок в Validate зависит от диффа** (#473). Два glow-профиля
|
||||
гоняются всегда; при правке `src/iso-*` добавляется `large-house-isometric-v1`,
|
||||
при правке `src/live-*`, `src/render-*`, `houseplan-render-lifecycle.ts`,
|
||||
`houseplan-card.ts` — `large-house-interaction-v1`, оба по три образца против
|
||||
абсолютных потолков `hardMaxMs` полных профилей (`budgets-*-smoke.json`).
|
||||
Это гейт на «в разы», а не «на проценты»: регрессия #160 (первый кадр 9 870 мс
|
||||
против потолка 3 500) ловится ещё в ревью, а не предрелизным гейтом под тегом.
|
||||
Классификацию делает `scripts/classify-changes.mjs`, набор профилей входит в
|
||||
ключ переиспользования `performance_smoke`. Ревьюер по-прежнему принимает
|
||||
зелёный Validate на SHA как подтверждение дешёвых гейтов — смок его часть.
|
||||
|
||||
Условие честности такого сужения: ревьюер обязан перечислить, какие гейты прогнал,
|
||||
какие нет и почему. Непрогнанный гейт становится видимым решением, а не молчаливым
|
||||
пропуском.
|
||||
|
||||
**Одно число — один источник.** Любая величина, которую пользователь видит
|
||||
дважды — превью против записи, подпись против площади, подсветка инструмента
|
||||
против сохранённого значения, — обязана считаться в одном месте. Три дефекта
|
||||
подряд имели ровно эту причину: #234 (резинка показывала 12 см, запись хранила
|
||||
24), #233 (подпись мерила по осевым линиям, площадь рядом — по полу) и способ,
|
||||
которым #234 нашли (подсветка «Толщины» врала согласованно с записью). Ревьюер
|
||||
отвечает на вопрос прямо: какое число в этом диффе видно дважды и один ли у него
|
||||
источник. Механическая часть правила закреплена тестом
|
||||
`test/single-source-numbers.test.mjs` — строку с единицей измерения собирает
|
||||
только канонический форматтер; смысловая часть остаётся за ревью.
|
||||
|
||||
**Гейт беты** (условие закрытия issue): CI Validate зелёный на точном SHA тега.
|
||||
|
||||
Часть гейтов запускается только здесь, то есть **после** пройденного код-ревью.
|
||||
@@ -703,9 +500,7 @@ demo/docs/capture.mjs`, коммит вместе с задачей.
|
||||
достаточен для продолжения релиза, повторное код-ревью не требуется — §11.4.
|
||||
|
||||
**Гейт стабильного релиза:** полный локальный прогон плюс Validate и Full
|
||||
Performance зелёные на точном SHA, плюс зелёный E2E на реальном Home Assistant:
|
||||
`release.yml` сам запускает `e2e.yml` в `houseplan-e2e` на теге и ждёт его
|
||||
зелёного (#514); статусов issue не касается.
|
||||
Performance зелёные на точном SHA; статусов issue не касается.
|
||||
|
||||
---
|
||||
|
||||
@@ -822,11 +617,9 @@ Performance зелёные на точном SHA, плюс зелёный E2E н
|
||||
|
||||
1. трейлер `Issue: #NN` у каждого коммита класса A/B, допускается несколько;
|
||||
2. имя ветки `issue/NN-slug` соответствует трейлерам;
|
||||
3. у класса A есть ТЗ: раздел `## ТЗ` или хотя бы один `AC1` в теле issue —
|
||||
либо архивный `docs/specs/NN-*.md` у задачи до 2026-09-10. Офлайн тела нет,
|
||||
и проверка молчит; с `--issues` — предупреждение (настоящий рубеж — ревью ТЗ).
|
||||
Добавление нового файла в `docs/specs/**` тоже предупреждение: каталог
|
||||
заморожен (#517);
|
||||
3. для класса A существует `docs/specs/NN-*.md` — **или** issue помечен `small`.
|
||||
Офлайн это предупреждение: лёгкий трек держит ТЗ в теле issue, и без чтения
|
||||
меток «ТЗ в issue» неотличимо от «ТЗ не написано». С `--issues` — отказ;
|
||||
4. `User-Visible: yes` → правки в обоих changelog в том же коммите;
|
||||
5. коммит только класса D невалиден без `Release: vX.Y.Z` либо
|
||||
`Baseline-Reviewed: <ссылка на прогон CI>`;
|
||||
@@ -893,8 +686,8 @@ S7-code-review → код-ревью → слияние в dev → S8-merged л
|
||||
```
|
||||
|
||||
Ревьюер — `anthropics/claude-code-action`. Он читает `docs/SCOPE.md`, `AGENTS.md`,
|
||||
этот документ и тело issue, публикует разбор комментарием, заводит issue на Medium-находки
|
||||
вне скоупа задачи (#202), кладёт документ в `docs/reviews/` ветки задачи и возвращает вердикт
|
||||
этот документ и тело issue, публикует разбор комментарием, заводит issue на каждую
|
||||
Medium-находку, кладёт документ в `docs/reviews/` ветки задачи и возвращает вердикт
|
||||
структурированным JSON. **Метку переставляет отдельный детерминированный шаг по
|
||||
вердикту, а не модель.**
|
||||
|
||||
@@ -910,42 +703,12 @@ S7-code-review → код-ревью → слияние в dev → S8-merged л
|
||||
4. многострочный текст внутри `run:` — только через heredoc: строка с нулевым
|
||||
отступом обрывает блок YAML, и скрипт обрезается без ошибки парсера.
|
||||
|
||||
**Ревью не начинается на красном коде** (#510). После фиксации материала конвейер
|
||||
запускает Validate с мутантами по диффу на этом SHA (`scripts/validate-gate.mjs`:
|
||||
`workflow_dispatch validate.yml -f mutants=true`) и ждёт его до 45 минут. Красный или
|
||||
пропавший прогон возвращает задачу в `S6-in-progress` с комментарием и ссылкой —
|
||||
код никто не читал, цикл ревью не израсходован. Мутанты по диффу вообще бегут
|
||||
только по запросу: на кандидате ревью, кандидате слияния (#492), кандидате беты,
|
||||
в ночном прогоне и на PR; обычный push обходится дешёвыми гейтами (~3 минуты).
|
||||
За 08–09.09 мутанты на каждом промежуточном пуше стоили 48 из 56 часов
|
||||
job-минут Validate и в основном отменялись следующим пушем.
|
||||
|
||||
Каждый раунд ревью платит только за то, что в нём изменилось (#518). Свидетель
|
||||
судится по **области своего якоря** — строкам патча плюс сорок строк с каждой
|
||||
стороны (`ANCHOR_RADIUS_LINES`): и в отпечатке журнала (#481), и в отборе по
|
||||
диффу, который читает ханки `git diff --unified=0`. Сторона гарда осталась
|
||||
файловой: у гарда якоря нет. Неоднозначный якорь и непрочитанные ханки дают
|
||||
прежний широкий ответ — незнание не доказательство. Приближение того же класса,
|
||||
что и сам отбор по диффу; нижняя граница — ночной полный гейт (#513). Шард
|
||||
считает свой план до установки окружения и при пустом плане не платит за
|
||||
npm ci, Python и Chromium, оставаясь исполненной job: доказательство гейта
|
||||
требует успешной job, а не пропущенной.
|
||||
|
||||
**Один хендофф — один пуш.** Перед пушем — локальный `node scripts/process-gate.mjs
|
||||
--issues` при доступном `gh` (хук без `gh` статус issue не проверяет и молчит);
|
||||
после `S7-code-review` в ветку не пушить, пока не пришёл вердикт или возврат: пуш
|
||||
поверх идущего ревью отменяет его и стоит 10–20 минут раннера, а после фиксации
|
||||
материала — ещё и слияние (#312). `S7` ставится один раз на заход, не после
|
||||
каждого фикса CI: красный Validate конвейер вернёт сам.
|
||||
|
||||
**Автор обязан дождаться вердикта, а не заканчивать сессию.** Ревью идёт от десяти
|
||||
минут до сорока пяти. Отчёт «передал на ревью» останавливает конвейер там, где он
|
||||
мог идти сам: вердикт придёт, а подхватить его будет некому. У агента нет часов —
|
||||
он существует только в момент своего хода, поэтому ожидание это опрос: раз в 90
|
||||
секунд, не более 30 попыток — `node scripts/wait-verdict.mjs --issue NN` делает его
|
||||
детерминированно и говорит только при смене состояния (#496). Смотреть на метку, а
|
||||
не на комментарий: метка и есть состояние. При `blocked` не ждать — задача ждёт
|
||||
владельца.
|
||||
секунд, не более 30 попыток. Смотреть на метку, а не на комментарий: метка и есть
|
||||
состояние. При `blocked` не ждать — задача ждёт владельца.
|
||||
|
||||
**После прогона ревью метка меняется всегда.** Инвариант появился не сразу: первая
|
||||
редакция при конфликте слияния оставляла метку на месте, и это оказалось тупиком —
|
||||
@@ -954,51 +717,6 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
|
||||
никто не может выйти и о котором никто не узнает, для конвейера хуже громкой
|
||||
ошибки.
|
||||
|
||||
**Ветка приводится к `dev` до ревью, а не после** (#257). Раньше ревью читало ветку
|
||||
как есть, а слияние делало ребейз — проверенный SHA и слитый SHA были разными
|
||||
коммитами. Пока расхождение с `dev` текстовое, ребейз упирается в конфликт и это
|
||||
видно; смысловое расхождение git склеивает молча, и в `dev` уезжает комбинация,
|
||||
которую ревьюер не читал. Именно так пришёл регресс #234. Шаг перед ревью делает
|
||||
одно из трёх:
|
||||
|
||||
- ветка уже содержит весь `dev` — ничего;
|
||||
- отстала и ребейзится чисто — ребейз, `push --force-with-lease`, ревью по
|
||||
приведённому состоянию. Факт ребейза передаётся в промпт, чтобы сработало
|
||||
правило §7.2 о полном разборе вместо дельты;
|
||||
- конфликт — возврат в `S6-in-progress` **до** запуска ревью. Цикл при этом не
|
||||
расходуется: код никто не читал, вердикта нет.
|
||||
|
||||
Проверка стоит до ревью не только ради совпадения SHA. Конфликт всё равно вернул бы
|
||||
задачу, но обнаруживался он после сорока пяти минут работы ревьюера и потраченных
|
||||
лимитов подписки, хотя виден за пять секунд до них.
|
||||
|
||||
`--force-with-lease` здесь обязателен с явным ожидаемым значением: между чтением
|
||||
ветки и пушем автор мог запушить коммит, и слепой `--force` потерял бы его молча.
|
||||
Расхождение lease — падение прогона, а не предупреждение.
|
||||
|
||||
**В `dev` уезжает точный кандидат, и только проверенный** (#492,
|
||||
`scripts/merge-candidate.mjs`). Ревью длится десятки минут, `dev` за это время
|
||||
двигается; ребейз после вердикта даёт дерево, которого никто не видел, — а чистый
|
||||
ребейз ничего не доказывает: соседняя правка в `dev` меняет поведение без единого
|
||||
конфликта. Шаг слияния поэтому:
|
||||
|
||||
- сверяет вершину ветки с материалом ревью (#312) — иначе `S6-in-progress`;
|
||||
- если `dev` не двигался — push с `--force-with-lease` на текущую вершину;
|
||||
- если двигался — ребейз (конфликт — `S6-in-progress`, как раньше), сравнение
|
||||
patch-id проверенного и получившегося диффа (различие — `S7-code-review`: вердикт
|
||||
к другому диффу не применим, §7.2), публикация кандидата в ветку задачи, запуск
|
||||
Validate с мутантами на ней (#510) и ожидание зелёного dispatch-прогона **на этом
|
||||
SHA** — push-прогон мутантов не несёт — и только затем push в `dev` с lease на ту
|
||||
вершину, поверх которой кандидат собран. Отклонённый lease — `dev` двинулся снова
|
||||
— новая попытка; после третьей — `S6-in-progress` с комментарием;
|
||||
- красный Validate на кандидате или прогон, не появившийся за три минуты, —
|
||||
`S6-in-progress` с ссылкой; `S8-merged` ставится только после push.
|
||||
|
||||
Проверка кандидата — обычный Validate ветки: лёгкий набор плюс диффозависимые
|
||||
гейты. Тяжёлые гейты остаются за кандидатом релиза (#479): слияние не превращает
|
||||
каждое движение `dev` в двадцатиминутный прогон, а проверяет ровно то, что
|
||||
проверил бы пуш той же дельты.
|
||||
|
||||
Поэтому зелёное код-ревью с неудавшимся слиянием ведёт не в `S8-merged`, а в
|
||||
`S6-in-progress`: работа действительно вернулась к автору, только осталась не
|
||||
правка кода, а ребейз. Вердикт при этом в силе, переделывать нечего. После ребейза
|
||||
@@ -1008,18 +726,6 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
|
||||
Если метка не сменилась, значит упал сам прогон, а не работа: смотреть логи и
|
||||
сообщать владельцу, а не продолжать опрос.
|
||||
|
||||
**Конвейер — идемпотентный контроллер, а событие лишь будит его** (#499). Guard
|
||||
читает метки issue текущими, а не из снимка события: прогон мог простоять в очереди,
|
||||
пока владелец снял метку — отозванный запрос не исполняется, и комментария об этом
|
||||
нет. Конвейер запускают только `S4-spec-review` и `S7-code-review`; остальные метки
|
||||
не создают ни одной job и не входят в concurrency-группу issue — прежде любая
|
||||
посторонняя метка вытесняла ожидающий запуск ревью. Зелёный вердикт применяется
|
||||
повторно **без вызова модели**, если последний документ этапа несёт записанный
|
||||
конвейером вердикт `green` с High 0 и дерево материала не изменилось ни в одном
|
||||
файле вне `docs/reviews/**` (сравнивает `git diff` по содержимому). Ребейз, правка
|
||||
теста, фикстуры или ТЗ дают отличие дерева и полный разбор — правило §7.2 не
|
||||
ослабляется, оно просто не касается дерева, которое уже читали.
|
||||
|
||||
Цикл считается **по этапу**: вердикт по ТЗ не расходует бюджет код-ревью. Раньше
|
||||
считались все вердикты подряд, и первое код-ревью #89 получило `r2/4`.
|
||||
|
||||
@@ -1105,8 +811,7 @@ Golden, браузерные смоки, performance и полный HA-харн
|
||||
- принятие golden-эталонов ради зелёного CI или по частичному артефакту;
|
||||
- закрытие issue до выпуска беты с зелёным CI;
|
||||
- переоткрытие закрытого issue вместо нового бага;
|
||||
- Medium-находки, оставленные как TODO в документе ревью: в скоупе — чинятся
|
||||
в текущем issue, вне скоупа — становятся отдельным (#202);
|
||||
- Medium-находки, оставленные как TODO в документе ревью;
|
||||
- **параллельные бэклоги** в файлах (`BACKLOG-*.md`, «планы» в docs);
|
||||
- ревью-документы вне репозитория;
|
||||
- попутные правки «раз уж я здесь»;
|
||||
@@ -1125,9 +830,9 @@ Golden, браузерные смоки, performance и полный HA-харн
|
||||
|
||||
1. ✅ **Метки созданы, бэклог размечен.** У всех открытых issue владельца ровно
|
||||
одна `S*`-метка, инварианты чистые.
|
||||
2. ✅ **Колонка «Статус ТЗ» убрана** — вместе со всем индексом: `docs/specs/`
|
||||
стал архивом, ТЗ переехало в тело issue (#517, 2026-09-10). Перенос старых
|
||||
документов ревью в `docs/reviews/` отменён: они описывают код, которого уже нет.
|
||||
2. ⏳ **Колонку «Статус ТЗ» из `docs/specs/README.md` убрать** — не сделано, §7.3
|
||||
п.1. Перенос старых документов ревью в `docs/reviews/` отменён: они описывают
|
||||
код, которого уже нет.
|
||||
3. ✅ **Гейт написан** — `scripts/process-gate.mjs` плюс job в `validate.yml`,
|
||||
issue #105. Прошёл **вне** флоу как инфраструктурная задача (§1, issue #118), а
|
||||
не через ТЗ и ревью, как предполагала прежняя редакция этого пункта.
|
||||
|
||||
@@ -1,205 +1,379 @@
|
||||
# 🏠 House Plan — a live home map for Home Assistant
|
||||
# 🏠 House Plan — interactive floor plan card for Home Assistant
|
||||
|
||||
[](https://github.com/hacs/integration)
|
||||
[](https://github.com/hacs/integration)
|
||||
[](https://github.com/Matysh/houseplan-card/releases)
|
||||
[](https://github.com/Matysh/houseplan-card/stargazers)
|
||||
[](https://github.com/Matysh/houseplan-card/actions)
|
||||
[](LICENSE)
|
||||
[](https://demo.houseplan.tech)
|
||||
[](https://t.me/ha_houseplan)
|
||||
|
||||
📘 **[Full user guide](docs/USER-GUIDE.md)** · 🇷🇺 **[Русский](README.ru.md)** · 🗂 **[Project issues](https://github.com/Matysh/houseplan-card/issues)**
|
||||
**Turn Home Assistant into a live, interactive map of your home.** Upload or draw
|
||||
a floor plan, outline the rooms with your mouse — and every smart device appears
|
||||
in its real place: live states, tap-to-toggle lights, temperature and humidity per
|
||||
room, Zigbee signal maps, glowing light pools and a fullscreen kiosk mode for wall
|
||||
tablets. No YAML, no Inkscape, no external editors — the whole floorplan lives
|
||||
right on your Lovelace dashboard.
|
||||
|
||||
<!-- docs-section: overview -->
|
||||
> **Use a desktop computer to edit plans.** View and kiosk are fully supported
|
||||
> on phones and tablets. The editors are designed primarily for a desktop
|
||||
> browser with a mouse and keyboard; individual editing operations on touch
|
||||
> devices may be awkward, limited, or unavailable.
|
||||
|
||||
## Your whole home at a glance
|
||||

|
||||
|
||||
House Plan turns Home Assistant into a live map of your home. Open the dedicated
|
||||
**House Plan** item in the Home Assistant sidebar, upload a plan or draw rooms,
|
||||
bind them to Home Assistant areas, and
|
||||
the area's devices appear automatically. You can immediately see where a light
|
||||
is on, a door is open, a room is too cold, Zigbee signal is weak, or a leak
|
||||
sensor has fired.
|
||||
> ### 🚀 Try it live — no install needed
|
||||
> **[demo.houseplan.tech](https://demo.houseplan.tech)** — a real Home Assistant
|
||||
> with a ready-made plan. Log in as **`demo`** / **`demo`** and click anything:
|
||||
> toggle lights, open the editors, break things. The stand resets itself to a
|
||||
> pristine state every hour.
|
||||
|
||||

|
||||
🇷🇺 [Документация на русском](README.ru.md) · 💬 [Telegram chat: **@ha_houseplan**](https://t.me/ha_houseplan)
|
||||
|
||||
Setup is entirely graphical: no floor-plan YAML, Inkscape, or external editor.
|
||||
Plan data and device positions live on the Home Assistant server and stay in
|
||||
sync across screens.
|
||||
**Feature highlights**
|
||||
|
||||
> **Edit on a desktop computer.** View and kiosk are fully supported on phones
|
||||
> and tablets. The editors are designed primarily for a mouse and keyboard;
|
||||
> individual touch editing operations may be awkward or unavailable. See the
|
||||
> exact [touch support contract](docs/TOUCH-SUPPORT.md).
|
||||
- ♾️ **An infinite canvas** — there is no "plan size" and no edge to run
|
||||
past: draw and place devices anywhere, pan at any zoom, zoom out to see
|
||||
everything, and let one tap fit the whole plan back on screen.
|
||||
- 🖱 **GUI-first floorplan editor** — rooms, doors & windows, island rooms,
|
||||
virtual walls and a visual decor layer, all drawn with clicks; room resize
|
||||
by dragging walls, with live lengths and areas as you drag; smart
|
||||
alignment guides and a live ruler in real meters/feet.
|
||||
- 🖼 **A backdrop you can move and scale** — drag the floor-plan picture into
|
||||
place and pull a corner to size it, with its real size in metres shown as
|
||||
you drag, so the drawing and the photo of your plan finally line up.
|
||||
- 💡 **Lights toggle on click** out of the box; wall-switch markers can control
|
||||
whole groups of lights (works for dumb switches and stateless remotes too).
|
||||
- 🌒 **“Light sources” fill** — a dark house where every lit lamp lights exactly
|
||||
the floor it can see: through doorways and open boundaries, stopped by walls,
|
||||
columns and partitions, which cast real shadows.
|
||||
- ☀️ **The sun on the plan** — set the compass and the backdrop lives with
|
||||
the day (white noon → golden hour → deep night), while windows on exterior
|
||||
walls cast real wedges of sunlight into the rooms; optional cloud cover
|
||||
from a weather entity.
|
||||
- 🪟 **Curtains and blinds open on a tap** — one action opens, closes or
|
||||
stops a cover, and the icon itself morphs between open and closed while a
|
||||
soft ring pulses as it travels.
|
||||
- 🌡 **Room cards** with temperature, humidity, Zigbee LQI and light count;
|
||||
comfort-range temperature fills, per-room signal heatmap.
|
||||
- 🚪 **Doors, windows and locks** with contact sensors — unlocking is always an
|
||||
explicit button, never an accidental tap.
|
||||
- 📺 **Kiosk mode** for wall tablets and TVs: fullscreen, swipe between floors,
|
||||
auto-carousel, per-screen icon sizes.
|
||||
- 🤖 **Live robot vacuums** — the dock marker stays put while a round puck
|
||||
drives the plan in real time, pouring its path out from under itself;
|
||||
current and previous cleanup runs are recorded server-side. Calibration is
|
||||
one click (rooms matched by name) or a drag-and-stretch overlay. A diagnostic
|
||||
source picker also covers registry-less map cameras without silently
|
||||
rebinding broken sources. Works with Xiaomi Cloud Map Extractor, Tasshack
|
||||
dreame-vacuum and Valetudo.
|
||||
- 🔔 New devices appear automatically with a red “new” dot; the layout is stored
|
||||
**server-side** — one shared plan for every user and screen, synced live.
|
||||
|
||||
<!-- docs-section: features -->
|
||||
---
|
||||
|
||||
## What House Plan provides
|
||||
## What it is and why
|
||||
|
||||
- **Live state and safe actions.** Lights and other safe devices can toggle from
|
||||
the plan; a lock cannot be opened by an accidental plan tap.
|
||||
- **Three built-in editors.** Plan creates rooms, walls and openings; Device
|
||||
places and configures markers; Background adds lines, labels and furniture.
|
||||
- **Area-aware rooms.** New devices appear automatically, while room cards can
|
||||
show temperature, humidity, light state and average LQI.
|
||||
- **Light and environment.** Room fills, lamp Glow, wall shadows, a day-cycle
|
||||
backdrop and sunlight through windows.
|
||||
- **Doors, windows, gates and vacuums.** Openings follow real contacts and locks;
|
||||
a robot can show its position, dock and travelled path.
|
||||
- **Several floors and screens.** Space tabs, swipe navigation, local viewport,
|
||||
and a separate initial floor for each card.
|
||||
- **Wall-display kiosk.** A plan-only view with fullscreen navigation and icon
|
||||
sizes saved for that display.
|
||||
House Plan shows your smart home the way it actually looks — on a floor plan. Instead of long lists of entities, you see rooms and devices in their real places: where the leak is, what the temperature is in the kids' room, whether the light is on in the hallway, whether the gate is open.
|
||||
|
||||

|
||||
This is convenient when:
|
||||
|
||||
<!-- docs-section: first-run -->
|
||||
- you have many devices and lists are awkward to use;
|
||||
- you need to grasp the state of the house "at a glance";
|
||||
- you want to give access to family members — anyone can figure out a picture;
|
||||
- you want a beautiful overview screen for a wall-mounted tablet.
|
||||
|
||||
## Your first working room
|
||||
The integration consists of two parts that are installed together:
|
||||
|
||||
1. Install the integration and open **House Plan** in the Home Assistant sidebar.
|
||||
2. Create the first **space**: upload SVG/PNG/JPG/WebP, reuse an uploaded image,
|
||||
or choose no image and draw the plan by hand.
|
||||
3. In Plan, select **Room outline**, place vertices, and click the first point to
|
||||
close the outline.
|
||||
4. Name the room and bind it to a Home Assistant area. Use “No area” for a room
|
||||
that has no devices.
|
||||
5. Open Device: devices from the bound area are already placed; drag their
|
||||
markers to the correct positions.
|
||||
6. Optionally use Background for lines, text and furniture.
|
||||
7. Return to View. The plan now displays live state and accepts safe actions.
|
||||
- **the Lovelace card** `houseplan-card` — the interactive plan itself;
|
||||
- **the server-side component** — stores the room markup and icon positions in Home Assistant, so the plan is identical in all browsers and on all devices.
|
||||
|
||||

|
||||
---
|
||||
|
||||

|
||||
## How it differs from alternatives
|
||||
|
||||

|
||||
A house plan in Home Assistant is usually built with `picture-elements`,
|
||||
`ha-floorplan`, or newer GUI cards that draw walls and furniture in the
|
||||
dashboard. Those either lock you into YAML/SVG, or store the plan in the
|
||||
Lovelace card config. House Plan is a **shared live map** backed by a Home
|
||||
Assistant integration:
|
||||
|
||||

|
||||
| | House Plan | picture-elements / ha-floorplan | GUI draw cards (e.g. easy-floorplan) |
|
||||
|---|---|---|---|
|
||||
| **Setup** | Entirely through the UI, with the mouse | Manual YAML / Inkscape SVG | In-card drawing of walls & furniture |
|
||||
| **Adding devices** | Automatic, by HA **area** | You type every entity by hand | Place entities by hand on the drawing |
|
||||
| **Icon coordinates** | Drag with the mouse | Count pixels into YAML | Drag on the canvas |
|
||||
| **Room markup** | Built-in outline editor bound to areas | External SVG editor | Draw walls yourself (furniture CAD) |
|
||||
| **Storage** | On the HA server (`.storage`, shared, multi-client) | In the dashboard YAML | In the card / dashboard YAML |
|
||||
| **Overlays** | Glow, climate, LQI, sun, vacuums, kiosk | Whatever you script in SVG/CSS | Varies by card |
|
||||
| **Zoom** | Smooth vector zoom | Usually a fixed image | SVG / virtual canvas |
|
||||
|
||||

|
||||
**One sentence:** House Plan is the shared, area-aware live map of your home —
|
||||
not a general-purpose CAD package. Its Background editor covers practical
|
||||
decor, labels and furniture; if you need unrestricted architectural drafting,
|
||||
a draw-centric tool may fit better. With a plan and HA areas, House Plan keeps
|
||||
every tablet on the same live layout.
|
||||
|
||||
Every workflow and edge case is in the [full user guide](docs/USER-GUIDE.md).
|
||||
The [Background editor contract](docs/DECOR-EDITOR.md) and
|
||||
[vacuum guide](docs/VACUUM.md) are the authorities for those subsystems.
|
||||
Key advantages in short:
|
||||
|
||||
<!-- docs-section: installation -->
|
||||
- **No code at all.** Everything — spaces, rooms, devices — is configured with clicks.
|
||||
- **Automatic device placement.** Outline a room and bind it to a Home Assistant area — the devices of that area appear on the plan by themselves.
|
||||
- **Manual additions of your own.** Any device, group or even a "virtual" point can be placed on the plan manually, with a name, icon, model, link and an attached PDF manual.
|
||||
- **Live states.** Temperature, Zigbee signal strength, on/off, open/closed — everything updates in real time.
|
||||
Icon colors follow one principle — **yellow means the device is doing its main job right now**:
|
||||
a light is shining, a socket is powering, a fan is spinning, a vacuum is
|
||||
cleaning, a radiator valve is actually heating (not merely enabled). For climate integrations,
|
||||
a reported work action is authoritative; when an integration exposes only its enabled HVAC mode,
|
||||
that mode is the best available fallback. Orange = open / unlocked.
|
||||
A pulsing red ring = an emergency (leak, smoke, gas). An RGB bulb's colour lives in its glow
|
||||
spot (glow fill), where the spot itself is the on/off indicator and the badge stays standard.
|
||||
A translucent icon = unavailable. Dark = idle.
|
||||
- **A coherent visual Background editor.** Draw lines/shapes, place labels and
|
||||
furniture, edit physical styles and transform every object with the same
|
||||
selection model. The plan image has its own move/resize/rotate tool, numeric
|
||||
properties and shared Undo/Redo.
|
||||
- **Crisp zoom.** Zooming in does not "blur" the picture: the plan, labels and icons remain vector-sharp at any scale.
|
||||
|
||||
---
|
||||
|
||||
## Wall tablet / TV (kiosk mode)
|
||||
|
||||
Add the card to a dedicated dashboard with a **panel view** and set `kiosk: true`
|
||||
(or tick "Wall device (kiosk) mode" in the card editor):
|
||||
|
||||
```yaml
|
||||
type: custom:houseplan-card
|
||||
kiosk: true
|
||||
cycle: 0 # seconds between auto space switches, 0 = off (nice for TVs)
|
||||
```
|
||||
|
||||
No header, no editors — just the live plan. Swipe to change floors (at 1:1),
|
||||
pinch to zoom, double-tap to reset. Long-press an empty spot for 3 seconds to
|
||||
tune icon and text sizes for THIS screen (saved per device). To hide Home
|
||||
Assistant's own header use the companion app's kiosk settings or the
|
||||
[kiosk-mode](https://github.com/NemesisRE/kiosk-mode) plugin.
|
||||
|
||||
## Installation
|
||||
|
||||
### HACS
|
||||
One click if you already run HACS:
|
||||
|
||||
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=Matysh&repository=houseplan-card&category=integration)
|
||||
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=Matysh&repository=houseplan-card&category=integration)
|
||||
|
||||
House Plan is in the HACS default catalog — no custom repository needed.
|
||||
|
||||
1. In HACS search for **House Plan** and install it.
|
||||
### Via HACS (recommended)
|
||||
|
||||
1. Open **HACS → menu (⋮) → Custom repositories**.
|
||||
2. Paste the URL of this repository, set the category to **Integration**, and click **Add**.
|
||||
3. Find **House Plan** in the list, install it and **restart Home Assistant**.
|
||||
4. Go to **Settings → Devices & Services → Add integration** and select **House Plan**.
|
||||
|
||||
The card is registered automatically — no need to add a Lovelace resource manually.
|
||||
|
||||
> **Card doesn't load (`Custom element doesn't exist: houseplan-card`) or you manage Lovelace
|
||||
> resources in YAML?** Add the resource manually pointing at the URL the integration *serves*:
|
||||
>
|
||||
> ```yaml
|
||||
> resources:
|
||||
> - url: /houseplan_files/houseplan-card.js
|
||||
> type: module
|
||||
> ```
|
||||
>
|
||||
> Do **not** use `/custom_components/houseplan/frontend/houseplan-card.js` — that is the file
|
||||
> on disk, which Home Assistant does not serve over HTTP (you'll get a `text/plain` MIME error
|
||||
> and the element never registers). The correct, integration-served URL is
|
||||
> `/houseplan_files/houseplan-card.js`. Both cards (`houseplan-card` and
|
||||
> `houseplan-space-card`) ship in that one file — no separate resource is needed.
|
||||
|
||||
### Manually
|
||||
|
||||
1. Copy the `custom_components/houseplan` folder into the `config/custom_components` directory of your Home Assistant.
|
||||
2. Restart Home Assistant.
|
||||
3. Open **Settings → Devices & services → Add integration → House Plan**.
|
||||
3. Add the integration: **Settings → Devices & Services → Add integration → House Plan**.
|
||||
|
||||
The sidebar page and optional dashboard cards are registered automatically.
|
||||
After installing or updating House Plan,
|
||||
restart Home Assistant and fully reload the page: `Ctrl+F5` on Windows/Linux or
|
||||
`Cmd+Shift+R` on macOS.
|
||||
### Adding a plan screen
|
||||
|
||||
#### Storage mode (Home Assistant default)
|
||||
|
||||
No YAML is normally needed. If automatic registration did not make the card
|
||||
available, open **Settings → Dashboards → menu ⋮ → Resources → Add
|
||||
resource**, enter `/houseplan_files/houseplan-card.js`, and select **JavaScript
|
||||
module**.
|
||||
|
||||
#### YAML resources mode (Home Assistant 2026.2+)
|
||||
|
||||
To manage resources in `configuration.yaml` independently of the dashboard
|
||||
mode, use:
|
||||
|
||||
```yaml
|
||||
lovelace:
|
||||
resource_mode: yaml
|
||||
resources:
|
||||
- url: /houseplan_files/houseplan-card.js
|
||||
type: module
|
||||
```
|
||||
|
||||
#### Legacy Home Assistant 2024.6–2026.1
|
||||
|
||||
Only for a full-YAML dashboard that is already managed in YAML, use:
|
||||
|
||||
```yaml
|
||||
lovelace:
|
||||
mode: yaml
|
||||
resources:
|
||||
- url: /houseplan_files/houseplan-card.js
|
||||
type: module
|
||||
```
|
||||
|
||||
`mode: yaml` changes the dashboard itself to YAML mode. Do not switch a storage
|
||||
dashboard to legacy YAML just for House Plan; use the Storage mode instructions
|
||||
above instead. Do not use the on-disk path inside `custom_components`; Home
|
||||
Assistant does not serve that path as a JavaScript module.
|
||||
|
||||
### Manual installation
|
||||
|
||||
Copy the complete `custom_components/houseplan` release folder to
|
||||
`config/custom_components`, restart Home Assistant, and add the House Plan
|
||||
integration. Do not copy only `houseplan-card.js`: the card also uses an
|
||||
internal manifest and content-hashed modules from the same release.
|
||||
|
||||
### Open House Plan
|
||||
|
||||
After the integration is added, open **House Plan** in the Home Assistant
|
||||
sidebar. This full-page view is the primary entry point and needs no dashboard
|
||||
or YAML setup. It remembers the last space but always returns from another HA
|
||||
page in View rather than reopening an editor. Users without editing permission
|
||||
see the same live plan without editor controls.
|
||||
|
||||
If the sidebar entry cannot be registered, the integration and existing
|
||||
dashboard cards continue to work; check **Settings → System → Repairs → System
|
||||
information → House Plan** after restarting and hard-refreshing HA.
|
||||
|
||||
### Optional dashboard card
|
||||
|
||||
Add the card only when House Plan must be embedded in a dashboard. In a Sections
|
||||
view it requests full width by default, while a manual size chosen in HA remains
|
||||
authoritative. Add it in the UI or as:
|
||||
Create a new dashboard tab (a "Panel" view works best) and add the card:
|
||||
|
||||
```yaml
|
||||
type: custom:houseplan-card
|
||||
title: House plan
|
||||
```
|
||||
|
||||
Different screens may start on different spaces:
|
||||
Nothing else needs to be specified — everything else is configured right on the screen.
|
||||
|
||||
```yaml
|
||||
type: custom:houseplan-card
|
||||
default_floor: ground
|
||||
```
|
||||
---
|
||||
|
||||
All cards share server-side rooms and coordinates. Current mode, viewport and
|
||||
selected space remain local to the screen. Revision checks and live sync cover
|
||||
concurrent clients, but avoid editing the same object in two browsers at once.
|
||||
## How to use
|
||||
|
||||
## Detailed documentation
|
||||
### Step 1. Add a space (floor)
|
||||
|
||||
- [Full user guide](docs/USER-GUIDE.md)
|
||||
- [Mouse/touch/keyboard matrix](docs/USER-GUIDE.md#6-navigation-zoom-and-input)
|
||||
- [Plan tools](docs/USER-GUIDE.md#plan-tools-at-a-glance)
|
||||
- [Background editor](docs/DECOR-EDITOR.md)
|
||||
- [Robot vacuums](docs/VACUUM.md)
|
||||
- [Touch support](docs/TOUCH-SUPPORT.md)
|
||||
On first open the plan is still empty — House Plan immediately offers to create the first space.
|
||||
If your Home Assistant already has **floors** configured, a wizard offers to create a space
|
||||
for each floor (names prefilled, a plan image is asked for one by one; any floor can be skipped).
|
||||
|
||||
<!-- docs-section: support -->
|
||||

|
||||
|
||||
## Support and feedback
|
||||
In the dialog, set a **name** (for example, "1st floor") and pick the background: **upload** a floor-plan image (SVG, PNG, JPG, WebP), **choose one already uploaded** to the server earlier, or select **"no background, I'll draw the rooms"** for a hand-drawn space. The canvas is infinite; an image keeps its own proportions by default and can be moved, resized or rotated at any time in the Background editor.
|
||||
|
||||
- Questions and plan examples: [Telegram @ha_houseplan](https://t.me/ha_houseplan).
|
||||
- Bugs and proposals: [GitHub Issues](https://github.com/Matysh/houseplan-card/issues).
|
||||
- Before reporting, update House Plan, restart HA and hard-refresh the page.
|
||||
Include the version, browser, logs and reproduction steps; private entity IDs
|
||||
may be replaced with fictional ones.
|
||||

|
||||
|
||||
Documentation screenshots are produced by the reproducible
|
||||
`npm run build && node demo/docs/capture.mjs` command using synthetic data only. Scenario version,
|
||||
source fingerprint and every image hash are recorded in the
|
||||
[screenshot index](docs/images/screenshots.json).
|
||||
> 💡 You can draw the background in any floor planner (for example, REMPLANNER) or photograph a paper plan. SVG works best — it stays crisp when zoomed in.
|
||||
|
||||
License: [MIT](LICENSE).
|
||||
Later you can add as many spaces as you like (floors, yard, garage) with the **+** button next to the tabs.
|
||||
|
||||
### Step 2. Outline the rooms
|
||||
|
||||
After the first space is added, the card switches to the **Plan** tab by itself. The card has three mode tabs in the header — **View** (default: display and device control only, nothing can be moved or edited), **Plan** (rooms, openings, labels, space settings) and **Devices** (placing and configuring markers); the edit tabs are shown to administrators. In Plan, click grid points, connecting them with lines, and close the room outline by clicking the first point.
|
||||
|
||||
As soon as the outline is closed, the room-save dialog appears. Here you need to **bind the room to a Home Assistant area** — this is exactly what enables the automation. For utility rooms with no devices (hall, sauna) there is a **"No area"** button.
|
||||
|
||||

|
||||
|
||||
While drawing, a ruler follows the cursor showing the current segment's real length (metres, or feet + inches on an imperial Home Assistant). The scale is set per space — the **"Scale (grid cell size)"** field in the space dialog says how many centimetres one grid cell represents (default 5 cm).
|
||||
|
||||
Rooms may not overlap: a click strictly inside an existing room, or an outline that would swallow one, is refused. Two more tools help you reshape the plan later:
|
||||
|
||||
- **Merge** — click a room, then a neighbour that shares a wall; they fuse into one. A dialog picks which name and area survive.
|
||||
- **Split** — click a room, then two points on its walls; the chord cuts it in two. The bigger part stays the room it was (name, area, devices); the smaller one asks for a new name and area.
|
||||
|
||||
|
||||
### Doors, windows, gates and locks
|
||||
|
||||
In markup mode the **"Opening"** tool places doors, windows and gates: click next to a wall and the
|
||||
opening snaps onto it. Pick the type, the **length in real centimetres** (defaults: door 90 cm,
|
||||
window 120 cm, gate 300 cm), an open/close sensor and — for doors and gates — a **lock entity**.
|
||||
|
||||
With a sensor bound, the plan comes alive: the door leaf swings on its hinge and the swing arc
|
||||
draws itself in as the real door opens; a window opens its two casements. While open, the moving
|
||||
parts take an accent colour. A gate keeps a 3–4 m opening compact on the plan: two half-width
|
||||
leaves open only 10° outwards, without a full-width swing arc, while contact, lock and light
|
||||
passage work exactly like a door. A door or gate with a lock shows a padlock badge next to it — green when
|
||||
locked, orange when unlocked. For safety the lock can **not** be toggled from the plan; a click
|
||||
on the opening shows a status card with both states instead.
|
||||
|
||||
Openings are easy to adjust later: hovering one highlights it, you can **drag it along the
|
||||
walls** (it slides around corners too), and a **double click opens its properties**.
|
||||
|
||||
### Step 3. Devices appear by themselves
|
||||
|
||||
As soon as you save a room bound to an area, **the devices of that area are automatically laid out inside the outline**. These are the same devices shown on the **Settings → Devices → (filtered by the room)** page — only the meaningful ones, without service records, bridges and duplicates.
|
||||
|
||||
By default only meaningful devices make it onto the plan: non-physical ones (service records, bridges, scenes, individual lamps folded into a light group) arrive with the **"Hide device from plan"** checkbox already ticked. The checkbox is yours from then on — every device dialog has it, virtual devices included. To see and un-hide them, open the device editor and press **"Hidden and disabled"**: user-hidden devices appear as translucent blue ghosts, a click opens the dialog. Hidden devices still count toward the room's Zigbee signal, but cast no light. A device disabled in Home Assistant appears there as a labelled grey service ghost and is excluded from all plan data/actions until it is enabled in HA again.
|
||||
|
||||
From here on you can just use the plan: clicking an icon opens the device card with the model, link and a button to jump into Home Assistant.
|
||||
|
||||

|
||||
|
||||
### Step 4. Zoom
|
||||
|
||||
The mouse wheel or the **- / ⊹ / +** buttons zoom the plan in and out; on a touch screen the two-finger pinch works. Zoomed out you see the whole plan, zoomed in you see the details, and everything stays crisp. The zoom level is remembered separately for each space.
|
||||
|
||||

|
||||
|
||||
### Step 5. Put the icons in their places
|
||||
|
||||
Switch to the **Devices** tab to arrange icons: drag them with the mouse, click one to open its editor. In **View** mode nothing can be moved — panning the map never displaces a sensor (a top user request). Positions are saved on the server and are identical in all browsers and devices. The **↺** button restores the automatic layout.
|
||||
|
||||

|
||||
|
||||
### Tap actions: control devices from the plan
|
||||
|
||||
By default a tap on an icon opens its info card. A device can instead use the
|
||||
universal **Toggle state** action. Its editor shows the exact entity or configured
|
||||
group, the current state and what the next tap will do; when nothing can be toggled,
|
||||
it says so and the tap is a quiet no-op rather than an unexpected info-card fallback.
|
||||
Lights keep their convenient toggle default. Covers and valves use open/close/stop
|
||||
semantics automatically, while locks, alarm panels and secure garage/door/gate covers
|
||||
remain blocked. An exact entity binding never falls through to a sibling switch, and
|
||||
temporarily unavailable group members are skipped without erasing the configuration.
|
||||
A **long press** still opens the info card and right-click still opens HA more-info.
|
||||
|
||||
### Icon rules
|
||||
|
||||
Which MDI icon a device gets is decided by **icon rules** — editable right in the card
|
||||
(the ⬡ button in the header): an ordered list of “name pattern → icon” regexes with a
|
||||
live test field, bilingual defaults (EN/RU) and a one-click reset. When no rule
|
||||
matches, the entity *device class* decides (thermometer for temperature sensors, etc.).
|
||||
|
||||
### Step 6. Adding your own devices manually
|
||||
|
||||
You can also place a **single entity** (not just a whole device): start typing in the binding search and individual entities appear next to devices — handy when one device exposes several values (e.g. temperature and humidity) and you want each as its own icon.
|
||||
|
||||
|
||||
Not everything has to be left to the automation. With the **+** button in the header you can place any device, group or a **virtual point** on the plan (for example, an "Inlet valve" that does not exist as a device). Set a name, icon, model, link, description and, if you wish, attach a **PDF manual**.
|
||||
|
||||
To represent a dumb physical lamp controlled by a smart relay, place a virtual
|
||||
point where the lamp really is and set **Light source → Always**. Manual colour,
|
||||
brightness and radius stay available even though the point has no HA entity.
|
||||
Then open the relay and add that plan source under **Controls other light
|
||||
sources**. The relay continues to show the aggregate working state, while Glow,
|
||||
room fill and statistics belong to the lamp's position. An unlinked passive
|
||||
Always source is deliberately constant-on. With several own `light.*`/`switch.*`
|
||||
entities, Always also offers a leading-entity selector; a missing saved choice
|
||||
is warned about and retained while a deterministic fallback is used.
|
||||
|
||||
To make that virtual lamp manually switchable without creating a Home
|
||||
Assistant helper, also choose **Tap action → Toggle state** on the lamp itself.
|
||||
This exact combination — virtual binding, **Light source → Always**, and
|
||||
**Toggle state** — stores a shared on/off state in the House Plan integration.
|
||||
It survives page reloads and Home Assistant restarts and updates Glow, room
|
||||
fill/statistics, full cards and `houseplan-space-card` together. Any signed-in
|
||||
dashboard viewer may toggle it. While this manual mode is active, saved
|
||||
**Controls other light sources** remain intact but are not called; changing the
|
||||
role, binding or tap action restores their normal behaviour. This operational
|
||||
state is deliberately not part of plan exports or Home Assistant entities.
|
||||
|
||||
The same dialog controls how the device looks on the plan. **Display** switches between the
|
||||
icon badge, an animated **presence ripple** (pulsing rings while the entity is active, a faint
|
||||
dot when idle — great for motion sensors) or both, with a per-device ring colour and size. The
|
||||
**icon size** (×0.5–3) and **rotation** are also per-device, so a wall valve can be small and
|
||||
turned the way it is mounted.
|
||||
|
||||

|
||||
|
||||
### Styling the plan with card-mod (advanced, unsupported)
|
||||
|
||||
The card ships finished and has no CSS field of its own — but if you already run [card-mod](https://github.com/thomasloven/lovelace-card-mod), every object on the plan now carries a stable hook you can aim at: `data-hp="device"` (plus `data-entity`, `data-area`), `data-hp="room"`, `data-hp="opening"`, `data-hp="decor"`, `data-hp="room-label"`, `data-hp="space-tab"`. We promise not to rename them; we do not ship card-mod, do not support it, and are not responsible for what your CSS does to the card. The full table, the examples and the limits are in **[docs/STYLING-HOOKS.md](docs/STYLING-HOOKS.md)**.
|
||||
|
||||
---
|
||||
|
||||
## Uninstalling
|
||||
|
||||
1. Remove the card (or the tab with the plan) from the dashboard.
|
||||
2. **Settings → Devices & Services → House Plan → Delete** the integration entry.
|
||||
3. Remove the integration from **HACS** (or delete the `custom_components/houseplan` folder if installed manually) and restart Home Assistant.
|
||||
4. Optionally delete the saved plan data: the `config/houseplan/` files (backgrounds and attachments) and the `houseplan.config` / `houseplan.layout` entries in the `config/.storage` directory.
|
||||
|
||||
---
|
||||
|
||||
## Getting help & sharing your plan
|
||||
|
||||
- 💬 **[Telegram chat — @ha_houseplan](https://t.me/ha_houseplan)** — questions,
|
||||
setup help, feature ideas, and screenshots of your plans. The fastest way to
|
||||
reach the author and other users.
|
||||
- 🐞 [GitHub issues](https://github.com/Matysh/houseplan-card/issues) — bug
|
||||
reports and feature requests (please attach your House Plan version).
|
||||
- 💡 [GitHub discussions](https://github.com/Matysh/houseplan-card/discussions) —
|
||||
longer-form ideas.
|
||||
- 📜 [Changelog](docs/CHANGELOG.md) — what changed in every version
|
||||
([на русском](docs/CHANGELOG.ru.md)).
|
||||
|
||||
When reporting a problem, the version number helps a lot: it is shown in the
|
||||
browser console on load (`HOUSEPLAN-CARD vX.Y.Z`) and in **Settings → Devices &
|
||||
Services → House Plan**.
|
||||
|
||||
---
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
**Do I need to write anything in YAML?** No. The only line is adding the card to the dashboard; everything else is done with the mouse.
|
||||
|
||||
**My devices did not appear on the plan.** A device appears only if its Home Assistant area is bound to a drawn room. Check that the device has a room assigned (Settings → Devices) and that the room is outlined and bound to that area. Open the device editor and press **"Hidden and disabled"**: a blue ghost is user-hidden and can be shown; a grey disabled ghost must first be enabled in Home Assistant.
|
||||
|
||||
**Can I hide an unwanted device or rename it?** Yes — click the device on the plan and press "Edit" in its card: there you can change the name, icon, model or hide the icon.
|
||||
|
||||
**Is the data stored in the cloud?** No. Everything is stored locally in your Home Assistant.
|
||||
|
||||
---
|
||||
|
||||
<p align="center"><sub>Screenshots were taken on a real Home Assistant configuration.</sub></p>
|
||||
|
||||
@@ -1,210 +1,351 @@
|
||||
# 🏠 House Plan — живой план дома для Home Assistant
|
||||
# 🏠 House Plan — интерактивный поэтажный план дома для Home Assistant
|
||||
|
||||
[](https://github.com/hacs/integration)
|
||||
[](https://github.com/hacs/integration)
|
||||
[](https://github.com/Matysh/houseplan-card/releases)
|
||||
[](https://github.com/Matysh/houseplan-card/actions)
|
||||
[](https://github.com/Matysh/houseplan-card/stargazers)
|
||||
[](https://demo.houseplan.tech)
|
||||
[](https://t.me/ha_houseplan)
|
||||
|
||||
📘 **[Полное руководство](docs/USER-GUIDE.ru.md)** · 🇬🇧 **[English](README.md)** · 🗂 **[Задачи проекта](https://github.com/Matysh/houseplan-card/issues)**
|
||||
📘 **[Полное руководство пользователя](docs/USER-GUIDE.ru.md)** · 🗂 **[Беклог проекта](https://github.com/users/Matysh/projects/1)**
|
||||
|
||||
<!-- docs-section: overview -->
|
||||
**Превратите Home Assistant в живую интерактивную карту дома.** Загрузите или
|
||||
нарисуйте план этажа, обведите комнаты мышкой — и умные устройства появятся на
|
||||
своих местах: живые состояния, свет по клику, температура и влажность по
|
||||
комнатам, карта Zigbee-сигнала, светящиеся пятна ламп и полноэкранный
|
||||
киоск-режим для настенного планшета. Без YAML, без Inkscape и внешних
|
||||
редакторов — весь план настраивается прямо на дашборде.
|
||||
|
||||
## Дом целиком — одним взглядом
|
||||
> **Редактировать планы рекомендуется на компьютере.** Режим просмотра и
|
||||
> киоск полноценно поддерживаются на телефонах и планшетах. Редакторы рассчитаны
|
||||
> прежде всего на desktop с мышью и клавиатурой: на touch-устройстве отдельные
|
||||
> операции могут быть менее удобны, работать ограниченно или отсутствовать.
|
||||
|
||||
House Plan превращает Home Assistant в живую карту дома. Откройте отдельный
|
||||
пункт **House Plan** в боковом меню Home Assistant, загрузите изображение плана
|
||||
или нарисуйте комнаты, свяжите их с зонами Home
|
||||
Assistant — и устройства появятся на плане автоматически. Сразу видно, где
|
||||
горит свет, открыта дверь, слишком холодно, слабый Zigbee-сигнал или сработал
|
||||
датчик протечки.
|
||||

|
||||
|
||||

|
||||
> ### 🚀 Попробовать вживую — без установки
|
||||
> **[demo.houseplan.tech](https://demo.houseplan.tech)** — настоящий Home
|
||||
> Assistant с готовым планом. Вход **`demo`** / **`demo`**, можно нажимать всё:
|
||||
> включать свет, открывать редакторы, ломать что угодно. Каждый час стенд сам
|
||||
> возвращается в исходное состояние.
|
||||
|
||||
Настройка выполняется в графическом интерфейсе: без YAML-разметки, Inkscape и
|
||||
внешнего редактора плана. Данные плана и расположение устройств хранятся на
|
||||
сервере Home Assistant и синхронизируются между экранами.
|
||||
🇬🇧 [Documentation in English](README.md) · 💬 [Чат в Telegram: **@ha_houseplan**](https://t.me/ha_houseplan)
|
||||
|
||||
> **Редактируйте на компьютере.** Режим просмотра и киоск полноценно работают
|
||||
> на телефонах и планшетах. Редакторы рассчитаны прежде всего на мышь и
|
||||
> клавиатуру; на touch отдельные операции могут быть неудобны или недоступны.
|
||||
> Подробный контракт: [поддержка touch](docs/TOUCH-SUPPORT.md).
|
||||
**Главное**
|
||||
|
||||
<!-- docs-section: features -->
|
||||
- ♾️ **Бесконечный холст** — нет «размера плана» и нет края, за который
|
||||
нельзя выйти: рисуйте и ставьте устройства где угодно, тащите план на
|
||||
любом зуме, отдаляйтесь, чтобы увидеть всё, и одной кнопкой вписывайте
|
||||
план обратно в экран.
|
||||
- 🖱 **Редакторы прямо в карточке** — комнаты, двери, окна и ворота, комнаты-острова,
|
||||
виртуальные стены и декор-слой рисуются кликами; размеры комнат меняются
|
||||
перетаскиванием стен с живыми длинами и площадями; помощник выравнивания и
|
||||
линейка в реальных метрах.
|
||||
- 💡 **Свет переключается кликом** из коробки; значок выключателя может
|
||||
управлять группой ламп (в т.ч. «тупые» выключатели и кнопки-пульты).
|
||||
- 🌒 **Заливка «Свет по источникам»** — тёмный дом, где каждая горящая лампа
|
||||
освещает ровно тот пол, который видит: через проёмы и открытые границы,
|
||||
а стены, колонны и перегородки его не пропускают и дают настоящие тени.
|
||||
- ☀️ **Солнце на плане** — задайте компас, и фон живёт вместе с днём
|
||||
(белый полдень → золотой час → глубокая ночь), а окна внешних стен пускают
|
||||
в комнаты настоящие клинья солнечного света; облачность — опционально, от
|
||||
weather-сущности.
|
||||
- 🪟 **Шторы открываются тапом** — одно действие открывает, закрывает или
|
||||
останавливает штору, а сам значок морфится между открытым и закрытым
|
||||
видом и мягко пульсирует кольцом, пока штора едет.
|
||||
- 🌡 **Карточки комнат**: температура, влажность, Zigbee-сигнал, свет «1 из 3»;
|
||||
температурная заливка по комфортным границам.
|
||||
- 🚪 **Двери, окна и замки** с датчиками — отпирание только явной кнопкой,
|
||||
никогда случайным тапом.
|
||||
- 📺 **Киоск-режим** для настенных планшетов и ТВ: полноэкранно, свайп между
|
||||
этажами, автокарусель, свои размеры на каждом экране.
|
||||
- 🤖 **Роботы-пылесосы вживую** — маркер-база стоит на месте, а круглая
|
||||
шайба ездит по плану в реальном времени, «выливая» путь из-под себя;
|
||||
текущая и прошлая уборки хранятся на сервере. Калибровка — в один клик
|
||||
(по именам комнат) или перетаскиванием призрака карты. Диагностика и явный
|
||||
выбор источника поддерживают registry-less камеры карт и не подменяют молча
|
||||
сломавшуюся привязку. Работают Xiaomi Cloud Map Extractor, dreame-vacuum
|
||||
(Tasshack) и Valetudo.
|
||||
- 🔔 Новые устройства сами появляются на плане с красной точкой; раскладка
|
||||
хранится **на сервере HA** — один план для всех экранов, живая синхронизация.
|
||||
|
||||
## Что умеет House Plan
|
||||
---
|
||||
|
||||
- **Живые состояния и безопасные действия.** Свет и другие безопасные устройства
|
||||
переключаются с плана; замок нельзя открыть случайным нажатием.
|
||||
- **Три встроенных редактора.** «План» создаёт комнаты, стены и проёмы;
|
||||
«Устройства» размещает и настраивает маркеры; «Подложка» добавляет линии,
|
||||
подписи и мебель.
|
||||
- **Комнаты, связанные с зонами HA.** Новые устройства появляются автоматически,
|
||||
а карточки комнат показывают температуру, влажность, свет и средний LQI.
|
||||
- **Свет и окружение.** Заливки комнат, Glow от ламп, тени от стен, дневной фон и
|
||||
солнечные лучи из окон.
|
||||
- **Двери, окна, ворота и пылесосы.** Проёмы отражают реальные датчики и замки;
|
||||
робот показывает позицию, базу и пройденный путь.
|
||||
- **Несколько этажей и экранов.** Вкладки пространств, жесты переключения,
|
||||
локальный масштаб и отдельный стартовый этаж для каждой карточки.
|
||||
- **Киоск для настенного экрана.** Только план, полноэкранная навигация и размеры
|
||||
значков, сохранённые отдельно для этого устройства.
|
||||
|
||||

|
||||
## Что это и зачем
|
||||
|
||||
<!-- docs-section: first-run -->
|
||||
House Plan показывает ваш умный дом так, как он выглядит на самом деле — на плане этажей. Вместо длинных списков сущностей вы видите комнаты и устройства на своих местах: где протечка, какая температура в детской, включён ли свет в прихожей, открыты ли ворота.
|
||||
|
||||
## Первая рабочая комната
|
||||
Это удобно, когда:
|
||||
|
||||
1. Установите интеграцию и откройте **House Plan** в боковом меню Home Assistant.
|
||||
2. Создайте первое **пространство**: загрузите SVG/PNG/JPG/WebP либо выберите
|
||||
вариант без изображения, чтобы нарисовать план вручную.
|
||||
3. В редакторе «План» выберите **Контур комнаты**, поставьте вершины и замкните
|
||||
контур нажатием на первую точку.
|
||||
4. Назовите комнату и свяжите её с зоной Home Assistant. Для помещения без
|
||||
устройств выберите «Без зоны».
|
||||
5. Откройте «Устройства»: устройства связанной зоны уже размещены автоматически;
|
||||
перетащите маркеры в нужные места.
|
||||
6. При необходимости оформите подложку линиями, текстом и мебелью.
|
||||
7. Вернитесь в «Просмотр» — теперь план показывает живые состояния и принимает
|
||||
безопасные действия.
|
||||
- устройств много, и списками пользоваться неудобно;
|
||||
- нужно быстро понять состояние дома «одним взглядом»;
|
||||
- хочется отдать доступ близким — по картинке разберётся любой;
|
||||
- вы хотите красивый обзорный экран для настенного планшета.
|
||||
|
||||

|
||||
Интеграция состоит из двух частей, которые ставятся вместе:
|
||||
|
||||

|
||||
- **карточка Lovelace** `houseplan-card` — сам интерактивный план;
|
||||
- **серверный компонент** — хранит разметку комнат и позиции иконок в Home Assistant, поэтому план одинаков во всех браузерах и на всех устройствах.
|
||||
|
||||

|
||||
---
|
||||
|
||||

|
||||
## Чем отличается от аналогов
|
||||
|
||||

|
||||
Обычно план дома в Home Assistant делают через `picture-elements`, `ha-floorplan`
|
||||
или новые GUI-карточки, где стены и мебель рисуют прямо на дашборде. Там либо
|
||||
YAML/SVG, либо конфиг живёт в YAML карточки. House Plan — это **общий живой
|
||||
план** на серверной интеграции Home Assistant:
|
||||
|
||||
Пошаговые сценарии, все инструменты и особые случаи описаны в
|
||||
[полном руководстве](docs/USER-GUIDE.ru.md). Возможности подложки отдельно
|
||||
зафиксированы в [документе редактора](docs/DECOR-EDITOR.md), а роботов — в
|
||||
[руководстве по пылесосам](docs/VACUUM.md).
|
||||
| | House Plan | picture-elements / ha-floorplan | GUI-рисовалки (напр. easy-floorplan) |
|
||||
|---|---|---|---|
|
||||
| **Настройка** | Полностью через интерфейс, мышкой | Ручной YAML / Inkscape SVG | Рисование стен и мебели в карточке |
|
||||
| **Добавление устройств** | Автоматически по **зоне** HA | Каждую сущность вписываете руками | Ставите сущности руками на чертёж |
|
||||
| **Координаты иконок** | Перетаскиваете мышью | Считаете пиксели в YAML | Drag на холсте |
|
||||
| **Разметка комнат** | Встроенный редактор контуров, привязка к зонам | Сторонний SVG-редактор | Сами рисуете стены (мебельный CAD) |
|
||||
| **Хранение** | На сервере HA (`.storage`, общее, multi-client) | В YAML дашборда | В карточке / YAML дашборда |
|
||||
| **Оверлеи** | Glow, климат, LQI, солнце, пылесосы, киоск | Что пропишете в SVG/CSS | Зависит от карточки |
|
||||
| **Масштаб** | Плавный векторный зум | Обычно фиксированная картинка | SVG / виртуальный холст |
|
||||
|
||||
<!-- docs-section: installation -->
|
||||
**Одной фразой:** House Plan — это общая, area-aware живая карта дома, а не
|
||||
универсальная CAD-система. Редактор подложки покрывает практический декор,
|
||||
надписи и мебель; для свободного архитектурного черчения лучше отдельный
|
||||
draw-инструмент. При наличии плана и зон HA House Plan держит один живой layout
|
||||
на всех планшетах.
|
||||
|
||||
Ключевые преимущества коротко:
|
||||
|
||||
- **Никакого кода.** Всё — пространства, комнаты, устройства — настраивается кликами.
|
||||
- **Автоматическое добавление устройств.** Обвели комнату и привязали её к зоне Home Assistant — устройства этой зоны сами появляются на плане.
|
||||
- **Ручное добавление своих.** Любое устройство, группу или даже «виртуальную» точку можно поставить на план вручную, задать имя, иконку, модель, ссылку и приложить PDF-инструкцию.
|
||||
- **Живые состояния.** Температура, уровень сигнала Zigbee, вкл/выкл, открыто/закрыто — всё обновляется в реальном времени.
|
||||
Цвета значков подчиняются одному принципу — **жёлтый значит «устройство прямо сейчас выполняет свою основную работу»**:
|
||||
лампа светит, розетка подаёт, вентилятор крутится, пылесос убирает, термоголовка
|
||||
реально греет (а не просто включена). Для climate-сущностей переданное действие приоритетно;
|
||||
если интеграция сообщает только включённый HVAC-режим, он служит лучшим доступным приближением.
|
||||
Оранжевый = открыто / не заперто. Пульсирующее красное
|
||||
кольцо = авария (протечка, дым, газ). Цвет RGB-лампы живёт в её пятне света (режим glow),
|
||||
где само пятно — индикатор включения, а подложка значка остаётся стандартной.
|
||||
Полупрозрачный значок = недоступно. Тёмный = покой.
|
||||
- **Единый визуальный редактор подложки.** Линии, фигуры, надписи и мебель используют общее выделение, физические стили и Undo/Redo. Картинка плана не прибита к холсту: отдельный инструмент двигает, масштабирует и поворачивает её, а числовой диалог задаёт точный размер и угол.
|
||||
- **Чёткий зум.** Приближение не «мылит» картинку: план, подписи и иконки остаются векторно-чёткими на любом масштабе.
|
||||
|
||||
---
|
||||
|
||||
## Настенный планшет / ТВ (киоск-режим)
|
||||
|
||||
Отдельный дашборд с view типа «панель», у карточки — `kiosk: true` (или
|
||||
галочка «Режим настенного устройства» в редакторе карточки):
|
||||
|
||||
```yaml
|
||||
type: custom:houseplan-card
|
||||
kiosk: true
|
||||
cycle: 0 # автосмена пространств каждые N секунд, 0 = выкл (удобно для ТВ)
|
||||
```
|
||||
|
||||
Без шапки и редакторов — только живой план. Свайп листает этажи (при 1:1),
|
||||
пинч — зум, двойной тап — сброс. Долгое нажатие (3 с) по пустому месту —
|
||||
настройка размеров значков и текста для ЭТОГО экрана (хранится на
|
||||
устройстве). Шапку самого Home Assistant скрывают настройки companion-app
|
||||
или плагин [kiosk-mode](https://github.com/NemesisRE/kiosk-mode).
|
||||
|
||||
## Установка
|
||||
|
||||
### Через HACS
|
||||
В один клик, если у вас уже есть HACS:
|
||||
|
||||
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=Matysh&repository=houseplan-card&category=integration)
|
||||
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=Matysh&repository=houseplan-card&category=integration)
|
||||
|
||||
House Plan входит в основной каталог HACS — пользовательский репозиторий
|
||||
добавлять не нужно.
|
||||
|
||||
1. Найдите **House Plan** в поиске HACS и установите.
|
||||
2. Перезапустите Home Assistant.
|
||||
3. Откройте **Настройки → Устройства и службы → Добавить интеграцию → House Plan**.
|
||||
### Через HACS (рекомендуется)
|
||||
|
||||
Страница в боковом меню и дополнительные карточки для дашборда регистрируются
|
||||
автоматически. После установки или обновления
|
||||
House Plan перезапустите Home Assistant и полностью перезагрузите
|
||||
страницу: `Ctrl+F5` в Windows/Linux или `Cmd+Shift+R` в macOS.
|
||||
1. Откройте **HACS → меню (⋮) → Custom repositories**.
|
||||
2. Вставьте URL этого репозитория, категория — **Integration**, и нажмите **Add**.
|
||||
3. Найдите в списке **House Plan**, установите и **перезапустите Home Assistant**.
|
||||
4. Перейдите в **Настройки → Устройства и службы → Добавить интеграцию** и выберите **House Plan**.
|
||||
|
||||
#### Режим Storage (по умолчанию в Home Assistant)
|
||||
Карточка подключается автоматически — добавлять ресурс Lovelace вручную не нужно.
|
||||
|
||||
Обычно YAML не нужен. Если авторегистрация не сделала карточку доступной,
|
||||
откройте **Настройки → Панели управления → меню ⋮ → Ресурсы → Добавить
|
||||
ресурс**, укажите `/houseplan_files/houseplan-card.js` и выберите тип
|
||||
**JavaScript-модуль**.
|
||||
|
||||
#### YAML-ресурсы в Home Assistant 2026.2+
|
||||
|
||||
Чтобы управлять ресурсами в `configuration.yaml` независимо от режима
|
||||
самой панели, используйте:
|
||||
|
||||
```yaml
|
||||
lovelace:
|
||||
resource_mode: yaml
|
||||
resources:
|
||||
- url: /houseplan_files/houseplan-card.js
|
||||
type: module
|
||||
```
|
||||
|
||||
#### Home Assistant 2024.6–2026.1: устаревший режим
|
||||
|
||||
Только для панели, которая уже полностью управляется через YAML:
|
||||
|
||||
```yaml
|
||||
lovelace:
|
||||
mode: yaml
|
||||
resources:
|
||||
- url: /houseplan_files/houseplan-card.js
|
||||
type: module
|
||||
```
|
||||
|
||||
`mode: yaml` переводит в YAML-режим саму панель. Не переключайте
|
||||
storage-панель в устаревший YAML только ради House Plan; используйте
|
||||
инструкцию для Storage выше. Не используйте путь к файлу внутри
|
||||
`custom_components`: Home Assistant не публикует его как JavaScript-модуль.
|
||||
> **Карточка не грузится (`Custom element doesn't exist: houseplan-card`) или вы ведёте ресурсы
|
||||
> Lovelace в YAML?** Добавьте ресурс вручную, указав URL, который *раздаёт сама интеграция*:
|
||||
>
|
||||
> ```yaml
|
||||
> resources:
|
||||
> - url: /houseplan_files/houseplan-card.js
|
||||
> type: module
|
||||
> ```
|
||||
>
|
||||
> **Не** используйте `/custom_components/houseplan/frontend/houseplan-card.js` — это путь к файлу
|
||||
> на диске, который Home Assistant не отдаёт по HTTP (получите ошибку MIME `text/plain`, и элемент
|
||||
> не зарегистрируется). Правильный URL, который раздаёт интеграция, — `/houseplan_files/houseplan-card.js`.
|
||||
> Обе карточки (`houseplan-card` и `houseplan-space-card`) лежат в этом одном файле — отдельный ресурс
|
||||
> не нужен.
|
||||
|
||||
### Вручную
|
||||
|
||||
Скопируйте всю папку `custom_components/houseplan` из релиза в
|
||||
`config/custom_components`, перезапустите Home Assistant и добавьте интеграцию
|
||||
House Plan. Одного `houseplan-card.js` недостаточно: карточке также нужны
|
||||
внутренний манифест и хешированные модули из того же релиза.
|
||||
1. Скопируйте папку `custom_components/houseplan` в каталог `config/custom_components` вашего Home Assistant.
|
||||
2. Перезапустите Home Assistant.
|
||||
3. Добавьте интеграцию: **Настройки → Устройства и службы → Добавить интеграцию → House Plan**.
|
||||
|
||||
### Открытие House Plan
|
||||
### Добавление экрана с планом
|
||||
|
||||
После добавления интеграции откройте **House Plan** в боковом меню Home
|
||||
Assistant. Это основной полноэкранный способ работы: отдельный дашборд и YAML
|
||||
не нужны. Запоминается последнее пространство, но после перехода на другую
|
||||
страницу HA панель всегда возвращается в режим просмотра, а не в редактор.
|
||||
Пользователь без права редактирования видит тот же живой план без редакторов.
|
||||
|
||||
Если пункт бокового меню зарегистрировать не удалось, интеграция и уже
|
||||
добавленные карточки на дашбордах продолжают работать. После перезапуска и
|
||||
жёсткого обновления HA проверьте состояние House Plan в системной информации.
|
||||
|
||||
### Необязательная карточка для дашборда
|
||||
|
||||
Добавляйте карточку, только если план нужно встроить в дашборд. В представлении
|
||||
Sections она по умолчанию занимает всю ширину; вручную выбранный в HA размер
|
||||
остаётся главным. Добавьте карточку через UI либо:
|
||||
Создайте новую вкладку дашборда (удобнее всего — в режиме «Панель»/Panel) и добавьте карточку:
|
||||
|
||||
```yaml
|
||||
type: custom:houseplan-card
|
||||
title: План дома
|
||||
```
|
||||
|
||||
Для нескольких экранов можно задать разные стартовые пространства:
|
||||
Больше ничего указывать не нужно — всё остальное настраивается прямо на экране.
|
||||
|
||||
```yaml
|
||||
type: custom:houseplan-card
|
||||
default_floor: ground
|
||||
```
|
||||
---
|
||||
|
||||
Все карточки используют общие серверные комнаты и координаты. Текущий режим,
|
||||
масштаб и выбранное пространство локальны для экрана. Одновременное
|
||||
редактирование поддерживает синхронизацию и проверку ревизий, но один объект
|
||||
лучше не менять параллельно в двух браузерах.
|
||||
## Как пользоваться
|
||||
|
||||
## Где искать подробности
|
||||
### Шаг 1. Добавьте пространство (этаж)
|
||||
|
||||
- [Полное руководство пользователя](docs/USER-GUIDE.ru.md)
|
||||
- [Матрица mouse/touch/keyboard](docs/USER-GUIDE.ru.md#6-навигация-масштаб-и-жесты)
|
||||
- [Инструменты плана](docs/USER-GUIDE.ru.md#инструменты-плана-в-короткой-таблице)
|
||||
- [Редактор подложки](docs/DECOR-EDITOR.md)
|
||||
- [Роботы-пылесосы](docs/VACUUM.md)
|
||||
- [Поддержка touch](docs/TOUCH-SUPPORT.md)
|
||||
При первом открытии план ещё пуст — House Plan сразу предложит создать первое пространство.
|
||||
|
||||
<!-- docs-section: support -->
|
||||
Если в вашем Home Assistant уже настроены **этажи**, мастер предложит создать
|
||||
пространство для каждого: названия подставятся сами, план попросит по очереди,
|
||||
любой этаж можно пропустить.
|
||||
|
||||
## Помощь и обратная связь
|
||||

|
||||
|
||||
- Вопросы и примеры планов: [Telegram @ha_houseplan](https://t.me/ha_houseplan).
|
||||
- Баги и предложения: [GitHub Issues](https://github.com/Matysh/houseplan-card/issues).
|
||||
- Перед отчётом обновите House Plan, перезапустите HA и выполните жёсткое
|
||||
обновление страницы (`Ctrl+F5`). Приложите версию, браузер, логи и шаги
|
||||
воспроизведения; приватные entity ID можно заменить вымышленными.
|
||||
В диалоге задайте **название** (например, «1 этаж») и выберите подложку: **загрузите** картинку плана (SVG, PNG, JPG, WebP), **возьмите уже загруженную** на сервер ранее или отметьте **«без подложки, нарисую комнаты сам»**. Холст бесконечный; картинка по умолчанию сохраняет пропорции, а подвинуть, изменить размер или повернуть её можно в любой момент в редакторе подложки.
|
||||
|
||||
Скриншоты в документации получены воспроизводимой командой
|
||||
`npm run build && node demo/docs/capture.mjs` только на синтетических данных. Версия сценариев,
|
||||
fingerprint исходников и хеш каждого изображения находятся в
|
||||
[индексе снимков](docs/images/screenshots.json).
|
||||

|
||||
|
||||
Лицензия: [MIT](LICENSE).
|
||||
> 💡 Подложку можно нарисовать в любом планировщике (например, РЕМПЛАННЕР) или сфотографировать бумажный план. Лучше всего SVG — он остаётся чётким при увеличении.
|
||||
|
||||
Позже можно добавить сколько угодно пространств (этажи, двор, гараж) кнопкой **+** рядом со вкладками.
|
||||
|
||||
### Шаг 2. Обведите комнаты
|
||||
|
||||
После добавления первого пространства карточка сама переходит в режим разметки. Кликайте по точкам сетки, соединяя их линиями, и замкните контур комнаты кликом по первой точке.
|
||||
|
||||
Как только контур замкнётся, появится окно сохранения комнаты. Здесь нужно **привязать комнату к зоне Home Assistant** — именно это включает автоматику. Для служебных помещений без устройств (холл, сауна) есть кнопка **«Без зоны»**.
|
||||
|
||||

|
||||
|
||||
Во время рисования у курсора показывается линейка с реальной длиной текущего отрезка (метры или футы+дюймы на имперской системе HA). Масштаб задаётся для каждого пространства — поле **«Масштаб (размер ячейки сетки)»** в диалоге пространства: сколько сантиметров в одной ячейке (по умолчанию 5 см).
|
||||
|
||||
Комнаты не могут пересекаться: клик строго внутри существующей комнаты или контур, охватывающий её, отклоняются. Ещё два инструмента помогают перекроить план позже:
|
||||
|
||||
- **Объединить** — кликните комнату, затем соседнюю с общей стеной; они сольются в одну. Диалог выбирает, чьё имя и зона останутся.
|
||||
- **Разделить** — кликните комнату, затем две точки на её стенах; хорда разрежет её надвое. Бо́льшая часть остаётся прежней комнатой (имя, зона, устройства), меньшая просит новое имя и зону.
|
||||
|
||||
|
||||
### Двери, окна, ворота и замки
|
||||
|
||||
В режиме разметки инструмент **«Проём»** ставит двери, окна и ворота: кликните рядом со стеной — проём
|
||||
примагнитится к ней. Выберите тип, **длину в реальных сантиметрах** (по умолчанию дверь 90 см,
|
||||
окно 120 см, ворота 300 см), датчик открытия и — для двери или ворот — **замок**.
|
||||
|
||||
С привязанным датчиком план оживает: створка двери поворачивается на петле, и дуга распахивания
|
||||
дорисовывается по мере открытия настоящей двери; окно раскрывает две створки. Пока открыто,
|
||||
подвижные части подсвечены акцентным цветом. Ворота не занимают полплана даже при ширине 3–4 м: две половинные створки показаны открытыми наружу всего на 10°, без большой дуги. Датчик, замок и пропуск света работают как у двери. У двери или ворот с замком рядом отображается замочек —
|
||||
зелёный, когда заперто, оранжевый, когда нет. Ради безопасности замок с плана **нельзя**
|
||||
переключить — клик по проёму показывает карточку с обоими статусами.
|
||||
|
||||
Проёмы легко поправить позже: при наведении проём подсвечивается, его можно **перетащить вдоль
|
||||
стен** (в том числе за угол), а **двойной клик открывает свойства**.
|
||||
|
||||
### Шаг 3. Устройства появляются сами
|
||||
|
||||
Как только вы сохранили комнату с привязкой к зоне, **устройства этой зоны автоматически расставляются внутри контура**. Берутся те же устройства, что показаны на странице **Настройки → Устройства → (фильтр по нужной комнате)** — только осмысленные, без служебных записей, мостов и дубликатов.
|
||||
|
||||
По умолчанию на план попадают только осмысленные устройства: нефизические (служебные записи, мосты, сцены, лампы, свёрнутые в световую группу) могут быть скрыты автоматически. Управление находится в левом нижнем углу диалога устройства: **«Скрыть»** убирает маркер после сохранения, а у уже скрытого маркера там же появляется **«Показать»**. Чтобы найти их, откройте редактор устройств и нажмите **«Скрытые и деактивированные»**: пользовательски скрытые устройства отображаются синими призраками. Деактивированное в HA устройство показывается серым служебным призраком и полностью исключается из данных и действий плана до повторной активации.
|
||||
|
||||
Дальше можно просто пользоваться планом: клик по иконке открывает карточку устройства с моделью, ссылкой и кнопкой перехода в Home Assistant.
|
||||
|
||||

|
||||
|
||||
### Шаг 4. Масштаб
|
||||
|
||||
Колесо мыши или кнопки **- / ⊹ / +** приближают и отдаляют план; на сенсорном экране работает «щипок» двумя пальцами. При отдалении виден весь план целиком, при приближении — детали, и всё остаётся чётким. Масштаб запоминается отдельно для каждого пространства.
|
||||
|
||||

|
||||
|
||||
### Шаг 5. Расставьте значки по местам
|
||||
|
||||
Расставлять значки нужно на вкладке **«Устройства»**: там они перетаскиваются мышью, а клик открывает редактор. В режиме **«Просмотр»** ничего сдвинуть нельзя — панорамирование карты больше не сдвигает датчики (главная просьба пользователей). Позиции сохраняются на сервере и одинаковы во всех браузерах и устройствах. Кнопка **↺** возвращает автоматическую раскладку.
|
||||
|
||||

|
||||
|
||||
### Управление с плана (tap actions)
|
||||
|
||||
По умолчанию тап по значку открывает инфо-карточку. В настройках карточки можно
|
||||
переключить **«Тап по устройству»** на *Переключить* — тогда тап включает/выключает
|
||||
свет, розетки, вентиляторы и увлажнители прямо с плана (режим настенного планшета).
|
||||
Для безопасности общий toggle не действует на замки, сигнализации, шторы/ворота и
|
||||
клапаны; для конкретного устройства toggle можно включить осознанно в его диалоге
|
||||
(кроме замков и сигнализаций — они с плана не переключаются никогда). **Долгое
|
||||
нажатие** всегда открывает инфо-карточку.
|
||||
|
||||
### Правила иконок
|
||||
|
||||
Какая MDI-иконка достанется устройству, решают **правила иконок** — редактируются
|
||||
прямо в карточке (кнопка ⬡ в шапке): упорядоченный список «шаблон имени → иконка»
|
||||
с живым тест-полем, двуязычные умолчания (EN/RU) и сброс одной кнопкой. Если ни одно
|
||||
правило не подошло — решает *device class* сущности (термометр для датчиков
|
||||
температуры и т.п.).
|
||||
|
||||
### Шаг 6. Добавление своих устройств вручную
|
||||
|
||||
Можно поставить и **отдельную сущность** (не только устройство целиком): начните печатать в поиске привязки — рядом с устройствами появятся отдельные сущности. Удобно, когда одно устройство отдаёт несколько значений (например, температуру и влажность), а вы хотите каждое своей иконкой.
|
||||
|
||||
|
||||
Не всё нужно оставлять на автоматику. Кнопкой **+** в шапке можно поставить на план любое устройство, группу или **виртуальную точку** (например, «Вентиль на вводе», которого нет как устройства). Задайте имя, иконку, модель, ссылку, описание и при желании приложите **PDF-инструкцию**.
|
||||
|
||||
В этом же диалоге настраивается вид устройства на плане. **Отображение** переключает значок,
|
||||
анимированную **пульсацию присутствия** (расходящиеся кольца, пока сущность активна, и тусклая
|
||||
точка в покое — идеально для датчиков движения) или то и другое сразу, с цветом и размером колец
|
||||
на устройство. **Размер значка** (×0,5–3) и **поворот** — тоже индивидуальные: вентиль на стене
|
||||
может быть маленьким и повёрнутым так, как он установлен.
|
||||
|
||||

|
||||
|
||||
### Свои стили через card-mod (для продвинутых, без поддержки)
|
||||
|
||||
Карточка приезжает готовой, и поля для CSS у неё нет — но если у вас уже стоит [card-mod](https://github.com/thomasloven/lovelace-card-mod), у каждого объекта плана теперь есть стабильный «крючок», за который можно зацепиться: `data-hp="device"` (плюс `data-entity`, `data-area`), `data-hp="room"`, `data-hp="opening"`, `data-hp="decor"`, `data-hp="room-label"`, `data-hp="space-tab"`. Мы обещаем их не переименовывать; сам card-mod мы не поставляем, не поддерживаем и за то, что ваш CSS сделает с карточкой, не отвечаем. Полная таблица, примеры и ограничения — в **[docs/STYLING-HOOKS.md](docs/STYLING-HOOKS.md)**.
|
||||
|
||||
---
|
||||
|
||||
## Удаление
|
||||
|
||||
1. Уберите карточку (или вкладку с планом) из дашборда.
|
||||
2. **Настройки → Устройства и службы → House Plan → Удалить** запись интеграции.
|
||||
3. Удалите интеграцию из **HACS** (или папку `custom_components/houseplan` при ручной установке) и перезапустите Home Assistant.
|
||||
4. При желании удалите сохранённые данные плана: файлы `config/houseplan/` (подложки и вложения) и записи `houseplan.config` / `houseplan.layout` в каталоге `config/.storage`.
|
||||
|
||||
---
|
||||
|
||||
## Помощь и обмен опытом
|
||||
|
||||
- 💬 **[Чат в Telegram — @ha_houseplan](https://t.me/ha_houseplan)** — вопросы,
|
||||
помощь с настройкой, идеи и скриншоты ваших планов. Самый быстрый способ
|
||||
связаться с автором и другими пользователями.
|
||||
- 🐞 [Issues на GitHub](https://github.com/Matysh/houseplan-card/issues) — баги
|
||||
и запросы фич (пожалуйста, указывайте версию House Plan).
|
||||
- 💡 [Discussions](https://github.com/Matysh/houseplan-card/discussions) — для
|
||||
развёрнутых обсуждений.
|
||||
- 📜 [История изменений](docs/CHANGELOG.ru.md) — что менялось в каждой версии.
|
||||
|
||||
Версия видна в консоли браузера при загрузке (`HOUSEPLAN-CARD vX.Y.Z`) и в
|
||||
**Настройки → Устройства и службы → House Plan** — с ней разбираться сильно
|
||||
быстрее.
|
||||
|
||||
---
|
||||
|
||||
## Часто задаваемые вопросы
|
||||
|
||||
**Нужно ли что-то писать в YAML?** Нет. Единственная строчка — это добавление карточки на дашборд; всё остальное делается мышкой.
|
||||
|
||||
**Мои устройства не появились на плане.** Устройство появляется, только если его зона в Home Assistant привязана к нарисованной комнате. Проверьте, что у устройства задана комната (Настройки → Устройства), а комната обведена и привязана к этой зоне. Откройте **«Скрытые и деактивированные»**: синий призрак можно показать в его диалоге, серый сначала нужно активировать в Home Assistant.
|
||||
|
||||
**Можно ли скрыть лишнее устройство или переименовать его?** Да — кликните по устройству на плане и в его карточке нажмите «Редактировать»: там можно сменить имя, иконку, модель или скрыть значок.
|
||||
|
||||
**Данные хранятся в облаке?** Нет. Всё хранится локально в вашем Home Assistant.
|
||||
|
||||
---
|
||||
|
||||
<p align="center"><sub>Скриншоты сделаны на реальной конфигурации Home Assistant.</sub></p>
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# Third-party notices
|
||||
|
||||
## VMware Clarity Assets - `compass-line.svg`
|
||||
|
||||
Source: <https://github.com/vmware-archive/clarity-assets/blob/bf6bdd0dd3f247f1a320d44d13fecdeda18c071c/icons/travel/compass-line.svg>
|
||||
|
||||
Copyright (c) 2018 VMware, Inc.
|
||||
|
||||
The MIT license (the "License") set forth below applies to all parts of the
|
||||
clarity-assets project. You may not use this file except in compliance with the
|
||||
License.
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## Tabler Icons — summary panel outline icons
|
||||
|
||||
Source: https://github.com/tabler/tabler-icons
|
||||
The settings, sidebar-right, eye, eye-off, plus and chevron paths were provided
|
||||
in the designer reference for #505 and are distributed as inline SVG paths.
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2026 Paweł Kuna
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Sergey Matyunin (Matysh)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,28 +0,0 @@
|
||||
# House Plan furniture pack 0.3.0
|
||||
|
||||
Canonical source artwork for the built-in House Plan furniture library.
|
||||
|
||||
- `svg/menu/`: 33 front-view category illustrations used only by the lazy
|
||||
editor bundle.
|
||||
- `svg/plan/`: 44 top-view drawings used on the plan.
|
||||
- `pack.json`: stable ids, category links, default dimensions and names. The
|
||||
filename deliberately avoids the `*manifest.json` suffix reserved by HACS.
|
||||
|
||||
The original author, Sergey Matyunin (`Matysh`), granted House Plan permission
|
||||
to use, modify and distribute all 77 SVG files under the repository MIT
|
||||
License without separate UI attribution in
|
||||
[issue #159](https://github.com/Matysh/houseplan-card/issues/159#issuecomment-5454085168).
|
||||
|
||||
The reviewed source archive is `houseplan-furniture-custom-0.3.0.zip`, attached
|
||||
to [issue #159](https://github.com/Matysh/houseplan-card/issues/159#issuecomment-5449707137),
|
||||
with SHA-256
|
||||
`9E969016EE3B4B4E3DB776FEC53C8B387B91368B118EB5E39911483DEF1B0953`.
|
||||
|
||||
The editable source is linked from `pack.json`. Generated TypeScript must
|
||||
not be edited by hand; run `npm run furniture:generate` after changing this
|
||||
directory.
|
||||
|
||||
Note (#369): the source archive referenced by the SHA-256 above may still
|
||||
carry the earlier romanisation of the author's name; the authoritative
|
||||
spelling is Sergey Matyunin (Сергей Матюнин), fixed 2026-08-29 by the
|
||||
owner's decision in issue #369. The archive bytes are unchanged.
|
||||
@@ -1,818 +0,0 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"view_schema": 2,
|
||||
"pack_id": "houseplan",
|
||||
"pack_version": "0.3.0",
|
||||
"title_ru": "Набор мебели: меню и план",
|
||||
"title_en": "Furniture pack: menu and plan",
|
||||
"author": "Sergey Matyunin (Matysh)",
|
||||
"license": "MIT",
|
||||
"source_url": "https://www.figma.com/design/cpGN8MhJGydwVOUzZv8dia/House-plan?node-id=271-95",
|
||||
"menu_icons": [
|
||||
{
|
||||
"id": "air_conditioner",
|
||||
"name_ru": "Кондиционер",
|
||||
"name_en": "Air conditioner",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/air_conditioner.svg"
|
||||
},
|
||||
{
|
||||
"id": "armchair",
|
||||
"name_ru": "Кресла",
|
||||
"name_en": "Armchairs",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/armchair.svg"
|
||||
},
|
||||
{
|
||||
"id": "bathtub",
|
||||
"name_ru": "Ванны",
|
||||
"name_en": "Bathtubs",
|
||||
"group": "sanitary",
|
||||
"file": "svg/menu/bathtub.svg"
|
||||
},
|
||||
{
|
||||
"id": "bed",
|
||||
"name_ru": "Кровати",
|
||||
"name_en": "Beds",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/bed.svg"
|
||||
},
|
||||
{
|
||||
"id": "bidet",
|
||||
"name_ru": "Биде",
|
||||
"name_en": "Bidets",
|
||||
"group": "sanitary",
|
||||
"file": "svg/menu/bidet.svg"
|
||||
},
|
||||
{
|
||||
"id": "boiler",
|
||||
"name_ru": "Бойлер",
|
||||
"name_en": "Water heater",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/boiler.svg"
|
||||
},
|
||||
{
|
||||
"id": "chair",
|
||||
"name_ru": "Стулья",
|
||||
"name_en": "Chairs",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/chair.svg"
|
||||
},
|
||||
{
|
||||
"id": "coffee_table",
|
||||
"name_ru": "Журнальные столы",
|
||||
"name_en": "Coffee tables",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/coffee_table.svg"
|
||||
},
|
||||
{
|
||||
"id": "computer",
|
||||
"name_ru": "Компьютер",
|
||||
"name_en": "Computer",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/computer.svg"
|
||||
},
|
||||
{
|
||||
"id": "cooktop",
|
||||
"name_ru": "Варочные панели",
|
||||
"name_en": "Cooktops",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/cooktop.svg"
|
||||
},
|
||||
{
|
||||
"id": "dining_table",
|
||||
"name_ru": "Обеденные столы",
|
||||
"name_en": "Dining tables",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/dining_table.svg"
|
||||
},
|
||||
{
|
||||
"id": "dishwasher",
|
||||
"name_ru": "Посудомоечная машина",
|
||||
"name_en": "Dishwasher",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/dishwasher.svg"
|
||||
},
|
||||
{
|
||||
"id": "dryer",
|
||||
"name_ru": "Сушильная машина",
|
||||
"name_en": "Dryer",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/dryer.svg"
|
||||
},
|
||||
{
|
||||
"id": "exercise",
|
||||
"name_ru": "Тренажёр",
|
||||
"name_en": "Exercise equipment",
|
||||
"group": "other",
|
||||
"file": "svg/menu/exercise.svg"
|
||||
},
|
||||
{
|
||||
"id": "fireplace",
|
||||
"name_ru": "Камин",
|
||||
"name_en": "Fireplace",
|
||||
"group": "other",
|
||||
"file": "svg/menu/fireplace.svg"
|
||||
},
|
||||
{
|
||||
"id": "fridge",
|
||||
"name_ru": "Холодильник",
|
||||
"name_en": "Refrigerator",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/fridge.svg"
|
||||
},
|
||||
{
|
||||
"id": "hood",
|
||||
"name_ru": "Вытяжка",
|
||||
"name_en": "Range hood",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/hood.svg"
|
||||
},
|
||||
{
|
||||
"id": "kitchen_cabinet",
|
||||
"name_ru": "Кухонные шкафы",
|
||||
"name_en": "Kitchen cabinets",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/kitchen_cabinet.svg"
|
||||
},
|
||||
{
|
||||
"id": "kitchen_sink",
|
||||
"name_ru": "Кухонные мойки",
|
||||
"name_en": "Kitchen sinks",
|
||||
"group": "sanitary",
|
||||
"file": "svg/menu/kitchen_sink.svg"
|
||||
},
|
||||
{
|
||||
"id": "nightstand",
|
||||
"name_ru": "Тумбы",
|
||||
"name_en": "Cabinets",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/nightstand.svg"
|
||||
},
|
||||
{
|
||||
"id": "oven",
|
||||
"name_ru": "Духовка",
|
||||
"name_en": "Oven",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/oven.svg"
|
||||
},
|
||||
{
|
||||
"id": "plant",
|
||||
"name_ru": "Растение",
|
||||
"name_en": "Plant",
|
||||
"group": "other",
|
||||
"file": "svg/menu/plant.svg"
|
||||
},
|
||||
{
|
||||
"id": "rug",
|
||||
"name_ru": "Ковер",
|
||||
"name_en": "Rug",
|
||||
"group": "other",
|
||||
"file": "svg/menu/rug.svg"
|
||||
},
|
||||
{
|
||||
"id": "shelving",
|
||||
"name_ru": "Стеллажи",
|
||||
"name_en": "Shelving",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/shelving.svg"
|
||||
},
|
||||
{
|
||||
"id": "shower",
|
||||
"name_ru": "Душ",
|
||||
"name_en": "Shower",
|
||||
"group": "sanitary",
|
||||
"file": "svg/menu/shower.svg"
|
||||
},
|
||||
{
|
||||
"id": "sink",
|
||||
"name_ru": "Раковина",
|
||||
"name_en": "Sink",
|
||||
"group": "sanitary",
|
||||
"file": "svg/menu/sink.svg"
|
||||
},
|
||||
{
|
||||
"id": "sofa",
|
||||
"name_ru": "Диваны",
|
||||
"name_en": "Sofas",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/sofa.svg"
|
||||
},
|
||||
{
|
||||
"id": "stairs",
|
||||
"name_ru": "Лестница",
|
||||
"name_en": "Stairs",
|
||||
"group": "other",
|
||||
"file": "svg/menu/stairs.svg"
|
||||
},
|
||||
{
|
||||
"id": "toilet",
|
||||
"name_ru": "Унитазы",
|
||||
"name_en": "Toilets",
|
||||
"group": "sanitary",
|
||||
"file": "svg/menu/toilet.svg"
|
||||
},
|
||||
{
|
||||
"id": "tv",
|
||||
"name_ru": "Телевизоры",
|
||||
"name_en": "Televisions",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/tv.svg"
|
||||
},
|
||||
{
|
||||
"id": "wardrobe",
|
||||
"name_ru": "Шкафы",
|
||||
"name_en": "Wardrobes",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/wardrobe.svg"
|
||||
},
|
||||
{
|
||||
"id": "washer",
|
||||
"name_ru": "Стиральная машина",
|
||||
"name_en": "Washing machine",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/washer.svg"
|
||||
},
|
||||
{
|
||||
"id": "work_table",
|
||||
"name_ru": "Рабочие столы",
|
||||
"name_en": "Desks",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/work_table.svg"
|
||||
}
|
||||
],
|
||||
"symbols": [
|
||||
{
|
||||
"id": "coffee_table",
|
||||
"operation": "replace",
|
||||
"name_ru": "Журнальный стол, прямоугольный",
|
||||
"name_en": "Rectangular coffee table",
|
||||
"group": "furniture",
|
||||
"width_cm": 120,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/coffee_table.svg",
|
||||
"menu_icon": "coffee_table",
|
||||
"notes": "Типовой размер; свободно стоящий предмет."
|
||||
},
|
||||
{
|
||||
"id": "coffee_table_round",
|
||||
"operation": "add",
|
||||
"name_ru": "Журнальный стол, круглый",
|
||||
"name_en": "Round coffee table",
|
||||
"group": "furniture",
|
||||
"width_cm": 80,
|
||||
"depth_cm": 80,
|
||||
"back": "top",
|
||||
"file": "svg/plan/coffee_table_round.svg",
|
||||
"menu_icon": "coffee_table",
|
||||
"notes": "Круглый свободно стоящий предмет."
|
||||
},
|
||||
{
|
||||
"id": "coffee_table_oval",
|
||||
"operation": "add",
|
||||
"name_ru": "Журнальный стол, овальный",
|
||||
"name_en": "Oval coffee table",
|
||||
"group": "furniture",
|
||||
"width_cm": 120,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/coffee_table_oval.svg",
|
||||
"menu_icon": "coffee_table",
|
||||
"notes": "Свободно стоящий предмет."
|
||||
},
|
||||
{
|
||||
"id": "coffee_table_rounded",
|
||||
"operation": "add",
|
||||
"name_ru": "Журнальный стол, скруглённый",
|
||||
"name_en": "Rounded coffee table",
|
||||
"group": "furniture",
|
||||
"width_cm": 120,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/coffee_table_rounded.svg",
|
||||
"menu_icon": "coffee_table",
|
||||
"notes": "Прямоугольная форма со скруглёнными углами."
|
||||
},
|
||||
{
|
||||
"id": "table_dining",
|
||||
"operation": "replace",
|
||||
"name_ru": "Обеденный стол, прямоугольный",
|
||||
"name_en": "Rectangular dining table",
|
||||
"group": "furniture",
|
||||
"width_cm": 160,
|
||||
"depth_cm": 90,
|
||||
"back": "top",
|
||||
"file": "svg/plan/table_dining.svg",
|
||||
"menu_icon": "dining_table",
|
||||
"notes": "Типовой стол на 4–6 мест."
|
||||
},
|
||||
{
|
||||
"id": "table_round",
|
||||
"operation": "replace",
|
||||
"name_ru": "Обеденный стол, круглый",
|
||||
"name_en": "Round dining table",
|
||||
"group": "furniture",
|
||||
"width_cm": 110,
|
||||
"depth_cm": 110,
|
||||
"back": "top",
|
||||
"file": "svg/plan/table_round.svg",
|
||||
"menu_icon": "dining_table",
|
||||
"notes": "Круглый стол на 4 места."
|
||||
},
|
||||
{
|
||||
"id": "table_dining_oval",
|
||||
"operation": "add",
|
||||
"name_ru": "Обеденный стол, овальный",
|
||||
"name_en": "Oval dining table",
|
||||
"group": "furniture",
|
||||
"width_cm": 180,
|
||||
"depth_cm": 100,
|
||||
"back": "top",
|
||||
"file": "svg/plan/table_dining_oval.svg",
|
||||
"menu_icon": "dining_table",
|
||||
"notes": "Овальный стол на 6 мест."
|
||||
},
|
||||
{
|
||||
"id": "table_dining_rounded",
|
||||
"operation": "add",
|
||||
"name_ru": "Обеденный стол, скруглённый",
|
||||
"name_en": "Rounded dining table",
|
||||
"group": "furniture",
|
||||
"width_cm": 160,
|
||||
"depth_cm": 90,
|
||||
"back": "top",
|
||||
"file": "svg/plan/table_dining_rounded.svg",
|
||||
"menu_icon": "dining_table",
|
||||
"notes": "Прямоугольная форма со скруглёнными углами."
|
||||
},
|
||||
{
|
||||
"id": "desk",
|
||||
"operation": "replace",
|
||||
"name_ru": "Рабочий стол, прямоугольный",
|
||||
"name_en": "Rectangular desk",
|
||||
"group": "furniture",
|
||||
"width_cm": 140,
|
||||
"depth_cm": 70,
|
||||
"back": "top",
|
||||
"file": "svg/plan/desk.svg",
|
||||
"menu_icon": "work_table",
|
||||
"notes": "BACK — длинная сторона у стены."
|
||||
},
|
||||
{
|
||||
"id": "desk_corner",
|
||||
"operation": "add",
|
||||
"name_ru": "Рабочий стол, угловой",
|
||||
"name_en": "Corner desk",
|
||||
"group": "furniture",
|
||||
"width_cm": 160,
|
||||
"depth_cm": 160,
|
||||
"back": "top",
|
||||
"file": "svg/plan/desk_corner.svg",
|
||||
"menu_icon": "work_table",
|
||||
"notes": "Угловой стол; основная задняя сторона направлена вверх."
|
||||
},
|
||||
{
|
||||
"id": "chair",
|
||||
"operation": "replace",
|
||||
"name_ru": "Стул",
|
||||
"name_en": "Chair",
|
||||
"group": "furniture",
|
||||
"width_cm": 50,
|
||||
"depth_cm": 50,
|
||||
"back": "top",
|
||||
"file": "svg/plan/chair.svg",
|
||||
"menu_icon": "chair",
|
||||
"notes": "Спинка находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "chair_bar",
|
||||
"operation": "add",
|
||||
"name_ru": "Барный стул",
|
||||
"name_en": "Bar stool",
|
||||
"group": "furniture",
|
||||
"width_cm": 45,
|
||||
"depth_cm": 48,
|
||||
"back": "top",
|
||||
"file": "svg/plan/chair_bar.svg",
|
||||
"menu_icon": "chair",
|
||||
"notes": "Спинка находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "armchair",
|
||||
"operation": "replace",
|
||||
"name_ru": "Кресло мягкое",
|
||||
"name_en": "Soft armchair",
|
||||
"group": "furniture",
|
||||
"width_cm": 90,
|
||||
"depth_cm": 90,
|
||||
"back": "top",
|
||||
"file": "svg/plan/armchair.svg",
|
||||
"menu_icon": "armchair",
|
||||
"notes": "Спинка находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "armchair_office",
|
||||
"operation": "add",
|
||||
"name_ru": "Кресло офисное",
|
||||
"name_en": "Office chair",
|
||||
"group": "furniture",
|
||||
"width_cm": 65,
|
||||
"depth_cm": 65,
|
||||
"back": "top",
|
||||
"file": "svg/plan/armchair_office.svg",
|
||||
"menu_icon": "armchair",
|
||||
"notes": "Спинка находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "sofa",
|
||||
"operation": "replace",
|
||||
"name_ru": "Диван двухместный",
|
||||
"name_en": "Two-seat sofa",
|
||||
"group": "furniture",
|
||||
"width_cm": 180,
|
||||
"depth_cm": 90,
|
||||
"back": "top",
|
||||
"file": "svg/plan/sofa.svg",
|
||||
"menu_icon": "sofa",
|
||||
"notes": "Спинка находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "sofa_three_seat",
|
||||
"operation": "add",
|
||||
"name_ru": "Диван трёхместный",
|
||||
"name_en": "Three-seat sofa",
|
||||
"group": "furniture",
|
||||
"width_cm": 240,
|
||||
"depth_cm": 90,
|
||||
"back": "top",
|
||||
"file": "svg/plan/sofa_three_seat.svg",
|
||||
"menu_icon": "sofa",
|
||||
"notes": "Спинка находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "sofa_corner_right",
|
||||
"operation": "add",
|
||||
"name_ru": "Угловой диван, правый",
|
||||
"name_en": "Right sectional sofa",
|
||||
"group": "furniture",
|
||||
"width_cm": 260,
|
||||
"depth_cm": 170,
|
||||
"back": "top",
|
||||
"file": "svg/plan/sofa_corner_right.svg",
|
||||
"menu_icon": "sofa",
|
||||
"notes": "Шезлонг справа при взгляде сверху; спинка сверху."
|
||||
},
|
||||
{
|
||||
"id": "bed_single",
|
||||
"operation": "replace",
|
||||
"name_ru": "Кровать односпальная",
|
||||
"name_en": "Single bed",
|
||||
"group": "furniture",
|
||||
"width_cm": 90,
|
||||
"depth_cm": 200,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bed_single.svg",
|
||||
"menu_icon": "bed",
|
||||
"notes": "Узкая кровать с одной подушкой; изголовье сверху."
|
||||
},
|
||||
{
|
||||
"id": "bed_double",
|
||||
"operation": "replace",
|
||||
"name_ru": "Кровать двуспальная",
|
||||
"name_en": "Double bed",
|
||||
"group": "furniture",
|
||||
"width_cm": 160,
|
||||
"depth_cm": 200,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bed_double.svg",
|
||||
"menu_icon": "bed",
|
||||
"notes": "Широкая кровать с двумя подушками; изголовье сверху."
|
||||
},
|
||||
{
|
||||
"id": "nightstand",
|
||||
"operation": "replace",
|
||||
"name_ru": "Тумба прикроватная",
|
||||
"name_en": "Bedside table",
|
||||
"group": "furniture",
|
||||
"width_cm": 50,
|
||||
"depth_cm": 40,
|
||||
"back": "top",
|
||||
"file": "svg/plan/nightstand.svg",
|
||||
"menu_icon": "nightstand",
|
||||
"notes": "Задняя сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "cabinet_tv",
|
||||
"operation": "add",
|
||||
"name_ru": "Тумба под телевизор",
|
||||
"name_en": "TV cabinet",
|
||||
"group": "furniture",
|
||||
"width_cm": 140,
|
||||
"depth_cm": 45,
|
||||
"back": "top",
|
||||
"file": "svg/plan/cabinet_tv.svg",
|
||||
"menu_icon": "nightstand",
|
||||
"notes": "Задняя длинная сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "cabinet_shoe",
|
||||
"operation": "add",
|
||||
"name_ru": "Тумба для обуви",
|
||||
"name_en": "Shoe cabinet",
|
||||
"group": "furniture",
|
||||
"width_cm": 80,
|
||||
"depth_cm": 35,
|
||||
"back": "top",
|
||||
"file": "svg/plan/cabinet_shoe.svg",
|
||||
"menu_icon": "nightstand",
|
||||
"notes": "Задняя сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "cabinet_sink",
|
||||
"operation": "add",
|
||||
"name_ru": "Тумба под раковину",
|
||||
"name_en": "Sink cabinet",
|
||||
"group": "furniture",
|
||||
"width_cm": 80,
|
||||
"depth_cm": 50,
|
||||
"back": "top",
|
||||
"file": "svg/plan/cabinet_sink.svg",
|
||||
"menu_icon": "nightstand",
|
||||
"notes": "Задняя сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "bookshelf",
|
||||
"operation": "replace",
|
||||
"name_ru": "Шкаф книжный",
|
||||
"name_en": "Bookcase",
|
||||
"group": "furniture",
|
||||
"width_cm": 100,
|
||||
"depth_cm": 35,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bookshelf.svg",
|
||||
"menu_icon": "wardrobe",
|
||||
"notes": "Задняя длинная сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "wall_unit",
|
||||
"operation": "add",
|
||||
"name_ru": "Шкаф-стенка",
|
||||
"name_en": "Wall unit",
|
||||
"group": "furniture",
|
||||
"width_cm": 240,
|
||||
"depth_cm": 45,
|
||||
"back": "top",
|
||||
"file": "svg/plan/wall_unit.svg",
|
||||
"menu_icon": "wardrobe",
|
||||
"notes": "Задняя длинная сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "wardrobe",
|
||||
"operation": "replace",
|
||||
"name_ru": "Шкаф гардеробный",
|
||||
"name_en": "Wardrobe",
|
||||
"group": "furniture",
|
||||
"width_cm": 180,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/wardrobe.svg",
|
||||
"menu_icon": "wardrobe",
|
||||
"notes": "Задняя длинная сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "kitchen_floor",
|
||||
"operation": "add",
|
||||
"name_ru": "Кухонный напольный модуль",
|
||||
"name_en": "Kitchen floor module",
|
||||
"group": "furniture",
|
||||
"width_cm": 60,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/kitchen_floor.svg",
|
||||
"menu_icon": "kitchen_cabinet",
|
||||
"notes": "Задняя сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "kitchen_floor_corner",
|
||||
"operation": "add",
|
||||
"name_ru": "Кухонный напольный угловой модуль",
|
||||
"name_en": "Kitchen floor corner module",
|
||||
"group": "furniture",
|
||||
"width_cm": 90,
|
||||
"depth_cm": 90,
|
||||
"back": "top",
|
||||
"file": "svg/plan/kitchen_floor_corner.svg",
|
||||
"menu_icon": "kitchen_cabinet",
|
||||
"notes": "Угловой модуль; основной BACK сверху."
|
||||
},
|
||||
{
|
||||
"id": "kitchen_wall",
|
||||
"operation": "add",
|
||||
"name_ru": "Кухонный навесной модуль",
|
||||
"name_en": "Kitchen wall module",
|
||||
"group": "furniture",
|
||||
"width_cm": 60,
|
||||
"depth_cm": 35,
|
||||
"back": "top",
|
||||
"file": "svg/plan/kitchen_wall.svg",
|
||||
"menu_icon": "kitchen_cabinet",
|
||||
"notes": "Задняя сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "kitchen_wall_corner",
|
||||
"operation": "add",
|
||||
"name_ru": "Кухонный навесной угловой модуль",
|
||||
"name_en": "Kitchen wall corner module",
|
||||
"group": "furniture",
|
||||
"width_cm": 60,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/kitchen_wall_corner.svg",
|
||||
"menu_icon": "kitchen_cabinet",
|
||||
"notes": "Угловой модуль; основной BACK сверху."
|
||||
},
|
||||
{
|
||||
"id": "shelf_floor",
|
||||
"operation": "add",
|
||||
"name_ru": "Стеллаж напольный",
|
||||
"name_en": "Floor shelving unit",
|
||||
"group": "furniture",
|
||||
"width_cm": 100,
|
||||
"depth_cm": 35,
|
||||
"back": "top",
|
||||
"file": "svg/plan/shelf_floor.svg",
|
||||
"menu_icon": "shelving",
|
||||
"notes": "Задняя длинная сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "shelf_wall",
|
||||
"operation": "add",
|
||||
"name_ru": "Полка настенная",
|
||||
"name_en": "Wall shelf",
|
||||
"group": "furniture",
|
||||
"width_cm": 100,
|
||||
"depth_cm": 25,
|
||||
"back": "top",
|
||||
"file": "svg/plan/shelf_wall.svg",
|
||||
"menu_icon": "shelving",
|
||||
"notes": "Задняя длинная сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "cooktop_two",
|
||||
"operation": "add",
|
||||
"name_ru": "Варочная панель, 2 конфорки",
|
||||
"name_en": "Two-burner cooktop",
|
||||
"group": "appliance",
|
||||
"width_cm": 30,
|
||||
"depth_cm": 50,
|
||||
"back": "top",
|
||||
"file": "svg/plan/cooktop_two.svg",
|
||||
"menu_icon": "cooktop",
|
||||
"notes": "Панель ориентирована управляющей стороной вниз."
|
||||
},
|
||||
{
|
||||
"id": "stove",
|
||||
"operation": "replace",
|
||||
"name_ru": "Варочная панель, 4 конфорки",
|
||||
"name_en": "Four-burner cooktop",
|
||||
"group": "appliance",
|
||||
"width_cm": 60,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/stove.svg",
|
||||
"menu_icon": "cooktop",
|
||||
"notes": "Панель ориентирована управляющей стороной вниз."
|
||||
},
|
||||
{
|
||||
"id": "tv",
|
||||
"operation": "replace",
|
||||
"name_ru": "Телевизор на подставке",
|
||||
"name_en": "TV on stand",
|
||||
"group": "appliance",
|
||||
"width_cm": 120,
|
||||
"depth_cm": 28,
|
||||
"back": "top",
|
||||
"file": "svg/plan/tv.svg",
|
||||
"menu_icon": "tv",
|
||||
"notes": "Задняя сторона экрана сверху."
|
||||
},
|
||||
{
|
||||
"id": "tv_wall",
|
||||
"operation": "add",
|
||||
"name_ru": "Телевизор на кронштейне",
|
||||
"name_en": "Wall-mounted TV",
|
||||
"group": "appliance",
|
||||
"width_cm": 120,
|
||||
"depth_cm": 15,
|
||||
"back": "top",
|
||||
"file": "svg/plan/tv_wall.svg",
|
||||
"menu_icon": "tv",
|
||||
"notes": "Сторона крепления к стене сверху."
|
||||
},
|
||||
{
|
||||
"id": "toilet",
|
||||
"operation": "replace",
|
||||
"name_ru": "Унитаз напольный",
|
||||
"name_en": "Floor-standing toilet",
|
||||
"group": "sanitary",
|
||||
"width_cm": 40,
|
||||
"depth_cm": 70,
|
||||
"back": "top",
|
||||
"file": "svg/plan/toilet.svg",
|
||||
"menu_icon": "toilet",
|
||||
"notes": "Сторона подключения к стене сверху."
|
||||
},
|
||||
{
|
||||
"id": "toilet_built_in",
|
||||
"operation": "add",
|
||||
"name_ru": "Унитаз встроенный",
|
||||
"name_en": "Built-in toilet",
|
||||
"group": "sanitary",
|
||||
"width_cm": 40,
|
||||
"depth_cm": 55,
|
||||
"back": "top",
|
||||
"file": "svg/plan/toilet_built_in.svg",
|
||||
"menu_icon": "toilet",
|
||||
"notes": "Инсталляция находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "bathtub",
|
||||
"operation": "replace",
|
||||
"name_ru": "Ванна прямоугольная",
|
||||
"name_en": "Rectangular bathtub",
|
||||
"group": "sanitary",
|
||||
"width_cm": 170,
|
||||
"depth_cm": 75,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bathtub.svg",
|
||||
"menu_icon": "bathtub",
|
||||
"notes": "Длинная задняя сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "bathtub_corner",
|
||||
"operation": "add",
|
||||
"name_ru": "Ванна угловая",
|
||||
"name_en": "Corner bathtub",
|
||||
"group": "sanitary",
|
||||
"width_cm": 140,
|
||||
"depth_cm": 140,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bathtub_corner.svg",
|
||||
"menu_icon": "bathtub",
|
||||
"notes": "Угловая ванна; основной BACK сверху."
|
||||
},
|
||||
{
|
||||
"id": "bidet",
|
||||
"operation": "replace",
|
||||
"name_ru": "Биде напольное",
|
||||
"name_en": "Floor-standing bidet",
|
||||
"group": "sanitary",
|
||||
"width_cm": 40,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bidet.svg",
|
||||
"menu_icon": "bidet",
|
||||
"notes": "Сторона подключения к стене сверху."
|
||||
},
|
||||
{
|
||||
"id": "bidet_built_in",
|
||||
"operation": "add",
|
||||
"name_ru": "Биде встроенное",
|
||||
"name_en": "Built-in bidet",
|
||||
"group": "sanitary",
|
||||
"width_cm": 40,
|
||||
"depth_cm": 50,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bidet_built_in.svg",
|
||||
"menu_icon": "bidet",
|
||||
"notes": "Инсталляция находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "kitchen_sink",
|
||||
"operation": "replace",
|
||||
"name_ru": "Кухонная мойка одинарная",
|
||||
"name_en": "Single kitchen sink",
|
||||
"group": "sanitary",
|
||||
"width_cm": 60,
|
||||
"depth_cm": 50,
|
||||
"back": "top",
|
||||
"file": "svg/plan/kitchen_sink.svg",
|
||||
"menu_icon": "kitchen_sink",
|
||||
"notes": "Задняя сторона столешницы сверху."
|
||||
},
|
||||
{
|
||||
"id": "kitchen_sink_double",
|
||||
"operation": "add",
|
||||
"name_ru": "Кухонная мойка двойная",
|
||||
"name_en": "Double kitchen sink",
|
||||
"group": "sanitary",
|
||||
"width_cm": 90,
|
||||
"depth_cm": 50,
|
||||
"back": "top",
|
||||
"file": "svg/plan/kitchen_sink_double.svg",
|
||||
"menu_icon": "kitchen_sink",
|
||||
"notes": "Задняя сторона столешницы сверху."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M20.5 39h69a2.5 2.5 0 0 1 2.5 2.5V69a2.5 2.5 0 0 1-2.5 2.5h-69A2.5 2.5 0 0 1 18 69V41.5a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25 63a2.5 2.5 0 0 1 2.5-2.5h55A2.5 2.5 0 0 1 85 63v8.5H25z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25 63a2.5 2.5 0 0 1 2.5-2.5h55A2.5 2.5 0 0 1 85 63v3.5H25z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 661 B |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M42.5 29h25a10 10 0 0 1 10 10v26.5a10 10 0 0 1-10 10h-25a10 10 0 0 1-10-10V39a10 10 0 0 1 10-10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M77.5 48.5a6 6 0 0 1 2 11.657V71.5a4 4 0 0 1-4 4H34a4 4 0 0 1-4-4V60.157A6.001 6.001 0 0 1 32 48.5a6 6 0 0 1 5.419 3.424A4 4 0 0 1 38 54v7h33.5v-6.5q0-.107.005-.213A6 6 0 0 1 77.5 48.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M42.75 57h24.5A4.75 4.75 0 0 1 72 61.75a4.75 4.75 0 0 1-4.75 4.75h-24.5A4.75 4.75 0 0 1 38 61.75 4.75 4.75 0 0 1 42.75 57M36.5 75.5h8l-1.674 5.441A1.5 1.5 0 0 1 41.392 82H38a1.5 1.5 0 0 1-1.5-1.5zm36 0h-8l1.674 5.441A1.5 1.5 0 0 0 67.608 82H71a1.5 1.5 0 0 0 1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 960 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M28 75h8l-1.674 5.441a1.5 1.5 0 0 1-1.434 1.059H29.5A1.5 1.5 0 0 1 28 80zm53 0h-8l1.674 5.441a1.5 1.5 0 0 0 1.434 1.059H79.5A1.5 1.5 0 0 0 81 80zM19.5 46.5H90a2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5H19.5A2.5 2.5 0 0 1 17 49a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.5 51.5H90L86.633 68a10 10 0 0 1-9.798 8h-44.17a10 10 0 0 1-9.798-8zM30 36v-2a5 5 0 1 0-10 0v12.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 666 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M16 75.5h8l-1.674 5.441A1.5 1.5 0 0 1 20.892 82H17.5a1.5 1.5 0 0 1-1.5-1.5zm78.5 0h-8l1.674 5.441A1.5 1.5 0 0 0 89.608 82H93a1.5 1.5 0 0 0 1.5-1.5zM25 29h60.5a2.5 2.5 0 0 1 2.5 2.5V68a2.5 2.5 0 0 1-2.5 2.5H25a2.5 2.5 0 0 1-2.5-2.5V31.5A2.5 2.5 0 0 1 25 29" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 59.5c0-5.523 4.477-10 10-10h58.5c5.523 0 10 4.477 10 10v6H16zm-1 6h80.5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H15a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1m15.5-20a5 5 0 0 1 5-5h11a5 5 0 0 1 5 5v4h-21zm28.5 0a5 5 0 0 1 5-5h11a5 5 0 0 1 5 5v4H59z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 801 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M36 57.5c0 5.523 4.477 10 10 10h18.5c5.523 0 10-4.477 10-10V53H36zm8.5 22a1 1 0 0 0 1 1H65a1 1 0 0 0 1-1v-12H44.5zm-8.5-32a1 1 0 0 1 1-1h36.5a1 1 0 0 1 1 1V53H36zM50.5 41a1 1 0 0 1 1-1H59a1 1 0 0 1 1 1v5.5h-9.5zM53 30a1 1 0 0 1 1-1h2.5a1 1 0 0 1 1 1v10H53zm-4 6a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 1-1h4V36z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 485 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M46.5 81.5h5V86a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1zm12.5 0h5V86a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1zM44 23h22.5a8 8 0 0 1 8 8v42.5a8 8 0 0 1-8 8H44a8 8 0 0 1-8-8V31a8 8 0 0 1 8-8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M36 31a8 8 0 0 1 8-8h22.5a8 8 0 0 1 8 8v42H36z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M48.5 42.5a7 7 0 1 0 14 0 7 7 0 1 0-14 0m6.5 0 3.5-3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 703 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M40.5 39.5C40.5 32.596 46.096 27 53 27h4c6.904 0 12.5 5.596 12.5 12.5v13a5 5 0 0 1-5 5h-19a5 5 0 0 1-5-5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M41.25 54h27.5A3.25 3.25 0 0 1 72 57.25a3.25 3.25 0 0 1-3.25 3.25h-27.5A3.25 3.25 0 0 1 38 57.25 3.25 3.25 0 0 1 41.25 54m28.25 6.5H64l1.88 21.63a1.5 1.5 0 0 0 1.495 1.37H68a1.5 1.5 0 0 0 1.5-1.5zm-29 0H46l-1.88 21.63a1.5 1.5 0 0 1-1.495 1.37H42a1.5 1.5 0 0 1-1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 690 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M25 51h5.238l-1.534 15.646A1.5 1.5 0 0 1 27.211 68H26.5a1.5 1.5 0 0 1-1.5-1.5zm60.238 0H80l1.534 15.646A1.5 1.5 0 0 0 83.027 68h.711a1.5 1.5 0 0 0 1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24.34 43.485A2.5 2.5 0 0 1 26.625 42h56.75a2.5 2.5 0 0 1 2.285 1.485l2.715 6.109A1 1 0 0 1 87.461 51H22.54a1 1 0 0 1-.914-1.406z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M87.5 51h-66v3a1 1 0 0 0 1 1h64a1 1 0 0 0 1-1z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 764 B |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M24.5 38h40a2.5 2.5 0 0 1 2.5 2.5v22a2.5 2.5 0 0 1-2.5 2.5h-40a2.5 2.5 0 0 1-2.5-2.5v-22a2.5 2.5 0 0 1 2.5-2.5m50 0h11a2.5 2.5 0 0 1 2.5 2.5v29a2.5 2.5 0 0 1-2.5 2.5h-11a2.5 2.5 0 0 1-2.5-2.5v-29a2.5 2.5 0 0 1 2.5-2.5M43 65h5v7h-5zm-6 7h16m23-12h8m-8-17h8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M35 56V46h5m-5 7h5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M39.5 53a3.5 3.5 0 1 0 0-7m15.036 8.536a5 5 0 1 1 0-7.071" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 745 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M27.5 29h55a2.5 2.5 0 0 1 2.5 2.5v47a2.5 2.5 0 0 1-2.5 2.5h-55a2.5 2.5 0 0 1-2.5-2.5v-47a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M33.5 66.5a8 8 0 1 0 16 0 8 8 0 1 0-16 0m26.5-23a8 8 0 1 0 16 0 8 8 0 1 0-16 0m3 23a5 5 0 1 0 10 0 5 5 0 1 0-10 0m-29.5-23a8 8 0 1 0 16 0 8 8 0 1 0-16 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 579 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M83.5 43H78l6.742 30.82A1.5 1.5 0 0 0 86.207 75h.54a1.5 1.5 0 0 0 1.482-1.732zm-56 0H33l-6.742 30.82A1.5 1.5 0 0 1 24.793 75h-.54a1.5 1.5 0 0 1-1.482-1.732zm-2.62-7.406A2.5 2.5 0 0 1 27.21 34h56.58a2.5 2.5 0 0 1 2.33 1.594l2.35 6.044A1 1 0 0 1 87.538 43H23.462a1 1 0 0 1-.932-1.362z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M88.5 43h-66v3a1 1 0 0 0 1 1h64a1 1 0 0 0 1-1z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 645 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M31.5 25h47a2.5 2.5 0 0 1 2.5 2.5v55a2.5 2.5 0 0 1-2.5 2.5h-47a2.5 2.5 0 0 1-2.5-2.5v-55a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.5 37.5h47A2.5 2.5 0 0 1 81 40v36a2.5 2.5 0 0 1-2.5 2.5h-47A2.5 2.5 0 0 1 29 76V40a2.5 2.5 0 0 1 2.5-2.5M43 42h24m-1.5-11a2 2 0 1 0 4 0 2 2 0 1 0-4 0m7.5 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0m-37.25 0h9.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 626 B |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M31.5 25h47a2.5 2.5 0 0 1 2.5 2.5v55a2.5 2.5 0 0 1-2.5 2.5h-47a2.5 2.5 0 0 1-2.5-2.5v-55a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.5 37.5h47A2.5 2.5 0 0 1 81 40v42.5a2.5 2.5 0 0 1-2.5 2.5h-47a2.5 2.5 0 0 1-2.5-2.5V40a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M38 60.5a17 17 0 1 0 34 0 17 17 0 1 0-34 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M43.5 60.5a11.5 11.5 0 1 0 23 0 11.5 11.5 0 1 0-23 0m22-29.5a2 2 0 1 0 4 0 2 2 0 1 0-4 0m7.5 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0m-37.25 0h9.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M55.994 55.5s-1.329 1.81-1.479 3.143c-.198 1.761 1.599 2.554 1.48 4.321-.1 1.479-1.48 3.536-1.48 3.536m5.979-11s-1.329 1.81-1.479 3.143c-.198 1.761 1.599 2.554 1.48 4.321-.1 1.479-1.48 3.536-1.48 3.536m-8.519-11s-.886 1.81-.986 3.143c-.132 1.761 1.066 2.554.986 4.321-.067 1.479-.986 3.536-.986 3.536" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="m60.752 62.03-5.379-2.312 11.066-25.75a1.5 1.5 0 0 1 1.97-.786l2.624 1.127a1.5 1.5 0 0 1 .786 1.97zm-18.648-4.512-4.353 1.189-4.474-16.382a1.5 1.5 0 0 1 1.052-1.842l1.459-.398a1.5 1.5 0 0 1 1.842 1.052z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M41.297 38.173a2.215 2.215 0 0 1 0 4.43H29.781a2.215 2.215 0 0 1 0-4.43zm33.219 38.091a2.657 2.657 0 1 1 0 5.315H28.453a2.657 2.657 0 1 1 0-5.315zm8.641-51.366a1.795 1.795 0 0 1 .68 2.41l-3.03 5.59a5 5 0 0 1-4.396 2.618H65.657a1.772 1.772 0 1 1 0-3.544h9.61a3 3 0 0 0 2.598-1.5l2.84-4.917a1.795 1.795 0 0 1 2.452-.657M29.339 65.787c0-6.699 6.457-11.503 12.873-9.579l24.291 7.288a10 10 0 0 1 7.127 9.578v3.19H29.339zm0 15.852h7.086l-1.394 3.486a1.5 1.5 0 0 1-1.393.943h-2.8a1.5 1.5 0 0 1-1.5-1.5zm44.291 0h-7.087l1.395 3.486a1.5 1.5 0 0 0 1.392.943h2.8a1.5 1.5 0 0 0 1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M35.54 66.077a4.872 4.872 0 1 0 9.743 0 4.872 4.872 0 1 0-9.744 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M26.672 34.425h55.99v39.47h-55.99Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M23 74.894a1 1 0 0 1 1-1h61.334a1 1 0 0 1 1 1v5.802a1 1 0 0 1-1 1H24a1 1 0 0 1-1-1zM23 29a1 1 0 0 1 1-1h61.334a1 1 0 0 1 1 1v4.425a1 1 0 0 1-1 1H24a1 1 0 0 1-1-1zm15.145 21.44a5 5 0 0 1 5-5h23.043a5 5 0 0 1 5 5v23.454H38.145z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M44.57 65.633a2.754 2.754 0 0 1 2.754-2.753H62.01v5.507H47.324a2.754 2.754 0 0 1-2.754-2.754m20.194 5.507a2.754 2.754 0 0 0-2.754-2.753H47.324v5.507H62.01a2.754 2.754 0 0 0 2.754-2.754" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M44.57 71.14a2.754 2.754 0 1 0 5.508 0 2.754 2.754 0 1 0-5.508 0m14.688-5.507a2.754 2.754 0 1 0 5.507 0 2.754 2.754 0 1 0-5.507 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M35.5 18H75a2.5 2.5 0 0 1 2.5 2.5v64A2.5 2.5 0 0 1 75 87H35.5a2.5 2.5 0 0 1-2.5-2.5v-64a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M33 46.5h44.5v38A2.5 2.5 0 0 1 75 87H35.5a2.5 2.5 0 0 1-2.5-2.5zm2.5 44a1 1 0 0 0 1 1h37a1 1 0 0 0 1-1V87h-39zm3-49.5v-9.5m0 30.5v-9.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 560 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M44.308 57h21.384L83 69H27zM44 37.5a2.5 2.5 0 0 1 2.5-2.5h17a2.5 2.5 0 0 1 2.5 2.5V57H44zM80.5 74h-51a2.5 2.5 0 0 1-2.5-2.5V70a1 1 0 0 1 1-1h54a1 1 0 0 1 1 1v1.5a2.5 2.5 0 0 1-2.5 2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 368 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M38 88a1 1 0 0 0 1 1h32a1 1 0 0 0 1-1v-3H38zm17-68h16.5a2.5 2.5 0 0 1 2.5 2.5v17a2.5 2.5 0 0 1-2.5 2.5H55zm-19 2.5a2.5 2.5 0 0 1 2.5-2.5H55v22H38.5a2.5 2.5 0 0 1-2.5-2.5zM59 33v5m-8-5v5m4 47h16.5a2.5 2.5 0 0 0 2.5-2.5v-17a2.5 2.5 0 0 0-2.5-2.5H55zm-19-2.5a2.5 2.5 0 0 0 2.5 2.5H55V63H38.5a2.5 2.5 0 0 0-2.5 2.5zM67 67h-5m-14 0h-5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 514 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M21.5 84h68a2.5 2.5 0 0 0 2.5-2.5v-35a2.5 2.5 0 0 0-2.5-2.5h-68a2.5 2.5 0 0 0-2.5 2.5v35a2.5 2.5 0 0 0 2.5 2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M26 78h26.5a1 1 0 0 0 1-1V51a1 1 0 0 0-1-1H26a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1m23-24v5m9.5 19H85a1 1 0 0 0 1-1V51a1 1 0 0 0-1-1H58.5a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1m-6-39.5a1 1 0 0 0-1-1H44a1 1 0 0 0-1 1V44h9.5zM58 33v-2a5 5 0 1 0-10 0v6.5M62 54v5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 671 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M76.5 73.5h-8l1.674 5.441A1.5 1.5 0 0 0 71.608 80H75a1.5 1.5 0 0 0 1.5-1.5zm-42.5 0h8l-1.674 5.441A1.5 1.5 0 0 1 38.892 80H35.5a1.5 1.5 0 0 1-1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M34.5 30.5h41A2.5 2.5 0 0 1 78 33v40.5a2.5 2.5 0 0 1-2.5 2.5h-41a2.5 2.5 0 0 1-2.5-2.5V33a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32 39.5h46v18H32Zm18.5 9H60M50.5 66H60" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 734 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M31.5 25h47a2.5 2.5 0 0 1 2.5 2.5v55a2.5 2.5 0 0 1-2.5 2.5h-47a2.5 2.5 0 0 1-2.5-2.5v-55a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.5 37.5h47A2.5 2.5 0 0 1 81 40v36a2.5 2.5 0 0 1-2.5 2.5h-47A2.5 2.5 0 0 1 29 76V40a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M40.5 47h29a2.5 2.5 0 0 1 2.5 2.5v20a2.5 2.5 0 0 1-2.5 2.5h-29a2.5 2.5 0 0 1-2.5-2.5v-20a2.5 2.5 0 0 1 2.5-2.5m25-16a2 2 0 1 0 4 0 2 2 0 1 0-4 0m7.5 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0m-37.25 0h9.5M43 42h24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 851 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M45.952 75.849a2.5 2.5 0 0 0 2.485 2.231H63.43a2.5 2.5 0 0 0 2.485-2.231l1.571-14.532H44.381zM60.236 44.78C58.423 36.174 66.805 29.605 77 31.87c0 8.155-8.609 16.763-16.763 12.912m-9.761 5.147c1.517-8.665-7.085-14.943-17.195-12.33.28 8.15 9.178 16.458 17.195 12.33" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M55.706 55.88v-2.944m0 0c1.133-5.89 7.09-10.956 12.912-15.404M55.706 52.936c-4.938-4.146-7.982-6.16-14.044-9.061m1.453 12.005h25.636a1 1 0 0 1 1 1v3.437a1 1 0 0 1-1 1H43.115a1 1 0 0 1-1-1v-3.436a1 1 0 0 1 1-1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 788 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M90.27 33.874h-5.622m-59.028 0H20m70.27 5.152h-5.622m-59.028 0H20m70.27 5.154h-5.622m-59.028 0H20m70.27 5.152h-5.622m-59.028 0H20m70.27 5.153h-5.622m-59.028 0H20m70.27 5.153h-5.622m-59.028 0H20m70.27 5.153h-5.622m-59.028 0H20m70.27 5.153h-5.622m-59.028 0H20m70.27 5.153h-5.622m-59.028 0H20M28.12 32h54.025a2.5 2.5 0 0 1 2.5 2.5v40.439a2.5 2.5 0 0 1-2.5 2.5H28.12a2.5 2.5 0 0 1-2.5-2.5V34.5a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25.62 34.5a2.5 2.5 0 0 1 2.5-2.5h1.717v45.439H28.12a2.5 2.5 0 0 1-2.5-2.5zm59.026 0a2.5 2.5 0 0 0-2.5-2.5H80.43v45.439h1.716a2.5 2.5 0 0 0 2.5-2.5zM37.8 54.485l17.801-8.432 17.801 8.432-17.8 8.432z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 926 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M28 58.5h53V39H28Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M64 58.5h5V47a1 1 0 0 0-1-1h-3a1 1 0 0 0-1 1zM52.5 39h-5V27.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1zm-12 0h-5V27.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1zm-5 29.5V71a1 1 0 0 1 1 1H49a1 1 0 0 1 1-1v-2.5a1 1 0 0 1-1-1H36.5a1 1 0 0 1-1 1m33.5-10h7v-14a1 1 0 0 0-1-1h-5a1 1 0 0 0-1 1zM47.5 39h-7V25a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1zm-12 34v4a1 1 0 0 1 1 1h14a1 1 0 0 1 1-1v-4a1 1 0 0 1-1-1h-14a1 1 0 0 1-1 1M28 78h53v7H28Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M28 89.5V23a2.5 2.5 0 0 1 2.5-2.5h48A2.5 2.5 0 0 1 81 23v66.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 911 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M31 84h49a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H31a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1m31.5-47.6v-4.686a11.7 11.7 0 0 0-3.441-8.283A11.77 11.77 0 0 0 50.75 20a11.77 11.77 0 0 0-8.309 3.431A11.7 11.7 0 0 0 39 31.714V84" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M72 39.25a10.25 10.25 0 1 0-20.5 0zm-17.25 5v2.5m7.25-2.5v2.5m7.25-2.5v2.5m-14.5 3.75V53M62 50.5V53m7.25-2.5V53m-14.5 3.75v2.5m7.25-2.5v2.5m7.25-2.5v2.5M54.75 63v2.5M62 63v2.5m7.25-2.5v2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 709 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M30 57c0 5.523 4.477 10 10 10h29.5c5.523 0 10-4.477 10-10v-6.5A2.5 2.5 0 0 0 77 48H32.5a2.5 2.5 0 0 0-2.5 2.5zm14 22a1 1 0 0 0 1 1h19.5a1 1 0 0 0 1-1V67H44zm6-36.5a1 1 0 0 1 1-1h7.5a1 1 0 0 1 1 1V48H50zM45 37v-2a5 5 0 1 1 10 0v6.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 415 B |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M59.5 29h24a5 5 0 0 1 5 5v32a5 5 0 0 1-5 5h-24a5 5 0 0 1-5-5V34a5 5 0 0 1 5-5m-34 0h24a5 5 0 0 1 5 5v32a5 5 0 0 1-5 5h-24a5 5 0 0 1-5-5V34a5 5 0 0 1 5-5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.5 44a5 5 0 0 1 5 5v12H86V49a5 5 0 0 1 5-5h.5a5 5 0 0 1 5 5v21.5a5 5 0 0 1-5 5H18a5 5 0 0 1-5-5V49a5 5 0 0 1 5-5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25.25 57h24.5a4.75 4.75 0 0 1 4.75 4.75 4.75 4.75 0 0 1-4.75 4.75h-24.5a4.75 4.75 0 0 1-4.75-4.75A4.75 4.75 0 0 1 25.25 57m34 0h24.5a4.75 4.75 0 0 1 4.75 4.75 4.75 4.75 0 0 1-4.75 4.75h-24.5a4.75 4.75 0 0 1-4.75-4.75A4.75 4.75 0 0 1 59.25 57M18.5 75.5h8l-1.674 5.441A1.5 1.5 0 0 1 23.392 82H20a1.5 1.5 0 0 1-1.5-1.5zm72 0h-8l1.674 5.441A1.5 1.5 0 0 0 85.608 82H89a1.5 1.5 0 0 0 1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M73.47 46.365v-17.62m-10.571 25.55V36.674M52.326 61.783V45.484M42.193 69.273v-15.86M28.979 77.761a1 1 0 0 1 1-1h53.624v6.489a1 1 0 0 1-1 1H29.98a1 1 0 0 1-1-1z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M26.777 57.497a1 1 0 0 1 1-1h2.846a1 1 0 0 1 1 1V83.25a1 1 0 0 1-1 1h-2.846a1 1 0 0 1-1-1z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="m73.113 28.633-41.492 32.93v-4.405l39.9-32.007a.7.7 0 0 1 .428-.151h3.724a1.762 1.762 0 1 1 0 3.524h-2.248a.5.5 0 0 0-.312.109M25.895 53.414a3.084 3.084 0 1 0 6.167 0 3.084 3.084 0 1 0-6.167 0m14.978 16.859a1 1 0 0 1 1-1h41.73v7.488h-42.73zm10.131-7.489a1 1 0 0 1 1-1h31.598v7.488H51.004z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M61.576 55.294a1 1 0 0 1 1-1h21.026v7.49H61.576z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M72.15 47.806a1 1 0 0 1 1-1h9.454a1 1 0 0 1 1 1v6.489H72.15z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M32 26.5a2.5 2.5 0 0 1 2.5-2.5H45a2.5 2.5 0 0 1 2.5 2.5V57H32zM47.5 52h29a2.5 2.5 0 0 1 2.5 2.5V57H47.5zm17.037 20.102C73.95 70.288 79 62.01 79 57H32c.5 7.5 8.072 7 7 11l-2.342 16.862a1 1 0 0 0 .99 1.138H66.72a1 1 0 0 0 .97-1.243z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 415 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M23.5 31H86a2.5 2.5 0 0 1 2.5 2.5v34A2.5 2.5 0 0 1 86 70H23.5a2.5 2.5 0 0 1-2.5-2.5v-34a2.5 2.5 0 0 1 2.5-2.5M50 70h9.5v8H50Zm-10 8h30M42.5 45.5h5m5 0h-5m0 0v9.75" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="m56.75 45.5 5 10 5-10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 500 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M28 23a2.5 2.5 0 0 1 2.5-2.5h24V85h-24a2.5 2.5 0 0 1-2.5-2.5zm2 65.5a1 1 0 0 0 1 1h47a1 1 0 0 0 1-1V85H30zM81 23a2.5 2.5 0 0 0-2.5-2.5h-24V85h24a2.5 2.5 0 0 0 2.5-2.5zM50.5 48v9.5m8-9.5v9.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 374 B |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M31.5 25h47a2.5 2.5 0 0 1 2.5 2.5v55a2.5 2.5 0 0 1-2.5 2.5h-47a2.5 2.5 0 0 1-2.5-2.5v-55a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.5 37.5h47A2.5 2.5 0 0 1 81 40v42.5a2.5 2.5 0 0 1-2.5 2.5h-47a2.5 2.5 0 0 1-2.5-2.5V40a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M38 60.5a17 17 0 1 0 34 0 17 17 0 1 0-34 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M43.5 60.5a11.5 11.5 0 1 0 23 0 11.5 11.5 0 1 0-23 0m22-29.5a2 2 0 1 0 4 0 2 2 0 1 0-4 0m7.5 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0m-37.25 0h9.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 949 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M88.5 47h-20v14h20zm-12 4h4m7.984-8h-66v4h66zm-66 4h4v26.5a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24.866 35.597A2.5 2.5 0 0 1 27.197 34h56.577a2.5 2.5 0 0 1 2.33 1.594l2.35 6.044a1 1 0 0 1-.93 1.362H23.46a1 1 0 0 1-.933-1.361zM88.5 61h-20v12.5A1.5 1.5 0 0 0 70 75h17a1.5 1.5 0 0 0 1.5-1.5zm-12 4h4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 623 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 90">
|
||||
<path d="M75.702 13.011q.012-.225.012-.453c0-4.843-3.917-8.768-8.748-8.768H23.034c-4.83 0-8.747 3.925-8.747 8.768q0 .228.011.453M77.132 72.09q.59.091 1.206.092c4.348 0 7.873-3.533 7.873-7.892V20.45c0-4.359-3.525-7.892-7.873-7.892-.924 0-1.812.16-2.636.453a7.89 7.89 0 0 0-5.237 7.438v.148m0 0a8.7 8.7 0 0 1-3.499.73H23.034a8.7 8.7 0 0 1-3.499-.73m50.93 0V64.29a7.89 7.89 0 0 0 6.667 7.8M14.298 13.01a7.89 7.89 0 0 1 5.237 7.438v.148m0 0V64.29a7.89 7.89 0 0 1-6.666 7.8q-.59.091-1.206.092c-4.348 0-7.873-3.533-7.873-7.892V20.45c0-4.359 3.525-7.892 7.873-7.892.924 0 1.811.16 2.635.453M77.132 72.09c-1.585 8.05-8.667 14.12-17.164 14.12H30.032c-8.496 0-15.578-6.07-17.163-14.12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 848 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 65 65">
|
||||
<path d="M56.968 18.845q0-.368-.05-.72M49.051 13.8H15.95a5.04 5.04 0 0 1-5.038-5.043 5.04 5.04 0 0 1 5.038-5.043h33.103a5.04 5.04 0 0 1 5.038 5.043 5.04 5.04 0 0 1-5.038 5.043m-33.103 0h-2.88a5.04 5.04 0 0 0-4.986 4.323M49.052 13.8h2.879a5.04 5.04 0 0 1 4.986 4.324m.051 24.826a4.32 4.32 0 0 0 4.318-4.323v-16.18a4.32 4.32 0 0 0-4.369-4.322m.051 24.826a4.32 4.32 0 0 1-4.318-4.323V22.447a4.32 4.32 0 0 1 4.267-4.322m-11.84 37.24h4.695c.736 0 1.446-.111 2.115-.317a7.21 7.21 0 0 0 5.081-6.889v-5.208M8.084 18.124h-.051a4.32 4.32 0 0 0-4.318 4.323v16.18a4.32 4.32 0 0 0 4.318 4.324 4.32 4.32 0 0 0 4.317-4.323V22.447a4.32 4.32 0 0 0-4.266-4.323m11.935 37.24h-4.79a7.2 7.2 0 0 1-2.04-.294 7.21 7.21 0 0 1-5.156-6.91v-5.21m37.045 12.414 5.177 5.183c.969.97 2.534.987 3.524.038a2.526 2.526 0 0 0 .038-3.605l-1.93-1.932m-6.81.316H20.02m-6.83-.294-1.909 1.91c-.998 1-.98 2.627.038 3.605.99.949 2.555.932 3.524-.038l5.177-5.183" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 170 75">
|
||||
<g>
|
||||
<path d="M3.885 6c0-1.243 1.068-2.25 2.386-2.25h157.458c1.318 0 2.386 1.007 2.386 2.25v63c0 1.243-1.068 2.25-2.386 2.25H6.27c-1.318 0-2.386-1.007-2.386-2.25z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.428 37.5c0-13.669 11.75-24.75 26.243-24.75h90.658c14.493 0 26.243 11.081 26.243 24.75s-11.75 24.75-26.243 24.75H39.67c-14.493 0-26.243-11.081-26.243-24.75" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.56 37.5c0 2.237-1.923 4.05-4.295 4.05s-4.294-1.813-4.294-4.05 1.923-4.05 4.294-4.05c2.372 0 4.295 1.813 4.295 4.05" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 857 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 140">
|
||||
<g>
|
||||
<path d="M3.862 6.224a2.36 2.36 0 0 1 2.362-2.362H133.97c1.197 0 2.168.97 2.168 2.168 0 71.857-58.251 130.108-130.108 130.108a2.17 2.17 0 0 1-2.168-2.168z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.31 32.207c0-10.437 8.46-18.897 18.897-18.897h87.628a2.13 2.13 0 0 1 2.13 2.13c0 58.833-47.692 106.526-106.524 106.526a2.13 2.13 0 0 1-2.13-2.131z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.207 27.483a4.724 4.724 0 1 1-9.449 0 4.724 4.724 0 0 1 9.449 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 794 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 200">
|
||||
<path d="M3.879 14.073c0 5.618 4.516 10.172 10.088 10.172h132.065c5.571 0 10.088-4.554 10.088-10.172m-81.165 47.16V46.438c0-5.107-4.106-9.247-9.171-9.247H45.607c-5.065 0-9.17 4.14-9.17 9.247v14.795m89.877 0V46.438c0-5.107-4.107-9.247-9.172-9.247H96.966c-5.065 0-9.171 4.14-9.171 9.247v14.795m-83.916 0H156.12m0 22.192H3.88m9.17 112.673h133.9c5.065 0 9.171-4.14 9.171-9.247V13.15c0-5.107-4.106-9.247-9.171-9.247H13.05c-5.064 0-9.17 4.14-9.17 9.247v173.7c0 5.107 4.106 9.247 9.17 9.247" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 659 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 200">
|
||||
<path d="M3.79 14.024c0 5.59 4.318 10.122 9.645 10.122h63.13c5.328 0 9.646-4.532 9.646-10.122m-22.797 46.93V46.23c0-5.082-3.926-9.201-8.769-9.201h-19.29c-4.842 0-8.768 4.12-8.768 9.201v14.723m-22.797 0h82.42m0 22.085H3.79m8.768 113.06h64.885c4.842 0 8.768-4.12 8.768-9.202V13.104c0-5.082-3.926-9.202-8.768-9.202H12.558c-4.843 0-8.768 4.12-8.768 9.202v173.792c0 5.082 3.925 9.202 8.768 9.202" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 565 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 60">
|
||||
<path d="M22.466 3.692v10.085c0 1.21-.92 2.192-2.055 2.192h-.822c-1.136 0-2.056-.981-2.056-2.192V3.692m4.933 8.77h2.878c1.135 0 2.056-.982 2.056-2.193v-.877c0-1.21-.92-2.192-2.056-2.192h-2.878m-4.933 5.262h-2.878c-1.135 0-2.055-.982-2.055-2.193v-.877c0-1.21.92-2.192 2.055-2.192h2.878M5.611 3.692h28.777c1.136 0 2.056.981 2.056 2.192v33.45c0 9.374-7.125 16.973-15.914 16.973h-1.06c-8.79 0-15.915-7.599-15.915-16.972V5.885c0-1.212.92-2.193 2.056-2.193m4.11 15.785h20.556c1.136 0 2.056.981 2.056 2.192v17.807c0 6.874-5.225 12.447-11.67 12.447h-1.327c-6.445 0-11.67-5.573-11.67-12.447V21.67c0-1.21.92-2.192 2.056-2.192m13.567 7.892c0 1.937-1.472 3.508-3.288 3.508-1.817 0-3.29-1.57-3.29-3.508s1.473-3.507 3.29-3.507 3.288 1.57 3.288 3.507" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 909 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 50">
|
||||
<path d="M17.318 12.182h-8.06c-.88 0-1.592.74-1.592 1.654v16.126c0 6.698 5.225 12.129 11.67 12.129h1.327c6.445 0 11.67-5.43 11.67-12.13V13.837c0-.914-.713-1.654-1.591-1.654H22.68m-.215-8.545v9.827c0 1.18-.92 2.136-2.055 2.136h-.822c-1.136 0-2.056-.956-2.056-2.136V3.637m10.689 8.545V3.637m-16.445 0v8.545M5.611 3.637h28.777c1.136 0 2.056.956 2.056 2.136v24.051c0 9.135-7.125 16.54-15.914 16.54h-1.06c-8.79 0-15.915-7.405-15.915-16.54V5.773c0-1.18.92-2.136 2.056-2.136M23.289 23.29c0 1.888-1.473 3.418-3.29 3.418s-3.288-1.53-3.288-3.418 1.472-3.418 3.289-3.418c1.816 0 3.288 1.53 3.288 3.418" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 764 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 35">
|
||||
<path d="M57.12 20.488H45.473c-.514 0-1.008.124-1.372.345s-.568.52-.568.832m4.076-8.236h5.434M6.119 3.5h87.762c1.276 0 2.31.895 2.31 2v24c0 1.105-1.034 2-2.31 2H6.12c-1.275 0-2.31-.895-2.31-2v-24c0-1.105 1.035-2 2.31-2m51 6.4v14.118H45.474a2.04 2.04 0 0 1-1.372-.517 1.7 1.7 0 0 1-.568-1.248V11.665c0-.468.204-.917.568-1.248a2.04 2.04 0 0 1 1.372-.517z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 527 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 35">
|
||||
<path d="m39.722 16.231 1.505-1.596m1.134 3.15 1.53-1.738M6.03 31.5h67.94c1.251 0 2.265-.895 2.265-2v-24c0-1.105-1.014-2-2.264-2H6.03c-1.251 0-2.265.895-2.265 2v24c0 1.105 1.014 2 2.265 2m41.966-14.305L46.92 17.1a3.4 3.4 0 0 1-1.813-.706l-4.583-3.62c-1.264-1-3.277-.326-3.475 1.161-.185 1.39-1.983 2.1-3.28 1.299l-1.532-.947c-.968-.513-2.2.103-2.2 1.1v3.463c0 1.559 1.43 2.823 3.196 2.823h14.51c1.404 0 2.542-1.005 2.542-2.245 0-1.153-.99-2.118-2.289-2.233" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 630 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 50">
|
||||
<g>
|
||||
<path d="M3.765 5.773c0-1.18 1.014-2.136 2.265-2.136h67.94c1.251 0 2.265.956 2.265 2.136v38.454c0 1.18-1.014 2.137-2.264 2.137H6.03c-1.251 0-2.265-.957-2.265-2.137z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M36.088 32.135c2.525 1.88 6.205 1.88 8.73 0s3.167-5.101 1.52-7.637l-5.879-8.043-5.893 8.043c-1.644 2.536-1.003 5.755 1.522 7.637" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 598 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 45">
|
||||
<path d="M56.772 17.88h5.315m0 0h5.315m-5.315 0v9.214m9.832-9.214 5.315 9.45 5.314-9.45M6.224 41.4h127.552c1.305 0 2.362-.94 2.362-2.1V5.7c0-1.16-1.057-2.1-2.362-2.1H6.224c-1.305 0-2.362.94-2.362 2.1v33.6c0 1.16 1.057 2.1 2.362 2.1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 406 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
|
||||
<path d="M39.452 12.433H10.548a4.398 4.398 0 1 1 0-8.796h28.904a4.398 4.398 0 0 1 0 8.796m-28.904 0H8.035a4.4 4.4 0 0 0-4.398 4.399V40.08a6.283 6.283 0 0 0 6.283 6.284h30.16a6.283 6.283 0 0 0 6.284-6.284V16.832c0-2.43-1.97-4.399-4.399-4.399h-2.513" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 421 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 48">
|
||||
<path d="M38.579 17.168c1.326 2.526 1.947 6.015 1.947 9.071 0 10.018-8.07 18.139-18.025 18.139S4.475 36.257 4.475 26.239c0-3.056.586-6.552 1.912-9.078m32.192.007a3.59 3.59 0 0 1-3.333-.976m3.333.976a3.6 3.6 0 0 0 1.765-.976m-33.827.996-.13-.027a3.6 3.6 0 0 1-1.731-.969m30.59 0a18 18 0 0 0-5.848-3.932 17.93 17.93 0 0 0-13.796 0 18 18 0 0 0-5.848 3.932 3.59 3.59 0 0 1-3.237.996m28.729-.996a3.59 3.59 0 0 0 5.098 0m-33.827.996a3.6 3.6 0 0 1-1.861-.996 3.644 3.644 0 0 1 0-5.13 25.2 25.2 0 0 1 8.187-5.505A25.1 25.1 0 0 1 22.5 3.623a25.1 25.1 0 0 1 9.657 1.934 25.2 25.2 0 0 1 8.187 5.505 3.644 3.644 0 0 1 0 5.13" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 786 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 60">
|
||||
<g>
|
||||
<path d="M3.84 5.884c0-1.21 1.048-2.192 2.34-2.192h107.64c1.293 0 2.34.981 2.34 2.192v48.23c0 1.212-1.047 2.193-2.34 2.193H6.18c-1.292 0-2.34-.981-2.34-2.192z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M103.056 15.093V9.83a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m0 35.078v-5.262a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m-93.6-35.077V9.83a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m0 35.078v-5.262a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1003 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 60">
|
||||
<g>
|
||||
<path d="M3.84 30c0-14.53 12.572-26.308 28.08-26.308h56.16c15.509 0 28.08 11.778 28.08 26.308s-12.571 26.307-28.08 26.307H31.92C16.412 56.307 3.84 44.53 3.84 30" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M80.592 15.093V9.83a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m0 35.077v-5.262a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877M31.92 15.093V9.83a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.876h-5.616a.936.877 0 0 1-.936-.876m0 35.077v-5.262a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.876h-5.616a.936.877 0 0 1-.936-.876" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1004 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
|
||||
<g>
|
||||
<path d="M3.765 40a36.235 36.235 0 1 0 72.47 0 36.235 36.235 0 1 0-72.47 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="m56.26 26.96-3.843-3.843a.906.906 0 0 1 0-1.28l3.843-3.844a.906.906 0 0 1 1.281 0l3.844 3.843a.906.906 0 0 1 0 1.281l-3.844 3.844a.906.906 0 0 1-1.28 0m-.001 34.66-3.843-3.843a.906.906 0 0 1 0-1.281l3.843-3.844a.906.906 0 0 1 1.281 0l3.843 3.844a.906.906 0 0 1 0 1.28l-3.843 3.844a.906.906 0 0 1-1.281 0m-34.423-34.66-3.843-3.843a.906.906 0 0 1 0-1.281l3.843-3.844a.906.906 0 0 1 1.281 0l3.844 3.844a.906.906 0 0 1 0 1.28l-3.844 3.844a.906.906 0 0 1-1.28 0m-.001 34.66-3.843-3.843a.906.906 0 0 1 0-1.281l3.843-3.843a.906.906 0 0 1 1.281 0l3.844 3.843a.906.906 0 0 1 0 1.281l-3.844 3.843a.906.906 0 0 1-1.28 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 988 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 60">
|
||||
<g>
|
||||
<path d="M3.84 12.461c0-4.843 4.19-8.77 9.36-8.77h93.6c5.17 0 9.36 3.927 9.36 8.77v35.077c0 4.843-4.19 8.77-9.36 8.77H13.2c-5.17 0-9.36-3.927-9.36-8.77z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M103.056 15.093V9.83a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m0 35.078v-5.262a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m-93.6-35.077V9.83a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m0 35.078v-5.262a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 997 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 50">
|
||||
<path d="M15 30.982v-1.71m3.086 5.128h1.542M15 39.527v-1.709M10.372 34.4h1.542M5.84 46.364h18.322c1.331 0 2.41-1.196 2.41-2.67V6.306c0-1.475-1.079-2.67-2.41-2.67H5.839c-1.331 0-2.41 1.195-2.41 2.67v37.386c0 1.475 1.079 2.67 2.41 2.67m14.56-29.91c0 3.304-2.417 5.982-5.4 5.982-2.981 0-5.4-2.678-5.4-5.981s2.419-5.982 5.4-5.982c2.983 0 5.4 2.678 5.4 5.982M18.858 34.4c0 2.36-1.727 4.273-3.857 4.273s-3.857-1.913-3.857-4.273S12.87 30.127 15 30.127s3.857 1.913 3.857 4.273" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 642 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 70">
|
||||
<g>
|
||||
<path d="M3.862 5.967c0-1.234 1.057-2.234 2.362-2.234h127.552c1.305 0 2.362 1 2.362 2.234v58.066c0 1.234-1.057 2.233-2.362 2.233H6.224c-1.305 0-2.362-1-2.362-2.233z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.531 15.346v-5.36a.945.893 0 0 1 .945-.893h5.67a.945.893 0 0 1 .944.893v5.36a.945.893 0 0 1-.945.894h-5.669a.945.893 0 0 1-.945-.894m113.379.001v-5.36a.945.893 0 0 1 .945-.894h5.67a.945.893 0 0 1 .944.894v5.36a.945.893 0 0 1-.945.893h-5.669a.945.893 0 0 1-.945-.893M9.53 60.013v-5.36a.945.893 0 0 1 .946-.893h5.669a.945.893 0 0 1 .945.893v5.36a.945.893 0 0 1-.945.894h-5.67a.945.893 0 0 1-.944-.894m113.38.001v-5.36a.945.893 0 0 1 .945-.894h5.669a.945.893 0 0 1 .945.894v5.36a.945.893 0 0 1-.945.893h-5.67a.945.893 0 0 1-.944-.893m-97.318-2.681h88.814" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 160">
|
||||
<g>
|
||||
<path d="M156.12 66.202a2.38 2.38 0 0 1-2.379 2.38H94.272c-13.138 0-23.788 10.65-23.788 23.787v61.372a2.38 2.38 0 0 1-2.379 2.38H6.257a2.38 2.38 0 0 1-2.378-2.38V6.257A2.38 2.38 0 0 1 6.257 3.88h147.484a2.38 2.38 0 0 1 2.38 2.378z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.588 16.249v-5.71a.95.95 0 0 1 .951-.95h5.71a.95.95 0 0 1 .95.95v5.71a.95.95 0 0 1-.95.951h-5.71a.95.95 0 0 1-.951-.951m133.212.001v-5.71a.95.95 0 0 1 .951-.951h5.71a.95.95 0 0 1 .95.951v5.71a.95.95 0 0 1-.95.951h-5.71a.95.95 0 0 1-.951-.951m0 45.672v-5.71a.95.95 0 0 1 .952-.951h5.709a.95.95 0 0 1 .951.951v5.71a.95.95 0 0 1-.951.951h-5.71a.95.95 0 0 1-.95-.951M9.587 149.46v-5.708a.95.95 0 0 1 .952-.952h5.709a.95.95 0 0 1 .951.952v5.709a.95.95 0 0 1-.951.951h-5.71a.95.95 0 0 1-.951-.951m47.576 0v-5.709a.95.95 0 0 1 .952-.951h5.709a.95.95 0 0 1 .951.951v5.71a.95.95 0 0 1-.951.95h-5.71a.95.95 0 0 1-.951-.95m51.382-90.396H95.569a34.6 34.6 0 0 0-34.6 34.6v12.976" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60">
|
||||
<path d="M3.692 5.884c0-1.21.981-2.192 2.192-2.192h48.23c1.212 0 2.193.981 2.193 2.192v48.23a2.19 2.19 0 0 1-2.192 2.193H5.885a2.19 2.19 0 0 1-2.193-2.192z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 329 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 90">
|
||||
<path d="M45.944 41.767a2.29 2.29 0 0 0 2.29 2.29H83.92a2.29 2.29 0 0 1 2.29 2.29V83.92a2.29 2.29 0 0 1-2.29 2.29H6.08a2.29 2.29 0 0 1-2.29-2.29V6.08a2.29 2.29 0 0 1 2.29-2.29h37.576a2.29 2.29 0 0 1 2.29 2.29z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 383 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 50">
|
||||
<path d="M38.188 8.7h10.598c1.211 0 2.193.956 2.193 2.136v28.2c0 1.18-.982 2.136-2.193 2.136H11.08c-1.21 0-2.192-.956-2.192-2.136v-28.2c0-1.18.981-2.136 2.192-2.136h10.599m10.885-5.128V15.11c0 1.18-.981 2.136-2.192 2.136h-.877c-1.21 0-2.192-.957-2.192-2.136V3.572m5.261 7.691h3.07c1.21 0 2.192-.956 2.192-2.136v-.855c0-1.18-.982-2.136-2.192-2.136h-3.07m-5.261 5.127h-3.07c-1.21 0-2.192-.956-2.192-2.136v-.855c0-1.18.982-2.136 2.193-2.136h3.069M5.817 46.299h48.231c1.21 0 2.192-.956 2.192-2.136V5.709c0-1.18-.981-2.137-2.192-2.137H5.818c-1.211 0-2.193.957-2.193 2.137v38.454c0 1.18.982 2.136 2.192 2.136M33.44 25.79c0 1.888-1.57 3.418-3.507 3.418s-3.508-1.53-3.508-3.418c0-1.887 1.57-3.418 3.508-3.418s3.507 1.53 3.507 3.418" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 897 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 50">
|
||||
<path d="M53.173 12.182c-.468 0-.847.354-.847.79s.38.79.847.79zm21.979 1.58c.467 0 .846-.353.846-.79 0-.436-.379-.79-.846-.79zM6.01 3.572v.79h77.842v-1.58H6.01zM86.14 5.71h-.847v38.454h1.694V5.709zM83.85 46.3v-.79H6.01v1.58h77.842zM3.72 44.163h.847V5.709H2.873v38.454zm2.29 2.136v-.79c-.797 0-1.443-.603-1.443-1.346H2.873c0 1.616 1.404 2.926 3.137 2.926zm80.131-2.136h-.847c0 .743-.646 1.346-1.442 1.346v1.58c1.732 0 3.136-1.31 3.136-2.926zm-2.29-40.59v.79c.797 0 1.443.602 1.443 1.346h1.694c0-1.617-1.404-2.927-3.136-2.927zm-77.841 0v-.79c-1.733 0-3.137 1.31-3.137 2.926h1.694c0-.744.646-1.347 1.443-1.347zm5.494 37.599v.79h28.39v-1.58h-28.39zm30.68-2.136h.846v-28.2h-1.694v28.2zm-32.97-28.2h-.846v28.2h1.694v-28.2zM39.895 8.7v-.79h-5.669v1.58h5.669zm-22.721 0v-.79h-5.669v1.58h5.669zm-7.958 2.136h.847c0-.744.645-1.346 1.442-1.346V7.91c-1.732 0-3.136 1.31-3.136 2.926zm32.968 0h.847c0-1.616-1.404-2.927-3.136-2.927v1.58c.797 0 1.442.603 1.442 1.347zm-2.29 30.336v.79c1.733 0 3.137-1.31 3.137-2.926h-1.694c0 .743-.645 1.346-1.442 1.346zm-28.389 0v-.79c-.797 0-1.442-.603-1.442-1.346H8.368c0 1.616 1.404 2.926 3.136 2.926zm13.737-23.927v.79h.916v-1.58h-.916zm3.205-2.136h.847V3.572H27.6V15.11zM22.952 3.572h-.847V15.11h1.693V3.572zm3.205 13.673v.79c1.732 0 3.136-1.31 3.136-2.926H27.6c0 .743-.646 1.346-1.443 1.346zm-.916 0v-.79c-.797 0-1.443-.603-1.443-1.346h-1.693c0 1.616 1.404 2.926 3.136 2.926zm3.205-5.982v.79h3.206v-1.58h-3.206zm5.495-2.136h.847v-.855h-1.694v.855zm-2.29-2.991v-.79h-3.205v1.58h3.206zm2.29 2.136h.847c0-1.616-1.404-2.926-3.136-2.926v1.58c.796 0 1.442.603 1.442 1.346zm-2.29 2.991v.79c1.733 0 3.137-1.31 3.137-2.926h-1.694c0 .743-.646 1.346-1.442 1.346zm-11.905 0v.79h3.206v-1.58h-3.206zm3.206-5.127v-.79h-3.206v1.58h3.206zm-5.495 2.136h-.847v.855h1.694v-.855zm2.29-2.136v-.79c-1.733 0-3.137 1.31-3.137 2.926h1.694c0-.743.646-1.346 1.442-1.346zm0 5.127v-.79c-.797 0-1.443-.603-1.443-1.346H16.61c0 1.616 1.404 2.926 3.136 2.926zm9.615 14.527h-.847c0 1.452-1.26 2.628-2.816 2.628v1.58c2.49 0 4.51-1.884 4.51-4.208zM25.7 29.208v-.79c-1.555 0-2.816-1.176-2.816-2.628H21.19c0 2.324 2.02 4.209 4.51 4.209zm-3.663-3.418h.847c0-1.451 1.26-2.628 2.816-2.628v-1.58c-2.49 0-4.51 1.884-4.51 4.208zm3.663-3.418v.79c1.555 0 2.816 1.177 2.816 2.628h1.694c0-2.324-2.02-4.208-4.51-4.208zm24.268 18.8v.79h28.39v-1.58h-28.39zm30.68-2.136h.846v-28.2H79.8v28.2zM78.356 8.7v-.79h-28.39v1.58h28.39zm-30.68 2.136h-.846v28.2h1.694v-28.2zm2.29-2.136v-.79c-1.732 0-3.136 1.31-3.136 2.926h1.694c0-.744.646-1.346 1.442-1.346zm30.68 2.136h.846c0-1.616-1.404-2.927-3.136-2.927v1.58c.797 0 1.443.603 1.443 1.347zm-2.29 30.336v.79c1.732 0 3.136-1.31 3.136-2.926H79.8c0 .743-.646 1.346-1.443 1.346zm-28.39 0v-.79c-.796 0-1.442-.603-1.442-1.346H46.83c0 1.616 1.404 2.926 3.136 2.926zm3.206-28.2v.79h21.979v-1.58h-21.98z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 35">
|
||||
<path d="m4.444 31.033 50.75-27.007m-50.752-.06 50.75 27.008M5.884 31.5h48.23c1.212 0 2.193-.895 2.193-2v-24c0-1.105-.981-2-2.192-2H5.885c-1.212 0-2.193.895-2.193 2v24c0 1.105.981 2 2.192 2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 363 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60">
|
||||
<path d="M3.772 29.59 29.88 4.383M4.57 55.43 55.43 30M4.175 4.383l26.701 23.863m-27.184.877L55.43 55.43M28.41 3.692H5.884c-1.21 0-2.192.981-2.192 2.192v48.23c0 1.212.981 2.193 2.192 2.193h48.23a2.19 2.19 0 0 0 2.193-2.192V31.589c0-1.21-.981-2.192-2.192-2.192h-21.32a2.19 2.19 0 0 1-2.193-2.192V5.885a2.19 2.19 0 0 0-2.192-2.193" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 501 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 40">
|
||||
<path d="M5.773 3.555h38.454a2.136 2.056 0 0 1 2.137 2.056v28.777a2.136 2.056 0 0 1-2.137 2.056H5.773a2.136 2.056 0 0 1-2.136-2.056V5.611a2.136 2.056 0 0 1 2.136-2.056" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 341 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 35">
|
||||
<g>
|
||||
<path d="M6.12 31.5h87.76a2.31 2 0 0 0 2.31-2v-24a2.31 2 0 0 0-2.31-2H6.12a2.31 2 0 0 0-2.31 2v24a2.31 2 0 0 0 2.31 2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.089 25.933c-.494.112-.776.546-.63.97s.664.675 1.157.564l-.263-.767zM90.912 9.067c.493-.112.775-.546.63-.97s-.664-.675-1.158-.564l.264.767zM11.875 26.956c.493-.112.775-.546.63-.97s-.664-.676-1.158-.564l.264.767zm3.988-2.556c-.493.112-.775.546-.63.969s.664.676 1.158.564l-.264-.766zm5.045.511c.493-.112.775-.545.63-.969s-.664-.676-1.158-.564l.264.766zm3.988-2.555c-.493.111-.775.545-.63.968.146.424.664.677 1.158.565l-.264-.767zm5.044.51c.494-.111.776-.545.63-.968-.145-.424-.664-.676-1.157-.565l.263.767zm3.99-2.555c-.495.112-.777.546-.63.97.145.423.663.675 1.157.563l-.264-.766zm5.043.511c.494-.112.776-.545.63-.969s-.664-.676-1.158-.564l.264.767zm3.989-2.555c-.494.111-.776.545-.63.969s.664.676 1.157.564l-.263-.767zm5.044.51c.494-.11.776-.545.63-.968-.146-.424-.664-.676-1.158-.565l.264.767zm3.989-2.555c-.494.112-.776.546-.63.97.145.423.664.675 1.157.564l-.264-.767zm5.044.511c.493-.111.775-.545.63-.969s-.664-.676-1.158-.564l.264.767zm3.988-2.555c-.493.112-.775.545-.63.969s.664.676 1.158.564l-.264-.767zm5.045.51c.493-.11.775-.545.63-.968-.146-.424-.664-.676-1.158-.564l.264.766zm3.988-2.555c-.493.112-.775.546-.63.97s.664.675 1.158.564l-.264-.767zm5.044.511c.494-.111.776-.545.63-.968-.145-.424-.664-.677-1.157-.565l.264.767zm3.99-2.555c-.494.112-.777.545-.63.969.145.423.663.676 1.157.564l-.264-.766zm5.043.511c.494-.112.776-.546.63-.969s-.664-.676-1.157-.564l.263.766zm3.989-2.556c-.494.112-.776.546-.63.97s.664.676 1.158.564l-.264-.767zM9.353 26.7l.263.767 2.259-.511-.264-.767-.264-.767-2.258.511zm6.774-1.533.264.766 4.517-1.022-.264-.767-.264-.766-4.517 1.022zm9.033-2.045.264.767 4.516-1.022-.264-.767-.263-.767-4.517 1.023zm9.033-2.044.264.766 4.516-1.022-.264-.766-.264-.767-4.516 1.022zm9.033-2.045.263.767 4.517-1.022-.264-.767-.264-.767-4.516 1.023zm9.032-2.044.264.767 4.517-1.023-.264-.766-.264-.767-4.516 1.022zm9.033-2.045.264.767 4.517-1.022-.264-.767-.264-.766-4.517 1.022zm9.033-2.044.264.767 4.516-1.023-.263-.766-.264-.767-4.517 1.022zm9.033-2.044.264.766 4.516-1.022-.264-.767-.263-.766-4.517 1.022zM88.39 8.81l.264.767 2.258-.511-.264-.767-.264-.767-2.258.511z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.089 9.067c-.494-.112-.776-.546-.63-.97s.664-.675 1.157-.564l-.263.767zm81.823 16.866c.493.112.775.546.63.97s-.664.675-1.158.564l.264-.767zM11.875 8.044c.493.112.775.546.63.97s-.664.676-1.158.564l.264-.767zm3.988 2.556c-.493-.112-.775-.546-.63-.969s.664-.676 1.158-.564l-.264.766zm5.045-.511c.493.112.775.545.63.969s-.664.676-1.158.564l.264-.766zm3.988 2.555c-.493-.111-.775-.545-.63-.968.146-.424.664-.677 1.158-.565l-.264.767zm5.044-.51c.494.111.776.545.63.968-.145.424-.664.676-1.157.565l.263-.767zm3.99 2.555c-.495-.112-.777-.546-.63-.97.145-.423.663-.675 1.157-.563l-.264.766zm5.043-.511c.494.112.776.545.63.969s-.664.676-1.158.564l.264-.767zm3.989 2.555c-.494-.111-.776-.545-.63-.969s.664-.676 1.157-.564l-.263.767zm5.044-.51c.494.11.776.545.63.968-.146.424-.664.676-1.158.565l.264-.767zm3.989 2.555c-.494-.112-.776-.546-.63-.97.145-.423.664-.675 1.157-.564l-.264.767zm5.044-.511c.493.111.775.545.63.969s-.664.676-1.158.564l.264-.767zm3.988 2.555c-.493-.112-.775-.545-.63-.969s.664-.676 1.158-.564l-.264.767zm5.045-.51c.493.11.775.545.63.968-.146.424-.664.676-1.158.564l.264-.766zm3.988 2.555c-.493-.112-.775-.546-.63-.97s.664-.675 1.158-.564l-.264.767zm5.044-.511c.494.111.776.545.63.968-.145.424-.664.677-1.157.565l.264-.767zm3.99 2.555c-.494-.112-.777-.545-.63-.969.145-.423.663-.676 1.157-.564l-.264.766zm5.043-.511c.494.112.776.546.63.969s-.664.676-1.157.564l.263-.766zm3.989 2.556c-.494-.112-.776-.546-.63-.97s.664-.676 1.158-.564l-.264.767zM9.353 8.3l.263-.767 2.259.511-.264.767-.264.767-2.258-.511zm6.774 1.533.264-.766 4.517 1.022-.264.767-.264.766-4.517-1.022zm9.033 2.045.264-.767 4.516 1.022-.264.767-.263.767-4.517-1.023zm9.033 2.044.264-.766 4.516 1.022-.264.766-.264.767-4.516-1.022zm9.033 2.045.263-.767 4.517 1.022-.264.767-.264.767-4.516-1.023zm9.032 2.044.264-.767 4.517 1.023-.264.766-.264.767-4.516-1.022zm9.033 2.045.264-.767 4.517 1.022-.264.767-.264.766-4.517-1.022zm9.033 2.044.264-.767 4.516 1.023-.263.766-.264.767-4.517-1.022zm9.033 2.044.264-.766 4.516 1.022-.264.767-.263.766-4.517-1.022zm9.033 2.045.264-.767 2.258.511-.264.767-.264.767-2.258-.511z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 25">
|
||||
<path d="M18.36 3.15v2.567c0 1.012-1.034 1.833-2.31 1.833h-.924c-1.275 0-2.309-.82-2.309-1.833V3.15m73.905 0v2.567c0 1.012-1.034 1.833-2.31 1.833h-.924c-1.275 0-2.31-.82-2.31-1.833V3.15m-75.29 0H93.65c1.276 0 2.31.82 2.31 1.833V19.65c0 1.012-1.034 1.833-2.31 1.833H5.888c-1.275 0-2.31-.82-2.31-1.833V4.983c0-1.012 1.035-1.833 2.31-1.833" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 511 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 90">
|
||||
<path d="M90 19.365v57.111m0-57.11V7.683m0 11.681c0 2.151-1.836 3.894-4.1 3.894H30.544a10.7 10.7 0 0 1-4.136-.825M90 19.365c0 2.151 1.836 3.894 4.1 3.894h55.356c1.472 0 2.871-.294 4.137-.825M90 76.476c0 5.376-4.59 9.735-10.25 9.735H30.543c-5.266 0-9.605-3.772-10.185-8.625M90 76.476c0 5.376 4.59 9.735 10.25 9.735h49.206c5.266 0 9.605-3.772 10.185-8.625M90 7.684c0-2.15-1.837-3.894-4.101-3.894H30.544c-5.661 0-10.25 4.358-10.25 9.735q0 .416.036.825M90 7.684c0-2.15 1.836-3.894 4.1-3.894h55.356c5.661 0 10.25 4.358 10.25 9.735q0 .416-.036.825m0 0a10.7 10.7 0 0 1 4.137-.825h2.05c5.662 0 10.252 4.358 10.252 9.734v45.43c0 5.376-4.59 9.734-10.251 9.734h-2.05c-1.484 0-2.894-.3-4.167-.837m0 0c-3.585-1.517-6.084-4.93-6.084-8.898V23.26q0-.417.036-.825c.32-3.622 2.728-6.68 6.077-8.084m-139.34 0c3.35 1.404 5.758 4.462 6.078 8.084q.036.408.036.825v45.43c0 3.967-2.5 7.38-6.085 8.897a10.7 10.7 0 0 1-4.166.837h-2.05c-5.662 0-10.251-4.358-10.251-9.735V23.26c0-5.376 4.59-9.734 10.25-9.734h2.05c1.473 0 2.872.294 4.138.825" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 170">
|
||||
<path d="M93.673 20.687V83.69m0-63.004v-12.6m0 12.6c0 2.32-1.913 4.2-4.273 4.2H31.704a10.8 10.8 0 0 1-4.311-.89m66.28-3.31c0 2.32 1.914 4.2 4.274 4.2h64.106c2.36 0 4.274-1.88 4.274-4.2M93.673 83.692c0 5.8-4.783 10.501-10.684 10.501H31.704c-5.489 0-10.011-4.068-10.616-9.304m72.585-1.197c0 5.8 4.784 10.501 10.684 10.501h51.286c5.9 0 10.684-4.701 10.684-10.5m0-63.005V83.69m0-63.004v-12.6m0 12.6c0 2.32 1.914 4.2 4.273 4.2h57.696c1.534 0 2.993-.317 4.312-.89m-66.28 59.695v71.923c0 5.799 4.782 10.5 10.684 10.5h51.284c5.49 0 10.012-4.068 10.616-9.304V84.888M93.673 8.086c0-2.32-1.913-4.2-4.273-4.2H31.704c-5.9 0-10.684 4.7-10.684 10.5q0 .45.038.89m72.615-7.19c0-2.32 1.914-4.2 4.274-4.2h64.106c2.36 0 4.274 1.88 4.274 4.2m0 0c0-2.32 1.914-4.2 4.273-4.2h57.696c5.9 0 10.685 4.7 10.685 10.5 0 .293.024.587 0 .874m-.07 69.628c1.327.58 2.797.904 4.344.904h2.136c5.901 0 10.684-4.702 10.684-10.501V24.887c0-5.8-4.783-10.5-10.684-10.5h-2.136c-5.596 0-10.187 4.227-10.647 9.61q-.038.44-.038.89V75.29c0 4.28 2.605 7.961 6.342 9.597M21.059 15.276a10.8 10.8 0 0 0-4.312-.89H14.61c-5.9 0-10.684 4.701-10.684 10.5v50.405c0 5.8 4.783 10.5 10.684 10.5h2.137c1.546 0 3.016-.322 4.342-.903 3.737-1.636 6.342-5.317 6.342-9.597V24.887q0-.45-.037-.89c-.334-3.907-2.844-7.206-6.335-8.72" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 90">
|
||||
<path d="M86.553 18.593V76.96m0-58.366V7.491m0 11.102c0 2.044-1.762 3.701-3.935 3.701H29.496c-1.413 0-2.756-.28-3.97-.784m61.027-2.917c0 2.044 1.762 3.701 3.935 3.701h59.024c2.174 0 3.935-1.657 3.935-3.7M86.553 76.958c0 5.11-4.404 9.252-9.837 9.252h-47.22c-5.054 0-9.218-3.585-9.775-8.198m66.832-1.054c0 5.11 4.404 9.252 9.837 9.252h47.22c5.433 0 9.837-4.142 9.837-9.252m0-58.366V76.96m0-58.366V7.491m0 11.102c0 2.044 1.762 3.701 3.936 3.701h53.122c1.413 0 2.756-.28 3.97-.784m-61.028 55.449c0 5.11 4.405 9.252 9.838 9.252h47.22c5.054 0 9.218-3.585 9.775-8.198M86.553 7.491c0-2.044-1.762-3.701-3.935-3.701H29.496c-5.433 0-9.838 4.142-9.838 9.252q0 .396.035.784m66.86-6.335c0-2.044 1.762-3.701 3.935-3.701h59.024c2.174 0 3.935 1.657 3.935 3.7m0 0c0-2.043 1.762-3.7 3.936-3.7h53.122c5.433 0 9.838 4.142 9.838 9.252 0 .258.022.517 0 .77m-.063 64.201c1.22.512 2.574.796 3.998.796h1.967c5.433 0 9.837-4.142 9.837-9.252V22.294c0-5.11-4.404-9.252-9.837-9.252h-1.967c-5.153 0-9.38 3.725-9.803 8.468q-.036.387-.035.784v47.263c0 3.771 2.399 7.015 5.84 8.456M19.693 13.826a10.3 10.3 0 0 0-3.97-.784h-1.967c-5.433 0-9.838 4.142-9.838 9.252v47.263c0 5.11 4.405 9.252 9.838 9.252h1.967c1.424 0 2.777-.284 3.998-.796 3.441-1.441 5.84-4.685 5.84-8.456V22.294a9 9 0 0 0-.035-.784c-.307-3.443-2.618-6.349-5.833-7.684" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60">
|
||||
<path d="M41.4 35.261a.877.877 0 1 0-1.754 0zm4.384 6.138a.877.877 0 1 0 0-1.753zM41.4 44.03a.877.877 0 1 0-1.753 0zm-4.384-2.63a.877.877 0 1 0 0-1.754zM5.885 3.691v.877h48.23V2.815H5.885zm50.422 2.192h-.877v48.23h1.754V5.885zm-2.192 50.423v-.877H5.885v1.754h48.23zM3.692 54.115h.877V5.885H2.815v48.23zm2.192 2.192v-.877a1.315 1.315 0 0 1-1.315-1.315H2.815a3.07 3.07 0 0 0 3.07 3.069zm50.423-2.192h-.877c0 .726-.589 1.315-1.315 1.315v1.754a3.07 3.07 0 0 0 3.069-3.07zM54.115 3.692v.877c.726 0 1.315.589 1.315 1.315h1.754a3.07 3.07 0 0 0-3.07-3.069zm-48.23 0v-.877a3.07 3.07 0 0 0-3.07 3.07h1.754c0-.727.589-1.316 1.315-1.316zm19.73 36.83h-.877a5.26 5.26 0 0 1-5.262 5.262v1.754a7.015 7.015 0 0 0 7.016-7.015zm-6.139 6.139v-.877a5.26 5.26 0 0 1-5.261-5.261h-1.754a7.015 7.015 0 0 0 7.015 7.015zm-6.138-6.138h.877a5.26 5.26 0 0 1 5.261-5.262v-1.754a7.015 7.015 0 0 0-7.015 7.016zm6.138-6.139v.877a5.26 5.26 0 0 1 5.262 5.262h1.754a7.015 7.015 0 0 0-7.016-7.016zm27.185-14.908h-.877a5.26 5.26 0 0 1-5.261 5.262v1.754a7.015 7.015 0 0 0 7.015-7.016zm-6.138 6.139v-.877a5.26 5.26 0 0 1-5.262-5.262h-1.754a7.015 7.015 0 0 0 7.016 7.016zm-6.139-6.139h.877a5.26 5.26 0 0 1 5.262-5.261v-1.754a7.015 7.015 0 0 0-7.016 7.015zm6.139-6.138v.877a5.26 5.26 0 0 1 5.261 5.261h1.754a7.015 7.015 0 0 0-7.015-7.015zm4.384 27.185h-.877a3.51 3.51 0 0 1-3.507 3.507v1.754a5.26 5.26 0 0 0 5.261-5.261zm-4.384 4.384v-.877a3.51 3.51 0 0 1-3.508-3.507H35.26a5.26 5.26 0 0 0 5.262 5.261zm-4.385-4.384h.877a3.51 3.51 0 0 1 3.508-3.508V35.26a5.26 5.26 0 0 0-5.262 5.262zm4.385-4.385v.877a3.51 3.51 0 0 1 3.507 3.508h1.754a5.26 5.26 0 0 0-5.261-5.262zm0 .877h.876V35.26h-1.753v1.754zm3.508 3.508v.876h1.753v-1.753h-1.753zm-3.508 5.261h.876V44.03h-1.753v1.754zm-5.261-5.261v.876h1.753v-1.753h-1.753zm-9.647-21.047h-.877a5.26 5.26 0 0 1-5.262 5.262v1.754a7.015 7.015 0 0 0 7.016-7.016zm-6.139 6.139v-.877a5.26 5.26 0 0 1-5.261-5.262h-1.754a7.015 7.015 0 0 0 7.015 7.016zm-6.138-6.139h.877a5.26 5.26 0 0 1 5.261-5.261v-1.754a7.015 7.015 0 0 0-7.015 7.015zm6.138-6.138v.877a5.26 5.26 0 0 1 5.262 5.261h1.754a7.015 7.015 0 0 0-7.016-7.015z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 90">
|
||||
<g>
|
||||
<path d="M3.879 6.08c0-1.265 1.065-2.29 2.378-2.29h147.484c1.314 0 2.38 1.025 2.38 2.29v77.84c0 1.265-1.066 2.29-2.38 2.29H6.257c-1.313 0-2.378-1.025-2.378-2.29z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M138.994 19.358v-5.495a.952.916 0 0 1 .951-.915h5.71a.952.916 0 0 1 .95.915v5.495a.952.916 0 0 1-.95.916h-5.71a.952.916 0 0 1-.951-.916m-.001 56.778v-5.495a.952.916 0 0 1 .952-.915h5.709a.952.916 0 0 1 .951.915v5.495a.952.916 0 0 1-.951.916h-5.71a.952.916 0 0 1-.95-.916m-125.6-56.778v-5.495a.952.916 0 0 1 .951-.916h5.71a.952.916 0 0 1 .95.916v5.495a.952.916 0 0 1-.95.915h-5.71a.952.916 0 0 1-.951-.915m-.001 56.779v-5.495a.952.916 0 0 1 .952-.916h5.709a.952.916 0 0 1 .951.916v5.495a.952.916 0 0 1-.951.915h-5.71a.952.916 0 0 1-.95-.915" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1007 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 100">
|
||||
<g>
|
||||
<path d="M3.892 50c0-25.51 21.418-46.19 47.838-46.19h76.54c26.42 0 47.839 20.68 47.839 46.19S154.69 96.19 128.27 96.19H51.73C25.31 96.19 3.892 75.51 3.892 50" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M122.53 19.514V13.97a.957.924 0 0 1 .956-.924h5.74a.957.924 0 0 1 .958.924v5.543a.957.924 0 0 1-.957.924h-5.74a.957.924 0 0 1-.958-.924m.001 66.516v-5.543a.957.924 0 0 1 .957-.924h5.74a.957.924 0 0 1 .957.924v5.543a.957.924 0 0 1-.957.924h-5.74a.957.924 0 0 1-.957-.924M49.816 19.515v-5.543a.957.924 0 0 1 .957-.924h5.74a.957.924 0 0 1 .958.924v5.542a.957.924 0 0 1-.957.924h-5.74a.957.924 0 0 1-.958-.924m0 66.516v-5.543a.957.924 0 0 1 .957-.924h5.74a.957.924 0 0 1 .957.924v5.543a.957.924 0 0 1-.957.923h-5.74a.957.924 0 0 1-.957-.924" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1001 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 90">
|
||||
<g>
|
||||
<path d="M3.879 17.527c0-7.587 6.39-13.737 14.272-13.737h123.696c7.883 0 14.273 6.15 14.273 13.737v54.947c0 7.587-6.39 13.737-14.273 13.737H18.151c-7.882 0-14.272-6.15-14.272-13.737z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M138.994 19.358v-5.495a.952.916 0 0 1 .951-.915h5.71a.952.916 0 0 1 .95.915v5.495a.952.916 0 0 1-.95.916h-5.71a.952.916 0 0 1-.951-.916m-.001 56.778v-5.495a.952.916 0 0 1 .952-.915h5.709a.952.916 0 0 1 .951.915v5.495a.952.916 0 0 1-.951.916h-5.71a.952.916 0 0 1-.95-.916m-125.6-56.778v-5.495a.952.916 0 0 1 .951-.916h5.71a.952.916 0 0 1 .95.916v5.495a.952.916 0 0 1-.95.915h-5.71a.952.916 0 0 1-.951-.915m-.001 56.779v-5.495a.952.916 0 0 1 .952-.916h5.709a.952.916 0 0 1 .951.916v5.495a.952.916 0 0 1-.951.915h-5.71a.952.916 0 0 1-.95-.915" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M3.826 55a51.174 51.174 0 1 0 102.349 0A51.174 51.174 0 1 0 3.826 55" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="m81.005 32.303-3.947-3.947a.93.93 0 0 1 0-1.316l3.947-3.948a.93.93 0 0 1 1.316 0l3.948 3.948a.93.93 0 0 1 0 1.316l-3.948 3.947a.93.93 0 0 1-1.316 0m0 54.897-3.947-3.948a.93.93 0 0 1 0-1.316l3.947-3.947a.93.93 0 0 1 1.316 0l3.948 3.947a.93.93 0 0 1 0 1.316L82.32 87.2a.93.93 0 0 1-1.316 0M27.04 32.304l-3.947-3.948a.93.93 0 0 1 0-1.315l3.948-3.948a.93.93 0 0 1 1.315 0l3.948 3.948a.93.93 0 0 1 0 1.315l-3.948 3.948a.93.93 0 0 1-1.315 0m-.001 54.604-3.947-3.948a.93.93 0 0 1 0-1.316l3.948-3.947a.93.93 0 0 1 1.315 0l3.948 3.947a.93.93 0 0 1 0 1.316l-3.948 3.948a.93.93 0 0 1-1.315 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 965 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 70">
|
||||
<g>
|
||||
<path d="M3.555 48.976c0 9.55 7.125 17.29 15.914 17.29h1.061c8.79 0 15.914-7.74 15.914-17.29V5.966c0-1.233-.92-2.233-2.056-2.233H5.611c-1.135 0-2.056 1-2.056 2.234z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.666 49.12c0 7.003 5.225 12.68 11.67 12.68h1.327c6.445 0 11.67-5.677 11.67-12.68V31.873c0-1.233-.92-2.233-2.056-2.233H9.722c-1.135 0-2.056 1-2.056 2.233zM33.792 3.733c1.465 0 2.652 1.29 2.652 2.882v12.103c0 1.592-1.187 2.882-2.652 2.882H6.208c-1.465 0-2.653-1.29-2.653-2.882V6.615c0-1.591 1.188-2.882 2.653-2.882z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24.11 50.186c0 2.467-1.84 4.467-4.11 4.467s-4.111-2-4.111-4.467 1.84-4.466 4.11-4.466 4.112 2 4.112 4.466" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1008 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 55">
|
||||
<g>
|
||||
<path d="M3.555 34.56c0 9.263 7.125 16.774 15.914 16.774h1.061c8.79 0 15.914-7.51 15.914-16.775V5.833c0-1.196-.92-2.166-2.056-2.166H5.611c-1.135 0-2.056.97-2.056 2.166z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.666 34.7c0 6.793 5.225 12.3 11.67 12.3h1.327c6.445 0 11.67-5.507 11.67-12.3V14.5c0-1.197-.92-2.167-2.056-2.167H9.722c-1.135 0-2.056.97-2.056 2.167z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24.11 35.733c0 2.394-1.84 4.334-4.11 4.334s-4.111-1.94-4.111-4.334 1.84-4.333 4.11-4.333 4.112 1.94 4.112 4.333" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 853 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 28">
|
||||
<path d="M45.134 10.766H6.18c-1.292 0-2.34.848-2.34 1.894v2.68c0 1.045 1.048 1.893 2.34 1.893h38.954m0-6.467V5.288c0-1.046 1.048-1.894 2.34-1.894h26.704c1.292 0 2.34.848 2.34 1.894v5.478m-31.384 0h31.384m0 0h37.302c1.293 0 2.34.848 2.34 1.894v2.68c0 1.045-1.047 1.893-2.34 1.893H76.518m-31.384 0v5.479c0 1.046 1.048 1.894 2.34 1.894h26.704c1.292 0 2.34-.848 2.34-1.894v-5.479m-31.384 0h31.384" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 567 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 15">
|
||||
<path d="M35.664 11.4c-.517 0-.936.269-.936.6s.419.6.936.6zm49.553 1.2c.517 0 .936-.269.936-.6s-.42-.6-.936-.6zM6.18 3v.6h107.64V2.4H6.18zm109.98 1.5h-.936v1.8h1.872V4.5zm-2.34 3.3v-.6H6.18v1.2h107.64zM3.84 6.3h.936V4.5H2.904v1.8zm2.34 1.5v-.6c-.775 0-1.404-.403-1.404-.9H2.904c0 1.16 1.467 2.1 3.276 2.1zm109.98-1.5h-.936c0 .497-.629.9-1.404.9v1.2c1.81 0 3.276-.94 3.276-2.1zM113.82 3v.6c.775 0 1.404.403 1.404.9h1.872c0-1.16-1.467-2.1-3.276-2.1zM6.18 3v-.6c-1.81 0-3.276.94-3.276 2.1h1.872c0-.497.629-.9 1.404-.9zm61.308 4.2h-.936V12h1.872V7.2zM52.512 12h.936V7.2h-1.872V12zm-16.848 0v.6h49.553v-1.2H35.664z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 784 B |