Skip to content

Справочник API фронтенда

Эта страница перечисляет каждое имя хука, слот представления, событие жизненного цикла, помощник навигации и экспорт @panomc/sdk, которые может использовать UI вашего дополнения — в одном месте. Это справочная страница, а не руководство.

Новичок здесь? Четыре вещи, каталогом которых является эта страница, простыми словами:

  • Хук = именованное место в странице хоста, где отрисовывается ваш компонент.
  • Слот представления = как хук, но элементы упорядочены (по числу приоритета) и могут быть индивидуально скрыты или показаны.
  • Событие жизненного цикла = момент во время загрузки страницы, в который вы можете выполнить код.
  • Помощник навигации = API для добавления или редактирования ссылок в меню сайта или боковой панели администратора.

Как читать эту страницу

Это набор плотных справочных таблиц, сгруппированных по области API (§1–§10). Вы не читаете её сверху вниз — вы прыгаете к разделу о том, что связываете. Каждый раздел открывается одним простым предложением о том, для чего он и где работает (тема, панель или оба). Если термин в ячейке таблицы незнаком, он почти наверняка определён в блоке «Понятия, которые предполагает эта страница» чуть ниже — прочтите этот блок один раз сначала.

Понятия, которые предполагает эта страница (прочтите один раз, ~60 секунд)

Эта страница переиспользует горстку слов десятки раз каждое. Вот каждое в одном простом предложении:

  • Хост — работающий фронтенд Pano, который загружает и выполняет JavaScript вашего дополнения. Это одно из двух приложений: тема или панель (следующий пункт). «Хост» здесь никогда не означает сервер, который вы арендуете.
  • Тема против панели — два фронтенда. Тема — публичный сайт, который видят игроки, по адресу /. Панель — админ-панель, по адресу /panel. Ваше дополнение может работать в обоих, и каждый предоставляет разные API. pano.isPanel говорит вашему коду, в котором из них он находится.
  • Хранилище Svelte — значение, на которое вы можете подписаться. В компоненте .svelte поставьте $ перед ним, чтобы читать его реактивно ($myStore). В обычном JS вызовите store.subscribe(fn) и сохраните функцию, которую он возвращает, чтобы позже прекратить слушать (протолкните эту функцию в this._unsubscribers, смотрите §1). Несколько API ниже «возвращают хранилище». (Документация Svelte: хранилища)
  • load(event) — необязательная функция, которую может экспортировать страница или хук. Хост вызывает её при подготовке страницы. event описывает запрос (его URL, параметры, cookies). Смотрите блок «три вида load()» ниже.
  • props — значения, которые компонент Svelte получает от того, что его отрисовало. Любой объект, который возвращает ваш load(), становится props вашего компонента.
  • SSR / гидрация — SSR («отрисовка на стороне сервера») означает, что при первом открытии посетителем страницы её HTML строится на сервере; последующие переходы строятся в браузере. Гидрация — это когда Svelte подключает построенный на сервере HTML в браузере, чтобы кнопки и т. д. стали интерактивными.
  • голый спецификатор / внешнийголый спецификатор — путь импорта без ./ или /, например import { x } from 'svelte'. Ваша сборка оставляет разрешённые из них внешними — она не копирует их в JS вашего дополнения; хост поставляет их во время выполнения. Смотрите §10.
  • узел разрешения — строка разрешения вроде x.y.z, которая решает, кому разрешено что-то видеть или делать. Список узлов и то, как дополнения определяют свои собственные, смотрите в Справочнике Backend API.

Три вида load() на этой странице

Слово load появляется в трёх разных ролях. Они не взаимозаменяемы — путаница между ними — классический баг первой недели:

ГдеВы пишетеОн получаетЧто означает его возврат
Страница (§2)export function load(event) в модуле страницыevent (запрос)props компонента страницы (четыре специальных ключа вроде pageTitle извлекаются для хоста)
Хук (§3)export function load(event) в модуле хукаevent (запрос)props компонента хука; верните { hookOptions: { invisible: true } }, чтобы скрыть хук
Обработчик жизненного цикла (§4, §6, §7)(data, event) => { … }, переданный вызову onLoad(...) / lifecycle.on(...)data (данные страницы, иногда изменяемые) и eventобычно ничего — вы читаете, или для помеченных событий подправляете, data на месте

Если вы хотите увидеть эти API в использовании в реальном дополнении, начните с Разработки фронтенда, которая строит UI Shoutbox шаг за шагом. Всё здесь — поверхность, из которой черпает та страница.

Для Kotlin-половины вашего дополнения у этой страницы есть родственница: Справочник Backend API делает ту же работу для поверхности бэкенда — жизненный цикл плагина, база данных, эндпоинты, разрешения и события.

Откуда всё это берётся

Вы пишете import { X } from 'svelte', но ваша сборка никогда не упаковывает Svelte в вывод вашего дополнения — она оставляет этот импорт внешним, а работающий сайт Pano (хост) поставляет одну общую копию во время выполнения. Вот почему разрешены только определённые импорты (смотрите §10). Дерево pano.*, задокументированное ниже, внедряется в ваш плагин как this.pano; модули @panomc/sdk — это замороженный список импортов в конце этой страницы. (Полный механизм: Архитектура.)

