feat(guide): collapsible nav — Modules (per-app) / Platform / Technical sections
Restructure reader sidebar into 3 collapsible top-level sections: Modules at top (each product app collapsible with its module list), then Platform and Technical. Auto-expands the section/app of the active module; search expands all. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,15 @@
|
||||
let activeModuleKey = '';
|
||||
let activeTab = 0;
|
||||
let search = '';
|
||||
// collapse state: 'modules' | 'platform' | 'technical' | 'app:<name>'
|
||||
let open: Record<string, boolean> = { modules: true };
|
||||
function toggle(key: string) { open = { ...open, [key]: !open[key] }; }
|
||||
function expandFor(key: string) {
|
||||
const app = key.split('.')[0];
|
||||
if (app === 'platform') open = { ...open, platform: true };
|
||||
else if (app === 'technical') open = { ...open, technical: true };
|
||||
else open = { ...open, modules: true, ['app:' + app]: true };
|
||||
}
|
||||
|
||||
marked.setOptions({ gfm: true, breaks: false });
|
||||
const render = (md: string): string => marked.parse(md, { async: false }) as string;
|
||||
@@ -38,13 +47,21 @@
|
||||
}))
|
||||
.filter(a => a.modules.length > 0);
|
||||
|
||||
// group nav: product-app "Modules" section (all except platform/technical) + platform + technical
|
||||
$: searching = !!search.trim();
|
||||
$: productApps = filtered.filter(a => a.app !== 'platform' && a.app !== 'technical');
|
||||
$: platformApp = filtered.find(a => a.app === 'platform') ?? null;
|
||||
$: technicalApp = filtered.find(a => a.app === 'technical') ?? null;
|
||||
const isOpen = (key: string) => searching || !!open[key];
|
||||
|
||||
async function loadNav() {
|
||||
loadingNav = true;
|
||||
try {
|
||||
nav = await fetchNav();
|
||||
// auto-select the first module
|
||||
const first = nav.find(a => a.modules.length)?.modules[0];
|
||||
if (first) selectModule(first.module);
|
||||
// auto-select the first product module (or first available)
|
||||
const first = (nav.find(a => a.app !== 'platform' && a.app !== 'technical' && a.modules.length)
|
||||
?? nav.find(a => a.modules.length))?.modules[0];
|
||||
if (first) { expandFor(first.module); selectModule(first.module); }
|
||||
} catch (e: any) {
|
||||
addToast('error', 'Failed to load guides', e?.response?.data?.message ?? e?.message ?? '');
|
||||
} finally {
|
||||
@@ -54,6 +71,7 @@
|
||||
|
||||
async function selectModule(key: string) {
|
||||
if (key === activeModuleKey && module) return;
|
||||
expandFor(key);
|
||||
activeModuleKey = key;
|
||||
loadingModule = true;
|
||||
module = null;
|
||||
@@ -88,21 +106,59 @@
|
||||
{:else if filtered.length === 0}
|
||||
<div class="side-msg">No guides found.</div>
|
||||
{:else}
|
||||
{#each filtered as app (app.app)}
|
||||
<div class="nav-group">
|
||||
<div class="nav-group-label">{appLabel(app.app)}</div>
|
||||
{#each app.modules as m (m.module)}
|
||||
<button
|
||||
class="nav-item"
|
||||
class:active={m.module === activeModuleKey}
|
||||
on:click={() => selectModule(m.module)}
|
||||
title={m.module}
|
||||
>
|
||||
{m.title}
|
||||
</button>
|
||||
{/each}
|
||||
<!-- MODULES (product apps) -->
|
||||
{#if productApps.length}
|
||||
<div class="nav-sec">
|
||||
<button class="sec-head" on:click={() => toggle('modules')}>
|
||||
<span class="chev">{isOpen('modules') ? '▾' : '▸'}</span> Modules
|
||||
</button>
|
||||
{#if isOpen('modules')}
|
||||
{#each productApps as app (app.app)}
|
||||
<div class="nav-app">
|
||||
<button class="app-head" on:click={() => toggle('app:' + app.app)}>
|
||||
<span class="chev">{isOpen('app:' + app.app) ? '▾' : '▸'}</span> {appLabel(app.app)}
|
||||
</button>
|
||||
{#if isOpen('app:' + app.app)}
|
||||
{#each app.modules as m (m.module)}
|
||||
<button class="nav-item nested" class:active={m.module === activeModuleKey}
|
||||
on:click={() => selectModule(m.module)} title={m.module}>{m.title}</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<!-- PLATFORM -->
|
||||
{#if platformApp}
|
||||
<div class="nav-sec">
|
||||
<button class="sec-head" on:click={() => toggle('platform')}>
|
||||
<span class="chev">{isOpen('platform') ? '▾' : '▸'}</span> Platform
|
||||
</button>
|
||||
{#if isOpen('platform')}
|
||||
{#each platformApp.modules as m (m.module)}
|
||||
<button class="nav-item" class:active={m.module === activeModuleKey}
|
||||
on:click={() => selectModule(m.module)} title={m.module}>{m.title}</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- TECHNICAL -->
|
||||
{#if technicalApp}
|
||||
<div class="nav-sec">
|
||||
<button class="sec-head" on:click={() => toggle('technical')}>
|
||||
<span class="chev">{isOpen('technical') ? '▾' : '▸'}</span> Technical
|
||||
</button>
|
||||
{#if isOpen('technical')}
|
||||
{#each technicalApp.modules as m (m.module)}
|
||||
<button class="nav-item" class:active={m.module === activeModuleKey}
|
||||
on:click={() => selectModule(m.module)} title={m.module}>{m.title}</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
@@ -177,17 +233,29 @@
|
||||
.side-nav { flex: 1; overflow-y: auto; padding: 0.25rem 0 1rem; }
|
||||
.side-msg { color: rgba(255,255,255,0.7); font-size: 0.85rem; padding: 0.75rem 1.25rem; }
|
||||
|
||||
.nav-group { margin-bottom: 0.5rem; }
|
||||
.nav-group-label {
|
||||
color: rgba(255,255,255,0.55); font-size: 0.7rem; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: 0.06em; padding: 0.5rem 1.25rem 0.25rem;
|
||||
.nav-sec { margin-bottom: 0.1rem; }
|
||||
.sec-head {
|
||||
display: flex; align-items: center; gap: 0.4rem; width: 100%; text-align: left;
|
||||
background: none; border: none; cursor: pointer;
|
||||
color: rgba(255,255,255,0.9); font-size: 0.78rem; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: 0.05em; padding: 0.6rem 1.1rem;
|
||||
}
|
||||
.sec-head:hover { background: rgba(255,255,255,0.06); }
|
||||
.app-head {
|
||||
display: flex; align-items: center; gap: 0.35rem; width: 100%; text-align: left;
|
||||
background: none; border: none; cursor: pointer;
|
||||
color: rgba(255,255,255,0.72); font-size: 0.82rem; font-weight: 600;
|
||||
padding: 0.35rem 1.1rem 0.35rem 1.5rem;
|
||||
}
|
||||
.app-head:hover { background: rgba(255,255,255,0.06); color: white; }
|
||||
.chev { font-size: 0.58rem; width: 9px; display: inline-block; color: rgba(255,255,255,0.55); flex-shrink: 0; }
|
||||
.nav-item {
|
||||
display: block; width: 100%; text-align: left;
|
||||
background: none; border: none; border-left: 3px solid transparent;
|
||||
color: rgba(255,255,255,0.82); font-size: 0.85rem;
|
||||
padding: 0.4rem 1.25rem 0.4rem 1.5rem; cursor: pointer;
|
||||
}
|
||||
.nav-item.nested { padding-left: 2.5rem; }
|
||||
.nav-item:hover { background: rgba(255,255,255,0.08); color: white; }
|
||||
.nav-item.active { background: rgba(255,255,255,0.14); color: white; border-left-color: white; font-weight: 600; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user