Skip to content

Frontend API Reference

This page lists every hook name, view slot, lifecycle event, navigation helper, and @panomc/sdk export your addon's UI can use — in one place. It is a lookup page, not a tutorial.

New here? The four things this page is a catalogue of, in plain words:

  • A hook = a named spot in a host page where your component gets rendered.
  • A view slot = like a hook, but the items are ordered (by a priority number) and can be hidden or shown individually.
  • A lifecycle event = a moment during page load that you can run code at.
  • A navigation helper = an API for adding or editing links in the site menu or the admin sidebar.

How to read this page

It is a set of dense reference tables, grouped by API area (§1–§10). You don't read it top to bottom — you jump to the section for the thing you're wiring up. Each section opens with one plain sentence saying what it's for and where it works (theme, panel, or both). If a term in a table cell is unfamiliar, it's almost certainly defined in the "Concepts this page assumes" box just below — read that box once first.

Concepts this page assumes (read once, ~60 seconds)

This page reuses a handful of words dozens of times each. Here is each in one plain sentence:

  • The host — the running Pano frontend that loads and runs your addon's JavaScript. It is one of two apps: the theme or the panel (next bullet). "Host" here never means a server you rent.
  • Theme vs panel — the two frontends. The theme is the public site players see, at /. The panel is the admin dashboard, at /panel. Your addon can run in both, and each exposes different APIs. pano.isPanel tells your code which one it is in.
  • Svelte store — a value you can subscribe to. In a .svelte component, put $ in front of it to read it reactively ($myStore). In plain JS, call store.subscribe(fn) and keep the function it returns so you can stop listening later (push that function into this._unsubscribers, see §1). Several APIs below "return a store". (Svelte docs: stores)
  • load(event) — an optional function a page or hook can export. The host calls it while preparing the page. event describes the request (its URL, params, cookies). See the "three kinds of load()" box below.
  • props — the values a Svelte component receives from whatever rendered it. Whatever object your load() returns becomes your component's props.
  • SSR / hydration — SSR ("server-side rendering") means the first time a visitor opens a page, its HTML is built on the server; later navigations are built in the browser. Hydration is Svelte wiring up that server-built HTML in the browser so buttons, etc. become interactive.
  • bare specifier / external — a bare specifier is an import path with no ./ or /, e.g. import { x } from 'svelte'. Your build leaves the allowed ones external — it does not copy them into your addon's JS; the host supplies them at runtime. See §10.
  • permission node — a permission string like x.y.z that decides who is allowed to see or do something. For the list of nodes and how addons define their own, see the Backend API Reference.

The three kinds of load() on this page

The word load shows up in three different roles. They are not interchangeable — mixing them up is a classic first-week bug:

WhereYou writeIt receivesWhat its return means
Page (§2)export function load(event) in a page moduleevent (the request)the page component's props (four special keys like pageTitle are pulled out for the host)
Hook (§3)export function load(event) in a hook moduleevent (the request)the hook component's props; return { hookOptions: { invisible: true } } to hide the hook
Lifecycle handler (§4, §6, §7)(data, event) => { … } passed to an onLoad(...) / lifecycle.on(...) calldata (the page's data, sometimes mutable) and eventusually nothing — you read, or for marked events tweak, data in place

If you want to see these APIs used in a real addon, start with Frontend Development, which builds the Shoutbox UI step by step. Everything here is the surface that page draws from.

For the Kotlin half of your addon, this page has a sibling: the Backend API Reference does the same job for the backend surface — the plugin lifecycle, database, endpoints, permissions, and events.

Where these come from

You write import { X } from 'svelte', but your build never bundles Svelte into your addon's output — it leaves that import external, and the running Pano site (the host) supplies the one shared copy at runtime. That's why only certain imports are allowed (see §10). The pano.* tree documented below is injected into your plugin as this.pano; the @panomc/sdk modules are the frozen import list at the end of this page. (Full mechanism: Architecture.)

0. The pano object

Everything is reached through the pano object the host injects into your plugin as this.pano. One flag lives at the top; the rest hangs off pano.ui.*.

PropertyTypeWhat it is
pano.isPanelbooleantrue when your code is running inside the admin panel, false inside the theme. Use it to run different registrations in each — i.e. if (this.pano.isPanel) { /* panel registrations */ } else { /* theme registrations */ }.

The panel and the theme expose different pano.ui trees — a helper that exists in one may not exist in the other. So throughout this page each section says where its members live: theme + panel, theme only, or panel only. When a whole section is one-sided (like §4, theme only), its heading and a box at the top say so.

1. Plugin entry contract

Your src/main.js default-exports a class that extends PanoPlugin (from @panomc/sdk). The host creates one instance of it, injects this.pano, and calls the lifecycle methods below.

The smallest possible entry file is just this:

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

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

Every table further down is easier to read once you picture that skeleton.

MemberKindPurpose
onLoad()method (override)Called once after the plugin loads. Do all your registrations here (the register(...) calls from sections 2–5 below). this.pano is available. Don't touch this.pano in the constructor — it is only injected just before onLoad() runs.
onUnload()method (override)Called when the plugin is torn down. Undo anything that must not linger (e.g. pano.ui.page.unregister(...)).
this.panopropertyThe injected API object documented on this page.
this.contextpropertyA plain object where your plugin keeps state it wants to share with its components (e.g. settings you fetched).
this.setContext(partial)methodCopies the keys of partial into this.context (like Object.assign — a shallow merge, one level deep) and notifies anything subscribed to the context that it changed.
this._unsubscribersarrayPush store-unsubscribe functions here (see the store bullet in the Concepts box); the host runs them all when the plugin is destroyed, so your subscriptions don't leak.

Two functions come from @panomc/sdk alongside the base class:

ExportPurpose
viewComponent(importer)Rule: always write viewComponent(() => import('./X.svelte')) when you hand a component to any register call — never the bare () => import(...) and never the component itself. (It attaches the correct shared-runtime mount/hydrate/unmount for the host; that plumbing is why the wrapper is mandatory.)
getPanoContext()Returns the current Pano host context. You can ignore this unless you need the raw host context from code that isn't a PanoPlugin method (a plain method has this.context instead) — rarely needed.

onContextUpdate is not called

If the class you scaffolded from the boilerplate contains an onContextUpdate() method, delete it — no host ever calls it. It is dead code; do not build behavior on it. Use onLoad() for setup, and store subscriptions (see the Concepts box) for reacting to changes.

2. Pages — pano.ui.page (theme + panel)

Use these to register whole pages of your own — under the theme (/…) or the panel (/panel/…).

CallPurpose
pano.ui.page.register(options)Register a page at a path.
pano.ui.page.unregister(path)Remove a page you registered (used for cleanup — see Frontend Development).
pano.ui.page.isPluginPage(path)true if some plugin has registered that path.

register(options):

OptionTypeMeaning
pathstringThe route (see path forms below).
componentviewComponent(...)The page component.
systemLayoutstringWrap your page in one of the host's built-in layouts (names listed below). Use MainLayout for a normal page; the other names match specific host sections (e.g. ProfileLayout, SettingsLayout).
layoutviewComponent(...)Use your own layout component instead of a built-in one.
resetLayoutbooleanRender with no host header, sidebar, or footer — your component gets the whole page. ("Chrome" is the general word for that surrounding host UI.)
permissionstringPermission node (a permission string like x.y.z — see the Backend API Reference for the list) required to view. If the current user lacks it, the page renders 404.

Path forms:

FormExampleMatches
literal/shoutboxexactly that path
dynamic segment/shout/[id] or /shout/:idone segment, captured as a param — read it in your load(event) as event.params.id
catch-all/docs/[...rest]the remaining segments (must be the final segment)
regexre:/shout/\d+the pattern must match the whole path, not just part of it — the host wraps it in ^…$ ("fully anchored") for you

A page module may also export load(event) (see the "three kinds of load()" box near the top). event describes the request — its URL, params, and cookies. Whatever object you return is handed to your page component as its props. Four special keys are removed from that returned object and used by the host chrome instead of being passed as props:

  • pageTitle — the title shown for the page (e.g. the browser tab).
  • breadcrumbs — the breadcrumb trail the host renders above the page.
  • sidebar — a sidebar for this page.
  • sidebarProps — the props handed to that sidebar.

systemLayout names — theme: AppLayout, AuthLayout, MainLayout, ProfileLayout, ThemeSettingsLayout, TicketsLayout.

systemLayout names — panel: AddonDetailLayout, AddonsLayout, AppLayout, MainLayout, MigrationLayout, PermissionsLayout, PlayerDetailLayout, PlayersLayout, PostsLayout, ServerLayout, ServerSettingsLayout, SettingsLayout, TicketsLayout, TranslationsLayout, ViewLayout.

Checkpoint — did my page register?

After a pano.ui.page.register({ path: '/your-path', component }) call, rebuild your addon and reload the site. Visiting /your-path should now show your component. Nothing there? Check that component is wrapped in viewComponent(() => import('./X.svelte')) and that register ran inside onLoad().

3. Hooks — pano.ui.hook (theme + panel)

A hook is a named hole in a host page: anything you register under that hook's name gets rendered at that fixed spot. A hook is a flat list of components under one name. (View slots in §4 add ordering and hiding on top of this same idea.)

CallWherePurpose
pano.ui.hook.register(options)theme + panelMount a component into a named hook.
pano.ui.hook.get(name)theme + panelReturns a store (see the Concepts box) of the components registered for name.
pano.ui.hook.setVisible(name, component, visible)theme onlyToggle a hook entry's visibility. Pass the same component reference you gave to register.

register(options):

OptionTypeMeaning
namestringThe hook name (tables below).
componentviewComponent(...)The component to mount.
permissionstringOnly render for users holding this permission node.
skipLoadbooleanDo not run the component's load() during page load. Use this when your hook fetches its own data on mount, so a page-load run would be wasted or would fail.
invisiblebooleanRegister but start hidden.

The hook load() / hookProps contract: a hook component's module may export load(event). The host runs it during page load — on the server the first time a page is opened (SSR), and in the browser when navigating between pages — and passes the result to your component as its props (that combined props object is the component's hookProps). A component can hide itself by returning { hookOptions: { invisible: true } } from its load().

