@@ -41,6 +41,9 @@ import { createProcessesView } from './ui/processes.js'
|
||||
import { createDataManager } from './ui/data-manager.js'
|
||||
import { formatMib, formatKilobitsPerSec } from './shared/format.js'
|
||||
import { chartOptionLabel } from './shared/container-names.js'
|
||||
import { initConfirmModal, appConfirm } from './ui/confirm-modal.js'
|
||||
|
||||
initConfirmModal()
|
||||
|
||||
const $ = (id) => document.getElementById(id)
|
||||
|
||||
@@ -924,7 +927,15 @@ function loadFleetView() {
|
||||
},
|
||||
onForget: async (peer) => {
|
||||
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)
|
||||
await manager.disconnect(peer.publicKeyHex).catch(() => {})
|
||||
log(`Forgot ${label}`)
|
||||
@@ -1601,7 +1612,15 @@ els.settingQvacIdle?.addEventListener('change', () => {
|
||||
els.btnQvacOpen?.addEventListener('click', () => showView('qvac'))
|
||||
|
||||
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 })
|
||||
const { savePeers } = await import('./client/peerCache.js')
|
||||
savePeers({}, { activePeerId: null, force: true })
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<link rel="stylesheet" href="./ui/styles.css" />
|
||||
</head>
|
||||
<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 class="titlebar-left">
|
||||
<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 { normalizeChartMode, CHART_MODE_LABEL } from '../shared/chart-types.js'
|
||||
import { appConfirm } from './confirm-modal.js'
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
@@ -486,10 +487,18 @@ export function createCustomDashboardView(opts) {
|
||||
syncEditChrome()
|
||||
render()
|
||||
})
|
||||
opts.els.deleteBtn?.addEventListener('click', () => {
|
||||
opts.els.deleteBtn?.addEventListener('click', async () => {
|
||||
const d = store().getActive()
|
||||
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)
|
||||
persist()
|
||||
render()
|
||||
|
||||
+11
-8
@@ -11,6 +11,7 @@ import {
|
||||
matchPointsPreset,
|
||||
matchBytesPreset,
|
||||
} from '../shared/retention.js'
|
||||
import { appConfirm } from './confirm-modal.js'
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
@@ -343,14 +344,16 @@ export function createDataManager(deps) {
|
||||
els.btnRefresh?.addEventListener('click', () => refresh())
|
||||
els.btnSave?.addEventListener('click', () => save())
|
||||
els.btnPruneDry?.addEventListener('click', () => runPrune(true))
|
||||
els.btnPruneNow?.addEventListener('click', () => {
|
||||
if (
|
||||
!confirm(
|
||||
'Prune warm history on the connected agent now? Points older than the retention window will be deleted.'
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
els.btnPruneNow?.addEventListener('click', async () => {
|
||||
const ok = await appConfirm({
|
||||
title: 'Prune warm history',
|
||||
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',
|
||||
danger: true,
|
||||
icon: '⌫',
|
||||
})
|
||||
if (!ok) return
|
||||
runPrune(false)
|
||||
})
|
||||
els.hotPreset?.addEventListener('change', () => {
|
||||
|
||||
+17
-6
@@ -13,6 +13,7 @@ import {
|
||||
} from './agents.js'
|
||||
import { chartIdsFromToolLog, mountChartEmbeds } from './chart-embed.js'
|
||||
import { createSwarmPanel } from './swarm-ui.js'
|
||||
import { appConfirm } from '../confirm-modal.js'
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
@@ -68,12 +69,22 @@ export function createQvacView(opts) {
|
||||
dashboards: opts.dashboards,
|
||||
getAutoNavigate: () => settings().qvacAutoNavigate || 'ask',
|
||||
confirmAction: (msg) => {
|
||||
try {
|
||||
// Must be strict true — Cancel returns false
|
||||
return typeof confirm === 'function' ? confirm(msg) : false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
const text = String(msg || 'Continue?')
|
||||
const danger =
|
||||
/silence|acknowledge|ack |run job|delete|forget|prune|remove|kill/i.test(text)
|
||||
const isNav = /open|switch|view|navigate/i.test(text)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 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