feat(web): reusable Modal + ConfirmProvider, replace native confirm()

Modal component: portal, ESC to close, Tab focus-trap, focus-on-open
(prefers [data-modal-autofocus]), focus restore, overlay click, scroll lock.
ConfirmProvider exposes useConfirm(): async confirm({...}) as a drop-in for
window.confirm; Enter confirms, ESC cancels. Task/account deletes now use it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMHQTtnQtQqL8muAXHr9kd
This commit is contained in:
2026-07-02 09:36:08 +07:00
parent fcbe438f32
commit 29ccbc22e9
6 changed files with 254 additions and 3 deletions
+9 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState, type ChangeEvent, type FormEvent } from 'r
import { createAccount, deleteAccount, getTask, importCSV, runTask, testAccounts, type TaskDetail as TaskDetailData } from '../api'
import { connectTaskWS, type TaskEvent } from '../ws'
import { StatusBadge } from '../components/StatusBadge'
import { useConfirm } from '../components/ConfirmProvider'
const emptyAccount = { src_login: '', src_pass: '', dst_login: '', dst_pass: '' }
@@ -40,6 +41,7 @@ export function TaskDetail({ id }: { id: number }) {
const [log, setLog] = useState<{ type: string; text: string }[]>([])
const [form, setForm] = useState(emptyAccount)
const [busy, setBusy] = useState<'test' | 'run' | 'add' | 'import' | 'delete' | null>(null)
const confirm = useConfirm()
const [error, setError] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
@@ -111,7 +113,13 @@ export function TaskDetail({ id }: { id: number }) {
}
async function onDeleteAccount(accId: number, login: string) {
if (!confirm(`Remove account "${login}" from this task?`)) return
const ok = await confirm({
title: 'Remove account',
message: `Remove account "${login}" from this task?`,
confirmLabel: 'Remove',
danger: true,
})
if (!ok) return
setBusy('delete')
setError(null)
try {
+9 -1
View File
@@ -1,6 +1,7 @@
import { useEffect, useState, type FormEvent } from 'react'
import { createTask, deleteTask, listEndpoints, listTasks, type Endpoint, type Task } from '../api'
import { StatusBadge } from '../components/StatusBadge'
import { useConfirm } from '../components/ConfirmProvider'
export function Tasks() {
const [tasks, setTasks] = useState<Task[] | null>(null)
@@ -10,6 +11,7 @@ export function Tasks() {
const [dstId, setDstId] = useState('')
const [error, setError] = useState<string | null>(null)
const [busy, setBusy] = useState(false)
const confirm = useConfirm()
function reload() {
listTasks()
@@ -23,7 +25,13 @@ export function Tasks() {
}, [])
async function onDeleteTask(id: number, taskName: string) {
if (!confirm(`Delete task "${taskName}" and all its accounts?`)) return
const ok = await confirm({
title: 'Delete task',
message: `Delete task "${taskName}" and all its accounts?`,
confirmLabel: 'Delete',
danger: true,
})
if (!ok) return
setError(null)
try {
await deleteTask(id)