Skip to content

Commit

Permalink
Merge pull request #6 from prax-wallet/jessepinho/initial-components
Browse files Browse the repository at this point in the history
Set up Storybook; create an Icon component as a proof of concept
  • Loading branch information
jessepinho authored Dec 19, 2024
2 parents 2c931ea + e57b6b0 commit a4599f1
Show file tree
Hide file tree
Showing 11 changed files with 1,869 additions and 45 deletions.
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
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

0 comments on commit a4599f1

Please sign in to comment.