Skip to content

Commit

Permalink
chore: Add theme provider
Browse files Browse the repository at this point in the history
  • Loading branch information
namick committed Feb 28, 2023
1 parent 19aae72 commit 4a36659
Show file tree
Hide file tree
Showing 19 changed files with 307 additions and 234 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { storiesOf } from '@storybook/react-native';
import { BrandConfigProvider, Theme } from 'src';
import { ThemeExampleScreen } from './ThemeExampleScreen';
import * as baseTheme from 'src/components/BrandConfigProvider/theme/base';

import { object, color } from '@storybook/addon-knobs';

storiesOf('BrandConfigProvider', module)
.add('Basic Theme Colors', () => {
const customColors: Record<string, string> = {};
Object.entries(baseTheme.colors).forEach(([key, value]) => {
customColors[key] = color(key, value);
});

const theme = new Theme({ colors: customColors });

return (
<BrandConfigProvider theme={theme}>
<ThemeExampleScreen />
</BrandConfigProvider>
);
})
.add('Default Theme', () => {
const customColors = object('theme.colors', baseTheme.colors);
const customSpacing = object('theme.spacing', baseTheme.spacing);

const theme = new Theme({ colors: customColors, spacing: customSpacing });

return (
<BrandConfigProvider theme={theme}>
<ThemeExampleScreen />
</BrandConfigProvider>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react';

import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { useTheme } from 'src/hooks/useTheme';

export function ThemeExampleScreen() {
const { theme } = useTheme();

const centerView: ViewStyle = {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: theme.colors.background,
};

const blackText: TextStyle = {
color: theme.colors.black,
};

const whiteText: TextStyle = {
color: theme.colors.white,
};

const primaryView: ViewStyle = {
backgroundColor: theme.colors.primary,
};

const secondaryView: ViewStyle = {
backgroundColor: theme.colors.secondary,
};

const tileView: ViewStyle = {
width: '63%',
margin: theme.spacing.large,
padding: theme.spacing.small,
borderColor: theme.colors.border,
borderWidth: 1,
borderRadius: 8,
};

const normalText: TextStyle = {
color: theme.colors.text,
};

const dimText: TextStyle = {
color: theme.colors.textDim,
};

const separatorView: ViewStyle = {
margin: theme.spacing.large,
width: '80%',
height: 1,
backgroundColor: theme.colors.separator,
};

const errorView: ViewStyle = {
margin: theme.spacing.large,
padding: theme.spacing.small,
backgroundColor: theme.colors.errorBackground,
borderColor: theme.colors.error,
borderWidth: 1,
borderRadius: 32,
};

const errorText: TextStyle = {
color: theme.colors.error,
};

return (
<View style={centerView}>
<View style={[tileView, primaryView]}>
<Text style={whiteText}>Primary color (white text)</Text>
</View>

<View style={[tileView, secondaryView]}>
<Text style={blackText}>Secondary color (black text)</Text>
</View>

<View style={tileView}>
<Text style={normalText}>This is normal text color</Text>
</View>

<View style={tileView}>
<Text style={dimText}>This is dim text color</Text>
</View>

<View style={separatorView} />

<View style={errorView}>
<Text style={errorText}>Error!</Text>
</View>
</View>
);
}
1 change: 1 addition & 0 deletions example/storybook/stories/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './BrandConfigProvider/BrandConfigProvider.stories';
import './Welcome/Welcome.stories';
import './OAuth.stories';
import './Tile.stories';
28 changes: 28 additions & 0 deletions src/components/BrandConfigProvider/BrandConfigProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { Theme } from './theme/Theme';
import { ThemeProvider } from './theme/ThemeProvider';

export const BrandConfigContext = React.createContext<{ theme: Theme }>({
theme: new Theme(),
});

interface Props {
theme?: Theme;
children: React.ReactNode;
}

export function BrandConfigProvider({ theme, children }: Props) {
if (!theme) {
theme = new Theme();
}

const context = {
theme,
};

return (
<BrandConfigContext.Provider value={context}>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</BrandConfigContext.Provider>
);
}
2 changes: 2 additions & 0 deletions src/components/BrandConfigProvider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { BrandConfigProvider } from './BrandConfigProvider';
export { Theme } from './theme/Theme';
33 changes: 33 additions & 0 deletions src/components/BrandConfigProvider/theme/Theme.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { colors as baseColors, spacing as baseSpacing } from './base';
import { Theme } from './Theme';

describe('Theme', () => {
test('loads defaults', () => {
const theme = new Theme();

expect(theme.colors).toStrictEqual(baseColors);
expect(theme.spacing).toStrictEqual(baseSpacing);
});

test('merges custom colors with default', () => {
expect(baseColors.background).not.toBe('pink');

const customColors = { background: 'pink' };

const theme = new Theme({ colors: customColors });

expect(theme.colors.background).toBe('pink');
expect(theme.colors.text).toBe(baseColors.text);
});

test('merges custom spacing with default', () => {
expect(baseSpacing.small).not.toBe(42);

const customSpacing = { small: 42 };

const theme = new Theme({ spacing: customSpacing });

expect(theme.spacing.small).toBe(42);
expect(theme.spacing.large).toBe(baseSpacing.large);
});
});
24 changes: 24 additions & 0 deletions src/components/BrandConfigProvider/theme/Theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as baseTheme from './base';

interface Props {
colors?: Partial<baseTheme.Colors>;
spacing?: Partial<baseTheme.Spacing>;
}

export class Theme {
colors: baseTheme.Colors = baseTheme.colors;
spacing: baseTheme.Spacing = baseTheme.spacing;

constructor({ colors, spacing }: Props = {}) {
this.mergeColors(colors || {});
this.mergeSpacing(spacing || {});
}

mergeColors(colors: Partial<baseTheme.Colors>) {
Object.assign(this.colors, colors);
}

mergeSpacing(spacing: Partial<baseTheme.Spacing>) {
Object.assign(this.spacing, spacing);
}
}
17 changes: 17 additions & 0 deletions src/components/BrandConfigProvider/theme/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { Theme } from './Theme';

interface Props {
theme: Theme;
children: React.ReactNode;
}

export function ThemeProvider({ theme, children }: Props) {
return (
<ThemeContext.Provider value={{ theme }}>{children}</ThemeContext.Provider>
);
}

export const ThemeContext = React.createContext<{ theme: Theme }>({
theme: new Theme(),
});
32 changes: 32 additions & 0 deletions src/components/BrandConfigProvider/theme/base/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const palette = {
neutral200: '#F4F2F1',
neutral300: '#D7CEC9',
neutral400: '#B6ACA6',
neutral600: '#564E4A',
neutral800: '#191015',

blueGray200: '#B0BEC5',
blueGray800: '#37474F',

error100: '#F2D6CD',
error500: '#C03403',
};

export const colors = {
white: '#FFFFFF',
black: '#000000',

text: palette.neutral800,
textDim: palette.neutral600,
border: palette.neutral400,
separator: palette.neutral300,
background: palette.neutral200,

primary: palette.blueGray800,
secondary: palette.blueGray200,

error: palette.error500,
errorBackground: palette.error100,
};

export type Colors = Partial<typeof colors>;
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ export const spacing = {
extraLarge: 32,
huge: 48,
massive: 64,
} as const;
};

export type Spacing = keyof typeof spacing;
export type Spacing = typeof spacing;
80 changes: 0 additions & 80 deletions src/components/Text.test.tsx

This file was deleted.

0 comments on commit 4a36659

Please sign in to comment.