diff --git a/shared/components/rstack-app-switcher.ts b/shared/components/rstack-app-switcher.ts index 7a2bfe8..9f41ad6 100644 --- a/shared/components/rstack-app-switcher.ts +++ b/shared/components/rstack-app-switcher.ts @@ -1,5 +1,5 @@ /** - * — Collapsible left sidebar to switch between rSpace modules. + * — Bottom-right FAB that expands upward to switch between rSpace modules. * * Attributes: * current — the active module ID (highlighted) @@ -170,8 +170,6 @@ 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; @@ -355,19 +353,16 @@ export class RStackAppSwitcher extends HTMLElement { const currentMod = this.#modules.find((m) => m.id === current); const badgeInfo = currentMod ? MODULE_BADGES[currentMod.id] : null; - const triggerContent = badgeInfo - ? `${badgeInfo.badge} ${currentMod!.name}` - : currentMod - ? `${currentMod.icon} ${currentMod.name}` - : `r✨ rSpace`; + const fabContent = badgeInfo + ? `${badgeInfo.badge}` + : `r✨`; this.#shadow.innerHTML = `
- - - @@ -375,29 +370,23 @@ 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 sidebar DOM with tracked state (survives re-renders) + // Sync panel 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(); @@ -408,7 +397,6 @@ export class RStackAppSwitcher extends HTMLElement { toggle(); }); - collapse.addEventListener("click", () => close()); backdrop.addEventListener("click", () => close()); // Close sidebar when clicking outside (on main content) @@ -589,75 +577,75 @@ const STYLES = ` .switcher { position: relative; } -.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 (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:hover, .trigger:active { background: var(--rs-bg-hover); } +.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-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; +@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.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); } -.caret { font-size: 0.7em; opacity: 0.6; } - -/* ── Sidebar backdrop (mobile) ── */ -.sidebar-backdrop { +/* ── Backdrop ── */ +.panel-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) { - .sidebar-backdrop.visible { display: block; opacity: 1; } + .panel-backdrop.visible { display: block; opacity: 1; } } -/* ── Sidebar panel ── */ -.sidebar { +/* ── Upward panel (above FAB) ── */ +.panel { position: fixed; - top: 56px; left: 0; bottom: 0; + bottom: 80px; right: 24px; width: 280px; + max-height: calc(100vh - 120px); overflow-y: auto; touch-action: pan-y; z-index: 10001; background: var(--rs-bg-surface); - border-right: 1px solid var(--rs-border); - box-shadow: var(--rs-shadow-lg); - transform: translateX(-100%); - transition: transform 0.25s ease; -} -.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; + 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; } -.collapse-btn:hover, .collapse-btn:active { - background: var(--rs-bg-hover); - color: var(--rs-text-primary); +.panel.open { + opacity: 1; + transform: translateY(0); + pointer-events: auto; } /* rStack header */ @@ -666,6 +654,7 @@ 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 { @@ -683,6 +672,7 @@ 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; @@ -789,8 +779,20 @@ 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: sidebar overlays instead of pushing */ +/* ── Mobile (< 640px) ── */ @media (max-width: 640px) { - .sidebar { box-shadow: 4px 0 20px rgba(0,0,0,0.3); } + .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); + } } `; diff --git a/website/public/shell.css b/website/public/shell.css index 6c0b02f..52c632d 100644 --- a/website/public/shell.css +++ b/website/public/shell.css @@ -422,33 +422,7 @@ body.rspace-banner-visible #app { opacity: 0.88; } -/* ── 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 */ -} +/* (sidebar push offsets removed — app switcher is now a floating FAB panel) */ /* ── Mobile adjustments ── */ @@ -529,12 +503,6 @@ body.rstack-sidebar-open #toolbar { /* 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; } }