toes/src/client/components/modal.tsx
2026-01-30 08:23:29 -08:00

109 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 { define } from '@because/forge'
import { theme } from '../themes'
let modalTitle: string | null = null
let modalContent: (() => Child) | null = null
let renderFn: (() => void) | null = null
export const initModal = (render: () => void) => {
renderFn = render
}
export const openModal = (title: string, content: () => Child) => {
modalTitle = title
modalContent = content
renderFn?.()
requestAnimationFrame(() => {
document.querySelector<HTMLInputElement>('[data-modal-body] input')?.focus()
})
}
export const closeModal = () => {
modalTitle = null
modalContent = null
renderFn?.()
}
export const rerenderModal = () => {
renderFn?.()
}
// 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: 'center',
justifyContent: 'center',
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>
)
}