Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set up Storybook; create an Icon component as a proof of concept #6

Merged
merged 5 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion react-native-expo/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// https://docs.expo.dev/guides/using-eslint/
module.exports = {
extends: ['expo', 'prettier'],
extends: ['expo', 'prettier', 'plugin:storybook/recommended'],
plugins: ['prettier'],
ignorePatterns: ['/dist/*'],
rules: {
Expand Down
2 changes: 2 additions & 0 deletions react-native-expo/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ yarn-error.*
/ios/
app-example
modules/penumbra-sdk-module/ios/Mobile.xcframework/**/*.a

*storybook.log
61 changes: 61 additions & 0 deletions react-native-expo/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import path from 'path';
import react from '@vitejs/plugin-react';
import type { StorybookConfig } from '@storybook/react-vite';
import svgr from 'vite-plugin-svgr';

const config: StorybookConfig = {
stories: ['../**/*.stories.@(js|jsx|mjs|ts|tsx)'],
async viteFinal(config) {
const { mergeConfig } = await import('vite');

return mergeConfig(config, {
optimizeDeps: {
// Required for libraries that include JSX in their `.js` files.
// @see https://github.com/vitejs/vite/discussions/3448#discussioncomment-5904031
esbuildOptions: {
loader: {
'.js': 'jsx',
},
},
},
plugins: [
// Allows us to use JSX without `import React from 'react'`
react({ jsxRuntime: 'automatic' }),
// Allows us to `import foo from './foo.svg'` in Storybook Web just like
// in React Native.
svgr({ svgrOptions: { exportType: 'default' } }),
],
resolve: {
alias: {
// Match the tsconfig.json alias
'@': path.resolve(__dirname, '../src'),

// Tells Storybook to use react-native-web anytime there's an import
// from react-native
'react-native': 'react-native-web',

// Use web-friendly versions of specific libraries
'react-native-svg': 'react-native-svg-web',
},
},
});
},
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
'@storybook/addon-react-native-web',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
core: {
builder: '@storybook/builder-vite',
},
typescript: {
reactDocgen: 'react-docgen-typescript',
},
};
export default config;
24 changes: 24 additions & 0 deletions react-native-expo/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { DripsyProvider } from 'dripsy';
import dripsyTheme from '../utils/dripsyTheme';
import type { Preview } from '@storybook/react';
import React from 'react';

const preview: Preview = {
decorators: [
Story => (
<DripsyProvider theme={dripsyTheme}>
<Story />
</DripsyProvider>
),
],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};

export default preview;
8 changes: 8 additions & 0 deletions react-native-expo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,11 @@ In short: all UI lives under the `components` directory. All route components li
### Components

Components MUST be named in UpperCamelCase: e.g., `DropdownMenu`.

## Storybook

We use [Storybook](https://storybook.js.org/) to develop and view UI components in isolation. While Storybook does provide React Native integration (via a separate app entry point), we instead use Storybook for web due to ease of use. This allow developers to view Storybook stories in their browser, while still developing the app in the iOS Simulator.

To make this possible, we use [`react-native-web`](https://necolas.github.io/react-native-web/) (as well as some other utility libraries) to convert React Native components into their HTML equivalents. See `.storybook/main.ts` for more.

Storybook stories should be placed next to the component file they represent, suffixed with `.stories.ts` or `.stories.tsx`. For example, the Storybook stories for a `Button` component would live under `Button/index.stories.ts`.
6 changes: 5 additions & 1 deletion react-native-expo/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Icon from '@/components/Icon';
import AppInitializationContext from '@/contexts/AppInitializationContext';
import { getBlockHeight } from '@/modules/penumbra-sdk-module';
import { CheckCircle } from 'lucide-react-native';
import React, { useContext, useEffect, useState } from 'react';
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';

Expand Down Expand Up @@ -43,7 +45,9 @@ export default function AppRoute() {

return (
<View style={styles.container}>
<Text style={styles.text}>view server block height: {counter}</Text>
<Text style={styles.text}>
<Icon IconComponent={CheckCircle} size='md' /> view server block height: {counter}
</Text>
</View>
);
}
Expand Down
37 changes: 37 additions & 0 deletions react-native-expo/components/Icon/index.stories.ts
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { CheckCircle, Home, Coins } from 'lucide-react-native';
import { Meta, StoryObj } from '@storybook/react/*';
import Icon from '.';
import { DripsyTheme } from '@/utils/dripsyTheme';

const COLORS = {
success: (colors: DripsyTheme['color']) => colors.success.main,
destructive: (colors: DripsyTheme['color']) => colors.destructive.main,
unshield: (colors: DripsyTheme['color']) => colors.unshield.main,
};

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
component: Icon,
tags: ['autodocs'],
argTypes: {
IconComponent: {
options: ['CheckCircle', 'Home', 'Coins'],
mapping: { CheckCircle, Home, Coins },
},
color: {
options: ['success', 'destructive', 'unshield'],
mapping: COLORS,
},
},
} satisfies Meta<typeof Icon>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {
IconComponent: CheckCircle,
size: 'md',
color: COLORS.success,
},
};
70 changes: 70 additions & 0 deletions react-native-expo/components/Icon/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { DripsyTheme } from '@/utils/dripsyTheme';
import { useDripsyTheme } from 'dripsy';
import { LucideIcon } from 'lucide-react-native';
import { ComponentProps, FC } from 'react';

export type IconSize = 'sm' | 'md' | 'lg';

export interface IconProps {
/**
* The icon import from `lucide-react` to render.
*
* ```tsx
* import { ChevronRight } from 'lucide-react-native';
* <Icon IconComponent={ChevronRight} />
* ```
*/
IconComponent: LucideIcon | FC;
/**
* - `sm`: 16px square
* - `md`: 24px square
* - `lg`: 32px square
*/
size: IconSize;
/**
* A function that takes the theme's `color` object and returns the hex color
* to render: `color={color => color.success.main}`
*/
color?: (themeColors: DripsyTheme['color']) => string;
}

const PROPS_BY_SIZE: Record<IconSize, ComponentProps<LucideIcon>> = {
sm: {
size: 16,
strokeWidth: 1,
},
md: {
size: 24,
strokeWidth: 1.5,
},
lg: {
size: 32,
strokeWidth: 2,
},
};

/**
* Renders the Lucide icon passed in via the `IconComponent` prop. Use this
* component rather than rendering Lucide icon components directly, since this
* component standardizes the stroke width and sizes throughout the Penumbra
* ecosystem.
*
* ```tsx
* <Icon
* IconComponent={ArrowRightLeft}
* size='sm'
* color={color => color.success.main}
* />
* ```
*/
export default function Icon({ IconComponent, size = 'sm', color }: IconProps) {
const { theme } = useDripsyTheme();

return (
<IconComponent
absoluteStrokeWidth
color={!color ? 'currentColor' : color(theme.color)}
{...PROPS_BY_SIZE[size]}
/>
);
}
28 changes: 25 additions & 3 deletions react-native-expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"ios": "expo run:ios",
"web": "expo start --web",
"test": "jest --watchAll",
"lint": "expo lint"
"lint": "expo lint",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"jest": {
"preset": "jest-expo"
Expand All @@ -32,30 +34,50 @@
"expo-symbols": "~0.2.0",
"expo-system-ui": "~4.0.4",
"expo-web-browser": "~14.0.1",
"lucide-react-native": "^0.468.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.3",
"react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.1.0",
"react-native-web": "~0.19.13",
"react-native-svg": "^15.10.1",
"react-native-web": "^0.19.13",
"react-native-webview": "13.12.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@chromatic-com/storybook": "^3.2.2",
"@react-native/babel-preset": "^0.76.5",
"@storybook/addon-essentials": "^8.4.7",
"@storybook/addon-interactions": "^8.4.7",
"@storybook/addon-onboarding": "^8.4.7",
"@storybook/addon-react-native-web": "^0.0.26",
"@storybook/blocks": "^8.4.7",
"@storybook/react": "^8.4.7",
"@storybook/react-vite": "^8.4.7",
"@storybook/test": "^8.4.7",
"@types/jest": "^29.5.12",
"@types/react": "~18.3.12",
"@types/react-test-renderer": "^18.3.0",
"@vitejs/plugin-react": "^4.3.4",
"babel-plugin-react-native-web": "^0.19.13",
"eslint": "^8.57.0",
"eslint-config-expo": "~8.0.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-storybook": "^0.11.1",
"jest": "^29.2.1",
"jest-expo": "~52.0.2",
"prettier": "^3.4.2",
"react-native-svg-web": "^1.0.9",
"react-test-renderer": "18.3.1",
"typescript": "^5.3.3"
"storybook": "^8.4.7",
"typescript": "^5.3.3",
"vite": "^6.0.3",
"vite-plugin-svgr": "^4.3.0",
"webpack": "^5.97.1"
},
"private": true
}
2 changes: 1 addition & 1 deletion react-native-expo/utils/dripsyTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const dripsyTheme = makeTheme({
breakpoints: BREAKPOINTS.map(breakpoint => `${theme.breakpoint[breakpoint]}px`),
});
export default dripsyTheme;
type DripsyTheme = typeof dripsyTheme;
export type DripsyTheme = typeof dripsyTheme;

declare module 'dripsy' {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
Expand Down
Loading