Перейти к содержимому

JavaScript в пакетах тем

Тема может включать готовый JavaScript-модуль с декоративными эффектами. Он устанавливается, обновляется, экспортируется и удаляется вместе с темой, без установки отдельного плагина. Обычные темы формата v1 продолжают работать без изменений.

В библиотеке тем загрузите ZIP или импортируйте публичный репозиторий, выберите тему и при необходимости подтвердите разрешение JavaScript. Флажок изначально снят: без него устанавливаются цвета и CSS, эффекты остаются выключенными. Каждое обновление требует нового подтверждения. Кнопка JavaScript в карточке установленной темы позволяет включить или отозвать разрешение. Только выбранная тема витрины может запускать эффект.

trusted-dom — доверенный код страницы, а не песочница. Код может читать DOM, обращаться к доступным посетителю API и выполнять действия от его имени. Ограниченный SDK помогает управлять ресурсами, но не лишает JavaScript доступа к window, document, сети или хранилищам. Разрешайте только проверенный код известного автора. Проверка файлов и SHA-256 не устанавливает безопасность автора. Согласие связывается с содержимым эффекта, ресурсами, манифестом, автором, источником и версией политики. Разрешения не переносятся с экспортом; восстановление резервной копии также выключает эффекты.

Для администраторов эффекты в обычном Mini App по умолчанию выключены. В разделе «Внешний вид» можно включить «JavaScript темы для администраторов» (WEBAPP_ADMIN_THEME_EFFECTS_ENABLED). Настройка разрешает запуск только активной темы с действующим согласием на её JavaScript; отзыв согласия и локальное отключение продолжают действовать. Доверенный код получает доступ к странице и API от имени администратора. В админ-панели и у неавторизованных посетителей эффекты не запускаются. Явный предпросмотр установленной темы в Mini App (theme_preview) доступен администратору независимо от этой настройки и также требует согласия. Живой предпросмотр в библиотеке запускается только по отдельному нажатию в iframe sandbox="allow-scripts" с непрозрачным origin, запретом сети и тестовой карточкой без данных аккаунта. Он уничтожается при закрытии или через 60 секунд. Это проверка внешнего вида, не аудит безопасности или гарантия совместимости. Публичная демоверсия документации показывает управление согласием, но не исполняет загруженный JS; живой предпросмотр эффектов доступен на установленном Core.

my-theme/
  theme.json
  theme.css
  theme-package.json
  effects/main.js
  LICENSE

В theme-package.json:

{
  "schema_version": 2,
  "version": "1.0.0",
  "license": "MIT",
  "effects": {
    "api_version": 1,
    "runtime": "trusted-dom",
    "entry": "effects/main.js",
    "styles": [],
    "assets": [],
    "targets": ["home.header.surface", "home.card.surface"],
    "description": {"ru": "Декоративное стекло"}
  }
}

entry — один UTF-8 ESM bundle до 2 MiB со встроенными зависимостями. Дополнительные JS-файлы, HTML, серверные хуки и установочные команды не принимаются. Нельзя рассчитывать на CDN, npm install, динамические чанки или относительный import: импортируйте зависимости при сборке. Список styles содержит до 8 CSS-файлов, assets — до 32 изображений/шрифтов суммарно до 5 MiB. Они проходят существующие проверки CSS/SVG/архива. Все пути локальные, без .. и ссылок. Эффекты не требуют изменения общего script-src или добавления unsafe-eval.

Пакет обязан выглядеть и оставаться работоспособным без JavaScript: отказ WebGL, ошибка модуля, отключение посетителем и prefers-reduced-motion оставляют CSS-тему.

Типы SDK: themeEffectsSdk.ts. Готовый модуль экспортирует mount(host, context) и возвращает { update(context), dispose() }. Обе операции могут быть асинхронными. Инициализация должна находиться внутри mount, а не на верхнем уровне модуля: браузер кеширует импорт, а монтирование повторяется после скрытия страницы.