0. Объект pano

Ко всему обращаются через объект pano, который хост внедряет в ваш плагин как this.pano. Один флаг живёт наверху; остальное свисает с pano.ui.*.

СвойствоТипЧто это
pano.isPanelbooleantrue, когда ваш код выполняется внутри админ-панели, false внутри темы. Используйте его, чтобы выполнять разные регистрации в каждой — то есть if (this.pano.isPanel) { /* panel registrations */ } else { /* theme registrations */ }.

Панель и тема предоставляют разные деревья pano.ui — помощник, существующий в одном, может не существовать в другом. Поэтому по всей этой странице каждый раздел говорит, где живут его члены: тема + панель, только тема или только панель. Когда весь раздел односторонний (как §4, только тема), это говорят его заголовок и блок вверху.

1. Контракт входа плагина

Ваш src/main.js экспортирует по умолчанию класс, расширяющий PanoPlugin (из @panomc/sdk). Хост создаёт один его экземпляр, внедряет this.pano и вызывает методы жизненного цикла ниже.

Наименьший возможный входной файл — это просто вот это:

js
import { PanoPlugin } from '@panomc/sdk';

export default class extends PanoPlugin {
  onLoad() {
    // your register(...) calls from sections 2–5 go here
  }
}

Каждую таблицу дальше вниз легче читать, представив этот скелет.

ЧленВидНазначение
onLoad()метод (переопределение)Вызывается один раз после загрузки плагина. Делайте здесь все ваши регистрации (вызовы register(...) из разделов 2–5 ниже). this.pano доступен. Не трогайте this.pano в конструкторе — он внедряется только прямо перед выполнением onLoad().
onUnload()метод (переопределение)Вызывается при уничтожении плагина. Отмените всё, что не должно задерживаться (например, pano.ui.page.unregister(...)).
this.panoсвойствоВнедрённый объект API, задокументированный на этой странице.
this.contextсвойствоПростой объект, где ваш плагин хранит состояние, которым хочет поделиться со своими компонентами (например, настройки, которые вы получили).
this.setContext(partial)методКопирует ключи partial в this.context (как Object.assignповерхностное слияние, на один уровень) и уведомляет всё, подписанное на контекст, что он изменился.
this._unsubscribersмассивПроталкивайте сюда функции отписки от хранилищ (смотрите пункт о хранилищах в блоке «Понятия»); хост выполняет их все при уничтожении плагина, так что ваши подписки не утекают.

Две функции приходят из @panomc/sdk вместе с базовым классом:

ЭкспортНазначение
viewComponent(importer)Правило: всегда пишите viewComponent(() => import('./X.svelte')), когда передаёте компонент любому вызову register — никогда голый () => import(...) и никогда сам компонент. (Он присоединяет правильные mount/hydrate/unmount общей среды выполнения для хоста; эта сантехника — причина, почему обёртка обязательна.)
getPanoContext()Возвращает текущий контекст хоста Pano. Можете игнорировать это, если только вам не нужен сырой контекст хоста из кода, который не является методом PanoPlugin (у обычного метода вместо этого есть this.context) — редко нужно.

onContextUpdate не вызывается

Если класс, который вы создали из boilerplate, содержит метод onContextUpdate(), удалите его — ни один хост его не вызывает. Это мёртвый код; не стройте на нём поведение. Используйте onLoad() для настройки, а подписки на хранилища (смотрите блок «Понятия») для реагирования на изменения.

2. Страницы — pano.ui.page (тема + панель)

Используйте это, чтобы регистрировать собственные целые страницы — под темой (/…) или панелью (/panel/…).

ВызовНазначение
pano.ui.page.register(options)Зарегистрировать страницу по пути.
pano.ui.page.unregister(path)Убрать страницу, которую вы зарегистрировали (используется для очистки — смотрите Разработку фронтенда).
pano.ui.page.isPluginPage(path)true, если какой-то плагин зарегистрировал этот путь.

register(options):

ОпцияТипЗначение
pathstringМаршрут (смотрите формы пути ниже).
componentviewComponent(...)Компонент страницы.
systemLayoutstringОбернуть вашу страницу в один из встроенных макетов хоста (имена перечислены ниже). Используйте MainLayout для обычной страницы; остальные имена соответствуют конкретным разделам хоста (например, ProfileLayout, SettingsLayout).
layoutviewComponent(...)Использовать собственный компонент макета вместо встроенного.
resetLayoutbooleanОтрисовать без шапки, боковой панели или футера хоста — ваш компонент получает всю страницу. («Chrome» — общее слово для этого окружающего UI хоста.)
permissionstringУзел разрешения (строка разрешения вроде x.y.z — смотрите Справочник Backend API для списка), необходимый для просмотра. Если у текущего пользователя его нет, страница отрисовывает 404.

Формы пути:

