From 8895c0fb759bfddf357ff26e50ff8c1beb9a6062 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 28 Feb 2026 17:08:45 -0800 Subject: [PATCH] feat: always-visible tab close buttons + rApp dropdown shows open apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tab close (×) buttons now visible at 35% opacity instead of hidden, brightening on hover so users can see they're clickable - [+] dropdown now shows all rApps including already-open ones - Already-open rApps shown dimmed with a cyan dot indicator - Clicking an open rApp surfaces it (switches tab) instead of duplicating Co-Authored-By: Claude Opus 4.6 --- shared/components/rstack-tab-bar.ts | 64 +++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/shared/components/rstack-tab-bar.ts b/shared/components/rstack-tab-bar.ts index dd3bdb8..310e981 100644 --- a/shared/components/rstack-tab-bar.ts +++ b/shared/components/rstack-tab-bar.ts @@ -282,21 +282,20 @@ export class RStackTabBar extends HTMLElement { const existingModuleIds = new Set(this.#layers.map(l => l.moduleId)); // Use server module list if available, fall back to MODULE_BADGES keys - const availableModules: Array<{ id: string; name: string; icon: string; description: string }> = + const allModules: Array<{ id: string; name: string; icon: string; description: string }> = this.#modules.length > 0 - ? this.#modules.filter(m => !existingModuleIds.has(m.id)) + ? this.#modules : Object.keys(MODULE_BADGES) - .filter(id => !existingModuleIds.has(id)) .map(id => ({ id, name: id, icon: "", description: "" })); - if (availableModules.length === 0) { - return `
All rApps added
`; + if (allModules.length === 0) { + return `
No rApps available
`; } // Group by category - const groups = new Map(); - const uncategorized: typeof availableModules = []; - for (const m of availableModules) { + const groups = new Map(); + const uncategorized: typeof allModules = []; + for (const m of allModules) { const cat = MODULE_CATEGORIES[m.id]; if (cat) { if (!groups.has(cat)) groups.set(cat, []); @@ -311,29 +310,33 @@ export class RStackTabBar extends HTMLElement { const items = groups.get(cat); if (!items || items.length === 0) continue; html += `
${cat}
`; - html += items.map(m => this.#renderAddMenuItem(m)).join(""); + html += items.map(m => this.#renderAddMenuItem(m, existingModuleIds.has(m.id))).join(""); } if (uncategorized.length > 0) { html += `
Other
`; - html += uncategorized.map(m => this.#renderAddMenuItem(m)).join(""); + html += uncategorized.map(m => this.#renderAddMenuItem(m, existingModuleIds.has(m.id))).join(""); } return `
${html}
`; } - #renderAddMenuItem(m: { id: string; name: string; icon: string; description: string }): string { + #renderAddMenuItem(m: { id: string; name: string; icon: string; description: string }, isOpen: boolean): string { const badge = MODULE_BADGES[m.id]; const badgeHtml = badge ? `${badge.badge}` : `${m.icon}`; + const openClass = isOpen ? " add-menu-item--open" : ""; + const openIndicator = isOpen ? `` : ""; + return ` - `; } @@ -615,11 +618,25 @@ export class RStackTabBar extends HTMLElement { item.addEventListener("click", (e) => { e.stopPropagation(); const moduleId = item.dataset.addModule!; + const isOpen = item.dataset.moduleOpen === "true"; this.#addMenuOpen = false; - this.dispatchEvent(new CustomEvent("layer-add", { - detail: { moduleId }, - bubbles: true, - })); + + if (isOpen) { + // Surface existing tab instead of adding a duplicate + const existing = this.#layers.find(l => l.moduleId === moduleId); + if (existing) { + this.setAttribute("active", existing.id); + this.dispatchEvent(new CustomEvent("layer-switch", { + detail: { layerId: existing.id, moduleId }, + bubbles: true, + })); + } + } else { + this.dispatchEvent(new CustomEvent("layer-add", { + detail: { moduleId }, + bubbles: true, + })); + } }); }); @@ -984,12 +1001,12 @@ const STYLES = ` color: inherit; font-size: 0.85rem; cursor: pointer; - opacity: 0; + opacity: 0.35; transition: opacity 0.15s, background 0.15s; padding: 0; line-height: 1; } -.tab:hover .tab-close { opacity: 0.5; } +.tab:hover .tab-close { opacity: 0.6; } .tab-close:hover { opacity: 1 !important; background: rgba(239,68,68,0.2); color: #ef4444; } /* ── Drag states ── */ @@ -1128,6 +1145,17 @@ const STYLES = ` text-overflow: ellipsis; } +.add-menu-item--open { + opacity: 0.55; +} +.add-menu-open-dot { + margin-left: auto; + font-size: 0.5rem; + color: #22d3ee; + flex-shrink: 0; + padding-left: 6px; +} + .add-menu-empty { padding: 12px; text-align: center;