export function mount(host, context) {
  const layers = host.targets("home.header.surface");
  const stop = host.animation((time) => {
    // Animate only your own nodes inside layers.
  });
  return {
    update(next) { context = next; },
    dispose() { stop(); }
  };
}

Доступные декоративные области: shell.background, home.header.surface, home.card.surface. Они имеют aria-hidden и pointer-events: none; бизнес-кнопки и их Svelte DOM не заменяются. home.card.surface сейчас относится к основной карточке подписки в обычном домашнем layout; компактный layout должен использовать фон/заголовок как fallback. Не завязывайтесь на CSS-классы или внутреннюю структуру приложения. Через watchTargets отслеживайте появление и исчезновение областей при навигации. context содержит вариант, цвета, язык и reduced motion без данных аккаунта. host.asset(name) возвращает URL объявленного ресурса.

SDK освобождает свои слушатели, таймеры, RAF и ResizeObserver через host.signal и cleanup. Адаптер освобождает собственные WebGL buffers/textures/programs, сторонние объекты и незавершённые операции. Поздний результат асинхронной инициализации должен проверять host.signal.aborted. Скрытие документа останавливает эффект; возврат может смонтировать ту же версию заново. Ошибка инициализации/обновления оставляет CSS и после запуска JS перезагружает страницу с theme_effects=off. Бесконечный синхронный цикл и намеренно вредоносный код остановить SDK не может.

  • Посетитель: кнопка «Отключить эффекты темы», настройка хранится на устройстве. Включение/отключение перезагружает документ. Аварийный вход: добавить ?theme_effects=off к URL.
  • Администратор: отключить JavaScript в карточке темы. Это отзывает также разрешения старых версий для отката. Обновление без флажка согласия имеет такой же результат.
  • Оператор: создать пустой файл <WEBAPP_THEMES_DIR>/_registry/effects-disabled. После удаления файла эффекты вновь доступны при сохранённом согласии.
  • Открытая витрина проверяет разрешение каждые 30 секунд (таймаут запроса 10 секунд), при удалении, смене версии/темы или отзыве разрешения освобождает ресурсы и перезагружается с выключенным JS. Это поведение штатного клиента, не принудительный отзыв доступа у уже исполняющегося злонамеренного скрипта. Сервер сразу перестаёт выдавать его ресурсы; исторические/retired пакеты не дают доступ к JS.
  • Реестр v1 при первой записи мигрирует в v2 с копией _registry/before-effects-v2.json. Старый Core не должен читать v2: перед откатом приложения сделайте полный backup и восстановите совместимое состояние, учитывая изменения после этой копии.
  • tilt.zip — небольшой пример эффекта указателя без зависимостей. Аналогично можно упаковать VanillaTilt, Vanta/Three.js или частицы: собрать зависимости заранее, привязаться к декоративным областям и реализовать полное освобождение ресурсов.

dashersw/liquid-glass-js нуждается в адаптере: исходные Container/Button управляют глобальным DOM, используют html2canvas(document.body), не предоставляют полного dispose и требуют внешнего скрипта в демонстрации. Для темы заранее соберите зависимости, ограничьте снимок авторским декоративным фоном, число WebGL-контекстов и размер canvas, удаляйте слушатели и GPU-ресурсы. Это изменит эффект преломления реальной страницы, зато не потребует снимать данные аккаунта. Исходный репозиторий напрямую не является установочным пакетом Minishop.

Исходники адаптеров находятся в examples/theme-effects-src/. Сборка:

npm --prefix frontend ci
node scripts/build_theme_effect_examples.mjs
node scripts/build_theme_effect_examples.mjs --check

Для реального релиза темы проверьте desktop/mobile, Chrome/Safari/Firefox и используемые Telegram WebView, отсутствие WebGL, reduced motion, сворачивание, повторную навигацию, обновление и отзыв согласия. Предпросмотр показывает только mock layout и не заменяет эти проверки.