feat(ui): shared toast, confirm, dialog, copy and IME-safe Enter primitives

One implementation each, for every page to use instead of its own copy:

- lib/toast.ts + components/Toaster.tsx: toast.success|error|info from
  anywhere, rendered in polite/assertive live regions; an error stays until
  dismissed, a success fades, a repeat collapses.
- lib/confirm.ts + components/ConfirmHost.tsx: await confirmDialog({...})
  and confirmDiscard() through the themed ConfirmModal, replacing
  window.confirm and the actions that asked nothing.
- hooks/useDialog.ts: the modal focus contract (focus in, Tab trapped,
  Escape, focus restored), extracted from ConfirmModal; dialogs stack so
  only the top one answers keys. ConfirmModal now uses it.
- hooks/useArmed.ts: two-click confirmation that disarms after 3 s.
- hooks/useUnsavedGuard.ts: beforeunload while a draft is dirty.
- lib/clipboard.ts copyText: Clipboard API with an execCommand fallback for
  non-secure origins, outcome toasted.
- lib/keys.ts isSubmitEnter: ignores the Enter that confirms an IME
  candidate (Vietnamese Telex, CJK).
This commit is contained in:
2026-10-05 13:17:09 +07:00
parent 215cae7393
commit 17a5c7470d
14 changed files with 557 additions and 50 deletions
+11 -1
View File
@@ -188,5 +188,15 @@
}
},
"examples": "Examples",
"help": "Help"
"help": "Help",
"copied": "Copied",
"copyFailed": "Could not copy to the clipboard",
"dismiss": "Dismiss",
"confirm": "Confirm",
"unsaved": {
"title": "Discard unsaved changes?",
"message": "Your edits have not been saved and will be lost.",
"discard": "Discard",
"keep": "Keep editing"
}
}
+11 -1
View File
@@ -188,5 +188,15 @@
}
},
"examples": "Ví dụ",
"help": "Trợ giúp"
"help": "Trợ giúp",
"copied": "Đã sao chép",
"copyFailed": "Không sao chép được vào bộ nhớ tạm",
"dismiss": "Đóng",
"confirm": "Xác nhận",
"unsaved": {
"title": "Bỏ các thay đổi chưa lưu?",
"message": "Các chỉnh sửa của bạn chưa được lưu và sẽ bị mất.",
"discard": "Bỏ thay đổi",
"keep": "Tiếp tục sửa"
}
}