Author SHA1 Message Date
Panfilov Konstantin 847c642ca6 preland#21: закрепить три боевых .ru и отключение остальных .online 2026-10-25 09:24:40 +03:00
Panfilov Konstantin a1aa25671f preland#21: готовые доменные SVG Zoomika, Zoomila и ZoomPixel 2026-10-25 09:09:18 +03:00
Panfilov Konstantin 965eb2e6a5 fix(utm): заменить публичную chob на banch (#327)
deploy-preland / deploy zoomstage.online (push) Successful in 42s
2026-10-25 05:09:55 +03:00
Panfilov Konstantin 08febba368 api#326 Сохранить прежний random и чужие JSON токены
deploy-preland / deploy zoomstage.online (push) Successful in 45s
2026-10-25 02:10:45 +03:00
Panfilov Konstantin 81e189ca67 api#326 Числовые UTM и новый шум каждого перехода 2026-10-25 02:05:48 +03:00
Panfilov Konstantin f30742c2fb preland#18 #19: popup ошибки загрузки и однострочный ИИ-заголовок
deploy-preland / deploy zoomstage.online (push) Successful in 55s
2026-10-24 10:59:20 +03:00
Panfilov Konstantin f4ce5afdc4 docs: snapshot verified stage consent runtime config (#17)
deploy-preland / deploy zoomstage.online (push) Successful in 43s
2026-10-24 05:05:45 +03:00
Panfilov Konstantin 89cc16eb4a feat: replace chb with consent groups and preserve runtime config (api#317, #17)
deploy-preland / deploy zoomstage.online (push) Successful in 42s
2026-10-24 04:35:44 +03:00
Panfilov Konstantin 188b78a115 fix: desktop-кнопка «Выберите файл» (api#315)
deploy-preland / deploy zoomstage.online (push) Successful in 41s
2026-10-23 23:36:34 +03:00
Panfilov Konstantin f87bef2443 fix: резервные форматы примеров и stage snapshot (#16)
deploy-preland / deploy zoomstage.online (push) Successful in 42s
2026-10-23 20:10:51 +03:00
Panfilov Konstantin 0e6611da73 fix(ui): ограничить fallback высоты первым экраном #16 2026-10-23 19:46:41 +03:00
Panfilov Konstantin 198ba573f9 fix(ui): согласовать первый экран и pending #16 2026-10-23 19:40:47 +03:00
Panfilov Konstantin 01933546a7 fix(theme): mirror bounded cookie propagation reconciliation
deploy-preland / deploy zoomstage.online (push) Successful in 39s
2026-10-21 22:05:02 +03:00
Panfilov Konstantin ad8c8eb849 build(theme): сохранять LF общих partial при export (#13) 2026-10-21 20:55:28 +03:00
Panfilov Konstantin 5ffb0f37e9 fix(theme): зеркалить canonical cookie при первом boot (#13) 2026-10-21 20:50:25 +03:00
Panfilov Konstantin c6b2b227c0 fix(theme): сохранять текущий выбор без storage (#13) 2026-10-21 20:46:23 +03:00
Panfilov Konstantin 9c06977e03 feat(theme): локальный выбор и показанная тема handoff (#13) 2026-10-21 20:42:28 +03:00
Panfilov Konstantin 3740839321 fix: совпадение видимого логотипа Preland и Tour (refs #12)
deploy-preland / deploy zoomstage.online (push) Successful in 40s
2026-10-21 17:35:07 +03:00
Panfilov Konstantin ec087ea636 feat: готовый бренд ZoomStage и минимальный доменный подвал (refs #12)
deploy-preland / deploy zoomstage.online (push) Successful in 44s
2026-10-21 17:24:14 +03:00
Panfilov Konstantin a2c3197a16 merge: сохранить историю локального Android-коммита без изменения проверенного дерева (refs #11)
deploy-preland / deploy zoomstage.online (push) Successful in 45s
2026-10-21 02:30:12 +03:00
Panfilov Konstantin 57196e32a9 docs: уточнить канонический SVG-контракт (refs #11) 2026-10-21 02:30:12 +03:00
Panfilov Konstantin f2ad7ecb4c merge: синхронизировать main с проверенным staging без старого кода (refs #11)
deploy-preland / deploy zoomstage.online (push) Successful in 45s
2026-10-21 02:27:08 +03:00
Panfilov Konstantin e9a2a0d568 fix: актуальные ресурсы Zoomzilla и проверяемый выпуск (refs #11) 2026-10-21 02:27:08 +03:00
Panfilov Konstantin 144dda6d88 fix: стабильная область кнопки при смене шрифта (api#301)
deploy-preland / deploy zoomstage.online (push) Successful in 34s
2026-10-20 20:11:13 +03:00
Panfilov Konstantin 0c9a406d68 fix: единая загрузка примеров и стабильный первый кадр (api#301)
deploy-preland / deploy zoomstage.online (push) Successful in 33s
2026-10-20 19:52:30 +03:00
Panfilov Konstantin 75ccf14889 Показывать ошибку после восстановления истории (#9)
deploy-preland / deploy zoomstage.online (push) Successful in 36s
2026-10-20 18:52:35 +03:00
Panfilov Konstantin 6a2532cb87 Обработать browser storage ошибки и показывать восстановленную форму (#9) 2026-10-20 18:43:25 +03:00
Panfilov Konstantin 4c5d3de7bc Merge remote-tracking branch 'origin/staging' into fix/issue299-resume 2026-10-20 18:33:12 +03:00
Panfilov Konstantin 5afa71ec84 Изолировать callbacks отменённой загрузки (#9) 2026-10-20 18:33:12 +03:00
Panfilov Konstantin d70d396670 Восстанавливать загрузку30мин после Reload и удерживать Back (#9) 2026-10-20 18:31:43 +03:00
Panfilov Konstantin 8274946610 chore(ci): отключить production и legacy автодеплой (refs #4) 2026-10-19 14:57:39 +03:00
Panfilov Konstantin 029cd16d46 feat: компактный Index и крупные миниатюры (refs anton/api#283)
deploy-preland / deploy barbariskatest (push) Failing after 1m36s
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 38s
deploy-preland / deploy zoompixel.ru (push) Successful in 37s
2026-10-19 14:38:31 +03:00
Panfilov Konstantin 3319499d5e fix: стабильные размеры Index при скрытии тулбаров
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 33s
deploy-preland / deploy zoompixel.ru (push) Successful in 34s
deploy-preland / deploy barbariskatest (push) Failing after 1m47s
2026-10-19 13:48:44 +03:00
Panfilov Konstantin 953c7dd8f2 fix(upload): кликабельная мобильная кнопка
deploy-preland / deploy barbariskatest (push) Successful in 57s
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 37s
deploy-preland / deploy zoompixel.ru (push) Successful in 34s
2026-10-19 12:26:21 +03:00
Panfilov Konstantin 21f8fbb6a4 feat(upload): фото и файлы отдельными путями на Android 2026-10-19 12:20:22 +03:00
Panfilov Konstantin 3ad7be0db3 feat(upload): фото и файлы отдельными путями на Android 2026-10-19 12:19:33 +03:00
Panfilov Konstantin 19547d1797 fix(ci): deploy only changed legacy application files
deploy-preland / deploy barbariskatest (push) Successful in 32s
deploy-preland / deploy zoompixel.online (push) Successful in 37s
deploy-preland / deploy zoompixel.ru (push) Successful in 33s
deploy-preland / deploy zoomstage.online (push) Skipped
2026-10-18 01:04:06 +03:00
Panfilov Konstantin 56456bb0cb fix(ci): tolerate existing legacy FTP directories
deploy-preland / deploy zoompixel.online (push) Successful in 35s
deploy-preland / deploy zoompixel.ru (push) Successful in 52s
deploy-preland / deploy barbariskatest (push) Failing after 1m49s
deploy-preland / deploy zoomstage.online (push) Skipped
2026-10-18 00:57:20 +03:00
Panfilov Konstantin 78b24c91f8 fix(ci): skip root directory creation on legacy FTP
deploy-preland / deploy barbariskatest (push) Failing after 30s
deploy-preland / deploy zoompixel.online (push) Successful in 28s
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.ru (push) Successful in 32s
2026-10-18 00:53:41 +03:00
Panfilov Konstantin be030f25a0 fix(ci): source legacy FTPS command file
deploy-preland / deploy barbariskatest (push) Failing after 35s
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 31s
deploy-preland / deploy zoompixel.ru (push) Successful in 31s
2026-10-18 00:50:00 +03:00
Panfilov Konstantin 498e2df7b9 fix(ci): use FTPS client for legacy assets
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.ru (push) Successful in 29s
deploy-preland / deploy barbariskatest (push) Failing after 30s
deploy-preland / deploy zoompixel.online (push) Successful in 33s
2026-10-18 00:46:02 +03:00
Panfilov Konstantin 97fe9646b7 fix(ci): verify legacy FTP retry payloads
deploy-preland / deploy zoompixel.ru (push) Successful in 28s
deploy-preland / deploy barbariskatest (push) Failing after 44s
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 31s
2026-10-18 00:40:02 +03:00
Panfilov Konstantin d646f98aa7 fix(ci): upload legacy files independently
deploy-preland / deploy barbariskatest (push) Failing after 45s
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 33s
deploy-preland / deploy zoompixel.ru (push) Successful in 31s
2026-10-18 00:34:31 +03:00
Panfilov Konstantin 35b6a334ba fix(ci): clone legacy release without node checkout
deploy-preland / deploy zoompixel.online (push) Successful in 32s
deploy-preland / deploy zoompixel.ru (push) Successful in 32s
deploy-preland / deploy barbariskatest (push) Failing after 43s
deploy-preland / deploy zoomstage.online (push) Skipped
2026-10-18 00:28:31 +03:00
Panfilov Konstantin a0dee79b22 fix(ci): run legacy release on production runner
deploy-preland / deploy zoompixel.online (push) Successful in 35s
deploy-preland / deploy zoompixel.ru (push) Successful in 33s
deploy-preland / deploy barbariskatest (push) Failing after 32s
deploy-preland / deploy zoomstage.online (push) Skipped
2026-10-18 00:19:05 +03:00
Panfilov Konstantin 730042385d fix(ci): use Alpine php83 for host config
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 34s
deploy-preland / deploy zoompixel.ru (push) Successful in 31s
deploy-preland / deploy barbariskatest (push) Canceled after 9m51s
2026-10-18 00:08:46 +03:00
Panfilov Konstantin a86978a499 chore(config): snapshot stage focus defaults
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 31s
deploy-preland / deploy zoompixel.ru (push) Successful in 37s
deploy-preland / deploy barbariskatest (push) Failing after 5m1s
2026-10-17 20:51:44 +03:00
Panfilov Konstantin ac4cc21361 fix(config): enable explicit face focus 2026-10-17 20:51:44 +03:00
Panfilov Konstantin f14ce88cc6 fix(deploy): preserve host preland config 2026-10-17 20:48:52 +03:00
Panfilov Konstantin 9835bb0ef8 feat(utm): propagate join focus mode 2026-10-17 20:46:33 +03:00
98 changed files with 3804 additions and 607 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
components/theme-client.php text eol=lf
components/theme-switch.php text eol=lf
components/theme-style.php text eol=lf
+88 -20
View File
@@ -23,9 +23,26 @@ jobs:
run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates git curl openssh-client sshpass php-cli run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates git curl openssh-client sshpass php-cli
- name: Checkout staging branch - name: Checkout staging branch
run: git clone --depth 1 --branch staging http://git.dira89kv.beget.tech:3000/anton/preland.git . env:
RELEASE_SHA: ${{ github.sha }}
run: |
set -eu
git clone --depth 1 --branch staging http://git.dira89kv.beget.tech:3000/anton/preland.git .
git fetch --depth 1 origin "$RELEASE_SHA"
git checkout --detach "$RELEASE_SHA"
[ "$(git rev-parse HEAD)" = "$RELEASE_SHA" ]
- name: Preserve host config and add focus if absent - name: Validate release and current stage sets before upload
run: |
set -eu
php bin/release-assets.php --check
for test in tests/check-*.php; do php "$test"; done
php bin/release-assets.php --primary
# /N/ по-прежнему принадлежит stage. Любое расхождение требует
# сначала перенести актуальный снимок в Git по задаче.
php bin/release-assets.php --stage-sets "$PRELAND_URL"
- name: Apply approved domain brand only if needed
env: env:
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }} SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }} SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }}
@@ -33,22 +50,24 @@ jobs:
SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }} SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }}
run: | run: |
set -eu set -eu
[ -n "$SFTP_HOST" ] && [ -n "$SFTP_USER" ] && [ -n "$SFTP_PASS" ] && [ -n "$SFTP_KNOWN_HOSTS" ] || { echo "не заданы секреты ZOOMPIXEL_SFTP_*"; exit 1; } status=0
php bin/release-brand.php --status "$PRELAND_URL" || status=$?
case "$status" in 0) exit 0 ;; 10) ;; *) exit "$status" ;; esac
# Только точная ранее согласованная пара заменяется по preland#12.
# Обычные выпуски с правильным брендом сюда не доходят.
umask 077 umask 077
known_hosts=$(mktemp); batch=$(mktemp); config=$(mktemp) known_hosts=$(mktemp); batch=$(mktemp); files=$(mktemp)
trap 'rm -f "$known_hosts" "$batch" "$config"' EXIT trap 'rm -f "$known_hosts" "$batch" "$files"' EXIT
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts" printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
printf '%s\n' "get -p $REMOTE_DIR/config.json $config" > "$batch" php bin/release-brand.php --files "$PRELAND_URL" > "$files"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch" revision=$(git rev-parse HEAD)
changed=0 while read -r source target; do
php bin/ensure-focus-config.php "$config" || changed=$? # Оборванный put не должен оставлять усечённый действующий SVG.
case "$changed" in printf '%s\n' "put -p $source $REMOTE_DIR/$target.upload-$revision" >> "$batch"
0) ;; printf '%s\n' "rename $REMOTE_DIR/$target.upload-$revision $REMOTE_DIR/$target" >> "$batch"
10) printf '%s\n' "put -p $config $REMOTE_DIR/config.json" > "$batch" done < "$files"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch" ;; SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
*) exit "$changed" ;; php bin/release-brand.php --status "$PRELAND_URL"
esac
- name: Upload over SFTP - name: Upload over SFTP
env: env:
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }} SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
@@ -68,7 +87,8 @@ jobs:
# Наборы `/N/` на stage — редактируемый контент. Кодовая доставка их # Наборы `/N/` на stage — редактируемый контент. Кодовая доставка их
# намеренно не перезаписывает: актуальное содержимое stage фиксируют в Git # намеренно не перезаписывает: актуальное содержимое stage фиксируют в Git
# отдельной синхронизацией по задаче. # отдельной синхронизацией по задаче.
git ls-files | grep -vE '^(\.gitea/|README\.md$|AGENTS\.md$|CLAUDE\.md$|\.gitignore$|requisites\.html$|config\.json$|[0-9]+/)' > "$files" # Положительный список: tests/bin/планы не публикуются.
php bin/release-assets.php --files | grep -vE '^(config\.json$|[0-9]+/)' > "$files"
if grep -q '[^A-Za-z0-9._/-]' "$files"; then echo "имена с недопустимыми символами:"; grep '[^A-Za-z0-9._/-]' "$files"; exit 1; fi if grep -q '[^A-Za-z0-9._/-]' "$files"; then echo "имена с недопустимыми символами:"; grep '[^A-Za-z0-9._/-]' "$files"; exit 1; fi
printf '%s\n' "-mkdir $REMOTE_DIR" > "$batch" printf '%s\n' "-mkdir $REMOTE_DIR" > "$batch"
while read -r file; do while read -r file; do
@@ -83,9 +103,36 @@ jobs:
done < "$files" done < "$files"
sort -u "$dirs" | while read -r dir; do printf '%s\n' "-mkdir $REMOTE_DIR/$dir" >> "$batch"; done sort -u "$dirs" | while read -r dir; do printf '%s\n' "-mkdir $REMOTE_DIR/$dir" >> "$batch"; done
while read -r file; do printf '%s\n' "put -p $file $REMOTE_DIR/$file" >> "$batch"; done < "$files" while read -r file; do printf '%s\n' "put -p $file $REMOTE_DIR/$file" >> "$batch"; done < "$files"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch" SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
echo "залито файлов: $(wc -l < "$files")" echo "залито файлов: $(wc -l < "$files")"
# api#326/#327: сначала совместимый PHP-код, затем точечная миграция UTM.
# chob→banch меняет только токен имени; веса/default владельца сохраняются.
# Остальные поля владельца и их JSON-типы сохраняются.
- name: Migrate numeric UTM in preserved host config
env:
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }}
SFTP_PASS: ${{ secrets.ZOOMPIXEL_SFTP_PASS }}
SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }}
run: |
set -eu
umask 077
known_hosts=$(mktemp); batch=$(mktemp); config=$(mktemp)
trap 'rm -f "$known_hosts" "$batch" "$config"' EXIT
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
printf '%s\n' "get -p $REMOTE_DIR/config.json $config" > "$batch"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
changed=0
php bin/ensure-utm-config.php "$config" || changed=$?
case "$changed" in
0) ;;
10) revision=$(git rev-parse HEAD)
printf '%s\n' "put -p $config $REMOTE_DIR/config.json.upload-$revision" "rename $REMOTE_DIR/config.json.upload-$revision $REMOTE_DIR/config.json" > "$batch"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST" ;;
*) exit "$changed" ;;
esac
- name: Check the page - name: Check the page
run: | run: |
set -eu set -eu
@@ -96,8 +143,29 @@ jobs:
printf '%s' "$page" | grep -q 'claim?key=' || { echo "переход не на claim"; exit 1; } printf '%s' "$page" | grep -q 'claim?key=' || { echo "переход не на claim"; exit 1; }
curl -fsS --max-time 30 "$PRELAND_URL/1/01_desk_1-source.avif" -o /dev/null || { echo "не опубликован первый набор примеров"; exit 1; } curl -fsS --max-time 30 "$PRELAND_URL/1/01_desk_1-source.avif" -o /dev/null || { echo "не опубликован первый набор примеров"; exit 1; }
curl -fsS --max-time 30 "$PRELAND_URL/2/01_desk_1-source.avif" -o /dev/null || { echo "не опубликован второй набор примеров"; exit 1; } curl -fsS --max-time 30 "$PRELAND_URL/2/01_desk_1-source.avif" -o /dev/null || { echo "не опубликован второй набор примеров"; exit 1; }
page2=$(curl -fsS --max-time 30 "$PRELAND_URL/?indpic=2&nc=$(date +%s)") page2=$(curl -fsS --max-time 30 "$PRELAND_URL/?indpic=abcd2efghi&nc=$(date +%s)")
printf '%s' "$page2" | grep -q '2/01_mob_1-source.avif' || { echo "indpic=2 не использует второй набор примеров"; exit 1; } printf '%s' "$page2" | grep -q '2/01_mob_1-source.avif' || { echo "indpic=2 не использует второй набор примеров"; exit 1; }
php bin/release-assets.php --remote "$PRELAND_URL"
php bin/release-brand.php --status "$PRELAND_URL"
- name: Remove previously published internal files
env:
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }}
SFTP_PASS: ${{ secrets.ZOOMPIXEL_SFTP_PASS }}
SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }}
run: |
set -eu
umask 077
known_hosts=$(mktemp); batch=$(mktemp)
trap 'rm -f "$known_hosts" "$batch"' EXIT
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
# Только точно известные служебные файлы прежнего deploy, без рекурсии.
printf '%s\n' "-rm $REMOTE_DIR/requisites.html" > "$batch"
git ls-files tests bin docs/superpowers | while read -r file; do
printf '%s\n' "-rm $REMOTE_DIR/$file" >> "$batch"
done
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
- name: Remove obsolete demo directories - name: Remove obsolete demo directories
env: env:
@@ -121,4 +189,4 @@ jobs:
done done
printf '%s\n' "-rmdir $REMOTE_DIR/images/$set" >> "$batch" printf '%s\n' "-rmdir $REMOTE_DIR/images/$set" >> "$batch"
done done
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch" SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+35
View File
@@ -1,5 +1,18 @@
# Инструкции агентов для `preland` # Инструкции агентов для `preland`
## Поручение 25.10.2026 — preland#21
Боевые цели теперь `zoompixel.ru` на основном сервере и `zoomila.ru`/`zoomika.ru`
на двух REG.RU-хостингах. Владелец явно разрешил первоначальное размещение этих трёх
целей; прежняя заморозка #4 для этой задачи снята. Единственный активный `.online` —
`zoomstage.online` (stage). `zoompixel.online`, `zoomika.online`, `zoomila.online` отключены,
каталоги убраны из рабочих FTP-папок с резервным сохранением; не включать без поручения.
На обоих REG.RU-хостингах владельцу нужен SFTP или FTP; существующие доступы описаны
в актуальном разделе README, пароли остаются в REG.RU. Доменные готовые SVG — в
`brands/`, существующий runtime-config сохранять. Обычный автодеплой остаётся только
на stage; новое поручение не включает production-автодеплой. При расхождении со старыми
разделами ниже действует это датированное решение и задача #21.
Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей — [AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md). [в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
@@ -45,3 +58,25 @@
## Актуальная маршрутизация (19.10.2026, preland#4) ## Актуальная маршрутизация (19.10.2026, preland#4)
До отдельного поручения владельца выкатывать только staging → zoomstage.online. Production zoompixel.ru/.online не обновлять. preland.anton.barbariskatest.ru больше не используется, не является целью и не должен упоминаться как требующий выпуска. CI запускает только stage. Сохранение runtime config и stage-наборов /N/ обязательно. Работа и решения — через Gitea. До отдельного поручения владельца выкатывать только staging → zoomstage.online. Production zoompixel.ru/.online не обновлять. preland.anton.barbariskatest.ru больше не используется, не является целью и не должен упоминаться как требующий выпуска. CI запускает только stage. Сохранение runtime config и stage-наборов /N/ обязательно. Работа и решения — через Gitea.
## Актуальные ресурсы (21.10.2026, preland#11)
- Git main и staging содержат одно проверенное дерево. Источники: текущая zoomzilla.ru — два логотипа Zoomzilla, четыре PDF, favicon и реквизиты; zoomstage.online — все файлы наборов /1 и /2. История Git не переписывается; известная старая заглушка запрещена в рабочей поставке.
- release/assets.json фиксирует SHA-256 всех статических ресурсов (текстовые SVG/CSS/HTML/webmanifest нормализуют CRLF→LF). До выпуска выполнить девять tests/check-*.php, php bin/release-assets.php --check, --primary и --stage-sets https://zoomstage.online. Если источник изменился, сначала синхронизировать актуальные файлы и manifest по задаче, затем повторить проверки. Не отключать guard ради выката.
- --primary читает именно текущую страницу основного сайта и проверяет её актуальные URL/реквизиты: старый сохранённый asset URL не доказывает актуальность. Новые логотипы должны пройти визуальную проверку обеих тем/шапки/подвала. Cache version в components/asset-url.php зависит от содержимого.
- По прямому выбору владельца21.10 requisites.html теперь входит в каноническую поставку. Единственное новое разрешённое изменение runtime-config — ключ requisites из release/requisites.json через bin/ensure-requisites-config.php; остальные байты полей сохраняются, повторный запуск ничего не меняет. Это явное исключение к прежнему host-only правилу реквизитов, а не разрешение заменять весь config.json. Git config.json хранит согласованный снимок; после выпуска сверять runtime в памяти, не сохранять потенциальные секреты в логи.
- /N/ по-прежнему редактируется на stage и не перезаписывается обычной кодовой доставкой; все150файлов перед выкатом сверяются с Git. Drift блокирует выпуск до импорта актуального набора. При полном переносе репозитория набор уже должен быть правильным.
- SFTP берёт положительный список из bin/release-assets.php --files: никакие tests/bin/release/plans не публикуются. Старые известные служебные файлы удаляются только по точному списку git ls-files, без рекурсивной очистки. После выпуска --remote сверяет все184ресурса.
- Production-преленды остаются замороженными по preland#4. Push main ничего не публикует; staging обновляет только zoomstage.online из точного event SHA.
## Доменный бренд и минимальный подвал (21.10.2026, preland#12)
Новое поручение владельца заменяет часть требований #11: в подвале остаются логотип и копирайт, без реквизитов и платёжных значков. Копирайт использует текущий HTTP_HOST без порта/trailing-dot, отдельно от config.host API; непроверенные Host и X-Forwarded-Host не выводятся.
Логотипы — готовые SVG-контуры, не текст на лету. Для zoomstage.online утверждено имя ZoomStage, слитно без зоны, обе темы. Источники лежат в brands/zoomstage.online/light.svg и dark.svg; release/brands.json содержит явную цель, имя и SHA. В docs/logo.svg и logo-alt.svg хранятся правильные canonical stage-копии для полного переноса. Их размеры182×47 совпадают с viewBox, чтобы загрузка SVG не меняла геометрию.
Обычный bin/release-assets.php --files исключает оба logo-alias из записи; runtime cache-version рассчитывается по реальному файлу хоста. Отдельный шаг release-brand.php --status разрешает только заранее согласованную миграцию #12:0 — оба SVG уже правильные, записи нет;10 — каждый файл имеет свой известный старый или новый SHA, можно закончить миграцию (включая частичный put);1 — неизвестный домен/хеш, остановка без подмены. --files выдаёт ровно утверждённые source→target. Иные логотипы не перезаписывать общим выкатом. Новые production-домены требуют отдельного добавления готовых пар и явного первоначального размещения; текущий stage-процесс их не активирует.
primary guard теперь сверяет PDF/favicon, а не заставляет возвращать Zoomzilla и реквизиты на преленд. Реквизиты не отображаются и больше автоматически не синхронизируются; хостовый config и его существующие значения не меняются этим поручением. Устаревшие requisites.html и patch-скрипт удалены из поставки. Статический manifest183, отдельный каталог брендов проверяется по собственным SHA. История #11 сохранена; её прежние инструкции о видимом бренде/реквизитах заменены этим датированным решением.
Перед выпуском: девять tests/check-*.php и lint, --check/--primary/--stage-sets, оба бренда шапки/подвала на desktop/mobile, перенос Host, отсутствие карт/реквизитов, холодная/медленная загрузка, реальные локальные Preland→Tour→Join/error/retry и геометрия. Production-преленды по #4 не включать; выпускать только staging→zoomstage.online. API продуктовый код не меняется.
+35
View File
@@ -1,5 +1,18 @@
# Инструкции агентов для `preland` # Инструкции агентов для `preland`
## Поручение 25.10.2026 — preland#21
Боевые цели теперь `zoompixel.ru` на основном сервере и `zoomila.ru`/`zoomika.ru`
на двух REG.RU-хостингах. Владелец явно разрешил первоначальное размещение этих трёх
целей; прежняя заморозка #4 для этой задачи снята. Единственный активный `.online` —
`zoomstage.online` (stage). `zoompixel.online`, `zoomika.online`, `zoomila.online` отключены,
каталоги убраны из рабочих FTP-папок с резервным сохранением; не включать без поручения.
На обоих REG.RU-хостингах владельцу нужен SFTP или FTP; существующие доступы описаны
в актуальном разделе README, пароли остаются в REG.RU. Доменные готовые SVG — в
`brands/`, существующий runtime-config сохранять. Обычный автодеплой остаётся только
на stage; новое поручение не включает production-автодеплой. При расхождении со старыми
разделами ниже действует это датированное решение и задача #21.
Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей — [AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md). [в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
@@ -42,3 +55,25 @@
План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на
политику и документацию не подключают MCP и не дают разрешения на публикацию или выкат. политику и документацию не подключают MCP и не дают разрешения на публикацию или выкат.
## Актуальные ресурсы (21.10.2026, preland#11)
- Git main и staging содержат одно проверенное дерево. Источники: текущая zoomzilla.ru — два логотипа Zoomzilla, четыре PDF, favicon и реквизиты; zoomstage.online — все файлы наборов /1 и /2. История Git не переписывается; известная старая заглушка запрещена в рабочей поставке.
- release/assets.json фиксирует SHA-256 всех статических ресурсов (текстовые SVG/CSS/HTML/webmanifest нормализуют CRLF→LF). До выпуска выполнить девять tests/check-*.php, php bin/release-assets.php --check, --primary и --stage-sets https://zoomstage.online. Если источник изменился, сначала синхронизировать актуальные файлы и manifest по задаче, затем повторить проверки. Не отключать guard ради выката.
- --primary читает именно текущую страницу основного сайта и проверяет её актуальные URL/реквизиты: старый сохранённый asset URL не доказывает актуальность. Новые логотипы должны пройти визуальную проверку обеих тем/шапки/подвала. Cache version в components/asset-url.php зависит от содержимого.
- По прямому выбору владельца21.10 requisites.html теперь входит в каноническую поставку. Единственное новое разрешённое изменение runtime-config — ключ requisites из release/requisites.json через bin/ensure-requisites-config.php; остальные байты полей сохраняются, повторный запуск ничего не меняет. Это явное исключение к прежнему host-only правилу реквизитов, а не разрешение заменять весь config.json. Git config.json хранит согласованный снимок; после выпуска сверять runtime в памяти, не сохранять потенциальные секреты в логи.
- /N/ по-прежнему редактируется на stage и не перезаписывается обычной кодовой доставкой; все150файлов перед выкатом сверяются с Git. Drift блокирует выпуск до импорта актуального набора. При полном переносе репозитория набор уже должен быть правильным.
- SFTP берёт положительный список из bin/release-assets.php --files: никакие tests/bin/release/plans не публикуются. Старые известные служебные файлы удаляются только по точному списку git ls-files, без рекурсивной очистки. После выпуска --remote сверяет все184ресурса.
- Production-преленды остаются замороженными по preland#4. Push main ничего не публикует; staging обновляет только zoomstage.online из точного event SHA.
## Доменный бренд и минимальный подвал (21.10.2026, preland#12)
Новое поручение владельца заменяет часть требований #11: в подвале остаются логотип и копирайт, без реквизитов и платёжных значков. Копирайт использует текущий HTTP_HOST без порта/trailing-dot, отдельно от config.host API; непроверенные Host и X-Forwarded-Host не выводятся.
Логотипы — готовые SVG-контуры, не текст на лету. Для zoomstage.online утверждено имя ZoomStage, слитно без зоны, обе темы. Источники лежат в brands/zoomstage.online/light.svg и dark.svg; release/brands.json содержит явную цель, имя и SHA. В docs/logo.svg и logo-alt.svg хранятся правильные canonical stage-копии для полного переноса. Их размеры182×47 совпадают с viewBox, чтобы загрузка SVG не меняла геометрию.
Обычный bin/release-assets.php --files исключает оба logo-alias из записи; runtime cache-version рассчитывается по реальному файлу хоста. Отдельный шаг release-brand.php --status разрешает только заранее согласованную миграцию #12:0 — оба SVG уже правильные, записи нет;10 — каждый файл имеет свой известный старый или новый SHA, можно закончить миграцию (включая частичный put);1 — неизвестный домен/хеш, остановка без подмены. --files выдаёт ровно утверждённые source→target. Иные логотипы не перезаписывать общим выкатом. Новые production-домены требуют отдельного добавления готовых пар и явного первоначального размещения; текущий stage-процесс их не активирует.
primary guard теперь сверяет PDF/favicon, а не заставляет возвращать Zoomzilla и реквизиты на преленд. Реквизиты не отображаются и больше автоматически не синхронизируются; хостовый config и его существующие значения не меняются этим поручением. Устаревшие requisites.html и patch-скрипт удалены из поставки. Статический manifest183, отдельный каталог брендов проверяется по собственным SHA. История #11 сохранена; её прежние инструкции о видимом бренде/реквизитах заменены этим датированным решением.
Перед выпуском: девять tests/check-*.php и lint, --check/--primary/--stage-sets, оба бренда шапки/подвала на desktop/mobile, перенос Host, отсутствие карт/реквизитов, холодная/медленная загрузка, реальные локальные Preland→Tour→Join/error/retry и геометрия. Production-преленды по #4 не включать; выпускать только staging→zoomstage.online. API продуктовый код не меняется.
+90 -25
View File
@@ -1,39 +1,78 @@
# Преленд zoomzilla # Преленд zoomzilla
## Боевые домены — поручение 25.10.2026, preland#21
Три боевые цели: `zoompixel.ru` на основном сервере, `zoomila.ru` и `zoomika.ru`
на двух отдельных хостингах REG.RU. Это отдельное разрешение владельца на первоначальное
размещение/обновление этих целей после заморозки #4. Единственный активный `.online` —
`zoomstage.online`, staging прелендингов. `zoompixel.online`, `zoomika.online` и
`zoomila.online` отключены по дополнительному решению владельца; их регистрация сохранена.
Рабочие FTP/SFTP-каталоги не должны содержать директории отключённых `.online`.
| Домен | Размещение | Пользователь SFTP/FTP | Каталог сайта |
|---|---|---|---|
| `zoomika.ru` | `server197.hosting.reg.ru`, `31.31.196.183` | `u3695919` | `/www/zoomika.ru` относительно домашнего каталога |
| `zoomila.ru` | `server124.hosting.reg.ru`, `37.140.192.195` | `u3695920` | `/www/zoomila.ru` относительно домашнего каталога |
| `zoompixel.ru` | основной сервер, `/srv/preland/domains/zoompixel.ru` | существующий административный доступ | отдельный контейнер `preland-zoompixel-ru-1` |
REG.RU выдаёт SFTP/SSH и FTP отдельно: SFTP использует порт 22 и пароль из блока
«Доступ в панель управления, SSH, SFTP»; FTP — порт 21 и отдельный пароль из блока
«Доступ к FTP». Пароли смотреть в кабинете REG.RU → нужный хостинг → «Доступы»;
не помещать в Git и журналы. Оба аккаунта уже созданы провайдером, сбрасывать пароли
для размещения не требуется. Полные домашние каталоги — `/var/www/<пользователь>/data`.
В `www` каждого REG.RU-аккаунта оставлен только соответствующий `.ru`. Сайты `.online`
выключены через ispmanager, их исходные файлы и установочные архивы сохранены вне webroot
в скрытом `.preland21-backup/` (каталог `site`, файлы `release.zip`, `initial-index.html`).
На основном сервере `zoompixel.online` отвечает HTTP/HTTPS 410; его контейнер удалён,
сервис исключён из Compose, исходники сохранены в `/srv/preland/backups/preland21-disabled-online/site`.
Логотипы для обеих тем — готовые SVG в `brands/<домен>/`, реестр — `release/brands.json`.
При первоначальном размещении копировать только положительный список `releaseFiles()`
и проверенные статические ресурсы (включая `/1`, `/2`), затем доменную пару в
`docs/logo.svg`/`docs/logo-alt.svg`. Служебные `bin/`, `tests/`, `.git/`, планы и
установочные архивы в webroot не входят. Архивы REG.RU хранить вне `/www`.
У новых пустых сайтов исходный config — проверенный stage-снимок; существующий
`zoompixel.ru/config.json` сохраняется байт-в-байт, старые UTM нормализуются в памяти.
Перед трафиком обязательны доверенный TLS (самоподписанный сертификат REG.RU не подходит),
HTTP→HTTPS, проверка двух тем/ширин, примеров, upload→claim→Tour→Join с UTM,
видимой ошибки и повторной попытки. Точный состав, коммиты, результаты и ограничения — #21.
Обычная автоматическая доставка по-прежнему только `staging` → `zoomstage.online`;
данное первоначальное размещение не включает production-автодеплой по push `main`.
Исторические разделы ниже описывают прежние цели; при расхождении действует этот раздел.
Одностраничный лендинг перед основным сайтом (`zoomzilla.ru`): посетитель с рекламы выбирает фото прямо на Одностраничный лендинг перед основным сайтом (`zoomzilla.ru`): посетитель с рекламы выбирает фото прямо на
преленде, файл сразу уходит в API сайта, а сам посетитель переходит на сайт — на страницу обработки этого фото. преленде, файл сразу уходит в API сайта, а сам посетитель переходит на сайт — на страницу обработки этого фото.
Варианты страницы и сайта (A/B) задаются метками в адресе. Код — обычный PHP без фреймворка и сборки; переводы — Варианты страницы и сайта (A/B) задаются метками в адресе. Код — обычный PHP без фреймворка и сборки; переводы —
маленькая заглушка `Yii::t` в `i18n.php`. маленькая заглушка `Yii::t` в `i18n.php`.
Активные production: `https://zoompixel.ru`, `https://zoompixel.online`; текущая цель выката — только `https://zoomstage.online`. Пример ссылки входа: Активные production: `https://zoompixel.ru`, `https://zoompixel.online`; текущая цель выката — только `https://zoomstage.online`. Пример ссылки входа:
`https://zoomstage.online/?indpic=1&theme=dark&degr=2&registration=after&bchoice=a&tp=1&utm_source=test&subid=CLICK1`. `https://zoomstage.online/?indpic=e9st1mm12b&theme=e9st1mm12b&degr=e9st2mm12b&reg=e9st2mm12b&banch=e9st2mm12b&tp=e9st1mm12b&utm_source=test&subid=CLICK1`.
## Как работает ## Как работает
### Метки ### Метки (api#326)
`index.php` разбирает параметры адреса по разделу `utm` из `config.json`. Для каждой настроенной метки: Из публичного значения учитывается только пятый символ: цифра1–9 выбирает вариант,0 на Preland выбирает его по прежним `weights`. Отсутствующий, короткий, массивный и невалидный ввод получает `default`. Открытые `1`, `rnd`, `dark`, старые имена `registration/bchoice/chob/chb` больше не выбирают варианты. Точная длина входной строки не ограничена; Unicode-шум не изменяет позицию управляющего символа.
- значение из адреса принимается, если оно есть в `weights`; | Метка | Числовые значения |
- `rnd` — случайное значение по весам (`weights`), так делится трафик между вариантами; |---|---|
- неизвестное значение отбрасывается, отсутствующее — заменяется на `default`. | `indpic` | номер настроенного набора1–9 (`/1/`, `/2/` и т.д.) |
| `theme` |1 dark,2 light |
| `degr` | номер настроенной группы1–9 |
| `reg` |1 before (PIN до оплаты),2 after |
| `banch` |1 без попапа банка,2 с попапом |
| `tp` | номер настроенной тарифной группы1–9 |
| `cons` | номер группы согласий1–9; существование проверяет API |
| `focus` |1 face,2 center |
Ненастроенные параметры (`utm_source`, `keyword`, `subid` и любые другие) проходят как есть. Перед переходом `config.json` хранит публичные числовые `weights/default`. Внутри PHP сохраняются прежние semantic enum. Runtime-конфиг мигрируется `bin/ensure-utm-config.php`: только согласованные UTM, с сохранением весов, default и JSON-типов соседних полей. Скрипт идемпотентен; старые focus/cons patch-скрипты больше не вызываются доставкой.
Preland всегда формирует полный resolved-набор своих восьми меток: явное допустимое значение остаётся, `rnd`
разрешается по весам, отсутствующее получает `default`. Вместе с внешними рекламными параметрами этот набор
уходит в `claim`, а затем сайт переносит его в `upscale`; ни один из восьми параметров не должен пропадать только
потому, что был назначен default.
| Метка | Что задаёт | Где | В api#327 публичное `chob` заменено на `banch`; внутренний `bchoice` сохраняется. Мигратор переименовывает текущий host-ключ токенно, сохраняя все остальные байты, включая веса и default. Одновременные `banch/chob/bchoice` блокируют patch без записи. До миграции код читает прежний host-config; сохранённые recovery-записи с `utmDigits.chob` или `redirectUtm` продолжают выбранный вариант и выпускают уже `banch`.
|---|---|---|
| `indpic` | картинка-пример на преленде | корневой каталог сета `/1/`, `/2/` и т. д. | Перед каждым переходом Preland создаёт для всех восьми меток новый шум из10 ASCII букв/цифр, с уже выбранной цифрой в пятой позиции. Retry и recovery меняют только шум; вариант повторно не выбирается. Выбранные цифры сохраняются в upload record. Старые записи IndexedDB адаптируются только на пути recovery, без публичных alias старого URL.
| `theme` | `light` / `dark` | преленд и сайт |
| `degr` | пресет ухудшения превью «До» | сайт (#42) | Чужие параметры (`utm_source`, `keyword`, `subid` и любые другие) сохраняют значения после обычного URL-кодирования. Технические `key/id/stage/shown_theme` не маскируются; внешний `key` не может подменить одноразовый ключ загрузки. `shown_theme` берётся из фактически показанной темы непосредственно при переходе.
| `registration` | `before` — PIN до оплаты, `after` — после | сайт, страница результата |
| `bchoice` | попап выбора банка (`a` — показать) | сайт, оплата (#44) |
| `chb` | галочки согласия на странице оплаты (`no` — не обязательны) | сайт, оплата |
| `tp` | группа тарифов | сайт, cpanel «Группы тарифов (tp)» |
| `focus` | первоначальная область лупы Join: `center`/`face` | сайт, cpanel после `tp` |
### Загрузка и переход на сайт ### Загрузка и переход на сайт
@@ -76,7 +115,7 @@ Preland всегда формирует полный resolved-набор сво
| `requisites` | нет | реквизиты в подвале — список строк, каждая отдельным абзацем (текст экранируется). Важнее `requisites.html`; без обоих — заглушка «ИП Иванов» | | `requisites` | нет | реквизиты в подвале — список строк, каждая отдельным абзацем (текст экранируется). Важнее `requisites.html`; без обоих — заглушка «ИП Иванов» |
| `assetsUriPrefix` | нет | откуда брать статику. Пусто — из своего каталога; если на хостинге нет каталога `images`, статика берётся с сайта (`https://<host>`) | | `assetsUriPrefix` | нет | откуда брать статику. Пусто — из своего каталога; если на хостинге нет каталога `images`, статика берётся с сайта (`https://<host>`) |
| `fontsUrl`, `faviconUrl`, `spritePath` | нет | адреса шрифтов, иконок и SVG-спрайта, если они не в `assetsUriPrefix` | | `fontsUrl`, `faviconUrl`, `spritePath` | нет | адреса шрифтов, иконок и SVG-спрайта, если они не в `assetsUriPrefix` |
| `logoAltExt` | нет | расширение второго логотипа (`svg` по умолчанию) | | `logoAltExt` | устарело | После preland#11 обе темы используют проверенные SVG Zoomzilla; расширение не меняется runtime-конфигом. |
| `title` | нет | заголовок страницы («Улучшение изображения») | | `title` | нет | заголовок страницы («Улучшение изображения») |
| `default_language` | нет | сейчас не действует: язык всегда `ru` | | `default_language` | нет | сейчас не действует: язык всегда `ru` |
@@ -99,7 +138,7 @@ Preland хранит наборы ровно как Index, поэтому их
N/05_... N/05_...
``` ```
`indpic=N` выбирает корневой каталог `/N/`. Набор копируется с Index в корень `indpic=abcdNefghi` выбирает корневой каталог `/N/`. Набор копируется с Index в корень
домена целиком: без переименования, вложения в `images/` и промежуточных домена целиком: без переименования, вложения в `images/` и промежуточных
каталогов. Устаревшие `images/demo*` не являются частью поставки. каталогов. Устаревшие `images/demo*` не являются частью поставки.
@@ -145,8 +184,8 @@ N/05_...
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' 'https://<host>/api/images/public/claim?key=x' curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' 'https://<host>/api/images/public/claim?key=x'
# 302 на https://<host>/ — точка передачи на сайте есть # 302 на https://<host>/ — точка передачи на сайте есть
``` ```
В браузере: открыть `https://<домен>/?indpic=1&theme=dark&subid=TEST`, выбрать фото — адрес сменится на В браузере: открыть `https://<домен>/?indpic=abcd1efghi&theme=abcd1efghi&subid=TEST`, выбрать фото — адрес сменится на
`https://<host>/public/upscale?id=…&indpic=1&theme=dark&subid=TEST…` (без `key=`), пойдёт обработка. `https://<host>/public/upscale?id=…&indpic=<новый шум с1>&theme=<новый шум с1>&subid=TEST…` (без `key=`), пойдёт обработка.
Это настоящая загрузка и обработка на сайте. Это настоящая загрузка и обработка на сайте.
## Автодеплой (Gitea Actions) ## Автодеплой (Gitea Actions)
@@ -173,3 +212,29 @@ Workflow `.gitea/workflows/deploy.yml` содержит единственную
## Android: фото и файлы (2026-10-19) ## Android: фото и файлы (2026-10-19)
Основная кнопка выбирает image/*; отдельная ссылка «Выбрать файлы (PDF и др.)» открывает общий picker без ограничения image/*. PDF в подписи скрывается по runtime API-policy. iOS/iPad/Desktop сохраняют прежний общий выбор. +много сохраняется в одной компактной mobile-строке. components/browser-upload.php и CSS upload-picker-links.php — лёгкие inline-копии соответствующих компонентов API: отдельный PHP-хост не использует Yii AssetManager, новый внешний запрос не добавляется. При изменении общего контракта синхронизировать обе копии; API mobile-picker.cjs покрывает обе реальные страницы. Android byte-copy FileProvider сохраняет исходные байты/имя/MIME и ограничен max_file_size_bytes; до ответа policy используется консервативный15MB предел копирования. UTM/client_key/claim/config.json не меняются. Основная кнопка выбирает image/*; отдельная ссылка «Выбрать файлы (PDF и др.)» открывает общий picker без ограничения image/*. PDF в подписи скрывается по runtime API-policy. iOS/iPad/Desktop сохраняют прежний общий выбор. +много сохраняется в одной компактной mobile-строке. components/browser-upload.php и CSS upload-picker-links.php — лёгкие inline-копии соответствующих компонентов API: отдельный PHP-хост не использует Yii AssetManager, новый внешний запрос не добавляется. При изменении общего контракта синхронизировать обе копии; API mobile-picker.cjs покрывает обе реальные страницы. Android byte-copy FileProvider сохраняет исходные байты/имя/MIME и ограничен max_file_size_bytes; до ответа policy используется консервативный15MB предел копирования. UTM/client_key/claim/config.json не меняются.
## Актуальные ресурсы (21.10.2026, preland#11)
- Git main и staging содержат одно проверенное дерево. Источники: текущая zoomzilla.ru — два логотипа Zoomzilla, четыре PDF, favicon и реквизиты; zoomstage.online — все файлы наборов /1 и /2. История Git не переписывается; известная старая заглушка запрещена в рабочей поставке.
- release/assets.json фиксирует SHA-256 всех статических ресурсов (текстовые SVG/CSS/HTML/webmanifest нормализуют CRLF→LF). До выпуска выполнить девять tests/check-*.php, php bin/release-assets.php --check, --primary и --stage-sets https://zoomstage.online. Если источник изменился, сначала синхронизировать актуальные файлы и manifest по задаче, затем повторить проверки. Не отключать guard ради выката.
- --primary читает именно текущую страницу основного сайта и проверяет её актуальные URL/реквизиты: старый сохранённый asset URL не доказывает актуальность. Новые логотипы должны пройти визуальную проверку обеих тем/шапки/подвала. Cache version в components/asset-url.php зависит от содержимого.
- По прямому выбору владельца21.10 requisites.html теперь входит в каноническую поставку. Единственное новое разрешённое изменение runtime-config — ключ requisites из release/requisites.json через bin/ensure-requisites-config.php; остальные байты полей сохраняются, повторный запуск ничего не меняет. Это явное исключение к прежнему host-only правилу реквизитов, а не разрешение заменять весь config.json. Git config.json хранит согласованный снимок; после выпуска сверять runtime в памяти, не сохранять потенциальные секреты в логи.
- /N/ по-прежнему редактируется на stage и не перезаписывается обычной кодовой доставкой; все150файлов перед выкатом сверяются с Git. Drift блокирует выпуск до импорта актуального набора. При полном переносе репозитория набор уже должен быть правильным.
- SFTP берёт положительный список из bin/release-assets.php --files: никакие tests/bin/release/plans не публикуются. Старые известные служебные файлы удаляются только по точному списку git ls-files, без рекурсивной очистки. После выпуска --remote сверяет все184ресурса.
- Production-преленды остаются замороженными по preland#4. Push main ничего не публикует; staging обновляет только zoomstage.online из точного event SHA.
Повторная проверка выпуска: php bin/release-assets.php --stage-requisites https://zoomstage.online проверяет именно runtime config; успешная загрузка requisites.html сама по себе недостаточна. Все обязательные SFTP-команды выполняются через batch -b и прекращают выкат при ошибке put.
## Доменный бренд и минимальный подвал (21.10.2026, preland#12)
Новое поручение владельца заменяет часть требований #11: в подвале остаются логотип и копирайт, без реквизитов и платёжных значков. Копирайт использует текущий HTTP_HOST без порта/trailing-dot, отдельно от config.host API; непроверенные Host и X-Forwarded-Host не выводятся.
Логотипы — готовые SVG-контуры, не текст на лету. Для zoomstage.online утверждено имя ZoomStage, слитно без зоны, обе темы. Источники лежат в brands/zoomstage.online/light.svg и dark.svg; release/brands.json содержит явную цель, имя и SHA. В docs/logo.svg и logo-alt.svg хранятся правильные canonical stage-копии для полного переноса. Их размеры182×47 совпадают с viewBox, чтобы загрузка SVG не меняла геометрию.
Обычный bin/release-assets.php --files исключает оба logo-alias из записи; runtime cache-version рассчитывается по реальному файлу хоста. Отдельный шаг release-brand.php --status разрешает только заранее согласованную миграцию #12:0 — оба SVG уже правильные, записи нет;10 — каждый файл имеет свой известный старый или новый SHA, можно закончить миграцию (включая частичный put);1 — неизвестный домен/хеш, остановка без подмены. --files выдаёт ровно утверждённые source→target. Иные логотипы не перезаписывать общим выкатом. Новые production-домены требуют отдельного добавления готовых пар и явного первоначального размещения; текущий stage-процесс их не активирует.
primary guard теперь сверяет PDF/favicon, а не заставляет возвращать Zoomzilla и реквизиты на преленд. Реквизиты не отображаются и больше автоматически не синхронизируются; хостовый config и его существующие значения не меняются этим поручением. Устаревшие requisites.html и patch-скрипт удалены из поставки. Статический manifest183, отдельный каталог брендов проверяется по собственным SHA. История #11 сохранена; её прежние инструкции о видимом бренде/реквизитах заменены этим датированным решением.
Перед выпуском: девять tests/check-*.php и lint, --check/--primary/--stage-sets, оба бренда шапки/подвала на desktop/mobile, перенос Host, отсутствие карт/реквизитов, холодная/медленная загрузка, реальные локальные Preland→Tour→Join/error/retry и геометрия. Production-преленды по #4 не включать; выпускать только staging→zoomstage.online. API продуктовый код не меняется.
Дополнение приёмки #12: tests/e2e/brand-footer.cjs проверяет именно видимый SVG по центру шапки, а не только headerbox. PUBLISHED=1 использует zoomstage.online (или PRELAND_URL); без него — изолированный pre301.localhost QA proxy. Запускать в имеющемся Docker Playwright с /candidate=репозиторий,/qa=артефакты; POST/PUT блокируются. Пропущенное при compact CSS правило Bootstrap navbar>.container восстановлено во всех3CSS: API уже имеет его из полного Bootstrap. Это устраняет переход logo y0→13.5px, который рамочный тест не замечал.
+27
View File
@@ -0,0 +1,27 @@
<?php
/** Готовые доменные SVG, без генерации логотипа при запросе страницы. */
declare(strict_types=1);
function prelandBrand(string $root, string $domain): array
{
$catalog = json_decode(file_get_contents($root . '/release/brands.json'), true, 512, JSON_THROW_ON_ERROR);
if (!isset($catalog[$domain])) throw new RuntimeException('Для домена не утверждён бренд: ' . $domain);
$brand = $catalog[$domain];
if (array_keys($brand['logos']) !== ['docs/logo.svg', 'docs/logo-alt.svg']) throw new RuntimeException('Нужна полная пара SVG');
foreach ($brand['logos'] as $target => $record) {
if (!preg_match('~^brands/' . preg_quote($domain, '~') . '/(?:light|dark)\.svg$~D', $record['file'])) throw new RuntimeException('Некорректный путь бренда');
$body = str_replace("\r\n", "\n", file_get_contents($root . '/' . $record['file']));
if (hash('sha256', $body) !== $record['sha256'] || str_contains($body, '<text') || !str_contains($body, '<title>' . $brand['label'] . '</title>')) throw new RuntimeException('Не совпал утверждённый готовый SVG: ' . $target);
}
return $brand;
}
function prelandBrandState(array $actual, array $brand): int
{
$expected = array_map(static fn(array $record): string => $record['sha256'], $brand['logos']);
if ($actual === $expected) return 0;
if (array_keys($actual) !== array_keys($expected)) return 1;
foreach ($expected as $path => $hash) {
if ($actual[$path] !== $hash && $actual[$path] !== ($brand['replace_from'][$path] ?? null)) return 1;
}
return 10;
}
+36
View File
@@ -0,0 +1,36 @@
<?php
/** preland#17: миграция только utm.chb → utm.cons; остальные значения владельца сохраняются. */
declare(strict_types=1);
if ($argc !== 2) {
fwrite(STDERR, "Использование: php bin/ensure-cons-config.php /path/to/config.json\n");
exit(2);
}
try {
$path = $argv[1];
$raw = @file_get_contents($path);
if ($raw === false) throw new RuntimeException('Не удалось прочитать config.json.');
// Object decode сохраняет типы соседних {} и объектов с числовыми ключами.
$config = json_decode($raw, false, 512, JSON_THROW_ON_ERROR);
if (!$config instanceof stdClass || !($config->utm ?? null) instanceof stdClass) throw new RuntimeException('В config.json отсутствует объект utm.');
$hasCons = property_exists($config->utm, 'cons');
$cons = $hasCons ? $config->utm->cons : (object) ['weights' => (object) ['1' => 100, '2' => 0], 'default' => '2'];
if (!$cons instanceof stdClass || !(is_array($cons->weights ?? null) || ($cons->weights ?? null) instanceof stdClass)
|| !(is_string($cons->default ?? null) || is_int($cons->default ?? null))
|| !preg_match('/^[1-9][0-9]*$/D', (string) $cons->default)
|| (float) $cons->default > 2147483647) {
throw new RuntimeException('Существующий utm.cons некорректен; автоматическая перезапись запрещена.');
}
if (!property_exists($config->utm, 'chb') && $hasCons) {
fwrite(STDOUT, "cons уже настроен: config не изменён.\n");
exit(0);
}
unset($config->utm->chb);
$config->utm->cons = $cons;
$encoded = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n";
if (file_put_contents($path, $encoded, LOCK_EX) === false) throw new RuntimeException('Не удалось записать config.json.');
fwrite(STDOUT, "Мигрированы только utm.chb/cons; остальные поля и существующие настройки cons сохранены.\n");
exit(10);
} catch (Throwable $error) {
fwrite(STDERR, $error->getMessage() . "\n");
exit(2);
}
+96
View File
@@ -0,0 +1,96 @@
<?php
/** api#326: заменяет только согласованные JSON-токены, сохраняя прочие байты. */
declare(strict_types=1);
/** Карта диапазонов исходного JSON; числовые токены никогда не сериализуются. */
function utmConfigNode(string $raw, int &$offset): array
{
$length=strlen($raw);
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
$start=$offset; $char=$raw[$offset] ?? '';
if ($char==='"') {
$offset++;
while ($offset<$length) {
if ($raw[$offset]==='\\') { $offset+=2; continue; }
if ($raw[$offset++]==='"') break;
}
return ['start'=>$start,'end'=>$offset,'type'=>'string','value'=>json_decode(substr($raw,$start,$offset-$start),true,512,JSON_THROW_ON_ERROR)];
}
if ($char==='{' || $char==='[') {
$object=$char==='{'; $close=$object ? '}' : ']'; $offset++; $properties=[];
while (true) {
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
if (($raw[$offset] ?? '')===$close) { $offset++; break; }
if ($object) {
$key=utmConfigNode($raw,$offset);
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
$offset++; // Двоеточие уже проверено полным JSON validation.
$value=utmConfigNode($raw,$offset);
$properties[]=['key'=>$key,'value'=>$value];
} else { utmConfigNode($raw,$offset); }
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
if (($raw[$offset] ?? '')===',') { $offset++; continue; }
$offset++; break;
}
return ['start'=>$start,'end'=>$offset,'type'=>$object ? 'object' : 'array','properties'=>$properties];
}
while ($offset<$length && !str_contains(",]} \t\r\n",$raw[$offset])) $offset++;
return ['start'=>$start,'end'=>$offset,'type'=>'scalar'];
}
/** Выбирает единственное свойство; неоднозначные управляющие поля не мигрирует. */
function utmConfigProperty(array $node, string $key): ?array
{
$found=null;
foreach ($node['properties'] ?? [] as $property) {
if ($property['key']['value']!==$key) continue;
if ($found!==null) throw new RuntimeException('Повторное управляющее поле: '.$key);
$found=$property;
}
return $found;
}
if ($argc!==2) { fwrite(STDERR,"Использование: php bin/ensure-utm-config.php config.json\n"); exit(2); }
try {
$path=$argv[1]; $raw=file_get_contents($path);
// Валидация всей грамматики без последующей сериализации decoded данных.
json_decode($raw,false,512,JSON_THROW_ON_ERROR|JSON_BIGINT_AS_STRING);
$offset=0; $root=utmConfigNode($raw,$offset);
$utm=utmConfigProperty($root,'utm')['value'] ?? null;
if (($root['type'] ?? null)!=='object' || ($utm['type'] ?? null)!=='object') throw new RuntimeException('Отсутствует объект utm');
$replacements=[];
$replace=static function(array $node,string $value) use (&$replacements): void {
$replacements[]=['start'=>$node['start'],'end'=>$node['end'],'text'=>json_encode($value,JSON_THROW_ON_ERROR)];
};
foreach (['theme'=>['dark'=>'1','light'=>'2'],'reg'=>['before'=>'1','after'=>'2'],'banch'=>['b'=>'1','a'=>'2'],'focus'=>['face'=>'1','center'=>'2']] as $key=>$map) {
$property=utmConfigProperty($utm,$key);
$aliases=['reg'=>['registration'],'banch'=>['chob','bchoice']][$key] ?? [];
foreach ($aliases as $old) {
$legacy=utmConfigProperty($utm,$old);
if ($property && $legacy) throw new RuntimeException('Конфликт старого и нового UTM: '.$key);
if ($legacy) { $replace($legacy['key'],$key); $property=$legacy; }
}
if (!$property) continue;
$setting=$property['value'];
$weights=utmConfigProperty($setting,'weights')['value'] ?? null;
$default=utmConfigProperty($setting,'default')['value'] ?? null;
if (($setting['type'] ?? null)!=='object' || ($weights['type'] ?? null)!=='object' || !$default) throw new RuntimeException('Некорректный UTM: '.$key);
$seen=[];
foreach ($weights['properties'] as $variant) {
$name=$variant['key']['value']; $digit=$map[$name] ?? $name;
if (!in_array($digit,['1','2'],true) || isset($seen[$digit])) throw new RuntimeException('Неоднозначный вариант UTM: '.$key);
$seen[$digit]=true;
if ($digit!==$name) $replace($variant['key'],$digit);
}
$value=json_decode(substr($raw,$default['start'],$default['end']-$default['start']),false,512,JSON_THROW_ON_ERROR);
if (!(is_string($value)||is_int($value))) throw new RuntimeException('Некорректный default UTM: '.$key);
$digit=$map[$value] ?? $value;
if (!in_array((string)$digit,['1','2'],true)) throw new RuntimeException('Некорректный default UTM: '.$key);
if ($digit!==$value) $replace($default,$digit);
}
if (!$replacements) { echo "Числовые UTM уже настроены; config не изменён.\n"; exit(0); }
usort($replacements,static fn(array $a,array $b): int=>$b['start']<=>$a['start']);
foreach ($replacements as $replacement) $raw=substr_replace($raw,$replacement['text'],$replacement['start'],$replacement['end']-$replacement['start']);
if (file_put_contents($path,$raw,LOCK_EX)===false) throw new RuntimeException('Не удалось записать config');
echo "Мигрированы только согласованные имена и enum UTM; остальные байты сохранены.\n"; exit(10);
} catch(Throwable $error) { fwrite(STDERR,$error->getMessage()."\n"); exit(2); }
+130
View File
@@ -0,0 +1,130 @@
<?php
/** preland#11: проверенный состав ресурсов; исполняется до SFTP. */
declare(strict_types=1);
require_once __DIR__ . '/brand-contract.php';
function releaseAssetBytes(string $path, string $bytes): string
{
return preg_match('/\.(svg|css|html|webmanifest)$/', $path) ? str_replace("\r\n", "\n", $bytes) : $bytes;
}
function releaseStaticFiles(string $root): array
{
$files = [];
foreach (scandir($root) as $name) {
if (in_array($name, ['docs', 'favicon', 'fonts', 'images'], true) || ctype_digit($name)) {
$directory = $root . '/' . $name;
if (!is_dir($directory)) continue;
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS));
foreach ($iterator as $entry) {
$path = str_replace('\\', '/', substr($entry->getPathname(), strlen($root) + 1));
if ($name === 'docs' && !preg_match('~^docs/[^/]+\.(svg|pdf)$~', $path)) continue;
if ($entry->isFile()) $files[] = $path;
}
} elseif (preg_match('/\.css$/', $name)) {
$files[] = $name;
}
}
sort($files, SORT_STRING);
return $files;
}
function validateReleaseAssets(string $root): array
{
$manifest = json_decode((string) file_get_contents($root . '/release/assets.json'), true, 512, JSON_THROW_ON_ERROR);
$files = $manifest['files'] ?? [];
$brands = json_decode(file_get_contents($root . '/release/brands.json'), true, 512, JSON_THROW_ON_ERROR);
foreach (array_keys($brands) as $domain) prelandBrand($root, $domain);
$required = ['docs/logo.svg', 'docs/logo-alt.svg', 'docs/privacy_policy.pdf', 'docs/public_offer.pdf', 'docs/tariffs.pdf', 'docs/user_agreement.pdf'];
foreach ($required as $path) {
if (!isset($files[$path])) throw new RuntimeException('В manifest отсутствует обязательный ресурс ' . $path);
}
$names = array_keys($files); sort($names, SORT_STRING);
if ($names !== releaseStaticFiles($root)) throw new RuntimeException('Состав статических ресурсов отличается от manifest; синхронизируйте актуальный набор');
foreach ($files as $path => $record) {
if (!preg_match('~^[A-Za-z0-9._/-]+$~', $path) || str_contains($path, '..')) throw new RuntimeException('Недопустимый путь ресурса');
$bytes = releaseAssetBytes($path, (string) file_get_contents($root . '/' . $path));
$blob = sha1('blob ' . strlen($bytes) . chr(0) . $bytes);
if ($blob === '91267982e1ad373b6ebd5fb2b761b34fe2a0c2a5' || preg_match('/WBE\s*70/i', $bytes)) {
throw new RuntimeException('Запрещённая заглушка бренда: ' . $path);
}
if (($record['sha256'] ?? '') !== hash('sha256', $bytes)) throw new RuntimeException('Ресурс не соответствует согласованному снимку: ' . $path);
}
return $files;
}
function releaseFiles(string $root): array
{
$files = array_values(array_filter(releaseStaticFiles($root), static fn(string $path): bool => !preg_match('~^(?:[0-9]+/|docs/logo(?:-alt)?\.svg$)~', $path)));
foreach (['.htaccess', 'index.php', 'i18n.php', 'template.php'] as $path) $files[] = $path;
foreach (['components', 'messages'] as $directory) {
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . '/' . $directory, FilesystemIterator::SKIP_DOTS));
foreach ($iterator as $entry) if ($entry->isFile() && $entry->getExtension() === 'php') {
$files[] = str_replace('\\', '/', substr($entry->getPathname(), strlen($root) + 1));
}
}
sort($files, SORT_STRING);
return $files;
}
function verifyRemoteReleaseAssets(array $files, string $base, bool $setsOnly): void
{
if (!preg_match('~^https://[A-Za-z0-9.-]+$~', $base)) throw new RuntimeException('Требуется HTTPS origin');
if (!$setsOnly) {
$brand = prelandBrand(dirname(__DIR__), (string) parse_url($base, PHP_URL_HOST));
foreach ($brand['logos'] as $path => $record) $files[$path]['sha256'] = $record['sha256'];
}
$count = 0;
foreach ($files as $path => $record) {
if ($setsOnly && !preg_match('~^[0-9]+/~', $path)) continue;
$context = stream_context_create(['http' => ['timeout' => 30, 'header' => "Cache-Control: no-cache\r\n"], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true]]);
$bytes = @file_get_contents($base . '/' . $path . '?release_sha=' . $record['sha256'], false, $context);
if ($bytes === false || hash('sha256', releaseAssetBytes($path, $bytes)) !== $record['sha256']) {
throw new RuntimeException('Хост отличается от Git: ' . $path . '. Сначала синхронизируйте актуальный снимок по задаче.');
}
$count++;
}
fwrite(STDERR, "OK: remote {$count} resources\n");
}
/** Сверка с текущей страницей основного сайта, а не с оставшимся старым asset URL. */
function verifyPrimaryAssets(string $root, array $files): void
{
$origin = 'https://zoomzilla.ru';
$context = stream_context_create(['http' => ['timeout' => 30, 'header' => "Cache-Control: no-cache\r\n"]]);
$html = @file_get_contents($origin . '/?theme=light&preland_asset_check=1', false, $context);
if ($html === false) throw new RuntimeException('Основной сайт недоступен для сверки ресурсов');
foreach ($files as $path => $record) {
if (str_starts_with($path, 'docs/logo')) continue;
if (!str_starts_with($path, 'docs/') && !str_starts_with($path, 'favicon/')) continue;
$name = basename($path === 'docs/logo.svg' ? 'logo-light.svg' : $path);
$pattern = '~(?:src|href)=["\']([^"\']*/' . preg_quote($name, '~') . '(?:\?[^"\']*)?)["\']~';
if (!preg_match($pattern, $html, $match)) {
// Часть размеров favicon ссылается только из manifest; общий каталог тот же.
if (!str_starts_with($path, 'favicon/') || !preg_match('~href=["\']([^"\']*/favicon\.ico)["\']~', $html, $base)) throw new RuntimeException('Текущая ссылка основного сайта не найдена: ' . $path);
$url = dirname($base[1]) . '/' . $name;
} else $url = html_entity_decode($match[1], ENT_QUOTES, 'UTF-8');
if (str_starts_with($url, '/')) $url = $origin . $url;
if (!str_starts_with($url, $origin . '/')) throw new RuntimeException('Неожиданный origin ресурса: ' . $path);
$bytes = @file_get_contents($url, false, $context);
if ($bytes === false || hash('sha256', releaseAssetBytes($path, $bytes)) !== $record['sha256']) {
throw new RuntimeException('Основной сайт обновлён: синхронизируйте ' . $path . ' в Git перед выпуском');
}
}
fwrite(STDERR, "OK: primary PDF/favicon актуальны\n");
}
if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
try {
$root = dirname(__DIR__);
$files = validateReleaseAssets($root);
$mode = $argv[1] ?? '--check';
if ($mode === '--files') echo implode("\n", releaseFiles($root)), "\n";
elseif ($mode === '--primary') verifyPrimaryAssets($root, $files);
elseif ($mode === '--remote' || $mode === '--stage-sets') verifyRemoteReleaseAssets($files, $argv[2] ?? '', $mode === '--stage-sets');
elseif ($mode === '--check') echo 'OK: release assets ', count($files), "\n";
else throw new RuntimeException('Неизвестный режим');
} catch (Throwable $error) {
fwrite(STDERR, $error->getMessage() . "\n"); exit(1);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
/** Отдельная явно согласованная миграция бренда. Обычная code-поставка SVG не пишет. */
declare(strict_types=1);
require __DIR__ . '/brand-contract.php';
try {
$mode = $argv[1] ?? ''; $url = $argv[2] ?? '';
if (!preg_match('~^https://([a-z0-9.-]+)$~D', $url, $match)) throw new RuntimeException('Требуется HTTPS origin цели');
$brand = prelandBrand(dirname(__DIR__), $match[1]);
if ($mode === '--files') {
foreach ($brand['logos'] as $target => $record) echo $record['file'], ' ', $target, "\n";
exit(0);
}
if ($mode !== '--status') throw new RuntimeException('Неизвестный режим бренда');
$actual = []; $expected = [];
foreach ($brand['logos'] as $target => $record) {
$context = stream_context_create(['http' => ['timeout' => 30, 'header' => "Cache-Control: no-cache\r\n"]]);
$body = @file_get_contents($url . '/' . $target . '?brand_check=' . $record['sha256'], false, $context);
if ($body === false) throw new RuntimeException('Не удалось прочитать бренд цели: ' . $target);
$actual[$target] = hash('sha256', str_replace("\r\n", "\n", $body));
$expected[$target] = $record['sha256'];
}
$state = prelandBrandState($actual, $brand);
if ($state === 0) { echo "OK: доменный бренд совпал; SVG не перезаписываются\n"; exit(0); }
if ($state === 10) { echo "Разрешена точная миграция бренда по preland#12\n"; exit(10); }
throw new RuntimeException('На хосте другой бренд: обычный deploy не имеет права его менять');
} catch (Throwable $error) { fwrite(STDERR, $error->getMessage() . "\n"); exit(1); }
+46
View File
@@ -0,0 +1,46 @@
<svg width="182" height="47" viewBox="0 0 182 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>Zoomika</title>
<g opacity="0.18" filter="url(#filter0_f_241_1963)">
<path d="M23.5 37C30.9558 37 37 30.9558 37 23.5C37 16.0442 30.9558 10 23.5 10C16.0442 10 10 16.0442 10 23.5C10 30.9558 16.0442 37 23.5 37Z" fill="url(#paint0_linear_241_1963)" />
</g>
<path d="M8.11627 23.5001C11.4128 16.2164 16.8442 12.5117 23.5 12.5117C30.1558 12.5117 35.5872 16.2164 38.8837 23.5001C35.5872 30.7838 30.1558 34.4885 23.5 34.4885C16.8442 34.4885 11.4128 30.7838 8.11627 23.5001Z" fill="#090812" stroke="url(#paint1_linear_241_1963)" stroke-width="2.6" stroke-linejoin="round" />
<path d="M23.5 29.7791C26.9678 29.7791 29.7791 26.9679 29.7791 23.5C29.7791 20.0322 26.9678 17.2209 23.5 17.2209C20.0322 17.2209 17.2209 20.0322 17.2209 23.5C17.2209 26.9679 20.0322 29.7791 23.5 29.7791Z" fill="url(#paint2_linear_241_1963)" />
<path d="M23.5 26.3255C25.0605 26.3255 26.3256 25.0604 26.3256 23.4999C26.3256 21.9394 25.0605 20.6743 23.5 20.6743C21.9395 20.6743 20.6744 21.9394 20.6744 23.4999C20.6744 25.0604 21.9395 26.3255 23.5 26.3255Z" fill="#090812" />
<path d="M28.8372 15.0232L29.936 17.6918L32.6046 18.7906L29.936 19.8895L28.8372 22.5581L27.7384 19.8895L25.0698 18.7906L27.7384 17.6918L28.8372 15.0232Z" fill="white" />
<path d="M14.7093 27.2673H13.7674C13.4206 27.2673 13.1395 27.5485 13.1395 27.8952V28.8371C13.1395 29.1839 13.4206 29.465 13.7674 29.465H14.7093C15.0561 29.465 15.3372 29.1839 15.3372 28.8371V27.8952C15.3372 27.5485 15.0561 27.2673 14.7093 27.2673Z" fill="#6AA7FF" />
<path d="M18.9477 29.1511H17.6919C17.2584 29.1511 16.907 29.5025 16.907 29.936V31.1918C16.907 31.6253 17.2584 31.9767 17.6919 31.9767H18.9477C19.3812 31.9767 19.7326 31.6253 19.7326 31.1918V29.936C19.7326 29.5025 19.3812 29.1511 18.9477 29.1511Z" fill="#7B5CFF" />
<path d="M23.814 30.407H21.9302C21.4101 30.407 20.9884 30.8287 20.9884 31.3488V33.2326C20.9884 33.7527 21.4101 34.1744 21.9302 34.1744H23.814C24.3341 34.1744 24.7558 33.7527 24.7558 33.2326V31.3488C24.7558 30.8287 24.3341 30.407 23.814 30.407Z" fill="#D21BCE" />
<path d="M31.3488 29.7791L36.0581 34.4884" stroke="url(#paint3_linear_241_1963)" stroke-width="2.6" stroke-linecap="round" />
<path d="M48.3163 34V33.604L56.8363 19.432H48.9163V16.72H61.8403V17.128L53.3683 31.276H61.6363V34H48.3163ZM70.4343 34.36C69.1383 34.36 68.0063 34.068 67.0383 33.484C66.0703 32.9 65.3183 32.096 64.7823 31.072C64.2543 30.04 63.9903 28.856 63.9903 27.52C63.9903 26.16 64.2623 24.968 64.8063 23.944C65.3503 22.92 66.1063 22.12 67.0743 21.544C68.0423 20.968 69.1623 20.68 70.4343 20.68C71.7383 20.68 72.8743 20.972 73.8423 21.556C74.8103 22.14 75.5623 22.948 76.0983 23.98C76.6343 25.004 76.9023 26.184 76.9023 27.52C76.9023 28.864 76.6303 30.052 76.0863 31.084C75.5503 32.108 74.7983 32.912 73.8303 33.496C72.8623 34.072 71.7303 34.36 70.4343 34.36ZM70.4343 31.648C71.5863 31.648 72.4423 31.264 73.0023 30.496C73.5623 29.728 73.8423 28.736 73.8423 27.52C73.8423 26.264 73.5583 25.264 72.9903 24.52C72.4223 23.768 71.5703 23.392 70.4343 23.392C69.6583 23.392 69.0183 23.568 68.5143 23.92C68.0183 24.264 67.6503 24.748 67.4103 25.372C67.1703 25.988 67.0503 26.704 67.0503 27.52C67.0503 28.776 67.3343 29.78 67.9023 30.532C68.4783 31.276 69.3223 31.648 70.4343 31.648ZM85.2703 34.36C83.9743 34.36 82.8423 34.068 81.8743 33.484C80.9063 32.9 80.1543 32.096 79.6183 31.072C79.0903 30.04 78.8263 28.856 78.8263 27.52C78.8263 26.16 79.0983 24.968 79.6423 23.944C80.1863 22.92 80.9423 22.12 81.9103 21.544C82.8783 20.968 83.9983 20.68 85.2703 20.68C86.5743 20.68 87.7103 20.972 88.6783 21.556C89.6463 22.14 90.3983 22.948 90.9343 23.98C91.4703 25.004 91.7383 26.184 91.7383 27.52C91.7383 28.864 91.4663 30.052 90.9223 31.084C90.3863 32.108 89.6343 32.912 88.6663 33.496C87.6983 34.072 86.5663 34.36 85.2703 34.36ZM85.2703 31.648C86.4223 31.648 87.2783 31.264 87.8383 30.496C88.3983 29.728 88.6783 28.736 88.6783 27.52C88.6783 26.264 88.3943 25.264 87.8263 24.52C87.2583 23.768 86.4063 23.392 85.2703 23.392C84.4943 23.392 83.8543 23.568 83.3503 23.92C82.8543 24.264 82.4863 24.748 82.2463 25.372C82.0063 25.988 81.8863 26.704 81.8863 27.52C81.8863 28.776 82.1703 29.78 82.7383 30.532C83.3143 31.276 84.1583 31.648 85.2703 31.648ZM109.946 34V26.128C109.946 25.24 109.726 24.548 109.286 24.052C108.846 23.548 108.25 23.296 107.498 23.296C107.034 23.296 106.618 23.404 106.25 23.62C105.882 23.828 105.59 24.14 105.374 24.556C105.158 24.964 105.05 25.452 105.05 26.02L103.766 25.264C103.758 24.368 103.958 23.58 104.366 22.9C104.782 22.212 105.342 21.676 106.046 21.292C106.75 20.908 107.534 20.716 108.398 20.716C109.83 20.716 110.926 21.148 111.686 22.012C112.454 22.868 112.838 23.996 112.838 25.396V34H109.946ZM94.3582 34V21.04H96.9142V25.06H97.2742V34H94.3582ZM102.17 34V26.164C102.17 25.26 101.95 24.556 101.51 24.052C101.07 23.548 100.47 23.296 99.7102 23.296C98.9742 23.296 98.3822 23.548 97.9342 24.052C97.4942 24.556 97.2742 25.212 97.2742 26.02L95.9782 25.156C95.9782 24.316 96.1862 23.56 96.6022 22.888C97.0182 22.216 97.5782 21.688 98.2822 21.304C98.9942 20.912 99.7902 20.716 100.67 20.716C101.638 20.716 102.446 20.924 103.094 21.34C103.75 21.748 104.238 22.308 104.558 23.02C104.886 23.724 105.05 24.52 105.05 25.408V34H102.17ZM115.0042 34L115.0042 21.04L118.0586 21.04L118.0586 34ZM116.5399 19.2006C116.0561 19.2006 115.6399 19.0389 115.2911 18.7155C114.9424 18.392 114.768 18.0025 114.768 17.5469C114.768 17.0856 114.9424 16.6933 115.2911 16.3698C115.6399 16.0464 116.0561 15.8847 116.5399 15.8847C117.0292 15.8847 117.4469 16.0464 117.7928 16.3698C118.1388 16.6933 118.3117 17.0856 118.3117 17.5469C118.3117 18.0025 118.1388 18.392 117.7928 18.7155C117.4469 19.0389 117.0292 19.2006 116.5399 19.2006ZM123.9839 29.9331L123.9754 26.2459L124.4648 26.2459L129.1223 21.04L132.6913 21.04L126.9623 27.4188L126.3295 27.4188ZM121.1995 34L121.1995 16.72L124.2539 16.72L124.2539 34ZM129.3332 34L125.1145 28.1022L127.1732 25.9506L132.9867 34ZM138.4447 34.2616C137.6234 34.2616 136.8851 34.1139 136.2298 33.8186C135.5745 33.5233 135.057 33.0859 134.6773 32.5066C134.2976 31.9272 134.1078 31.2128 134.1078 30.3634C134.1078 29.6322 134.2428 29.0275 134.5128 28.5494C134.7828 28.0713 135.1512 27.6888 135.6181 27.4019C136.085 27.115 136.6123 26.897 137.2001 26.748C137.7879 26.5989 138.3969 26.4906 139.0269 26.4231C139.7862 26.3444 140.4022 26.2727 140.8747 26.208C141.3472 26.1433 141.6917 26.042 141.9083 25.9042C142.1248 25.7664 142.2331 25.5513 142.2331 25.2588L142.2331 25.2081C142.2331 24.5725 142.0447 24.0803 141.6678 23.7316C141.2909 23.3828 140.7481 23.2084 140.0394 23.2084C139.2912 23.2084 138.6992 23.3716 138.2633 23.6978C137.8273 24.0241 137.5334 24.4094 137.3815 24.8538L134.5297 24.4488C134.7547 23.6613 135.1259 23.0017 135.6434 22.4702C136.1609 21.9386 136.7937 21.5392 137.5419 21.272C138.29 21.0048 139.1169 20.8713 140.0225 20.8713C140.6469 20.8713 141.2684 20.9444 141.8872 21.0906C142.5059 21.2369 143.0712 21.4773 143.5831 21.812C144.095 22.1467 144.507 22.6009 144.8192 23.1747C145.1314 23.7484 145.2875 24.4656 145.2875 25.3263L145.2875 34L142.3512 34L142.3512 32.2197L142.25 32.2197C142.0644 32.5797 141.8042 32.9158 141.4695 33.228C141.1348 33.5402 140.7158 33.7905 140.2123 33.9789CLine truncated
<defs>
<filter id="filter0_f_241_1963" x="0" y="0" width="47" height="47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="5" result="effect1_foregroundBlur_241_1963" />
</filter>
<linearGradient id="paint0_linear_241_1963" x1="7.80233" y1="9.05814" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint1_linear_241_1963" x1="7.80232" y1="9.05823" x2="40.4535" y2="38.5699" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint2_linear_241_1963" x1="7.80231" y1="9.05816" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint3_linear_241_1963" x1="7.8023" y1="9.05812" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint4_linear_241_1963" x1="47.1163" y1="24.5" x2="180" y2="24.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9F9F9" />
<stop offset="1" stop-color="#8EA2D9" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

+46
View File
@@ -0,0 +1,46 @@
<svg width="182" height="47" viewBox="0 0 182 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>Zoomika</title>
<g opacity="0.18" filter="url(#filter0_f_241_2041)">
<path d="M23.5 37C30.9558 37 37 30.9558 37 23.5C37 16.0442 30.9558 10 23.5 10C16.0442 10 10 16.0442 10 23.5C10 30.9558 16.0442 37 23.5 37Z" fill="url(#paint0_linear_241_2041)" />
</g>
<path d="M8.11628 23.5001C11.4128 16.2164 16.8442 12.5117 23.5 12.5117C30.1558 12.5117 35.5872 16.2164 38.8837 23.5001C35.5872 30.7838 30.1558 34.4885 23.5 34.4885C16.8442 34.4885 11.4128 30.7838 8.11628 23.5001Z" fill="#090812" stroke="url(#paint1_linear_241_2041)" stroke-width="2.6" stroke-linejoin="round" />
<path d="M23.5 29.7791C26.9678 29.7791 29.7791 26.9679 29.7791 23.5C29.7791 20.0322 26.9678 17.2209 23.5 17.2209C20.0322 17.2209 17.2209 20.0322 17.2209 23.5C17.2209 26.9679 20.0322 29.7791 23.5 29.7791Z" fill="url(#paint2_linear_241_2041)" />
<path d="M23.5 26.3255C25.0605 26.3255 26.3256 25.0604 26.3256 23.4999C26.3256 21.9394 25.0605 20.6743 23.5 20.6743C21.9395 20.6743 20.6744 21.9394 20.6744 23.4999C20.6744 25.0604 21.9395 26.3255 23.5 26.3255Z" fill="#090812" />
<path d="M28.8372 15.0232L29.936 17.6918L32.6047 18.7906L29.936 19.8895L28.8372 22.5581L27.7384 19.8895L25.0698 18.7906L27.7384 17.6918L28.8372 15.0232Z" fill="white" />
<path d="M14.7093 27.2673H13.7674C13.4207 27.2673 13.1395 27.5485 13.1395 27.8952V28.8371C13.1395 29.1839 13.4207 29.465 13.7674 29.465H14.7093C15.0561 29.465 15.3372 29.1839 15.3372 28.8371V27.8952C15.3372 27.5485 15.0561 27.2673 14.7093 27.2673Z" fill="#6AA7FF" />
<path d="M18.9477 29.1511H17.6919C17.2584 29.1511 16.907 29.5025 16.907 29.936V31.1918C16.907 31.6253 17.2584 31.9767 17.6919 31.9767H18.9477C19.3812 31.9767 19.7326 31.6253 19.7326 31.1918V29.936C19.7326 29.5025 19.3812 29.1511 18.9477 29.1511Z" fill="#7B5CFF" />
<path d="M23.814 30.407H21.9302C21.4101 30.407 20.9884 30.8287 20.9884 31.3488V33.2326C20.9884 33.7527 21.4101 34.1744 21.9302 34.1744H23.814C24.3341 34.1744 24.7558 33.7527 24.7558 33.2326V31.3488C24.7558 30.8287 24.3341 30.407 23.814 30.407Z" fill="#D21BCE" />
<path d="M31.3488 29.7791L36.0581 34.4884" stroke="url(#paint3_linear_241_2041)" stroke-width="2.6" stroke-linecap="round" />
<path d="M48.3163 34V33.604L56.8363 19.432H48.9163V16.72H61.8403V17.128L53.3683 31.276H61.6363V34H48.3163ZM70.4343 34.36C69.1383 34.36 68.0063 34.068 67.0383 33.484C66.0703 32.9 65.3183 32.096 64.7823 31.072C64.2543 30.04 63.9903 28.856 63.9903 27.52C63.9903 26.16 64.2623 24.968 64.8063 23.944C65.3503 22.92 66.1063 22.12 67.0743 21.544C68.0423 20.968 69.1623 20.68 70.4343 20.68C71.7383 20.68 72.8743 20.972 73.8423 21.556C74.8103 22.14 75.5623 22.948 76.0983 23.98C76.6343 25.004 76.9023 26.184 76.9023 27.52C76.9023 28.864 76.6303 30.052 76.0863 31.084C75.5503 32.108 74.7983 32.912 73.8303 33.496C72.8623 34.072 71.7303 34.36 70.4343 34.36ZM70.4343 31.648C71.5863 31.648 72.4423 31.264 73.0023 30.496C73.5623 29.728 73.8423 28.736 73.8423 27.52C73.8423 26.264 73.5583 25.264 72.9903 24.52C72.4223 23.768 71.5703 23.392 70.4343 23.392C69.6583 23.392 69.0183 23.568 68.5143 23.92C68.0183 24.264 67.6503 24.748 67.4103 25.372C67.1703 25.988 67.0503 26.704 67.0503 27.52C67.0503 28.776 67.3343 29.78 67.9023 30.532C68.4783 31.276 69.3223 31.648 70.4343 31.648ZM85.2703 34.36C83.9743 34.36 82.8423 34.068 81.8743 33.484C80.9063 32.9 80.1543 32.096 79.6183 31.072C79.0903 30.04 78.8263 28.856 78.8263 27.52C78.8263 26.16 79.0983 24.968 79.6423 23.944C80.1863 22.92 80.9423 22.12 81.9103 21.544C82.8783 20.968 83.9983 20.68 85.2703 20.68C86.5743 20.68 87.7103 20.972 88.6783 21.556C89.6463 22.14 90.3983 22.948 90.9343 23.98C91.4703 25.004 91.7383 26.184 91.7383 27.52C91.7383 28.864 91.4663 30.052 90.9223 31.084C90.3863 32.108 89.6343 32.912 88.6663 33.496C87.6983 34.072 86.5663 34.36 85.2703 34.36ZM85.2703 31.648C86.4223 31.648 87.2783 31.264 87.8383 30.496C88.3983 29.728 88.6783 28.736 88.6783 27.52C88.6783 26.264 88.3943 25.264 87.8263 24.52C87.2583 23.768 86.4063 23.392 85.2703 23.392C84.4943 23.392 83.8543 23.568 83.3503 23.92C82.8543 24.264 82.4863 24.748 82.2463 25.372C82.0063 25.988 81.8863 26.704 81.8863 27.52C81.8863 28.776 82.1703 29.78 82.7383 30.532C83.3143 31.276 84.1583 31.648 85.2703 31.648ZM109.946 34V26.128C109.946 25.24 109.726 24.548 109.286 24.052C108.846 23.548 108.25 23.296 107.498 23.296C107.034 23.296 106.618 23.404 106.25 23.62C105.882 23.828 105.59 24.14 105.374 24.556C105.158 24.964 105.05 25.452 105.05 26.02L103.766 25.264C103.758 24.368 103.958 23.58 104.366 22.9C104.782 22.212 105.342 21.676 106.046 21.292C106.75 20.908 107.534 20.716 108.398 20.716C109.83 20.716 110.926 21.148 111.686 22.012C112.454 22.868 112.838 23.996 112.838 25.396V34H109.946ZM94.3582 34V21.04H96.9142V25.06H97.2742V34H94.3582ZM102.17 34V26.164C102.17 25.26 101.95 24.556 101.51 24.052C101.07 23.548 100.47 23.296 99.7102 23.296C98.9742 23.296 98.3822 23.548 97.9342 24.052C97.4942 24.556 97.2742 25.212 97.2742 26.02L95.9782 25.156C95.9782 24.316 96.1862 23.56 96.6022 22.888C97.0182 22.216 97.5782 21.688 98.2822 21.304C98.9942 20.912 99.7902 20.716 100.67 20.716C101.638 20.716 102.446 20.924 103.094 21.34C103.75 21.748 104.238 22.308 104.558 23.02C104.886 23.724 105.05 24.52 105.05 25.408V34H102.17ZM115.0042 34L115.0042 21.04L118.0586 21.04L118.0586 34ZM116.5399 19.2006C116.0561 19.2006 115.6399 19.0389 115.2911 18.7155C114.9424 18.392 114.768 18.0025 114.768 17.5469C114.768 17.0856 114.9424 16.6933 115.2911 16.3698C115.6399 16.0464 116.0561 15.8847 116.5399 15.8847C117.0292 15.8847 117.4469 16.0464 117.7928 16.3698C118.1388 16.6933 118.3117 17.0856 118.3117 17.5469C118.3117 18.0025 118.1388 18.392 117.7928 18.7155C117.4469 19.0389 117.0292 19.2006 116.5399 19.2006ZM123.9839 29.9331L123.9754 26.2459L124.4648 26.2459L129.1223 21.04L132.6913 21.04L126.9623 27.4188L126.3295 27.4188ZM121.1995 34L121.1995 16.72L124.2539 16.72L124.2539 34ZM129.3332 34L125.1145 28.1022L127.1732 25.9506L132.9867 34ZM138.4447 34.2616C137.6234 34.2616 136.8851 34.1139 136.2298 33.8186C135.5745 33.5233 135.057 33.0859 134.6773 32.5066C134.2976 31.9272 134.1078 31.2128 134.1078 30.3634C134.1078 29.6322 134.2428 29.0275 134.5128 28.5494C134.7828 28.0713 135.1512 27.6888 135.6181 27.4019C136.085 27.115 136.6123 26.897 137.2001 26.748C137.7879 26.5989 138.3969 26.4906 139.0269 26.4231C139.7862 26.3444 140.4022 26.2727 140.8747 26.208C141.3472 26.1433 141.6917 26.042 141.9083 25.9042C142.1248 25.7664 142.2331 25.5513 142.2331 25.2588L142.2331 25.2081C142.2331 24.5725 142.0447 24.0803 141.6678 23.7316C141.2909 23.3828 140.7481 23.2084 140.0394 23.2084C139.2912 23.2084 138.6992 23.3716 138.2633 23.6978C137.8273 24.0241 137.5334 24.4094 137.3815 24.8538L134.5297 24.4488C134.7547 23.6613 135.1259 23.0017 135.6434 22.4702C136.1609 21.9386 136.7937 21.5392 137.5419 21.272C138.29 21.0048 139.1169 20.8713 140.0225 20.8713C140.6469 20.8713 141.2684 20.9444 141.8872 21.0906C142.5059 21.2369 143.0712 21.4773 143.5831 21.812C144.095 22.1467 144.507 22.6009 144.8192 23.1747C145.1314 23.7484 145.2875 24.4656 145.2875 25.3263L145.2875 34L142.3512 34L142.3512 32.2197L142.25 32.2197C142.0644 32.5797 141.8042 32.9158 141.4695 33.228C141.1348 33.5402 140.7158 33.7905 140.2123 33.9789CLine truncated
<defs>
<filter id="filter0_f_241_2041" x="0" y="0" width="47" height="47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="5" result="effect1_foregroundBlur_241_2041" />
</filter>
<linearGradient id="paint0_linear_241_2041" x1="7.80233" y1="9.05814" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint1_linear_241_2041" x1="7.80233" y1="9.05823" x2="40.4535" y2="38.5699" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint2_linear_241_2041" x1="7.80233" y1="9.05816" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint3_linear_241_2041" x1="7.80233" y1="9.05812" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint4_linear_241_2041" x1="47.1163" y1="24.5" x2="180" y2="24.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#0F0911" />
<stop offset="1" stop-color="#960A97" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

+46
View File
@@ -0,0 +1,46 @@
<svg width="182" height="47" viewBox="0 0 182 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>Zoomila</title>
<g opacity="0.18" filter="url(#filter0_f_241_1963)">
<path d="M23.5 37C30.9558 37 37 30.9558 37 23.5C37 16.0442 30.9558 10 23.5 10C16.0442 10 10 16.0442 10 23.5C10 30.9558 16.0442 37 23.5 37Z" fill="url(#paint0_linear_241_1963)" />
</g>
<path d="M8.11627 23.5001C11.4128 16.2164 16.8442 12.5117 23.5 12.5117C30.1558 12.5117 35.5872 16.2164 38.8837 23.5001C35.5872 30.7838 30.1558 34.4885 23.5 34.4885C16.8442 34.4885 11.4128 30.7838 8.11627 23.5001Z" fill="#090812" stroke="url(#paint1_linear_241_1963)" stroke-width="2.6" stroke-linejoin="round" />
<path d="M23.5 29.7791C26.9678 29.7791 29.7791 26.9679 29.7791 23.5C29.7791 20.0322 26.9678 17.2209 23.5 17.2209C20.0322 17.2209 17.2209 20.0322 17.2209 23.5C17.2209 26.9679 20.0322 29.7791 23.5 29.7791Z" fill="url(#paint2_linear_241_1963)" />
<path d="M23.5 26.3255C25.0605 26.3255 26.3256 25.0604 26.3256 23.4999C26.3256 21.9394 25.0605 20.6743 23.5 20.6743C21.9395 20.6743 20.6744 21.9394 20.6744 23.4999C20.6744 25.0604 21.9395 26.3255 23.5 26.3255Z" fill="#090812" />
<path d="M28.8372 15.0232L29.936 17.6918L32.6046 18.7906L29.936 19.8895L28.8372 22.5581L27.7384 19.8895L25.0698 18.7906L27.7384 17.6918L28.8372 15.0232Z" fill="white" />
<path d="M14.7093 27.2673H13.7674C13.4206 27.2673 13.1395 27.5485 13.1395 27.8952V28.8371C13.1395 29.1839 13.4206 29.465 13.7674 29.465H14.7093C15.0561 29.465 15.3372 29.1839 15.3372 28.8371V27.8952C15.3372 27.5485 15.0561 27.2673 14.7093 27.2673Z" fill="#6AA7FF" />
<path d="M18.9477 29.1511H17.6919C17.2584 29.1511 16.907 29.5025 16.907 29.936V31.1918C16.907 31.6253 17.2584 31.9767 17.6919 31.9767H18.9477C19.3812 31.9767 19.7326 31.6253 19.7326 31.1918V29.936C19.7326 29.5025 19.3812 29.1511 18.9477 29.1511Z" fill="#7B5CFF" />
<path d="M23.814 30.407H21.9302C21.4101 30.407 20.9884 30.8287 20.9884 31.3488V33.2326C20.9884 33.7527 21.4101 34.1744 21.9302 34.1744H23.814C24.3341 34.1744 24.7558 33.7527 24.7558 33.2326V31.3488C24.7558 30.8287 24.3341 30.407 23.814 30.407Z" fill="#D21BCE" />
<path d="M31.3488 29.7791L36.0581 34.4884" stroke="url(#paint3_linear_241_1963)" stroke-width="2.6" stroke-linecap="round" />
<path d="M48.3163 34V33.604L56.8363 19.432H48.9163V16.72H61.8403V17.128L53.3683 31.276H61.6363V34H48.3163ZM70.4343 34.36C69.1383 34.36 68.0063 34.068 67.0383 33.484C66.0703 32.9 65.3183 32.096 64.7823 31.072C64.2543 30.04 63.9903 28.856 63.9903 27.52C63.9903 26.16 64.2623 24.968 64.8063 23.944C65.3503 22.92 66.1063 22.12 67.0743 21.544C68.0423 20.968 69.1623 20.68 70.4343 20.68C71.7383 20.68 72.8743 20.972 73.8423 21.556C74.8103 22.14 75.5623 22.948 76.0983 23.98C76.6343 25.004 76.9023 26.184 76.9023 27.52C76.9023 28.864 76.6303 30.052 76.0863 31.084C75.5503 32.108 74.7983 32.912 73.8303 33.496C72.8623 34.072 71.7303 34.36 70.4343 34.36ZM70.4343 31.648C71.5863 31.648 72.4423 31.264 73.0023 30.496C73.5623 29.728 73.8423 28.736 73.8423 27.52C73.8423 26.264 73.5583 25.264 72.9903 24.52C72.4223 23.768 71.5703 23.392 70.4343 23.392C69.6583 23.392 69.0183 23.568 68.5143 23.92C68.0183 24.264 67.6503 24.748 67.4103 25.372C67.1703 25.988 67.0503 26.704 67.0503 27.52C67.0503 28.776 67.3343 29.78 67.9023 30.532C68.4783 31.276 69.3223 31.648 70.4343 31.648ZM85.2703 34.36C83.9743 34.36 82.8423 34.068 81.8743 33.484C80.9063 32.9 80.1543 32.096 79.6183 31.072C79.0903 30.04 78.8263 28.856 78.8263 27.52C78.8263 26.16 79.0983 24.968 79.6423 23.944C80.1863 22.92 80.9423 22.12 81.9103 21.544C82.8783 20.968 83.9983 20.68 85.2703 20.68C86.5743 20.68 87.7103 20.972 88.6783 21.556C89.6463 22.14 90.3983 22.948 90.9343 23.98C91.4703 25.004 91.7383 26.184 91.7383 27.52C91.7383 28.864 91.4663 30.052 90.9223 31.084C90.3863 32.108 89.6343 32.912 88.6663 33.496C87.6983 34.072 86.5663 34.36 85.2703 34.36ZM85.2703 31.648C86.4223 31.648 87.2783 31.264 87.8383 30.496C88.3983 29.728 88.6783 28.736 88.6783 27.52C88.6783 26.264 88.3943 25.264 87.8263 24.52C87.2583 23.768 86.4063 23.392 85.2703 23.392C84.4943 23.392 83.8543 23.568 83.3503 23.92C82.8543 24.264 82.4863 24.748 82.2463 25.372C82.0063 25.988 81.8863 26.704 81.8863 27.52C81.8863 28.776 82.1703 29.78 82.7383 30.532C83.3143 31.276 84.1583 31.648 85.2703 31.648ZM109.946 34V26.128C109.946 25.24 109.726 24.548 109.286 24.052C108.846 23.548 108.25 23.296 107.498 23.296C107.034 23.296 106.618 23.404 106.25 23.62C105.882 23.828 105.59 24.14 105.374 24.556C105.158 24.964 105.05 25.452 105.05 26.02L103.766 25.264C103.758 24.368 103.958 23.58 104.366 22.9C104.782 22.212 105.342 21.676 106.046 21.292C106.75 20.908 107.534 20.716 108.398 20.716C109.83 20.716 110.926 21.148 111.686 22.012C112.454 22.868 112.838 23.996 112.838 25.396V34H109.946ZM94.3582 34V21.04H96.9142V25.06H97.2742V34H94.3582ZM102.17 34V26.164C102.17 25.26 101.95 24.556 101.51 24.052C101.07 23.548 100.47 23.296 99.7102 23.296C98.9742 23.296 98.3822 23.548 97.9342 24.052C97.4942 24.556 97.2742 25.212 97.2742 26.02L95.9782 25.156C95.9782 24.316 96.1862 23.56 96.6022 22.888C97.0182 22.216 97.5782 21.688 98.2822 21.304C98.9942 20.912 99.7902 20.716 100.67 20.716C101.638 20.716 102.446 20.924 103.094 21.34C103.75 21.748 104.238 22.308 104.558 23.02C104.886 23.724 105.05 24.52 105.05 25.408V34H102.17ZM115.0042 34L115.0042 21.04L118.0586 21.04L118.0586 34ZM116.5399 19.2006C116.0561 19.2006 115.6399 19.0389 115.2911 18.7155C114.9424 18.392 114.768 18.0025 114.768 17.5469C114.768 17.0856 114.9424 16.6933 115.2911 16.3698C115.6399 16.0464 116.0561 15.8847 116.5399 15.8847C117.0292 15.8847 117.4469 16.0464 117.7928 16.3698C118.1388 16.6933 118.3117 17.0856 118.3117 17.5469C118.3117 18.0025 118.1388 18.392 117.7928 18.7155C117.4469 19.0389 117.0292 19.2006 116.5399 19.2006ZM124.2539 16.72L124.2539 34L121.1995 34L121.1995 16.72ZM131.1241 34.2616C130.3028 34.2616 129.5646 34.1139 128.9092 33.8186C128.2539 33.5233 127.7364 33.0859 127.3568 32.5066C126.9771 31.9272 126.7872 31.2128 126.7872 30.3634C126.7872 29.6322 126.9222 29.0275 127.1922 28.5494C127.4622 28.0713 127.8307 27.6888 128.2975 27.4019C128.7644 27.115 129.2917 26.897 129.8796 26.748C130.4674 26.5989 131.0763 26.4906 131.7063 26.4231C132.4657 26.3444 133.0816 26.2727 133.5541 26.208C134.0266 26.1433 134.3711 26.042 134.5877 25.9042C134.8042 25.7664 134.9125 25.5513 134.9125 25.2588L134.9125 25.2081C134.9125 24.5725 134.7241 24.0803 134.3472 23.7316C133.9703 23.3828 133.4275 23.2084 132.7188 23.2084C131.9706 23.2084 131.3786 23.3716 130.9427 23.6978C130.5067 24.0241 130.2128 24.4094 130.061 24.8538L127.2091 24.4488C127.4341 23.6613 127.8053 23.0017 128.3228 22.4702C128.8403 21.9386 129.4731 21.5392 130.2213 21.272C130.9694 21.0048 131.7963 20.8713 132.7019 20.8713C133.3263 20.8713 133.9478 20.9444 134.5666 21.0906C135.1853 21.2369 135.7507 21.4773 136.2625 21.812C136.7744 22.1467 137.1864 22.6009 137.4986 23.1747C137.8108 23.7484 137.9669 24.4656 137.9669 25.3263L137.9669 34L135.0307 34L135.0307 32.2197L134.9294 32.2197C134.7438 32.5797 134.4836 32.9158 134.1489 33.228C133.8142 33.5402 133.3952 33.7905 132.8917 33.9789C132.3883 34.1673 131.7991 34.2616 131.1241 34.2616ZM131.9172 32.0172C132.5303 32.0172 133.0619 31.8948 133.5119 31.6502C133.9619 31.4055 134.3092 31.0806 134.5539 30.67Line truncated
<defs>
<filter id="filter0_f_241_1963" x="0" y="0" width="47" height="47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="5" result="effect1_foregroundBlur_241_1963" />
</filter>
<linearGradient id="paint0_linear_241_1963" x1="7.80233" y1="9.05814" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint1_linear_241_1963" x1="7.80232" y1="9.05823" x2="40.4535" y2="38.5699" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint2_linear_241_1963" x1="7.80231" y1="9.05816" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint3_linear_241_1963" x1="7.8023" y1="9.05812" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint4_linear_241_1963" x1="47.1163" y1="24.5" x2="180" y2="24.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9F9F9" />
<stop offset="1" stop-color="#8EA2D9" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.2 KiB

+46
View File
@@ -0,0 +1,46 @@
<svg width="182" height="47" viewBox="0 0 182 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>Zoomila</title>
<g opacity="0.18" filter="url(#filter0_f_241_2041)">
<path d="M23.5 37C30.9558 37 37 30.9558 37 23.5C37 16.0442 30.9558 10 23.5 10C16.0442 10 10 16.0442 10 23.5C10 30.9558 16.0442 37 23.5 37Z" fill="url(#paint0_linear_241_2041)" />
</g>
<path d="M8.11628 23.5001C11.4128 16.2164 16.8442 12.5117 23.5 12.5117C30.1558 12.5117 35.5872 16.2164 38.8837 23.5001C35.5872 30.7838 30.1558 34.4885 23.5 34.4885C16.8442 34.4885 11.4128 30.7838 8.11628 23.5001Z" fill="#090812" stroke="url(#paint1_linear_241_2041)" stroke-width="2.6" stroke-linejoin="round" />
<path d="M23.5 29.7791C26.9678 29.7791 29.7791 26.9679 29.7791 23.5C29.7791 20.0322 26.9678 17.2209 23.5 17.2209C20.0322 17.2209 17.2209 20.0322 17.2209 23.5C17.2209 26.9679 20.0322 29.7791 23.5 29.7791Z" fill="url(#paint2_linear_241_2041)" />
<path d="M23.5 26.3255C25.0605 26.3255 26.3256 25.0604 26.3256 23.4999C26.3256 21.9394 25.0605 20.6743 23.5 20.6743C21.9395 20.6743 20.6744 21.9394 20.6744 23.4999C20.6744 25.0604 21.9395 26.3255 23.5 26.3255Z" fill="#090812" />
<path d="M28.8372 15.0232L29.936 17.6918L32.6047 18.7906L29.936 19.8895L28.8372 22.5581L27.7384 19.8895L25.0698 18.7906L27.7384 17.6918L28.8372 15.0232Z" fill="white" />
<path d="M14.7093 27.2673H13.7674C13.4207 27.2673 13.1395 27.5485 13.1395 27.8952V28.8371C13.1395 29.1839 13.4207 29.465 13.7674 29.465H14.7093C15.0561 29.465 15.3372 29.1839 15.3372 28.8371V27.8952C15.3372 27.5485 15.0561 27.2673 14.7093 27.2673Z" fill="#6AA7FF" />
<path d="M18.9477 29.1511H17.6919C17.2584 29.1511 16.907 29.5025 16.907 29.936V31.1918C16.907 31.6253 17.2584 31.9767 17.6919 31.9767H18.9477C19.3812 31.9767 19.7326 31.6253 19.7326 31.1918V29.936C19.7326 29.5025 19.3812 29.1511 18.9477 29.1511Z" fill="#7B5CFF" />
<path d="M23.814 30.407H21.9302C21.4101 30.407 20.9884 30.8287 20.9884 31.3488V33.2326C20.9884 33.7527 21.4101 34.1744 21.9302 34.1744H23.814C24.3341 34.1744 24.7558 33.7527 24.7558 33.2326V31.3488C24.7558 30.8287 24.3341 30.407 23.814 30.407Z" fill="#D21BCE" />
<path d="M31.3488 29.7791L36.0581 34.4884" stroke="url(#paint3_linear_241_2041)" stroke-width="2.6" stroke-linecap="round" />
<path d="M48.3163 34V33.604L56.8363 19.432H48.9163V16.72H61.8403V17.128L53.3683 31.276H61.6363V34H48.3163ZM70.4343 34.36C69.1383 34.36 68.0063 34.068 67.0383 33.484C66.0703 32.9 65.3183 32.096 64.7823 31.072C64.2543 30.04 63.9903 28.856 63.9903 27.52C63.9903 26.16 64.2623 24.968 64.8063 23.944C65.3503 22.92 66.1063 22.12 67.0743 21.544C68.0423 20.968 69.1623 20.68 70.4343 20.68C71.7383 20.68 72.8743 20.972 73.8423 21.556C74.8103 22.14 75.5623 22.948 76.0983 23.98C76.6343 25.004 76.9023 26.184 76.9023 27.52C76.9023 28.864 76.6303 30.052 76.0863 31.084C75.5503 32.108 74.7983 32.912 73.8303 33.496C72.8623 34.072 71.7303 34.36 70.4343 34.36ZM70.4343 31.648C71.5863 31.648 72.4423 31.264 73.0023 30.496C73.5623 29.728 73.8423 28.736 73.8423 27.52C73.8423 26.264 73.5583 25.264 72.9903 24.52C72.4223 23.768 71.5703 23.392 70.4343 23.392C69.6583 23.392 69.0183 23.568 68.5143 23.92C68.0183 24.264 67.6503 24.748 67.4103 25.372C67.1703 25.988 67.0503 26.704 67.0503 27.52C67.0503 28.776 67.3343 29.78 67.9023 30.532C68.4783 31.276 69.3223 31.648 70.4343 31.648ZM85.2703 34.36C83.9743 34.36 82.8423 34.068 81.8743 33.484C80.9063 32.9 80.1543 32.096 79.6183 31.072C79.0903 30.04 78.8263 28.856 78.8263 27.52C78.8263 26.16 79.0983 24.968 79.6423 23.944C80.1863 22.92 80.9423 22.12 81.9103 21.544C82.8783 20.968 83.9983 20.68 85.2703 20.68C86.5743 20.68 87.7103 20.972 88.6783 21.556C89.6463 22.14 90.3983 22.948 90.9343 23.98C91.4703 25.004 91.7383 26.184 91.7383 27.52C91.7383 28.864 91.4663 30.052 90.9223 31.084C90.3863 32.108 89.6343 32.912 88.6663 33.496C87.6983 34.072 86.5663 34.36 85.2703 34.36ZM85.2703 31.648C86.4223 31.648 87.2783 31.264 87.8383 30.496C88.3983 29.728 88.6783 28.736 88.6783 27.52C88.6783 26.264 88.3943 25.264 87.8263 24.52C87.2583 23.768 86.4063 23.392 85.2703 23.392C84.4943 23.392 83.8543 23.568 83.3503 23.92C82.8543 24.264 82.4863 24.748 82.2463 25.372C82.0063 25.988 81.8863 26.704 81.8863 27.52C81.8863 28.776 82.1703 29.78 82.7383 30.532C83.3143 31.276 84.1583 31.648 85.2703 31.648ZM109.946 34V26.128C109.946 25.24 109.726 24.548 109.286 24.052C108.846 23.548 108.25 23.296 107.498 23.296C107.034 23.296 106.618 23.404 106.25 23.62C105.882 23.828 105.59 24.14 105.374 24.556C105.158 24.964 105.05 25.452 105.05 26.02L103.766 25.264C103.758 24.368 103.958 23.58 104.366 22.9C104.782 22.212 105.342 21.676 106.046 21.292C106.75 20.908 107.534 20.716 108.398 20.716C109.83 20.716 110.926 21.148 111.686 22.012C112.454 22.868 112.838 23.996 112.838 25.396V34H109.946ZM94.3582 34V21.04H96.9142V25.06H97.2742V34H94.3582ZM102.17 34V26.164C102.17 25.26 101.95 24.556 101.51 24.052C101.07 23.548 100.47 23.296 99.7102 23.296C98.9742 23.296 98.3822 23.548 97.9342 24.052C97.4942 24.556 97.2742 25.212 97.2742 26.02L95.9782 25.156C95.9782 24.316 96.1862 23.56 96.6022 22.888C97.0182 22.216 97.5782 21.688 98.2822 21.304C98.9942 20.912 99.7902 20.716 100.67 20.716C101.638 20.716 102.446 20.924 103.094 21.34C103.75 21.748 104.238 22.308 104.558 23.02C104.886 23.724 105.05 24.52 105.05 25.408V34H102.17ZM115.0042 34L115.0042 21.04L118.0586 21.04L118.0586 34ZM116.5399 19.2006C116.0561 19.2006 115.6399 19.0389 115.2911 18.7155C114.9424 18.392 114.768 18.0025 114.768 17.5469C114.768 17.0856 114.9424 16.6933 115.2911 16.3698C115.6399 16.0464 116.0561 15.8847 116.5399 15.8847C117.0292 15.8847 117.4469 16.0464 117.7928 16.3698C118.1388 16.6933 118.3117 17.0856 118.3117 17.5469C118.3117 18.0025 118.1388 18.392 117.7928 18.7155C117.4469 19.0389 117.0292 19.2006 116.5399 19.2006ZM124.2539 16.72L124.2539 34L121.1995 34L121.1995 16.72ZM131.1241 34.2616C130.3028 34.2616 129.5646 34.1139 128.9092 33.8186C128.2539 33.5233 127.7364 33.0859 127.3568 32.5066C126.9771 31.9272 126.7872 31.2128 126.7872 30.3634C126.7872 29.6322 126.9222 29.0275 127.1922 28.5494C127.4622 28.0713 127.8307 27.6888 128.2975 27.4019C128.7644 27.115 129.2917 26.897 129.8796 26.748C130.4674 26.5989 131.0763 26.4906 131.7063 26.4231C132.4657 26.3444 133.0816 26.2727 133.5541 26.208C134.0266 26.1433 134.3711 26.042 134.5877 25.9042C134.8042 25.7664 134.9125 25.5513 134.9125 25.2588L134.9125 25.2081C134.9125 24.5725 134.7241 24.0803 134.3472 23.7316C133.9703 23.3828 133.4275 23.2084 132.7188 23.2084C131.9706 23.2084 131.3786 23.3716 130.9427 23.6978C130.5067 24.0241 130.2128 24.4094 130.061 24.8538L127.2091 24.4488C127.4341 23.6613 127.8053 23.0017 128.3228 22.4702C128.8403 21.9386 129.4731 21.5392 130.2213 21.272C130.9694 21.0048 131.7963 20.8713 132.7019 20.8713C133.3263 20.8713 133.9478 20.9444 134.5666 21.0906C135.1853 21.2369 135.7507 21.4773 136.2625 21.812C136.7744 22.1467 137.1864 22.6009 137.4986 23.1747C137.8108 23.7484 137.9669 24.4656 137.9669 25.3263L137.9669 34L135.0307 34L135.0307 32.2197L134.9294 32.2197C134.7438 32.5797 134.4836 32.9158 134.1489 33.228C133.8142 33.5402 133.3952 33.7905 132.8917 33.9789C132.3883 34.1673 131.7991 34.2616 131.1241 34.2616ZM131.9172 32.0172C132.5303 32.0172 133.0619 31.8948 133.5119 31.6502C133.9619 31.4055 134.3092 31.0806 134.5539 30.67Line truncated
<defs>
<filter id="filter0_f_241_2041" x="0" y="0" width="47" height="47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="5" result="effect1_foregroundBlur_241_2041" />
</filter>
<linearGradient id="paint0_linear_241_2041" x1="7.80233" y1="9.05814" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint1_linear_241_2041" x1="7.80233" y1="9.05823" x2="40.4535" y2="38.5699" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint2_linear_241_2041" x1="7.80233" y1="9.05816" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint3_linear_241_2041" x1="7.80233" y1="9.05812" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint4_linear_241_2041" x1="47.1163" y1="24.5" x2="180" y2="24.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#0F0911" />
<stop offset="1" stop-color="#960A97" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.2 KiB

+46
View File
@@ -0,0 +1,46 @@
<svg width="182" height="47" viewBox="0 0 182 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>ZoomPixel</title>
<g opacity="0.18" filter="url(#filter0_f_241_1963)">
<path d="M23.5 37C30.9558 37 37 30.9558 37 23.5C37 16.0442 30.9558 10 23.5 10C16.0442 10 10 16.0442 10 23.5C10 30.9558 16.0442 37 23.5 37Z" fill="url(#paint0_linear_241_1963)" />
</g>
<path d="M8.11627 23.5001C11.4128 16.2164 16.8442 12.5117 23.5 12.5117C30.1558 12.5117 35.5872 16.2164 38.8837 23.5001C35.5872 30.7838 30.1558 34.4885 23.5 34.4885C16.8442 34.4885 11.4128 30.7838 8.11627 23.5001Z" fill="#090812" stroke="url(#paint1_linear_241_1963)" stroke-width="2.6" stroke-linejoin="round" />
<path d="M23.5 29.7791C26.9678 29.7791 29.7791 26.9679 29.7791 23.5C29.7791 20.0322 26.9678 17.2209 23.5 17.2209C20.0322 17.2209 17.2209 20.0322 17.2209 23.5C17.2209 26.9679 20.0322 29.7791 23.5 29.7791Z" fill="url(#paint2_linear_241_1963)" />
<path d="M23.5 26.3255C25.0605 26.3255 26.3256 25.0604 26.3256 23.4999C26.3256 21.9394 25.0605 20.6743 23.5 20.6743C21.9395 20.6743 20.6744 21.9394 20.6744 23.4999C20.6744 25.0604 21.9395 26.3255 23.5 26.3255Z" fill="#090812" />
<path d="M28.8372 15.0232L29.936 17.6918L32.6046 18.7906L29.936 19.8895L28.8372 22.5581L27.7384 19.8895L25.0698 18.7906L27.7384 17.6918L28.8372 15.0232Z" fill="white" />
<path d="M14.7093 27.2673H13.7674C13.4206 27.2673 13.1395 27.5485 13.1395 27.8952V28.8371C13.1395 29.1839 13.4206 29.465 13.7674 29.465H14.7093C15.0561 29.465 15.3372 29.1839 15.3372 28.8371V27.8952C15.3372 27.5485 15.0561 27.2673 14.7093 27.2673Z" fill="#6AA7FF" />
<path d="M18.9477 29.1511H17.6919C17.2584 29.1511 16.907 29.5025 16.907 29.936V31.1918C16.907 31.6253 17.2584 31.9767 17.6919 31.9767H18.9477C19.3812 31.9767 19.7326 31.6253 19.7326 31.1918V29.936C19.7326 29.5025 19.3812 29.1511 18.9477 29.1511Z" fill="#7B5CFF" />
<path d="M23.814 30.407H21.9302C21.4101 30.407 20.9884 30.8287 20.9884 31.3488V33.2326C20.9884 33.7527 21.4101 34.1744 21.9302 34.1744H23.814C24.3341 34.1744 24.7558 33.7527 24.7558 33.2326V31.3488C24.7558 30.8287 24.3341 30.407 23.814 30.407Z" fill="#D21BCE" />
<path d="M31.3488 29.7791L36.0581 34.4884" stroke="url(#paint3_linear_241_1963)" stroke-width="2.6" stroke-linecap="round" />
<path d="M48.3163 34V33.604L56.8363 19.432H48.9163V16.72H61.8403V17.128L53.3683 31.276H61.6363V34H48.3163ZM70.4343 34.36C69.1383 34.36 68.0063 34.068 67.0383 33.484C66.0703 32.9 65.3183 32.096 64.7823 31.072C64.2543 30.04 63.9903 28.856 63.9903 27.52C63.9903 26.16 64.2623 24.968 64.8063 23.944C65.3503 22.92 66.1063 22.12 67.0743 21.544C68.0423 20.968 69.1623 20.68 70.4343 20.68C71.7383 20.68 72.8743 20.972 73.8423 21.556C74.8103 22.14 75.5623 22.948 76.0983 23.98C76.6343 25.004 76.9023 26.184 76.9023 27.52C76.9023 28.864 76.6303 30.052 76.0863 31.084C75.5503 32.108 74.7983 32.912 73.8303 33.496C72.8623 34.072 71.7303 34.36 70.4343 34.36ZM70.4343 31.648C71.5863 31.648 72.4423 31.264 73.0023 30.496C73.5623 29.728 73.8423 28.736 73.8423 27.52C73.8423 26.264 73.5583 25.264 72.9903 24.52C72.4223 23.768 71.5703 23.392 70.4343 23.392C69.6583 23.392 69.0183 23.568 68.5143 23.92C68.0183 24.264 67.6503 24.748 67.4103 25.372C67.1703 25.988 67.0503 26.704 67.0503 27.52C67.0503 28.776 67.3343 29.78 67.9023 30.532C68.4783 31.276 69.3223 31.648 70.4343 31.648ZM85.2703 34.36C83.9743 34.36 82.8423 34.068 81.8743 33.484C80.9063 32.9 80.1543 32.096 79.6183 31.072C79.0903 30.04 78.8263 28.856 78.8263 27.52C78.8263 26.16 79.0983 24.968 79.6423 23.944C80.1863 22.92 80.9423 22.12 81.9103 21.544C82.8783 20.968 83.9983 20.68 85.2703 20.68C86.5743 20.68 87.7103 20.972 88.6783 21.556C89.6463 22.14 90.3983 22.948 90.9343 23.98C91.4703 25.004 91.7383 26.184 91.7383 27.52C91.7383 28.864 91.4663 30.052 90.9223 31.084C90.3863 32.108 89.6343 32.912 88.6663 33.496C87.6983 34.072 86.5663 34.36 85.2703 34.36ZM85.2703 31.648C86.4223 31.648 87.2783 31.264 87.8383 30.496C88.3983 29.728 88.6783 28.736 88.6783 27.52C88.6783 26.264 88.3943 25.264 87.8263 24.52C87.2583 23.768 86.4063 23.392 85.2703 23.392C84.4943 23.392 83.8543 23.568 83.3503 23.92C82.8543 24.264 82.4863 24.748 82.2463 25.372C82.0063 25.988 81.8863 26.704 81.8863 27.52C81.8863 28.776 82.1703 29.78 82.7383 30.532C83.3143 31.276 84.1583 31.648 85.2703 31.648ZM109.946 34V26.128C109.946 25.24 109.726 24.548 109.286 24.052C108.846 23.548 108.25 23.296 107.498 23.296C107.034 23.296 106.618 23.404 106.25 23.62C105.882 23.828 105.59 24.14 105.374 24.556C105.158 24.964 105.05 25.452 105.05 26.02L103.766 25.264C103.758 24.368 103.958 23.58 104.366 22.9C104.782 22.212 105.342 21.676 106.046 21.292C106.75 20.908 107.534 20.716 108.398 20.716C109.83 20.716 110.926 21.148 111.686 22.012C112.454 22.868 112.838 23.996 112.838 25.396V34H109.946ZM94.3582 34V21.04H96.9142V25.06H97.2742V34H94.3582ZM102.17 34V26.164C102.17 25.26 101.95 24.556 101.51 24.052C101.07 23.548 100.47 23.296 99.7102 23.296C98.9742 23.296 98.3822 23.548 97.9342 24.052C97.4942 24.556 97.2742 25.212 97.2742 26.02L95.9782 25.156C95.9782 24.316 96.1862 23.56 96.6022 22.888C97.0182 22.216 97.5782 21.688 98.2822 21.304C98.9942 20.912 99.7902 20.716 100.67 20.716C101.638 20.716 102.446 20.924 103.094 21.34C103.75 21.748 104.238 22.308 104.558 23.02C104.886 23.724 105.05 24.52 105.05 25.408V34H102.17ZM114.768 34L114.768 16.72L121.248 16.72C122.5755 16.72 123.6907 16.9675 124.5935 17.4625C125.4963 17.9575 126.1797 18.6367 126.6438 19.5002C127.1078 20.3636 127.3399 21.3438 127.3399 22.4406C127.3399 23.5488 127.1064 24.5331 126.6396 25.3938C126.1727 26.2544 125.4836 26.9308 124.5724 27.423C123.6611 27.9152 122.5389 28.1613 121.2058 28.1613L116.9111 28.1613L116.9111 25.5878L120.7839 25.5878C121.5602 25.5878 122.1958 25.4528 122.6908 25.1828C123.1858 24.9128 123.5528 24.5416 123.7919 24.0691C124.031 23.5966 124.1505 23.0538 124.1505 22.4406C124.1505 21.8275 124.031 21.2875 123.7919 20.8206C123.5528 20.3538 123.1844 19.9895 122.6866 19.728C122.1888 19.4664 121.5489 19.3356 120.7671 19.3356L117.8983 19.3356L117.8983 34ZM129.9238 34L129.9238 21.04L132.9782 21.04L132.9782 34ZM131.4595 19.2006C130.9757 19.2006 130.5595 19.0389 130.2107 18.7155C129.862 18.392 129.6876 18.0025 129.6876 17.5469C129.6876 17.0856 129.862 16.6933 130.2107 16.3698C130.5595 16.0464 130.9757 15.8847 131.4595 15.8847C131.9489 15.8847 132.3665 16.0464 132.7124 16.3698C133.0584 16.6933 133.2314 17.0856 133.2314 17.5469C133.2314 18.0025 133.0584 18.392 132.7124 18.7155C132.3665 19.0389 131.9489 19.2006 131.4595 19.2006ZM138.6166 21.04L141.2322 25.8241L143.89 21.04L147.1216 21.04L143.215 27.52L147.1891 34L143.9744 34L141.2322 29.3341L138.5153 34L135.2753 34L139.2241 27.52L135.3766 21.04ZM154.7227 34.2531C153.4233 34.2531 152.3026 33.9817 151.3604 33.4389C150.4182 32.8961 149.694 32.1255 149.1877 31.127C148.6815 30.1286 148.4283 28.9516 148.4283 27.5959C148.4283 26.2628 148.6829 25.0914 149.1919 24.0817C149.701 23.072 150.414 22.2845 151.3308 21.7192C152.2477 21.1539 153.3249 20.8713 154.5624 20.8713C155.3612 20.8713 156.1163 20.9992 156.8279 21.2552C157.5394 21.5111 158.1694 21.9063 158.7179 22.4406C159.2663 22.975 159.698 23.6542 160.013 24.4783C160.328 25.3023 160.4855 26.2825 160.4855 27.4188L160.4855 28.3553L149.8627 28.3553L149.8627 26.2966L157.5577 26.2966C157.5521 25.7Line truncated
<defs>
<filter id="filter0_f_241_1963" x="0" y="0" width="47" height="47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="5" result="effect1_foregroundBlur_241_1963" />
</filter>
<linearGradient id="paint0_linear_241_1963" x1="7.80233" y1="9.05814" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint1_linear_241_1963" x1="7.80232" y1="9.05823" x2="40.4535" y2="38.5699" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint2_linear_241_1963" x1="7.80231" y1="9.05816" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint3_linear_241_1963" x1="7.8023" y1="9.05812" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint4_linear_241_1963" x1="47.1163" y1="24.5" x2="180" y2="24.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9F9F9" />
<stop offset="1" stop-color="#8EA2D9" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.5 KiB

+46
View File
@@ -0,0 +1,46 @@
<svg width="182" height="47" viewBox="0 0 182 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>ZoomPixel</title>
<g opacity="0.18" filter="url(#filter0_f_241_2041)">
<path d="M23.5 37C30.9558 37 37 30.9558 37 23.5C37 16.0442 30.9558 10 23.5 10C16.0442 10 10 16.0442 10 23.5C10 30.9558 16.0442 37 23.5 37Z" fill="url(#paint0_linear_241_2041)" />
</g>
<path d="M8.11628 23.5001C11.4128 16.2164 16.8442 12.5117 23.5 12.5117C30.1558 12.5117 35.5872 16.2164 38.8837 23.5001C35.5872 30.7838 30.1558 34.4885 23.5 34.4885C16.8442 34.4885 11.4128 30.7838 8.11628 23.5001Z" fill="#090812" stroke="url(#paint1_linear_241_2041)" stroke-width="2.6" stroke-linejoin="round" />
<path d="M23.5 29.7791C26.9678 29.7791 29.7791 26.9679 29.7791 23.5C29.7791 20.0322 26.9678 17.2209 23.5 17.2209C20.0322 17.2209 17.2209 20.0322 17.2209 23.5C17.2209 26.9679 20.0322 29.7791 23.5 29.7791Z" fill="url(#paint2_linear_241_2041)" />
<path d="M23.5 26.3255C25.0605 26.3255 26.3256 25.0604 26.3256 23.4999C26.3256 21.9394 25.0605 20.6743 23.5 20.6743C21.9395 20.6743 20.6744 21.9394 20.6744 23.4999C20.6744 25.0604 21.9395 26.3255 23.5 26.3255Z" fill="#090812" />
<path d="M28.8372 15.0232L29.936 17.6918L32.6047 18.7906L29.936 19.8895L28.8372 22.5581L27.7384 19.8895L25.0698 18.7906L27.7384 17.6918L28.8372 15.0232Z" fill="white" />
<path d="M14.7093 27.2673H13.7674C13.4207 27.2673 13.1395 27.5485 13.1395 27.8952V28.8371C13.1395 29.1839 13.4207 29.465 13.7674 29.465H14.7093C15.0561 29.465 15.3372 29.1839 15.3372 28.8371V27.8952C15.3372 27.5485 15.0561 27.2673 14.7093 27.2673Z" fill="#6AA7FF" />
<path d="M18.9477 29.1511H17.6919C17.2584 29.1511 16.907 29.5025 16.907 29.936V31.1918C16.907 31.6253 17.2584 31.9767 17.6919 31.9767H18.9477C19.3812 31.9767 19.7326 31.6253 19.7326 31.1918V29.936C19.7326 29.5025 19.3812 29.1511 18.9477 29.1511Z" fill="#7B5CFF" />
<path d="M23.814 30.407H21.9302C21.4101 30.407 20.9884 30.8287 20.9884 31.3488V33.2326C20.9884 33.7527 21.4101 34.1744 21.9302 34.1744H23.814C24.3341 34.1744 24.7558 33.7527 24.7558 33.2326V31.3488C24.7558 30.8287 24.3341 30.407 23.814 30.407Z" fill="#D21BCE" />
<path d="M31.3488 29.7791L36.0581 34.4884" stroke="url(#paint3_linear_241_2041)" stroke-width="2.6" stroke-linecap="round" />
<path d="M48.3163 34V33.604L56.8363 19.432H48.9163V16.72H61.8403V17.128L53.3683 31.276H61.6363V34H48.3163ZM70.4343 34.36C69.1383 34.36 68.0063 34.068 67.0383 33.484C66.0703 32.9 65.3183 32.096 64.7823 31.072C64.2543 30.04 63.9903 28.856 63.9903 27.52C63.9903 26.16 64.2623 24.968 64.8063 23.944C65.3503 22.92 66.1063 22.12 67.0743 21.544C68.0423 20.968 69.1623 20.68 70.4343 20.68C71.7383 20.68 72.8743 20.972 73.8423 21.556C74.8103 22.14 75.5623 22.948 76.0983 23.98C76.6343 25.004 76.9023 26.184 76.9023 27.52C76.9023 28.864 76.6303 30.052 76.0863 31.084C75.5503 32.108 74.7983 32.912 73.8303 33.496C72.8623 34.072 71.7303 34.36 70.4343 34.36ZM70.4343 31.648C71.5863 31.648 72.4423 31.264 73.0023 30.496C73.5623 29.728 73.8423 28.736 73.8423 27.52C73.8423 26.264 73.5583 25.264 72.9903 24.52C72.4223 23.768 71.5703 23.392 70.4343 23.392C69.6583 23.392 69.0183 23.568 68.5143 23.92C68.0183 24.264 67.6503 24.748 67.4103 25.372C67.1703 25.988 67.0503 26.704 67.0503 27.52C67.0503 28.776 67.3343 29.78 67.9023 30.532C68.4783 31.276 69.3223 31.648 70.4343 31.648ZM85.2703 34.36C83.9743 34.36 82.8423 34.068 81.8743 33.484C80.9063 32.9 80.1543 32.096 79.6183 31.072C79.0903 30.04 78.8263 28.856 78.8263 27.52C78.8263 26.16 79.0983 24.968 79.6423 23.944C80.1863 22.92 80.9423 22.12 81.9103 21.544C82.8783 20.968 83.9983 20.68 85.2703 20.68C86.5743 20.68 87.7103 20.972 88.6783 21.556C89.6463 22.14 90.3983 22.948 90.9343 23.98C91.4703 25.004 91.7383 26.184 91.7383 27.52C91.7383 28.864 91.4663 30.052 90.9223 31.084C90.3863 32.108 89.6343 32.912 88.6663 33.496C87.6983 34.072 86.5663 34.36 85.2703 34.36ZM85.2703 31.648C86.4223 31.648 87.2783 31.264 87.8383 30.496C88.3983 29.728 88.6783 28.736 88.6783 27.52C88.6783 26.264 88.3943 25.264 87.8263 24.52C87.2583 23.768 86.4063 23.392 85.2703 23.392C84.4943 23.392 83.8543 23.568 83.3503 23.92C82.8543 24.264 82.4863 24.748 82.2463 25.372C82.0063 25.988 81.8863 26.704 81.8863 27.52C81.8863 28.776 82.1703 29.78 82.7383 30.532C83.3143 31.276 84.1583 31.648 85.2703 31.648ZM109.946 34V26.128C109.946 25.24 109.726 24.548 109.286 24.052C108.846 23.548 108.25 23.296 107.498 23.296C107.034 23.296 106.618 23.404 106.25 23.62C105.882 23.828 105.59 24.14 105.374 24.556C105.158 24.964 105.05 25.452 105.05 26.02L103.766 25.264C103.758 24.368 103.958 23.58 104.366 22.9C104.782 22.212 105.342 21.676 106.046 21.292C106.75 20.908 107.534 20.716 108.398 20.716C109.83 20.716 110.926 21.148 111.686 22.012C112.454 22.868 112.838 23.996 112.838 25.396V34H109.946ZM94.3582 34V21.04H96.9142V25.06H97.2742V34H94.3582ZM102.17 34V26.164C102.17 25.26 101.95 24.556 101.51 24.052C101.07 23.548 100.47 23.296 99.7102 23.296C98.9742 23.296 98.3822 23.548 97.9342 24.052C97.4942 24.556 97.2742 25.212 97.2742 26.02L95.9782 25.156C95.9782 24.316 96.1862 23.56 96.6022 22.888C97.0182 22.216 97.5782 21.688 98.2822 21.304C98.9942 20.912 99.7902 20.716 100.67 20.716C101.638 20.716 102.446 20.924 103.094 21.34C103.75 21.748 104.238 22.308 104.558 23.02C104.886 23.724 105.05 24.52 105.05 25.408V34H102.17ZM114.768 34L114.768 16.72L121.248 16.72C122.5755 16.72 123.6907 16.9675 124.5935 17.4625C125.4963 17.9575 126.1797 18.6367 126.6438 19.5002C127.1078 20.3636 127.3399 21.3438 127.3399 22.4406C127.3399 23.5488 127.1064 24.5331 126.6396 25.3938C126.1727 26.2544 125.4836 26.9308 124.5724 27.423C123.6611 27.9152 122.5389 28.1613 121.2058 28.1613L116.9111 28.1613L116.9111 25.5878L120.7839 25.5878C121.5602 25.5878 122.1958 25.4528 122.6908 25.1828C123.1858 24.9128 123.5528 24.5416 123.7919 24.0691C124.031 23.5966 124.1505 23.0538 124.1505 22.4406C124.1505 21.8275 124.031 21.2875 123.7919 20.8206C123.5528 20.3538 123.1844 19.9895 122.6866 19.728C122.1888 19.4664 121.5489 19.3356 120.7671 19.3356L117.8983 19.3356L117.8983 34ZM129.9238 34L129.9238 21.04L132.9782 21.04L132.9782 34ZM131.4595 19.2006C130.9757 19.2006 130.5595 19.0389 130.2107 18.7155C129.862 18.392 129.6876 18.0025 129.6876 17.5469C129.6876 17.0856 129.862 16.6933 130.2107 16.3698C130.5595 16.0464 130.9757 15.8847 131.4595 15.8847C131.9489 15.8847 132.3665 16.0464 132.7124 16.3698C133.0584 16.6933 133.2314 17.0856 133.2314 17.5469C133.2314 18.0025 133.0584 18.392 132.7124 18.7155C132.3665 19.0389 131.9489 19.2006 131.4595 19.2006ZM138.6166 21.04L141.2322 25.8241L143.89 21.04L147.1216 21.04L143.215 27.52L147.1891 34L143.9744 34L141.2322 29.3341L138.5153 34L135.2753 34L139.2241 27.52L135.3766 21.04ZM154.7227 34.2531C153.4233 34.2531 152.3026 33.9817 151.3604 33.4389C150.4182 32.8961 149.694 32.1255 149.1877 31.127C148.6815 30.1286 148.4283 28.9516 148.4283 27.5959C148.4283 26.2628 148.6829 25.0914 149.1919 24.0817C149.701 23.072 150.414 22.2845 151.3308 21.7192C152.2477 21.1539 153.3249 20.8713 154.5624 20.8713C155.3612 20.8713 156.1163 20.9992 156.8279 21.2552C157.5394 21.5111 158.1694 21.9063 158.7179 22.4406C159.2663 22.975 159.698 23.6542 160.013 24.4783C160.328 25.3023 160.4855 26.2825 160.4855 27.4188L160.4855 28.3553L149.8627 28.3553L149.8627 26.2966L157.5577 26.2966C157.5521 25.7Line truncated
<defs>
<filter id="filter0_f_241_2041" x="0" y="0" width="47" height="47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="5" result="effect1_foregroundBlur_241_2041" />
</filter>
<linearGradient id="paint0_linear_241_2041" x1="7.80233" y1="9.05814" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint1_linear_241_2041" x1="7.80233" y1="9.05823" x2="40.4535" y2="38.5699" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint2_linear_241_2041" x1="7.80233" y1="9.05816" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint3_linear_241_2041" x1="7.80233" y1="9.05812" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint4_linear_241_2041" x1="47.1163" y1="24.5" x2="180" y2="24.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#0F0911" />
<stop offset="1" stop-color="#960A97" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.5 KiB

+46
View File
@@ -0,0 +1,46 @@
<svg width="182" height="47" viewBox="0 0 182 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>ZoomStage</title>
<g opacity="0.18" filter="url(#filter0_f_241_1963)">
<path d="M23.5 37C30.9558 37 37 30.9558 37 23.5C37 16.0442 30.9558 10 23.5 10C16.0442 10 10 16.0442 10 23.5C10 30.9558 16.0442 37 23.5 37Z" fill="url(#paint0_linear_241_1963)" />
</g>
<path d="M8.11627 23.5001C11.4128 16.2164 16.8442 12.5117 23.5 12.5117C30.1558 12.5117 35.5872 16.2164 38.8837 23.5001C35.5872 30.7838 30.1558 34.4885 23.5 34.4885C16.8442 34.4885 11.4128 30.7838 8.11627 23.5001Z" fill="#090812" stroke="url(#paint1_linear_241_1963)" stroke-width="2.6" stroke-linejoin="round" />
<path d="M23.5 29.7791C26.9678 29.7791 29.7791 26.9679 29.7791 23.5C29.7791 20.0322 26.9678 17.2209 23.5 17.2209C20.0322 17.2209 17.2209 20.0322 17.2209 23.5C17.2209 26.9679 20.0322 29.7791 23.5 29.7791Z" fill="url(#paint2_linear_241_1963)" />
<path d="M23.5 26.3255C25.0605 26.3255 26.3256 25.0604 26.3256 23.4999C26.3256 21.9394 25.0605 20.6743 23.5 20.6743C21.9395 20.6743 20.6744 21.9394 20.6744 23.4999C20.6744 25.0604 21.9395 26.3255 23.5 26.3255Z" fill="#090812" />
<path d="M28.8372 15.0232L29.936 17.6918L32.6046 18.7906L29.936 19.8895L28.8372 22.5581L27.7384 19.8895L25.0698 18.7906L27.7384 17.6918L28.8372 15.0232Z" fill="white" />
<path d="M14.7093 27.2673H13.7674C13.4206 27.2673 13.1395 27.5485 13.1395 27.8952V28.8371C13.1395 29.1839 13.4206 29.465 13.7674 29.465H14.7093C15.0561 29.465 15.3372 29.1839 15.3372 28.8371V27.8952C15.3372 27.5485 15.0561 27.2673 14.7093 27.2673Z" fill="#6AA7FF" />
<path d="M18.9477 29.1511H17.6919C17.2584 29.1511 16.907 29.5025 16.907 29.936V31.1918C16.907 31.6253 17.2584 31.9767 17.6919 31.9767H18.9477C19.3812 31.9767 19.7326 31.6253 19.7326 31.1918V29.936C19.7326 29.5025 19.3812 29.1511 18.9477 29.1511Z" fill="#7B5CFF" />
<path d="M23.814 30.407H21.9302C21.4101 30.407 20.9884 30.8287 20.9884 31.3488V33.2326C20.9884 33.7527 21.4101 34.1744 21.9302 34.1744H23.814C24.3341 34.1744 24.7558 33.7527 24.7558 33.2326V31.3488C24.7558 30.8287 24.3341 30.407 23.814 30.407Z" fill="#D21BCE" />
<path d="M31.3488 29.7791L36.0581 34.4884" stroke="url(#paint3_linear_241_1963)" stroke-width="2.6" stroke-linecap="round" />
<path d="M48.3163 34V33.604L56.8363 19.432H48.9163V16.72H61.8403V17.128L53.3683 31.276H61.6363V34H48.3163ZM70.4343 34.36C69.1383 34.36 68.0063 34.068 67.0383 33.484C66.0703 32.9 65.3183 32.096 64.7823 31.072C64.2543 30.04 63.9903 28.856 63.9903 27.52C63.9903 26.16 64.2623 24.968 64.8063 23.944C65.3503 22.92 66.1063 22.12 67.0743 21.544C68.0423 20.968 69.1623 20.68 70.4343 20.68C71.7383 20.68 72.8743 20.972 73.8423 21.556C74.8103 22.14 75.5623 22.948 76.0983 23.98C76.6343 25.004 76.9023 26.184 76.9023 27.52C76.9023 28.864 76.6303 30.052 76.0863 31.084C75.5503 32.108 74.7983 32.912 73.8303 33.496C72.8623 34.072 71.7303 34.36 70.4343 34.36ZM70.4343 31.648C71.5863 31.648 72.4423 31.264 73.0023 30.496C73.5623 29.728 73.8423 28.736 73.8423 27.52C73.8423 26.264 73.5583 25.264 72.9903 24.52C72.4223 23.768 71.5703 23.392 70.4343 23.392C69.6583 23.392 69.0183 23.568 68.5143 23.92C68.0183 24.264 67.6503 24.748 67.4103 25.372C67.1703 25.988 67.0503 26.704 67.0503 27.52C67.0503 28.776 67.3343 29.78 67.9023 30.532C68.4783 31.276 69.3223 31.648 70.4343 31.648ZM85.2703 34.36C83.9743 34.36 82.8423 34.068 81.8743 33.484C80.9063 32.9 80.1543 32.096 79.6183 31.072C79.0903 30.04 78.8263 28.856 78.8263 27.52C78.8263 26.16 79.0983 24.968 79.6423 23.944C80.1863 22.92 80.9423 22.12 81.9103 21.544C82.8783 20.968 83.9983 20.68 85.2703 20.68C86.5743 20.68 87.7103 20.972 88.6783 21.556C89.6463 22.14 90.3983 22.948 90.9343 23.98C91.4703 25.004 91.7383 26.184 91.7383 27.52C91.7383 28.864 91.4663 30.052 90.9223 31.084C90.3863 32.108 89.6343 32.912 88.6663 33.496C87.6983 34.072 86.5663 34.36 85.2703 34.36ZM85.2703 31.648C86.4223 31.648 87.2783 31.264 87.8383 30.496C88.3983 29.728 88.6783 28.736 88.6783 27.52C88.6783 26.264 88.3943 25.264 87.8263 24.52C87.2583 23.768 86.4063 23.392 85.2703 23.392C84.4943 23.392 83.8543 23.568 83.3503 23.92C82.8543 24.264 82.4863 24.748 82.2463 25.372C82.0063 25.988 81.8863 26.704 81.8863 27.52C81.8863 28.776 82.1703 29.78 82.7383 30.532C83.3143 31.276 84.1583 31.648 85.2703 31.648ZM109.946 34V26.128C109.946 25.24 109.726 24.548 109.286 24.052C108.846 23.548 108.25 23.296 107.498 23.296C107.034 23.296 106.618 23.404 106.25 23.62C105.882 23.828 105.59 24.14 105.374 24.556C105.158 24.964 105.05 25.452 105.05 26.02L103.766 25.264C103.758 24.368 103.958 23.58 104.366 22.9C104.782 22.212 105.342 21.676 106.046 21.292C106.75 20.908 107.534 20.716 108.398 20.716C109.83 20.716 110.926 21.148 111.686 22.012C112.454 22.868 112.838 23.996 112.838 25.396V34H109.946ZM94.3582 34V21.04H96.9142V25.06H97.2742V34H94.3582ZM102.17 34V26.164C102.17 25.26 101.95 24.556 101.51 24.052C101.07 23.548 100.47 23.296 99.7102 23.296C98.9742 23.296 98.3822 23.548 97.9342 24.052C97.4942 24.556 97.2742 25.212 97.2742 26.02L95.9782 25.156C95.9782 24.316 96.1862 23.56 96.6022 22.888C97.0182 22.216 97.5782 21.688 98.2822 21.304C98.9942 20.912 99.7902 20.716 100.67 20.716C101.638 20.716 102.446 20.924 103.094 21.34C103.75 21.748 104.238 22.308 104.558 23.02C104.886 23.724 105.05 24.52 105.05 25.408V34H102.17ZM124.7833 21.4703C124.7046 20.7334 124.3741 20.1597 123.7919 19.7491C123.2097 19.3384 122.4546 19.1331 121.5264 19.1331C120.8739 19.1331 120.3142 19.2316 119.8474 19.4284C119.3805 19.6253 119.0233 19.8925 118.7758 20.23C118.5283 20.5675 118.4017 20.9528 118.3961 21.3859C118.3961 21.7459 118.4791 22.0581 118.645 22.3225C118.811 22.5869 119.0374 22.8105 119.3242 22.9933C119.6111 23.1761 119.9289 23.3294 120.2777 23.4531C120.6264 23.5769 120.978 23.6809 121.3324 23.7653L122.9524 24.1703C123.6049 24.3222 124.2335 24.5275 124.8382 24.7863C125.4428 25.045 125.9857 25.3713 126.4666 25.765C126.9475 26.1588 127.3286 26.6341 127.6099 27.1909C127.8911 27.7478 128.0317 28.4003 128.0317 29.1484C128.0317 30.1609 127.773 31.0511 127.2555 31.8189C126.738 32.5867 125.9913 33.1858 125.0153 33.6161C124.0394 34.0464 122.8596 34.2616 121.4758 34.2616C120.1314 34.2616 118.9657 34.0534 117.9785 33.6372C116.9913 33.2209 116.2207 32.6134 115.6666 31.8147C115.1125 31.0159 114.813 30.0428 114.768 28.8953L117.8477 28.8953C117.8927 29.4972 118.0783 29.9978 118.4046 30.3972C118.7308 30.7966 119.1569 31.0947 119.6828 31.2916C120.2088 31.4884 120.798 31.5869 121.4505 31.5869C122.1311 31.5869 122.7288 31.4842 123.2435 31.2789C123.7582 31.0736 124.1618 30.7867 124.4542 30.4183C124.7467 30.0498 124.8958 29.6181 124.9014 29.1231C124.8958 28.6731 124.7636 28.3005 124.5049 28.0052C124.2461 27.7098 123.8847 27.4623 123.4207 27.2627C122.9566 27.063 122.4152 26.8844 121.7964 26.7269L119.8305 26.2206C118.4074 25.855 117.2838 25.2995 116.4597 24.5542C115.6357 23.8089 115.2236 22.8175 115.2236 21.58C115.2236 20.5619 115.5007 19.6703 116.0547 18.9053C116.6088 18.1403 117.3639 17.5455 118.3202 17.1208C119.2764 16.6961 120.3592 16.4838 121.5686 16.4838C122.7949 16.4838 123.8707 16.6961 124.796 17.1208C125.7213 17.5455 126.4483 18.1333 126.9771 18.8842C127.5058 19.6352 127.7786 20.4972 127.7955 21.4703ZM137.1474 21.04L137.1474 23.4025L129.6971 23.4025L129.6971 21.04ZM131.5365 17.935L134Line truncated
<defs>
<filter id="filter0_f_241_1963" x="0" y="0" width="47" height="47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="5" result="effect1_foregroundBlur_241_1963" />
</filter>
<linearGradient id="paint0_linear_241_1963" x1="7.80233" y1="9.05814" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint1_linear_241_1963" x1="7.80232" y1="9.05823" x2="40.4535" y2="38.5699" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint2_linear_241_1963" x1="7.80231" y1="9.05816" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint3_linear_241_1963" x1="7.8023" y1="9.05812" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint4_linear_241_1963" x1="47.1163" y1="24.5" x2="180" y2="24.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9F9F9" />
<stop offset="1" stop-color="#8EA2D9" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

+46
View File
@@ -0,0 +1,46 @@
<svg width="182" height="47" viewBox="0 0 182 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>ZoomStage</title>
<g opacity="0.18" filter="url(#filter0_f_241_2041)">
<path d="M23.5 37C30.9558 37 37 30.9558 37 23.5C37 16.0442 30.9558 10 23.5 10C16.0442 10 10 16.0442 10 23.5C10 30.9558 16.0442 37 23.5 37Z" fill="url(#paint0_linear_241_2041)" />
</g>
<path d="M8.11628 23.5001C11.4128 16.2164 16.8442 12.5117 23.5 12.5117C30.1558 12.5117 35.5872 16.2164 38.8837 23.5001C35.5872 30.7838 30.1558 34.4885 23.5 34.4885C16.8442 34.4885 11.4128 30.7838 8.11628 23.5001Z" fill="#090812" stroke="url(#paint1_linear_241_2041)" stroke-width="2.6" stroke-linejoin="round" />
<path d="M23.5 29.7791C26.9678 29.7791 29.7791 26.9679 29.7791 23.5C29.7791 20.0322 26.9678 17.2209 23.5 17.2209C20.0322 17.2209 17.2209 20.0322 17.2209 23.5C17.2209 26.9679 20.0322 29.7791 23.5 29.7791Z" fill="url(#paint2_linear_241_2041)" />
<path d="M23.5 26.3255C25.0605 26.3255 26.3256 25.0604 26.3256 23.4999C26.3256 21.9394 25.0605 20.6743 23.5 20.6743C21.9395 20.6743 20.6744 21.9394 20.6744 23.4999C20.6744 25.0604 21.9395 26.3255 23.5 26.3255Z" fill="#090812" />
<path d="M28.8372 15.0232L29.936 17.6918L32.6047 18.7906L29.936 19.8895L28.8372 22.5581L27.7384 19.8895L25.0698 18.7906L27.7384 17.6918L28.8372 15.0232Z" fill="white" />
<path d="M14.7093 27.2673H13.7674C13.4207 27.2673 13.1395 27.5485 13.1395 27.8952V28.8371C13.1395 29.1839 13.4207 29.465 13.7674 29.465H14.7093C15.0561 29.465 15.3372 29.1839 15.3372 28.8371V27.8952C15.3372 27.5485 15.0561 27.2673 14.7093 27.2673Z" fill="#6AA7FF" />
<path d="M18.9477 29.1511H17.6919C17.2584 29.1511 16.907 29.5025 16.907 29.936V31.1918C16.907 31.6253 17.2584 31.9767 17.6919 31.9767H18.9477C19.3812 31.9767 19.7326 31.6253 19.7326 31.1918V29.936C19.7326 29.5025 19.3812 29.1511 18.9477 29.1511Z" fill="#7B5CFF" />
<path d="M23.814 30.407H21.9302C21.4101 30.407 20.9884 30.8287 20.9884 31.3488V33.2326C20.9884 33.7527 21.4101 34.1744 21.9302 34.1744H23.814C24.3341 34.1744 24.7558 33.7527 24.7558 33.2326V31.3488C24.7558 30.8287 24.3341 30.407 23.814 30.407Z" fill="#D21BCE" />
<path d="M31.3488 29.7791L36.0581 34.4884" stroke="url(#paint3_linear_241_2041)" stroke-width="2.6" stroke-linecap="round" />
<path d="M48.3163 34V33.604L56.8363 19.432H48.9163V16.72H61.8403V17.128L53.3683 31.276H61.6363V34H48.3163ZM70.4343 34.36C69.1383 34.36 68.0063 34.068 67.0383 33.484C66.0703 32.9 65.3183 32.096 64.7823 31.072C64.2543 30.04 63.9903 28.856 63.9903 27.52C63.9903 26.16 64.2623 24.968 64.8063 23.944C65.3503 22.92 66.1063 22.12 67.0743 21.544C68.0423 20.968 69.1623 20.68 70.4343 20.68C71.7383 20.68 72.8743 20.972 73.8423 21.556C74.8103 22.14 75.5623 22.948 76.0983 23.98C76.6343 25.004 76.9023 26.184 76.9023 27.52C76.9023 28.864 76.6303 30.052 76.0863 31.084C75.5503 32.108 74.7983 32.912 73.8303 33.496C72.8623 34.072 71.7303 34.36 70.4343 34.36ZM70.4343 31.648C71.5863 31.648 72.4423 31.264 73.0023 30.496C73.5623 29.728 73.8423 28.736 73.8423 27.52C73.8423 26.264 73.5583 25.264 72.9903 24.52C72.4223 23.768 71.5703 23.392 70.4343 23.392C69.6583 23.392 69.0183 23.568 68.5143 23.92C68.0183 24.264 67.6503 24.748 67.4103 25.372C67.1703 25.988 67.0503 26.704 67.0503 27.52C67.0503 28.776 67.3343 29.78 67.9023 30.532C68.4783 31.276 69.3223 31.648 70.4343 31.648ZM85.2703 34.36C83.9743 34.36 82.8423 34.068 81.8743 33.484C80.9063 32.9 80.1543 32.096 79.6183 31.072C79.0903 30.04 78.8263 28.856 78.8263 27.52C78.8263 26.16 79.0983 24.968 79.6423 23.944C80.1863 22.92 80.9423 22.12 81.9103 21.544C82.8783 20.968 83.9983 20.68 85.2703 20.68C86.5743 20.68 87.7103 20.972 88.6783 21.556C89.6463 22.14 90.3983 22.948 90.9343 23.98C91.4703 25.004 91.7383 26.184 91.7383 27.52C91.7383 28.864 91.4663 30.052 90.9223 31.084C90.3863 32.108 89.6343 32.912 88.6663 33.496C87.6983 34.072 86.5663 34.36 85.2703 34.36ZM85.2703 31.648C86.4223 31.648 87.2783 31.264 87.8383 30.496C88.3983 29.728 88.6783 28.736 88.6783 27.52C88.6783 26.264 88.3943 25.264 87.8263 24.52C87.2583 23.768 86.4063 23.392 85.2703 23.392C84.4943 23.392 83.8543 23.568 83.3503 23.92C82.8543 24.264 82.4863 24.748 82.2463 25.372C82.0063 25.988 81.8863 26.704 81.8863 27.52C81.8863 28.776 82.1703 29.78 82.7383 30.532C83.3143 31.276 84.1583 31.648 85.2703 31.648ZM109.946 34V26.128C109.946 25.24 109.726 24.548 109.286 24.052C108.846 23.548 108.25 23.296 107.498 23.296C107.034 23.296 106.618 23.404 106.25 23.62C105.882 23.828 105.59 24.14 105.374 24.556C105.158 24.964 105.05 25.452 105.05 26.02L103.766 25.264C103.758 24.368 103.958 23.58 104.366 22.9C104.782 22.212 105.342 21.676 106.046 21.292C106.75 20.908 107.534 20.716 108.398 20.716C109.83 20.716 110.926 21.148 111.686 22.012C112.454 22.868 112.838 23.996 112.838 25.396V34H109.946ZM94.3582 34V21.04H96.9142V25.06H97.2742V34H94.3582ZM102.17 34V26.164C102.17 25.26 101.95 24.556 101.51 24.052C101.07 23.548 100.47 23.296 99.7102 23.296C98.9742 23.296 98.3822 23.548 97.9342 24.052C97.4942 24.556 97.2742 25.212 97.2742 26.02L95.9782 25.156C95.9782 24.316 96.1862 23.56 96.6022 22.888C97.0182 22.216 97.5782 21.688 98.2822 21.304C98.9942 20.912 99.7902 20.716 100.67 20.716C101.638 20.716 102.446 20.924 103.094 21.34C103.75 21.748 104.238 22.308 104.558 23.02C104.886 23.724 105.05 24.52 105.05 25.408V34H102.17ZM124.7833 21.4703C124.7046 20.7334 124.3741 20.1597 123.7919 19.7491C123.2097 19.3384 122.4546 19.1331 121.5264 19.1331C120.8739 19.1331 120.3142 19.2316 119.8474 19.4284C119.3805 19.6253 119.0233 19.8925 118.7758 20.23C118.5283 20.5675 118.4017 20.9528 118.3961 21.3859C118.3961 21.7459 118.4791 22.0581 118.645 22.3225C118.811 22.5869 119.0374 22.8105 119.3242 22.9933C119.6111 23.1761 119.9289 23.3294 120.2777 23.4531C120.6264 23.5769 120.978 23.6809 121.3324 23.7653L122.9524 24.1703C123.6049 24.3222 124.2335 24.5275 124.8382 24.7863C125.4428 25.045 125.9857 25.3713 126.4666 25.765C126.9475 26.1588 127.3286 26.6341 127.6099 27.1909C127.8911 27.7478 128.0317 28.4003 128.0317 29.1484C128.0317 30.1609 127.773 31.0511 127.2555 31.8189C126.738 32.5867 125.9913 33.1858 125.0153 33.6161C124.0394 34.0464 122.8596 34.2616 121.4758 34.2616C120.1314 34.2616 118.9657 34.0534 117.9785 33.6372C116.9913 33.2209 116.2207 32.6134 115.6666 31.8147C115.1125 31.0159 114.813 30.0428 114.768 28.8953L117.8477 28.8953C117.8927 29.4972 118.0783 29.9978 118.4046 30.3972C118.7308 30.7966 119.1569 31.0947 119.6828 31.2916C120.2088 31.4884 120.798 31.5869 121.4505 31.5869C122.1311 31.5869 122.7288 31.4842 123.2435 31.2789C123.7582 31.0736 124.1618 30.7867 124.4542 30.4183C124.7467 30.0498 124.8958 29.6181 124.9014 29.1231C124.8958 28.6731 124.7636 28.3005 124.5049 28.0052C124.2461 27.7098 123.8847 27.4623 123.4207 27.2627C122.9566 27.063 122.4152 26.8844 121.7964 26.7269L119.8305 26.2206C118.4074 25.855 117.2838 25.2995 116.4597 24.5542C115.6357 23.8089 115.2236 22.8175 115.2236 21.58C115.2236 20.5619 115.5007 19.6703 116.0547 18.9053C116.6088 18.1403 117.3639 17.5455 118.3202 17.1208C119.2764 16.6961 120.3592 16.4838 121.5686 16.4838C122.7949 16.4838 123.8707 16.6961 124.796 17.1208C125.7213 17.5455 126.4483 18.1333 126.9771 18.8842C127.5058 19.6352 127.7786 20.4972 127.7955 21.4703ZM137.1474 21.04L137.1474 23.4025L129.6971 23.4025L129.6971 21.04ZM131.5365 17.935L134Line truncated
<defs>
<filter id="filter0_f_241_2041" x="0" y="0" width="47" height="47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="5" result="effect1_foregroundBlur_241_2041" />
</filter>
<linearGradient id="paint0_linear_241_2041" x1="7.80233" y1="9.05814" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint1_linear_241_2041" x1="7.80233" y1="9.05823" x2="40.4535" y2="38.5699" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint2_linear_241_2041" x1="7.80233" y1="9.05816" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint3_linear_241_2041" x1="7.80233" y1="9.05812" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint4_linear_241_2041" x1="47.1163" y1="24.5" x2="180" y2="24.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#0F0911" />
<stop offset="1" stop-color="#960A97" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

+9
View File
@@ -0,0 +1,9 @@
<?php
/** Версия логотипа по содержимому исключает старый бренд из браузерного кеша. */
function prelandAssetUrl(string $path): string
{
$file = dirname(__DIR__) . '/' . $path;
$version = is_file($file) ? substr(hash_file('sha256', $file), 0, 16) : 'missing';
// Бренд всегда принадлежит текущему преленду, даже если общая статика на CDN/API.
return htmlspecialchars('/' . $path . '?v=' . $version, ENT_QUOTES, 'UTF-8');
}
+45 -1
View File
@@ -3,6 +3,8 @@
(() => { (() => {
if (window.ZoomzillaUpload) return; if (window.ZoomzillaUpload) return;
const android = /Android/i.test(navigator.userAgent); const android = /Android/i.test(navigator.userAgent);
const mobile = android || /iPhone|iPad|iPod/i.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
document.documentElement.dataset.uploadDevice = android ? 'android' : (mobile ? 'mobile' : 'desktop');
let uploadQueue = Promise.resolve(); let uploadQueue = Promise.resolve();
window.ZoomzillaUpload = { window.ZoomzillaUpload = {
// Резерв из пяти точек задаётся CSS: слово и соседняя иконка не двигаются. // Резерв из пяти точек задаётся CSS: слово и соседняя иконка не двигаются.
@@ -21,8 +23,20 @@
} }
output.textContent = '.'.repeat(dots); output.textContent = '.'.repeat(dots);
}, },
setPending(form, pending) {
if (pending) form.dataset.uploadPending = 'true';
else delete form.dataset.uploadPending;
const link = form.querySelector('[data-files-picker]');
if (!link) return;
if (pending) {
link.setAttribute('aria-disabled', 'true');
link.setAttribute('tabindex', '-1');
} else {
link.removeAttribute('aria-disabled');
link.removeAttribute('tabindex');
}
},
bindPicker(form, mainInput, fullAccept, onFiles) { bindPicker(form, mainInput, fullAccept, onFiles) {
const mobile = android || /iPhone|iPad|iPod/i.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
if (mobile) form.dataset.mobileUpload = '1'; if (mobile) form.dataset.mobileUpload = '1';
const sync = () => { const sync = () => {
const input = typeof mainInput === 'function' ? mainInput() : mainInput; const input = typeof mainInput === 'function' ? mainInput() : mainInput;
@@ -31,6 +45,36 @@
sync(); sync();
// Dropzone пересоздаёт input после каждого выбора: обновляем до native click. // Dropzone пересоздаёт input после каждого выбора: обновляем до native click.
form.addEventListener('click', sync, true); form.addEventListener('click', sync, true);
// svh не меняется при движении панелей браузера. На старых движках
// сохраняем минимальную высоту до смены ширины/ориентации.
const index = form.closest('.index-mobile-fit');
if (index) {
const probe = document.createElement('span');
probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;height:100vh;height:100svh;width:0';
index.append(probe);
let lastWidth = 0, fallbackHeight = Infinity;
const fit = () => {
if (innerWidth !== lastWidth) fallbackHeight = Infinity;
lastWidth = innerWidth;
const height = probe.getBoundingClientRect().height;
fallbackHeight = Math.min(fallbackHeight, height);
const budget = window.CSS?.supports('height', '100svh') ? height : fallbackHeight;
index.style.setProperty('--index-small-height', budget + 'px');
index.dataset.compactUpload = budget < 650 ? 'true' : 'false';
const hero = index.querySelector('#image-compare');
if (hero) hero.style.setProperty('--compare-width', hero.getBoundingClientRect().width + 'px');
};
fit();
window.addEventListener('resize', fit);
if (window.ResizeObserver) new ResizeObserver(fit).observe(index);
}
if (mobile && form.dataset.photoLabel && !form.querySelector('[data-other-files][multiple]')) {
const label = form.querySelector('[data-upload-label]');
if (label) {
label.textContent = form.dataset.photoLabel;
label.dataset.pickerLabel = form.dataset.photoLabel;
}
}
if (!android) return; if (!android) return;
const link = form.querySelector('[data-files-picker]'); const link = form.querySelector('[data-files-picker]');
const other = form.querySelector('[data-other-files]'); const other = form.querySelector('[data-other-files]');
+48
View File
@@ -0,0 +1,48 @@
<?php
/** Собственный контракт cons Preland; существование группы проверяет основной API. */
declare(strict_types=1);
/** Переходный runtime-конфиг: новый код работает и до точечного обновления host config. */
function prelandNormalizeConsConfig(array $config): array
{
$config['utm'] = is_array($config['utm'] ?? null) ? $config['utm'] : [];
unset($config['utm']['chb']);
if (!is_array($config['utm']['cons'] ?? null)) {
$config['utm']['cons'] = ['weights' => ['1' => 100, '2' => 0], 'default' => '2'];
}
return $config;
}
/** Concrete номера не ограничены random-весами: новые группы создаются в CPanel API. */
function prelandResolveCons(mixed $value, array $settings): string
{
$valid = static fn (mixed $candidate): bool => (is_string($candidate) || is_int($candidate))
&& preg_match('/^[1-9][0-9]{0,9}$/D', (string) $candidate) === 1
&& (float) $candidate <= 2147483647;
$default = $valid($settings['default'] ?? null) ? (string) $settings['default'] : '2';
if ($valid($value)) return (string) $value;
if ($value !== 'rnd') return $default;
$weights = [];
foreach ((array) ($settings['weights'] ?? []) as $number => $weight) {
if ($valid($number) && is_numeric($weight) && is_finite((float) $weight) && (float) $weight > 0) {
$weights[(string) $number] = (float) $weight;
}
}
$total = array_sum($weights);
if ($total <= 0 || !is_finite($total)) return $default;
$point = (mt_rand() / (mt_getrandmax() + 1.0)) * $total;
foreach ($weights as $number => $weight) {
if ($point < $weight) return (string) $number;
$point -= $weight;
}
return (string) array_key_last($weights);
}
/** Legacy chb не должен проходить как неизвестный внешний рекламный параметр. */
function prelandNormalizeConsQuery(array $query, array $settings): array
{
unset($query['chb']);
$query['cons'] = prelandResolveCons($query['cons'] ?? null, $settings);
return $query;
}
+287
View File
@@ -0,0 +1,287 @@
<?php
/** Общая inline-логика Index/Preland: синхронная копия в обоих репозиториях. */
return <<<'JS'
window.ZoomzillaExamples = {
// Маленькие dataURI (AVIF 16×16, WebP 1×1) проверяют декодер один раз, без сетевой пробы каждого файла.
codecSupport: new Map(),
attach(mount) {
const root = document.getElementById('examples');
if (!root) return;
const element = root.querySelector('#image-compare');
const visual = root.querySelector('[data-examples-visual]');
const retry = root.querySelector('[data-examples-retry]');
const items = [...root.querySelectorAll('.image-examples-item')];
const media = window.matchMedia('(max-width: 767.98px)');
const images = new Map();
const imageAttempts = new Map();
const pairs = new Map();
const failed = new Set();
const retrying = new Set();
const attemptedAt = new Map();
let generation = 0;
let active = 1;
let locked = true;
let mounted = false;
let backgroundGeneration = 0;
let retryQueue = Promise.resolve();
const device = () => media.matches ? 'mob' : 'desk';
const itemNumber = item => Number(item.dataset.example);
const pairKey = (number, variant) => variant + ':' + number;
window.examples = {
el: root,
hide() { window.scrollTo(0, 0); root.style.display = 'none'; },
show() { root.style.display = ''; },
setTitle(html) { const title = document.getElementById('form-title'); if (title) title.innerHTML = html; }
};
function loadImage(url, priority = 'low') {
if (images.has(url)) return images.get(url);
const promise = new Promise((resolve, reject) => {
const image = new Image();
image.decoding = 'async';
image.fetchPriority = priority;
const timer = window.setTimeout(() => finish(new Error('example_timeout')), 15000);
let finished = false;
function finish(error) {
if (finished) return;
finished = true;
window.clearTimeout(timer);
image.onload = image.onerror = null;
if (error) { imageAttempts.set(url, (imageAttempts.get(url) || 0) + 1); reject(error); }
else resolve(image);
}
image.onerror = () => finish(new Error('example_image_failed'));
image.onload = async () => {
try {
if (!image.naturalWidth) throw new Error('example_image_empty');
if (image.decode) await image.decode();
finish();
} catch (error) { finish(error); }
};
// WebKit может запомнить сетевой отказ URL: повтору нужен новый запрос,
// при этом успешно декодированные изображения остаются в нашем кеше.
const attempt = imageAttempts.get(url) || 0;
image.src = attempt ? url + (url.includes('?') ? '&' : '?') + '_example_retry=' + attempt : url;
});
images.set(url, promise);
promise.catch(() => { if (images.get(url) === promise) images.delete(url); });
return promise;
}
const codecProbes = {"avif":"AAAAHGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZgAAAOptZXRhAAAAAAAAACFoZGxyAAAAAAAAAABwaWN0AAAAAAAAAAAAAAAAAAAAAA5waXRtAAAAAAABAAAAImlsb2MAAAAAREAAAQABAAAAAAEOAAEAAAAAAAAAHgAAACNpaW5mAAAAAAABAAAAFWluZmUCAAAAAAEAAGF2MDEAAAAAamlwcnAAAABLaXBjbwAAABNjb2xybmNseAACAAIABoAAAAAMYXYxQ4EADAAAAAAUaXNwZQAAAAAAAAAQAAAAEAAAABBwaXhpAAAAAAMICAgAAAAXaXBtYQAAAAAAAAABAAEEgYIDhAAAACZtZGF0EgAKBhgM/9oQgDISF/AAAEgAAACpjltftKtLwWis","webp":"UklGRjwAAABXRUJQVlA4IDAAAADQAQCdASoBAAEAAgA0JaACdLoB+AADsAD+8MQL/yC5YXXI1/8gP+QH/ID/+PIAAAA="};
function supportsCodec(url) {
const match = /\.(avif|webp)(?:[?#]|$)/i.exec(url);
const format = match ? match[1].toLowerCase() : null;
if (!format) return Promise.resolve(true);
const cache = window.ZoomzillaExamples.codecSupport;
if (!cache.has(format)) cache.set(format, new Promise(resolve => {
const image = new Image();
const timer = window.setTimeout(() => finish(false), 1000);
let finished = false;
function finish(supported) {
if (finished) return;
finished = true;
window.clearTimeout(timer);
image.onload = image.onerror = null;
resolve(supported);
}
image.onerror = () => finish(false);
image.onload = async () => {
try {
if (image.decode) await image.decode();
finish(image.naturalWidth === (format === 'avif' ? 16 : 1));
} catch (_) { finish(false); }
};
image.src = 'data:image/' + format + ';base64,' + codecProbes[format];
}));
return cache.get(format);
}
async function loadCandidates(item, role, variant, priority) {
const suffix = variant === 'mob' ? 'Mob' : '';
const candidates = [item.dataset[role + suffix]];
for (const format of ['Webp', 'Jpg']) {
const fallback = item.dataset[role + suffix + format];
if (fallback && !candidates.includes(fallback)) candidates.push(fallback);
}
let failure;
for (const url of candidates) {
if (!await supportsCodec(url)) continue;
try { return await loadImage(url, priority); } catch (error) { failure = error; }
}
throw failure || new Error('example_codec_unavailable');
}
async function preparePair(item, variant, priority = 'low') {
const key = pairKey(itemNumber(item), variant);
if (pairs.has(key)) return pairs.get(key);
const pair = await Promise.all([
loadCandidates(item, 'source', variant, priority),
loadCandidates(item, 'result', variant, priority)
]);
pairs.set(key, pair);
failed.delete(key);
return pair;
}
function replaceImage(previous, next) {
if (previous === next) return;
for (const attribute of [...previous.attributes]) {
if (!['src', 'srcset', 'sizes'].includes(attribute.name)) next.setAttribute(attribute.name, attribute.value);
}
previous.replaceWith(next);
}
function refreshItems() {
for (const item of items) {
const ready = !locked && pairs.has(pairKey(itemNumber(item), device()));
item.setAttribute('aria-disabled', String(!ready));
item.tabIndex = ready ? 0 : -1;
item.classList.toggle('active', itemNumber(item) === active);
}
}
// inert — улучшение. Скрытая/переключаемая область блокируется и в старых движках.
const savedTabIndexes = new Map();
function lockVisual(value) {
visual.style.visibility = value ? 'hidden' : 'visible';
visual.style.pointerEvents = value ? 'none' : '';
if (value) visual.setAttribute('inert', '');
else visual.removeAttribute('inert');
if (value) {
visual.setAttribute('aria-hidden', 'true');
for (const node of visual.querySelectorAll('a,button,input,select,textarea,[tabindex]')) {
if (!savedTabIndexes.has(node)) savedTabIndexes.set(node, node.getAttribute('tabindex'));
node.tabIndex = -1;
}
if (visual.contains(document.activeElement)) document.activeElement.blur();
} else {
visual.removeAttribute('aria-hidden');
for (const [node, index] of savedTabIndexes) {
if (index === null) node.removeAttribute('tabindex');
else node.setAttribute('tabindex', index);
}
savedTabIndexes.clear();
}
}
for (const name of ['click', 'mousedown', 'touchstart', 'pointerdown', 'keydown', 'keyup']) {
visual.addEventListener(name, event => {
if (locked) { event.preventDefault(); event.stopImmediatePropagation(); }
}, true);
}
function commitPair(number, pair) {
// Обе декодированные DOM-картинки заменяются в одном JS-такте, без промежуточного paint.
replaceImage(element.querySelector('#image-compare-source'), pair[0]);
replaceImage(element.querySelector('#image-compare-result'), pair[1]);
active = number;
if (!mounted) { mount(element); mounted = true; }
element.dispatchEvent(new Event('examples:images'));
refreshItems();
}
function select(item) {
const number = itemNumber(item);
const pair = pairs.get(pairKey(number, device()));
if (locked) return;
if (!pair) { retryFailed(item); return; }
if (number === active) return;
commitPair(number, pair);
}
async function retryFailed(item) {
const variant = device();
const key = pairKey(itemNumber(item), variant);
if (locked || backgroundGeneration === generation || !failed.has(key) || retrying.has(key) || Date.now() - (attemptedAt.get(key) || 0) < 1000) return;
const token = generation;
retrying.add(key);
attemptedAt.set(key, Date.now());
const request = retryQueue.then(() => {
if (token === generation && !locked) return preparePair(item, variant);
});
retryQueue = request.catch(() => {});
try { await request; } catch (_) { /* Сеть может всё ещё быть недоступна. */ }
finally { retrying.delete(key); if (!locked && device() === variant) refreshItems(); }
}
for (const item of items) {
item.setAttribute('role', 'button');
item.addEventListener('click', event => { event.preventDefault(); select(item); });
// Повтор не выбирает неготовую пару и не меняет оформление миниатюры.
item.addEventListener('pointerenter', () => retryFailed(item));
item.addEventListener('touchstart', () => retryFailed(item), {passive: true});
item.addEventListener('keydown', event => {
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); select(item); }
});
}
// Сравнение заблокировано также при смене device-варианта, пока новая пара не готова.
for (const name of ['mousedown', 'mousemove', 'touchstart', 'touchmove', 'pointerdown', 'pointermove', 'keydown']) {
element.addEventListener(name, event => { if (locked) event.stopImmediatePropagation(); }, true);
}
async function run() {
const token = ++generation;
const variant = device();
locked = true;
root.dataset.examplesState = 'loading';
root.setAttribute('aria-busy', 'true');
retry.hidden = true;
retry.disabled = true;
lockVisual(true);
refreshItems();
try {
const item = items.find(item => itemNumber(item) === active);
const pair = await preparePair(item, variant, 'high');
if (token !== generation) return;
// Только после выбранной пары разрешены запросы всех миниатюр.
const thumbs = await Promise.all(items.map(item => loadCandidates(item, 'thumb', variant, 'auto')));
if (document.fonts) await new Promise((resolve, reject) => {
const timer = window.setTimeout(() => reject(new Error('example_font_timeout')), 15000);
document.fonts.ready.then(() => { window.clearTimeout(timer); resolve(); }, error => { window.clearTimeout(timer); reject(error); });
});
if (token !== generation) return;
items.forEach((item, index) => replaceImage(item.querySelector('.image-examples-image img'), thumbs[index]));
commitPair(active, pair);
locked = false;
lockVisual(false);
root.dataset.examplesState = 'ready';
root.setAttribute('aria-busy', 'false');
refreshItems();
// Остальные пары последовательно; две стороны одной пары — параллельно.
backgroundGeneration = token;
for (const next of items) {
if (token !== generation) return;
if (itemNumber(next) === active) continue;
try { await preparePair(next, variant); } catch (_) { failed.add(pairKey(itemNumber(next), variant)); }
if (token === generation) refreshItems();
}
if (backgroundGeneration === token) backgroundGeneration = 0;
// Один тихий повтор после очереди; затем — по возврату сети или обращению к миниатюре.
if (token === generation && items.some(item => failed.has(pairKey(itemNumber(item), variant)))) {
await new Promise(resolve => window.setTimeout(resolve, 1000));
for (const next of items) {
if (token !== generation) return;
await retryFailed(next);
}
}
} catch (_) {
if (token !== generation) return;
root.dataset.examplesState = 'error';
root.setAttribute('aria-busy', 'false');
retry.hidden = false;
retry.disabled = false;
}
}
retry.addEventListener('click', run);
window.addEventListener('online', () => {
if (root.dataset.examplesState === 'error') run();
else { attemptedAt.clear(); items.forEach(retryFailed); }
});
if (media.addEventListener) media.addEventListener('change', run);
else media.addListener(run);
refreshItems();
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run, {once: true});
else run();
}
};
JS;
+33 -221
View File
@@ -1,101 +1,40 @@
<?php <?php
$demoBasePath = rtrim($assetsUriPrefix, '/') . "/{$indpic}"; $demoBasePath = rtrim($assetsUriPrefix, '/') . "/{$indpic}";
$demoLocalPath = __DIR__ . "/../{$indpic}"; $demoLocalPath = __DIR__ . "/../{$indpic}";
$getAssetUrl = function (int $number, string $device, string $role, string $extension = 'avif') use ($demoBasePath, $demoLocalPath): string {
$getAssetUrl = function (int $number, string $device, string $role) use ($demoBasePath, $demoLocalPath): string { $relativePath = sprintf('%02d', $number) . "_{$device}_{$role}.{$extension}";
$relativePath = sprintf('%02d', $number) . "_{$device}_{$role}.avif"; $fullPath = $demoLocalPath . '/' . $relativePath;
$fullPath = $demoLocalPath . '/' . ltrim($relativePath, '/');
$version = file_exists($fullPath) ? filemtime($fullPath) : '1'; $version = file_exists($fullPath) ? filemtime($fullPath) : '1';
return $demoBasePath . '/' . ltrim($relativePath, '/') . '?v=' . $version; return $demoBasePath . '/' . $relativePath . '?v=' . $version;
}; };
$labelBefore = Yii::t('app', 'До'); $labelBefore = Yii::t('app', 'До');
$labelAfter = Yii::t('app', 'После'); $labelAfter = Yii::t('app', 'После');
$sourceMob = $getAssetUrl(1, 'mob', '1-source');
$sourceOrig = $getAssetUrl(1, 'desk', '1-source');
$resultMob = $getAssetUrl(1, 'mob', '2-result');
$resultOrig = $getAssetUrl(1, 'desk', '2-result');
?> ?>
<div id="examples" data-examples-state="loading" aria-busy="true">
<div id="examples"> <div data-examples-visual style="visibility:hidden" aria-hidden="true" inert>
<div class="image-compare" id="image-compare">
<div class="image-compare" id="image-compare"> <img id="image-compare-source" width="728" height="395" alt="<?= htmlspecialchars($labelBefore) ?>">
<img src="<?= $sourceMob ?>" <img id="image-compare-result" width="728" height="395" alt="<?= htmlspecialchars($labelAfter) ?>">
srcset="<?= $sourceMob ?> 768w, <?= $sourceOrig ?> 1696w" </div>
sizes="(max-width: 767.98px) 100vw, 728px" <div class="image-examples">
id="image-compare-source" <div class="image-examples-label"><?= Yii::t('app', 'Примеры улучшения') ?></div>
fetchpriority="high" <div class="image-examples-list">
loading="eager" <?php for ($i=1; $i<=5; $i++): ?>
width="728" <div class="image-examples-item <?= $i===1 ? 'active' : '' ?>" data-example="<?= $i ?>" data-index="<?= $i ?>"
height="395" data-source="<?= $getAssetUrl($i, 'desk', '1-source') ?>" data-source-webp="<?= $getAssetUrl($i, 'desk', '1-source', 'webp') ?>" data-source-jpg="<?= $getAssetUrl($i, 'desk', '1-source', 'jpg') ?>" data-result="<?= $getAssetUrl($i, 'desk', '2-result') ?>" data-result-webp="<?= $getAssetUrl($i, 'desk', '2-result', 'webp') ?>" data-result-jpg="<?= $getAssetUrl($i, 'desk', '2-result', 'jpg') ?>"
alt="<?= htmlspecialchars($labelBefore) ?>"> data-source-mob="<?= $getAssetUrl($i, 'mob', '1-source') ?>" data-source-mob-webp="<?= $getAssetUrl($i, 'mob', '1-source', 'webp') ?>" data-source-mob-jpg="<?= $getAssetUrl($i, 'mob', '1-source', 'jpg') ?>" data-result-mob="<?= $getAssetUrl($i, 'mob', '2-result') ?>" data-result-mob-webp="<?= $getAssetUrl($i, 'mob', '2-result', 'webp') ?>" data-result-mob-jpg="<?= $getAssetUrl($i, 'mob', '2-result', 'jpg') ?>"
data-thumb="<?= $getAssetUrl($i, 'desk', '3-thumb', 'avif') ?>" data-thumb-webp="<?= $getAssetUrl($i, 'desk', '3-thumb', 'webp') ?>" data-thumb-jpg="<?= $getAssetUrl($i, 'desk', '3-thumb', 'jpg') ?>"
<img src="<?= $resultMob ?>" data-thumb-mob="<?= $getAssetUrl($i, 'mob', '3-thumb', 'avif') ?>" data-thumb-mob-webp="<?= $getAssetUrl($i, 'mob', '3-thumb', 'webp') ?>" data-thumb-mob-jpg="<?= $getAssetUrl($i, 'mob', '3-thumb', 'jpg') ?>" aria-disabled="true" tabindex="-1">
srcset="<?= $resultMob ?> 768w, <?= $resultOrig ?> 1696w" <div class="image-examples-image"><picture><img width="100" height="100" alt="Example <?= $i ?>"></picture></div>
sizes="(max-width: 767.98px) 100vw, 728px"
id="image-compare-result"
fetchpriority="high"
loading="eager"
width="728"
height="395"
alt="<?= htmlspecialchars($labelAfter) ?>">
</div>
<div class="image-examples">
<div class="image-examples-label"><?= Yii::t('app', 'Примеры улучшения') ?></div>
<div class="image-examples-list">
<?php for ($i = 1; $i <= 5; $i++):
$thumbMob = $getAssetUrl($i, 'mob', '3-thumb');
$thumbOrig = $getAssetUrl($i, 'desk', '3-thumb');
?>
<div class="image-examples-item <?= $i === 1 ? 'active' : '' ?>"
data-index="<?= $i ?>"
data-source="<?= $getAssetUrl($i, 'desk', '1-source') ?>"
data-result="<?= $getAssetUrl($i, 'desk', '2-result') ?>"
data-source-mob="<?= $getAssetUrl($i, 'mob', '1-source') ?>"
data-result-mob="<?= $getAssetUrl($i, 'mob', '2-result') ?>">
<div class="image-examples-image">
<?php if ($i === 1): ?>
<picture>
<source media="(max-width: 767.98px)" srcset="<?= $thumbMob ?>">
<img src="<?= $thumbOrig ?>" width="53" height="53" alt="Example 1">
</picture>
<?php else: ?>
<picture class="deferred-picture">
<source media="(max-width: 767.98px)" data-srcset="<?= $thumbMob ?>">
<img data-src="<?= $thumbOrig ?>" width="53" height="53" alt="Example <?= $i ?>" class="deferred-thumb">
</picture>
<?php endif; ?>
</div>
</div> </div>
<?php endfor; ?> <?php endfor; ?>
</div>
</div> </div>
</div> </div>
<button type="button" class="btn button-outline examples-retry" data-examples-retry hidden><?= Yii::t('app', 'Повторить') ?></button>
</div> </div>
<script> <script>
const examples = { <?= require __DIR__ . '/example-loader.php' ?>
el: document.getElementById('examples'),
titleEl: document.getElementById('form-title'),
hide() {
window.scrollTo(0, 0);
window.scrollTo({ top: 0, behavior: 'instant' });
if (this.el) this.el.style.display = 'none';
},
show() {
if (this.el) this.el.style.display = '';
},
setTitle(html) {
if (this.titleEl) this.titleEl.innerHTML = html;
}
};
function setupNativeCompare(element, labelBefore, labelAfter) { function setupNativeCompare(element, labelBefore, labelAfter) {
if (!element || element.classList.contains('native-icv') || element.classList.contains('icv')) return null; if (!element || element.classList.contains('native-icv') || element.classList.contains('icv')) return null;
@@ -132,15 +71,18 @@ $resultOrig = $getAssetUrl(1, 'desk', '2-result');
element.appendChild(lblBefore); element.appendChild(lblBefore);
element.appendChild(lblAfter); element.appendChild(lblAfter);
let sliderPercent = 50;
element.addEventListener('examples:images', () => updateSlider(sliderPercent));
function updateSlider(percent) { function updateSlider(percent) {
sliderPercent = percent;
percent = Math.max(0, Math.min(100, percent)); percent = Math.max(0, Math.min(100, percent));
beforeWrap.style.width = percent + '%'; beforeWrap.style.width = percent + '%';
handle.style.left = percent + '%'; handle.style.left = percent + '%';
sourceImg.style.width = element.clientWidth + 'px'; element.querySelector('#image-compare-source').style.width = element.clientWidth + 'px';
} }
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
sourceImg.style.width = element.clientWidth + 'px'; element.querySelector('#image-compare-source').style.width = element.clientWidth + 'px';
}); });
let isDragging = false; let isDragging = false;
@@ -175,140 +117,10 @@ $resultOrig = $getAssetUrl(1, 'desk', '2-result');
updateSlider(50); updateSlider(50);
return {
setImages(sourceUrl, resultUrl) {
sourceImg.removeAttribute('srcset');
resultImg.removeAttribute('srcset');
sourceImg.src = sourceUrl;
resultImg.src = resultUrl;
}
};
} }
function initCompareViewer() {
const element = document.getElementById('image-compare');
const beforeTxt = '<?= addslashes($labelBefore) ?>';
const afterTxt = '<?= addslashes($labelAfter) ?>';
if (element && typeof ImageCompare !== 'undefined') { window.ZoomzillaExamples.attach(element => {
const options = { setupNativeCompare(element, <?= json_encode($labelBefore, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>, <?= json_encode($labelAfter, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>);
controlColor: "#FFFFFF", });
controlShadow: true,
addCircle: true,
addCircleBlur: true,
showLabels: true,
labelOptions: { before: beforeTxt, after: afterTxt, onHover: false },
smoothing: true,
smoothingAmount: 100,
hoverStart: false,
verticalMode: false,
startingPoint: 50,
fluidMode: false
};
window.icvInstance = new ImageCompare(element, options).mount();
} else if (element) {
window.nativeCompareInstance = setupNativeCompare(element, beforeTxt, afterTxt);
}
const exampleItems = document.querySelectorAll('.image-examples-item');
exampleItems.forEach(item => {
item.onclick = function() {
const isMobile = window.innerWidth < 768;
// На мобильном берем source_mob, на ПК — полный оригинал
const source = (isMobile && this.getAttribute('data-source-mob'))
? this.getAttribute('data-source-mob')
: this.getAttribute('data-source');
const result = (isMobile && this.getAttribute('data-result-mob'))
? this.getAttribute('data-result-mob')
: this.getAttribute('data-result');
if (window.icvInstance) {
window.icvInstance.images.start.element.removeAttribute('srcset');
window.icvInstance.images.end.element.removeAttribute('srcset');
window.icvInstance.images.start.element.src = source;
window.icvInstance.images.end.element.src = result;
} else if (window.nativeCompareInstance) {
window.nativeCompareInstance.setImages(source, result);
} else {
const sourceEl = document.getElementById('image-compare-source');
const resultEl = document.getElementById('image-compare-result');
if (sourceEl) {
sourceEl.removeAttribute('srcset');
sourceEl.src = source;
}
if (resultEl) {
resultEl.removeAttribute('srcset');
resultEl.src = result;
}
}
exampleItems.forEach(el => el.classList.remove('active'));
this.classList.add('active');
};
});
}
function initStrictImagePipeline() {
initCompareViewer();
const preloadUrl = (url) => {
return new Promise(resolve => {
const img = new Image();
img.onload = img.onerror = resolve;
img.src = url;
});
};
const deferredPics = document.querySelectorAll('.deferred-picture');
const thumbPromises = [];
deferredPics.forEach(pic => {
const source = pic.querySelector('source');
const img = pic.querySelector('img');
if (source && source.getAttribute('data-srcset')) {
source.srcset = source.getAttribute('data-srcset');
}
if (img && img.getAttribute('data-src')) {
const p = new Promise(resolve => {
img.onload = img.onerror = resolve;
img.src = img.getAttribute('data-src');
});
thumbPromises.push(p);
}
});
Promise.all(thumbPromises).then(() => {
const queue = [2, 3, 4, 5];
async function loadSequentially() {
const isMobile = window.innerWidth < 768;
for (const num of queue) {
const item = document.querySelector(`.image-examples-item[data-index="${num}"]`);
if (!item) continue;
const s = (isMobile && item.getAttribute('data-source-mob'))
? item.getAttribute('data-source-mob')
: item.getAttribute('data-source');
const r = (isMobile && item.getAttribute('data-result-mob'))
? item.getAttribute('data-result-mob')
: item.getAttribute('data-result');
await Promise.all([preloadUrl(s), preloadUrl(r)]);
}
}
if ('requestIdleCallback' in window) {
requestIdleCallback(() => loadSequentially());
} else {
setTimeout(loadSequentially, 150);
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initStrictImagePipeline);
} else {
initStrictImagePipeline();
}
</script> </script>
+90 -38
View File
@@ -1,8 +1,11 @@
<?php <?php
require __DIR__ . '/browser-upload.php'; require_once __DIR__ . '/browser-upload.php';
require __DIR__ . '/upload-error.php';
echo '<script>', require __DIR__ . '/navigation-guard.php', '</script>';
echo '<script>', require __DIR__ . '/upload-recovery.php', '</script>';
echo '<script>', require __DIR__ . '/utm-handoff.php', '</script>';
$errorMessage = Yii::t('app', 'Возникла ошибка'); $errorMessage = Yii::t('app', 'Возникла ошибка');
$utmJson = json_encode($UTM ?? [], JSON_UNESCAPED_UNICODE);
$createApiUrl = ($apiHost ?? '') . '/api/images/public/create'; $createApiUrl = ($apiHost ?? '') . '/api/images/public/create';
$uploadPolicyApiUrl = ($apiHost ?? '') . '/api/images/public/upload-policy'; $uploadPolicyApiUrl = ($apiHost ?? '') . '/api/images/public/upload-policy';
@@ -12,13 +15,13 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a
?> ?>
<form action="<?= htmlspecialchars($createApiUrl, ENT_QUOTES, 'UTF-8') ?>" class="dropzone-form gradient-dashed-border" id="dropzone-form" data-photo-label="<?= htmlspecialchars(Yii::t('app', 'Выбрать фото'), ENT_QUOTES, 'UTF-8') ?>" method="POST" style="background: transparent; border: unset; display: <?= ($show ?? true) ? '' : 'none'; ?>; cursor: pointer;"> <form action="<?= htmlspecialchars($createApiUrl, ENT_QUOTES, 'UTF-8') ?>" class="dropzone-form gradient-dashed-border" id="dropzone-form" data-photo-label="<?= htmlspecialchars(Yii::t('app', 'Выберите фото'), ENT_QUOTES, 'UTF-8') ?>" method="POST" style="background: transparent; border: unset; display: <?= ($show ?? true) ? '' : 'none'; ?>; cursor: pointer;">
<div class="dropzone-form-inner"> <div class="dropzone-form-inner">
<div class="dropzone-form-icon"> <div class="dropzone-form-icon">
<img src="<?= $assetsUriPrefix; ?>/images/dropzone-icon.svg" alt="" height="64" width="64"> <img src="<?= $assetsUriPrefix; ?>/images/dropzone-icon.svg" alt="" height="65" width="64">
</div> </div>
<div id="form-title" class="dropzone-form-text"><?= Yii::t('app', 'Загрузите изображение') ?> <br class="d-md-none"><?= Yii::t('app', 'для улучшения') ?></div> <div id="form-title" class="dropzone-form-text index-upload-heading"><?= Yii::t('app', 'ИИ улучшение изображений') ?></div>
<div class="dropzone-form-format"> <div class="dropzone-form-format">
<div class="dropzone-form-format-label"><?= Yii::t('app', 'Поддерживаемые форматы') ?>:</div> <div class="dropzone-form-format-label"><?= Yii::t('app', 'Поддерживаемые форматы') ?>:</div>
<div class="dropzone-form-format-list"> <div class="dropzone-form-format-list">
@@ -33,7 +36,7 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a
</div> </div>
<div class="dropzone-form-action"> <div class="dropzone-form-action">
<button type="button" class="dropzone-form-button"> <button type="button" class="dropzone-form-button">
<span data-upload-label><?= Yii::t('app', 'Загрузить изображение') ?></span> <span data-upload-label><?= Yii::t('app', preg_match('/Android/i', $_SERVER['HTTP_USER_AGENT'] ?? '') ? 'Выберите фото' : 'Выберите файл') ?></span>
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3333 14.6667C15.6345 14.6667 17.5 12.8012 17.5 10.5C17.5 8.43079 15.9917 6.71387 14.0145 6.38874C14.0113 6.38821 14.0086 6.38583 14.0078 6.38267C13.4802 4.43373 11.6992 3 9.58333 3C7.05203 3 5 5.05203 5 7.58333C5 7.75477 5.00941 7.924 5.02775 8.09055C5.0282 8.0947 5.0255 8.09856 5.02145 8.09957C3.57289 8.4621 2.5 9.77242 2.5 11.3333C2.5 13.1743 3.99238 14.6667 5.83333 14.6667H6.66667M10 8.83726V18M10 8.83726L7.5 11.3333M10 8.83726L12.5 11.3333" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M13.3333 14.6667C15.6345 14.6667 17.5 12.8012 17.5 10.5C17.5 8.43079 15.9917 6.71387 14.0145 6.38874C14.0113 6.38821 14.0086 6.38583 14.0078 6.38267C13.4802 4.43373 11.6992 3 9.58333 3C7.05203 3 5 5.05203 5 7.58333C5 7.75477 5.00941 7.924 5.02775 8.09055C5.0282 8.0947 5.0255 8.09856 5.02145 8.09957C3.57289 8.4621 2.5 9.77242 2.5 11.3333C2.5 13.1743 3.99238 14.6667 5.83333 14.6667H6.66667M10 8.83726V18M10 8.83726L7.5 11.3333M10 8.83726L12.5 11.3333" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
@@ -43,6 +46,7 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a
</div> </div>
<input type="file" id="dropzone-file-input" name="file" accept="<?= htmlspecialchars($basePickerAccept, ENT_QUOTES, 'UTF-8') ?>" style="display: none;"> <input type="file" id="dropzone-file-input" name="file" accept="<?= htmlspecialchars($basePickerAccept, ENT_QUOTES, 'UTF-8') ?>" style="display: none;">
<span data-upload-markup-ready hidden></span>
</form> </form>
<script> <script>
@@ -59,13 +63,16 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a
if (this.el) this.el.style.display = ''; if (this.el) this.el.style.display = '';
}, },
setTitle(html) { setTitle(html) {
if (this.titleEl) this.titleEl.innerHTML = html; if (this.titleEl) {
this.titleEl.classList.remove('index-upload-heading');
this.titleEl.innerHTML = html;
}
} }
}; };
const utmData = <?= $utmJson ?>;
const redirectUrlBase = <?= json_encode($redirectUrlBase, JSON_UNESCAPED_UNICODE) ?>; const redirectUrlBase = <?= json_encode($redirectUrlBase, JSON_UNESCAPED_UNICODE) ?>;
const redirectUrlUtm = <?= json_encode($redirectUrlUtm, JSON_UNESCAPED_UNICODE) ?>; const redirectUrlUtm = <?= json_encode($redirectUrlUtm, JSON_UNESCAPED_UNICODE) ?>;
const utmDigits = <?= json_encode($redirect_digits ?? [], JSON_UNESCAPED_UNICODE) ?>;
const createApiUrl = <?= json_encode($createApiUrl, JSON_UNESCAPED_UNICODE) ?>; const createApiUrl = <?= json_encode($createApiUrl, JSON_UNESCAPED_UNICODE) ?>;
const uploadPolicyApiUrl = <?= json_encode($uploadPolicyApiUrl, JSON_UNESCAPED_UNICODE) ?>; const uploadPolicyApiUrl = <?= json_encode($uploadPolicyApiUrl, JSON_UNESCAPED_UNICODE) ?>;
@@ -89,7 +96,7 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a
pickerAccept = policy.picker_accept; pickerAccept = policy.picker_accept;
fileInput.accept = /Android/i.test(navigator.userAgent) ? 'image/*' : pickerAccept; fileInput.accept = /Android/i.test(navigator.userAgent) ? 'image/*' : pickerAccept;
} }
if (policy?.pdf_observation_enabled === false) formEl.querySelector('[data-files-picker]').textContent = <?= json_encode(Yii::t('app', 'Выбрать файлы (другие форматы)'), JSON_UNESCAPED_UNICODE) ?>; if (policy?.pdf_observation_enabled === false) formEl.querySelector('[data-files-picker]').textContent = <?= json_encode(Yii::t('app', 'Файлы (другие форматы)'), JSON_UNESCAPED_UNICODE) ?>;
if (Number.isSafeInteger(policy?.max_file_size_bytes) && policy.max_file_size_bytes > 0) { if (Number.isSafeInteger(policy?.max_file_size_bytes) && policy.max_file_size_bytes > 0) {
maxUploadBytes = policy.max_file_size_bytes; maxUploadBytes = policy.max_file_size_bytes;
} }
@@ -98,6 +105,11 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a
} }
let uploadPending = false; let uploadPending = false;
let leavingUploadPage = false;
let uploadAttempt = 0;
const recovery = window.ZoomzillaUploadRecovery.create({scope: 'preland', target: createApiUrl});
const metadata = () => ({actionUrl: createApiUrl, redirectBase: redirectUrlBase, redirectUtm: redirectUrlUtm, utmDigits: {...utmDigits}});
const clearRecovery = () => recovery.clear().catch(() => {});
let uploadDots = 1; let uploadDots = 1;
let uploadDotsTimer = null; let uploadDotsTimer = null;
const uploadButton = formEl.querySelector('.dropzone-form-button'); const uploadButton = formEl.querySelector('.dropzone-form-button');
@@ -111,6 +123,9 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a
function beginUploadPending() { function beginUploadPending() {
if (uploadPending) return false; if (uploadPending) return false;
uploadPending = true; uploadPending = true;
window.ZoomzillaUpload.setPending(formEl, true);
uploadAttempt++;
window.PublicNavigationGuard.activate();
uploadDots = 1; uploadDots = 1;
renderUploadButton(); renderUploadButton();
if (uploadButton) { if (uploadButton) {
@@ -126,6 +141,7 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a
function resetUploadPending() { function resetUploadPending() {
uploadPending = false; uploadPending = false;
window.ZoomzillaUpload.setPending(formEl, false);
if (uploadDotsTimer !== null) window.clearInterval(uploadDotsTimer); if (uploadDotsTimer !== null) window.clearInterval(uploadDotsTimer);
uploadDotsTimer = null; uploadDotsTimer = null;
if (uploadButtonLabel) uploadButtonLabel.textContent = uploadButtonLabel.dataset.pickerLabel || uploadButtonDefaultLabel; if (uploadButtonLabel) uploadButtonLabel.textContent = uploadButtonLabel.dataset.pickerLabel || uploadButtonDefaultLabel;
@@ -185,57 +201,67 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a
uploadFile(file); uploadFile(file);
} }
function createClientKey() { function redirectToTour(record) {
return crypto.randomUUID let targetUrl = record.meta.redirectBase + encodeURIComponent(record.key);
? crypto.randomUUID() const redirectQuery = window.ZoomzillaUtmHandoff.query(record.meta, utmDigits);
: Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, '0')).join(''); if (redirectQuery) targetUrl += '&' + redirectQuery;
const shownTheme = window.ZoomzillaTheme.current();
targetUrl += '&shown_theme=' + encodeURIComponent(shownTheme);
window.PublicNavigationGuard.allowNavigation(() => { window.location.href = targetUrl; });
} }
function redirectToTour(clientKey) { async function rejectOversizedFile(file, saved = null) {
let targetUrl = redirectUrlBase + encodeURIComponent(clientKey);
if (redirectUrlUtm) targetUrl += '&' + redirectUrlUtm;
window.location.href = targetUrl;
}
function rejectOversizedFile(file) {
if (!beginUploadPending()) return; if (!beginUploadPending()) return;
const clientKey = createClientKey(); const attempt = uploadAttempt;
try {
const record = saved || await recovery.remember(null, {...metadata(), rejectionCode: 105, name: file.name, size: file.size});
if (attempt !== uploadAttempt || leavingUploadPage) return;
const clientKey = record.key;
const formData = new FormData(); const formData = new FormData();
formData.append('rejection_code', '105'); formData.append('rejection_code', '105');
formData.append('original_name', file.name || 'upload'); formData.append('original_name', file.name || 'upload');
formData.append('original_size', String(file.size || 0)); formData.append('original_size', String(file.size || 0));
formData.append('client_key', clientKey); formData.append('client_key', clientKey);
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('POST', createApiUrl || formEl.getAttribute('action') || '/api/images/public/create', true); xhr.open('POST', record.meta.actionUrl, true);
xhr.withCredentials = true; xhr.withCredentials = true;
xhr.setRequestHeader('Accept', 'application/json'); xhr.setRequestHeader('Accept', 'application/json');
xhr.onload = () => { xhr.onload = async () => {
if (attempt !== uploadAttempt || leavingUploadPage) return;
if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status >= 200 && xhr.status < 300) {
redirectToTour(clientKey); try { const result = JSON.parse(xhr.responseText); if (!result?.id) throw new Error(); await recovery.accepted({id: result.id}); redirectToTour(record); }
catch (_) { showUploadError(genericErrorMsg); }
return; return;
} }
showUploadError(genericErrorMsg); showUploadError(genericErrorMsg, false, xhr.status === 408 || xhr.status >= 500);
}; };
xhr.onerror = () => showUploadError(genericErrorMsg); xhr.onerror = () => { if (attempt === uploadAttempt && !leavingUploadPage) showUploadError(genericErrorMsg, true, true); };
xhr.send(formData); xhr.send(formData);
} catch (_) { if (attempt === uploadAttempt && !leavingUploadPage) showUploadError(genericErrorMsg); }
} }
function uploadFile(file) { function uploadFile(file, saved = null) {
const attempt = uploadAttempt;
// Тот же обход нестабильного Android FileProvider, что на основном сайте. // Тот же обход нестабильного Android FileProvider, что на основном сайте.
window.ZoomzillaUpload.prepare(file, maxUploadBytes || 15 * 1024 * 1024) window.ZoomzillaUpload.prepare(file, maxUploadBytes || 15 * 1024 * 1024)
.then(bytes => uploadPreparedFile(file, bytes)) .then(async bytes => {
.catch(() => showUploadError(genericErrorMsg)); if (attempt !== uploadAttempt || leavingUploadPage) return;
const record = saved || await recovery.remember(new File([bytes], file.name, {type: file.type, lastModified: file.lastModified}), metadata());
if (attempt === uploadAttempt && !leavingUploadPage) uploadPreparedFile(file, bytes, record);
})
.catch(() => { if (attempt === uploadAttempt && !leavingUploadPage) showUploadError(genericErrorMsg); });
} }
function uploadPreparedFile(file, bytes) { function uploadPreparedFile(file, bytes, record) {
const attempt = uploadAttempt;
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
const formData = new FormData(); const formData = new FormData();
// Ключ загрузки: по нему сайт передаёт её в сессию посетителя (/api/images/public/claim, годен 30 мин). // Ключ загрузки: по нему сайт передаёт её в сессию посетителя (/api/images/public/claim, годен 30 мин).
// randomUUID есть в HTTPS-контексте; где его нет (старый Safari), есть getRandomValues. // randomUUID есть в HTTPS-контексте; где его нет (старый Safari), есть getRandomValues.
const clientKey = createClientKey(); const clientKey = record.key;
formData.append('file', bytes, file.name || 'upload'); formData.append('file', bytes, file.name || 'upload');
formData.append('client_key', clientKey); formData.append('client_key', clientKey);
const actionUrl = createApiUrl || formEl.getAttribute('action') || '/api/images/public/create'; const actionUrl = record.meta.actionUrl;
xhr.open('POST', actionUrl, true); xhr.open('POST', actionUrl, true);
xhr.withCredentials = true; xhr.withCredentials = true;
xhr.setRequestHeader('Accept', 'application/json'); xhr.setRequestHeader('Accept', 'application/json');
@@ -247,7 +273,8 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a
} }
}; };
xhr.onload = () => { xhr.onload = async () => {
if (attempt !== uploadAttempt || leavingUploadPage) return;
if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status >= 200 && xhr.status < 300) {
let response; let response;
try { try {
@@ -269,24 +296,49 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a
} }
// Переход: $redirect_url_base . client_key . ($redirect_url_utm ? '&' . $redirect_url_utm : '') // Переход: $redirect_url_base . client_key . ($redirect_url_utm ? '&' . $redirect_url_utm : '')
redirectToTour(clientKey); try { await recovery.accepted({id: imageId}); redirectToTour(record); }
catch (_) { showUploadError(genericErrorMsg); }
} else { } else {
showUploadError(genericErrorMsg); showUploadError(genericErrorMsg, false, xhr.status === 408 || xhr.status >= 500);
} }
}; };
xhr.onerror = () => { xhr.onerror = () => {
showUploadError(genericErrorMsg); if (attempt === uploadAttempt && !leavingUploadPage) showUploadError(genericErrorMsg, true, true);
}; };
xhr.send(formData); xhr.send(formData);
} }
function showUploadError(msg) { function showUploadError(msg, keepRecovery = false, connection = false) {
if (leavingUploadPage) return;
uploadAttempt++;
if (!keepRecovery) clearRecovery();
resetUploadPending(); resetUploadPending();
form.show(); form.show();
if (typeof preview !== 'undefined' && preview) preview.showError(msg);
fileInput.value = ''; fileInput.value = '';
window.PublicNavigationGuard.allowNavigation(() => {
window.ZoomzillaUploadError.show(msg, connection);
});
} }
window.addEventListener('pagehide', () => { leavingUploadPage = true; });
window.addEventListener('pageshow', event => {
if (!event.persisted) return;
leavingUploadPage = false;
uploadAttempt++;
const wasUploading = uploadPending;
resetUploadPending(); clearRecovery(); fileInput.value = '';
if (wasUploading) window.PublicNavigationGuard.allowNavigation();
});
(async () => {
try {
const saved = await recovery.load();
if (!saved) return;
if (performance.getEntriesByType('navigation')[0]?.type !== 'reload' || saved.sourceUrl !== location.href || saved.meta.actionUrl !== createApiUrl) { clearRecovery(); return; }
if (saved.result?.id) { beginUploadPending(); redirectToTour(saved); }
else if (saved.file) { beginUploadPending(); uploadFile(saved.file, saved); }
else if (saved.meta.rejectionCode === 105) rejectOversizedFile(saved.meta, saved);
} catch (_) { clearRecovery(); }
})();
})(); })();
</script> </script>
+70
View File
@@ -0,0 +1,70 @@
<?php
// Общий inline-контракт Back для незавершённой публичной операции (api#299/preland#9).
return <<<'JS'
(() => {
let active = false;
let hasHistoryEntry = false;
let releasingHistoryEntry = false;
let returningToSentinel = false;
function pushGuardEntries() {
// Помечаем и текущую запись Tour, и sentinel сверху. Иначе первый Back
// останавливался на немаркированной записи, а второй мог уйти в about:blank.
window.history.replaceState({ publicOperationGuard: true }, '', window.location.href);
window.history.pushState({ publicOperationGuard: true }, '', window.location.href);
hasHistoryEntry = true;
}
function onPopState(event) {
if (releasingHistoryEntry) {
releasingHistoryEntry = false;
return;
}
if (active) {
if (returningToSentinel) {
returningToSentinel = false;
return;
}
returningToSentinel = true;
window.history.go(1);
}
}
function deactivate(onReleased) {
if (!active) {
if (onReleased) {
if (releasingHistoryEntry) {
window.addEventListener('popstate', onReleased, { once: true });
} else {
onReleased();
}
}
return;
}
active = false;
if (hasHistoryEntry) {
hasHistoryEntry = false;
releasingHistoryEntry = true;
if (onReleased) window.addEventListener('popstate', onReleased, { once: true });
window.history.back();
return;
}
if (onReleased) onReleased();
}
window.addEventListener('popstate', onPopState);
window.PublicNavigationGuard = {
activate() {
if (active) return;
releasingHistoryEntry = false;
returningToSentinel = false;
active = true;
pushGuardEntries();
},
deactivate,
allowNavigation: deactivate,
};
})();
JS;
+11
View File
@@ -0,0 +1,11 @@
<?php
/** Домен преленда не связан с config.host основного сайта/API. */
function prelandSiteDomain(array $server): string
{
foreach ([$server['HTTP_HOST'] ?? '', $server['SERVER_NAME'] ?? ''] as $candidate) {
if (!is_string($candidate) || preg_match('/[\s\/@?#<>]/', $candidate)) continue;
$host = strtolower(rtrim(preg_replace('/:\d+$/', '', $candidate), '.'));
if (strlen($host) <= 253 && preg_match('/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/D', $host)) return $host;
}
return '';
}
+100
View File
@@ -0,0 +1,100 @@
<?php
/** Общий inline runtime API/независимого PHP-преленда. Переменные задаёт вызывающий layout. */
$themeInitial = isset($themeInitial) && $themeInitial === 'dark' ? 'dark' : 'light';
$themeSurface = isset($themeSurface) && in_array($themeSurface, ['public', 'private', 'preland'], true) ? $themeSurface : 'public';
?>
<script>
(function (initial, surface) {
'use strict';
if (window.ZoomzillaTheme) { window.ZoomzillaTheme.refresh(); return; }
const key = 'theme_choice';
let shown = initial;
const canonical = value => typeof value === 'string' && value.length <= 128 && /^v1:(light|dark):(manual|preland):[A-Za-z0-9_-]{16,64}(?![\s\S])/.test(value) ? value : null;
const themeOf = value => value.split(':')[1];
function readCookie() {
try {
for (const part of document.cookie.split(';')) {
const cookie = part.trim();
if (!cookie.startsWith(key + '=')) continue;
const value = canonical(decodeURIComponent(cookie.slice(key.length + 1)));
if (value) return value;
}
} catch (_) {}
return null;
}
function read() {
const cookie = readCookie();
if (cookie) return cookie;
try { return canonical(window.localStorage.getItem(key)); } catch (_) { return null; }
}
function apply(theme) {
shown = theme;
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
if (document.body) document.body.classList.toggle('theme-dark', theme === 'dark');
document.querySelectorAll('[data-theme-choice]').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.themeChoice === theme)));
}
function uuid() {
if (window.crypto && typeof window.crypto.randomUUID === 'function') return window.crypto.randomUUID();
const bytes = new Uint8Array(16);
if (window.crypto && window.crypto.getRandomValues) window.crypto.getRandomValues(bytes);
else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
bytes[6] = (bytes[6] & 15) | 64; bytes[8] = (bytes[8] & 63) | 128;
const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20);
}
function refresh() { const value = read(); apply(value ? themeOf(value) : shown); }
const storageTimers = {soon: null, settled: null, trailing: null};
function refreshAfterStorage() {
refresh();
// Между процессами storage notification может прийти раньше cookie.
// Только повторно читаем authority: event.newValue не задаёт порядок выбора.
const schedule = (slot, delay) => {
storageTimers[slot] = window.setTimeout(() => {
storageTimers[slot] = null;
refresh();
}, delay);
};
if (storageTimers.soon === null) schedule('soon', 0);
if (storageTimers.settled === null) schedule('settled', 50);
// Новое notification получает собственное trailing окно; ранние passes
// не перезапускаются. Максимум три timers, после тишины повторов нет.
if (storageTimers.trailing !== null) window.clearTimeout(storageTimers.trailing);
schedule('trailing', 250);
}
function choose(theme) {
if (theme !== 'light' && theme !== 'dark') return;
const from = shown;
const value = 'v1:' + theme + ':manual:' + uuid().replace(/-/g, '');
try { document.cookie = key + '=' + encodeURIComponent(value) + '; Expires=Tue, 19 Jan 2038 03:14:07 GMT; Path=/; SameSite=Lax' + (location.protocol === 'https:' ? '; Secure' : ''); } catch (_) {}
try { window.localStorage.setItem(key, value); } catch (_) {}
apply(theme);
if (from === theme) return;
const detail = {schema_version: 1, event_id: uuid(), from, to: theme, source: 'manual', surface, host: location.hostname, path: location.pathname, occurred_at: new Date().toISOString()};
try { document.dispatchEvent(new CustomEvent('theme_changed', {detail})); } catch (_) {}
}
window.ZoomzillaTheme = Object.freeze({current: () => shown, choose, refresh});
// Pointer-клик не сбрасывает ввод/selection; клавиатурная навигация остаётся штатной.
document.addEventListener('mousedown', event => {
const button = event.target instanceof Element ? event.target.closest('[data-theme-choice]') : null;
const active = document.activeElement;
if (button && active && (active.matches('input,textarea') || active.isContentEditable)) event.preventDefault();
});
document.addEventListener('click', event => {
const button = event.target instanceof Element ? event.target.closest('[data-theme-choice]') : null;
if (button && !button.disabled) choose(button.dataset.themeChoice);
});
window.addEventListener('storage', event => { if (event.key === key || event.key === null) refreshAfterStorage(); });
window.addEventListener('pageshow', refresh);
document.addEventListener('DOMContentLoaded', refresh);
document.addEventListener('pjax:end', refresh);
refresh();
// Только первый boot зеркалит валидную cookie и уведомляет другие вкладки
// о server import. Read-only refresh/reinit не создают цикл storage events.
const bootCookie = readCookie();
if (bootCookie) {
apply(themeOf(bootCookie));
try { if (window.localStorage.getItem(key) !== bootCookie) window.localStorage.setItem(key, bootCookie); } catch (_) {}
}
})(<?= json_encode($themeInitial, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>, <?= json_encode($themeSurface, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>);
</script>
+16
View File
@@ -0,0 +1,16 @@
<?php
/** preland#13: только оформление; не меняет рекламный набор и не подключает Yii. */
function prelandThemePreference(): ?array
{
$value = $_COOKIE['theme_choice'] ?? null;
if (!is_string($value) || strlen($value) > 128
|| !preg_match('/\Av1:(light|dark):(manual|preland):([A-Za-z0-9_-]{16,64})\z/', $value, $match)) {
return null;
}
return ['theme' => $match[1], 'source' => $match[2], 'revision' => $match[3]];
}
function prelandThemeResolve(string $fallback): string
{
return prelandThemePreference()['theme'] ?? $fallback;
}
+27
View File
@@ -0,0 +1,27 @@
<style>
/* api#260 / preland#13: утверждённый общий control, без замены DOM страницы. */
html[data-theme="light"] { color-scheme: light; }
html[data-theme="dark"] { color-scheme: dark; }
body.theme-dark { background-color: #000; }
.theme-switch { width:88px;height:44px;border-radius:14px;display:flex;flex:0 0 88px;box-shadow:inset 0 0 0 1px #d8d9e1;background:#f1f2f6;isolation:isolate; }
.theme-switch button { position:relative;width:44px;height:44px;padding:10px;display:flex;align-items:center;justify-content:center;border:0;background:transparent;border-radius:14px;cursor:pointer;flex:none;color:#000; }
.theme-switch button::before { content:'';position:absolute;inset:4px;border-radius:10px;z-index:-1; }
.theme-switch button[aria-pressed="true"]::before { background:#fff;box-shadow:0 1px 4px #10182818; }
.theme-switch button:focus-visible { outline:2px solid #7754df;outline-offset:3px; }
.theme-switch svg { display:block;width:24px;height:24px;flex:none;opacity:.6;pointer-events:none; }
.theme-switch button[aria-pressed="true"] svg { opacity:1; }
.theme-dark .theme-switch { background:#0b0b10;box-shadow:inset 0 0 0 1px #39343f; }
.theme-dark .theme-switch button { color:#fff; }
.theme-dark .theme-switch button[aria-pressed="true"]::before { background:#30283c;box-shadow:inset 0 0 0 1px #655374; }
.header .navbar>.container>div.d-flex { gap:24px;flex-shrink:0; }
.header .btn-header { margin-right:0!important; }
body:not(.theme-dark) .header img[src$="/coin-wh.svg"] { filter:brightness(.35) sepia(.6) saturate(4) hue-rotate(228deg); }
@media(max-width:767.98px) {
.header .navbar>.container>div.d-flex { gap:12px; }
.theme-switch { width:64px;height:30px;flex-basis:64px;border-radius:9px; }
.theme-switch button { width:32px;height:30px;padding:5px 6px;border-radius:9px; }
.theme-switch button::before { inset:3px;border-radius:6px; }
.theme-switch button::after { content:'';position:absolute;top:-7px;bottom:-7px;left:0;right:0; }
.theme-switch svg { width:20px;height:20px; }
}
</style>
+15
View File
@@ -0,0 +1,15 @@
<?php
/** Одинаковый SSR control в Yii и standalone PHP; переводы передаются явно. */
$themeLabels = $themeLabels ?? ['group' => 'Тема оформления', 'light' => 'Светлая тема', 'dark' => 'Тёмная тема'];
$themeInitial = isset($themeInitial) && $themeInitial === 'dark' ? 'dark' : 'light';
$themeEscape = static fn(string $value): string => htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
?>
<div class="theme-switch" role="group" aria-label="<?= $themeEscape($themeLabels['group']) ?>">
<button type="button" data-theme-choice="light" aria-label="<?= $themeEscape($themeLabels['light']) ?>" title="<?= $themeEscape($themeLabels['light']) ?>" aria-pressed="<?= $themeInitial === 'light' ? 'true' : 'false' ?>">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M10 3.33333V2.5M14.7141 5.28571L15.3033 4.69645M16.6667 10H17.5M14.7141 14.7143L15.3033 15.3035M10 16.6667V17.5M5.28605 14.7139L4.6968 15.3032M3.33333 10H2.5M5.28605 5.28605L4.6968 4.6968M14.1667 10C14.1667 12.3012 12.3012 14.1667 10 14.1667C7.69881 14.1667 5.83333 12.3012 5.83333 10C5.83333 7.69881 7.69881 5.83333 10 5.83333C12.3012 5.83333 14.1667 7.69881 14.1667 10Z" stroke="currentColor" stroke-linecap="square"/></svg>
</button>
<button type="button" data-theme-choice="dark" aria-label="<?= $themeEscape($themeLabels['dark']) ?>" title="<?= $themeEscape($themeLabels['dark']) ?>" aria-pressed="<?= $themeInitial === 'dark' ? 'true' : 'false' ?>">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M11.1504 3.43439C11.4811 4.1663 11.667 4.97841 11.667 5.8338C11.6668 9.05529 9.05455 11.6668 5.83301 11.6668C4.97779 11.6668 4.16636 11.4809 3.43457 11.1502C3.97969 14.2838 6.71044 16.6668 10 16.6668C13.6819 16.6668 16.667 13.6817 16.667 9.99981C16.6669 6.71047 14.2838 3.97958 11.1504 3.43439Z" stroke="currentColor" stroke-linejoin="round"/></svg>
</button>
</div>
<script>if(window.ZoomzillaTheme)window.ZoomzillaTheme.refresh();</script>
+59
View File
@@ -0,0 +1,59 @@
<?php
/** api#321: ошибка загрузки поверх страницы, без перестройки первого экрана. */
$uploadErrorCopy = json_encode([
'title' => Yii::t('app', 'Ошибка связи'),
'network' => Yii::t('app', 'Не удалось загрузить изображение. Проверьте подключение к интернету и попробуйте ещё раз.'),
'other' => Yii::t('app', 'Не удалось загрузить изображение'),
'fallback' => Yii::t('app', 'Попробуйте ещё раз.'),
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<style>
.upload-error-popup{position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,.65);display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box;overflow:auto}
.upload-error-popup[hidden]{display:none!important}
.upload-error-popup .upload-error-card{position:relative;width:100%;max-width:440px;padding:38px 32px 30px;text-align:center;border-radius:24px;border:1px solid rgba(255,255,255,.15);box-shadow:0 20px 80px #0008;background:#484848 linear-gradient(to bottom,rgba(61,135,227,.25),rgba(153,6,148,.25));color:#fff;box-sizing:border-box;font-family:Inter,sans-serif;max-height:100%;overflow:auto}
.upload-error-popup .upload-error-card:focus{outline:none}
.upload-error-popup h2{font-size:24px;line-height:1.3;font-weight:600;margin:0 0 16px;color:inherit}
.upload-error-popup p{font-size:16px;line-height:24px;color:#e5e5e7;margin:0 0 24px;overflow-wrap:anywhere;white-space:pre-line}
.upload-error-popup .upload-error-close{position:absolute;top:5px;right:5px;display:flex;align-items:center;justify-content:center;width:44px;height:44px;border:0;background:none;color:#e5e5e7;cursor:pointer;font-size:28px;line-height:1}
.upload-error-popup .upload-error-ok{min-width:160px;height:48px;border-radius:16px;padding:0 28px;font:500 16px Inter,sans-serif;color:#fff;border:0;cursor:pointer;background:linear-gradient(180deg,#3d87e3 0%,#990694 100%)}
.upload-error-popup button:focus-visible{outline:2px solid #fff;outline-offset:3px}
@media(max-width:767.98px){.upload-error-popup .upload-error-card{padding:36px 24px 24px}.upload-error-popup h2{font-size:22px}.upload-error-popup p{font-size:15px;line-height:23px;margin-bottom:22px}.upload-error-popup .upload-error-ok{width:100%}}
</style>
<div class="upload-error-popup" id="upload-error-popup" hidden role="alertdialog" aria-modal="true" aria-labelledby="upload-error-title" aria-describedby="upload-error-text">
<div class="upload-error-card" tabindex="-1">
<button class="upload-error-close" type="button" aria-label="<?= htmlspecialchars(Yii::t('app', 'Закрыть'), ENT_QUOTES, 'UTF-8') ?>">×</button>
<h2 id="upload-error-title"></h2>
<p id="upload-error-text"></p>
<button class="upload-error-ok" type="button">OK</button>
</div>
</div>
<script>
(() => {
const popup=document.getElementById('upload-error-popup');
if(!popup || window.ZoomzillaUploadError) return;
document.body.appendChild(popup);
const copy=<?= $uploadErrorCopy ?>, card=popup.querySelector('.upload-error-card');
const close=popup.querySelector('.upload-error-close'), ok=popup.querySelector('.upload-error-ok');
let previousOverflow='', previousFocus=null;
function hide(){
if(popup.hidden)return;
popup.hidden=true;document.body.style.overflow=previousOverflow;
const target=document.querySelector('#dropzone-form .dropzone-form-button') || previousFocus;
if(target && !target.disabled && target.isConnected)target.focus({preventScroll:true});
}
window.ZoomzillaUploadError={show(message,connection=false){
if(popup.hidden){previousOverflow=document.body.style.overflow;previousFocus=document.activeElement;}
popup.querySelector('h2').textContent=connection?copy.title:copy.other;
popup.querySelector('p').textContent=connection?copy.network:(message||copy.fallback);
popup.hidden=false;document.body.style.overflow='hidden';card.focus({preventScroll:true});
},hide};
close.addEventListener('click',hide);ok.addEventListener('click',hide);
popup.addEventListener('keydown',event=>{
if(event.key==='Escape'){event.preventDefault();hide();return;}
if(event.key==='Tab'){
event.preventDefault();
if(document.activeElement===close)ok.focus();else if(document.activeElement===ok)close.focus();else(event.shiftKey?ok:close).focus();
}
});
})();
</script>
+2 -19
View File
@@ -1,22 +1,5 @@
<?php /* CSS синхронна API upload-picker-links; отдельный хост без Yii assets. */ ?> <?php /* CSS синхронна API upload-picker-links; отдельный хост без Yii assets. */ ?>
<style>
[data-picker-or][hidden], [data-files-picker][hidden] { display: none !important; }
#dropzone-form[data-mobile-upload] { padding: 16px; }
#dropzone-form[data-mobile-upload] .dropzone-form-icon img { width: 44px; height: 44px; }
#dropzone-form[data-mobile-upload] .dropzone-form-text { margin-top: 8px; font-size: 16px; line-height: 24px; }
#dropzone-form[data-mobile-upload] .dropzone-form-format { margin-top: 10px; flex-direction: column; gap: 6px; }
#dropzone-form[data-mobile-upload] .dropzone-form-format-label { font-size: 11px; line-height: 16px; }
#dropzone-form[data-mobile-upload] .dropzone-form-format-list { flex-wrap: nowrap; gap: 4px; justify-content: center; }
#dropzone-form[data-mobile-upload] .dropzone-form-format-item { padding: 3px 5px; border-radius: 6px; font-size: 11px; line-height: 15px; white-space: nowrap; }
#dropzone-form[data-mobile-upload] .dropzone-form-action { margin-top: 12px; display: flex; flex-direction: column; align-items: center; gap: 6px; }
#dropzone-form[data-mobile-upload] .dropzone-form-button { width: 100%; pointer-events: auto; }
#dropzone-form[data-mobile-upload] [data-picker-or] { display: block; margin: 0; font-size: 10px; line-height: 14px; color: #979aa0; }
#dropzone-form[data-mobile-upload] [data-files-picker] { display: block; position: relative; margin: -2px 0 0; font-size: 12px; line-height: 18px; text-decoration: underline; text-underline-offset: 3px; pointer-events: auto; }
#dropzone-form.theme-dark [data-files-picker], .theme-dark #dropzone-form [data-files-picker] { color: #b6caff; }
/* Расширяем область нажатия вниз, не меняя утверждённый видимый зазор4px. */
#dropzone-form [data-files-picker]::after { content: ''; position: absolute; inset: 0 -8px -14px; }
#dropzone-form[data-mobile-upload] .dropzone-form-format-item[data-format-optional] { display: none; }
</style>
<span data-picker-or hidden><?= Yii::t('app', 'или') ?></span> <span data-picker-or hidden><?= Yii::t('app', 'или') ?></span>
<a href="#" data-files-picker hidden><?= Yii::t('app', 'Выбрать файлы (PDF и др.)') ?></a> <a href="#" data-files-picker hidden><?= Yii::t('app', 'Файлы (PDF и др.)') ?></a>
<input type="file" data-other-files style="display:none"> <input type="file" data-other-files style="display:none">
+25
View File
@@ -0,0 +1,25 @@
<?php
/** Синхронная inline-CSS API/Preland: мобильный вид до инициализации picker. */
return <<<'CSS'
[data-picker-or][hidden], [data-files-picker][hidden] { display: none !important; }
:is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) { padding: 16px; }
:is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-icon img { width: 44px; height: 44px; }
:is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-text { margin-top: 8px; font-size: 16px; line-height: 24px; }
:is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-format { margin-top: 10px; flex-direction: column; gap: 6px; }
:is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-format-label { font-size: 11px; line-height: 16px; }
:is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-format-list { flex-wrap: nowrap; gap: 4px; justify-content: center; }
:is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-format-item { padding: 3px 5px; border-radius: 6px; font-size: 11px; line-height: 15px; white-space: nowrap; }
:is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-action { margin-top: 12px; display: flex; flex-direction: column; align-items: center; gap: 6px; }
:is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-button { width: 100%; pointer-events: auto; }
:is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) [data-picker-or] { display: block; margin: 0; font-size: 10px; line-height: 14px; color: #979aa0; }
:is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) [data-files-picker] { display: block; position: relative; margin: -2px 0 0; font-size: 12px; line-height: 18px; text-decoration: underline; text-underline-offset: 3px; pointer-events: auto; }
#dropzone-form.theme-dark [data-files-picker], .theme-dark #dropzone-form [data-files-picker] { color: #b6caff; }
/* Расширяем область нажатия вниз, не меняя утверждённый видимый зазор4px. */
#dropzone-form [data-files-picker]::after { content: ''; position: absolute; inset: 0 -8px -14px; }
:is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-format-item[data-format-optional] { display: none; }
html[data-upload-device=android] #dropzone-form [data-picker-or][hidden], html[data-upload-device=android] #dropzone-form [data-files-picker][hidden] { display: block !important; }
/* api#320: однострочный заголовок; правило общее для API и Preland, включая Android picker. */
@media(max-width:767.98px){
.index-mobile-fit #dropzone-form #form-title.index-upload-heading{font-size:clamp(16px,5.2vw,22px);font-weight:400;line-height:1.25;white-space:nowrap}
}
CSS;
+208
View File
@@ -0,0 +1,208 @@
<?php
/** Общее восстановление текущей публичной загрузки; UI и навигацией управляет вызывающая форма. */
return <<<'JS'
(() => {
if (window.ZoomzillaUploadRecovery) return;
const ttl = 30 * 60 * 1000;
const prefix = 'zoomzilla-upload-recovery:';
const slots = new Map();
const randomKey = () => Array.from(crypto.getRandomValues(new Uint8Array(16)), byte => byte.toString(16).padStart(2, '0')).join('');
const failure = message => new Error('Upload recovery: ' + message);
let storageInitialized = false;
const initializeStorage = () => {
if (storageInitialized) return;
// Дублирование вкладки копирует sessionStorage даже без opener. Новый
// заход отвязывает только указатели: IndexedDB исходной вкладки не трогаем.
if (performance.getEntriesByType('navigation')[0]?.type !== 'reload') {
for (const name of Object.keys(sessionStorage)) {
if (name.startsWith(prefix)) sessionStorage.removeItem(name);
}
}
storageInitialized = true;
};
// Только ограниченный JSON: не теряем молча поля, NaN или функции при сериализации.
const json = value => {
const text = JSON.stringify(value, (key, item) => {
if (item === undefined || typeof item === 'function' || typeof item === 'symbol' || typeof item === 'bigint' || (typeof item === 'number' && !Number.isFinite(item))) throw failure('invalid JSON');
return item;
});
if (!text || text.length > 65536) throw failure('metadata too large');
return JSON.parse(text);
};
const valid = record => {
try {
return record && typeof record.key === 'string' && record.key.length > 0 && record.key.length <= 200
&& Number.isFinite(record.createdAt) && record.createdAt <= Date.now()
&& record.expiresAt === record.createdAt + ttl && record.expiresAt > Date.now()
&& typeof record.sourceUrl === 'string' && record.sourceUrl.length <= 16384
&& typeof record.slot === 'string' && json(record.meta) !== undefined
&& (record.result === null ? (record.blob === null || ((record.blob instanceof ArrayBuffer || record.blob instanceof Blob) && record.fileInfo && typeof record.fileInfo.name === 'string' && typeof record.fileInfo.type === 'string' && Number.isFinite(record.fileInfo.lastModified))) : (record.blob === null && record.fileInfo === null && json(record.result) !== undefined));
} catch (_) { return false; }
};
const open = () => new Promise((resolve, reject) => {
let request, settled = false;
const finish = (error, db) => {
if (settled) { if (db) db.close(); return; }
settled = true;
window.clearTimeout(timer);
if (error) reject(error); else resolve(db);
};
const timer = window.setTimeout(() => finish(failure('open timeout')), 10000);
try { request = indexedDB.open('zoomzilla-upload-recovery', 1); } catch (error) { finish(error); return; }
request.onupgradeneeded = () => request.result.createObjectStore('uploads');
request.onerror = () => finish(request.error || failure('open failed'));
request.onblocked = () => finish(failure('database blocked'));
request.onsuccess = () => {
request.result.onversionchange = () => request.result.close();
finish(null, request.result);
};
});
const transaction = async callback => {
const db = await open();
try {
return await new Promise((resolve, reject) => {
let tx, result, settled = false;
try { tx = db.transaction('uploads', 'readwrite'); } catch (error) { reject(error); return; }
const finish = error => {
if (settled) return;
settled = true;
window.clearTimeout(timer);
if (error) reject(error); else resolve(result);
};
const fail = error => {
finish(error);
try { tx.abort(); } catch (_) { /* Транзакция могла уже завершиться. */ }
};
const timer = window.setTimeout(() => fail(failure('transaction timeout')), 10000);
tx.oncomplete = () => finish();
tx.onabort = () => finish(tx.error || failure('transaction aborted'));
// WebKit может дать request/transaction error без последующего abort.
tx.onerror = event => fail(event.target.error || tx.error || failure('transaction failed'));
try {
const store = tx.objectStore('uploads');
// Общая уборка сирот/протухших записей, включая давно закрытые вкладки.
const cursor = store.openCursor();
cursor.onsuccess = () => {
if (settled) return;
try {
const entry = cursor.result;
if (!entry) return;
if (!valid(entry.value)) entry.delete();
entry.continue();
} catch (error) { fail(error); }
};
callback(store, value => { result = value; }, fail);
} catch (error) { fail(error); }
});
} finally { db.close(); }
};
const unpack = record => ({
key: record.key, createdAt: record.createdAt, expiresAt: record.expiresAt,
sourceUrl: record.sourceUrl, meta: json(record.meta), result: record.result === null ? null : json(record.result),
// Structured clone может вернуть Blob без File-прототипа: метаданные хранятся отдельно.
file: record.blob === null ? null : new File([record.blob], record.fileInfo.name, {type: record.fileInfo.type, lastModified: record.fileInfo.lastModified})
});
window.ZoomzillaUploadRecovery = {
create({scope, target}) {
if (typeof scope !== 'string' || !scope || typeof target !== 'string' || !target) throw failure('scope and target required');
const slot = prefix + 'slot:' + JSON.stringify([scope, target]);
if (!slots.has(slot)) slots.set(slot, {queue: Promise.resolve(), generation: 0});
const state = slots.get(slot);
const enqueue = operation => {
const pending = state.queue.then(operation);
state.queue = pending.catch(() => {});
return pending;
};
const get = (store, id, done, fail) => {
if (!id) { done(null); return; }
const request = store.get(id);
request.onsuccess = () => { try { done(request.result || null); } catch (error) { fail(error); } };
};
const current = record => valid(record) && record.slot === slot;
return {
remember(file, meta, key) {
const generation = state.generation;
return enqueue(async () => {
if (generation !== state.generation) throw failure('cancelled');
initializeStorage();
const previous = sessionStorage.getItem(slot);
// Неудачная новая попытка никогда не восстанавливает прежний файл.
sessionStorage.removeItem(slot);
const metadata = json(meta);
if (file !== null && !(file instanceof File)) throw failure('prepared File required');
// ArrayBuffer не зависит от поддержки Blob-файлов в IndexedDB
// (в WebKit её отсутствие проявляется асинхронным UnknownError).
const bytes = file === null ? null : await file.arrayBuffer();
if (generation !== state.generation) throw failure('cancelled');
if (key !== undefined && (typeof key !== 'string' || !key || key.length > 200)) throw failure('invalid key');
let tab = sessionStorage.getItem(prefix + 'tab');
if (!tab) { tab = randomKey(); sessionStorage.setItem(prefix + 'tab', tab); }
const id = tab + ':' + randomKey();
let record;
await transaction((store, done, fail) => get(store, previous, old => {
const resumed = old && old.key === key;
if (resumed && !current(old)) throw failure('expired resume');
const createdAt = resumed ? old.createdAt : Date.now();
record = {slot, key: key || randomKey(), createdAt, expiresAt: createdAt + ttl,
sourceUrl: resumed ? old.sourceUrl : location.href, meta: metadata,
blob: bytes, fileInfo: file === null ? null : {name: file.name, type: file.type, lastModified: file.lastModified}, result: null};
if (previous) store.delete(previous);
store.put(record, id);
done(null);
}, fail));
if (generation !== state.generation) {
await transaction(store => store.delete(id));
throw failure('cancelled');
}
try { sessionStorage.setItem(slot, id); } catch (error) {
// Если указатель недоступен, не оставляем Blob до конца TTL.
try { await transaction(store => store.delete(id)); } catch (_) { /* TTL уберёт сироту. */ }
throw error;
}
return unpack(record);
});
},
load() {
const generation = state.generation;
return enqueue(async () => {
if (generation !== state.generation) return null;
initializeStorage();
const id = sessionStorage.getItem(slot);
const record = await transaction((store, done, fail) => get(store, id, stored => {
if (stored && current(stored)) { done(stored); return; }
if (id) store.delete(id);
done(null);
}, fail));
if (generation !== state.generation) return null;
if (!record) { if (sessionStorage.getItem(slot) === id) sessionStorage.removeItem(slot); return null; }
return unpack(record);
});
},
accepted(result) {
const generation = state.generation;
return enqueue(async () => {
initializeStorage();
const receipt = json(result);
if (receipt === null) throw failure('receipt required');
const id = sessionStorage.getItem(slot);
return transaction((store, done, fail) => get(store, id, record => {
if (generation !== state.generation || !current(record)) throw failure('missing pending upload');
record.blob = null;
record.fileInfo = null;
record.result = receipt;
store.put(record, id);
done(unpack(record));
}, fail));
});
},
clear() {
state.generation++;
let id;
try { initializeStorage(); id = sessionStorage.getItem(slot); sessionStorage.removeItem(slot); } catch (error) { return Promise.reject(error); }
return enqueue(() => transaction(store => { if (id) store.delete(id); }));
}
};
}
};
})();
JS;
+88
View File
@@ -0,0 +1,88 @@
<?php
/** api#326: внешняя граница UTM; semantic-контекст шаблонов остаётся прежним. */
declare(strict_types=1);
function prelandUtmEnums(): array
{
return ['theme'=>['1'=>'dark','2'=>'light'], 'reg'=>['1'=>'before','2'=>'after'], 'banch'=>['1'=>'b','2'=>'a'], 'focus'=>['1'=>'face','2'=>'center']];
}
/** Совместимость host-config при доставке кода до точечной миграции. */
function prelandNumericUtmConfig(array $config): array
{
$settings = is_array($config['utm'] ?? null) ? $config['utm'] : [];
foreach (['registration'=>'reg','chob'=>'banch','bchoice'=>'banch'] as $old=>$new) {
if (!isset($settings[$new]) && isset($settings[$old])) $settings[$new] = $settings[$old];
unset($settings[$old]);
}
unset($settings['chb']);
foreach (prelandUtmEnums() as $key=>$map) {
if (!is_array($settings[$key] ?? null)) continue;
$weights=[];
foreach ((array)($settings[$key]['weights'] ?? []) as $variant=>$weight) {
$digit=array_search((string)$variant,$map,true);
$weights[$digit === false ? $variant : $digit]=$weight;
}
$settings[$key]['weights']=$weights;
$default=$settings[$key]['default'] ?? null;
$digit=array_search($default,$map,true);
if ($digit !== false) $settings[$key]['default']=(string)$digit;
}
$config['utm']=$settings;
return $config;
}
/** Выбирает random один раз, до рендера/сохранения загрузки. */
function prelandResolveUtm(array $query, array $config): array
{
$config=prelandNumericUtmConfig($config);
$defaults=['indpic'=>'1','theme'=>'2','degr'=>'1','reg'=>'2','banch'=>'2','tp'=>'1','cons'=>'2','focus'=>'1'];
$enums=prelandUtmEnums(); $digits=[]; $context=$query;
foreach (['registration','bchoice','chob','chb'] as $old) unset($context[$old]);
foreach ($defaults as $key=>$fallback) {
unset($context[$key]);
$settings=$config['utm'][$key] ?? [];
$valid=static fn($digit): bool => preg_match('/^[1-9]$/D',(string)$digit)===1
&& (!isset($enums[$key]) || isset($enums[$key][$digit]))
&& (in_array($key,['cons','theme','reg','banch','focus'],true) || isset($settings['weights'][$digit]));
$default=(string)($settings['default'] ?? $fallback);
if (!$valid($default)) $default=$fallback;
$value=$query[$key] ?? null;
$digit=is_string($value) && preg_match('/\A.{4}([0-9])/us',$value,$match) ? $match[1] : null;
if ($digit==='0') {
// Generic resolver исторически выбирает первый ключ при sum<=0;
// cons имеет отдельный контракт: только положительные веса и default.
if ($key !== 'cons') {
$weights=(array)($settings['weights'] ?? []);
$total=array_sum($weights);
$digit=$weights ? (string)array_key_first($weights) : $default;
if ($total>0 && is_finite((float)$total)) {
$point=mt_rand()/mt_getrandmax()*$total; $current=0.0;
foreach ($weights as $variant=>$weight) {
$current+=(float)$weight;
$digit=(string)$variant;
if ($point<=$current) break;
}
}
} else {
$weights=[];
foreach ((array)($settings['weights'] ?? []) as $variant=>$weight) {
if ($valid($variant) && is_numeric($weight) && is_finite((float)$weight) && (float)$weight>0) $weights[$variant]=(float)$weight;
}
$total=array_sum($weights); $digit=$default;
if ($total>0 && is_finite($total)) {
$point=mt_rand()/(mt_getrandmax()+1.0)*$total;
foreach ($weights as $variant=>$weight) {
if ($point<$weight) { $digit=(string)$variant; break; }
$point-=$weight;
}
}
}
}
if (!$valid($digit ?? '')) $digit=$default;
$digits[$key]=$digit;
$internal=['reg'=>'registration','banch'=>'bchoice'][$key] ?? $key;
$context[$internal]=$enums[$key][$digit] ?? $digit;
}
return ['digits'=>$digits,'context'=>$context];
}
+41
View File
@@ -0,0 +1,41 @@
<?php
/** api#326: новый шум при каждой навигации; recovery хранит выбранные цифры. */
return <<<'JS'
(function () {
const keys = ['indpic','theme','degr','reg','banch','tp','cons','focus'];
const enums = {theme:{dark:'1',light:'2'},reg:{before:'1',after:'2'},banch:{b:'1',a:'2'},focus:{face:'1',center:'2'}};
const legacy = {reg:'registration',banch:'bchoice'};
const valid = (key, value) => /^[1-9]$/.test(value || '') && (!enums[key] || Object.values(enums[key]).includes(value));
function restore(meta, defaults) {
const query = new URLSearchParams(meta.redirectUtm || '');
const digits = {};
for (const key of keys) {
let value = meta.utmDigits?.[key] ?? (key === 'banch' ? meta.utmDigits?.chob : undefined);
if (!valid(key, value)) {
// Только сохранённый record: старые enum/открытые цифры не являются raw URL alias.
const stored = query.get(legacy[key] || key) ?? query.get(key) ?? (key === 'banch' ? query.get('chob') : null);
value = enums[key]?.[stored] || stored;
if (!valid(key, value)) value = Array.from(stored || '')[4];
}
digits[key] = valid(key, value) ? value : defaults[key];
}
return digits;
}
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
function mask(digit) {
const bytes = new Uint8Array(10);
crypto.getRandomValues(bytes);
const chars = Array.from(bytes, value => alphabet[value % alphabet.length]);
chars[4] = digit;
return chars.join('');
}
function query(meta, defaults) {
const params = new URLSearchParams(meta.redirectUtm || '');
const digits = restore(meta, defaults);
for (const key of ['registration','bchoice','chob','chb','key','shown_theme',...keys]) params.delete(key);
for (const key of keys) params.set(key, mask(digits[key]));
return params.toString();
}
window.ZoomzillaUtmHandoff = {restore, query};
})();
JS;
+65 -66
View File
@@ -1,69 +1,68 @@
{ {
"host": "zoomzilla.ru", "host": "zoomzilla.ru",
"requisites": [
"requisites": [ "ИП Кардаполова Яна Виталийсовна",
"ИП Иванов Иван Иванович", "346886, Ростовская обл., г. Батайск,",
"123123, г. Москва, Пресненская наб., 12", "ДНТ «Труд», ул. 11-я линия, д. 1195",
"ИНН: 123404385406", "ИНН: 616202304132,",
"ОГРНИП: 123420000019175" "ОГРНИП: 326619600205915"
], ],
"utm": {
"utm": { "indpic": {
"indpic": { "weights": {
"weights": { "1": 1,
"1": 1, "2": 0
"2": 0 },
}, "default": "1"
"default": "1" },
}, "theme": {
"theme": { "weights": {
"weights": { "2": 0,
"light": 0, "1": 1
"dark": 1 },
}, "default": "1"
"default": "dark" },
}, "degr": {
"degr": { "weights": {
"weights": { "1": 1,
"1": 1, "2": 0
"2": 0 },
}, "default": "1"
"default": "1" },
}, "reg": {
"registration": { "weights": {
"weights": { "1": 0,
"before": 0, "2": 1
"after": 1 },
}, "default": "2"
"default": "after" },
}, "banch": {
"bchoice": { "weights": {
"weights": { "2": 1,
"a": 1, "1": 0
"b": 0 },
}, "default": "2"
"default": "a" },
}, "tp": {
"chb": { "weights": {
"weights": { "1": 0,
"a": 1, "2": 1
"b": 0 },
}, "default": "2"
"default": "b" },
}, "focus": {
"tp": { "weights": {
"weights": { "2": 0,
"1": 0, "1": 1
"2": 1 },
}, "default": "1"
"default": "2" },
}, "cons": {
"focus": { "weights": {
"weights": { "1": 100,
"center": 0, "2": 0
"face": 1 },
}, "default": "2"
"default": "face" }
} }
}
} }
+40 -23
View File
@@ -1,29 +1,46 @@
<svg xmlns="http://www.w3.org/2000/svg" width="252" height="58" viewBox="0 0 252 58" fill="none"> <svg width="182" height="47" viewBox="0 0 182 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>ZoomStage</title>
<path d="M112.076 8.8L103.832 34H97.568L92.024 16.936L86.3 34H80.072L71.792 8.8H77.84L83.528 26.512L89.468 8.8H94.868L100.628 26.656L106.496 8.8H112.076ZM133.836 20.896C135.276 21.352 136.404 22.12 137.22 23.2C138.036 24.256 138.444 25.564 138.444 27.124C138.444 29.332 137.58 31.036 135.852 32.236C134.148 33.412 131.652 34 128.364 34H115.332V8.8H127.644C130.716 8.8 133.068 9.388 134.7 10.564C136.356 11.74 137.184 13.336 137.184 15.352C137.184 16.576 136.884 17.668 136.284 18.628C135.708 19.588 134.892 20.344 133.836 20.896ZM121.128 13.192V19.132H126.924C128.364 19.132 129.456 18.88 130.2 18.376C130.944 17.872 131.316 17.128 131.316 16.144C131.316 15.16 130.944 14.428 130.2 13.948C129.456 13.444 128.364 13.192 126.924 13.192H121.128ZM127.932 29.608C129.468 29.608 130.62 29.356 131.388 28.852C132.18 28.348 132.576 27.568 132.576 26.512C132.576 24.424 131.028 23.38 127.932 23.38H121.128V29.608H127.932ZM162.371 29.32V34H142.859V8.8H161.903V13.48H148.655V18.952H160.355V23.488H148.655V29.32H162.371ZM185.015 8.8V12.58L175.655 34H169.319L178.355 13.552H170.003V17.656H164.819V8.8H185.015ZM197.708 34.432C195.62 34.432 193.76 33.916 192.128 32.884C190.496 31.852 189.212 30.364 188.276 28.42C187.364 26.452 186.908 24.112 186.908 21.4C186.908 18.688 187.364 16.36 188.276 14.416C189.212 12.448 190.496 10.948 192.128 9.916C193.76 8.884 195.62 8.368 197.708 8.368C199.796 8.368 201.656 8.884 203.288 9.916C204.92 10.948 206.192 12.448 207.104 14.416C208.04 16.36 208.508 18.688 208.508 21.4C208.508 24.112 208.04 26.452 207.104 28.42C206.192 30.364 204.92 31.852 203.288 32.884C201.656 33.916 199.796 34.432 197.708 34.432ZM197.708 29.5C199.244 29.5 200.444 28.84 201.308 27.52C202.196 26.2 202.64 24.16 202.64 21.4C202.64 18.64 202.196 16.6 201.308 15.28C200.444 13.96 199.244 13.3 197.708 13.3C196.196 13.3 194.996 13.96 194.108 15.28C193.244 16.6 192.812 18.64 192.812 21.4C192.812 24.16 193.244 26.2 194.108 27.52C194.996 28.84 196.196 29.5 197.708 29.5Z" fill="url(#paint0_linear)"/> <g opacity="0.18" filter="url(#filter0_f_241_1963)">
<g clip-path="url(#clip0)"> <path d="M23.5 37C30.9558 37 37 30.9558 37 23.5C37 16.0442 30.9558 10 23.5 10C16.0442 10 10 16.0442 10 23.5C10 30.9558 16.0442 37 23.5 37Z" fill="url(#paint0_linear_241_1963)" />
<g opacity="0.2">
<path opacity="0.2" d="M11.7632 38.9239C12.9756 37.7274 14.5725 36.9859 16.2788 36.8271C15.1981 36.7236 14.1075 36.858 13.0857 37.2205C12.0638 37.5831 11.136 38.1648 10.3689 38.9239L14.2502 42.7577C14.4826 42.5282 14.7338 42.3111 14.985 42.1374L11.7632 38.9239Z" fill="#BDBDBD"/>
</g>
<path d="M20.7944 38.9239C21.1486 39.2786 21.4663 39.6672 21.7427 40.084C20.3716 39.9581 18.9893 40.1324 17.6942 40.5946C16.3992 41.0567 15.2232 41.7953 14.2502 42.7577L10.3689 38.9239C11.7524 37.5601 13.6271 36.7942 15.5816 36.7942C17.5362 36.7942 19.4109 37.5601 20.7944 38.9239V38.9239ZM62 26.07L37.6321 2L31.1068 8.45176L50.1615 27.2735C50.3496 27.4585 50.499 27.6785 50.6009 27.9207C50.7028 28.1629 50.7552 28.4226 50.7552 28.6848C50.7552 28.9471 50.7028 29.2068 50.6009 29.449C50.499 29.6912 50.3496 29.9111 50.1615 30.0962L30.2589 49.7678C30.1549 52.0743 29.1803 54.2588 27.5269 55.8908L29.6748 58.0124L62 26.07Z" fill="url(#paint1_linear)"/>
<path opacity="0.2" d="M28.8521 55.8784C30.5026 54.2495 31.477 52.07 31.5841 49.7678L51.4992 30.0961C51.8762 29.7208 52.0877 29.2135 52.0877 28.6848C52.0877 28.1561 51.8762 27.6488 51.4992 27.2735L32.4257 8.45174L36.3258 4.5993C36.866 4.06706 37.5588 3.71186 38.3104 3.58191C37.7194 3.47529 37.1111 3.51219 36.5378 3.68942C35.9645 3.86666 35.4434 4.17892 35.0195 4.5993L31.1068 8.45174L50.1615 27.2735C50.3497 27.4585 50.499 27.6785 50.6009 27.9207C50.7028 28.1629 50.7552 28.4225 50.7552 28.6848C50.7552 28.9471 50.7028 29.2068 50.6009 29.449C50.499 29.6912 50.3497 29.9111 50.1615 30.0961L30.2589 49.7678C30.1549 52.0743 29.1803 54.2588 27.5269 55.8908L29.6748 58.0124L30.3406 57.392L28.8521 55.8784Z" fill="#BDBDBD"/>
<path d="M29.6748 9.85997L24.3679 4.61792L0 28.6755L5.30693 33.9176L8.93699 37.5033C10.7386 35.9704 13.0603 35.1672 15.4364 35.2547C17.8125 35.3423 20.0673 36.3141 21.7482 37.9753C23.4291 39.6365 24.412 41.8641 24.4995 44.2112C24.587 46.5583 23.7727 48.8513 22.22 50.6301L24.3679 52.7517L30.0203 47.1685L48.7358 28.6755L29.6748 9.85997Z" fill="#7241E2"/>
<g opacity="0.15">
<path opacity="0.15" d="M23.4196 50.6301C24.77 49.2981 25.6772 47.5909 26.0199 45.7365C26.3627 43.882 26.1248 41.9683 25.338 40.2509C24.5512 38.5334 23.2529 37.0937 21.6163 36.124C19.9798 35.1542 18.0827 34.7005 16.1783 34.8233C17.9807 34.9354 19.712 35.5589 21.1645 36.619C22.617 37.6791 23.7289 39.1306 24.3668 40.7995C25.0047 42.4684 25.1414 44.2837 24.7606 46.0274C24.3798 47.7712 23.4977 49.3694 22.22 50.6301L24.3679 52.7518L24.996 52.1314L23.4196 50.6301Z" fill="#BDBDBD"/>
<path opacity="0.15" d="M6.51276 33.93L1.19955 28.6755L22.9423 7.21103C23.4931 6.66825 24.2027 6.31018 24.9708 6.18744C24.3882 6.09254 23.791 6.13587 23.2286 6.31384C22.6662 6.49182 22.1548 6.79934 21.7364 7.21103L0 28.6755L5.30693 33.9176L8.93699 37.5033C9.13577 37.3039 9.34543 37.1155 9.56503 36.9387L6.51276 33.93Z" fill="#BDBDBD"/>
</g>
</g> </g>
<path d="M8.11627 23.5001C11.4128 16.2164 16.8442 12.5117 23.5 12.5117C30.1558 12.5117 35.5872 16.2164 38.8837 23.5001C35.5872 30.7838 30.1558 34.4885 23.5 34.4885C16.8442 34.4885 11.4128 30.7838 8.11627 23.5001Z" fill="#090812" stroke="url(#paint1_linear_241_1963)" stroke-width="2.6" stroke-linejoin="round" />
<path d="M23.5 29.7791C26.9678 29.7791 29.7791 26.9679 29.7791 23.5C29.7791 20.0322 26.9678 17.2209 23.5 17.2209C20.0322 17.2209 17.2209 20.0322 17.2209 23.5C17.2209 26.9679 20.0322 29.7791 23.5 29.7791Z" fill="url(#paint2_linear_241_1963)" />
<path d="M23.5 26.3255C25.0605 26.3255 26.3256 25.0604 26.3256 23.4999C26.3256 21.9394 25.0605 20.6743 23.5 20.6743C21.9395 20.6743 20.6744 21.9394 20.6744 23.4999C20.6744 25.0604 21.9395 26.3255 23.5 26.3255Z" fill="#090812" />
<path d="M28.8372 15.0232L29.936 17.6918L32.6046 18.7906L29.936 19.8895L28.8372 22.5581L27.7384 19.8895L25.0698 18.7906L27.7384 17.6918L28.8372 15.0232Z" fill="white" />
<path d="M14.7093 27.2673H13.7674C13.4206 27.2673 13.1395 27.5485 13.1395 27.8952V28.8371C13.1395 29.1839 13.4206 29.465 13.7674 29.465H14.7093C15.0561 29.465 15.3372 29.1839 15.3372 28.8371V27.8952C15.3372 27.5485 15.0561 27.2673 14.7093 27.2673Z" fill="#6AA7FF" />
<path d="M18.9477 29.1511H17.6919C17.2584 29.1511 16.907 29.5025 16.907 29.936V31.1918C16.907 31.6253 17.2584 31.9767 17.6919 31.9767H18.9477C19.3812 31.9767 19.7326 31.6253 19.7326 31.1918V29.936C19.7326 29.5025 19.3812 29.1511 18.9477 29.1511Z" fill="#7B5CFF" />
<path d="M23.814 30.407H21.9302C21.4101 30.407 20.9884 30.8287 20.9884 31.3488V33.2326C20.9884 33.7527 21.4101 34.1744 21.9302 34.1744H23.814C24.3341 34.1744 24.7558 33.7527 24.7558 33.2326V31.3488C24.7558 30.8287 24.3341 30.407 23.814 30.407Z" fill="#D21BCE" />
<path d="M31.3488 29.7791L36.0581 34.4884" stroke="url(#paint3_linear_241_1963)" stroke-width="2.6" stroke-linecap="round" />
<path d="M48.3163 34V33.604L56.8363 19.432H48.9163V16.72H61.8403V17.128L53.3683 31.276H61.6363V34H48.3163ZM70.4343 34.36C69.1383 34.36 68.0063 34.068 67.0383 33.484C66.0703 32.9 65.3183 32.096 64.7823 31.072C64.2543 30.04 63.9903 28.856 63.9903 27.52C63.9903 26.16 64.2623 24.968 64.8063 23.944C65.3503 22.92 66.1063 22.12 67.0743 21.544C68.0423 20.968 69.1623 20.68 70.4343 20.68C71.7383 20.68 72.8743 20.972 73.8423 21.556C74.8103 22.14 75.5623 22.948 76.0983 23.98C76.6343 25.004 76.9023 26.184 76.9023 27.52C76.9023 28.864 76.6303 30.052 76.0863 31.084C75.5503 32.108 74.7983 32.912 73.8303 33.496C72.8623 34.072 71.7303 34.36 70.4343 34.36ZM70.4343 31.648C71.5863 31.648 72.4423 31.264 73.0023 30.496C73.5623 29.728 73.8423 28.736 73.8423 27.52C73.8423 26.264 73.5583 25.264 72.9903 24.52C72.4223 23.768 71.5703 23.392 70.4343 23.392C69.6583 23.392 69.0183 23.568 68.5143 23.92C68.0183 24.264 67.6503 24.748 67.4103 25.372C67.1703 25.988 67.0503 26.704 67.0503 27.52C67.0503 28.776 67.3343 29.78 67.9023 30.532C68.4783 31.276 69.3223 31.648 70.4343 31.648ZM85.2703 34.36C83.9743 34.36 82.8423 34.068 81.8743 33.484C80.9063 32.9 80.1543 32.096 79.6183 31.072C79.0903 30.04 78.8263 28.856 78.8263 27.52C78.8263 26.16 79.0983 24.968 79.6423 23.944C80.1863 22.92 80.9423 22.12 81.9103 21.544C82.8783 20.968 83.9983 20.68 85.2703 20.68C86.5743 20.68 87.7103 20.972 88.6783 21.556C89.6463 22.14 90.3983 22.948 90.9343 23.98C91.4703 25.004 91.7383 26.184 91.7383 27.52C91.7383 28.864 91.4663 30.052 90.9223 31.084C90.3863 32.108 89.6343 32.912 88.6663 33.496C87.6983 34.072 86.5663 34.36 85.2703 34.36ZM85.2703 31.648C86.4223 31.648 87.2783 31.264 87.8383 30.496C88.3983 29.728 88.6783 28.736 88.6783 27.52C88.6783 26.264 88.3943 25.264 87.8263 24.52C87.2583 23.768 86.4063 23.392 85.2703 23.392C84.4943 23.392 83.8543 23.568 83.3503 23.92C82.8543 24.264 82.4863 24.748 82.2463 25.372C82.0063 25.988 81.8863 26.704 81.8863 27.52C81.8863 28.776 82.1703 29.78 82.7383 30.532C83.3143 31.276 84.1583 31.648 85.2703 31.648ZM109.946 34V26.128C109.946 25.24 109.726 24.548 109.286 24.052C108.846 23.548 108.25 23.296 107.498 23.296C107.034 23.296 106.618 23.404 106.25 23.62C105.882 23.828 105.59 24.14 105.374 24.556C105.158 24.964 105.05 25.452 105.05 26.02L103.766 25.264C103.758 24.368 103.958 23.58 104.366 22.9C104.782 22.212 105.342 21.676 106.046 21.292C106.75 20.908 107.534 20.716 108.398 20.716C109.83 20.716 110.926 21.148 111.686 22.012C112.454 22.868 112.838 23.996 112.838 25.396V34H109.946ZM94.3582 34V21.04H96.9142V25.06H97.2742V34H94.3582ZM102.17 34V26.164C102.17 25.26 101.95 24.556 101.51 24.052C101.07 23.548 100.47 23.296 99.7102 23.296C98.9742 23.296 98.3822 23.548 97.9342 24.052C97.4942 24.556 97.2742 25.212 97.2742 26.02L95.9782 25.156C95.9782 24.316 96.1862 23.56 96.6022 22.888C97.0182 22.216 97.5782 21.688 98.2822 21.304C98.9942 20.912 99.7902 20.716 100.67 20.716C101.638 20.716 102.446 20.924 103.094 21.34C103.75 21.748 104.238 22.308 104.558 23.02C104.886 23.724 105.05 24.52 105.05 25.408V34H102.17ZM124.7833 21.4703C124.7046 20.7334 124.3741 20.1597 123.7919 19.7491C123.2097 19.3384 122.4546 19.1331 121.5264 19.1331C120.8739 19.1331 120.3142 19.2316 119.8474 19.4284C119.3805 19.6253 119.0233 19.8925 118.7758 20.23C118.5283 20.5675 118.4017 20.9528 118.3961 21.3859C118.3961 21.7459 118.4791 22.0581 118.645 22.3225C118.811 22.5869 119.0374 22.8105 119.3242 22.9933C119.6111 23.1761 119.9289 23.3294 120.2777 23.4531C120.6264 23.5769 120.978 23.6809 121.3324 23.7653L122.9524 24.1703C123.6049 24.3222 124.2335 24.5275 124.8382 24.7863C125.4428 25.045 125.9857 25.3713 126.4666 25.765C126.9475 26.1588 127.3286 26.6341 127.6099 27.1909C127.8911 27.7478 128.0317 28.4003 128.0317 29.1484C128.0317 30.1609 127.773 31.0511 127.2555 31.8189C126.738 32.5867 125.9913 33.1858 125.0153 33.6161C124.0394 34.0464 122.8596 34.2616 121.4758 34.2616C120.1314 34.2616 118.9657 34.0534 117.9785 33.6372C116.9913 33.2209 116.2207 32.6134 115.6666 31.8147C115.1125 31.0159 114.813 30.0428 114.768 28.8953L117.8477 28.8953C117.8927 29.4972 118.0783 29.9978 118.4046 30.3972C118.7308 30.7966 119.1569 31.0947 119.6828 31.2916C120.2088 31.4884 120.798 31.5869 121.4505 31.5869C122.1311 31.5869 122.7288 31.4842 123.2435 31.2789C123.7582 31.0736 124.1618 30.7867 124.4542 30.4183C124.7467 30.0498 124.8958 29.6181 124.9014 29.1231C124.8958 28.6731 124.7636 28.3005 124.5049 28.0052C124.2461 27.7098 123.8847 27.4623 123.4207 27.2627C122.9566 27.063 122.4152 26.8844 121.7964 26.7269L119.8305 26.2206C118.4074 25.855 117.2838 25.2995 116.4597 24.5542C115.6357 23.8089 115.2236 22.8175 115.2236 21.58C115.2236 20.5619 115.5007 19.6703 116.0547 18.9053C116.6088 18.1403 117.3639 17.5455 118.3202 17.1208C119.2764 16.6961 120.3592 16.4838 121.5686 16.4838C122.7949 16.4838 123.8707 16.6961 124.796 17.1208C125.7213 17.5455 126.4483 18.1333 126.9771 18.8842C127.5058 19.6352 127.7786 20.4972 127.7955 21.4703ZM137.1474 21.04L137.1474 23.4025L129.6971 23.4025L129.6971 21.04ZM131.5365 17.935L134Line truncated
<defs> <defs>
<linearGradient id="paint0_linear" x1="71" y1="21.5" x2="210" y2="21.5" gradientUnits="userSpaceOnUse"> <filter id="filter0_f_241_1963" x="0" y="0" width="47" height="47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<stop stop-color="#7241E2"/> <feFlood flood-opacity="0" result="BackgroundImageFix" />
<stop offset="1" stop-color="#F413B5"/> <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="5" result="effect1_foregroundBlur_241_1963" />
</filter>
<linearGradient id="paint0_linear_241_1963" x1="7.80233" y1="9.05814" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient> </linearGradient>
<linearGradient id="paint1_linear" x1="10.3689" y1="30.0062" x2="62" y2="30.0062" gradientUnits="userSpaceOnUse"> <linearGradient id="paint1_linear_241_1963" x1="7.80232" y1="9.05823" x2="40.4535" y2="38.5699" gradientUnits="userSpaceOnUse">
<stop stop-color="#662AF0"/> <stop stop-color="#6AA7FF" />
<stop offset="1" stop-color="#EF1AB3"/> <stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint2_linear_241_1963" x1="7.80231" y1="9.05816" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint3_linear_241_1963" x1="7.8023" y1="9.05812" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint4_linear_241_1963" x1="47.1163" y1="24.5" x2="180" y2="24.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9F9F9" />
<stop offset="1" stop-color="#8EA2D9" />
</linearGradient> </linearGradient>
<clipPath id="clip0">
<rect width="62" height="56" fill="white" transform="translate(0 2)"/>
</clipPath>
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+40 -23
View File
@@ -1,29 +1,46 @@
<svg xmlns="http://www.w3.org/2000/svg" width="252" height="58" viewBox="0 0 252 58" fill="none"> <svg width="182" height="47" viewBox="0 0 182 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>ZoomStage</title>
<path d="M112.076 8.8L103.832 34H97.568L92.024 16.936L86.3 34H80.072L71.792 8.8H77.84L83.528 26.512L89.468 8.8H94.868L100.628 26.656L106.496 8.8H112.076ZM133.836 20.896C135.276 21.352 136.404 22.12 137.22 23.2C138.036 24.256 138.444 25.564 138.444 27.124C138.444 29.332 137.58 31.036 135.852 32.236C134.148 33.412 131.652 34 128.364 34H115.332V8.8H127.644C130.716 8.8 133.068 9.388 134.7 10.564C136.356 11.74 137.184 13.336 137.184 15.352C137.184 16.576 136.884 17.668 136.284 18.628C135.708 19.588 134.892 20.344 133.836 20.896ZM121.128 13.192V19.132H126.924C128.364 19.132 129.456 18.88 130.2 18.376C130.944 17.872 131.316 17.128 131.316 16.144C131.316 15.16 130.944 14.428 130.2 13.948C129.456 13.444 128.364 13.192 126.924 13.192H121.128ZM127.932 29.608C129.468 29.608 130.62 29.356 131.388 28.852C132.18 28.348 132.576 27.568 132.576 26.512C132.576 24.424 131.028 23.38 127.932 23.38H121.128V29.608H127.932ZM162.371 29.32V34H142.859V8.8H161.903V13.48H148.655V18.952H160.355V23.488H148.655V29.32H162.371ZM185.015 8.8V12.58L175.655 34H169.319L178.355 13.552H170.003V17.656H164.819V8.8H185.015ZM197.708 34.432C195.62 34.432 193.76 33.916 192.128 32.884C190.496 31.852 189.212 30.364 188.276 28.42C187.364 26.452 186.908 24.112 186.908 21.4C186.908 18.688 187.364 16.36 188.276 14.416C189.212 12.448 190.496 10.948 192.128 9.916C193.76 8.884 195.62 8.368 197.708 8.368C199.796 8.368 201.656 8.884 203.288 9.916C204.92 10.948 206.192 12.448 207.104 14.416C208.04 16.36 208.508 18.688 208.508 21.4C208.508 24.112 208.04 26.452 207.104 28.42C206.192 30.364 204.92 31.852 203.288 32.884C201.656 33.916 199.796 34.432 197.708 34.432ZM197.708 29.5C199.244 29.5 200.444 28.84 201.308 27.52C202.196 26.2 202.64 24.16 202.64 21.4C202.64 18.64 202.196 16.6 201.308 15.28C200.444 13.96 199.244 13.3 197.708 13.3C196.196 13.3 194.996 13.96 194.108 15.28C193.244 16.6 192.812 18.64 192.812 21.4C192.812 24.16 193.244 26.2 194.108 27.52C194.996 28.84 196.196 29.5 197.708 29.5Z" fill="url(#paint0_linear)"/> <g opacity="0.18" filter="url(#filter0_f_241_2041)">
<g clip-path="url(#clip0)"> <path d="M23.5 37C30.9558 37 37 30.9558 37 23.5C37 16.0442 30.9558 10 23.5 10C16.0442 10 10 16.0442 10 23.5C10 30.9558 16.0442 37 23.5 37Z" fill="url(#paint0_linear_241_2041)" />
<g opacity="0.2">
<path opacity="0.2" d="M11.7632 38.9239C12.9756 37.7274 14.5725 36.9859 16.2788 36.8271C15.1981 36.7236 14.1075 36.858 13.0857 37.2205C12.0638 37.5831 11.136 38.1648 10.3689 38.9239L14.2502 42.7577C14.4826 42.5282 14.7338 42.3111 14.985 42.1374L11.7632 38.9239Z" fill="#BDBDBD"/>
</g>
<path d="M20.7944 38.9239C21.1486 39.2786 21.4663 39.6672 21.7427 40.084C20.3716 39.9581 18.9893 40.1324 17.6942 40.5946C16.3992 41.0567 15.2232 41.7953 14.2502 42.7577L10.3689 38.9239C11.7524 37.5601 13.6271 36.7942 15.5816 36.7942C17.5362 36.7942 19.4109 37.5601 20.7944 38.9239V38.9239ZM62 26.07L37.6321 2L31.1068 8.45176L50.1615 27.2735C50.3496 27.4585 50.499 27.6785 50.6009 27.9207C50.7028 28.1629 50.7552 28.4226 50.7552 28.6848C50.7552 28.9471 50.7028 29.2068 50.6009 29.449C50.499 29.6912 50.3496 29.9111 50.1615 30.0962L30.2589 49.7678C30.1549 52.0743 29.1803 54.2588 27.5269 55.8908L29.6748 58.0124L62 26.07Z" fill="url(#paint1_linear)"/>
<path opacity="0.2" d="M28.8521 55.8784C30.5026 54.2495 31.477 52.07 31.5841 49.7678L51.4992 30.0961C51.8762 29.7208 52.0877 29.2135 52.0877 28.6848C52.0877 28.1561 51.8762 27.6488 51.4992 27.2735L32.4257 8.45174L36.3258 4.5993C36.866 4.06706 37.5588 3.71186 38.3104 3.58191C37.7194 3.47529 37.1111 3.51219 36.5378 3.68942C35.9645 3.86666 35.4434 4.17892 35.0195 4.5993L31.1068 8.45174L50.1615 27.2735C50.3497 27.4585 50.499 27.6785 50.6009 27.9207C50.7028 28.1629 50.7552 28.4225 50.7552 28.6848C50.7552 28.9471 50.7028 29.2068 50.6009 29.449C50.499 29.6912 50.3497 29.9111 50.1615 30.0961L30.2589 49.7678C30.1549 52.0743 29.1803 54.2588 27.5269 55.8908L29.6748 58.0124L30.3406 57.392L28.8521 55.8784Z" fill="#BDBDBD"/>
<path d="M29.6748 9.85997L24.3679 4.61792L0 28.6755L5.30693 33.9176L8.93699 37.5033C10.7386 35.9704 13.0603 35.1672 15.4364 35.2547C17.8125 35.3423 20.0673 36.3141 21.7482 37.9753C23.4291 39.6365 24.412 41.8641 24.4995 44.2112C24.587 46.5583 23.7727 48.8513 22.22 50.6301L24.3679 52.7517L30.0203 47.1685L48.7358 28.6755L29.6748 9.85997Z" fill="#7241E2"/>
<g opacity="0.15">
<path opacity="0.15" d="M23.4196 50.6301C24.77 49.2981 25.6772 47.5909 26.0199 45.7365C26.3627 43.882 26.1248 41.9683 25.338 40.2509C24.5512 38.5334 23.2529 37.0937 21.6163 36.124C19.9798 35.1542 18.0827 34.7005 16.1783 34.8233C17.9807 34.9354 19.712 35.5589 21.1645 36.619C22.617 37.6791 23.7289 39.1306 24.3668 40.7995C25.0047 42.4684 25.1414 44.2837 24.7606 46.0274C24.3798 47.7712 23.4977 49.3694 22.22 50.6301L24.3679 52.7518L24.996 52.1314L23.4196 50.6301Z" fill="#BDBDBD"/>
<path opacity="0.15" d="M6.51276 33.93L1.19955 28.6755L22.9423 7.21103C23.4931 6.66825 24.2027 6.31018 24.9708 6.18744C24.3882 6.09254 23.791 6.13587 23.2286 6.31384C22.6662 6.49182 22.1548 6.79934 21.7364 7.21103L0 28.6755L5.30693 33.9176L8.93699 37.5033C9.13577 37.3039 9.34543 37.1155 9.56503 36.9387L6.51276 33.93Z" fill="#BDBDBD"/>
</g>
</g> </g>
<path d="M8.11628 23.5001C11.4128 16.2164 16.8442 12.5117 23.5 12.5117C30.1558 12.5117 35.5872 16.2164 38.8837 23.5001C35.5872 30.7838 30.1558 34.4885 23.5 34.4885C16.8442 34.4885 11.4128 30.7838 8.11628 23.5001Z" fill="#090812" stroke="url(#paint1_linear_241_2041)" stroke-width="2.6" stroke-linejoin="round" />
<path d="M23.5 29.7791C26.9678 29.7791 29.7791 26.9679 29.7791 23.5C29.7791 20.0322 26.9678 17.2209 23.5 17.2209C20.0322 17.2209 17.2209 20.0322 17.2209 23.5C17.2209 26.9679 20.0322 29.7791 23.5 29.7791Z" fill="url(#paint2_linear_241_2041)" />
<path d="M23.5 26.3255C25.0605 26.3255 26.3256 25.0604 26.3256 23.4999C26.3256 21.9394 25.0605 20.6743 23.5 20.6743C21.9395 20.6743 20.6744 21.9394 20.6744 23.4999C20.6744 25.0604 21.9395 26.3255 23.5 26.3255Z" fill="#090812" />
<path d="M28.8372 15.0232L29.936 17.6918L32.6047 18.7906L29.936 19.8895L28.8372 22.5581L27.7384 19.8895L25.0698 18.7906L27.7384 17.6918L28.8372 15.0232Z" fill="white" />
<path d="M14.7093 27.2673H13.7674C13.4207 27.2673 13.1395 27.5485 13.1395 27.8952V28.8371C13.1395 29.1839 13.4207 29.465 13.7674 29.465H14.7093C15.0561 29.465 15.3372 29.1839 15.3372 28.8371V27.8952C15.3372 27.5485 15.0561 27.2673 14.7093 27.2673Z" fill="#6AA7FF" />
<path d="M18.9477 29.1511H17.6919C17.2584 29.1511 16.907 29.5025 16.907 29.936V31.1918C16.907 31.6253 17.2584 31.9767 17.6919 31.9767H18.9477C19.3812 31.9767 19.7326 31.6253 19.7326 31.1918V29.936C19.7326 29.5025 19.3812 29.1511 18.9477 29.1511Z" fill="#7B5CFF" />
<path d="M23.814 30.407H21.9302C21.4101 30.407 20.9884 30.8287 20.9884 31.3488V33.2326C20.9884 33.7527 21.4101 34.1744 21.9302 34.1744H23.814C24.3341 34.1744 24.7558 33.7527 24.7558 33.2326V31.3488C24.7558 30.8287 24.3341 30.407 23.814 30.407Z" fill="#D21BCE" />
<path d="M31.3488 29.7791L36.0581 34.4884" stroke="url(#paint3_linear_241_2041)" stroke-width="2.6" stroke-linecap="round" />
<path d="M48.3163 34V33.604L56.8363 19.432H48.9163V16.72H61.8403V17.128L53.3683 31.276H61.6363V34H48.3163ZM70.4343 34.36C69.1383 34.36 68.0063 34.068 67.0383 33.484C66.0703 32.9 65.3183 32.096 64.7823 31.072C64.2543 30.04 63.9903 28.856 63.9903 27.52C63.9903 26.16 64.2623 24.968 64.8063 23.944C65.3503 22.92 66.1063 22.12 67.0743 21.544C68.0423 20.968 69.1623 20.68 70.4343 20.68C71.7383 20.68 72.8743 20.972 73.8423 21.556C74.8103 22.14 75.5623 22.948 76.0983 23.98C76.6343 25.004 76.9023 26.184 76.9023 27.52C76.9023 28.864 76.6303 30.052 76.0863 31.084C75.5503 32.108 74.7983 32.912 73.8303 33.496C72.8623 34.072 71.7303 34.36 70.4343 34.36ZM70.4343 31.648C71.5863 31.648 72.4423 31.264 73.0023 30.496C73.5623 29.728 73.8423 28.736 73.8423 27.52C73.8423 26.264 73.5583 25.264 72.9903 24.52C72.4223 23.768 71.5703 23.392 70.4343 23.392C69.6583 23.392 69.0183 23.568 68.5143 23.92C68.0183 24.264 67.6503 24.748 67.4103 25.372C67.1703 25.988 67.0503 26.704 67.0503 27.52C67.0503 28.776 67.3343 29.78 67.9023 30.532C68.4783 31.276 69.3223 31.648 70.4343 31.648ZM85.2703 34.36C83.9743 34.36 82.8423 34.068 81.8743 33.484C80.9063 32.9 80.1543 32.096 79.6183 31.072C79.0903 30.04 78.8263 28.856 78.8263 27.52C78.8263 26.16 79.0983 24.968 79.6423 23.944C80.1863 22.92 80.9423 22.12 81.9103 21.544C82.8783 20.968 83.9983 20.68 85.2703 20.68C86.5743 20.68 87.7103 20.972 88.6783 21.556C89.6463 22.14 90.3983 22.948 90.9343 23.98C91.4703 25.004 91.7383 26.184 91.7383 27.52C91.7383 28.864 91.4663 30.052 90.9223 31.084C90.3863 32.108 89.6343 32.912 88.6663 33.496C87.6983 34.072 86.5663 34.36 85.2703 34.36ZM85.2703 31.648C86.4223 31.648 87.2783 31.264 87.8383 30.496C88.3983 29.728 88.6783 28.736 88.6783 27.52C88.6783 26.264 88.3943 25.264 87.8263 24.52C87.2583 23.768 86.4063 23.392 85.2703 23.392C84.4943 23.392 83.8543 23.568 83.3503 23.92C82.8543 24.264 82.4863 24.748 82.2463 25.372C82.0063 25.988 81.8863 26.704 81.8863 27.52C81.8863 28.776 82.1703 29.78 82.7383 30.532C83.3143 31.276 84.1583 31.648 85.2703 31.648ZM109.946 34V26.128C109.946 25.24 109.726 24.548 109.286 24.052C108.846 23.548 108.25 23.296 107.498 23.296C107.034 23.296 106.618 23.404 106.25 23.62C105.882 23.828 105.59 24.14 105.374 24.556C105.158 24.964 105.05 25.452 105.05 26.02L103.766 25.264C103.758 24.368 103.958 23.58 104.366 22.9C104.782 22.212 105.342 21.676 106.046 21.292C106.75 20.908 107.534 20.716 108.398 20.716C109.83 20.716 110.926 21.148 111.686 22.012C112.454 22.868 112.838 23.996 112.838 25.396V34H109.946ZM94.3582 34V21.04H96.9142V25.06H97.2742V34H94.3582ZM102.17 34V26.164C102.17 25.26 101.95 24.556 101.51 24.052C101.07 23.548 100.47 23.296 99.7102 23.296C98.9742 23.296 98.3822 23.548 97.9342 24.052C97.4942 24.556 97.2742 25.212 97.2742 26.02L95.9782 25.156C95.9782 24.316 96.1862 23.56 96.6022 22.888C97.0182 22.216 97.5782 21.688 98.2822 21.304C98.9942 20.912 99.7902 20.716 100.67 20.716C101.638 20.716 102.446 20.924 103.094 21.34C103.75 21.748 104.238 22.308 104.558 23.02C104.886 23.724 105.05 24.52 105.05 25.408V34H102.17ZM124.7833 21.4703C124.7046 20.7334 124.3741 20.1597 123.7919 19.7491C123.2097 19.3384 122.4546 19.1331 121.5264 19.1331C120.8739 19.1331 120.3142 19.2316 119.8474 19.4284C119.3805 19.6253 119.0233 19.8925 118.7758 20.23C118.5283 20.5675 118.4017 20.9528 118.3961 21.3859C118.3961 21.7459 118.4791 22.0581 118.645 22.3225C118.811 22.5869 119.0374 22.8105 119.3242 22.9933C119.6111 23.1761 119.9289 23.3294 120.2777 23.4531C120.6264 23.5769 120.978 23.6809 121.3324 23.7653L122.9524 24.1703C123.6049 24.3222 124.2335 24.5275 124.8382 24.7863C125.4428 25.045 125.9857 25.3713 126.4666 25.765C126.9475 26.1588 127.3286 26.6341 127.6099 27.1909C127.8911 27.7478 128.0317 28.4003 128.0317 29.1484C128.0317 30.1609 127.773 31.0511 127.2555 31.8189C126.738 32.5867 125.9913 33.1858 125.0153 33.6161C124.0394 34.0464 122.8596 34.2616 121.4758 34.2616C120.1314 34.2616 118.9657 34.0534 117.9785 33.6372C116.9913 33.2209 116.2207 32.6134 115.6666 31.8147C115.1125 31.0159 114.813 30.0428 114.768 28.8953L117.8477 28.8953C117.8927 29.4972 118.0783 29.9978 118.4046 30.3972C118.7308 30.7966 119.1569 31.0947 119.6828 31.2916C120.2088 31.4884 120.798 31.5869 121.4505 31.5869C122.1311 31.5869 122.7288 31.4842 123.2435 31.2789C123.7582 31.0736 124.1618 30.7867 124.4542 30.4183C124.7467 30.0498 124.8958 29.6181 124.9014 29.1231C124.8958 28.6731 124.7636 28.3005 124.5049 28.0052C124.2461 27.7098 123.8847 27.4623 123.4207 27.2627C122.9566 27.063 122.4152 26.8844 121.7964 26.7269L119.8305 26.2206C118.4074 25.855 117.2838 25.2995 116.4597 24.5542C115.6357 23.8089 115.2236 22.8175 115.2236 21.58C115.2236 20.5619 115.5007 19.6703 116.0547 18.9053C116.6088 18.1403 117.3639 17.5455 118.3202 17.1208C119.2764 16.6961 120.3592 16.4838 121.5686 16.4838C122.7949 16.4838 123.8707 16.6961 124.796 17.1208C125.7213 17.5455 126.4483 18.1333 126.9771 18.8842C127.5058 19.6352 127.7786 20.4972 127.7955 21.4703ZM137.1474 21.04L137.1474 23.4025L129.6971 23.4025L129.6971 21.04ZM131.5365 17.935L134Line truncated
<defs> <defs>
<linearGradient id="paint0_linear" x1="71" y1="21.5" x2="210" y2="21.5" gradientUnits="userSpaceOnUse"> <filter id="filter0_f_241_2041" x="0" y="0" width="47" height="47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<stop stop-color="#7241E2"/> <feFlood flood-opacity="0" result="BackgroundImageFix" />
<stop offset="1" stop-color="#F413B5"/> <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="5" result="effect1_foregroundBlur_241_2041" />
</filter>
<linearGradient id="paint0_linear_241_2041" x1="7.80233" y1="9.05814" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient> </linearGradient>
<linearGradient id="paint1_linear" x1="10.3689" y1="30.0062" x2="62" y2="30.0062" gradientUnits="userSpaceOnUse"> <linearGradient id="paint1_linear_241_2041" x1="7.80233" y1="9.05823" x2="40.4535" y2="38.5699" gradientUnits="userSpaceOnUse">
<stop stop-color="#662AF0"/> <stop stop-color="#6AA7FF" />
<stop offset="1" stop-color="#EF1AB3"/> <stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint2_linear_241_2041" x1="7.80233" y1="9.05816" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint3_linear_241_2041" x1="7.80233" y1="9.05812" x2="40.4535" y2="38.5698" gradientUnits="userSpaceOnUse">
<stop stop-color="#6AA7FF" />
<stop offset="0.5" stop-color="#7B5CFF" />
<stop offset="1" stop-color="#D21BCE" />
</linearGradient>
<linearGradient id="paint4_linear_241_2041" x1="47.1163" y1="24.5" x2="180" y2="24.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#0F0911" />
<stop offset="1" stop-color="#960A97" />
</linearGradient> </linearGradient>
<clipPath id="clip0">
<rect width="62" height="56" fill="white" transform="translate(0 2)"/>
</clipPath>
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+15
View File
@@ -0,0 +1,15 @@
# preland#11 — актуальные ресурсы и проверяемая доставка
Решения владельца21.10.2026: держать актуальные ресурсы в Git main/staging; удалить заглушку соstage; PDF и реквизиты взять с текущей zoomzilla.ru. Основной реестр — Gitea anton/preland#11, связаны #10 (расследование) и api#304 (срочный Tour, уже выпущен).
1. Источники и карта: логотипы обеих тем/шапка/подвал,4PDF,9favicon,3fonts,10images,5CSS,реквизиты и150файлов /1,/2. Конфиг домена и UTM сохранять.
2. RED известной заглушки и старых реквизитов; импорт текущих файлов, фиксированный manifest184, запрет известного blob независимо от правки manifest, версии logo по содержимому.
3. Положительный список поставки; точный eventSHA; preflight текущихprimaryресурсов и150stageфайлов, блокировка drift. Только согласованный config.requisites меняется точечно; requisites.html теперь канонический.
4. Девять PHP-проверок, both-theme4width/2set brand+thumbnail, first-paint с задержками, real local Preland/Index→Tour→Join/error/retry; независимыйreview.
5. Сохранить точное testedstagingдерево, добавить main историческим родителем через merge-sours, проверить treeSHA и FFобе ветки к одному SHA. Автодеплой толькоstage. PostSHA184, защищённыеhostconfigполя/N/productionprelandбезизменений, видимыйбренд/подвал/интеракции. KB/Giteaзакрытьпослефактическойпроверки.
Ruling: не импортировать устаревший код main в staging — сохранить проверенное дерево и историюобеихветок; ценаошибки — возвратlegacydemo/layout, предотвращаетсяtreeSHA+CI.
Ruling: реквизиты теперьверсионируютсяпопрямомувыборувладельца; configредактируетсятольковэтомключе, полнаязаменазапрещена.
Ruling: footerpaymentнаstage768переполнялсядозадачи; скопированоужедействующееAPIправилоmax-width100%, не введённовыйдизайн. RED784→GREENбезoverflow.
Ограничения: физическиеустройстванепроверены; боевыеuploadsужеиспользованывпредыдущейприёмке, здесьреальныеuploadсценариитольковизолированномDocker.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+13 -63
View File
@@ -1,6 +1,10 @@
<?php <?php
require_once __DIR__ . '/i18n.php'; require_once __DIR__ . '/i18n.php';
require_once __DIR__ . '/components/asset-url.php';
require_once __DIR__ . '/components/site-domain.php';
require_once __DIR__ . '/components/theme-preference.php';
require_once __DIR__ . '/components/utm-codec.php';
$configFile = __DIR__ . '/config.json'; $configFile = __DIR__ . '/config.json';
$config = file_exists($configFile) ? json_decode(file_get_contents($configFile), true) : []; $config = file_exists($configFile) ? json_decode(file_get_contents($configFile), true) : [];
@@ -10,64 +14,14 @@ if (json_last_error() !== JSON_ERROR_NONE) {
} }
Yii::init($config['default_language'] ?? 'ru'); Yii::init($config['default_language'] ?? 'ru');
$config = prelandNumericUtmConfig($config);
function getWeightedRandom($weights) $resolvedUtm = prelandResolveUtm($_GET, $config);
{ $UTM = $resolvedUtm['context'];
if (empty($weights)) { $get_params = array_diff_key($_GET, array_fill_keys(['indpic','theme','degr','reg','banch','tp','cons','focus','registration','bchoice','chob','chb','key'], true));
return ''; // Числа — внутренний snapshot для recovery. Публичная оболочка создаётся при навигации.
} $redirect_digits = $resolvedUtm['digits'];
$themeInitial = prelandThemeResolve(($UTM['theme'] ?? 'light') === 'dark' ? 'dark' : 'light');
$totalWeight = array_sum($weights); $themeSurface = 'preland';
if ($totalWeight <= 0) {
$keys = array_keys($weights);
return (string)$keys[0];
}
$rand = (mt_rand() / mt_getrandmax()) * $totalWeight;
$current = 0.0;
foreach ($weights as $key => $weight) {
$current += (float)$weight;
if ($rand <= $current) {
return (string)$key;
}
}
$keys = array_keys($weights);
return (string)end($keys);
}
$get_params = $_GET;
if (isset($config['utm']) && is_array($config['utm'])) {
foreach ($get_params as $key => $value) {
if (!isset($config['utm'][$key])) {
continue;
}
if (isset($config['utm'][$key]['weights'][$value])) {
continue;
}
if ($value !== 'rnd') {
unset($get_params[$key]);
continue;
}
$weights = $config['utm'][$key]['weights'] ?? [];
$get_params[$key] = getWeightedRandom($weights);
}
foreach ($config['utm'] as $key => $value) {
if (isset($get_params[$key])) {
continue;
}
$get_params[$key] = $config['utm'][$key]['default'];
}
}
$UTM = $get_params;
$host = $config['host'] ?? ($_SERVER['HTTP_HOST'] ?? ''); $host = $config['host'] ?? ($_SERVER['HTTP_HOST'] ?? '');
$apiHost = $host ? (preg_match('#^https?://#', $host) ? rtrim($host, '/') : 'https://' . rtrim($host, '/')) : ''; $apiHost = $host ? (preg_match('#^https?://#', $host) ? rtrim($host, '/') : 'https://' . rtrim($host, '/')) : '';
@@ -96,7 +50,7 @@ $indpic = isset($UTM['indpic']) && preg_match('/^[a-zA-Z0-9_-]+$/', (string)$UTM
function renderComponent(string $name, array $params = []): void function renderComponent(string $name, array $params = []): void
{ {
global $assetsUriPrefix, $fontsUrl, $favicon, $logoAltExt, $spritePath, $UTM, $config, $indpic, $apiHost, $redirect_url_base, $redirect_url_utm; global $redirect_digits, $assetsUriPrefix, $fontsUrl, $favicon, $logoAltExt, $spritePath, $UTM, $config, $indpic, $apiHost, $redirect_url_base, $redirect_url_utm;
extract($params, EXTR_SKIP); extract($params, EXTR_SKIP);
$componentFile = __DIR__ . '/components/' . $name . '.php'; $componentFile = __DIR__ . '/components/' . $name . '.php';
if (file_exists($componentFile)) { if (file_exists($componentFile)) {
@@ -105,7 +59,3 @@ function renderComponent(string $name, array $params = []): void
} }
require_once __DIR__ . '/template.php'; require_once __DIR__ . '/template.php';
?>
<style>
<?= file_get_contents(__DIR__ . '/style.min.css'); ?>
</style>
+8
View File
@@ -137,6 +137,11 @@ return [
'Загрузите изображение' => 'Upload an image', 'Загрузите изображение' => 'Upload an image',
'Загрузить' => 'Upload', 'Загрузить' => 'Upload',
'Загрузить изображение' => 'Upload Image', 'Загрузить изображение' => 'Upload Image',
'Выберите файл' => 'Choose file',
'Улучшить изображение' => 'Enhance image',
'Выберите фото' => 'Choose photo',
'Файлы (PDF и др.)' => 'Files (PDF, etc.)',
'Файлы (другие форматы)' => 'Files (other formats)',
'Выбрать фото' => 'Choose photo', 'Выбрать фото' => 'Choose photo',
'или' => 'or', 'или' => 'or',
'Выбрать файлы (PDF и др.)' => 'Choose files (PDF, etc.)', 'Выбрать файлы (PDF и др.)' => 'Choose files (PDF, etc.)',
@@ -370,4 +375,7 @@ return [
'х4' => '4×', 'х4' => '4×',
'—' => '—', '—' => '—',
'₽' => '$', '₽' => '$',
'Тема оформления' => 'Appearance',
'Светлая тема' => 'Light theme',
'Тёмная тема' => 'Dark theme',
]; ];
+1
View File
@@ -146,6 +146,7 @@ return [
'Загрузите изображение' => 'Загрузите изображение', 'Загрузите изображение' => 'Загрузите изображение',
'Загрузить' => 'Загрузить', 'Загрузить' => 'Загрузить',
'Загрузить изображение' => 'Загрузить изображение', 'Загрузить изображение' => 'Загрузить изображение',
'Выберите файл' => 'Выберите файл',
'Загрузка тарифов...' => 'Загрузка тарифов...', 'Загрузка тарифов...' => 'Загрузка тарифов...',
'Загрузка...' => 'Загрузка...', 'Загрузка...' => 'Загрузка...',
'Запись в историю добавлена!' => 'Запись в историю добавлена!', 'Запись в историю добавлена!' => 'Запись в историю добавлена!',
+859
View File
@@ -0,0 +1,859 @@
{
"issue": "anton/preland#12",
"captured": "2026-10-21",
"hashing": "SHA-256; SVG/CSS/HTML/webmanifest CRLF normalized to LF",
"files": {
"1/01_desk_1-source.avif": {
"sha256": "d81af6d3757d0e932af04d956cd4083e7c7c547c9fbac290d27f5ce8bbaede1c",
"source": "https://zoomstage.online/1/01_desk_1-source.avif"
},
"1/01_desk_1-source.jpg": {
"sha256": "6e84a4d61e8494d2f287e5e6ae2237e3a04f05dff506c0d203f992ee02cbba18",
"source": "https://zoomstage.online/1/01_desk_1-source.jpg"
},
"1/01_desk_1-source.webp": {
"sha256": "d66f5c2e940d260b28f1d122feb385be5e3afe4b1094edd4e92b62a56afb3dd7",
"source": "https://zoomstage.online/1/01_desk_1-source.webp"
},
"1/01_desk_2-result.avif": {
"sha256": "05f51c0302f00b7a017fd92fca42e0f148487746090e2789f0dfb28df098ef25",
"source": "https://zoomstage.online/1/01_desk_2-result.avif"
},
"1/01_desk_2-result.jpg": {
"sha256": "2c202a1e9e2ddcbbfcd66ab250eb7041b7c034ae2b8f40b74651a20a11092b76",
"source": "https://zoomstage.online/1/01_desk_2-result.jpg"
},
"1/01_desk_2-result.webp": {
"sha256": "9569d7d575a57a06d2974db2818ed02c14f9e9910d2c622a11fdd5d37164e228",
"source": "https://zoomstage.online/1/01_desk_2-result.webp"
},
"1/01_desk_3-thumb.avif": {
"sha256": "f03f35fb43691d9eda8793be07682094e5f358809b3d11e9466a66b2a95d3fee",
"source": "https://zoomstage.online/1/01_desk_3-thumb.avif"
},
"1/01_desk_3-thumb.jpg": {
"sha256": "4df8f1b4d6a71d6ad5486c576b215bde5ddc9bd325dc55dbfeaa1722faa90003",
"source": "https://zoomstage.online/1/01_desk_3-thumb.jpg"
},
"1/01_desk_3-thumb.webp": {
"sha256": "84e0e7c7ddaf56a1fcd95951afd40f30b669abd0a89ef98775479cb22efcb9f3",
"source": "https://zoomstage.online/1/01_desk_3-thumb.webp"
},
"1/01_mob_1-source.avif": {
"sha256": "218eb7fc3c0d0ecee03454b46984e86d6ed48e9a87942d0959373dd3aeb9217c",
"source": "https://zoomstage.online/1/01_mob_1-source.avif"
},
"1/01_mob_1-source.jpg": {
"sha256": "dd92e0108066b171ccfaf410efdafe7e1cdc81d48da166d239362f087aa9d6ef",
"source": "https://zoomstage.online/1/01_mob_1-source.jpg"
},
"1/01_mob_1-source.webp": {
"sha256": "30b5c8229aea67374fa70db9e39066748db43d2ba15f6c7c68f10570bcff31d9",
"source": "https://zoomstage.online/1/01_mob_1-source.webp"
},
"1/01_mob_2-result.avif": {
"sha256": "d0cdb18f65cafc2abcec237acf33b8684472375ff373bf8c180dcf40b7ea9b24",
"source": "https://zoomstage.online/1/01_mob_2-result.avif"
},
"1/01_mob_2-result.jpg": {
"sha256": "53e7b5d9c34575d76a5781922366e306be625ff01f105bf199864230ba938b65",
"source": "https://zoomstage.online/1/01_mob_2-result.jpg"
},
"1/01_mob_2-result.webp": {
"sha256": "c4c75a1b60c34fe005a2ad4384dc941beee7247e7c9adfbc92cb237e7d8e09cf",
"source": "https://zoomstage.online/1/01_mob_2-result.webp"
},
"1/01_mob_3-thumb.avif": {
"sha256": "767d9c9e4da5f2ab3a504a4ef1456f4b5b76a9bdb024d2fd606f62cdeae539d2",
"source": "https://zoomstage.online/1/01_mob_3-thumb.avif"
},
"1/01_mob_3-thumb.jpg": {
"sha256": "bf19f05864048b961184aa93da76eb8ea04d22bad08cb6b5b4e43a1ae3ae254a",
"source": "https://zoomstage.online/1/01_mob_3-thumb.jpg"
},
"1/01_mob_3-thumb.webp": {
"sha256": "4b17692c86a231fea852d6284ac75f866c3d333849e1374b8d69706fab4a1c38",
"source": "https://zoomstage.online/1/01_mob_3-thumb.webp"
},
"1/02_desk_1-source.avif": {
"sha256": "1c042a21d14b3f6d1f04c107bf7799256478a04cbc266af3915b71cef560b84f",
"source": "https://zoomstage.online/1/02_desk_1-source.avif"
},
"1/02_desk_1-source.jpg": {
"sha256": "41f8dc885efdb159cd0283502dcfc7a7f215dddc01563599e6b83b0f27833bf9",
"source": "https://zoomstage.online/1/02_desk_1-source.jpg"
},
"1/02_desk_1-source.webp": {
"sha256": "e8d709607266e2f06807171ddf698cb0e3fa3f6b819447774dc0066cfe02e2be",
"source": "https://zoomstage.online/1/02_desk_1-source.webp"
},
"1/02_desk_2-result.avif": {
"sha256": "5e99f674fe14f59d4b953015873844295f69387a6e08ee3344097b874e03a42f",
"source": "https://zoomstage.online/1/02_desk_2-result.avif"
},
"1/02_desk_2-result.jpg": {
"sha256": "16ad5e78b26a686e40023f90b4999c5e483cd7f11510e4086562d1e1d1c26ffb",
"source": "https://zoomstage.online/1/02_desk_2-result.jpg"
},
"1/02_desk_2-result.webp": {
"sha256": "85479727ac36566b40e6892bae392c5c3b921c1f93eaec94bf3c7bdf2cb810b8",
"source": "https://zoomstage.online/1/02_desk_2-result.webp"
},
"1/02_desk_3-thumb.avif": {
"sha256": "f734a69a307c6a42d7fbad2014ee78ffb227ad2ec91cf8827220cff6ccbf959f",
"source": "https://zoomstage.online/1/02_desk_3-thumb.avif"
},
"1/02_desk_3-thumb.jpg": {
"sha256": "bba137700d987ce7fc1a27799f341766e14252375dfdd62a2ed22a52b42b584f",
"source": "https://zoomstage.online/1/02_desk_3-thumb.jpg"
},
"1/02_desk_3-thumb.webp": {
"sha256": "968dcd286a23f3815d13b648f9456f833b8c5065461cba55a1c1416b8de842b2",
"source": "https://zoomstage.online/1/02_desk_3-thumb.webp"
},
"1/02_mob_1-source.avif": {
"sha256": "b975eeef8d57a6d92a8d2f3c49eeedc76159c0bb1106cedbbfcba916082b3de6",
"source": "https://zoomstage.online/1/02_mob_1-source.avif"
},
"1/02_mob_1-source.jpg": {
"sha256": "83c98111cc63aa0d8af166174c17ab7270f27abce278aa09a6e62d6ad532fbd2",
"source": "https://zoomstage.online/1/02_mob_1-source.jpg"
},
"1/02_mob_1-source.webp": {
"sha256": "b3cfbe26ee2239b3e26b03e5b762b901285c482cb3e38ef87a8094d17b1277da",
"source": "https://zoomstage.online/1/02_mob_1-source.webp"
},
"1/02_mob_2-result.avif": {
"sha256": "758bea1e70eaa8c7bb4387b93b29e01ebd73f6f7cd886dccb515f33d633d21dc",
"source": "https://zoomstage.online/1/02_mob_2-result.avif"
},
"1/02_mob_2-result.jpg": {
"sha256": "bb587caf5834c9fe247631f87160703dc5e436c9240a3a1ccfe5e3a29ffb0df5",
"source": "https://zoomstage.online/1/02_mob_2-result.jpg"
},
"1/02_mob_2-result.webp": {
"sha256": "83debd3926dd0786ea7e379c6c78bd9e3001f41a4c7252a0f2caf01144b3a0cd",
"source": "https://zoomstage.online/1/02_mob_2-result.webp"
},
"1/02_mob_3-thumb.avif": {
"sha256": "38ba7656ac0556ce0173747791a6ec62602e988c25a88c4f7ffd78617f552320",
"source": "https://zoomstage.online/1/02_mob_3-thumb.avif"
},
"1/02_mob_3-thumb.jpg": {
"sha256": "b34b92a88a4d6618c47ede2f97a005e68a6a8f7ed4d8aaaa72180ea3e34abe2e",
"source": "https://zoomstage.online/1/02_mob_3-thumb.jpg"
},
"1/02_mob_3-thumb.webp": {
"sha256": "044bc3d59f2beb2e24b6f46ec8adbaada0c54277af11d281ec73a1bf77802d95",
"source": "https://zoomstage.online/1/02_mob_3-thumb.webp"
},
"1/03_desk_1-source.avif": {
"sha256": "1c945bc12bafd62c1f696887e64f5f1177339d6b6ca07b1b3abc04353ab76f4a",
"source": "https://zoomstage.online/1/03_desk_1-source.avif"
},
"1/03_desk_1-source.jpg": {
"sha256": "181e751a28448e6e50b8def188031b624598e413a38463229c25acc0ca1e9aaf",
"source": "https://zoomstage.online/1/03_desk_1-source.jpg"
},
"1/03_desk_1-source.webp": {
"sha256": "c034f5ef757effb3838e677dd5dc059468a8ef928e1a8068a64d938c45a706b7",
"source": "https://zoomstage.online/1/03_desk_1-source.webp"
},
"1/03_desk_2-result.avif": {
"sha256": "868a879a01c44d7d062dd52e565f1ca3c5c142a194c2ed34d3d858bbcaec99a1",
"source": "https://zoomstage.online/1/03_desk_2-result.avif"
},
"1/03_desk_2-result.jpg": {
"sha256": "2f3ffdbe2e94b6b10b44d98b43ed0a1ff29e24697528c1e6794e0f59e66f05c0",
"source": "https://zoomstage.online/1/03_desk_2-result.jpg"
},
"1/03_desk_2-result.webp": {
"sha256": "6def0f71d9b723ed5aa57e60966548b17e919874b7468a9f0d4cf1c9151a7782",
"source": "https://zoomstage.online/1/03_desk_2-result.webp"
},
"1/03_desk_3-thumb.avif": {
"sha256": "799ddb6d2825039051014687584a84deac0f17df835a96f5d164a61d2fae7809",
"source": "https://zoomstage.online/1/03_desk_3-thumb.avif"
},
"1/03_desk_3-thumb.jpg": {
"sha256": "7c872aebd11a98f26289df28b29bd6287ec725ddb7282d4ce3a940450426dfdb",
"source": "https://zoomstage.online/1/03_desk_3-thumb.jpg"
},
"1/03_desk_3-thumb.webp": {
"sha256": "6ea725369244d903fb33015d15a3ca4da68b09e7572c34b1c4df55592f7bb87c",
"source": "https://zoomstage.online/1/03_desk_3-thumb.webp"
},
"1/03_mob_1-source.avif": {
"sha256": "54e07c4c45c87bbe098930d5a1d462c63b8deb54d80c82342c98d9ba397d030a",
"source": "https://zoomstage.online/1/03_mob_1-source.avif"
},
"1/03_mob_1-source.jpg": {
"sha256": "76bd246fb6dd2c3029d3697e4cd58f78055aa8eea1264f4002e412877b76e2e9",
"source": "https://zoomstage.online/1/03_mob_1-source.jpg"
},
"1/03_mob_1-source.webp": {
"sha256": "0e57e3e248217a747914561e3fd1d0266d0023283d8b224c238fad1055b3b6ce",
"source": "https://zoomstage.online/1/03_mob_1-source.webp"
},
"1/03_mob_2-result.avif": {
"sha256": "eab15220712f4c6bf76a616a9820b47f3813ddeefe86d10ca8d79be1f3fa6713",
"source": "https://zoomstage.online/1/03_mob_2-result.avif"
},
"1/03_mob_2-result.jpg": {
"sha256": "180db5c1586a1a60f5348c58effd52a3dae3aaece5a73af95582df59a87ed61f",
"source": "https://zoomstage.online/1/03_mob_2-result.jpg"
},
"1/03_mob_2-result.webp": {
"sha256": "1cfb9d19807bdb655467424fa6e0ffaf30508d6a525fcf11060d29225e0a7c4a",
"source": "https://zoomstage.online/1/03_mob_2-result.webp"
},
"1/03_mob_3-thumb.avif": {
"sha256": "dfa4f76bbb32c1cce8b615f381c97efdd808d7270b4d783f594b46d2251cb299",
"source": "https://zoomstage.online/1/03_mob_3-thumb.avif"
},
"1/03_mob_3-thumb.jpg": {
"sha256": "a92745852596697f8efd2a488e19d67ccbfd252bda85670e768e3bd0517620fd",
"source": "https://zoomstage.online/1/03_mob_3-thumb.jpg"
},
"1/03_mob_3-thumb.webp": {
"sha256": "c51cdb975086282e140383fb19629b5c6f76948ff124be60650ffcb284c58bce",
"source": "https://zoomstage.online/1/03_mob_3-thumb.webp"
},
"1/04_desk_1-source.avif": {
"sha256": "65cb679b4ced37aae4761ab3ee6264ea3eebff4d78d86f5d46e6f4ea3afc89b8",
"source": "https://zoomstage.online/1/04_desk_1-source.avif"
},
"1/04_desk_1-source.jpg": {
"sha256": "dd919226b04a3b64d13f6167457aabd7a11c28a3deb2e67bc68b14dd295c2298",
"source": "https://zoomstage.online/1/04_desk_1-source.jpg"
},
"1/04_desk_1-source.webp": {
"sha256": "b84791838665c9c1f805561b5e4ce51560f2a8bef1960b369fff96f795d40ad3",
"source": "https://zoomstage.online/1/04_desk_1-source.webp"
},
"1/04_desk_2-result.avif": {
"sha256": "6916aad05d6780d591d61881dbea099dbf17a3cad20b90e5df5bac1411ec9441",
"source": "https://zoomstage.online/1/04_desk_2-result.avif"
},
"1/04_desk_2-result.jpg": {
"sha256": "f87ebfad0761aee740b6cbd3b49c560e51285a97bcd87299302128266de38e82",
"source": "https://zoomstage.online/1/04_desk_2-result.jpg"
},
"1/04_desk_2-result.webp": {
"sha256": "46fb0e3eabd78f903e3f962b286a0e86d05e1eab0fd14687bde957348439384f",
"source": "https://zoomstage.online/1/04_desk_2-result.webp"
},
"1/04_desk_3-thumb.avif": {
"sha256": "fe518d47c5b0a97ba14f1b4cd25f06e5ba341a7eb18bdf60dc5787254528aa74",
"source": "https://zoomstage.online/1/04_desk_3-thumb.avif"
},
"1/04_desk_3-thumb.jpg": {
"sha256": "cb33a65fc7988e80a211c5c0868b79813fc5f6ab673a66632ef5e79a11d12335",
"source": "https://zoomstage.online/1/04_desk_3-thumb.jpg"
},
"1/04_desk_3-thumb.webp": {
"sha256": "19f6fca38d2bda54a784d8789960db4b83f6c7d91200929083794fcbbb4b1b7c",
"source": "https://zoomstage.online/1/04_desk_3-thumb.webp"
},
"1/04_mob_1-source.avif": {
"sha256": "9272edcf3875ad80908933a4f99291caa3bf0388681ccc707bb32d3695f547cc",
"source": "https://zoomstage.online/1/04_mob_1-source.avif"
},
"1/04_mob_1-source.jpg": {
"sha256": "e7f552ae69e2d225e3c026e6fe3bcb9037a77a13fc457f4acb9302f3e2e664d2",
"source": "https://zoomstage.online/1/04_mob_1-source.jpg"
},
"1/04_mob_1-source.webp": {
"sha256": "47e22debe67835117c8b976add82e4757b0b37c16c8074e32e9e2001aa766dd6",
"source": "https://zoomstage.online/1/04_mob_1-source.webp"
},
"1/04_mob_2-result.avif": {
"sha256": "9365055422b8879dafff37ad4550463d4a37fa3d845a861720d65f18b6f7c17c",
"source": "https://zoomstage.online/1/04_mob_2-result.avif"
},
"1/04_mob_2-result.jpg": {
"sha256": "47baa6717063700ebeae7628fc0986ade2bd48009ff9a703aca96a2a120a069a",
"source": "https://zoomstage.online/1/04_mob_2-result.jpg"
},
"1/04_mob_2-result.webp": {
"sha256": "4f9eac846f26e36c92c4554112ed195e187f59957bf1eb05392c66923d92fd61",
"source": "https://zoomstage.online/1/04_mob_2-result.webp"
},
"1/04_mob_3-thumb.avif": {
"sha256": "4b4b44c176e1614d1c52c945e62be62dcccbf759af9ec554b835a40ab68dec75",
"source": "https://zoomstage.online/1/04_mob_3-thumb.avif"
},
"1/04_mob_3-thumb.jpg": {
"sha256": "2f237eb02fc3107548fbcf4e516cb022299045f4338d3bb5fb0c406c29950b67",
"source": "https://zoomstage.online/1/04_mob_3-thumb.jpg"
},
"1/04_mob_3-thumb.webp": {
"sha256": "ebe0250b95135ff36b478cba1725fe76b4a2e82e3423f0480a20dc420bdd04ee",
"source": "https://zoomstage.online/1/04_mob_3-thumb.webp"
},
"1/05_desk_1-source.avif": {
"sha256": "0190471497dc2c4ad4515366f7a875e0a63dc02850e0f2831308d9119e6635b3",
"source": "https://zoomstage.online/1/05_desk_1-source.avif"
},
"1/05_desk_1-source.jpg": {
"sha256": "7a97d9909f67dce302b1707ccdc4a6eee83351faed91e9e9229fcb163117083e",
"source": "https://zoomstage.online/1/05_desk_1-source.jpg"
},
"1/05_desk_1-source.webp": {
"sha256": "0a0ca94e76bc36ac0439c65739ab16efbd444cedb53e7119946bfc3dd3abadd9",
"source": "https://zoomstage.online/1/05_desk_1-source.webp"
},
"1/05_desk_2-result.avif": {
"sha256": "f7d43aa554dcf0f798770cf21b890002b55b4b175949fbeafc24bcee125707fd",
"source": "https://zoomstage.online/1/05_desk_2-result.avif"
},
"1/05_desk_2-result.jpg": {
"sha256": "652015813ad038945e780d97f9fe793eff8819927cab4cc27af99d7cf032d32c",
"source": "https://zoomstage.online/1/05_desk_2-result.jpg"
},
"1/05_desk_2-result.webp": {
"sha256": "29305239d806b67c19dcb47b898bf9bf46f2fa2f0301896c01d13e24140a8e8e",
"source": "https://zoomstage.online/1/05_desk_2-result.webp"
},
"1/05_desk_3-thumb.avif": {
"sha256": "d02811e4deff120d29be88d3a8119a4f931828c69ca812179922753c5e70d684",
"source": "https://zoomstage.online/1/05_desk_3-thumb.avif"
},
"1/05_desk_3-thumb.jpg": {
"sha256": "068cacabeca713c006acdbe24a695a7271849832174515bd307e3eb40b468748",
"source": "https://zoomstage.online/1/05_desk_3-thumb.jpg"
},
"1/05_desk_3-thumb.webp": {
"sha256": "cccd83ada4e4c2ddd22bbb9f0c1087f656465e7aea350adb148b91bf300ae9b5",
"source": "https://zoomstage.online/1/05_desk_3-thumb.webp"
},
"1/05_mob_1-source.avif": {
"sha256": "032d2dfa889891765294790a018ded36ffa03d09e7bcdce2e64e4127e3bdd60d",
"source": "https://zoomstage.online/1/05_mob_1-source.avif"
},
"1/05_mob_1-source.jpg": {
"sha256": "1d2e9696fb2d858685794eca3ae9daa26477d4b69f30d7c38ba0299e0beaceb9",
"source": "https://zoomstage.online/1/05_mob_1-source.jpg"
},
"1/05_mob_1-source.webp": {
"sha256": "3b92e3c669b4edb03358c2b0e41de4d3d7e9562f56fa59430995632241c9cd7b",
"source": "https://zoomstage.online/1/05_mob_1-source.webp"
},
"1/05_mob_2-result.avif": {
"sha256": "5af8bcacbf69b73da9777a623e114a49904156855ba3711a480cc4067ee4621e",
"source": "https://zoomstage.online/1/05_mob_2-result.avif"
},
"1/05_mob_2-result.jpg": {
"sha256": "6a6df8a6958c4747dc8c83af92eb6364336959593d95c0575f49e91b0d7da27f",
"source": "https://zoomstage.online/1/05_mob_2-result.jpg"
},
"1/05_mob_2-result.webp": {
"sha256": "dc4049048ce69aecc38245dcb35f2ff4db68a335bd4945da542ef85878a5412e",
"source": "https://zoomstage.online/1/05_mob_2-result.webp"
},
"1/05_mob_3-thumb.avif": {
"sha256": "9f60e27a7a3937788fe4c42731a0f0405e6b48079862f9ee14382728654661b8",
"source": "https://zoomstage.online/1/05_mob_3-thumb.avif"
},
"1/05_mob_3-thumb.jpg": {
"sha256": "5dd8457be9a4662cf680428f5d86829c06812c37417aa2fd7508ef3e6ade8891",
"source": "https://zoomstage.online/1/05_mob_3-thumb.jpg"
},
"1/05_mob_3-thumb.webp": {
"sha256": "5b125c06a4f91a9acf20302b7785c1c6b04e72f2837eb8cc7654bddcfb2b8010",
"source": "https://zoomstage.online/1/05_mob_3-thumb.webp"
},
"2/01_desk_1-source.avif": {
"sha256": "05693cd307bbd8cc899408b40d0eb069f2ebca3a6d562572dfbe9beb1fe09e6a",
"source": "https://zoomstage.online/2/01_desk_1-source.avif"
},
"2/01_desk_1-source.jpg": {
"sha256": "2aa5e882db83e33c7585cea726a1a2eb7b1771180c04f2a7b2f0e19fbefa5d35",
"source": "https://zoomstage.online/2/01_desk_1-source.jpg"
},
"2/01_desk_1-source.webp": {
"sha256": "f7451a5557c45b7049183d7e762a343846bf86173c6f4e6ae64628412abebf72",
"source": "https://zoomstage.online/2/01_desk_1-source.webp"
},
"2/01_desk_2-result.avif": {
"sha256": "a98abb80ec628c0af732ea70bbe9eca0b6fa2be7404aac9b398396f2049a81fd",
"source": "https://zoomstage.online/2/01_desk_2-result.avif"
},
"2/01_desk_2-result.jpg": {
"sha256": "4d99b39af2b30a0431b406cae60a9600cbcf5be2aac53a3eb006e9b5dced2bd8",
"source": "https://zoomstage.online/2/01_desk_2-result.jpg"
},
"2/01_desk_2-result.webp": {
"sha256": "3e384151667ab4561037ad629a22c2ca789d1d1ee6ef4d90839bcdf7010b7b53",
"source": "https://zoomstage.online/2/01_desk_2-result.webp"
},
"2/01_desk_3-thumb.avif": {
"sha256": "403960e1e6ace0fd8b41df74fac06c2f69178dfcba58f09c7ea4e60a166b1a6e",
"source": "https://zoomstage.online/2/01_desk_3-thumb.avif"
},
"2/01_desk_3-thumb.jpg": {
"sha256": "09189ca450ea070087ea704c2ef1f3ad1c4764c42c5b79318c2917896c2e8cad",
"source": "https://zoomstage.online/2/01_desk_3-thumb.jpg"
},
"2/01_desk_3-thumb.webp": {
"sha256": "9d1ff2dd86970e8bf115bda669876c1a652d6212375c0b251a9f80ee553f42c0",
"source": "https://zoomstage.online/2/01_desk_3-thumb.webp"
},
"2/01_mob_1-source.avif": {
"sha256": "90225b3b0f89187519d10e2bc407cda247e7910efe72c6b3df0d64cecd12385d",
"source": "https://zoomstage.online/2/01_mob_1-source.avif"
},
"2/01_mob_1-source.jpg": {
"sha256": "fb4fbc67b3838c402fcadb5ce22ec0cbc4d8f89d3901f3f365590e49553287ae",
"source": "https://zoomstage.online/2/01_mob_1-source.jpg"
},
"2/01_mob_1-source.webp": {
"sha256": "98bf7f4c6ffb7e1bd16802f68ced0728d4c6d71ea982cab5f21b1501e3673ca3",
"source": "https://zoomstage.online/2/01_mob_1-source.webp"
},
"2/01_mob_2-result.avif": {
"sha256": "e23bdac9af1daf3f4c62464f7de42399741d46e00fe627726f06f9e8f54c463a",
"source": "https://zoomstage.online/2/01_mob_2-result.avif"
},
"2/01_mob_2-result.jpg": {
"sha256": "537a51311e74521ee1386103e59005bed2a8025f8d986253884bb8b15739e1f3",
"source": "https://zoomstage.online/2/01_mob_2-result.jpg"
},
"2/01_mob_2-result.webp": {
"sha256": "38d16c07bbf2ecd4b430c9d19b57ab518467bcc39f81986a7aeb21b1c11aa157",
"source": "https://zoomstage.online/2/01_mob_2-result.webp"
},
"2/01_mob_3-thumb.avif": {
"sha256": "8bd19f88579f4e70af7909e9c9989cde0d95e4b320381f433566bf389ba80177",
"source": "https://zoomstage.online/2/01_mob_3-thumb.avif"
},
"2/01_mob_3-thumb.jpg": {
"sha256": "4c6d6858289727872c0aa73c9f2dd44197d3bb46efe0c8a4619f2974e97175e1",
"source": "https://zoomstage.online/2/01_mob_3-thumb.jpg"
},
"2/01_mob_3-thumb.webp": {
"sha256": "d1a0ba2c7e7595a244fd8cbaa167956336855657e2b65f3e65f225e2442cc27b",
"source": "https://zoomstage.online/2/01_mob_3-thumb.webp"
},
"2/02_desk_1-source.avif": {
"sha256": "114dfdf3d6b4157ccf2c70c0658a4ce63c0c4416fe7512fb228722ebad88352c",
"source": "https://zoomstage.online/2/02_desk_1-source.avif"
},
"2/02_desk_1-source.jpg": {
"sha256": "256d22ac9c7b30214c761658eddf2984cf46d6a319e1e2b3a3f7f2bae8db840d",
"source": "https://zoomstage.online/2/02_desk_1-source.jpg"
},
"2/02_desk_1-source.webp": {
"sha256": "2fd47a431be1c90f83d864d3722fa82594dc952a7ac0ecb53427d3b44b903b63",
"source": "https://zoomstage.online/2/02_desk_1-source.webp"
},
"2/02_desk_2-result.avif": {
"sha256": "580c7487ffecce9c42c31243ffb1e10c0a1ee6237d1780a52491753c7247904f",
"source": "https://zoomstage.online/2/02_desk_2-result.avif"
},
"2/02_desk_2-result.jpg": {
"sha256": "95c6930aff6a7b1f13a1216bd2c2e7bf33274295c7827b79558705f65fe9a698",
"source": "https://zoomstage.online/2/02_desk_2-result.jpg"
},
"2/02_desk_2-result.webp": {
"sha256": "f002f826030ff3d220480bcc822d556ac3934e7c75c36475fbd9bef05810f928",
"source": "https://zoomstage.online/2/02_desk_2-result.webp"
},
"2/02_desk_3-thumb.avif": {
"sha256": "0c53447ee56c0cac91e7006b2d235965c31603cb689108ba22afd72c8dbd7b5b",
"source": "https://zoomstage.online/2/02_desk_3-thumb.avif"
},
"2/02_desk_3-thumb.jpg": {
"sha256": "cfd28c223a9107aa5a9481988fe421c461d040e9bfcb5c20d1d4e6c3a28478eb",
"source": "https://zoomstage.online/2/02_desk_3-thumb.jpg"
},
"2/02_desk_3-thumb.webp": {
"sha256": "d6716f7e0011143ff7712567552c6ebf66f862e789904f721c6833f6f949976a",
"source": "https://zoomstage.online/2/02_desk_3-thumb.webp"
},
"2/02_mob_1-source.avif": {
"sha256": "15c457ddc50eeee2df0bfac945de31b0c77631a135168f417ec5fbac4c07767f",
"source": "https://zoomstage.online/2/02_mob_1-source.avif"
},
"2/02_mob_1-source.jpg": {
"sha256": "b03a29d34c3f88b59097c7747b5791c478bbd5dd3e0ba645e82ff3d6a1be8446",
"source": "https://zoomstage.online/2/02_mob_1-source.jpg"
},
"2/02_mob_1-source.webp": {
"sha256": "e044e15e5d7857c0cf70981427b7c8082427093fd27ef39ff5f912e892c6fe3c",
"source": "https://zoomstage.online/2/02_mob_1-source.webp"
},
"2/02_mob_2-result.avif": {
"sha256": "8f75bdcdc8f1406c20a2664df31ae0aec2a6ae14638469028d8d83ae70cd4adc",
"source": "https://zoomstage.online/2/02_mob_2-result.avif"
},
"2/02_mob_2-result.jpg": {
"sha256": "9ddc102433c7a064c660b0a0c852fad47a399605c4d5c730b830139215cb7705",
"source": "https://zoomstage.online/2/02_mob_2-result.jpg"
},
"2/02_mob_2-result.webp": {
"sha256": "37043364124819bc6eb4760ca0e299aab2aa8aa36e291482d6f0a3725816e3c0",
"source": "https://zoomstage.online/2/02_mob_2-result.webp"
},
"2/02_mob_3-thumb.avif": {
"sha256": "e7ee1b0e7886289c125c8c46726c287fd0abdc8cdf469a0636b8b28079729d04",
"source": "https://zoomstage.online/2/02_mob_3-thumb.avif"
},
"2/02_mob_3-thumb.jpg": {
"sha256": "e8ba5aa406468f0f0be1f985a844aa33ed25b078c2c6ace95c640d5a158e2af2",
"source": "https://zoomstage.online/2/02_mob_3-thumb.jpg"
},
"2/02_mob_3-thumb.webp": {
"sha256": "37d2f43762cc15ce84fff68a50f688dd79e8c124797d58b819d83f8fe92580ee",
"source": "https://zoomstage.online/2/02_mob_3-thumb.webp"
},
"2/03_desk_1-source.avif": {
"sha256": "33afc3a2a73b0dec174fcb1d900b2ef66c1ecf5b67edb97a25648c7a93cb1b83",
"source": "https://zoomstage.online/2/03_desk_1-source.avif"
},
"2/03_desk_1-source.jpg": {
"sha256": "752d8eb31d5874ccdab42d9275065c6d95bb554a12bde6bfafce4b6c2eda673d",
"source": "https://zoomstage.online/2/03_desk_1-source.jpg"
},
"2/03_desk_1-source.webp": {
"sha256": "d4386879477591872078eeb46a225717ece6717b717884f4841b58d981e74815",
"source": "https://zoomstage.online/2/03_desk_1-source.webp"
},
"2/03_desk_2-result.avif": {
"sha256": "832fd61bdf1b36225c6122f488b2fc655c9ef45209dd0db24e47469e09ccc62c",
"source": "https://zoomstage.online/2/03_desk_2-result.avif"
},
"2/03_desk_2-result.jpg": {
"sha256": "0986040558bd81eb454981519fffffc518dc2595fd8def98a696de58b6c714f4",
"source": "https://zoomstage.online/2/03_desk_2-result.jpg"
},
"2/03_desk_2-result.webp": {
"sha256": "de09ce8024af78fa5176ce7b4aeaaa01dc5c25a41c4d73c248596a8c63de974f",
"source": "https://zoomstage.online/2/03_desk_2-result.webp"
},
"2/03_desk_3-thumb.avif": {
"sha256": "73a0931dfe921a0c6afd0f34308a4ec2f358c19921abb0386968e4235d7bf677",
"source": "https://zoomstage.online/2/03_desk_3-thumb.avif"
},
"2/03_desk_3-thumb.jpg": {
"sha256": "60d6e5e78535e73bf72740a9908e79d4bd7030b8ab5684f4cb0ea5533a1073ba",
"source": "https://zoomstage.online/2/03_desk_3-thumb.jpg"
},
"2/03_desk_3-thumb.webp": {
"sha256": "6cc52a6d96ef01d5ffd74682fce859fe831948229aefcf3b0dc959f8ed1fbaf6",
"source": "https://zoomstage.online/2/03_desk_3-thumb.webp"
},
"2/03_mob_1-source.avif": {
"sha256": "1912d1d5050f00cd03857ee0776a54443ac398f91abcc043cbc5f040325d5f71",
"source": "https://zoomstage.online/2/03_mob_1-source.avif"
},
"2/03_mob_1-source.jpg": {
"sha256": "7668d0a7c406c31e170bf98c578b391357a515fe4ffc376a4f530b7757f396fc",
"source": "https://zoomstage.online/2/03_mob_1-source.jpg"
},
"2/03_mob_1-source.webp": {
"sha256": "f345fdc850ca662388d5bdc6adb6a6579aa129baf808acb117dd848682a4c341",
"source": "https://zoomstage.online/2/03_mob_1-source.webp"
},
"2/03_mob_2-result.avif": {
"sha256": "78044eb77595fdeaca2ba8531d6c41b5b9e0b0d4b7474760f0c515446f82756f",
"source": "https://zoomstage.online/2/03_mob_2-result.avif"
},
"2/03_mob_2-result.jpg": {
"sha256": "40131ba99d2a102abc4f18c94223639bd498f9d9837426b5697b146a6ee91496",
"source": "https://zoomstage.online/2/03_mob_2-result.jpg"
},
"2/03_mob_2-result.webp": {
"sha256": "2e649b961c6819270b7536e709c9bdf893acb421f65241599290ebc1a53d4b7b",
"source": "https://zoomstage.online/2/03_mob_2-result.webp"
},
"2/03_mob_3-thumb.avif": {
"sha256": "da91d0c4414bf9feb109ba6457ddd16ed65d6ba4a175db882e8fd289d0eadd28",
"source": "https://zoomstage.online/2/03_mob_3-thumb.avif"
},
"2/03_mob_3-thumb.jpg": {
"sha256": "6b99576ca8ca5cd5131b453c40d39ae7430d81b6b65d9bd431d0533bad349a1a",
"source": "https://zoomstage.online/2/03_mob_3-thumb.jpg"
},
"2/03_mob_3-thumb.webp": {
"sha256": "45223504e5d8ff23102343586aaf5bb4497f7038e47abf3309bcf948e5bbfaaf",
"source": "https://zoomstage.online/2/03_mob_3-thumb.webp"
},
"2/04_desk_1-source.avif": {
"sha256": "75167d61ba549e6cd7857ca2173f7e407fde4f899842eac8838a83eb804c0030",
"source": "https://zoomstage.online/2/04_desk_1-source.avif"
},
"2/04_desk_1-source.jpg": {
"sha256": "2d7b4e3e706cb4e2527fff0b4a9e557f609c728d74e9cbc8cc8f24581a48bf00",
"source": "https://zoomstage.online/2/04_desk_1-source.jpg"
},
"2/04_desk_1-source.webp": {
"sha256": "903eea777948a5a627464abada4e51c8b1aee9937e0856fde738450745953c9e",
"source": "https://zoomstage.online/2/04_desk_1-source.webp"
},
"2/04_desk_2-result.avif": {
"sha256": "e3e56d8cdcf60151d66fd9c5905febb8153aa333d1330f628f4235334c29120d",
"source": "https://zoomstage.online/2/04_desk_2-result.avif"
},
"2/04_desk_2-result.jpg": {
"sha256": "f631a34a741ab17f493e45ef9c5868e550856207810d6f20bff889358eb01963",
"source": "https://zoomstage.online/2/04_desk_2-result.jpg"
},
"2/04_desk_2-result.webp": {
"sha256": "f387ecb2e3e00720f5de37ad96ee577ac19f01ea091e86e833d7db9c1ca87f1f",
"source": "https://zoomstage.online/2/04_desk_2-result.webp"
},
"2/04_desk_3-thumb.avif": {
"sha256": "e018297227e7abc582aea91c1eca9e47c3781ecb56dd61596badf9c1e9efd1db",
"source": "https://zoomstage.online/2/04_desk_3-thumb.avif"
},
"2/04_desk_3-thumb.jpg": {
"sha256": "82c31dede3dc117a359cdcab81db9d7e0f615d386ee76748a4a4679e596ebb43",
"source": "https://zoomstage.online/2/04_desk_3-thumb.jpg"
},
"2/04_desk_3-thumb.webp": {
"sha256": "d247ef5916e553300bd5cad2e6917489a56f5e275e12ed24f8d516cae5ebf677",
"source": "https://zoomstage.online/2/04_desk_3-thumb.webp"
},
"2/04_mob_1-source.avif": {
"sha256": "436a746e9b07dbca356b2304415e5a3d38c130dcad771a474ece4c566e3923f0",
"source": "https://zoomstage.online/2/04_mob_1-source.avif"
},
"2/04_mob_1-source.jpg": {
"sha256": "8e9487d89f0f333c894d4b8791605d5ae3d2530aa53e5d20220aef3afcb0ce31",
"source": "https://zoomstage.online/2/04_mob_1-source.jpg"
},
"2/04_mob_1-source.webp": {
"sha256": "d566c804d9687dead9196f80a6d4ce40e9d667700621a07e15b50e02ecf149b6",
"source": "https://zoomstage.online/2/04_mob_1-source.webp"
},
"2/04_mob_2-result.avif": {
"sha256": "0226d6d6dd058b403d144c1abf73177e1b90b24d64be9127c830f6e64fb33f89",
"source": "https://zoomstage.online/2/04_mob_2-result.avif"
},
"2/04_mob_2-result.jpg": {
"sha256": "0117b590c82a086296cf6bca3782518ba57f18b2af4e119afd76bfc56977b4f3",
"source": "https://zoomstage.online/2/04_mob_2-result.jpg"
},
"2/04_mob_2-result.webp": {
"sha256": "c5b07346dcc09f77d1cc724ee89cdefd9a14566bf23d3314ab4b1864b10e2619",
"source": "https://zoomstage.online/2/04_mob_2-result.webp"
},
"2/04_mob_3-thumb.avif": {
"sha256": "ed98c7aa386b988ece328f7a9dbaae6f733a2d32318bcde3ecf75fa2676ffe03",
"source": "https://zoomstage.online/2/04_mob_3-thumb.avif"
},
"2/04_mob_3-thumb.jpg": {
"sha256": "d1a3e8bf1a6ad0fb58f402ed919a90a5be3586887e4d43fb8a949f04811972d8",
"source": "https://zoomstage.online/2/04_mob_3-thumb.jpg"
},
"2/04_mob_3-thumb.webp": {
"sha256": "b28bd3b61bd19790536e1e22956f7a66652c1f73739b7ed95437359906749238",
"source": "https://zoomstage.online/2/04_mob_3-thumb.webp"
},
"2/05_desk_1-source.avif": {
"sha256": "8fb22211499a316a708011d18f11069de884e316467242a4999046e8dbb35aa0",
"source": "https://zoomstage.online/2/05_desk_1-source.avif"
},
"2/05_desk_1-source.jpg": {
"sha256": "8414aebaf6e34d287eed884eff8db9e56d1d30927a568e714dd4221b1fbda170",
"source": "https://zoomstage.online/2/05_desk_1-source.jpg"
},
"2/05_desk_1-source.webp": {
"sha256": "92e3d15e39c065c03a75868516618a2357e2762defb4a3815bba73c6b197e2f4",
"source": "https://zoomstage.online/2/05_desk_1-source.webp"
},
"2/05_desk_2-result.avif": {
"sha256": "44bd818c5ac218e6f2065e469bae2999e7deac507c73509c24d47fdcb689946f",
"source": "https://zoomstage.online/2/05_desk_2-result.avif"
},
"2/05_desk_2-result.jpg": {
"sha256": "98ee8d691eb8a92ec03c1d1e1298357bd9d6aa5606a2afda74a80afcbbe8ae2b",
"source": "https://zoomstage.online/2/05_desk_2-result.jpg"
},
"2/05_desk_2-result.webp": {
"sha256": "eaf28272f55fc078a35758ffc5cebe19cea17b408e47a05851c07d6c15e0e625",
"source": "https://zoomstage.online/2/05_desk_2-result.webp"
},
"2/05_desk_3-thumb.avif": {
"sha256": "755be0874ad7c56d64782ccb709481d987c1b87a2e8b9c04d994118e261f9b6e",
"source": "https://zoomstage.online/2/05_desk_3-thumb.avif"
},
"2/05_desk_3-thumb.jpg": {
"sha256": "cdd5d752af32cdfa1471bcdba2924b5a83adb489967b666722bfb0307a0b5221",
"source": "https://zoomstage.online/2/05_desk_3-thumb.jpg"
},
"2/05_desk_3-thumb.webp": {
"sha256": "f471e438efe927cd8fe366416520dbb2b866f855bd41e70e42a48dc79f461435",
"source": "https://zoomstage.online/2/05_desk_3-thumb.webp"
},
"2/05_mob_1-source.avif": {
"sha256": "f6b50e370340a6e4df76e73372155c5897cbe621bf90f1afe9f8aaef8425f1f4",
"source": "https://zoomstage.online/2/05_mob_1-source.avif"
},
"2/05_mob_1-source.jpg": {
"sha256": "f848e12d9c0e96720ac8fa85eae38d3961475dd0d00a8c3817a3a1f4dad01454",
"source": "https://zoomstage.online/2/05_mob_1-source.jpg"
},
"2/05_mob_1-source.webp": {
"sha256": "2bb823ec807e2766363c06f614f9f89b9e23724d63118aca525c1639d673e78f",
"source": "https://zoomstage.online/2/05_mob_1-source.webp"
},
"2/05_mob_2-result.avif": {
"sha256": "4907bb130e8c9fa0bd4391b2f6205973645ae47b22611967f7543f09227f284f",
"source": "https://zoomstage.online/2/05_mob_2-result.avif"
},
"2/05_mob_2-result.jpg": {
"sha256": "4896bab2b2a411ba8bb8b6b4df004651d7727a900e563a9344584cd5fadf4562",
"source": "https://zoomstage.online/2/05_mob_2-result.jpg"
},
"2/05_mob_2-result.webp": {
"sha256": "e7d05c058fca2956c9f0c3c63b52f405dd26d7766a0ea1841101e587142c38e9",
"source": "https://zoomstage.online/2/05_mob_2-result.webp"
},
"2/05_mob_3-thumb.avif": {
"sha256": "4fecf25c3b250fb9b41e4a6fd03b85b9e23fb838d38dd8427c3ed938921bc2e5",
"source": "https://zoomstage.online/2/05_mob_3-thumb.avif"
},
"2/05_mob_3-thumb.jpg": {
"sha256": "1bfeca6d5b9b40b89d1deec43b08351fd9d4e8f9eb57032ab376c42806ea3a32",
"source": "https://zoomstage.online/2/05_mob_3-thumb.jpg"
},
"2/05_mob_3-thumb.webp": {
"sha256": "71fef2a518f1cb8da99ad6cef34c1603fa8abdd839182d70825b692430684bf8",
"source": "https://zoomstage.online/2/05_mob_3-thumb.webp"
},
"bootstrap.min.c.css": {
"sha256": "e3450fead306400ba24a20e2af963e6d9902ce6532eeed3ef5dbbe3b84543210",
"source": "https://zoomstage.online/bootstrap.min.c.css"
},
"bootstrap.min.css": {
"sha256": "f886516f3d41e9e7bd994c7f7a39a89cafae9483f90396cb0ddeafe8d1ea5e72",
"source": "https://zoomstage.online/bootstrap.min.css"
},
"docs/logo-alt.svg": {
"sha256": "bf31f25bd0cb636b0a4625747be4a0e4a7cd8f9d5c4e1fc43580556e039d32e8",
"source": "brands/zoomstage.online/dark.svg"
},
"docs/logo.svg": {
"sha256": "cce6e077430eab850b7f4409d1c42f95347459efe753d0d65415d65b6f3a5b01",
"source": "brands/zoomstage.online/light.svg"
},
"docs/privacy_policy.pdf": {
"sha256": "61b483911be875810a7920efa5ddb9c9a132633cfe1d5d9efeb8f9d487cacf83",
"source": "https://zoomzilla.ru/assets/6fd2ff6b/docs/privacy_policy.pdf"
},
"docs/public_offer.pdf": {
"sha256": "4c1f05bcb818c688e89ad5b4ee5432cd810b943603340b1859fe54426fe27272",
"source": "https://zoomzilla.ru/assets/6fd2ff6b/docs/public_offer.pdf"
},
"docs/tariffs.pdf": {
"sha256": "d6d73d7e2a1d976d56d3a5ede0a5ce2e9144743e2fcbd14a8d73d6e159c383b9",
"source": "https://zoomzilla.ru/assets/6fd2ff6b/docs/tariffs.pdf"
},
"docs/user_agreement.pdf": {
"sha256": "2f5902d689e229abc4b3971831681e6e792cdedebcc9764cfe81521a10185a58",
"source": "https://zoomzilla.ru/assets/6fd2ff6b/docs/user_agreement.pdf"
},
"favicon/android-chrome-192x192.png": {
"sha256": "83e311e0f534c76de9c576da347daa4d8e0fd4f16150092d8f06441803238417",
"source": "https://zoomzilla.ru/assets/8f259a2e/android-chrome-192x192.png"
},
"favicon/android-chrome-512x512.png": {
"sha256": "1516ed9097f8abb66a632dde6c5bb7afcf096118710330387a18b3eaf2c1b793",
"source": "https://zoomzilla.ru/assets/8f259a2e/android-chrome-512x512.png"
},
"favicon/apple-touch-icon.png": {
"sha256": "0f8595d2116c03b879e5687fb0696b1802025c01437d689328bee5ce289d0a55",
"source": "https://zoomzilla.ru/assets/8f259a2e/apple-touch-icon.png"
},
"favicon/favicon-16x16.png": {
"sha256": "180344989e829ebb9a359a4210e9a6d55151f8637f004dfe0d7e1e127d81b867",
"source": "https://zoomzilla.ru/assets/8f259a2e/favicon-16x16.png"
},
"favicon/favicon-32x32.png": {
"sha256": "ecce74843584fa071053560f0e0b345ef91d707df87d3525bfc96286865745ab",
"source": "https://zoomzilla.ru/assets/8f259a2e/favicon-32x32.png"
},
"favicon/favicon-48x48.png": {
"sha256": "45a472452abfee5cd63387331e73150a718d6b11da0cba92b9b49d52699b2132",
"source": "https://zoomzilla.ru/assets/8f259a2e/favicon-48x48.png"
},
"favicon/favicon.ico": {
"sha256": "cbd8332a0def715eee9072e0ac81241728a5cc4a1b91aef042a35771c7c5526b",
"source": "https://zoomzilla.ru/assets/8f259a2e/favicon.ico"
},
"favicon/favicon.svg": {
"sha256": "85d52e14155f2f83f92c77b5188a5304902970988a0e639c6fa6b2683f794280",
"source": "https://zoomzilla.ru/assets/8f259a2e/favicon.svg"
},
"favicon/site.webmanifest": {
"sha256": "58b5df675b9c5d25a4b6b470a157be834f3f8f6862a4da86fa5b279ebd8136bd",
"source": "https://zoomzilla.ru/assets/8f259a2e/site.webmanifest"
},
"fonts/Inter-Medium.woff2": {
"sha256": "d6e2531976fa618cede5863027366e4011760b0610d7136a8f7d7ed149c26348",
"source": "https://zoomstage.online/fonts/Inter-Medium.woff2"
},
"fonts/Inter-Regular.woff2": {
"sha256": "413a527c0ed1833fea654b2bb065bda9ebe46bc1f585cf2d481f8a79dbc51610",
"source": "https://zoomstage.online/fonts/Inter-Regular.woff2"
},
"fonts/Inter-SemiBold.woff2": {
"sha256": "07d609457ea2e93be9e7b5dd8e9a9a156d2e80144bcd94cf0adf05c553da053b",
"source": "https://zoomstage.online/fonts/Inter-SemiBold.woff2"
},
"images/bg.jpg": {
"sha256": "2742589ea5f82d7fd68ddc98c4fb7a685e5eb79e33d428d519d5b7577e8a307b",
"source": "https://zoomstage.online/images/bg.jpg"
},
"images/confused.svg": {
"sha256": "ac3eb4691f4ec9556d3f2a70b8cb349fa9afc08dfd9526b20a6242b6b8d35048",
"source": "https://zoomstage.online/images/confused.svg"
},
"images/dark-bg.jpg": {
"sha256": "0d3fa322a523308abb7ffb899507d8b6218bc01c056da9ae741e44910479df61",
"source": "https://zoomstage.online/images/dark-bg.jpg"
},
"images/dropzone-bg.svg": {
"sha256": "431ba883e78ea0385e3f60539c1d3814ef93c05c0df6dbdea2d79836e9fe4a16",
"source": "https://zoomstage.online/images/dropzone-bg.svg"
},
"images/dropzone-icon.svg": {
"sha256": "8cb388e9bc4f8467ca0ebc0ab4ac3e78912f8481dcdd8a5434b7d6c921c64a47",
"source": "https://zoomstage.online/images/dropzone-icon.svg"
},
"images/modal-bg-2.svg": {
"sha256": "217f3df853073426079c6d79cffeeb0b499a5d671b46adb2e186de5e210757c2",
"source": "https://zoomstage.online/images/modal-bg-2.svg"
},
"images/payment.svg": {
"sha256": "031ec0a1e202ccecb128bbe2d1cc79b497166144a1a97529da2fe5293142044c",
"source": "https://zoomstage.online/images/payment.svg"
},
"images/sandglass.svg": {
"sha256": "914f8be205f4535690a5a6d64745e20a18fdc3a9ffc83d2574cde1c0a3100ad3",
"source": "https://zoomstage.online/images/sandglass.svg"
},
"images/spinner.svg": {
"sha256": "102699ae937c28b44270e49be78ee607cb795a6cc4004d38e2bd18cee3944845",
"source": "https://zoomstage.online/images/spinner.svg"
},
"images/sprite.svg": {
"sha256": "36c7e1c142c883f746eb5851278a34249897ec16aadfbbb9d9bbf5b11336459b",
"source": "https://zoomstage.online/images/sprite.svg"
},
"style.css": {
"sha256": "7bc7744dff998008439c968e347d3ae39f2de6f073427dbde8c04d8cca9af562",
"source": "https://zoomstage.online/style.css"
},
"style.min.c.css": {
"sha256": "7c3d669e77c8d7e93cc7743b684b5e4c4e72289f452b05f140744506549d12e9",
"source": "https://zoomstage.online/style.min.c.css"
},
"style.min.css": {
"sha256": "4b161a767d1b18215d2428b4aba802877e140bd14af497de826c0ac50584def7",
"source": "https://zoomstage.online/style.min.css"
}
}
}
+62
View File
@@ -0,0 +1,62 @@
{
"zoomstage.online": {
"label": "ZoomStage",
"issue": 12,
"logos": {
"docs/logo.svg": {
"file": "brands/zoomstage.online/light.svg",
"sha256": "cce6e077430eab850b7f4409d1c42f95347459efe753d0d65415d65b6f3a5b01"
},
"docs/logo-alt.svg": {
"file": "brands/zoomstage.online/dark.svg",
"sha256": "bf31f25bd0cb636b0a4625747be4a0e4a7cd8f9d5c4e1fc43580556e039d32e8"
}
},
"replace_from": {
"docs/logo.svg": "1be54a2f9aa40b6d8740fd3367f045c5edf1cdea87c26d67b60cf663d4a2436f",
"docs/logo-alt.svg": "919b0a436e6ffea1ec833a28e5b8c6f4d26029944a7d555ca4d61da63ea1f86f"
}
},
"zoomila.ru": {
"label": "Zoomila",
"issue": 21,
"logos": {
"docs/logo.svg": {
"file": "brands/zoomila.ru/light.svg",
"sha256": "5dec39ea1c5966b01382b9e36ac8dd9d832855a6f0c73d21ea065ed1a55ac614"
},
"docs/logo-alt.svg": {
"file": "brands/zoomila.ru/dark.svg",
"sha256": "06341251d98a4562e0308685da7f5a6d674c6bff4cd997f473fc30579c91e89e"
}
}
},
"zoomika.ru": {
"label": "Zoomika",
"issue": 21,
"logos": {
"docs/logo.svg": {
"file": "brands/zoomika.ru/light.svg",
"sha256": "9b38cfe87b8f4ba34744e1d35e064a89937ec69ad2b5e342096d1c99f6906ecd"
},
"docs/logo-alt.svg": {
"file": "brands/zoomika.ru/dark.svg",
"sha256": "0a5ecf7de391ebcd18b10f1eb7e0c2170dc52c6e1d7a5f57b83a5aff5dcd7762"
}
}
},
"zoompixel.ru": {
"label": "ZoomPixel",
"issue": 21,
"logos": {
"docs/logo.svg": {
"file": "brands/zoompixel.ru/light.svg",
"sha256": "ffe43ed2643144ed4a5ead8a9f0bc16c6adcdd4b3984e5411dc69d5f98ff3bdd"
},
"docs/logo-alt.svg": {
"file": "brands/zoompixel.ru/dark.svg",
"sha256": "a069b40fe1e6d15a928872ba48b430aa3a13fcfbf5ca8282fd68cc20b5686f0c"
}
}
}
}
View File
Whitespace-only changes.
+76 -54
View File
@@ -1,7 +1,7 @@
body { body {
font-family: "Inter", sans-serif; font-family: "Inter", sans-serif;
/* fix WBE70 */ /* Корневые отступы страницы задаёт общий layout. */
padding: unset !important; padding: unset !important;
overflow: auto !important; overflow: auto !important;
} }
@@ -108,39 +108,6 @@ body {
border: none; border: none;
} }
.theme-toggle {
align-items: center;
background-color: #F8F9FB;
border: 1px solid #E5E5E7;
padding: 4px;
box-sizing: border-box;
border-radius: 16px;
}
.theme-toggle .btn {
padding: 0;
border: none;
border-radius: 0;
background-color: transparent;
display: flex;
justify-content: center;
align-items: center;
width: 32px;
height: 32px;
outline: none !important;
box-shadow: none;
color: #728490;
}
.theme-toggle .btn:not(.dark) {
color: #990694;
}
.theme-toggle .btn svg {
width: 20px;
height: 20px;
}
.image-compare { .image-compare {
border-radius: 24px; border-radius: 24px;
} }
@@ -2137,15 +2104,6 @@ body:not(.theme-dark) .theme-light-hidden {
background-image: url(../images/dark-bg.jpg); background-image: url(../images/dark-bg.jpg);
} }
.theme-dark .theme-toggle {
background-color: #0B0B10;
border-color: #323539;
}
.theme-dark .theme-toggle .btn:not(.dark) {
color: #323539;
}
.theme-dark .btn-header { .theme-dark .btn-header {
border-color: #fff; border-color: #fff;
color: #fff; color: #fff;
@@ -2368,11 +2326,7 @@ body:not(.theme-dark) .theme-light-hidden {
border-radius: 8px; border-radius: 8px;
} }
.theme-toggle { .image-examples {
border-radius: 8px;
}
.image-examples {
display: block; display: block;
text-align: center; text-align: center;
} }
@@ -2808,11 +2762,11 @@ body:not(.theme-dark) .theme-light-hidden {
.index-mobile-fit #image-compare .icv__img { position: absolute; top: 0; bottom: 0; left: 0; width: 100cqw; height: 100%; object-fit: cover; object-position: center; } .index-mobile-fit #image-compare .icv__img { position: absolute; top: 0; bottom: 0; left: 0; width: 100cqw; height: 100%; object-fit: cover; object-position: center; }
.index-mobile-fit #image-compare .icv__img-b { left: auto; right: 0; } .index-mobile-fit #image-compare .icv__img-b { left: auto; right: 0; }
.index-mobile-fit #image-compare.native-icv img { width: 100cqw !important; } .index-mobile-fit #image-compare.native-icv img { width: 100cqw !important; }
.index-mobile-fit #dropzone-form[data-mobile-upload] { padding: 12px; margin-top: 12px; } .index-mobile-fit :is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) { padding: 12px; margin-top: 12px; }
.index-mobile-fit #dropzone-form[data-mobile-upload] .dropzone-form-icon img { width: 36px; height: 36px; } .index-mobile-fit :is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-icon img { width: 36px; height: 36px; }
.index-mobile-fit #dropzone-form[data-mobile-upload] .dropzone-form-text { margin-top: 6px; } .index-mobile-fit :is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-text { margin-top: 6px; }
.index-mobile-fit #dropzone-form[data-mobile-upload] .dropzone-form-format { margin-top: 8px; } .index-mobile-fit :is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-format { margin-top: 8px; }
.index-mobile-fit #dropzone-form[data-mobile-upload] .dropzone-form-action { margin-top: 10px; } .index-mobile-fit :is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-action { margin-top: 10px; }
.index-mobile-fit .image-examples { max-width: min(100%, 50svh); margin: 12px auto 0; } .index-mobile-fit .image-examples { max-width: min(100%, 50svh); margin: 12px auto 0; }
.index-mobile-fit .image-examples-label { display: none; } .index-mobile-fit .image-examples-label { display: none; }
.index-mobile-fit .image-examples-list { gap: 8px; } .index-mobile-fit .image-examples-list { gap: 8px; }
@@ -2910,7 +2864,75 @@ body { isolation: isolate; }
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
} }
:is(.index-mobile-fit, .tour-processing) #preview { height: auto; aspect-ratio: 1696 / 920; } :is(.index-mobile-fit, .tour-processing) #preview { height: 100%; aspect-ratio: auto; }
} }
@media (max-width: 767.98px) { .wrapper { --site-header-height: 68px; } } @media (max-width: 767.98px) { .wrapper { --site-header-height: 68px; } }
/* /api#298 */ /* /api#298 */
/* api#301: геометрия примеров существует до загрузки изображений и viewer. */
.index-mobile-fit .dropzone-form-action { display: flex; justify-content: center; }
.index-mobile-fit #examples { position: relative; }
.index-mobile-fit #image-compare { position: relative; width: 100%; aspect-ratio: 1696 / 920; overflow: hidden; }
.index-mobile-fit #image-compare > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.index-mobile-fit #examples .examples-retry { position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%); }
.index-mobile-fit #examples .examples-retry[hidden] { display: none; }
.index-mobile-fit #examples .image-examples-item { cursor: pointer; }
.index-mobile-fit #examples .image-examples-item.active { border-color: #007bff; }
/* /api#301 */
@media (min-width: 768px) {
/* Обе колонки занимают свои ячейки и при частично полученном HTML. */
.index-mobile-fit > .container > .row { display: grid; grid-template-columns: 5fr 7fr; }
.index-mobile-fit > .container > .row > .col-md-5 { grid-column: 1; grid-row: 1; max-width: none; }
.index-mobile-fit > .container > .row > .col-md-7 { grid-column: 2; grid-row: 1; max-width: none; }
}
/* Разметка может прийти частями: показываем только полностью полученную форму. */
@media (max-width: 767.98px) {
/* Высоты существующего компактного вида: 243/201px контент + 24px padding. */
html[data-upload-device=android] .index-mobile-fit #dropzone-form { min-height: 267px; }
html[data-upload-device=mobile] .index-mobile-fit #dropzone-form { min-height: 225px; }
html[data-upload-device=android] .index-mobile-fit .dropzone-form-action { min-height: 90px; }
}
/* preland#11: общий с API адаптивный платёжный бейдж подвала. */
.footer-main img[src$="/images/payment.svg"]{max-width:100%;height:auto;}
/* preland#12: восстановлен пропущенный в critical CSS Bootstrap-контракт navbar. */
.navbar>.container{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between;}
/* api#313 / preland#16: первый экран, стабильный бюджет высоты и явное состояние загрузки. */
#dropzone-form[data-upload-pending="true"] .dropzone-form-button {
background: #979aa0; opacity: 1; filter: none; cursor: progress;
}
#dropzone-form[data-upload-pending="true"] .dropzone-form-action [data-files-picker] {
color: #979aa0; pointer-events: none; cursor: default;
}
.wrapper { min-height: 100vh; min-height: 100svh; }
.wrapper .main-section.index-mobile-fit,
.wrapper .main-content-wrapper { min-height: calc(100vh - var(--site-header-height)); }
@supports (height: 100svh) {
.wrapper .main-section.index-mobile-fit,
.wrapper .main-content-wrapper { min-height: calc(100svh - var(--site-header-height)); }
}
@media (min-width: 768px) {
.index-mobile-fit .index-upload-heading { font-size: 24px; font-weight: 600; }
}
@media (max-width: 767.98px) {
.index-mobile-fit { --index-small-height: 100vh; }
.index-mobile-fit #image-compare { max-height: clamp(120px, calc(var(--index-small-height) - 360px), 240px); }
.index-mobile-fit #image-compare .icv__img,
.index-mobile-fit #image-compare.native-icv img { width: var(--compare-width, 100%) !important; }
html[data-upload-device=android] .index-mobile-fit #dropzone-form { min-height: 243px; }
html[data-upload-device=mobile] .index-mobile-fit #dropzone-form { min-height: 201px; }
.index-mobile-fit[data-compact-upload=true] .dropzone-form-icon { display: none; }
.index-mobile-fit[data-compact-upload=true] #dropzone-form #form-title { margin-top: 0; }
html[data-upload-device] .index-mobile-fit[data-compact-upload=true] #dropzone-form { min-height: 0; }
}
@supports (height: 100svh) {
.index-mobile-fit { --index-small-height: 100svh; }
}
/* Без :has форма остаётся доступной; современные движки сохраняют partial-HTML gate. */
@supports selector(:has(*)) {
.index-mobile-fit #dropzone-form:not(:has([data-upload-markup-ready])) { visibility: hidden; }
}
/* /api#313 / preland#16 */
+43
View File
@@ -841,3 +841,46 @@ padding-top: 2svh !important;
@media(max-width:767.98px){.result-download-title{font-size:24px;line-height:1.2;}} @media(max-width:767.98px){.result-download-title{font-size:24px;line-height:1.2;}}
@media (min-width:768px){.image-compare .icv__label{top:10px;width:92px;height:36px;display:flex;align-items:center;justify-content:center;padding:4px 10px;font:400 16px/26px Inter,sans-serif;color:#fff;backdrop-filter:none;-webkit-backdrop-filter:none;background:rgb(0 0 0 / 25%);border:1px solid rgb(255 255 255 / 80%);box-shadow:none;text-shadow:none;}} @media (min-width:768px){.image-compare .icv__label{top:10px;width:92px;height:36px;display:flex;align-items:center;justify-content:center;padding:4px 10px;font:400 16px/26px Inter,sans-serif;color:#fff;backdrop-filter:none;-webkit-backdrop-filter:none;background:rgb(0 0 0 / 25%);border:1px solid rgb(255 255 255 / 80%);box-shadow:none;text-shadow:none;}}
.upload-loading-dots{display:inline-grid;text-align:left;}.upload-loading-dots::before{content:'.....';visibility:hidden;grid-area:1 / 1;}.upload-loading-dots > span{grid-area:1 / 1;} .upload-loading-dots{display:inline-grid;text-align:left;}.upload-loading-dots::before{content:'.....';visibility:hidden;grid-area:1 / 1;}.upload-loading-dots > span{grid-area:1 / 1;}
/* preland#11: общий с API адаптивный платёжный бейдж подвала. */
.footer-main img[src$="/images/payment.svg"]{max-width:100%;height:auto;}
/* preland#12: восстановлен пропущенный в critical CSS Bootstrap-контракт navbar. */
.navbar>.container{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between;}
/* api#313 / preland#16: первый экран, стабильный бюджет высоты и явное состояние загрузки. */
#dropzone-form[data-upload-pending="true"] .dropzone-form-button {
background: #979aa0; opacity: 1; filter: none; cursor: progress;
}
#dropzone-form[data-upload-pending="true"] .dropzone-form-action [data-files-picker] {
color: #979aa0; pointer-events: none; cursor: default;
}
.wrapper { min-height: 100vh; min-height: 100svh; }
.wrapper .main-section.index-mobile-fit,
.wrapper .main-content-wrapper { min-height: calc(100vh - var(--site-header-height)); }
@supports (height: 100svh) {
.wrapper .main-section.index-mobile-fit,
.wrapper .main-content-wrapper { min-height: calc(100svh - var(--site-header-height)); }
}
@media (min-width: 768px) {
.index-mobile-fit .index-upload-heading { font-size: 24px; font-weight: 600; }
}
@media (max-width: 767.98px) {
.index-mobile-fit { --index-small-height: 100vh; }
.index-mobile-fit #image-compare { max-height: clamp(120px, calc(var(--index-small-height) - 360px), 240px); }
.index-mobile-fit #image-compare .icv__img,
.index-mobile-fit #image-compare.native-icv img { width: var(--compare-width, 100%) !important; }
html[data-upload-device=android] .index-mobile-fit #dropzone-form { min-height: 243px; }
html[data-upload-device=mobile] .index-mobile-fit #dropzone-form { min-height: 201px; }
.index-mobile-fit[data-compact-upload=true] .dropzone-form-icon { display: none; }
.index-mobile-fit[data-compact-upload=true] #dropzone-form #form-title { margin-top: 0; }
html[data-upload-device] .index-mobile-fit[data-compact-upload=true] #dropzone-form { min-height: 0; }
}
@supports (height: 100svh) {
.index-mobile-fit { --index-small-height: 100svh; }
}
/* Без :has форма остаётся доступной; современные движки сохраняют partial-HTML gate. */
@supports selector(:has(*)) {
.index-mobile-fit #dropzone-form:not(:has([data-upload-markup-ready])) { visibility: hidden; }
}
/* /api#313 / preland#16 */
+74 -6
View File
@@ -7,11 +7,11 @@
.index-mobile-fit #image-compare .icv__img { position: absolute; top: 0; bottom: 0; left: 0; width: 100cqw; height: 100%; object-fit: cover; object-position: center; } .index-mobile-fit #image-compare .icv__img { position: absolute; top: 0; bottom: 0; left: 0; width: 100cqw; height: 100%; object-fit: cover; object-position: center; }
.index-mobile-fit #image-compare .icv__img-b { left: auto; right: 0; } .index-mobile-fit #image-compare .icv__img-b { left: auto; right: 0; }
.index-mobile-fit #image-compare.native-icv img { width: 100cqw !important; } .index-mobile-fit #image-compare.native-icv img { width: 100cqw !important; }
.index-mobile-fit #dropzone-form[data-mobile-upload] { padding: 12px; margin-top: 12px; } .index-mobile-fit :is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) { padding: 12px; margin-top: 12px; }
.index-mobile-fit #dropzone-form[data-mobile-upload] .dropzone-form-icon img { width: 36px; height: 36px; } .index-mobile-fit :is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-icon img { width: 36px; height: 36px; }
.index-mobile-fit #dropzone-form[data-mobile-upload] .dropzone-form-text { margin-top: 6px; } .index-mobile-fit :is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-text { margin-top: 6px; }
.index-mobile-fit #dropzone-form[data-mobile-upload] .dropzone-form-format { margin-top: 8px; } .index-mobile-fit :is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-format { margin-top: 8px; }
.index-mobile-fit #dropzone-form[data-mobile-upload] .dropzone-form-action { margin-top: 10px; } .index-mobile-fit :is(#dropzone-form[data-mobile-upload], html[data-upload-device="mobile"] #dropzone-form, html[data-upload-device="android"] #dropzone-form) .dropzone-form-action { margin-top: 10px; }
.index-mobile-fit .image-examples { max-width: min(100%, 50svh); margin: 12px auto 0; } .index-mobile-fit .image-examples { max-width: min(100%, 50svh); margin: 12px auto 0; }
.index-mobile-fit .image-examples-label { display: none; } .index-mobile-fit .image-examples-label { display: none; }
.index-mobile-fit .image-examples-list { gap: 8px; } .index-mobile-fit .image-examples-list { gap: 8px; }
@@ -111,7 +111,75 @@ body { isolation: isolate; }
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
} }
:is(.index-mobile-fit, .tour-processing) #preview { height: auto; aspect-ratio: 1696 / 920; } :is(.index-mobile-fit, .tour-processing) #preview { height: 100%; aspect-ratio: auto; }
} }
@media (max-width: 767.98px) { .wrapper { --site-header-height: 68px; } } @media (max-width: 767.98px) { .wrapper { --site-header-height: 68px; } }
/* /api#298 */ /* /api#298 */
/* api#301: геометрия примеров существует до загрузки изображений и viewer. */
.index-mobile-fit .dropzone-form-action { display: flex; justify-content: center; }
.index-mobile-fit #examples { position: relative; }
.index-mobile-fit #image-compare { position: relative; width: 100%; aspect-ratio: 1696 / 920; overflow: hidden; }
.index-mobile-fit #image-compare > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.index-mobile-fit #examples .examples-retry { position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%); }
.index-mobile-fit #examples .examples-retry[hidden] { display: none; }
.index-mobile-fit #examples .image-examples-item { cursor: pointer; }
.index-mobile-fit #examples .image-examples-item.active { border-color: #007bff; }
/* /api#301 */
@media (min-width: 768px) {
/* Обе колонки занимают свои ячейки и при частично полученном HTML. */
.index-mobile-fit > .container > .row { display: grid; grid-template-columns: 5fr 7fr; }
.index-mobile-fit > .container > .row > .col-md-5 { grid-column: 1; grid-row: 1; max-width: none; }
.index-mobile-fit > .container > .row > .col-md-7 { grid-column: 2; grid-row: 1; max-width: none; }
}
/* Разметка может прийти частями: показываем только полностью полученную форму. */
@media (max-width: 767.98px) {
/* Высоты существующего компактного вида: 243/201px контент + 24px padding. */
html[data-upload-device=android] .index-mobile-fit #dropzone-form { min-height: 267px; }
html[data-upload-device=mobile] .index-mobile-fit #dropzone-form { min-height: 225px; }
html[data-upload-device=android] .index-mobile-fit .dropzone-form-action { min-height: 90px; }
}
/* preland#11: общий с API адаптивный платёжный бейдж подвала. */
.footer-main img[src$="/images/payment.svg"]{max-width:100%;height:auto;}
/* preland#12: восстановлен пропущенный в critical CSS Bootstrap-контракт navbar. */
.navbar>.container{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between;}
/* api#313 / preland#16: первый экран, стабильный бюджет высоты и явное состояние загрузки. */
#dropzone-form[data-upload-pending="true"] .dropzone-form-button {
background: #979aa0; opacity: 1; filter: none; cursor: progress;
}
#dropzone-form[data-upload-pending="true"] .dropzone-form-action [data-files-picker] {
color: #979aa0; pointer-events: none; cursor: default;
}
.wrapper { min-height: 100vh; min-height: 100svh; }
.wrapper .main-section.index-mobile-fit,
.wrapper .main-content-wrapper { min-height: calc(100vh - var(--site-header-height)); }
@supports (height: 100svh) {
.wrapper .main-section.index-mobile-fit,
.wrapper .main-content-wrapper { min-height: calc(100svh - var(--site-header-height)); }
}
@media (min-width: 768px) {
.index-mobile-fit .index-upload-heading { font-size: 24px; font-weight: 600; }
}
@media (max-width: 767.98px) {
.index-mobile-fit { --index-small-height: 100vh; }
.index-mobile-fit #image-compare { max-height: clamp(120px, calc(var(--index-small-height) - 360px), 240px); }
.index-mobile-fit #image-compare .icv__img,
.index-mobile-fit #image-compare.native-icv img { width: var(--compare-width, 100%) !important; }
html[data-upload-device=android] .index-mobile-fit #dropzone-form { min-height: 243px; }
html[data-upload-device=mobile] .index-mobile-fit #dropzone-form { min-height: 201px; }
.index-mobile-fit[data-compact-upload=true] .dropzone-form-icon { display: none; }
.index-mobile-fit[data-compact-upload=true] #dropzone-form #form-title { margin-top: 0; }
html[data-upload-device] .index-mobile-fit[data-compact-upload=true] #dropzone-form { min-height: 0; }
}
@supports (height: 100svh) {
.index-mobile-fit { --index-small-height: 100svh; }
}
/* Без :has форма остаётся доступной; современные движки сохраняют partial-HTML gate. */
@supports selector(:has(*)) {
.index-mobile-fit #dropzone-form:not(:has([data-upload-markup-ready])) { visibility: hidden; }
}
/* /api#313 / preland#16 */
+13 -34
View File
@@ -3,6 +3,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<?php require __DIR__ . '/components/theme-style.php'; ?>
<?php require __DIR__ . '/components/theme-client.php'; ?>
<title><?= $page_title ?? $default_title ?></title> <title><?= $page_title ?? $default_title ?></title>
@@ -47,18 +49,22 @@
<link rel="preload" as="image" href="<?= $urlSrcOrig ?>" media="(min-width: 768px)" type="image/avif" fetchpriority="high"> <link rel="preload" as="image" href="<?= $urlSrcOrig ?>" media="(min-width: 768px)" type="image/avif" fetchpriority="high">
<link rel="preload" as="image" href="<?= $urlResOrig ?>" media="(min-width: 768px)" type="image/avif" fetchpriority="high"> <link rel="preload" as="image" href="<?= $urlResOrig ?>" media="(min-width: 768px)" type="image/avif" fetchpriority="high">
<!-- <link rel="stylesheet" href="style.min.css"> --> <style><?= file_get_contents(__DIR__ . '/style.min.css'); ?></style>
<style><?= require __DIR__ . '/components/upload-picker-style.php'; ?></style>
<?php require_once __DIR__ . '/components/browser-upload.php'; ?>
</head> </head>
<body class="<?= (isset($UTM['theme']) && $UTM['theme'] == 'dark') ? 'theme-dark' : '' ?>" style="<?= (isset($UTM['theme']) && $UTM['theme'] == 'dark') ? 'background: black' : '' ?>"> <body class="<?= $themeInitial === 'dark' ? 'theme-dark' : '' ?>">
<script>window.ZoomzillaTheme.refresh();</script>
<div class="wrapper"> <div class="wrapper">
<header class="header"> <header class="header">
<nav class="navbar navbar-dark py-1 py-md-3 px-0"> <nav class="navbar navbar-dark py-1 py-md-3 px-0">
<div class="container"> <div class="container">
<span class="navbar-brand" href="<?= $apiHost ?: '/' ?>" > <span class="navbar-brand" href="<?= $apiHost ?: '/' ?>" >
<img src="<?= $assetsUriPrefix; ?>/docs/logo.svg" width="160" height="40" alt="Logo"> <img src="<?= prelandAssetUrl('docs/logo.svg') ?>" width="182" height="47" alt="Логотип <?= htmlspecialchars(prelandSiteDomain($_SERVER), ENT_QUOTES, 'UTF-8') ?>">
<img src="<?= $assetsUriPrefix; ?>/docs/logo-alt.<?= $logoAltExt ?>" width="160" height="40" alt="Logo-Alt"> <img src="<?= prelandAssetUrl('docs/logo-alt.svg') ?>" width="182" height="47" alt="Логотип <?= htmlspecialchars(prelandSiteDomain($_SERVER), ENT_QUOTES, 'UTF-8') ?>">
</span> </span>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<?php $themeLabels = ['group' => Yii::t('app', 'Тема оформления'), 'light' => Yii::t('app', 'Светлая тема'), 'dark' => Yii::t('app', 'Тёмная тема')]; require __DIR__ . '/components/theme-switch.php'; ?>
</div> </div>
</div> </div>
</nav> </nav>
@@ -97,41 +103,14 @@
<div class="row py-5 footer-main"> <div class="row py-5 footer-main">
<div class="col-md-4 mb-4 mb-md-0"> <div class="col-md-4 mb-4 mb-md-0">
<div class="footer-logo"> <div class="footer-logo">
<img src="<?= $assetsUriPrefix; ?>/docs/logo.svg" width="240" height="60" alt="Logo"> <img src="<?= prelandAssetUrl('docs/logo.svg') ?>" width="182" height="47" alt="Логотип <?= htmlspecialchars(prelandSiteDomain($_SERVER), ENT_QUOTES, 'UTF-8') ?>">
<img src="<?= $assetsUriPrefix; ?>/docs/logo-alt.<?= $logoAltExt ?>" width="240" height="60" alt="Logo"> <img src="<?= prelandAssetUrl('docs/logo-alt.svg') ?>" width="182" height="47" alt="Логотип <?= htmlspecialchars(prelandSiteDomain($_SERVER), ENT_QUOTES, 'UTF-8') ?>">
</div> </div>
</div> </div>
<div class="col-md-4 mb-4 mb-md-0">
</div>
<div class="col-md-4">
<div class="h5 mb-3"><?= Yii::t('app', 'Наши реквизиты') ?></div>
<?php
// реквизиты: config.json (requisites — строки) → requisites.html → образец
$requisites = array_filter((array) ($config['requisites'] ?? []), fn ($line) => is_scalar($line) && trim((string) $line) !== '');
$requisitesFile = __DIR__ . '/requisites.html';
if ($requisites) {
foreach ($requisites as $line) {
?><p class="mb-0"><?= htmlspecialchars((string) $line, ENT_QUOTES, 'UTF-8') ?></p>
<?php }
} elseif (file_exists($requisitesFile) && filesize($requisitesFile) > 0) {
echo file_get_contents($requisitesFile);
} else {
?>
<p class="mb-0">ИП Иванов Иван Иванович</p>
<p class="mb-0">123123, г. Москва, Пресненская наб., 12</p>
<p class="mb-0">ИНН: 123404385406</p>
<p class="mb-0">ОГРНИП: 123420000019175</p>
<?php
}
?>
<?php if (Yii::t('app', 'system.hide.footer') == 'false') { ?>
<img src="<?= $assetsUriPrefix; ?>/images/payment.svg" width="272" height="40" alt="" class="mt-4">
<?php } ?>
</div>
</div> </div>
<div class="row footer-bottom py-3"> <div class="row footer-bottom py-3">
<div class="col-6"> <div class="col-6">
<p class="m-0"><?= date('Y') ?>© <?= parse_url($apiHost, PHP_URL_HOST) ?: Yii::$app->request->hostName ?></p> <p class="m-0"><?= date('Y') ?>© <?= htmlspecialchars(prelandSiteDomain($_SERVER), ENT_QUOTES, 'UTF-8') ?></p>
</div> </div>
<?php if (Yii::t('app', 'system.hide.footer') == 'false') { ?> <?php if (Yii::t('app', 'system.hide.footer') == 'false') { ?>
<div class="col-6"> <div class="col-6">
+33
View File
@@ -0,0 +1,33 @@
<?php
/** preland#17: точечный, идемпотентный patch только chb/cons и его defaults/weights. */
declare(strict_types=1);
$script=dirname(__DIR__).'/bin/ensure-cons-config.php';
if(!is_file($script)){fwrite(STDERR,"FAIL: patch cons ещё не реализован.\n");exit(1);}
$fixture=['host'=>'keep.example','requisites'=>['Не менять'],'utm'=>['theme'=>['default'=>'dark','weights'=>['light'=>7]],'chb'=>['default'=>'b','weights'=>['a'=>1,'b'=>0]],'focus'=>['default'=>'face']]];
$path=tempnam(sys_get_temp_dir(),'cons-config-');
try{
file_put_contents($path,json_encode($fixture,JSON_THROW_ON_ERROR));
exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($script).' '.escapeshellarg($path),$output,$code);
$actual=json_decode(file_get_contents($path),true,512,JSON_THROW_ON_ERROR);
if($code!==10||isset($actual['utm']['chb'])||($actual['utm']['cons']??null)!==['weights'=>['1'=>100,'2'=>0],'default'=>'2'])throw new RuntimeException('Patch должен заменить только согласованный ключ chb на cons.');
$comparison=$actual;unset($comparison['utm']['cons']);$old=$fixture;unset($old['utm']['chb']);
if($comparison!==$old)throw new RuntimeException('Patch изменил соседние поля runtime config.');
$before=file_get_contents($path);exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($script).' '.escapeshellarg($path),$output,$code);
if($code!==0||file_get_contents($path)!==$before)throw new RuntimeException('Повторный patch должен оставить байты без изменений.');
$custom=$actual;$custom['utm']['cons']=['weights'=>['1'=>20,'3'=>80],'default'=>'3'];
file_put_contents($path,json_encode($custom,JSON_THROW_ON_ERROR));$before=file_get_contents($path);
exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($script).' '.escapeshellarg($path),$output,$code);
if($code!==0||file_get_contents($path)!==$before)throw new RuntimeException('Следующий deploy не должен сбросить настроенный владельцем cons.');
foreach([[],true,1.5,'2147483648','0']as$invalid){
$broken=$custom;$broken['utm']['cons']['default']=$invalid;
file_put_contents($path,json_encode($broken,JSON_THROW_ON_ERROR));$before=file_get_contents($path);
exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($script).' '.escapeshellarg($path).' 2>/dev/null',$output,$code);
if($code!==2||file_get_contents($path)!==$before)throw new RuntimeException('Некорректный существующий cons должен остановить deploy без перезаписи config.');
}
$objectFixture='{"host":"keep","extra":{},"map":{"0":"a","1":"b"},"list":[],"utm":{"chb":{"default":"a"},"other":{"weights":{}}}}';
file_put_contents($path,$objectFixture);exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($script).' '.escapeshellarg($path),$output,$code);
$beforeObject=json_decode($objectFixture,false,512,JSON_THROW_ON_ERROR);$afterObject=json_decode(file_get_contents($path),false,512,JSON_THROW_ON_ERROR);
unset($beforeObject->utm->chb,$afterObject->utm->cons);
if($code!==10||json_encode($beforeObject)!==json_encode($afterObject))throw new RuntimeException('Patch должен сохранять типы соседних пустых и numeric-key JSON объектов.');
echo "PASS: cons config migration preserves other fields and JSON types, idempotent\n";
}finally{@unlink($path);}
+10
View File
@@ -0,0 +1,10 @@
<?php
/** preland#17: consent-конфиг мигрируется после совместимого кода, без замены runtime-снимком. */
declare(strict_types=1);
$root=dirname(__DIR__);$workflow=file_get_contents($root.'/.gitea/workflows/deploy.yml');
$upload=strpos($workflow,'- name: Upload over SFTP');$patch=strpos($workflow,'- name: Migrate numeric UTM in preserved host config');
if($upload===false||$patch===false||$patch<$upload)throw new RuntimeException('Consent config patch должен выполняться после совместимого кода.');
$section=substr($workflow,$patch,strpos($workflow,'- name: Check the page',$patch)-$patch);
foreach(['get -p $REMOTE_DIR/config.json','ensure-utm-config.php','config.json.upload-$revision','rename $REMOTE_DIR/config.json.upload-$revision']as$expected){if(!str_contains($section,$expected))throw new RuntimeException('Отсутствует безопасный этап: '.$expected);}
if(str_contains($section,'put -p config.json '))throw new RuntimeException('Снимок Git не должен заменять runtime config.');
echo "PASS: consent config patch follows compatible code and uses atomic targeted replacement\n";
+32
View File
@@ -0,0 +1,32 @@
<?php
/** preland#17: concrete cons независим от random-весов, старый chb не управляет переходом. */
declare(strict_types=1);
$file = dirname(__DIR__) . '/components/cons-utm.php';
if (!is_file($file)) {
fwrite(STDERR, "FAIL: нормализация cons ещё не реализована.\n");
exit(1);
}
require $file;
$checks = 0;
$expect = static function (bool $ok, string $message) use (&$checks): void {
if (!$ok) throw new RuntimeException($message);
$checks++;
};
$fixture = ['host'=>'api.example.test','utm'=>['theme'=>['default'=>'dark','weights'=>['dark'=>3]],'chb'=>['default'=>'a','weights'=>['a'=>1]]]];
$normalized = prelandNormalizeConsConfig($fixture);
$expect(!isset($normalized['utm']['chb']), 'Старый chb должен быть удалён из runtime-копии конфига.');
$expect($normalized['host'] === $fixture['host'] && $normalized['utm']['theme'] === $fixture['utm']['theme'], 'Соседние поля должны сохраняться.');
$expect($fixture['utm']['chb']['default'] === 'a', 'Нормализация не должна менять исходный массив.');
$settings = $normalized['utm']['cons'];
$expect(prelandResolveCons(null, $settings) === '2', 'Отсутствующее значение должно получить default2.');
foreach (['1','2','3','99'] as $value) $expect(prelandResolveCons($value, $settings) === $value, 'Concrete cons должен приниматься независимо от весов: '.$value);
foreach (['0','-1','a','1x','',[],['1']] as $value) $expect(prelandResolveCons($value, $settings) === '2', 'Неверное значение должно получить default2.');
for ($i=0; $i<1000; $i++) $expect(prelandResolveCons('rnd', $settings) === '1', 'rnd обязан выбрать только группу1.');
$settings = ['default'=>'2','weights'=>['2'=>0,'1'=>100]];
for ($i=0; $i<1000; $i++) $expect(prelandResolveCons('rnd', $settings) === '1', 'Нулевой вес не должен случайно попасть в выборку.');
$query = prelandNormalizeConsQuery(['chb'=>'a','cons'=>'3','utm_source'=>'ad','subid'=>'42'], $settings);
$expect($query === ['cons'=>'3','utm_source'=>'ad','subid'=>'42'], 'Query должен удалить только chb и нормализовать cons.');
$query = prelandNormalizeConsQuery(['chb'=>['b'],'utm_source'=>'ad'], $settings);
$expect(!isset($query['chb']) && $query['cons'] === '2' && $query['utm_source'] === 'ad', 'Array legacy chb не должен влиять на fallback.');
echo "PASS: cons resolver {$checks} checks\n";
+25
View File
@@ -0,0 +1,25 @@
<?php
/** preland#16: публичная форма отдаёт согласованные подписи до выполнения JavaScript. */
require dirname(__DIR__) . '/i18n.php';
Yii::init('ru');
$_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Linux; Android 10)';
$assetsUriPrefix = '';
ob_start();
require dirname(__DIR__) . '/components/form.php';
$html = ob_get_clean();
$expected = [
'/id="form-title"[^>]*>(.*?)<\/div>/s' => 'ИИ улучшение изображений',
'/data-upload-label>(.*?)<\/span>/s' => 'Выберите фото',
'/data-files-picker hidden>(.*?)<\/a>/s' => 'Файлы (PDF и др.)',
];
$failed = 0;
foreach ($expected as $pattern => $label) {
preg_match($pattern, $html, $match);
$actual = trim(strip_tags($match[1] ?? ''));
if ($actual !== $label) {
fwrite(STDERR, "FAIL: ожидалось «{$label}», получено «{$actual}»\n");
$failed++;
}
}
if ($failed) exit(1);
echo "OK: публичные подписи SSR (3 проверки)\n";
+46
View File
@@ -0,0 +1,46 @@
<?php
/** api#326: точечная миграция должна сохранять варианты и типы чужих JSON-полей. */
declare(strict_types=1);
$script=dirname(__DIR__).'/bin/ensure-utm-config.php';
if(!is_file($script))throw new RuntimeException('Нет миграции числового runtime config');
$file=tempnam(sys_get_temp_dir(),'utm');
try{
$raw='{"host":"keep","empty":{},"list":[],"map":{"0":"a","1":"b"},"utm":{"registration":{"weights":{"before":3,"after":7},"default":"after"},"bchoice":{"weights":{"a":8,"b":2},"default":"b"},"theme":{"weights":{"light":7,"dark":3},"default":"light"},"focus":{"weights":{"center":6,"face":4},"default":"face"},"indpic":{"weights":{"1":9,"2":1},"default":"2"},"custom":{"x":{}}}}';
file_put_contents($file,$raw);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code);
if($code!==10)throw new RuntimeException('Первый patch должен вернуть10');
$actual=json_decode(file_get_contents($file)); $before=json_decode($raw);
foreach(['host','empty','list','map'] as $key)if(serialize($actual->$key)!==serialize($before->$key))throw new RuntimeException('Изменён соседний JSON тип');
if(isset($actual->utm->registration,$actual->utm->bchoice))throw new RuntimeException('Остались старые имена');
if(json_encode($actual->utm->reg)!=='{"weights":{"1":3,"2":7},"default":"2"}'||json_encode($actual->utm->banch)!=='{"weights":{"2":8,"1":2},"default":"1"}')throw new RuntimeException('Изменён смысл reg/banch');
if($actual->utm->theme->default!=='2'||$actual->utm->focus->default!=='1'||serialize($actual->utm->custom)!==serialize($before->utm->custom))throw new RuntimeException('Неверная миграция enum');
$first=file_get_contents($file);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code);
if($code!==0||file_get_contents($file)!==$first)throw new RuntimeException('Patch не идемпотентен');
file_put_contents($file,'{"utm":{"theme":{"weights":{"1":0,"2":1},"default":2}}}');
$numeric=file_get_contents($file);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code);
if($code!==0||file_get_contents($file)!==$numeric)throw new RuntimeException('Уже числовой integer default должен сохраняться');
file_put_contents($file,'{"utm":{"registration":{"weights":{"before":1},"default":"before"},"reg":{"weights":{"1":1},"default":"1"}}}');
$conflict=file_get_contents($file);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file).' 2>/dev/null',$out,$code);
if($code!==2||file_get_contents($file)!==$conflict)throw new RuntimeException('Неоднозначный config должен блокировать patch без записи');
$untouched='"foreign" : {"customFloat":1.0,"big":9223372036854775809,"nested":[1.00,{},[],{"exponent":2e+03,"negative":-0.0}]}';
$tokenFixture='{'.$untouched.',"utm":{"theme":{"weights":{"dark":1.0,"light":0},"default":"dark"},"custom":{"big":9223372036854775809}}}';
file_put_contents($file,$tokenFixture);
exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code);
$patched=file_get_contents($file);
if($code!==10||!str_contains($patched,$untouched)||!str_contains($patched,'"custom":{"big":9223372036854775809}')||!str_contains($patched,'"weights":{"1":1.0,"2":0}'))throw new RuntimeException('Patch изменил исходные foreign/weight numeric tokens');
$expectedTokens=str_replace(['"dark":','"light":','"default":"dark"'],['"1":','"2":','"default":"1"'],$tokenFixture);
if($patched!==$expectedTokens)throw new RuntimeException('Изменены байты вне согласованных source ranges');
exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code);
if($code!==0||file_get_contents($file)!==$patched)throw new RuntimeException('Token patch не идемпотентен');
$current='{ "other":1.0,"huge":9223372036854775809,"utm": {"chob" : { "weights":{"2":9e+01,"1":-0.0},"default":2 },"nested":{"chob":"keep"}}}';
file_put_contents($file,$current);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code);
$renamed=str_replace('"chob" :','"banch" :',$current);
if($code!==10||file_get_contents($file)!==$renamed)throw new RuntimeException('api#327: миграция chob→banch должна менять только токен имени');
exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code);
if($code!==0||file_get_contents($file)!==$renamed)throw new RuntimeException('api#327: повторная миграция не должна писать');
foreach(['"chob":{"weights":{"1":1},"default":"1"},"banch":{"weights":{"2":1},"default":"2"}', '"bchoice":{"weights":{"a":1},"default":"a"},"chob":{"weights":{"1":1},"default":"1"}'] as $conflicting){
$conflict='{"utm":{'.$conflicting.'}}';file_put_contents($file,$conflict);
exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file).' 2>/dev/null',$out,$code);
if($code!==2||file_get_contents($file)!==$conflict)throw new RuntimeException('api#327: конфликт aliases должен блокировать patch без записи');
}
echo "OK: numeric config migration preserves weights/default/foreign JSON types\n";
}finally{unlink($file);}
+37
View File
@@ -0,0 +1,37 @@
<?php
/** api#326: публичные маски выбирают внутренние варианты без открытых alias. */
declare(strict_types=1);
$_GET = ['indpic'=>'abcd2', 'theme'=>'abcd2x', 'degr'=>'abcd2', 'reg'=>'abcd1', 'banch'=>'abcd1', 'tp'=>'abcd2', 'cons'=>'abcd3', 'focus'=>'abcd2', 'registration'=>'before', 'bchoice'=>'b', 'chb'=>'a', 'utm_source'=>'abcd9same', 'subid'=>'тест + &'];
$_SERVER['HTTP_HOST']='zoomstage.online';
ob_start(); require dirname(__DIR__).'/index.php'; $html=ob_get_clean();
$expected=['indpic'=>'2','theme'=>'light','degr'=>'2','registration'=>'before','bchoice'=>'b','tp'=>'2','cons'=>'3','focus'=>'center'];
foreach($expected as $key=>$value) if(($UTM[$key]??null)!==$value) throw new RuntimeException("Неверный внутренний вариант $key");
if(($UTM['utm_source']??null)!=='abcd9same'||($UTM['subid']??null)!=='тест + &') throw new RuntimeException('Чужие значения изменены');
if(!function_exists('prelandResolveUtm')) throw new RuntimeException('Отсутствует единая граница UTM');
$config=['utm'=>['indpic'=>['weights'=>['1'=>0,'2'=>100],'default'=>'1'],'theme'=>['weights'=>['1'=>3,'2'=>7],'default'=>'1']]];
foreach(['2','rnd','dark','abcdx','abc',[],null,'abcd0'] as $value) {
$resolved=prelandResolveUtm(['theme'=>$value],$config);
if($value==='abcd0') continue;
if($resolved['digits']['theme']!=='1')throw new RuntimeException('Invalid theme обязан использовать default');
}
if(prelandResolveUtm(["theme"=>"абвг2"],$config)["digits"]["theme"]!=="2")throw new RuntimeException("Unicode fifth symbol");
for($i=0;$i<50;$i++) if(prelandResolveUtm(['indpic'=>'abcd0'],$config)['digits']['indpic']!=='2')throw new RuntimeException('0 нарушил веса');
$settings=json_decode(file_get_contents(dirname(__DIR__).'/config.json'),true);
$defaults=['indpic'=>'1','theme'=>'1','degr'=>'1','reg'=>'2','banch'=>'2','tp'=>'2','cons'=>'2','focus'=>'1'];
foreach(array_keys($defaults) as $key) foreach(['1','rnd','abcdx','abc',[],null,"\xffabcd2"] as $invalid) {
$resolved=prelandResolveUtm([$key=>$invalid,'registration'=>'before','bchoice'=>'b','chb'=>'a'],$settings);
if($resolved['digits'][$key]!==$defaults[$key])throw new RuntimeException('Открытый/invalid URL изменил '.$key);
if(isset($resolved['context']['chb']))throw new RuntimeException('Legacy chb прошёл как foreign');
}
if(prelandResolveUtm(['tp'=>'abcd9'],$settings)['digits']['tp']!=='2')throw new RuntimeException('Ненастроенная группа tp должна fallback');
if(prelandResolveUtm(['cons'=>'abcd9'],$settings)['digits']['cons']!=='9')throw new RuntimeException('Cons concrete не зависит от random weights');
$zero=['utm'=>['theme'=>['weights'=>['2'=>0,'1'=>0],'default'=>'1'],'cons'=>['weights'=>['1'=>0,'2'=>0],'default'=>'2']]];
if(prelandResolveUtm(['theme'=>'abcd0'],$zero)['digits']['theme']!=='2')throw new RuntimeException('Generic zero weights обязаны выбрать первый ключ, как до #326');
if(prelandResolveUtm(['cons'=>'abcd0'],$zero)['digits']['cons']!=='2')throw new RuntimeException('Cons zero weights обязаны сохранить special default');
echo "OK: numeric public boundary, defaults, weighted0, foreign values\n";
foreach (['abcd1','1','b',[]] as $old) {
$resolved=prelandResolveUtm(['chob'=>$old,'bchoice'=>'b','chb'=>'a'],$settings);
if($resolved['digits']['banch']!=='2'||isset($resolved['context']['chob']))throw new RuntimeException('api#327: внешний chob должен игнорироваться и удаляться');
}
$oldConfig=['utm'=>['chob'=>['weights'=>['1'=>5,'2'=>0],'default'=>'1']]];
if(prelandResolveUtm([],$oldConfig)['digits']['banch']!=='1')throw new RuntimeException('api#327: старый host config обязан работать до миграции');
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
$root = dirname(__DIR__);
foreach (['light', 'dark'] as $theme) foreach ([
'zoomstage.online' => 'zoomstage.online',
'New-Preland.example:8080' => 'new-preland.example',
'www.example.test.' => 'www.example.test',
'<script>alert(1)</script>' => 'fixture.local',
] as $host => $expected) {
$html = shell_exec(PHP_BINARY . ' ' . escapeshellarg(__DIR__ . '/render-preland.php') . ' ' . escapeshellarg($host) . ' ' . escapeshellarg($theme));
if (!preg_match('~<footer\b.*?</footer>~s', $html, $match)) throw new RuntimeException('Подвал не найден');
$footer = $match[0];
foreach (['Наши реквизиты', 'payment.svg', 'ИНН:', 'ОГРНИП:', 'ИП Иванов', 'ИП Кардаполова'] as $forbidden) {
if (str_contains($footer, $forbidden)) throw new RuntimeException('В подвале остался ' . $forbidden);
}
if (!str_contains($footer, date('Y') . '© ' . $expected)) throw new RuntimeException('Неправильный домен копирайта: ' . $host);
if (substr_count($footer, '<img ') !== 2) throw new RuntimeException('Нужны только два тематических SVG логотипа');
if (str_contains($footer, '<script>')) throw new RuntimeException('Host попал в HTML без проверки');
}
echo "OK: минимальный подвал, две темы, перенос домена/порт/trailing-dot и invalid-host\n";
+76
View File
@@ -0,0 +1,76 @@
<?php
/** preland#11: старый бренд запрещён независимо от обновления manifest. */
declare(strict_types=1);
$root = dirname(__DIR__);
require_once $root . '/components/asset-url.php';
$assetsUriPrefix = 'https://zoomzilla.ru';
if (!str_starts_with(prelandAssetUrl('docs/logo.svg'), '/docs/logo.svg?v=')) throw new RuntimeException('Доменный логотип не должен браться с общего assets/API хоста');
$oldBlob = '91267982e1ad373b6ebd5fb2b761b34fe2a0c2a5';
foreach (['docs/logo.svg', 'docs/logo-alt.svg'] as $file) {
$body = str_replace("\r\n", "\n", file_get_contents($root . '/' . $file));
if (sha1('blob ' . strlen($body) . chr(0) . $body) === $oldBlob) {
fwrite(STDERR, "FAILED: {$file} содержит запрещённую заглушку бренда\n");
exit(1);
}
}
require $root . '/bin/release-assets.php';
validateReleaseAssets($root);
$files = releaseFiles($root);
foreach (['docs/privacy_policy.pdf', 'template.php'] as $file) {
if (!in_array($file, $files, true)) throw new RuntimeException('Не включён ' . $file);
}
foreach ($files as $file) {
if (preg_match('~^(tests/|bin/|release/|docs/superpowers/|config\.json$|[0-9]+/|docs/logo(?:-alt)?\.svg$)~', $file)) {
throw new RuntimeException('В кодовую поставку попал защищённый/служебный файл ' . $file);
}
}
// Повреждение/удаление ресурса и подмена manifest не должны проходить preflight.
$fixture = sys_get_temp_dir() . '/preland11-' . bin2hex(random_bytes(6));
mkdir($fixture);
$copy = static function (string $relative) use ($root, $fixture): void {
$target = $fixture . '/' . $relative;
if (!is_dir(dirname($target))) mkdir(dirname($target), 0700, true);
copy($root . '/' . $relative, $target);
};
$manifest = json_decode(file_get_contents($root . '/release/assets.json'), true, 512, JSON_THROW_ON_ERROR);
foreach (array_keys($manifest['files']) as $file) $copy($file);
$copy('release/assets.json');
$copy('release/brands.json');
$brands = json_decode(file_get_contents($root . '/release/brands.json'), true, 512, JSON_THROW_ON_ERROR);
foreach ($brands as $brand) foreach ($brand['logos'] as $record) $copy($record['file']);
$brand = prelandBrand($root, 'zoomstage.online');
$new = array_map(static fn(array $record): string => $record['sha256'], $brand['logos']);
$old = $brand['replace_from'];
foreach ([[$old, 10], [$new, 0], [['docs/logo.svg' => $new['docs/logo.svg'], 'docs/logo-alt.svg' => $old['docs/logo-alt.svg']], 10], [['docs/logo.svg' => $old['docs/logo.svg'], 'docs/logo-alt.svg' => $new['docs/logo-alt.svg']], 10], [['docs/logo.svg' => 'unknown', 'docs/logo-alt.svg' => $new['docs/logo-alt.svg']], 1]] as [$actual, $state]) {
if (prelandBrandState($actual, $brand) !== $state) throw new RuntimeException('Миграция бренда не восстанавливается после частичного put');
}
try { prelandBrand($root, 'unregistered.example'); throw new LogicException('Неизвестная цель принята'); }
catch (RuntimeException $expected) {}
try {
validateReleaseAssets($fixture);
$mustFail = static function (callable $mutate) use ($fixture): void {
$mutate();
try { validateReleaseAssets($fixture); } catch (RuntimeException $e) { return; }
throw new RuntimeException('Повреждённый ресурс принят');
};
$mustFail(fn() => file_put_contents($fixture . '/docs/logo.svg', 'broken'));
$copy('docs/logo.svg');
$mustFail(fn() => unlink($fixture . '/docs/public_offer.pdf'));
$copy('docs/public_offer.pdf');
$mustFail(function () use ($fixture, $manifest): void {
unset($manifest['files']['docs/logo-alt.svg']);
file_put_contents($fixture . '/release/assets.json', json_encode($manifest));
});
$copy('release/assets.json');
$mustFail(function () use ($fixture, $manifest): void {
$body = '<svg>WBE70</svg>';
file_put_contents($fixture . '/docs/logo.svg', $body);
$manifest['files']['docs/logo.svg']['sha256'] = hash('sha256', $body);
file_put_contents($fixture . '/release/assets.json', json_encode($manifest));
});
} finally {
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($fixture, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
foreach ($iterator as $item) $item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
rmdir($fixture);
}
echo "OK: текущие ресурсы, повреждение/удаление/подмена manifest и состав поставки\n";
+20
View File
@@ -0,0 +1,20 @@
<?php
/** preland#13: local manual не переписывает рекламную UTM; строгий standalone canonical format. */
require dirname(__DIR__) . '/components/theme-preference.php';
$revision = str_repeat('a', 16);
$checks = 0;
foreach (['light','dark'] as $theme) foreach (['manual','preland'] as $source) {
$_COOKIE['theme_choice'] = "v1:$theme:$source:$revision";
if (prelandThemePreference() !== ['theme'=>$theme,'source'=>$source,'revision'=>$revision]
|| prelandThemeResolve($theme === 'light' ? 'dark' : 'light') !== $theme) throw new RuntimeException('Canonical preference не побеждает fallback');
$checks++;
}
foreach ([null, [], false, 1, '', "v1:light:manual:$revision\n", 'v1:dark:other:'.$revision, 'v1:dark:manual:'.str_repeat('a',15), 'v1:dark:manual:'.str_repeat('a',65), str_repeat('a',129)] as $value) {
$_COOKIE['theme_choice'] = $value;
if (prelandThemePreference() !== null || prelandThemeResolve('light') !== 'light') throw new RuntimeException('Невалидная preference принята');
$checks++;
}
$source = file_get_contents(dirname(__DIR__) . '/components/form.php');
if (!str_contains($source, 'const shownTheme = window.ZoomzillaTheme.current();')
|| !str_contains($source, "targetUrl += '&shown_theme=' + encodeURIComponent(shownTheme)")) throw new RuntimeException('Handoff должен читать показанную тему в момент навигации');
echo "OK: $checks standalone theme cases + dynamic shown_theme handoff\n";
+6 -9
View File
@@ -11,6 +11,7 @@ $_GET = [
'utm_source' => 'stage-ad', 'utm_source' => 'stage-ad',
'subid' => 'click-42', 'subid' => 'click-42',
'key' => 'must-not-replace-client-key', 'key' => 'must-not-replace-client-key',
'chob' => 'abcd1ignored',
]; ];
$_SERVER['HTTP_HOST'] = 'zoomstage.online'; $_SERVER['HTTP_HOST'] = 'zoomstage.online';
@@ -32,14 +33,6 @@ if (!is_string($query)) {
parse_str($query, $params); parse_str($query, $params);
$config = json_decode((string) file_get_contents(dirname(__DIR__) . '/config.json'), true); $config = json_decode((string) file_get_contents(dirname(__DIR__) . '/config.json'), true);
$expected = [ $expected = [
'indpic' => '1',
'theme' => 'dark',
'degr' => '1',
'registration' => 'after',
'bchoice' => 'a',
'chb' => 'b',
'tp' => $config['utm']['tp']['default'] ?? null,
'focus' => 'face',
'utm_source' => 'stage-ad', 'utm_source' => 'stage-ad',
'subid' => 'click-42', 'subid' => 'click-42',
]; ];
@@ -48,10 +41,14 @@ ksort($params);
ksort($expected); ksort($expected);
if ($params !== $expected) { if ($params !== $expected) {
fwrite(STDERR, "FAIL: claim URL обязан нести все resolved UTM и внешние параметры.\n"); fwrite(STDERR, "FAIL: foreign query обязан сохранить внешние параметры; собственные цифры хранятся отдельно.\n");
exit(1); exit(1);
} }
if ($redirect_digits !== ['indpic'=>'1','theme'=>'1','degr'=>'1','reg'=>'2','banch'=>'2','tp'=>'2','cons'=>'2','focus'=>'1']) {
throw new RuntimeException('Type-in обязан сохранить полный resolved numeric snapshot для handoff/recovery.');
}
if (isset($params['key'])) { if (isset($params['key'])) {
fwrite(STDERR, "FAIL: внешний key не должен подменять одноразовый ключ загрузки.\n"); fwrite(STDERR, "FAIL: внешний key не должен подменять одноразовый ключ загрузки.\n");
exit(1); exit(1);
+7 -5
View File
@@ -22,12 +22,12 @@ if (!is_array($config)) {
} }
$focus = $config['utm']['focus'] ?? null; $focus = $config['utm']['focus'] ?? null;
if (($focus['weights'] ?? null) !== ['center' => 0, 'face' => 1] if (($focus['weights'] ?? null) !== ['2' => 0, '1' => 1]
|| ($focus['default'] ?? null) !== 'face') { || ($focus['default'] ?? null) !== '1') {
$failures[] = 'Рабочий config.json должен фиксировать явно согласованный focus=face.'; $failures[] = 'Рабочий config.json должен фиксировать явно согласованный focus=face.';
} }
foreach (['indpic', 'theme', 'degr', 'registration', 'bchoice', 'chb', 'tp'] as $key) { foreach (['indpic', 'theme', 'degr', 'reg', 'banch', 'cons', 'tp'] as $key) {
$setting = $config['utm'][$key] ?? null; $setting = $config['utm'][$key] ?? null;
if (!is_array($setting) || !is_array($setting['weights'] ?? null) || !is_string($setting['default'] ?? null)) { if (!is_array($setting) || !is_array($setting['weights'] ?? null) || !is_string($setting['default'] ?? null)) {
$failures[] = "Рабочий config.json должен хранить валидный stage-снимок UTM {$key}."; $failures[] = "Рабочий config.json должен хранить валидный stage-снимок UTM {$key}.";
@@ -39,8 +39,10 @@ $workflow = (string) file_get_contents($root . '/.gitea/workflows/deploy.yml');
$stageStart = strpos($workflow, ' deploy-zoomstage:'); $stageStart = strpos($workflow, ' deploy-zoomstage:');
$stageBlock = $stageStart === false ? '' : substr($workflow, $stageStart); $stageBlock = $stageStart === false ? '' : substr($workflow, $stageStart);
if ($stageBlock === '' || !str_contains($stageBlock, 'config\\.json$') if ($stageBlock === '' || !str_contains($stageBlock, 'config\\.json$')
|| !str_contains($stageBlock, 'ensure-focus-config.php') || !str_contains($stageBlock, 'ensure-utm-config.php')
|| !str_contains($stageBlock, 'Preserve host config and add focus if absent')) { || !str_contains($stageBlock, 'Migrate numeric UTM in preserved host config')
|| str_contains($stageBlock, 'php bin/ensure-focus-config.php')
|| str_contains($stageBlock, 'php bin/ensure-cons-config.php')) {
$failures[] = 'Stage обязан сохранять хостовый config.json и согласованный focus-контракт.'; $failures[] = 'Stage обязан сохранять хостовый config.json и согласованный focus-контракт.';
} }
if (preg_match_all('/^ [a-z][a-z0-9-]*:/m', substr($workflow, strpos($workflow, 'jobs:')), $jobs) !== 1 if (preg_match_all('/^ [a-z][a-z0-9-]*:/m', substr($workflow, strpos($workflow, 'jobs:')), $jobs) !== 1
+31
View File
@@ -0,0 +1,31 @@
const {chromium}=require('/e2e/node_modules/playwright-core'),fs=require('fs'),http=require('http'),assert=require('assert/strict'),crypto=require('crypto');
const production=process.env.PUBLISHED==='1';
const server=production?null:http.createServer((req,res)=>{const r=http.request(new URL(req.url,'http://api:8001'),{method:req.method,headers:{...req.headers,connection:'close'}},up=>{res.writeHead(up.statusCode,up.headers);up.pipe(res)});req.pipe(r)}).listen(8121);
const base=process.env.PRELAND_URL||(production?'https://zoomstage.online':'http://pre301.localhost:8121');
(async()=>{const browser=await chromium.launch({args:['--no-sandbox']});const out=[];
try{for(const width of [1440,768,390,320])for(const theme of ['light','dark'])for(const indpic of [1,2]){
console.log('CASE',width,theme,indpic);
const ctx=await browser.newContext({viewport:{width,height:900},isMobile:width<768,hasTouch:width<768}),page=await ctx.newPage(),errors=[];
page.on('pageerror',e=>errors.push(e.message));
await page.route('**/*',route=>['GET','HEAD','OPTIONS'].includes(route.request().method())?route.continue():route.abort());
await page.goto(base+'/?theme=abcd'+(theme==='dark'?'1':'2')+'&indpic=abcd'+indpic,{waitUntil:'networkidle'});
await page.waitForFunction(()=>document.querySelector('#examples').dataset.examplesState==='ready');
assert.equal(await page.title(),'Улучшение изображения');assert(await page.locator('#dropzone-form').isVisible());
const footer=await page.locator('.footer').innerText();assert(!/Наши реквизиты|ИНН:|ОГРНИП:|Кардаполов|Иванов/.test(footer));assert.equal(await page.locator('.footer img[src*="payment.svg"] ').count(),0);assert(footer.includes(new URL(base).hostname));
const alignment=await page.evaluate(()=>{const h=document.querySelector('.header').getBoundingClientRect(),logo=[...document.querySelectorAll('.navbar-brand img')].find(e=>getComputedStyle(e).display!=='none').getBoundingClientRect();return {delta:logo.y-h.y-(h.height-logo.height)/2}});assert(Math.abs(alignment.delta)<1,'Видимый логотип должен быть по центру шапки');
const logos=await page.locator('.navbar-brand img,.footer-logo img').evaluateAll(es=>es.map(e=>({src:e.src,visible:getComputedStyle(e).display!=='none',width:e.naturalWidth,loaded:e.complete})));
assert.equal(logos.length,4);assert.equal(logos.filter(l=>l.visible).length,2);
for(const logo of logos){const url=new URL(logo.src);assert(url.searchParams.get('v'));const file=url.pathname.slice(1);const bytes=fs.readFileSync('/candidate/'+file);const digest=production?JSON.parse(fs.readFileSync('/candidate/release/brands.json'))[new URL(base).hostname].logos[file].sha256:crypto.createHash('sha256').update(bytes).digest('hex');assert.equal(url.searchParams.get('v'),digest.slice(0,16));if(logo.visible)assert(logo.loaded&&logo.width>0);}
assert(logos.filter(l=>l.visible).every(l=>new URL(l.src).pathname.endsWith(theme==='dark'?'logo-alt.svg':'logo.svg')));
await page.waitForFunction(()=>document.querySelectorAll('.image-examples-item')[1].getAttribute('aria-disabled')==='false');
await page.locator('.image-examples-item').nth(1).click();
await page.waitForFunction(()=>/02_(desk|mob)_1-source/.test(document.querySelector('#image-compare-source').src));
assert((await page.locator('#image-compare-source').getAttribute('src')).includes('/'+indpic+'/'));
assert(/02_(desk|mob)_2-result/.test(await page.locator('#image-compare-result').getAttribute('src')));
if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth)){await page.screenshot({path:'/qa/overflow.png',fullPage:true});console.log(await page.evaluate(()=>[...document.querySelectorAll('body *')].map(e=>({tag:e.tagName,cls:e.className,x:e.getBoundingClientRect().x,right:e.getBoundingClientRect().right,width:e.getBoundingClientRect().width})).filter(e=>e.right>innerWidth+1)));}
assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth),false);assert.deepEqual(errors,[]);
const row={width,theme,indpic,logos,errors};out.push(row);
if(indpic===1&&(width===1440||width===390))await page.screenshot({path:'/qa/brand-'+(production?'published':'local')+'-'+width+'-'+theme+'.png',fullPage:true});
await ctx.close();
}fs.writeFileSync('/qa/brand-'+(production?'published':'local')+'.json',JSON.stringify(out,null,2));console.log('PASS brand + thumbnail interactions',out.length,'cases');}
finally{await browser.close();server?.close();}})().catch(e=>{console.error(e);process.exit(1)});
+38
View File
@@ -0,0 +1,38 @@
// api#326: реальный handoff codec, включая сохранённые legacy records.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const source = fs.readFileSync('/app/components/utm-handoff.php','utf8').replace(/\r\n/g,'\n').split("<<<'JS'\n")[1].split('\nJS;')[0];
const context = {window:{},URLSearchParams,Uint8Array,crypto:require('node:crypto').webcrypto};
vm.runInNewContext(source,context);
const handoff=context.window.ZoomzillaUtmHandoff;
const defaults={indpic:'1',theme:'1',degr:'1',reg:'2',banch:'2',tp:'1',cons:'2',focus:'1'};
const selected={indpic:'2',theme:'2',degr:'2',reg:'1',banch:'1',tp:'2',cons:'3',focus:'2'};
const foreign='utm_source=abcd9same&subid=%D1%82%D0%B5%D1%81%D1%82+%2B+%26&id=42&stage=keep';
for(const meta of [
{redirectUtm:foreign,utmDigits:selected},
{redirectUtm:foreign,utmDigits:{...selected,banch:undefined,chob:'1'}},
{redirectUtm:foreign+'&indpic=abcd2&theme=абвг2&degr=abcd2&reg=abcd1&chob=abcd1&tp=abcd2&cons=abcd3&focus=abcd2'},
{redirectUtm:foreign+'&indpic=2&theme=light&degr=2&registration=before&bchoice=b&tp=2&cons=3&focus=center&chb=a'},
{redirectUtm:foreign+'&indpic=abcd2&theme=абвг2&degr=abcd2&reg=abcd1&banch=abcd1&tp=abcd2&cons=abcd3&focus=abcd2'}
]){
const before=JSON.stringify(meta);
const first=new URLSearchParams(handoff.query(meta,defaults));
const second=new URLSearchParams(handoff.query(meta,defaults));
for(const [key,digit] of Object.entries(selected)){
assert.match(first.get(key),/^[A-Za-z0-9]{10}$/);
assert.equal(first.get(key)[4],digit);
assert.equal(second.get(key)[4],digit);
assert.notEqual(first.get(key),second.get(key));
}
for(const query of [first,second]){
assert.equal(query.get('utm_source'),'abcd9same');
assert.equal(query.get('subid'),'тест + &');
assert.equal(query.get('id'),'42'); assert.equal(query.get('stage'),'keep');
for(const legacy of ['registration','bchoice','chb','chob'])assert.equal(query.has(legacy),false);
}
assert.equal(JSON.stringify(meta),before);
}
const typeIn=new URLSearchParams(handoff.query({redirectUtm:'',utmDigits:defaults},defaults));
for(const [key,digit]of Object.entries(defaults))assert.equal(typeIn.get(key)[4],digit);
console.log('OK: all8 fresh noise/repeated handoff, legacy recovery, type-in, foreign/technical values');
+6
View File
@@ -0,0 +1,6 @@
<?php
if (PHP_SAPI !== 'cli') { http_response_code(404); exit; }
$_SERVER['HTTP_HOST'] = $argv[1] ?? 'zoomstage.online';
$_SERVER['SERVER_NAME'] = 'fixture.local';
$_GET = ['theme' => 'abcd' . (($argv[2] ?? 'light') === 'dark' ? '1' : '2'), 'indpic' => 'abcd1'];
require dirname(__DIR__) . '/index.php';