This commit is contained in:
Morten Olsen
2022-05-19 15:57:20 +02:00
parent 6181eeb0c8
commit 2b0ad8592b
156 changed files with 26987 additions and 14366 deletions

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

@@ -0,0 +1,3 @@
/node_modules
/.expo
/public

View File

@@ -0,0 +1,58 @@
const path = require('path');
const { mergeConfig } = require('vite');
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
framework: '@storybook/react',
// core: {
// builder: '@storybook/builder-vite',
// },
typescript: {
check: false,
checkOptions: {},
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
compilerOptions: {
allowSyntheticDefaultImports: false,
esModuleInterop: false,
},
}
},
staticsDirs: ['../public', '../public/assets'],
async webpackFinal(config, { configType }) {
config.resolve.alias['react-native'] = 'react-native-web';
config.module.rules.push({
loader: 'babel-loader',
test: /\.jsx?$/,
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
include: [
/node_modules\/.*react-native.*/,
],
});
return config;
},
async viteFinal(config) {
return mergeConfig(config, {
optimizeDeps: {
esbuildOptions: {
loader: {
'.js': 'jsx',
},
},
},
base: './',
resolve: {
alias: {
'@': path.resolve(__dirname, '..', 'src'),
'react-native': 'react-native-web',
},
},
assetsDir: './public/assets',
});
},
};

View File

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

View File

@@ -0,0 +1,31 @@
import { theme } from './theme';
import { addDecorator } from "@storybook/react";
import { Provider } from '../src/theme';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import "@fontsource/montserrat";
const ThemeDecorator = (storyFn: any) => (
<SafeAreaProvider>
<Provider>{storyFn()}</Provider>
</SafeAreaProvider>
)
addDecorator(ThemeDecorator);
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
options: {
storySort: (b: any, a: any) =>
a[1].kind === b[1].kind ? 0 : a[1].id.localeCompare(b[1].id, undefined, { numeric: true }),
},
docs: {
theme,
},
};

View File

@@ -0,0 +1,38 @@
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: 'Morten\'s App Design System',
//brandImage: 'https://zn-prod-euw1-cdn.s3.eu-west-1.amazonaws.com/logo-dark-gradient.png',
});
export { theme };

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

@@ -0,0 +1,89 @@
{
"name": "@morten-olsen/ui",
"version": "1.0.0",
"homepage": "/bob-the-algorithm",
"jest": {
"preset": "jest-expo"
},
"resolutions": {
"@types/react": "~17.0.21",
"@types/react-dom": "~18.0.3"
},
"scripts": {
"build": "yarn clean && run-p build:*",
"build:types": "tsc --emitDeclarationOnly && tsc-alias",
"build:script": "vite build",
"clean": "rimraf dist",
"lint:types": "tsc --noEmit",
"preview": "vite preview",
"dev": "start-storybook -p 6006",
"build:storybook": "build-storybook -o public",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@expo/vector-icons": "^12.0.0",
"@fontsource/montserrat": "^4.5.10",
"@react-navigation/bottom-tabs": "^6.0.5",
"@react-navigation/native": "^6.0.2",
"@react-navigation/native-stack": "^6.1.0",
"@react-navigation/stack": "^6.2.1",
"chroma-js": "^2.4.2",
"date-fns": "^2.28.0",
"feather-icons": "^4.29.0",
"parse-css-color": "^0.2.1",
"react-feather": "^2.0.9",
"react-native-calendar-strip": "^2.2.5",
"react-native-calendars": "^1.1284.0",
"react-native-collapsible": "^1.6.0",
"react-native-gesture-handler": "^2.4.2",
"react-native-get-random-values": "^1.8.0",
"react-native-safe-area-context": "3.3.2",
"react-native-screens": "~3.10.1",
"string-to-color": "^2.2.2",
"styled-components": "^5.3.5"
},
"peerDependencies": {
"react": "17.0.1",
"react-dom": "17.0.1",
"react-native": "0.64.3"
},
"devDependencies": {
"@babel/core": "^7.12.9",
"@bunchtogether/vite-plugin-flow": "^1.0.2",
"@originjs/vite-plugin-commonjs": "^1.0.3",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
"@rollup/plugin-babel": "^5.3.1",
"@storybook/addon-actions": "^6.4.22",
"@storybook/addon-essentials": "^6.4.22",
"@storybook/addon-links": "^6.4.22",
"@storybook/builder-vite": "^0.1.33",
"@storybook/react": "^6.4.22",
"@storybook/theming": "^6.4.22",
"@types/chroma-js": "^2.1.3",
"@types/feather-icons": "^4.7.0",
"@types/react": "~17.0.21",
"@types/react-dom": "^18.0.3",
"@types/styled-components-react-native": "^5.1.3",
"autoprefixer": "^10.4.7",
"babel-plugin-module-resolver": "^4.1.0",
"esbuild-plugin-flow": "^0.3.2",
"expo-cli": "^5.4.3",
"flow-remove-types": "^2.178.0",
"jest": "^26.6.3",
"jest-expo": "~44.0.1",
"postcss": "^8.4.13",
"react": "17.0.1",
"react-docgen": "^5.4.0",
"react-docgen-typescript": "^2.2.2",
"react-dom": "17.0.1",
"react-native-web": "0.17.1",
"react-refresh": "^0.13.0",
"react-test-renderer": "17.0.1",
"storybook": "^6.4.22",
"typescript": "~4.3.5",
"vite": "^2.9.9",
"webpack-hot-middleware": "^2.25.1",
"webpack-merge": "^5.8.0"
}
}

View File

@@ -0,0 +1,39 @@
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs';
import { Button } from '.';
<Meta title="Components/Button" component={Button} />
# Button
Has following variants: Primary, secondary, outlined, text only, destructive, disabled and comes in 4 sizes.
## Variants
`type: "primary"`
<Canvas>
<Story name="Primary">
<Button type="primary" title="Foo" />
</Story>
</Canvas>
`type: "secondary"`
<Canvas>
<Story name="Secondary">
<Button type="secondary" title="Foo" />
</Story>
</Canvas>
`type: "destructive"`
<Canvas>
<Story name="Destructive">
<Button type="destructive" title="Foo" />
</Story>
</Canvas>
## Component arguments
<ArgsTable of={Button} />

View File

