toes/src/client/components/modal.tsx

110 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Child } from 'hono/jsx'
import { render } from 'hono/jsx/dom'
import { define } from '@because/forge'
import { theme } from '../themes'
let modalTitle: string | null = null
let modalContent: (() => Child) | null = null
const root = document.getElementById('modal')!
const renderModal = () => {
render(<Modal />, root)
}
export const openModal = (title: string, content: () => Child) => {
modalTitle = title
modalContent = content
renderModal()
requestAnimationFrame(() => {
document.querySelector<HTMLInputElement>('[data-modal-body] input')?.focus()
})
}
export const closeModal = () => {
modalTitle = null
modalContent = null
renderModal()
}
export { renderModal }
// ESC key handler
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modalContent) {
closeModal()
}
})
const ModalBackdrop = define('ModalBackdrop', {
position: 'fixed',
inset: 0,
background: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
paddingTop: '20vh',
zIndex: 1000,
})
const ModalBox = define('ModalBox', {
background: theme('colors-bg'),
borderRadius: theme('radius-md'),
border: `1px solid ${theme('colors-border')}`,
boxShadow: '0 4px 24px rgba(0, 0, 0, 0.2)',
maxWidth: 500,
width: '90%',
maxHeight: '80vh',
overflow: 'auto',
})
const ModalHeader = define('ModalHeader', {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '16px 20px',
borderBottom: `1px solid ${theme('colors-border')}`,
})
const ModalTitle = define('ModalTitle', {
fontSize: 16,
fontWeight: 600,
margin: 0,
})
const ModalCloseButton = define('ModalCloseButton', {
base: 'button',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 4,
fontSize: 18,
color: theme('colors-textMuted'),
lineHeight: 1,
selectors: {
'&:hover': { color: theme('colors-text') },
},
})
const ModalBody = define('ModalBody', {
padding: 20,
})
export const Modal = () => {
if (!modalContent) return null
return (
<ModalBackdrop onClick={closeModal}>
<ModalBox onClick={(e: MouseEvent) => e.stopPropagation()}>
<ModalHeader>
<ModalTitle>{modalTitle}</ModalTitle>
<ModalCloseButton onClick={closeModal}>×</ModalCloseButton>
</ModalHeader>
<ModalBody data-modal-body>
{modalContent()}
</ModalBody>
</ModalBox>
</ModalBackdrop>
)
}