# Surfaces

> A popup, a side panel, both or neither — how to start each one, and what the buyer sees.

An extension can show itself in a **popup** (the small window under the toolbar button), a **side panel** (the column Chrome docks beside the page), both, or neither — a tool that works from a context menu, a keyboard shortcut or a content script has no page of its own at all.

Toolaby works the same way on every one of them. Pick the case below that is yours.

## The one rule that decides most of this

**Chrome gives the toolbar click to a popup whenever the manifest names one, and never opens two surfaces at once.** So:

| Your extension         | What the toolbar button does                                                                                                   |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| A popup                | Opens the popup.                                                                                                               |
| A side panel, no popup | Opens the side panel — the platform sets that up for you.                                                                      |
| Both                   | Opens the popup. The side panel needs its own way in: a button in your popup, a keyboard command, or Chrome's side-panel menu. |
| Neither                | Nothing. Your own code decides when to sell.                                                                                   |

***

## 1. I want an extension with no popup

Nothing of yours opens on a click; the work happens in the background, a content script, a context menu or a shortcut.

```bash
npx -y toolaby@latest create <tool-id> --surfaces none
```

In an extension that already exists, `npx -y toolaby@latest wire <tool-id>` sees there is no popup and writes no page of the platform's. There is nothing to stand in front of.

The gate goes where the work is:

```js
const permit = await toolaby.gate();
if (!permit.allowed) {
  // Nothing is on screen to explain it, so say it yourself:
  await toolaby.openPaymentPage();      // the checkout page, with the plans
  return;
}
```

`permit.reason` is `sign_in`, `paid`, `limit`, `feature`, `count` or `refused` — [Objects](https://toolaby.app/docs/reference/objects.md#permit) has the shapes. A notification, a badge on the icon or a message in your own content script are all fair; the platform does not put anything on screen unless you give it a page.

**A middle way:** if you want a buyer to see the plans when they click the icon, but you have no popup of your own, let the platform's popup *be* the popup. `wire` writes no Toolaby popup for an extension without a popup, so get its two files first: on the tool's **Set up** page, select **Wire it by hand**, then **Download the files**, and put `toolaby-popup.html` and `toolaby-popup.js` beside your manifest. Then name it in the manifest and leave `appPopup` empty:

```json
"action": { "default_popup": "toolaby-popup.html" }
```

```js
// toolaby.config.js
appPopup: '',   // nothing of yours to open on to — the panel stays
```

The click then always shows the panel: signed out, the sign-in doors; metered, the meter; unpaid, the plans; unlocked, the plan held and the account.

## 2. I want an extension with only a popup

```bash
npx -y toolaby@latest create <tool-id> --surfaces popup
```

Or, in an extension that already has one:

```bash
npx -y toolaby@latest wire <tool-id>
```

The wiring makes `toolaby-popup.html` the action popup and remembers yours as `appPopup`. At every open the platform's popup either **opens your popup** at once — the buyer never sees the platform's — or, when the person may not use the extension yet, shows the panel: sign in, the meter, the plans. Opening spends no free use where your code counts uses with `gate()` (`countOpens: false`). `wire` adds one thing to your popup: the [account bar](https://toolaby.app/docs/toolaby-popup.md#the-account-bar) at its foot, between Toolaby's markers, in a plain extension; a WXT project gets it from the module. `accountBar: false` in `toolaby.config.js` keeps it out.

**Or draw the panel inside your own popup** and keep the platform's page out of the way: what Plasmo needs, or a design of your own. Draw it only when the person may not use the extension yet, or after a refusal; while they may, the popup is your extension, with the [account line](https://toolaby.app/docs/toolaby-popup.md#the-account-bar) at its foot:

```js
appPopup: '',   // no page of the platform's in front
```

```js
import { toolaby, loadPaywall, applyBrand, ensureStyles, renderPaywall, renderUnlocked, refusalOf } from './toolaby.js';
```

[Frameworks](https://toolaby.app/docs/frameworks.md#plasmo) has the whole page, written out.

## 3. I want an extension with a popup and a side panel

```bash
npx -y toolaby@latest create <tool-id>
```

Both is the default. In an extension that already has both, `npx -y toolaby@latest wire <tool-id>` finds them — `side_panel.default_path` in a plain manifest, `entrypoints/sidepanel.html` in WXT, the `defineManifest` source in CRXJS — and stands one page of the platform's in front of each, remembering them as `appPopup` and `appSidePanel`.

The toolbar click stays the popup's, so **give the side panel a door**. From your popup:

```js
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
await chrome.sidePanel.open({ tabId: tab.id });   // needs a click, which this is
window.close();
```

`create` writes exactly that button into the popup it makes, and hides it when there is no side panel beside it.

## 4. I want an extension with only a side panel

```bash
npx -y toolaby@latest create <tool-id> --surfaces side-panel
```

With a side panel and no popup, the platform tells Chrome to open the panel on the toolbar click (`sidePanel.setPanelBehavior`), so the button does what your buyer expects. Nothing to wire by hand.

In an extension that already has one, `npx -y toolaby@latest wire <tool-id>` puts `toolaby-sidepanel.html` in front of yours and adds the `sidePanel` permission.

## 5. I already have one and want to add the other

Add the page as your framework wants it (`entrypoints/sidepanel.html` in WXT; a `side_panel` key and a file in a plain manifest), then:

```bash
npx -y toolaby@latest upgrade <tool-id>
```

It re-reads the folder, writes the platform's page for the new surface and fills in `appSidePanel` (or `appPopup`). In an extension whose own pages draw the panel (`appPopup: ''`), the new page stays in front too, and the platform's is written beside it for `showPaywall()`. Rebuild and reload.

**Dropping one is the other way round, and there is a trap:** delete the page *and* the key that names it. **Chrome refuses to load an extension whose `side_panel.default_path` names a file that is not there** — the whole extension, not just the panel.

***

## What the panel looks like on each

The platform draws one panel, laid out for the surface it is on:

* **In a popup** — 336px wide, the header, then the state.
* **In a side panel** — the column fills the panel up to a readable width, centred, with the mark and what it has to say together in the middle of the column. A tall state fills it and scrolls.

**Configure → Branding → Popup** previews both, in your brand, one state at a time, and so does **Preview** on a tool's **Pricing** page.

A page of your own that draws the panel in a **side panel** must say so, or it will be sized as a 336px popup:

```html
<html data-toolaby-surface="sidepanel">
```

That is the whole difference; everything else — `loadPaywall`, the renderers, the account bar — is the same on both.

## What is the same everywhere

Whatever you open, the platform is one thing behind it: `toolaby.gate()` answers by what the tool's Free plan holds and what this device holds, `has()` asks about a feature, and the account, licences, devices and the version check are the same. Surfaces decide **where a buyer meets you**, never **what they may do** — that is [Access and features](https://toolaby.app/docs/free-plan.md).

Firefox is not supported yet: it has no `externally_connectable`, which the sign-in and purchase handoffs use. See [Frameworks](https://toolaby.app/docs/frameworks.md#other-bundlers).
