This commit is contained in:
Morten Olsen
2024-06-27 11:54:49 +02:00
commit 7db07922aa
69 changed files with 21566 additions and 0 deletions

1
packages/ui/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/storybook-static/

View File

@@ -0,0 +1,52 @@
import type { StorybookConfig } from "@storybook/react-webpack5";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-onboarding",
"@storybook/addon-links",
"@storybook/addon-essentials",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
],
typescript: {
check: false,
checkOptions: {},
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
compilerOptions: {
allowSyntheticDefaultImports: false,
esModuleInterop: false,
},
}
},
framework: "@storybook/react-webpack5",
docs: {
autodocs: "tag",
},
webpackFinal: (config) => {
config!.resolve!.alias = {
'react-native$': 'react-native-web',
};
config!.module!.rules!.push({
loader: 'babel-loader',
test: /\.tsx?$/,
options: {
presets: ['babel-preset-expo', '@babel/preset-typescript'],
},
});
config!.module!.rules!.push({
loader: 'babel-loader',
test: /\.jsx?$/,
options: {
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
},
include: [
/node_modules\/.*react-native.*/,
],
});
return config;
},
};
export default config;

View File

@@ -0,0 +1,6 @@
import { addons } from '@storybook/manager-api';
import { theme } from './theme';
addons.setConfig({
theme,
});

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { Decorator, Parameters } from '@storybook/react';
import "@fontsource/montserrat";
import { ThemeProvider } from 'styled-components';
import { theme } from './theme';
import { Provider, light } from '../src/theme/theme';
const ThemeDecorator: Decorator = (storyFn) => (
<ThemeProvider theme={theme}>
<Provider theme={light}>{storyFn()}</Provider>
</ThemeProvider>
)
export const decorators = [ThemeDecorator];
export const parameters: Parameters = {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
backgrounds: {
default: 'dark',
values: [
{
name: 'dark',
value: '#444',
default: true,
},
{
name: 'light',
value: '#ddd',
},
],
},
docs: {
// theme,
},
};

View File

@@ -0,0 +1,37 @@
import { create } from '@storybook/theming';
const theme = create({
base: 'light',
colorPrimary: '#156E80',
colorSecondary: '#156E80',
// UI
appBg: 'white',
appContentBg: '#F7F9FA',
appBorderColor: '#D4DBDE',
appBorderRadius: 4,
// Typography
fontBase: '"Rubik", sans-serif',
fontCode: 'monospace',
// Text colors
textColor: '#003143',
textInverseColor: 'rgba(255,255,255,0.9)',
// Toolbar default and active colors
barTextColor: '#609CA9',
barSelectedColor: '#156E80',
barBg: 'white',
// Form colors
inputBg: 'white',
inputBorder: '#156E80',
inputTextColor: '#003143',
inputBorderRadius: 4,
brandTitle: 'React Native Ref Design System',
});
export { theme };

55
packages/ui/package.json Normal file
View File

@@ -0,0 +1,55 @@
{
"name": "@react-ref/ui",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"files": [
"dist"
],
"scripts": {
"build": "tsc --build",
"build:dev": "tsc --build --watch",
"build:storybook": "storybook build",
"test:storybook": "test-storybook",
"dev:storybook": "storybook dev -p 6006"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/preset-typescript": "^7.24.1",
"@chromatic-com/storybook": "^1.3.3",
"@expo/vector-icons": "^13.0.0",
"@storybook/addon-essentials": "^8.0.9",
"@storybook/addon-interactions": "^8.0.9",
"@storybook/addon-links": "^8.0.9",
"@storybook/addon-onboarding": "^8.0.9",
"@storybook/blocks": "^8.0.9",
"@storybook/manager-api": "^8.0.9",
"@storybook/react": "^8.0.9",
"@storybook/react-webpack5": "^8.0.9",
"@storybook/test": "^8.0.9",
"@storybook/test-runner": "^0.17.0",
"@storybook/theming": "^8.0.9",
"@types/feather-icons": "^4.29.4",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"babel-loader": "^9.1.3",
"babel-preset-expo": "^10.0.2",
"expo": "^50.0.17",
"react": "^18.2.0",
"react-docgen-typescript": "^2.2.2",
"react-dom": "^18.2.0",
"react-native-web": "^0.19.11",
"storybook": "^8.0.9",
"typescript": "^5.4.5"
},
"dependencies": {
"@fontsource/montserrat": "^5.0.18",
"@tanstack/react-query": "^5.32.0",
"date-fns": "^3.6.0",
"feather-icons": "^4.29.1",
"react-native-safe-area-context": "^4.10.1",
"styled-components": "^6.1.8"
}
}

