fix(shell): move subnav/tabbar inside <main> and unify pill styles
Subnav and tabbar were rendered outside <main id="app">, causing them to be hidden behind the fixed header. Move them inside <main>, add sticky positioning (top: 92px), and consolidate pill CSS into a shared .rapp-nav-pill class. Also refactors tabbar to use URL subpaths instead of ?tab= query params. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a658b20fb1
commit
b60c0f565e
|
|
@ -63,8 +63,12 @@ export interface ShellOptions {
|
|||
enabledModules?: string[] | null;
|
||||
/** Whether this space has client-side encryption enabled */
|
||||
spaceEncrypted?: boolean;
|
||||
/** Optional tab bar rendered below the subnav. Uses ?tab= query params. */
|
||||
/** Optional tab bar rendered below the subnav. */
|
||||
tabs?: Array<{ id: string; label: string; icon?: string }>;
|
||||
/** Active tab ID (matched from URL path by server). First tab if omitted. */
|
||||
activeTab?: string;
|
||||
/** Base path for tab links (default: /{space}/{moduleId}). Set to e.g. "/{space}/rnetwork/crm" for sub-pages. */
|
||||
tabBasePath?: string;
|
||||
}
|
||||
|
||||
export function renderShell(opts: ShellOptions): string {
|
||||
|
|
@ -169,8 +173,6 @@ export function renderShell(opts: ShellOptions): string {
|
|||
<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>
|
||||
</rstack-tab-bar>
|
||||
</div>
|
||||
${renderModuleSubNav(moduleId, spaceSlug, visibleModules)}
|
||||
${opts.tabs ? renderTabBar(opts.tabs) : ''}
|
||||
<div id="rapp-info-panel" class="rapp-info-panel" style="display:none">
|
||||
<div class="rapp-info-panel__header">
|
||||
<span class="rapp-info-panel__title">About</span>
|
||||
|
|
@ -180,6 +182,8 @@ export function renderShell(opts: ShellOptions): string {
|
|||
</div>
|
||||
<rstack-user-dashboard space="${escapeAttr(spaceSlug)}" style="display:none"></rstack-user-dashboard>
|
||||
<main id="app"${moduleId === "rspace" ? ' class="canvas-layout"' : ''}>
|
||||
${renderModuleSubNav(moduleId, spaceSlug, visibleModules)}
|
||||
${opts.tabs ? renderTabBar(opts.tabs, opts.activeTab, opts.tabBasePath || `/${escapeAttr(spaceSlug)}/${escapeAttr(moduleId)}`) : ''}
|
||||
${body}
|
||||
</main>
|
||||
<rstack-collab-overlay module-id="${escapeAttr(moduleId)}" space="${escapeAttr(spaceSlug)}"></rstack-collab-overlay>
|
||||
|
|
@ -1261,9 +1265,15 @@ const SUBNAV_CSS = `
|
|||
border-bottom: 1px solid var(--rs-border);
|
||||
background: var(--rs-bg-surface);
|
||||
scrollbar-width: none;
|
||||
position: sticky;
|
||||
top: 92px;
|
||||
z-index: 100;
|
||||
}
|
||||
.rapp-subnav::-webkit-scrollbar { display: none; }
|
||||
.rapp-subnav__pill {
|
||||
@media (max-width: 640px) {
|
||||
.rapp-subnav { top: 0; }
|
||||
}
|
||||
.rapp-nav-pill {
|
||||
padding: 0.3125rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8125rem;
|
||||
|
|
@ -1273,8 +1283,8 @@ const SUBNAV_CSS = `
|
|||
border: 1px solid transparent;
|
||||
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
||||
}
|
||||
.rapp-subnav__pill:hover { color: var(--rs-text-primary); background: var(--rs-bg-surface-raised); }
|
||||
.rapp-subnav__pill--active {
|
||||
.rapp-nav-pill:hover { color: var(--rs-text-primary); background: var(--rs-bg-surface-raised); }
|
||||
.rapp-nav-pill--active {
|
||||
color: var(--rs-text-primary);
|
||||
background: var(--rs-bg-surface-raised);
|
||||
border-color: var(--rs-border);
|
||||
|
|
@ -1310,77 +1320,52 @@ function renderModuleSubNav(moduleId: string, spaceSlug: string, modules: Module
|
|||
const base = `/${escapeAttr(spaceSlug)}/${escapeAttr(moduleId)}`;
|
||||
|
||||
const pills = [
|
||||
`<a class="rapp-subnav__pill" href="${base}" data-subnav-root>${escapeHtml(mod.name)}</a>`,
|
||||
`<a class="rapp-nav-pill" href="${base}" data-subnav-root>${escapeHtml(mod.name)}</a>`,
|
||||
...items.map(it =>
|
||||
`<a class="rapp-subnav__pill" href="${base}/${escapeAttr(it.path)}">${it.icon ? escapeHtml(it.icon) + ' ' : ''}${escapeHtml(it.label)}</a>`
|
||||
`<a class="rapp-nav-pill" href="${base}/${escapeAttr(it.path)}">${it.icon ? escapeHtml(it.icon) + ' ' : ''}${escapeHtml(it.label)}</a>`
|
||||
),
|
||||
];
|
||||
|
||||
return `<nav class="rapp-subnav" id="rapp-subnav">${pills.join('')}</nav>
|
||||
<script>(function(){var ps=document.querySelectorAll('.rapp-subnav__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-subnav__pill--active');matched=true}else if(h&&p.startsWith(h+'/')&&!a.hasAttribute('data-subnav-root')){a.classList.add('rapp-subnav__pill--active');matched=true}});if(!matched){var root=document.querySelector('[data-subnav-root]');if(root)root.classList.add('rapp-subnav__pill--active')}})()</script>`;
|
||||
<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>`;
|
||||
}
|
||||
|
||||
// ── rApp tab bar (in-page tabs via ?tab= query params) ──
|
||||
// ── rApp tab bar (in-page tabs via URL subpaths) ──
|
||||
|
||||
const TABBAR_CSS = `
|
||||
.rapp-tabbar {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 1rem;
|
||||
padding: 0.375rem 1rem;
|
||||
overflow-x: auto;
|
||||
border-bottom: 1px solid var(--rs-border);
|
||||
background: var(--rs-bg-surface);
|
||||
scrollbar-width: none;
|
||||
position: sticky;
|
||||
top: 92px;
|
||||
z-index: 100;
|
||||
}
|
||||
.rapp-tabbar::-webkit-scrollbar { display: none; }
|
||||
.rapp-tabbar__pill {
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
color: var(--rs-text-muted);
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
font-family: inherit;
|
||||
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
||||
}
|
||||
.rapp-tabbar__pill:hover { color: var(--rs-text-primary); background: var(--rs-bg-surface-raised); }
|
||||
.rapp-tabbar__pill--active {
|
||||
color: var(--rs-text-primary);
|
||||
background: var(--rs-bg-surface-raised);
|
||||
border-color: var(--rs-border);
|
||||
font-weight: 500;
|
||||
@media (max-width: 640px) {
|
||||
.rapp-tabbar { top: 0; }
|
||||
}
|
||||
`;
|
||||
|
||||
function renderTabBar(tabs: Array<{ id: string; label: string; icon?: string }>): string {
|
||||
function renderTabBar(tabs: Array<{ id: string; label: string; icon?: string }>, activeTab: string | undefined, basePath: string): string {
|
||||
if (tabs.length === 0) return '';
|
||||
|
||||
const pills = tabs.map(t =>
|
||||
`<button class="rapp-tabbar__pill" data-tab-id="${escapeAttr(t.id)}">${t.icon ? escapeHtml(t.icon) + ' ' : ''}${escapeHtml(t.label)}</button>`
|
||||
).join('');
|
||||
const active = activeTab || tabs[0]?.id || '';
|
||||
const base = basePath;
|
||||
|
||||
const pills = tabs.map(t => {
|
||||
const isActive = t.id === active;
|
||||
return `<a class="rapp-nav-pill${isActive ? ' rapp-nav-pill--active' : ''}" href="${base}/${escapeAttr(t.id)}" data-tab-id="${escapeAttr(t.id)}">${t.icon ? escapeHtml(t.icon) + ' ' : ''}${escapeHtml(t.label)}</a>`;
|
||||
}).join('');
|
||||
|
||||
return `<nav class="rapp-tabbar" id="rapp-tabbar">${pills}</nav>
|
||||
<script>(function(){
|
||||
var pills = document.querySelectorAll('.rapp-tabbar__pill');
|
||||
var params = new URLSearchParams(location.search);
|
||||
var active = params.get('tab') || pills[0]?.dataset.tabId || '';
|
||||
pills.forEach(function(btn) {
|
||||
if (btn.dataset.tabId === active) btn.classList.add('rapp-tabbar__pill--active');
|
||||
btn.addEventListener('click', function() {
|
||||
var id = btn.dataset.tabId;
|
||||
pills.forEach(function(p) { p.classList.remove('rapp-tabbar__pill--active'); });
|
||||
btn.classList.add('rapp-tabbar__pill--active');
|
||||
var u = new URL(location.href);
|
||||
u.searchParams.set('tab', id);
|
||||
history.replaceState(null, '', u);
|
||||
document.dispatchEvent(new CustomEvent('rapp-tab-change', { detail: { tab: id } }));
|
||||
});
|
||||
});
|
||||
// Dispatch initial tab on load so components can read it
|
||||
document.dispatchEvent(new CustomEvent('rapp-tab-change', { detail: { tab: active } }));
|
||||
document.dispatchEvent(new CustomEvent('rapp-tab-change', { detail: { tab: '${escapeAttr(active)}' } }));
|
||||
})()</script>`;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue