Fix tab close modal, section headers, and credential storage
Tab close confirmation:
- Add is-visible to both backdrop AND modal element — the modal CSS
starts with visibility:hidden/opacity:0, so the modal was invisible
despite the backdrop blur appearing
Section headers (sidebar + profile editor):
- Add mousedown preventDefault() on all clickable section headers —
WKWebView initiates a text selection gesture on mousedown for divs,
swallowing the first click and requiring a double-click
- Fix sidebar delete profile to use custom modal instead of blocked
window.confirm()
Credential storage:
- buildSecretCallback now respects profile's secretMode field:
- 'saved': checks keychain/vault first, prompts if missing, then
persists to keychain (preferred) or encrypted vault
- 'session-only': caches in memory per-session, prompts once
- 'ask-every-time': always prompts
- Previously the callback always prompted and never saved
- Change default secretMode from 'ask-every-time' to 'saved' in both
Go defaults and JS frontend defaults
This commit is contained in:
@@ -16,8 +16,8 @@ const DEFAULTS = {
|
||||
protocol: 'ssh',
|
||||
ssh: {
|
||||
authMethods: ['publickey', 'password', 'keyboard-interactive'],
|
||||
secretMode: 'ask-every-time',
|
||||
keySecretMode: 'ask-every-time',
|
||||
secretMode: 'saved',
|
||||
keySecretMode: 'saved',
|
||||
hostKeyPolicy: 'ask',
|
||||
terminalType: 'xterm-256color',
|
||||
keepaliveInterval: 30,
|
||||
@@ -459,6 +459,9 @@ class ProfileEditor {
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent mousedown from starting a text selection gesture in WKWebView,
|
||||
// which would swallow the first click and require a double-click.
|
||||
header.addEventListener('mousedown', (e) => e.preventDefault());
|
||||
header.addEventListener('click', () => {
|
||||
collapsed = !collapsed;
|
||||
content.style.display = collapsed ? 'none' : 'block';
|
||||
|
||||
@@ -117,6 +117,9 @@ class Sidebar {
|
||||
<span>${escapeHtml(title)}</span>
|
||||
<span class="section-count">${profiles.length}</span>
|
||||
`;
|
||||
// Prevent mousedown from starting a text selection gesture in WKWebView,
|
||||
// which would swallow the first click and require a double-click.
|
||||
header.addEventListener('mousedown', (e) => e.preventDefault());
|
||||
header.addEventListener('click', () => this.toggleSection(key));
|
||||
section.appendChild(header);
|
||||
|
||||
@@ -305,7 +308,9 @@ class Sidebar {
|
||||
* @private
|
||||
*/
|
||||
async _deleteProfile(profile) {
|
||||
const ok = confirm(`Delete "${profile.displayName || profile.host}"?\nThis cannot be undone.`);
|
||||
// Use a custom confirmation modal — window.confirm() is silently
|
||||
// blocked in WKWebView and always returns false.
|
||||
const ok = await this._confirmDelete(profile);
|
||||
if (!ok) return;
|
||||
|
||||
try {
|
||||
@@ -318,6 +323,52 @@ class Sidebar {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a confirmation modal for profile deletion.
|
||||
* @param {Object} profile
|
||||
* @returns {Promise<boolean>}
|
||||
* @private
|
||||
*/
|
||||
_confirmDelete(profile) {
|
||||
return new Promise((resolve) => {
|
||||
const backdrop = createElement('div', 'modal-backdrop');
|
||||
const modal = createElement('div', 'modal modal-sm');
|
||||
const label = escapeHtml(profile.displayName || profile.host || 'Untitled');
|
||||
modal.innerHTML = `
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Delete Profile</h3>
|
||||
<button class="btn btn-icon btn-xs modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="font-size:var(--text-sm);color:var(--text-secondary)">
|
||||
Delete <strong>${label}</strong>? This cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-sm confirm-cancel">Cancel</button>
|
||||
<button class="btn btn-sm btn-danger confirm-ok">Delete</button>
|
||||
</div>
|
||||
`;
|
||||
backdrop.appendChild(modal);
|
||||
document.body.appendChild(backdrop);
|
||||
requestAnimationFrame(() => {
|
||||
backdrop.classList.add('is-visible');
|
||||
modal.classList.add('is-visible');
|
||||
});
|
||||
|
||||
const close = (val) => {
|
||||
backdrop.classList.remove('is-visible');
|
||||
modal.classList.remove('is-visible');
|
||||
setTimeout(() => backdrop.remove(), 200);
|
||||
resolve(val);
|
||||
};
|
||||
modal.querySelector('.modal-close').addEventListener('click', () => close(false));
|
||||
modal.querySelector('.confirm-cancel').addEventListener('click', () => close(false));
|
||||
modal.querySelector('.confirm-ok').addEventListener('click', () => close(true));
|
||||
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(false); });
|
||||
});
|
||||
}
|
||||
|
||||
// ── Filtering ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -172,10 +172,14 @@ class TabManager {
|
||||
`;
|
||||
backdrop.appendChild(modal);
|
||||
document.body.appendChild(backdrop);
|
||||
requestAnimationFrame(() => backdrop.classList.add('is-visible'));
|
||||
requestAnimationFrame(() => {
|
||||
backdrop.classList.add('is-visible');
|
||||
modal.classList.add('is-visible');
|
||||
});
|
||||
|
||||
const close = (val) => {
|
||||
backdrop.classList.remove('is-visible');
|
||||
modal.classList.remove('is-visible');
|
||||
setTimeout(() => backdrop.remove(), 200);
|
||||
resolve(val);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user