mirror of
https://github.com/morten-olsen/bob-the-algorithm.git
synced 2026-02-08 00:46:25 +01:00
monorepo
This commit is contained in:
39
packages/ui/src/components/base/button/index.stories.mdx
Normal file
39
packages/ui/src/components/base/button/index.stories.mdx
Normal 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} />
|
||||
|
||||
82
packages/ui/src/components/base/button/index.tsx
Normal file
82
packages/ui/src/components/base/button/index.tsx
Normal 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 };
|
||||
29
packages/ui/src/components/base/group/header.tsx
Normal file
29
packages/ui/src/components/base/group/header.tsx
Normal 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 };
|
||||
75
packages/ui/src/components/base/group/index.tsx
Normal file
75
packages/ui/src/components/base/group/index.tsx
Normal 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 };
|
||||
18
packages/ui/src/components/base/horizontal/index.tsx
Normal file
18
packages/ui/src/components/base/horizontal/index.tsx
Normal 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 };
|
||||
29
packages/ui/src/components/base/icon/index.native.tsx
Normal file
29
packages/ui/src/components/base/icon/index.native.tsx
Normal 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 };
|
||||
31
packages/ui/src/components/base/icon/index.tsx
Normal file
31
packages/ui/src/components/base/icon/index.tsx
Normal 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 };
|
||||
9
packages/ui/src/components/base/index.ts
Normal file
9
packages/ui/src/components/base/index.ts
Normal 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';
|
||||
53
packages/ui/src/components/base/list/index.tsx
Normal file
53
packages/ui/src/components/base/list/index.tsx
Normal 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 };
|
||||
24
packages/ui/src/components/base/modal/index.tsx
Normal file
24
packages/ui/src/components/base/modal/index.tsx
Normal 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 };
|
||||
3
packages/ui/src/components/base/modal/react-modal.tsx
Normal file
3
packages/ui/src/components/base/modal/react-modal.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Modal } from 'react-native';
|
||||
|
||||
export default Modal;
|
||||
43
packages/ui/src/components/base/modal/react-modal.web.tsx
Normal file
43
packages/ui/src/components/base/modal/react-modal.web.tsx
Normal 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;
|
||||
39
packages/ui/src/components/base/page/index.tsx
Normal file
39
packages/ui/src/components/base/page/index.tsx
Normal 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 };
|
||||
69
packages/ui/src/components/base/popup/index.tsx
Normal file
69
packages/ui/src/components/base/popup/index.tsx
Normal 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 };
|
||||
58
packages/ui/src/components/base/row/cell.tsx
Normal file
58
packages/ui/src/components/base/row/cell.tsx
Normal 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 };
|
||||
26
packages/ui/src/components/base/row/index.stories.mdx
Normal file
26
packages/ui/src/components/base/row/index.stories.mdx
Normal 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} />
|
||||
|
||||
3
packages/ui/src/components/base/row/index.ts
Normal file
3
packages/ui/src/components/base/row/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './cell';
|
||||
export * from './row';
|
||||
export * from './placeholder-icon';
|
||||
28
packages/ui/src/components/base/row/placeholder-icon.tsx
Normal file
28
packages/ui/src/components/base/row/placeholder-icon.tsx
Normal 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 };
|
||||
67
packages/ui/src/components/base/row/row.tsx
Normal file
67
packages/ui/src/components/base/row/row.tsx
Normal 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 };
|
||||
142
packages/ui/src/components/date/calendar/index.tsx
Normal file
142
packages/ui/src/components/date/calendar/index.tsx
Normal 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 };
|
||||
71
packages/ui/src/components/date/entry/index.tsx
Normal file
71
packages/ui/src/components/date/entry/index.tsx
Normal 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 };
|
||||
3
packages/ui/src/components/date/index.ts
Normal file
3
packages/ui/src/components/date/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './calendar';
|
||||
export * from './entry';
|
||||
export * from './strip';
|
||||
106
packages/ui/src/components/date/strip/index.tsx
Normal file
106
packages/ui/src/components/date/strip/index.tsx
Normal 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 };
|
||||
53
packages/ui/src/components/form/checkbox/index.stories.mdx
Normal file
53
packages/ui/src/components/form/checkbox/index.stories.mdx
Normal 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} />
|
||||
|
||||
35
packages/ui/src/components/form/checkbox/index.tsx
Normal file
35
packages/ui/src/components/form/checkbox/index.tsx
Normal 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 };
|
||||
@@ -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} />
|
||||
|
||||
84
packages/ui/src/components/form/date-selector/index.tsx
Normal file
84
packages/ui/src/components/form/date-selector/index.tsx
Normal 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 };
|
||||
6
packages/ui/src/components/form/index.ts
Normal file
6
packages/ui/src/components/form/index.ts
Normal 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';
|
||||
@@ -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} />
|
||||
|
||||
116
packages/ui/src/components/form/optional-selector/index.tsx
Normal file
116
packages/ui/src/components/form/optional-selector/index.tsx
Normal 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 }
|
||||
26
packages/ui/src/components/form/selector/index.stories.mdx
Normal file
26
packages/ui/src/components/form/selector/index.stories.mdx
Normal 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} />
|
||||
|
||||
65
packages/ui/src/components/form/selector/index.tsx
Normal file
65
packages/ui/src/components/form/selector/index.tsx
Normal 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 };
|
||||
53
packages/ui/src/components/form/text-input/index.stories.mdx
Normal file
53
packages/ui/src/components/form/text-input/index.stories.mdx
Normal 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} />
|
||||
|
||||
62
packages/ui/src/components/form/text-input/index.tsx
Normal file
62
packages/ui/src/components/form/text-input/index.tsx
Normal 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 };
|
||||
57
packages/ui/src/components/form/time/index.tsx
Normal file
57
packages/ui/src/components/form/time/index.tsx
Normal 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 };
|
||||
4
packages/ui/src/components/index.ts
Normal file
4
packages/ui/src/components/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './base';
|
||||
export * from './date';
|
||||
export * from './form';
|
||||
export * from './layouts';
|
||||
29
packages/ui/src/components/layouts/form.tsx
Normal file
29
packages/ui/src/components/layouts/form.tsx
Normal 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 };
|
||||
1
packages/ui/src/components/layouts/index.ts
Normal file
1
packages/ui/src/components/layouts/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './form';
|
||||
50
packages/ui/src/examples/calendar.stories.tsx
Normal file
50
packages/ui/src/examples/calendar.stories.tsx
Normal 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>;
|
||||
|
||||
|
||||
128
packages/ui/src/examples/form.stories.tsx
Normal file
128
packages/ui/src/examples/form.stories.tsx
Normal 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>;
|
||||
|
||||
|
||||
70
packages/ui/src/foundation/colors.stories.tsx
Normal file
70
packages/ui/src/foundation/colors.stories.tsx
Normal 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 />;
|
||||
|
||||
52
packages/ui/src/foundation/icons.stories.tsx
Normal file
52
packages/ui/src/foundation/icons.stories.tsx
Normal 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>;
|
||||
|
||||
69
packages/ui/src/foundation/spacings.stories.tsx
Normal file
69
packages/ui/src/foundation/spacings.stories.tsx
Normal 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 />;
|
||||
|
||||
69
packages/ui/src/foundation/typography.stories.tsx
Normal file
69
packages/ui/src/foundation/typography.stories.tsx
Normal 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
3
packages/ui/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './components';
|
||||
export * from './theme';
|
||||
export * from './typography';
|
||||
4
packages/ui/src/theme/index.ts
Normal file
4
packages/ui/src/theme/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './provider';
|
||||
export * from './theme';
|
||||
export * from './light';
|
||||
|
||||
57
packages/ui/src/theme/light.ts
Normal file
57
packages/ui/src/theme/light.ts
Normal 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 };
|
||||
16
packages/ui/src/theme/provider.tsx
Normal file
16
packages/ui/src/theme/provider.tsx
Normal 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
5
packages/ui/src/theme/styled.d.ts
vendored
Normal 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.
|
||||
}
|
||||
46
packages/ui/src/theme/theme.ts
Normal file
46
packages/ui/src/theme/theme.ts
Normal 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 };
|
||||
51
packages/ui/src/typography/index.tsx
Normal file
51
packages/ui/src/typography/index.tsx
Normal 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 };
|
||||
Reference in New Issue
Block a user