From 8ca351c4a19a1cda1fe6c0554070ea7f5c97ee81 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Wed, 15 Apr 2026 00:00:00 +0100 Subject: [PATCH] Fix UI visibility, form CSS, and quick-connect modal - Fix drawer/modal backdrop class names and is-visible transitions - Add protocol tab, settings tab, and command palette CSS - Add form-section-content as CSS alias for form-section-body - Add missing form CSS: form-hint, form-toggle, jump-host-row, preset-row - Add btn-outline, form-input-sm, form-select-sm styles - Replace window.prompt() with custom modal for WKWebView compatibility - Remove diagnostic banner and instrumentation --- frontend/src/css/components.css | 12 +++ frontend/src/css/forms.css | 79 +++++++++++++- frontend/src/css/modals.css | 177 ++++++++++++++++++++++++++++++++ frontend/src/js/app.js | 66 +++++++++++- frontend/src/js/profiles.js | 8 +- frontend/src/js/secrets.js | 16 ++- frontend/src/js/settings.js | 8 +- 7 files changed, 357 insertions(+), 9 deletions(-) diff --git a/frontend/src/css/components.css b/frontend/src/css/components.css index 05d96ca..cdfd10d 100644 --- a/frontend/src/css/components.css +++ b/frontend/src/css/components.css @@ -132,6 +132,18 @@ .btn-group { display: inline-flex; } + +/* Outline button — transparent background, subtle border */ +.btn-outline { + background: transparent; + border: 1px solid var(--border); + color: var(--text-secondary); +} +.btn-outline:hover { + background: var(--surface-2); + color: var(--text-primary); +} + .btn-group .btn { border-radius: 0; } diff --git a/frontend/src/css/forms.css b/frontend/src/css/forms.css index ea4795a..394492f 100644 --- a/frontend/src/css/forms.css +++ b/frontend/src/css/forms.css @@ -97,12 +97,14 @@ color: var(--text-tertiary); } -.form-section-body { +.form-section-body, +.form-section-content { padding: var(--space-4); display: none; } -.form-section.is-open .form-section-body { +.form-section.is-open .form-section-body, +.form-section.is-open .form-section-content { display: block; } @@ -437,3 +439,76 @@ color: var(--text-tertiary); flex: 1; } + +/* ----------------------------------------------------------------------- + Form hint — inline help text below fields + ----------------------------------------------------------------------- */ +.form-hint { + font-size: var(--text-xs); + color: var(--text-tertiary); + margin-top: var(--space-1); + line-height: var(--leading-relaxed); +} + +.form-hint.secret-error { + color: var(--danger); +} + +/* ----------------------------------------------------------------------- + Toggle wrapper — label + toggle switch in a row + ----------------------------------------------------------------------- */ +.form-toggle-wrapper { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + padding: var(--space-1) 0; +} + +.form-toggle-label { + font-size: var(--text-sm); + color: var(--text-secondary); + flex: 1; +} + +/* ----------------------------------------------------------------------- + Jump host rows + ----------------------------------------------------------------------- */ +.jump-hosts-list { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.jump-host-row { + display: grid; + grid-template-columns: 1fr 80px 1fr auto; + gap: var(--space-2); + align-items: center; +} + +/* ----------------------------------------------------------------------- + Payload preset rows + ----------------------------------------------------------------------- */ +.payload-presets-list { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.preset-row { + display: grid; + grid-template-columns: 120px 1fr 100px auto; + gap: var(--space-2); + align-items: center; +} + +/* ----------------------------------------------------------------------- + Small / compact form controls — used inside repeater rows + ----------------------------------------------------------------------- */ +.form-input-sm, +.form-select-sm { + font-size: var(--text-xs); + padding: var(--space-1) var(--space-2); + height: 30px; +} diff --git a/frontend/src/css/modals.css b/frontend/src/css/modals.css index cd64732..23df078 100644 --- a/frontend/src/css/modals.css +++ b/frontend/src/css/modals.css @@ -233,6 +233,183 @@ flex-shrink: 0; } +/* Protocol selector tabs inside the drawer */ +.drawer-protocol-tabs { + display: flex; + gap: 0; + padding: 0 var(--space-5); + border-bottom: 1px solid var(--border-subtle); + flex-shrink: 0; +} + +.drawer-proto-tab { + position: relative; + padding: var(--space-2) var(--space-4); + background: none; + border: none; + color: var(--text-secondary); + font-size: var(--text-sm); + font-weight: var(--weight-medium); + cursor: pointer; + letter-spacing: 0.04em; + transition: color var(--transition-fast); +} + +.drawer-proto-tab:hover { + color: var(--text-primary); +} + +.drawer-proto-tab.active { + color: var(--accent); +} + +.drawer-proto-tab.active::after { + content: ''; + position: absolute; + bottom: -1px; + left: var(--space-2); + right: var(--space-2); + height: 2px; + background: var(--accent); + border-radius: 1px; +} + +/* Settings tab bar */ +.settings-tab-bar { + display: flex; + gap: 0; + padding: 0 var(--space-5); + border-bottom: 1px solid var(--border-subtle); + flex-shrink: 0; +} + +.settings-tab { + position: relative; + padding: var(--space-2) var(--space-3); + background: none; + border: none; + color: var(--text-secondary); + font-size: var(--text-sm); + font-weight: var(--weight-medium); + cursor: pointer; + transition: color var(--transition-fast); +} + +.settings-tab:hover { + color: var(--text-primary); +} + +.settings-tab.active { + color: var(--accent); +} + +.settings-tab.active::after { + content: ''; + position: absolute; + bottom: -1px; + left: var(--space-1); + right: var(--space-1); + height: 2px; + background: var(--accent); + border-radius: 1px; +} + +.settings-body { + padding: var(--space-5); +} + +/* Command palette */ +.palette-overlay { + position: fixed; + inset: 0; + z-index: var(--z-modal-backdrop); + background: rgba(10, 10, 20, 0.50); + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 20vh; +} + +.palette { + width: 520px; + max-width: calc(100vw - var(--space-8)); + max-height: 400px; + display: flex; + flex-direction: column; + background: var(--bg-elevated); + border: 1px solid var(--border-default); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-xl); + overflow: hidden; +} + +.palette-input { + width: 100%; + padding: var(--space-3) var(--space-4); + background: transparent; + border: none; + border-bottom: 1px solid var(--border-subtle); + color: var(--text-primary); + font-size: var(--text-base); + outline: none; +} + +.palette-list { + flex: 1; + overflow-y: auto; + padding: var(--space-1) 0; +} + +.palette-item { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-2) var(--space-4); + cursor: pointer; + transition: background var(--transition-fast); +} + +.palette-item:hover, +.palette-item-active { + background: var(--bg-hover); +} + +.palette-item-label { + flex: 1; + font-size: var(--text-sm); + color: var(--text-primary); +} + +.palette-item-detail { + font-size: var(--text-xs); + color: var(--text-tertiary); +} + +.palette-item-shortcut { + font-size: var(--text-xs); + color: var(--text-tertiary); + background: var(--bg-surface); + padding: var(--space-half) var(--space-1); + border-radius: var(--radius-sm); + font-family: var(--font-mono); +} + +.palette-section { + padding: var(--space-1) var(--space-4); + font-size: var(--text-xs); + font-weight: var(--weight-semibold); + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.palette-empty { + padding: var(--space-6) var(--space-4); + text-align: center; + font-size: var(--text-sm); + color: var(--text-tertiary); +} + /* ----------------------------------------------------------------------- Confirmation dialog ----------------------------------------------------------------------- */ diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js index 6e94c98..6360cee 100644 --- a/frontend/src/js/app.js +++ b/frontend/src/js/app.js @@ -335,7 +335,7 @@ class App { * Prompts the user for a host/port and opens a quick connection. */ async quickConnect() { - const input = prompt('Quick connect (host[:port]):'); + const input = await this._inputPrompt('Quick Connect', 'Enter host[:port]', 'e.g. 192.168.1.1:22'); if (!input || !input.trim()) return; const parts = input.trim().split(':'); @@ -505,7 +505,69 @@ class App { */ _bindClick(id, handler) { const el = document.getElementById(id); - if (el) el.addEventListener('click', handler); + if (el) { + el.addEventListener('click', (e) => { + try { + handler(e); + } catch (err) { + console.error('Click handler error for', id, err); + } + }); + } + } + + /** + * Shows a simple text-input prompt modal (replaces window.prompt which + * is unavailable in WKWebView). + * @param {string} title - Modal heading. + * @param {string} label - Label above the input. + * @param {string} [placeholder] - Placeholder text. + * @returns {Promise} Resolves with the value or null if cancelled. + * @private + */ + _inputPrompt(title, label, placeholder) { + const createElement = window.NexusTerminal.createElement; + return new Promise((resolve) => { + const backdrop = createElement('div', 'modal-backdrop'); + const modal = createElement('div', 'modal modal-sm'); + modal.innerHTML = ` + + + + `; + backdrop.appendChild(modal); + document.body.appendChild(backdrop); + requestAnimationFrame(() => backdrop.classList.add('is-visible')); + + const input = modal.querySelector('input'); + const close = (value) => { + backdrop.classList.remove('is-visible'); + setTimeout(() => backdrop.remove(), 200); + resolve(value); + }; + + modal.querySelector('.modal-close').addEventListener('click', () => close(null)); + modal.querySelector('.prompt-cancel').addEventListener('click', () => close(null)); + modal.querySelector('.prompt-ok').addEventListener('click', () => close(input.value)); + backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(null); }); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') close(input.value); + if (e.key === 'Escape') close(null); + }); + + setTimeout(() => input.focus(), 50); + }); } } diff --git a/frontend/src/js/profiles.js b/frontend/src/js/profiles.js index 2a02b1a..f082da7 100644 --- a/frontend/src/js/profiles.js +++ b/frontend/src/js/profiles.js @@ -107,7 +107,7 @@ class ProfileEditor { render(profile, isNew) { this.close(); - const overlay = createElement('div', 'drawer-overlay'); + const overlay = createElement('div', 'drawer-backdrop'); const drawer = createElement('div', 'drawer'); // ── Header ────────────────────────────────────────────── @@ -183,6 +183,12 @@ class ProfileEditor { document.body.appendChild(overlay); this.drawer = overlay; + // Trigger visibility after append so CSS transitions animate + requestAnimationFrame(() => { + overlay.classList.add('is-visible'); + drawer.classList.add('is-visible'); + }); + // Prevent overlay click from reaching through the drawer drawer.addEventListener('click', (e) => e.stopPropagation()); diff --git a/frontend/src/js/secrets.js b/frontend/src/js/secrets.js index 7b10879..56c65ac 100644 --- a/frontend/src/js/secrets.js +++ b/frontend/src/js/secrets.js @@ -29,7 +29,7 @@ class SecretPrompt { return new Promise((resolve, reject) => { this._close(); // dismiss any existing prompt - const overlay = createElement('div', 'modal-overlay'); + const overlay = createElement('div', 'modal-backdrop'); const modal = createElement('div', 'modal modal-secret'); // Header @@ -63,6 +63,11 @@ class SecretPrompt { document.body.appendChild(overlay); this._overlay = overlay; + requestAnimationFrame(() => { + overlay.classList.add('is-visible'); + modal.classList.add('is-visible'); + }); + const input = modal.querySelector('.secret-input'); input.focus(); @@ -103,7 +108,7 @@ class SecretPrompt { return new Promise((resolve, reject) => { this._close(); - const overlay = createElement('div', 'modal-overlay'); + const overlay = createElement('div', 'modal-backdrop'); const modal = createElement('div', 'modal modal-secret'); const title = isNew ? 'Create Master Password' : 'Unlock Secret Store'; @@ -151,6 +156,11 @@ class SecretPrompt { document.body.appendChild(overlay); this._overlay = overlay; + requestAnimationFrame(() => { + overlay.classList.add('is-visible'); + modal.classList.add('is-visible'); + }); + const passInput = modal.querySelector('.master-password'); const confirmInput = modal.querySelector('.master-confirm'); const errorEl = modal.querySelector('.secret-error'); @@ -211,7 +221,7 @@ class SecretPrompt { return new Promise((resolve) => { this._close(); - const overlay = createElement('div', 'modal-overlay'); + const overlay = createElement('div', 'modal-backdrop'); const modal = createElement('div', 'modal modal-secret modal-hostkey'); const header = createElement('div', 'modal-header'); diff --git a/frontend/src/js/settings.js b/frontend/src/js/settings.js index dbf3513..1e126da 100644 --- a/frontend/src/js/settings.js +++ b/frontend/src/js/settings.js @@ -86,7 +86,7 @@ class Settings { _render() { this.close(); - const overlay = createElement('div', 'modal-overlay'); + const overlay = createElement('div', 'modal-backdrop'); const modal = createElement('div', 'modal modal-settings'); // Header @@ -136,6 +136,12 @@ class Settings { modal.addEventListener('click', (e) => e.stopPropagation()); document.body.appendChild(overlay); this._overlay = overlay; + + // Trigger visibility after append so CSS transitions animate + requestAnimationFrame(() => { + overlay.classList.add('is-visible'); + modal.classList.add('is-visible'); + }); } /**