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:
2026-04-15 00:00:00 +01:00
parent 8752ecef2d
commit 018f25e601
8 changed files with 118 additions and 107 deletions
+45 -74
View File
@@ -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);
}
}