revert: restore app switcher to left sidebar

Reverts 4420d9c — the FAB change was applied to the wrong component.
The intended target is the rSpace canvas toolbar, not the app switcher.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-06 14:05:38 -04:00
parent f26f7e14bd
commit 8321a9015a
2 changed files with 104 additions and 74 deletions

View File

@ -1,5 +1,5 @@
/** /**
* <rstack-app-switcher> Bottom-right FAB that expands upward to switch between rSpace modules. * <rstack-app-switcher> Collapsible left sidebar to switch between rSpace modules.
* *
* Attributes: * Attributes:
* current the active module ID (highlighted) * current the active module ID (highlighted)
@ -170,6 +170,8 @@ export class RStackAppSwitcher extends HTMLElement {
} }
disconnectedCallback() { disconnectedCallback() {
// Clean up body class and outside-click listener on removal
document.body.classList.remove("rstack-sidebar-open");
if (this.#outsideClickHandler) { if (this.#outsideClickHandler) {
document.removeEventListener("pointerdown", this.#outsideClickHandler); document.removeEventListener("pointerdown", this.#outsideClickHandler);
this.#outsideClickHandler = null; this.#outsideClickHandler = null;
@ -353,16 +355,19 @@ export class RStackAppSwitcher extends HTMLElement {
const currentMod = this.#modules.find((m) => m.id === current); const currentMod = this.#modules.find((m) => m.id === current);
const badgeInfo = currentMod ? MODULE_BADGES[currentMod.id] : null; const badgeInfo = currentMod ? MODULE_BADGES[currentMod.id] : null;
const fabContent = badgeInfo const triggerContent = badgeInfo
? `<span class="fab-badge" style="background:${badgeInfo.color}">${badgeInfo.badge}</span>` ? `<span class="trigger-badge" style="background:${badgeInfo.color}">${badgeInfo.badge}</span> ${currentMod!.name}`
: `<span class="fab-badge rstack-gradient">r✨</span>`; : currentMod
? `${currentMod.icon} ${currentMod.name}`
: `<span class="trigger-badge rstack-gradient">r✨</span> rSpace`;
this.#shadow.innerHTML = ` this.#shadow.innerHTML = `
<style>${STYLES}</style> <style>${STYLES}</style>
<div class="switcher"> <div class="switcher">
<button class="fab-trigger" id="trigger" title="Switch rApp">${fabContent}</button> <button class="trigger" id="trigger">${triggerContent} <span class="caret"></span></button>
<div class="panel-backdrop" id="backdrop"></div> <div class="sidebar-backdrop" id="backdrop"></div>
<div class="panel" id="sidebar"> <div class="sidebar" id="sidebar">
<button class="collapse-btn" id="collapse" title="Close sidebar"></button>
${this.#renderGroupedModules(current)} ${this.#renderGroupedModules(current)}
</div> </div>
</div> </div>
@ -370,23 +375,29 @@ export class RStackAppSwitcher extends HTMLElement {
const trigger = this.#shadow.getElementById("trigger")!; const trigger = this.#shadow.getElementById("trigger")!;
const sidebar = this.#shadow.getElementById("sidebar")!; const sidebar = this.#shadow.getElementById("sidebar")!;
const collapse = this.#shadow.getElementById("collapse")!;
const backdrop = this.#shadow.getElementById("backdrop")!; const backdrop = this.#shadow.getElementById("backdrop")!;
// Sync panel DOM with tracked state (survives re-renders) // Sync sidebar DOM with tracked state (survives re-renders)
if (this.#isOpen) { if (this.#isOpen) {
sidebar.classList.add("open"); sidebar.classList.add("open");
backdrop.classList.add("visible"); backdrop.classList.add("visible");
document.body.classList.add("rstack-sidebar-open");
} else {
document.body.classList.remove("rstack-sidebar-open");
} }
const open = () => { const open = () => {
this.#isOpen = true; this.#isOpen = true;
sidebar.classList.add("open"); sidebar.classList.add("open");
backdrop.classList.add("visible"); backdrop.classList.add("visible");
document.body.classList.add("rstack-sidebar-open");
}; };
const close = () => { const close = () => {
this.#isOpen = false; this.#isOpen = false;
sidebar.classList.remove("open"); sidebar.classList.remove("open");
backdrop.classList.remove("visible"); backdrop.classList.remove("visible");
document.body.classList.remove("rstack-sidebar-open");
}; };
const toggle = () => { const toggle = () => {
if (this.#isOpen) close(); else open(); if (this.#isOpen) close(); else open();
@ -397,6 +408,7 @@ export class RStackAppSwitcher extends HTMLElement {
toggle(); toggle();
}); });
collapse.addEventListener("click", () => close());
backdrop.addEventListener("click", () => close()); backdrop.addEventListener("click", () => close());
// Close sidebar when clicking outside (on main content) // Close sidebar when clicking outside (on main content)
@ -577,75 +589,75 @@ const STYLES = `
.switcher { position: relative; } .switcher { position: relative; }
/* ── FAB trigger (bottom-right) ── */ .trigger {
.fab-trigger { display: flex; align-items: center; gap: 6px;
position: fixed; padding: 6px 14px; border-radius: 8px; border: none;
bottom: 24px; right: 24px; font-size: 0.9rem; font-weight: 600; cursor: pointer;
width: 48px; height: 48px; transition: background 0.15s;
border-radius: 50%; background: var(--rs-btn-secondary-bg); color: var(--rs-text-primary);
border: none; white-space: nowrap; min-width: 0; flex-shrink: 1;
background: var(--rs-bg-surface); overflow: hidden; text-overflow: ellipsis;
box-shadow: 0 2px 12px rgba(0,0,0,0.25), 0 0 0 1px var(--rs-border);
cursor: pointer;
z-index: 10002;
display: flex; align-items: center; justify-content: center;
transition: transform 0.15s, box-shadow 0.15s;
animation: fab-pulse 3s ease-in-out infinite;
} }
.fab-trigger:hover { .trigger:hover, .trigger:active { background: var(--rs-bg-hover); }
transform: scale(1.08);
box-shadow: 0 4px 20px rgba(0,0,0,0.3), 0 0 0 2px var(--rs-accent, #6366f1);
animation: none;
}
.fab-trigger:active { transform: scale(0.95); }
@keyframes fab-pulse { .trigger-badge {
0%, 100% { box-shadow: 0 2px 12px rgba(0,0,0,0.25), 0 0 0 1px var(--rs-border); } display: inline-flex; align-items: center; justify-content: center;
50% { box-shadow: 0 2px 12px rgba(0,0,0,0.25), 0 0 0 3px rgba(99,102,241,0.25); } width: 22px; height: 22px; border-radius: 5px;
font-size: 0.65rem; font-weight: 900; color: #1e293b;
line-height: 1; flex-shrink: 0; white-space: nowrap;
} }
.trigger-badge.rstack-gradient {
.fab-badge {
display: flex; align-items: center; justify-content: center;
width: 32px; height: 32px; border-radius: 8px;
font-size: 0.8rem; font-weight: 900; color: #1e293b;
line-height: 1; white-space: nowrap;
}
.fab-badge.rstack-gradient {
background: linear-gradient(135deg, #67e8f9, #c4b5fd, #fda4af); background: linear-gradient(135deg, #67e8f9, #c4b5fd, #fda4af);
} }
/* ── Backdrop ── */ .caret { font-size: 0.7em; opacity: 0.6; }
.panel-backdrop {
/* ── Sidebar backdrop (mobile) ── */
.sidebar-backdrop {
display: none; position: fixed; inset: 0; z-index: 10000; display: none; position: fixed; inset: 0; z-index: 10000;
background: rgba(0,0,0,0.4); opacity: 0; transition: opacity 0.25s ease; background: rgba(0,0,0,0.4); opacity: 0; transition: opacity 0.25s ease;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.panel-backdrop.visible { display: block; opacity: 1; } .sidebar-backdrop.visible { display: block; opacity: 1; }
} }
/* ── Upward panel (above FAB) ── */ /* ── Sidebar panel ── */
.panel { .sidebar {
position: fixed; position: fixed;
bottom: 80px; right: 24px; top: 56px; left: 0; bottom: 0;
width: 280px; width: 280px;
max-height: calc(100vh - 120px);
overflow-y: auto; overflow-y: auto;
touch-action: pan-y; touch-action: pan-y;
z-index: 10001; z-index: 10001;
background: var(--rs-bg-surface); background: var(--rs-bg-surface);
border: 1px solid var(--rs-border); border-right: 1px solid var(--rs-border);
border-radius: 12px; box-shadow: var(--rs-shadow-lg);
box-shadow: 0 8px 32px rgba(0,0,0,0.3); transform: translateX(-100%);
opacity: 0; transition: transform 0.25s ease;
transform: translateY(20px);
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
} }
.panel.open { .sidebar.open {
opacity: 1; transform: translateX(0);
transform: translateY(0); }
pointer-events: auto;
/* Collapse button */
.collapse-btn {
position: absolute;
top: 8px; right: 8px;
width: 28px; height: 28px;
border-radius: 6px;
border: 1px solid var(--rs-border);
background: var(--rs-bg-surface);
color: var(--rs-text-secondary);
font-size: 1rem; line-height: 1;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: background 0.15s, color 0.15s;
z-index: 1;
}
.collapse-btn:hover, .collapse-btn:active {
background: var(--rs-bg-hover);
color: var(--rs-text-primary);
} }
/* rStack header */ /* rStack header */
@ -654,7 +666,6 @@ a.rstack-header {
padding: 12px 14px; border-bottom: 1px solid var(--rs-border-subtle); padding: 12px 14px; border-bottom: 1px solid var(--rs-border-subtle);
text-decoration: none; color: inherit; cursor: pointer; text-decoration: none; color: inherit; cursor: pointer;
transition: background 0.12s; transition: background 0.12s;
border-radius: 12px 12px 0 0;
} }
a.rstack-header:hover, a.rstack-header:active { background: var(--rs-bg-hover); } a.rstack-header:hover, a.rstack-header:active { background: var(--rs-bg-hover); }
.rstack-badge { .rstack-badge {
@ -672,7 +683,6 @@ a.rstack-header:hover, a.rstack-header:active { background: var(--rs-bg-hover);
.rstack-footer { .rstack-footer {
padding: 10px 14px; text-align: center; padding: 10px 14px; text-align: center;
border-top: 1px solid var(--rs-border-subtle); border-top: 1px solid var(--rs-border-subtle);
border-radius: 0 0 12px 12px;
} }
.rstack-footer a { .rstack-footer a {
font-size: 0.7rem; opacity: 0.4; text-decoration: none; color: inherit; font-size: 0.7rem; opacity: 0.4; text-decoration: none; color: inherit;
@ -779,20 +789,8 @@ a.rstack-header:hover, a.rstack-header:active { background: var(--rs-bg-hover);
} }
.catalog-btn--remove:hover { border-color: #ef4444; background: rgba(239,68,68,0.1); } .catalog-btn--remove:hover { border-color: #ef4444; background: rgba(239,68,68,0.1); }
/* ── Mobile (< 640px) ── */ /* Mobile: sidebar overlays instead of pushing */
@media (max-width: 640px) { @media (max-width: 640px) {
.fab-trigger { .sidebar { box-shadow: 4px 0 20px rgba(0,0,0,0.3); }
width: 40px; height: 40px;
bottom: 16px; right: 16px;
}
.fab-badge {
width: 26px; height: 26px;
font-size: 0.7rem;
}
.panel {
bottom: 64px; right: 16px;
width: calc(100vw - 32px);
max-height: calc(100vh - 100px);
}
} }
`; `;

View File

@ -422,7 +422,33 @@ body.rspace-banner-visible #app {
opacity: 0.88; opacity: 0.88;
} }
/* (sidebar push offsets removed — app switcher is now a floating FAB panel) */ /* ── Sidebar push offsets ── */
rstack-user-dashboard,
.rspace-iframe-wrap,
#toolbar {
transition: margin-left 0.25s ease, left 0.25s ease;
}
body.rstack-sidebar-open .rstack-tab-row {
left: 280px;
}
body.rstack-sidebar-open #app {
margin-left: 280px;
}
body.rstack-sidebar-open rstack-user-dashboard {
left: 280px;
}
body.rstack-sidebar-open .rspace-iframe-wrap {
left: 280px;
}
body.rstack-sidebar-open #toolbar {
left: 292px; /* 280px sidebar + 12px original margin */
}
/* ── Mobile adjustments ── */ /* ── Mobile adjustments ── */
@ -503,6 +529,12 @@ body.rspace-banner-visible #app {
/* Hide settings/history on mobile — accessible via identity dropdown */ /* Hide settings/history on mobile — accessible via identity dropdown */
.rstack-header__settings-btn, .rstack-header__settings-btn,
.rstack-header__history-btn { display: none; } .rstack-header__history-btn { display: none; }
/* Sidebar overlays on mobile — no push offsets */
body.rstack-sidebar-open .rstack-tab-row { left: 0; }
body.rstack-sidebar-open #app { margin-left: 0; }
body.rstack-sidebar-open rstack-user-dashboard { left: 0; }
body.rstack-sidebar-open .rspace-iframe-wrap { left: 0; }
body.rstack-sidebar-open #toolbar { left: 12px; }
.rapp-nav__btn, .rapp-nav__back { min-height: 36px; } .rapp-nav__btn, .rapp-nav__back { min-height: 36px; }
} }