Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b98cfa8
feat: csf factories
dannyhw Dec 15, 2025
ccf6327
Merge remote-tracking branch 'origin/next' into dannyhw/feat/csf-fact…
dannyhw Feb 27, 2026
9387241
remove comment
dannyhw Feb 27, 2026
a2177ce
feat: add factories examples and tests
dannyhw Feb 27, 2026
644d7a5
fix: jest config
dannyhw Feb 27, 2026
37221b3
fix: test fn mock resolves different file for different pacakges
dannyhw Feb 27, 2026
cfc816a
fix: action issues
dannyhw Feb 27, 2026
d0bca32
fix: resolve duplicate storybook instances and improve action event h…
dannyhw Feb 27, 2026
2a8f78f
chore: add example without factories
dannyhw Feb 27, 2026
ad9c382
refactor: update type annotations in Start.tsx for improved type safety
dannyhw Feb 27, 2026
af21a9e
refactor: simplify type annotations in Start.tsx for clarity
dannyhw Feb 27, 2026
0e44b1b
comment
dannyhw Feb 28, 2026
cc7ebfc
roadmap update
dannyhw Feb 28, 2026
8d69dcf
feat: integrate theming support in ActionLogger and Inspect components
dannyhw Feb 28, 2026
61b3335
Merge remote-tracking branch 'origin/next' into dannyhw/feat/csf-fact…
dannyhw Feb 28, 2026
86248c1
Merge branch 'next' into dannyhw/feat/csf-factories
dannyhw Feb 28, 2026
5ac5bb1
Merge branch 'next' into dannyhw/feat/csf-factories
dannyhw Feb 28, 2026
d84e366
fix: custom titles not working as expected
dannyhw Mar 1, 2026
7e42bb2
fix: use rn specific define preview and composition
dannyhw Mar 1, 2026
e5277ef
changeset
dannyhw Mar 1, 2026
6088e25
fix: actions
dannyhw Mar 1, 2026
826b546
refactor: update .gitignore and enhance CLAUDE.md documentation
dannyhw Mar 1, 2026
d8783c5
fix: type inference
dannyhw Mar 1, 2026
cdfe8ba
fix
dannyhw Mar 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/flat-impalas-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@storybook/react-native': patch
---

feat: adds csf next support
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ dist
.cache
junit.xml
coverage/
*.lerna_backup
build
/**/LICENSE
docs/public
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pnpm-lock.yaml
dist/
examples/expo-example/.expo
examples/expo-example/.rnstorybook/storybook.requires.ts
examples/expo-example/.rnstorybook-nofactories/storybook.requires.ts
docs/.docusaurus
docs/build
.claude/
Expand Down
116 changes: 36 additions & 80 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,97 +1,53 @@
This file provides guidance to agents when working with code in this repository.

Always check first if the react-native-best-practices skill can be used
check available mcp and skills

## Development Commands
use pnpm for commands, check package.json for scripts

```bash
# Initial Setup
pnpm install
pnpm build

# Development
pnpm dev # Watch all packages for changes
pnpm example # Run the expo example app with Storybook

# Story Generation
pnpm -F expo-example storybook-generate # Regenerate storybook.requires.ts
## On-Device Testing Tools

# Testing
pnpm test # Run unit tests across all packages
pnpm test:ci # Run tests in CI mode
- use agent-device to control a simulator `agent-device --help`
- use rn-logs to get metro logs `rn-logs logs --help`
- use the storybook mcp to select stories and get story list

# Code Quality
pnpm lint # Run ESLint across the codebase
pnpm format:check # Check Prettier formatting
pnpm format:fix # Auto-fix Prettier formatting
use curl to send events to channel server, such as to update the args:

# Documentation (from docs/ directory)
cd docs
pnpm start # Start development server
pnpm build # Build documentation
pnpm serve # Serve built documentation
```sh
curl -X POST http://localhost:7007/send-event \
-H "Content-Type: application/json" \
-d '{
"type": "updateStoryArgs",
"args": [{
"storyId": "controlexamples-controlexample--example",
"updatedArgs": { "name": "Alice", "age": 25 }
}]
}'
```

## Architecture Overview

**pnpm workspaces monorepo** managed by Lerna containing React Native Storybook packages.

### Packages

**Apps**

- examples/expo-example - Expo example app showcasing Storybook
- docs - Documentation site for Storybook React Native

**Core:**

- `@storybook/react-native` - Main package providing Storybook functionality
- `@storybook/react-native-ui` - Full UI components for on-device Storybook
- `@storybook/react-native-ui-lite` - Lightweight UI components
- `@storybook/react-native-ui-common` - Shared UI components
- `@storybook/react-native-theming` - Theming utilities

**On-Device Addons:**
### agent-device (iOS/Android Simulator Control)

- `@storybook/addon-ondevice-actions` - Log component interactions
- `@storybook/addon-ondevice-backgrounds` - Change story backgrounds
- `@storybook/addon-ondevice-controls` - Dynamically edit component props
- `@storybook/addon-ondevice-notes` - Add markdown documentation to stories

### Build System & Metro Configuration

- Uses **tsup** for TypeScript compilation (ES2022, CommonJS output)
- Each package has its own `tsup.config.ts`
- `pnpm prepare` in a package builds it

The `withStorybook` Metro wrapper (for Metro-based projects):

- Enables `unstable_allowRequireContext` for dynamic story imports
- Automatically generates `storybook.requires.ts` file
- Optional WebSocket server for remote control
- Can be conditionally enabled/disabled via `enabled` option
- Supports `liteMode` for reduced bundle size

The `StorybookPlugin` (for Re.Pack/Rspack/Webpack projects):
```bash
agent-device open host.exp.Exponent --relaunch # Relaunch Expo Go
agent-device snapshot -c # Take accessibility snapshot (shows @refs)
agent-device click @e14 # Click element by ref from snapshot
agent-device find "Press me" click # Find text and click it
```

- Alternative to `withStorybook` for non-Metro bundlers
- Imported from `@storybook/react-native/repack/withStorybook`
- Requires `enablePackageExports: true` in rspack resolve options
- Uses `DefinePlugin` for build-time `STORYBOOK_ENABLED` constant
- No `require.context` configuration needed (rspack handles it natively)
- Same options as `withStorybook` (enabled, configPath, useJs, docTools, liteMode, websockets)
After relaunching, you need to press the "Expo Example" to go to it.

### Testing
### rn-logs (React Native Log Streaming)

- Uses **jest** with `jest-expo` preset
- `universal-test-renderer` for portable story testing
- Story generation tested with Node's native test runner
```bash
rn-logs apps # List running apps
rn-logs logs --app "host.exp.Exponent" # Stream logs from Expo Go
```

### Key Concepts
## Key Concepts

1. **CSF (Component Story Format)** - Standard story syntax
2. **On-device UI** - Native UI that runs directly on mobile devices
3. **Story requires generation** - Automatic generation of story imports via Metro
4. **Portable stories** - Reuse stories in unit tests
5. **WebSocket support** - Remote control stories from external devices
6. **Lite mode** - Alternative UI without heavy dependencies (reanimated, etc.)
3. **Story requires generation** - Automatic generation of story imports via Metro (`storybook.requires.ts`)
4. **Portable stories** - Reuse stories in unit tests via `universal-test-renderer`
5. web storybook codebase can be referenced and likely can be found at ../storybook (from root)

additional information in docs folder and readme file
12 changes: 6 additions & 6 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

For v11:

- csf factories
- backgrounds with globals
- addons into core
- make lite ui the default and remove dependencies from controls
- [x] csf factories
- [x] backgrounds with globals
- [ ] addons into core
- [ ] make lite ui the default and remove dependencies from controls

stretch goals:

- simple docs implementation
- dev tooling like vscode extension and rn dev tools integration
- [ ] simple docs implementation
- [x] dev tooling like vscode extension and rn dev tools integration
41 changes: 41 additions & 0 deletions examples/expo-example/.rnstorybook-nofactories/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { LiteUI } from '@storybook/react-native-ui-lite';
import { StatusBar, View } from 'react-native';
import { SafeAreaView, SafeAreaProvider } from 'react-native-safe-area-context';
import { view } from './storybook.requires';

const isScreenshotTesting = process.env.EXPO_PUBLIC_SCREENSHOT_TESTING === 'true';
const isLiteUI = process.env.EXPO_PUBLIC_LITE_UI === 'true';

const StorybookUIRoot = view.getStorybookUI({
shouldPersistSelection: true,
storage: {
getItem: AsyncStorage.getItem,
setItem: AsyncStorage.setItem,
},
enableWebsockets: true,

CustomUIComponent: isScreenshotTesting
? ({ children, story }) => {
return (
<SafeAreaProvider>
<SafeAreaView style={{ flex: 1 }}>
<StatusBar hidden />
<View
style={{ flex: 1 }}
accessibilityLabel={story?.id}
testID={story?.id}
accessible
>
{children}
</View>
</SafeAreaView>
</SafeAreaProvider>
);
}
: isLiteUI
? LiteUI
: undefined,
});

export default StorybookUIRoot;
28 changes: 28 additions & 0 deletions examples/expo-example/.rnstorybook-nofactories/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { StorybookConfig } from '@storybook/react-native';

const main: StorybookConfig = {
stories: [
'../components/**/!(*.factories).stories.?(ts|tsx|js|jsx)',
'../other_components/**/!(*.factories).stories.?(ts|tsx|js|jsx)',
{
directory: '../../../packages/react-native-ui',
titlePrefix: 'react-native-ui',
files: '**/!(*.factories).stories.?(ts|tsx|js|jsx)',
},
],
addons: [
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-actions',
'@storybook/addon-ondevice-notes',
'storybook-addon-deep-controls',
],
reactNative: {
playFn: false,
},
features: {
ondeviceBackgrounds: true,
},
framework: '@storybook/react-native',
};

export default main;
37 changes: 37 additions & 0 deletions examples/expo-example/.rnstorybook-nofactories/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Appearance } from 'react-native';

const preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
options: {
storySort: {
method: 'alphabetical' as const,
includeNames: true,
order: ['ControlExamples', ['ControlExample'], 'InteractionExample', 'DeepControls'],
},
},
hideFullScreenButton: false,
noSafeArea: false,
my_param: 'anything',
layout: 'padded',
storybookUIVisibility: 'visible',
backgrounds: {
options: {
dark: { name: 'dark', value: '#333' },
light: { name: 'plain', value: '#fff' },
app: { name: 'app', value: '#eeeeee' },
},
},
},
initialGlobals: {
backgrounds: { value: Appearance.getColorScheme() === 'dark' ? 'dark' : 'plain' },
},
};

export default preview;
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* do not change this file, it is auto generated by storybook. */
/// <reference types="@storybook/react-native/metro-env" />
import { start, updateView, View, type Features } from '@storybook/react-native';

import "@storybook/addon-ondevice-controls/register";
import "@storybook/addon-ondevice-actions/register";
import "@storybook/addon-ondevice-notes/register";
import "storybook-addon-deep-controls/register";

const normalizedStories = [
{
titlePrefix: "",
directory: "./components",
files: "**/!(*.factories).stories.?(ts|tsx|js|jsx)",
importPathMatcher: /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/,
req: require.context(
'../components',
true,
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/
),
},
{
titlePrefix: "",
directory: "./other_components",
files: "**/!(*.factories).stories.?(ts|tsx|js|jsx)",
importPathMatcher: /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/,
req: require.context(
'../other_components',
true,
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/
),
},
{
titlePrefix: "react-native-ui",
directory: "../../packages/react-native-ui",
files: "**/!(*.factories).stories.?(ts|tsx|js|jsx)",
importPathMatcher: /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/,
req: require.context(
'../../../packages/react-native-ui',
true,
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/
),
}
];


declare global {
var view: View;
var STORIES: typeof normalizedStories;
var STORYBOOK_WEBSOCKET: { host: string; port: number } | undefined;
var FEATURES: Features;
}


const annotations = [
require('./preview'),
require("@storybook/react-native/preview"),
require('storybook-addon-deep-controls/preview')
];

globalThis.STORIES = normalizedStories;
globalThis.STORYBOOK_WEBSOCKET = { host: '192.168.1.172', port: 7007 };

module?.hot?.accept?.();

globalThis.FEATURES.ondeviceBackgrounds = true;

const options = {
"playFn": false
}

if (!globalThis.view) {
globalThis.view = start({
annotations,
storyEntries: normalizedStories,
options,
});
} else {
updateView(globalThis.view, annotations, normalizedStories, options);
}

export const view: View = globalThis.view;
8 changes: 3 additions & 5 deletions examples/expo-example/.rnstorybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { StorybookConfig } from '@storybook/react-native';
import { defineMain } from '@storybook/react-native/node';

const main: StorybookConfig = {
export default defineMain({
stories: [
'../components/**/*.stories.?(ts|tsx|js|jsx)',
'../other_components/**/*.stories.?(ts|tsx|js|jsx)',
Expand All @@ -26,6 +26,4 @@ const main: StorybookConfig = {
},

framework: '@storybook/react-native',
};

export default main;
});
Loading