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:
parent
b0eebc4cbc
commit
29d49c7b26
|
|
@ -282,21 +282,20 @@ export class RStackTabBar extends HTMLElement {
|
||||||
const existingModuleIds = new Set(this.#layers.map(l => l.moduleId));
|
const existingModuleIds = new Set(this.#layers.map(l => l.moduleId));
|
||||||
|
|
||||||
// Use server module list if available, fall back to MODULE_BADGES keys
|
// 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.length > 0
|
||||||
? this.#modules.filter(m => !existingModuleIds.has(m.id))
|
? this.#modules
|
||||||
: Object.keys(MODULE_BADGES)
|
: Object.keys(MODULE_BADGES)
|
||||||
.filter(id => !existingModuleIds.has(id))
|
|
||||||
.map(id => ({ id, name: id, icon: "", description: "" }));
|
.map(id => ({ id, name: id, icon: "", description: "" }));
|
||||||
|
|
||||||
if (availableModules.length === 0) {
|
if (allModules.length === 0) {
|
||||||
return `<div class="add-menu" id="add-menu"><div class="add-menu-empty">All rApps added</div></div>`;
|
return `<div class="add-menu" id="add-menu"><div class="add-menu-empty">No rApps available</div></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group by category
|
// Group by category
|
||||||
const groups = new Map<string, typeof availableModules>();
|
const groups = new Map<string, typeof allModules>();
|
||||||
const uncategorized: typeof availableModules = [];
|
const uncategorized: typeof allModules = [];
|
||||||
for (const m of availableModules) {
|
for (const m of allModules) {
|
||||||
const cat = MODULE_CATEGORIES[m.id];
|
const cat = MODULE_CATEGORIES[m.id];
|
||||||
if (cat) {
|
if (cat) {
|
||||||
if (!groups.has(cat)) groups.set(cat, []);
|
if (!groups.has(cat)) groups.set(cat, []);
|
||||||
|
|
@ -311,29 +310,33 @@ export class RStackTabBar extends HTMLElement {
|
||||||
const items = groups.get(cat);
|
const items = groups.get(cat);
|
||||||
if (!items || items.length === 0) continue;
|
if (!items || items.length === 0) continue;
|
||||||
html += `<div class="add-menu-category">${cat}</div>`;
|
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) {
|
if (uncategorized.length > 0) {
|
||||||
html += `<div class="add-menu-category">Other</div>`;
|
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>`;
|
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 badge = MODULE_BADGES[m.id];
|
||||||
const badgeHtml = badge
|
const badgeHtml = badge
|
||||||
? `<span class="add-menu-badge" style="background:${badge.color}">${badge.badge}</span>`
|
? `<span class="add-menu-badge" style="background:${badge.color}">${badge.badge}</span>`
|
||||||
: `<span class="add-menu-icon">${m.icon}</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 `
|
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}
|
${badgeHtml}
|
||||||
<div class="add-menu-text">
|
<div class="add-menu-text">
|
||||||
<span class="add-menu-name">${m.name} <span class="add-menu-emoji">${m.icon}</span></span>
|
<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>` : ""}
|
${m.description ? `<span class="add-menu-desc">${m.description}</span>` : ""}
|
||||||
</div>
|
</div>
|
||||||
|
${openIndicator}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
@ -615,11 +618,25 @@ export class RStackTabBar extends HTMLElement {
|
||||||
item.addEventListener("click", (e) => {
|
item.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const moduleId = item.dataset.addModule!;
|
const moduleId = item.dataset.addModule!;
|
||||||
|
const isOpen = item.dataset.moduleOpen === "true";
|
||||||
this.#addMenuOpen = false;
|
this.#addMenuOpen = false;
|
||||||
this.dispatchEvent(new CustomEvent("layer-add", {
|
|
||||||
detail: { moduleId },
|
if (isOpen) {
|
||||||
bubbles: true,
|
// 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;
|
color: inherit;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0;
|
opacity: 0.35;
|
||||||
transition: opacity 0.15s, background 0.15s;
|
transition: opacity 0.15s, background 0.15s;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
line-height: 1;
|
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; }
|
.tab-close:hover { opacity: 1 !important; background: rgba(239,68,68,0.2); color: #ef4444; }
|
||||||
|
|
||||||
/* ── Drag states ── */
|
/* ── Drag states ── */
|
||||||
|
|
@ -1128,6 +1145,17 @@ const STYLES = `
|
||||||
text-overflow: ellipsis;
|
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 {
|
.add-menu-empty {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue