0 ? ' on' : '')} />
}
// useEscape runs fn when Escape is pressed while active — every menu and dialog closes that way.
export function useEscape(fn: () => void, active = true) {
const ref = useRef(fn); ref.current = fn
useEffect(() => {
if (!active) return
const h = (e: KeyboardEvent) => { if (e.key === 'Escape') ref.current() }
window.addEventListener('keydown', h)
return () => window.removeEventListener('keydown', h)
}, [active])
}
// ---- dialogs: askYes / askText replace the browser's confirm() and prompt() ----
// Same promise shape as the natives (false / null on cancel), drawn like the rest of the
// console, usable on a phone, closed by Escape or a tap outside.
interface Ask { id: number; msg: string; input?: string; ok: string; danger: boolean; done: (v: string | boolean | null) => void }
let asks: Ask[] = []
let nextId = 0
const askListeners = new Set<(a: Ask[]) => void>()
const emitAsks = () => askListeners.forEach((l) => l(asks))
function ask(msg: string, input: string | undefined, ok?: string): Promise
{
return new Promise((resolve) => {
const id = ++nextId
const danger = /^(delete|remove|revoke|close|send sig|stop|disable|reset|discard|force)/i.test(msg) || /\bforce\b/i.test(msg)
asks = [...asks, { id, msg, input, ok: ok ?? (input !== undefined ? 'OK' : danger ? msg.split(/[ ?]/)[0] : 'OK'), danger, done: (v) => { asks = asks.filter((a) => a.id !== id); emitAsks(); resolve(v) } }]
emitAsks()
})
}
export const askYes = (msg: string, ok?: string) => ask(msg, undefined, ok) as Promise
export const askText = (msg: string, initial = '', ok?: string) => ask(msg, initial, ok) as Promise
function AskBox({ a }: { a: Ask }) {
const [val, setVal] = useState(a.input ?? '')
const cancel = () => a.done(a.input !== undefined ? null : false)
const accept = () => a.done(a.input !== undefined ? val : true)
useEscape(cancel)
return (
)
}
export function Dialogs() {
const [list, setList] = useState(asks)
useEffect(() => { askListeners.add(setList); return () => { askListeners.delete(setList) } }, [])
const top = list[list.length - 1]
return top ? : null
}
// Unsaved work: whoever holds some says so, and whoever is about to navigate away asks.
let unsaved = ''
export function setUnsaved(what: string) { unsaved = what }
export const hasUnsaved = () => unsaved !== ''
export async function okToLeave(): Promise {
if (!unsaved || (await askYes(`Discard unsaved changes to ${unsaved}?`, 'Discard'))) { unsaved = ''; return true }
return false
}