Fix Pear Holesail client createRequire crash; polish confirm modals
CI / test (push) Successful in 9m59s
CI / test (push) Successful in 9m59s
Load holesail via dynamic ESM import (no createRequire) so Pear can connect local tunnels. Route confirmations through a designed modal with icons and animations instead of native dialogs.
This commit is contained in:
@@ -2549,11 +2549,15 @@ function populateContainerDetails(config, container) {
|
||||
if (typeof window.peardockOps?.loadTunnelsView === 'function') {
|
||||
// warm tunnels view cache
|
||||
}
|
||||
// Offer local connect
|
||||
// Offer local connect via designed modal
|
||||
if (url && window.peardockOps?.connectLocalTunnel) {
|
||||
const open = window.confirm(
|
||||
'Tunnel created. Open a local Holesail client and browser now?'
|
||||
);
|
||||
const open = window.peardockOps.askUserConfirm
|
||||
? await window.peardockOps.askUserConfirm(
|
||||
'Open local tunnel?',
|
||||
'Start a local Holesail client and open this tunnel in your browser?',
|
||||
{ confirmLabel: 'Open tunnel', icon: 'fa-network-wired' }
|
||||
)
|
||||
: false;
|
||||
if (open) {
|
||||
await window.peardockOps.connectLocalTunnel(url, { openBrowser: true });
|
||||
}
|
||||
@@ -4200,10 +4204,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
if (terminalClearBtn) {
|
||||
terminalClearBtn.addEventListener('click', () => {
|
||||
if (detailsTerminalSession && detailsTerminalSession.xterm && confirm('Clear terminal?')) {
|
||||
detailsTerminalSession.xterm.clear();
|
||||
}
|
||||
terminalClearBtn.addEventListener('click', async () => {
|
||||
if (!detailsTerminalSession?.xterm) return;
|
||||
const ok = window.peardockOps?.askUserConfirm
|
||||
? await window.peardockOps.askUserConfirm(
|
||||
'Clear terminal?',
|
||||
'Clear the visible terminal buffer for this session?',
|
||||
{ confirmLabel: 'Clear', icon: 'fa-eraser' }
|
||||
)
|
||||
: true;
|
||||
if (ok) detailsTerminalSession.xterm.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4223,52 +4233,86 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Confirmation helper — respects Settings → confirm destructive actions.
|
||||
* Async-friendly: if confirm is disabled, runs onConfirm immediately.
|
||||
* Confirmation helper — designed modal only (never native window.confirm).
|
||||
* Respects Settings → confirm destructive actions when opts.destructive !== false.
|
||||
* @param {string} message
|
||||
* @param {Function} onConfirm
|
||||
* @param {{ title?: string, requireText?: string, danger?: boolean, info?: boolean, confirmLabel?: string, destructive?: boolean }} [opts]
|
||||
*/
|
||||
function showConfirmModal(message, onConfirm, opts = {}) {
|
||||
const run = () => {
|
||||
if (typeof onConfirm === 'function') onConfirm();
|
||||
};
|
||||
|
||||
// Settings gate: skip dialog when user disabled confirms
|
||||
if (window.peardockOps?.shouldConfirmDestructive && !window.peardockOps.shouldConfirmDestructive()) {
|
||||
const destructive = opts.destructive !== false && opts.info !== true;
|
||||
|
||||
// Settings gate: skip dialog when user disabled destructive confirms
|
||||
if (
|
||||
destructive &&
|
||||
window.peardockOps?.shouldConfirmDestructive &&
|
||||
!window.peardockOps.shouldConfirmDestructive()
|
||||
) {
|
||||
run();
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer modern confirm dialog when available
|
||||
if (window.peardockOps?.confirmDestructive) {
|
||||
const title = opts.title || (destructive ? 'Confirm' : 'Continue');
|
||||
|
||||
if (window.peardockOps?.confirmDialog) {
|
||||
window.peardockOps
|
||||
.confirmDestructive(opts.title || 'Confirm', message, opts.requireText)
|
||||
.confirmDialog({
|
||||
title,
|
||||
body: message,
|
||||
danger: opts.danger ?? destructive,
|
||||
info: opts.info,
|
||||
requireText: opts.requireText,
|
||||
confirmLabel: opts.confirmLabel || (destructive ? 'Confirm' : 'Continue'),
|
||||
})
|
||||
.then((ok) => {
|
||||
if (ok) run();
|
||||
})
|
||||
.catch(() => {
|
||||
// fall through to bootstrap modal
|
||||
showBootstrapConfirm(message, onConfirm);
|
||||
});
|
||||
.catch(() => showBootstrapConfirm(message, onConfirm, opts));
|
||||
return;
|
||||
}
|
||||
if (window.peardockOps?.askUserConfirm) {
|
||||
window.peardockOps
|
||||
.askUserConfirm(title, message, {
|
||||
danger: opts.danger ?? destructive,
|
||||
info: opts.info,
|
||||
confirmLabel: opts.confirmLabel,
|
||||
})
|
||||
.then((ok) => {
|
||||
if (ok) run();
|
||||
})
|
||||
.catch(() => showBootstrapConfirm(message, onConfirm, opts));
|
||||
return;
|
||||
}
|
||||
|
||||
showBootstrapConfirm(message, onConfirm);
|
||||
showBootstrapConfirm(message, onConfirm, opts);
|
||||
}
|
||||
|
||||
function showBootstrapConfirm(message, onConfirm) {
|
||||
function showBootstrapConfirm(message, onConfirm, opts = {}) {
|
||||
const modalEl = document.getElementById('confirmModal');
|
||||
if (!modalEl || typeof bootstrap === 'undefined') {
|
||||
if (window.confirm(message) && typeof onConfirm === 'function') onConfirm();
|
||||
// Last resort: still avoid ugly defaults when possible
|
||||
console.warn('[WARN] confirm modal unavailable; action cancelled for safety');
|
||||
return;
|
||||
}
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||
const messageEl = document.getElementById('confirmModalMessage');
|
||||
const titleEl = document.getElementById('confirmModalLabel');
|
||||
const confirmBtn = document.getElementById('confirmModalBtn');
|
||||
|
||||
if (titleEl && opts.title) titleEl.textContent = opts.title;
|
||||
if (messageEl) messageEl.textContent = message;
|
||||
|
||||
if (confirmBtn && confirmBtn.parentNode) {
|
||||
const newConfirmBtn = confirmBtn.cloneNode(true);
|
||||
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
|
||||
if (opts.confirmLabel) newConfirmBtn.textContent = opts.confirmLabel;
|
||||
newConfirmBtn.className = opts.danger === false && opts.info
|
||||
? 'btn btn-primary'
|
||||
: 'btn btn-danger';
|
||||
newConfirmBtn.addEventListener('click', () => {
|
||||
modal.hide();
|
||||
if (typeof onConfirm === 'function') onConfirm();
|
||||
@@ -5827,15 +5871,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
// Reset all peers lives in Settings (not the sidebar)
|
||||
document.getElementById('settings-reset-peers-btn')?.addEventListener('click', () => {
|
||||
document.getElementById('settings-reset-peers-btn')?.addEventListener('click', async () => {
|
||||
const count = Object.keys(connections).length;
|
||||
if (count === 0) {
|
||||
showAlert('info', 'No saved peers to reset');
|
||||
return;
|
||||
}
|
||||
const ok = window.confirm(
|
||||
`Remove all ${count} saved peer(s) from this client? You will need to re-add their public keys.`
|
||||
);
|
||||
const ok = window.peardockOps?.confirmDestructive
|
||||
? await window.peardockOps.confirmDestructive(
|
||||
'Reset all peers?',
|
||||
`Remove all ${count} saved peer(s) from this client? You will need to re-add their public keys.`
|
||||
)
|
||||
: false;
|
||||
if (ok) resetAllPeers();
|
||||
});
|
||||
|
||||
|
||||
+69
-35
@@ -1,37 +1,70 @@
|
||||
/**
|
||||
* Pear-side Holesail client: bind a local port that proxies to a remote hs:// tunnel.
|
||||
* Optional — requires the `holesail` package (already a peardock dependency).
|
||||
*
|
||||
* Avoids Node's `createRequire` (unsupported in Pear's ESM loader).
|
||||
* Uses dynamic import so CJS packages resolve in both Node and Pear/Bare.
|
||||
*/
|
||||
import { createRequire } from 'module'
|
||||
import net from 'net'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
/** @type {Map<string, { instance: any, info: object, localPort: number }>} */
|
||||
const localClients = new Map()
|
||||
|
||||
let HolesailCtor = null
|
||||
let holesailLoadError = null
|
||||
|
||||
function loadHolesail() {
|
||||
/**
|
||||
* Load the holesail constructor via ESM dynamic import (CJS interop).
|
||||
* @returns {Promise<Function>}
|
||||
*/
|
||||
async function loadHolesail() {
|
||||
if (HolesailCtor) return HolesailCtor
|
||||
HolesailCtor = require('holesail')
|
||||
return HolesailCtor
|
||||
if (holesailLoadError) throw holesailLoadError
|
||||
try {
|
||||
const mod = await import('holesail')
|
||||
// CJS default / named interop across Node, Pear, Bare
|
||||
const Ctor =
|
||||
(typeof mod === 'function' && mod) ||
|
||||
mod?.default ||
|
||||
mod?.Holesail ||
|
||||
(mod?.default && mod.default.default) ||
|
||||
null
|
||||
if (typeof Ctor !== 'function') {
|
||||
throw new Error(
|
||||
'holesail package loaded but no constructor export was found'
|
||||
)
|
||||
}
|
||||
HolesailCtor = Ctor
|
||||
return HolesailCtor
|
||||
} catch (err) {
|
||||
holesailLoadError = err
|
||||
const msg = err?.message || String(err)
|
||||
throw new Error(
|
||||
`Could not load holesail in this runtime (${msg}). ` +
|
||||
`Copy the hs:// URL and run: npx holesail <url>`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a free TCP port on 127.0.0.1
|
||||
* Pick a free TCP port on 127.0.0.1 (falls back to random high port if net unavailable).
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
export function findFreePort() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const s = net.createServer()
|
||||
s.listen(0, '127.0.0.1', () => {
|
||||
const addr = s.address()
|
||||
const port = typeof addr === 'object' && addr ? addr.port : 0
|
||||
s.close((err) => (err ? reject(err) : resolve(port)))
|
||||
export async function findFreePort() {
|
||||
try {
|
||||
const net = await import('net')
|
||||
const createServer = net.createServer || net.default?.createServer
|
||||
if (typeof createServer !== 'function') throw new Error('net.createServer missing')
|
||||
return await new Promise((resolve, reject) => {
|
||||
const s = createServer()
|
||||
s.listen(0, '127.0.0.1', () => {
|
||||
const addr = s.address()
|
||||
const port = typeof addr === 'object' && addr ? addr.port : 0
|
||||
s.close((err) => (err ? reject(err) : resolve(port)))
|
||||
})
|
||||
s.on('error', reject)
|
||||
})
|
||||
s.on('error', reject)
|
||||
})
|
||||
} catch {
|
||||
// Pear / restricted environments: best-effort ephemeral range
|
||||
return 41000 + Math.floor(Math.random() * 10000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +80,7 @@ export async function connectLocalHolesail(urlOrKey, opts = {}) {
|
||||
return localClients.get(url)
|
||||
}
|
||||
|
||||
const Holesail = loadHolesail()
|
||||
const Holesail = await loadHolesail()
|
||||
const localPort = opts.localPort || (await findFreePort())
|
||||
const host = opts.host || '127.0.0.1'
|
||||
|
||||
@@ -102,27 +135,28 @@ export function listLocalHolesail() {
|
||||
|
||||
function tryOpenBrowser(href) {
|
||||
try {
|
||||
// Pear / Electron
|
||||
if (typeof window !== 'undefined' && window.open) {
|
||||
if (typeof window !== 'undefined' && typeof window.open === 'function') {
|
||||
window.open(href, '_blank')
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
const { exec } = require('child_process')
|
||||
const platform = process.platform
|
||||
const cmd =
|
||||
platform === 'darwin'
|
||||
? `open "${href}"`
|
||||
: platform === 'win32'
|
||||
? `start "" "${href}"`
|
||||
: `xdg-open "${href}"`
|
||||
exec(cmd)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
// Best-effort outside browser (Node only) — never use createRequire
|
||||
import('child_process')
|
||||
.then((cp) => {
|
||||
const exec = cp.exec || cp.default?.exec
|
||||
if (typeof exec !== 'function') return
|
||||
const platform = globalThis.process?.platform || ''
|
||||
const cmd =
|
||||
platform === 'darwin'
|
||||
? `open "${href}"`
|
||||
: platform === 'win32'
|
||||
? `start "" "${href}"`
|
||||
: `xdg-open "${href}"`
|
||||
exec(cmd)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
+84
-18
@@ -64,55 +64,104 @@ export function suggestedMark(source = 'Suggested') {
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm dialog promise.
|
||||
* @param {{ title: string, body: string, confirmLabel?: string, danger?: boolean, requireText?: string }} opts
|
||||
* Confirm / info dialog (designed modal — never native window.confirm).
|
||||
* @param {{
|
||||
* title: string,
|
||||
* body: string,
|
||||
* confirmLabel?: string,
|
||||
* cancelLabel?: string,
|
||||
* danger?: boolean,
|
||||
* info?: boolean,
|
||||
* icon?: string,
|
||||
* requireText?: string,
|
||||
* hideCancel?: boolean,
|
||||
* }} opts
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export function confirmDialog(opts) {
|
||||
return new Promise((resolve) => {
|
||||
const previouslyFocused = document.activeElement
|
||||
const backdrop = el('div', { className: 'pd-modal-backdrop', role: 'presentation' })
|
||||
const danger = Boolean(opts.danger)
|
||||
const info = Boolean(opts.info) && !danger
|
||||
const icon =
|
||||
opts.icon ||
|
||||
(danger ? 'fa-triangle-exclamation' : info ? 'fa-circle-info' : 'fa-circle-question')
|
||||
const tone = danger ? 'danger' : info ? 'info' : 'primary'
|
||||
|
||||
const backdrop = el('div', {
|
||||
className: 'pd-modal-backdrop',
|
||||
role: 'presentation',
|
||||
})
|
||||
const modal = el('div', {
|
||||
className: 'pd-modal',
|
||||
className: `pd-modal pd-modal--${tone}`,
|
||||
role: 'dialog',
|
||||
'aria-modal': 'true',
|
||||
'aria-labelledby': 'pd-confirm-title',
|
||||
})
|
||||
modal.innerHTML = `
|
||||
<div class="pd-modal-icon pd-modal-icon--${tone}" aria-hidden="true">
|
||||
<i class="fas ${escapeHtml(icon)}"></i>
|
||||
</div>
|
||||
<h3 class="pd-modal-title" id="pd-confirm-title">${escapeHtml(opts.title)}</h3>
|
||||
<p class="pd-modal-body">${escapeHtml(opts.body)}</p>
|
||||
${opts.requireText ? `<input class="form-control bg-dark text-white mb-3 pd-confirm-input" placeholder="Type ${escapeHtml(opts.requireText)} to confirm">` : ''}
|
||||
${
|
||||
opts.requireText
|
||||
? `<div class="pd-modal-confirm-field">
|
||||
<label class="pd-modal-label">Type <strong>${escapeHtml(opts.requireText)}</strong> to confirm</label>
|
||||
<input class="form-control bg-dark text-white pd-confirm-input" autocomplete="off" spellcheck="false" placeholder="${escapeHtml(opts.requireText)}">
|
||||
</div>`
|
||||
: ''
|
||||
}
|
||||
<div class="pd-modal-actions">
|
||||
<button type="button" class="btn btn-outline-secondary pd-cancel">Cancel</button>
|
||||
<button type="button" class="btn ${opts.danger ? 'btn-danger' : 'btn-primary'} pd-ok">${escapeHtml(opts.confirmLabel || 'Confirm')}</button>
|
||||
${
|
||||
opts.hideCancel
|
||||
? ''
|
||||
: `<button type="button" class="btn btn-outline-secondary pd-cancel">${escapeHtml(opts.cancelLabel || 'Cancel')}</button>`
|
||||
}
|
||||
<button type="button" class="btn ${danger ? 'btn-danger' : 'btn-primary'} pd-ok">
|
||||
${escapeHtml(opts.confirmLabel || (info ? 'OK' : 'Confirm'))}
|
||||
</button>
|
||||
</div>`
|
||||
backdrop.appendChild(modal)
|
||||
document.body.appendChild(backdrop)
|
||||
// enter animation
|
||||
requestAnimationFrame(() => backdrop.classList.add('pd-modal-backdrop--visible'))
|
||||
|
||||
const input = modal.querySelector('.pd-confirm-input')
|
||||
const ok = modal.querySelector('.pd-ok')
|
||||
const cancel = modal.querySelector('.pd-cancel')
|
||||
const close = (val) => {
|
||||
backdrop.remove()
|
||||
document.removeEventListener('keydown', onKey)
|
||||
if (previouslyFocused && typeof previouslyFocused.focus === 'function') {
|
||||
try {
|
||||
previouslyFocused.focus()
|
||||
} catch {
|
||||
// ignore
|
||||
backdrop.classList.remove('pd-modal-backdrop--visible')
|
||||
backdrop.classList.add('pd-modal-backdrop--leaving')
|
||||
setTimeout(() => {
|
||||
backdrop.remove()
|
||||
document.removeEventListener('keydown', onKey)
|
||||
if (previouslyFocused && typeof previouslyFocused.focus === 'function') {
|
||||
try {
|
||||
previouslyFocused.focus()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve(val)
|
||||
resolve(val)
|
||||
}, 160)
|
||||
}
|
||||
const onKey = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
close(false)
|
||||
}
|
||||
if (e.key === 'Enter' && document.activeElement !== input) {
|
||||
e.preventDefault()
|
||||
ok?.click()
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', onKey)
|
||||
cancel.onclick = () => close(false)
|
||||
if (cancel) cancel.onclick = () => close(false)
|
||||
ok.onclick = () => {
|
||||
if (opts.requireText && input && input.value !== opts.requireText) {
|
||||
input.classList.add('is-invalid')
|
||||
input.focus()
|
||||
return
|
||||
}
|
||||
close(true)
|
||||
@@ -120,7 +169,24 @@ export function confirmDialog(opts) {
|
||||
backdrop.addEventListener('click', (e) => {
|
||||
if (e.target === backdrop) close(false)
|
||||
})
|
||||
setTimeout(() => (input || ok)?.focus?.(), 0)
|
||||
setTimeout(() => (input || ok)?.focus?.(), 30)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-destructive confirm (e.g. open tunnel, clear log).
|
||||
* @param {string} title
|
||||
* @param {string} body
|
||||
* @param {{ confirmLabel?: string, info?: boolean, icon?: string }} [opts]
|
||||
*/
|
||||
export async function askConfirm(title, body, opts = {}) {
|
||||
return confirmDialog({
|
||||
title,
|
||||
body,
|
||||
confirmLabel: opts.confirmLabel || 'Continue',
|
||||
info: opts.info !== false && !opts.danger,
|
||||
danger: opts.danger,
|
||||
icon: opts.icon,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+16
-2
@@ -28,7 +28,7 @@ import {
|
||||
suggestName,
|
||||
suggestFromImage,
|
||||
} from '../client/snapshot.js'
|
||||
import { openCommandPalette, confirmDialog, statusBadge } from './components.js'
|
||||
import { openCommandPalette, confirmDialog, askConfirm, statusBadge } from './components.js'
|
||||
import notificationManager from '../libs/notifications.js'
|
||||
import {
|
||||
DEFAULT_TEMPLATE_LIST_URLS,
|
||||
@@ -974,7 +974,19 @@ export function shouldConfirmDestructive() {
|
||||
|
||||
export async function confirmDestructive(title, body, requireText) {
|
||||
if (!shouldConfirmDestructive()) return true
|
||||
return confirmDialog({ title, body, danger: true, requireText, confirmLabel: 'Confirm' })
|
||||
return confirmDialog({
|
||||
title,
|
||||
body,
|
||||
danger: true,
|
||||
requireText,
|
||||
confirmLabel: 'Confirm',
|
||||
icon: 'fa-triangle-exclamation',
|
||||
})
|
||||
}
|
||||
|
||||
/** Non-destructive designed modal confirm (replaces window.confirm). */
|
||||
export async function askUserConfirm(title, body, opts = {}) {
|
||||
return askConfirm(title, body, opts)
|
||||
}
|
||||
|
||||
export function initOpsApp({ navigateToView, sendCommand }) {
|
||||
@@ -1183,6 +1195,8 @@ export function initOpsApp({ navigateToView, sendCommand }) {
|
||||
openSmartNetworkModal,
|
||||
hydrateFormFromImage,
|
||||
confirmDestructive,
|
||||
askUserConfirm,
|
||||
confirmDialog,
|
||||
shouldConfirmDestructive,
|
||||
loadSettings,
|
||||
applySettings,
|
||||
|
||||
+87
-9
@@ -130,24 +130,102 @@
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(2, 6, 23, 0.72);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 3000;
|
||||
padding: 1rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.16s ease;
|
||||
}
|
||||
.pd-modal-backdrop--visible {
|
||||
opacity: 1;
|
||||
}
|
||||
.pd-modal-backdrop--leaving {
|
||||
opacity: 0;
|
||||
}
|
||||
.pd-modal {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem 1.5rem;
|
||||
max-width: 28rem;
|
||||
background: linear-gradient(165deg, #152032 0%, #0f172a 55%, #0b1220 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 1.35rem 1.5rem 1.25rem;
|
||||
max-width: 26rem;
|
||||
width: 100%;
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.45);
|
||||
box-shadow:
|
||||
0 24px 60px rgba(0, 0, 0, 0.55),
|
||||
0 0 0 1px rgba(52, 211, 153, 0.06);
|
||||
transform: translateY(8px) scale(0.98);
|
||||
transition: transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
.pd-modal-backdrop--visible .pd-modal {
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
.pd-modal--danger {
|
||||
box-shadow:
|
||||
0 24px 60px rgba(0, 0, 0, 0.55),
|
||||
0 0 0 1px rgba(248, 113, 113, 0.12);
|
||||
}
|
||||
.pd-modal-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-bottom: 0.85rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.pd-modal-icon--danger {
|
||||
background: rgba(248, 113, 113, 0.14);
|
||||
color: #f87171;
|
||||
}
|
||||
.pd-modal-icon--info {
|
||||
background: rgba(56, 189, 248, 0.14);
|
||||
color: #38bdf8;
|
||||
}
|
||||
.pd-modal-icon--primary {
|
||||
background: rgba(52, 211, 153, 0.14);
|
||||
color: #34d399;
|
||||
}
|
||||
.pd-modal-title {
|
||||
margin: 0 0 0.45rem;
|
||||
color: var(--text-primary, #f4f7fb);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.pd-modal-body {
|
||||
color: var(--text-secondary, #c8d1df);
|
||||
margin: 0 0 1.15rem;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.pd-modal-confirm-field {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
.pd-modal-label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted, #9aa8bc);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.pd-modal-actions {
|
||||
display: flex;
|
||||
gap: 0.55rem;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.pd-modal-actions .btn {
|
||||
border-radius: 10px;
|
||||
min-width: 5.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.pd-confirm-input.is-invalid {
|
||||
border-color: #f87171 !important;
|
||||
box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.15);
|
||||
}
|
||||
.pd-modal-title { margin: 0 0 0.5rem; color: var(--text-primary, #f4f7fb); font-size: 1.1rem; }
|
||||
.pd-modal-body { color: var(--text-secondary, #c8d1df); margin-bottom: 1rem; }
|
||||
.pd-modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; }
|
||||
|
||||
.pd-palette {
|
||||
width: min(32rem, 100%);
|
||||
|
||||
Reference in New Issue
Block a user