View File

@@ -0,0 +1,31 @@
import styled from "styled-components/native";
import { Title1 } from "../../../typography/typography";
interface AvatarProps {
name: string;
url?: string;
}
const Wrapper = styled.View`
align-items: center;
width: 50px;
height: 50px;
align-items: center;
justify-content: center;
`;
const Image = styled.Image`
width: 50px;
height: 50px;
border-radius: 25px;
`;
const Avatar: React.FC<AvatarProps> = ({ name, url }) => {
return (
<Wrapper>
{url ? <Image source={{ uri: url }} /> : <Title1>{name[0]}</Title1>}
</Wrapper>
);
};
export { Avatar };

View File

@@ -0,0 +1,4 @@
export * from "./avatar/avatar";
export * from "./icon/icon";
export * from "./relative-time/relative-time";
export * from "./carusel/carusel";

View File

@@ -0,0 +1,29 @@
import React from "react";
import { type Meta, type StoryObj } from "@storybook/react";
import { Carusel } from "./carusel";
import { CourseThumb, type CourseThumbProps } from "../../course/thumb/thumb";
type Story = StoryObj<typeof Carusel<CourseThumbProps>>;
const courses = new Array(10).fill(0).map<CourseThumbProps>((_, i) => ({
course: {
id: `${i}`,
title: `Course ${i}`,
cover: `https://via.placeholder.com/300?text=Course+${i}`,
lessions: 10,
duration: 60,
},
}));
export const Message: Story = {
args: {
items: courses,
getKey: (item) => item.course.id,
renderItem: (item) => <CourseThumb course={item.course} />,
},
};
export default {
title: "Components/Base/Carusel",
component: Carusel,
} as Meta<typeof Carusel>;

View File

@@ -0,0 +1,38 @@
import React from "react";
import styled from "styled-components/native";
import type { Theme } from "../../../theme/theme.types";
interface CaruselProps<T> {
items: T[];
getKey: (item: T, index: number) => string;
renderItem: (item: T) => React.ReactNode;
}
const Scroll = styled.ScrollView`
flex-grow: 0;
`;
const Wrapper = styled.View<{ theme: Theme }>`
flex: 1;
flex-direction: row;
justify-content: space-between;
gap: ${({ theme }) => theme.margins.small}px;
padding: ${({ theme }) => theme.margins.small}px;
`;
// eslint-disable-next-line react/function-component-definition
function Carusel<T>({ items, getKey, renderItem }: CaruselProps<T>) {
return (
<Scroll horizontal>
<Wrapper>
{items.map((item, index) => (
<React.Fragment key={getKey(item, index)}>
{renderItem(item)}
</React.Fragment>
))}
</Wrapper>
</Scroll>
);
}
export { Carusel, type CaruselProps };

View File

@@ -0,0 +1,25 @@
import React from "react";
import { Feather } from "@expo/vector-icons";
import { useTheme } from "styled-components/native";
import { type Theme } from "../../../theme/theme";
type IconNames = keyof typeof Feather.glyphMap;
interface IconProps {
size?: number;
color?: keyof Theme["colors"];
name: IconNames;
}
const Icon = ({ size, color, name }: IconProps) => {
const theme = useTheme();
return (
<Feather
name={name}
color={color ? theme.colors[color] : theme.colors.icon}
size={size ?? theme.sizes.icons}
/>
);
};
export type { IconNames };
export { Icon };

View File

@@ -0,0 +1,31 @@
import React from "react";
import { icons } from "feather-icons";
import { useTheme } from "styled-components/native";
import { type Theme } from "../../../theme/theme";
type IconNames = keyof typeof icons;
interface IconProps {
size?: number;
color?: keyof Theme["colors"];
name: IconNames;
}
const Icon = ({ size = 24, color, name }: IconProps) => {
const theme = useTheme();
return (
<svg
dangerouslySetInnerHTML={{
__html: icons[name].toSvg({
color: color ? theme.colors[color] : undefined,
}),
}}
viewBox="0 0 24 24"
width={size}
height={size}
fill={color ? theme.colors[color] : undefined}
/>
);
};
export type { IconNames };
export { Icon };

