refactor(shell): move minimize chevron from tab row to subnav bar

The minimize toggle now lives at the right edge of each rApp's subnav
bar instead of the tab row. When minimized, both header and tab row
slide up and the subnav becomes a thin fixed strip with just the
restore chevron. Every rApp already has a subnav (except canvas which
has its own chrome).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-31 10:23:42 -07:00
parent f6767ca841
commit 7888596623
2 changed files with 27 additions and 38 deletions

View File

@ -319,7 +319,6 @@ 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>
@ -1543,7 +1542,6 @@ 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">
@ -1566,15 +1564,6 @@ 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)}';
@ -1982,6 +1971,7 @@ const INFO_PANEL_CSS = `
const SUBNAV_CSS = `
.rapp-subnav {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 1rem;
overflow-x: auto;
@ -2056,11 +2046,10 @@ function renderModuleSubNav(moduleId: string, spaceSlug: string, modules: Module
}
}
// Don't render if no sub-paths
if (items.length === 0) return '';
const base = (isSubdomain ?? IS_PRODUCTION) ? `/${escapeAttr(moduleId)}` : `/${escapeAttr(spaceSlug)}/${escapeAttr(moduleId)}`;
const minimizeBtn = `<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>`;
const pills = [
`<a class="rapp-nav-pill" href="${base}" data-subnav-root>${escapeHtml(mod.name)}</a>`,
...items.map(it =>
@ -2069,7 +2058,7 @@ function renderModuleSubNav(moduleId: string, spaceSlug: string, modules: Module
...(mod.externalApp ? [`<a class="rapp-nav-pill rapp-nav-pill--external" href="${escapeAttr(mod.externalApp.url)}" target="_blank" rel="noopener">Open ${escapeHtml(mod.externalApp.name)} &#8599;</a>`] : []),
];
return `<nav class="rapp-subnav" id="rapp-subnav">${pills.join('')}</nav>
return `<nav class="rapp-subnav" id="rapp-subnav">${pills.join('')}${minimizeBtn}</nav>
<script>(function(){var ps=document.querySelectorAll('#rapp-subnav .rapp-nav-pill'),p=location.pathname.replace(/\\/$/,'');var matched=false;ps.forEach(function(a){var h=a.getAttribute('href');if(h&&h===p){a.classList.add('rapp-nav-pill--active');matched=true}else if(h&&p.startsWith(h+'/')&&!a.hasAttribute('data-subnav-root')){a.classList.add('rapp-nav-pill--active');matched=true}});if(!matched){var root=document.querySelector('[data-subnav-root]');if(root)root.classList.add('rapp-nav-pill--active')}})()</script>`;
}

View File

@ -488,13 +488,15 @@ body.rstack-sidebar-open #toolbar {
.hover-reveal:active { opacity: 1 !important; }
}
/* ── Header minimize toggle ── */
/* ── Header minimize toggle (lives in .rapp-subnav) ── */
.rapp-minimize-btn {
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px; padding: 0; margin-right: 2px;
width: 24px; height: 24px; padding: 0;
margin-left: auto; /* push to right edge of subnav */
flex-shrink: 0;
background: none; border: 1px solid transparent; border-radius: 6px;
color: var(--rs-text-muted); cursor: pointer; flex-shrink: 0;
color: var(--rs-text-muted); cursor: pointer;
transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.rapp-minimize-btn:hover {
@ -507,25 +509,29 @@ body.rspace-headers-minimized .rapp-minimize-btn svg {
transform: rotate(180deg);
}
/* Minimized state: slide header up, shrink tab row to thin restore strip */
/* Minimized state: hide header + tab row, subnav becomes 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;
transform: translateY(-100%);
pointer-events: none;
}
body.rspace-headers-minimized #app {
padding-top: 24px;
padding-top: 0;
}
body.rspace-headers-minimized #app.canvas-layout {
padding-top: 24px;
padding-top: 0;
}
body.rspace-headers-minimized .rapp-subnav {
top: 25px;
position: fixed;
top: 0; left: 0; right: 0;
z-index: 9997;
height: 28px;
padding: 2px 8px;
overflow: hidden;
justify-content: flex-end;
}
/* Smooth transitions for header minimize/restore */
@ -533,33 +539,27 @@ body.rspace-headers-minimized .rapp-subnav {
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;
transition: margin-left 0.25s ease, left 0.25s ease, transform 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;
max-height: 0;
overflow: hidden;
transform: none;
}
body.rspace-headers-minimized .rapp-subnav {
top: auto;
position: sticky;
top: 0;
}
.rapp-minimize-btn { display: flex; } /* keep visible on mobile unlike info btn */
}