Storybook Creation Skill
Create or update Storybook stories for the Daedalus wallet application. This skill covers writing .stories.tsx files, domain wrappers, mock data, and registering stories — following existing project conventions.
Storybook Stack
| Component | Technology |
|---|---|
| Storybook | 6.4 with webpack5 builder |
| Story API | storiesOf() (legacy API — NOT CSF) |
| Knobs | @storybook/addon-knobs |
| Actions | @storybook/addon-actions |
| State Management | @dump247/storybook-state (withState) |
| Custom Addon | DaedalusMenu (theme/locale/OS switcher) |
| Styling | SCSS Modules (co-located with components) |
| i18n | react-intl (en-US, ja-JP) |
| Component Library | react-polymorph (via StoryDecorator) |
Important: Daedalus uses the
storiesOf()API, NOT Component Story Format (CSF). Do NOT useexport default { title: ... }or named exports.
Directory Structure
Centralized Stories (default)
Stories are organized by domain under storybook/stories/:
storybook/stories/
├── index.ts # Master entry — imports all story files
├── _support/ # Shared infrastructure
│ ├── StoryWrapper.tsx # Top-level wrapper (theme/locale/OS)
│ ├── StoryDecorator.tsx # react-polymorph ThemeProvider
│ ├── StoryProvider.tsx # MobX provider + mock data
│ ├── StoryLayout.tsx # Full app UI shell (sidebar/topbar)
│ ├── config.ts # Theme/locale/OS constants
│ ├── utils.ts # Mock data generators
│ └── environment.ts # Environment stubs
├── common/ # Shared widgets, dropdowns
├── wallets/ # Wallet features (send, receive, tokens...)
│ ├── _utils/ # WalletsWrapper domain decorator
│ ├── index.ts # Domain barrel import
│ ├── send/
│ │ └── WalletSend.stories.tsx
│ └── ...
├── staking/ # Delegation, stake pools
├── voting/ # Voting features
├── settings/ # Profile, preferences
├── assets/ # Native tokens, NFTs
├── news/ # Newsfeed, alerts, overlays
├── navigation/ # Sidebar, categories
├── notifications/ # System notifications
├── dapps/ # dApp connector
└── nodes/ # Node status
Co-located Stories (feature modules)
For self-contained feature modules, stories can live next to the component:
source/renderer/app/features/{feature}/ui/
├── MyComponent.tsx
├── MyComponent.scss
└── MyComponent.story.tsx # Co-located story
When to Use Which Pattern
| Pattern | When to Use |
|---|---|
| Centralized | Default for all domain-level features (wallets, staking, etc.) |
| Co-located | Self-contained feature modules under source/renderer/app/features/ |
Step 1: Determine Story Location
New domain in storybook/stories/
Create a new directory and optionally a domain barrel file:
storybook/stories/{domain}/
├── index.ts # Optional barrel import
├── {FeatureName}.stories.tsx # Story file
└── _utils/ # Optional domain wrapper
└── {Domain}Wrapper.tsx
Existing domain
Add the story file to the appropriate subdirectory:
storybook/stories/{domain}/{sub-feature}/
└── {FeatureName}.stories.tsx
Co-located feature story
source/renderer/app/features/{feature}/ui/
└── {ComponentName}.story.tsx
Step 2: Write the Story File
File Naming
| Pattern | Convention | Example |
|---|---|---|
| Centralized | {ComponentName}.stories.tsx |
WalletSend.stories.tsx |
| Co-located | {ComponentName}.story.tsx |
DiscreetModeToggle.story.tsx |
Story Naming Convention
Use storiesOf('Domain / Sub-feature', module) with / as separator:
storiesOf('Wallets / Send', module)
storiesOf('Common / Widgets', module)
storiesOf('Staking', module)
storiesOf('Discreet Mode / Discreet Mode Toggle', module)
Minimal Story Template
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { withKnobs, boolean, text, number } from '@storybook/addon-knobs';
import MyComponent from '../../../../source/renderer/app/components/{domain}/MyComponent';
storiesOf('{Domain} / {Feature}', module)
.addDecorator(withKnobs)
.add('default', () => (
<MyComponent
label={text('Label', 'Default')}
disabled={boolean('Disabled', false)}
/>
));
Full Story Template (with decorators + state)
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { observable, action as mobxAction } from 'mobx';
import {
withKnobs,
boolean,
text,
number,
select,
} from '@storybook/addon-knobs';
import StoryDecorator from '../_support/StoryDecorator';
import StoryProvider from '../_support/StoryProvider';
import StoryLayout from '../_support/StoryLayout';
import MyComponent from '../../../../source/renderer/app/components/{domain}/MyComponent';
storiesOf('{Domain} / {Feature}', module)
.addDecorator((story: any, context: any) => {
const
const state = observable({
value: '',
onChange: mobxAction((newValue) => {
state.value = newValue;
onChangeAction(newValue);
}),
});
return (
<StoryDecorator propsForChildren={state}>
<StoryProvider>
<StoryLayout activeSidebarCategory="/{domain}" {...context}>
{story()}
</StoryLayout>
</StoryProvider>
</StoryDecorator>
);
})
.addDecorator(withKnobs)
.add('default', () => (
<MyComponent
label={text('Label', 'Default')}
isActive={boolean('Active', true)}
count={number('Count', 5)}
/>
))
.add('loading', () => (
<MyComponent isLoading />
))
.add('error', () => (
<MyComponent error="Something went wrong" />
));
Story with withState (for interactive two-way binding)
import { withState } from '@dump247/storybook-state';
storiesOf('{Domain} / {Feature}', module)
.addDecorator(withKnobs)
.add(
'interactive',
withState({ value: '' }, (store) => (
<MyInput
value={store.state.value}
=> store.set({ value })}
/>
))
);
Step 3: Use Support Infrastructure
Decorator Stack
The standard decorator stack wraps stories with theme, intl, and layout:
| Decorator | Purpose | Import Path |
|---|---|---|
StoryWrapper |
Top-level: theme, locale, OS selection (auto-applied) | ../_support/StoryWrapper |
StoryDecorator |
react-polymorph ThemeProvider + skin |
../_support/StoryDecorator |
StoryProvider |
MobX store provider + mock data | ../_support/StoryProvider |
StoryLayout |
Full app shell (sidebar, topbar, content area) | ../_support/StoryLayout |
StoryWrapper is automatically applied via storybook/preview.tsx — do NOT add it manually.
When to use each:
- Simple component (no app context needed): Just
withKnobsdecorator — no StoryDecorator/Provider/Layout. - Component needing theme: Add
StoryDecorator. - Component needing MobX stores: Add
StoryDecorator+StoryProvider. - Full page/screen component: Add all three:
StoryDecorator+StoryProvider+StoryLayout.
Domain Wrappers
For domains with shared layout (e.g., wallets with navigation tabs), create a domain wrapper:
// storybook/stories/{domain}/_utils/{Domain}Wrapper.tsx
import React from 'react';
import { withKnobs } from '@storybook/addon-knobs';
import StoryLayout from '../../_support/StoryLayout';
import StoryProvider from '../../_support/StoryProvider';
import StoryDecorator from '../../_support/StoryDecorator';
export default function (story: any, context: any) {
const storyWithKnobs = withKnobs(story, context);
return (
<StoryDecorator>
<StoryProvider>
<StoryLayout activeSidebarCategory="/{domain}" {...context}>
{storyWithKnobs}
</StoryLayout>
</StoryProvider>
</StoryDecorator>
);
}
Use it in stories:
import DomainWrapper from '../_utils/DomainWrapper';
storiesOf('{Domain} / {Feature}', module)
.addDecorator(DomainWrapper)
.add('default', () => <MyComponent />);
activeSidebarCategory Values
| Category | Value |
|---|---|
| Wallets | /wallets |
| Staking | /staking |
| Settings | /settings |
| Voting | /voting |
| None/Other | null |
Step 4: Mock Data
Available Mock Data Generators
Import from storybook/stories/_support/utils.ts:
import {
generateHash,
generatePolicyIdHash,
generateWallet,
generateTransaction,
generateAssetToken,
generateAssetDomain,
generateRewardForWallet,
EXAMPLE_METADATA,
} from '../_support/utils';
| Generator | Returns | Key Parameters |
|---|---|---|
generateHash() |
SHA-512 hex string | (none) |
generatePolicyIdHash() |
SHA-224 hex string | (none) |
generateWallet() |
Wallet instance |
name, amount, assets, reward, delegatedStakePool, hasPassword, status, isHardwareWallet, id |
generateTransaction() |
WalletTransaction |
type, date, amount, deposit, state, ... |
generateAssetToken() |
AssetToken object |
policyId, assetName, fingerprint, quantity, metadata |
generateAssetDomain() |
Asset instance |
policyId, assetName, fingerprint, metadata |
generateRewardForWallet() |
Reward object |
wallet, unspent |
Example mock data setup
import BigNumber from 'bignumber.js';
import { generateWallet, generateAssetToken, generateHash } from '../_support/utils';
import Wallet, { WalletSyncStateStatuses } from '../../../../source/renderer/app/domains/Wallet';
const wallet = generateWallet('Test Wallet', '500000000');
const syncingWallet = generateWallet(
'Syncing Wallet',
'100000000',
{ available: [], total: [] },
0,
null,
false,
WalletSyncStateStatuses.SYNCING
);
const asset = generateAssetToken(
'65bc72542b0ca20391caaf66a4d4e7897d282f9c136cd3513136945c',
'',
'token_fingerprint_here',
100,
{ name: 'TestToken', ticker: 'TT', description: 'A test token', url: '', logo: '' }
);
Step 5: Register the Story
Centralized stories
Add the import to the appropriate barrel file:
If a domain
index.tsexists (e.g.,storybook/stories/wallets/index.ts):import './{sub-feature}/{FeatureName}.stories';If no domain
index.tsexists, add the import tostorybook/stories/index.ts:// {Domain} import './{domain}/{FeatureName}.stories';
Co-located stories
Add the import to storybook/stories/index.ts with the full relative path:
import '../../source/renderer/app/features/{feature}/ui/{ComponentName}.story';
Registration Checklist
- Story file created with correct naming convention
- Import added to domain
index.tsOR rootstorybook/stories/index.ts - Story renders in Storybook without errors
Step 6: Cover Component States
Every story file should demonstrate the component's key visual states:
Required States
storiesOf('{Domain} / {Feature}', module)
.addDecorator(/* ... */)
.add('default', () => <Component /* happy path props */ />)
.add('loading', () => <Component isLoading />)
.add('empty', () => <Component items={[]} />)
.add('error', () => <Component error="Something went wrong" />);
Recommended Additional States
| State | When to Include |
|---|---|
disabled |
Component can be disabled |
with data |
Component needs populated mock data |
hardware wallet |
Component behaves differently for HW wallets |
syncing |
Component shows sync progress |
restoring |
Component shows restore progress |
legacy wallet |
Component has legacy wallet variant |
with knobs |
Interactive prop editing for complex components |
Knobs Reference
Use @storybook/addon-knobs for interactive props:
import {
withKnobs,
text, // text('Label', 'default value')
boolean, // boolean('Disabled', false)
number, // number('Count', 5)
select, // select('Size', { Small: 'sm', Large: 'lg' }, 'sm')
date, // date('Start Date')
object, // object('Config', { key: 'value' })
array, // array('Items', ['a', 'b'])
} from '@storybook/addon-knobs';
Updating Existing Stories
When updating stories for an existing feature:
- Find the existing story file — search
storybook/stories/orsource/renderer/app/features/for.stories.tsxor.story.tsxfiles. - Add new story entries — append
.add()calls for new states/variants. - Update mock data — if the component's props changed, update mock data to match.
- Update imports — if new dependencies are needed, add them.
- Do NOT change the
storiesOf()title unless the feature was renamed.
Adding a new state to an existing story
// Find the existing storiesOf chain and append:
.add('new state name', () => (
<ExistingComponent
newProp={text('New Prop', 'value')}
/* existing required props */
/>
));
Internationalization in Stories
For components using react-intl messages:
import { defineMessages, IntlProvider } from 'react-intl';
import enMessages from '../../../../source/renderer/app/i18n/locales/en-US.json';
import jpMessages from '../../../../source/renderer/app/i18n/locales/ja-JP.json';
const { intl: enIntl } = new IntlProvider({
locale: 'en-US',
messages: enMessages,
}).getChildContext();
// Use in story:
.add('with i18n', (_, props) => (
<Component
label={enIntl.formatMessage(messages.myLabel)}
/>
))
Note:
StoryWrapperalready providesIntlProviderglobally. You only need manual intl setup when you need to accessintl.formatMessage()directly in story code (not in the component).
Common Pitfalls
| Pitfall | Solution |
|---|---|
| Story not appearing in Storybook | Ensure import is added to index.ts barrel file |
| Blank/white story | Missing StoryDecorator (theme provider) — add it as a decorator |
| MobX errors | Component needs stores — add StoryProvider decorator |
| Missing sidebar/layout | Add StoryLayout decorator with correct activeSidebarCategory |
| Styles not loading | SCSS modules are auto-loaded; check component imports |
storiesOf title not grouping |
Use / separator (with spaces): 'Domain / Feature' |
| Using CSF exports | Daedalus uses storiesOf() API — do NOT use export default |
| Arrow functions in decorators | Use function keyword if you need this context |
Validation
After creating or updating a story:
- Build check:
yarn storybook:build— story must compile without errors - Visual check:
yarn storybook— story renders correctly athttp://localhost:6006 - Theme check: Switch themes in the DaedalusMenu addon — verify all themes render properly
- Locale check: Switch to Japanese locale — verify i18n strings render
- State coverage: Verify all visual states are represented (default, loading, error, empty, etc.)
Quick Reference
New Story Checklist
- Determine location (centralized vs co-located)
- Create story file with correct naming (
*.stories.tsxor*.story.tsx) - Use
storiesOf()API with correct domain/feature title - Add appropriate decorators (StoryDecorator, StoryProvider, StoryLayout)
- Add
withKnobsfor interactive props - Use
action()for event handler props - Cover all key visual states (default, loading, error, empty)
- Use mock data generators from
_support/utils.ts - Register story in barrel
index.ts - Verify story renders in Storybook