Jaimodiji's picture
Deploying multiplayer tldraw app
2ec5eb1 verified
import { useState, useEffect } from 'react'
import { createRoot } from 'react-dom/client'
import { colors } from '../constants/theme'
interface ThemeColors {
bg: string
border: string
text: string
hover: string
selected: string
textMuted: string
}
interface ModalProps {
title?: string
message: string
type: 'alert' | 'prompt' | 'confirm'
defaultValue?: string
onConfirm: (value?: string) => void
onCancel: () => void
theme: ThemeColors
}
function Modal({ title, message, type, defaultValue = '', onConfirm, onCancel, theme }: ModalProps) {
const [inputValue, setInputValue] = useState(defaultValue)
// Focus input on mount
useEffect(() => {
if (type === 'prompt') {
const input = document.getElementById('custom-modal-input')
if (input) input.focus()
}
}, [type])
const handleConfirm = () => {
onConfirm(type === 'prompt' ? inputValue : undefined)
}
return (
<div style={{
position: 'fixed',
inset: 0,
zIndex: 9999,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: colors.overlay,
backdropFilter: 'blur(2px)',
fontFamily: 'Inter, sans-serif',
}}>
<div style={{
background: theme.bg,
border: `1px solid ${theme.border}`,
borderRadius: 12,
padding: 20,
width: '90%',
maxWidth: 360,
boxShadow: '0 8px 30px rgba(0,0,0,0.12)',
color: theme.text,
}}>
{title && (
<div style={{
fontSize: 16,
fontWeight: 600,
marginBottom: 8,
color: theme.text,
}}>
{title}
</div>
)}
<div style={{
fontSize: 14,
lineHeight: 1.5,
marginBottom: 20,
color: theme.text,
opacity: 0.8,
}}>
{message}
</div>
{type === 'prompt' && (
<input
id="custom-modal-input"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleConfirm()
if (e.key === 'Escape') onCancel()
}}
style={{
width: '100%',
padding: '10px 12px',
background: theme.hover,
border: `1px solid ${theme.border}`,
borderRadius: 6,
color: theme.text,
fontSize: 14,
marginBottom: 20,
outline: 'none',
}}
/>
)}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
{(type === 'prompt' || type === 'confirm') && (
<button
onClick={onCancel}
style={{
padding: '8px 16px',
background: 'transparent',
border: 'none',
color: theme.textMuted,
fontSize: 13,
fontWeight: 500,
cursor: 'pointer',
borderRadius: 6,
}}
>
Cancel
</button>
)}
<button
onClick={handleConfirm}
style={{
padding: '8px 16px',
background: theme.selected,
color: '#fff',
border: 'none',
borderRadius: 6,
fontSize: 13,
fontWeight: 500,
cursor: 'pointer',
}}
>
OK
</button>
</div>
</div>
</div>
)
}
// Utility to mount the modal dynamically
export function showModal(props: Omit<ModalProps, 'onConfirm' | 'onCancel' | 'theme'>): Promise<string | undefined | boolean> {
return new Promise((resolve) => {
// Detect Theme
let isDark = false
try {
const prefs = localStorage.getItem('tldraw_global_prefs')
if (prefs) {
const parsed = JSON.parse(prefs)
if (parsed.colorScheme === 'dark') isDark = true
}
} catch (e) { }
const theme: ThemeColors = {
bg: isDark ? colors.panelBgDark : colors.panelBg,
border: isDark ? colors.borderDark : colors.border,
text: isDark ? colors.textDark : colors.text,
hover: isDark ? colors.hoverDark : colors.hover,
selected: colors.selected,
textMuted: colors.textMuted
}
const div = document.createElement('div')
document.body.appendChild(div)
const root = createRoot(div)
const cleanup = () => {
root.unmount()
document.body.removeChild(div)
}
const onConfirm = (value?: string) => {
cleanup()
resolve(props.type === 'prompt' ? value : true)
}
const onCancel = () => {
cleanup()
resolve(props.type === 'prompt' ? undefined : false)
}
root.render(
<Modal
{...props}
theme={theme}
onConfirm={onConfirm}
onCancel={onCancel}
/>
)
})
}