Create or update Storybook stories for Daedalus React components. Use this when asked to write stories, visual tests, storybook entries, or component demos for new or existing features.
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.
| 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.
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
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
| 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/ |
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
Add the story file to the appropriate subdirectory:
storybook/stories/{domain}/{sub-feature}/
└── {FeatureName}.stories.tsx
source/renderer/app/features/{feature}/ui/
└── {ComponentName}.story.tsx
| Pattern | Convention | Example |
|---|---|---|
| Centralized | {ComponentName}.stories.tsx | WalletSend.stories.tsx |
| Co-located | {ComponentName}.story.tsx | DiscreetModeToggle.story.tsx |
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)
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)}
onClick={action('onClick')}
/>
));
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 onChangeAction = action('onChange');
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)}
onClick={action('onClick')}
/>
))
.add('loading', () => (
<MyComponent isLoading />
))
.add('error', () => (
<MyComponent error="Something went wrong" />
));
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}
onChange={(value) => store.set({ value })}
/>
))
);
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:
withKnobs decorator — no StoryDecorator/Provider/Layout.StoryDecorator.StoryDecorator + StoryProvider.StoryDecorator + StoryProvider + StoryLayout.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 />);
| Category | Value |
|---|---|
| Wallets | /wallets |
| Staking | /staking |
| Settings | /settings |
| Voting | /voting |
| None/Other | null |
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 |
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: '' }
);
Add the import to the appropriate barrel file:
If a domain index.ts exists (e.g., storybook/stories/wallets/index.ts):
import './{sub-feature}/{FeatureName}.stories';
If no domain index.ts exists, add the import to storybook/stories/index.ts:
// {Domain}
import './{domain}/{FeatureName}.stories';
Add the import to storybook/stories/index.ts with the full relative path:
import '../../source/renderer/app/features/{feature}/ui/{ComponentName}.story';
index.ts OR root storybook/stories/index.tsEvery story file should demonstrate the component's key visual 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" />);
| 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 |
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';
When updating stories for an existing feature:
storybook/stories/ or source/renderer/app/features/ for .stories.tsx or .story.tsx files..add() calls for new states/variants.storiesOf() title unless the feature was renamed.// Find the existing storiesOf chain and append:
.add('new state name', () => (
<ExistingComponent
newProp={text('New Prop', 'value')}
/* existing required props */
/>
));
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).
| 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 |
After creating or updating a story:
yarn storybook:build — story must compile without errorsyarn storybook — story renders correctly at http://localhost:6006*.stories.tsx or *.story.tsx)storiesOf() API with correct domain/feature titlewithKnobs for interactive propsaction() for event handler props_support/utils.tsindex.ts