Compare commits

..
Author SHA1 Message Date
github-actions[bot] e2ff2acb7a dev-build: 674e589ad8
Source: 674e589ad8
Issue: #657
User-Visible: no
2026-09-29 06:30:42 +00:00
2105 changed files with 5036 additions and 488477 deletions
-14
View File
@@ -1,14 +0,0 @@
* text=auto eol=lf
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.ico binary
*.pdf binary
*.mp4 binary
*.webm binary
*.zip binary
*.woff binary
*.woff2 binary
-14
View File
@@ -1,14 +0,0 @@
#!/bin/sh
set -eu
message_file=$1
# Git-generated merge commits do not represent an independently authored
# product change and inherit provenance from their parents.
case "${message_file##*/}" in
MERGE_MSG) exit 0 ;;
esac
repo_root=$(git rev-parse --show-toplevel)
node "$repo_root/scripts/validate-commit-provenance.mjs" \
--message-file "$message_file" --staged --check-hook-mode
-90
View File
@@ -1,90 +0,0 @@
#!/bin/sh
set -eu
# PROCESS.md 10.1: the blocking process gate lives here, because commits go
# straight to dev without pull requests and GitHub blocks nothing on its side.
# CI still runs the same script (10.3), but by then the code is already in dev —
# that catch-up pass reports, it does not prevent.
#
# Git feeds one line per ref on stdin:
# <local ref> <local sha> <remote ref> <remote sha>
repo_root=$(git rev-parse --show-toplevel)
gate="$repo_root/scripts/process-gate.mjs"
zero=$(printf '%040d' 0)
# The gate reasons about commits. A repository without it — an old checkout, a
# bisect, a worktree from before the script existed — must still be pushable.
if [ ! -f "$gate" ]; then
exit 0
fi
# Reading issue status needs gh, and a hook that cannot work on a train is a
# hook people disable. Offline the checks that need no network still run, and the
# strict pass happens in CI, where gh is always present.
issues_flag=""
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
issues_flag="--issues"
else
echo "process-gate: gh недоступен, проверка статуса issue пропущена — её выполнит CI" >&2
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
continue
fi
# Tags carry no process state of their own: the commit they point at was
# already checked when it was pushed.
case "$local_ref" in
refs/tags/*) continue ;;
esac
if [ "$remote_sha" = "$zero" ]; then
# A branch that does not exist on the remote yet. Everything it adds on top
# of dev is new, so that is the range — not the whole history, which would
# drag in every violation committed before the gate existed.
base=$(git merge-base "$local_sha" refs/remotes/origin/dev 2>/dev/null || true)
if [ -z "$base" ]; then
echo "process-gate: не нашёл общего предка с origin/dev, проверяю последние 20 коммитов" >&2
base="$local_sha~20"
fi
else
base="$remote_sha"
fi
echo "process-gate: $local_ref, диапазон ${base}..${local_sha}" >&2
# shellcheck disable=SC2086
if ! node "$gate" --range "${base}..${local_sha}" --target-ref "$remote_ref" $issues_flag >&2; then
status=1
fi
done
if [ "$status" -ne 0 ]; then
cat >&2 <<'EOF'
Push остановлен: нарушен процесс (PROCESS.md §10.2).
Починить надо причину, а не симптом. Если нарушение уже опубликовано, его
исправляет следующий коммит плюс issue с меткой `process` — не force-push
(§12, правило 17).
Обойти проверку можно через `git push --no-verify`, и тогда то же самое найдёт
job `process-gate` в Validate — уже после того, как код окажется в dev.
EOF
fi
exit "$status"
-36
View File
@@ -1,36 +0,0 @@
name: Bug report
description: Something in House Plan does not work as expected
labels: [bug]
body:
- type: input
id: version
attributes:
label: House Plan version
placeholder: v1.13.0
validations:
required: true
- type: input
id: ha_version
attributes:
label: Home Assistant version
placeholder: "2026.6"
validations:
required: true
- type: textarea
id: what
attributes:
label: What happened / what did you expect?
description: Steps to reproduce help a lot.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs / browser console errors
description: "Settings → System → Logs (search: houseplan) and the browser console (F12)."
render: text
- type: textarea
id: diagnostics
attributes:
label: Diagnostics
description: "Settings → Devices & services → House Plan → ⋯ → Download diagnostics (personal fields are redacted automatically)."
-8
View File
@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: 💬 Telegram chat (@ha_houseplan)
url: https://t.me/ha_houseplan
about: Questions, setup help, ideas and screenshots — the fastest way to get an answer.
- name: 💡 GitHub discussions
url: https://github.com/Matysh/houseplan-card/discussions
about: Longer-form ideas and show-and-tell.
@@ -1,14 +0,0 @@
name: Feature request
description: An idea to make House Plan better
labels: [enhancement]
body:
- type: textarea
id: problem
attributes:
label: What problem would this solve?
validations:
required: true
- type: textarea
id: proposal
attributes:
label: How do you imagine it working?
-96
View File
@@ -1,96 +0,0 @@
name: Анонс релиза
# 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
# remains allowed to send a test message.
on:
release:
types: [published]
workflow_dispatch: {}
workflow_call:
inputs:
reusable:
required: true
type: boolean
tag:
required: true
type: string
release_name:
required: true
type: string
url:
required: true
type: string
prerelease:
required: true
type: boolean
ref:
required: true
type: string
secrets:
TELEGRAM_BOT_TOKEN:
required: true
TELEGRAM_CHAT_ID:
required: true
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
with:
ref: ${{ inputs.ref }}
- name: Send to Telegram
env:
TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
CALLED: ${{ inputs.reusable }}
INPUT_TAG: ${{ inputs.tag }}
INPUT_NAME: ${{ inputs.release_name }}
INPUT_URL: ${{ inputs.url }}
INPUT_PRE: ${{ inputs.prerelease }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_NAME: ${{ github.event.release.name }}
RELEASE_URL: ${{ github.event.release.html_url }}
RELEASE_PRE: ${{ github.event.release.prerelease }}
# The body goes through env, never through shell interpolation —
# release notes are arbitrary text.
RELEASE_BODY: ${{ github.event.release.body }}
EVENT: ${{ github.event_name }}
run: |
set -euo pipefail
if [ "$EVENT" = "workflow_dispatch" ] && [ "$CALLED" != "true" ]; then
TEXT="✅ Тест: оповещения о релизах houseplan-card подключены."
else
if [ "$CALLED" = "true" ]; then
TAG=$INPUT_TAG
NAME=$INPUT_NAME
URL=$INPUT_URL
PRE=$INPUT_PRE
BODY=$(cat docs/RELEASE-NOTES.md)
else
TAG=$RELEASE_TAG
NAME=$RELEASE_NAME
URL=$RELEASE_URL
PRE=$RELEASE_PRE
BODY=$RELEASE_BODY
fi
if [ "$PRE" = "true" ]; then
echo "Prerelease Telegram announcement is disabled"
exit 0
fi
KIND="🏠 Релиз"
SUMMARY=$(printf '%s' "$BODY" | head -c 2500)
TEXT=$(printf '%s houseplan-card %s — %s\n\n%s\n\n%s' \
"$KIND" "$TAG" "$NAME" "$SUMMARY" "$URL")
fi
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
-154
View File
@@ -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
-92
View File
@@ -1,92 +0,0 @@
name: Мутационный гейт
# Реестр известных поломок (issue #85): каждый мутант ломает продуктовый код
# известным способом, и объявленный тест ОБЯЗАН на этом покраснеть. Тест,
# оставшийся зелёным на сломанном коде, ничего не защищает — он лишь выглядит
# защитой, и это хуже его отсутствия.
#
# Прогон дорогой, поэтому он не входит в Validate и не идёт на каждый push.
# Его место — перед стабильным релизом (PROCESS.md §8) и раз в неделю по
# расписанию, чтобы дрейф тестов не копился до релиза. Дешёвая половина —
# «якоря патчей живы, guard-файлы существуют» — идёт с обычными юнитами:
# test/mutation-gate.test.mjs.
#
# #332: бандл собирается только мутантам с браузерным гвардом (guardNeedsBundle),
# компиляция тестов в worktree стартует с тёплого test-build (инкрементальный
# tsc), а реестр режется на четыре чересполосных шарда — полный прогон
# укладывается в десятки минут вместо часов. Локальный дифф-режим:
# node scripts/mutation-gate.mjs --changed origin/dev..HEAD
on:
workflow_dispatch:
inputs:
ref:
description: Git ref whose mutation guards must be proved
required: false
default: dev
schedule:
# Понедельник, 05:20 UTC — до начала рабочего дня владельца.
- cron: '20 5 * * 1'
permissions:
contents: read
concurrency:
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.
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || 'dev' }}
fetch-depth: 0
- uses: actions/setup-node@v7
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
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Установить Chromium
if: steps.pw.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- name: Реестр применим к текущему коду
run: node scripts/mutation-gate.mjs --check
- name: Тёплый test-build для инкрементальной компиляции мутантов
run: npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs
- name: Каждый тест ловит свою поломку
run: node scripts/mutation-gate.mjs --shard=${{ matrix.shard }}/4
-265
View File
@@ -1,265 +0,0 @@
name: Полные бенчмарки производительности
on:
# Every main promotion is a stable-release candidate and must have an
# exact-SHA full comparison before stable assets are published.
push:
branches:
- main
schedule:
- cron: "0 4 * * 1"
workflow_dispatch:
inputs:
comparison_ref:
description: "Optional baseline tag, branch or SHA; empty uses the candidate parent"
required: false
type: string
permissions:
contents: read
concurrency:
group: full-performance-${{ github.ref }}
cancel-in-progress: false
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.
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
profile:
- large-house
- isometric
- plan-snap
- interaction
- blend
- overlay
- space-default
- space-glow
steps:
- name: Check out candidate
uses: actions/checkout@v7
with:
path: candidate
fetch-depth: 2
- name: Resolve comparison SHA
id: base
working-directory: candidate
env:
EVENT_NAME: ${{ github.event_name }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
MANUAL_BASE: ${{ inputs.comparison_ref }}
run: |
set -euo pipefail
if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
git fetch --force --tags --prune --unshallow origin
else
git fetch --force --tags --prune origin
fi
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ -n "$MANUAL_BASE" ]; then
sha="$(git rev-parse "${MANUAL_BASE}^{commit}" 2>/dev/null || true)"
source="manual comparison ref $MANUAL_BASE"
elif [ "$EVENT_NAME" = "push" ] && [ -n "$PUSH_BEFORE_SHA" ] && ! printf '%s' "$PUSH_BEFORE_SHA" | grep -Eq '^0+$'; then
sha="$PUSH_BEFORE_SHA"
source="push before"
else
sha="$(git rev-parse HEAD^ 2>/dev/null || true)"
source="candidate parent"
fi
requested_sha="$sha"
usable=true
reason=""
if [ -z "$sha" ] || ! git cat-file -e "${sha}^{commit}" 2>/dev/null; then
usable=false
reason="commit is not present after fetching all remote refs"
elif [ "$source" = "push before" ] && ! git merge-base --is-ancestor "$sha" HEAD; then
usable=false
reason="commit is no longer an ancestor of the pushed revision"
fi
if [ "$usable" != true ]; then
parent_sha="$(git rev-parse HEAD^ 2>/dev/null || true)"
if [ -n "$parent_sha" ] && [ "$parent_sha" != "$(git rev-parse HEAD)" ]; then
sha="$parent_sha"
source="candidate parent (unusable requested-base fallback)"
echo "::warning::Comparison SHA ${requested_sha:-none} is unusable ($reason); using candidate parent $sha."
usable=true
fi
fi
if [ "$usable" != true ]; then
fallback_tag=""
fallback_sha=""
head_sha="$(git rev-parse HEAD)"
while IFS= read -r tag; do
case "$tag" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*) continue ;;
esac
tag_sha="$(git rev-list -n 1 "$tag")"
if [ "$tag_sha" != "$head_sha" ]; then
fallback_tag="$tag"
fallback_sha="$tag_sha"
break
fi
done < <(git tag --merged HEAD --sort=-version:refname)
if [ -z "$fallback_sha" ]; then
echo "::error::No usable comparison commit or previous release tag is reachable from HEAD."
exit 1
fi
sha="$fallback_sha"
source="release tag $fallback_tag"
echo "::warning::Using $fallback_tag ($sha) as the comparison base."
fi
if ! git cat-file -e "${sha}:demo/bundle-freshness.mjs" 2>/dev/null; then
echo "::warning::Comparison $sha predates HP-PERF-01; using candidate parent HEAD^."
sha="$(git rev-parse HEAD^)"
source="candidate parent (HP-PERF-01 compatibility)"
fi
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "Comparison base: $sha ($source)" >> "$GITHUB_STEP_SUMMARY"
- name: Check out base SHA
uses: actions/checkout@v7
with:
ref: ${{ steps.base.outputs.sha }}
path: baseline
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
cache-dependency-path: |
candidate/package-lock.json
baseline/package-lock.json
- 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
- name: Build both exact source trees
run: |
npm --prefix candidate run build
(cd candidate && node scripts/bundle-sync.mjs)
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
fi
- name: Capture base and candidate profile
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
;;
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
- 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 --output=../artifacts/performance/isometric-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
if: always()
uses: actions/upload-artifact@v7
with:
name: full-performance-${{ matrix.profile }}
path: artifacts/performance
File diff suppressed because it is too large Load Diff
-258
View File
@@ -1,258 +0,0 @@
name: Публикация пре-релиза (ручная)
run-name: Publish ${{ inputs.tag }}
on:
workflow_dispatch:
inputs:
tag:
description: "Exact prerelease tag, for example v1.61.0-beta.4"
required: true
type: string
permissions:
contents: write
actions: read
concurrency:
group: publish-prerelease-${{ inputs.tag }}
cancel-in-progress: false
jobs:
gate:
name: "Гейт: зелёная Проверка и релизный контракт"
runs-on: ubuntu-latest
outputs:
sha: ${{ steps.candidate.outputs.sha }}
tag: ${{ steps.candidate.outputs.tag }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
fetch-depth: 0
- uses: actions/setup-node@v7
with: { node-version: 22 }
- name: Pin the current dev candidate
id: candidate
env:
TAG: ${{ inputs.tag }}
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
test "$REF_NAME" = "dev" || {
echo "::error::Prereleases must be dispatched from the dev branch, got $REF_NAME"
exit 1
}
SHA=$(git rev-parse HEAD)
git fetch origin dev
test "$(git rev-parse origin/dev)" = "$SHA" || {
echo "::error::The dispatched SHA is no longer the origin/dev tip"
exit 1
}
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Verify version, changelogs and bilingual release notes
env:
TAG: ${{ inputs.tag }}
run: node scripts/release-contract.mjs "$TAG" --repo="$GITHUB_REPOSITORY"
- name: Require green Validate for this exact SHA
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
SHA: ${{ steps.candidate.outputs.sha }}
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
with:
ref: ${{ needs.gate.outputs.sha }}
fetch-depth: 0
- uses: actions/setup-node@v7
with: { node-version: 22 }
- name: Build and verify both release assets before publication
env:
TAG: ${{ needs.gate.outputs.tag }}
run: |
set -euo pipefail
npm ci
npm run build
node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend
npm run bundle:budget
VERSION=${TAG#v}
grep -RFq "$VERSION" dist
(cd custom_components/houseplan && zip -qr ../../houseplan.zip .)
node scripts/verify-houseplan-zip.mjs houseplan.zip \
custom_components/houseplan/frontend "$VERSION"
test -s dist/houseplan-card.js
test -s houseplan.zip
- name: Create or verify the annotated tag
env:
TAG: ${{ needs.gate.outputs.tag }}
SHA: ${{ needs.gate.outputs.sha }}
run: |
set -euo pipefail
REMOTE=$(git ls-remote --tags origin "refs/tags/$TAG" "refs/tags/$TAG^{}")
if [ -n "$REMOTE" ]; then
PEELED=$(printf '%s\n' "$REMOTE" | awk -v ref="refs/tags/$TAG^{}" '$2 == ref {print $1}')
test -n "$PEELED" || {
echo "::error::Existing remote tag $TAG is not annotated"
exit 1
}
test "$PEELED" = "$SHA" || {
echo "::error::Existing tag $TAG points to $PEELED, expected $SHA"
exit 1
}
git fetch --force origin "refs/tags/$TAG:refs/tags/$TAG"
test "$(git cat-file -t "refs/tags/$TAG")" = "tag"
else
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" "$SHA" -m "$TAG"
git push origin "$TAG"
fi
- name: Stage, verify and publish the prerelease
id: release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.gate.outputs.tag }}
run: |
set -euo pipefail
if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag \
--draft --prerelease --title "$TAG" --notes-file docs/RELEASE-NOTES.md
fi
WAS_DRAFT=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq .isDraft)
echo "newly_published=$WAS_DRAFT" >> "$GITHUB_OUTPUT"
gh release upload "$TAG" dist/houseplan-card.js houseplan.zip \
--repo "$GITHUB_REPOSITORY" --clobber
RELEASE_JSON=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \
--json tagName,isDraft,isPrerelease,assets,url)
export RELEASE_JSON TAG
node <<'NODE'
const release = JSON.parse(process.env.RELEASE_JSON);
if (release.tagName !== process.env.TAG) throw new Error('release tag mismatch');
const assets = new Map(release.assets.map((asset) => [asset.name, asset]));
for (const name of ['houseplan-card.js', 'houseplan.zip']) {
if (!(Number(assets.get(name)?.size) > 0)) throw new Error(`${name} is missing or empty`);
}
NODE
gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --draft=false --prerelease \
--title "$TAG" --notes-file docs/RELEASE-NOTES.md
- name: Verify the public release and assets
id: verify
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.gate.outputs.tag }}
SHA: ${{ needs.gate.outputs.sha }}
run: |
set -euo pipefail
RELEASE_JSON=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \
--json tagName,isDraft,isPrerelease,assets,url)
export RELEASE_JSON TAG
node <<'NODE'
const release = JSON.parse(process.env.RELEASE_JSON);
if (release.tagName !== process.env.TAG || release.isDraft || !release.isPrerelease)
throw new Error('release is not a public prerelease for the requested tag');
const assets = new Map(release.assets.map((asset) => [asset.name, asset]));
for (const name of ['houseplan-card.js', 'houseplan.zip']) {
if (!(Number(assets.get(name)?.size) > 0)) throw new Error(`${name} is missing or empty`);
}
NODE
test "$(git rev-list -n 1 "$TAG")" = "$SHA"
URL=$(node -p "JSON.parse(process.env.RELEASE_JSON).url")
echo "url=$URL" >> "$GITHUB_OUTPUT"
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
env:
EXPECTED_TAG: ${{ needs.gate.outputs.tag }}
with:
script: |
const releases = await github.paginate(github.rest.repos.listReleases, {
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
});
const first = releases.find((release) => release.prerelease && !release.draft);
if (first?.tag_name !== process.env.EXPECTED_TAG) {
core.setFailed(
`HACS prerelease discovery is stale: ${first?.tag_name ?? 'none'} precedes ` +
process.env.EXPECTED_TAG,
);
}
# PROCESS.md 10.2 item 10: closing issues and stripping status labels happens
# because a beta was published, not because someone remembered to do it. The
# manual step was skipped twice, and both times it broke the invariant that a
# closed issue carries no status label — the one thing `verify` relies on.
#
# 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
permissions:
contents: read
# Deliberately the stock token, not a PAT: events caused by GITHUB_TOKEN do
# not start workflows, so removing the label cannot wake the review
# pipeline. A PAT here would build a cascade out of a bookkeeping step.
issues: write
steps:
- name: Close the S8-merged queue and strip status labels
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
TAG: ${{ needs.gate.outputs.tag }}
URL: ${{ needs.publish.outputs.url }}
run: |
set -euo pipefail
# Only the owner's issues take part in the process; issues filed by
# anyone else never carry status labels and are not ours to close.
numbers=$(gh issue list --repo "$REPO" --state open --label S8-merged \
--author Matysh --limit 100 --json number --jq '.[].number')
if [ -z "$numbers" ]; then
echo "the S8-merged queue is empty, nothing to close"
else
for n in $numbers; do
gh issue comment "$n" --repo "$REPO" \
--body "Выпущено в \`$TAG\` · [релиз]($URL)"
# Label first, then close. If the run dies between the two steps an
# open issue without a status is visible and fixable in the flow;
# the reverse order would recreate the exact breakage this job is
# here to prevent.
gh issue edit "$n" --repo "$REPO" --remove-label S8-merged
gh issue close "$n" --repo "$REPO" --reason completed
echo "closed #$n"
done
fi
# Targeted at the defect that actually recurs, not at the invariant in
# general: no closed issue may still carry S8-merged.
leftover=$(gh issue list --repo "$REPO" --state closed --label S8-merged \
--limit 100 --json number --jq 'length')
test "$leftover" = "0" || {
echo "::error::$leftover closed issues still carry S8-merged"
exit 1
}
announce:
name: Комментарий о публикации
needs: [gate, publish]
if: ${{ needs.publish.outputs.newly_published == 'true' }}
uses: ./.github/workflows/announce.yml
with:
reusable: true
tag: ${{ needs.gate.outputs.tag }}
release_name: ${{ needs.gate.outputs.tag }}
url: ${{ needs.publish.outputs.url }}
prerelease: true
ref: ${{ needs.gate.outputs.tag }}
secrets: inherit
-41
View File
@@ -1,41 +0,0 @@
name: HACS-zip к релизу
# 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
# (owner request, 2026-08-08). Like announce.yml, the workflow file lives at
# the TAGGED commit: betas cut from dev pick it up as soon as this file is on
# dev, stable tags once it reaches main.
# workflow_dispatch lets us attach the zip to an EXISTING release (needed
# once for the latest stable after the hacs.json change reaches main).
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Existing release tag to attach the zip to"
required: true
permissions:
contents: write
jobs:
zip:
name: Собрать houseplan.zip и приложить к релизу
runs-on: ubuntu-latest
steps:
- name: Resolve tag
id: tag
env:
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
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
- name: Upload asset
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "${{ steps.tag.outputs.tag }}" houseplan.zip --clobber --repo "$GITHUB_REPOSITORY"
-102
View File
@@ -1,102 +0,0 @@
name: "Релиз: ассеты после зелёной проверки"
on:
release:
types: [published]
permissions:
contents: write
actions: read
jobs:
# AUD-159B7-02: publishing a GitHub Release used to BE the gate — this
# workflow only built and uploaded, so an asset shipped while both Validate
# runs for the very same commit were red. The asset now waits for a green
# Validate of the EXACT commit the tag points at, and is withheld otherwise.
#
# 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
with:
ref: ${{ github.event.release.tag_name }}
fetch-depth: 0
- uses: actions/setup-node@v7
with: { node-version: 22 }
- name: Require a green Validate for this exact commit
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
# HEAD is the peeled commit even when TAG is annotated. Do not trust
# 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"
node scripts/release-gate.mjs "$SHA"
- name: Require full performance for a stable release
if: ${{ !github.event.release.prerelease }}
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
SHA=$(git rev-parse HEAD)
node scripts/release-gate.mjs "$SHA" --workflow=performance.yml --label="Полные бенчмарки производительности"
build:
name: Сборка бандла и загрузка ассетов
needs: gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.release.tag_name }}
- uses: actions/setup-node@v7
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
npm run continuity:screencast
- name: Upload failed continuity frames
if: ${{ failure() && !github.event.release.prerelease }}
uses: actions/upload-artifact@v7
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
- name: Attach card to release
uses: softprops/action-gh-release@v3
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
# make that distribution failure impossible to miss in the release run.
if: ${{ github.event.release.prerelease }}
needs: build
runs-on: ubuntu-latest
steps:
- name: Verify the published tag is the prerelease HACS will discover
uses: actions/github-script@v9
with:
script: |
const releases = await github.paginate(github.rest.repos.listReleases, {
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
});
const first = releases.find((r) => r.prerelease && !r.draft);
const expected = context.payload.release.tag_name;
if (first?.tag_name !== expected) {
core.setFailed(
`HACS prerelease discovery is stale: GitHub returns ${first?.tag_name ?? 'none'} before ${expected}. ` +
`Use an rc/new version line or correct the release ordering before announcing the update.`,
);
}
-865
View File
@@ -1,865 +0,0 @@
name: Проверка (CI)
on:
push:
# The branch commit is the release-gate authority. An annotated tag points
# to the same SHA and must not duplicate the browser validation jobs.
branches:
- '**'
# Документы ревью конвейер пишет пачками — 340 коммитов за месяц, и каждый
# гонял лёгкую половину Validate впустую (≈15 часов раннера в месяц).
# Релизного кандидата это не затрагивает: тег всегда стоит на коммите,
# который меняет версию и бандл, а не только `docs/reviews/**`.
paths-ignore:
- 'docs/reviews/**'
pull_request:
# A new push supersedes an unfinished validation for the same branch or PR.
# Exact-SHA release gates never depend on an obsolete commit.
concurrency:
group: validate-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
# Четыре секундных скрипта жили четырьмя job, каждая со своим checkout и
# setup-node ради нескольких секунд работы (#336). Здесь они шаги одной job.
#
# Независимость при этом сохранена намеренно: у каждого шага
# `continue-on-error`, поэтому упавший провенанс не прячет результат
# процессного гейта, а вердикт в конце падает, если упал хоть один. Прежняя
# запись «job независимый: краснеет сам и не роняет остальные» продолжает
# действовать — на уровне шагов, с той же гранулярностью в логе.
preflight:
name: "Предполётные проверки: документация, провенанс, процесс"
runs-on: ubuntu-latest
# `actions: read` — чтение списка прогонов Validate (#388), `issues: read` —
# проверка 8 процессного гейта. Права перечислены явно, потому что job
# обращается к API сверх содержимого репозитория.
permissions:
contents: read
actions: read
issues: read
steps:
# `blob:none` при полной истории (#345): этой job нужны сообщения
# коммитов, трейлеры и ИМЕНА изменённых файлов, а не содержимое старых
# ревизий. Полный клон стоит 215 МБ против 26 МБ, и 32% этого веса —
# скриншоты документации, переснятые 196 раз.
#
# Коммиты и деревья скачиваются целиком, поэтому диапазоны и `merge-base`
# работают как раньше. Единственная догрузка по требованию здесь —
# `git show origin/main:.github/workflows/process.yml`: один блоб.
- uses: actions/checkout@v7
with: { fetch-depth: 0, filter: 'blob:none' }
- uses: actions/setup-node@v7
with: { node-version: 22 }
- name: "Документация: гайды, ченджлоги, скриншот-индекс"
id: docs
continue-on-error: true
run: node scripts/check-docs.mjs --external
# Конвейер читает `process.yml` из ветки по умолчанию, поэтому файл обязан
# совпадать в `main` и `dev`. До этой проверки совпадение держалось на
# дисциплине: каждая правка требовала двух пушей и ручной сверки.
- name: "Процесс: process.yml идентичен в main и dev"
id: workflow_sync
continue-on-error: true
run: |
git fetch --quiet origin main dev
if diff <(git show origin/main:.github/workflows/process.yml) \
<(git show origin/dev:.github/workflows/process.yml); then
echo "main и dev идентичны"
else
echo "РАСХОЖДЕНИЕ: process.yml в main и dev различаются."
echo "Конвейер исполняет версию из ветки по умолчанию, поэтому"
echo "правку нужно отправить в обе ветки."
exit 1
fi
# Оба гейта ниже судят САМ диапазон коммитов, а не объём проверок, и до
# #388 брали его от головы предыдущего пуша. Прогон предыдущего пуша
# штатно отменяется следующим (concurrency), и тогда его коммиты не судит
# никто: свой прогон отменён, а следующий пуш сравнивает уже с ними.
#
# База — последний предок с УСПЕШНО завершённым Validate. Фолбэк остаётся
# прежним `before`: расширять диапазон здесь нельзя, иначе гейт, который
# сам красит прогон, лишает следующий пуш зелёного предка и запирает dev
# в красноте навсегда.
- name: "База диапазона: последний доказанно зелёный предок"
id: range
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.sha }}
REPO: ${{ github.repository }}
FALLBACK: ${{ github.event.before }}
run: |
# `status=completed`, а не `success`: гейтам диапазона нужен факт
# «коммит судили», а не «вердикт был оправдательный». Упавший прогон
# коммит судил; отменённый — нет, его отсеивает judgedShas (#388).
gh api -X GET "repos/$REPO/actions/workflows/validate.yml/runs" \
-f branch=dev -f status=completed -F per_page=100 \
> /tmp/validate-runs.json || echo '{}' > /tmp/validate-runs.json
node scripts/classify-base.mjs --head="$HEAD_SHA" --mode=range \
--fallback="$FALLBACK" --runs=/tmp/validate-runs.json
- name: "Провенанс коммитов: трейлеры и эталоны"
id: provenance
continue-on-error: true
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ steps.range.outputs.base || github.event.before }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.sha }}
DEVELOPMENT_BRANCH: dev
run: |
git fetch -q origin "refs/heads/$DEVELOPMENT_BRANCH:refs/remotes/origin/$DEVELOPMENT_BRANCH"
node scripts/validate-commit-provenance.mjs --check-hook-mode --github-range
# Догоняющая проверка процесса (PROCESS.md §10.3). Хуки ловят нарушение на
# машине автора, но их можно обойти `--no-verify`, а коммиты идут прямо в
# dev без PR — GitHub на своей стороне не блокирует ничего. Это последнее
# место, где нарушение правила №1 ловится машиной. Откат — удалить шаг,
# скрипт остаётся рабочим.
- name: "Процессный гейт: диапазон, трейлеры, статусы issue"
id: process_gate
continue-on-error: true
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ steps.range.outputs.base || github.event.before }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.sha }}
DEVELOPMENT_BRANCH: dev
TARGET_REF: ${{ github.ref }}
# Публичный репозиторий: штатного токена хватает на чтение issue.
GH_TOKEN: ${{ github.token }}
run: |
git fetch -q origin "refs/heads/$DEVELOPMENT_BRANCH:refs/remotes/origin/$DEVELOPMENT_BRANCH"
node scripts/process-gate.mjs --github-range --issues
# Вердикт обязан быть громким и перечислять всё упавшее сразу: иначе
# слияние job стоило бы автору лишнего круга «починил одно — узнал о
# втором».
- name: Вердикт предполётных проверок
env:
DOCS: ${{ steps.docs.outcome }}
WORKFLOW_SYNC: ${{ steps.workflow_sync.outcome }}
PROVENANCE: ${{ steps.provenance.outcome }}
PROCESS_GATE: ${{ steps.process_gate.outcome }}
run: |
fail=0
check() {
if [ "$2" = "success" ]; then
echo "ok $1"
else
echo "FAIL $1 ($2)"
echo "- **$1**: $2" >> "$GITHUB_STEP_SUMMARY"
fail=1
fi
}
echo "### Предполётные проверки" >> "$GITHUB_STEP_SUMMARY"
check "документация" "$DOCS"
check "process.yml в main и dev" "$WORKFLOW_SYNC"
check "провенанс коммитов" "$PROVENANCE"
check "процессный гейт" "$PROCESS_GATE"
exit $fail
# Классификация изменённых путей: тяжёлые job идут только там, где менялось
# относящееся к ним. НА DEV ФИЛЬТРОВ НЕТ: гейт беты принимает «зелёный Validate
# на точном SHA», и если объём прогона зависит от diff, «зелёный» перестаёт
# значить одно и то же — кандидат релиза (манифесты + changelog) пропустил бы
# браузерные тесты, а прогон с пропущенными job всё равно success. Фильтры
# экономят на ветках задач, где Validate — ранний сигнал: настоящую приёмку
# там делает код-ревью, которое гоняет гейты само (#127).
changes:
name: Классификация изменённых файлов
runs-on: ubuntu-latest
# Чтение списка прогонов Validate — единственное, что этой job нужно сверх
# содержимого репозитория (#387).
permissions:
contents: read
actions: read
outputs:
frontend: ${{ steps.classify.outputs.frontend }}
backend: ${{ steps.classify.outputs.backend }}
integration: ${{ steps.classify.outputs.integration }}
base: ${{ steps.base.outputs.base }}
# Разные вещи под разными именами намеренно: `base` — до какого коммита
# классифицировать файлы ветки (#387), `range_base` — от какого коммита
# судить сам диапазон на dev (#388). Общее имя означало бы, что
# потребитель однажды молча возьмёт чужую базу.
range_base: ${{ steps.base.outputs.range_base }}
steps:
# `git diff --name-only` содержимого файлов не читает вовсе, поэтому
# блобы истории этой job не нужны ни на одном шаге (#345).
- uses: actions/checkout@v7
with: { fetch-depth: 0, filter: 'blob:none' }
# База диапазона — самый новый предок с УСПЕШНО завершённым Validate
# (#387). Прежде бралась голова предыдущего пуша (`github.event.before`),
# то есть допущение «до этого всё проверено». Concurrency отменяет прогон
# предыдущего пуша штатно, и на #86 (r5) это дало зелёный статус ветки
# без единого исполненного тяжёлого гейта.
- id: base
if: github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.sha }}
BRANCH: ${{ github.ref_name }}
REPO: ${{ github.repository }}
REF: ${{ github.ref }}
FALLBACK: ${{ github.event.before }}
run: |
git fetch -q origin dev
# Недоступность API — не отказ гейта: пустой ответ уводит базу в
# сторону БОЛЬШЕГО объёма проверок, а не меньшего.
# Один запрос на оба режима: `completed` — надмножество `success`,
# а нужный предикат применяет скрипт (#387 — успех, #388 — судимость).
gh api -X GET "repos/$REPO/actions/workflows/validate.yml/runs" \
-f branch="$BRANCH" -f status=completed -F per_page=100 \
> /tmp/validate-runs.json || echo '{}' > /tmp/validate-runs.json
if [ "$REF" = "refs/heads/dev" ]; then
# На dev классифицировать нечего (всё true), но база диапазона
# нужна гейту «новый код не добавляет any» в job frontend (#388).
node scripts/classify-base.mjs --head="$HEAD_SHA" --mode=range \
--name=range_base --fallback="$FALLBACK" --runs=/tmp/validate-runs.json
exit 0
fi
merge_base=$(git merge-base origin/dev "$HEAD_SHA" || git rev-parse "$HEAD_SHA~1")
node scripts/classify-base.mjs --head="$HEAD_SHA" \
--merge-base="$merge_base" --runs=/tmp/validate-runs.json
- id: classify
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
GREEN_BASE: ${{ steps.base.outputs.base }}
HEAD_SHA: ${{ github.sha }}
REF: ${{ github.ref }}
run: |
if [ "$REF" = "refs/heads/dev" ]; then
echo "dev: без фильтров, всё true"
printf 'frontend=true\nbackend=true\nintegration=true\n' >> "$GITHUB_OUTPUT"
exit 0
fi
zero=$(printf '%040d' 0)
# #347: непустой before, которого больше не существует, — это
# force-push (ребейз/аменд переписал историю ветки). Диапазон в этом
# случае НЕ угадывается: merge-base после переписанной истории уже
# выдавал «изменились только два doc-файла» при реальном диффе в
# custom_components/**, и тяжёлые job тихо скипались, а общий статус
# рана оставался success — ровно класс тихого пропуска #171/#207.
# Force-push редок, полный прогон дешевле ложного «зелёного».
#
# Проверка сохранена как есть и после #387: там база стала доказуемой
# (зелёный предок), но механизм, из-за которого merge-base врал на
# переписанной истории, не разобран до конца. Снимать защиту, не
# объяснив её, — способ получить #347 второй раз.
if [ -n "$BEFORE_SHA" ] && [ "$BEFORE_SHA" != "$zero" ] \
&& ! git cat-file -e "$BEFORE_SHA" 2>/dev/null; then
echo "force-push: before=$BEFORE_SHA переписан — полный прогон без классификации"
{
echo "### Классификация изменённых файлов"
echo "\`before\` (=$BEFORE_SHA) переписан force-push'ем: диапазон недоказуем,"
echo "все тяжёлые job запускаются без фильтра путей (#347)."
} >> "$GITHUB_STEP_SUMMARY"
printf 'frontend=true\nbackend=true\nintegration=true\n' >> "$GITHUB_OUTPUT"
exit 0
fi
# Диапазон считается от последнего ДОКАЗАННО зелёного предка (#387),
# а не от головы предыдущего пуша: его прогон штатно отменяется
# следующим пушем. PR — исключение: там база задана событием.
base="$GREEN_BASE"
if [ "$EVENT_NAME" = "pull_request" ]; then base="$BASE_SHA"; fi
if [ -z "$base" ]; then
echo "база не определена — полный прогон без классификации"
printf 'frontend=true\nbackend=true\nintegration=true\n' >> "$GITHUB_OUTPUT"
exit 0
fi
files=$(git diff --name-only "$base" "$HEAD_SHA")
printf '%s\n' "$files" | head -50
has() { printf '%s\n' "$files" | grep -qE "$1" && echo true || echo false; }
{
echo "frontend=$(has '^(src/|demo/|test/|dist/|custom_components/houseplan/frontend/|package(-lock)?\.json$|rollup\.config\.mjs$|tsconfig)')"
echo "backend=$(has '^(custom_components/.*\.py$|tests_backend/|scripts/support-relay/|pytest\.ini$)')"
echo "integration=$(has '^(custom_components/houseplan/manifest\.json$|hacs\.json$|custom_components/.*\.py$|custom_components/.*/translations/)')"
} >> "$GITHUB_OUTPUT"
# Переиспользование результата тяжёлой job (#208). Ключ = входы поведения
# (sourceFingerprint: src/**, demo/fixtures, demo/golden/*.mjs, манифесты
# сборки) ПЛЮС оснастка именно этой job. Маркер в кэше пишет только успешный
# прогон с тем же ключом, поэтому попадание доказывает: job с побайтово теми
# же входами уже завершилась успешно.
#
# Это НЕ фильтр путей из job `changes` (на dev они отключены намеренно): там
# объём прогона угадывается по путям и «зелёный» начинает значить разное,
# здесь эквивалентность входов доказана хешем.
#
# Свойство, снимающее главный риск: релизный кандидат бампает версию, а
# CARD_VERSION и package.json входят в фингерпринт, поэтому ключи кандидата
# заведомо новые и полный набор гейтов перед бетой и релизом идёт всегда.
reuse:
name: "Переиспользование: это дерево уже проверено"
runs-on: ubuntu-latest
outputs:
smoke: ${{ steps.probe.outputs.smoke }}
golden: ${{ steps.probe.outputs.golden }}
performance_smoke: ${{ steps.probe.outputs.performance_smoke }}
backend: ${{ steps.probe.outputs.backend }}
smoke_key: ${{ steps.keys.outputs.smoke }}
golden_key: ${{ steps.keys.outputs.golden }}
performance_smoke_key: ${{ steps.keys.outputs.performance_smoke }}
backend_key: ${{ steps.keys.outputs.backend }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with: { node-version: 22 }
- name: Ключи переиспользования
id: keys
run: |
for job in smoke golden performance_smoke backend; do
key=$(node scripts/gate-reuse.mjs --job="$job")
echo "$job=$key" >> "$GITHUB_OUTPUT"
echo "$job: $key"
done
# lookup-only: маркер только проверяется, но не восстанавливается —
# сохранять его в этой job нечего, она ничего не прогоняла.
- name: Маркер smoke
id: m_smoke
uses: actions/cache/restore@v6
with:
path: .reuse-marker
key: reuse-smoke-${{ steps.keys.outputs.smoke }}
lookup-only: true
- name: Маркер golden
id: m_golden
uses: actions/cache/restore@v6
with:
path: .reuse-marker
key: reuse-golden-${{ steps.keys.outputs.golden }}
lookup-only: true
- name: Маркер performance_smoke
id: m_perf
uses: actions/cache/restore@v6
with:
path: .reuse-marker
key: reuse-performance_smoke-${{ steps.keys.outputs.performance_smoke }}
lookup-only: true
- name: Маркер backend
id: m_backend
uses: actions/cache/restore@v6
with:
path: .reuse-marker
key: reuse-backend-${{ steps.keys.outputs.backend }}
lookup-only: true
- name: Что переиспользуем
id: probe
env:
SMOKE: ${{ steps.m_smoke.outputs.cache-hit }}
GOLDEN: ${{ steps.m_golden.outputs.cache-hit }}
PERF: ${{ steps.m_perf.outputs.cache-hit }}
BACKEND: ${{ steps.m_backend.outputs.cache-hit }}
run: |
# Пропуск обязан быть громким: молчаливый skip — тот самый тихий
# успех, который уже дважды стоил нам дня (#171, #207).
waive() {
if [ "$2" = "true" ]; then
echo "$1=true" >> "$GITHUB_OUTPUT"
echo "::notice::$1 не прогоняется: входы побайтово те же, что в предыдущем успешном прогоне (#208)"
echo "- **$1** переиспользована: входы не менялись" >> "$GITHUB_STEP_SUMMARY"
else
echo "$1=false" >> "$GITHUB_OUTPUT"
echo "- $1: прогоняется" >> "$GITHUB_STEP_SUMMARY"
fi
}
echo "### Переиспользование гейтов (#208)" >> "$GITHUB_STEP_SUMMARY"
waive smoke "$SMOKE"
waive golden "$GOLDEN"
waive performance_smoke "$PERF"
waive backend "$BACKEND"
hacs:
name: "HACS: валидация репозитория"
needs: changes
if: needs.changes.outputs.integration == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: HACS validation
uses: hacs/action@main
with:
category: integration
hassfest:
name: "Hassfest: манифест интеграции"
needs: changes
if: needs.changes.outputs.integration == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Hassfest validation
uses: home-assistant/actions/hassfest@master
frontend:
name: "Фронтенд: типы, юниты, мутанты, синхрон бандла"
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
# Полная история без блобов (#342): гейт «новый код не добавляет any»
# diff-aware, ему нужен диапазон, а содержимое старых ревизий — нет.
- uses: actions/checkout@v7
with: { fetch-depth: 0, filter: 'blob:none' }
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
- name: Typecheck
run: npm run typecheck
# Долг типизации снимается при извлечении подсистем (#34), а не разовой
# заменой: в src/** его 1034 вхождения в 49 файлах. Гейт держит приращение
# на нуле — судит только добавленные строки (#342).
- name: "Новый код не добавляет any"
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
PROVEN_BASE: ${{ needs.changes.outputs.range_base }}
HEAD_SHA: ${{ github.sha }}
REF: ${{ github.ref }}
run: |
set -euo pipefail
git fetch -q origin dev
zero=$(printf '%040d' 0)
base=""
# Пуш прямо в dev: диапазон — то, что добавлено с последнего
# ДОКАЗАННО зелёного предка (#388), а не с головы предыдущего пуша:
# его прогон штатно отменяется следующим, и добавленные им строки не
# судил бы никто. Фолбэк — прежний `before`.
if [ "$EVENT_NAME" != "pull_request" ] && [ "$REF" = "refs/heads/dev" ]; then
base="${PROVEN_BASE:-$BEFORE_SHA}"
fi
# Ветка, PR, новая история: точки отсчёта нет, берём merge-base с dev.
# Там диапазон и так шире, поэтому #388 этих случаев не касается.
if [ -z "$base" ] || [ "$base" = "$zero" ] \
|| ! git cat-file -e "$base" 2>/dev/null; then
base=$(git merge-base origin/dev "$HEAD_SHA" || echo "")
fi
if [ -z "$base" ]; then
echo "не нашёл базу диапазона — гейт пропущен, это увидит следующий пуш"
exit 0
fi
node scripts/no-new-any.mjs --base "$base" --head "$HEAD_SHA"
- name: Unit tests
run: npm test
- name: Build
run: npm run build
# Entry, manifest and every content-hashed chunk are one artifact (#337).
- name: Card bundle trees in sync
run: |
node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend
npm run bundle:budget
# Бандл собирается один раз и уезжает артефактом (#336). Прежде каждая из
# пяти браузерных job собирала его заново: `bundle:sync` — это `tsc
# --noEmit` плюс rollup, около минуты на job.
#
# Подмену артефакта ловить не нужно отдельной проверкой: `assertFreshDemoBundle`
# сверяет вшитый в бандл отпечаток с `sourceFingerprint` выкачанного
# дерева, и каждая браузерная job делает это перед первым кадром.
- name: Выложить бандл для браузерных job
uses: actions/upload-artifact@v7
with:
name: card-bundle
path: dist/
retention-days: 1
if-no-files-found: error
# Несколько браузерных контрактов импортируют чистые функции из
# скомпилированного тестового дерева. Оно создаётся `npm test`, но не
# входит в production-бандл и потому едет отдельным артефактом (#336).
- name: Выложить тестовые модули для смоков
uses: actions/upload-artifact@v7
with:
name: card-test-build
path: test-build/
retention-days: 1
if-no-files-found: error
smoke:
name: Смоки в браузере (шард ${{ matrix.shard }} из 3)
# Gated on `frontend` so a typecheck failure does not burn browser minutes.
needs: [frontend, reuse]
if: needs.reuse.outputs.smoke != 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
# Смоки шардируются: последовательный прогон занимал ~7.5 минут и был
# критическим путём всего Validate. Три шарда режут его примерно вдвое;
# цена — трижды `npm ci` и сборка, около двух оплаченных минут раннера.
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
# Браузеры кэшируются, а apt не запускается вовсе: на GitHub-раннере
# системные библиотеки Chromium уже в образе, а --with-deps тратил минуты
# и подолгу перебирал недоступное azure-зеркало (#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
# Бандл приходит артефактом из job `frontend` (#336): та же сборка, но
# один раз на прогон. Свежесть проверяет сам продукт — `assertFreshDemoBundle`
# сверяет вшитый отпечаток с исходниками этого дерева.
- name: Забрать собранный бандл
uses: actions/download-artifact@v7
with:
name: card-bundle
path: dist
- name: Забрать тестовые модули
uses: actions/download-artifact@v7
with:
name: card-test-build
path: test-build
- name: Разложить бандл по копиям
run: node scripts/bundle-sync.mjs
# Гард исключений внутри карточки объявлен в demo/serve.mjs с 2026-07-27 и
# до #404 не срабатывал ни разу: счётчик читался раньше, чем Playwright
# доставлял pageerror. Такое ловится только запуском, поэтому пробы живут
# здесь — в единственной job с настоящим браузером. Один шард из трёх:
# проверка не зависит от разбиения, а платить за неё трижды незачем.
- name: "Гард исключений умеет падать (#404)"
if: matrix.shard == 1
run: node demo/guard/verify-guard.mjs
- name: Smoke suite (шард ${{ matrix.shard }} из 3)
env:
SHARD: ${{ matrix.shard }}
SHARDS: '3'
run: |
fail=0
index=0
ran=0
mkdir -p /tmp/smoke-logs
# Деление по порядковому номеру файла: список отсортирован, поэтому
# разбиение детерминировано и не зависит от времени прогона.
for f in demo/smoke_*.mjs; do
index=$((index + 1))
if [ $(( (index - 1) % SHARDS + 1 )) -ne "$SHARD" ]; then continue; fi
ran=$((ran + 1))
name=$(basename "$f" .mjs)
if timeout --kill-after=10s 180s node "$f" > "/tmp/smoke-logs/$name.log" 2>&1; then
echo "ok $name"
else
status=$?
echo "FAIL $name"
if [ "$status" -eq 124 ]; then
echo "diagnostic smoke-timeout: $name exceeded 180s"
fi
# Диагностические строки смока печатаются раньше вердикта, и
# `tail -20` их срезал: на #411 сам смок сообщал, НАСКОЛЬКО
# разошлись кадры (введено в #302), а в логе прогона осталось
# только «expected true, got false». Числа теперь достаются
# адресно — они и есть разница между «чинить» и «гадать».
grep -aE 'pixel-diffs|^EXC |diagnostic' "/tmp/smoke-logs/$name.log" | tail -5 || true
tail -20 "/tmp/smoke-logs/$name.log"
fail=1
fi
done
echo "--- шард ${SHARD}/${SHARDS}: прогнано ${ran} из ${index}"
# Пустой шард — признак, что деление сломалось, а не что работы нет.
if [ "$ran" -eq 0 ]; then echo "шард пуст: проверьте деление"; exit 1; fi
exit $fail
- name: Upload smoke logs
if: failure()
uses: actions/upload-artifact@v7
with:
name: smoke-logs-${{ matrix.shard }}
path: /tmp/smoke-logs
# Маркер переиспользования пишется ОДИН раз и только когда прошли все шарды:
# частично прогнанная матрица не имеет права выглядеть как выполненная работа.
smoke_done:
name: "Смоки: все шарды зелёные"
needs: [smoke, reuse]
if: needs.reuse.outputs.smoke != 'true'
runs-on: ubuntu-latest
steps:
- name: Записать маркер успеха
run: |
printf '%s\n' "smoke прогнана успешно (3 шарда)" \
"SHA: ${{ github.sha }}" \
"прогон: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
> .reuse-marker
- uses: actions/cache/save@v6
# Гонка двух прогонов с одинаковым ключом даёт «Cache already exists».
# Это не отказ гейта: работа выполнена, маркер уже записал сосед — job
# краснеть из-за этого не должна.
continue-on-error: true
with:
path: .reuse-marker
key: reuse-smoke-${{ needs.reuse.outputs.smoke_key }}
golden:
name: Golden-кадры против принятых эталонов
# Deterministic visual correctness stays in every prerelease gate: it is
# inexpensive and catches a different class of regressions than timings.
needs: [frontend, reuse]
if: needs.reuse.outputs.golden != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
# Браузеры кэшируются, а apt не запускается вовсе: на GitHub-раннере
# системные библиотеки Chromium уже в образе, а --with-deps тратил минуты
# и подолгу перебирал недоступное azure-зеркало (#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
# Бандл приходит артефактом из job `frontend` (#336): та же сборка, но
# один раз на прогон. Свежесть проверяет сам продукт — `assertFreshDemoBundle`
# сверяет вшитый отпечаток с исходниками этого дерева.
- name: Забрать собранный бандл
uses: actions/download-artifact@v7
with:
name: card-bundle
path: dist
- name: Разложить бандл по копиям
run: node scripts/bundle-sync.mjs
# Маркер предыдущего падения с ТЕМ ЖЕ ключом (#386). Восстанавливается до
# прогона, потому что после падения шаги кэша уже не помогут отличить
# виновника от свидетеля.
- name: Прежнее падение на тех же входах
uses: actions/cache/restore@v6
with:
path: .fail-marker
key: fail-golden-${{ needs.reuse.outputs.golden_key }}
- name: Capture or verify golden matrix
id: golden
run: |
if find demo/golden/baselines -maxdepth 1 -name '*.png' -print -quit | grep -q .; then
echo "has_baselines=true" >> "$GITHUB_OUTPUT"
npm run golden:verify
else
echo "has_baselines=false" >> "$GITHUB_OUTPUT"
npm run golden:capture
fi
- name: Upload golden candidates/diffs
if: failure() || steps.golden.outputs.has_baselines == 'false'
uses: actions/upload-artifact@v7
with:
name: golden-images
path: artifacts/golden
# Красная golden обязана сказать, ЧЕЙ это долг (#386). Без этого письмо
# «Run failed» называет коммит, который пушнули следующим: 29 августа так
# был назван документ ревью `0f7b6f5`, а сцену без эталона добавил
# `dbbe94ae`. Вердикт job не меняется — меняется только формулировка.
- name: Кто уронил golden
id: fail_note
if: failure()
env:
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: node scripts/gate-reuse.mjs --job=golden --note --marker=.fail-marker
- name: Запомнить первопричину
if: failure() && steps.fail_note.outputs.first == 'true'
# Гонка двух прогонов даёт «Cache already exists»; для маркера падения
# это тем более не отказ гейта — job и так красная по существу.
continue-on-error: true
uses: actions/cache/save@v6
with:
path: .fail-marker
key: fail-golden-${{ needs.reuse.outputs.golden_key }}
# Маркер пишется последним шагом: он существует только если всё выше
# прошло. Кэш сохраняется post-шагом, то есть тоже лишь при успехе job.
- name: Записать маркер успеха
run: |
printf '%s\n' "golden прогнана успешно" \
"SHA: ${{ github.sha }}" \
"прогон: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
> .reuse-marker
- uses: actions/cache/save@v6
# Гонка двух прогонов с одинаковым ключом даёт «Cache already exists».
# Это не отказ гейта: работа выполнена, маркер уже записал сосед — job
# краснеть из-за этого не должна.
continue-on-error: true
with:
path: .reuse-marker
key: reuse-golden-${{ needs.reuse.outputs.golden_key }}
performance_smoke:
name: "Перф-смок: бюджет времени кадра"
# Candidate-only catastrophic-regression guard for ordinary pushes and
# prereleases. The expensive same-runner comparison lives in performance.yml.
needs: [frontend, reuse]
if: needs.reuse.outputs.performance_smoke != 'true'
runs-on: ubuntu-latest
# 15 минут не хватало, когда установка браузера шла через apt: замер
# начинался на исходе окна (#206). Запас на холодный кэш — при попадании
# job укладывается в те же минуты, что и раньше.
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
# Браузеры кэшируются, а apt не запускается вовсе: на GitHub-раннере
# системные библиотеки Chromium уже в образе, а --with-deps тратил минуты
# и подолгу перебирал недоступное azure-зеркало (#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
# Бандл приходит артефактом из job `frontend` (#336): та же сборка, но
# один раз на прогон. Свежесть проверяет сам продукт — `assertFreshDemoBundle`
# сверяет вшитый отпечаток с исходниками этого дерева.
- name: Забрать собранный бандл
uses: actions/download-artifact@v7
with:
name: card-bundle
path: dist
- name: Разложить бандл по копиям
run: node scripts/bundle-sync.mjs
- name: Capture the heaviest Glow state
run: |
npm run benchmark:glow -- --profile=large-house-glow-overlay-v1 --variants=60 --samples=3 --warmups=1 --output=artifacts/performance-smoke/candidate.json
npm run benchmark:glow -- --profile=large-space-card-glow-v1 --variants=60 --samples=3 --warmups=1 --output=artifacts/performance-smoke/space-candidate.json
- name: Enforce absolute smoke ceilings
run: |
npm run benchmark:compare -- --absolute-only --budgets=demo/performance/budgets-glow-smoke.json --candidate=artifacts/performance-smoke/candidate.json --output=artifacts/performance-smoke/comparison.json
npm run benchmark:compare -- --absolute-only --budgets=demo/performance/budgets-space-glow-smoke.json --candidate=artifacts/performance-smoke/space-candidate.json --output=artifacts/performance-smoke/space-comparison.json
# #330 AC7: перф-контракт ограничений стыков — быстрый (без браузера),
# ловит возврат квадратичных путей в оба зеркала прямо на пуше.
- name: Бюджеты ограничений стыков (#330)
run: npm run benchmark:junction-limits
- name: Upload performance smoke report
if: always()
uses: actions/upload-artifact@v7
with:
name: performance-smoke
path: artifacts/performance-smoke
# Маркер пишется последним шагом: он существует только если всё выше
# прошло. Кэш сохраняется post-шагом, то есть тоже лишь при успехе job.
- name: Записать маркер успеха
run: |
printf '%s\n' "performance_smoke прогнана успешно" \
"SHA: ${{ github.sha }}" \
"прогон: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
> .reuse-marker
- uses: actions/cache/save@v6
# Гонка двух прогонов с одинаковым ключом даёт «Cache already exists».
# Это не отказ гейта: работа выполнена, маркер уже записал сосед — job
# краснеть из-за этого не должна.
continue-on-error: true
with:
path: .reuse-marker
key: reuse-performance_smoke-${{ needs.reuse.outputs.performance_smoke_key }}
backend:
name: "Бэкенд: pytest в Home Assistant"
needs: [changes, reuse]
if: needs.changes.outputs.backend == 'true' && needs.reuse.outputs.backend != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# Browser fixtures are generated by their real ESM factories and then
# validated through the Python CONFIG_SCHEMA/LAYOUT_SCHEMA in the same test.
- uses: actions/setup-node@v7
with: { node-version: 22 }
- uses: actions/setup-python@v7
with: { python-version: "3.14" }
- run: |
# Точные версии — в tests_backend/requirements.txt (#392): без них
# резолвер выбирает набор сам, и «зелёный backend» значит разное
# в разные дни. #42 ставит из того же файла ruff для шага линта.
pip install -r tests_backend/requirements.txt
pip list --format=columns | grep -Ei 'homeassistant|voluptuous|^pytest '
- name: Линт бэкенда (ruff, узкий набор)
run: python -m ruff check custom_components/houseplan
# #42: типизация была измеримой только локально — CI её не исполнял, и
# регрессия в любом из allowlist-модулей проходила молча. Список модулей
# берётся ИЗ pyproject.toml, а не дублируется здесь: разошедшийся дубль
# означал бы зелёный шаг, проверяющий не то. Пустой список — отказ.
- name: Типы бэкенда (mypy strict по allowlist)
run: |
modules=$(python -c "
import tomllib
cfg = tomllib.load(open('pyproject.toml', 'rb'))
for o in cfg['tool']['mypy'].get('overrides', []):
if o.get('strict'):
print(' '.join('-p ' + m for m in o['module']))
")
test -n "$modules" || { echo '::error::strict-allowlist в pyproject.toml пуст — проверять нечего'; exit 1; }
echo "mypy strict: $modules"
python -m mypy $modules
# #42: защита от тихого скипа HA-harness — импорт и порог collect
- name: HA-harness присутствует
run: |
python -c "import homeassistant"
collected=$(python -m pytest tests_backend/ -q --collect-only 2>/dev/null | grep -c "test_ha_" || true)
echo "collected HA-harness tests: $collected"
test "$collected" -ge 50
- name: Backend unit tests (pure + HA harness, coverage)
run: >
python -m pytest tests_backend/ -q
--cov=custom_components/houseplan --cov-branch
--cov-report=xml --cov-report=term
- name: Support relay unit tests
run: python -m unittest discover -s scripts/support-relay/tests -q
# #42: гейт «не ниже baseline» — пороги 90/95 поднимаются trivial-правкой числа
- name: Порог покрытия не ниже baseline
run: |
baseline=$(head -1 scripts/backend-coverage-baseline.txt)
actual=$(python -c "import xml.etree.ElementTree as ET; print(round(float(ET.parse('coverage.xml').getroot().get('line-rate'))*100, 1))")
echo "coverage: $actual% (baseline $baseline%)"
python -c "import sys; sys.exit(0 if float('$actual') >= float('$baseline') - 0.1 else 1)"
- name: Артефакт coverage.xml
uses: actions/upload-artifact@v4
with:
name: backend-coverage
path: coverage.xml
# Маркер пишется последним шагом: он существует только если всё выше
# прошло. Кэш сохраняется post-шагом, то есть тоже лишь при успехе job.
- name: Записать маркер успеха
run: |
printf '%s\n' "backend прогнана успешно" \
"SHA: ${{ github.sha }}" \
"прогон: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
> .reuse-marker
- uses: actions/cache/save@v6
# Гонка двух прогонов с одинаковым ключом даёт «Cache already exists».
# Это не отказ гейта: работа выполнена, маркер уже записал сосед — job
# краснеть из-за этого не должна.
continue-on-error: true
with:
path: .reuse-marker
key: reuse-backend-${{ needs.reuse.outputs.backend_key }}
-16
View File
@@ -1,16 +0,0 @@
node_modules/
tsout/
test-build/
*.log
__pycache__/
.pytest_cache/
.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-assets.json
demo/srv/assets/houseplan-assets/
-455
View File
@@ -1,455 +0,0 @@
# AGENTS.md
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/`.
- **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.
## Read this first
**`docs/SCOPE.md` before anything else.** It was fixed with the owner and states
its own authority: features are built, improved and accepted **only** if they
serve a job listed there. It carries the mission, the three personas, the core
user jobs and the out-of-scope list.
Its central consequence: **View mode is the product for two of the three
personas.** Editors are admin-only tools and must never leak interactions into
View.
For work that changes visible behaviour, also read `docs/USER-GUIDE.ru.md` —
interface wording comes from there and is not invented, or the UI starts speaking
developer.
Then `PROCESS.md` (the full process), `docs/STATUS.md` (where the release line
is), and for non-trivial changes `docs/ARCHITECTURE.md` plus the canonical
document of the subsystem you touch: `SUN.md`, `LIGHT.md`, `CANVAS.md`,
`WALL-THICKNESS.md`, `UX-MODES.md`, `CONFIG-COMPATIBILITY.md`,
`TOUCH-SUPPORT.md`.
Standard commands live in `package.json` scripts, `CONTRIBUTING.md` and
`docs/DEVELOPMENT.md`.
## Canonical backlog and status
[GitHub Issues](https://github.com/Matysh/houseplan-card/issues) are the canonical
task records: problem, scope, acceptance criteria and discussion.
**Status lives in labels:** `S1-new`, `S2-analysis`, `S3-spec`, `S4-spec-review`,
`S5-ready`, `S6-in-progress`, `S7-code-review`, `S8-merged`, plus `blocked` on top
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
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.
`PROCESS.md` §5 and §5.1 hold the criteria.
An issue filed by an outsider is worked exactly like one of the owner's own, once
the owner has decided to take it. The check sits **at the entrance**, not on every
step: while an issue carries no status label it is outside the process and the
invariants do not apply to it; once a label is on, the task is in flight and **who
filed it stops mattering**.
Applying that first label *is* the owner's explicit decision, and the platform
already guarantees it — only someone with write access can label. The earlier rule
made outside reports be refiled as the owner's own issues, which turned out to be
work for nothing: on #123 the spec was already written by the time the guard
refused.
Specs, audits and ADRs may live under `docs/`, but must link to their issue and
must not become a parallel task list. When repository documentation disagrees with
Issues, the issue wins.
## Rule #1
> Changing product code without an issue is forbidden. Code changes only when the
> issue exists and sits in "Ready for development" or later.
Check before touching product code:
```
gh issue view <NN> --repo Matysh/houseplan-card --json number,state,labels
```
The label must be one of `S5-ready`, `S6-in-progress`, `S7-code-review`. Anything
else — refuse and say why. "Issue #83 is in `S2-analysis`, code is off limits.
Start with the spec?" is the correct answer, not a smaller patch.
## Change classes
| Class | Paths | Issue required |
|---|---|---|
| **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` |
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`,
`pytest.ini`, `.gitignore`, `.gitattributes`, `.githooks/**` and the rest of
`.github/**` are class B. Where paths overlap, **D beats A**: the built bundle
lives inside `custom_components/houseplan/frontend/` and would otherwise read as
product source.
## Commits
Hooks install themselves: `package.json` runs `"prepare": "node
scripts/install-hooks.mjs"`, so `npm ci` sets `core.hooksPath` in every fresh
clone. Verify with `git config core.hooksPath` — expect `.githooks`.
Every non-merge commit carries **terminal** trailers:
```text
Issue: #123
User-Visible: yes
```
One `Issue:` line per issue if a commit closes several. `User-Visible: no` for
tests, refactors, tooling and documentation that does not change the product.
`User-Visible: yes` requires edits to **both** changelogs — `docs/CHANGELOG.md`
and `docs/CHANGELOG.ru.md` — in the same commit.
A commit touching `demo/golden/baselines/**` additionally requires:
```text
Release: v1.62.0-beta.9
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/<run-id>
```
Never invent a review link and never rewrite published history to satisfy
trailers. `.githooks/commit-msg` and the `provenance` CI job both run
`scripts/validate-commit-provenance.mjs`.
Branch: `issue/<NN>-slug`. Direct commits to `dev`, no PR — the owner's decision;
CI checks after the fact, and a violation is fixed with a follow-up commit, never
a force-push.
**Push after every task, not before a beta.** While work sits unpushed there is
nothing to review, and reviewing twenty tasks at once is not review. `dev` may hold
unreviewed code while a task is in flight; what matters is its state when the
reviewer says it is accepted.
**Standing permission: push `issue/<NN>-slug` without asking.** The reviewer runs
in CI and can only read what is on the remote — an unpushed spec or commit means
the review either stalls or judges the wrong tree. Pushing a task branch publishes
nothing to users and does not touch the integration branch, so it needs no command.
**Do not merge into `dev` by hand.** On a green code review the pipeline rebases
the task branch onto `dev`, pushes it, and only then sets `S8-merged` — the label
asserts the code is in `dev`, so the merge has to happen first or the label lies
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. 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.
Everything else still requires the owner's explicit command: pushing `main`,
creating tags, publishing betas and releases, closing issues.
## Working trees (#115)
One checkout, one `HEAD`: two agents sharing a directory inherit each other's
branch, and twice in one hour a commit landed on someone else's task branch that
way. The layout is therefore fixed:
- **`houseplan-card-src/houseplan-card`** — the author's tree. Task branches live
here; nobody else commits in it. Unfamiliar local changes belong to the author
or the owner — never reset or clean them away.
- **`houseplan-card-src/hp-dev`** — the owner's worktree, permanently on `dev`. For owner-side operations that must not disturb the
author's tree: pushing `dev`, restoring a hook's executable bit, emergencies.
- **The reviewer and the infrastructure agent own no local tree.** The reviewer
runs in CI on a fresh checkout. The infrastructure agent reads via `git show`
and publishes through the GitHub API; it makes no local commits at all, so it
needs no `HEAD` of its own. Its scratch worktrees live outside the repo and are
pruned after use.
A worktree is only usable on the machine that created it: the `.git` file records
an absolute path in that machine's format. One created from a Linux sandbox is
dead on Windows and vice versa — create worktrees on the machine that will use
them, which for `hp-dev` means the owner's.
## Two-agent workflow
**Codex** writes analysis, specs and all product code. **Claude** reviews specs and
code and owns infrastructure and distribution. The owner rules on disputes, closes
issues and commands releases.
Author and reviewer are different models, which is what "a fresh session without
implementation context" means in practice. The reviewer never edits product code;
the author never grades their own work.
**Infrastructure-only work runs outside this flow.** CI, scripts, labels, demo
stands, the landing page and distribution are Claude's alone, and running them
through spec-writing and review buys nothing: the spec would restate what is
already unambiguous, and author and reviewer would be the same role. So no spec
file, no spec review, no code review, no walk through `S1`…`S8`.
The test for "infrastructure only" is mechanical: **not a single class A file** —
nothing under `src/**`, no `custom_components/**/*.py`, no manifests, no i18n. A
task that touches class A even once is not infrastructure and takes the full flow;
there is no such thing as "mostly infrastructure". The strictness is deliberate:
a loose reading would turn this into the route by which product changes skip
review.
What stays mandatory either way: an issue exists, both trailers are on every
commit, `typecheck`, `test` and `build` are green, and any non-obvious decision is
written down in the code or the issue rather than kept in someone's head.
**Review starts by itself.** Applying `S4-spec-review` or `S7-code-review` fires the
pipeline, which reviews without anyone asking and takes ten to forty-five minutes.
**Having applied one of those labels, wait for the result instead of ending the
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. 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.
What the new label means:
| Now reads | What happened | What you do |
|---|---|---|
| `S5-ready` | the spec is accepted | write the code |
| `S3-spec` | the spec came back | read the verdict, revise, re-apply `S4-spec-review` |
| `S6-in-progress` | the code came back | revise, re-apply `S7-code-review` — **or**, if the verdict was green and only the merge conflicted, just rebase and re-apply. The comment says which |
| `S8-merged` | accepted and already in `dev` | nothing |
| `review-4` | the cycle limit is spent | stop, the owner decides |
**After a review run the label always changes.** If it did not, the run itself
failed rather than the work — say so to the owner instead of polling on.
**A failed pre-release gate does not send the issue back to review.** The
implementation loop runs only typecheck, unit and build; golden, browser smokes,
performance and the full HA harness run before a beta, which is after the code
review has passed and the issue sits in `S8-merged`. Some defects cannot surface
any earlier.
Fix it, re-run what failed, and a green run is enough for the release to continue.
The issue stays in `S8-merged`. Record the **exact command and its result** in the
issue — "verified" without a command proves nothing. Trailers as usual, and
`User-Visible: yes` still means both changelogs in the same commit.
The exception covers repairing the defect the gate named, not carrying on
development under the name of a repair. It goes through the normal flow — a new
issue, or back to `S6-in-progress` — if the fix changes a behaviour contract, gives
the user something new, reaches a subsystem the task never touched, or is
comparable in size to the task itself. And editing the gate so it stops failing is
concealment, not repair; the exception is a defect proven to be **in the fixture**,
as on #89, where the sun sat at azimuth 180° and the only window faced north, so no
ray was ever built.
Baselines are still accepted only via `npm run golden:accept -- --reviewed` on a
complete Linux CI artefact. "So the gate goes green" is not a reason.
The exchange happens in **issue comments** — there is no local message bus. Verdict
format:
```text
Verdict: green/yellow/red · cycle r<N>/4 · High: N · Medium: N → in-task | #… · 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
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.
**Four review cycles** (two on the light track). The counter lives in the document
name, `-r1`…`-r4`; the fourth adds the `review-4` label. There is no fifth attempt:
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.
## Specs
`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.
The owner answers exactly two kinds of question: **what a person sees or does**, and
**how much user-visible change belongs in this issue**. Behaviour in a boundary case,
which persona wins when two conflict, what counts as acceptable degradation, whether
a neighbouring behaviour is in scope here or becomes its own issue.
Everything a user cannot observe is yours to settle: where state is stored, which
module carries the guard, naming, file layout, test strategy, migration mechanics,
development policy. Decide it, record it in an explicit "assumed, change freely"
block, and let the reviewer challenge it. A technical disagreement between author and
reviewer is settled by the verdict, not by the owner; it reaches him only when the
cycle limit is exhausted.
Split a mixed question instead of escalating all of it. "Where does this state live"
is technical. "Does it survive a page reload and follow the plan across screens" is
product. Ask the second, decide the first.
Ask in one batched issue comment, each question carrying a proposed default, and put
`blocked` on top of `S3-spec` while waiting. A question with a default costs the
owner seconds; one without costs him minutes.
## Gates
```
npm run typecheck
npm test
npm run build
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:
```
npm run bundle:sync # dist → custom_components + demo/srv/assets (#255)
npm run bundle:budget # initial View graph <= 256000 B gzip (#337)
```
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
`S7-code-review`, run the smokes named in its AC locally** — `node
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.
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.
**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.
Locally only the pure subset runs; `python -m pytest tests_backend/ -q` without
Home Assistant **silently skips** `test_ha_*.py` (`conftest.py` ignores them when
`homeassistant` is not importable), so a green result proves nothing. Say so in the
report instead of claiming the backend was verified. Cloud agents have the harness
at `.venv-backend/bin/python`.
**Running the app / smoke suite**: build a fresh bundle and copy it into the demo
assets first, then run `node demo/smoke_*.mjs`. No real Home Assistant server is
required: `demo/srv/demo.html` stubs `hass`, registries and `callService`.
**Golden images**: `npm run golden:capture` and `npm run golden:verify` refuse a
stale demo bundle. Build and copy first, then review `artifacts/golden/actual/` and
`diff/`. Update baselines only with `npm run golden:accept -- --reviewed`, using the
complete Linux CI artifact; never accept a partial scenario or images merely to make
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.
**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.
**"Verified" without a named command and its result is not evidence.**
## Environments
**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.
Known environment-sensitive smoke: `demo/smoke_opening_measure.mjs` fails two
sub-checks (`place_dialog_x_magnetised`, `place_committed_x_center`) under the pinned
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
`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.
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`.
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
after a re-render** (an F5 refresh, or nudging `card.hass = {...card.hass}`). The
smoke launcher `demo/serve.mjs` already does this nudge; a plain browser session does
not. This is a harness limitation, not a card bug.
## Promotion rule
Every new feature or material behaviour change must be published as a beta/RC
before it can enter a stable release, even when its local audit is clean. The
stable release commit is promotion-only: version fields, generated bundle
snapshots and changelog/release metadata. Do not add feature source code in
that commit. An explicit owner-requested emergency hotfix is the only exception
and must be called out in the release handoff.
A `Release vX.Y.Z-beta.N candidate` commit is **not** promotion-only: it carries
the work itself and follows the ordinary rules, trailers included.
Issues are closed in a batch when a beta ships, not when implementation ends: that
way a bug found in the beta returns to the same task, and the beta announcement can
list what went in. Status labels are stripped as the issues close.
-119
View File
@@ -1,119 +0,0 @@
# Contributing to House Plan
Thanks for your interest! The project is one HACS package: a storage **integration**
(`custom_components/houseplan/`, Python) and a **Lovelace card** (`src/`, TypeScript + Lit).
## Changelog
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
writing code? The **[Telegram chat @ha_houseplan](https://t.me/ha_houseplan)**
is the quickest route to the author and other users. Bugs and concrete feature
requests still belong in [issues](https://github.com/Matysh/houseplan-card/issues).
## Backlog and work status
[GitHub Issues](https://github.com/Matysh/houseplan-card/issues) are the only
active backlog. An issue owns scope and acceptance criteria; its **labels** own
priority and workflow status — `PROCESS.md` §9 holds the vocabulary. Before
starting planned work, link it to an existing issue or create one, and keep it
current until the verified result is closed. Design specs and ADRs may support an
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
npm ci # frontend toolchain
npm run typecheck # tsc --noEmit (strict)
npm test # node:test — pure logic, i18n parity, tap-action security
npm run build # tsc + rollup → dist/houseplan-card.js
pip install pytest voluptuous && python -m pytest tests_backend -q # pure backend tests
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.
## Ground rules
- **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.
- 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) —
see `resolveToggleIntent` in `src/device-toggle.ts`; don't weaken it.
- Every commit follows the issue and trailer contract in `PROCESS.md`.
- Follow the Integration Quality Scale where applicable —
`custom_components/houseplan/quality_scale.yaml` tracks the self-assessment.
## Architecture
Start with `docs/ARCHITECTURE.md` (data model, WS API, coordinate system) and
`docs/STATUS.md` (current state). Release: bump the version in `package.json`,
`manifest.json`, `const.py`, `CARD_VERSION`, tag `vX.Y.Z`, publish a GitHub release —
the workflow attaches the card bundle.
+7
View File
@@ -0,0 +1,7 @@
{
"schema": 1,
"source": "674e589ad841c6580f31d1a495bb207f13e6931e",
"fingerprint": "a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e",
"files": 30,
"builtAt": "2026-09-29T06:30:42.564Z"
}
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 JB (justbusiness)
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.
-1090
View File
File diff suppressed because it is too large Load Diff
-160
View File
@@ -1,160 +0,0 @@
# 🏠 House Plan — a live home map for Home Assistant
[![HACS Default](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration)
[![GitHub release](https://img.shields.io/github/v/release/Matysh/houseplan-card)](https://github.com/Matysh/houseplan-card/releases)
[![CI](https://github.com/Matysh/houseplan-card/actions/workflows/validate.yml/badge.svg)](https://github.com/Matysh/houseplan-card/actions)
[![Live demo](https://img.shields.io/badge/demo-try_it_live-00c853?logo=homeassistant&logoColor=white)](https://demo.houseplan.tech)
[![Telegram chat](https://img.shields.io/badge/Telegram-chat-2CA5E0?logo=telegram&logoColor=white)](https://t.me/ha_houseplan)
📘 **[Full user guide](docs/USER-GUIDE.md)** · 🇷🇺 **[Русский](README.ru.md)** · 🗂 **[Project issues](https://github.com/Matysh/houseplan-card/issues)**
<!-- docs-section: overview -->
## Your whole home at a glance
House Plan turns Home Assistant into a live map of your home. Upload a plan or
draw rooms directly on the dashboard, 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.
![Synthetic home in View mode with rooms, devices, light and climate](docs/images/01-view-desktop.png)
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.
> **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).
<!-- docs-section: features -->
## What House Plan provides
- **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.
![The same synthetic home in touch View mode](docs/images/02-view-touch.png)
<!-- docs-section: first-run -->
## Your first working room
1. Install the integration and add the card to a dashboard.
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.
![Creating the first space](docs/images/03-space-create.png)
![Closing a room outline on its first point](docs/images/04-room-contour-close.png)
![A selected partition and the Plan context tray](docs/images/05-plan-context-tray.png)
![Device settings with binding provenance and the exact action result](docs/images/06-device-editor.png)
![Live presentation preview for the same device](docs/images/06-device-display-preview.png)
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.
<!-- docs-section: installation -->
## Installation
### HACS
[![Open the repository in HACS](https://my.home-assistant.io/badges/hacs_repository.svg)](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.
2. Restart Home Assistant.
3. Open **Settings → Devices & services → Add integration → House Plan**.
The card is registered automatically. If you manage Lovelace resources
manually, use the URL served by the integration:
```yaml
resources:
- url: /houseplan_files/houseplan-card.js
type: module
```
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.
### Add the card
Create a dashboard view (Panel works best) and add the card in the UI or as:
```yaml
type: custom:houseplan-card
title: House plan
```
Different screens may start on different spaces:
```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.
## Detailed documentation
- [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)
<!-- docs-section: support -->
## Support and feedback
- 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).
License: [MIT](LICENSE).
-165
View File
@@ -1,165 +0,0 @@
# 🏠 House Plan — живой план дома для Home Assistant
[![HACS Default](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration)
[![GitHub release](https://img.shields.io/github/v/release/Matysh/houseplan-card)](https://github.com/Matysh/houseplan-card/releases)
[![CI](https://github.com/Matysh/houseplan-card/actions/workflows/validate.yml/badge.svg)](https://github.com/Matysh/houseplan-card/actions)
[![Live demo](https://img.shields.io/badge/демо-попробовать-00c853?logo=homeassistant&logoColor=white)](https://demo.houseplan.tech)
[![Telegram chat](https://img.shields.io/badge/Telegram-чат-2CA5E0?logo=telegram&logoColor=white)](https://t.me/ha_houseplan)
📘 **[Полное руководство](docs/USER-GUIDE.ru.md)** · 🇬🇧 **[English](README.md)** · 🗂 **[Задачи проекта](https://github.com/Matysh/houseplan-card/issues)**
<!-- docs-section: overview -->
## Дом целиком — одним взглядом
House Plan превращает Home Assistant в живую карту дома. Загрузите изображение
плана или нарисуйте комнаты прямо на дашборде, свяжите их с зонами Home
Assistant — и устройства появятся на плане автоматически. Сразу видно, где
горит свет, открыта дверь, слишком холодно, слабый Zigbee-сигнал или сработал
датчик протечки.
![Синтетический дом в режиме просмотра: комнаты, устройства, свет и климат](docs/images/01-view-desktop.png)
Настройка выполняется в графическом интерфейсе: без YAML-разметки, Inkscape и
внешнего редактора плана. Данные плана и расположение устройств хранятся на
сервере Home Assistant и синхронизируются между экранами.
> **Редактируйте на компьютере.** Режим просмотра и киоск полноценно работают
> на телефонах и планшетах. Редакторы рассчитаны прежде всего на мышь и
> клавиатуру; на touch отдельные операции могут быть неудобны или недоступны.
> Подробный контракт: [поддержка touch](docs/TOUCH-SUPPORT.md).
<!-- docs-section: features -->
## Что умеет House Plan
- **Живые состояния и безопасные действия.** Свет и другие безопасные устройства
переключаются с плана; замок нельзя открыть случайным нажатием.
- **Три встроенных редактора.** «План» создаёт комнаты, стены и проёмы;
«Устройства» размещает и настраивает маркеры; «Подложка» добавляет линии,
подписи и мебель.
- **Комнаты, связанные с зонами HA.** Новые устройства появляются автоматически,
а карточки комнат показывают температуру, влажность, свет и средний LQI.
- **Свет и окружение.** Заливки комнат, Glow от ламп, тени от стен, дневной фон и
солнечные лучи из окон.
- **Двери, окна, ворота и пылесосы.** Проёмы отражают реальные датчики и замки;
робот показывает позицию, базу и пройденный путь.
- **Несколько этажей и экранов.** Вкладки пространств, жесты переключения,
локальный масштаб и отдельный стартовый этаж для каждой карточки.
- **Киоск для настенного экрана.** Только план, полноэкранная навигация и размеры
значков, сохранённые отдельно для этого устройства.
![Тот же синтетический дом в touch-режиме просмотра](docs/images/02-view-touch.png)
<!-- docs-section: first-run -->
## Первая рабочая комната
1. Установите интеграцию и добавьте карточку на дашборд.
2. Создайте первое **пространство**: загрузите SVG/PNG/JPG/WebP либо выберите
вариант без изображения, чтобы нарисовать план вручную.
3. В редакторе «План» выберите **Контур комнаты**, поставьте вершины и замкните
контур нажатием на первую точку.
4. Назовите комнату и свяжите её с зоной Home Assistant. Для помещения без
устройств выберите «Без зоны».
5. Откройте «Устройства»: устройства связанной зоны уже размещены автоматически;
перетащите маркеры в нужные места.
6. При необходимости оформите подложку линиями, текстом и мебелью.
7. Вернитесь в «Просмотр» — теперь план показывает живые состояния и принимает
безопасные действия.
![Создание первого пространства](docs/images/03-space-create.png)
![Замыкание контура комнаты на первой точке](docs/images/04-room-contour-close.png)
![Выбранная перегородка и контекстная панель редактора плана](docs/images/05-plan-context-tray.png)
![Настройка устройства с источником привязки и точным результатом действия](docs/images/06-device-editor.png)
![Живой предпросмотр отображения того же устройства](docs/images/06-device-display-preview.png)
Пошаговые сценарии, все инструменты и особые случаи описаны в
[полном руководстве](docs/USER-GUIDE.ru.md). Возможности подложки отдельно
зафиксированы в [документе редактора](docs/DECOR-EDITOR.md), а роботов — в
[руководстве по пылесосам](docs/VACUUM.md).
<!-- docs-section: installation -->
## Установка
### Через HACS
[![Открыть репозиторий в HACS](https://my.home-assistant.io/badges/hacs_repository.svg)](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**.
Карточка регистрируется автоматически. Если ресурсы Lovelace управляются вручную,
добавьте именно URL, который публикует интеграция:
```yaml
resources:
- url: /houseplan_files/houseplan-card.js
type: module
```
Не используйте путь к файлу внутри `custom_components`: Home Assistant не
публикует его как JavaScript-модуль.
### Вручную
Скопируйте всю папку `custom_components/houseplan` из релиза в
`config/custom_components`, перезапустите Home Assistant и добавьте интеграцию
House Plan. Одного `houseplan-card.js` недостаточно: карточке также нужны
внутренний манифест и хешированные модули из того же релиза.
### Добавление карточки
Создайте представление дашборда (лучше Panel) и добавьте карточку через UI либо:
```yaml
type: custom:houseplan-card
title: План дома
```
Для нескольких экранов можно задать разные стартовые пространства:
```yaml
type: custom:houseplan-card
default_floor: ground
```
Все карточки используют общие серверные комнаты и координаты. Текущий режим,
масштаб и выбранное пространство локальны для экрана. Одновременное
редактирование поддерживает синхронизацию и проверку ревизий, но один объект
лучше не менять параллельно в двух браузерах.
## Где искать подробности
- [Полное руководство пользователя](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)
<!-- docs-section: support -->
## Помощь и обратная связь
- Вопросы и примеры планов: [Telegram @ha_houseplan](https://t.me/ha_houseplan).
- Баги и предложения: [GitHub Issues](https://github.com/Matysh/houseplan-card/issues).
- Перед отчётом обновите House Plan, перезапустите HA и выполните жёсткое
обновление страницы (`Ctrl+F5`). Приложите версию, браузер, логи и шаги
воспроизведения; приватные entity ID можно заменить вымышленными.
Скриншоты в документации получены воспроизводимой командой
`npm run build && node demo/docs/capture.mjs` только на синтетических данных. Версия сценариев,
fingerprint исходников и хеш каждого изображения находятся в
[индексе снимков](docs/images/screenshots.json).
Лицензия: [MIT](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.
-818
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More