ФормаПримерСовпадает с
буквальная/shoutboxровно этим путём
динамический сегмент/shout/[id] или /shout/:idодним сегментом, захваченным как параметр — читайте его в вашем load(event) как event.params.id
перехват всего/docs/[...rest]оставшимися сегментами (должен быть финальным сегментом)
регэкспre:/shout/\d+паттерн должен совпасть со всем путём, а не только частью — хост оборачивает его в ^…$ («полностью заякорено») за вас

Модуль страницы также может экспортировать load(event) (смотрите блок «три вида load()» вверху). event описывает запрос — его URL, параметры и cookies. Любой объект, который вы возвращаете, передаётся вашему компоненту страницы как его props. Четыре специальных ключа удаляются из этого возвращённого объекта и используются chrome хоста вместо передачи как props:

  • pageTitle — заголовок, показываемый для страницы (например, вкладка браузера).
  • breadcrumbs — цепочка хлебных крошек, которую хост отрисовывает над страницей.
  • sidebar — боковая панель для этой страницы.
  • sidebarProps — props, переданные этой боковой панели.

Имена systemLayout — тема: AppLayout, AuthLayout, MainLayout, ProfileLayout, ThemeSettingsLayout, TicketsLayout.

Имена systemLayout — панель: AddonDetailLayout, AddonsLayout, AppLayout, MainLayout, MigrationLayout, PermissionsLayout, PlayerDetailLayout, PlayersLayout, PostsLayout, ServerLayout, ServerSettingsLayout, SettingsLayout, TicketsLayout, TranslationsLayout, ViewLayout.

Контрольная точка — зарегистрировалась ли моя страница?

После вызова pano.ui.page.register({ path: '/your-path', component }) пересоберите ваше дополнение и перезагрузите сайт. Посещение /your-path теперь должно показывать ваш компонент. Ничего там нет? Проверьте, что component обёрнут в viewComponent(() => import('./X.svelte')) и что register выполнился внутри onLoad().

3. Хуки — pano.ui.hook (тема + панель)

Хук — именованная дыра в странице хоста: всё, что вы регистрируете под именем этого хука, отрисовывается в этом фиксированном месте. Хук — плоский список компонентов под одним именем. (Слоты представлений в §4 добавляют упорядочивание и скрытие поверх этой же идеи.)

ВызовГдеНазначение
pano.ui.hook.register(options)тема + панельСмонтировать компонент в именованный хук.
pano.ui.hook.get(name)тема + панельВозвращает хранилище (смотрите блок «Понятия») компонентов, зарегистрированных для name.
pano.ui.hook.setVisible(name, component, visible)только темаПереключить видимость записи хука. Передайте ту же ссылку на компонент, которую вы дали register.

register(options):

ОпцияТипЗначение
namestringИмя хука (таблицы ниже).
componentviewComponent(...)Компонент для монтирования.
permissionstringОтрисовывать только для пользователей, держащих этот узел разрешения.
skipLoadbooleanНе выполнять load() компонента во время загрузки страницы. Используйте это, когда ваш хук получает собственные данные при монтировании, так что выполнение при загрузке страницы было бы потрачено впустую или упало бы.
invisiblebooleanЗарегистрировать, но начать скрытым.

Контракт load() / hookProps хука: модуль компонента хука может экспортировать load(event). Хост выполняет его во время загрузки страницы — на сервере при первом открытии страницы (SSR) и в браузере при переходе между страницами — и передаёт результат вашему компоненту как его props (этот объединённый объект props — это hookProps компонента). Компонент может скрыть себя, вернув { hookOptions: { invisible: true } } из своего load().

Чтение имени хука

Имя хука говорит вам примерно, где он отрисовывается. Префикс — это область: theme: = публичный сайт, page: = область содержимого страницы, panel: = админ-панель. Середина называет страницу или таблицу. Суффикс называет место: :top, :bottom, :content, :sidebar или позицию в таблице вроде :header:... / :row:.... Не уверены, где именно один приземляется? Зарегистрируйте компонент, отрисовывающий видимую метку, пересоберите и посмотрите.

Столбец Extra prop ниже перечисляет любой prop, который хост передаёт вашему компоненту в дополнение к результату вашего load().

Имена хуков темы

Имя хукаExtra prop
theme:top
page:top
page:home:top
theme:post-detail:bottompost
theme:support:content

Имена хуков панели

Табличные хуки и prop tag

Хуки панели, чьи имена содержат table:header или table:row, отрисовываются внутри строк <tr> хоста. Хост передаёт вам tag (либо 'th' для ячейки заголовка, либо 'td' для ячейки тела); отрисовывайте <svelte:element this={tag}>…</svelte:element>, чтобы ваша ячейка была валидным HTML таблицы.

