Skip to content

Commit

Permalink
feat: support branding (#639)
Browse files Browse the repository at this point in the history
* feat: support branding

* fix: make title also clickable
refactor: add explanation to theme.brand

* fix: typo

* fix: typo

* fix: logo not rendering on mobile and limit to just image source prop

* fix: types

* fix: allow react element

* fix: update story

* fix

---------

Co-authored-by: Daniel Williams <[email protected]>
  • Loading branch information
tlow92 and dannyhw authored Nov 28, 2024
1 parent 55ef108 commit ddc397a
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 229 deletions.
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

0 comments on commit ddc397a

Please sign in to comment.