<!-- demo:embed example="extensions/1-custom-widget" -->

You've built a custom Element (C1) and a custom Control (C2). The third extensibility surface is the **Widget** — the palette item the buyer drags from the left rail onto the canvas. A widget bundles "the Element class + its preset content + a palette icon" into one drag-droppable thing. **Time budget: 30 minutes.**

This is the smallest of the three extension surfaces. If your project only needs one new component, a Widget is often the right shape — it's faster to ship than a custom Element + Control combo.

## What a Widget IS

A Widget is **a class that wraps a pre-built block of content + a palette icon + a label**. The buyer drags the icon, drops a fully-formed block onto the canvas. The shipped catalogue has 20+ widgets — Heading, Paragraph, Image, Button, Divider, List, Icon, Social, RSS, …. Each is ≈ 50-100 LOC.

Subclass `BaseWidget`:

```javascript
class CalloutWidget extends BaseWidget {
    constructor() {
        super();
        // Build the block this widget will drop onto the canvas
        // when dragged — a heading + a paragraph in a single Block.
        this.block = new BlockElement([
            new HeadingElement({ text: 'Important update', formats: { font_size: 24 } }),
            new PElement({ text: 'Read this carefully — it changes how you work.' }),
        ]);
    }

    getName() { return 'Callout'; }
    getIcon() { return 'campaign'; }
}
```

The 3 mandatory hooks:

- **`constructor()`** — build `this.block`, the `BlockElement` (or any single Element subclass) the buyer drops onto the canvas.
- **`getName()`** — palette label. Routes through `I18n.t('widgets.<key>')` for translated builds (see [i18n](/docs/14-i18n/)).
- **`getIcon()`** — Material Symbols Rounded icon name (`campaign`, `image`, `format_list_bulleted`, etc.). Browsers render the ligature via the font.

## Pre-flight

Confirm the demo runs:

<!-- buyer:try -->
```bash
php -S localhost:8000 -t demo/
```

Open <http://localhost:8000/examples/extensions/1-custom-widget/>. You should see a Callout palette item in the left rail. Drag it onto the canvas — a pre-built heading + paragraph block lands. Every step below reproduces this.

## Step 1 — Subclass `BaseWidget`

<!-- buyer:try -->
```javascript
class CalloutWidget extends BaseWidget {
    constructor() {
        super();
        this.block = new BlockElement([
            new HeadingElement({
                text: 'Important update',
                formats: { font_size: 24, font_weight: 700 }
            }),
            new PElement({
                text: 'This is the body of the callout. Edit me.',
                formats: { font_size: 14, line_height: 22 }
            }),
        ]);
    }

    getName() { return 'Callout'; }
    getIcon() { return 'campaign'; }
}
```

The `this.block` you build is the canonical **template content** — when the buyer drags the icon onto canvas, the engine clones this block, gives the clone its own ids, and inserts it. The clone is fully editable (controls + drag + delete) — your prebuilt content is just the seed.

## Step 2 — Register with the palette

The widgets palette doesn't auto-discover — you call `addWidget()` after `Builder` boots. Inside the load callback so the engine has registered its stock widgets first:

<!-- buyer:try -->
```javascript
const builder = new Builder({
    mainContainer:    '#MyCanvas',
    widgetsContainer: '#MyWidgets',
    settingsContainer: '#MySettings',
});

builder.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL, () => {
    builder.widgetsBox.addWidget(new CalloutWidget(), { group: 'Custom' });
    builder.widgetsBox.render();
});
```

Two non-obvious points (W6.C.B.3 + W6.C.14 lessons):

- **The palette is empty by default unless you call `addWidget()`** — even for the stock widgets like Heading + Image. The shipped `demo/builder.php` registers them in its boot block; if you're authoring your own host page, you'll need to register every widget you want surfaced.
- **`render()` after every batch of `addWidget` calls.** Otherwise the palette DOM doesn't update until the next mutation.

The `group` option (`'Custom'` here) becomes the section heading in the palette UI; the buyer sees groups like "Layout", "Content", "Forms", "Custom".