Имя хукаExtra prop
panel:plugin-detail:contentaddon
panel:plugin-detail:content:<pluginId>addon
panel:player-detail:bottomplayerData
panel:player-detail:sidebarplayerData
panel:post-editor:actions:rightpost
panel:post-editor:sidebar:beforepost
panel:post-editor:sidebar:afterpost
panel:post-editor:content:bottompost
panel:posts:layout:actions:right
panel:posts:table:header:starttag="th"
panel:posts:table:header:after-titletag="th"
panel:posts:table:header:after-categorytag="th"
panel:posts:table:header:after-viewstag="th"
panel:posts:table:header:after-authortag="th"
panel:posts:table:header:endtag="th"
panel:posts:table:row:startpost, tag="td"
panel:posts:table:row:after-thumbnailpost, tag="td"
panel:posts:table:row:after-titlepost, tag="td"
panel:posts:table:row:after-categorypost, tag="td"
panel:posts:table:row:after-viewspost, tag="td"
panel:posts:table:row:after-authorpost, tag="td"
panel:posts:table:row:endpost, tag="td"
panel:players:table:header:starttag="th"
panel:players:table:header:after-nametag="th"
panel:players:table:header:after-perm-grouptag="th"
panel:players:table:header:after-statustag="th"
panel:players:table:header:after-last-logintag="th"
panel:players:table:header:endtag="th"
panel:players:table:row:startplayer, tag="td"
panel:players:table:row:after-nameplayer, tag="td"
panel:players:table:row:after-perm-groupplayer, tag="td"
panel:players:table:row:after-statusplayer, tag="td"
panel:players:table:row:after-last-loginplayer, tag="td"
panel:players:table:row:endplayer, tag="td"
panel:post-categories:table:header:starttag="th"
panel:post-categories:table:header:after-categorytag="th"
panel:post-categories:table:header:after-descriptiontag="th"
panel:post-categories:table:header:after-urltag="th"
panel:post-categories:table:header:endtag="th"
panel:post-categories:table:row:startcategory, tag="td"
panel:post-categories:table:row:after-categorycategory, tag="td"
panel:post-categories:table:row:after-descriptioncategory, tag="td"
panel:post-categories:table:row:after-urlcategory, tag="td"
panel:post-categories:table:row:endcategory, tag="td"

Props post, playerData, addon, player и category выше — это те же объекты, что уже использует страница хоста для этого поста / игрока / дополнения / категории. Их точные поля здесь не перечислены — сделайте console.log prop, чтобы осмотреть его, или откройте код соответствующей страницы хоста.

Хуки с суффиксом :<pluginId>

panel:plugin-detail:content:<pluginId> отрисовывается только на странице деталей вашего дополнения — подставьте свой собственный pluginId (id, который объявляет ваш плагин — смотрите Справочник Backend API). Это стандартное место для панели настроек дополнения.

Контрольная точка — отрисовался ли мой хук?

После pano.ui.hook.register({ name: 'theme:top', component }) пересоберите и перезагрузите страницу темы. Ваш компонент должен появиться в месте этого хука. Если нет, подтвердите, что вы использовали реальное имя хука из таблиц выше и обернули компонент в viewComponent(...).

4. Слоты представлений — pano.ui.view (только тема)

Слот представления — именованный контейнер, отрисовывающий упорядоченный по приоритету список компонентов плагина (дополнительные методы входа, дополнительные строки профиля и так далее). Как хук, но каждый элемент слота несёт id и priority, так что элементы можно индивидуально скрывать, переупорядочивать или заменять.

Хуки против слотов представлений одним взглядом:

Хук (§3)Слот представления (§4)
Упорядочиваниенет (плоский список)по priority (выше отрисовывается первым)
id на элементнетда (id) — позволяет скрыть/переместить/заменить один элемент
Где работаеттема + панельтолько тема

pano.ui.view / pano.ui.sidebar существуют только в теме

  • Строите для панели? Пропустите весь этот раздел — панель вообще не предоставляет view.register/hide/show/move/get/onLoad/load или pano.ui.sidebar.
  • Исключение панели 1: дополнительные строки в модальном окне редактирования игрока имеют собственный выделенный API — смотрите «Панель: строки модального окна редактирования игрока» ниже.
  • Исключение панели 2: единственный член pano.ui.view панели — это pano.ui.view.themes.editMenu — смотрите §8.
ВызовНазначение
pano.ui.view.register({ viewId, id, component, priority })Добавить компонент в слот viewId. priority по умолчанию 10; повторная регистрация того же id заменяет его.
pano.ui.view.hide(viewId, id)Скрыть элемент, не удаляя его.
pano.ui.view.show(viewId, id)Снова показать его.
pano.ui.view.move(viewId, id, priority)Изменить приоритет элемента.
pano.ui.view.get(viewId)Возвращает хранилище (смотрите блок «Понятия») видимых, упорядоченных элементов.
pano.ui.view.onLoad(viewId, handler)Выполнять ваш обработчик каждый раз, когда данные этого слота загружаются. (Под капотом это событие жизненного цикла theme:view:<viewId>:load — смотрите §6.)
pano.ui.view.load(viewId, event)Выполнить конвейер загрузки слота и получить разрешённые элементы (для страницы плагина, которая сама содержит слот).

pano.ui.sidebar.*псевдоним того же реестра с теми же методами, за исключением того, что ключ контейнера — sidebarId, а не viewIdonLoad срабатывает theme:sidebar:<id>:load).

Форма элемента слота: { id, component, priority, props? }. Более высокий priority отрисовывается первым. Нет поля permission на элемент — элементы слота не фильтруются по разрешениям. Чтобы ограничить один, проверьте разрешение внутри вашего компонентаimport { hasPermission } from '@panomc/sdk/utils/auth' — и не отрисовывайте ничего, если оно не проходит. (Хуки и ссылки навигации поддерживают опцию permission.)

