From 8321a9015ac5a3b3aa5fdbd23c8b6154dcb5cb93 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 6 Apr 2026 14:05:38 -0400 Subject: [PATCH] revert: restore app switcher to left sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- shared/components/rstack-app-switcher.ts | 144 +++++++++++------------ website/public/shell.css | 34 +++++- 2 files changed, 104 insertions(+), 74 deletions(-) diff --git a/shared/components/rstack-app-switcher.ts b/shared/components/rstack-app-switcher.ts index 9f41ad6..7a2bfe8 100644 --- a/shared/components/rstack-app-switcher.ts +++ b/shared/components/rstack-app-switcher.ts @@ -1,5 +1,5 @@ /** - * — Bottom-right FAB that expands upward to switch between rSpace modules. + * — Collapsible left sidebar to switch between rSpace modules. * * Attributes: * current — the active module ID (highlighted) @@ -170,6 +170,8 @@ export class RStackAppSwitcher extends HTMLElement { } disconnectedCallback() { + // Clean up body class and outside-click listener on removal + document.body.classList.remove("rstack-sidebar-open"); if (this.#outsideClickHandler) { document.removeEventListener("pointerdown", this.#outsideClickHandler); this.#outsideClickHandler = null; @@ -353,16 +355,19 @@ export class RStackAppSwitcher extends HTMLElement { const currentMod = this.#modules.find((m) => m.id === current); const badgeInfo = currentMod ? MODULE_BADGES[currentMod.id] : null; - const fabContent = badgeInfo - ? `${badgeInfo.badge}` - : `r✨`; + const triggerContent = badgeInfo + ? `${badgeInfo.badge} ${currentMod!.name}` + : currentMod + ? `${currentMod.icon} ${currentMod.name}` + : `r✨ rSpace`; this.#shadow.innerHTML = `
- -
- @@ -370,23 +375,29 @@ export class RStackAppSwitcher extends HTMLElement { const trigger = this.#shadow.getElementById("trigger")!; const sidebar = this.#shadow.getElementById("sidebar")!; + const collapse = this.#shadow.getElementById("collapse")!; 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) { sidebar.classList.add("open"); backdrop.classList.add("visible"); + document.body.classList.add("rstack-sidebar-open"); + } else { + document.body.classList.remove("rstack-sidebar-open"); } const open = () => { this.#isOpen = true; sidebar.classList.add("open"); backdrop.classList.add("visible"); + document.body.classList.add("rstack-sidebar-open"); }; const close = () => { this.#isOpen = false; sidebar.classList.remove("open"); backdrop.classList.remove("visible"); + document.body.classList.remove("rstack-sidebar-open"); }; const toggle = () => { if (this.#isOpen) close(); else open(); @@ -397,6 +408,7 @@ export class RStackAppSwitcher extends HTMLElement { toggle(); }); + collapse.addEventListener("click", () => close()); backdrop.addEventListener("click", () => close()); // Close sidebar when clicking outside (on main content) @@ -577,75 +589,75 @@ const STYLES = ` .switcher { position: relative; } -/* ── FAB trigger (bottom-right) ── */ -.fab-trigger { - position: fixed; - bottom: 24px; right: 24px; - width: 48px; height: 48px; - border-radius: 50%; - border: none; - background: var(--rs-bg-surface); - 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; +.trigger { + display: flex; align-items: center; gap: 6px; + padding: 6px 14px; border-radius: 8px; border: none; + font-size: 0.9rem; font-weight: 600; cursor: pointer; + transition: background 0.15s; + background: var(--rs-btn-secondary-bg); color: var(--rs-text-primary); + white-space: nowrap; min-width: 0; flex-shrink: 1; + overflow: hidden; text-overflow: ellipsis; } -.fab-trigger: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); } +.trigger:hover, .trigger:active { background: var(--rs-bg-hover); } -@keyframes fab-pulse { - 0%, 100% { box-shadow: 0 2px 12px rgba(0,0,0,0.25), 0 0 0 1px var(--rs-border); } - 50% { box-shadow: 0 2px 12px rgba(0,0,0,0.25), 0 0 0 3px rgba(99,102,241,0.25); } +.trigger-badge { + display: inline-flex; align-items: center; justify-content: center; + 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; } - -.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 { +.trigger-badge.rstack-gradient { background: linear-gradient(135deg, #67e8f9, #c4b5fd, #fda4af); } -/* ── Backdrop ── */ -.panel-backdrop { +.caret { font-size: 0.7em; opacity: 0.6; } + +/* ── Sidebar backdrop (mobile) ── */ +.sidebar-backdrop { display: none; position: fixed; inset: 0; z-index: 10000; background: rgba(0,0,0,0.4); opacity: 0; transition: opacity 0.25s ease; -webkit-tap-highlight-color: transparent; } @media (max-width: 640px) { - .panel-backdrop.visible { display: block; opacity: 1; } + .sidebar-backdrop.visible { display: block; opacity: 1; } } -/* ── Upward panel (above FAB) ── */ -.panel { +/* ── Sidebar panel ── */ +.sidebar { position: fixed; - bottom: 80px; right: 24px; + top: 56px; left: 0; bottom: 0; width: 280px; - max-height: calc(100vh - 120px); overflow-y: auto; touch-action: pan-y; z-index: 10001; background: var(--rs-bg-surface); - border: 1px solid var(--rs-border); - border-radius: 12px; - box-shadow: 0 8px 32px rgba(0,0,0,0.3); - opacity: 0; - transform: translateY(20px); - pointer-events: none; - transition: opacity 0.2s ease, transform 0.2s ease; + border-right: 1px solid var(--rs-border); + box-shadow: var(--rs-shadow-lg); + transform: translateX(-100%); + transition: transform 0.25s ease; } -.panel.open { - opacity: 1; - transform: translateY(0); - pointer-events: auto; +.sidebar.open { + transform: translateX(0); +} + +/* 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 */ @@ -654,7 +666,6 @@ a.rstack-header { padding: 12px 14px; border-bottom: 1px solid var(--rs-border-subtle); text-decoration: none; color: inherit; cursor: pointer; transition: background 0.12s; - border-radius: 12px 12px 0 0; } a.rstack-header:hover, a.rstack-header:active { background: var(--rs-bg-hover); } .rstack-badge { @@ -672,7 +683,6 @@ a.rstack-header:hover, a.rstack-header:active { background: var(--rs-bg-hover); .rstack-footer { padding: 10px 14px; text-align: center; border-top: 1px solid var(--rs-border-subtle); - border-radius: 0 0 12px 12px; } .rstack-footer a { 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); } -/* ── Mobile (< 640px) ── */ +/* Mobile: sidebar overlays instead of pushing */ @media (max-width: 640px) { - .fab-trigger { - 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); - } + .sidebar { box-shadow: 4px 0 20px rgba(0,0,0,0.3); } } `; diff --git a/website/public/shell.css b/website/public/shell.css index 52c632d..6c0b02f 100644 --- a/website/public/shell.css +++ b/website/public/shell.css @@ -422,7 +422,33 @@ body.rspace-banner-visible #app { 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 ── */ @@ -503,6 +529,12 @@ body.rspace-banner-visible #app { /* Hide settings/history on mobile — accessible via identity dropdown */ .rstack-header__settings-btn, .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; } }