Reading a hook name

A hook name tells you roughly where it renders. The prefix is the area: theme: = the public site, page: = the page content area, panel: = the admin panel. The middle names the page or table. The suffix names the spot: :top, :bottom, :content, :sidebar, or a table position like :header:... / :row:.... Not sure exactly where one lands? Register a component that renders a visible marker, rebuild, and look.

The Extra prop column below lists any prop the host passes to your component in addition to your load() result.

Theme hook names

Hook nameExtra prop
theme:top
page:top
page:home:top
theme:post-detail:bottompost
theme:support:content

Panel hook names

Table hooks and the tag prop

The panel hooks whose names contain table:header or table:row render inside the host's <tr> rows. The host passes you tag (either 'th' for a header cell or 'td' for a body cell); render <svelte:element this={tag}>…</svelte:element> so your cell is valid table HTML.

Hook nameExtra 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"

The post, playerData, addon, player, and category props above are the same objects the host page already uses for that post / player / addon / category. Their exact fields aren't listed here — console.log the prop to inspect it, or open the matching host page's code.

:<pluginId>-suffixed hooks

panel:plugin-detail:content:<pluginId> renders only on your addon's detail page — substitute your own pluginId (the id your plugin declares — see the Backend API Reference). This is the standard place to put an addon's settings panel.

Checkpoint — did my hook render?

After pano.ui.hook.register({ name: 'theme:top', component }), rebuild and reload a themed page. Your component should appear at that hook's spot. If not, confirm you used a real hook name from the tables above and wrapped the component in viewComponent(...).

4. View slots — pano.ui.view (theme only)