@@ -0,0 +1,82 @@
import React from 'react';
import styled from 'styled-components/native';
import { IconNames, Icon } from '../icon';
import { Theme } from '../../../theme';
import { Link } from '../../../typography';
import { StyleProp, ViewStyle } from 'react-native';
type AccessibilityRole = 'button' | 'link' | 'image' | 'keyboardkey' | 'search' | 'text' | 'adjustable' | 'header' | 'imagebutton';
type ButtonProps = {
title?: string;
icon?: IconNames;
onPress?: () => any;
style?: StyleProp<ViewStyle>;
accessibilityRole?: AccessibilityRole;
accessibilityLabel?: string;
accessibilityHint?: string;
type?: 'primary' | 'secondary' | 'destructive';
}
const Touch = styled.TouchableOpacity``;
const Wrapper = styled.View<{
style?: StyleProp<ViewStyle>;
background?: keyof Theme['colors'],
border: keyof Theme['colors'],
theme: Theme,
}>`
background: ${({ background, theme }) => background ? theme.colors[background] : 'transparent'};
border-color: ${({ border, theme }) => theme.colors[border]};
border-width: 1px;
padding:
${({ theme }) => theme.margins.medium}px;
${({ theme }) => theme.margins.small}px;
border-radius: ${({ theme }) => theme.sizes.corners}px;
align-items: center;
`;
const getColors = (type: ButtonProps['type']): [keyof Theme['colors'], keyof Theme['colors'], keyof Theme['colors']] => {
switch (type) {
case 'primary': {
return ['background', 'primary', 'primary'];
}
case 'secondary': {
return ['primary', 'background', 'primary'];
}
case 'destructive': {
return ['background', 'destructive', 'destructive'];
}
}
throw new Error('Button type not supported');
};
const Button = ({
title,
icon,
type = 'primary',
onPress,
accessibilityHint,
accessibilityRole,
accessibilityLabel,
style,
}: ButtonProps) => {
const [textColor, backgroundColor, borderColor] = getColors(type);
return (
<Touch
onPress={onPress}
accessible
accessibilityHint={accessibilityHint}
accessibilityRole={accessibilityRole}
accessibilityLabel={accessibilityLabel}
>
<Wrapper style={style} background={backgroundColor} border={borderColor}>
{title && <Link color={textColor}>{title}</Link>}
{icon && <Icon name={icon} color={textColor} />}
</Wrapper>
</Touch>
);
};
export type { ButtonProps, AccessibilityRole };
export { Button };

View File

@@ -0,0 +1,29 @@
import React, { ReactNode } from 'react';
import { Icon } from '../icon';
import { Row, Cell } from '../row';
interface Props {
title: string;
add?: () => void;
onPress?: () => void;
left?: ReactNode;
}
function Header({ title, add, onPress, left }: Props) {
return (
<Row
onPress={onPress}
left={left}
title={title}
right={
add && (
<Cell onPress={add}>
<Icon name="plus-circle" size={18} />
</Cell>
)
}
/>
);
}
export { Header };

View File

@@ -0,0 +1,75 @@
import React, { ReactNode, useState } from 'react';
import styled from 'styled-components/native';
import Collapsible from 'react-native-collapsible';
import { Body1 } from '../../../typography';
import { Icon } from '../icon';
import { Row, Cell, RowProps } from '../row';
import { Header } from './header';
interface ListProps<T> {
title: string;
items: T[];
startHidden?: boolean;
getKey: (item: T) => any;
render: (item: T) => RowProps;
add?: () => void;
}
interface ChildProps {
title: string;
startHidden?: boolean;
add?: () => void;
children?: ReactNode;
}
const Wrapper = styled.View`
border-radius: 7px;
background: ${({ theme }) => theme.colors.background};
shadow-offset: 0 0;
shadow-opacity: 0.1;
shadow-color: ${({ theme }) => theme.colors.shadow};
shadow-radius: 5px;
`;
function Group<T = any>(props: ListProps<T> | ChildProps) {
const [visible, setVisible] = useState(!props.startHidden);
const { title, items, getKey, render, add, children } =
props as ListProps<T> & ChildProps;
return (
<Row>
<Wrapper>
<>
<Header
left={
<Cell><Icon name={visible ? 'chevron-down' : 'chevron-up'} size={18} /></Cell>
}
title={title}
add={add}
onPress={() => setVisible(!visible)}
/>
<Collapsible collapsed={!visible}>
{items && items.map((item, i) => (
<Row key={getKey(item) || i} {...render(item)} />
))}
{children}
{!children && (!items || items.length === 0) && (
<Row
left={
<Cell>
<Icon color="textShade" name="maximize" />
</Cell>
}
>
<Body1 style={{ marginLeft: 10 }} color="textShade">
Empty
</Body1>
</Row>
)}
</Collapsible>
</>
</Wrapper>
</Row>
);
}
export { Group };

View File

