Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m24s Details

This commit is contained in:
Jeff Emmett 2026-04-06 14:05:44 -04:00
commit cff10bee5d
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:
* 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
? `<span class="fab-badge" style="background:${badgeInfo.color}">${badgeInfo.badge}</span>`
: `<span class="fab-badge rstack-gradient">r✨</span>`;
const triggerContent = badgeInfo
? `<span class="trigger-badge" style="background:${badgeInfo.color}">${badgeInfo.badge}</span> ${currentMod!.name}`
: currentMod
? `${currentMod.icon} ${currentMod.name}`
: `<span class="trigger-badge rstack-gradient">r✨</span> rSpace`;
this.#shadow.innerHTML = `
<style>${STYLES}</style>
<div class="switcher">
<button class="fab-trigger" id="trigger" title="Switch rApp">${fabContent}</button>
<div class="panel-backdrop" id="backdrop"></div>
<div class="panel" id="sidebar">
<button class="trigger" id="trigger">${triggerContent} <span class="caret"></span></button>
<div class="sidebar-backdrop" id="backdrop"></div>
<div class="sidebar" id="sidebar">
<button class="collapse-btn" id="collapse" title="Close sidebar"></button>
${this.#renderGroupedModules(current)}
</div>
</div>
@ -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); }
}
`;

View File

@ -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; }
}