A view slot is a named container that renders a priority-ordered list of plugin components (extra login methods, extra profile rows, and so on). Like a hook, but each slot item carries an id and a priority, so items can be individually hidden, reordered, or replaced.

Hooks vs view slots at a glance:

Hook (§3)View slot (§4)
Orderingnone (a flat list)by priority (higher renders first)
Per-item idnoyes (id) — lets you hide/move/replace one item
Where it workstheme + paneltheme only

pano.ui.view / pano.ui.sidebar exist only in the theme

  • Building for the panel? Skip this whole section — the panel does not expose view.register/hide/show/move/get/onLoad/load or pano.ui.sidebar at all.
  • Panel exception 1: extra rows in the player edit modal have their own dedicated API — see "Panel: player edit-modal rows" below.
  • Panel exception 2: the panel's only pano.ui.view member is pano.ui.view.themes.editMenu — see §8.
CallPurpose
pano.ui.view.register({ viewId, id, component, priority })Add a component to slot viewId. priority defaults to 10; re-registering the same id replaces it.
pano.ui.view.hide(viewId, id)Hide an item without removing it.
pano.ui.view.show(viewId, id)Un-hide it.
pano.ui.view.move(viewId, id, priority)Change an item's priority.
pano.ui.view.get(viewId)Returns a store (see the Concepts box) of the visible, ordered items.
pano.ui.view.onLoad(viewId, handler)Run your handler each time this slot's data is loaded. (Under the hood this is the lifecycle event theme:view:<viewId>:load — see §6.)
pano.ui.view.load(viewId, event)Run the slot's load pipeline and get the resolved items (for a plugin page that itself hosts a slot).

pano.ui.sidebar.* is an alias of the same registry with the same methods, except the container key is sidebarId instead of viewId (and onLoad fires theme:sidebar:<id>:load).

Slot item shape: { id, component, priority, props? }. Higher priority renders first. There is no per-item permission field — slot items are not permission-filtered. To restrict one, check the permission inside your componentimport { hasPermission } from '@panomc/sdk/utils/auth' — and render nothing if it fails. (Hooks and nav links do support a permission option.)

Theme slot IDs

Slot IDWhere it renders
login-contentlogin page body
login-alt-methodsalternative login methods
register-contentregister page body
register-alt-methodsalternative register methods
profile-contentprofile page body
profile-card-rowsrows on the profile card
settings-contentsettings page body
settings-card-rowsrows on the settings card
tickets-contentsupport tickets page body
navbar-rightright side of the navbar
navbar-profile-dropdownprofile dropdown menu
support-contentsupport page body
support-optionssupport page options list
reset-password-contentreset-password page body
renew-password-contentrenew-password page body
activate-contentaccount-activation page body
activate-new-email-contentnew-email-activation page body

Panel: player edit-modal rows

The panel has no pano.ui.view slot registry. Its one extension point of this kind — extra rows in the player edit modal — has its own dedicated API instead:

CallPurpose
pano.ui.player.editModal.cardRows.edit(callback)Edit the list of card rows shown in the player edit modal. callback receives the current rows array; mutate it in place and return it.
pano.ui.player.editModal.cardRows.get()Read the current card rows.

The row objects mirror the modal's existing rows; their exact fields aren't listed here — call cardRows.get() (or console.log the array your edit callback receives) to inspect them. Avatar- and social-login-style addons use this to add a row to that modal.

Priority conventions

Match these numbers so your items land in a sensible order relative to other installed addons. Reminder: higher priority renders first, so the negative -100 deliberately puts support injections last.

Slot kindConvention
player-edit-modal rows100
settings card rows105
profile card rows90
alternative auth methods50
support injection-100
everything else10 (default)

5. Navigation — pano.ui.nav

Add or edit links in the site menu (theme) or the admin sidebar (panel). The theme and the panel expose different helpers.

Theme (theme only):