ID слотов темы

ID слотаГде отрисовывается
login-contentтело страницы входа
login-alt-methodsальтернативные методы входа
register-contentтело страницы регистрации
register-alt-methodsальтернативные методы регистрации
profile-contentтело страницы профиля
profile-card-rowsстроки на карточке профиля
settings-contentтело страницы настроек
settings-card-rowsстроки на карточке настроек
tickets-contentтело страницы тикетов поддержки
navbar-rightправая сторона навбара
navbar-profile-dropdownвыпадающее меню профиля
support-contentтело страницы поддержки
support-optionsсписок опций страницы поддержки
reset-password-contentтело страницы сброса пароля
renew-password-contentтело страницы обновления пароля
activate-contentтело страницы активации аккаунта
activate-new-email-contentтело страницы активации нового email

Панель: строки модального окна редактирования игрока

У панели нет реестра слотов pano.ui.view. Её единственная точка расширения такого рода — дополнительные строки в модальном окне редактирования игрока — вместо этого имеет собственный выделенный API:

ВызовНазначение
pano.ui.player.editModal.cardRows.edit(callback)Отредактировать список строк карточки, показываемых в модальном окне редактирования игрока. callback получает текущий массив строк; измените его на месте и верните.
pano.ui.player.editModal.cardRows.get()Прочитать текущие строки карточки.

Объекты строк зеркалят существующие строки модального окна; их точные поля здесь не перечислены — вызовите cardRows.get() (или сделайте console.log массива, который получает ваш колбэк edit), чтобы осмотреть их. Дополнения в стиле аватаров и социального входа используют это, чтобы добавить строку в это модальное окно.

Соглашения о приоритетах

Соблюдайте эти числа, чтобы ваши элементы приземлились в разумном порядке относительно других установленных дополнений. Напоминание: выше приоритет отрисовывается первым, так что отрицательный -100 намеренно ставит инъекции поддержки последними.

Вид слотаСоглашение
строки модального окна редактирования игрока100
строки карточки настроек105
строки карточки профиля90
альтернативные методы аутентификации50
инъекция поддержки-100
всё остальное10 (по умолчанию)

5. Навигация — pano.ui.nav

Добавляйте или редактируйте ссылки в меню сайта (тема) или боковой панели администратора (панель). Тема и панель предоставляют разные помощники.

Тема (только тема):

ВызовНазначение
pano.ui.nav.site.editNavLinks(callback)Синхронный. Получает текущий массив ссылок; либо измените его на месте, либо верните новый массив. Результат пересортировывается хостом по priority каждой ссылки.
pano.ui.nav.site.getNavLinks()Хранилище текущих навигационных ссылок сайта.
pano.ui.nav.profileDropdown.edit(callback) / .get()Редактировать / читать элементы выпадающего меню профиля (это редактирует слот navbar-profile-dropdown из §4).
pano.ui.nav.rightComponents.edit(callback) / .get()Редактировать / читать компоненты правой части навбара (это редактирует слот navbar-right из §4).
pano.ui.nav.onLoad(handler)Подписаться на theme:navbar:load (событие жизненного цикла — смотрите §6).

Форма навигационной ссылки темы{ href, text, icon?, target?, startsWith, loginRequired?, permission?, priority? }, поле за полем:

ПолеОбязательноЗначение
hrefдаКуда указывает ссылка, например /shoutbox.
textдаВидимая надпись. Если строка содержит ., она трактуется как ключ перевода и прогоняется через _ (смотрите §9 / Локализация); иначе показывается как есть.
startsWithдаBoolean, контролирующий подсветку активной ссылки. Когда true, ссылка подсвечивается всякий раз, когда путь текущего URL начинается с её href (так /shoutbox остаётся подсвеченной на /shoutbox/123); когда false, подсвечивает только точное совпадение пути.
iconнетИконка как строка CSS-класса, отрисовываемая на <i>, например 'fa-solid fa-comments'.
targetнетСтандартный target якоря, например '_blank' для новой вкладки.
loginRequiredнетЕсли true, показывать ссылку только вошедшим пользователям.
permissionнетУзел разрешения; показывать ссылку только пользователям, которые его держат.
priorityнетПорядок сортировки среди ссылок (меньшее число идёт первым; ссылки без него сортируются последними).

Панель (только панель):

ВызовНазначение
pano.ui.nav.site.editNavLinks(async handler)Асинхронный, должен вернуть массив. Редактирует ссылки главной боковой панели панели.
pano.ui.nav.server.editNavLinks(async handler)Асинхронный, должен вернуть массив. Редактирует ссылки боковой панели раздела сервера.

Колбэки навигации панели должны вернуть массив

editNavLinks темы принимает изменение на месте, но editNavLinks панели (и server.editNavLinks) — асинхронные и устанавливают список тем, что вы возвращаете — забудьте return, и вы сотрёте меню. Это выглядит так:

js
pano.ui.nav.site.editNavLinks(async (links) => {
  links.push({ href: '/panel/shoutbox', text: 'Shoutbox' });
  return links; // forget this line and the sidebar goes blank
});

