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.
@using BlazingStory.Addons
@using BlazingStory.ToolKit.Buttons
@using BlazingStory.ToolKit.Icons
@inject IJSRuntime JSRuntime
<IconButton Icon="SvgIconType.Grid"
Title="Toggle my feature"
Active="@_enabled"
/>
@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, fromBlazingStory.Addons). - Use
Globals["key"] = valueto share state with the decorator. Keys are arbitrary strings; use namespaced names (e.g.,"myaddon.key"). - Values stored in
GlobalArgumentsare serialized as strings in the decorator — booleans become"True"/"False"or use"true"/nullexplicitly. - To persist state across page loads, use
@inject IJSRuntime JSRuntimeand call localStorage (e.g., pattern used in built-in addons). - Use
ToolKitcomponents for visual consistency (see ToolKit section below).
Panel component
Receives IStory as a cascading parameter. Use <PanelTitle> for the tab label.
@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 aSectionContentmechanism — it does not appear inline in the component output.- The
Storycascading parameter gives access to the currently selected story metadata. - Scoped CSS is not supported in addon components. Use a regular
.cssfile 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.
@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:
@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
GlobalArgumentsarrive as strings. Booleans written as"true"/nullcan be checked withv == "true". - The decorator is a sibling to the story component, not a wrapper around it.
- Scoped CSS is not supported — use inline
<style>tags orImportStyleSheet.
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/ArgumentsResetinOnParametersSetwhen a panel needs to re-render on argument updates; unsubscribe onIDisposable.Dispose. - Treat
Argsas read-only snapshots; mutate state viaAddOrUpdateArgumentAsyncinstead of writing into the dictionary. Storymay benullbefore a story is selected — always null-check.
Step 5: Create the addon class
Create a C# class implementing IAddon in the same folder:
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:
ordercontrols position within each slot. Built-in addons use 100–900. Custom addons are typically placed after the built-ins (on the right), so1000+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.matchpredicate controls visibility.ViewModevalues:Story,Docs,CustomPage.AddPreviewDecoratorhas noorderormatch— decorators are always active.- Omit
AddPanel,AddToolbarContent, orAddPreviewDecoratorcalls 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:
<BlazingStoryApp => builder.Addons.Register<MyFeatureAddon>()" />
If OnInitialize already has content, extract it into a method:
<BlazingStoryApp />
@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="..."> |
Toolbar toggle buttons |
<PopupMenu><Trigger>...</Trigger><MenuItems>...</MenuItems></PopupMenu> |
Dropdown menus in toolbar |
<MenuItem 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+Iconsfor<IconButton Icon="SvgIconType.X" />). - Panel components typically need
@using BlazingStory.Abstractions(forIStory) and@using BlazingStory.Addons(for<PanelTitle>). - Preview decorator components need
@using BlazingStory.Abstractionsonly when they consume theIStorycascading parameter. - Addon classes (C#) need
using BlazingStory.Addons;(forIAddon,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
ViewModematch logic applied - The registration line added to
App.razor - Any assumptions made (e.g., localStorage persistence omitted for simplicity)