(function() {
/**
* Nexus Terminal — sidebar module.
* Renders the profile list with search, sections (favourites, recent, all),
* and context menus.
*/
const { createElement, escapeHtml, fuzzyMatch, protocolColour, dismissContextMenus, truncate } = window.NexusTerminal;
/** Maximum number of profiles shown in the "Recent" section. */
const RECENT_LIMIT = 5;
class Sidebar {
/**
* @param {App} app
*/
constructor(app) {
this.app = app;
this.el = document.getElementById('sidebar-profiles');
this.searchQuery = '';
// Section collapse state
this.collapsedSections = new Set();
// Dismiss context menus on any outside click
document.addEventListener('click', () => dismissContextMenus());
}
// ── Public API ───────────────────────────────────────────────────
/**
* Full re-render of the sidebar profile list.
* Profiles are organised into:
* 1. Favourites (if any)
* 2. Recent (last 5 connected)
* 3. One section per group (sorted alphabetically)
* 4. Ungrouped (profiles with no group assigned)
*/
render() {
if (!this.el) return;
this.el.innerHTML = '';
const profiles = this._filteredProfiles();
// Favourites
const favourites = profiles.filter((p) => p.favourite);
if (favourites.length > 0) {
this._renderSection('★ Favourites', 'favourites', favourites);
}
// Recent
const recent = profiles
.filter((p) => p.lastConnected)
.sort((a, b) => new Date(b.lastConnected) - new Date(a.lastConnected))
.slice(0, RECENT_LIMIT);
if (recent.length > 0) {
this._renderSection('Recent', 'recent', recent);
}
// ── Group-based sections ─────────────────────────────────────
// Collect distinct groups and sort them alphabetically.
const groupMap = new Map();
const ungrouped = [];
for (const p of profiles) {
const grp = (p.folder || '').trim();
if (grp) {
if (!groupMap.has(grp)) groupMap.set(grp, []);
groupMap.get(grp).push(p);
} else {
ungrouped.push(p);
}
}
// Render each group as its own collapsible section
const sortedGroups = [...groupMap.keys()].sort((a, b) => a.localeCompare(b));
for (const grp of sortedGroups) {
this._renderSection(grp, `group:${grp}`, groupMap.get(grp));
}
// Ungrouped connections
if (ungrouped.length > 0) {
const label = sortedGroups.length > 0 ? 'Ungrouped' : 'All Connections';
this._renderSection(label, 'ungrouped', ungrouped);
}
// Empty state
if (profiles.length === 0) {
const empty = createElement('div', 'sidebar-empty');
empty.innerHTML = `
`;
this.el.appendChild(empty);
}
}
/**
* Filters the displayed profiles by query.
* @param {string} query
*/
search(query) {
this.searchQuery = query.trim();
this.render();
}
/**
* Highlights the sidebar item for the currently active profile.
* @param {string|null} profileId
*/
highlightActive(profileId) {
this.el.querySelectorAll('.sidebar-item').forEach((item) => {
item.classList.toggle('sidebar-item-active', item.dataset.profileId === profileId);
});
}
/**
* Toggles the collapsed state of a named section.
* @param {string} sectionName
*/
toggleSection(sectionName) {
if (this.collapsedSections.has(sectionName)) {
this.collapsedSections.delete(sectionName);
} else {
this.collapsedSections.add(sectionName);
}
this.render();
}
// ── Rendering helpers ────────────────────────────────────────────
/**
* Renders a collapsible section of profiles.
* @param {string} title
* @param {string} key
* @param {Array