feat: initial

This commit is contained in:
unix
2020-03-19 01:15:58 +08:00
parent a37c630d54
commit 1a258b23d0
181 changed files with 17112 additions and 0 deletions

12
components/modal/index.ts Normal file
View File

@@ -0,0 +1,12 @@
import Modal from './modal'
import ModalTitle from './modal-title'
import ModalSubtitle from './modal-subtitle'
import ModalContent from './modal-content'
import ModalAction from './modal-action'
Modal.Title = ModalTitle
Modal.Subtitle = ModalSubtitle
Modal.Content = ModalContent
Modal.Action = ModalAction
export default Modal

View File

@@ -0,0 +1,78 @@
import React, { MouseEvent, useMemo } from 'react'
import withDefaults from '../utils/with-defaults'
import useTheme from '../styles/use-theme'
import { useModalContext } from './modal-context'
type ModalActionEvent = MouseEvent<HTMLButtonElement> & {
close: () => void
}
interface Props {
className?: string
passive?: boolean
disabled?: boolean
onClick?: (event: ModalActionEvent) => void
}
const defaultProps = {
className: '',
passive: false,
disabled: false,
onClick: (event: ModalActionEvent) => event.close && event.close(),
}
export type ModalActionProps = Props & typeof defaultProps & React.ButtonHTMLAttributes<any>
const ModalAction: React.FC<ModalActionProps> = React.memo(({
className, children, onClick, passive, disabled, ...props
}) => {
const theme = useTheme()
const { close } = useModalContext()
const clickHandler = (event: MouseEvent<HTMLButtonElement>) => {
if (disabled) return
const actionEvent = Object.assign({}, event, {
close: close,
})
onClick(actionEvent)
}
const color = useMemo(() => {
return passive || disabled ? theme.palette.accents_5 : theme.palette.foreground
}, [theme.palette, passive, disabled])
const bgColor = useMemo(() => {
return disabled ? theme.palette.accents_1 : theme.palette.background
}, [theme.palette, disabled])
return (
<>
<button className={className} onClick={clickHandler} {...props}>{children}</button>
<style jsx>{`
button {
font-size: .75rem;
text-transform: uppercase;
display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
outline: none;
text-decoration: none;
transition: all 200ms ease-in-out 0s;
border: none;
color: ${color};
background-color: ${bgColor};
cursor: ${disabled ? 'not-allowed' : 'pointer'};
flex: 1;
}
button:hover {
color: ${disabled ? color : theme.palette.foreground};
background-color: ${disabled ? bgColor : theme.palette.accents_1};
}
`}</style>
</>
)
})
export default withDefaults(ModalAction, defaultProps)

View File

@@ -0,0 +1,42 @@
import React from 'react'
import useTheme from '../styles/use-theme'
const ModalActions: React.FC<React.PropsWithChildren<{}>> = React.memo(({
children, ...props
}) => {
const theme = useTheme()
return (
<>
<div />
<footer {...props}>
{children}
</footer>
<style jsx>{`
footer {
display: flex;
overflow: hidden;
width: 100%;
height: 3.625rem;
position: absolute;
bottom: 0;
left: 0;
right: 0;
border-top: 1px solid ${theme.palette.border};
border-bottom-left-radius: ${theme.layout.radius};
border-bottom-right-radius: ${theme.layout.radius};
}
footer > :global(button+button) {
border-left: 1px solid ${theme.palette.border};
}
div {
height: 3.625rem;
}
`}</style>
</>
)
})
export default ModalActions

View File

@@ -0,0 +1,37 @@
import React from 'react'
import withDefaults from '../utils/with-defaults'
import useTheme from '../styles/use-theme'
interface Props {
className?: string
}
const defaultProps = {
className: ''
}
export type ModalContentProps = Props & typeof defaultProps & React.HTMLAttributes<HTMLElement>
const ModalContent: React.FC<ModalContentProps> = React.memo(({
className, children, ...props
}) => {
const theme = useTheme()
return (
<>
<div className={`content ${className}`} {...props}>{children}</div>
<style jsx>{`
.content {
margin: 0;
padding: ${theme.layout.gap} 0 ${theme.layout.gapHalf} 0;
}
.content :global(p) {
margin: 0;
}
`}</style>
</>
)
})
export default withDefaults(ModalContent, defaultProps)

View File

@@ -0,0 +1,15 @@
import React from 'react'
export interface ModalConfig {
close: () => void
open: () => void
}
const defaultContext = {
close: () => {},
open: () => {},
}
export const ModalContext = React.createContext<ModalConfig>(defaultContext)
export const useModalContext = (): ModalConfig => React.useContext<ModalConfig>(ModalContext)

View File

@@ -0,0 +1,42 @@
import React from 'react'
import withDefaults from '../utils/with-defaults'
import useTheme from '../styles/use-theme'
interface Props {
className?: string
}
const defaultProps = {
className: ''
}
export type ModalSubtitleProps = Props & typeof defaultProps & React.HTMLAttributes<HTMLHeadingElement>
const ModalSubtitle: React.FC<ModalSubtitleProps> = React.memo(({
className, children, ...props
}) => {
const theme = useTheme()
return (
<>
<p className={className} {...props}>{children}</p>
<style jsx>{`
p {
font-size: .875rem;
line-height: 1.6;
font-weight: normal;
text-align: center;
margin: 0;
display: inline-flex;
justify-content: center;
align-items: center;
word-break: break-word;
text-transform: uppercase;
color: ${theme.palette.accents_5};
}
`}</style>
</>
)
})
export default withDefaults(ModalSubtitle, defaultProps)

View File

@@ -0,0 +1,42 @@
import React from 'react'
import withDefaults from '../utils/with-defaults'
import useTheme from '../styles/use-theme'
interface Props {
className?: string
}
const defaultProps = {
className: ''
}
export type ModalTitleProps = Props & typeof defaultProps & React.HTMLAttributes<any>
const ModalTitle: React.FC<ModalTitleProps> = React.memo(({
className, children, ...props
}) => {
const theme = useTheme()
return (
<>
<h2 className={className} {...props}>{children}</h2>
<style jsx>{`
h2 {
font-size: 1.5rem;
line-height: 1.6;
font-weight: normal;
text-align: center;
margin: 0;
display: inline-flex;
justify-content: center;
align-items: center;
word-break: break-word;
text-transform: uppercase;
color: ${theme.palette.foreground};
}
`}</style>
</>
)
})
export default withDefaults(ModalTitle, defaultProps)

View File

@@ -0,0 +1,72 @@
import React from 'react'
import withDefaults from '../utils/with-defaults'
import useTheme from '../styles/use-theme'
import CSSTransition from '../shared/css-transition'
interface Props {
className?: string
width?: string
visible?: boolean
}
const defaultProps = {
className: '',
width: '26rem',
visible: false,
}
export type ModalWrapperProps = Props & typeof defaultProps
const ModalWrapper: React.FC<React.PropsWithChildren<ModalWrapperProps>> = React.memo(({
className, width, children, visible, ...props
}) => {
const theme = useTheme()
return (
<CSSTransition name="wrapper" visible={visible} clearTime={300}>
<div className={`wrapper ${className}`} {...props}>
{children}
<style jsx>{`
.wrapper {
max-width: 90%;
width: ${width};
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
box-sizing: border-box;
background-color: ${theme.palette.background};
color: ${theme.palette.foreground};
border-radius: ${theme.layout.radius};
padding: ${theme.layout.gap};
opacity: 0;
transform: translate3d(0px, -40px, 0px);
transition: opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s, transform 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s;
}
.wrapper-enter {
opacity: 0;
transform: translate3d(0px, -40px, 0px);
}
.wrapper-enter-active {
opacity: 1;
transform: translate3d(0px, 0px, 0px);
}
.wrapper-leave {
opacity: 1;
transform: translate3d(0px, 0px, 0px);
}
.wrapper-leave-active {
opacity: 0;
transform: translate3d(0px, -50px, 0px);
}
`}</style>
</div>
</CSSTransition>
)
})
export default withDefaults(ModalWrapper, defaultProps)

View File

@@ -0,0 +1,87 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import usePortal from '../utils/use-portal'
import ModalTitle from './modal-title'
import ModalSubtitle from './modal-subtitle'
import ModalWrapper from './modal-wrapper'
import ModalContent from './modal-content'
import ModalAction from './modal-action'
import ModalActions from './modal-actions'
import Backdrop from '../shared/backdrop'
import { ModalConfig, ModalContext } from './modal-context'
import { pickChild } from '../utils/collections'
interface Props {
disableBackdropClick?: boolean
onClose: () => void
onOpen: () => void
open?: boolean
}
const defaultProps = {
disableBackdropClick: false,
onClose: () => {},
onOpen: () => {},
open: false,
}
export type ModalProps = Props & typeof defaultProps & React.HTMLAttributes<any>
const Modal: React.FC<React.PropsWithChildren<ModalProps>> = React.memo(({
children, disableBackdropClick, onClose, onOpen, open
}) => {
const portal = usePortal('modal')
const [visible, setVisible] = useState<boolean>(open)
const [withoutActionsChildren, ActionsChildren] = pickChild(children, ModalAction)
const hasActions = ActionsChildren && React.Children.count(ActionsChildren) > 0
const closeModal = useCallback(() => {
setVisible(false)
onClose && onClose()
}, [open])
const openModal = useCallback(() => {
setVisible(true)
onOpen && onOpen()
}, [])
useEffect(() => {
setVisible(open)
}, [open])
const closeFromBackdrop = useCallback(() => {
if (disableBackdropClick && hasActions) return
closeModal()
}, [disableBackdropClick])
const modalConfig: ModalConfig = useMemo(() => ({
close: closeModal,
open: openModal,
}), [])
if (!portal) return null
return createPortal(
(
<ModalContext.Provider value={modalConfig}>
<Backdrop onClick={closeFromBackdrop} visible={visible} offsetY={25}>
<ModalWrapper visible={visible}>
{withoutActionsChildren}
{hasActions && <ModalActions>{ActionsChildren}</ModalActions>}
</ModalWrapper>
</Backdrop>
</ModalContext.Provider>
), portal
)
})
Modal.defaultProps = defaultProps
type ModalComponent<P = {}> = React.FC<P> & {
Title: typeof ModalTitle
Subtitle: typeof ModalSubtitle
Content: typeof ModalContent
Action: typeof ModalAction
}
type ComponentProps = Partial<typeof defaultProps> & Omit<Props, keyof typeof defaultProps>
export default Modal as ModalComponent<ComponentProps>

View File

@@ -0,0 +1,26 @@
import { Dispatch, MutableRefObject, SetStateAction } from 'react'
import useCurrentState from 'components/utils/use-current-state'
const useModal = (initialVisible: boolean = false): {
visible: boolean
setVisible: Dispatch<SetStateAction<boolean>>
currentRef: MutableRefObject<boolean>
bindings: {
open: boolean
onClose: () => void
}
} => {
const [visible, setVisible, currentRef] = useCurrentState<boolean>(initialVisible)
return {
visible,
setVisible,
currentRef,
bindings: {
open: visible,
onClose: () => setVisible(false),
},
}
}
export default useModal