CallPurpose
pano.ui.nav.site.editNavLinks(callback)Synchronous. Receives the current links array; either mutate it in place or return a new array. The result is re-sorted by the host by each link's priority.
pano.ui.nav.site.getNavLinks()Store of the current site nav links.
pano.ui.nav.profileDropdown.edit(callback) / .get()Edit / read the profile-dropdown items (this edits the navbar-profile-dropdown slot from §4).
pano.ui.nav.rightComponents.edit(callback) / .get()Edit / read the navbar-right components (this edits the navbar-right slot from §4).
pano.ui.nav.onLoad(handler)Subscribe to theme:navbar:load (a lifecycle event — see §6).

Theme nav-link shape{ href, text, icon?, target?, startsWith, loginRequired?, permission?, priority? }, field by field:

FieldRequiredMeaning
hrefyesWhere the link points, e.g. /shoutbox.
textyesThe visible label. If the string contains a . it is treated as a translation key and run through _ (see §9 / Localization); otherwise it is shown as-is.
startsWithyesA boolean that controls active-link highlighting. When true, the link is highlighted whenever the current URL path starts with its href (so /shoutbox stays highlighted on /shoutbox/123); when false, only an exact path match highlights it.
iconnoAn icon CSS class string rendered on an <i>, e.g. 'fa-solid fa-comments'.
targetnoStandard anchor target, e.g. '_blank' for a new tab.
loginRequirednoIf true, only show the link to logged-in users.
permissionnoPermission node; only show the link to users who hold it.
prioritynoSort order among links (lower number runs first; links without one sort last).

Panel (panel only):

CallPurpose
pano.ui.nav.site.editNavLinks(async handler)Async, must return the array. Edits the panel's main sidebar links.
pano.ui.nav.server.editNavLinks(async handler)Async, must return the array. Edits the server-section sidebar links.

Panel nav callbacks must return the array

The theme's editNavLinks accepts an in-place mutation, but the panel's editNavLinks (and server.editNavLinks) are async and set the list to whatever you return — forget the return and you wipe the menu. It looks like this:

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. Lifecycle events

Load-time events the host fires while a page's data is being prepared. Every handler has the signature async (data, event)event is the same request-event object your load() receives (§2), and data is the page's in-progress data. In most handlers you just read data; for the marked events below you may modify it (see the Data notes column). Register with the shortcut helpers below, or with the generic primitive:

CallPurpose
pano.ui.lifecycle.on(name, handler)Subscribe to any lifecycle event by name (theme + panel).
pano.ui.lifecycle.execute(name, data, event)Theme only. Run a lifecycle yourself — e.g. your plugin renders its own login page and wants the host's login lifecycle (and other addons' handlers) to run on it. The panel's pano.ui.lifecycle exposes only on.

Theme lifecycle events

EventShortcutData notes
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 } — you may set data.error = '…'; after your handler runs the host reads it and shows it on the login page
theme:register:loadpano.ui.auth.register.onLoad(h)data = { error, username, event } — you may set data.error and data.username; the host reads them back
theme:reset-password:loadpano.ui.auth.resetPassword.onLoad(h)
theme:activate:loadpano.ui.auth.activate.onLoad(h)data = { token }token is the activation code from the link in the user's email (taken from the URL)
theme:activate-new-email:loadpano.ui.auth.activateNewEmail.onLoad(h)data = { token } — same idea, for confirming a new email
theme:renew-password:loadpano.ui.auth.renewPassword.onLoad(h)data = { token } — same idea, for a password-reset link
theme:post-detail:loadpano.ui.post.onLoad(h)
theme:support:loadpano.ui.support.onLoad(h)
theme:view:<viewId>:loadpano.ui.view.onLoad(viewId, h)fired per slot
theme:sidebar:<id>:loadpano.ui.sidebar.onLoad(id, h)fired per sidebar

Panel lifecycle events

EventShortcutData notes
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. Auth surfaces (theme only)

Helpers for the auth pages. <page> is one of login, register, resetPassword, activate, activateNewEmail, renewPassword.