View File

@@ -0,0 +1,26 @@
import { formatDistanceToNow } from "date-fns";
import { useEffect, useState } from "react";
interface RelativeTimeProps {
date: Date;
}
const format = (date: Date) => formatDistanceToNow(date, { addSuffix: true });
const RelativeTime: React.FC<RelativeTimeProps> = ({ date }) => {
const [displayDate, setDisplayDate] = useState(format(date));
useEffect(() => {
const interval = setInterval(() => {
setDisplayDate(format(date));
}, 1000 * 30);
setDisplayDate(format(date));
return () => {
clearInterval(interval);
};
}, [date]);
return <>{displayDate}</>;
};
export { RelativeTime };

View File

@@ -0,0 +1,2 @@
export * from "./message/message";
export * from "./input/input";

View File

@@ -0,0 +1,64 @@
import React, { useCallback, useState } from "react";
import { type Meta, type StoryObj } from "@storybook/react";
import { userEvent, within, expect } from '@storybook/test';
import { CommunityInput } from "./input";
import { Body1 } from "../../../typography/typography";
type Story = StoryObj<typeof CommunityInput>;
export const Empty: Story = {
args: {},
};
export const WithText: Story = {
args: {
value: "Hello, world!",
},
};
export const Interaction: Story = {
render: () => {
const [value, setValue] = useState("");
const [submitted, setSubmitted] = useState("");
const onSend = useCallback(() => {
setSubmitted(value);
setValue('');
}, [value]);
return (
<>
<div style={{ display: 'none' }}>
<Body1 testID="submitted-text">{submitted}</Body1>
</div>
<CommunityInput
value={value}
onChange={(e) => setValue(e)}
onSend={onSend}
/>
</>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const submitted = canvas.getByTestId('submitted-text');
const textField = canvas.getByTestId('community-input-text');
const button = canvas.getByTestId('community-input-button');
expect(submitted).toHaveTextContent('');
expect(textField).toHaveValue('');
await userEvent.type(textField, 'Hey there!');
expect(submitted).toHaveTextContent('');
expect(textField).toHaveValue('Hey there!');
await userEvent.click(button);
expect(submitted).toHaveTextContent('Hey there!');
expect(textField).toHaveValue('');
}
};
export default {
title: "Components/Community/Input",
component: CommunityInput,
} as Meta<typeof CommunityInput>;

View File

@@ -0,0 +1,58 @@
import styled from "styled-components/native";
import { Body1 } from "../../../typography/typography";
import { Icon } from "../../base/icon/icon";
interface CommunityInputProps {
sending?: boolean;
value: string;
onChange?: (value: string) => void;
onSend?: () => void;
}
const Wrapper = styled.View`
overflow: hidden;
border-radius: ${({ theme }) => theme.sizes.corners}px;
background-color: ${({ theme }) => theme.colors.background};
padding: ${({ theme }) => theme.margins.small}px;
gap: ${({ theme }) => theme.margins.medium}px;
flex-direction: row;
`;
const InputField = styled.TextInput`
flex: 1;
color: ${({ theme }) => theme.colors.text};
`;
const Send = styled.TouchableOpacity`
flex-direction: row;
align-items: center;
gap: ${({ theme }) => theme.margins.small}px;
background: ${({ theme }) => theme.colors.primary};
padding: ${({ theme }) => theme.margins.small}px;
border-radius: ${({ theme }) => theme.sizes.corners}px;
`;
const CommunityInput: React.FC<CommunityInputProps> = ({
value,
onChange,
onSend,
}) => {
return (
<Wrapper>
<InputField
placeholder="Join the conversation"
value={value}
testID="community-input-text"
onChangeText={onChange}
/>
<Send onPress={onSend} testID="community-input-button">
<>
<Icon color="primaryContrast" name="send" />
<Body1 color="primaryContrast">Send</Body1>
</>
</Send>
</Wrapper>
);
};
export { CommunityInput, type CommunityInputProps };

View File

@@ -0,0 +1,21 @@
import { type Meta, type StoryObj } from "@storybook/react";
import { CommunityMessage } from "./message";
type Story = StoryObj<typeof CommunityMessage>;
export const Message: Story = {
args: {
user: {
name: "User name",
avatar: "https://via.placeholder.com/300",
},
content: "Message content",
publishedAt: new Date("2021-01-01T00:00:00"),
liked: false,
},
};
export default {
title: "Components/Community/Message",
component: CommunityMessage,
} as Meta<typeof CommunityMessage>;

View File

@@ -0,0 +1,73 @@
import styled from "styled-components/native";
import { Body1, Caption } from "../../../typography/typography";
import { Icon } from "../../base/icon/icon";
import { TouchableOpacity } from "react-native";
import { RelativeTime } from "../../base/relative-time/relative-time";
import { Avatar } from "../../base/avatar/avatar";
interface CommunityMessageProps {
id: string;
user: {
name: string;
avatar: string;
};
publishedAt: Date;
content: string;
liked: boolean;
onLikeChange?: (liked: boolean) => void;
}
const Wrapper = styled.View`
overflow: hidden;
border-radius: ${({ theme }) => theme.sizes.corners}px;
background-color: ${({ theme }) => theme.colors.background};
padding: ${({ theme }) => theme.margins.medium}px;
gap: ${({ theme }) => theme.margins.medium}px;
`;
const Header = styled.View`
flex-direction: row;
gap: ${({ theme }) => theme.margins.medium}px;
align-items: center;
`;
const HeaderInfo = styled.View``;
const Actions = styled.View`
flex-direction: row;
gap: ${({ theme }) => theme.margins.small}px;
`;
const CommunityMessage: React.FC<CommunityMessageProps> = ({
user,
publishedAt,
content,
liked,
onLikeChange,
}) => {
return (
<Wrapper>
<Header>
<Avatar url={user.avatar} name={user.name} />
<HeaderInfo>
<Body1>{user.name}</Body1>
<Caption color="textShade">
<RelativeTime date={publishedAt} />
</Caption>
</HeaderInfo>
</Header>
<Body1>{content}</Body1>
<Actions>
<TouchableOpacity
onPress={() => {
onLikeChange && onLikeChange(!liked);
}}
>
<Icon name={liked ? "heart" : "airplay"} size={16} />
</TouchableOpacity>
</Actions>
</Wrapper>
);
};
export { CommunityMessage, type CommunityMessageProps };

View File

@@ -0,0 +1,3 @@
export * from "./base/base";
export * from "./community/community";
export * from "./course/course";

View File

@@ -0,0 +1 @@
export * from "./thumb/thumb";

View File

@@ -0,0 +1,46 @@
import { type Meta, type StoryObj } from "@storybook/react";
import { CourseThumb } from "./thumb";
type Story = StoryObj<typeof CourseThumb>;
export const WithProgress: Story = {
args: {
course: {
title: "Course title",
cover: "https://via.placeholder.com/300",
lessions: 10,
duration: 60,
},
progress: {
percentage: 27,
},
},
};
export const WithoutProgress: Story = {
args: {
course: {
title: "Course title",
cover: "https://via.placeholder.com/300",
lessions: 10,
duration: 60,
},
},
};
export const Bookmarked: Story = {
args: {
course: {
title: "Course title",
cover: "https://via.placeholder.com/300",
lessions: 10,
duration: 60,
},
bookmarked: true,
},
};
export default {
title: "Components/Course/Thumb",
component: CourseThumb,
} as Meta<typeof CourseThumb>;

View File

@@ -0,0 +1,113 @@
import styled from "styled-components/native";
import { Caption, Title1 } from "../../../typography/typography";
import { Icon } from "../../base/icon/icon";
import { TouchableOpacity } from "react-native";
interface CourseThumbProps {
onPress?: () => void;
onBookmarkChange?: (state: boolean) => void;
course: {
id: string;
title: string;
cover: string;
lessions: number;
duration: number;
};
bookmarked?: boolean;
progress?: {
percentage: number;
};
}
const Wrapper = styled.View`
overflow: hidden;
width: 300px;
border-radius: ${({ theme }) => theme.sizes.corners}px;
background-color: ${({ theme }) => theme.colors.background};
position: relative;
`;
const Cover = styled.Image`
width: 100%;
aspect-ratio: 16/9;
border-top-left-radius: ${({ theme }) => theme.sizes.corners}px;
border-top-right-radius: ${({ theme }) => theme.sizes.corners}px;
`;
const Info = styled.View`
padding: ${({ theme }) => theme.margins.small}px;
gap: ${({ theme }) => theme.margins.small}px;
`;
const Meta = styled.View`
flex-direction: row;
gap: ${({ theme }) => theme.margins.small}px;
`;
const MetaItem = styled.View`
flex-direction: row;
align-items: center;
gap: ${({ theme }) => theme.margins.small}px;
`;
const ProgressWrapper = styled.View`
background-color: ${({ theme }) => theme.colors.primary};
position: absolute;
top: ${({ theme }) => theme.margins.small}px;
left: ${({ theme }) => theme.margins.small}px;
padding: ${({ theme }) => theme.margins.small}px;
border-radius: ${({ theme }) => theme.sizes.corners}px;
`;
const BookmarkWrapper = styled.View`
position: absolute;
top: ${({ theme }) => theme.margins.small}px;
right: ${({ theme }) => theme.margins.small}px;
padding: ${({ theme }) => theme.margins.small}px;
`;
const CourseThumb: React.FC<CourseThumbProps> = ({
course,
progress,
bookmarked,
onPress,
onBookmarkChange,
}) => {
return (
<TouchableOpacity onPress={onPress}>
<Wrapper>
<Cover source={{ uri: course.cover }} />
{progress ? (
<ProgressWrapper>
<Caption color="primaryContrast">
{Math.round(progress.percentage)}% completed
</Caption>
</ProgressWrapper>
) : null}
<BookmarkWrapper>
<TouchableOpacity
onPress={() => {
onBookmarkChange && onBookmarkChange(!bookmarked);
}}
>
<Icon name={bookmarked ? "book-open" : "book"} size={24} />
</TouchableOpacity>
</BookmarkWrapper>
<Info>
<Title1>{course.title}</Title1>
<Meta>
<MetaItem>
<Caption>{course.lessions} lessons</Caption>
</MetaItem>
<MetaItem>
<Icon name="clock" size={16} />
<Caption>{course.duration} min</Caption>
</MetaItem>
</Meta>
</Info>
</Wrapper>
</TouchableOpacity>
);
};
export { CourseThumb, type CourseThumbProps };

View File

@@ -0,0 +1,74 @@
import React from "react";
import { type Meta } from "@storybook/react";
import { useTheme } from "styled-components/native";
import styled from "styled-components";
const Table = styled.table`
background: white;
border-radius: 5px;
width: 100%;
max-width: 900px;
margin: auto;
border-collapse: collapse;
border-spacing: 0;
td {
margin: 0;
padding: 15px 15px;
}
`;
const Thead = styled.thead`
font-weight: bold;
`;
const Row = styled.tr`
padding: 0 15px;
&:nth-child(even) {
background: rgba(0, 0, 0, 0.05);
}
`;
const Example = styled.div<{ color: string }>`
background: ${(props) => props.color};
width: 50px;
height: 50px;
`;
const SpacingComponent = () => {
const theme = useTheme();
return (
<Table>
<Thead>
<tr>
<td>Example</td>
<td>Name</td>
<td>Color</td>
</tr>
</Thead>
<tbody>
{Object.entries(theme.colors).map(([key, value]) => {
return (
<Row key={key}>
<td>
<Example color={value} />
</td>
<td>{key}</td>
<td>{value}</td>
</Row>
);
})}
</tbody>
</Table>
);
};
export default {
title: "Foundation/Colors",
component: SpacingComponent,
} as Meta<typeof SpacingComponent>;
export const Colors = () => {
return <SpacingComponent />;
};

View File

@@ -0,0 +1,47 @@
import React from "react";
import { type Meta } from "@storybook/react";
import styled from "styled-components";
import { Icon } from "../components/base/icon/icon";
import { icons } from "feather-icons";
const Table = styled.div`
background: white;
border-radius: 5px;
width: 100%;
max-width: 900px;
margin: auto;
display: flex;
flex-wrap: wrap;
`;
const Row = styled.div`
padding: 35px;
&:nth-child(even) {
background: rgba(0, 0, 0, 0.05);
}
`;
const Name = styled.div`
text-align: center;
font-weight: bold;
`;
export const Icons = () => {
return (
<Table>
{Object.keys(icons).map((key) => {
return (
<Row key={key}>
<Icon name={key as keyof typeof icons} size={120} />
<Name>{key}</Name>
</Row>
);
})}
</Table>
);
};
export default {
title: "Foundation/Icons",
component: Icons,
} as Meta<typeof Icons>;

View File

@@ -0,0 +1,74 @@
import React from "react";
import { type Meta } from "@storybook/react";
import { useTheme } from "styled-components/native";
import styled from "styled-components";
const Table = styled.table`
background: white;
border-radius: 5px;
width: 100%;
max-width: 900px;
margin: auto;
border-collapse: collapse;
border-spacing: 0;
td {
margin: 0;
padding: 15px 15px;
}
`;
const Thead = styled.thead`
font-weight: bold;
`;
const Row = styled.tr`
padding: 0 15px;
&:nth-child(even) {
background: rgba(0, 0, 0, 0.05);
}
`;
const Example = styled.div<{ size: number }>`
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
background: red;
`;
const SpacingComponent = () => {
const theme = useTheme();
return (
<Table>
<Thead>
<tr>
<td>Example</td>
<td>Name</td>
<td>Size</td>
</tr>
</Thead>
<tbody>
{Object.entries(theme.margins).map(([key, value]) => {
return (
<Row key={key}>
<td>
<Example size={value} />
</td>
<td>{key}</td>
<td>{value}px</td>
</Row>
);
})}
</tbody>
</Table>
);
};
export default {
title: "Foundation/Spacing",
component: SpacingComponent,
} as Meta<typeof SpacingComponent>;
export const Spacing = () => {
return <SpacingComponent />;
};

View File

@@ -0,0 +1,74 @@
import React from "react";
import { type Meta } from "@storybook/react";
import { useTheme } from "styled-components/native";
import { types } from "../typography/typography";
import styled from "styled-components";
const Table = styled.table`
background: white;
border-radius: 5px;
width: 100%;
max-width: 900px;
margin: auto;
border-collapse: collapse;
border-spacing: 0;
td {
margin: 0;
padding: 15px 15px;
}
`;
const Thead = styled.thead`
font-weight: bold;
`;
const Row = styled.tr`
padding: 0 15px;
&:nth-child(even) {
background: rgba(0, 0, 0, 0.05);
}
`;
const TypographyComponent = () => {
const theme = useTheme();
return (
<Table>
<Thead>
<tr>
<td>Example</td>
<td>Name</td>
<td>Size</td>
<td>Weight</td>
<td>Spacing</td>
</tr>
</Thead>
<tbody>
{Object.entries(theme.typography).map(([key, value]) => {
const Component = types[key as keyof typeof types];
return (
<Row key={key}>
<td>
<Component>{key}</Component>
</td>
<td>{key}</td>
<td>{value.size ?? 1}x</td>
<td>{value.weight ?? "normal"}</td>
<td>{value.spacing ?? 0}px</td>
</Row>
);
})}
</tbody>
</Table>
);
};
export default {
title: "Foundation/Typography",
component: TypographyComponent,
} as Meta<typeof TypographyComponent>;
export const Typography = () => {
return <TypographyComponent />;
};

3
packages/ui/src/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from "./components/components";
export * from "./typography/typography";
export * from "./theme/theme";

11
packages/ui/src/theme/styled.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
import { } from "styled-components/native";
import { type Theme } from "./theme.types"; // Import type from above file
declare module "styled-components" {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DefaultTheme extends Theme { }
}
declare module "styled-components/native" {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DefaultTheme extends Theme { }
}

View File

@@ -0,0 +1,58 @@
import { Platform } from "react-native";
import { type Theme } from "./theme";
const light: Theme = {
typography: {
Jumbo: {
weight: "bold",
size: 2.8,
},
Title1: {
weight: "bold",
},
Title2: {
weight: "bold",
size: 1.3,
},
Body1: {},
Overline: {
size: 0.8,
upperCase: true,
},
Caption: {
size: 0.8,
},
Link: {
upperCase: true,
weight: "bold",
},
},
colors: {
primary: "#156e80",
primaryContrast: "#fff",
icon: "#156e80",
destructive: "#e74c3c",
shade: "#ededed",
input: "#ddd",
secondary: "#ff9f43",
shadow: "#000",
background: "#fff",
text: "#000",
textShade: "#999",
},
sizes: {
corners: 5,
icons: 24,
},
margins: {
small: 8,
medium: 16,
large: 24,
},
font: {
family: Platform.OS === "web" ? "Montserrat" : undefined,
baseSize: Platform.OS === "web" ? 14 : 11,
},
};
export { light };

View File

@@ -0,0 +1,13 @@
import { ThemeProvider } from "styled-components/native";
import { type Theme } from "./theme.types";
interface ProviderProps {
children: React.ReactNode;
theme: Theme;
}
const Provider: React.FC<ProviderProps> = ({ children, theme }) => (
<ThemeProvider theme={theme}>{children}</ThemeProvider>
);
export { Provider };

View File

@@ -0,0 +1,3 @@
export * from "./theme.provider";
export * from "./theme.types";
export * from "./theme.light";

View File

@@ -0,0 +1,47 @@
interface Typography {
family?: string;
size?: number;
spacing?: number;
weight?: string;
upperCase?: boolean;
}
interface Theme {
typography: {
Jumbo: Typography;
Title2: Typography;
Title1: Typography;
Body1: Typography;
Caption: Typography;
Overline: Typography;
Link: Typography;
};
colors: {
primary: string;
primaryContrast: string;
destructive: string;
icon: string;
input: string;
secondary: string;
background: string;
shadow: string;
shade: string;
text: string;
textShade: string;
};
sizes: {
corners: number;
icons: number;
};
margins: {
small: number;
medium: number;
large: number;
};
font: {
family?: string;
baseSize: number;
};
}
export type { Theme, Typography };

View File

@@ -0,0 +1,57 @@
import styled from "styled-components/native";
import type { Theme, Typography } from "../theme/theme";
interface TextProps {
color?: keyof Theme["colors"];
bold?: boolean;
theme: Theme;
}
const BaseText = styled.Text<TextProps>`
${({ theme }) =>
theme.font.family ? `font-family: ${theme.font.family};` : ""}
color: ${({ color, theme }) =>
color ? theme.colors[color] : theme.colors.text};
font-weight: ${({ bold }) => (bold ? "bold" : "normal")};
font-size: ${({ theme }) => theme.font.baseSize}px;
`;
const get = (name: keyof Theme["typography"], theme: Theme): Typography => {
const typography = theme.typography[name];
return typography;
};
const createTypography = (name: keyof Theme["typography"]) => {
const Component = styled(BaseText)<TextProps>`
font-size: ${({ theme }) =>
theme.font.baseSize * (get(name, theme).size ?? 1)}px;
font-weight: ${({ bold, theme }) =>
typeof bold !== "undefined"
? "bold"
: get(name, theme).weight ?? "normal"};
${({ theme }) =>
get(name, theme).upperCase ? "text-transform: uppercase;" : ""}
`;
return Component;
};
const Jumbo = createTypography("Jumbo");
const Title2 = createTypography("Title2");
const Title1 = createTypography("Title1");
const Body1 = createTypography("Body1");
const Overline = createTypography("Overline");
const Caption = createTypography("Caption");
const Link = createTypography("Link");
const types: { [key in keyof Theme["typography"]]: typeof BaseText } = {
Jumbo,
Title2,
Title1,
Body1,
Overline,
Caption,
Link,
};
export type { TextProps };
export { types, Jumbo, Title2, Title1, Body1, Overline, Caption, Link };

14
packages/ui/tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"noEmit": false,
"declaration": true,
"sourceMap": true,
"declarationMap": true
},
"include": ["src"],
"exclude": ["**/*.test.ts", "**/*.test.tsx", "**/*.stories.tsx", "**/*.stories.ts", "node_modules", "dist"]
}