diff --git a/public/scripts/components/ClusterMembersComponent.js b/public/scripts/components/ClusterMembersComponent.js
index e6dcb14..8a33db5 100644
--- a/public/scripts/components/ClusterMembersComponent.js
+++ b/public/scripts/components/ClusterMembersComponent.js
@@ -19,6 +19,97 @@ class ClusterMembersComponent extends Component {
this.render();
}
}, 200);
+
+ // Drawer state for desktop
+ this.detailsDrawer = null;
+ this.detailsDrawerContent = null;
+ this.detailsDrawerBackdrop = null;
+ this.activeDrawerComponent = null;
+ }
+
+ // Determine if we should use desktop drawer behavior
+ isDesktop() {
+ try {
+ return window && window.innerWidth >= 1024; // desktop threshold
+ } catch (_) {
+ return false;
+ }
+ }
+
+ ensureDrawer() {
+ if (this.detailsDrawer) return;
+ // Create backdrop
+ this.detailsDrawerBackdrop = document.createElement('div');
+ this.detailsDrawerBackdrop.className = 'details-drawer-backdrop';
+ document.body.appendChild(this.detailsDrawerBackdrop);
+
+ // Create drawer
+ this.detailsDrawer = document.createElement('div');
+ this.detailsDrawer.className = 'details-drawer';
+
+ // Header with close button
+ const header = document.createElement('div');
+ header.className = 'details-drawer-header';
+ header.innerHTML = `
+
Node Details
+
+ `;
+ this.detailsDrawer.appendChild(header);
+
+ // Content container
+ this.detailsDrawerContent = document.createElement('div');
+ this.detailsDrawerContent.className = 'details-drawer-content';
+ this.detailsDrawer.appendChild(this.detailsDrawerContent);
+
+ document.body.appendChild(this.detailsDrawer);
+
+ // Close handlers
+ const close = () => this.closeDrawer();
+ header.querySelector('.drawer-close').addEventListener('click', close);
+ this.detailsDrawerBackdrop.addEventListener('click', close);
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') close();
+ });
+ }
+
+ openDrawerForMember(memberIp) {
+ this.ensureDrawer();
+
+ // Clear previous component if any
+ if (this.activeDrawerComponent && typeof this.activeDrawerComponent.unmount === 'function') {
+ try { this.activeDrawerComponent.unmount(); } catch (_) {}
+ }
+ this.detailsDrawerContent.innerHTML = 'Loading detailed information...
';
+
+ // Load and mount NodeDetails into drawer
+ const nodeDetailsVM = new NodeDetailsViewModel();
+ const nodeDetailsComponent = new NodeDetailsComponent(this.detailsDrawerContent, nodeDetailsVM, this.eventBus);
+ this.activeDrawerComponent = nodeDetailsComponent;
+
+ nodeDetailsVM.loadNodeDetails(memberIp).then(() => {
+ nodeDetailsComponent.mount();
+ }).catch((error) => {
+ logger.error('Failed to load node details for drawer:', error);
+ this.detailsDrawerContent.innerHTML = `
+
+ Error loading node details:
+ ${this.escapeHtml(error.message)}
+
+ `;
+ });
+
+ // Open drawer
+ this.detailsDrawer.classList.add('open');
+ this.detailsDrawerBackdrop.classList.add('visible');
+ }
+
+ closeDrawer() {
+ if (this.detailsDrawer) this.detailsDrawer.classList.remove('open');
+ if (this.detailsDrawerBackdrop) this.detailsDrawerBackdrop.classList.remove('visible');
}
mount() {
@@ -409,8 +500,16 @@ class ClusterMembersComponent extends Component {
this.addEventListener(card, 'click', async (e) => {
if (e.target === expandIcon) return;
- const isExpanding = !card.classList.contains('expanded');
+ // On desktop, open slide-in drawer instead of inline expand
+ if (this.isDesktop()) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.openDrawerForMember(memberIp);
+ return;
+ }
+ // Mobile/low-res: keep inline expand/collapse
+ const isExpanding = !card.classList.contains('expanded');
if (isExpanding) {
await this.expandCard(card, memberIp, memberDetails);
} else {
@@ -422,9 +521,11 @@ class ClusterMembersComponent extends Component {
if (expandIcon) {
this.addEventListener(expandIcon, 'click', async (e) => {
e.stopPropagation();
-
+ if (this.isDesktop()) {
+ this.openDrawerForMember(memberIp);
+ return;
+ }
const isExpanding = !card.classList.contains('expanded');
-
if (isExpanding) {
await this.expandCard(card, memberIp, memberDetails);
} else {
diff --git a/public/scripts/components/NodeDetailsComponent.js b/public/scripts/components/NodeDetailsComponent.js
index cc23619..fe12e6e 100644
--- a/public/scripts/components/NodeDetailsComponent.js
+++ b/public/scripts/components/NodeDetailsComponent.js
@@ -475,9 +475,14 @@ class NodeDetailsComponent extends Component {
uploadBtn.disabled = true;
uploadBtn.textContent = '⏳ Uploading...';
- // Get the member IP from the card
+ // Get the member IP from the card if available, otherwise fallback to view model state
const memberCard = this.container.closest('.member-card');
- const memberIp = memberCard.dataset.memberIp;
+ let memberIp = null;
+ if (memberCard && memberCard.dataset && memberCard.dataset.memberIp) {
+ memberIp = memberCard.dataset.memberIp;
+ } else if (this.viewModel && typeof this.viewModel.get === 'function') {
+ memberIp = this.viewModel.get('nodeIp');
+ }
if (!memberIp) {
throw new Error('Could not determine target node IP address');
diff --git a/public/styles/main.css b/public/styles/main.css
index 42835eb..8ac58df 100644
--- a/public/styles/main.css
+++ b/public/styles/main.css
@@ -413,7 +413,6 @@ p {
}
.endpoint-item:hover {
- background: rgba(0, 0, 0, 0.25);
border-color: rgba(255, 255, 255, 0.15);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
@@ -2860,6 +2859,72 @@ p {
display: inline-flex;
}
+/* Desktop slide-in details drawer */
+.details-drawer-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.3);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.2s ease;
+ z-index: 999;
+}
+.details-drawer-backdrop.visible { opacity: 1; pointer-events: auto; }
+
+.details-drawer {
+ position: fixed;
+ top: 0;
+ right: 0;
+ height: 100vh;
+ width: clamp(33.333vw, 520px, 90vw);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ border-left: 1px solid var(--border-primary);
+ box-shadow: var(--shadow-primary);
+ transform: translateX(100%);
+ transition: transform 0.25s ease;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+}
+.details-drawer.open { transform: translateX(0); }
+
+.details-drawer-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid var(--border-secondary);
+ background: var(--bg-secondary);
+}
+.details-drawer-header .drawer-title {
+ font-weight: 600;
+}
+.drawer-close {
+ background: transparent;
+ border: 1px solid var(--border-secondary);
+ color: var(--text-secondary);
+ border-radius: 8px;
+ padding: 0.35rem;
+ cursor: pointer;
+}
+.drawer-close:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+.drawer-close svg { width: 18px; height: 18px; }
+
+.details-drawer-content {
+ padding: 1rem;
+ overflow: auto;
+}
+
+/* Only enable drawer on wider screens; on small keep inline cards */
+@media (max-width: 1023px) {
+ .details-drawer,
+ .details-drawer-backdrop { display: none; }
+}
+
/* Labels section styling in node details */
.detail-section {
margin-top: 20px;