Skip to content

Commit

Permalink
feat(foundation): added Avatar component
Browse files Browse the repository at this point in the history
  • Loading branch information
bang9 committed Feb 24, 2022
1 parent 2006249 commit 0e451c3
Show file tree
Hide file tree
Showing 11 changed files with 282 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/uikit-react-native-foundation/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export { default as Icon } from './ui/Icon';
export { default as Text } from './ui/Text';
export { default as Modal } from './ui/Modal';
export { default as Header } from './ui/Header';
export { default as Avatar } from './ui/Avatar';
export { default as Dialogue } from './ui/Dialogue';

/** Styles **/
Expand Down
19 changes: 17 additions & 2 deletions packages/uikit-react-native-foundation/src/theme/Typography.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,97 @@
import { defaultScaleFactor } from '../styles/scaleFactor';
import type { Typography } from '../types';
import type { FontAttributes, Typography } from '../types';

export const createTypography = (overrides: Partial<Typography> = {}, scaleFactor = defaultScaleFactor): Typography => {
type TypographyOverrides = Partial<Typography> & {
shared?: Partial<FontAttributes>;
};
export const createTypography = (overrides: TypographyOverrides = {}, scaleFactor = defaultScaleFactor): Typography => {
return {
h1: {
fontWeight: '500',
fontSize: scaleFactor(18),
lineHeight: scaleFactor(20),
...overrides.h1,
...overrides.shared,
},
h2: {
fontWeight: 'bold',
fontSize: scaleFactor(16),
lineHeight: scaleFactor(20),
letterSpacing: scaleFactor(-0.2),
...overrides.h2,
...overrides.shared,
},
subtitle1: {
fontWeight: '500',
fontSize: scaleFactor(16),
lineHeight: scaleFactor(22),
letterSpacing: scaleFactor(-0.2),
...overrides.subtitle1,
...overrides.shared,
},
subtitle2: {
fontWeight: 'normal',
fontSize: scaleFactor(16),
lineHeight: scaleFactor(22),
...overrides.subtitle2,
...overrides.shared,
},
body1: {
fontWeight: 'normal',
fontSize: scaleFactor(16),
lineHeight: scaleFactor(20),
...overrides.body1,
...overrides.shared,
},
body2: {
fontWeight: '500',
fontSize: scaleFactor(14),
lineHeight: scaleFactor(16),
...overrides.body2,
...overrides.shared,
},
body3: {
fontWeight: 'normal',
fontSize: scaleFactor(14),
lineHeight: scaleFactor(20),
...overrides.body3,
...overrides.shared,
},
button: {
fontWeight: 'bold',
fontSize: scaleFactor(14),
lineHeight: scaleFactor(16),
letterSpacing: scaleFactor(0.4),
...overrides.button,
...overrides.shared,
},
caption1: {
fontWeight: 'bold',
fontSize: scaleFactor(12),
lineHeight: scaleFactor(12),
...overrides.caption1,
...overrides.shared,
},
caption2: {
fontWeight: 'normal',
fontSize: scaleFactor(12),
lineHeight: scaleFactor(12),
...overrides.caption2,
...overrides.shared,
},
caption3: {
fontWeight: 'bold',
fontSize: scaleFactor(11),
lineHeight: scaleFactor(12),
...overrides.caption3,
...overrides.shared,
},
caption4: {
fontWeight: 'normal',
fontSize: scaleFactor(11),
lineHeight: scaleFactor(12),
...overrides.caption4,
...overrides.shared,
},
};
};
Expand Down
15 changes: 14 additions & 1 deletion packages/uikit-react-native-foundation/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@ import type { TextStyle } from 'react-native';

import type Palette from './theme/Palette';

export type TypoName = `h${1 | 2}` | `subtitle${1 | 2}` | `body${1 | 2 | 3}` | 'button' | `caption${1 | 2 | 3 | 4}`;
export type TypoName =
| 'h1'
| 'h2'
| 'subtitle1'
| 'subtitle2'
| 'body1'
| 'body2'
| 'body3'
| 'button'
| 'caption1'
| 'caption2'
| 'caption3'
| 'caption4';
export type FontAttributes = Pick<TextStyle, 'fontFamily' | 'fontSize' | 'lineHeight' | 'letterSpacing' | 'fontWeight'>;
export type Typography = Record<TypoName, FontAttributes>;

Expand Down Expand Up @@ -38,6 +50,7 @@ export type UIKitColors = {
background: string;
borderBottom: string;
};

input: {
typeDefault: {
text: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { useMemo } from 'react';
import { StyleProp, View, ViewStyle } from 'react-native';

const MAX = 4;

type Props = {
size?: number;
containerStyle?: StyleProp<ViewStyle>;
};
const AvatarGroup: React.FC<Props> = ({ children, containerStyle, size = 56 }) => {
const childAmount = React.Children.count(children);
if (childAmount === 1) return <View style={containerStyle}>{children}</View>;

const avatars = useMemo(() => {
return (
React.Children.map(children, (child, index) => {
if (index + 1 > MAX) return child;
if (!React.isValidElement(child)) return child;

const top = getTopPoint(index, childAmount) * size;
const left = getLeftPoint(index) * size;
const width = getWidthPoint(index, childAmount) * size;
const height = getHeightPoint(index, childAmount) * size;
const innerLeft = -getInnerLeft(index, childAmount) * size;
const innerTop = -getInnerTop(childAmount) * size;

return (
<View style={{ overflow: 'hidden', position: 'absolute', top, left, width, height }}>
{React.cloneElement(child, { size, square: true, containerStyle: { left: innerLeft, top: innerTop } })}
</View>
);
})?.slice(0, 4) ?? []
);
}, [children]);

return (
<View style={[containerStyle, { overflow: 'hidden', width: size, height: size, borderRadius: size }]}>
{avatars}
</View>
);
};

const getHeightPoint = (_: number, total: number) => {
if (total === 2) return 1;
return 0.5;
};
const getWidthPoint = (idx: number, total: number) => {
if (total === 3 && idx === 0) return 1;
return 0.5;
};
const getTopPoint = (idx: number, total: number) => {
if (total === 2) return -0.025;
if (total === 3 && idx === 0) return -0.025;
if (total === 3 && idx !== 0) return 0.525;
if (idx === 0 || idx === 1) return -0.025;
return 0.525;
};
const getLeftPoint = (idx: number) => {
if (idx === 0 || idx === 2) return -0.025;
return 0.525;
};
const getInnerLeft = (idx: number, total: number) => {
if (total === 3 && idx === 0) return 0;
return 0.25;
};
const getInnerTop = (total: number) => {
if (total === 2) return 0;
return 0.25;
};

export default AvatarGroup;
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { StyleProp, View, ViewStyle } from 'react-native';

import createStyleSheet from '../../styles/createStyleSheet';
import useUIKitTheme from '../../theme/useUIKitTheme';
import Icon from '../Icon';

type Props = {
icon: keyof typeof Icon.Assets;
size?: number;
backgroundColor?: string;
containerStyle?: StyleProp<ViewStyle>;
};
const AvatarIcon: React.FC<Props> = ({ size = 56, icon, containerStyle, backgroundColor }) => {
const { colors, palette } = useUIKitTheme();
return (
<View
style={[
styles.container,
{
width: size,
height: size,
borderRadius: size / 2,
backgroundColor: backgroundColor ?? palette.background300,
},
containerStyle,
]}
>
<Icon icon={icon} size={size / 2} color={colors.onBackgroundReverse01} />
</View>
);
};
const styles = createStyleSheet({
container: {
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
},
});
export default AvatarIcon;
58 changes: 58 additions & 0 deletions packages/uikit-react-native-foundation/src/ui/Avatar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import { Image, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';

import createStyleSheet from '../../styles/createStyleSheet';
import useUIKitTheme from '../../theme/useUIKitTheme';
import Icon from '../Icon';
import AvatarGroup from './AvatarGroup';
import AvatarIcon from './AvatarIcon';

type SubComponents = { Group: typeof AvatarGroup; Icon: typeof AvatarIcon };
type Props = {
uri?: string;
size?: number;
square?: boolean;
muted?: boolean;
containerStyle?: StyleProp<ViewStyle>;
};
const Avatar: React.FC<Props> & SubComponents = ({ uri, square, muted = false, size = 56, containerStyle }) => {
const { colors, palette } = useUIKitTheme();
return (
<View
style={[
styles.container,
{ width: size, height: size, borderRadius: square ? 0 : size / 2, backgroundColor: palette.background300 },
containerStyle,
]}
>
{uri ? (
<Image source={{ uri }} resizeMode={'cover'} style={StyleSheet.absoluteFill} />
) : (
<Icon icon={'user'} size={size / 2} color={colors.onBackgroundReverse01} />
)}
{muted && <MutedOverlay size={size} />}
</View>
);
};

const MutedOverlay: React.FC<{ size: number }> = ({ size }) => {
const { palette } = useUIKitTheme();
return (
<View style={[styles.container, StyleSheet.absoluteFill]}>
<View style={[StyleSheet.absoluteFill, { backgroundColor: palette.primary300, opacity: 0.5 }]} />
<Icon color={palette.onBackgroundDark01} icon={'mute'} size={size * 0.72} />
</View>
);
};

const styles = createStyleSheet({
container: {
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
});

Avatar.Group = AvatarGroup;
Avatar.Icon = AvatarIcon;
export default Avatar;
1 change: 1 addition & 0 deletions sample/.storybook/storybook.requires.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ argsEnhancers.forEach((enhancer) => addArgsEnhancer(enhancer));

const getStories = () => {
return [
require("../stories/Avatar.stories.tsx"),
require("../stories/Dialogue.stories.tsx"),
require("../stories/GroupChannelPreview.stories.tsx"),
require("../stories/Icon.stories.tsx"),
Expand Down
1 change: 1 addition & 0 deletions sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"storybook-watcher": "sb-rn-watcher"
},
"dependencies": {
"@faker-js/faker": "^6.0.0-alpha.7",
"@react-native-async-storage/async-storage": "^1.15.17",
"@react-native-community/datetimepicker": "^5.1.0",
"@react-native-community/slider": "^4.2.0",
Expand Down
62 changes: 62 additions & 0 deletions sample/stories/Avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react-native';
import React from 'react';

import { Avatar as AvatarComponent, useUIKitTheme } from '@sendbird/uikit-react-native-foundation';

import { getMockImage } from './constant';

const AvatarMeta: ComponentMeta<typeof AvatarComponent> = {
title: 'Avatar',
component: AvatarComponent,
argTypes: {},
args: {},
};

export default AvatarMeta;

type AvatarStory = ComponentStory<typeof AvatarComponent>;
export const Avatar: AvatarStory = () => <DefaultAvatar />;
export const AvatarGroup: AvatarStory = () => <GroupedAvatar />;

const margin = { marginBottom: 12 };

const DefaultAvatar: React.FC = () => {
const { colors } = useUIKitTheme();

return (
<>
<AvatarComponent uri={getMockImage()} containerStyle={margin} />
<AvatarComponent uri={getMockImage()} muted containerStyle={margin} />
<AvatarComponent containerStyle={margin} />
<AvatarComponent.Icon icon={'broadcast'} backgroundColor={colors.secondary} containerStyle={margin} />
<AvatarComponent.Icon icon={'channels'} containerStyle={margin} />
</>
);
};
const GroupedAvatar: React.FC = () => {
return (
<>
<AvatarComponent.Group containerStyle={margin}>
<AvatarComponent uri={getMockImage()} />
</AvatarComponent.Group>

<AvatarComponent.Group containerStyle={margin}>
<AvatarComponent uri={getMockImage()} />
<AvatarComponent uri={getMockImage()} />
</AvatarComponent.Group>

<AvatarComponent.Group containerStyle={margin}>
<AvatarComponent />
<AvatarComponent uri={getMockImage()} />
<AvatarComponent uri={getMockImage()} />
</AvatarComponent.Group>

<AvatarComponent.Group>
<AvatarComponent />
<AvatarComponent uri={getMockImage()} />
<AvatarComponent muted uri={getMockImage()} />
<AvatarComponent uri={getMockImage()} />
</AvatarComponent.Group>
</>
);
};
Loading

0 comments on commit 0e451c3

Please sign in to comment.