CallPurpose
pano.ui.auth.<page>.content.edit(callback) / .get()Edit / read that page's content slot. This is the same slot as §4's <page>-content (e.g. pano.ui.auth.login.content.edit edits the login-content slot) — a shortcut to it, not a separate mechanism.
pano.ui.auth.<page>.onLoad(handler)Subscribe to that page's load event (the theme:<page>:load events in §6).
pano.ui.auth.login.alternativeMethods.add(method) / .get()Add / read an alternative login method (e.g. a social-login button).
pano.ui.auth.register.alternativeMethods.add(method) / .get()Same for register.
pano.ui.auth.login.load(event)Run the login load flow (for a plugin page that presents its own login). Returns { error, username, event }.
pano.ui.auth.register.load(event)Same for a plugin registration page.
pano.ui.auth.login.form.get()Returns the theme's own login-form body component, so a plugin page can render the standard login form inside its own layout.
pano.ui.auth.register.form.get()Same, for the register form.

resetPassword, activate, activateNewEmail, and renewPassword expose only content.edit/content.get and onLoad.

8. Miscellaneous

CallWherePurpose
pano.ui.avatar.updateVersion()theme + panelIncrement the avatar "cache-buster" — a version number appended to avatar image URLs — so browsers re-download the picture instead of showing the cached (old) one. Call it after a user changes their avatar.
pano.ui.avatar.getVersion()theme + panelStore of the current avatar version string.
pano.ui.view.themes.editMenu(async handler)panel onlyEdit the themes-page context-menu items. Async; the handler receives the current items and must return the array.
pano.ui.posts.editMenu(async handler)panel onlyEdit the posts context-menu items. Async; must return the array.

For the two editMenu calls, the exact fields of a menu item aren't listed here — console.log the array your handler receives to see each item's shape before you add or change one.

9. @panomc/sdk module exports

This is the frozen @panomc/sdk import surface — each specifier maps to a stable host runtime module. Import from these exact paths, and never deep-import inside these packages (e.g. @panomc/sdk/utils/api/something will not resolve). (Svelte's own specifiers and any npm package you bundle also resolve — see §10 for the complete import picture.)

SpecifierExports
@panomc/sdkPanoPlugin, viewComponent, getPanoContext
@panomc/sdk/utils/apiApiUtil (default), NETWORK_ERROR, networkErrorBody, buildQueryParams
@panomc/sdk/utils/authhasPermission(permission, user)
@panomc/sdk/utils/tooltiptooltip (also default)
@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 appears twice — in @panomc/sdk and @panomc/sdk/utils/component. It's the same function; import from either (@panomc/sdk is the usual path).

The exports whose names aren't self-explanatory:

  • _ — the translation function/store: $_('some.key') in a component. See Localization.
  • languageLoading, currentLanguage — stores for the current i18n state.
  • tooltip — a Svelte action: attach it with use:tooltip on an element.
  • copy — copies a string to the clipboard.
  • hasPermission(permission, user) — returns true if user holds permission; use it to show/hide UI (see §4).
  • showToast / limitTitle — pop up a toast notification / truncate a long title string for one (see the showToast signature below).
  • NoContent — an "empty state" placeholder component.
  • PageLoading / PageLoader — loading-state UI (two variants; drop each in to see which fits your case).
  • PlayerHead — renders a Minecraft player's head image.

Which @panomc/sdk/variables you actually need

Everyday use: API_URL, UI_URL, PANEL_URL (and PRERELEASE to detect a prerelease build). The rest — CSRF_TOKEN_COOKIE_NAME, JWT_COOKIE_NAME, CSRF_HEADER, COOKIE_PREFIX, the update*Url setters — are auth/security plumbing that ApiUtil already handles for you; you rarely touch them.

@panomc/sdk/svelte = SvelteKit's own APIs

These mirror SvelteKit's exports — page, navigating, browser (from $app/state / $app/environment), base (from $app/paths), goto, invalidate, invalidateAll (from $app/navigation), and error, redirect (from @sveltejs/kit). See the SvelteKit docs for how each behaves. Import them from @panomc/sdk/svelte, not from $app/..., so they resolve to the host runtime.

No Button, Card, or Input

@panomc/sdk/components/panel and .../theme export exactly the components listed above. There is no generic Button/Card/Input — some earlier examples referenced components that never existed; the list above is authoritative. Build simple controls with plain markup, or reuse the listed components.

ApiUtil method signatures (all async, all take a single options object):

MethodOptions
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 }