6. События жизненного цикла

События времени загрузки, которые хост генерирует, пока подготавливаются данные страницы. Каждый обработчик имеет сигнатуру async (data, event)event — тот же объект события запроса, который получает ваш load() (§2), а data — данные страницы в процессе подготовки. В большинстве обработчиков вы просто читаете data; для помеченных событий ниже вы можете изменять его (смотрите столбец «Заметки о data»). Регистрируйте через помощники-ярлыки ниже или через общий примитив:

ВызовНазначение
pano.ui.lifecycle.on(name, handler)Подписаться на любое событие жизненного цикла по имени (тема + панель).
pano.ui.lifecycle.execute(name, data, event)Только тема. Запустить жизненный цикл самостоятельно — например, ваш плагин отрисовывает собственную страницу входа и хочет, чтобы жизненный цикл входа хоста (и обработчики других дополнений) выполнились на ней. pano.ui.lifecycle панели предоставляет только on.

События жизненного цикла темы

СобытиеЯрлыкЗаметки о data
theme:app:loadpano.ui.app.onLoad(h)
theme:navbar:loadpano.ui.nav.onLoad(h)
theme:profile:loadpano.ui.profile.onLoad(h)
theme:settings:loadpano.ui.settings.onLoad(h)
theme:tickets:loadpano.ui.tickets.onLoad(h)
theme:login:loadpano.ui.auth.login.onLoad(h)data = { error, event } — вы можете установить data.error = '…'; после выполнения вашего обработчика хост читает его и показывает на странице входа
theme:register:loadpano.ui.auth.register.onLoad(h)data = { error, username, event } — вы можете установить data.error и data.username; хост читает их обратно
theme:reset-password:loadpano.ui.auth.resetPassword.onLoad(h)
theme:activate:loadpano.ui.auth.activate.onLoad(h)data = { token }token — код активации из ссылки в письме пользователя (взят из URL)
theme:activate-new-email:loadpano.ui.auth.activateNewEmail.onLoad(h)data = { token } — та же идея, для подтверждения нового email
theme:renew-password:loadpano.ui.auth.renewPassword.onLoad(h)data = { token } — та же идея, для ссылки сброса пароля
theme:post-detail:loadpano.ui.post.onLoad(h)
theme:support:loadpano.ui.support.onLoad(h)
theme:view:<viewId>:loadpano.ui.view.onLoad(viewId, h)срабатывает для каждого слота
theme:sidebar:<id>:loadpano.ui.sidebar.onLoad(id, h)срабатывает для каждой боковой панели

События жизненного цикла панели

СобытиеЯрлыкЗаметки о data
panel:posts:loadpano.ui.posts.onLoad(h)
panel:addon-detail:loadpano.ui.addon.onLoad(h)data = { addon }
panel:player-detail:edit-modal:loadpano.ui.player.onEditLoad(h)data = { player }

7. Поверхности аутентификации (только тема)

Помощники для страниц аутентификации. <page> — это одно из login, register, resetPassword, activate, activateNewEmail, renewPassword.

ВызовНазначение
pano.ui.auth.<page>.content.edit(callback) / .get()Редактировать / читать слот содержимого этой страницы. Это тот же слот, что <page>-content из §4 (например, pano.ui.auth.login.content.edit редактирует слот login-content) — ярлык к нему, а не отдельный механизм.
pano.ui.auth.<page>.onLoad(handler)Подписаться на событие загрузки этой страницы (события theme:<page>:load в §6).
pano.ui.auth.login.alternativeMethods.add(method) / .get()Добавить / читать альтернативный метод входа (например, кнопку социального входа).
pano.ui.auth.register.alternativeMethods.add(method) / .get()То же для регистрации.
pano.ui.auth.login.load(event)Запустить поток загрузки входа (для страницы плагина, которая представляет собственный вход). Возвращает { error, username, event }.
pano.ui.auth.register.load(event)То же для страницы регистрации плагина.
pano.ui.auth.login.form.get()Возвращает собственный компонент тела формы входа темы, чтобы страница плагина могла отрисовать стандартную форму входа внутри собственного макета.
pano.ui.auth.register.form.get()То же для формы регистрации.

resetPassword, activate, activateNewEmail и renewPassword предоставляют только content.edit/content.get и onLoad.

8. Разное

ВызовГдеНазначение
pano.ui.avatar.updateVersion()тема + панельУвеличить «сбрасыватель кэша» аватара — номер версии, добавляемый к URL изображений аватара — чтобы браузеры перезагрузили картинку вместо показа кэшированной (старой). Вызывайте его после того, как пользователь сменил аватар.
pano.ui.avatar.getVersion()тема + панельХранилище текущей строки версии аватара.
pano.ui.view.themes.editMenu(async handler)только панельРедактировать элементы контекстного меню страницы тем. Асинхронный; обработчик получает текущие элементы и должен вернуть массив.
pano.ui.posts.editMenu(async handler)только панельРедактировать элементы контекстного меню постов. Асинхронный; должен вернуть массив.

