Blazing Story — Story Implementation
Create a .stories.razor file for a Blazor component 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 write a story file is already available without them.
Implement the story 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: Identify the target component
From $ARGUMENTS or the user's message, determine the component name (e.g., Button, Rating).
Step 2: Locate the component file
Search the workspace for a .razor file matching the component name (e.g., Button.razor). Read it to understand:
- All
[Parameter] properties and their types
- Any
RenderFragment parameters (e.g., ChildContent)
- Enum types used by parameters
Step 3: Locate the stories project
Find the stories project directory — it is typically a separate project named *.Stories or containing a Stories/ subfolder. Look for existing .stories.razor files to confirm the correct location and the @using conventions used.
Step 4: Determine the story file path
Place the new file inside the Stories/ folder of the stories project, mirroring the category structure if one already exists. Name the file ComponentName.stories.razor.
Example: MyApp.Stories/Stories/Components/Button.stories.razor
Step 5: Write the story file
Use the following structure:
@attribute [Stories("Category/ComponentName")]
@* Add @using directives only for namespaces not already imported via _Imports.razor *@
<Stories TComponent="ComponentName" Layout="typeof(CenteredLayout)">
@* Place all <ArgType> elements first, then the <Story> elements. *@
<ArgType For="_ => _.EnumParam" Control="ControlType.Radio" />
<ArgType For="_ => _.ColorParam" Control="ControlType.Color" />
<Story Name="Default">
<Arguments>
<Arg For="_ => _.SomeParam" Value="someValue" />
@* Only when the component has a RenderFragment parameter — reference a @code field: *@
<Arg For="_ => _.ChildContent" Value="_content" />
</Arguments>
<Template>
<ComponentName @attributes="context.Args" />
</Template>
</Story>
</Stories>
@code {
// Define RenderFragment values here only when the component has RenderFragment parameters.
private RenderFragment _content = @<text>Label</text>;
}
Rules
File naming
- Must end in
.stories.razor to enable the "Show code" feature in Blazing Story.
[Stories("...")] path
- Use
/ as separator. The path becomes the sidebar navigation tree.
- Mirror the folder path under
Stories/ (e.g., file at Stories/Components/Button.stories.razor → [Stories("Components/Button")]).
<Stories TComponent="...">
TComponent is the Blazor component type.
Layout is optional. Since v1.0.0-preview.81, Blazing Story ships three built-in presets in the BlazingStory.Components.Layouts namespace:
CenteredLayout — centers the component horizontally and vertically; good default for buttons, badges, icons, and compact UI elements.
FullFrameLayout — expands to fill the preview frame while retaining margins; good for panels, cards, and layout-sensitive containers.
NoMarginLayout — removes all margins so content extends edge-to-edge; good for full-bleed page shells or components that control their own spacing.
- Omit
Layout entirely if you are unsure or if no layout is needed.
- These presets can also be applied at the
<Story> level for a single variant. Layouts at different levels do not override each other — they nest: the app-level layout wraps outermost, the <Stories>-level layout wraps inside it, and the <Story>-level layout wraps the innermost layer.
- Check whether
BlazingStory.Components.Layouts is already imported via _Imports.razor; if not, add @using BlazingStory.Components.Layouts at the top of the story file.
Custom layouts
If the built-in presets don't meet your needs, create a component that @inherits LayoutComponentBase and renders @Body.
The following CSS custom properties and HTML attributes are available inside the preview frame and are useful when styling a custom layout:
| Name |
Where |
Description |
--bs-preview-body-margin |
CSS custom property on <body> |
Controls the body margin. Undefined by default; built-in layouts use var(--bs-preview-body-margin, 16px). Set to 0px to remove the margin entirely. |
--bs-zoom |
CSS custom property on <body> |
Current zoom level of the preview frame. Always reference with a fallback: var(--bs-zoom, 1). |
data-bs-parent-frame |
HTML attribute on <body> |
Frame context: "docs" when embedded in a Docs page, "story" when displayed as a standalone Story page. |
For complete implementation examples, see the built-in layout components: BlazingStory/Components/Layouts
<ArgType>
- Controls how a parameter appears in the Controls panel.
- Available
ControlType values — this is the complete, exhaustive list. Do not invent or guess any other member (e.g., there is no ControlType.Boolean, ControlType.Text, ControlType.Number, or ControlType.Toggle):
ControlType.Default — auto-detected from the parameter type (no need to specify explicitly). This already covers bool, string, and numeric parameters with a sensible built-in editor, so for those types simply omit <ArgType> entirely rather than guessing a Control value.
ControlType.Radio — radio buttons; good for enums with 2–4 values.
ControlType.Select — dropdown; good for enums with 5+ values.
ControlType.Color — color picker; use for string or Color parameters representing a color.
- If a parameter's desired UI is not covered by one of the four values above, do not fall back to guessing a plausible-sounding enum member name. Instead, either omit
<ArgType> (relying on ControlType.Default) or use a custom parameter controller (see below).
Custom parameter controllers (since v1.0.0-preview.87)
- When none of the built-in
ControlType options fit a parameter, supply your own component as the Controls-panel editor by placing it as the child content of <ArgType>:<ArgType For="_ => _.TestEnum">
<MyCustomController />
</ArgType>
When <ArgType> has child content, the custom controller renders in place of the default control; Control="..." is ignored for that parameter.
- A custom controller component must derive from
ParameterControllerBase (in the BlazingStory.Addons.BuiltIns.Panel.Controls.ParameterControllers.Controllers namespace, shipped in the BlazingStory.Addons.BuiltIns assembly):@using BlazingStory.Addons.BuiltIns.Panel.Controls.ParameterControllers.Controllers
@inherits ParameterControllerBase
@* render the editing UI for the current parameter here *@
@code {
// Read the current parameter value:
private MyEnum GetValue()
=> this.Context.Value == null ? MyEnum.None : (MyEnum)this.Context.Value;
// Write a new value back through the Controls panel:
private async Task OnChange(MyEnum newValue)
{
await this.OnInputAsync(newValue);
}
}
- Inheriting from
ParameterControllerBase gives the component two things:
this.Context — a ParameterControllerContext exposing the bound parameter. Members:
| Member |
Type |
Description |
Context.Value |
object? |
The current parameter value. Cast it to the parameter's type to read it (it may be null). |
Context.Key |
string |
Unique key identifying this controller instance. |
Context.Parameter |
IComponentParameter |
Metadata about the bound parameter. |
Context.OnInput |
EventCallback<ParameterInputEventArgs> |
The underlying input callback; normally you call OnInputAsync instead. |
this.OnInputAsync(object? value) — call this to push a UI-entered value back into the parameter so the previewed component updates.
- The controller's lifecycle behaves like any Blazor component, so use
OnInitialized/OnParametersSet for setup (e.g. enumerating enum names) and read this.Context from there onward.
- Useful when the parameter needs richer editing than a single control — for example a
[Flags] enum rendered as a group of checkboxes, where each toggle sets/clears a bit and calls OnInputAsync with the combined value.
<Story Name="...">
- Each
<Story> represents one variant shown in the sidebar.
- Always include a
"Default" story as the baseline.
- Add further stories for meaningful parameter combinations (e.g.,
"Large", "Disabled", "With Icon").
<Arguments> and <Arg>
<Template>
- Always add
@attributes="context.Args" to the component tag to wire up the Controls panel.
- Pass only parameters that cannot be handled via
@attributes (e.g., event callbacks, non-parameter child content) directly in the markup.
Null-forgiving operator
@using directives
- Check whether the component namespace is already imported globally (e.g., via
_Imports.razor). Add @using only if needed.
Step 6: Verify
After writing the file, briefly summarize:
- The file path created
- The stories added and which parameter variants they cover
- Any
ArgType customizations applied
- Any assumptions made (e.g., chosen
Layout, omitted optional parameters)
1---2name: blazing-story-story3description: Implement a Blazing Story story file (.stories.razor) for a Blazor UI component. Use when the user says "create a story for component X", "add stories for X", or similar requests in a Blazing Story (.NET / Blazor / Storybook) project.4license: Unlicense5---67# Blazing Story — Story Implementation89Create a `.stories.razor` file for a Blazor component in the currently open Blazing Story project.1011## Investigation policy1213The 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 write a story file is already available without them.1415Implement the story relying primarily on:1617- The guidance in this skill file18- Your own knowledge of C#, .NET, Blazor, and general web/UI development19- Other relevant skills available in this environment20- Already-configured MCP servers and tools21- Read-only exploration of the current project (`ls`, `Glob`, `Grep`, `Read`)2223**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.2425If 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.2627This policy may be relaxed only when strictly unavoidable.2829## Step 1: Identify the target component3031From `$ARGUMENTS` or the user's message, determine the component name (e.g., `Button`, `Rating`).3233## Step 2: Locate the component file3435Search the workspace for a `.razor` file matching the component name (e.g., `Button.razor`). Read it to understand:3637- All `[Parameter]` properties and their types38- Any `RenderFragment` parameters (e.g., `ChildContent`)39- Enum types used by parameters4041## Step 3: Locate the stories project4243Find the stories project directory — it is typically a separate project named `*.Stories` or containing a `Stories/` subfolder. Look for existing `.stories.razor` files to confirm the correct location and the `@using` conventions used.4445## Step 4: Determine the story file path4647Place the new file inside the `Stories/` folder of the stories project, mirroring the category structure if one already exists. Name the file `ComponentName.stories.razor`.4849Example: `MyApp.Stories/Stories/Components/Button.stories.razor`5051## Step 5: Write the story file5253Use the following structure:5455```razor56@attribute [Stories("Category/ComponentName")]5758@* Add @using directives only for namespaces not already imported via _Imports.razor *@5960<Stories TComponent="ComponentName" Layout="typeof(CenteredLayout)">6162 @* Place all <ArgType> elements first, then the <Story> elements. *@63 <ArgType For="_ => _.EnumParam" Control="ControlType.Radio" />64 <ArgType For="_ => _.ColorParam" Control="ControlType.Color" />6566 <Story Name="Default">67 <Arguments>68 <Arg For="_ => _.SomeParam" Value="someValue" />69 @* Only when the component has a RenderFragment parameter — reference a @code field: *@70 <Arg For="_ => _.ChildContent" Value="_content" />71 </Arguments>72 <Template>73 <ComponentName @attributes="context.Args" />74 </Template>75 </Story>7677</Stories>7879@code {80 // Define RenderFragment values here only when the component has RenderFragment parameters.81 private RenderFragment _content = @<text>Label</text>;82}83```8485### Rules8687**File naming**88- Must end in `.stories.razor` to enable the "Show code" feature in Blazing Story.8990**`[Stories("...")]` path**91- Use `/` as separator. The path becomes the sidebar navigation tree.92- Mirror the folder path under `Stories/` (e.g., file at `Stories/Components/Button.stories.razor` → `[Stories("Components/Button")]`).9394**`<Stories TComponent="...">`**95- `TComponent` is the Blazor component type.96- `Layout` is optional. Since v1.0.0-preview.81, Blazing Story ships three built-in presets in the `BlazingStory.Components.Layouts` namespace:97 - `CenteredLayout` — centers the component horizontally and vertically; good default for buttons, badges, icons, and compact UI elements.98 - `FullFrameLayout` — expands to fill the preview frame while retaining margins; good for panels, cards, and layout-sensitive containers.99 - `NoMarginLayout` — removes all margins so content extends edge-to-edge; good for full-bleed page shells or components that control their own spacing.100 - Omit `Layout` entirely if you are unsure or if no layout is needed.101- These presets can also be applied at the `<Story>` level for a single variant. Layouts at different levels do **not** override each other — they **nest**: the app-level layout wraps outermost, the `<Stories>`-level layout wraps inside it, and the `<Story>`-level layout wraps the innermost layer.102- Check whether `BlazingStory.Components.Layouts` is already imported via `_Imports.razor`; if not, add `@using BlazingStory.Components.Layouts` at the top of the story file.103104**Custom layouts**105- If the built-in presets don't meet your needs, create a component that `@inherits LayoutComponentBase` and renders `@Body`.106- The following CSS custom properties and HTML attributes are available inside the preview frame and are useful when styling a custom layout:107108 | Name | Where | Description |109 |---|---|---|110 | `--bs-preview-body-margin` | CSS custom property on `<body>` | Controls the body margin. Undefined by default; built-in layouts use `var(--bs-preview-body-margin, 16px)`. Set to `0px` to remove the margin entirely. |111 | `--bs-zoom` | CSS custom property on `<body>` | Current zoom level of the preview frame. Always reference with a fallback: `var(--bs-zoom, 1)`. |112 | `data-bs-parent-frame` | HTML attribute on `<body>` | Frame context: `"docs"` when embedded in a Docs page, `"story"` when displayed as a standalone Story page. |113114- For complete implementation examples, see the built-in layout components: [BlazingStory/Components/Layouts](https://github.com/jsakamoto/BlazingStory/tree/main/BlazingStory/Components/Layouts)115116**`<ArgType>`**117- Controls how a parameter appears in the Controls panel.118- Available `ControlType` values — this is the **complete, exhaustive list**. Do not invent or guess any other member (e.g., there is no `ControlType.Boolean`, `ControlType.Text`, `ControlType.Number`, or `ControlType.Toggle`):119 - `ControlType.Default` — auto-detected from the parameter type (no need to specify explicitly). This already covers `bool`, `string`, and numeric parameters with a sensible built-in editor, so for those types simply omit `<ArgType>` entirely rather than guessing a `Control` value.120 - `ControlType.Radio` — radio buttons; good for enums with 2–4 values.121 - `ControlType.Select` — dropdown; good for enums with 5+ values.122 - `ControlType.Color` — color picker; use for `string` or `Color` parameters representing a color.123- If a parameter's desired UI is not covered by one of the four values above, do not fall back to guessing a plausible-sounding enum member name. Instead, either omit `<ArgType>` (relying on `ControlType.Default`) or use a **custom parameter controller** (see below).124125**Custom parameter controllers (since v1.0.0-preview.87)**126- When none of the built-in `ControlType` options fit a parameter, supply your own component as the Controls-panel editor by placing it as the child content of `<ArgType>`:127 ```razor128 <ArgType For="_ => _.TestEnum">129 <MyCustomController />130 </ArgType>131 ```132 When `<ArgType>` has child content, the custom controller renders in place of the default control; `Control="..."` is ignored for that parameter.133- A custom controller component must derive from `ParameterControllerBase` (in the `BlazingStory.Addons.BuiltIns.Panel.Controls.ParameterControllers.Controllers` namespace, shipped in the `BlazingStory.Addons.BuiltIns` assembly):134 ```razor135 @using BlazingStory.Addons.BuiltIns.Panel.Controls.ParameterControllers.Controllers136 @inherits ParameterControllerBase137138 @* render the editing UI for the current parameter here *@139140 @code {141 // Read the current parameter value:142 private MyEnum GetValue()143 => this.Context.Value == null ? MyEnum.None : (MyEnum)this.Context.Value;144145 // Write a new value back through the Controls panel:146 private async Task OnChange(MyEnum newValue)147 {148 await this.OnInputAsync(newValue);149 }150 }151 ```152- Inheriting from `ParameterControllerBase` gives the component two things:153 - `this.Context` — a `ParameterControllerContext` exposing the bound parameter. Members:154155 | Member | Type | Description |156 |---|---|---|157 | `Context.Value` | `object?` | The current parameter value. Cast it to the parameter's type to read it (it may be `null`). |158 | `Context.Key` | `string` | Unique key identifying this controller instance. |159 | `Context.Parameter` | `IComponentParameter` | Metadata about the bound parameter. |160 | `Context.OnInput` | `EventCallback<ParameterInputEventArgs>` | The underlying input callback; normally you call `OnInputAsync` instead. |161162 - `this.OnInputAsync(object? value)` — call this to push a UI-entered value back into the parameter so the previewed component updates.163- The controller's lifecycle behaves like any Blazor component, so use `OnInitialized`/`OnParametersSet` for setup (e.g. enumerating enum names) and read `this.Context` from there onward.164- Useful when the parameter needs richer editing than a single control — for example a `[Flags]` enum rendered as a group of checkboxes, where each toggle sets/clears a bit and calls `OnInputAsync` with the combined value.165166**`<Story Name="...">`**167- Each `<Story>` represents one variant shown in the sidebar.168- Always include a `"Default"` story as the baseline.169- Add further stories for meaningful parameter combinations (e.g., `"Large"`, `"Disabled"`, `"With Icon"`).170171**`<Arguments>` and `<Arg>`**172- Use `<Arg For="_ => _.ParamName" Value="..." />` to set initial parameter values for a story.173- For `RenderFragment` parameters, define the value in the `@code` block and reference it via `<Arg>`:174 ```razor175 <Arg For="_ => _.ChildContent" Value="_content" />176177 @code {178 private RenderFragment _content = @<text>Click me</text>;179 }180 ```181- Do **not** hardcode `RenderFragment` content directly in the `<Template>` markup — this prevents runtime modification via the Controls panel.182183**`<Template>`**184- Always add `@attributes="context.Args"` to the component tag to wire up the Controls panel.185- Pass only parameters that cannot be handled via `@attributes` (e.g., event callbacks, non-parameter child content) directly in the markup.186187**Null-forgiving operator**188- When the component type is nullable, use `_=>_!.PropertyName` in `For` lambdas:189 ```razor190 <ArgType For="_=>_!.Color" Control="ControlType.Color" />191 ```192193**`@using` directives**194- Check whether the component namespace is already imported globally (e.g., via `_Imports.razor`). Add `@using` only if needed.195196## Step 6: Verify197198After writing the file, briefly summarize:199- The file path created200- The stories added and which parameter variants they cover201- Any `ArgType` customizations applied202- Any assumptions made (e.g., chosen `Layout`, omitted optional parameters)