What each option means (not every option appears on every method — use the signature table above):

OptionMeaning
pathThe API path, relative to /api — pass 'shoutbox/list' and the util calls /api/shoutbox/list.
requestThe load(event) argument. Pass it whenever you call from inside a load() so the request has the CSRF token and works during SSR (see the note under the example).
bodyThe request payload (an object, sent as JSON; or a FormData for file uploads). POST/PUT only.
headersExtra request headers. POST/PUT/DELETE.
csrfTokenCSRF token. You normally omit it — the util reads it from the session via request.
tokenA bearer token; when set it is sent as Authorization: Bearer <token>. Omit for normal logged-in calls — cookies handle auth.
blobSet true when the response is a file/binary, so it's read as a Blob instead of parsed as JSON.
handlerOptional (data, reject) => data callback that post-processes the parsed response before it is returned to you.
onUploadProgressUpload-progress callback (POST/PUT/customRequest) — use it to drive a progress bar.
data(customRequest only) the raw fetch options — method, body, headers. The get/post/etc. helpers build this for you.

A minimal GET during page load — note path (relative to /api) and request: event in position:

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
}

Always pass request: event when you call these inside a load(). If you forget, the call can't pick up the CSRF token or reuse the server's fetch during SSR — it may fail, or it may silently re-run in the browser after the page loads. That last case is the confusing one: it looks like it works when you click around the site, but breaks on a fresh page load or a hard refresh.

showToast signature: showToast(text, params = {}, toastComponent).

ArgumentMeaning
textThe message. If it's a translation key (contains a .) it's translated; otherwise shown as-is.
paramsWith the default toast, these become the translation values interpolated into text. With a custom toastComponent, they're passed to that component as its props. Optional (defaults to {}).
toastComponentOptional custom Svelte component to render instead of the default toast.

10. What you may import

Only the frozen list resolves to the host runtime

Your build deliberately does not bundle these imports into your addon's JS — it leaves them external, and the host provides them at runtime so every addon shares one Svelte instance. Import anything outside the list and it will not resolve at runtime.

Allowed bare specifiers are exactly:

  • Every @panomc/sdk specifier in the §9 table.
  • Svelte: svelte, svelte/store, svelte/transition, svelte/easing, svelte/motion, svelte/animate, svelte/legacy, svelte/events, svelte/attachments, svelte/reactivity, svelte/reactivity/window, and svelte-i18n.
  • A fixed set of Svelte internals (advanced — the compiler emits these imports into your built code for you; you never write them by hand): svelte/internal, svelte/internal/client, svelte/internal/disclose-version, svelte/internal/flags/legacy, svelte/internal/flags/async, and svelte/internal/flags/tracing. This is an exact list, not a svelte/internal/* wildcard; any other svelte/internal/... subpath does not resolve.

Anything else — chart.js, svelte-select, any other npm package — must be bundled into your addon by your rollup build, not imported bare. You don't do anything special for this: the boilerplate's rollup build already bundles anything you bun add and then import normally. (The market addon does exactly this to ship Chart.js.)

Never add svelte to your package.json

The SDK controls which Svelte version everyone compiles with (its version is pinned), and the build fails on a mismatch. A second copy of svelte in your package.json silently breaks your pages (the two copies disagree during hydration). See Architecture.

Known dead surfaces (don't use)

For completeness, two members exist on the surface but do nothing — don't build on them:

  • pano.debug — a boolean flag on the pano object. It is currently hardcoded false and no host ever sets it, so do not use it to detect a development build.
  • onContextUpdate() — an old boilerplate method that no host ever calls (see §1).

Where to next