Для двух вызовов editMenu точные поля элемента меню здесь не перечислены — сделайте console.log массива, который получает ваш обработчик, чтобы увидеть форму каждого элемента, прежде чем добавлять или менять его.

9. Экспорты модулей @panomc/sdk

Это замороженная поверхность импортов @panomc/sdk — каждый спецификатор соответствует стабильному модулю среды выполнения хоста. Импортируйте из этих точных путей и никогда не делайте глубокий импорт внутри этих пакетов (например, @panomc/sdk/utils/api/something не разрешится). (Собственные спецификаторы Svelte и любой npm-пакет, который вы упаковываете, также разрешаются — смотрите §10 для полной картины импортов.)

СпецификаторЭкспорты
@panomc/sdkPanoPlugin, viewComponent, getPanoContext
@panomc/sdk/utils/apiApiUtil (по умолчанию), NETWORK_ERROR, networkErrorBody, buildQueryParams
@panomc/sdk/utils/authhasPermission(permission, user)
@panomc/sdk/utils/tooltiptooltip (также по умолчанию)
@panomc/sdk/utils/textcopy
@panomc/sdk/utils/language_, languageLoading, currentLanguage, Languages, init, getAcceptedLanguage, loadLanguage, changeLanguage, getLanguageByLocale
@panomc/sdk/utils/componentviewComponent
@panomc/sdk/toastsshowToast, limitTitle
@panomc/sdk/components/themePlayerHead, NoContent, Date, Toast, PageTitle, PageActions, Pagination
@panomc/sdk/components/panelNoContent, Editor, DragAndDropZone, Date, Toast, PageLoading, PageActions, PageLoader, PageNavItem, PageNav, Pagination, CardFilters, CardFiltersItem, CardHeader, SearchInput
@panomc/sdk/variablesAPI_URL, UI_URL, PANEL_URL, SETUP_URL, PANO_WEBSITE_URL, PANO_WEBSITE_API_URL, PRERELEASE, COOKIE_PREFIX, CSRF_TOKEN_COOKIE_NAME, JWT_COOKIE_NAME, CSRF_HEADER, updateApiUrl, updatePanoWebsiteUrl, updatePanoWebsiteApiUrl
@panomc/sdk/sveltepage, base, navigating, browser, goto, invalidate, invalidateAll, error, redirect
@panomc/sdk/internalsetPanoContext, getPanoContext

viewComponent появляется дважды — в @panomc/sdk и @panomc/sdk/utils/component. Это та же функция; импортируйте из любого (@panomc/sdk — обычный путь).

Экспорты, чьи имена не самоочевидны:

  • _ — функция/хранилище перевода: $_('some.key') в компоненте. Смотрите Локализацию.
  • languageLoading, currentLanguage — хранилища для текущего состояния i18n.
  • tooltip — действие Svelte: присоедините его через use:tooltip на элементе.
  • copy — копирует строку в буфер обмена.
  • hasPermission(permission, user) — возвращает true, если user держит permission; используйте, чтобы показывать/скрывать UI (смотрите §4).
  • showToast / limitTitle — всплывающее уведомление-тост / обрезка длинной строки заголовка для него (смотрите сигнатуру showToast ниже).
  • NoContent — компонент-заглушка «пустого состояния».
  • PageLoading / PageLoader — UI состояния загрузки (два варианта; вставьте каждый, чтобы увидеть, какой подходит вашему случаю).
  • PlayerHead — отрисовывает изображение головы игрока Minecraft.

Какие @panomc/sdk/variables вам реально нужны

Повседневное использование: API_URL, UI_URL, PANEL_URLPRERELEASE для определения предрелизной сборки). Остальное — CSRF_TOKEN_COOKIE_NAME, JWT_COOKIE_NAME, CSRF_HEADER, COOKIE_PREFIX, сеттеры update*Url — это сантехника аутентификации/безопасности, которую ApiUtil уже обрабатывает за вас; вы редко их трогаете.

@panomc/sdk/svelte = собственные API SvelteKit

Они зеркалят экспорты SvelteKit — page, navigating, browser (из $app/state / $app/environment), base (из $app/paths), goto, invalidate, invalidateAll (из $app/navigation) и error, redirect (из @sveltejs/kit). Смотрите документацию SvelteKit о том, как каждый ведёт себя. Импортируйте их из @panomc/sdk/svelte, а не из $app/..., чтобы они разрешались в среду выполнения хоста.

Нет Button, Card или Input

@panomc/sdk/components/panel и .../theme экспортируют ровно перечисленные выше компоненты. Нет универсальных Button/Card/Input — некоторые ранние примеры ссылались на компоненты, которых никогда не существовало; список выше — авторитетный. Стройте простые контролы обычной разметкой или переиспользуйте перечисленные компоненты.

Сигнатуры методов ApiUtil (все async, все принимают единственный объект опций):

МетодОпции
ApiUtil.get(...){ path, request, csrfToken, token, blob, handler }
ApiUtil.post(...){ path, request, body, headers, csrfToken, token, blob, handler, onUploadProgress }
ApiUtil.put(...){ path, request, body, headers, csrfToken, token, blob, handler, onUploadProgress }
ApiUtil.delete(...){ path, request, headers, csrfToken, token, blob, handler }
ApiUtil.customRequest(...){ path, data, request, csrfToken, token, blob, handler, onUploadProgress }

