You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
One-way actions: replace window.confirm / window.prompt with styled dialogs #135
The last surfaces still using a browser popup instead of the app's own. #131 (PR #132) gave every add/edit form an accessible <Dialog>; these eight kept window.confirm / window.prompt.
The inconsistency runs the wrong way: "add a grade" gets a designed sheet, "void a confirmed order" gets an OS text box.
The call sites
All eight are the one-way and corrective actions from #59 / #60 / #69 — the ones that most deserve a considered UI.
Yes/no — window.confirm (5)
Where
Action
Consequence
DailyEntryPage.tsx:218
Submit day
Freezes the entry, creates egg lots; corrections need a manager adjustment
SalesPage.tsx:242
Confirm order
Allocates stock from inventory (FIFO)
SalesPage.tsx:260
Cancel draft
Order becomes read-only, can't be confirmed
FlocksPage.tsx:298
Deplete flock
Stops accepting new entries
FlocksPage.tsx:307
Archive flock
Disappears from pickers and the dashboard
Reason required — window.prompt (3)
These write an audit reason, so the text itself is the point.
window.prompt can be blocked outright — browsers let users suppress it, and it is unavailable in some contexts. An action that silently no-ops is worse than an ugly one.
Validation lands too late — the three prompts check "reason is required" after the popup closes, so a blank reason means retyping from scratch. In a dialog the check is inline and the text survives.
Blocks the JS thread — nothing can render or animate while a native popup is up.
Untestable in the good way — each site currently needs a window.confirm / window.prompt stub in its Vitest suite. A React dialog is asserted like any other UI.
No room to explain — the copy is already doing real work (FIFO allocation, what "backfill still works" means). A dialog body can hold it; a one-line popup string can't.
Design
One hook over the existing Dialog — no new primitive, no new a11y work.
const{ confirm, askReason, confirmDialog }=useConfirm();if(!(awaitconfirm({ title, body, confirmLabel, destructive })))return;constreason=awaitaskReason({ title, body, confirmLabel, destructive });if(reason===null)return;// never resolves to an empty string
one element rendered per screen ({confirmDialog}) even where both shapes are used — Sales needs all four
destructive styles the action red; it varies genuinely (5 destructive, 3 one-way-but-constructive), so it is not dead config
initial focus follows DOM order and lands where it should: Cancel for a yes/no (the safe default — a stray Enter must not deplete a flock), the textarea for a reason (the user has to type). No focus special-casing needed.
Escape / backdrop / Cancel all resolve as "no", inherited from Dialog
Danger button tokens
A single shared token cannot clear both AA on white text and 3:1 against the surface in both themes, so --danger / --danger-press are themed, matching the lighter-on-press convention --aubergine already sets.
The real cost
window.confirm and window.prompt are synchronous, so today the call sites read:
A React dialog can't return inline, so each handler becomes async and awaits. Mechanical, but it touches control flow — worth doing one screen at a time and keeping each screen's existing guards (inFlight, busy, idempotency-key scoping) intact rather than restructuring them.
Two specifics:
DailyEntryPage checks a synchronous re-entry guard (inFlight.current) before the confirm. Awaiting opens a window between the check and the set, so the guard must be re-checked after the await.
Sales already opens two dialogs from the same panel; a confirm must not stack on top of the payment dialog.
Checklist
useConfirm hook + Dialog-based confirm/reason UI
Vitest: confirm resolves true/false, Cancel/Escape/backdrop reject, reason trims and rejects blank without closing, focus lands on Cancel (yes/no) and the textarea (reason), a second ask settles the first
--danger / --danger-press tokens, both themes, contrast-checked
Migrate DailyEntryPage submit (plus the re-entry guard re-check)
changed the title [-]One-way actions: replace window.confirm with a styled confirmation dialog[/-][+]One-way actions: replace window.confirm / window.prompt with styled dialogs[/+]on Jul 22, 2026
Scope corrected before starting: the original body listed 5 window.confirm sites. A census for window.prompt as well turns up 3 more, and they are the worse offenders — HistoryPage.tsx:201 (void entry), SalesPage.tsx:305 (void payment), SalesPage.tsx:330 (void order). All three collect a required audit reason, validate it only after the popup has closed, and can be suppressed outright by the browser.
Folding them in rather than filing a sibling: removing window.confirm while leaving window.prompt behind would leave main with a mixed idiom, and the reason dialogs reuse the same hook. Body and title updated; 8 sites, one hook with two shapes.
The last surfaces still using a browser popup instead of the app's own. #131 (PR #132) gave every add/edit form an accessible
<Dialog>; these eight keptwindow.confirm/window.prompt.The inconsistency runs the wrong way: "add a grade" gets a designed sheet, "void a confirmed order" gets an OS text box.
The call sites
All eight are the one-way and corrective actions from #59 / #60 / #69 — the ones that most deserve a considered UI.
Yes/no —
window.confirm(5)DailyEntryPage.tsx:218SalesPage.tsx:242SalesPage.tsx:260FlocksPage.tsx:298FlocksPage.tsx:307Reason required —
window.prompt(3)These write an audit reason, so the text itself is the point.
HistoryPage.tsx:201SalesPage.tsx:305SalesPage.tsx:330Why replace them
window.promptcan be blocked outright — browsers let users suppress it, and it is unavailable in some contexts. An action that silently no-ops is worse than an ugly one.window.confirm/window.promptstub in its Vitest suite. A React dialog is asserted like any other UI.Design
One hook over the existing
Dialog— no new primitive, no new a11y work.{confirmDialog}) even where both shapes are used — Sales needs all fourdestructivestyles the action red; it varies genuinely (5 destructive, 3 one-way-but-constructive), so it is not dead configCancelfor a yes/no (the safe default — a stray Enter must not deplete a flock), the textarea for a reason (the user has to type). No focus special-casing needed.DialogDanger button tokens
A single shared token cannot clear both AA on white text and 3:1 against the surface in both themes, so
--danger/--danger-pressare themed, matching the lighter-on-press convention--auberginealready sets.The real cost
window.confirmandwindow.promptare synchronous, so today the call sites read:A React dialog can't return inline, so each handler becomes
asyncand awaits. Mechanical, but it touches control flow — worth doing one screen at a time and keeping each screen's existing guards (inFlight,busy, idempotency-key scoping) intact rather than restructuring them.Two specifics:
DailyEntryPagechecks a synchronous re-entry guard (inFlight.current) before the confirm. Awaiting opens a window between the check and the set, so the guard must be re-checked after the await.Checklist
useConfirmhook +Dialog-based confirm/reason UI--danger/--danger-presstokens, both themes, contrast-checkedDailyEntryPagesubmit (plus the re-entry guard re-check)SalesPageconfirm + cancel + void order + void paymentFlocksPagedeplete + archiveHistoryPagevoid entrywindow.confirm/window.promptstubs from the affected suites; assert the dialog insteadspecs/product/GLOSSARY.md— extend the Dialog entry to cover confirmations and void reasonsRelated
Dialogcomponent this builds onPart of epic #14.