> For the complete documentation index, see [llms.txt](https://docs.tatakai.me/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tatakai.me/extensions/extension-capabilities/ui-slots.md).

# UI slots

Code-driven contributions (trust-gated) allow extensions to inject React components into the app's UI: **pages** (full routes) and **slots** (embedded components in predefined locations).

***

## Capability Declaration

```json
{
  "capabilities": ["ui"],
  "contributes": {
    "rendererEntry": "renderer.js"
  }
}
```

{% hint style="warning" %}
**Requires trust** — only loads for trusted extensions.
{% endhint %}

***

## ExtensionContext Registry API

```ts
interface ExtensionContext {
  registry: ExtensionRegistry;
  // ... other fields
}

// ExtensionRegistry methods (called via ctx.registry)
registerPage(page: ExtensionPage): void;
registerSlot(slot: ExtensionSlot): void;
```

### ExtensionPage

```ts
interface ExtensionPage {
  id: string;                    // Namespaced: `${extensionId}:my-page`
  path: string;                  // Route path (e.g. "/my-page", "/settings/my-feature")
  component: React.ComponentType; // Page component
  label: string;                 // Sidebar/menu label
  icon?: string;                 // Lucide icon name or component
}
```

### ExtensionSlot

```ts
interface ExtensionSlot {
  id: string;                    // Namespaced: `${extensionId}:my-slot`
  slotId: string;                // Predefined slot ID (see below)
  component: React.ComponentType<any>; // Slot component
}
```

***

## Predefined Slot IDs

| Slot ID                   | Location                   | Description                |
| ------------------------- | -------------------------- | -------------------------- |
| `header:actions`          | Header toolbar (right)     | Buttons, menus, search     |
| `header:user-menu`        | User dropdown              | Profile, settings, logout  |
| `sidebar:top`             | Sidebar top section        | Above navigation           |
| `sidebar:bottom`          | Sidebar bottom section     | Below navigation           |
| `sidebar:extensions`      | Extensions sidebar section | Extension-specific nav     |
| `settings:panels`         | Settings page              | Additional settings panels |
| `player:controls`         | Video player controls      | Custom buttons, overlays   |
| `reader:toolbar`          | Manga/novel reader toolbar | Custom reading controls    |
| `anime:detail:actions`    | Anime detail page          | Buttons below poster       |
| `manga:detail:actions`    | Manga detail page          | Buttons below cover        |
| `novel:detail:actions`    | Novel detail page          | Buttons below cover        |
| `profile:tabs`            | User profile               | Additional profile tabs    |
| `community:post:actions`  | Community post             | Post interaction buttons   |
| `search:filters`          | Search page                | Additional filter chips    |
| `home:sections`           | Home page                  | Custom content sections    |
| `watch-together:controls` | Watch Together session     | Session-specific controls  |

***

## Page Registration

```ts
// renderer.js
import { MyPage } from './pages/MyPage';
import { MySettingsPanel } from './pages/MySettingsPanel';

export async function activate(ctx) {
  // Full page route
  ctx.registry.registerPage({
    id: `${ctx.extensionId}:my-page`,
    path: '/my-feature',
    component: MyPage,
    label: 'My Feature',
    icon: 'sparkles',
  });

  // Settings panel (lazy-loaded via route /settings/my-feature)
  ctx.registry.registerPage({
    id: `${ctx.extensionId}:settings`,
    path: '/settings/my-feature',
    component: MySettingsPanel,
    label: 'My Feature',
    icon: 'settings',
  });
}
```

**Routing:** Pages mount at `path` under the app's router. Settings panels appear in Settings → Extensions.

***

## Slot Registration

```ts
// renderer.js
import { HeaderActionButton } from './slots/HeaderActionButton';
import { PlayerQualityBadge } from './slots/PlayerQualityBadge';
import { SidebarExtensionLink } from './slots/SidebarExtensionLink';

export async function activate(ctx) {
  // Header action button
  ctx.registry.registerSlot({
    id: `${ctx.extensionId}:header-action`,
    slotId: 'header:actions',
    component: HeaderActionButton,
  });

  // Player control
  ctx.registry.registerSlot({
    id: `${ctx.extensionId}:player-badge`,
    slotId: 'player:controls',
    component: PlayerQualityBadge,
  });

  // Sidebar link
  ctx.registry.registerSlot({
    id: `${ctx.extensionId}:sidebar-link`,
    slotId: 'sidebar:extensions',
    component: SidebarExtensionLink,
  });
}
```

***

## Rendering Slots

**Component:** `src/core/extensions/ExtensionSlot.tsx`

```tsx
// In any component
import { ExtensionSlot } from '@/core/extensions/ExtensionSlot';

function Header() {
  return (
    <header>
      <ExtensionSlot slotId="header:actions" />
    </header>
  );
}

function VideoPlayer() {
  return (
    <div className="player-controls">
      <ExtensionSlot slotId="player:controls" />
    </div>
  );
}
```

**Behavior:**

* Multiple components per slot → rendered in registration order
* Empty slot → renders nothing (no wrapper)
* Components receive no props by default (use context/hooks for data)

***

## Slot Component Patterns

### Header Action Button

```tsx
// slots/HeaderActionButton.tsx
import { useExtensionStore } from '@/features/extensions/hooks/useExtensionStore';

export function HeaderActionButton() {
  const { enabledExtensions } = useExtensionStore();
  const myExt = enabledExtensions.find(e => e.id === 'my-extension');
  
  if (!myExt) return null;
  
  return (
    <Button variant="ghost" size="icon" onClick={() => navigate('/my-feature')}>
      <Sparkles className="h-4 w-4" />
    </Button>
  );
}
```

### Player Control

```tsx
// slots/PlayerQualityBadge.tsx
import { usePlayerStore } from '@/features/player/hooks/usePlayerStore';

export function PlayerQualityBadge() {
  const { quality, sources } = usePlayerStore();
  const mySource = sources.find(s => s.source.includes('MyExt'));
  
  if (!mySource) return null;
  
  return (
    <Badge variant="outline" className="ml-2">
      {mySource.quality} · {mySource.source}
    </Badge>
  );
}
```

### Settings Panel

```tsx
// pages/MySettingsPanel.tsx
import { ExtensionSettingsBase } from '@/features/extensions/components/ExtensionSettingsBase';

export function MySettingsPanel() {
  return (
    <ExtensionSettingsBase extensionId="my-extension">
      <SettingsSection title="My Feature Settings">
        <SettingToggle
          label="Enable Feature"
          key="myFeatureEnabled"
          default={true}
        />
        <SettingSelect
          label="Default Quality"
          key="defaultQuality"
          options={['1080p', '720p', '480p']}
          default="1080p"
        />
      </SettingsSection>
    </ExtensionSettingsBase>
  );
}
```

***

## Using App's React Instance

{% hint style="danger" %}
**Critical:** Renderer bundle **MUST** use the app's React instance (passed via `ctx.React`), never bundle its own. A second React instance breaks hooks and context.
{% endhint %}

```ts
// renderer.js - CORRECT
export async function activate(ctx) {
  const { React } = ctx;  // Use app's React
  const MyComponent = React.memo(() => <div>Hello</div>);
  ctx.registry.registerPage({ ..., component: MyComponent });
}
```

```ts
// WRONG - bundles own React
import React from 'react';  // ❌ Breaks hooks/context
```

The loader publishes `window.React = React` as a belt-and-suspenders fallback.

***

## Navigation & Context

```ts
// ExtensionContext.navigate
ctx.navigate('/my-feature');  // Programmatic navigation

// ExtensionContext.storage (namespaced localStorage)
ctx.storage.set('key', 'value');
const val = ctx.storage.get('key');

// ExtensionContext.log (namespaced console)
ctx.log('MyExtension', 'Page mounted');
```

***

## Unloading

```ts
// renderer.js
export async function activate(ctx) {
  // ... register pages/slots
  
  return () => {
    // Cleanup: unregister, cancel timers, close connections
    ctx.registry.unregisterAll(ctx.extensionId); // Auto-called by loader
  };
}
```

The loader calls `dispose()` on unload and runs `extensionRegistry.unregisterAll(extensionId)`.

***

## Testing Checklist

* [ ] Uses `ctx.React` (not bundled React)
* [ ] Page registers with valid `path`, `component`, `label`
* [ ] Slot registers with valid predefined `slotId`
* [ ] Slot component renders without props
* [ ] Multiple slots per `slotId` render in order
* [ ] Settings panel uses `ExtensionSettingsBase`
* [ ] Namespaced `storage` works
* [ ] `navigate` works for programmatic routing
* [ ] `dispose` cleans up timers/subscriptions
* [ ] Works in web, desktop, and mobile builds

***

## Related

* [Extension Overview](/extensions/extension-overview.md)
* [Manifest Reference](/extensions/extension-manifest.md)
* [Services/Modules](/extensions/extension-capabilities/services-and-modules.md)
* [Renderer Loader](/extensions/renderer-loader.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.tatakai.me/extensions/extension-capabilities/ui-slots.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
