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

feat: support branding #639

Merged
merged 14 commits into from
Nov 28, 2024
10 changes: 10 additions & 0 deletions examples/expo-example/.storybook/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Text } from 'react-native';
import { view } from './storybook.requires';
import AsyncStorage from '@react-native-async-storage/async-storage';

Expand All @@ -14,6 +15,15 @@ const StorybookUIRoot = view.getStorybookUI({
// initialSelection: { kind: 'TextInput', name: 'Basic' },
// onDeviceUI: false,
// host: '192.168.1.69',
/* theme: {
brand: {
image: {
uri: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/512px-React-icon.svg.png',
width: 25,
height: 25,
} ,
},
}, */
});

export default StorybookUIRoot;
10 changes: 5 additions & 5 deletions examples/expo-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"dependencies": {
"@babel/preset-env": "^7.25.4",
"@expo/metro-runtime": "~4.0.0",
"@gorhom/bottom-sheet": "^5.0.5",
"@gorhom/bottom-sheet": "^5.0.6",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/datetimepicker": "8.2.0",
"@react-native-community/slider": "4.5.5",
Expand All @@ -33,7 +33,7 @@
"@storybook/addon-ondevice-controls": "^8.4.3-alpha.1",
"@storybook/addon-ondevice-notes": "^8.4.3-alpha.1",
"@storybook/addon-react-native-server": "0.0.6",
"@storybook/addon-react-native-web": "^0.0.22",
"@storybook/addon-react-native-web": "^0.0.26",
"@storybook/addon-webpack5-compiler-babel": "^3.0.3",
"@storybook/blocks": "^8.4.2",
"@storybook/builder-webpack5": "^8.4.2",
Expand All @@ -43,12 +43,12 @@
"@storybook/react-native-theming": "^8.4.3-alpha.1",
"@storybook/react-webpack5": "^8.4.2",
"@storybook/test": "^8.4.2",
"expo": "~52.0.5",
"expo": "~52.0.11",
"history": "^5.3.0",
"querystring": "^0.2.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.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",
Expand All @@ -70,7 +70,7 @@
"babel-loader": "^9.1.3",
"babel-plugin-react-docgen-typescript": "^1.5.1",
"jest": "^29.7.0",
"jest-expo": "~52.0.0",
"jest-expo": "~52.0.2",
"metro-react-native-babel-preset": "^0.77.0",
"typescript": "^5.3.3"
}
Expand Down
14 changes: 2 additions & 12 deletions packages/react-native-theming/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,7 @@ export const theme: StorybookThemeWeb = {
barBg: light.barBg,

// Brand logo/text
brand: {
title: light.brandTitle,
url: light.brandUrl,
image: light.brandImage || (light.brandTitle ? null : undefined),
target: light.brandTarget,
},
brand: undefined,
};

