Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/screenshots/nav-category-subgroups.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 20 additions & 11 deletions docs/src/components/Sidebar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,26 @@ const base = import.meta.env.BASE_URL;
</h3>
<ul class="space-y-0.5">
{section.items.map((item) => (
<li>
<a
href={`${base}docs/${item.slug}/`}
class:list={[
'sidebar-link',
{ active: currentSlug === item.slug },
]}
>
{item.title}
</a>
</li>
<>
{item.category && (
<li class="pt-3 first:pt-0">
<span class="block px-3 pb-1 text-xs font-semibold uppercase tracking-wider text-surface-400 dark:text-surface-500 select-none">
{item.category}
</span>
</li>
)}
<li>
<a
href={`${base}docs/${item.slug}/`}
class:list={[
'sidebar-link',
{ active: currentSlug === item.slug },
]}
>
{item.title}
</a>
</li>
</>
))}
</ul>
</div>
Expand Down
16 changes: 11 additions & 5 deletions docs/src/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface NavItem {
title: string;
slug: string;
category?: string; // Visual group header in sidebar
}

export interface NavSection {
Expand Down Expand Up @@ -40,25 +41,29 @@ export const NAV_SECTIONS: NavSection[] = [
title: 'Features',
dir: 'features',
items: [
{ title: 'Team Setup', slug: 'features/team-setup' },
// Team & Configuration
{ title: 'Team Setup', slug: 'features/team-setup', category: 'Team & Configuration' },
{ title: 'Work Routing', slug: 'features/routing' },
{ title: 'Model Selection', slug: 'features/model-selection' },
{ title: 'Response Modes', slug: 'features/response-modes' },
{ title: 'Parallel Execution', slug: 'features/parallel-execution' },
// Agent Intelligence
{ title: 'Parallel Execution', slug: 'features/parallel-execution', category: 'Agent Intelligence' },
{ title: 'Memory', slug: 'features/memory' },
{ title: 'Skills', slug: 'features/skills' },
{ title: 'Directives', slug: 'features/directives' },
{ title: 'Ceremonies', slug: 'features/ceremonies' },
{ title: 'Reviewer Protocol', slug: 'features/reviewer-protocol' },
{ title: 'GitHub Issues', slug: 'features/github-issues' },
// Workflow & Projects
{ title: 'GitHub Issues', slug: 'features/github-issues', category: 'Workflow & Projects' },
{ title: 'GitLab Issues', slug: 'features/gitlab-issues' },
{ title: 'Labels & Triage', slug: 'features/labels' },
{ title: 'PRD Mode', slug: 'features/prd-mode' },
{ title: 'Project Boards', slug: 'features/project-boards' },
{ title: 'Ralph — Work Monitor', slug: 'features/ralph' },
{ title: '@copilot Coding Agent', slug: 'features/copilot-coding-agent' },
{ title: 'Human Team Members', slug: 'features/human-team-members' },
{ title: 'Consult Mode', slug: 'features/consult-mode' },
// Extensibility
{ title: 'Consult Mode', slug: 'features/consult-mode', category: 'Extensibility' },
{ title: 'Remote Control', slug: 'features/remote-control' },
{ title: 'VS Code', slug: 'features/vscode' },
{ title: 'Git Worktrees', slug: 'features/worktrees' },
Expand All @@ -67,7 +72,8 @@ export const NAV_SECTIONS: NavSection[] = [
{ title: 'Marketplace', slug: 'features/marketplace' },
{ title: 'Plugins', slug: 'features/plugins' },
{ title: 'MCP', slug: 'features/mcp' },
{ title: 'Notifications', slug: 'features/notifications' },
// Platform & Scale
{ title: 'Notifications', slug: 'features/notifications', category: 'Platform & Scale' },
{ title: 'Enterprise Platforms', slug: 'features/enterprise-platforms' },
{ title: 'Squad RC', slug: 'features/squad-rc' },
{ title: 'Streams', slug: 'features/streams' },
Expand Down
98 changes: 98 additions & 0 deletions test/docs-build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,101 @@ describe('Docs Build Script (Astro)', () => {
expect(html).toContain('id="search-modal"');
});
});

// --- Nav size-limit + sub-grouping tests (issue #62, P2) ---

describe('Nav section order matches upstream (issue #62, P2)', () => {
it('sections follow upstream order: Get Started, Guide, Features, Reference, Scenarios, Concepts, Cookbook', async () => {
const { NAV_SECTIONS } = await import('../docs/src/navigation.ts');
const titles = NAV_SECTIONS.map((s: { title: string }) => s.title);
const expected = ['Get Started', 'Guide', 'Features', 'Reference', 'Scenarios', 'Concepts', 'Cookbook'];
expect(titles).toEqual(expected);
});
});

describe('Nav section size limit (issue #62, P2)', () => {
// Sections with known large item counts — excluded from the 20-item limit.
// Features uses category sub-groups to manage its 30+ items visually.
// Scenarios is a flat enumeration of use-case pages.
const KNOWN_LARGE_SECTIONS = ['Features', 'Scenarios'];

it('no section exceeds 20 items unless in the known-large exclusion list', async () => {
const { NAV_SECTIONS } = await import('../docs/src/navigation.ts');
const violations: string[] = [];
for (const section of NAV_SECTIONS) {
if (KNOWN_LARGE_SECTIONS.includes(section.title)) continue;
if (section.items.length > 20) {
violations.push(`${section.title} has ${section.items.length} items (limit: 20)`);
}
}
expect(violations, 'Sections exceeding size limit').toEqual([]);
});
});

// --- Nav sub-grouping tests (issue #62, P2) ---

describe('NavItem type — optional category field', () => {
it('NavItem accepts an optional category property', async () => {
// Dynamic import so we exercise the real exported type at runtime.
// TypeScript type-level check: the assignment below would fail tsc
// if NavItem did not include `category?: string`.
const { NAV_SECTIONS } = await import('../docs/src/navigation.ts');

// Construct a NavItem with the optional category field
const itemWithCategory: { title: string; slug: string; category?: string } = {
title: 'Test Item',
slug: 'features/test-item',
category: 'Integrations',
};

// Verify the shape is compatible — title and slug are required
expect(itemWithCategory).toHaveProperty('title');
expect(itemWithCategory).toHaveProperty('slug');
expect(itemWithCategory).toHaveProperty('category', 'Integrations');

// A NavItem WITHOUT category should also work (backward-compat)
const itemWithoutCategory: { title: string; slug: string; category?: string } = {
title: 'Another Item',
slug: 'features/another-item',
};
expect(itemWithoutCategory).toHaveProperty('title');
expect(itemWithoutCategory).not.toHaveProperty('category');

// Existing NAV_SECTIONS should load without error
expect(NAV_SECTIONS.length).toBeGreaterThan(0);
const features = NAV_SECTIONS.find((s: { title: string }) => s.title === 'Features');
expect(features).toBeDefined();
expect(features!.items.length).toBeGreaterThan(0);
});
});

describe('Sidebar category headers (issue #62, P2)', () => {
/**
* Manual verification plan (until Sidebar.astro is updated):
*
* Once the `category` field is added to NavItem and Sidebar.astro renders
* category headers, verify:
*
* 1. Items with `category: "Core"` appear under a "Core" sub-header.
* 2. Items WITHOUT a category appear in their original order (no header).
* 3. Category headers are visually distinct (smaller font, muted color).
* 4. Category headers are NOT clickable links.
* 5. Keyboard / screen-reader navigation skips category headers.
*
* Automated check below confirms Sidebar.astro exists and renders the
* Features section. After implementation, add assertions for category
* header elements (e.g., `data-nav-category` attribute).
*/
it('Sidebar component exists and renders Features section', () => {
const sidebarPath = join(DOCS_DIR, 'src', 'components', 'Sidebar.astro');
expect(existsSync(sidebarPath), 'Sidebar.astro must exist').toBe(true);

const source = readFileSync(sidebarPath, 'utf-8');
expect(source).toContain('NAV_SECTIONS');
expect(source).toContain('section.title');
});

it.todo('category headers render as non-clickable sub-headings in the sidebar');
it.todo('items without a category render normally (no extra header)');
it.todo('category headers have data-nav-category attribute for test hooks');
});
Loading