Что означает каждая опция (не каждая опция появляется в каждом методе — используйте таблицу сигнатур выше):

ОпцияЗначение
pathПуть API, относительно /api — передайте 'shoutbox/list', и утилита вызовет /api/shoutbox/list.
requestАргумент load(event). Передавайте его всякий раз, когда вызываете из load(), чтобы у запроса был CSRF-токен и он работал во время SSR (смотрите заметку под примером).
bodyПолезная нагрузка запроса (объект, отправляемый как JSON; или FormData для загрузки файлов). Только POST/PUT.
headersДополнительные заголовки запроса. POST/PUT/DELETE.
csrfTokenCSRF-токен. Обычно вы его опускаете — утилита читает его из сессии через request.
tokenBearer-токен; когда задан, отправляется как Authorization: Bearer <token>. Опускайте для обычных вызовов вошедшего пользователя — cookies обрабатывают аутентификацию.
blobУстановите true, когда ответ — файл/бинарник, чтобы он читался как Blob, а не парсился как JSON.
handlerНеобязательный колбэк (data, reject) => data, который постобрабатывает разобранный ответ перед возвратом вам.
onUploadProgressКолбэк прогресса загрузки (POST/PUT/customRequest) — используйте его для управления полосой прогресса.
data(только customRequest) сырые опции fetch — method, body, headers. Помощники get/post/и т. д. строят это за вас.

Минимальный GET во время загрузки страницы — обратите внимание на path (относительно /api) и request: event на месте:

js
import ApiUtil from '@panomc/sdk/utils/api';

export async function load(event) {
  // pass request: event so the call works during SSR (the first page view)
  const response = await ApiUtil.get({ path: 'your-endpoint', request: event });
  return { response }; // this object becomes your page component's props
}

Всегда передавайте request: event, когда вызываете их внутри load(). Если забудете, вызов не сможет подхватить CSRF-токен или переиспользовать fetch сервера во время SSR — он может упасть или незаметно перезапуститься в браузере после загрузки страницы. Последний случай — самый запутанный: выглядит так, будто работает, когда вы кликаете по сайту, но ломается при свежей загрузке страницы или жёстком обновлении.

Сигнатура showToast: showToast(text, params = {}, toastComponent).

АргументЗначение
textСообщение. Если это ключ перевода (содержит .), оно переводится; иначе показывается как есть.
paramsС тостом по умолчанию они становятся значениями перевода, интерполируемыми в text. С кастомным toastComponent они передаются этому компоненту как его props. Необязательно (по умолчанию {}).
toastComponentНеобязательный кастомный компонент Svelte для отрисовки вместо тоста по умолчанию.

10. Что вы можете импортировать

Только замороженный список разрешается в среду выполнения хоста

Ваша сборка намеренно не упаковывает эти импорты в JS вашего дополнения — она оставляет их внешними, а хост предоставляет их во время выполнения, чтобы каждое дополнение разделяло один экземпляр Svelte. Импортируйте что-либо вне списка, и оно не разрешится во время выполнения.

Разрешённые голые спецификаторы — это ровно:

  • Каждый спецификатор @panomc/sdk из таблицы §9.
  • Svelte: svelte, svelte/store, svelte/transition, svelte/easing, svelte/motion, svelte/animate, svelte/legacy, svelte/events, svelte/attachments, svelte/reactivity, svelte/reactivity/window и svelte-i18n.
  • Фиксированный набор внутренностей Svelte (продвинутое — компилятор вставляет эти импорты в ваш собранный код за вас; вы никогда не пишете их вручную): svelte/internal, svelte/internal/client, svelte/internal/disclose-version, svelte/internal/flags/legacy, svelte/internal/flags/async и svelte/internal/flags/tracing. Это точный список, не шаблон svelte/internal/*; любой другой подпуть svelte/internal/... не разрешается.

Всё остальное — chart.js, svelte-select, любой другой npm-пакет — должно быть упаковано в ваше дополнение вашей сборкой rollup, а не импортировано голым. Вы не делаете для этого ничего особого: сборка rollup из boilerplate уже упаковывает всё, что вы bun add, а затем import обычным образом. (Дополнение market делает именно это, чтобы поставить Chart.js.)

Никогда не добавляйте svelte в ваш package.json

SDK контролирует, с какой версией Svelte все компилируют (его версия зафиксирована), и сборка падает при несовпадении. Вторая копия svelte в вашем package.json незаметно ломает ваши страницы (две копии расходятся во время гидрации). Смотрите Архитектуру.

Известные мёртвые поверхности (не используйте)

Для полноты: два члена существуют на поверхности, но ничего не делают — не стройте на них:

  • pano.debug — булев флаг на объекте pano. Сейчас он жёстко зашит как false, и ни один хост его не устанавливает, так что не используйте его для определения сборки разработки.
  • onContextUpdate() — старый метод boilerplate, который ни один хост не вызывает (смотрите §1).

Куда дальше