Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211// src/layout/file-tree/PromptDialog.tsximport { useState, useEffect, useRef, useCallback, type KeyboardEvent } from 'react'import { createPortal } from 'react-dom'import './PromptDialog.css'
export interface PromptDialogProps { isOpen: boolean title: string /** Label shown above the input */ label?: string /** Initial value for the input */ defaultValue?: string /** Placeholder text */ placeholder?: string /** Submit button text */ submitLabel?: string onSubmit: (value: string) => void onClose: () => void}
function addEscapeKeyListener(isOpen: boolean, onClose: () => void) { if (!isOpen) return undefined function handleKeyDown(e: globalThis.KeyboardEvent) { if (e.key === 'Escape') { e.stopPropagation() onClose() } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown)}
export function PromptDialog({ isOpen, title, label, defaultValue = '', placeholder, submitLabel = 'OK', onSubmit, onClose,}: PromptDialogProps) { const [value, setValue] = useState(defaultValue) const inputRef = useRef<HTMLInputElement>(null)
// Reset value and focus when opening useEffect(() => { if (isOpen) { setValue(defaultValue) const timer = setTimeout(() => { inputRef.current?.focus() inputRef.current?.select() }, 0) return () => clearTimeout(timer) } }, [isOpen, defaultValue])
// Escape to close useEffect(() => { return addEscapeKeyListener(isOpen, onClose) }, [isOpen, onClose])
const handleSubmit = useCallback(() => { const trimmed = value.trim() if (trimmed) { onSubmit(trimmed) } }, [value, onSubmit])
const handleKeyDown = useCallback((e: KeyboardEvent) => { if (e.key === 'Enter' && value.trim()) { handleSubmit() } }, [value, handleSubmit])
const handleBackdropClick = useCallback((e: React.MouseEvent) => { if (e.target === e.currentTarget) onClose() }, [onClose])
if (!isOpen) return null
return createPortal( <div className="prompt-dialog__backdrop" onClick={handleBackdropClick}> <div role="dialog" aria-modal="true" aria-labelledby="prompt-dialog-title" className="prompt-dialog" onClick={e => e.stopPropagation()} > <h2 id="prompt-dialog-title" className="prompt-dialog__title">{title}</h2> {label && <label className="prompt-dialog__label">{label}</label>} <input ref={inputRef} type="text" value={value} onChange={e => setValue(e.target.value)} onKeyDown={handleKeyDown} placeholder={placeholder} className="prompt-dialog__input" /> <div className="prompt-dialog__buttons"> <button type="button" className="prompt-dialog__button prompt-dialog__button--cancel" onClick={onClose} > Cancel </button> <button type="button" className="prompt-dialog__button prompt-dialog__button--submit" disabled={!value.trim()} onClick={handleSubmit} > {submitLabel} </button> </div> </div> </div>, document.body )}
export interface ConfirmDialogProps { isOpen: boolean title: string /** Descriptive message */ message: string /** Confirm button text */ confirmLabel?: string /** Whether this is a destructive action (styles button red) */ destructive?: boolean onConfirm: () => void onClose: () => void}
export function ConfirmDialog({ isOpen, title, message, confirmLabel = 'Confirm', destructive = false, onConfirm, onClose,}: ConfirmDialogProps) { const confirmRef = useRef<HTMLButtonElement>(null)
const cancelRef = useRef<HTMLButtonElement>(null)
// For destructive actions, focus Cancel (safe default). Otherwise focus Confirm. useEffect(() => { if (isOpen) { const timer = setTimeout(() => { if (destructive) { cancelRef.current?.focus() } else { confirmRef.current?.focus() } }, 0) return () => clearTimeout(timer) } }, [isOpen, destructive])
// Escape to close useEffect(() => { return addEscapeKeyListener(isOpen, onClose) }, [isOpen, onClose])
const handleBackdropClick = useCallback((e: React.MouseEvent) => { if (e.target === e.currentTarget) onClose() }, [onClose])
if (!isOpen) return null
return createPortal( <div className="prompt-dialog__backdrop" onClick={handleBackdropClick}> <div role="alertdialog" aria-modal="true" aria-labelledby="confirm-dialog-title" aria-describedby="confirm-dialog-message" className="prompt-dialog" onClick={e => e.stopPropagation()} > <h2 id="confirm-dialog-title" className="prompt-dialog__title">{title}</h2> <p id="confirm-dialog-message" className="prompt-dialog__message">{message}</p> <div className="prompt-dialog__buttons"> <button ref={cancelRef} type="button" className="prompt-dialog__button prompt-dialog__button--cancel" onClick={onClose} > Cancel </button> <button ref={confirmRef} type="button" className={`prompt-dialog__button ${destructive ? 'prompt-dialog__button--destructive' : 'prompt-dialog__button--submit'}`} onClick={onConfirm} > {confirmLabel} </button> </div> </div> </div>, document.body )}