@@ -0,0 +1,18 @@
import styled from "styled-components/native";
type Props = {
children: React.ReactNode;
};
const Wrapper = styled.View`
flex-direction: row;
`;
const Horizontal = ({ children }: Props) => {
return (
<Wrapper>
{children}
</Wrapper>
);
};
export { Horizontal };

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { Feather, } from '@expo/vector-icons';
import { useTheme } from 'styled-components/native';
import { Theme } from '../../../theme';
type IconNames = keyof typeof Feather.glyphMap;
type Props = {
size?: number;
color?: keyof Theme['colors'];
name: IconNames;
}
function Icon({
size,
color,
name,
}: Props) {
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 { Theme } from '../../../theme';
type IconNames = keyof typeof icons;
type Props = {
size?: number;
color?: keyof Theme['colors'];
name: IconNames;
}
function Icon({
size = 24,
color,
name,
}: Props) {
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,9 @@
export * from './icon';
export * from './horizontal';
export * from './modal';
export * from './page';
export * from './popup';
export * from './row';
export * from './button';
export * from './group';
export * from './list';

View File

@@ -0,0 +1,53 @@
import { FlatList } from "react-native";
import { Button } from "../button";
import { Icon } from "../icon";
import { Cell, Row, RowProps } from "../row";
type ListProps<T> = {
add?: () => void;
remove?: (item: T) => any;
getKey: (item: T) => string;
items: T[];
render: (item: T) => RowProps;
}
function List<T>({
add,
remove,
getKey,
items,
render,
}: ListProps<T>) {
return (
<>
{!!add && <Button title="Add" onPress={add}/>}
<FlatList
data={items}
keyExtractor={item => getKey(item)}
renderItem={({ item }) => {
const {right, ...props} = render(item);
return (
<Row
{...props}
right={(
<>
{right}
{!!remove && (
<Cell onPress={() => remove(item)}>
<Icon
name="trash"
color="destructive"
/>
</Cell>
)}
</>
)}
/>
);
}}
/>
</>
);
}
export { List };

View File

@@ -0,0 +1,24 @@
import { ReactNode } from 'react';
import Wrapper from './react-modal';
import { Popup } from '../popup';
type ModalProps = {
visible: boolean;
onClose: () => void;
children: ReactNode;
}
const Modal: React.FC<ModalProps> = ({ visible, onClose, children }) => (
<Wrapper
transparent
visible={visible}
animationType="slide"
onRequestClose={onClose}
onDismiss={onClose}
>
<Popup onClose={onClose}>
{children}
</Popup>
</Wrapper>
);
export { Modal };

View File

@@ -0,0 +1,3 @@
import { Modal } from 'react-native';
export default Modal;

View File

@@ -0,0 +1,43 @@
import ReactDOM from 'react-dom';
import React, { useMemo, useEffect, ReactNode } from 'react';
interface Props {
visible: boolean;
children: ReactNode;
}
const Modal: React.FC<Props> = ({ visible, children }) => {
const elm = useMemo(() => {
const newElm = document.createElement('div');
newElm.style.position = 'fixed';
newElm.style.display = 'flex';
newElm.style.flexDirection = 'column';
newElm.style.left = '0px';
newElm.style.top = '0px';
newElm.style.width = '100%';
newElm.style.height = '100%';
newElm.style.transition = 'transform 0.3s';
newElm.style.transform = 'translateY(100%)';
return newElm;
}, []);
useEffect(() => {
document.body.appendChild(elm);
return () => {
document.body.removeChild(elm);
};
}, [elm]);
useEffect(() => {
if (visible) {
elm.style.transform = 'translateY(0)';
} else {
elm.style.transform = 'translateY(100%)';
}
}, [elm, visible]);
return ReactDOM.createPortal(
<>{children}</>,
elm,
);
};
export default Modal;

View File

@@ -0,0 +1,39 @@
import React, { useState, useEffect } from 'react';
import styled from 'styled-components/native';
import { Keyboard, Platform } from 'react-native';
const KeyboardAvoiding = styled.KeyboardAvoidingView`
flex: 1;
`;
const Pressable = styled.Pressable`
flex: 1;
`
// background-color: ${({ theme }) => theme.colors.background};
const Page: React.FC = ({ children }) => {
const [keyboardShown, setKeyboardShown] = useState(false);
useEffect(() => {
const keyboardDidShow = () => setKeyboardShown(true);
const keyboardDidHide = () => setKeyboardShown(false);
Keyboard.addListener('keyboardDidShow', keyboardDidShow);
Keyboard.addListener('keyboardDidHide', keyboardDidHide);
return () => {
Keyboard.removeListener('keyboardDidShow', keyboardDidShow);
Keyboard.removeListener('keyboardDidHide', keyboardDidHide);
};
}, []);
return (
<Pressable
disabled={!keyboardShown}
onPress={() => Keyboard.dismiss()}
>
<KeyboardAvoiding behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
{children}
</KeyboardAvoiding>
</Pressable>
);
};
export { Page };

View File

@@ -0,0 +1,69 @@
import React, { ReactNode, useCallback, useRef } from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import styled from 'styled-components/native';
import { Icon } from '../icon';
import { Row, Cell, RowProps } from '../row';
import { Page } from '../page';
import { ScrollView } from 'react-native';
type Props = RowProps & {
onClose?: () => void;
children: ReactNode;
}
const Top = styled.Pressable`
flex: 1;
`;
const Wrapper = styled.View`
background: ${({ theme }) => theme.colors.background};
width: 100%;
max-width: 500px;
shadow-color: ${({ theme }) => theme.colors.shadow};
shadow-offset: 0 0;
shadow-opacity: 1;
shadow-radius: 200px;
border-radius: 12px;
margin-bottom: -12px;
max-height: 80%;
`;
const Outer = styled.View`
flex: 1;
align-items: center;
`;
const Content = styled.ScrollView`
`;
const Popup: React.FC<Props> = ({ children, onClose, right, ...rowProps }) => {
const insets = useSafeAreaInsets();
return (
<Page>
<Outer>
<Top onPress={onClose} />
<Wrapper style={{ paddingBottom: insets.bottom + 12 }}>
<Row
{...rowProps}
right={
<>
{right}
<Cell onPress={onClose}>
<Icon name="x-circle" />
</Cell>
</>
}
/>
<Content
alwaysBounceVertical={false}
>
{children}
</Content>
</Wrapper>
</Outer>
</Page>
);
};
export { Popup };

View File

@@ -0,0 +1,58 @@
import React, { ReactNode } from 'react';
import { StyleProp, TouchableOpacity, ViewStyle } from 'react-native';
import styled from 'styled-components/native';
import { Theme } from '../../../theme';
type CellProps = {
style?: StyleProp<ViewStyle>;
accessibilityRole?: TouchableOpacity['props']['accessibilityRole'];
accessibilityLabel?: string;
accessibilityHint?: string;
children?: ReactNode;
onPress?: () => any;
background?: string;
}
const Wrapper = styled.View<{
background?: string;
theme: Theme;
}>`
padding: ${({ theme }) => theme.margins.medium / 2}px;
align-items: center;
justify-content: center;
${({ background }) => (background ? `background: ${background};` : '')}
`;
const Touch = styled.TouchableOpacity`
`;
const Cell: React.FC<CellProps> = ({ children, onPress, ...props}) => {
const {
accessibilityLabel,
accessibilityRole,
accessibilityHint,
...others
} = props;
const node = (
<Wrapper {...others}>
{children}
</Wrapper>
);
if (onPress) {
return (
<Touch
accessible
accessibilityRole={accessibilityRole || 'button'}
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}
onPress={onPress}
>
{node}
</Touch>
);
}
return node;
};
export type { CellProps };
export { Cell };

View File

@@ -0,0 +1,26 @@
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs';
import { Row, Cell } from '.';
import { Body1 } from '../../../typography'
<Meta title="Components/Row" component={Row} />
# Row
<Canvas>
<Story name="Primary">
<Row
left={<Cell><Body1>Left</Body1></Cell>}
right={<Cell><Body1>Right</Body1></Cell>}
overline="Overline"
title="Title"
description="Description"
>
<Body1>Children</Body1>
</Row>
</Story>
</Canvas>
## Component arguments
<ArgsTable of={Row} />

View File

@@ -0,0 +1,3 @@
export * from './cell';
export * from './row';
export * from './placeholder-icon';

View File

@@ -0,0 +1,28 @@
import React from 'react';
import styled from 'styled-components/native';
import { Cell } from './cell';
interface Props {
color?: string;
size?: number;
onPress?: () => void;
}
const Icon = styled.View<{ size: number; color: string }>`
background: ${({ color }) => color};
width: ${({ size }) => size}px;
height: ${({ size }) => size}px;
border-radius: ${({ size }) => size / 4}px;
`;
const PlaceholderIcon: React.FC<Props> = ({
color = 'red',
size = 24,
onPress,
}) => (
<Cell onPress={onPress}>
<Icon color={color} size={size} />
</Cell>
);
export { PlaceholderIcon };

View File

@@ -0,0 +1,67 @@
import React, { ReactNode } from 'react';
import styled from 'styled-components/native';
import { Title2, Body1, Overline } from '../../../typography';
import { Cell, CellProps } from './cell';
type RowProps = CellProps & {
top?: ReactNode;
left?: ReactNode;
right?: ReactNode;
title?: ReactNode;
overline?: ReactNode;
description?: ReactNode;
children?: ReactNode;
}
const Children = styled.View`
width: 100%;
`;
const RowCell = styled(Cell)`
flex-direction: row;
width: 100%;
`;
const ContentCell = styled(Cell)`
align-items: flex-start;
flex: 1;
`;
const componentOrString = (
input: ReactNode,
Component: React.FC<{ children: ReactNode }>
) => {
if (!input) {
return null;
}
if (typeof input === 'string') {
return <Component>{input}</Component>;
}
return input;
};
const Row = ({
top,
left,
right,
title,
overline,
description,
children,
...cellProps
}: RowProps) => (
<RowCell {...cellProps}>
{left}
<ContentCell>
{!!top}
{componentOrString(overline, Overline)}
{componentOrString(title, Title2)}
{componentOrString(description, Body1)}
{!!children && <Children>{children}</Children>}
</ContentCell>
{right}
</RowCell>
);
export type { RowProps };
export { Row };

View File

@@ -0,0 +1,142 @@
import { add } from "date-fns";
import { useMemo, useState } from "react";
import styled from "styled-components/native";
import { Body1 } from '../../../typography';
import { Cell, Row, Icon } from "../../base";
type Props = {
startDate?: Date;
selected?: Date;
onSelect?: (date: Date) => void;
};
const Wrapper = styled.View`
`;
const Week = styled.View`
flex-direction: row;
`;
const Day = styled.View`
flex: 1;
align-items: center;
justify-content: center;
height: 40px;
`;
const days = [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
];
const Calendar = ({
startDate = new Date(),
selected,
onSelect,
}: Props) => {
const [current, setCurrent] = useState(
startDate,
);
const weeks = useMemo(
() => {
const weeks: Date[][] = [];
const startDay = new Date(
current.getFullYear(),
current.getMonth(),
1,
);
const endDay = new Date(
current.getFullYear(),
current.getMonth() + 1,
0,
);
let currentDate = startDay;
let week: Date[] = [];
while (currentDate <= endDay) {
week.push(currentDate);
if (currentDate.getDay() === 6) {
weeks.push(week);
week = [];
}
currentDate = new Date(currentDate.getTime() + 86400000);
}
if (week.length > 0) {
weeks.push(week);
}
return weeks;
},
[current],
);
const weekdays = useMemo(
() => new Array(7).fill(0).map((_, i) => days[i]),
[current],
);
const offsetStart = useMemo(
() => new Array(new Date(current.getFullYear(), current.getMonth(), 1).getDay()).fill(0),
[current],
);
const offsetEnd = useMemo(
() => new Array(6 - new Date(current.getFullYear(), current.getMonth() + 1, 0).getDay()).fill(0),
[current],
);
return (
<Wrapper>
<Row
left={(
<Cell onPress={() => setCurrent(add(current, { months: - 1 }))}>
<Icon name="chevron-left" />
</Cell>
)}
right={(
<Cell onPress={() => setCurrent(add(current, { months: 1 }))}>
<Icon name="chevron-right" />
</Cell>
)}
title={current.toLocaleString('default', { month: 'long' })}
/>
<Week>
{weekdays.map((day, i) => (
<Day key={i}>
<Body1>
{day}
</Body1>
</Day>
))}
</Week>
{weeks.map((week, index) => (
<Week key={index}>
{index === 0 && offsetStart.map((_, index) => (
<Day key={index} />
))}
{week.map((day, index) => (
<Day key={index}>
<Body1>
{day.getDate()}
</Body1>
</Day>
))}
{index === weeks.length - 1 && offsetEnd.map((_, index) => (
<Day key={index} />
))}
</Week>
))}
</Wrapper>
)
};
export { Calendar };

View File

@@ -0,0 +1,71 @@
import { Body1, Caption, Overline } from "../../../typography";
import { Row, Cell, Icon, RowProps } from '../../base';
import styled from "styled-components/native"
import stringToColor from 'string-to-color';
import { useMemo } from "react";
import chroma from 'chroma-js';
import { format } from "date-fns";
type Props = RowProps & {
start: Date;
end: Date;
height?: number;
title: string;
location?: string;
checked?: boolean;
onChangeChecked?: (checked: boolean) => void;
}
const Wrapper = styled(Row)<{
height?: number;
color: string;
}>`
background-color: ${({ color }) => color};
border-color: ${({ color }) => chroma(color).darken(0.1).hex()};
border-width: 2px;
border-radius: ${({ theme }) => theme.sizes.corners}px;
${({ height }) => height && `height: ${height}px`};
`;
const CalendarEntry = ({
start,
end,
height,
title,
location,
checked,
onChangeChecked,
...row
}: Props) => {
const color = useMemo(() => {
const base = stringToColor(title);
return chroma(base).darken(0.9).desaturate(2.5).luminance(0.1).hex();
}, [title]);
const time = useMemo(() => {
const startTime = format(start, 'HH:mm');
const endTime = format(end, 'HH:mm');
return `${startTime} - ${endTime}`;
}, [start, end]);
return (
<Wrapper
{...row}
left={typeof checked !== 'undefined' && (
<>
<Cell onPress={onChangeChecked ? () => onChangeChecked(!checked) : undefined}>
<Icon color="background" name={checked ? 'check' : 'square'} />
</Cell>
{row.left}
</>
)}
height={height}
color={color}
>
<Overline color="background">{location}</Overline>
<Body1 color="background">{title}</Body1>
<Caption color="background">{time}</Caption>
</Wrapper>
);
};
export { CalendarEntry };

View File

@@ -0,0 +1,3 @@
export * from './calendar';
export * from './entry';
export * from './strip';

View File

@@ -0,0 +1,106 @@
import { add } from 'date-fns';
import { useMemo, useState } from 'react';
import styled, { useTheme } from 'styled-components/native';
import { Overline, Title2 } from '../../../typography';
import { Icon, Cell } from '../../base';
type Props = {
start?: Date;
selected?: Date;
onSelect?: (date: Date) => void;
}
type DayProps ={
selected?: boolean;
date: Date;
onPress?: (date: Date) => void;
};
const Wrapper = styled.View`
flex-direction: row;
justify-content: center;
align-items: center;
`
const TitleWrapper = styled.View`
margin-top: ${({ theme }) => theme.margins.medium}px;
justify-content: center;
align-items: center;
`;
const DateWrapper = styled.TouchableOpacity<{
selected?: boolean;
}>`
padding: ${({ theme }) => theme.margins.small}px 0;
border-radius: ${({ theme }) => theme.sizes.corners}px;
flex: 1;
background-color: ${({ selected, theme }) => selected ? theme.colors.primary : 'transparent'};
align-items: center;
justify-content: center;
max-width: 60px;
`;
const Day = ({ date, selected, onPress }: DayProps) => {
const textColor = selected ? 'background' : 'text';
return (
<DateWrapper selected={selected} onPress={onPress ? () => onPress(date) : undefined}>
<Title2 color={textColor}>{date.getDate()}</Title2>
<Overline color={textColor}>{date.toLocaleString('en-us', { weekday: 'short' })}</Overline>
</DateWrapper>
);
}
const CalendarStrip: React.FC<Props> = ({ start, selected, onSelect }) => {
const [current, setCurrent] = useState(start || selected || new Date());
const firstDayOfWeek = useMemo(() => {
const currentDay = current.getDay();
const firstDay = add(current, { days: -currentDay + 1 });
return firstDay;
}, [current]);
const days = useMemo(() => {
const days = new Array(7).fill(null).map((_, i) => ({
date: add(firstDayOfWeek, { days: i }),
}));
return days;
}, [firstDayOfWeek]);
const months = useMemo<[number, number]>(() => {
const startMonth = firstDayOfWeek.getMonth();
const endMonth = add(firstDayOfWeek, { days: 7 }).getMonth();
return [startMonth, endMonth];
}, [firstDayOfWeek]);
const monthLabel = useMemo(() => {
console.log(months);
if (months[0] === months[1]) {
return new Date(0, months[0], 1).toLocaleString('en-us', { month: 'long' });
} else {
return `${new Date(0, months[0], 1).toLocaleString('en-us', { month: 'long' })} - ${new Date(0, months[1], 1).toLocaleString('en-us', { month: 'long' })}`;
}
}, [months]);
return (
<>
<TitleWrapper>
<Title2>{monthLabel}</Title2>
</TitleWrapper>
<Wrapper>
<Cell onPress={() => setCurrent(add(current, { weeks: -1 }))}>
<Icon name="chevron-left" />
</Cell>
{days.map(({ date }) => (
<Day
date={date}
selected={date.getTime() === selected?.getTime()}
onPress={onSelect}
key={date.toString()}
/>
))}
<Cell onPress={() => setCurrent(add(current, { weeks: 1 }))}>
<Icon name="chevron-right" />
</Cell>
</Wrapper>
</>
);
};
export { CalendarStrip };

View File

@@ -0,0 +1,53 @@
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs';
import { Checkbox } from '.';
import { Button } from '../../base';
<Meta title="Components/Forms/Checkbox" component={Checkbox} />
# Checkbox
Has following variants: Primary, secondary, outlined, text only, destructive, disabled and comes in 4 sizes.
<Canvas>
<Story name="Checked">
<Checkbox label="Foo" value={true} />
</Story>
</Canvas>
<Canvas>
<Story name="Unchecked">
<Checkbox label="Foo" value={false} />
</Story>
</Canvas>
<Canvas>
<Story name="With right">
<Checkbox
label="Foo"
value={false}
right={
<Cell><Button title="Test" /></Cell>
}
/>
</Story>
</Canvas>
<Canvas>
<Story name="With left">
<Checkbox
label="Foo"
value={false}
left={
<Cell><Button title="Test" /></Cell>
}
/>
</Story>
</Canvas>
## Component arguments
<ArgsTable of={Checkbox} />

View File

@@ -0,0 +1,35 @@
import { Row, RowProps, Cell, Icon } from "../../base"
type CheckboxProps = RowProps & {
value?: boolean;
label: string;
onChangeValue: (value: boolean) => void;
}
const Checkbox = ({
value,
label,
onChangeValue,
...rowProps
}: CheckboxProps) => {
return (
<Row
{...rowProps}
description={label}
onPress={() => onChangeValue(!value)}
left={(
<>
{rowProps.left}
<Cell>
<Icon
name={value ? 'check-square' : 'square'}
color={value ? 'primary' : 'input'}
/>
</Cell>
</>
)}
/>
);
};
export { Checkbox };

View File

@@ -0,0 +1,18 @@
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs';
import { DateSelector } from '.';
<Meta title="Components/Forms/DateSelector" component={DateSelector} />
<Canvas>
<Story name="Normal">
<DateSelector
label="Foo"
/>
</Story>
</Canvas>
## Component arguments
<ArgsTable of={DateSelector} />

View File

@@ -0,0 +1,84 @@
import React, { useState } from 'react';
import { Calendar } from '../../date';
import styled, { useTheme } from 'styled-components/native';
import { Row, Button, Modal, RowProps } from '../../base';
type Day = {
year: number;
month: number;
date: number;
}
const Wrapper = styled(Row)`
padding: 0;
border-color: ${({ theme }) => theme.colors.input};
border-width: 0.5px;
border-radius: ${({ theme }) => theme.sizes.corners}px;
`;
const toId = (day: Day) => [
day.year.toString().padStart(4, '0'),
day.month.toString().padStart(2, '0'),
day.date.toString().padStart(2, '0'),
].join('-');
type Props = RowProps & {
label: string;
selected?: Day;
} & ({
allowClear: true,
onSelect: (input?: Day) => void;
} | {
allowClear?: false,
onSelect: (input: Day) => void;
})
const DateSelector: React.FC<Props> = ({ label, selected, onSelect, allowClear, ...rowProps }) => {
const theme = useTheme();
const [visible, setVisible] = useState(false);
const marked: any = {};
if (selected) {
marked[toId(selected)] = {
selected: true,
marked: true,
selectedColor: theme.colors.primary,
};
}
return (
<Row>
<Wrapper
{...rowProps}
overline={label}
onPress={() => setVisible(true)}
title={selected ? toId(selected) : 'Not set'}
>
<Modal visible={visible} onClose={() => setVisible(false)}>
{visible && (<Calendar
hideArrows={false}
renderArrow={direction => <div />}
enableSwipeMonths={true}
onDayPress={({ year, month, day }) => {
onSelect({ year, month, date: day });
setVisible(false);
}}
current={selected ? toId(selected) : undefined}
/>)}
{allowClear && (
<Row>
<Button
title="Clear"
onPress={() => {
onSelect(undefined as any);
setVisible(false);
}}
/>
</Row>
)}
</Modal>
</Wrapper>
</Row>
);
};
export { DateSelector };

View File

@@ -0,0 +1,6 @@
export * from './text-input';
export * from './checkbox';
export * from './selector';
export * from './time';
export * from './date-selector';
export * from './optional-selector';

View File

@@ -0,0 +1,41 @@
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs';
import { OptionalSelector } from '.';
<Meta title="Components/Forms/OptionalSelector" component={OptionalSelector} />
# Optional Selector
Has following variants: Primary, secondary, outlined, text only, destructive, disabled and comes in 4 sizes.
<Canvas>
<Story name="Disabled">
<OptionalSelector
label="Foo"
items={[]}
enabledText="Enabled"
disabledText="Disabled"
render={a => { title: a }}
/>
</Story>
</Canvas>
<Canvas>
<Story name="Enabled">
<OptionalSelector
enabled
selected={['bar']}
label="Foo"
items={['foo', 'bar']}
render={a => ({ title: a })}
getKey={a => a}
enabledText="Enabled"
disabledText="Disabled"
/>
</Story>
</Canvas>
## Component arguments
<ArgsTable of={OptionalSelector} />

View File

@@ -0,0 +1,116 @@
import { Body1 } from "../../../typography";
import { useCallback } from "react";
import styled from "styled-components/native";
import { Row, RowProps, Cell, Icon } from "../../base";
type Props<T> = {
label: string;
setEnabled: (enabled: boolean) => void;
enabled: boolean;
onChange: (items: T[]) => void;
items: T[];
enabledText: string;
disabledText: string;
selected?: T[];
render: (item: T) => RowProps;
getKey: (item: T) => string;
};
const Wrapper = styled.View`
border-radius: 5px;
background: ${({ theme }) => theme.colors.shade};
border-radius: 7px;
shadow-offset: 0 0;
shadow-opacity: 0.1;
shadow-color: ${({ theme }) => theme.colors.shadow};
shadow-radius: 5px;
`;
const Top = styled.View`
flex-direction: row;
`;
const Touch = styled.TouchableOpacity`
flex: 1;
`;
const Content = styled.View`
`;
const TopButton = styled.View<{ selected: boolean }>`
background: ${({ selected, theme }) => selected ? theme.colors.shade : theme.colors.background};
padding: ${({ theme }) => theme.margins.small}px;
align-items: center;
justify-content: center;
`
function OptionalSelector<T>({
label,
enabled,
setEnabled,
onChange,
items,
enabledText,
disabledText,
selected,
render,
getKey,
}: Props<T>) {
const toggle = useCallback(
(item: T) => {
if (!selected) {
return onChange([item]);
}
const nextId = getKey(item);
const current = selected.find(i => getKey(i) === nextId);
if (current) {
onChange(selected.filter(i => i !== current));
} else {
onChange([...selected, item]);
}
},
[selected, getKey]
)
return (
<Row overline={label}>
<Wrapper>
<Top>
<Touch onPress={() => setEnabled(false)}>
<TopButton selected={!enabled}>
<Body1>{disabledText}</Body1>
</TopButton>
</Touch>
<Touch onPress={() => setEnabled(true)}>
<TopButton selected={enabled}>
<Body1>{enabledText}</Body1>
</TopButton>
</Touch>
</Top>
{enabled && (
<Content>
{items.map((item) => {
const { left, ...props } = render(item);
const isSelected = !!selected && selected.includes(item);
return (
<Row
key={getKey(item)}
{...props}
left={(
<>
<Cell onPress={() => toggle(item)}>
<Icon name={isSelected ? 'check-circle' : 'circle'} />
</Cell>
{left}
</>
)}
/>
);
})}
</Content>
)}
</Wrapper>
</Row>
)
}
export { OptionalSelector }

View File

@@ -0,0 +1,26 @@
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs';
import { Selector } from '.';
import { Button } from '../../base';
<Meta title="Components/Forms/Selector" component={Selector} />
<Canvas>
<Story name="With left">
<Selector
label="Foo"
getId={a => a}
render={a => ({ title: a })}
onChangeSelected={() => {}}
items={[
'Foo',
'Bar',
]}
/>
</Story>
</Canvas>
## Component arguments
<ArgsTable of={Selector} />

View File

@@ -0,0 +1,65 @@
import { useCallback, useMemo, useState } from "react";
import styled from "styled-components/native";
import { Modal } from "../../base/modal";
import { Row, RowProps } from "../../base/row";
type SelectorProps<T> = {
label: string;
items: T[];
render: (item: T) => RowProps;
getId: (item: T) => string;
selected?: T;
onChangeSelected: (item?: T) => void;
}
const Wrapper = styled(Row)`
padding: ${({ theme }) => theme.margins.small}px;
border-color: ${({ theme }) => theme.colors.input};
border-width: 2px;
border-radius: ${({ theme }) => theme.sizes.corners}px;
`;
function Selector<T = any>({
items,
render,
label,
getId,
selected,
onChangeSelected,
}: SelectorProps<T>) {
const [visible, setVisible] = useState(false);
const selectedItem = useMemo(
() => selected ? items.find(i => getId(i) === getId(selected)) : undefined,
[selected, items],
);
const select = useCallback(
(item: T) => {
onChangeSelected(item);
setVisible(false);
},
[onChangeSelected, setVisible],
);
return (
<>
<Modal visible={visible} onClose={() => setVisible(false)}>
{items.map((item) => (
<Row
key={getId(item)}
{...render(item)}
onPress={() => select(item)}
/>
))}
</Modal>
<Row>
<Wrapper
overline={label}
title={'Select'}
{...(selectedItem ? render(selectedItem) : {})}
onPress={() => setVisible(true)}
/>
</Row>
</>
)
}
export { Selector };

View File

@@ -0,0 +1,53 @@
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs';
import { TextInput } from '.';
import { Button } from '../../base';
<Meta title="Components/Forms/TextInput" component={TextInput} />
# Checkbox
Has following variants: Primary, secondary, outlined, text only, destructive, disabled and comes in 4 sizes.
<Canvas>
<Story name="Empty">
<TextInput label="Foo" />
</Story>
</Canvas>
<Canvas>
<Story name="With value">
<TextInput label="Foo" value={false} />
</Story>
</Canvas>
<Canvas>
<Story name="With right">
<TextInput
label="Foo"
value={false}
right={
<Cell><Button title="Test" /></Cell>
}
/>
</Story>
</Canvas>
<Canvas>
<Story name="With left">
<TextInput
label="Foo"
value={false}
left={
<Cell><Button title="Test" /></Cell>
}
/>
</Story>
</Canvas>
## Component arguments
<ArgsTable of={TextInput} />

View File

@@ -0,0 +1,62 @@
import React from 'react';
import { TextInputProps as ReactTextInputProps } from 'react-native';
import styled from 'styled-components/native';
import { Overline } from '../../../typography';
import { Cell, IconNames, Row, RowProps, Icon } from '../../base';
type TextInputProps = RowProps & {
label: string;
icon?: IconNames;
placeholder?: string;
value: string;
onChangeText: (text: string) => any;
onBlur?: () => any;
type?: ReactTextInputProps['textContentType'];
}
const InputField = styled.TextInput`
margin: ${({ theme }) => theme.margins.small}px 0;
color: ${({ theme }) => theme.colors.text};
font-size: ${({ theme }) => theme.font.baseSize}px;
width: 100%;
`;
const Wrapper = styled(Row)`
padding: 0;
border-color: ${({ theme }) => theme.colors.input};
border-width: 0.5px;
border-radius: ${({ theme }) => theme.sizes.corners}px;
`;
const TextInput = ({
label,
icon,
type,
onBlur,
placeholder, value, onChangeText, children, ...row }: TextInputProps) => {
return (
<Row {...row}>
<Wrapper
left={icon && (
<Cell>
<Icon name={icon} />
</Cell>
)}
>
<Overline>{label}</Overline>
<InputField
placeholder={placeholder}
value={value}
onBlur={onBlur}
textContentType={type}
secureTextEntry={type === 'password'}
onChangeText={onChangeText}
/>
{children}
</Wrapper>
</Row>
);
};
export type { TextInputProps };
export { TextInput };

View File

@@ -0,0 +1,57 @@
import React, { useCallback, useEffect, useState } from 'react';
import { TextInput, TextInputProps } from '../text-input';
type Time = {
hour: number;
minute: number;
};
type Props = TextInputProps & {
value?: Time;
onChange: (time?: Time) => any;
}
const timeToString = (time: Time) => {
const hour = time.hour < 10 ? `0${time.hour}` : time.hour;
const minute = time.minute < 10 ? `0${time.minute}` : time.minute;
return `${hour}:${minute}`;
};
const stringToTime = (value: string) => {
const [hour = '0', minute = '0'] = value.split(':');
return {
hour: parseInt(hour, 10),
minute: parseInt(minute, 10),
};
};
const TimeInput: React.FC<Props> = ({
value,
onChange,
children,
...rest
}) => {
const [current, setCurrent] = useState(value ? timeToString(value) : '');
useEffect(() => {
setCurrent(value ? timeToString(value) : '');
}, [value]);
const onBlur = useCallback(
() => {
onChange(stringToTime(current));
},
[current, onChange],
);
return (
<TextInput
{...rest}
value={current}
onChangeText={setCurrent}
onBlur={onBlur}
/>
);
};
export { TimeInput };

View File

@@ -0,0 +1,4 @@
export * from './base';
export * from './date';
export * from './form';
export * from './layouts';

View File

@@ -0,0 +1,29 @@
import styled from "styled-components/native";
import { Page, Row } from '../base';
type Props = {
title?: string;
children: React.ReactNode;
};
const Wrapper = styled.View`
max-width: 900px;
width: 100%;
margin: 0 auto;
border-radius: ${({ theme }) => theme.sizes.corners}px;
padding: ${({ theme }) => theme.margins.medium}px;
background: ${({ theme }) => theme.colors.background};
`;
const FormLayout = ({ children, title }: Props) => {
return (
<Page>
<Wrapper>
{ title && <Row title={title} />}
{children}
</Wrapper>
</Page>
);
};
export { FormLayout };

View File

@@ -0,0 +1 @@
export * from './form';

View File

@@ -0,0 +1,50 @@
import React from 'react';
import { ComponentMeta } from '@storybook/react';
import { CalendarEntry, CalendarStrip, Row } from '..';
import { FormLayout } from '../components';
export const Agenda = () => {
const [selectedDate, setSelectedDate] = React.useState<Date>();
return (
<FormLayout>
<CalendarStrip
selected={selectedDate}
onSelect={setSelectedDate}
/>
<Row>
<CalendarEntry
start={new Date(2020, 0, 1, 9, 0, 0)}
end={new Date(2020, 0, 1, 11, 0, 0)}
checked={true}
title="Ride mountain bike"
location="Mountain bike park"
/>
</Row>
<Row>
<CalendarEntry
start={new Date(2020, 0, 1, 12, 0, 0)}
end={new Date(2020, 0, 1, 12, 30, 0)}
checked={false}
title="Pick up kids"
location="The playground"
/>
</Row>
<Row>
<CalendarEntry
start={new Date(2020, 0, 1, 19, 0, 0)}
end={new Date(2020, 0, 1, 19, 30, 0)}
title="Read a book"
location="Home"
/>
</Row>
</FormLayout>
)
}
export default {
title: 'Examples/Calendar',
component: Agenda,
} as ComponentMeta<typeof Agenda>;

View File

@@ -0,0 +1,128 @@
import React from 'react';
import { ComponentMeta } from '@storybook/react';
import { FormLayout, Group, TextInput, Button, Checkbox, Row, Horizontal, Selector } from '..';
const countries = new Array(100).fill(0).map((_, i) => ({
id: i,
name: `Country ${i}`,
}));
export const Login = () => {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const [remember, setRemember] = React.useState(false);
return (
<FormLayout title="Login">
<TextInput
icon="user"
label="Username"
placeholder="Enter your username"
value={username}
onChangeText={setUsername}
/>
<TextInput
label="Password"
icon="lock"
placeholder="Enter your password"
value={password}
onChangeText={setPassword}
/>
<Checkbox
label="Remember me"
value={remember}
onChangeValue={setRemember}
/>
<Row>
<Button title="Login" />
</Row>
<Row>
<Button title="Forgot password" type="secondary" />
</Row>
</FormLayout>
)
}
export const Signup = () => {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const [passwordRepeat, setPasswordRepeat] = React.useState('');
const [country, setCountry] = React.useState<typeof countries[0]>();
return (
<FormLayout title="Signup">
<TextInput
icon="user"
label="Username"
type="username"
placeholder="Enter your username"
value={username}
onChangeText={setUsername}
/>
<TextInput
label="Password"
icon="lock"
type="password"
placeholder="Enter your password"
value={password}
onChangeText={setPassword}
/>
<TextInput
label="Repeat password"
icon="lock"
type="password"
placeholder="Repeat your password"
value={passwordRepeat}
onChangeText={setPasswordRepeat}
/>
<Group title="Address">
<TextInput
label="Street"
placeholder="Nowhere st. 1"
type="streetAddressLine1"
value={username}
onChangeText={setUsername}
/>
<Horizontal>
<TextInput
label="Zip code"
placeholder="12345"
type="postalCode"
flex={1}
value={username}
onChangeText={setUsername}
/>
<TextInput
label="City"
type="addressCity"
flex={1}
placeholder="Nowhere"
value={username}
onChangeText={setUsername}
/>
</Horizontal>
<Selector
label="Country"
items={countries}
getId={item => item.id.toString()}
render={item => ({ title: item.name })}
selected={country}
onChangeSelected={setCountry}
/>
</Group>
<Checkbox
label="Sign up for the newsletter"
/>
<Row>
<Button title="Login" />
</Row>
</FormLayout>
)
}
export default {
title: 'Examples/Forms',
component: Login,
} as ComponentMeta<typeof Login>;

View File

@@ -0,0 +1,70 @@
import React from 'react';
import { ComponentMeta } from '@storybook/react';
import { useTheme } from 'styled-components/native';
import styled from 'styled-components';
const Table = styled.table`
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, .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 ComponentMeta<typeof SpacingComponent>;
export const Colors = () => <SpacingComponent />;

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { ComponentMeta } from '@storybook/react';
import { useTheme } from 'styled-components/native';
import { Icon } from '../components/base/icon';
import styled from 'styled-components';
import { icons } from 'feather-icons';
const Table = styled.div`
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, .05);
}
`;
const Name = styled.div`
text-align: center;
font-weight: bold;
`;
export const Icons = () => {
const theme = useTheme();
return (
<Table>
{Object.entries(icons).map(([key, value]) => {
return (
<Row>
<Icon name={key} size={220} />
<Name>
{key}
</Name>
</Row>
)
})}
</Table>
)
}
export default {
title: 'Foundation/Icons',
component: Icons,
} as ComponentMeta<typeof Icons>;

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { ComponentMeta } from '@storybook/react';
import { useTheme } from 'styled-components/native';
import styled from 'styled-components';
const Table = styled.table`
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, .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 ComponentMeta<typeof SpacingComponent>;
export const Spacing = () => <SpacingComponent />;

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { ComponentMeta } from '@storybook/react';
import { useTheme } from 'styled-components/native';
import { types } from '../typography';
import styled from 'styled-components';
const Table = styled.table`
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, .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];
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 ComponentMeta<typeof TypographyComponent>;
export const Typography = () => <TypographyComponent />;

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

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

View File

@@ -0,0 +1,4 @@
export * from './provider';
export * from './theme';
export * from './light';

View File

@@ -0,0 +1,57 @@
import { Platform } from 'react-native';
import { 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',
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' ? 12 : 10,
},
};
export { light };

View File

@@ -0,0 +1,16 @@
import { ThemeProvider } from 'styled-components/native';
import { light } from './light';
type Props = {
children: React.ReactNode;
};
const Provider: React.FC<Props> = ({ children }) => {
return (
<ThemeProvider theme={light}>
{children}
</ThemeProvider>
);
};
export { Provider };

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

@@ -0,0 +1,5 @@
import {} from 'styled-components';
import { Theme } from './theme'; // Import type from above file
declare module 'styled-components' {
export interface DefaultTheme extends Theme {} // extends the global DefaultTheme with our ThemeType.
}

View File

@@ -0,0 +1,46 @@
type Typography = {
family?: string;
size?: number;
spacing?: number;
weight?: string;
upperCase?: boolean;
};
type Theme = {
typography: {
Jumbo: Typography;
Title2: Typography;
Title1: Typography;
Body1: Typography;
Caption: Typography;
Overline: Typography;
Link: Typography;
}
colors: {
primary: 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 { Theme, Typography };

View File

@@ -0,0 +1,51 @@
import styled from 'styled-components/native';
import { Theme, Typography } from '../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 };

View File

@@ -0,0 +1,7 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"outDir": "./dist"
}
}

View File

@@ -0,0 +1,28 @@
/* eslint-disable no-undef */
import { defineConfig } from 'vite';
import { peerDependencies, dependencies } from './package.json';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
build: {
lib: {
entry: path.resolve(__dirname, 'src', 'index.ts'),
formats: ['es', 'cjs'],
fileName: (ext) => `index.${ext}.js`,
// for UMD name: 'GlobalName'
},
outDir: './public',
rollupOptions: {
external: [...Object.keys(peerDependencies), ...Object.keys(dependencies)],
},
target: 'esnext',
sourcemap: true,
emptyOutDir: false,
},
});