# Blazing Story Addon

> Implement a custom addon for a Blazing Story application. Use when the user asks to create, add, or implement an addon with specific functionality — such as toolbar buttons, panel tabs, or preview decorators — in a Blazing Story (.NET / Blazor / Storybook) project.

- Skill: `blazingstory/blazing-story-addon` (Agent Skill)
- Install (CLI): `npx skillmds@latest add blazingstory/blazing-story-addon`
- Raw SKILL.md: https://api.skillmd.com/api/skills/blazingstory/blazing-story-addon/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Unlicense
- Author: BlazingStory (https://skillmd.com/u/blazingstory)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/blazingstory/blazing-story-addon

---


# Blazing Story — Addon Implementation

Create and register a custom addon in the currently open Blazing Story project.

## Investigation policy

The main goal of this policy is to free the developer from the hassle of approving "may I run this command?" prompts one by one. Many of those prompts come from operations that poke around outside the project — and most of the knowledge needed to build an addon is already available without them.

Implement the addon relying primarily on:

- The guidance in this skill file
- Your own knowledge of C#, .NET, Blazor, and general web/UI development
- Other relevant skills available in this environment
- Already-configured MCP servers and tools
- Read-only exploration of the current project (`ls`, `Glob`, `Grep`, `Read`)

**Avoid** operations that inspect the NuGet package cache folder, decompile Blazing Story DLLs, or otherwise probe the installed package contents. These are slow, require the developer's per-command approval, and disrupt the flow of work.

If implementation details that are not covered above become necessary, consult the published source code on GitHub at https://github.com/jsakamoto/BlazingStory **instead of** digging into the local NuGet cache or decompiling DLLs.

This policy may be relaxed only when strictly unavoidable.

## Step 1: Understand the requirements

From `$ARGUMENTS` or the user's message, identify:

- **What UI to add**: toolbar button/toggle, popup menu, panel tab, preview decorator, or a combination
- **What behavior is needed**: toggle state, CSS injection, JavaScript invocation, panel content, etc.
- **Whether toolbar ↔ decorator communication is needed** (i.e., toolbar action affects the preview frame)

## Step 2: Locate the stories project

Find the stories project (typically `*.Stories/`) and its `App.razor` or equivalent file that contains `<BlazingStoryApp>`. This is where the addon will be registered.

Also check for an existing `_Imports.razor` to understand which namespaces are globally available.

## Step 3: Determine which components to create

An addon can consist of up to three Razor components:

| Component type | When to create |
|---|---|
| **Toolbar content** | When you need a button, toggle, or menu in the top toolbar |
| **Panel** | When you need a new tab in the bottom panel area |
| **Preview decorator** | When you need to inject CSS/JS or react to toolbar state inside the preview frame |

Create only the components that are needed. A simple toggle+CSS addon needs just a toolbar component and a decorator.

## Step 4: Create the Razor components

### Folder convention

Place all files for one addon in a dedicated subfolder inside the stories project:

```
MyApp.Stories/
└── Addons/
    └── MyFeature/
        ├── MyFeatureToolbarContent.razor
        ├── MyFeaturePanel.razor          (if needed)
        └── MyFeaturePreviewDecorator.razor  (if needed)
```

---

### Toolbar content component

Receives mutable `GlobalArguments` as a cascading parameter. Write to it to propagate state to the preview decorator.

```razor
@using BlazingStory.Addons
@using BlazingStory.ToolKit.Buttons
@using BlazingStory.ToolKit.Icons
@inject IJSRuntime JSRuntime

<IconButton Icon="SvgIconType.Grid"
            Title="Toggle my feature"
            Active="@_enabled"
            OnClick="OnClick" />

@code {
    [CascadingParameter]
    public GlobalArguments Globals { get; set; } = default!;

    private bool _enabled = false;

    private void OnClick()
    {
        _enabled = !_enabled;
        Globals["myfeature.enabled"] = _enabled ? "true" : null;
    }
}
```

**Key rules for toolbar content:**
- Cascading parameter type is `GlobalArguments` (mutable, from `BlazingStory.Addons`).
- Use `Globals["key"] = value` to share state with the decorator. Keys are arbitrary strings; use namespaced names (e.g., `"myaddon.key"`).
- Values stored in `GlobalArguments` are serialized as strings in the decorator — booleans become `"True"`/`"False"` or use `"true"`/`null` explicitly.
- To persist state across page loads, use `@inject IJSRuntime JSRuntime` and call localStorage (e.g., pattern used in built-in addons).
- Use `ToolKit` components for visual consistency (see ToolKit section below).

---

### Panel component

Receives `IStory` as a cascading parameter. Use `<PanelTitle>` for the tab label.

```razor
@using BlazingStory.Abstractions
@using BlazingStory.Addons

<PanelTitle>
    My Panel
</PanelTitle>

<div class="my-panel">
    <!-- panel content here -->
</div>

@code {
    [CascadingParameter(Name = "Story")]
    public IStory? Story { get; set; }
}
```

**Key rules for panels:**
- `<PanelTitle>` content is rendered as the tab label via a `SectionContent` mechanism — it does not appear inline in the component output.
- The `Story` cascading parameter gives access to the currently selected story metadata.
- Scoped CSS is **not supported** in addon components. Use a regular `.css` file and load it with `<ImportStyleSheet Href="..." />` or a `<link>` tag.

---

### Preview decorator component

Rendered alongside (not wrapping) the story in the preview frame. Receives read-only cascading parameters.

```razor
@using BlazingStory.Abstractions

@code {
    [CascadingParameter(Name = "Globals")]
    public IReadOnlyDictionary<string, string>? Globals { get; set; }

    [CascadingParameter(Name = "Args")]
    public IReadOnlyDictionary<string, string>? Args { get; set; }

    [CascadingParameter(Name = "Story")]
    public IStory? Story { get; set; }
}
```

To inject a conditional `<style>` or invoke JS based on toolbar state:

```razor
@inject IJSRuntime JSRuntime

@if (_enabled)
{
    <style>* { outline: 1px solid red; }</style>
}

@code {
    [CascadingParameter(Name = "Globals")]
    public IReadOnlyDictionary<string, string>? Globals { get; set; }

    private bool _enabled = false;

    protected override async Task OnParametersSetAsync()
    {
        _enabled = Globals?.TryGetValue("myfeature.enabled", out var v) == true && v == "true";
    }
}
```

**Key rules for preview decorators:**
- Cascading parameter type is `IReadOnlyDictionary<string, string>?` (read-only, string values only).
- Values from `GlobalArguments` arrive as strings. Booleans written as `"true"`/`null` can be checked with `v == "true"`.
- The decorator is a **sibling** to the story component, not a wrapper around it.
- Scoped CSS is **not supported** — use inline `<style>` tags or `ImportStyleSheet`.

### `IStory` cascading parameter reference

Both the panel and the preview decorator receive the currently selected story as a `[CascadingParameter(Name = "Story")] IStory? Story`. Members on `BlazingStory.Abstractions.IStory`:

| Member | Type | Description |
|---|---|---|
| `Title` | `string` | Display title of the story (e.g., `"Examples/UI/Button"`). |
| `Name` | `string` | Name of this story (e.g., `"Primary"`). |
| `Description` | `RenderFragment?` | Optional descriptive content render fragment. |
| `ComponentType` | `Type` | CLR type of the target UI component. |
| `StoriesRazorDescriptor` | `StoriesRazorDescriptor` | Descriptor of the Stories Razor component defining this story. |
| `Context` | `IStoryContext` | Arguments and parameter state for the story (see below). |
| `NavigationPath` | `string` | Navigation path string (e.g., `"examples-ui-button--primary"`). |

`IStory.Context` exposes `IStoryContext` with:

| Member | Type | Description |
|---|---|---|
| `Args` | `IReadOnlyDictionary<string, object?>` | Current argument values keyed by parameter name. |
| `Parameters` | `IEnumerable<IComponentParameter>` | Component parameters associated with this story. |
| `ArgumentChanged` | `event AsyncEventHandler?` | Raised when any argument value changes. |
| `ArgumentsReset` | `event AsyncEventHandler?` | Raised when arguments are reset to defaults. |
| `ShouldRender` | `event EventHandler?` | Raised to request a re-render of the story. |
| `GetNoEventParameterCount()` | `int` | Count of parameters that are not event callbacks. |
| `InitArgument(name, value)` | `void` | Initialize an argument with a name and value. |
| `ResetArgumentsAsync()` | `ValueTask` | Reset all arguments to their defaults. |
| `AddOrUpdateArgumentAsync(name, newValue)` | `ValueTask` | Add or update an argument value. |
| `InvokeShouldRender()` | `void` | Notify the story that it should re-render. |

Each `IComponentParameter` in `Context.Parameters` exposes:

| Member | Type | Description |
|---|---|---|
| `Name` | `string` | Parameter name. |
| `Type` | `Type` | CLR type of the parameter. |
| `TypeStructure` | `TypeStructure` | Nullability and generic structure of the parameter type. |
| `Summary` | `MarkupString` | Summary description from XML documentation. |
| `Required` | `bool` | Whether the parameter is required. |
| `Control` | `ControlType` | UI control type used to edit this parameter. |
| `DefaultValue` | `object?` | Default value of the parameter. |
| `UpdateSummaryFromXmlDocCommentAsync()` | `ValueTask` | Refresh `Summary` from the XML doc comment file. |
| `GetParameterTypeStrings()` | `IEnumerable<string>` | String representations of the parameter type. |

**Usage notes:**
- Subscribe to `Context.ArgumentChanged` / `ArgumentsReset` in `OnParametersSet` when a panel needs to re-render on argument updates; unsubscribe on `IDisposable.Dispose`.
- Treat `Args` as read-only snapshots; mutate state via `AddOrUpdateArgumentAsync` instead of writing into the dictionary.
- `Story` may be `null` before a story is selected — always null-check.

## Step 5: Create the addon class

Create a C# class implementing `IAddon` in the same folder:

```csharp
using BlazingStory.Addons;

namespace MyApp.Stories.Addons.MyFeature;

public class MyFeatureAddon : IAddon
{
    public void Initialize(IAddonBuilder builder)
    {
        builder.AddToolbarContent<MyFeatureToolbarContent>(order: 1000,
            match: viewMode => viewMode is ViewMode.Story or ViewMode.Docs);
        builder.AddPanel<MyFeaturePanel>(order: 1000,
            match: viewMode => viewMode == ViewMode.Story);
        builder.AddPreviewDecorator<MyFeaturePreviewDecorator>();
    }
}
```

**Key rules for the addon class:**
- `order` controls position within each slot. Built-in addons use 100–900. Custom addons are typically placed **after** the built-ins (on the right), so `1000+` is the common choice. However, to place a custom addon **before** the built-ins (on the left), use a value **below 100**; to place it **between** specific built-ins, pick a value that fits the desired position within the 100–900 range.
- `match` predicate controls visibility. `ViewMode` values: `Story`, `Docs`, `CustomPage`.
- `AddPreviewDecorator` has no `order` or `match` — decorators are always active.
- Omit `AddPanel`, `AddToolbarContent`, or `AddPreviewDecorator` calls for component types you are not using.

## Step 6: Register the addon

Open the `App.razor` (or equivalent) file that contains `<BlazingStoryApp>` and add the registration:

```razor
<BlazingStoryApp OnInitialize="builder => builder.Addons.Register<MyFeatureAddon>()" />
```

If `OnInitialize` already has content, extract it into a method:

```razor
<BlazingStoryApp OnInitialize="Configure" />

@code {
    private static void Configure(IBlazingStoryConfigurator builder)
    {
        builder.Addons.Register<ExistingAddon>();
        builder.Addons.Register<MyFeatureAddon>();
    }
}
```

## BlazingStory.ToolKit components

Use these components (already available in the stories project) to keep the addon UI consistent with the built-in addons:

| Component | Use for |
|---|---|
| `<IconButton Icon="SvgIconType.X" Active="..." OnClick="...">` | Toolbar toggle buttons |
| `<PopupMenu><Trigger>...</Trigger><MenuItems>...</MenuItems></PopupMenu>` | Dropdown menus in toolbar |
| `<MenuItem OnClick="..." Active="...">` | Menu items inside `<PopupMenu>` |
| `<MenuItemDivider />` | Dividers between menu item groups |
| `<Badge Text="..." />` | Count badges in panel titles |
| `<ImportStyleSheet Href="..." />` | Load/unload a stylesheet conditionally |
| `<ToolBar>`, `<TabButton>`, `<TabButtonGroup>` | Tab UIs inside panels |
| `<Separator />`, `<Spacer />` | Toolbar spacing |

## Namespace reference

Addon components and classes reference types from several BlazingStory packages. Add the matching `@using` (Razor) or `using` (C#) directive to each file that references the type. Namespace-per-type cheatsheet:

| Namespace | Types defined there |
|---|---|
| `BlazingStory.Abstractions` | `IStory`, `IStoryContext`, `IComponentParameter` |
| `BlazingStory.Addons` | `IAddon`, `IAddonBuilder`, `GlobalArguments`, `PanelTitle`, `ViewMode` |
| `BlazingStory.ToolKit.Buttons` | `IconButton`, `ToggleButton`, `SquareButton`, `CornerButton`, `ResetButton` |
| `BlazingStory.ToolKit.Icons` | `SvgIconType`, `SvgIcon`, `Badge` |
| `BlazingStory.ToolKit.Menus` | `PopupMenu`, `MenuItem`, `MenuItemDivider` |
| `BlazingStory.ToolKit.Styles` | `ImportStyleSheet` |
| `BlazingStory.ToolKit.ToolBar` | `ToolBar`, `TabButton`, `TabButtonGroup`, `Separator`, `Spacer` |
| `BlazingStory.ToolKit.Inputs` | `ColorInput`, `NumberInput`, `TextArea`, `Select`, `RadioGroup`, `NullInputRadio` |

Rules of thumb:
- Toolbar content components typically need `@using BlazingStory.Addons`, plus ToolKit namespaces for whichever UI components are used (e.g., `Buttons` + `Icons` for `<IconButton Icon="SvgIconType.X" />`).
- Panel components typically need `@using BlazingStory.Abstractions` (for `IStory`) and `@using BlazingStory.Addons` (for `<PanelTitle>`).
- Preview decorator components need `@using BlazingStory.Abstractions` only when they consume the `IStory` cascading parameter.
- Addon classes (C#) need `using BlazingStory.Addons;` (for `IAddon`, `IAddonBuilder`, `ViewMode`).

## Step 7: Verify

After creating all files, summarize:
- Files created (component(s), addon class)
- Which slots are registered (toolbar / panel / decorator) and their `order`
- The `ViewMode` match logic applied
- The registration line added to `App.razor`
- Any assumptions made (e.g., localStorage persistence omitted for simplicity)

