feat: always-visible tab close buttons + rApp dropdown shows open apps

- 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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-28 17:08:45 -08:00
parent b0eebc4cbc
commit 29d49c7b26
1 changed files with 46 additions and 18 deletions

View File

@ -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 `<div class="add-menu" id="add-menu"><div class="add-menu-empty">All rApps added</div></div>`;
if (allModules.length === 0) {
return `<div class="add-menu" id="add-menu"><div class="add-menu-empty">No rApps available</div></div>`;
}
// Group by category
const groups = new Map<string, typeof availableModules>();
const uncategorized: typeof availableModules = [];
for (const m of availableModules) {
const groups = new Map<string, typeof allModules>();
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 += `<div class="add-menu-category">${cat}</div>`;
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 += `<div class="add-menu-category">Other</div>`;
html += uncategorized.map(m => this.#renderAddMenuItem(m)).join("");
html += uncategorized.map(m => this.#renderAddMenuItem(m, existingModuleIds.has(m.id))).join("");
}
return `<div class="add-menu" id="add-menu">${html}</div>`;
}
#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
? `<span class="add-menu-badge" style="background:${badge.color}">${badge.badge}</span>`
: `<span class="add-menu-icon">${m.icon}</span>`;
const openClass = isOpen ? " add-menu-item--open" : "";
const openIndicator = isOpen ? `<span class="add-menu-open-dot" title="Already open">●</span>` : "";
return `
<button class="add-menu-item" data-add-module="${m.id}">
<button class="add-menu-item${openClass}" data-add-module="${m.id}" data-module-open="${isOpen}">
${badgeHtml}
<div class="add-menu-text">
<span class="add-menu-name">${m.name} <span class="add-menu-emoji">${m.icon}</span></span>
${m.description ? `<span class="add-menu-desc">${m.description}</span>` : ""}
</div>
${openIndicator}
</button>
`;
}
@ -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;