export const darkTheme: StorybookThemeWeb = {
Expand Down Expand Up @@ -215,10 +210,5 @@ export const darkTheme: StorybookThemeWeb = {
barBg: dark.barBg,

// Brand logo/text
brand: {
title: dark.brandTitle,
url: dark.brandUrl,
image: dark.brandImage || (dark.brandTitle ? null : undefined),
target: dark.brandTarget,
},
brand: undefined,
};
17 changes: 11 additions & 6 deletions packages/react-native-theming/src/web-theme.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { TextStyle } from 'react-native';
import type { ImageProps, ImageSourcePropType, TextStyle } from 'react-native';
import { transparentize } from 'polished';
import { ReactElement } from 'react';

export const color = {
// Official color palette
Expand Down Expand Up @@ -171,10 +172,14 @@ export type Typography = typeof typography;

export type TextSize = number | string;
export interface Brand {
title: string | undefined;
url: string | null | undefined;
image: string | null | undefined;
target: string | null | undefined;
// Will replace the storybook logo with this title
title?: string | undefined;
// This url we be opened when clicking the branded logo or title
url?: string | null | undefined;
// Define a an image source to replace storybook logo with
image?: ImageSourcePropType | ReactElement | null | undefined;
resizeMode?: ImageProps['resizeMode'] | null | undefined;
target?: string | null | undefined;
}

export interface StorybookThemeWeb {
Expand Down Expand Up @@ -215,7 +220,7 @@ export interface StorybookThemeWeb {
barSelectedColor: string;
barBg: string;

brand: Brand;
brand?: Brand;

// [key: string]: any;
}
Expand Down
16 changes: 4 additions & 12 deletions packages/react-native-ui/src/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import { MobileMenuDrawer, MobileMenuDrawerRef } from './MobileMenuDrawer';
import { Sidebar } from './Sidebar';
import { DEFAULT_REF_ID } from './constants';
import { BottomBarToggleIcon } from './icon/BottomBarToggleIcon';
import { DarkLogo } from './icon/DarkLogo';
import { Logo } from './icon/Logo';

import { MenuIcon } from './icon/MenuIcon';
import { StorybookLogo } from './StorybookLogo';
import { useStoreBooleanState } from './hooks/useStoreState';

export const Layout = ({
Expand Down Expand Up @@ -79,11 +79,7 @@ export const Layout = ({
justifyContent: 'space-between',
}}
>
{theme.base === 'light' ? (
<Logo height={25} width={125} />
) : (
<DarkLogo height={25} width={125} />
)}
<StorybookLogo theme={theme} />

<IconButton onPress={() => setDesktopSidebarOpen(false)} Icon={MenuIcon} />
</View>
Expand Down Expand Up @@ -164,11 +160,7 @@ export const Layout = ({

<MobileMenuDrawer ref={mobileMenuDrawerRef}>
<View style={{ paddingLeft: 16, paddingTop: 4, paddingBottom: 4 }}>
{theme.base === 'light' ? (
<Logo height={25} width={125} />
) : (
<DarkLogo height={25} width={125} />
)}
<StorybookLogo theme={theme} />
</View>
<Sidebar
extra={[]}
Expand Down
76 changes: 76 additions & 0 deletions packages/react-native-ui/src/StorybookLogo.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { StoryObj, Meta } from '@storybook/react';
import { StorybookLogo } from './StorybookLogo';
import { Theme, theme } from '@storybook/react-native-theming';
import { Text } from 'react-native';

const meta = {
component: StorybookLogo,
title: 'UI/StorybookLogo',
args: {
theme: null,
},
} satisfies Meta<typeof StorybookLogo>;

export default meta;

type Story = StoryObj<typeof meta>;

export const TitleLogo: Story = {
args: {
theme: {
...theme,
brand: { title: 'React Native' },
} satisfies Theme,
},
};

export const ImageLogo: Story = {
args: {
theme: {
...theme,
brand: {
image: {
uri: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/512px-React-icon.svg.png',
height: 25,
width: 25,
},
},
} satisfies Theme,
},
};

export const ImageUrlLogo: Story = {
args: {
theme: {
...theme,
brand: {
image: {
uri: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/512px-React-icon.svg.png',
width: 25,
height: 25,
},
title: 'React Native',
url: 'https://reactnative.dev',
},
} satisfies Theme,
},
};

export const ImageSourceLogo: Story = {
args: {
theme: {
...theme,
brand: {
image: require('./assets/react-native-logo.png'),
resizeMode: 'contain',
url: 'https://reactnative.dev',
},
} satisfies Theme,
},
};

export const ImageElementLogo: Story = {
args: {
theme: { ...theme, brand: { image: <Text>Element</Text> } } satisfies Theme,
},
};
106 changes: 106 additions & 0 deletions packages/react-native-ui/src/StorybookLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Theme } from '@storybook/react-native-theming';
import { FC, isValidElement, ReactElement, useEffect, useMemo } from 'react';
import { Image, Linking, StyleProp, Text, TextStyle, TouchableOpacity } from 'react-native';
import { DarkLogo } from './icon/DarkLogo';
import { Logo } from './icon/Logo';

const WIDTH = 125;
const HEIGHT = 25;

const NoBrandLogo: FC<{ theme: Theme }> = ({ theme }) =>
theme.base === 'light' ? (
<Logo height={HEIGHT} width={WIDTH} />
) : (
<DarkLogo height={HEIGHT} width={WIDTH} />
);

function isElement(value: unknown): value is ReactElement {
return isValidElement(value);
}

const BrandLogo: FC<{ theme: Theme }> = ({ theme }) => {
const imageHasNoWidthOrHeight =
typeof theme.brand.image === 'object' &&
typeof theme.brand.image === 'object' &&
'uri' in theme.brand.image &&
(!('height' in theme.brand.image) || !('width' in theme.brand.image));

useEffect(() => {
if (imageHasNoWidthOrHeight) {
console.warn(
"STORYBOOK: When using a remote image as the brand logo, you must also set the width and height.\nFor example: brand: { image: { uri: 'https://sb.com/img.png', height: 25, width: 25}}"
);
}
}, [imageHasNoWidthOrHeight]);

if (!theme.brand.image) {
return null;
}

if (isElement(theme.brand.image)) {
return theme.brand.image;
}

const image = (
<Image
source={theme.brand.image}
resizeMode={theme.brand.resizeMode ?? 'contain'}
style={imageHasNoWidthOrHeight ? { width: WIDTH, height: HEIGHT } : undefined}
/>
);

if (theme.brand.url) {
return (
<TouchableOpacity
onPress={() => {
if (theme.brand.url) Linking.openURL(theme.brand.url);
}}
>
{image}
</TouchableOpacity>
);
} else {
return image;
}
};

const BrandTitle: FC<{ theme: Theme }> = ({ theme }) => {
const brandTitleStyle = useMemo<StyleProp<TextStyle>>(() => {
return {
width: WIDTH,
height: HEIGHT,
color: theme.color.defaultText,
fontSize: theme.typography.size.m1,
};
}, [theme]);

const title = (
<Text style={brandTitleStyle} numberOfLines={1} ellipsizeMode="tail">
{theme.brand.title}
</Text>
);

if (theme.brand.url) {
return (
<TouchableOpacity
onPress={() => {
if (theme.brand.url) Linking.openURL(theme.brand.url);
}}
>
{title}
</TouchableOpacity>
);
} else {
return title;
}
};

export const StorybookLogo: FC<{ theme: Theme }> = ({ theme }) => {
if (theme.brand?.image) {
return <BrandLogo theme={theme} />;
} else if (theme.brand?.title) {
return <BrandTitle theme={theme} />;
} else {
return <NoBrandLogo theme={theme} />;
}
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"jest": "^29.7.0",
"jotai": "^2.6.2",
"react": "18.3.1",
"react-native": "0.76.1",
"react-native": "0.76.3",
"react-test-renderer": "^18.3.1",
"tsup": "^7.2.0",
"typescript": "^5.3.3"
Expand Down
Loading