Разработка фронтенда
Что даёт вам эта страница: единственную точку входа, общую для фронтенда каждого дополнения — src/main.js — плюс карту к двум тематическим страницам, которые строят сам UI.
У вашего дополнения две половины: бэкенд на Kotlin и фронтенд на Svelte (визуальная часть, которую пользователи видят и по которой кликают). При сборке дополнения скомпилированный бэкенд Kotlin и ваши собранные файлы Svelte архивируются вместе в один .jar-файл — этот целый файл и есть ваше дополнение. Вы не делаете для этого ничего особого; сборка справляется сама.
Pano запускает два отдельных сайта: тему (то, что посетители видят на yoursite.com) и панель (админ-панель на yoursite.com/panel). Ваше одно дополнение добавляет UI на оба. Один файл — src/main.js — точка входа для обоих.
Сквозной пример на этих страницах — Shoutbox: небольшой виджет недавних «выкриков» вверху главной страницы плюс панель, где админы ими управляют. Эта страница связывает main.js; две тематические страницы — UI темы и UI панели — строят сами экраны (смотрите Куда дальше).
Если ИИ-ассистент или старое руководство даёт вам API, которого нет на этих страницах или в Справочнике API фронтенда, значит его не существует. Частые вымышленные или удалённые вызовы перечислены внизу этой страницы.
Новичок в Svelte?
UI дополнений пишутся на Svelte, так же как темы Pano. Если вы никогда его не использовали, интерактивное руководство Svelte охватывает всё, что нужно UI дополнения.
Прежде чем начать
У вас должно быть дополнение, созданное командой bunx @panomc/plugin-kit new (Начало работы). В каркасе нет файла src/main.js (дополнению только с представлениями он не нужен) — вы создадите его на этой странице. Запустите цикл разработки командой bun run dev и держите его запущенным всё время; каждое изменение на этих страницах перезагружается горячо, так что вы можете сразу видеть результат. (Превращение готового дополнения в релиз рассмотрено в Сборка и публикация.)
Представления: один файл на страницу или виджет
Интерфейс плагина на сайте это набор именованных представлений. Представление это один файл .svelte в src/theme/views/. Его имя это имя файла, id: <ns>:<Name> (ns это id плагина без pano-plugin-, то есть shoutbox:ShoutboxWidget). Всё о представлении лежит в этом файле: кода регистрации нет, в main.js ничего добавлять не нужно.
<!-- src/theme/views/ShoutboxPage.svelte -> view "shoutbox:ShoutboxPage", /shoutbox -->
<script module>
import { api } from '@panomc/sdk/plugin-api';
export const view = { path: '/shoutbox' };
export async function load(event) {
const res = await api.get({ path: '/shouts', request: event });
return { shouts: res.items ?? [] };
}
</script>
<script>
let { shouts = [] } = $props();
</script>
{#each shouts as shout}<p>{shout.message}</p>{/each}export const view содержит только литералы (его читают, но не выполняют). Ключи:
| Ключ | Значение |
|---|---|
path (плюс layout, permission, loginRequired) | представление является страницей |
slot, hook, sidebar, priority | представление само монтируется в слот, хук или боковую панель |
block | создано, чтобы тема размещала его через <PluginBlock id="shoutbox:Latest" /> |
home: { label } | предложить эту страницу админу как вариант главной |
widget | дополнительно собрать веб-компонент (<pano-shoutbox-latest>) для любой веб-страницы |
controller | load страницы берётся из этого контроллера |
contract | целочисленная версия, под которую пишется переопределение темы (по умолчанию 1) |
Правила, которые обеспечивает сборка (каждое с файлом, строкой и исправлением): представление импортирует только svelte*, svelte-i18n, @panomc/sdk*, другие представления и помощники; не вызывает setContext и getContext; нет файлов с одинаковыми именами; нет id слота вне вашего пространства имён.
Стили. Представления плагина по умолчанию рисуются в стиле vanilla, и vanilla не переопределяет ни одно из них, поэтому внешний вид живёт в ваших представлениях. Задавайте его через семантические классы (shoutbox-latest__title) и при необходимости блоком <style>. bunx pano-plugin check --styles badge завершается ошибкой (правило style-block-scope), если селектор не начинается с класса shoutbox-..., есть :global, либо keyframes и пользовательские свойства названы иначе. Значения темы читайте через var(--pano-*). Другие темы перерисовывают ваши представления по-своему.
Только SDK 2. Плагин, собранный без panoSdk 2, пропускается. Элемент с component без view отбрасывается с ошибкой; используйте export const view в файле или view: 'shoutbox:Name'.
Помощники. Любой .js рядом с представлениями импортируется как обычно, поставляется читаемым исходником и уходит с представлением, когда тема его перерисовывает.
Контроллеры. Логика, не являющаяся разметкой (корзина, форматтер), пишется в src/theme/controllers/<name>.js как независимый от фреймворка defineController({ name, version, state, actions }) из @panomc/plugin-kit/controller. Он поставляется скомпилированным и закрытым. Представления, страницы тем, виджеты и не-Svelte фронтенды используют один и тот же: plugin('shoutbox').require('counter') из @panomc/sdk/controllers. Удаляя или переименовывая ключ state или action, поднимите version: иначе сборка завершится ошибкой.
Примеры данных. bunx pano-plugin samples ShoutboxPage пишет ShoutboxPage.samples.js (пустое, загрузка, заполненное, ошибка). Каталог тем /__pano/views отрисовывает их, а bunx pano-plugin check использует.
Виджеты. Добавьте widget: true в view, и сборка дополнительно создаст веб-компонент. Встраивается так: <script type="module" src="https://your-pano/api/v1/widgets/loader.js"></script> и <pano-shoutbox-latest></pano-shoutbox-latest>. Одно и то же представление работает в теме и на любом другом сайте; данные по-прежнему приходят из его load.
Контракт. Тема может перерисовать ваше представление (theme-core eject-view shoutbox:ShoutboxPage). Сборка пишет pano-plugin.lock.json (закоммитьте его) и падает со строкой для исправления, если вы убрали проп, не подняв contract.
Всё, что не является представлением (страницы панели, хуки панели, обработчики жизненного цикла), по-прежнему регистрируется в src/main.js, как описано ниже.
Файлы вашего дополнения
Вот раскладка, на которую ссылаются эти страницы. Папки theme/ и panel/ — просто аккуратное соглашение для разделения компонентов посетителей и компонентов админов — Pano не принуждает к именам, вы могли бы организовать файлы как угодно.
src/
├─ main.js ← entry point; Pano loads this first
├─ theme/ ← components shown to visitors (the public site)
│ └─ ShoutboxWidget.svelte
└─ panel/ ← components shown to admins (the dashboard)
├─ ShoutboxSettings.svelte
└─ ShoutboxPage.svelteТочка входа: src/main.js
Всё начинается в main.js. Он экспортирует по умолчанию класс, расширяющий PanoPlugin. Pano создаёт один экземпляр вашего класса, устанавливает на нём this.pano за вас (вы никогда не устанавливаете его сами), затем вызывает ваш метод onLoad().
Он вызывает onLoad() дважды — один раз в панели и один раз в теме. Тема и панель — две отдельные работающие программы, и каждая загружает ваше дополнение независимо, так что onLoad() действительно выполняется дважды, по одному в каждой. Это нормально, а не баг. Проверка pano.isPanel — это то, как вы даёте каждой стороне разный UI: он true в панели и false в теме.
Вот скелет, на котором строится всё дополнение. Одна строка в нём — export const _ = ... — выглядит пугающе; пока скопируйте её как есть, это помощник перевода, полностью объяснённый в разделе Перевод текста ниже, и вам не нужно понимать его, чтобы использовать скелет.
// src/main.js
import { PanoPlugin, viewComponent } from '@panomc/sdk';
import { api } from '@panomc/sdk/plugin-api';
import { derived } from 'svelte/store';
import { _ as i18n } from '@panomc/sdk/utils/language';
export const pluginId = 'pano-plugin-shoutbox';
// A translate function scoped to your addon's keys — see the i18n section below.
export const _ = derived(i18n, ($t) => (key, options) => $t(`plugins.${pluginId}.${key}`, options));
export default class ShoutboxUiPlugin extends PanoPlugin {
onLoad() {
const { pano } = this;
if (pano.isPanel) {
// panel registrations go here
} else {
// theme registrations go here
}
}
onUnload() {}
}Несколько важных правил, все из которых соблюдают встроенные дополнения (работающие эталонные дополнения в организации PanoMC на GitHub):
- Класс должен быть экспортом
default. Pano ищет ровно один. this.pano— весь ваш API. Pano устанавливает его за вас до выполненияonLoad()— внутриonLoad()вы просто читаетеpano.isPanelи используетеpano.ui.*.pluginIdдолжен точно совпадать с ID вашего дополнения из Разработки бэкенда / вашего манифеста плагина. Переводы и хук страницы деталей панели привязаны к нему, так что несовпадение незаметно их ломает.- Оборачивайте каждый компонент панели в
viewComponent(() => import('./File.svelte')). Это не опционально. Каждый компонент, который вы передаёте вызовуregisterв панели — хуки, страницы, слоты представлений — должен быть обёрнут так. (На стороне сайта оборачивать нечего: регистрация называет представление,view: 'shoutbox:Name'.) Простыми словами: это передаёт Pano рецепт загрузки вашего файла вместо самого файла, чтобы Pano мог загрузить его в нужный момент с собственной копией Svelte страницы. Детали вам не нужны — смотрите Архитектуру, если любопытно. - Держите общее состояние в
main.js. Переменные, которые вы объявляете на верхнем уровнеmain.js(как хранилище_выше), существуют ровно один раз и разделяются всеми вашими компонентами. Сборка гарантирует это только дляmain.js, так что не полагайтесь на другие файлы для общего состояния. (Механика в Архитектуре.) onUnload()выполняется при отключении вашего дополнения. Пока оставьте его пустым; большинству дополнений он никогда не нужен.
Никогда не объявляйте svelte в package.json
Ваш бандл не поставляет Svelte, svelte-i18n или @panomc/sdk — хост предоставляет их, чтобы вся страница разделяла один экземпляр Svelte. Если ваша сборка начинает падать сразу после того, как вы (или библиотека, установленная через bun add) добавили svelte в package.json, вот причина: удалите svelte из package.json. Простыми словами, страница может выполнять только одну копию Svelte, и хост её уже предоставляет; добавление своей фиксирует вторую копию и ломает гидрацию (шаг, где отрисованный на сервере HTML подключается, чтобы стать интерактивным в браузере). Смотрите Архитектуру почему.
Проверьте прогресс
Этот скелет сам по себе пока ничего не рисует на экране — он просто настраивает ваше дополнение. С запущенным bun run dev перезагрузите и yoursite.com (тему), и yoursite.com/panel (панель) и откройте консоль браузера (F12). Вы должны увидеть никаких красных ошибок от вашего дополнения. Если увидите ошибку об отсутствующем экспорте default или плохом pluginId, исправьте это, прежде чем идти дальше — всё, что ниже, надстраивается на этом работающем скелете.
Теперь заполните две ветки. То, что идёт в каждую, имеет свою страницу: UI темы для ветки else (тема), UI панели для ветки if (pano.isPanel). Остальная часть этой страницы охватывает две вещи, которые используют обе ветки.
Перевод текста в ваших компонентах
В отличие от разработки темы, никакой помощник перевода не внедряется в компоненты вашего дополнения автоматически — компоненты панели импортируют _ из вашего собственного main.js. (Представление сайта не может импортировать main.js: сборка отклоняет файл вне src/theme/; там возьмите то же хранилище через const _ = plugin('shoutbox')._; из @panomc/sdk/controllers.) Этот _ — пугающая строка из скелета:
export const _ = derived(i18n, ($t) => (key, options) => $t(`plugins.${pluginId}.${key}`, options));Вот всё, что она делает: это Svelte derived хранилище (значение, которое пересчитывается при изменении его источника), оборачивающее функцию перевода Pano, и оно автоматически добавляет к каждому переданному ключу префикс plugins.<pluginId>.. Так что в компоненте вы пишете $_('widget.title'), и оно ищет plugins.pano-plugin-shoutbox.widget.title. Вам не нужно понимать, как оно устроено внутри, чтобы использовать его — просто импортируйте и читайте с префиксом $:
<script>
import { _ } from '../main.js';
</script>
<h2>{$_('widget.title')}</h2>{$_('widget.title')} разрешается в ключ plugins.pano-plugin-shoutbox.widget.title в ваших файлах локализации. Смотрите Локализацию, где эти ключи живут и как они разбиты по пространствам имён.
Проверьте прогресс
Пока вы реально не добавите этот ключ в Локализации, экран показывает сырую строку ключа (plugins.pano-plugin-shoutbox.widget.title) вместо приятной надписи. На этом этапе так и должно быть — это ваш знак, что помощник подключён правильно и просто ждёт файл локализации.
Старые и выдуманные ИИ API, которых не существует
Если ИИ-инструмент, старое руководство или каркас предлагают что-либо из этого, игнорируйте — ничего из этого не существует. Используйте только то, что есть на этих страницах и в Справочнике API фронтенда.
pano.ui.page.register({ name, view, scopes })— реальныйpage.registerпринимает{ path, component, permission, ... }в панели (смотрите UI панели) и{ path, view: 'shoutbox:Name', ... }на сайте. Формыname/scopesне существует, аview— это id представления, а не объект.import { Button, Card } from '@panomc/sdk/components/panel'— в SDK нет такой библиотеки компонентов.onContextUpdate— более старый boilerplate определяет этот метод, но ни один хост его не вызывает. Если ваш каркасныйmain.jsсодержитonContextUpdate, удалите его.api.get('/shouts')с обычной строкой или путь, начинающийся с/api, — каждый вызов принимает объект опций с относительным путём, напримерapi.get({ path: '/shouts' }).pano.utils.toast— такого нет; тосты приходят только из@panomc/sdk/toasts.
Куда дальше
main.js настроен. Теперь стройте UI:
- Показать что-то на сайте → UI темы — хуки темы, виджет главной страницы, данные с отрисовкой на сервере и вызов вашего API.
- Добавить админские экраны → UI панели — разделы настроек, полноценные страницы панели со ссылками навигации и тосты.
- Полный справочник → Справочник API фронтенда — каждое имя хука, слот представления, событие жизненного цикла и экспорт
@panomc/sdkв одном месте. - Локализация — где живут ваши строки
plugins.<pluginId>.<key>и как панель позволяет админам их переопределять. - Разработка бэкенда — Kotlin-сторона эндпоинтов и разрешений, которые вызывают эти страницы.
- Сборка и публикация — превратите готовое дополнение в релизный jar. Релизная сборка обязана включать UI, так что никогда не используйте для неё
-Pnoui(-Pnoui— флаг Gradle, который пропускает сборку UI при итерации только над бэкендом — смотрите «Сборка и публикация»). - Эталонные дополнения — встроенные дополнения в организации PanoMC на GitHub — работающий эталон для каждого паттерна на этих страницах.