Three system confirm() dialogs sat in the middle of dealing. The worst was on Next Hand — the most-pressed button in a session, asking "are you sure?" every single time, stealing focus and, on a phone, taking over the screen. Next Hand now just happens, and a toast says where the button landed with an Undo beside it for the few seconds anyone would notice a misclick. The same undo covers the pot wizard, which is the more valuable case: paying the wrong player is a real mistake, and rolling it back restores the hand history entry and the session standings along with the seats. The snapshot is in-memory only — an undo is for the mistake you just made, not one from last night. The two genuinely destructive actions keep a confirmation, but as an in-page dialog that matches the table instead of the browser's own: Escape or the backdrop cancels, Enter confirms, and on a phone the buttons go full width with Cancel nearest the thumb. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
116 lines
4.3 KiB
JavaScript
116 lines
4.3 KiB
JavaScript
// Toasts and confirmation dialogs, so the table never has to fall back on the
|
|
// browser's own alert boxes. A system confirm() steals focus, looks nothing like
|
|
// the rest of the page, and on a phone it's a full-screen interruption — all of
|
|
// which is a bad trade for a dealer trying to keep a game moving.
|
|
|
|
// A short-lived message at the bottom of the screen, optionally with one action
|
|
// (in practice: Undo). Only one is ever on screen; a second replaces the first.
|
|
const Toast = {
|
|
DEFAULT_DURATION_MS: 8000,
|
|
timer: null,
|
|
|
|
show(message, options = {}) {
|
|
const host = document.getElementById("toastHost");
|
|
if (!host) return;
|
|
|
|
this.dismiss();
|
|
|
|
const toast = document.createElement("div");
|
|
toast.className = "toast";
|
|
toast.setAttribute("role", "status");
|
|
|
|
const text = document.createElement("span");
|
|
text.className = "toastText";
|
|
text.textContent = message;
|
|
toast.appendChild(text);
|
|
|
|
if (options.actionLabel && options.onAction) {
|
|
const action = document.createElement("button");
|
|
action.type = "button";
|
|
action.className = "toastAction";
|
|
action.textContent = options.actionLabel;
|
|
action.addEventListener("click", () => {
|
|
this.dismiss();
|
|
options.onAction();
|
|
});
|
|
toast.appendChild(action);
|
|
}
|
|
|
|
const close = document.createElement("button");
|
|
close.type = "button";
|
|
close.className = "toastClose";
|
|
close.setAttribute("aria-label", "Dismiss");
|
|
close.textContent = "✕";
|
|
close.addEventListener("click", () => this.dismiss());
|
|
toast.appendChild(close);
|
|
|
|
host.appendChild(toast);
|
|
// Next frame, so the entrance transition actually runs.
|
|
requestAnimationFrame(() => toast.classList.add("toastVisible"));
|
|
|
|
this.timer = setTimeout(
|
|
() => this.dismiss(),
|
|
options.duration || this.DEFAULT_DURATION_MS
|
|
);
|
|
},
|
|
|
|
dismiss() {
|
|
if (this.timer) {
|
|
clearTimeout(this.timer);
|
|
this.timer = null;
|
|
}
|
|
const host = document.getElementById("toastHost");
|
|
if (host) host.innerHTML = "";
|
|
},
|
|
};
|
|
|
|
// Styled stand-in for confirm(). Resolves true if the dealer goes through with it.
|
|
const Dialog = {
|
|
confirm({ title, message, confirmLabel = "Confirm", cancelLabel = "Cancel", danger = false }) {
|
|
return new Promise(resolve => {
|
|
const overlay = document.getElementById("confirmOverlay");
|
|
const modal = overlay.querySelector("#confirmModal");
|
|
overlay.querySelector("#confirmTitle").textContent = title;
|
|
overlay.querySelector("#confirmMessage").textContent = message;
|
|
|
|
const confirmBtn = overlay.querySelector("#confirmAcceptBtn");
|
|
const cancelBtn = overlay.querySelector("#confirmCancelBtn");
|
|
confirmBtn.textContent = confirmLabel;
|
|
cancelBtn.textContent = cancelLabel;
|
|
confirmBtn.classList.toggle("holdemBtnDanger", danger);
|
|
confirmBtn.classList.toggle("holdemBtnAccent", !danger);
|
|
|
|
const finish = result => {
|
|
overlay.hidden = true;
|
|
confirmBtn.removeEventListener("click", onAccept);
|
|
cancelBtn.removeEventListener("click", onCancel);
|
|
overlay.removeEventListener("click", onBackdrop);
|
|
document.removeEventListener("keydown", onKey);
|
|
resolve(result);
|
|
};
|
|
const onAccept = () => finish(true);
|
|
const onCancel = () => finish(false);
|
|
const onBackdrop = e => {
|
|
if (e.target === overlay) finish(false);
|
|
};
|
|
const onKey = e => {
|
|
if (e.key === "Escape") finish(false);
|
|
if (e.key === "Enter") finish(true);
|
|
};
|
|
|
|
confirmBtn.addEventListener("click", onAccept);
|
|
cancelBtn.addEventListener("click", onCancel);
|
|
overlay.addEventListener("click", onBackdrop);
|
|
document.addEventListener("keydown", onKey);
|
|
|
|
overlay.hidden = false;
|
|
modal.focus();
|
|
confirmBtn.focus();
|
|
});
|
|
},
|
|
};
|
|
|
|
if (typeof module !== "undefined" && module.exports) {
|
|
module.exports = { Toast, Dialog };
|
|
}
|