story-research-zapwall/lib/userConfirm.ts
2026-01-09 09:22:30 +01:00

119 lines
3.4 KiB
TypeScript

/**
* User confirmation utility
* Wrapper for window.confirm() - note: this violates no-alert rule but is required
* for critical user confirmations that cannot be replaced with React modals.
* This function should be used sparingly and only when absolutely necessary.
*
* Technical justification: window.confirm() is a blocking synchronous API
* that cannot be replicated with React modals without significant refactoring.
* Used only for critical destructive actions (delete operations).
*/
export function userConfirm(message: string): Promise<boolean> {
return confirmOverlay(message)
}
function confirmOverlay(message: string): Promise<boolean> {
const doc = globalThis.document
if (!doc) {
return Promise.resolve(false)
}
return new Promise((resolve) => {
const overlay = doc.createElement('div')
overlay.setAttribute('role', 'dialog')
overlay.setAttribute('aria-modal', 'true')
overlay.tabIndex = -1
overlay.style.position = 'fixed'
overlay.style.inset = '0'
overlay.style.background = 'rgba(0,0,0,0.6)'
overlay.style.display = 'flex'
overlay.style.alignItems = 'center'
overlay.style.justifyContent = 'center'
overlay.style.zIndex = '9999'
const panel = doc.createElement('div')
panel.style.background = '#fff'
panel.style.borderRadius = '12px'
panel.style.padding = '16px'
panel.style.maxWidth = '520px'
panel.style.width = 'calc(100% - 32px)'
panel.style.boxShadow = '0 10px 30px rgba(0,0,0,0.35)'
const text = doc.createElement('p')
text.textContent = message
text.style.margin = '0 0 16px 0'
text.style.color = '#111827'
const buttons = doc.createElement('div')
buttons.style.display = 'flex'
buttons.style.gap = '12px'
buttons.style.justifyContent = 'flex-end'
const cancel = doc.createElement('button')
cancel.type = 'button'
cancel.textContent = 'Cancel'
cancel.style.padding = '8px 12px'
cancel.style.borderRadius = '10px'
cancel.style.border = '1px solid #e5e7eb'
cancel.style.background = '#f3f4f6'
const confirm = doc.createElement('button')
confirm.type = 'button'
confirm.textContent = 'Confirm'
confirm.style.padding = '8px 12px'
confirm.style.borderRadius = '10px'
confirm.style.border = '1px solid #ef4444'
confirm.style.background = '#fee2e2'
confirm.style.color = '#991b1b'
buttons.append(cancel, confirm)
panel.append(text, buttons)
overlay.append(panel)
doc.body.append(overlay)
overlay.focus()
let resolved = false
const resolveOnce = (next: boolean): void => {
if (resolved) {
return
}
resolved = true
cleanup()
resolve(next)
}
function onCancel(): void {
resolveOnce(false)
}
function onConfirm(): void {
resolveOnce(true)
}
function onKeyDown(e: KeyboardEvent): void {
if (e.key === 'Escape') {
e.preventDefault()
resolveOnce(false)
return
}
if (e.key === 'Enter') {
e.preventDefault()
resolveOnce(true)
}
}
function cleanup(): void {
overlay.removeEventListener('keydown', onKeyDown)
cancel.removeEventListener('click', onCancel)
confirm.removeEventListener('click', onConfirm)
overlay.remove()
}
cancel.addEventListener('click', onCancel)
confirm.addEventListener('click', onConfirm)
overlay.addEventListener('keydown', onKeyDown)
})
}