Repository navigation
Description
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.
| Where | Action | Consequence |
|---|---|---|
HistoryPage.tsx:201 |
Void daily entry | Frees the day for re-entry (#82) |
SalesPage.tsx:305 |
Void payment | The order's outstanding amount grows back |
SalesPage.tsx:330 |
Void confirmed order | Allocated stock returns to the exact lots it came from |
Why replace them
- Unstyleable — no aubergine palette, no dark mode, no destructive treatment. The night theme (UI design revamp — distinctive visual identity #52) stops at the popup edge.
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.- 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.
- Mobile — system chrome with the origin in the title, jarring next to the bottom-sheet dialogs (Inline add/edit forms → modal dialogs (less-intrusive capture) #131).
- Untestable in the good way — each site currently needs a
window.confirm/window.promptstub 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 (!(await confirm({ title, body, confirmLabel, destructive }))) return;
const reason = await askReason({ 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 destructivestyles 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:
Cancelfor 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:
if (!window.confirm("...")) return;
void run(async () => { ... });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:
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.- Sales already opens two dialogs from the same panel; a confirm must not stack on top of the payment dialog.
Checklist
-
useConfirmhook +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-presstokens, both themes, contrast-checked - Migrate
DailyEntryPagesubmit (plus the re-entry guard re-check) - Migrate
SalesPageconfirm + cancel + void order + void payment - Migrate
FlocksPagedeplete + archive - Migrate
HistoryPagevoid entry - Drop the
window.confirm/window.promptstubs from the affected suites; assert the dialog instead - Verify the one-way guards still hold (no double-submit, idempotency keys unchanged)
- Light + dark screenshots, desktop + mobile
-
specs/product/GLOSSARY.md— extend the Dialog entry to cover confirmations and void reasons - Help page "Adding & correcting" — say that one-way actions ask first and voids need a reason
Related
- Inline add/edit forms → modal dialogs (less-intrusive capture) #131 / PR F131: move add/edit capture into accessible modal dialogs #132 — the
Dialogcomponent this builds on - Capture screens: move Water / expense-add / Sales add-line forms into dialogs? #133 — capture screens, an independent decision
- F13: Destructive-action guards — confirms + prefill-failure overwrite fix #59 / F14: Void confirmed sales order — return allocated stock #60 / F17: Edit submitted daily entries — lock job + manager adjust with lot/movement reconciliation #69 / F17 follow-up: voided daily entry permanently blocks re-entering that day #82 — the one-way and void semantics these popups guard
Part of epic #14.
Activity
[-]One-way actions: replace window.confirm with a styled confirmation dialog[/-][+]One-way actions: replace window.confirm / window.prompt with styled dialogs[/+]mforce commentedon Jul 22, 2026
Scope corrected before starting: the original body listed 5
window.confirmsites. A census forwindow.promptas 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.confirmwhile leavingwindow.promptbehind 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.