feat(shell): add header minimize toggle for more viewport space

Adds a [^] chevron button at the right end of the tab row that collapses
all header bars into a thin 24px restore strip. State persists via
localStorage across page reloads. Works on both desktop (fixed) and
mobile (sticky) layouts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-30 23:35:12 -07:00
parent 7fec0cb699
commit 36ae954da4
2 changed files with 95 additions and 2 deletions

View File

@ -318,6 +318,7 @@ export function renderShell(opts: ShellOptions): string {
<rstack-tab-bar space="${escapeAttr(spaceSlug)}" active="" view-mode="flat">
<rstack-collab-overlay module-id="${escapeAttr(moduleId)}" space="${escapeAttr(spaceSlug)}"></rstack-collab-overlay>
<button class="rapp-info-btn" id="rapp-info-btn" title="About this rApp"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></button>
<button class="rapp-minimize-btn" id="header-minimize-btn" title="Minimize headers"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></button>
</rstack-tab-bar>
</div>
<div id="rapp-info-overlay" class="rapp-info-overlay" style="display:none"></div>
@ -352,6 +353,14 @@ export function renderShell(opts: ShellOptions): string {
window.__rspaceInstallPrompt = () => { e.prompt(); return e.userChoice; };
window.dispatchEvent(new CustomEvent("rspace-install-available"));
});
// ── Header minimize toggle ──
if (localStorage.getItem('rspace_headers_minimized') === '1') {
document.body.classList.add('rspace-headers-minimized');
}
document.getElementById('header-minimize-btn')?.addEventListener('click', () => {
const minimized = document.body.classList.toggle('rspace-headers-minimized');
localStorage.setItem('rspace_headers_minimized', minimized ? '1' : '0');
});
// ── Settings panel toggle (close history first) ──
document.getElementById('settings-btn')?.addEventListener('click', () => {
document.querySelector('rstack-history-panel')?.close();
@ -1533,6 +1542,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
<div class="rstack-tab-row">
<rstack-tab-bar space="${escapeAttr(spaceSlug)}" active="" view-mode="flat">
<rstack-collab-overlay module-id="${escapeAttr(moduleId)}" space="${escapeAttr(spaceSlug)}" mode="badge-only"></rstack-collab-overlay>
<button class="rapp-minimize-btn" id="header-minimize-btn" title="Minimize headers"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></button>
</rstack-tab-bar>
</div>
<div class="rspace-iframe-wrap">
@ -1555,6 +1565,15 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
import '/shell.js';
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
// ── Header minimize toggle ──
if (localStorage.getItem('rspace_headers_minimized') === '1') {
document.body.classList.add('rspace-headers-minimized');
}
document.getElementById('header-minimize-btn')?.addEventListener('click', () => {
const minimized = document.body.classList.toggle('rspace-headers-minimized');
localStorage.setItem('rspace_headers_minimized', minimized ? '1' : '0');
});
const tabBar = document.querySelector('rstack-tab-bar');
const spaceSlug = '${escapeAttr(spaceSlug)}';
const currentModuleId = '${escapeAttr(moduleId)}';

View File

@ -326,8 +326,6 @@ body {
/* ── Sidebar push offsets ── */
.rstack-tab-row,
#app,
rstack-user-dashboard,
.rspace-iframe-wrap,
#toolbar {
@ -489,3 +487,79 @@ body.rstack-sidebar-open #toolbar {
.hover-reveal { opacity: 0.5 !important; }
.hover-reveal:active { opacity: 1 !important; }
}
/* ── Header minimize toggle ── */
.rapp-minimize-btn {
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px; padding: 0; margin-right: 2px;
background: none; border: 1px solid transparent; border-radius: 6px;
color: var(--rs-text-muted); cursor: pointer; flex-shrink: 0;
transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.rapp-minimize-btn:hover {
color: var(--rs-text-primary); background: var(--rs-bg-hover); border-color: var(--rs-border);
}
.rapp-minimize-btn svg {
transition: transform 0.25s ease;
}
body.rspace-headers-minimized .rapp-minimize-btn svg {
transform: rotate(180deg);
}
/* Minimized state: slide header up, shrink tab row to thin restore strip */
body.rspace-headers-minimized .rstack-header {
transform: translateY(-100%);
pointer-events: none;
}
body.rspace-headers-minimized .rstack-tab-row {
top: 0;
height: 24px;
overflow: hidden;
justify-content: flex-end;
}
body.rspace-headers-minimized #app {
padding-top: 24px;
}
body.rspace-headers-minimized #app.canvas-layout {
padding-top: 24px;
}
body.rspace-headers-minimized .rapp-subnav {
top: 25px;
}
/* Smooth transitions for header minimize/restore */
.rstack-header {
transition: transform 0.25s ease;
}
.rstack-tab-row {
transition: margin-left 0.25s ease, left 0.25s ease, top 0.25s ease, height 0.25s ease;
}
#app {
transition: margin-left 0.25s ease, left 0.25s ease, padding-top 0.25s ease;
}
.rapp-subnav {
transition: top 0.25s ease;
}
/* Mobile: minimized state adjustments (sticky, not fixed) */
@media (max-width: 640px) {
body.rspace-headers-minimized .rstack-header {
transform: translateY(-100%);
max-height: 0;
overflow: hidden;
padding: 0;
border: 0;
}
body.rspace-headers-minimized .rstack-tab-row {
top: auto;
height: 24px;
}
body.rspace-headers-minimized #app {
padding-top: 0;
}
body.rspace-headers-minimized .rapp-subnav {
top: auto;
}
.rapp-minimize-btn { display: flex; } /* keep visible on mobile unlike info btn */
}