diff --git a/server/shell.ts b/server/shell.ts
index c27d3b1..2a4e21c 100644
--- a/server/shell.ts
+++ b/server/shell.ts
@@ -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 {
- ${renderModuleSubNav(moduleId, spaceSlug, visibleModules)}
- ${opts.tabs ? renderTabBar(opts.tabs) : ''}
+ ${renderModuleSubNav(moduleId, spaceSlug, visibleModules)}
+ ${opts.tabs ? renderTabBar(opts.tabs, opts.activeTab, opts.tabBasePath || `/${escapeAttr(spaceSlug)}/${escapeAttr(moduleId)}`) : ''}
${body}
@@ -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 = [
- `
${escapeHtml(mod.name)}`,
+ `
${escapeHtml(mod.name)}`,
...items.map(it =>
- `
${it.icon ? escapeHtml(it.icon) + ' ' : ''}${escapeHtml(it.label)}`
+ `
${it.icon ? escapeHtml(it.icon) + ' ' : ''}${escapeHtml(it.label)}`
),
];
return `
-`;
+`;
}
-// ── 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 =>
- `
`
- ).join('');
+ const active = activeTab || tabs[0]?.id || '';
+ const base = basePath;
+
+ const pills = tabs.map(t => {
+ const isActive = t.id === active;
+ return `
${t.icon ? escapeHtml(t.icon) + ' ' : ''}${escapeHtml(t.label)}`;
+ }).join('');
return `
`;
}