Notur Frontend SDK Reference
This document covers the frontend SDK (@notur/sdk), bridge hooks, slot system, theme system, webpack configuration, and CLI tools for extension developers.
Table of Contents
- Architecture Overview
- CLI Tools
- SDK API: createExtension
- SDK Types
- Bridge Hooks
- SDK Hooks
- Slot System
- Theme System
- Webpack Configuration
Architecture Overview
The Notur frontend has three layers:
- Bridge Runtime (
bridge.js) -- Loaded in the panel's HTML. Createswindow.__NOTUR__and provides the PluginRegistry, SlotRenderer, hooks, and theme engine. - Extension SDK (
@notur/sdk) -- NPM package used by extension developers. ProvidescreateExtension(), type definitions, and convenience hooks. - Extension Bundles -- Each extension ships a pre-built JS bundle that is loaded after
bridge.jsand registers itself viacreateExtension().
React and ReactDOM are not bundled with extensions. They are externalized and use the panel's existing instances via window.React and window.ReactDOM.
Runtime flow
flowchart TD
A["Panel HTML"] --> B["bridge.js"]
B --> C["window.__NOTUR__ registry + hooks"]
A --> D["Extension bundle"]
D --> E["createExtension()"]
E --> F["registry.registerExtension()"]
F --> G["SlotRenderer + Router"]
G --> H["Your components render"]CLI Tools
The SDK includes command-line tools for extension development.
notur-pack
Package your extension into a .notur archive for distribution. This can be run directly on your development machine without needing access to a Pterodactyl server.
# From your extension directory (using npx, yarn dlx, pnpm dlx, or bunx)
npx notur-pack
# Specify a different path
npx notur-pack /path/to/my-extension
# Custom output filename
npx notur-pack --output my-extension.noturOutput:
Packing My Extension v1.0.0...
Found 12 files
Created: acme-my-extension-1.0.0.notur
Checksum: a7b5cf768de...
Upload this file to your Pterodactyl admin panel at /admin/notur/extensionsWhat it creates:
A .notur file is a tar.gz archive containing:
- All extension files (excluding
node_modules,.git,vendor,.idea,.vscode) - A
checksums.jsonwith SHA-256 hashes for integrity verification - A companion
.sha256file with the archive checksum
Installation:
Upload the .notur file via:
- Admin UI (recommended): Navigate to
/admin/notur/extensionsand use the upload form - CLI:
php artisan notur:install /path/to/extension.notur
SDK API: createExtension
The primary entry point for registering an extension. Supports two calling conventions:
import { createExtension } from '@notur/sdk';
// Simplified (recommended) — id at top level, name/version auto-resolved from manifest:
createExtension({
id: 'acme/analytics',
slots: [{ slot: 'dashboard.widgets', component: Widget }],
});
// Full — explicit config object (backward compatible):
createExtension({
config: { id: 'acme/analytics', name: 'Analytics', version: '1.0.0' },
slots: [{ slot: 'dashboard.widgets', component: Widget }],
});Parameters
definition: ExtensionDefinition | SimpleExtensionDefinition
Simplified form (recommended):
type SimpleExtensionDefinition = Omit<ExtensionDefinition, 'config'> & {
id: string; // Extension ID — name/version auto-resolved from extension.yaml
};Full form (backward compatible):
interface ExtensionDefinition {
config: ExtensionConfig;
slots?: SlotConfig[];
routes?: RouteConfig[];
cssIsolation?: CssIsolationConfig | boolean;
onInit?: () => void;
onDestroy?: () => void;
}CSS Isolation
Enable the root-class CSS isolation helper by setting cssIsolation:
createExtension({
config: { id: 'acme/analytics' },
cssIsolation: true, // uses the default class: notur-ext--acme-analytics
slots: [/* ... */],
});You can also pass an explicit class name:
createExtension({
config: { id: 'acme/analytics' },
cssIsolation: { mode: 'root-class', className: 'notur-ext--acme-analytics' },
});If frontend.css_isolation is declared in extension.yaml, the SDK will pick it up automatically. If you omit name and version from config, the SDK will auto-fill them from extension.yaml.
Full Example
import * as React from 'react';
import { createExtension } from '@notur/sdk';
const DashboardWidget: React.FC<{ extensionId: string }> = ({ extensionId }) => {
return <div>Hello from my extension!</div>;
};
const SettingsPage: React.FC = () => {
return <div>Settings page content</div>;
};
createExtension({
id: 'acme/my-extension',
slots: [
{
slot: 'dashboard.widgets',
component: DashboardWidget,
order: 10,
},
{
slot: 'server.subnav',
component: () => null, // Nav items use label/icon, not rendered component
label: 'Settings',
icon: 'cog',
permission: 'my-ext.view',
},
],
routes: [
{
area: 'server',
path: '/my-settings',
name: 'Settings',
component: SettingsPage,
icon: 'cog',
permission: 'my-ext.view',
},
],
onInit: () => {
console.log('Extension initialized');
},
onDestroy: () => {
console.log('Extension destroyed');
},
});What happens on registration
createExtension()callsregistry.registerExtension()with your slots and routes.- The registry registers each slot and route internally.
- The
onInitcallback is invoked (if provided). - A log message
[Notur] Extension registered: {id} v{version}is printed to the console.
SDK Types
ExtensionConfig
interface ExtensionConfig {
id: string; // Extension ID matching extension.yaml (e.g., "acme/analytics")
name?: string; // Human-readable name (optional, auto-filled)
version?: string; // Semantic version string (optional, auto-filled)
}SlotConfig
interface SlotConfig {
slot: string; // Slot ID (see Slot System below)
component: React.ComponentType<any>; // React component to render
order?: number; // Render order (lower = first, default: 0)
priority?: number; // Priority (higher renders earlier, default: 0)
label?: string; // Display label (for nav slots)
icon?: string; // Icon name (for nav slots)
permission?: string; // Required permission
props?: Record<string, any>; // Static props passed to the component
when?: SlotRenderCondition; // Conditional render rules
}Slots are sorted by priority (higher first), then by order (lower first).
RouteConfig
interface RouteConfig {
area: 'server' | 'dashboard' | 'account'; // Which router area
path: string; // Route path (e.g., "/analytics")
name: string; // Display name for nav
component: React.ComponentType<any>; // Page component
icon?: string; // Icon for nav item
permission?: string; // Required permission
}CssIsolationConfig
interface CssIsolationConfig {
mode: 'root-class';
className?: string;
}NoturApi
The global API exposed on window.__NOTUR__:
interface NoturApi {
version: string;
registry: {
registerSlot: (registration: any) => void;
registerRoute: (area: string, route: any) => void;
registerExtension: (ext: any) => void;
getSlot: (slotId: string) => any[];
getRoutes: (area: string) => any[];
on: (event: string, callback: () => void) => () => void;
};
hooks: {
useSlot: (slotId: string) => any[];
useExtensionApi: (options: { extensionId: string }) => any;
useExtensionState: <T>(extensionId: string, initialState: T) => [T, (partial: Partial<T>) => void];
useNoturTheme: () => any;
useRoutes: (area: string) => any[];
};
unregisterExtension: (id: string) => void;
emitEvent: (event: string, data?: unknown) => void;
onEvent: (event: string, callback: (data?: unknown) => void) => () => void;
SLOT_IDS: Record<string, string>;
}Access it via:
import { getNoturApi } from '@notur/sdk';
const api = getNoturApi(); // throws if bridge.js hasn't loaded yetConditional Slot Rendering
Use the when field on a slot to control when it renders. It supports:
area/areas:'server' | 'dashboard' | 'account' | 'admin' | 'auth' | 'other'server,dashboard,account,admin,auth: boolean flagspathorpathStartsWith: string or array of stringspathIncludes: string or array of stringspathMatches: RegExp or regex stringpermission: string or array of strings (uses server permissions when available)
slots: [
{
slot: 'server.subnav',
component: AnalyticsNav,
when: { server: true, permission: 'analytics.view' },
},
{
slot: 'dashboard.widgets',
component: DashboardWidget,
when: { pathStartsWith: '/dashboard' },
props: { compact: true },
},
]Bridge Hooks
These hooks are provided by the bridge runtime and are available on window.__NOTUR__.hooks. They can also be imported in extension code by accessing the global.
useSlot(slotId: SlotId): SlotRegistration[]
Returns all component registrations for a given slot. Subscribes to change events so the consuming component re-renders when extensions register or unregister slot components.
const { useSlot } = window.__NOTUR__.hooks;
const MyComponent: React.FC = () => {
const widgets = useSlot('dashboard.widgets');
return (
<div>
{widgets.map((reg, i) => (
<reg.component key={i} extensionId={reg.extensionId} />
))}
</div>
);
};Behavior:
- Returns an empty array if the bridge hasn't initialized yet.
- Polls every 50ms until the bridge is ready (handles late initialization).
- Listens to both
slot:{slotId}andextension:registeredevents for re-renders.
useExtensionApi<T>(options: UseExtensionApiOptions)
HTTP client scoped to an extension's namespaced API endpoints.
const { useExtensionApi } = window.__NOTUR__.hooks;
const MyComponent: React.FC = () => {
const api = useExtensionApi({ extensionId: 'acme/analytics' });
React.useEffect(() => {
api.get('/stats').then(data => console.log(data));
}, []);
return (
<div>
{api.loading && <p>Loading...</p>}
{api.error && <p>Error: {api.error}</p>}
{api.data && <pre>{JSON.stringify(api.data, null, 2)}</pre>}
</div>
);
};Options:
interface UseExtensionApiOptions {
extensionId: string; // Your extension ID
baseUrl?: string; // Override the default base URL
}Default base URL: /api/client/notur/{extensionId}
Returned object:
| Property/Method | Type | Description |
|---|---|---|
data | T | null | Latest response data |
loading | boolean | Whether a request is in flight |
error | string | null | Error message from last failed request |
get(path) | (path: string) => Promise<T> | GET request |
post(path, body?) | (path: string, body?: any) => Promise<T> | POST request |
put(path, body?) | (path: string, body?: any) => Promise<T> | PUT request |
patch(path, body?) | (path: string, body?: any) => Promise<T> | PATCH request |
delete(path) | (path: string) => Promise<T> | DELETE request |
request(path, options?) | (path: string, options?: RequestInit) => Promise<T> | Custom request |
Features:
- Automatically includes CSRF tokens for mutation methods (POST, PUT, PATCH, DELETE).
- Uses
credentials: 'same-origin'for cookie-based Pterodactyl auth. - Guards against state updates on unmounted components.
- Handles 204 No Content responses (e.g., successful DELETE).
- Extracts structured error messages from Laravel error responses.
useExtensionState<T>(extensionId: string, initialState: T): [T, setState, resetState]
Shared state scoped to an extension. All components from the same extension share one store instance, so state changes are reflected everywhere.
const { useExtensionState } = window.__NOTUR__.hooks;
const Counter: React.FC = () => {
const [state, setState, resetState] = useExtensionState('acme/analytics', {
count: 0,
lastUpdated: null as string | null,
});
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => setState({ count: state.count + 1, lastUpdated: new Date().toISOString() })}>
Increment
</button>
<button onClick={resetState}>Reset</button>
</div>
);
};Return value:
| Index | Type | Description |
|---|---|---|
[0] | T | Current state |
[1] | (partial: Partial<T>) => void | Merge partial state (like React's setState) |
[2] | () => void | Reset state back to initialState |
Features:
- State store is automatically created on first use and cleaned up when the last subscriber unmounts.
- Uses a pub/sub pattern internally -- changes propagate to all subscribers immediately.
- The store is keyed by extension ID, so different extensions never share state.
useNoturTheme(): Record<string, string>
Access the current Notur theme CSS custom properties.
const { useNoturTheme } = window.__NOTUR__.hooks;
const ThemedBox: React.FC = () => {
const theme = useNoturTheme();
return (
<div style={{
background: theme['--notur-bg-secondary'],
color: theme['--notur-text-primary'],
borderRadius: theme['--notur-radius-md'],
}}>
Themed content
</div>
);
};SDK Hooks
These hooks are exported from @notur/sdk and provide access to Pterodactyl panel context.
useServerContext(): ServerContext | null
Access the current server context. Only available on server-scoped pages.
import { useServerContext } from '@notur/sdk';
const ServerInfo: React.FC = () => {
const server = useServerContext();
if (!server) return <p>Not on a server page</p>;
return (
<div>
<p>Server: {server.name}</p>
<p>UUID: {server.uuid}</p>
<p>Status: {server.status}</p>
</div>
);
};ServerContext shape:
interface ServerContext {
uuid: string;
name: string;
node: string;
isOwner: boolean;
status: string | null;
permissions: string[];
}useUserContext(): UserContext | null
Access the current user information.
import { useUserContext } from '@notur/sdk';
const UserGreeting: React.FC = () => {
const user = useUserContext();
if (!user) return null;
return <p>Hello, {user.username}!</p>;
};UserContext shape:
interface UserContext {
uuid: string;
username: string;
email: string;
isAdmin: boolean;
}The hook first tries to read from window.PterodactylUser. If unavailable, it falls back to fetching /api/client/account.
usePermission(permission: string): boolean
Check if the current user has a specific permission in the current server context.
import { usePermission } from '@notur/sdk';
const AdminPanel: React.FC = () => {
const canAdmin = usePermission('analytics.admin');
if (!canAdmin) return null;
return <div>Admin-only content</div>;
};Permission resolution:
- Returns
trueif the user is the server owner (isOwner). - Returns
trueif the user's permissions include*(wildcard). - Returns
trueif the user's permissions include the exact permission string. - Returns
falseif not on a server page (no server context).
Slot System
Slots are predefined injection points in the Pterodactyl panel where extensions can render components.
Slot render lifecycle
sequenceDiagram
participant Ext as "Extension bundle"
participant Reg as "PluginRegistry"
participant UI as "Panel UI"
Ext->>Reg: "registerExtension(slots, routes)"
Reg-->>UI: "emit slot change events"
UI->>Reg: "useSlot(slotId)"
Reg-->>UI: "list of registrations"
UI-->>UI: "render each component in order"Available Slot IDs
| Constant | Slot ID | Location | Type | Description |
|---|---|---|---|---|
NAVBAR | navbar | Navigation bar | portal | Top navigation bar |
NAVBAR_LEFT | navbar.left | Navigation bar | portal | Navbar left area (near logo) |
NAVBAR_BEFORE | navbar.before | Navigation bar | portal | Navbar items (before built-ins) |
NAVBAR_AFTER | navbar.after | Navigation bar | portal | Navbar items (after built-ins) |
SERVER_SUBNAV | server.subnav | Server sub-navigation | nav | Server sub-navigation |
SERVER_SUBNAV_BEFORE | server.subnav.before | Server sub-navigation | nav | Server sub-navigation (before built-ins) |
SERVER_SUBNAV_AFTER | server.subnav.after | Server sub-navigation | nav | Server sub-navigation (after built-ins) |
SERVER_HEADER | server.header | Server area | portal | Server header area |
SERVER_PAGE | server.page | Server area | route | Server area page |
SERVER_FOOTER | server.footer | Server area | portal | Server footer area |
SERVER_TERMINAL_BUTTONS | server.terminal.buttons | Server console | portal | Terminal power buttons |
SERVER_CONSOLE_HEADER | server.console.header | Server console | portal | Console page header |
SERVER_CONSOLE_INFO_BEFORE | server.console.info.before | Server console | portal | Console info (before details) |
SERVER_CONSOLE_INFO_AFTER | server.console.info.after | Server console | portal | Console info (after details) |
SERVER_CONSOLE_SIDEBAR | server.console.sidebar | Server console | portal | Console sidebar area |
SERVER_CONSOLE_COMMAND | server.console.command | Server console | portal | Console command row |
SERVER_CONSOLE_FOOTER | server.console.footer | Server console | portal | Console page footer |
SERVER_FILES_ACTIONS | server.files.actions | Server files | portal | File manager toolbar |
SERVER_FILES_HEADER | server.files.header | Server files | portal | File manager header |
SERVER_FILES_FOOTER | server.files.footer | Server files | portal | File manager footer |
SERVER_FILES_DROPDOWN | server.files.dropdown | Server files | portal | File manager dropdown items |
SERVER_FILES_EDIT_BEFORE | server.files.edit.before | Server files | portal | File editor (before content) |
SERVER_FILES_EDIT_AFTER | server.files.edit.after | Server files | portal | File editor (after content) |
SERVER_DATABASES_BEFORE | server.databases.before | Server databases | portal | Databases page (before content) |
SERVER_DATABASES_AFTER | server.databases.after | Server databases | portal | Databases page (after content) |
SERVER_SCHEDULES_BEFORE | server.schedules.before | Server schedules | portal | Schedules list (before content) |
SERVER_SCHEDULES_AFTER | server.schedules.after | Server schedules | portal | Schedules list (after content) |
SERVER_SCHEDULES_EDIT_BEFORE | server.schedules.edit.before | Server schedules | portal | Schedule editor (before content) |
SERVER_SCHEDULES_EDIT_AFTER | server.schedules.edit.after | Server schedules | portal | Schedule editor (after content) |
SERVER_USERS_BEFORE | server.users.before | Server users | portal | Users page (before content) |
SERVER_USERS_AFTER | server.users.after | Server users | portal | Users page (after content) |
SERVER_BACKUPS_BEFORE | server.backups.before | Server backups | portal | Backups page (before content) |
SERVER_BACKUPS_AFTER | server.backups.after | Server backups | portal | Backups page (after content) |
SERVER_BACKUPS_DROPDOWN | server.backups.dropdown | Server backups | portal | Backup row dropdown items |
SERVER_NETWORK_BEFORE | server.network.before | Server network | portal | Network page (before content) |
SERVER_NETWORK_AFTER | server.network.after | Server network | portal | Network page (after content) |
SERVER_STARTUP_BEFORE | server.startup.before | Server startup | portal | Startup page (before content) |
SERVER_STARTUP_AFTER | server.startup.after | Server startup | portal | Startup page (after content) |
SERVER_SETTINGS_BEFORE | server.settings.before | Server settings | portal | Settings page (before content) |
SERVER_SETTINGS_AFTER | server.settings.after | Server settings | portal | Settings page (after content) |
DASHBOARD_HEADER | dashboard.header | Dashboard | portal | Dashboard header area |
DASHBOARD_WIDGETS | dashboard.widgets | Dashboard | portal | Dashboard widgets |
DASHBOARD_SERVERLIST_BEFORE | dashboard.serverlist.before | Dashboard server list | portal | Dashboard server list (before) |
DASHBOARD_SERVERLIST_AFTER | dashboard.serverlist.after | Dashboard server list | portal | Dashboard server list (after) |
DASHBOARD_SERVERROW_NAME_BEFORE | dashboard.serverrow.name.before | Dashboard server row | portal | Dashboard server row name (before) |
DASHBOARD_SERVERROW_NAME_AFTER | dashboard.serverrow.name.after | Dashboard server row | portal | Dashboard server row name (after) |
DASHBOARD_SERVERROW_DESCRIPTION_BEFORE | dashboard.serverrow.description.before | Dashboard server row | portal | Dashboard server row description (before) |
DASHBOARD_SERVERROW_DESCRIPTION_AFTER | dashboard.serverrow.description.after | Dashboard server row | portal | Dashboard server row description (after) |
DASHBOARD_SERVERROW_LIMITS | dashboard.serverrow.limits | Dashboard server row | portal | Dashboard server row resource limits |
DASHBOARD_FOOTER | dashboard.footer | Dashboard | portal | Dashboard footer area |
DASHBOARD_PAGE | dashboard.page | Dashboard | route | Dashboard page |
ACCOUNT_HEADER | account.header | Account | portal | Account header area |
ACCOUNT_PAGE | account.page | Account | route | Account page |
ACCOUNT_FOOTER | account.footer | Account | portal | Account footer area |
ACCOUNT_SUBNAV | account.subnav | Account sub-navigation | nav | Account sub-navigation |
ACCOUNT_SUBNAV_BEFORE | account.subnav.before | Account sub-navigation | nav | Account sub-navigation (before built-ins) |
ACCOUNT_SUBNAV_AFTER | account.subnav.after | Account sub-navigation | nav | Account sub-navigation (after built-ins) |
ACCOUNT_OVERVIEW_BEFORE | account.overview.before | Account | portal | Account overview (before content) |
ACCOUNT_OVERVIEW_AFTER | account.overview.after | Account | portal | Account overview (after content) |
ACCOUNT_API_BEFORE | account.api.before | Account | portal | Account API (before content) |
ACCOUNT_API_AFTER | account.api.after | Account | portal | Account API (after content) |
ACCOUNT_SSH_BEFORE | account.ssh.before | Account | portal | Account SSH (before content) |
ACCOUNT_SSH_AFTER | account.ssh.after | Account | portal | Account SSH (after content) |
AUTH_CONTAINER_BEFORE | auth.container.before | Authentication | portal | Authentication container (before content) |
AUTH_CONTAINER_AFTER | auth.container.after | Authentication | portal | Authentication container (after content) |
Slot Types
portal-- Component is rendered into a container<div>via React portals. Use for widgets, buttons, and inline content.nav-- Component metadata (label, icon) is used to render a navigation item. The actual page component is registered as a route.route-- Component is rendered as a full page when the user navigates to the registered path.
Using Slot IDs in Code
The slot IDs are available as constants:
// From bridge runtime
const { SLOT_IDS } = window.__NOTUR__;
// Constants:
SLOT_IDS.NAVBAR // 'navbar'
SLOT_IDS.NAVBAR_LEFT // 'navbar.left'
SLOT_IDS.NAVBAR_BEFORE // 'navbar.before'
SLOT_IDS.NAVBAR_AFTER // 'navbar.after'
SLOT_IDS.SERVER_SUBNAV // 'server.subnav'
SLOT_IDS.SERVER_SUBNAV_BEFORE // 'server.subnav.before'
SLOT_IDS.SERVER_SUBNAV_AFTER // 'server.subnav.after'
SLOT_IDS.SERVER_HEADER // 'server.header'
SLOT_IDS.SERVER_PAGE // 'server.page'
SLOT_IDS.SERVER_FOOTER // 'server.footer'
SLOT_IDS.SERVER_TERMINAL_BUTTONS // 'server.terminal.buttons'
SLOT_IDS.SERVER_CONSOLE_HEADER // 'server.console.header'
SLOT_IDS.SERVER_CONSOLE_INFO_BEFORE // 'server.console.info.before'
SLOT_IDS.SERVER_CONSOLE_INFO_AFTER // 'server.console.info.after'
SLOT_IDS.SERVER_CONSOLE_SIDEBAR // 'server.console.sidebar'
SLOT_IDS.SERVER_CONSOLE_COMMAND // 'server.console.command'
SLOT_IDS.SERVER_CONSOLE_FOOTER // 'server.console.footer'
SLOT_IDS.SERVER_FILES_ACTIONS // 'server.files.actions'
SLOT_IDS.SERVER_FILES_HEADER // 'server.files.header'
SLOT_IDS.SERVER_FILES_FOOTER // 'server.files.footer'
SLOT_IDS.SERVER_FILES_DROPDOWN // 'server.files.dropdown'
SLOT_IDS.SERVER_FILES_EDIT_BEFORE // 'server.files.edit.before'
SLOT_IDS.SERVER_FILES_EDIT_AFTER // 'server.files.edit.after'
SLOT_IDS.SERVER_DATABASES_BEFORE // 'server.databases.before'
SLOT_IDS.SERVER_DATABASES_AFTER // 'server.databases.after'
SLOT_IDS.SERVER_SCHEDULES_BEFORE // 'server.schedules.before'
SLOT_IDS.SERVER_SCHEDULES_AFTER // 'server.schedules.after'
SLOT_IDS.SERVER_SCHEDULES_EDIT_BEFORE // 'server.schedules.edit.before'
SLOT_IDS.SERVER_SCHEDULES_EDIT_AFTER // 'server.schedules.edit.after'
SLOT_IDS.SERVER_USERS_BEFORE // 'server.users.before'
SLOT_IDS.SERVER_USERS_AFTER // 'server.users.after'
SLOT_IDS.SERVER_BACKUPS_BEFORE // 'server.backups.before'
SLOT_IDS.SERVER_BACKUPS_AFTER // 'server.backups.after'
SLOT_IDS.SERVER_BACKUPS_DROPDOWN // 'server.backups.dropdown'
SLOT_IDS.SERVER_NETWORK_BEFORE // 'server.network.before'
SLOT_IDS.SERVER_NETWORK_AFTER // 'server.network.after'
SLOT_IDS.SERVER_STARTUP_BEFORE // 'server.startup.before'
SLOT_IDS.SERVER_STARTUP_AFTER // 'server.startup.after'
SLOT_IDS.SERVER_SETTINGS_BEFORE // 'server.settings.before'
SLOT_IDS.SERVER_SETTINGS_AFTER // 'server.settings.after'
SLOT_IDS.DASHBOARD_HEADER // 'dashboard.header'
SLOT_IDS.DASHBOARD_WIDGETS // 'dashboard.widgets'
SLOT_IDS.DASHBOARD_SERVERLIST_BEFORE // 'dashboard.serverlist.before'
SLOT_IDS.DASHBOARD_SERVERLIST_AFTER // 'dashboard.serverlist.after'
SLOT_IDS.DASHBOARD_SERVERROW_NAME_BEFORE // 'dashboard.serverrow.name.before'
SLOT_IDS.DASHBOARD_SERVERROW_NAME_AFTER // 'dashboard.serverrow.name.after'
SLOT_IDS.DASHBOARD_SERVERROW_DESCRIPTION_BEFORE // 'dashboard.serverrow.description.before'
SLOT_IDS.DASHBOARD_SERVERROW_DESCRIPTION_AFTER // 'dashboard.serverrow.description.after'
SLOT_IDS.DASHBOARD_SERVERROW_LIMITS // 'dashboard.serverrow.limits'
SLOT_IDS.DASHBOARD_FOOTER // 'dashboard.footer'
SLOT_IDS.DASHBOARD_PAGE // 'dashboard.page'
SLOT_IDS.ACCOUNT_HEADER // 'account.header'
SLOT_IDS.ACCOUNT_PAGE // 'account.page'
SLOT_IDS.ACCOUNT_FOOTER // 'account.footer'
SLOT_IDS.ACCOUNT_SUBNAV // 'account.subnav'
SLOT_IDS.ACCOUNT_SUBNAV_BEFORE // 'account.subnav.before'
SLOT_IDS.ACCOUNT_SUBNAV_AFTER // 'account.subnav.after'
SLOT_IDS.ACCOUNT_OVERVIEW_BEFORE // 'account.overview.before'
SLOT_IDS.ACCOUNT_OVERVIEW_AFTER // 'account.overview.after'
SLOT_IDS.ACCOUNT_API_BEFORE // 'account.api.before'
SLOT_IDS.ACCOUNT_API_AFTER // 'account.api.after'
SLOT_IDS.ACCOUNT_SSH_BEFORE // 'account.ssh.before'
SLOT_IDS.ACCOUNT_SSH_AFTER // 'account.ssh.after'
SLOT_IDS.AUTH_CONTAINER_BEFORE // 'auth.container.before'
SLOT_IDS.AUTH_CONTAINER_AFTER // 'auth.container.after'Theme System
Notur provides a CSS custom property-based theme system that derives its defaults from the Pterodactyl panel's styles.
CSS Custom Properties
All Notur theme variables are prefixed with --notur-. Extensions should use these variables for consistent styling.
Colors
| Variable | Default | Purpose |
|---|---|---|
--notur-primary | #0967d2 | Primary accent color |
--notur-primary-light | #47a3f3 | Light primary |
--notur-primary-dark | #03449e | Dark primary |
--notur-secondary | #7c8b9a | Secondary color |
--notur-success | #27ab83 | Success / positive |
--notur-danger | #e12d39 | Danger / destructive |
--notur-warning | #f7c948 | Warning |
--notur-info | #2bb0ed | Informational |
Backgrounds
| Variable | Default | Purpose |
|---|---|---|
--notur-bg-primary | #0b0d10 | Primary background |
--notur-bg-secondary | rgba(17, 19, 24, 0.68) | Secondary background (cards, inputs) |
--notur-bg-tertiary | rgba(25, 28, 35, 0.8) | Tertiary background (hover states) |
Text
| Variable | Default | Purpose |
|---|---|---|
--notur-text-primary | #f1f5f9 | Primary text |
--notur-text-secondary | #cbd5e1 | Secondary text |
--notur-text-muted | #94a3b8 | Muted / placeholder text |
Borders and Radius
| Variable | Default | Purpose |
|---|---|---|
--notur-border | rgba(148, 163, 184, 0.18) | Border color |
--notur-radius-sm | 6px | Small border radius |
--notur-radius-md | 12px | Medium border radius |
--notur-radius-lg | 18px | Large border radius |
Glass Effects
| Variable | Default | Purpose |
|---|---|---|
--notur-glass-bg | rgba(17, 19, 24, 0.55) | Glass surface background |
--notur-glass-border | rgba(255, 255, 255, 0.12) | Glass border |
--notur-glass-highlight | rgba(255, 255, 255, 0.06) | Subtle highlight for glass edges |
--notur-glass-shadow | 0 16px 40px rgba(0, 0, 0, 0.55) | Soft depth shadow |
--notur-glass-blur | 16px | Backdrop blur amount |
Typography
| Variable | Default | Purpose |
|---|---|---|
--notur-font-sans | -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif | Sans-serif font stack |
--notur-font-mono | "Fira Code", "JetBrains Mono", monospace | Monospace font stack |
Using Theme Variables
.my-widget {
background: var(--notur-glass-bg, var(--notur-bg-secondary));
color: var(--notur-text-primary);
border: 1px solid var(--notur-glass-border, var(--notur-border));
border-radius: var(--notur-radius-lg);
box-shadow: var(--notur-glass-shadow, 0 12px 30px rgba(0, 0, 0, 0.35));
backdrop-filter: blur(var(--notur-glass-blur, 12px));
font-family: var(--notur-font-sans);
padding: 1rem;
}
.my-widget .title {
color: var(--notur-primary);
font-weight: 600;
}
.my-widget .danger-text {
color: var(--notur-danger);
}Or inline with the useNoturTheme hook (see Bridge Hooks above).
Theme Extraction
On initialization, the bridge runtime attempts to extract theme values from the live panel DOM using three strategies:
- CSS custom properties -- Reads
--ptero-*properties from:rootand maps them to--notur-*equivalents. - Computed styles -- Probes known panel elements (e.g.,
#appbackground color, input borders) and derives Notur variables. - Stylesheet scanning -- Scans all loaded stylesheets for
--ptero-*declarations in:rootrules.
If no values can be extracted, the defaults listed above are applied.
Theme Extensions
Theme extensions can override CSS custom properties by shipping a CSS file that redefines :root properties:
/* my-theme.css */
:root {
--notur-primary: #6366f1;
--notur-bg-primary: #0f172a;
--notur-bg-secondary: #1e293b;
--notur-text-primary: #e2e8f0;
}Default Glass Surfaces
The bridge runtime applies glass styling by default to dashboard widgets and extension route pages. If you want the same look elsewhere, wrap your UI with these classes:
<div class="notur-surface notur-surface--card">
<!-- your content -->
</div>You can also use notur-surface--page for full-page layouts.
Webpack Configuration
The SDK ships a base webpack configuration for building extension frontend bundles.
Using the Base Config
// webpack.config.js
const base = require('@notur/sdk/webpack.extension.config');
module.exports = {
...base,
entry: './resources/frontend/src/index.tsx',
output: {
...base.output,
filename: 'extension.js',
path: __dirname + '/resources/frontend/dist',
},
};What the Base Config Does
- Sets React and ReactDOM as externals (uses
window.Reactandwindow.ReactDOM). - Configures TypeScript/TSX compilation via
ts-loaderorbabel-loader. - Outputs a single bundle file suitable for browser loading.
- Sets
libraryTarget: 'umd'for compatibility.
Building
# Production build (using npx, yarn dlx, pnpm dlx, or bunx)
npx webpack --mode production
# Development build (with source maps)
npx webpack --mode developmentImportant Notes
- Do not bundle React. The panel already includes React. Your extension's webpack config must externalize it:js
externals: { 'react': 'React', 'react-dom': 'ReactDOM', } - Output goes to
resources/frontend/dist/by convention. - The output filename should match the
frontend.bundlefield in yourextension.yaml. - Bundles are loaded in the browser after
bridge.js, sowindow.__NOTUR__is guaranteed to exist.
SDK Exports
The @notur/sdk package exports:
// Functions
export { createExtension } from './createExtension';
export { getNoturApi } from './types';
// Types
export type { ExtensionConfig, SlotConfig, RouteConfig, ExtensionDefinition, SimpleExtensionDefinition, NoturApi } from './types';
// Hooks
export { useServerContext } from './hooks/useServerContext';
export { useUserContext } from './hooks/useUserContext';
export { usePermission } from './hooks/usePermission';
export { useExtensionConfig } from './hooks/useExtensionConfig';
export { useNoturEvent, useEmitEvent } from './hooks/useNoturEvent';
export { useNavigate } from './hooks/useNavigate';
export { createScopedEventChannel } from './events';useExtensionConfig(extensionId: string, options?): { config, loading, error, refresh }
Fetches public settings declared in extension.yaml under admin.settings with public: true.
import { useExtensionConfig } from '@notur/sdk';
const AnalyticsWidget: React.FC = () => {
const { config, loading, error } = useExtensionConfig('acme/server-analytics');
if (loading) return <p>Loading settings...</p>;
if (error) return <p>Failed to load settings: {error}</p>;
return <p>Mode: {config.mode ?? 'default'}</p>;
};Options:
baseUrl(string): override the API base URL (default/api/client/notur)initial(object): initial settings value before fetch completespollInterval(number): refresh interval in milliseconds (enables live reload)
Live reload example:
const { config } = useExtensionConfig('acme/server-analytics', { pollInterval: 5000 });useNoturEvent(event: string, callback: (data?: unknown) => void): void
Subscribe to a Notur event. The callback is invoked whenever the specified event is emitted. The subscription is automatically cleaned up when the component unmounts.
import { useNoturEvent } from '@notur/sdk';
const Notifications: React.FC = () => {
useNoturEvent('analytics:updated', (data) => {
console.log('Analytics data updated:', data);
});
return <div>Listening for events...</div>;
};useEmitEvent(): (event: string, data?: unknown) => void
Returns a function to emit a Notur event. Other extensions or components subscribed to the event via useNoturEvent or onEvent will receive the data.
import { useEmitEvent } from '@notur/sdk';
const RefreshButton: React.FC = () => {
const emit = useEmitEvent();
return (
<button onClick={() => emit('analytics:refresh', { force: true })}>
Refresh
</button>
);
};createScopedEventChannel(extensionId: string): { eventName, emit, on }
Creates a namespaced event channel to prevent collisions with other extensions on the shared event bus.
import { createScopedEventChannel } from '@notur/sdk';
const channel = createScopedEventChannel('acme/server-analytics');
channel.emit('refresh', { source: 'widget' });
const unsubscribe = channel.on('refresh', (payload) => {
console.log('Scoped event payload:', payload);
});The generated event name format is:
ext:{extensionId}:{eventName}- Example:
ext:acme/server-analytics:refresh
useNavigate(): (path: string) => void
Returns a navigation function that integrates with the panel's client-side router. Use this instead of window.location for SPA-style navigation.
import { useNavigate } from '@notur/sdk';
const GoToSettings: React.FC = () => {
const navigate = useNavigate();
return (
<button onClick={() => navigate('/account/settings')}>
Go to Settings
</button>
);
};