@@ -41,6 +41,9 @@ import { createProcessesView } from './ui/processes.js'
|
|||||||
import { createDataManager } from './ui/data-manager.js'
|
import { createDataManager } from './ui/data-manager.js'
|
||||||
import { formatMib, formatKilobitsPerSec } from './shared/format.js'
|
import { formatMib, formatKilobitsPerSec } from './shared/format.js'
|
||||||
import { chartOptionLabel } from './shared/container-names.js'
|
import { chartOptionLabel } from './shared/container-names.js'
|
||||||
|
import { initConfirmModal, appConfirm } from './ui/confirm-modal.js'
|
||||||
|
|
||||||
|
initConfirmModal()
|
||||||
|
|
||||||
const $ = (id) => document.getElementById(id)
|
const $ = (id) => document.getElementById(id)
|
||||||
|
|
||||||
@@ -924,7 +927,15 @@ function loadFleetView() {
|
|||||||
},
|
},
|
||||||
onForget: async (peer) => {
|
onForget: async (peer) => {
|
||||||
const label = peer.alias || peer.id.slice(0, 12)
|
const label = peer.alias || peer.id.slice(0, 12)
|
||||||
if (!confirm(`Forget ${label}?`)) return
|
const ok = await appConfirm({
|
||||||
|
title: 'Forget agent',
|
||||||
|
message: `Remove “${label}” from saved agents?`,
|
||||||
|
detail: 'You can reconnect later with an invite or public key.',
|
||||||
|
confirmLabel: 'Forget',
|
||||||
|
danger: true,
|
||||||
|
icon: '⊘',
|
||||||
|
})
|
||||||
|
if (!ok) return
|
||||||
removeBookmark(peer.publicKeyHex)
|
removeBookmark(peer.publicKeyHex)
|
||||||
await manager.disconnect(peer.publicKeyHex).catch(() => {})
|
await manager.disconnect(peer.publicKeyHex).catch(() => {})
|
||||||
log(`Forgot ${label}`)
|
log(`Forgot ${label}`)
|
||||||
@@ -1601,7 +1612,15 @@ els.settingQvacIdle?.addEventListener('change', () => {
|
|||||||
els.btnQvacOpen?.addEventListener('click', () => showView('qvac'))
|
els.btnQvacOpen?.addEventListener('click', () => showView('qvac'))
|
||||||
|
|
||||||
els.btnResetPeers?.addEventListener('click', async () => {
|
els.btnResetPeers?.addEventListener('click', async () => {
|
||||||
if (!confirm('Forget all saved agents and disconnect?')) return
|
const ok = await appConfirm({
|
||||||
|
title: 'Forget all agents',
|
||||||
|
message: 'Disconnect and clear every saved agent from this device?',
|
||||||
|
detail: 'Bookmarks, peer cache, and active sessions will be removed. Invites will be required to reconnect.',
|
||||||
|
confirmLabel: 'Forget all',
|
||||||
|
danger: true,
|
||||||
|
icon: '⊘',
|
||||||
|
})
|
||||||
|
if (!ok) return
|
||||||
await manager.disconnectAll({ forget: true })
|
await manager.disconnectAll({ forget: true })
|
||||||
const { savePeers } = await import('./client/peerCache.js')
|
const { savePeers } = await import('./client/peerCache.js')
|
||||||
savePeers({}, { activePeerId: null, force: true })
|
savePeers({}, { activePeerId: null, force: true })
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<link rel="stylesheet" href="./ui/styles.css" />
|
<link rel="stylesheet" href="./ui/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body data-accent="teal" data-density="comfortable" data-reduce-motion="0">
|
<body data-accent="teal" data-density="comfortable" data-reduce-motion="0">
|
||||||
|
<!-- Confirm modal is injected by ui/confirm-modal.js -->
|
||||||
<div id="titlebar" role="banner">
|
<div id="titlebar" role="banner">
|
||||||
<div class="titlebar-left">
|
<div class="titlebar-left">
|
||||||
<pear-ctrl></pear-ctrl>
|
<pear-ctrl></pear-ctrl>
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/**
|
||||||
|
* App confirmation modal — replaces window.confirm with a polished UI.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { initConfirmModal, appConfirm } from './confirm-modal.js'
|
||||||
|
* initConfirmModal()
|
||||||
|
* const ok = await appConfirm({ title: 'Delete?', message: '…', danger: true })
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* title?: string,
|
||||||
|
* message?: string,
|
||||||
|
* detail?: string,
|
||||||
|
* confirmLabel?: string,
|
||||||
|
* cancelLabel?: string,
|
||||||
|
* danger?: boolean,
|
||||||
|
* icon?: string,
|
||||||
|
* }} ConfirmOptions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {ReturnType<typeof createConfirmModal>|null} */
|
||||||
|
let singleton = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} [mount]
|
||||||
|
*/
|
||||||
|
export function initConfirmModal(mount = document.body) {
|
||||||
|
if (singleton) return singleton
|
||||||
|
singleton = createConfirmModal(mount)
|
||||||
|
return singleton
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string|ConfirmOptions} opts
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export function appConfirm(opts) {
|
||||||
|
if (!singleton) initConfirmModal()
|
||||||
|
return singleton.confirm(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} mount
|
||||||
|
*/
|
||||||
|
export function createConfirmModal(mount) {
|
||||||
|
const root = document.createElement('div')
|
||||||
|
root.id = 'app-confirm-root'
|
||||||
|
root.className = 'app-confirm-root'
|
||||||
|
root.setAttribute('aria-hidden', 'true')
|
||||||
|
root.innerHTML = `
|
||||||
|
<div class="app-confirm-backdrop" data-confirm-dismiss></div>
|
||||||
|
<div class="app-confirm-dialog" role="alertdialog" aria-modal="true" aria-labelledby="app-confirm-title" aria-describedby="app-confirm-msg">
|
||||||
|
<div class="app-confirm-glow" aria-hidden="true"></div>
|
||||||
|
<div class="app-confirm-icon" id="app-confirm-icon" aria-hidden="true">?</div>
|
||||||
|
<div class="app-confirm-body">
|
||||||
|
<h2 class="app-confirm-title" id="app-confirm-title">Confirm</h2>
|
||||||
|
<p class="app-confirm-message" id="app-confirm-msg"></p>
|
||||||
|
<p class="app-confirm-detail muted hidden" id="app-confirm-detail"></p>
|
||||||
|
</div>
|
||||||
|
<div class="app-confirm-actions">
|
||||||
|
<button type="button" class="btn btn-ghost app-confirm-cancel" id="app-confirm-cancel">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary app-confirm-ok" id="app-confirm-ok">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
mount.appendChild(root)
|
||||||
|
|
||||||
|
const dialog = root.querySelector('.app-confirm-dialog')
|
||||||
|
const titleEl = root.querySelector('#app-confirm-title')
|
||||||
|
const msgEl = root.querySelector('#app-confirm-msg')
|
||||||
|
const detailEl = root.querySelector('#app-confirm-detail')
|
||||||
|
const iconEl = root.querySelector('#app-confirm-icon')
|
||||||
|
const okBtn = root.querySelector('#app-confirm-ok')
|
||||||
|
const cancelBtn = root.querySelector('#app-confirm-cancel')
|
||||||
|
|
||||||
|
/** @type {((v: boolean) => void)|null} */
|
||||||
|
let resolveFn = null
|
||||||
|
/** @type {HTMLElement|null} */
|
||||||
|
let lastFocus = null
|
||||||
|
|
||||||
|
function close(result) {
|
||||||
|
if (!resolveFn) return
|
||||||
|
const r = resolveFn
|
||||||
|
resolveFn = null
|
||||||
|
root.classList.remove('is-open')
|
||||||
|
root.setAttribute('aria-hidden', 'true')
|
||||||
|
document.body.classList.remove('app-confirm-open')
|
||||||
|
window.setTimeout(() => {
|
||||||
|
root.classList.remove('is-visible')
|
||||||
|
}, 220)
|
||||||
|
try {
|
||||||
|
lastFocus?.focus?.()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
lastFocus = null
|
||||||
|
r(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string|ConfirmOptions} raw
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
function confirm(raw) {
|
||||||
|
const opts = typeof raw === 'string' ? { message: raw } : raw || {}
|
||||||
|
if (resolveFn) {
|
||||||
|
// Stack: resolve previous as false
|
||||||
|
resolveFn(false)
|
||||||
|
resolveFn = null
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolveFn = resolve
|
||||||
|
lastFocus = /** @type {HTMLElement|null} */ (document.activeElement)
|
||||||
|
|
||||||
|
const danger = Boolean(opts.danger)
|
||||||
|
titleEl.textContent = opts.title || (danger ? 'Please confirm' : 'Confirm')
|
||||||
|
msgEl.textContent = opts.message || 'Are you sure?'
|
||||||
|
if (opts.detail) {
|
||||||
|
detailEl.textContent = opts.detail
|
||||||
|
detailEl.classList.remove('hidden')
|
||||||
|
} else {
|
||||||
|
detailEl.textContent = ''
|
||||||
|
detailEl.classList.add('hidden')
|
||||||
|
}
|
||||||
|
iconEl.textContent = opts.icon || (danger ? '!' : '?')
|
||||||
|
root.dataset.danger = danger ? '1' : '0'
|
||||||
|
okBtn.textContent = opts.confirmLabel || (danger ? 'Confirm' : 'Continue')
|
||||||
|
cancelBtn.textContent = opts.cancelLabel || 'Cancel'
|
||||||
|
okBtn.className = danger
|
||||||
|
? 'btn app-confirm-ok app-confirm-ok-danger'
|
||||||
|
: 'btn btn-primary app-confirm-ok'
|
||||||
|
|
||||||
|
root.classList.add('is-visible')
|
||||||
|
// next frame for enter animation
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
root.classList.add('is-open')
|
||||||
|
document.body.classList.add('app-confirm-open')
|
||||||
|
// Safer default: focus Cancel for destructive actions
|
||||||
|
;(danger ? cancelBtn : okBtn).focus()
|
||||||
|
})
|
||||||
|
root.setAttribute('aria-hidden', 'false')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
okBtn.addEventListener('click', () => close(true))
|
||||||
|
cancelBtn.addEventListener('click', () => close(false))
|
||||||
|
root.querySelector('[data-confirm-dismiss]')?.addEventListener('click', () => close(false))
|
||||||
|
|
||||||
|
root.addEventListener('keydown', (ev) => {
|
||||||
|
if (!resolveFn) return
|
||||||
|
if (ev.key === 'Escape') {
|
||||||
|
ev.preventDefault()
|
||||||
|
close(false)
|
||||||
|
} else if (ev.key === 'Enter' && document.activeElement !== cancelBtn) {
|
||||||
|
// Enter confirms unless Cancel is focused
|
||||||
|
if (document.activeElement === okBtn || document.activeElement === dialog) {
|
||||||
|
ev.preventDefault()
|
||||||
|
close(true)
|
||||||
|
}
|
||||||
|
} else if (ev.key === 'Tab') {
|
||||||
|
// Simple focus trap
|
||||||
|
const focusables = [cancelBtn, okBtn]
|
||||||
|
const i = focusables.indexOf(/** @type {HTMLElement} */ (document.activeElement))
|
||||||
|
if (ev.shiftKey) {
|
||||||
|
if (i <= 0) {
|
||||||
|
ev.preventDefault()
|
||||||
|
okBtn.focus()
|
||||||
|
}
|
||||||
|
} else if (i === focusables.length - 1 || i < 0) {
|
||||||
|
ev.preventDefault()
|
||||||
|
cancelBtn.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { confirm, close: () => close(false) }
|
||||||
|
}
|
||||||
+11
-2
@@ -10,6 +10,7 @@
|
|||||||
*/
|
*/
|
||||||
import { drawChart, seriesColor } from './charts.js'
|
import { drawChart, seriesColor } from './charts.js'
|
||||||
import { normalizeChartMode, CHART_MODE_LABEL } from '../shared/chart-types.js'
|
import { normalizeChartMode, CHART_MODE_LABEL } from '../shared/chart-types.js'
|
||||||
|
import { appConfirm } from './confirm-modal.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
@@ -486,10 +487,18 @@ export function createCustomDashboardView(opts) {
|
|||||||
syncEditChrome()
|
syncEditChrome()
|
||||||
render()
|
render()
|
||||||
})
|
})
|
||||||
opts.els.deleteBtn?.addEventListener('click', () => {
|
opts.els.deleteBtn?.addEventListener('click', async () => {
|
||||||
const d = store().getActive()
|
const d = store().getActive()
|
||||||
if (!d) return
|
if (!d) return
|
||||||
if (!confirm(`Delete dashboard “${d.name}”?`)) return
|
const ok = await appConfirm({
|
||||||
|
title: 'Delete dashboard',
|
||||||
|
message: `Delete “${d.name}”?`,
|
||||||
|
detail: 'Tiles and layout for this board will be removed. Other dashboards are unaffected.',
|
||||||
|
confirmLabel: 'Delete',
|
||||||
|
danger: true,
|
||||||
|
icon: '⌫',
|
||||||
|
})
|
||||||
|
if (!ok) return
|
||||||
store().remove(d.id)
|
store().remove(d.id)
|
||||||
persist()
|
persist()
|
||||||
render()
|
render()
|
||||||
|
|||||||
+11
-8
@@ -11,6 +11,7 @@ import {
|
|||||||
matchPointsPreset,
|
matchPointsPreset,
|
||||||
matchBytesPreset,
|
matchBytesPreset,
|
||||||
} from '../shared/retention.js'
|
} from '../shared/retention.js'
|
||||||
|
import { appConfirm } from './confirm-modal.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{
|
* @param {{
|
||||||
@@ -343,14 +344,16 @@ export function createDataManager(deps) {
|
|||||||
els.btnRefresh?.addEventListener('click', () => refresh())
|
els.btnRefresh?.addEventListener('click', () => refresh())
|
||||||
els.btnSave?.addEventListener('click', () => save())
|
els.btnSave?.addEventListener('click', () => save())
|
||||||
els.btnPruneDry?.addEventListener('click', () => runPrune(true))
|
els.btnPruneDry?.addEventListener('click', () => runPrune(true))
|
||||||
els.btnPruneNow?.addEventListener('click', () => {
|
els.btnPruneNow?.addEventListener('click', async () => {
|
||||||
if (
|
const ok = await appConfirm({
|
||||||
!confirm(
|
title: 'Prune warm history',
|
||||||
'Prune warm history on the connected agent now? Points older than the retention window will be deleted.'
|
message: 'Delete points older than the retention window on the connected agent?',
|
||||||
)
|
detail: 'This cannot be undone. A dry-run is available if you want to preview first.',
|
||||||
) {
|
confirmLabel: 'Prune now',
|
||||||
return
|
danger: true,
|
||||||
}
|
icon: '⌫',
|
||||||
|
})
|
||||||
|
if (!ok) return
|
||||||
runPrune(false)
|
runPrune(false)
|
||||||
})
|
})
|
||||||
els.hotPreset?.addEventListener('change', () => {
|
els.hotPreset?.addEventListener('change', () => {
|
||||||
|
|||||||
+17
-6
@@ -13,6 +13,7 @@ import {
|
|||||||
} from './agents.js'
|
} from './agents.js'
|
||||||
import { chartIdsFromToolLog, mountChartEmbeds } from './chart-embed.js'
|
import { chartIdsFromToolLog, mountChartEmbeds } from './chart-embed.js'
|
||||||
import { createSwarmPanel } from './swarm-ui.js'
|
import { createSwarmPanel } from './swarm-ui.js'
|
||||||
|
import { appConfirm } from '../confirm-modal.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{
|
* @param {{
|
||||||
@@ -68,12 +69,22 @@ export function createQvacView(opts) {
|
|||||||
dashboards: opts.dashboards,
|
dashboards: opts.dashboards,
|
||||||
getAutoNavigate: () => settings().qvacAutoNavigate || 'ask',
|
getAutoNavigate: () => settings().qvacAutoNavigate || 'ask',
|
||||||
confirmAction: (msg) => {
|
confirmAction: (msg) => {
|
||||||
try {
|
const text = String(msg || 'Continue?')
|
||||||
// Must be strict true — Cancel returns false
|
const danger =
|
||||||
return typeof confirm === 'function' ? confirm(msg) : false
|
/silence|acknowledge|ack |run job|delete|forget|prune|remove|kill/i.test(text)
|
||||||
} catch {
|
const isNav = /open|switch|view|navigate/i.test(text)
|
||||||
return false
|
return appConfirm({
|
||||||
}
|
title: danger
|
||||||
|
? 'Confirm agent action'
|
||||||
|
: isNav
|
||||||
|
? 'Switch view'
|
||||||
|
: 'Confirm',
|
||||||
|
message: text,
|
||||||
|
confirmLabel: danger ? 'Allow' : isNav ? 'Open' : 'Continue',
|
||||||
|
cancelLabel: 'Not now',
|
||||||
|
danger,
|
||||||
|
icon: danger ? '!' : isNav ? '↗' : '?',
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+252
@@ -4902,3 +4902,255 @@ html[data-theme='light'] .proc-detail-cmd {
|
|||||||
max-width: 40vw;
|
max-width: 40vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── App confirmation modal ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
body.app-confirm-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-root {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
opacity 0.22s ease,
|
||||||
|
visibility 0.22s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-root.is-visible {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-root.is-open {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 70% 50% at 50% 40%, rgba(52, 211, 153, 0.07), transparent 60%),
|
||||||
|
rgba(4, 6, 10, 0.68);
|
||||||
|
backdrop-filter: blur(16px) saturate(1.2);
|
||||||
|
-webkit-backdrop-filter: blur(16px) saturate(1.2);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .app-confirm-backdrop {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 70% 50% at 50% 40%, rgba(52, 211, 153, 0.1), transparent 60%),
|
||||||
|
rgba(15, 23, 42, 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-root[data-danger='1'] .app-confirm-backdrop {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 70% 50% at 50% 40%, rgba(248, 113, 113, 0.1), transparent 60%),
|
||||||
|
rgba(4, 6, 10, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .app-confirm-root[data-danger='1'] .app-confirm-backdrop {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 70% 50% at 50% 40%, rgba(248, 113, 113, 0.12), transparent 60%),
|
||||||
|
rgba(15, 23, 42, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-dialog {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: min(420px, 100%);
|
||||||
|
padding: 28px 28px 22px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
background:
|
||||||
|
linear-gradient(165deg, rgba(255, 255, 255, 0.055) 0%, transparent 42%),
|
||||||
|
var(--bg-secondary);
|
||||||
|
box-shadow:
|
||||||
|
var(--shadow-md),
|
||||||
|
0 0 0 1px rgba(52, 211, 153, 0.08),
|
||||||
|
0 24px 64px rgba(0, 0, 0, 0.45);
|
||||||
|
transform: translateY(14px) scale(0.96);
|
||||||
|
opacity: 0;
|
||||||
|
transition:
|
||||||
|
transform 0.28s cubic-bezier(0.22, 1, 0.36, 1),
|
||||||
|
opacity 0.22s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-root.is-open .app-confirm-dialog {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-root[data-danger='1'] .app-confirm-dialog {
|
||||||
|
box-shadow:
|
||||||
|
var(--shadow-md),
|
||||||
|
0 0 0 1px rgba(248, 113, 113, 0.14),
|
||||||
|
0 24px 64px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-glow {
|
||||||
|
position: absolute;
|
||||||
|
top: -40%;
|
||||||
|
left: 50%;
|
||||||
|
width: 140%;
|
||||||
|
height: 70%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: radial-gradient(
|
||||||
|
ellipse at center,
|
||||||
|
color-mix(in srgb, var(--accent-primary) 22%, transparent),
|
||||||
|
transparent 68%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-root[data-danger='1'] .app-confirm-glow {
|
||||||
|
background: radial-gradient(
|
||||||
|
ellipse at center,
|
||||||
|
rgba(248, 113, 113, 0.22),
|
||||||
|
transparent 68%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-icon {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: var(--accent-primary);
|
||||||
|
background: color-mix(in srgb, var(--accent-primary) 14%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent-primary) 35%, transparent);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 4px color-mix(in srgb, var(--accent-primary) 8%, transparent),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
|
animation: appConfirmIconIn 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-root[data-danger='1'] .app-confirm-icon {
|
||||||
|
color: #f87171;
|
||||||
|
background: rgba(248, 113, 113, 0.12);
|
||||||
|
border-color: rgba(248, 113, 113, 0.38);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 4px rgba(248, 113, 113, 0.08),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes appConfirmIconIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.7) rotate(-8deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) rotate(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-body {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-title {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-message {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-detail {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-detail.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-actions {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-actions .btn {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 40px;
|
||||||
|
border-radius: 11px;
|
||||||
|
font-weight: 560;
|
||||||
|
font-size: 13.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-cancel:focus-visible,
|
||||||
|
.app-confirm-ok:focus-visible {
|
||||||
|
outline: 2px solid color-mix(in srgb, var(--accent-primary) 55%, transparent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-ok-danger {
|
||||||
|
background: rgba(248, 113, 113, 0.16) !important;
|
||||||
|
border-color: rgba(248, 113, 113, 0.45) !important;
|
||||||
|
color: #fca5a5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-ok-danger:hover:not(:disabled) {
|
||||||
|
background: rgba(248, 113, 113, 0.26) !important;
|
||||||
|
color: #fecaca !important;
|
||||||
|
border-color: rgba(248, 113, 113, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-confirm-ok-danger:focus-visible {
|
||||||
|
outline-color: rgba(248, 113, 113, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .app-confirm-dialog {
|
||||||
|
box-shadow:
|
||||||
|
0 20px 50px rgba(15, 23, 42, 0.18),
|
||||||
|
0 0 0 1px rgba(52, 211, 153, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] .app-confirm-ok-danger {
|
||||||
|
color: #dc2626 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.app-confirm-root,
|
||||||
|
.app-confirm-dialog,
|
||||||
|
.app-confirm-icon {
|
||||||
|
transition: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
.app-confirm-dialog {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user