Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8435fdb34 | ||
|
|
66bd3d6922 |
@@ -9,12 +9,6 @@
|
||||
# локально через `npm run docs:accept -- --reviewed --from=<распакованный>`.
|
||||
# Та же конструкция, что у golden-эталонов, и по той же причине: картинки
|
||||
# попадают в репозиторий через явное решение, а не через бота.
|
||||
#
|
||||
# Снимать здесь больше не обязанность, а удобство (#401). Приёмка проверяет не
|
||||
# место съёмки, а её воспроизводимость: каждый кадр, не объявленный изменённым,
|
||||
# должен совпасть с закоммиченным байт-в-байт. Эта джоба потому и удобна, что
|
||||
# среда у неё та же, в которой снят закоммиченный набор, — но принять получится
|
||||
# из любой, где кадры воспроизводятся, и не получится ни из одной, где нет.
|
||||
name: Скриншоты документации
|
||||
|
||||
on:
|
||||
@@ -77,40 +71,14 @@ jobs:
|
||||
"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
|
||||
- name: Capture
|
||||
run: node demo/docs/capture.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: |
|
||||
# Поле манифеста «до» — из закоммиченного состояния, «после» — из
|
||||
|
||||
@@ -61,15 +61,10 @@ jobs:
|
||||
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.13'
|
||||
|
||||
- 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 '
|
||||
run: pip install pytest voluptuous pytest-homeassistant-custom-component home-assistant-frontend
|
||||
|
||||
- name: Кэш браузеров Playwright
|
||||
id: pw
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
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.
|
||||
# compared, while serialising all five pairs cannot fit the job timeout.
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -39,8 +39,6 @@ jobs:
|
||||
- plan-snap
|
||||
- blend
|
||||
- overlay
|
||||
- space-default
|
||||
- space-glow
|
||||
steps:
|
||||
- name: Check out candidate
|
||||
uses: actions/checkout@v7
|
||||
@@ -202,18 +200,6 @@ jobs:
|
||||
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
|
||||
@@ -241,12 +227,6 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -226,20 +226,6 @@ jobs:
|
||||
git checkout -q "origin/$branch"
|
||||
echo "материал ревью: ветка $branch, $(git rev-parse --short HEAD)"
|
||||
echo "name=$branch" >> "$GITHUB_OUTPUT"
|
||||
# Якоря материала, устойчивые к ребейзу (#413, #414). SHA коммита
|
||||
# ребейз меняет — содержимое нет: git адресует деревья и блобы их
|
||||
# хешем. Снимаются здесь, где рабочая копия ЕЩЁ равна тому, что
|
||||
# ревьюер прочтёт; в шаге публикации дерево уже сброшено на целевую
|
||||
# ветку, и спрашивать его поздно.
|
||||
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
echo "tree=$(git rev-parse 'HEAD^{tree}')" >> "$GITHUB_OUTPUT"
|
||||
# ТЗ задачи: блоб переживает и ребейз, и удаление ветки, пока текст
|
||||
# где-нибудь достижим. Файлов может не быть (инфраструктурная
|
||||
# задача) или быть несколько (разбитое ТЗ) — тогда список пуст либо
|
||||
# длиннее одного.
|
||||
specs=$(git ls-files -s -- "docs/specs/${NUM}-*.md" \
|
||||
| awk '{print $2" "$4}' | tr '\n' ';')
|
||||
echo "specs=$specs" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::ветка issue/${NUM}-* не найдена на origin — ревью пойдёт по dev"
|
||||
echo "МАТЕРИАЛ НЕ ЗАПУШЕН" >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -280,18 +266,9 @@ jobs:
|
||||
before=$(git rev-parse "origin/$BRANCH")
|
||||
echo "dev впереди на $behind коммит(ов) — привожу ветку"
|
||||
if ! git rebase origin/dev; then
|
||||
# Список снимается ДО abort: он же снимает состояние конфликта, и
|
||||
# тогда автору достаётся «не ребейзится» без единого имени файла (#364).
|
||||
files=$(git diff --name-only --diff-filter=U | sort -u | paste -sd'\n' -)
|
||||
git rebase --abort || true
|
||||
{
|
||||
echo 'conflict=true'
|
||||
echo 'conflicts<<EOF_FILES'
|
||||
printf '%s\n' "${files:-(git не назвал файлы)}"
|
||||
echo 'EOF_FILES'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "conflict=true" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::ветка $BRANCH не ребейзится на dev без конфликта — ревью не запускается"
|
||||
printf 'конфликтуют:\n%s\n' "${files:-(git не назвал файлы)}"
|
||||
exit 0
|
||||
fi
|
||||
# --force-with-lease с явным ожидаемым значением обязателен: между
|
||||
@@ -331,17 +308,10 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||||
NUM: ${{ github.event.issue.number }}
|
||||
BRANCH: ${{ steps.branch.outputs.name }}
|
||||
CONFLICTS: ${{ steps.rebase.outputs.conflicts }}
|
||||
run: |
|
||||
cat > /tmp/stale.md <<EOF
|
||||
**Ревью не запускалось:** ветка \`$BRANCH\` не ребейзится на \`dev\` без конфликта. Код никто не читал, вердикта нет, цикл ревью не израсходован.
|
||||
|
||||
Конфликтуют:
|
||||
|
||||
\`\`\`
|
||||
$CONFLICTS
|
||||
\`\`\`
|
||||
|
||||
Проверка стоит до ревью намеренно: конфликт всё равно вернул бы задачу, но уже после сорока минут работы ревьюера и потраченных лимитов.
|
||||
|
||||
Задача переведена в \`S6-in-progress\`. Осталось:
|
||||
@@ -643,9 +613,6 @@ jobs:
|
||||
STAGE: ${{ needs.guard.outputs.stage }}
|
||||
CYCLE: ${{ needs.guard.outputs.cycle }}
|
||||
SOURCE: ${{ runner.temp }}/review-document.md
|
||||
MATERIAL_SHA: ${{ steps.branch.outputs.sha }}
|
||||
MATERIAL_TREE: ${{ steps.branch.outputs.tree }}
|
||||
MATERIAL_SPECS: ${{ steps.branch.outputs.specs }}
|
||||
run: |
|
||||
# Ветки задачи может не быть: у задач, размеченных до появления
|
||||
# конвейера, ТЗ лежит прямо в dev. Раньше шаг в этом случае молча
|
||||
@@ -659,31 +626,14 @@ jobs:
|
||||
marker=CODE-REVIEW
|
||||
if [ "$STAGE" = "spec" ]; then marker=SPEC-REVIEW; fi
|
||||
doc="docs/reviews/${marker}-${NUM}-r${CYCLE}.md"
|
||||
# Документ спасается ПЕРВЫМ делом. Ревьюер мог написать его по старому
|
||||
# пути прямо в рабочую копию, а дальше эта копия будет отброшена
|
||||
# целиком — и вместе с ней пропал бы артефакт (#220).
|
||||
if [ ! -f "$SOURCE" ] && [ -f "$doc" ]; then
|
||||
cp "$doc" "$SOURCE"
|
||||
echo "документ найден в рабочей копии и сохранён в $SOURCE"
|
||||
fi
|
||||
# Reset, а не checkout+clean, и вот почему (#365).
|
||||
#
|
||||
# 28.08 коммит bb2919f уехал в dev с тридцатью файлами вместо одного
|
||||
# markdown: откатил отревьюженную реализацию #359, вернул старые чанки
|
||||
# и держал dev откаченным три часа. Механизм воспроизведён:
|
||||
# `git checkout -- .` восстанавливает рабочее дерево ИЗ ИНДЕКСА, а
|
||||
# `git clean -fd` убирает неотслеживаемое — ни то, ни другое индекс не
|
||||
# трогает. Ревьюер работает с Bash и в ходе проверки «умеет ли тест
|
||||
# падать» вполне может сделать `git add`; всё, что осталось у него в
|
||||
# индексе, прежняя уборка сохраняла, и следующий же `git commit`
|
||||
# забирал это вместе с документом. Сообщение при этом невинное, и от
|
||||
# рутины инцидент отличается только диффом.
|
||||
#
|
||||
# `reset --hard` снимает и индекс, и дерево разом. Терять нечего:
|
||||
# документ приезжает извне репозитория, из RUNNER_TEMP.
|
||||
git fetch -q origin "$target"
|
||||
git reset -q --hard "origin/$target"
|
||||
git clean -fdq -e node_modules >/dev/null 2>&1 || true
|
||||
# Рабочая копия отбрасывается ДО того, как документ попадёт в дерево:
|
||||
# ревьюер правит код, проверяя «умеет ли тест падать», и его правки
|
||||
# публиковаться не должны.
|
||||
git checkout -- . 2>/dev/null || true
|
||||
# docs/reviews исключён из уборки: ревьюер мог написать документ по
|
||||
# старому пути, и клин не должен его съесть до `git add` — ровно так
|
||||
# оба пути остаются работоспособными.
|
||||
git clean -fd -e docs/reviews -e node_modules >/dev/null 2>&1 || true
|
||||
# Документ приезжает извне репозитория (#220). Три раунда подряд он
|
||||
# терялся, пока лежал некоммитнутым файлом в том же дереве, которое
|
||||
# ревьюер мутирует и затем восстанавливает: `git checkout -- .` плюс
|
||||
@@ -694,21 +644,12 @@ jobs:
|
||||
mkdir -p docs/reviews
|
||||
cp "$SOURCE" "$doc"
|
||||
echo "документ взят из $SOURCE ($(wc -c < "$doc") байт)"
|
||||
# Якоря дописывает конвейер, а не ревьюер (#414). Дисциплина здесь
|
||||
# уже подводила: на #403 SHA сняли до ребейза и не сверили перед
|
||||
# выводом — через раунд команда из §2.10 не работала. Машина же
|
||||
# снимает якоря в момент чтения материала и ошибиться в них не
|
||||
# может; блок помечен как машинный, чтобы никто не правил его руками.
|
||||
node scripts/review-doc-guard.mjs --anchor="$doc" \
|
||||
--sha="$MATERIAL_SHA" --tree="$MATERIAL_TREE" \
|
||||
--branch="${BRANCH:-dev}" --specs="$MATERIAL_SPECS"
|
||||
else
|
||||
echo "::warning::$SOURCE не найден — документа для публикации нет"
|
||||
# Совместимость: ревьюер мог написать по старому пути, если промпт
|
||||
# ещё не обновился в этой ветке.
|
||||
echo "::warning::$SOURCE не найден — ищу документ в рабочей копии"
|
||||
fi
|
||||
# Индексируется ровно один путь, а не каталог: `git add docs/reviews`
|
||||
# забрал бы всё, что там окажется, а после reset там не должно быть
|
||||
# ничего постороннего — но полагаться на «не должно» здесь нельзя.
|
||||
git add -- "$doc" 2>/dev/null || true
|
||||
git add docs/reviews 2>/dev/null || true
|
||||
if git diff --cached --quiet; then
|
||||
# Пустая рабочая копия — ещё не провал: ревьюер иногда коммитит
|
||||
# документ сам, своим app-токеном мимо этого шага (CODE-REVIEW-150-r1,
|
||||
@@ -726,8 +667,6 @@ jobs:
|
||||
echo "::error::вердикт есть, а документа нет: ни $SOURCE, ни $doc в рабочей копии, ни $doc в $target — ревью без артефакта (#171, #220)"
|
||||
exit 1
|
||||
fi
|
||||
# Первый рубеж: что вообще проиндексировано.
|
||||
git diff --cached --name-only | node scripts/review-doc-guard.mjs
|
||||
git -c user.name="claude[bot]" \
|
||||
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
|
||||
commit -q -F - <<EOF
|
||||
@@ -736,10 +675,6 @@ jobs:
|
||||
Issue: #$NUM
|
||||
User-Visible: no
|
||||
EOF
|
||||
# Второй рубеж, и он главный: что пуш ДОБАВИТ в целевую ветку. Первый
|
||||
# судит намерение шага, этот — результат, а расходились они именно
|
||||
# тогда, когда база оказывалась не той.
|
||||
git diff --name-only "origin/$target...HEAD" | node scripts/review-doc-guard.mjs
|
||||
# Публикация в dev идёт из детачнутого состояния поверх ветки задачи
|
||||
# либо dev, поэтому push нужен с явным перебазированием при гонке:
|
||||
# dev мог уйти вперёд, пока шло ревью — оно длится до 45 минут.
|
||||
@@ -754,9 +689,6 @@ jobs:
|
||||
echo "::error::документ ревью не удалось опубликовать в $target: конфликт (#171)"
|
||||
exit 1
|
||||
fi
|
||||
# После ребейза набор путей другой — проверяется заново. Форс здесь
|
||||
# запрещён и не появляется: ветка двигается только вперёд.
|
||||
git diff --name-only "origin/$target...HEAD" | node scripts/review-doc-guard.mjs
|
||||
git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \
|
||||
"HEAD:$target"
|
||||
fi
|
||||
@@ -770,42 +702,6 @@ jobs:
|
||||
fi
|
||||
echo "документ опубликован в $target: $doc"
|
||||
|
||||
# Материал раунда обязан быть достижим с origin (#413).
|
||||
#
|
||||
# SPEC-REVIEW-403-r2 объявил материал на `HEAD = 83005c3c`, и тот же SHA
|
||||
# независимо назвал автор ТЗ в комментарии issue. Коммит существовал, но
|
||||
# к моменту публикации был осиротевшим: ветку перебазировали за 15 минут
|
||||
# ДО публикации документа, спец-коммит переехал в 94502d3d с тем же
|
||||
# сообщением и тем же содержимым. Через раунд команда `git diff
|
||||
# 83005c3c..HEAD` из §2.10 буквально не работала, и r3 восстанавливал
|
||||
# реальный коммит по содержимому диффа руками.
|
||||
#
|
||||
# Проверка стоит ПОСЛЕ публикации намеренно. Артефакт ревью терялся здесь
|
||||
# трижды (#171, #220), и «вердикт без документа» в этом репозитории
|
||||
# дороже мёртвой ссылки: документ сначала спасается, потом судится. Шаг
|
||||
# при этом идёт ДО «Переставить метку», поэтому инвариант «метка не
|
||||
# сменилась = прогон упал» сохраняется.
|
||||
#
|
||||
# Достижимость считается от `refs/remotes/origin/*`, а не от локальных
|
||||
# ссылок: осиротевший 83005c3c до сих пор лежит в клоне автора и
|
||||
# достижим там из необновлённой локальной ветки. Читателю отчёта от этого
|
||||
# пользы нет — он достанет только то, что есть на origin.
|
||||
- name: "Материал раунда воспроизводим (#413)"
|
||||
if: steps.rebase.outputs.conflict != 'true'
|
||||
env:
|
||||
NUM: ${{ github.event.issue.number }}
|
||||
STAGE: ${{ needs.guard.outputs.stage }}
|
||||
CYCLE: ${{ needs.guard.outputs.cycle }}
|
||||
BRANCH: ${{ steps.branch.outputs.name }}
|
||||
run: |
|
||||
marker=CODE-REVIEW
|
||||
if [ "$STAGE" = "spec" ]; then marker=SPEC-REVIEW; fi
|
||||
doc="docs/reviews/${marker}-${NUM}-r${CYCLE}.md"
|
||||
target="${BRANCH:-dev}"
|
||||
git fetch -q origin "$target"
|
||||
# Судится опубликованная версия, а не рабочая копия: именно её прочтёт
|
||||
# следующий раунд.
|
||||
git show "origin/$target:$doc" | node scripts/review-doc-guard.mjs --doc=-
|
||||
- name: Решение по вердикту
|
||||
id: decide
|
||||
if: steps.rebase.outputs.conflict != 'true'
|
||||
@@ -838,30 +734,6 @@ jobs:
|
||||
echo "from=$from" >> "$GITHUB_OUTPUT"
|
||||
echo "to=$to" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Ревью идёт десятки минут, а dev за это время двигается (28 августа —
|
||||
# четыре раза за день). Вердикт при этом вынесен по дереву, которое уже не
|
||||
# совпадает с вершиной линии, и слияние приведёт ветку к dev — то есть в
|
||||
# dev уедет код, отличный от прочитанного (§7.2). Молчать об этом нельзя,
|
||||
# но и шуметь на каждом прогоне ни к чему: строка появляется только когда
|
||||
# dev действительно ушёл и вердикт зелёный, то есть слияние вот-вот
|
||||
# случится (#364).
|
||||
- name: dev ушёл вперёд, пока шло ревью
|
||||
if: steps.rebase.outputs.conflict != 'true' && needs.guard.outputs.stage == 'code'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||||
NUM: ${{ github.event.issue.number }}
|
||||
MATERIAL: ${{ steps.material.outputs.sha }}
|
||||
GREEN: ${{ steps.decide.outputs.green }}
|
||||
run: |
|
||||
git fetch -q origin dev
|
||||
moved=$(git rev-list --count "$MATERIAL..origin/dev")
|
||||
echo "dev продвинулся на $moved коммит(ов) с момента фиксации материала"
|
||||
echo "- dev продвинулся на **$moved** коммит(ов) во время ревью" >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ "$moved" -eq 0 ] || [ "$GREEN" != "true" ]; then exit 0; fi
|
||||
short=$(git rev-parse --short "$MATERIAL")
|
||||
gh issue comment "$NUM" --repo "${{ github.repository }}" --body \
|
||||
"Пока шло ревью, \`dev\` продвинулся на $moved коммит(ов). Материал ревью — \`$short\`. Вердикт вынесен по дереву, которое уже не совпадает с вершиной линии: слияние приведёт ветку к dev, и это другой код (§7.2)."
|
||||
|
||||
# S8-merged утверждает, что код в dev. Значит слияние обязано произойти
|
||||
# ДО метки, иначе она врёт в промежутке.
|
||||
#
|
||||
|
||||
@@ -32,13 +32,6 @@ jobs:
|
||||
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 нужны сообщения
|
||||
# коммитов, трейлеры и ИМЕНА изменённых файлов, а не содержимое старых
|
||||
@@ -76,38 +69,12 @@ jobs:
|
||||
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 }}
|
||||
BEFORE_SHA: ${{ github.event.before }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
DEVELOPMENT_BRANCH: dev
|
||||
@@ -125,7 +92,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BEFORE_SHA: ${{ steps.range.outputs.base || github.event.before }}
|
||||
BEFORE_SHA: ${{ github.event.before }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
DEVELOPMENT_BRANCH: dev
|
||||
@@ -173,65 +140,20 @@ jobs:
|
||||
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: |
|
||||
@@ -241,45 +163,39 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
zero=$(printf '%040d' 0)
|
||||
base="$BEFORE_SHA"
|
||||
if [ "$EVENT_NAME" = "pull_request" ]; then base="$BASE_SHA"; fi
|
||||
# #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 переписан — полный прогон без классификации"
|
||||
# force-push (ребейз/аменд переписал историю ветки). Диапазон в
|
||||
# этом случае НЕ угадывается: merge-base после переписанной истории
|
||||
# уже выдавал «изменились только два doc-файла» при реальном диффе
|
||||
# в custom_components/**, и тяжёлые job тихо скипались, а общий
|
||||
# статус рана оставался success — ровно класс тихого пропуска
|
||||
# #171/#207. Force-push редок, полный прогон дешевле ложного
|
||||
# «зелёного».
|
||||
if [ -n "$base" ] && [ "$base" != "$zero" ] \
|
||||
&& ! git cat-file -e "$base" 2>/dev/null; then
|
||||
echo "force-push: before=$base переписан — полный прогон без классификации"
|
||||
{
|
||||
echo "### Классификация изменённых файлов"
|
||||
echo "\`before\` (=$BEFORE_SHA) переписан force-push'ем: диапазон недоказуем,"
|
||||
echo "\`before\` (=$base) переписан 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
|
||||
# Новая ветка: before нулевой, диапазон считается от merge-base с dev,
|
||||
# иначе классифицировалась бы вся история.
|
||||
if [ -z "$base" ] || [ "$base" = "$zero" ]; then
|
||||
git fetch -q origin dev
|
||||
base=$(git merge-base origin/dev "$HEAD_SHA" || echo "$HEAD_SHA~1")
|
||||
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 "backend=$(has '^(custom_components/.*\.py$|tests_backend/|pytest\.ini$)')"
|
||||
echo "integration=$(has '^(custom_components/houseplan/manifest\.json$|hacs\.json$|custom_components/.*\.py$|custom_components/.*/translations/)')"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -404,10 +320,7 @@ jobs:
|
||||
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
|
||||
@@ -415,40 +328,6 @@ jobs:
|
||||
- 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
|
||||
@@ -489,7 +368,6 @@ jobs:
|
||||
needs: [frontend, reuse]
|
||||
if: needs.reuse.outputs.smoke != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
# Смоки шардируются: последовательный прогон занимал ~7.5 минут и был
|
||||
# критическим путём всего Validate. Три шарда режут его примерно вдвое;
|
||||
# цена — трижды `npm ci` и сборка, около двух оплаченных минут раннера.
|
||||
@@ -533,14 +411,6 @@ jobs:
|
||||
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 }}
|
||||
@@ -557,20 +427,10 @@ jobs:
|
||||
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
|
||||
if 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
|
||||
@@ -647,14 +507,6 @@ jobs:
|
||||
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: |
|
||||
@@ -671,25 +523,6 @@ jobs:
|
||||
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: Записать маркер успеха
|
||||
@@ -752,11 +585,9 @@ jobs:
|
||||
- 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)
|
||||
@@ -796,57 +627,10 @@ jobs:
|
||||
- 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
|
||||
with: { python-version: "3.13" }
|
||||
- run: pip install pytest voluptuous pytest-homeassistant-custom-component home-assistant-frontend
|
||||
- name: Backend unit tests (pure + HA harness)
|
||||
run: python -m pytest tests_backend/ -q
|
||||
# Маркер пишется последним шагом: он существует только если всё выше
|
||||
# прошло. Кэш сохраняется post-шагом, то есть тоже лишь при успехе job.
|
||||
- name: Записать маркер успеха
|
||||
|
||||
@@ -193,35 +193,6 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
кода отвечает на вопрос «оно вообще работает»: каждый AC либо доказан
|
||||
автотестом — и ревьюер убедился, что **тест умеет падать**, — либо разобран по
|
||||
коду с явной записью «проверено чтением, не исполнением».
|
||||
- **Защитный AC доказывается таблицей «чем краснеет» (#435).** Для каждого AC,
|
||||
заявляющего защиту — валидация, гард, лимит, отказ, инвариант, — в документе
|
||||
ревью обязательна строка из трёх столбцов: **AC · чем доказан** (точная
|
||||
команда или имя теста) **· чем краснеет** — мутация, снятая защита или
|
||||
отрицательная проба, с результатом прогона. Пустой третий столбец — находка
|
||||
Medium, а не примечание.
|
||||
|
||||
«Тест умеет падать» без названной мутации и её вывода доказательством не
|
||||
является. Аудит v1.71.0-beta.1 нашёл пять контрактов #51 и #423, где тест
|
||||
оставался зелёным на снятой защите; все пять прошли код-ревью как доказанные,
|
||||
а два теста были записаны в закрытие coverage-ratchet под именами, обещавшими
|
||||
то, чего они не проверяли (#430).
|
||||
|
||||
Мутант в `scripts/mutation-gate.mjs` обязателен, когда защита живёт в
|
||||
продуктовом коде и проверяется дорогим гейтом (смок, бэкенд, golden): там
|
||||
ревьюер не воспроизведёт отрицательный прогон второй раз. Для чистых юнитов
|
||||
достаточно прогона со снятой защитой, приведённого в документе.
|
||||
|
||||
Считаются **защитные AC без названного свидетеля**, а не мутанты на
|
||||
подсистему: у #421 мутанты были, и дыра всё равно проехала. «Сколько мутантов
|
||||
принесла задача» остаётся признаком — у #423 их ноль, и именно у #423 нашёлся
|
||||
тест, спрашивавший регулярку, находит ли она подстроку, которую сам же и
|
||||
вырезал.
|
||||
|
||||
Правило не распространяется на AC, не заявляющие защиту (расположение, текст,
|
||||
формат вывода): там свидетель — обычное сравнение ожидаемого с фактическим, и
|
||||
третий столбец превратился бы в ритуал. И не отменяет «проверено чтением»:
|
||||
тогда во втором столбце стоит «чтением», а не имя теста, и читатель ревью
|
||||
видит разницу.
|
||||
- **High блокируют.** Medium **в скоупе задачи** чинится в текущем issue:
|
||||
без High это жёлтый вердикт и возврат автору, фикс проходит повторный цикл.
|
||||
Medium **вне скоупа** — отдельный issue (#202).
|
||||
@@ -266,36 +237,16 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
|
||||
Порядок:
|
||||
|
||||
1. найти вердикт предыдущего раунда и **материал, на котором он получен**.
|
||||
Материал объявлен блоком «Материал раунда» в конце документа предыдущего
|
||||
раунда: конвейер дописывает туда SHA ветки, **дерево** материала и **блоб**
|
||||
каждого ТЗ вместе с командами поиска (issue #416). Блок машинный — править
|
||||
его руками не нужно и не следует;
|
||||
1. найти вердикт предыдущего раунда и **SHA, на котором он получен**; SHA в
|
||||
вердикте не назван — это находка;
|
||||
2. объявить дельту: `git diff <тот SHA>..HEAD` для кода, дифф файла ТЗ либо тела
|
||||
issue для этапа ТЗ.
|
||||
|
||||
**Если SHA не резолвится — это не находка, а обычное дело.** Ветку задачи
|
||||
между раундами перебазируют, сквошат или удаляют, и SHA умирает: по корпусу
|
||||
ревью таких объявлений 98 из 804. Материал в этом случае берётся по якорям,
|
||||
которые ребейз не меняет, потому что адресуются содержимым:
|
||||
|
||||
```
|
||||
git log --all --format='%H %T' | grep <дерево>
|
||||
git log --all --find-object=<блоб> -- <путь к ТЗ>
|
||||
```
|
||||
|
||||
Находкой остаётся другое: **SHA, мёртвый уже в момент публикации отчёта** —
|
||||
он означает, что значение сняли до `amend` или `rebase` и не сверили перед
|
||||
выводом, как требует §7.2. Это отличие не теоретическое: на #403 оба
|
||||
источника, автор и ревьюер, независимо назвали один и тот же осиротевший
|
||||
SHA, и следующий раунд восстанавливал коммит по содержимому диффа руками
|
||||
(issue #413). Конвейер теперь такую публикацию останавливает сам;
|
||||
issue для этапа ТЗ;
|
||||
3. по каждой находке предыдущего раунда показать, **чем именно она закрыта** —
|
||||
строкой кода или текста, а не заявлением автора;
|
||||
4. заново проверять только те AC, чьё доказательство дельта задевает;
|
||||
5. **раздел «Унаследовано из r<N−1>»** обязателен: что принято без повторной
|
||||
проверки, со ссылкой на документ того раунда и его материал. Без перечня
|
||||
сокращение превращается в молчаливое доверие.
|
||||
проверки, со ссылкой на документ того раунда и SHA. Без перечня сокращение
|
||||
превращается в молчаливое доверие.
|
||||
|
||||
Дешёвые гейты (`typecheck`, `test`, `build` со сверкой копий бандла) гоняются в
|
||||
каждом раунде: код изменился, а стоят они минуты. Тяжёлые — по дельте (§10.2).
|
||||
@@ -616,23 +567,12 @@ npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend
|
||||
# копия стенда собирается `npm run bundle:sync`, в репозитории её нет (#255)
|
||||
node scripts/smoke-select.mjs --base origin/dev --head HEAD # какие смоки относятся к диффу
|
||||
node demo/smoke_<целевые>.mjs
|
||||
node scripts/no-new-any.mjs --base origin/dev --head HEAD # новый код не добавляет any
|
||||
npm run golden:verify # если менялся визуал
|
||||
node scripts/check-docs.mjs # если менялся src/**
|
||||
node scripts/model-invariants.mjs --config <экспорт> # если правилась геометрия или ссылки
|
||||
python -m pytest tests_backend -q # py3.13, если менялся бэкенд
|
||||
```
|
||||
|
||||
**Новый код не добавляет `any`** (#342). В `src/**` уже 1034 вхождения явного
|
||||
`any` в 49 файлах; перетипизировать это одним заходом — месяц риска ради нуля
|
||||
пользовательской ценности, поэтому долг снимается при плановом извлечении
|
||||
подсистем (#34), а не разовой заменой. Гейт `scripts/no-new-any.mjs` судит
|
||||
**только добавленные строки**: существующий долг на нетронутой строке законен,
|
||||
правка строки со старым `any` — новая ответственность. Исключение объявляется на
|
||||
той же строке, `// any-ok: <конкретная причина>`; голый маркер и причины вида
|
||||
«todo» не проходят. Текст разбирается парсером TypeScript, поэтому слово «any» в
|
||||
комментарии, строке или идентификаторе ложных срабатываний не даёт.
|
||||
|
||||
**Объём гейтов на код-ревью соразмерен задаче** (issue #127). Всегда:
|
||||
`typecheck`, `npm test`, `npm run build` со сверкой трёх копий бандла, а при
|
||||
любом diff'е по `src/**` — ещё и `node scripts/check-docs.mjs`. По
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Sergey Matyunin (Matysh)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,28 +0,0 @@
|
||||
# House Plan furniture pack 0.3.0
|
||||
|
||||
Canonical source artwork for the built-in House Plan furniture library.
|
||||
|
||||
- `svg/menu/`: 33 front-view category illustrations used only by the lazy
|
||||
editor bundle.
|
||||
- `svg/plan/`: 44 top-view drawings used on the plan.
|
||||
- `pack.json`: stable ids, category links, default dimensions and names. The
|
||||
filename deliberately avoids the `*manifest.json` suffix reserved by HACS.
|
||||
|
||||
The original author, Sergey Matyunin (`Matysh`), granted House Plan permission
|
||||
to use, modify and distribute all 77 SVG files under the repository MIT
|
||||
License without separate UI attribution in
|
||||
[issue #159](https://github.com/Matysh/houseplan-card/issues/159#issuecomment-5454085168).
|
||||
|
||||
The reviewed source archive is `houseplan-furniture-custom-0.3.0.zip`, attached
|
||||
to [issue #159](https://github.com/Matysh/houseplan-card/issues/159#issuecomment-5449707137),
|
||||
with SHA-256
|
||||
`9E969016EE3B4B4E3DB776FEC53C8B387B91368B118EB5E39911483DEF1B0953`.
|
||||
|
||||
The editable source is linked from `pack.json`. Generated TypeScript must
|
||||
not be edited by hand; run `npm run furniture:generate` after changing this
|
||||
directory.
|
||||
|
||||
Note (#369): the source archive referenced by the SHA-256 above may still
|
||||
carry the earlier romanisation of the author's name; the authoritative
|
||||
spelling is Sergey Matyunin (Сергей Матюнин), fixed 2026-08-29 by the
|
||||
owner's decision in issue #369. The archive bytes are unchanged.
|
||||
@@ -1,818 +0,0 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"view_schema": 2,
|
||||
"pack_id": "houseplan",
|
||||
"pack_version": "0.3.0",
|
||||
"title_ru": "Набор мебели: меню и план",
|
||||
"title_en": "Furniture pack: menu and plan",
|
||||
"author": "Sergey Matyunin (Matysh)",
|
||||
"license": "MIT",
|
||||
"source_url": "https://www.figma.com/design/cpGN8MhJGydwVOUzZv8dia/House-plan?node-id=271-95",
|
||||
"menu_icons": [
|
||||
{
|
||||
"id": "air_conditioner",
|
||||
"name_ru": "Кондиционер",
|
||||
"name_en": "Air conditioner",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/air_conditioner.svg"
|
||||
},
|
||||
{
|
||||
"id": "armchair",
|
||||
"name_ru": "Кресла",
|
||||
"name_en": "Armchairs",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/armchair.svg"
|
||||
},
|
||||
{
|
||||
"id": "bathtub",
|
||||
"name_ru": "Ванны",
|
||||
"name_en": "Bathtubs",
|
||||
"group": "sanitary",
|
||||
"file": "svg/menu/bathtub.svg"
|
||||
},
|
||||
{
|
||||
"id": "bed",
|
||||
"name_ru": "Кровати",
|
||||
"name_en": "Beds",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/bed.svg"
|
||||
},
|
||||
{
|
||||
"id": "bidet",
|
||||
"name_ru": "Биде",
|
||||
"name_en": "Bidets",
|
||||
"group": "sanitary",
|
||||
"file": "svg/menu/bidet.svg"
|
||||
},
|
||||
{
|
||||
"id": "boiler",
|
||||
"name_ru": "Бойлер",
|
||||
"name_en": "Water heater",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/boiler.svg"
|
||||
},
|
||||
{
|
||||
"id": "chair",
|
||||
"name_ru": "Стулья",
|
||||
"name_en": "Chairs",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/chair.svg"
|
||||
},
|
||||
{
|
||||
"id": "coffee_table",
|
||||
"name_ru": "Журнальные столы",
|
||||
"name_en": "Coffee tables",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/coffee_table.svg"
|
||||
},
|
||||
{
|
||||
"id": "computer",
|
||||
"name_ru": "Компьютер",
|
||||
"name_en": "Computer",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/computer.svg"
|
||||
},
|
||||
{
|
||||
"id": "cooktop",
|
||||
"name_ru": "Варочные панели",
|
||||
"name_en": "Cooktops",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/cooktop.svg"
|
||||
},
|
||||
{
|
||||
"id": "dining_table",
|
||||
"name_ru": "Обеденные столы",
|
||||
"name_en": "Dining tables",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/dining_table.svg"
|
||||
},
|
||||
{
|
||||
"id": "dishwasher",
|
||||
"name_ru": "Посудомоечная машина",
|
||||
"name_en": "Dishwasher",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/dishwasher.svg"
|
||||
},
|
||||
{
|
||||
"id": "dryer",
|
||||
"name_ru": "Сушильная машина",
|
||||
"name_en": "Dryer",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/dryer.svg"
|
||||
},
|
||||
{
|
||||
"id": "exercise",
|
||||
"name_ru": "Тренажёр",
|
||||
"name_en": "Exercise equipment",
|
||||
"group": "other",
|
||||
"file": "svg/menu/exercise.svg"
|
||||
},
|
||||
{
|
||||
"id": "fireplace",
|
||||
"name_ru": "Камин",
|
||||
"name_en": "Fireplace",
|
||||
"group": "other",
|
||||
"file": "svg/menu/fireplace.svg"
|
||||
},
|
||||
{
|
||||
"id": "fridge",
|
||||
"name_ru": "Холодильник",
|
||||
"name_en": "Refrigerator",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/fridge.svg"
|
||||
},
|
||||
{
|
||||
"id": "hood",
|
||||
"name_ru": "Вытяжка",
|
||||
"name_en": "Range hood",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/hood.svg"
|
||||
},
|
||||
{
|
||||
"id": "kitchen_cabinet",
|
||||
"name_ru": "Кухонные шкафы",
|
||||
"name_en": "Kitchen cabinets",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/kitchen_cabinet.svg"
|
||||
},
|
||||
{
|
||||
"id": "kitchen_sink",
|
||||
"name_ru": "Кухонные мойки",
|
||||
"name_en": "Kitchen sinks",
|
||||
"group": "sanitary",
|
||||
"file": "svg/menu/kitchen_sink.svg"
|
||||
},
|
||||
{
|
||||
"id": "nightstand",
|
||||
"name_ru": "Тумбы",
|
||||
"name_en": "Cabinets",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/nightstand.svg"
|
||||
},
|
||||
{
|
||||
"id": "oven",
|
||||
"name_ru": "Духовка",
|
||||
"name_en": "Oven",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/oven.svg"
|
||||
},
|
||||
{
|
||||
"id": "plant",
|
||||
"name_ru": "Растение",
|
||||
"name_en": "Plant",
|
||||
"group": "other",
|
||||
"file": "svg/menu/plant.svg"
|
||||
},
|
||||
{
|
||||
"id": "rug",
|
||||
"name_ru": "Ковер",
|
||||
"name_en": "Rug",
|
||||
"group": "other",
|
||||
"file": "svg/menu/rug.svg"
|
||||
},
|
||||
{
|
||||
"id": "shelving",
|
||||
"name_ru": "Стеллажи",
|
||||
"name_en": "Shelving",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/shelving.svg"
|
||||
},
|
||||
{
|
||||
"id": "shower",
|
||||
"name_ru": "Душ",
|
||||
"name_en": "Shower",
|
||||
"group": "sanitary",
|
||||
"file": "svg/menu/shower.svg"
|
||||
},
|
||||
{
|
||||
"id": "sink",
|
||||
"name_ru": "Раковина",
|
||||
"name_en": "Sink",
|
||||
"group": "sanitary",
|
||||
"file": "svg/menu/sink.svg"
|
||||
},
|
||||
{
|
||||
"id": "sofa",
|
||||
"name_ru": "Диваны",
|
||||
"name_en": "Sofas",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/sofa.svg"
|
||||
},
|
||||
{
|
||||
"id": "stairs",
|
||||
"name_ru": "Лестница",
|
||||
"name_en": "Stairs",
|
||||
"group": "other",
|
||||
"file": "svg/menu/stairs.svg"
|
||||
},
|
||||
{
|
||||
"id": "toilet",
|
||||
"name_ru": "Унитазы",
|
||||
"name_en": "Toilets",
|
||||
"group": "sanitary",
|
||||
"file": "svg/menu/toilet.svg"
|
||||
},
|
||||
{
|
||||
"id": "tv",
|
||||
"name_ru": "Телевизоры",
|
||||
"name_en": "Televisions",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/tv.svg"
|
||||
},
|
||||
{
|
||||
"id": "wardrobe",
|
||||
"name_ru": "Шкафы",
|
||||
"name_en": "Wardrobes",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/wardrobe.svg"
|
||||
},
|
||||
{
|
||||
"id": "washer",
|
||||
"name_ru": "Стиральная машина",
|
||||
"name_en": "Washing machine",
|
||||
"group": "appliance",
|
||||
"file": "svg/menu/washer.svg"
|
||||
},
|
||||
{
|
||||
"id": "work_table",
|
||||
"name_ru": "Рабочие столы",
|
||||
"name_en": "Desks",
|
||||
"group": "furniture",
|
||||
"file": "svg/menu/work_table.svg"
|
||||
}
|
||||
],
|
||||
"symbols": [
|
||||
{
|
||||
"id": "coffee_table",
|
||||
"operation": "replace",
|
||||
"name_ru": "Журнальный стол, прямоугольный",
|
||||
"name_en": "Rectangular coffee table",
|
||||
"group": "furniture",
|
||||
"width_cm": 120,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/coffee_table.svg",
|
||||
"menu_icon": "coffee_table",
|
||||
"notes": "Типовой размер; свободно стоящий предмет."
|
||||
},
|
||||
{
|
||||
"id": "coffee_table_round",
|
||||
"operation": "add",
|
||||
"name_ru": "Журнальный стол, круглый",
|
||||
"name_en": "Round coffee table",
|
||||
"group": "furniture",
|
||||
"width_cm": 80,
|
||||
"depth_cm": 80,
|
||||
"back": "top",
|
||||
"file": "svg/plan/coffee_table_round.svg",
|
||||
"menu_icon": "coffee_table",
|
||||
"notes": "Круглый свободно стоящий предмет."
|
||||
},
|
||||
{
|
||||
"id": "coffee_table_oval",
|
||||
"operation": "add",
|
||||
"name_ru": "Журнальный стол, овальный",
|
||||
"name_en": "Oval coffee table",
|
||||
"group": "furniture",
|
||||
"width_cm": 120,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/coffee_table_oval.svg",
|
||||
"menu_icon": "coffee_table",
|
||||
"notes": "Свободно стоящий предмет."
|
||||
},
|
||||
{
|
||||
"id": "coffee_table_rounded",
|
||||
"operation": "add",
|
||||
"name_ru": "Журнальный стол, скруглённый",
|
||||
"name_en": "Rounded coffee table",
|
||||
"group": "furniture",
|
||||
"width_cm": 120,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/coffee_table_rounded.svg",
|
||||
"menu_icon": "coffee_table",
|
||||
"notes": "Прямоугольная форма со скруглёнными углами."
|
||||
},
|
||||
{
|
||||
"id": "table_dining",
|
||||
"operation": "replace",
|
||||
"name_ru": "Обеденный стол, прямоугольный",
|
||||
"name_en": "Rectangular dining table",
|
||||
"group": "furniture",
|
||||
"width_cm": 160,
|
||||
"depth_cm": 90,
|
||||
"back": "top",
|
||||
"file": "svg/plan/table_dining.svg",
|
||||
"menu_icon": "dining_table",
|
||||
"notes": "Типовой стол на 4–6 мест."
|
||||
},
|
||||
{
|
||||
"id": "table_round",
|
||||
"operation": "replace",
|
||||
"name_ru": "Обеденный стол, круглый",
|
||||
"name_en": "Round dining table",
|
||||
"group": "furniture",
|
||||
"width_cm": 110,
|
||||
"depth_cm": 110,
|
||||
"back": "top",
|
||||
"file": "svg/plan/table_round.svg",
|
||||
"menu_icon": "dining_table",
|
||||
"notes": "Круглый стол на 4 места."
|
||||
},
|
||||
{
|
||||
"id": "table_dining_oval",
|
||||
"operation": "add",
|
||||
"name_ru": "Обеденный стол, овальный",
|
||||
"name_en": "Oval dining table",
|
||||
"group": "furniture",
|
||||
"width_cm": 180,
|
||||
"depth_cm": 100,
|
||||
"back": "top",
|
||||
"file": "svg/plan/table_dining_oval.svg",
|
||||
"menu_icon": "dining_table",
|
||||
"notes": "Овальный стол на 6 мест."
|
||||
},
|
||||
{
|
||||
"id": "table_dining_rounded",
|
||||
"operation": "add",
|
||||
"name_ru": "Обеденный стол, скруглённый",
|
||||
"name_en": "Rounded dining table",
|
||||
"group": "furniture",
|
||||
"width_cm": 160,
|
||||
"depth_cm": 90,
|
||||
"back": "top",
|
||||
"file": "svg/plan/table_dining_rounded.svg",
|
||||
"menu_icon": "dining_table",
|
||||
"notes": "Прямоугольная форма со скруглёнными углами."
|
||||
},
|
||||
{
|
||||
"id": "desk",
|
||||
"operation": "replace",
|
||||
"name_ru": "Рабочий стол, прямоугольный",
|
||||
"name_en": "Rectangular desk",
|
||||
"group": "furniture",
|
||||
"width_cm": 140,
|
||||
"depth_cm": 70,
|
||||
"back": "top",
|
||||
"file": "svg/plan/desk.svg",
|
||||
"menu_icon": "work_table",
|
||||
"notes": "BACK — длинная сторона у стены."
|
||||
},
|
||||
{
|
||||
"id": "desk_corner",
|
||||
"operation": "add",
|
||||
"name_ru": "Рабочий стол, угловой",
|
||||
"name_en": "Corner desk",
|
||||
"group": "furniture",
|
||||
"width_cm": 160,
|
||||
"depth_cm": 160,
|
||||
"back": "top",
|
||||
"file": "svg/plan/desk_corner.svg",
|
||||
"menu_icon": "work_table",
|
||||
"notes": "Угловой стол; основная задняя сторона направлена вверх."
|
||||
},
|
||||
{
|
||||
"id": "chair",
|
||||
"operation": "replace",
|
||||
"name_ru": "Стул",
|
||||
"name_en": "Chair",
|
||||
"group": "furniture",
|
||||
"width_cm": 50,
|
||||
"depth_cm": 50,
|
||||
"back": "top",
|
||||
"file": "svg/plan/chair.svg",
|
||||
"menu_icon": "chair",
|
||||
"notes": "Спинка находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "chair_bar",
|
||||
"operation": "add",
|
||||
"name_ru": "Барный стул",
|
||||
"name_en": "Bar stool",
|
||||
"group": "furniture",
|
||||
"width_cm": 45,
|
||||
"depth_cm": 48,
|
||||
"back": "top",
|
||||
"file": "svg/plan/chair_bar.svg",
|
||||
"menu_icon": "chair",
|
||||
"notes": "Спинка находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "armchair",
|
||||
"operation": "replace",
|
||||
"name_ru": "Кресло мягкое",
|
||||
"name_en": "Soft armchair",
|
||||
"group": "furniture",
|
||||
"width_cm": 90,
|
||||
"depth_cm": 90,
|
||||
"back": "top",
|
||||
"file": "svg/plan/armchair.svg",
|
||||
"menu_icon": "armchair",
|
||||
"notes": "Спинка находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "armchair_office",
|
||||
"operation": "add",
|
||||
"name_ru": "Кресло офисное",
|
||||
"name_en": "Office chair",
|
||||
"group": "furniture",
|
||||
"width_cm": 65,
|
||||
"depth_cm": 65,
|
||||
"back": "top",
|
||||
"file": "svg/plan/armchair_office.svg",
|
||||
"menu_icon": "armchair",
|
||||
"notes": "Спинка находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "sofa",
|
||||
"operation": "replace",
|
||||
"name_ru": "Диван двухместный",
|
||||
"name_en": "Two-seat sofa",
|
||||
"group": "furniture",
|
||||
"width_cm": 180,
|
||||
"depth_cm": 90,
|
||||
"back": "top",
|
||||
"file": "svg/plan/sofa.svg",
|
||||
"menu_icon": "sofa",
|
||||
"notes": "Спинка находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "sofa_three_seat",
|
||||
"operation": "add",
|
||||
"name_ru": "Диван трёхместный",
|
||||
"name_en": "Three-seat sofa",
|
||||
"group": "furniture",
|
||||
"width_cm": 240,
|
||||
"depth_cm": 90,
|
||||
"back": "top",
|
||||
"file": "svg/plan/sofa_three_seat.svg",
|
||||
"menu_icon": "sofa",
|
||||
"notes": "Спинка находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "sofa_corner_right",
|
||||
"operation": "add",
|
||||
"name_ru": "Угловой диван, правый",
|
||||
"name_en": "Right sectional sofa",
|
||||
"group": "furniture",
|
||||
"width_cm": 260,
|
||||
"depth_cm": 170,
|
||||
"back": "top",
|
||||
"file": "svg/plan/sofa_corner_right.svg",
|
||||
"menu_icon": "sofa",
|
||||
"notes": "Шезлонг справа при взгляде сверху; спинка сверху."
|
||||
},
|
||||
{
|
||||
"id": "bed_single",
|
||||
"operation": "replace",
|
||||
"name_ru": "Кровать односпальная",
|
||||
"name_en": "Single bed",
|
||||
"group": "furniture",
|
||||
"width_cm": 90,
|
||||
"depth_cm": 200,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bed_single.svg",
|
||||
"menu_icon": "bed",
|
||||
"notes": "Узкая кровать с одной подушкой; изголовье сверху."
|
||||
},
|
||||
{
|
||||
"id": "bed_double",
|
||||
"operation": "replace",
|
||||
"name_ru": "Кровать двуспальная",
|
||||
"name_en": "Double bed",
|
||||
"group": "furniture",
|
||||
"width_cm": 160,
|
||||
"depth_cm": 200,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bed_double.svg",
|
||||
"menu_icon": "bed",
|
||||
"notes": "Широкая кровать с двумя подушками; изголовье сверху."
|
||||
},
|
||||
{
|
||||
"id": "nightstand",
|
||||
"operation": "replace",
|
||||
"name_ru": "Тумба прикроватная",
|
||||
"name_en": "Bedside table",
|
||||
"group": "furniture",
|
||||
"width_cm": 50,
|
||||
"depth_cm": 40,
|
||||
"back": "top",
|
||||
"file": "svg/plan/nightstand.svg",
|
||||
"menu_icon": "nightstand",
|
||||
"notes": "Задняя сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "cabinet_tv",
|
||||
"operation": "add",
|
||||
"name_ru": "Тумба под телевизор",
|
||||
"name_en": "TV cabinet",
|
||||
"group": "furniture",
|
||||
"width_cm": 140,
|
||||
"depth_cm": 45,
|
||||
"back": "top",
|
||||
"file": "svg/plan/cabinet_tv.svg",
|
||||
"menu_icon": "nightstand",
|
||||
"notes": "Задняя длинная сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "cabinet_shoe",
|
||||
"operation": "add",
|
||||
"name_ru": "Тумба для обуви",
|
||||
"name_en": "Shoe cabinet",
|
||||
"group": "furniture",
|
||||
"width_cm": 80,
|
||||
"depth_cm": 35,
|
||||
"back": "top",
|
||||
"file": "svg/plan/cabinet_shoe.svg",
|
||||
"menu_icon": "nightstand",
|
||||
"notes": "Задняя сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "cabinet_sink",
|
||||
"operation": "add",
|
||||
"name_ru": "Тумба под раковину",
|
||||
"name_en": "Sink cabinet",
|
||||
"group": "furniture",
|
||||
"width_cm": 80,
|
||||
"depth_cm": 50,
|
||||
"back": "top",
|
||||
"file": "svg/plan/cabinet_sink.svg",
|
||||
"menu_icon": "nightstand",
|
||||
"notes": "Задняя сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "bookshelf",
|
||||
"operation": "replace",
|
||||
"name_ru": "Шкаф книжный",
|
||||
"name_en": "Bookcase",
|
||||
"group": "furniture",
|
||||
"width_cm": 100,
|
||||
"depth_cm": 35,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bookshelf.svg",
|
||||
"menu_icon": "wardrobe",
|
||||
"notes": "Задняя длинная сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "wall_unit",
|
||||
"operation": "add",
|
||||
"name_ru": "Шкаф-стенка",
|
||||
"name_en": "Wall unit",
|
||||
"group": "furniture",
|
||||
"width_cm": 240,
|
||||
"depth_cm": 45,
|
||||
"back": "top",
|
||||
"file": "svg/plan/wall_unit.svg",
|
||||
"menu_icon": "wardrobe",
|
||||
"notes": "Задняя длинная сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "wardrobe",
|
||||
"operation": "replace",
|
||||
"name_ru": "Шкаф гардеробный",
|
||||
"name_en": "Wardrobe",
|
||||
"group": "furniture",
|
||||
"width_cm": 180,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/wardrobe.svg",
|
||||
"menu_icon": "wardrobe",
|
||||
"notes": "Задняя длинная сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "kitchen_floor",
|
||||
"operation": "add",
|
||||
"name_ru": "Кухонный напольный модуль",
|
||||
"name_en": "Kitchen floor module",
|
||||
"group": "furniture",
|
||||
"width_cm": 60,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/kitchen_floor.svg",
|
||||
"menu_icon": "kitchen_cabinet",
|
||||
"notes": "Задняя сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "kitchen_floor_corner",
|
||||
"operation": "add",
|
||||
"name_ru": "Кухонный напольный угловой модуль",
|
||||
"name_en": "Kitchen floor corner module",
|
||||
"group": "furniture",
|
||||
"width_cm": 90,
|
||||
"depth_cm": 90,
|
||||
"back": "top",
|
||||
"file": "svg/plan/kitchen_floor_corner.svg",
|
||||
"menu_icon": "kitchen_cabinet",
|
||||
"notes": "Угловой модуль; основной BACK сверху."
|
||||
},
|
||||
{
|
||||
"id": "kitchen_wall",
|
||||
"operation": "add",
|
||||
"name_ru": "Кухонный навесной модуль",
|
||||
"name_en": "Kitchen wall module",
|
||||
"group": "furniture",
|
||||
"width_cm": 60,
|
||||
"depth_cm": 35,
|
||||
"back": "top",
|
||||
"file": "svg/plan/kitchen_wall.svg",
|
||||
"menu_icon": "kitchen_cabinet",
|
||||
"notes": "Задняя сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "kitchen_wall_corner",
|
||||
"operation": "add",
|
||||
"name_ru": "Кухонный навесной угловой модуль",
|
||||
"name_en": "Kitchen wall corner module",
|
||||
"group": "furniture",
|
||||
"width_cm": 60,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/kitchen_wall_corner.svg",
|
||||
"menu_icon": "kitchen_cabinet",
|
||||
"notes": "Угловой модуль; основной BACK сверху."
|
||||
},
|
||||
{
|
||||
"id": "shelf_floor",
|
||||
"operation": "add",
|
||||
"name_ru": "Стеллаж напольный",
|
||||
"name_en": "Floor shelving unit",
|
||||
"group": "furniture",
|
||||
"width_cm": 100,
|
||||
"depth_cm": 35,
|
||||
"back": "top",
|
||||
"file": "svg/plan/shelf_floor.svg",
|
||||
"menu_icon": "shelving",
|
||||
"notes": "Задняя длинная сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "shelf_wall",
|
||||
"operation": "add",
|
||||
"name_ru": "Полка настенная",
|
||||
"name_en": "Wall shelf",
|
||||
"group": "furniture",
|
||||
"width_cm": 100,
|
||||
"depth_cm": 25,
|
||||
"back": "top",
|
||||
"file": "svg/plan/shelf_wall.svg",
|
||||
"menu_icon": "shelving",
|
||||
"notes": "Задняя длинная сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "cooktop_two",
|
||||
"operation": "add",
|
||||
"name_ru": "Варочная панель, 2 конфорки",
|
||||
"name_en": "Two-burner cooktop",
|
||||
"group": "appliance",
|
||||
"width_cm": 30,
|
||||
"depth_cm": 50,
|
||||
"back": "top",
|
||||
"file": "svg/plan/cooktop_two.svg",
|
||||
"menu_icon": "cooktop",
|
||||
"notes": "Панель ориентирована управляющей стороной вниз."
|
||||
},
|
||||
{
|
||||
"id": "stove",
|
||||
"operation": "replace",
|
||||
"name_ru": "Варочная панель, 4 конфорки",
|
||||
"name_en": "Four-burner cooktop",
|
||||
"group": "appliance",
|
||||
"width_cm": 60,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/stove.svg",
|
||||
"menu_icon": "cooktop",
|
||||
"notes": "Панель ориентирована управляющей стороной вниз."
|
||||
},
|
||||
{
|
||||
"id": "tv",
|
||||
"operation": "replace",
|
||||
"name_ru": "Телевизор на подставке",
|
||||
"name_en": "TV on stand",
|
||||
"group": "appliance",
|
||||
"width_cm": 120,
|
||||
"depth_cm": 28,
|
||||
"back": "top",
|
||||
"file": "svg/plan/tv.svg",
|
||||
"menu_icon": "tv",
|
||||
"notes": "Задняя сторона экрана сверху."
|
||||
},
|
||||
{
|
||||
"id": "tv_wall",
|
||||
"operation": "add",
|
||||
"name_ru": "Телевизор на кронштейне",
|
||||
"name_en": "Wall-mounted TV",
|
||||
"group": "appliance",
|
||||
"width_cm": 120,
|
||||
"depth_cm": 15,
|
||||
"back": "top",
|
||||
"file": "svg/plan/tv_wall.svg",
|
||||
"menu_icon": "tv",
|
||||
"notes": "Сторона крепления к стене сверху."
|
||||
},
|
||||
{
|
||||
"id": "toilet",
|
||||
"operation": "replace",
|
||||
"name_ru": "Унитаз напольный",
|
||||
"name_en": "Floor-standing toilet",
|
||||
"group": "sanitary",
|
||||
"width_cm": 40,
|
||||
"depth_cm": 70,
|
||||
"back": "top",
|
||||
"file": "svg/plan/toilet.svg",
|
||||
"menu_icon": "toilet",
|
||||
"notes": "Сторона подключения к стене сверху."
|
||||
},
|
||||
{
|
||||
"id": "toilet_built_in",
|
||||
"operation": "add",
|
||||
"name_ru": "Унитаз встроенный",
|
||||
"name_en": "Built-in toilet",
|
||||
"group": "sanitary",
|
||||
"width_cm": 40,
|
||||
"depth_cm": 55,
|
||||
"back": "top",
|
||||
"file": "svg/plan/toilet_built_in.svg",
|
||||
"menu_icon": "toilet",
|
||||
"notes": "Инсталляция находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "bathtub",
|
||||
"operation": "replace",
|
||||
"name_ru": "Ванна прямоугольная",
|
||||
"name_en": "Rectangular bathtub",
|
||||
"group": "sanitary",
|
||||
"width_cm": 170,
|
||||
"depth_cm": 75,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bathtub.svg",
|
||||
"menu_icon": "bathtub",
|
||||
"notes": "Длинная задняя сторона сверху."
|
||||
},
|
||||
{
|
||||
"id": "bathtub_corner",
|
||||
"operation": "add",
|
||||
"name_ru": "Ванна угловая",
|
||||
"name_en": "Corner bathtub",
|
||||
"group": "sanitary",
|
||||
"width_cm": 140,
|
||||
"depth_cm": 140,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bathtub_corner.svg",
|
||||
"menu_icon": "bathtub",
|
||||
"notes": "Угловая ванна; основной BACK сверху."
|
||||
},
|
||||
{
|
||||
"id": "bidet",
|
||||
"operation": "replace",
|
||||
"name_ru": "Биде напольное",
|
||||
"name_en": "Floor-standing bidet",
|
||||
"group": "sanitary",
|
||||
"width_cm": 40,
|
||||
"depth_cm": 60,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bidet.svg",
|
||||
"menu_icon": "bidet",
|
||||
"notes": "Сторона подключения к стене сверху."
|
||||
},
|
||||
{
|
||||
"id": "bidet_built_in",
|
||||
"operation": "add",
|
||||
"name_ru": "Биде встроенное",
|
||||
"name_en": "Built-in bidet",
|
||||
"group": "sanitary",
|
||||
"width_cm": 40,
|
||||
"depth_cm": 50,
|
||||
"back": "top",
|
||||
"file": "svg/plan/bidet_built_in.svg",
|
||||
"menu_icon": "bidet",
|
||||
"notes": "Инсталляция находится сверху."
|
||||
},
|
||||
{
|
||||
"id": "kitchen_sink",
|
||||
"operation": "replace",
|
||||
"name_ru": "Кухонная мойка одинарная",
|
||||
"name_en": "Single kitchen sink",
|
||||
"group": "sanitary",
|
||||
"width_cm": 60,
|
||||
"depth_cm": 50,
|
||||
"back": "top",
|
||||
"file": "svg/plan/kitchen_sink.svg",
|
||||
"menu_icon": "kitchen_sink",
|
||||
"notes": "Задняя сторона столешницы сверху."
|
||||
},
|
||||
{
|
||||
"id": "kitchen_sink_double",
|
||||
"operation": "add",
|
||||
"name_ru": "Кухонная мойка двойная",
|
||||
"name_en": "Double kitchen sink",
|
||||
"group": "sanitary",
|
||||
"width_cm": 90,
|
||||
"depth_cm": 50,
|
||||
"back": "top",
|
||||
"file": "svg/plan/kitchen_sink_double.svg",
|
||||
"menu_icon": "kitchen_sink",
|
||||
"notes": "Задняя сторона столешницы сверху."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M20.5 39h69a2.5 2.5 0 0 1 2.5 2.5V69a2.5 2.5 0 0 1-2.5 2.5h-69A2.5 2.5 0 0 1 18 69V41.5a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25 63a2.5 2.5 0 0 1 2.5-2.5h55A2.5 2.5 0 0 1 85 63v8.5H25z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25 63a2.5 2.5 0 0 1 2.5-2.5h55A2.5 2.5 0 0 1 85 63v3.5H25z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 661 B |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M42.5 29h25a10 10 0 0 1 10 10v26.5a10 10 0 0 1-10 10h-25a10 10 0 0 1-10-10V39a10 10 0 0 1 10-10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M77.5 48.5a6 6 0 0 1 2 11.657V71.5a4 4 0 0 1-4 4H34a4 4 0 0 1-4-4V60.157A6.001 6.001 0 0 1 32 48.5a6 6 0 0 1 5.419 3.424A4 4 0 0 1 38 54v7h33.5v-6.5q0-.107.005-.213A6 6 0 0 1 77.5 48.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M42.75 57h24.5A4.75 4.75 0 0 1 72 61.75a4.75 4.75 0 0 1-4.75 4.75h-24.5A4.75 4.75 0 0 1 38 61.75 4.75 4.75 0 0 1 42.75 57M36.5 75.5h8l-1.674 5.441A1.5 1.5 0 0 1 41.392 82H38a1.5 1.5 0 0 1-1.5-1.5zm36 0h-8l1.674 5.441A1.5 1.5 0 0 0 67.608 82H71a1.5 1.5 0 0 0 1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 960 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M28 75h8l-1.674 5.441a1.5 1.5 0 0 1-1.434 1.059H29.5A1.5 1.5 0 0 1 28 80zm53 0h-8l1.674 5.441a1.5 1.5 0 0 0 1.434 1.059H79.5A1.5 1.5 0 0 0 81 80zM19.5 46.5H90a2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5H19.5A2.5 2.5 0 0 1 17 49a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.5 51.5H90L86.633 68a10 10 0 0 1-9.798 8h-44.17a10 10 0 0 1-9.798-8zM30 36v-2a5 5 0 1 0-10 0v12.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 666 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M16 75.5h8l-1.674 5.441A1.5 1.5 0 0 1 20.892 82H17.5a1.5 1.5 0 0 1-1.5-1.5zm78.5 0h-8l1.674 5.441A1.5 1.5 0 0 0 89.608 82H93a1.5 1.5 0 0 0 1.5-1.5zM25 29h60.5a2.5 2.5 0 0 1 2.5 2.5V68a2.5 2.5 0 0 1-2.5 2.5H25a2.5 2.5 0 0 1-2.5-2.5V31.5A2.5 2.5 0 0 1 25 29" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 59.5c0-5.523 4.477-10 10-10h58.5c5.523 0 10 4.477 10 10v6H16zm-1 6h80.5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H15a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1m15.5-20a5 5 0 0 1 5-5h11a5 5 0 0 1 5 5v4h-21zm28.5 0a5 5 0 0 1 5-5h11a5 5 0 0 1 5 5v4H59z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 801 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M36 57.5c0 5.523 4.477 10 10 10h18.5c5.523 0 10-4.477 10-10V53H36zm8.5 22a1 1 0 0 0 1 1H65a1 1 0 0 0 1-1v-12H44.5zm-8.5-32a1 1 0 0 1 1-1h36.5a1 1 0 0 1 1 1V53H36zM50.5 41a1 1 0 0 1 1-1H59a1 1 0 0 1 1 1v5.5h-9.5zM53 30a1 1 0 0 1 1-1h2.5a1 1 0 0 1 1 1v10H53zm-4 6a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 1-1h4V36z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 485 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M46.5 81.5h5V86a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1zm12.5 0h5V86a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1zM44 23h22.5a8 8 0 0 1 8 8v42.5a8 8 0 0 1-8 8H44a8 8 0 0 1-8-8V31a8 8 0 0 1 8-8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M36 31a8 8 0 0 1 8-8h22.5a8 8 0 0 1 8 8v42H36z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M48.5 42.5a7 7 0 1 0 14 0 7 7 0 1 0-14 0m6.5 0 3.5-3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 703 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M40.5 39.5C40.5 32.596 46.096 27 53 27h4c6.904 0 12.5 5.596 12.5 12.5v13a5 5 0 0 1-5 5h-19a5 5 0 0 1-5-5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M41.25 54h27.5A3.25 3.25 0 0 1 72 57.25a3.25 3.25 0 0 1-3.25 3.25h-27.5A3.25 3.25 0 0 1 38 57.25 3.25 3.25 0 0 1 41.25 54m28.25 6.5H64l1.88 21.63a1.5 1.5 0 0 0 1.495 1.37H68a1.5 1.5 0 0 0 1.5-1.5zm-29 0H46l-1.88 21.63a1.5 1.5 0 0 1-1.495 1.37H42a1.5 1.5 0 0 1-1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 690 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M25 51h5.238l-1.534 15.646A1.5 1.5 0 0 1 27.211 68H26.5a1.5 1.5 0 0 1-1.5-1.5zm60.238 0H80l1.534 15.646A1.5 1.5 0 0 0 83.027 68h.711a1.5 1.5 0 0 0 1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24.34 43.485A2.5 2.5 0 0 1 26.625 42h56.75a2.5 2.5 0 0 1 2.285 1.485l2.715 6.109A1 1 0 0 1 87.461 51H22.54a1 1 0 0 1-.914-1.406z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M87.5 51h-66v3a1 1 0 0 0 1 1h64a1 1 0 0 0 1-1z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 764 B |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M24.5 38h40a2.5 2.5 0 0 1 2.5 2.5v22a2.5 2.5 0 0 1-2.5 2.5h-40a2.5 2.5 0 0 1-2.5-2.5v-22a2.5 2.5 0 0 1 2.5-2.5m50 0h11a2.5 2.5 0 0 1 2.5 2.5v29a2.5 2.5 0 0 1-2.5 2.5h-11a2.5 2.5 0 0 1-2.5-2.5v-29a2.5 2.5 0 0 1 2.5-2.5M43 65h5v7h-5zm-6 7h16m23-12h8m-8-17h8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M35 56V46h5m-5 7h5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M39.5 53a3.5 3.5 0 1 0 0-7m15.036 8.536a5 5 0 1 1 0-7.071" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 745 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M27.5 29h55a2.5 2.5 0 0 1 2.5 2.5v47a2.5 2.5 0 0 1-2.5 2.5h-55a2.5 2.5 0 0 1-2.5-2.5v-47a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M33.5 66.5a8 8 0 1 0 16 0 8 8 0 1 0-16 0m26.5-23a8 8 0 1 0 16 0 8 8 0 1 0-16 0m3 23a5 5 0 1 0 10 0 5 5 0 1 0-10 0m-29.5-23a8 8 0 1 0 16 0 8 8 0 1 0-16 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 579 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M83.5 43H78l6.742 30.82A1.5 1.5 0 0 0 86.207 75h.54a1.5 1.5 0 0 0 1.482-1.732zm-56 0H33l-6.742 30.82A1.5 1.5 0 0 1 24.793 75h-.54a1.5 1.5 0 0 1-1.482-1.732zm-2.62-7.406A2.5 2.5 0 0 1 27.21 34h56.58a2.5 2.5 0 0 1 2.33 1.594l2.35 6.044A1 1 0 0 1 87.538 43H23.462a1 1 0 0 1-.932-1.362z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M88.5 43h-66v3a1 1 0 0 0 1 1h64a1 1 0 0 0 1-1z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 645 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M31.5 25h47a2.5 2.5 0 0 1 2.5 2.5v55a2.5 2.5 0 0 1-2.5 2.5h-47a2.5 2.5 0 0 1-2.5-2.5v-55a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.5 37.5h47A2.5 2.5 0 0 1 81 40v36a2.5 2.5 0 0 1-2.5 2.5h-47A2.5 2.5 0 0 1 29 76V40a2.5 2.5 0 0 1 2.5-2.5M43 42h24m-1.5-11a2 2 0 1 0 4 0 2 2 0 1 0-4 0m7.5 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0m-37.25 0h9.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 626 B |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M31.5 25h47a2.5 2.5 0 0 1 2.5 2.5v55a2.5 2.5 0 0 1-2.5 2.5h-47a2.5 2.5 0 0 1-2.5-2.5v-55a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.5 37.5h47A2.5 2.5 0 0 1 81 40v42.5a2.5 2.5 0 0 1-2.5 2.5h-47a2.5 2.5 0 0 1-2.5-2.5V40a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M38 60.5a17 17 0 1 0 34 0 17 17 0 1 0-34 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M43.5 60.5a11.5 11.5 0 1 0 23 0 11.5 11.5 0 1 0-23 0m22-29.5a2 2 0 1 0 4 0 2 2 0 1 0-4 0m7.5 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0m-37.25 0h9.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M55.994 55.5s-1.329 1.81-1.479 3.143c-.198 1.761 1.599 2.554 1.48 4.321-.1 1.479-1.48 3.536-1.48 3.536m5.979-11s-1.329 1.81-1.479 3.143c-.198 1.761 1.599 2.554 1.48 4.321-.1 1.479-1.48 3.536-1.48 3.536m-8.519-11s-.886 1.81-.986 3.143c-.132 1.761 1.066 2.554.986 4.321-.067 1.479-.986 3.536-.986 3.536" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="m60.752 62.03-5.379-2.312 11.066-25.75a1.5 1.5 0 0 1 1.97-.786l2.624 1.127a1.5 1.5 0 0 1 .786 1.97zm-18.648-4.512-4.353 1.189-4.474-16.382a1.5 1.5 0 0 1 1.052-1.842l1.459-.398a1.5 1.5 0 0 1 1.842 1.052z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M41.297 38.173a2.215 2.215 0 0 1 0 4.43H29.781a2.215 2.215 0 0 1 0-4.43zm33.219 38.091a2.657 2.657 0 1 1 0 5.315H28.453a2.657 2.657 0 1 1 0-5.315zm8.641-51.366a1.795 1.795 0 0 1 .68 2.41l-3.03 5.59a5 5 0 0 1-4.396 2.618H65.657a1.772 1.772 0 1 1 0-3.544h9.61a3 3 0 0 0 2.598-1.5l2.84-4.917a1.795 1.795 0 0 1 2.452-.657M29.339 65.787c0-6.699 6.457-11.503 12.873-9.579l24.291 7.288a10 10 0 0 1 7.127 9.578v3.19H29.339zm0 15.852h7.086l-1.394 3.486a1.5 1.5 0 0 1-1.393.943h-2.8a1.5 1.5 0 0 1-1.5-1.5zm44.291 0h-7.087l1.395 3.486a1.5 1.5 0 0 0 1.392.943h2.8a1.5 1.5 0 0 0 1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M35.54 66.077a4.872 4.872 0 1 0 9.743 0 4.872 4.872 0 1 0-9.744 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M26.672 34.425h55.99v39.47h-55.99Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M23 74.894a1 1 0 0 1 1-1h61.334a1 1 0 0 1 1 1v5.802a1 1 0 0 1-1 1H24a1 1 0 0 1-1-1zM23 29a1 1 0 0 1 1-1h61.334a1 1 0 0 1 1 1v4.425a1 1 0 0 1-1 1H24a1 1 0 0 1-1-1zm15.145 21.44a5 5 0 0 1 5-5h23.043a5 5 0 0 1 5 5v23.454H38.145z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M44.57 65.633a2.754 2.754 0 0 1 2.754-2.753H62.01v5.507H47.324a2.754 2.754 0 0 1-2.754-2.754m20.194 5.507a2.754 2.754 0 0 0-2.754-2.753H47.324v5.507H62.01a2.754 2.754 0 0 0 2.754-2.754" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M44.57 71.14a2.754 2.754 0 1 0 5.508 0 2.754 2.754 0 1 0-5.508 0m14.688-5.507a2.754 2.754 0 1 0 5.507 0 2.754 2.754 0 1 0-5.507 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M35.5 18H75a2.5 2.5 0 0 1 2.5 2.5v64A2.5 2.5 0 0 1 75 87H35.5a2.5 2.5 0 0 1-2.5-2.5v-64a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M33 46.5h44.5v38A2.5 2.5 0 0 1 75 87H35.5a2.5 2.5 0 0 1-2.5-2.5zm2.5 44a1 1 0 0 0 1 1h37a1 1 0 0 0 1-1V87h-39zm3-49.5v-9.5m0 30.5v-9.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 560 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M44.308 57h21.384L83 69H27zM44 37.5a2.5 2.5 0 0 1 2.5-2.5h17a2.5 2.5 0 0 1 2.5 2.5V57H44zM80.5 74h-51a2.5 2.5 0 0 1-2.5-2.5V70a1 1 0 0 1 1-1h54a1 1 0 0 1 1 1v1.5a2.5 2.5 0 0 1-2.5 2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 368 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M38 88a1 1 0 0 0 1 1h32a1 1 0 0 0 1-1v-3H38zm17-68h16.5a2.5 2.5 0 0 1 2.5 2.5v17a2.5 2.5 0 0 1-2.5 2.5H55zm-19 2.5a2.5 2.5 0 0 1 2.5-2.5H55v22H38.5a2.5 2.5 0 0 1-2.5-2.5zM59 33v5m-8-5v5m4 47h16.5a2.5 2.5 0 0 0 2.5-2.5v-17a2.5 2.5 0 0 0-2.5-2.5H55zm-19-2.5a2.5 2.5 0 0 0 2.5 2.5H55V63H38.5a2.5 2.5 0 0 0-2.5 2.5zM67 67h-5m-14 0h-5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 514 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M21.5 84h68a2.5 2.5 0 0 0 2.5-2.5v-35a2.5 2.5 0 0 0-2.5-2.5h-68a2.5 2.5 0 0 0-2.5 2.5v35a2.5 2.5 0 0 0 2.5 2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M26 78h26.5a1 1 0 0 0 1-1V51a1 1 0 0 0-1-1H26a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1m23-24v5m9.5 19H85a1 1 0 0 0 1-1V51a1 1 0 0 0-1-1H58.5a1 1 0 0 0-1 1v26a1 1 0 0 0 1 1m-6-39.5a1 1 0 0 0-1-1H44a1 1 0 0 0-1 1V44h9.5zM58 33v-2a5 5 0 1 0-10 0v6.5M62 54v5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 671 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M76.5 73.5h-8l1.674 5.441A1.5 1.5 0 0 0 71.608 80H75a1.5 1.5 0 0 0 1.5-1.5zm-42.5 0h8l-1.674 5.441A1.5 1.5 0 0 1 38.892 80H35.5a1.5 1.5 0 0 1-1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M34.5 30.5h41A2.5 2.5 0 0 1 78 33v40.5a2.5 2.5 0 0 1-2.5 2.5h-41a2.5 2.5 0 0 1-2.5-2.5V33a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32 39.5h46v18H32Zm18.5 9H60M50.5 66H60" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 734 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M31.5 25h47a2.5 2.5 0 0 1 2.5 2.5v55a2.5 2.5 0 0 1-2.5 2.5h-47a2.5 2.5 0 0 1-2.5-2.5v-55a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.5 37.5h47A2.5 2.5 0 0 1 81 40v36a2.5 2.5 0 0 1-2.5 2.5h-47A2.5 2.5 0 0 1 29 76V40a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M40.5 47h29a2.5 2.5 0 0 1 2.5 2.5v20a2.5 2.5 0 0 1-2.5 2.5h-29a2.5 2.5 0 0 1-2.5-2.5v-20a2.5 2.5 0 0 1 2.5-2.5m25-16a2 2 0 1 0 4 0 2 2 0 1 0-4 0m7.5 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0m-37.25 0h9.5M43 42h24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 851 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M45.952 75.849a2.5 2.5 0 0 0 2.485 2.231H63.43a2.5 2.5 0 0 0 2.485-2.231l1.571-14.532H44.381zM60.236 44.78C58.423 36.174 66.805 29.605 77 31.87c0 8.155-8.609 16.763-16.763 12.912m-9.761 5.147c1.517-8.665-7.085-14.943-17.195-12.33.28 8.15 9.178 16.458 17.195 12.33" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M55.706 55.88v-2.944m0 0c1.133-5.89 7.09-10.956 12.912-15.404M55.706 52.936c-4.938-4.146-7.982-6.16-14.044-9.061m1.453 12.005h25.636a1 1 0 0 1 1 1v3.437a1 1 0 0 1-1 1H43.115a1 1 0 0 1-1-1v-3.436a1 1 0 0 1 1-1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 788 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M90.27 33.874h-5.622m-59.028 0H20m70.27 5.152h-5.622m-59.028 0H20m70.27 5.154h-5.622m-59.028 0H20m70.27 5.152h-5.622m-59.028 0H20m70.27 5.153h-5.622m-59.028 0H20m70.27 5.153h-5.622m-59.028 0H20m70.27 5.153h-5.622m-59.028 0H20m70.27 5.153h-5.622m-59.028 0H20m70.27 5.153h-5.622m-59.028 0H20M28.12 32h54.025a2.5 2.5 0 0 1 2.5 2.5v40.439a2.5 2.5 0 0 1-2.5 2.5H28.12a2.5 2.5 0 0 1-2.5-2.5V34.5a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25.62 34.5a2.5 2.5 0 0 1 2.5-2.5h1.717v45.439H28.12a2.5 2.5 0 0 1-2.5-2.5zm59.026 0a2.5 2.5 0 0 0-2.5-2.5H80.43v45.439h1.716a2.5 2.5 0 0 0 2.5-2.5zM37.8 54.485l17.801-8.432 17.801 8.432-17.8 8.432z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 926 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M28 58.5h53V39H28Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M64 58.5h5V47a1 1 0 0 0-1-1h-3a1 1 0 0 0-1 1zM52.5 39h-5V27.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1zm-12 0h-5V27.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1zm-5 29.5V71a1 1 0 0 1 1 1H49a1 1 0 0 1 1-1v-2.5a1 1 0 0 1-1-1H36.5a1 1 0 0 1-1 1m33.5-10h7v-14a1 1 0 0 0-1-1h-5a1 1 0 0 0-1 1zM47.5 39h-7V25a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1zm-12 34v4a1 1 0 0 1 1 1h14a1 1 0 0 1 1-1v-4a1 1 0 0 1-1-1h-14a1 1 0 0 1-1 1M28 78h53v7H28Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M28 89.5V23a2.5 2.5 0 0 1 2.5-2.5h48A2.5 2.5 0 0 1 81 23v66.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 911 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M31 84h49a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H31a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1m31.5-47.6v-4.686a11.7 11.7 0 0 0-3.441-8.283A11.77 11.77 0 0 0 50.75 20a11.77 11.77 0 0 0-8.309 3.431A11.7 11.7 0 0 0 39 31.714V84" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M72 39.25a10.25 10.25 0 1 0-20.5 0zm-17.25 5v2.5m7.25-2.5v2.5m7.25-2.5v2.5m-14.5 3.75V53M62 50.5V53m7.25-2.5V53m-14.5 3.75v2.5m7.25-2.5v2.5m7.25-2.5v2.5M54.75 63v2.5M62 63v2.5m7.25-2.5v2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 709 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M30 57c0 5.523 4.477 10 10 10h29.5c5.523 0 10-4.477 10-10v-6.5A2.5 2.5 0 0 0 77 48H32.5a2.5 2.5 0 0 0-2.5 2.5zm14 22a1 1 0 0 0 1 1h19.5a1 1 0 0 0 1-1V67H44zm6-36.5a1 1 0 0 1 1-1h7.5a1 1 0 0 1 1 1V48H50zM45 37v-2a5 5 0 1 1 10 0v6.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 415 B |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M59.5 29h24a5 5 0 0 1 5 5v32a5 5 0 0 1-5 5h-24a5 5 0 0 1-5-5V34a5 5 0 0 1 5-5m-34 0h24a5 5 0 0 1 5 5v32a5 5 0 0 1-5 5h-24a5 5 0 0 1-5-5V34a5 5 0 0 1 5-5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.5 44a5 5 0 0 1 5 5v12H86V49a5 5 0 0 1 5-5h.5a5 5 0 0 1 5 5v21.5a5 5 0 0 1-5 5H18a5 5 0 0 1-5-5V49a5 5 0 0 1 5-5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25.25 57h24.5a4.75 4.75 0 0 1 4.75 4.75 4.75 4.75 0 0 1-4.75 4.75h-24.5a4.75 4.75 0 0 1-4.75-4.75A4.75 4.75 0 0 1 25.25 57m34 0h24.5a4.75 4.75 0 0 1 4.75 4.75 4.75 4.75 0 0 1-4.75 4.75h-24.5a4.75 4.75 0 0 1-4.75-4.75A4.75 4.75 0 0 1 59.25 57M18.5 75.5h8l-1.674 5.441A1.5 1.5 0 0 1 23.392 82H20a1.5 1.5 0 0 1-1.5-1.5zm72 0h-8l1.674 5.441A1.5 1.5 0 0 0 85.608 82H89a1.5 1.5 0 0 0 1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M73.47 46.365v-17.62m-10.571 25.55V36.674M52.326 61.783V45.484M42.193 69.273v-15.86M28.979 77.761a1 1 0 0 1 1-1h53.624v6.489a1 1 0 0 1-1 1H29.98a1 1 0 0 1-1-1z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M26.777 57.497a1 1 0 0 1 1-1h2.846a1 1 0 0 1 1 1V83.25a1 1 0 0 1-1 1h-2.846a1 1 0 0 1-1-1z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="m73.113 28.633-41.492 32.93v-4.405l39.9-32.007a.7.7 0 0 1 .428-.151h3.724a1.762 1.762 0 1 1 0 3.524h-2.248a.5.5 0 0 0-.312.109M25.895 53.414a3.084 3.084 0 1 0 6.167 0 3.084 3.084 0 1 0-6.167 0m14.978 16.859a1 1 0 0 1 1-1h41.73v7.488h-42.73zm10.131-7.489a1 1 0 0 1 1-1h31.598v7.488H51.004z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M61.576 55.294a1 1 0 0 1 1-1h21.026v7.49H61.576z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M72.15 47.806a1 1 0 0 1 1-1h9.454a1 1 0 0 1 1 1v6.489H72.15z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M32 26.5a2.5 2.5 0 0 1 2.5-2.5H45a2.5 2.5 0 0 1 2.5 2.5V57H32zM47.5 52h29a2.5 2.5 0 0 1 2.5 2.5V57H47.5zm17.037 20.102C73.95 70.288 79 62.01 79 57H32c.5 7.5 8.072 7 7 11l-2.342 16.862a1 1 0 0 0 .99 1.138H66.72a1 1 0 0 0 .97-1.243z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 415 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M23.5 31H86a2.5 2.5 0 0 1 2.5 2.5v34A2.5 2.5 0 0 1 86 70H23.5a2.5 2.5 0 0 1-2.5-2.5v-34a2.5 2.5 0 0 1 2.5-2.5M50 70h9.5v8H50Zm-10 8h30M42.5 45.5h5m5 0h-5m0 0v9.75" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="m56.75 45.5 5 10 5-10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 500 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<path d="M28 23a2.5 2.5 0 0 1 2.5-2.5h24V85h-24a2.5 2.5 0 0 1-2.5-2.5zm2 65.5a1 1 0 0 0 1 1h47a1 1 0 0 0 1-1V85H30zM81 23a2.5 2.5 0 0 0-2.5-2.5h-24V85h24a2.5 2.5 0 0 0 2.5-2.5zM50.5 48v9.5m8-9.5v9.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 374 B |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M31.5 25h47a2.5 2.5 0 0 1 2.5 2.5v55a2.5 2.5 0 0 1-2.5 2.5h-47a2.5 2.5 0 0 1-2.5-2.5v-55a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.5 37.5h47A2.5 2.5 0 0 1 81 40v42.5a2.5 2.5 0 0 1-2.5 2.5h-47a2.5 2.5 0 0 1-2.5-2.5V40a2.5 2.5 0 0 1 2.5-2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M38 60.5a17 17 0 1 0 34 0 17 17 0 1 0-34 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M43.5 60.5a11.5 11.5 0 1 0 23 0 11.5 11.5 0 1 0-23 0m22-29.5a2 2 0 1 0 4 0 2 2 0 1 0-4 0m7.5 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0m-37.25 0h9.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 949 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M88.5 47h-20v14h20zm-12 4h4m7.984-8h-66v4h66zm-66 4h4v26.5a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24.866 35.597A2.5 2.5 0 0 1 27.197 34h56.577a2.5 2.5 0 0 1 2.33 1.594l2.35 6.044a1 1 0 0 1-.93 1.362H23.46a1 1 0 0 1-.933-1.361zM88.5 61h-20v12.5A1.5 1.5 0 0 0 70 75h17a1.5 1.5 0 0 0 1.5-1.5zm-12 4h4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 623 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 90">
|
||||
<path d="M75.702 13.011q.012-.225.012-.453c0-4.843-3.917-8.768-8.748-8.768H23.034c-4.83 0-8.747 3.925-8.747 8.768q0 .228.011.453M77.132 72.09q.59.091 1.206.092c4.348 0 7.873-3.533 7.873-7.892V20.45c0-4.359-3.525-7.892-7.873-7.892-.924 0-1.812.16-2.636.453a7.89 7.89 0 0 0-5.237 7.438v.148m0 0a8.7 8.7 0 0 1-3.499.73H23.034a8.7 8.7 0 0 1-3.499-.73m50.93 0V64.29a7.89 7.89 0 0 0 6.667 7.8M14.298 13.01a7.89 7.89 0 0 1 5.237 7.438v.148m0 0V64.29a7.89 7.89 0 0 1-6.666 7.8q-.59.091-1.206.092c-4.348 0-7.873-3.533-7.873-7.892V20.45c0-4.359 3.525-7.892 7.873-7.892.924 0 1.811.16 2.635.453M77.132 72.09c-1.585 8.05-8.667 14.12-17.164 14.12H30.032c-8.496 0-15.578-6.07-17.163-14.12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 848 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 65 65">
|
||||
<path d="M56.968 18.845q0-.368-.05-.72M49.051 13.8H15.95a5.04 5.04 0 0 1-5.038-5.043 5.04 5.04 0 0 1 5.038-5.043h33.103a5.04 5.04 0 0 1 5.038 5.043 5.04 5.04 0 0 1-5.038 5.043m-33.103 0h-2.88a5.04 5.04 0 0 0-4.986 4.323M49.052 13.8h2.879a5.04 5.04 0 0 1 4.986 4.324m.051 24.826a4.32 4.32 0 0 0 4.318-4.323v-16.18a4.32 4.32 0 0 0-4.369-4.322m.051 24.826a4.32 4.32 0 0 1-4.318-4.323V22.447a4.32 4.32 0 0 1 4.267-4.322m-11.84 37.24h4.695c.736 0 1.446-.111 2.115-.317a7.21 7.21 0 0 0 5.081-6.889v-5.208M8.084 18.124h-.051a4.32 4.32 0 0 0-4.318 4.323v16.18a4.32 4.32 0 0 0 4.318 4.324 4.32 4.32 0 0 0 4.317-4.323V22.447a4.32 4.32 0 0 0-4.266-4.323m11.935 37.24h-4.79a7.2 7.2 0 0 1-2.04-.294 7.21 7.21 0 0 1-5.156-6.91v-5.21m37.045 12.414 5.177 5.183c.969.97 2.534.987 3.524.038a2.526 2.526 0 0 0 .038-3.605l-1.93-1.932m-6.81.316H20.02m-6.83-.294-1.909 1.91c-.998 1-.98 2.627.038 3.605.99.949 2.555.932 3.524-.038l5.177-5.183" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 170 75">
|
||||
<g>
|
||||
<path d="M3.885 6c0-1.243 1.068-2.25 2.386-2.25h157.458c1.318 0 2.386 1.007 2.386 2.25v63c0 1.243-1.068 2.25-2.386 2.25H6.27c-1.318 0-2.386-1.007-2.386-2.25z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.428 37.5c0-13.669 11.75-24.75 26.243-24.75h90.658c14.493 0 26.243 11.081 26.243 24.75s-11.75 24.75-26.243 24.75H39.67c-14.493 0-26.243-11.081-26.243-24.75" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.56 37.5c0 2.237-1.923 4.05-4.295 4.05s-4.294-1.813-4.294-4.05 1.923-4.05 4.294-4.05c2.372 0 4.295 1.813 4.295 4.05" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 857 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 140">
|
||||
<g>
|
||||
<path d="M3.862 6.224a2.36 2.36 0 0 1 2.362-2.362H133.97c1.197 0 2.168.97 2.168 2.168 0 71.857-58.251 130.108-130.108 130.108a2.17 2.17 0 0 1-2.168-2.168z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.31 32.207c0-10.437 8.46-18.897 18.897-18.897h87.628a2.13 2.13 0 0 1 2.13 2.13c0 58.833-47.692 106.526-106.524 106.526a2.13 2.13 0 0 1-2.13-2.131z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.207 27.483a4.724 4.724 0 1 1-9.449 0 4.724 4.724 0 0 1 9.449 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 794 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 200">
|
||||
<path d="M3.879 14.073c0 5.618 4.516 10.172 10.088 10.172h132.065c5.571 0 10.088-4.554 10.088-10.172m-81.165 47.16V46.438c0-5.107-4.106-9.247-9.171-9.247H45.607c-5.065 0-9.17 4.14-9.17 9.247v14.795m89.877 0V46.438c0-5.107-4.107-9.247-9.172-9.247H96.966c-5.065 0-9.171 4.14-9.171 9.247v14.795m-83.916 0H156.12m0 22.192H3.88m9.17 112.673h133.9c5.065 0 9.171-4.14 9.171-9.247V13.15c0-5.107-4.106-9.247-9.171-9.247H13.05c-5.064 0-9.17 4.14-9.17 9.247v173.7c0 5.107 4.106 9.247 9.17 9.247" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 659 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 200">
|
||||
<path d="M3.79 14.024c0 5.59 4.318 10.122 9.645 10.122h63.13c5.328 0 9.646-4.532 9.646-10.122m-22.797 46.93V46.23c0-5.082-3.926-9.201-8.769-9.201h-19.29c-4.842 0-8.768 4.12-8.768 9.201v14.723m-22.797 0h82.42m0 22.085H3.79m8.768 113.06h64.885c4.842 0 8.768-4.12 8.768-9.202V13.104c0-5.082-3.926-9.202-8.768-9.202H12.558c-4.843 0-8.768 4.12-8.768 9.202v173.792c0 5.082 3.925 9.202 8.768 9.202" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 565 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 60">
|
||||
<path d="M22.466 3.692v10.085c0 1.21-.92 2.192-2.055 2.192h-.822c-1.136 0-2.056-.981-2.056-2.192V3.692m4.933 8.77h2.878c1.135 0 2.056-.982 2.056-2.193v-.877c0-1.21-.92-2.192-2.056-2.192h-2.878m-4.933 5.262h-2.878c-1.135 0-2.055-.982-2.055-2.193v-.877c0-1.21.92-2.192 2.055-2.192h2.878M5.611 3.692h28.777c1.136 0 2.056.981 2.056 2.192v33.45c0 9.374-7.125 16.973-15.914 16.973h-1.06c-8.79 0-15.915-7.599-15.915-16.972V5.885c0-1.212.92-2.193 2.056-2.193m4.11 15.785h20.556c1.136 0 2.056.981 2.056 2.192v17.807c0 6.874-5.225 12.447-11.67 12.447h-1.327c-6.445 0-11.67-5.573-11.67-12.447V21.67c0-1.21.92-2.192 2.056-2.192m13.567 7.892c0 1.937-1.472 3.508-3.288 3.508-1.817 0-3.29-1.57-3.29-3.508s1.473-3.507 3.29-3.507 3.288 1.57 3.288 3.507" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 909 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 50">
|
||||
<path d="M17.318 12.182h-8.06c-.88 0-1.592.74-1.592 1.654v16.126c0 6.698 5.225 12.129 11.67 12.129h1.327c6.445 0 11.67-5.43 11.67-12.13V13.837c0-.914-.713-1.654-1.591-1.654H22.68m-.215-8.545v9.827c0 1.18-.92 2.136-2.055 2.136h-.822c-1.136 0-2.056-.956-2.056-2.136V3.637m10.689 8.545V3.637m-16.445 0v8.545M5.611 3.637h28.777c1.136 0 2.056.956 2.056 2.136v24.051c0 9.135-7.125 16.54-15.914 16.54h-1.06c-8.79 0-15.915-7.405-15.915-16.54V5.773c0-1.18.92-2.136 2.056-2.136M23.289 23.29c0 1.888-1.473 3.418-3.29 3.418s-3.288-1.53-3.288-3.418 1.472-3.418 3.289-3.418c1.816 0 3.288 1.53 3.288 3.418" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 764 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 35">
|
||||
<path d="M57.12 20.488H45.473c-.514 0-1.008.124-1.372.345s-.568.52-.568.832m4.076-8.236h5.434M6.119 3.5h87.762c1.276 0 2.31.895 2.31 2v24c0 1.105-1.034 2-2.31 2H6.12c-1.275 0-2.31-.895-2.31-2v-24c0-1.105 1.035-2 2.31-2m51 6.4v14.118H45.474a2.04 2.04 0 0 1-1.372-.517 1.7 1.7 0 0 1-.568-1.248V11.665c0-.468.204-.917.568-1.248a2.04 2.04 0 0 1 1.372-.517z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 527 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 35">
|
||||
<path d="m39.722 16.231 1.505-1.596m1.134 3.15 1.53-1.738M6.03 31.5h67.94c1.251 0 2.265-.895 2.265-2v-24c0-1.105-1.014-2-2.264-2H6.03c-1.251 0-2.265.895-2.265 2v24c0 1.105 1.014 2 2.265 2m41.966-14.305L46.92 17.1a3.4 3.4 0 0 1-1.813-.706l-4.583-3.62c-1.264-1-3.277-.326-3.475 1.161-.185 1.39-1.983 2.1-3.28 1.299l-1.532-.947c-.968-.513-2.2.103-2.2 1.1v3.463c0 1.559 1.43 2.823 3.196 2.823h14.51c1.404 0 2.542-1.005 2.542-2.245 0-1.153-.99-2.118-2.289-2.233" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 630 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 50">
|
||||
<g>
|
||||
<path d="M3.765 5.773c0-1.18 1.014-2.136 2.265-2.136h67.94c1.251 0 2.265.956 2.265 2.136v38.454c0 1.18-1.014 2.137-2.264 2.137H6.03c-1.251 0-2.265-.957-2.265-2.137z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M36.088 32.135c2.525 1.88 6.205 1.88 8.73 0s3.167-5.101 1.52-7.637l-5.879-8.043-5.893 8.043c-1.644 2.536-1.003 5.755 1.522 7.637" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 598 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 45">
|
||||
<path d="M56.772 17.88h5.315m0 0h5.315m-5.315 0v9.214m9.832-9.214 5.315 9.45 5.314-9.45M6.224 41.4h127.552c1.305 0 2.362-.94 2.362-2.1V5.7c0-1.16-1.057-2.1-2.362-2.1H6.224c-1.305 0-2.362.94-2.362 2.1v33.6c0 1.16 1.057 2.1 2.362 2.1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 406 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
|
||||
<path d="M39.452 12.433H10.548a4.398 4.398 0 1 1 0-8.796h28.904a4.398 4.398 0 0 1 0 8.796m-28.904 0H8.035a4.4 4.4 0 0 0-4.398 4.399V40.08a6.283 6.283 0 0 0 6.283 6.284h30.16a6.283 6.283 0 0 0 6.284-6.284V16.832c0-2.43-1.97-4.399-4.399-4.399h-2.513" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 421 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 48">
|
||||
<path d="M38.579 17.168c1.326 2.526 1.947 6.015 1.947 9.071 0 10.018-8.07 18.139-18.025 18.139S4.475 36.257 4.475 26.239c0-3.056.586-6.552 1.912-9.078m32.192.007a3.59 3.59 0 0 1-3.333-.976m3.333.976a3.6 3.6 0 0 0 1.765-.976m-33.827.996-.13-.027a3.6 3.6 0 0 1-1.731-.969m30.59 0a18 18 0 0 0-5.848-3.932 17.93 17.93 0 0 0-13.796 0 18 18 0 0 0-5.848 3.932 3.59 3.59 0 0 1-3.237.996m28.729-.996a3.59 3.59 0 0 0 5.098 0m-33.827.996a3.6 3.6 0 0 1-1.861-.996 3.644 3.644 0 0 1 0-5.13 25.2 25.2 0 0 1 8.187-5.505A25.1 25.1 0 0 1 22.5 3.623a25.1 25.1 0 0 1 9.657 1.934 25.2 25.2 0 0 1 8.187 5.505 3.644 3.644 0 0 1 0 5.13" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 786 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 60">
|
||||
<g>
|
||||
<path d="M3.84 5.884c0-1.21 1.048-2.192 2.34-2.192h107.64c1.293 0 2.34.981 2.34 2.192v48.23c0 1.212-1.047 2.193-2.34 2.193H6.18c-1.292 0-2.34-.981-2.34-2.192z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M103.056 15.093V9.83a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m0 35.078v-5.262a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m-93.6-35.077V9.83a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m0 35.078v-5.262a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1003 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 60">
|
||||
<g>
|
||||
<path d="M3.84 30c0-14.53 12.572-26.308 28.08-26.308h56.16c15.509 0 28.08 11.778 28.08 26.308s-12.571 26.307-28.08 26.307H31.92C16.412 56.307 3.84 44.53 3.84 30" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M80.592 15.093V9.83a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m0 35.077v-5.262a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877M31.92 15.093V9.83a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.876h-5.616a.936.877 0 0 1-.936-.876m0 35.077v-5.262a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.876h-5.616a.936.877 0 0 1-.936-.876" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1004 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
|
||||
<g>
|
||||
<path d="M3.765 40a36.235 36.235 0 1 0 72.47 0 36.235 36.235 0 1 0-72.47 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="m56.26 26.96-3.843-3.843a.906.906 0 0 1 0-1.28l3.843-3.844a.906.906 0 0 1 1.281 0l3.844 3.843a.906.906 0 0 1 0 1.281l-3.844 3.844a.906.906 0 0 1-1.28 0m-.001 34.66-3.843-3.843a.906.906 0 0 1 0-1.281l3.843-3.844a.906.906 0 0 1 1.281 0l3.843 3.844a.906.906 0 0 1 0 1.28l-3.843 3.844a.906.906 0 0 1-1.281 0m-34.423-34.66-3.843-3.843a.906.906 0 0 1 0-1.281l3.843-3.844a.906.906 0 0 1 1.281 0l3.844 3.844a.906.906 0 0 1 0 1.28l-3.844 3.844a.906.906 0 0 1-1.28 0m-.001 34.66-3.843-3.843a.906.906 0 0 1 0-1.281l3.843-3.843a.906.906 0 0 1 1.281 0l3.844 3.843a.906.906 0 0 1 0 1.281l-3.844 3.843a.906.906 0 0 1-1.28 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 988 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 60">
|
||||
<g>
|
||||
<path d="M3.84 12.461c0-4.843 4.19-8.77 9.36-8.77h93.6c5.17 0 9.36 3.927 9.36 8.77v35.077c0 4.843-4.19 8.77-9.36 8.77H13.2c-5.17 0-9.36-3.927-9.36-8.77z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M103.056 15.093V9.83a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m0 35.078v-5.262a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m-93.6-35.077V9.83a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877m0 35.078v-5.262a.936.877 0 0 1 .936-.877h5.616a.936.877 0 0 1 .936.877v5.262a.936.877 0 0 1-.936.877h-5.616a.936.877 0 0 1-.936-.877" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 997 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 50">
|
||||
<path d="M15 30.982v-1.71m3.086 5.128h1.542M15 39.527v-1.709M10.372 34.4h1.542M5.84 46.364h18.322c1.331 0 2.41-1.196 2.41-2.67V6.306c0-1.475-1.079-2.67-2.41-2.67H5.839c-1.331 0-2.41 1.195-2.41 2.67v37.386c0 1.475 1.079 2.67 2.41 2.67m14.56-29.91c0 3.304-2.417 5.982-5.4 5.982-2.981 0-5.4-2.678-5.4-5.981s2.419-5.982 5.4-5.982c2.983 0 5.4 2.678 5.4 5.982M18.858 34.4c0 2.36-1.727 4.273-3.857 4.273s-3.857-1.913-3.857-4.273S12.87 30.127 15 30.127s3.857 1.913 3.857 4.273" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 642 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 70">
|
||||
<g>
|
||||
<path d="M3.862 5.967c0-1.234 1.057-2.234 2.362-2.234h127.552c1.305 0 2.362 1 2.362 2.234v58.066c0 1.234-1.057 2.233-2.362 2.233H6.224c-1.305 0-2.362-1-2.362-2.233z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.531 15.346v-5.36a.945.893 0 0 1 .945-.893h5.67a.945.893 0 0 1 .944.893v5.36a.945.893 0 0 1-.945.894h-5.669a.945.893 0 0 1-.945-.894m113.379.001v-5.36a.945.893 0 0 1 .945-.894h5.67a.945.893 0 0 1 .944.894v5.36a.945.893 0 0 1-.945.893h-5.669a.945.893 0 0 1-.945-.893M9.53 60.013v-5.36a.945.893 0 0 1 .946-.893h5.669a.945.893 0 0 1 .945.893v5.36a.945.893 0 0 1-.945.894h-5.67a.945.893 0 0 1-.944-.894m113.38.001v-5.36a.945.893 0 0 1 .945-.894h5.669a.945.893 0 0 1 .945.894v5.36a.945.893 0 0 1-.945.893h-5.67a.945.893 0 0 1-.944-.893m-97.318-2.681h88.814" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 160">
|
||||
<g>
|
||||
<path d="M156.12 66.202a2.38 2.38 0 0 1-2.379 2.38H94.272c-13.138 0-23.788 10.65-23.788 23.787v61.372a2.38 2.38 0 0 1-2.379 2.38H6.257a2.38 2.38 0 0 1-2.378-2.38V6.257A2.38 2.38 0 0 1 6.257 3.88h147.484a2.38 2.38 0 0 1 2.38 2.378z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.588 16.249v-5.71a.95.95 0 0 1 .951-.95h5.71a.95.95 0 0 1 .95.95v5.71a.95.95 0 0 1-.95.951h-5.71a.95.95 0 0 1-.951-.951m133.212.001v-5.71a.95.95 0 0 1 .951-.951h5.71a.95.95 0 0 1 .95.951v5.71a.95.95 0 0 1-.95.951h-5.71a.95.95 0 0 1-.951-.951m0 45.672v-5.71a.95.95 0 0 1 .952-.951h5.709a.95.95 0 0 1 .951.951v5.71a.95.95 0 0 1-.951.951h-5.71a.95.95 0 0 1-.95-.951M9.587 149.46v-5.708a.95.95 0 0 1 .952-.952h5.709a.95.95 0 0 1 .951.952v5.709a.95.95 0 0 1-.951.951h-5.71a.95.95 0 0 1-.951-.951m47.576 0v-5.709a.95.95 0 0 1 .952-.951h5.709a.95.95 0 0 1 .951.951v5.71a.95.95 0 0 1-.951.95h-5.71a.95.95 0 0 1-.951-.95m51.382-90.396H95.569a34.6 34.6 0 0 0-34.6 34.6v12.976" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60">
|
||||
<path d="M3.692 5.884c0-1.21.981-2.192 2.192-2.192h48.23c1.212 0 2.193.981 2.193 2.192v48.23a2.19 2.19 0 0 1-2.192 2.193H5.885a2.19 2.19 0 0 1-2.193-2.192z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 329 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 90">
|
||||
<path d="M45.944 41.767a2.29 2.29 0 0 0 2.29 2.29H83.92a2.29 2.29 0 0 1 2.29 2.29V83.92a2.29 2.29 0 0 1-2.29 2.29H6.08a2.29 2.29 0 0 1-2.29-2.29V6.08a2.29 2.29 0 0 1 2.29-2.29h37.576a2.29 2.29 0 0 1 2.29 2.29z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 383 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 50">
|
||||
<path d="M38.188 8.7h10.598c1.211 0 2.193.956 2.193 2.136v28.2c0 1.18-.982 2.136-2.193 2.136H11.08c-1.21 0-2.192-.956-2.192-2.136v-28.2c0-1.18.981-2.136 2.192-2.136h10.599m10.885-5.128V15.11c0 1.18-.981 2.136-2.192 2.136h-.877c-1.21 0-2.192-.957-2.192-2.136V3.572m5.261 7.691h3.07c1.21 0 2.192-.956 2.192-2.136v-.855c0-1.18-.982-2.136-2.192-2.136h-3.07m-5.261 5.127h-3.07c-1.21 0-2.192-.956-2.192-2.136v-.855c0-1.18.982-2.136 2.193-2.136h3.069M5.817 46.299h48.231c1.21 0 2.192-.956 2.192-2.136V5.709c0-1.18-.981-2.137-2.192-2.137H5.818c-1.211 0-2.193.957-2.193 2.137v38.454c0 1.18.982 2.136 2.192 2.136M33.44 25.79c0 1.888-1.57 3.418-3.507 3.418s-3.508-1.53-3.508-3.418c0-1.887 1.57-3.418 3.508-3.418s3.507 1.53 3.507 3.418" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 897 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 50">
|
||||
<path d="M53.173 12.182c-.468 0-.847.354-.847.79s.38.79.847.79zm21.979 1.58c.467 0 .846-.353.846-.79 0-.436-.379-.79-.846-.79zM6.01 3.572v.79h77.842v-1.58H6.01zM86.14 5.71h-.847v38.454h1.694V5.709zM83.85 46.3v-.79H6.01v1.58h77.842zM3.72 44.163h.847V5.709H2.873v38.454zm2.29 2.136v-.79c-.797 0-1.443-.603-1.443-1.346H2.873c0 1.616 1.404 2.926 3.137 2.926zm80.131-2.136h-.847c0 .743-.646 1.346-1.442 1.346v1.58c1.732 0 3.136-1.31 3.136-2.926zm-2.29-40.59v.79c.797 0 1.443.602 1.443 1.346h1.694c0-1.617-1.404-2.927-3.136-2.927zm-77.841 0v-.79c-1.733 0-3.137 1.31-3.137 2.926h1.694c0-.744.646-1.347 1.443-1.347zm5.494 37.599v.79h28.39v-1.58h-28.39zm30.68-2.136h.846v-28.2h-1.694v28.2zm-32.97-28.2h-.846v28.2h1.694v-28.2zM39.895 8.7v-.79h-5.669v1.58h5.669zm-22.721 0v-.79h-5.669v1.58h5.669zm-7.958 2.136h.847c0-.744.645-1.346 1.442-1.346V7.91c-1.732 0-3.136 1.31-3.136 2.926zm32.968 0h.847c0-1.616-1.404-2.927-3.136-2.927v1.58c.797 0 1.442.603 1.442 1.347zm-2.29 30.336v.79c1.733 0 3.137-1.31 3.137-2.926h-1.694c0 .743-.645 1.346-1.442 1.346zm-28.389 0v-.79c-.797 0-1.442-.603-1.442-1.346H8.368c0 1.616 1.404 2.926 3.136 2.926zm13.737-23.927v.79h.916v-1.58h-.916zm3.205-2.136h.847V3.572H27.6V15.11zM22.952 3.572h-.847V15.11h1.693V3.572zm3.205 13.673v.79c1.732 0 3.136-1.31 3.136-2.926H27.6c0 .743-.646 1.346-1.443 1.346zm-.916 0v-.79c-.797 0-1.443-.603-1.443-1.346h-1.693c0 1.616 1.404 2.926 3.136 2.926zm3.205-5.982v.79h3.206v-1.58h-3.206zm5.495-2.136h.847v-.855h-1.694v.855zm-2.29-2.991v-.79h-3.205v1.58h3.206zm2.29 2.136h.847c0-1.616-1.404-2.926-3.136-2.926v1.58c.796 0 1.442.603 1.442 1.346zm-2.29 2.991v.79c1.733 0 3.137-1.31 3.137-2.926h-1.694c0 .743-.646 1.346-1.442 1.346zm-11.905 0v.79h3.206v-1.58h-3.206zm3.206-5.127v-.79h-3.206v1.58h3.206zm-5.495 2.136h-.847v.855h1.694v-.855zm2.29-2.136v-.79c-1.733 0-3.137 1.31-3.137 2.926h1.694c0-.743.646-1.346 1.442-1.346zm0 5.127v-.79c-.797 0-1.443-.603-1.443-1.346H16.61c0 1.616 1.404 2.926 3.136 2.926zm9.615 14.527h-.847c0 1.452-1.26 2.628-2.816 2.628v1.58c2.49 0 4.51-1.884 4.51-4.208zM25.7 29.208v-.79c-1.555 0-2.816-1.176-2.816-2.628H21.19c0 2.324 2.02 4.209 4.51 4.209zm-3.663-3.418h.847c0-1.451 1.26-2.628 2.816-2.628v-1.58c-2.49 0-4.51 1.884-4.51 4.208zm3.663-3.418v.79c1.555 0 2.816 1.177 2.816 2.628h1.694c0-2.324-2.02-4.208-4.51-4.208zm24.268 18.8v.79h28.39v-1.58h-28.39zm30.68-2.136h.846v-28.2H79.8v28.2zM78.356 8.7v-.79h-28.39v1.58h28.39zm-30.68 2.136h-.846v28.2h1.694v-28.2zm2.29-2.136v-.79c-1.732 0-3.136 1.31-3.136 2.926h1.694c0-.744.646-1.346 1.442-1.346zm30.68 2.136h.846c0-1.616-1.404-2.927-3.136-2.927v1.58c.797 0 1.443.603 1.443 1.347zm-2.29 30.336v.79c1.732 0 3.136-1.31 3.136-2.926H79.8c0 .743-.646 1.346-1.443 1.346zm-28.39 0v-.79c-.796 0-1.442-.603-1.442-1.346H46.83c0 1.616 1.404 2.926 3.136 2.926zm3.206-28.2v.79h21.979v-1.58h-21.98z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 35">
|
||||
<path d="m4.444 31.033 50.75-27.007m-50.752-.06 50.75 27.008M5.884 31.5h48.23c1.212 0 2.193-.895 2.193-2v-24c0-1.105-.981-2-2.192-2H5.885c-1.212 0-2.193.895-2.193 2v24c0 1.105.981 2 2.192 2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 363 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60">
|
||||
<path d="M3.772 29.59 29.88 4.383M4.57 55.43 55.43 30M4.175 4.383l26.701 23.863m-27.184.877L55.43 55.43M28.41 3.692H5.884c-1.21 0-2.192.981-2.192 2.192v48.23c0 1.212.981 2.193 2.192 2.193h48.23a2.19 2.19 0 0 0 2.193-2.192V31.589c0-1.21-.981-2.192-2.192-2.192h-21.32a2.19 2.19 0 0 1-2.193-2.192V5.885a2.19 2.19 0 0 0-2.192-2.193" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 501 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 40">
|
||||
<path d="M5.773 3.555h38.454a2.136 2.056 0 0 1 2.137 2.056v28.777a2.136 2.056 0 0 1-2.137 2.056H5.773a2.136 2.056 0 0 1-2.136-2.056V5.611a2.136 2.056 0 0 1 2.136-2.056" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 341 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 35">
|
||||
<g>
|
||||
<path d="M6.12 31.5h87.76a2.31 2 0 0 0 2.31-2v-24a2.31 2 0 0 0-2.31-2H6.12a2.31 2 0 0 0-2.31 2v24a2.31 2 0 0 0 2.31 2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.089 25.933c-.494.112-.776.546-.63.97s.664.675 1.157.564l-.263-.767zM90.912 9.067c.493-.112.775-.546.63-.97s-.664-.675-1.158-.564l.264.767zM11.875 26.956c.493-.112.775-.546.63-.97s-.664-.676-1.158-.564l.264.767zm3.988-2.556c-.493.112-.775.546-.63.969s.664.676 1.158.564l-.264-.766zm5.045.511c.493-.112.775-.545.63-.969s-.664-.676-1.158-.564l.264.766zm3.988-2.555c-.493.111-.775.545-.63.968.146.424.664.677 1.158.565l-.264-.767zm5.044.51c.494-.111.776-.545.63-.968-.145-.424-.664-.676-1.157-.565l.263.767zm3.99-2.555c-.495.112-.777.546-.63.97.145.423.663.675 1.157.563l-.264-.766zm5.043.511c.494-.112.776-.545.63-.969s-.664-.676-1.158-.564l.264.767zm3.989-2.555c-.494.111-.776.545-.63.969s.664.676 1.157.564l-.263-.767zm5.044.51c.494-.11.776-.545.63-.968-.146-.424-.664-.676-1.158-.565l.264.767zm3.989-2.555c-.494.112-.776.546-.63.97.145.423.664.675 1.157.564l-.264-.767zm5.044.511c.493-.111.775-.545.63-.969s-.664-.676-1.158-.564l.264.767zm3.988-2.555c-.493.112-.775.545-.63.969s.664.676 1.158.564l-.264-.767zm5.045.51c.493-.11.775-.545.63-.968-.146-.424-.664-.676-1.158-.564l.264.766zm3.988-2.555c-.493.112-.775.546-.63.97s.664.675 1.158.564l-.264-.767zm5.044.511c.494-.111.776-.545.63-.968-.145-.424-.664-.677-1.157-.565l.264.767zm3.99-2.555c-.494.112-.777.545-.63.969.145.423.663.676 1.157.564l-.264-.766zm5.043.511c.494-.112.776-.546.63-.969s-.664-.676-1.157-.564l.263.766zm3.989-2.556c-.494.112-.776.546-.63.97s.664.676 1.158.564l-.264-.767zM9.353 26.7l.263.767 2.259-.511-.264-.767-.264-.767-2.258.511zm6.774-1.533.264.766 4.517-1.022-.264-.767-.264-.766-4.517 1.022zm9.033-2.045.264.767 4.516-1.022-.264-.767-.263-.767-4.517 1.023zm9.033-2.044.264.766 4.516-1.022-.264-.766-.264-.767-4.516 1.022zm9.033-2.045.263.767 4.517-1.022-.264-.767-.264-.767-4.516 1.023zm9.032-2.044.264.767 4.517-1.023-.264-.766-.264-.767-4.516 1.022zm9.033-2.045.264.767 4.517-1.022-.264-.767-.264-.766-4.517 1.022zm9.033-2.044.264.767 4.516-1.023-.263-.766-.264-.767-4.517 1.022zm9.033-2.044.264.766 4.516-1.022-.264-.767-.263-.766-4.517 1.022zM88.39 8.81l.264.767 2.258-.511-.264-.767-.264-.767-2.258.511z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.089 9.067c-.494-.112-.776-.546-.63-.97s.664-.675 1.157-.564l-.263.767zm81.823 16.866c.493.112.775.546.63.97s-.664.675-1.158.564l.264-.767zM11.875 8.044c.493.112.775.546.63.97s-.664.676-1.158.564l.264-.767zm3.988 2.556c-.493-.112-.775-.546-.63-.969s.664-.676 1.158-.564l-.264.766zm5.045-.511c.493.112.775.545.63.969s-.664.676-1.158.564l.264-.766zm3.988 2.555c-.493-.111-.775-.545-.63-.968.146-.424.664-.677 1.158-.565l-.264.767zm5.044-.51c.494.111.776.545.63.968-.145.424-.664.676-1.157.565l.263-.767zm3.99 2.555c-.495-.112-.777-.546-.63-.97.145-.423.663-.675 1.157-.563l-.264.766zm5.043-.511c.494.112.776.545.63.969s-.664.676-1.158.564l.264-.767zm3.989 2.555c-.494-.111-.776-.545-.63-.969s.664-.676 1.157-.564l-.263.767zm5.044-.51c.494.11.776.545.63.968-.146.424-.664.676-1.158.565l.264-.767zm3.989 2.555c-.494-.112-.776-.546-.63-.97.145-.423.664-.675 1.157-.564l-.264.767zm5.044-.511c.493.111.775.545.63.969s-.664.676-1.158.564l.264-.767zm3.988 2.555c-.493-.112-.775-.545-.63-.969s.664-.676 1.158-.564l-.264.767zm5.045-.51c.493.11.775.545.63.968-.146.424-.664.676-1.158.564l.264-.766zm3.988 2.555c-.493-.112-.775-.546-.63-.97s.664-.675 1.158-.564l-.264.767zm5.044-.511c.494.111.776.545.63.968-.145.424-.664.677-1.157.565l.264-.767zm3.99 2.555c-.494-.112-.777-.545-.63-.969.145-.423.663-.676 1.157-.564l-.264.766zm5.043-.511c.494.112.776.546.63.969s-.664.676-1.157.564l.263-.766zm3.989 2.556c-.494-.112-.776-.546-.63-.97s.664-.676 1.158-.564l-.264.767zM9.353 8.3l.263-.767 2.259.511-.264.767-.264.767-2.258-.511zm6.774 1.533.264-.766 4.517 1.022-.264.767-.264.766-4.517-1.022zm9.033 2.045.264-.767 4.516 1.022-.264.767-.263.767-4.517-1.023zm9.033 2.044.264-.766 4.516 1.022-.264.766-.264.767-4.516-1.022zm9.033 2.045.263-.767 4.517 1.022-.264.767-.264.767-4.516-1.023zm9.032 2.044.264-.767 4.517 1.023-.264.766-.264.767-4.516-1.022zm9.033 2.045.264-.767 4.517 1.022-.264.767-.264.766-4.517-1.022zm9.033 2.044.264-.767 4.516 1.023-.263.766-.264.767-4.517-1.022zm9.033 2.044.264-.766 4.516 1.022-.264.767-.263.766-4.517-1.022zm9.033 2.045.264-.767 2.258.511-.264.767-.264.767-2.258-.511z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 25">
|
||||
<path d="M18.36 3.15v2.567c0 1.012-1.034 1.833-2.31 1.833h-.924c-1.275 0-2.309-.82-2.309-1.833V3.15m73.905 0v2.567c0 1.012-1.034 1.833-2.31 1.833h-.924c-1.275 0-2.31-.82-2.31-1.833V3.15m-75.29 0H93.65c1.276 0 2.31.82 2.31 1.833V19.65c0 1.012-1.034 1.833-2.31 1.833H5.888c-1.275 0-2.31-.82-2.31-1.833V4.983c0-1.012 1.035-1.833 2.31-1.833" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 511 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 90">
|
||||
<path d="M90 19.365v57.111m0-57.11V7.683m0 11.681c0 2.151-1.836 3.894-4.1 3.894H30.544a10.7 10.7 0 0 1-4.136-.825M90 19.365c0 2.151 1.836 3.894 4.1 3.894h55.356c1.472 0 2.871-.294 4.137-.825M90 76.476c0 5.376-4.59 9.735-10.25 9.735H30.543c-5.266 0-9.605-3.772-10.185-8.625M90 76.476c0 5.376 4.59 9.735 10.25 9.735h49.206c5.266 0 9.605-3.772 10.185-8.625M90 7.684c0-2.15-1.837-3.894-4.101-3.894H30.544c-5.661 0-10.25 4.358-10.25 9.735q0 .416.036.825M90 7.684c0-2.15 1.836-3.894 4.1-3.894h55.356c5.661 0 10.25 4.358 10.25 9.735q0 .416-.036.825m0 0a10.7 10.7 0 0 1 4.137-.825h2.05c5.662 0 10.252 4.358 10.252 9.734v45.43c0 5.376-4.59 9.734-10.251 9.734h-2.05c-1.484 0-2.894-.3-4.167-.837m0 0c-3.585-1.517-6.084-4.93-6.084-8.898V23.26q0-.417.036-.825c.32-3.622 2.728-6.68 6.077-8.084m-139.34 0c3.35 1.404 5.758 4.462 6.078 8.084q.036.408.036.825v45.43c0 3.967-2.5 7.38-6.085 8.897a10.7 10.7 0 0 1-4.166.837h-2.05c-5.662 0-10.251-4.358-10.251-9.735V23.26c0-5.376 4.59-9.734 10.25-9.734h2.05c1.473 0 2.872.294 4.138.825" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 170">
|
||||
<path d="M93.673 20.687V83.69m0-63.004v-12.6m0 12.6c0 2.32-1.913 4.2-4.273 4.2H31.704a10.8 10.8 0 0 1-4.311-.89m66.28-3.31c0 2.32 1.914 4.2 4.274 4.2h64.106c2.36 0 4.274-1.88 4.274-4.2M93.673 83.692c0 5.8-4.783 10.501-10.684 10.501H31.704c-5.489 0-10.011-4.068-10.616-9.304m72.585-1.197c0 5.8 4.784 10.501 10.684 10.501h51.286c5.9 0 10.684-4.701 10.684-10.5m0-63.005V83.69m0-63.004v-12.6m0 12.6c0 2.32 1.914 4.2 4.273 4.2h57.696c1.534 0 2.993-.317 4.312-.89m-66.28 59.695v71.923c0 5.799 4.782 10.5 10.684 10.5h51.284c5.49 0 10.012-4.068 10.616-9.304V84.888M93.673 8.086c0-2.32-1.913-4.2-4.273-4.2H31.704c-5.9 0-10.684 4.7-10.684 10.5q0 .45.038.89m72.615-7.19c0-2.32 1.914-4.2 4.274-4.2h64.106c2.36 0 4.274 1.88 4.274 4.2m0 0c0-2.32 1.914-4.2 4.273-4.2h57.696c5.9 0 10.685 4.7 10.685 10.5 0 .293.024.587 0 .874m-.07 69.628c1.327.58 2.797.904 4.344.904h2.136c5.901 0 10.684-4.702 10.684-10.501V24.887c0-5.8-4.783-10.5-10.684-10.5h-2.136c-5.596 0-10.187 4.227-10.647 9.61q-.038.44-.038.89V75.29c0 4.28 2.605 7.961 6.342 9.597M21.059 15.276a10.8 10.8 0 0 0-4.312-.89H14.61c-5.9 0-10.684 4.701-10.684 10.5v50.405c0 5.8 4.783 10.5 10.684 10.5h2.137c1.546 0 3.016-.322 4.342-.903 3.737-1.636 6.342-5.317 6.342-9.597V24.887q0-.45-.037-.89c-.334-3.907-2.844-7.206-6.335-8.72" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 90">
|
||||
<path d="M86.553 18.593V76.96m0-58.366V7.491m0 11.102c0 2.044-1.762 3.701-3.935 3.701H29.496c-1.413 0-2.756-.28-3.97-.784m61.027-2.917c0 2.044 1.762 3.701 3.935 3.701h59.024c2.174 0 3.935-1.657 3.935-3.7M86.553 76.958c0 5.11-4.404 9.252-9.837 9.252h-47.22c-5.054 0-9.218-3.585-9.775-8.198m66.832-1.054c0 5.11 4.404 9.252 9.837 9.252h47.22c5.433 0 9.837-4.142 9.837-9.252m0-58.366V76.96m0-58.366V7.491m0 11.102c0 2.044 1.762 3.701 3.936 3.701h53.122c1.413 0 2.756-.28 3.97-.784m-61.028 55.449c0 5.11 4.405 9.252 9.838 9.252h47.22c5.054 0 9.218-3.585 9.775-8.198M86.553 7.491c0-2.044-1.762-3.701-3.935-3.701H29.496c-5.433 0-9.838 4.142-9.838 9.252q0 .396.035.784m66.86-6.335c0-2.044 1.762-3.701 3.935-3.701h59.024c2.174 0 3.935 1.657 3.935 3.7m0 0c0-2.043 1.762-3.7 3.936-3.7h53.122c5.433 0 9.838 4.142 9.838 9.252 0 .258.022.517 0 .77m-.063 64.201c1.22.512 2.574.796 3.998.796h1.967c5.433 0 9.837-4.142 9.837-9.252V22.294c0-5.11-4.404-9.252-9.837-9.252h-1.967c-5.153 0-9.38 3.725-9.803 8.468q-.036.387-.035.784v47.263c0 3.771 2.399 7.015 5.84 8.456M19.693 13.826a10.3 10.3 0 0 0-3.97-.784h-1.967c-5.433 0-9.838 4.142-9.838 9.252v47.263c0 5.11 4.405 9.252 9.838 9.252h1.967c1.424 0 2.777-.284 3.998-.796 3.441-1.441 5.84-4.685 5.84-8.456V22.294a9 9 0 0 0-.035-.784c-.307-3.443-2.618-6.349-5.833-7.684" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60">
|
||||
<path d="M41.4 35.261a.877.877 0 1 0-1.754 0zm4.384 6.138a.877.877 0 1 0 0-1.753zM41.4 44.03a.877.877 0 1 0-1.753 0zm-4.384-2.63a.877.877 0 1 0 0-1.754zM5.885 3.691v.877h48.23V2.815H5.885zm50.422 2.192h-.877v48.23h1.754V5.885zm-2.192 50.423v-.877H5.885v1.754h48.23zM3.692 54.115h.877V5.885H2.815v48.23zm2.192 2.192v-.877a1.315 1.315 0 0 1-1.315-1.315H2.815a3.07 3.07 0 0 0 3.07 3.069zm50.423-2.192h-.877c0 .726-.589 1.315-1.315 1.315v1.754a3.07 3.07 0 0 0 3.069-3.07zM54.115 3.692v.877c.726 0 1.315.589 1.315 1.315h1.754a3.07 3.07 0 0 0-3.07-3.069zm-48.23 0v-.877a3.07 3.07 0 0 0-3.07 3.07h1.754c0-.727.589-1.316 1.315-1.316zm19.73 36.83h-.877a5.26 5.26 0 0 1-5.262 5.262v1.754a7.015 7.015 0 0 0 7.016-7.015zm-6.139 6.139v-.877a5.26 5.26 0 0 1-5.261-5.261h-1.754a7.015 7.015 0 0 0 7.015 7.015zm-6.138-6.138h.877a5.26 5.26 0 0 1 5.261-5.262v-1.754a7.015 7.015 0 0 0-7.015 7.016zm6.138-6.139v.877a5.26 5.26 0 0 1 5.262 5.262h1.754a7.015 7.015 0 0 0-7.016-7.016zm27.185-14.908h-.877a5.26 5.26 0 0 1-5.261 5.262v1.754a7.015 7.015 0 0 0 7.015-7.016zm-6.138 6.139v-.877a5.26 5.26 0 0 1-5.262-5.262h-1.754a7.015 7.015 0 0 0 7.016 7.016zm-6.139-6.139h.877a5.26 5.26 0 0 1 5.262-5.261v-1.754a7.015 7.015 0 0 0-7.016 7.015zm6.139-6.138v.877a5.26 5.26 0 0 1 5.261 5.261h1.754a7.015 7.015 0 0 0-7.015-7.015zm4.384 27.185h-.877a3.51 3.51 0 0 1-3.507 3.507v1.754a5.26 5.26 0 0 0 5.261-5.261zm-4.384 4.384v-.877a3.51 3.51 0 0 1-3.508-3.507H35.26a5.26 5.26 0 0 0 5.262 5.261zm-4.385-4.384h.877a3.51 3.51 0 0 1 3.508-3.508V35.26a5.26 5.26 0 0 0-5.262 5.262zm4.385-4.385v.877a3.51 3.51 0 0 1 3.507 3.508h1.754a5.26 5.26 0 0 0-5.261-5.262zm0 .877h.876V35.26h-1.753v1.754zm3.508 3.508v.876h1.753v-1.753h-1.753zm-3.508 5.261h.876V44.03h-1.753v1.754zm-5.261-5.261v.876h1.753v-1.753h-1.753zm-9.647-21.047h-.877a5.26 5.26 0 0 1-5.262 5.262v1.754a7.015 7.015 0 0 0 7.016-7.016zm-6.139 6.139v-.877a5.26 5.26 0 0 1-5.261-5.262h-1.754a7.015 7.015 0 0 0 7.015 7.016zm-6.138-6.139h.877a5.26 5.26 0 0 1 5.261-5.261v-1.754a7.015 7.015 0 0 0-7.015 7.015zm6.138-6.138v.877a5.26 5.26 0 0 1 5.262 5.261h1.754a7.015 7.015 0 0 0-7.016-7.015z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 90">
|
||||
<g>
|
||||
<path d="M3.879 6.08c0-1.265 1.065-2.29 2.378-2.29h147.484c1.314 0 2.38 1.025 2.38 2.29v77.84c0 1.265-1.066 2.29-2.38 2.29H6.257c-1.313 0-2.378-1.025-2.378-2.29z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M138.994 19.358v-5.495a.952.916 0 0 1 .951-.915h5.71a.952.916 0 0 1 .95.915v5.495a.952.916 0 0 1-.95.916h-5.71a.952.916 0 0 1-.951-.916m-.001 56.778v-5.495a.952.916 0 0 1 .952-.915h5.709a.952.916 0 0 1 .951.915v5.495a.952.916 0 0 1-.951.916h-5.71a.952.916 0 0 1-.95-.916m-125.6-56.778v-5.495a.952.916 0 0 1 .951-.916h5.71a.952.916 0 0 1 .95.916v5.495a.952.916 0 0 1-.95.915h-5.71a.952.916 0 0 1-.951-.915m-.001 56.779v-5.495a.952.916 0 0 1 .952-.916h5.709a.952.916 0 0 1 .951.916v5.495a.952.916 0 0 1-.951.915h-5.71a.952.916 0 0 1-.95-.915" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1007 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 100">
|
||||
<g>
|
||||
<path d="M3.892 50c0-25.51 21.418-46.19 47.838-46.19h76.54c26.42 0 47.839 20.68 47.839 46.19S154.69 96.19 128.27 96.19H51.73C25.31 96.19 3.892 75.51 3.892 50" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M122.53 19.514V13.97a.957.924 0 0 1 .956-.924h5.74a.957.924 0 0 1 .958.924v5.543a.957.924 0 0 1-.957.924h-5.74a.957.924 0 0 1-.958-.924m.001 66.516v-5.543a.957.924 0 0 1 .957-.924h5.74a.957.924 0 0 1 .957.924v5.543a.957.924 0 0 1-.957.924h-5.74a.957.924 0 0 1-.957-.924M49.816 19.515v-5.543a.957.924 0 0 1 .957-.924h5.74a.957.924 0 0 1 .958.924v5.542a.957.924 0 0 1-.957.924h-5.74a.957.924 0 0 1-.958-.924m0 66.516v-5.543a.957.924 0 0 1 .957-.924h5.74a.957.924 0 0 1 .957.924v5.543a.957.924 0 0 1-.957.923h-5.74a.957.924 0 0 1-.957-.924" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1001 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 90">
|
||||
<g>
|
||||
<path d="M3.879 17.527c0-7.587 6.39-13.737 14.272-13.737h123.696c7.883 0 14.273 6.15 14.273 13.737v54.947c0 7.587-6.39 13.737-14.273 13.737H18.151c-7.882 0-14.272-6.15-14.272-13.737z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M138.994 19.358v-5.495a.952.916 0 0 1 .951-.915h5.71a.952.916 0 0 1 .95.915v5.495a.952.916 0 0 1-.95.916h-5.71a.952.916 0 0 1-.951-.916m-.001 56.778v-5.495a.952.916 0 0 1 .952-.915h5.709a.952.916 0 0 1 .951.915v5.495a.952.916 0 0 1-.951.916h-5.71a.952.916 0 0 1-.95-.916m-125.6-56.778v-5.495a.952.916 0 0 1 .951-.916h5.71a.952.916 0 0 1 .95.916v5.495a.952.916 0 0 1-.95.915h-5.71a.952.916 0 0 1-.951-.915m-.001 56.779v-5.495a.952.916 0 0 1 .952-.916h5.709a.952.916 0 0 1 .951.916v5.495a.952.916 0 0 1-.951.915h-5.71a.952.916 0 0 1-.95-.915" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
|
||||
<g>
|
||||
<path d="M3.826 55a51.174 51.174 0 1 0 102.349 0A51.174 51.174 0 1 0 3.826 55" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="m81.005 32.303-3.947-3.947a.93.93 0 0 1 0-1.316l3.947-3.948a.93.93 0 0 1 1.316 0l3.948 3.948a.93.93 0 0 1 0 1.316l-3.948 3.947a.93.93 0 0 1-1.316 0m0 54.897-3.947-3.948a.93.93 0 0 1 0-1.316l3.947-3.947a.93.93 0 0 1 1.316 0l3.948 3.947a.93.93 0 0 1 0 1.316L82.32 87.2a.93.93 0 0 1-1.316 0M27.04 32.304l-3.947-3.948a.93.93 0 0 1 0-1.315l3.948-3.948a.93.93 0 0 1 1.315 0l3.948 3.948a.93.93 0 0 1 0 1.315l-3.948 3.948a.93.93 0 0 1-1.315 0m-.001 54.604-3.947-3.948a.93.93 0 0 1 0-1.316l3.948-3.947a.93.93 0 0 1 1.315 0l3.948 3.947a.93.93 0 0 1 0 1.316l-3.948 3.948a.93.93 0 0 1-1.315 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 965 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 70">
|
||||
<g>
|
||||
<path d="M3.555 48.976c0 9.55 7.125 17.29 15.914 17.29h1.061c8.79 0 15.914-7.74 15.914-17.29V5.966c0-1.233-.92-2.233-2.056-2.233H5.611c-1.135 0-2.056 1-2.056 2.234z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.666 49.12c0 7.003 5.225 12.68 11.67 12.68h1.327c6.445 0 11.67-5.677 11.67-12.68V31.873c0-1.233-.92-2.233-2.056-2.233H9.722c-1.135 0-2.056 1-2.056 2.233zM33.792 3.733c1.465 0 2.652 1.29 2.652 2.882v12.103c0 1.592-1.187 2.882-2.652 2.882H6.208c-1.465 0-2.653-1.29-2.653-2.882V6.615c0-1.591 1.188-2.882 2.653-2.882z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24.11 50.186c0 2.467-1.84 4.467-4.11 4.467s-4.111-2-4.111-4.467 1.84-4.466 4.11-4.466 4.112 2 4.112 4.466" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1008 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 55">
|
||||
<g>
|
||||
<path d="M3.555 34.56c0 9.263 7.125 16.774 15.914 16.774h1.061c8.79 0 15.914-7.51 15.914-16.775V5.833c0-1.196-.92-2.166-2.056-2.166H5.611c-1.135 0-2.056.97-2.056 2.166z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.666 34.7c0 6.793 5.225 12.3 11.67 12.3h1.327c6.445 0 11.67-5.507 11.67-12.3V14.5c0-1.197-.92-2.167-2.056-2.167H9.722c-1.135 0-2.056.97-2.056 2.167z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24.11 35.733c0 2.394-1.84 4.334-4.11 4.334s-4.111-1.94-4.111-4.334 1.84-4.333 4.11-4.333 4.112 1.94 4.112 4.333" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 853 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 28">
|
||||
<path d="M45.134 10.766H6.18c-1.292 0-2.34.848-2.34 1.894v2.68c0 1.045 1.048 1.893 2.34 1.893h38.954m0-6.467V5.288c0-1.046 1.048-1.894 2.34-1.894h26.704c1.292 0 2.34.848 2.34 1.894v5.478m-31.384 0h31.384m0 0h37.302c1.293 0 2.34.848 2.34 1.894v2.68c0 1.045-1.047 1.893-2.34 1.893H76.518m-31.384 0v5.479c0 1.046 1.048 1.894 2.34 1.894h26.704c1.292 0 2.34-.848 2.34-1.894v-5.479m-31.384 0h31.384" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 567 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 15">
|
||||
<path d="M35.664 11.4c-.517 0-.936.269-.936.6s.419.6.936.6zm49.553 1.2c.517 0 .936-.269.936-.6s-.42-.6-.936-.6zM6.18 3v.6h107.64V2.4H6.18zm109.98 1.5h-.936v1.8h1.872V4.5zm-2.34 3.3v-.6H6.18v1.2h107.64zM3.84 6.3h.936V4.5H2.904v1.8zm2.34 1.5v-.6c-.775 0-1.404-.403-1.404-.9H2.904c0 1.16 1.467 2.1 3.276 2.1zm109.98-1.5h-.936c0 .497-.629.9-1.404.9v1.2c1.81 0 3.276-.94 3.276-2.1zM113.82 3v.6c.775 0 1.404.403 1.404.9h1.872c0-1.16-1.467-2.1-3.276-2.1zM6.18 3v-.6c-1.81 0-3.276.94-3.276 2.1h1.872c0-.497.629-.9 1.404-.9zm61.308 4.2h-.936V12h1.872V7.2zM52.512 12h.936V7.2h-1.872V12zm-16.848 0v.6h49.553v-1.2H35.664z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 784 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 45">
|
||||
<path d="M6.336 41.4h227.328a2.418 2.1 0 0 0 2.418-2.1V5.7a2.418 2.1 0 0 0-2.418-2.1H6.336a2.418 2.1 0 0 0-2.418 2.1v33.6a2.418 2.1 0 0 0 2.418 2.1m43.744-5.88V9.48m46.432 26.04V9.48m46.433 26.04V9.48m46.433 26.04V9.48" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 393 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 60">
|
||||
<path d="M93.87 20.25a3.3 3.3 0 0 0-1.072-2.407 3.8 3.8 0 0 0-2.586-.997c-.97 0-1.9.358-2.587.997a3.3 3.3 0 0 0-1.071 2.407c0 2.838 1.225 5.107 3.658 6.81h-.015m0 0 14.58 7.537c.57.295 1.046.726 1.377 1.25.331.523.505 1.12.505 1.727v1.401a3.3 3.3 0 0 1-1.071 2.407 3.8 3.8 0 0 1-2.587.998H77.393a3.8 3.8 0 0 1-2.586-.998 3.3 3.3 0 0 1-1.072-2.407v-1.4c0-.608.174-1.205.505-1.728a3.55 3.55 0 0 1 1.377-1.25zM6.284 3.691h167.433c1.32 0 2.392.981 2.392 2.192v48.23c0 1.212-1.071 2.193-2.392 2.193H6.284c-1.321 0-2.392-.981-2.392-2.192V5.885c0-1.212 1.07-2.193 2.392-2.193" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 743 B |
@@ -13,11 +13,12 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from . import websocket_api as hp_ws
|
||||
from .const import (
|
||||
ASSETS_DIR,
|
||||
DOMAIN,
|
||||
FILES_DIR,
|
||||
FILES_URL,
|
||||
FRONTEND_URL,
|
||||
PLANS_DIR,
|
||||
PLANS_URL,
|
||||
VERSION,
|
||||
)
|
||||
from .geometry_migration import migrate_config, migrate_layout, pending_from_config
|
||||
@@ -37,16 +38,10 @@ async def async_setup(hass: HomeAssistant, config) -> bool:
|
||||
"""Register global handlers (survive config-entry reloads): WS commands, HTTP view."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hp_ws.async_register(hass)
|
||||
from .http_api import HouseplanContentView, HouseplanImportPreviewView, HouseplanUploadView
|
||||
from .frontend_assets import HouseplanFrontendAssetView
|
||||
from .http_api import (
|
||||
HouseplanContentView,
|
||||
HouseplanDecorAssetUploadView,
|
||||
HouseplanImportPreviewView,
|
||||
HouseplanUploadView,
|
||||
)
|
||||
|
||||
hass.http.register_view(HouseplanUploadView())
|
||||
hass.http.register_view(HouseplanDecorAssetUploadView())
|
||||
hass.http.register_view(HouseplanContentView())
|
||||
hass.http.register_view(HouseplanImportPreviewView())
|
||||
hass.http.register_view(HouseplanFrontendAssetView())
|
||||
@@ -90,13 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: HouseplanConfigEntry) ->
|
||||
card_path = Path(__file__).parent / "frontend" / "houseplan-card.js"
|
||||
plans_path = Path(hass.config.path(PLANS_DIR))
|
||||
files_path = Path(hass.config.path(FILES_DIR))
|
||||
assets_path = Path(hass.config.path(ASSETS_DIR))
|
||||
await hass.async_add_executor_job(
|
||||
lambda: (
|
||||
plans_path.mkdir(parents=True, exist_ok=True),
|
||||
files_path.mkdir(parents=True, exist_ok=True),
|
||||
assets_path.mkdir(parents=True, exist_ok=True),
|
||||
)
|
||||
lambda: (plans_path.mkdir(parents=True, exist_ok=True), files_path.mkdir(parents=True, exist_ok=True))
|
||||
)
|
||||
|
||||
# Static paths cannot be unregistered — register once per HA run.
|
||||
@@ -249,9 +239,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: HouseplanConfigEntry) ->
|
||||
hass.bus.async_fire("houseplan_config_updated", {"rev": optimize_revs[0]})
|
||||
hass.bus.async_fire("houseplan_layout_updated", {"rev": optimize_revs[1]})
|
||||
if recovered_import:
|
||||
current = (await data.config_store.async_load() or {}).get("config") or {}
|
||||
await recorder.async_purge_orphans(current)
|
||||
await recorder.async_refresh()
|
||||
current = (await data.config_store.async_load() or {}).get("config") or {}
|
||||
live_ids = {str(marker.get("id")) for marker in current.get("markers") or []}
|
||||
for marker_id in list(recorder.book.data):
|
||||
if marker_id not in live_ids:
|
||||
await recorder.async_delete(marker_id)
|
||||
|
||||
await async_check_plan_files(hass, entry)
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
"""Bounded, shared integrity verification for content-addressed decor assets."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import stat
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ASSET_INTEGRITY_CACHE_ENTRIES = 256
|
||||
ASSET_HASH_CHUNK_BYTES = 64 * 1024
|
||||
ASSET_INTEGRITY_FOLLOWER_TIMEOUT_SECONDS = 30.0
|
||||
_HASS_DATA_KEY = "asset_integrity_verifier"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FileSignature:
|
||||
"""File version facts available without reading its content."""
|
||||
|
||||
size: int
|
||||
mtime_ns: int
|
||||
ctime_ns: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _CacheEntry:
|
||||
signature: FileSignature
|
||||
digest: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Flight:
|
||||
event: threading.Event
|
||||
digest: str | None = None
|
||||
|
||||
|
||||
def _signature(path: Path) -> FileSignature:
|
||||
current = path.stat()
|
||||
if not stat.S_ISREG(current.st_mode):
|
||||
raise OSError("asset is not a regular file")
|
||||
return FileSignature(
|
||||
size=current.st_size,
|
||||
mtime_ns=current.st_mtime_ns,
|
||||
ctime_ns=current.st_ctime_ns,
|
||||
)
|
||||
|
||||
|
||||
def _stream_sha256(path: Path) -> str:
|
||||
"""Hash a blob without retaining its bytes in memory."""
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(ASSET_HASH_CHUNK_BYTES):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
class AssetIntegrityVerifier:
|
||||
"""Thread-safe LRU digest cache with per-file-version single-flight."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
max_entries: int = ASSET_INTEGRITY_CACHE_ENTRIES,
|
||||
*,
|
||||
hasher: Callable[[Path], str] | None = None,
|
||||
event_factory: Callable[[], threading.Event] | None = None,
|
||||
) -> None:
|
||||
if max_entries < 1:
|
||||
raise ValueError("max_entries must be positive")
|
||||
self._max_entries = max_entries
|
||||
self._hasher = hasher or _stream_sha256
|
||||
self._event_factory = event_factory or threading.Event
|
||||
self._lock = threading.Lock()
|
||||
self._cache: OrderedDict[str, _CacheEntry] = OrderedDict()
|
||||
self._inflight: dict[tuple[str, FileSignature], _Flight] = {}
|
||||
|
||||
def verify(self, path: Path, expected_digest: str) -> bool:
|
||||
"""Return whether one stable file version has the expected digest."""
|
||||
try:
|
||||
canonical = str(path.resolve())
|
||||
except (OSError, RuntimeError):
|
||||
return False
|
||||
try:
|
||||
before = _signature(path)
|
||||
except OSError:
|
||||
with self._lock:
|
||||
self._cache.pop(canonical, None)
|
||||
return False
|
||||
|
||||
key = (canonical, before)
|
||||
with self._lock:
|
||||
cached = self._cache.get(canonical)
|
||||
if cached is not None and cached.signature == before:
|
||||
self._cache.move_to_end(canonical)
|
||||
return cached.digest == expected_digest
|
||||
if cached is not None:
|
||||
self._cache.pop(canonical, None)
|
||||
flight = self._inflight.get(key)
|
||||
owner = flight is None
|
||||
if owner:
|
||||
flight = _Flight(self._event_factory())
|
||||
self._inflight[key] = flight
|
||||
|
||||
assert flight is not None
|
||||
if not owner:
|
||||
if not flight.event.wait(ASSET_INTEGRITY_FOLLOWER_TIMEOUT_SECONDS):
|
||||
return False
|
||||
return flight.digest == expected_digest
|
||||
|
||||
digest: str | None = None
|
||||
stable = False
|
||||
try:
|
||||
digest = self._hasher(path)
|
||||
# Never publish a digest for bytes that changed while they were read.
|
||||
stable = _signature(path) == before
|
||||
except Exception as err: # noqa: BLE001 - filesystem/hash seam fails dark
|
||||
_LOGGER.debug("House Plan asset integrity check failed: %s", err)
|
||||
finally:
|
||||
with self._lock:
|
||||
if stable and digest is not None:
|
||||
self._cache[canonical] = _CacheEntry(before, digest)
|
||||
self._cache.move_to_end(canonical)
|
||||
while len(self._cache) > self._max_entries:
|
||||
self._cache.popitem(last=False)
|
||||
flight.digest = digest
|
||||
self._inflight.pop(key, None)
|
||||
flight.event.set()
|
||||
return stable and digest == expected_digest
|
||||
|
||||
|
||||
def get_asset_integrity_verifier(hass: Any) -> AssetIntegrityVerifier:
|
||||
"""Return the single verifier shared by HTTP and WS on this HA instance."""
|
||||
domain_data = hass.data.setdefault(DOMAIN, {})
|
||||
verifier = domain_data.get(_HASS_DATA_KEY)
|
||||
if not isinstance(verifier, AssetIntegrityVerifier):
|
||||
verifier = AssetIntegrityVerifier()
|
||||
domain_data[_HASS_DATA_KEY] = verifier
|
||||
return verifier
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
||||
from .const import CONF_ADMIN_ONLY, DOMAIN
|
||||
|
||||
@@ -29,9 +29,6 @@ MAX_PLANS_FILES = 200
|
||||
MAX_PLANS_LISTED = 60
|
||||
MAX_FILES_BYTES = 1024 * 1024 * 1024
|
||||
MAX_FILES_COUNT = 1000
|
||||
MAX_DECOR_ASSET_BYTES = 2 * 1024 * 1024
|
||||
MAX_DECOR_ASSETS_BYTES = 256 * 1024 * 1024
|
||||
MAX_DECOR_ASSETS_COUNT = 200
|
||||
# Refuse to write when the disk is nearly full: filling the config partition
|
||||
# breaks .storage, the recorder and backups, not just this card.
|
||||
MIN_FREE_BYTES = 512 * 1024 * 1024
|
||||
@@ -49,29 +46,14 @@ PLAN_ORPHAN_TTL_S = 3600
|
||||
# action says so, plus staging folders after PLAN_ORPHAN_TTL_S.
|
||||
SCHEDULED_GRACE_S = 30 * 24 * 3600
|
||||
FILES_DIR = "houseplan/files"
|
||||
ASSETS_DIR = "houseplan/assets"
|
||||
CONF_ADMIN_ONLY = "admin_only"
|
||||
VERSION = "1.71.0-beta.3"
|
||||
DECOR_ASSETS_API_VERSION = 1
|
||||
|
||||
# #43: the support transport is deliberately not configurable. A user supplied
|
||||
# URL would turn the integration into an SSRF proxy and make the privacy notice
|
||||
# false. The relay is deployed and operated by the project; changing it is a
|
||||
# reviewed release change, not a Home Assistant option.
|
||||
SUPPORT_RELAY_URL = "https://support.houseplan.tech/v1/reports"
|
||||
SUPPORT_PREVIEW_TTL_S = 10 * 60
|
||||
SUPPORT_API_VERSION = 1
|
||||
MAX_SUPPORT_PREVIEWS_PER_USER = 3
|
||||
MAX_SUPPORT_PREVIEWS_TOTAL = 3
|
||||
MAX_SUPPORT_ATTACHMENT_BYTES = 8 * 1024 * 1024
|
||||
MAX_SUPPORT_MESSAGE_CODEPOINTS = 10_000
|
||||
MAX_SUPPORT_CONTACT_CODEPOINTS = 320
|
||||
VERSION = "1.69.0-beta.1"
|
||||
|
||||
# Portable backup format. This is deliberately independent from the Home
|
||||
# Assistant Store version above: storage migrations and files exported by a
|
||||
# user have different compatibility lifecycles.
|
||||
PLAN_MODEL_VERSION = 9
|
||||
EXPORT_VERSION = 2
|
||||
EXPORT_VERSION = 1
|
||||
MAX_EXPORT_BYTES = 8 * 1024 * 1024
|
||||
IMPORT_PREVIEW_TTL_S = 10 * 60
|
||||
MAX_IMPORT_PREVIEWS_PER_USER = 3
|
||||
@@ -81,47 +63,8 @@ MAX_IMPORT_PREVIEWS_PER_USER = 3
|
||||
# accounts.
|
||||
MAX_IMPORT_PREVIEWS_TOTAL = 3
|
||||
|
||||
DEFAULT_CONFIG: dict[str, object] = {
|
||||
DEFAULT_CONFIG: dict = {
|
||||
"spaces": [],
|
||||
"markers": [],
|
||||
"settings": {"bg_mode": "daynight"},
|
||||
}
|
||||
|
||||
# #42: THE stable public error-code contract. Every code a user-facing
|
||||
# failure can carry — send_error literals, exception-class codes and the
|
||||
# literal MarkerControlError codes — lives here; the scanner in
|
||||
# tests_backend/test_backend_quality.py fails when a source emits a code
|
||||
# outside this set (fail-closed), and every fixed code has a localized
|
||||
# `backup.error.<code>` message on the frontend.
|
||||
ERROR_CODES: frozenset[str] = frozenset({
|
||||
"capacity_exceeded", "commit_failed", "conflict",
|
||||
"content_confirmation_required", "duplicate_marker_control",
|
||||
"future_model", "in_use", "invalid_config", "invalid_content",
|
||||
"invalid_data", "invalid_format", "invalid_image", "invalid_json", "invalid_layout",
|
||||
"invalid_light_entity", "invalid_marker_control", "invalid_name",
|
||||
"invalid_partition_opening_host",
|
||||
"invalid_partition_opening_jamb_margin", "invalid_passage_fields",
|
||||
"invalid_space_id", "invalid_toggle_entity", "invalid_vacuum_map_route",
|
||||
"invalid_value_badge",
|
||||
"invalid_value_badge_attribute", "invalid_value_badge_position",
|
||||
"invalid_value_badge_source", "invalid_value_source",
|
||||
"invalid_value_source_attribute", "io_error", "marker_control_cycle",
|
||||
"marker_control_missing", "marker_control_not_light",
|
||||
"marker_control_self", "missing_content", "missing_plan", "no_backup",
|
||||
"not_ready", "not_toggleable", "nothing_to_repair", "preview_expired",
|
||||
"preview_owner_mismatch", "space_in_use", "space_not_found",
|
||||
"support_invalid_message", "support_package_too_large",
|
||||
"support_preview_expired", "support_rate_limited", "support_rejected",
|
||||
"support_unavailable",
|
||||
"too_large", "unauthorized", "unsupported_export_version", "unsupported_image",
|
||||
"value_badge_source_required", "wall_model_client_outdated",
|
||||
"wall_model_migration_blocked",
|
||||
})
|
||||
|
||||
# Template-code families: f-string codes carry one of these prefixes and are
|
||||
# served by the generic per-code fallback on the frontend.
|
||||
ERROR_CODE_FAMILIES: tuple[str, ...] = (
|
||||
"junction_limit_",
|
||||
"value_badge_",
|
||||
"value_source_",
|
||||
)
|
||||
|
||||
@@ -10,11 +10,11 @@ import copy
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
|
||||
COORDINATE_DECIMALS = 9
|
||||
COORDINATE_FACTOR = 10**COORDINATE_DECIMALS
|
||||
LATTICE_GRID_N = 240
|
||||
LATTICE_NOISE_STEPS = 1e-4
|
||||
DECOR_BOX_KINDS = ("rect", "ellipse", "furniture", "image")
|
||||
|
||||
|
||||
def canonicalize_number(value: Any) -> Any:
|
||||
@@ -151,7 +151,7 @@ def canonicalize_config_geometry(config: Any) -> Any:
|
||||
kind = decor.get("kind")
|
||||
if kind == "line":
|
||||
_lattice_fields(decor, ("x1", "y1", "x2", "y2"))
|
||||
elif kind in DECOR_BOX_KINDS:
|
||||
elif kind in ("rect", "ellipse", "furniture"):
|
||||
_lattice_fields(decor, ("x", "y", "w", "h"))
|
||||
_scalar_fields(decor, ("angle",))
|
||||
elif kind == "text":
|
||||
|
||||
@@ -1,466 +0,0 @@
|
||||
"""Content-addressed custom images for the decorative layer (#51).
|
||||
|
||||
Upload plumbing hands this module untrusted bytes. It either returns one fully
|
||||
validated canonical asset or rejects the whole file; SVG is never repaired or
|
||||
partially stripped.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import stat
|
||||
import struct
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from xml.parsers import expat
|
||||
|
||||
from .const import CONTENT_URL, MAX_DECOR_ASSET_BYTES
|
||||
|
||||
ASSET_ID_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
ASSET_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".webp", ".svg"})
|
||||
MIME_BY_EXT = {
|
||||
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
||||
".webp": "image/webp", ".svg": "image/svg+xml",
|
||||
}
|
||||
EXT_BY_MIME = {
|
||||
"image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp",
|
||||
"image/svg+xml": ".svg",
|
||||
}
|
||||
MAX_RASTER_DIMENSION = 16_384
|
||||
MAX_RASTER_PIXELS = (128 * 1024 * 1024) // 4
|
||||
MAX_SVG_ELEMENTS = 5_000
|
||||
MAX_SVG_DEPTH = 64
|
||||
MAX_SVG_ATTR_CHARS = 512_000
|
||||
MAX_SVG_ATTR_VALUE_CHARS = 65_536
|
||||
_SVG_TAGS = frozenset({
|
||||
"svg", "g", "defs", "title", "desc", "path", "rect", "circle", "ellipse",
|
||||
"line", "polyline", "polygon", "clipPath", "mask", "linearGradient",
|
||||
"radialGradient", "stop",
|
||||
})
|
||||
_SVG_ATTRS = frozenset({
|
||||
"xmlns", "viewBox", "width", "height", "x", "y", "x1", "y1", "x2", "y2",
|
||||
"cx", "cy", "r", "rx", "ry", "d", "points", "transform", "fill", "fill-rule",
|
||||
"fill-opacity", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin",
|
||||
"stroke-dasharray", "stroke-dashoffset", "stroke-opacity", "opacity", "offset",
|
||||
"stop-color", "stop-opacity", "gradientUnits", "gradientTransform", "id",
|
||||
"clip-path", "mask", "preserveAspectRatio", "href",
|
||||
})
|
||||
_LOCAL_REF = re.compile(r"^url\(#[A-Za-z_][A-Za-z0-9_.:-]*\)$")
|
||||
_LENGTH = re.compile(r"^\s*([0-9]+(?:\.[0-9]+)?)\s*(?:px)?\s*$", re.I)
|
||||
_SVG_NAMESPACE = "http://www.w3.org/2000/svg"
|
||||
_SVG_UNIT_INTERVAL_ATTRS = frozenset({
|
||||
"opacity", "fill-opacity", "stroke-opacity", "stop-opacity", "offset",
|
||||
})
|
||||
|
||||
|
||||
class DecorAssetError(ValueError):
|
||||
"""Stable validation failure suitable for an API error response."""
|
||||
|
||||
def __init__(self, code: str, message: str) -> None:
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ValidatedAsset:
|
||||
data: bytes
|
||||
mime: str
|
||||
ext: str
|
||||
width: int
|
||||
height: int
|
||||
|
||||
@property
|
||||
def asset_id(self) -> str:
|
||||
return hashlib.sha256(self.data).hexdigest()
|
||||
|
||||
|
||||
def _check_size(data: bytes) -> None:
|
||||
if not data:
|
||||
raise DecorAssetError("invalid_image", "The image is empty")
|
||||
if len(data) > MAX_DECOR_ASSET_BYTES:
|
||||
raise DecorAssetError("too_large", "The image exceeds the 2 MiB limit")
|
||||
|
||||
|
||||
def _raster_dimensions(data: bytes, ext: str) -> tuple[int, int]:
|
||||
try:
|
||||
if ext == ".png":
|
||||
if (len(data) < 45 or data[:8] != b"\x89PNG\r\n\x1a\n"
|
||||
or data[12:16] != b"IHDR" or b"IDAT" not in data
|
||||
or data[-8:-4] != b"IEND"):
|
||||
raise ValueError
|
||||
return struct.unpack(">II", data[16:24])
|
||||
if ext in (".jpg", ".jpeg"):
|
||||
if len(data) < 4 or data[:2] != b"\xff\xd8":
|
||||
raise ValueError
|
||||
pos = 2
|
||||
while pos + 4 <= len(data):
|
||||
if data[pos] != 0xFF:
|
||||
pos += 1
|
||||
continue
|
||||
marker = data[pos + 1]
|
||||
pos += 2
|
||||
if marker in (0xD8, 0xD9) or 0xD0 <= marker <= 0xD7:
|
||||
continue
|
||||
length = int.from_bytes(data[pos:pos + 2], "big")
|
||||
if length < 2 or pos + length > len(data):
|
||||
raise ValueError
|
||||
if marker in {0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF}:
|
||||
if length < 7:
|
||||
raise ValueError
|
||||
return (
|
||||
int.from_bytes(data[pos + 5:pos + 7], "big"),
|
||||
int.from_bytes(data[pos + 3:pos + 5], "big"),
|
||||
)
|
||||
pos += length
|
||||
raise ValueError
|
||||
if ext == ".webp":
|
||||
if len(data) < 30 or data[:4] != b"RIFF" or data[8:12] != b"WEBP":
|
||||
raise ValueError
|
||||
kind = data[12:16]
|
||||
if kind == b"VP8X":
|
||||
return (
|
||||
1 + int.from_bytes(data[24:27], "little"),
|
||||
1 + int.from_bytes(data[27:30], "little"),
|
||||
)
|
||||
if kind == b"VP8 ":
|
||||
idx = data.find(b"\x9d\x01\x2a", 20)
|
||||
if idx < 0 or idx + 7 > len(data):
|
||||
raise ValueError
|
||||
return (
|
||||
int.from_bytes(data[idx + 3:idx + 5], "little") & 0x3FFF,
|
||||
int.from_bytes(data[idx + 5:idx + 7], "little") & 0x3FFF,
|
||||
)
|
||||
if kind == b"VP8L" and data[20] == 0x2F:
|
||||
bits = int.from_bytes(data[21:25], "little")
|
||||
return (bits & 0x3FFF) + 1, ((bits >> 14) & 0x3FFF) + 1
|
||||
raise ValueError
|
||||
except (IndexError, struct.error, ValueError) as err:
|
||||
raise DecorAssetError(
|
||||
"invalid_image", "The raster image is corrupt or has the wrong type",
|
||||
) from err
|
||||
raise DecorAssetError("unsupported_image", "Unsupported image type")
|
||||
|
||||
|
||||
def _validate_dimensions(width: float, height: float) -> tuple[int, int]:
|
||||
if not math.isfinite(width) or not math.isfinite(height) or width <= 0 or height <= 0:
|
||||
raise DecorAssetError("invalid_image", "The image has no usable dimensions")
|
||||
w, h = int(round(width)), int(round(height))
|
||||
if w <= 0 or h <= 0:
|
||||
raise DecorAssetError("invalid_image", "The image has no usable dimensions")
|
||||
if w > MAX_RASTER_DIMENSION or h > MAX_RASTER_DIMENSION or w * h > MAX_RASTER_PIXELS:
|
||||
raise DecorAssetError("too_large", "Decoded image dimensions exceed the safety limit")
|
||||
return w, h
|
||||
|
||||
|
||||
def _svg_number(value: str | None) -> float | None:
|
||||
if value is None:
|
||||
return None
|
||||
match = _LENGTH.fullmatch(value)
|
||||
return float(match.group(1)) if match else None
|
||||
|
||||
|
||||
def _validate_svg_unit_interval(name: str, value: str) -> None:
|
||||
"""Reject non-finite and out-of-range opacity/gradient values."""
|
||||
raw = value.strip()
|
||||
percent = raw.endswith("%")
|
||||
try:
|
||||
number = float(raw[:-1] if percent else raw)
|
||||
except ValueError as err:
|
||||
raise DecorAssetError("invalid_image", f"The SVG {name} value is invalid") from err
|
||||
limit = 100 if percent else 1
|
||||
if not math.isfinite(number) or number < 0 or number > limit:
|
||||
raise DecorAssetError("invalid_image", f"The SVG {name} value is out of range")
|
||||
|
||||
|
||||
def _reject_svg_declarations(data: bytes) -> None:
|
||||
"""Parse with encoding-aware expat guards before building an XML tree."""
|
||||
parser = expat.ParserCreate()
|
||||
|
||||
def reject(*_args: Any) -> None:
|
||||
raise DecorAssetError(
|
||||
"invalid_image", "DTD, entities and processing instructions are forbidden",
|
||||
)
|
||||
|
||||
parser.StartDoctypeDeclHandler = reject
|
||||
parser.EntityDeclHandler = reject
|
||||
parser.ExternalEntityRefHandler = reject
|
||||
parser.ProcessingInstructionHandler = reject
|
||||
try:
|
||||
parser.Parse(data, True)
|
||||
except DecorAssetError:
|
||||
raise
|
||||
except (expat.ExpatError, UnicodeError) as err:
|
||||
raise DecorAssetError("invalid_image", "The SVG is not valid XML") from err
|
||||
|
||||
|
||||
def _validate_svg(data: bytes) -> ValidatedAsset:
|
||||
# Byte-substring checks are bypassable with UTF-16/UTF-32. Expat detects
|
||||
# the declared/input encoding first and our handlers reject declarations
|
||||
# before entity expansion can allocate an ElementTree.
|
||||
_reject_svg_declarations(data)
|
||||
try:
|
||||
root = ET.fromstring(data)
|
||||
except (ET.ParseError, UnicodeError) as err:
|
||||
raise DecorAssetError("invalid_image", "The SVG is not valid XML") from err
|
||||
if root.tag.rsplit("}", 1)[-1] != "svg":
|
||||
raise DecorAssetError("invalid_image", "Only an SVG root is accepted")
|
||||
count = attr_chars = 0
|
||||
ids: set[str] = set()
|
||||
refs: list[str] = []
|
||||
ref_graph: dict[str, set[str]] = {}
|
||||
stack: list[tuple[ET.Element, int, str | None]] = [(root, 1, None)]
|
||||
while stack:
|
||||
node, depth, inherited_owner = stack.pop()
|
||||
count += 1
|
||||
if count > MAX_SVG_ELEMENTS or depth > MAX_SVG_DEPTH:
|
||||
raise DecorAssetError("too_large", "The SVG structure exceeds the safety limit")
|
||||
if node.tag.startswith("{"):
|
||||
namespace, tag = node.tag[1:].split("}", 1)
|
||||
if namespace != _SVG_NAMESPACE:
|
||||
raise DecorAssetError("unsupported_image", "Unknown SVG namespace")
|
||||
else:
|
||||
raise DecorAssetError("unsupported_image", "The SVG namespace is required")
|
||||
if tag not in _SVG_TAGS:
|
||||
raise DecorAssetError("unsupported_image", f"Unsupported SVG element: {tag}")
|
||||
node_id = node.attrib.get("id")
|
||||
owner = node_id or inherited_owner
|
||||
if node.text and node.text.strip():
|
||||
if tag not in {"title", "desc"} or len(node.text) > 4096:
|
||||
raise DecorAssetError("unsupported_image", "Unsupported or oversized SVG text")
|
||||
for raw_name, value in node.attrib.items():
|
||||
if len(value) > MAX_SVG_ATTR_VALUE_CHARS:
|
||||
raise DecorAssetError("too_large", "An SVG attribute exceeds the safety limit")
|
||||
if raw_name.startswith("{"):
|
||||
raise DecorAssetError("unsupported_image", "Namespaced SVG attributes are unsupported")
|
||||
name = raw_name
|
||||
attr_chars += len(raw_name) + len(value)
|
||||
if attr_chars > MAX_SVG_ATTR_CHARS:
|
||||
raise DecorAssetError("too_large", "The SVG attributes exceed the safety limit")
|
||||
if name.lower().startswith("on") or name not in _SVG_ATTRS:
|
||||
raise DecorAssetError("unsupported_image", f"Unsupported SVG attribute: {name}")
|
||||
if name in _SVG_UNIT_INTERVAL_ATTRS:
|
||||
_validate_svg_unit_interval(name, value)
|
||||
low = value.strip().lower()
|
||||
if any(token in low for token in ("javascript:", "data:", "http:", "https:", "//")):
|
||||
raise DecorAssetError("invalid_image", "External SVG resources are forbidden")
|
||||
if "url(" in low:
|
||||
if not _LOCAL_REF.fullmatch(value.strip()):
|
||||
raise DecorAssetError("invalid_image", "Only local SVG references are allowed")
|
||||
ref = value.strip()[5:-1]
|
||||
refs.append(ref)
|
||||
if owner:
|
||||
ref_graph.setdefault(owner, set()).add(ref)
|
||||
if name == "href":
|
||||
if not re.fullmatch(r"#[A-Za-z_][A-Za-z0-9_.:-]*", value.strip()):
|
||||
raise DecorAssetError("invalid_image", "Only local SVG references are allowed")
|
||||
ref = value.strip()[1:]
|
||||
refs.append(ref)
|
||||
if owner:
|
||||
ref_graph.setdefault(owner, set()).add(ref)
|
||||
if name == "id":
|
||||
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_.:-]*", value) or value in ids:
|
||||
raise DecorAssetError("invalid_image", "SVG ids must be unique and well formed")
|
||||
ids.add(value)
|
||||
stack.extend((child, depth + 1, owner) for child in node)
|
||||
if any(ref not in ids for ref in refs):
|
||||
raise DecorAssetError("invalid_image", "The SVG contains an unresolved local reference")
|
||||
|
||||
visiting: set[str] = set()
|
||||
visited: set[str] = set()
|
||||
|
||||
def _visit(node_id: str) -> None:
|
||||
if node_id in visiting:
|
||||
raise DecorAssetError("invalid_image", "The SVG contains a cyclic local reference")
|
||||
if node_id in visited:
|
||||
return
|
||||
visiting.add(node_id)
|
||||
for ref in ref_graph.get(node_id, ()):
|
||||
_visit(ref)
|
||||
visiting.remove(node_id)
|
||||
visited.add(node_id)
|
||||
|
||||
for node_id in ids:
|
||||
_visit(node_id)
|
||||
view_box = root.attrib.get("viewBox")
|
||||
width = _svg_number(root.attrib.get("width"))
|
||||
height = _svg_number(root.attrib.get("height"))
|
||||
if view_box:
|
||||
try:
|
||||
parts = [float(part) for part in re.split(r"[\s,]+", view_box.strip())]
|
||||
if len(parts) != 4:
|
||||
raise ValueError
|
||||
width, height = parts[2], parts[3]
|
||||
except ValueError as err:
|
||||
raise DecorAssetError("invalid_image", "The SVG viewBox is invalid") from err
|
||||
w, h = _validate_dimensions(width or 0, height or 0)
|
||||
ET.register_namespace("", _SVG_NAMESPACE)
|
||||
canonical = ET.tostring(
|
||||
root, encoding="utf-8", xml_declaration=False, short_empty_elements=True,
|
||||
)
|
||||
_check_size(canonical)
|
||||
try:
|
||||
ET.fromstring(canonical)
|
||||
except ET.ParseError as err: # pragma: no cover - serializer invariant
|
||||
raise DecorAssetError("invalid_image", "Canonical SVG could not be reparsed") from err
|
||||
return ValidatedAsset(canonical, "image/svg+xml", ".svg", w, h)
|
||||
|
||||
|
||||
def validate_asset(
|
||||
data: bytes, filename: str, declared_mime: str | None = None,
|
||||
) -> ValidatedAsset:
|
||||
"""Validate and canonicalise one complete upload."""
|
||||
_check_size(data)
|
||||
ext = Path(filename).suffix.lower()
|
||||
if ext not in ASSET_EXTENSIONS:
|
||||
raise DecorAssetError("unsupported_image", "Use PNG, JPEG, WebP or SVG")
|
||||
expected_mime = MIME_BY_EXT[ext]
|
||||
claimed = str(declared_mime or "").split(";", 1)[0].strip().lower()
|
||||
if claimed and claimed != "application/octet-stream" and claimed != expected_mime:
|
||||
raise DecorAssetError("invalid_format", "The filename, MIME type and image bytes disagree")
|
||||
if ext == ".svg":
|
||||
return _validate_svg(data)
|
||||
width, height = _validate_dimensions(*_raster_dimensions(data, ext))
|
||||
# Home Assistant ships Pillow. Header parsing above keeps this helper
|
||||
# independently testable, while a full decode here catches valid-looking
|
||||
# but truncated/corrupt payloads before they enter the authenticated store.
|
||||
try:
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image
|
||||
|
||||
with Image.open(BytesIO(data)) as image:
|
||||
image.load()
|
||||
if image.size != (width, height):
|
||||
raise DecorAssetError("invalid_image", "Image dimensions are inconsistent")
|
||||
if getattr(image, "is_animated", False):
|
||||
raise DecorAssetError("unsupported_image", "Animated images are unsupported")
|
||||
except ImportError:
|
||||
# The Home Assistant runtime includes Pillow. Keeping the pure parser
|
||||
# usable without the full HA dependency set lets repository-local
|
||||
# security tests still exercise signatures, dimensions and SVG.
|
||||
pass
|
||||
except DecorAssetError:
|
||||
raise
|
||||
except Exception as err: # noqa: BLE001 - decoder failures are one public error
|
||||
raise DecorAssetError("invalid_image", "The raster image cannot be decoded") from err
|
||||
mime = expected_mime
|
||||
return ValidatedAsset(data, mime, EXT_BY_MIME[mime], width, height)
|
||||
|
||||
|
||||
def asset_refs(config: dict[str, Any] | None) -> dict[str, list[dict[str, str]]]:
|
||||
"""Return server-authoritative references grouped by asset id."""
|
||||
out: dict[str, list[dict[str, str]]] = {}
|
||||
for space in (config or {}).get("spaces") or []:
|
||||
if not isinstance(space, dict):
|
||||
continue
|
||||
sid = str(space.get("id") or "")
|
||||
for shape in space.get("decor") or []:
|
||||
if not isinstance(shape, dict) or shape.get("kind") != "image":
|
||||
continue
|
||||
aid = shape.get("asset_id")
|
||||
if isinstance(aid, str) and ASSET_ID_RE.fullmatch(aid):
|
||||
out.setdefault(aid, []).append({
|
||||
"space_id": sid, "decor_id": str(shape.get("id") or ""),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def asset_meta_path(root: Path, asset_id: str) -> Path:
|
||||
return root / f"{asset_id}.json"
|
||||
|
||||
|
||||
def _physical_asset_inventory(root: Path) -> list[tuple[Path, int]]:
|
||||
"""Observe regular promoted blobs once, tolerating concurrent removals."""
|
||||
try:
|
||||
if not root.is_dir():
|
||||
return []
|
||||
entries = list(root.iterdir())
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
inventory: list[tuple[Path, int]] = []
|
||||
for path in entries:
|
||||
if path.suffix not in ASSET_EXTENSIONS or not ASSET_ID_RE.fullmatch(path.stem):
|
||||
continue
|
||||
try:
|
||||
current = path.stat(follow_symlinks=False)
|
||||
except OSError:
|
||||
continue
|
||||
if stat.S_ISREG(current.st_mode):
|
||||
inventory.append((path, current.st_size))
|
||||
return sorted(inventory, key=lambda item: item[0].name)
|
||||
|
||||
|
||||
def physical_asset_blobs(root: Path) -> list[Path]:
|
||||
"""Return exact promoted blob files, independently from their sidecars."""
|
||||
return [path for path, _size in _physical_asset_inventory(root)]
|
||||
|
||||
|
||||
def physical_asset_usage(root: Path) -> tuple[int, int]:
|
||||
"""Return promoted blob count and actual bytes used for quota checks."""
|
||||
inventory = _physical_asset_inventory(root)
|
||||
return len(inventory), sum(size for _path, size in inventory)
|
||||
|
||||
|
||||
def _read_catalog_row(root: Path, path: Path) -> dict[str, Any] | None:
|
||||
"""Read one sidecar through the validation shared by list and resolve."""
|
||||
try:
|
||||
row = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(row, dict):
|
||||
return None
|
||||
aid = str(row.get("asset_id") or "")
|
||||
ext = row.get("ext")
|
||||
blob = root / f"{aid}{ext}"
|
||||
if (
|
||||
path.stem != aid
|
||||
or not ASSET_ID_RE.fullmatch(aid)
|
||||
or ext not in ASSET_EXTENSIONS
|
||||
or not blob.is_file()
|
||||
):
|
||||
return None
|
||||
return row
|
||||
except (OSError, ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def read_asset(root: Path, asset_id: str) -> dict[str, Any] | None:
|
||||
"""Read one exact catalog row without scanning unrelated sidecars."""
|
||||
if not ASSET_ID_RE.fullmatch(asset_id):
|
||||
return None
|
||||
return _read_catalog_row(root, asset_meta_path(root, asset_id))
|
||||
|
||||
|
||||
def read_catalog(root: Path) -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
if not root.is_dir():
|
||||
return rows
|
||||
for path in root.glob("*.json"):
|
||||
row = _read_catalog_row(root, path)
|
||||
if row is not None:
|
||||
rows.append(row)
|
||||
return sorted(
|
||||
rows,
|
||||
key=lambda row: (str(row.get("created_at", "")), str(row["asset_id"])),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
|
||||
def public_asset(
|
||||
row: dict[str, Any], used_by: list[dict[str, str]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
aid = str(row["asset_id"])
|
||||
ext = str(row["ext"])
|
||||
return {
|
||||
"asset_id": aid,
|
||||
"name": row.get("name", "image"),
|
||||
"mime": row.get("mime"),
|
||||
"width": row.get("width"),
|
||||
"height": row.get("height"),
|
||||
"bytes": row.get("bytes"),
|
||||
"url": f"{CONTENT_URL}/assets/_/{aid}{ext}",
|
||||
"used_by": used_by or [],
|
||||
}
|
||||
@@ -1,125 +1,111 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "56c4f193c6899f80181491944c9d6679698be4658a0ed5ba54dc08d1409538e3",
|
||||
"fingerprint": "420f1f87e66a879316f23c4dd573991c8cf2bfcf5ef013191e55e4216031f3be",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-BYuSDJH2.js",
|
||||
"houseplan-assets/houseplan-card-BarpA18i.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 293163,
|
||||
"initialViewGzipBytes": 256012,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-oWJolMYh.js",
|
||||
"houseplan-assets/de-CF711iTL.js",
|
||||
"houseplan-assets/editor-DBDu1fmS.js",
|
||||
"houseplan-assets/fr-A4-MjeBb.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-B9vxxIYH.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-2O6rGYyJ.js"
|
||||
"houseplan-assets/de-L64iNsLu.js",
|
||||
"houseplan-assets/editor-B45l6syb.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-D6vKsLjO.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BUDnRMK5.js",
|
||||
"houseplan-assets/space-deletion-05RPnoUh.js"
|
||||
],
|
||||
"lazyGzipBytes": 222109,
|
||||
"lazyGzipBytes": 160359,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-oWJolMYh.js",
|
||||
"houseplan-assets/editor-DBDu1fmS.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-B9vxxIYH.js"
|
||||
"houseplan-assets/editor-B45l6syb.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-D6vKsLjO.js",
|
||||
"houseplan-assets/space-deletion-05RPnoUh.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 166061,
|
||||
"lazyEditorGzipBytes": 131837,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-oWJolMYh.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-2O6rGYyJ.js"
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BUDnRMK5.js",
|
||||
"houseplan-assets/space-deletion-05RPnoUh.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 14241,
|
||||
"lazyOnboardingGzipBytes": 7198,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-CF711iTL.js",
|
||||
"houseplan-assets/fr-A4-MjeBb.js"
|
||||
"houseplan-assets/de-L64iNsLu.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 49090,
|
||||
"lazyLocaleGzipBytes": 21878,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-oWJolMYh.js",
|
||||
"sha256": "77bedfa35c239b51d4395faff3decd94de86ab24d16e35746ea592696b6eeba2",
|
||||
"rawBytes": 21418,
|
||||
"gzipBytes": 7283,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BYuSDJH2.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-CF711iTL.js",
|
||||
"sha256": "fc710917dd87b57c5d2bba9b636a28473373540d02348a4175fee903771fb6ec",
|
||||
"rawBytes": 82140,
|
||||
"gzipBytes": 24814,
|
||||
"path": "houseplan-assets/de-L64iNsLu.js",
|
||||
"sha256": "472ff7eb5679b16bacedce870ac7a23379c8a03ee3470b660c0c996a76f29b7e",
|
||||
"rawBytes": 71477,
|
||||
"gzipBytes": 21878,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-DBDu1fmS.js",
|
||||
"sha256": "5e321ab61ddb1ac20c3911c9e82e8ddc6a62404d5d8dbca6780db37cc6ff0c98",
|
||||
"rawBytes": 3826,
|
||||
"gzipBytes": 1585,
|
||||
"path": "houseplan-assets/editor-B45l6syb.js",
|
||||
"sha256": "2df6678b9adc8c3423d6e250b111461f7f3a75f4377a36ed72e5cbee51ce4bf2",
|
||||
"rawBytes": 3831,
|
||||
"gzipBytes": 1583,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BYuSDJH2.js"
|
||||
"houseplan-assets/houseplan-card-BarpA18i.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-A4-MjeBb.js",
|
||||
"sha256": "d247900cdb0bdd2988b3f601bb08e7182c54c2ea05918a8d8edbd6cb5fdc8a70",
|
||||
"rawBytes": 84267,
|
||||
"gzipBytes": 24276,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-BYuSDJH2.js",
|
||||
"sha256": "f39bf9406326084842e9819c4eb8591cee812635e7b78c25809a72dc8f7f5373",
|
||||
"rawBytes": 1040585,
|
||||
"gzipBytes": 292365,
|
||||
"path": "houseplan-assets/houseplan-card-BarpA18i.js",
|
||||
"sha256": "b3276d12591e3face8174420b981e9d7626ca9ab02667982ad83943d93194ef1",
|
||||
"rawBytes": 910025,
|
||||
"gzipBytes": 255830,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-CF711iTL.js",
|
||||
"houseplan-assets/editor-DBDu1fmS.js",
|
||||
"houseplan-assets/fr-A4-MjeBb.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-B9vxxIYH.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-2O6rGYyJ.js"
|
||||
"houseplan-assets/de-L64iNsLu.js",
|
||||
"houseplan-assets/editor-B45l6syb.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-D6vKsLjO.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BUDnRMK5.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-B9vxxIYH.js",
|
||||
"sha256": "c0df87b7ae1657caa84c811f8efc016c0063d5a17221e221029b00176f5dc466",
|
||||
"rawBytes": 597836,
|
||||
"gzipBytes": 157193,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-D6vKsLjO.js",
|
||||
"sha256": "8cf01ac6ca2eb3ee4f8f1191ab15eb92884ee25013356ea4e12bfffdeb0b1fd2",
|
||||
"rawBytes": 504959,
|
||||
"gzipBytes": 129700,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-oWJolMYh.js",
|
||||
"houseplan-assets/houseplan-card-BYuSDJH2.js"
|
||||
"houseplan-assets/houseplan-card-BarpA18i.js",
|
||||
"houseplan-assets/space-deletion-05RPnoUh.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-2O6rGYyJ.js",
|
||||
"sha256": "e7ce402a46b4f03687f86ae010905d4d75b5b972e7784a1e34b4fd36c463c117",
|
||||
"rawBytes": 28088,
|
||||
"gzipBytes": 6958,
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-BUDnRMK5.js",
|
||||
"sha256": "2439e9cdf2c7873b737d46261a8f489c398c5434bc1fecd71a481ca80519443d",
|
||||
"rawBytes": 26356,
|
||||
"gzipBytes": 6644,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-oWJolMYh.js",
|
||||
"houseplan-assets/houseplan-card-BYuSDJH2.js"
|
||||
"houseplan-assets/houseplan-card-BarpA18i.js",
|
||||
"houseplan-assets/space-deletion-05RPnoUh.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/space-deletion-05RPnoUh.js",
|
||||
"sha256": "2cf11df1b1bcd463ef90c1089b35ae89be7ce4d92370bb6a9ee3e94987a3d65f",
|
||||
"rawBytes": 911,
|
||||
"gzipBytes": 554,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "cf7f4bd0b7bd554d86d54119c7367b553e9f5d8272ee2e95b48469c169e25928",
|
||||
"rawBytes": 1183,
|
||||
"gzipBytes": 798,
|
||||
"sha256": "aa89ff5b93e00716f50c19cbb8efd81900451bca43220ddf3a74d9de7bf6a57e",
|
||||
"rawBytes": 190,
|
||||
"gzipBytes": 182,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BYuSDJH2.js"
|
||||
"houseplan-assets/houseplan-card-BarpA18i.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="420f1f87e66a879316f23c4dd573991c8cf2bfcf5ef013191e55e4216031f3be";import{c as e,l as o,t,d2 as s,A as a,d3 as i,d4 as l,d5 as n,E as r,b as c}from"./houseplan-card-BarpA18i.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>f[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(h.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${d(h.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
|
||||
@@ -1,14 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="56c4f193c6899f80181491944c9d6679698be4658a0ed5ba54dc08d1409538e3";import{k as e,l as o,t,dm as s,A as a,dn as i,dp as l,dq as n,E as r,c}from"./houseplan-card-BYuSDJH2.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(f.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${d(f.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
|
||||