Fix layout, terminal sizing, notifications, and sidebar styling
Layout: - Add panel-container and session-panel CSS so workspace content doesn't overlap sidebar - Session panels use position: absolute to fill workspace area - Workspace is now flex column for proper empty-state/panel stacking Terminal: - xterm.js overrides now target .session-panel in addition to .terminal-container so terminal fills available space - Added debounced window resize handler for terminal re-fit Notifications: - Rewrote Notifications class to emit .notification-* classes matching the existing polished CSS (was emitting .toast-* with no CSS) - Icon is now a small coloured circle badge instead of oversized SVG Sidebar: - Fixed class mismatches: sidebar-dot → protocol-indicator, sidebar-item-sub → sidebar-item-host, sidebar-star → sidebar-item-star - protocolColour() now returns proto-ssh etc. matching CSS selectors Other: - Replaced prompt() in tab rename with custom _inputPrompt modal
This commit is contained in:
@@ -623,11 +623,24 @@
|
||||
|
||||
.notification-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: var(--text-lg);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-size: 11px;
|
||||
font-weight: var(--weight-bold);
|
||||
line-height: 1;
|
||||
margin-top: 1px;
|
||||
color: var(--text-inverse);
|
||||
background: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.notification-success .notification-icon { background: var(--success); }
|
||||
.notification-error .notification-icon { background: var(--error); }
|
||||
.notification-warning .notification-icon { background: var(--warning); }
|
||||
.notification-info .notification-icon { background: var(--info); }
|
||||
|
||||
.notification-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -665,16 +678,9 @@
|
||||
|
||||
/* Colour variants (left border accent) */
|
||||
.notification-success { border-left: 3px solid var(--success); }
|
||||
.notification-success .notification-icon { color: var(--success); }
|
||||
|
||||
.notification-error { border-left: 3px solid var(--error); }
|
||||
.notification-error .notification-icon { color: var(--error); }
|
||||
|
||||
.notification-error { border-left: 3px solid var(--error); }
|
||||
.notification-warning { border-left: 3px solid var(--warning); }
|
||||
.notification-warning .notification-icon { color: var(--warning); }
|
||||
|
||||
.notification-info { border-left: 3px solid var(--info); }
|
||||
.notification-info .notification-icon { color: var(--info); }
|
||||
.notification-info { border-left: 3px solid var(--info); }
|
||||
|
||||
|
||||
/* =======================================================================
|
||||
@@ -790,7 +796,7 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
padding: var(--space-8);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -70,6 +70,24 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--bg-base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Panel container — fills workspace, holds session panels */
|
||||
.panel-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Individual session panel — one per tab, fills the panel container */
|
||||
.session-panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Terminal container — fills workspace */
|
||||
|
||||
@@ -67,37 +67,50 @@
|
||||
}
|
||||
|
||||
/* xterm.js overrides for consistent look */
|
||||
.terminal-container .xterm {
|
||||
.terminal-container .xterm,
|
||||
.session-panel .xterm {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
padding: var(--space-1);
|
||||
}
|
||||
|
||||
.terminal-container .xterm-viewport {
|
||||
.terminal-container .xterm-screen,
|
||||
.session-panel .xterm-screen {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.terminal-container .xterm-viewport,
|
||||
.session-panel .xterm-viewport {
|
||||
/* Allow xterm's own scrollbar to work */
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.terminal-container .xterm-viewport::-webkit-scrollbar {
|
||||
.terminal-container .xterm-viewport::-webkit-scrollbar,
|
||||
.session-panel .xterm-viewport::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.terminal-container .xterm-viewport::-webkit-scrollbar-track {
|
||||
.terminal-container .xterm-viewport::-webkit-scrollbar-track,
|
||||
.session-panel .xterm-viewport::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.terminal-container .xterm-viewport::-webkit-scrollbar-thumb {
|
||||
.terminal-container .xterm-viewport::-webkit-scrollbar-thumb,
|
||||
.session-panel .xterm-viewport::-webkit-scrollbar-thumb {
|
||||
background: var(--border-strong);
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
.terminal-container .xterm-viewport::-webkit-scrollbar-thumb:hover {
|
||||
.terminal-container .xterm-viewport::-webkit-scrollbar-thumb:hover,
|
||||
.session-panel .xterm-viewport::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* Ensure cursor blink is visible */
|
||||
.terminal-container .xterm-cursor-layer {
|
||||
.terminal-container .xterm-cursor-layer,
|
||||
.session-panel .xterm-cursor-layer {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Selection styling within terminal */
|
||||
.terminal-container .xterm .xterm-selection div {
|
||||
.terminal-container .xterm .xterm-selection div,
|
||||
.session-panel .xterm .xterm-selection div {
|
||||
background: var(--accent-strong) !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -295,8 +295,9 @@ class App {
|
||||
searchInput.addEventListener('input', (e) => this.sidebar.search(e.target.value));
|
||||
}
|
||||
|
||||
// Window resize → re-fit active terminal
|
||||
window.addEventListener('resize', () => this.terminals.fitActive());
|
||||
// Window resize → re-fit active terminal (debounced)
|
||||
const { debounce } = window.NexusTerminal;
|
||||
window.addEventListener('resize', debounce(() => this.terminals.fitActive(), 100));
|
||||
}
|
||||
|
||||
// ── Connection lifecycle ─────────────────────────────────────────
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
|
||||
const { createElement, escapeHtml } = window.NexusTerminal;
|
||||
|
||||
/** Map of notification type to SVG icon path data. */
|
||||
/** Small SVG icons for each notification type (16×16 viewBox). */
|
||||
const ICONS = {
|
||||
success: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
error: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
warning: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z',
|
||||
info: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
success: '✓',
|
||||
error: '✕',
|
||||
warning: '!',
|
||||
info: 'i',
|
||||
};
|
||||
|
||||
class Notifications {
|
||||
@@ -20,106 +20,77 @@ class Notifications {
|
||||
/** @type {HTMLElement} */
|
||||
this.container = document.getElementById('notifications');
|
||||
if (!this.container) {
|
||||
// Create the container if it does not yet exist
|
||||
this.container = createElement('div', 'notifications-container', { id: 'notifications' });
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a toast notification.
|
||||
* Displays a notification.
|
||||
* @param {string} message - Notification text.
|
||||
* @param {'info'|'success'|'warning'|'error'} [type='info'] - Severity.
|
||||
* @param {number} [duration=4000] - Auto-dismiss delay in ms. Pass 0 to keep until dismissed.
|
||||
* @param {number} [duration=4000] - Auto-dismiss delay in ms.
|
||||
*/
|
||||
show(message, type = 'info', duration = 4000) {
|
||||
const toast = createElement('div', `toast toast-${type}`);
|
||||
const el = createElement('div', `notification notification-${type}`);
|
||||
|
||||
// Icon
|
||||
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
iconSvg.setAttribute('class', 'toast-icon');
|
||||
iconSvg.setAttribute('viewBox', '0 0 24 24');
|
||||
iconSvg.setAttribute('fill', 'none');
|
||||
iconSvg.setAttribute('stroke', 'currentColor');
|
||||
iconSvg.setAttribute('stroke-width', '2');
|
||||
const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
iconPath.setAttribute('stroke-linecap', 'round');
|
||||
iconPath.setAttribute('stroke-linejoin', 'round');
|
||||
iconPath.setAttribute('d', ICONS[type] || ICONS.info);
|
||||
iconSvg.appendChild(iconPath);
|
||||
toast.appendChild(iconSvg);
|
||||
const icon = createElement('span', 'notification-icon', {
|
||||
textContent: ICONS[type] || ICONS.info,
|
||||
});
|
||||
el.appendChild(icon);
|
||||
|
||||
// Message
|
||||
const msg = createElement('span', 'toast-message', { textContent: message });
|
||||
toast.appendChild(msg);
|
||||
// Body
|
||||
const body = createElement('div', 'notification-body');
|
||||
const msg = createElement('span', 'notification-message', { textContent: message });
|
||||
body.appendChild(msg);
|
||||
el.appendChild(body);
|
||||
|
||||
// Dismiss button
|
||||
const close = createElement('button', 'toast-close', { textContent: '×', 'aria-label': 'Dismiss' });
|
||||
close.addEventListener('click', () => this._dismiss(toast));
|
||||
toast.appendChild(close);
|
||||
const close = createElement('button', 'notification-close', {
|
||||
textContent: '×',
|
||||
'aria-label': 'Dismiss',
|
||||
});
|
||||
close.addEventListener('click', () => this._dismiss(el));
|
||||
el.appendChild(close);
|
||||
|
||||
// Insert at the top of the stack
|
||||
this.container.prepend(toast);
|
||||
|
||||
// Trigger enter animation on next frame
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add('toast-enter');
|
||||
});
|
||||
this.container.prepend(el);
|
||||
|
||||
// Auto-dismiss
|
||||
if (duration > 0) {
|
||||
toast._timer = setTimeout(() => this._dismiss(toast), duration);
|
||||
el._timer = setTimeout(() => this._dismiss(el), duration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand: success notification.
|
||||
* @param {string} message
|
||||
*/
|
||||
success(message) {
|
||||
this.show(message, 'success');
|
||||
}
|
||||
/** @param {string} message */
|
||||
success(message) { this.show(message, 'success'); }
|
||||
|
||||
/** @param {string} message */
|
||||
error(message) { this.show(message, 'error', 6000); }
|
||||
|
||||
/** @param {string} message */
|
||||
warning(message) { this.show(message, 'warning'); }
|
||||
|
||||
/** @param {string} message */
|
||||
info(message) { this.show(message, 'info'); }
|
||||
|
||||
/**
|
||||
* Shorthand: error notification (longer display time).
|
||||
* @param {string} message
|
||||
*/
|
||||
error(message) {
|
||||
this.show(message, 'error', 6000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand: warning notification.
|
||||
* @param {string} message
|
||||
*/
|
||||
warning(message) {
|
||||
this.show(message, 'warning');
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand: informational notification.
|
||||
* @param {string} message
|
||||
*/
|
||||
info(message) {
|
||||
this.show(message, 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates out and removes a toast element.
|
||||
* @param {HTMLElement} toast
|
||||
* Animates out and removes a notification element.
|
||||
* @param {HTMLElement} el
|
||||
* @private
|
||||
*/
|
||||
_dismiss(toast) {
|
||||
if (toast._dismissed) return;
|
||||
toast._dismissed = true;
|
||||
clearTimeout(toast._timer);
|
||||
_dismiss(el) {
|
||||
if (el._dismissed) return;
|
||||
el._dismissed = true;
|
||||
clearTimeout(el._timer);
|
||||
|
||||
toast.classList.add('toast-exit');
|
||||
toast.addEventListener('animationend', () => toast.remove(), { once: true });
|
||||
el.classList.add('is-leaving');
|
||||
el.addEventListener('animationend', () => el.remove(), { once: true });
|
||||
|
||||
// Safety net — remove after 500 ms even if animation event does not fire
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) toast.remove();
|
||||
if (el.parentNode) el.remove();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ class Sidebar {
|
||||
}
|
||||
|
||||
// Protocol colour dot
|
||||
const dot = createElement('span', `sidebar-dot ${protocolColour(profile.protocol)}`);
|
||||
const dot = createElement('span', `protocol-indicator ${protocolColour(profile.protocol)}`);
|
||||
item.appendChild(dot);
|
||||
|
||||
// Text content
|
||||
@@ -164,7 +164,7 @@ class Sidebar {
|
||||
const subtitle = profile.host
|
||||
? `${profile.host}:${profile.port}`
|
||||
: profile.protocol.toUpperCase();
|
||||
const subEl = createElement('span', 'sidebar-item-sub', {
|
||||
const subEl = createElement('span', 'sidebar-item-host', {
|
||||
textContent: truncate(subtitle, 30),
|
||||
});
|
||||
textWrapper.appendChild(subEl);
|
||||
@@ -172,7 +172,7 @@ class Sidebar {
|
||||
|
||||
// Favourite star
|
||||
if (profile.favourite) {
|
||||
const star = createElement('span', 'sidebar-star', { textContent: '★' });
|
||||
const star = createElement('span', 'sidebar-item-star is-favourite', { textContent: '★' });
|
||||
item.appendChild(star);
|
||||
}
|
||||
|
||||
|
||||
@@ -446,10 +446,12 @@ class TabManager {
|
||||
_promptRename(sessionId) {
|
||||
const session = this.app.state.getSession(sessionId);
|
||||
const current = session?.tabName || '';
|
||||
const name = prompt('Tab name:', current);
|
||||
if (name !== null && name.trim()) {
|
||||
this.renameTab(sessionId, name.trim());
|
||||
}
|
||||
// Use the app's custom prompt modal (window.prompt is unavailable in WKWebView)
|
||||
this.app._inputPrompt('Rename Tab', 'Tab name', current).then((name) => {
|
||||
if (name !== null && name.trim()) {
|
||||
this.renameTab(sessionId, name.trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -207,9 +207,9 @@ function dismissContextMenus() {
|
||||
*/
|
||||
function protocolColour(protocol) {
|
||||
switch (protocol) {
|
||||
case 'ssh': return 'protocol-ssh';
|
||||
case 'telnet': return 'protocol-telnet';
|
||||
case 'raw': return 'protocol-raw';
|
||||
case 'ssh': return 'proto-ssh';
|
||||
case 'telnet': return 'proto-telnet';
|
||||
case 'raw': return 'proto-raw';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user