Last Updated: October 2026
Slot fills let plugins inject React components into named extension points in the admin and player-facing UI. Each slot renders all registered fills in priority order (lower number = first).
How Slots Work
Host application renders: Plugin registers a fill:
┌─────────────────────────┐ fills.register(
│ <MainLayout> │ 'layout.main.header.end',
│ <Header> │ MyBadgeComponent,
│ ... │ { priority: 10, providerType: 'my-plugin' }
│ <Slot │ );
│ name="layout.main.header.end"
│ /> │
│ </Header> │ At render time:
│ </MainLayout> │ <Slot> → MyBadgeComponent renders here
└─────────────────────────┘
Each fill is wrapped in a <PluginErrorBoundary>. A crash in a fill component is isolated — it will not crash the host application or other fills.
Registering a Fill
const { fills, React } = window.__pokerEngine;
function MyComponent(props) {
// props contains whatever the slot passes through — varies per slot
return React.createElement('div', { className: 'my-fill' }, 'Hello from plugin');
}
fills.register('slot-name', MyComponent, {
priority: 10, // Lower = renders earlier (default: 10)
providerType: 'my-plugin', // Required — used for cleanup on deactivation
});
All fills registered with your providerType are automatically removed when the plugin is deactivated.
Available Slots
layout.admin.sidebar.end
Location: Bottom of the admin panel sidebar, below all navigation items.
Renders in: client/src/layouts/AdminLayout.tsx
Props passed to fill: none
Use this slot to inject extra navigation items or widgets into the admin sidebar.
function AdminNavItem() {
const { React } = window.__pokerEngine;
return React.createElement(
'a',
{ href: '/admin/plugins/my-plugin/settings', className: 'sidebar-link' },
'My Plugin'
);
}
fills.register('layout.admin.sidebar.end', AdminNavItem, {
priority: 10,
providerType: 'my-plugin',
});
Note: Pages registered with
pages.register({ area: 'admin', nav: { ... } })are automatically inserted as nav items via theAdminPluginNavItemscore fill. You only need this slot for custom nav UI that differs from standard nav items.
layout.main.sidebar.end
Location: Bottom of the main player-facing sidebar, below all navigation items.
Renders in: client/src/layouts/MainLayout.tsx
Props passed to fill: none
Use this slot to inject extra navigation items into the player sidebar.
Note: Pages registered with
pages.register({ area: 'user', nav: { ... } })are automatically inserted via theUserPluginNavItemscore fill.
layout.main.header.end
Location: Right side of the main player-facing header bar.
Renders in: client/src/layouts/MainLayout.tsx
Props passed to fill: none
Use this slot to inject badges, status indicators, or action buttons into the player header.
function HeaderBadge() {
const { React } = window.__pokerEngine;
return React.createElement(
'span',
{ style: { background: '#7c3aed', color: '#fff', padding: '2px 8px', borderRadius: 4 } },
'Beta'
);
}
fills.register('layout.main.header.end', HeaderBadge, {
priority: 10,
providerType: 'my-plugin',
});
auth.login.below-form
Location: Below the login form on the player login page.
Renders in: client/src/pages/auth/LoginPage.tsx
Props passed to fill: none
Use this slot to inject additional login options, notices, or promotional content below the standard login form.
function SSOLoginButton() {
const { React } = window.__pokerEngine;
return React.createElement(
'button',
{ onClick: () => redirectToSSO(), className: 'btn-sso' },
'Log in with My Platform'
);
}
fills.register('auth.login.below-form', SSOLoginButton, {
priority: 10,
providerType: 'my-plugin',
});
auth.register.below-form
Location: Below the registration form on the player registration page.
Renders in: client/src/pages/auth/RegisterPage.tsx
Props passed to fill: none
Use this slot to inject additional content below the standard registration form (e.g., terms links, promotional content, third-party registration options).
cashier.deposit.method-list
Location: Inside the cashier deposit tab, in the payment method selection area.
Renders in: client/src/pages/cashier/CashierPage.tsx
Props passed to fill: { selectedMethod, amount }
Use this slot to render custom deposit method UI. selectedMethod is the active deposit method ID and amount is the current deposit amount input. selectedMethod holds the method id from GET /api/v1/cashier/payment-methods. Every fill renders regardless of the selection, so return null unless it is your method’s ID. Typically used alongside registry.registerProvider() on the server side to add a custom payment provider.
function MyDepositMethod({ selectedMethod }) {
const { React } = window.__pokerEngine;
if (selectedMethod !== MY_METHOD_ID) return null;
return React.createElement(
'div',
{ className: 'payment-method' },
React.createElement('img', { src: '/plugins/my-plugin/logo.png', alt: 'My Provider' }),
React.createElement('span', null, 'Pay with My Provider')
);
}
fills.register('cashier.deposit.method-list', MyDepositMethod, {
priority: 20,
providerType: 'my-plugin',
});
cashier.withdraw.method-list
Location: Inside the cashier withdrawal tab, in the payment method selection area.
Renders in: client/src/pages/cashier/CashierPage.tsx
Props passed to fill: { selectedMethod, onDestinationChange, onMetadataChange }
Mirrors cashier.deposit.method-list but for withdrawal methods. selectedMethod is the active withdrawal method ID. Call onDestinationChange(destination) when your UI collects the payout destination/account. Call onMetadataChange(metadata) with provider-specific withdrawal metadata that should be submitted with the request.
profile.edit.after
Location: Below the edit profile form on the player Profile page (Edit Profile tab).
Renders in: client/src/pages/profile/ProfilePage.tsx
Props passed to fill: none
Use this slot to inject additional settings or integrations below the profile edit form. Fills render inside the edit tab’s content area, after the save button.
function WalletConnect() {
const { React } = window.__pokerEngine;
return React.createElement(
'div',
{ style: { marginTop: '16px', padding: '16px', background: '#1f2937', borderRadius: '8px' } },
'Connect your external wallet here'
);
}
fills.register('profile.edit.after', WalletConnect, {
priority: 10,
providerType: 'my-plugin',
});
admin.users.row.flags
Location: Inside the flags column of each user row in the Admin → Users management table (both desktop and mobile views).
Renders in: client/src/pages/admin/UsersManagementPage.tsx
Props passed to fill: { user } — the AdminUserData object for the row being rendered, containing id, username, email, isBot, isAgent, etc.
Use this slot to add flag badges for plugin-specific user attributes (e.g., linked wallets, external integrations). The fill should render a small inline badge consistent with the existing flag style (rounded pill, 12px icon, text-xs).
function MyPluginFlag({ user }) {
const { React } = window.__pokerEngine;
const [linked, setLinked] = React.useState(false);
React.useEffect(() => {
fetch(`/api/v1/my-plugin/status?userId=${user.id}`, { credentials: 'include' })
.then(r => r.json())
.then(d => { if (d?.data?.linked) setLinked(true); });
}, [user.id]);
if (!linked) return null;
return React.createElement(
'span',
{
className: 'inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-xs font-medium',
style: { background: 'rgba(100, 200, 100, 0.1)', color: '#4ade80' },
title: 'My Plugin connected',
},
'Connected'
);
}
fills.register('admin.users.row.flags', MyPluginFlag, {
priority: 10,
providerType: 'my-plugin',
});
admin.user-detail.integrations
Location: Inside the Activity section of the Admin -> Users detail drawer.
Renders in: client/src/components/admin/UserDetailDrawer.tsx
Props passed to fill: { user } — the selected AdminUserData object.
Use this slot to show compact integration-specific details for one user after an admin opens the detail drawer. Prefer this slot over admin.users.row.flags when your fill needs a network request or detailed account metadata.
lobby.top
Location: Above the table list on the Lobby page.
Renders in: client/src/pages/lobby/LobbyPage.tsx
Props passed to fill: none
Use this slot to add banners, announcements, or promotions.
plugin.settings.before-form
Location: Above the settings form on the Admin Panel plugin settings page.
Renders in: client/src/pages/admin/PluginSettingsPage.tsx
Props passed to fill: none
The slot renders on every plugin’s settings page. To target your own plugin only, use the plugin.settings.after-form.{providerType} variant below.
plugin.settings.after-form
Location: Below the settings form on the Admin Panel plugin settings page.
Renders in: client/src/pages/admin/PluginSettingsPage.tsx
Props passed to fill: none
Renders on every plugin’s settings page. A per-plugin variant, plugin.settings.after-form.{providerType}, renders directly after it on that plugin’s settings page only (for example plugin.settings.after-form.my-plugin).
Slots Summary
| Slot Name | Location | Props |
|---|---|---|
layout.admin.sidebar.end | Admin sidebar — bottom | none |
layout.main.sidebar.end | Player sidebar — bottom | none |
layout.main.header.end | Player header — right side | none |
auth.login.below-form | Login page — below form | none |
auth.register.below-form | Registration page — below form | none |
cashier.deposit.method-list | Cashier — deposit methods area | { selectedMethod, amount } |
cashier.withdraw.method-list | Cashier — withdrawal methods area | { selectedMethod, onDestinationChange, onMetadataChange } |
profile.edit.after | Profile — below edit form | none |
admin.users.row.flags | Admin users table — flags column | { user } |
admin.user-detail.integrations | Admin users detail drawer — activity section | { user } |
lobby.top | Lobby — above table list | none |
plugin.settings.before-form | Plugin settings — above form | none |
plugin.settings.after-form | Plugin settings — below form | none |
plugin.settings.after-form.{providerType} | One plugin’s settings — below form | none |
Implementation Notes
- Multiple fills in the same slot render as sibling elements in DOM order of priority.
- If no fills are registered for a slot, the
<Slot>rendersnull— no empty wrapper element. - The
SlotFillRegistryis reactive; fills registered after initial render appear immediately. - Fill cleanup on deactivation is handled via
fillRegistry.unregisterPlugin(providerType), called byusePluginSyncwhen it receives aplugin:deactivated,plugin:auto-disabledorplugin:updatedWebSocket event.
Related Files
| File | Purpose |
|---|---|
client/src/core/slot-fill/registry.ts | SlotFillRegistry implementation |
client/src/core/slot-fill/components.tsx | <Slot>, <Fill>, <SlotFillProvider> |
client/src/core/slot-fill/plugin-error-boundary.tsx | <PluginErrorBoundary> |
client/src/main.tsx | Registers AdminPluginNavItems and UserPluginNavItems as core fills |
client/src/hooks/usePluginSync.ts | Calls fillRegistry.unregisterPlugin() on deactivation |