## Step 3 — Per-widget palette polish (optional)

If you want a custom palette icon style (a rounded chip vs the plain icon), use the `[widget-class="<ClassName>"]` styling hook:

<!-- buyer:try -->
```css
[widget-class="CalloutWidget"] {
    /* Brand-colored chip behind the icon */
    background: linear-gradient(135deg, #5b21b6, #a855f7);
    color: #ffffff;
    border-radius: 6px;
}

[widget-class="CalloutWidget"]:hover {
    box-shadow: 0 2px 8px rgba(91, 33, 182, 0.3);
}
```

The engine renders each palette item with `widget-class="<className>"` as an attribute, giving you a deterministic CSS hook per widget without touching the engine's stylesheet.

## Step 4 — i18n (optional)

If your build supports multiple languages, route the palette label through `I18n.t()`:

<!-- buyer:try -->
```javascript
class CalloutWidget extends BaseWidget {
    /* …constructor… */
    getName() { return I18n.t('widgets.callout'); }
}
```

Add the key to your locale dict:

```javascript
I18n.init({
    'widgets.callout': 'Callout',  // English
    // 'widgets.callout': 'Mise en avant',  // French build
    // 'widgets.callout': 'Highlight',      // Spanish build
});
```

There are **two i18n key namespaces** (W6.C.14 lesson): palette labels route through `widgets.*`; canvas + panel labels route through `elements.*`. A locale pack must translate both — translating only one and you get a half-translated UI. [Multi-language locale packs](/docs/14-i18n/) covers the full story.

## Step 5 — Test

<!-- buyer:verify -->
1. Open the builder. The Callout widget appears under "Custom" in the palette.
2. Drag it onto the canvas. The pre-built block lands; the clone is editable.
3. Edit the heading. Save. Reload. The edited content survives.
4. Drag a second Callout next to the first. Each is independent (no shared state).

If step 1 fails (the widget isn't in the palette), confirm `addWidget` was called inside the `load()` callback (or after `load()`'s promise resolves). If you call it before `load()` runs, the palette renders empty because `widgetsBox` doesn't exist yet.

## Did you make it?

You should now have:

- ✅ A `CalloutWidget` class subclassing `BaseWidget` with the 3 hooks (`constructor`, `getName`, `getIcon`).
- ✅ `this.block` built in the constructor with the prebuilt content.
- ✅ Widget registered in the load callback via `widgetsBox.addWidget(new CalloutWidget(), { group: 'Custom' })`.
- ✅ `widgetsBox.render()` called after the batch.
- ✅ Drag-drop working: dragging the icon drops a clone of `this.block` onto the canvas.

If a step didn't work:

- 🩹 *Widget not appearing in palette* → it was registered too early (before `load()` callback) or `widgetsBox.render()` wasn't called.
- 🩹 *Drag does nothing* → confirm `widgetsContainer` constructor option points at your real DOM element. If `null`, the widgets box doesn't render at all.
- 🩹 *Drop creates a block but the heading text is wrong* → confirm your `this.block`'s `HeadingElement` constructor received the right args. The engine mostly takes the constructor verbatim.
- 🩹 *Palette items load but no styling on your custom one* → use the `[widget-class="…"]` selector, not a class on the widget element directly (the engine's DOM ships with `widget-class` as an attribute).

What's next?

- → [Custom overlays (advanced)](/docs/12-overlay-dev/) — per-element on-canvas affordances (resize handles, drag indicators, image crop).
- 🔗 [`examples/extensions/1-custom-widget/`](/examples/extensions/1-custom-widget/) — the runnable Callout end-to-end.
- 🔗 `src/includes/BaseWidget.js` — the parent class.
- 🔗 `src/includes/HeadingWidget.js` + `ParagraphWidget.js` — engine-shipped widgets for reference.

Stuck?

- 🐛 grep `src/includes/<Name>Widget.js` — the catalogue of shipped widgets is your library of working examples.
- 🐛 file an issue with: your CalloutWidget class, the boot block calling `addWidget`, console errors.
