Appearance
Sidebar Nav Cleanup Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Remove the sidebar's duplicate top-of-page navigation bars and make nested sidebar items visually read as children of their parent group.
Architecture: Two independent, presentation-only changes. Task 1 deletes the redundant SubNavTabs pill-row from three layout components (it duplicates sidebar navigation that already exists) and removes the now-dead SubNavTabs component itself. Task 2 adds a CSS-only "connector rail + shrink" treatment to NavGroup.jsx's nested (depth > 0) items, distinguishing them visually from top-level sidebar buttons without changing any click/navigation behavior.
Tech Stack: React 18, Vite, Tailwind CSS 4 (utility classes), custom neo-brutalist CSS in client/src/standalone.css, Vitest + React Testing Library.
Global Constraints
- Presentation-layer only. No routing, API, or data changes in either task.
- No change to
RadixShell.jsx's nav-tree construction (buildCatalogGroup,accountGroup,toolsGroup) — only howNavGrouprenders what it's given. - Design doc:
docs/superpowers/specs/2026-07-11-sidebar-nav-cleanup-design.md.
Task 1: Remove duplicate top-of-page sub-nav bars
Files:
- Modify:
client/src/pages/catalog/CatalogGameLayout.jsx - Modify:
client/src/pages/catalog/CatalogGameSettingsLayout.jsx - Modify:
client/src/pages/settings/SettingsLayout.jsx - Modify:
client/src/pages/catalog/CatalogGameLayout.test.jsx - Modify:
client/src/pages/catalog/CatalogGameSettingsLayout.test.jsx - Delete:
client/src/components/SubNavTabs.jsx - Delete:
client/src/components/SubNavTabs.test.jsx
Interfaces:
Consumes: nothing new — these files already exist from the earlier Navigation & Dashboard Restructure plan.
Produces:
CatalogGameLayout,CatalogGameSettingsLayout,SettingsLayoutno longer accept/use anisEmbeddedprop (dropped from their signatures —App.jsxstill passes it; React ignores the unconsumed prop, noApp.jsxchange needed).[ ] Step 1: Update the failing tests first
Remove the sub-nav-button assertions from both catalog layout tests (they duplicate coverage that already exists via RadixShell.test.jsx/NavGroup.test.jsx, and the buttons are being removed from these pages).
Replace client/src/pages/catalog/CatalogGameLayout.test.jsx in full:
jsx
import { describe, it, expect, vi, beforeEach } from 'vitest';
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
vi.mock('../../utils/api', () => ({
default: { get: vi.fn(() => Promise.resolve({ data: { games: [{ id: 'mtg', name: 'Magic: The Gathering' }] } })) },
}));
import CatalogGameLayout from './CatalogGameLayout';
function renderAt(path) {
return render(
<MemoryRouter initialEntries={[path]}>
<Routes>
<Route path="/catalog/:game" element={<CatalogGameLayout />}>
<Route path="singles" element={<div>singles-content</div>} />
</Route>
<Route path="/dashboard" element={<div>dashboard-page</div>} />
</Routes>
</MemoryRouter>
);
}
describe('CatalogGameLayout', () => {
beforeEach(() => vi.clearAllMocks());
it('renders the game heading and outlet, with no duplicate sub-nav buttons', async () => {
renderAt('/catalog/mtg/singles');
await waitFor(() => expect(screen.getByText('singles-content')).toBeInTheDocument());
expect(screen.getByRole('heading', { name: 'Magic: The Gathering' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Singles' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Sealed' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Sets' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Settings' })).not.toBeInTheDocument();
});
it('redirects to /dashboard for an unknown game', async () => {
renderAt('/catalog/lorcana/singles');
await waitFor(() => expect(screen.getByText('dashboard-page')).toBeInTheDocument());
});
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Replace client/src/pages/catalog/CatalogGameSettingsLayout.test.jsx in full:
jsx
import { describe, it, expect } from 'vitest';
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import CatalogGameSettingsLayout from './CatalogGameSettingsLayout';
describe('CatalogGameSettingsLayout', () => {
it('renders the shared-settings note and the outlet, with no duplicate sub-nav buttons', () => {
render(
<MemoryRouter initialEntries={['/catalog/mtg/settings/pricing']}>
<Routes>
<Route path="/catalog/:game/settings" element={<CatalogGameSettingsLayout />}>
<Route path="pricing" element={<div>pricing-content</div>} />
</Route>
</Routes>
</MemoryRouter>
);
expect(screen.getByText(/these settings apply to all of your enabled catalogs/i)).toBeInTheDocument();
expect(screen.getByText('pricing-content')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Pricing' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Sync Settings' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Metafields' })).not.toBeInTheDocument();
});
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- [ ] Step 2: Run to verify they fail
Run: npm run test:client -- client/src/pages/catalog/CatalogGameLayout.test.jsx client/src/pages/catalog/CatalogGameSettingsLayout.test.jsx Expected: FAIL — both tests' queryByRole('button', ...) negative assertions fail against the current components, which still render the SubNavTabs buttons (e.g. CatalogGameLayout's test fails on expect(screen.queryByRole('button', { name: 'Singles' })).not.toBeInTheDocument() because that button currently exists).
- [ ] Step 3: Update the three layout components
Replace client/src/pages/catalog/CatalogGameLayout.jsx in full:
jsx
import { useEffect, useState } from 'react';
import { Navigate, Outlet, useParams } from 'react-router-dom';
import api from '../../utils/api';
import { Text } from '@/components/retroui';
export default function CatalogGameLayout() {
const { game } = useParams();
const [games, setGames] = useState(null);
useEffect(() => {
let cancelled = false;
api.get('/games').then(({ data }) => {
if (!cancelled) setGames(data.games || []);
}).catch(() => {
if (!cancelled) setGames([]);
});
return () => { cancelled = true; };
}, []);
if (games === null) {
return <Text as="p" className="text-sm text-muted-foreground">Loading...</Text>;
}
const gameInfo = games.find((g) => g.id === game);
if (!gameInfo) {
return <Navigate to="/dashboard" replace />;
}
return (
<div className="space-y-4 sm:space-y-6">
<div className="space-y-1 max-w-5xl mx-auto">
<Text as="h1" className="text-2xl sm:text-3xl font-bold">{gameInfo.name}</Text>
</div>
<Outlet />
</div>
);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Replace client/src/pages/catalog/CatalogGameSettingsLayout.jsx in full:
jsx
import { Outlet } from 'react-router-dom';
import { Text } from '@/components/retroui';
export default function CatalogGameSettingsLayout() {
return (
<div className="space-y-4 sm:space-y-6">
<div className="max-w-5xl mx-auto">
<Text as="p" className="text-xs text-muted-foreground">
These settings apply to all of your enabled catalogs, not just this one.
</Text>
</div>
<Outlet />
</div>
);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Replace client/src/pages/settings/SettingsLayout.jsx in full:
jsx
import { Outlet } from 'react-router-dom';
export default function SettingsLayout() {
return (
<div className="space-y-4 sm:space-y-6">
<div className="space-y-1 max-w-5xl mx-auto">
<h1 className="text-2xl sm:text-3xl font-bold">Store Settings</h1>
</div>
<Outlet />
</div>
);
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
- [ ] Step 4: Run to verify the tests pass
Run: npm run test:client -- client/src/pages/catalog/CatalogGameLayout.test.jsx client/src/pages/catalog/CatalogGameSettingsLayout.test.jsx Expected: PASS (2 test files, 3 tests total)
- [ ] Step 5: Delete the now-dead SubNavTabs component and its test
bash
git rm client/src/components/SubNavTabs.jsx client/src/components/SubNavTabs.test.jsx1
- [ ] Step 6: Confirm nothing else references SubNavTabs
Run: grep -rn "SubNavTabs" client/src --include=*.jsx --include=*.js Expected: no output.
- [ ] Step 7: Run the full client suite
Run: npm run test:client Expected: PASS — no failures elsewhere (in particular, confirm no other test imported SubNavTabs indirectly).
- [ ] Step 8: Lint
Run: npx eslint client/src/pages/catalog/CatalogGameLayout.jsx client/src/pages/catalog/CatalogGameSettingsLayout.jsx client/src/pages/settings/SettingsLayout.jsx client/src/pages/catalog/CatalogGameLayout.test.jsx client/src/pages/catalog/CatalogGameSettingsLayout.test.jsx Expected: no errors.
- [ ] Step 9: Commit
bash
git add client/src/pages/catalog/CatalogGameLayout.jsx client/src/pages/catalog/CatalogGameSettingsLayout.jsx client/src/pages/settings/SettingsLayout.jsx client/src/pages/catalog/CatalogGameLayout.test.jsx client/src/pages/catalog/CatalogGameSettingsLayout.test.jsx
git commit -m "fix(client): remove duplicate top-of-page sub-nav (sidebar is now the sole nav for these sections)"1
2
2
Task 2: Sidebar hierarchy — connector rail + shrink treatment for nested items
Files:
- Modify:
client/src/components/NavGroup.jsx - Modify:
client/src/standalone.css - Modify:
client/src/components/NavGroup.test.jsx
Interfaces:
Consumes: nothing new.
Produces:
NavGroupapplies anav-link-nestedCSS class (plustext-xsinstead oftext-sm) to every button it renders atdepth > 0. Depth-0 buttons are unaffected — same classes as before.[ ] Step 1: Write the failing test
Add a new test to the end of the describe('NavGroup', ...) block in client/src/components/NavGroup.test.jsx (keep all 4 existing tests unchanged, just append this one before the closing });):
jsx
it('applies the nested-item treatment only to items below depth 0', () => {
render(
<>
<NavGroup
item={{ key: 'dashboard', label: 'Dashboard', path: '/dashboard' }}
depth={0}
isActive={() => false}
expandedKeys={new Set()}
onToggle={() => {}}
onNavigate={() => {}}
/>
<NavGroup
item={{ key: 'general', label: 'General', path: '/settings/general' }}
depth={1}
isActive={() => false}
expandedKeys={new Set()}
onToggle={() => {}}
onNavigate={() => {}}
/>
</>
);
expect(screen.getByRole('button', { name: 'Dashboard' })).not.toHaveClass('nav-link-nested');
expect(screen.getByRole('button', { name: 'General' })).toHaveClass('nav-link-nested');
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- [ ] Step 2: Run to verify it fails
Run: npm run test:client -- client/src/components/NavGroup.test.jsx Expected: FAIL — nav-link-nested class doesn't exist yet, so toHaveClass('nav-link-nested') on the "General" button fails.
- [ ] Step 3: Update NavGroup.jsx
Replace client/src/components/NavGroup.jsx in full:
jsx
import { ChevronDownIcon, ChevronRightIcon } from '@radix-ui/react-icons';
import { Badge } from '@/components/retroui';
// Recursive sidebar nav row. A node with `children` renders as a
// collapsible group; a node with `path` renders as a leaf link.
// Used for the 2-3 level Store Settings / per-game Catalog tree in RadixShell.
//
// Items below depth 0 get the `nav-link-nested` treatment (thinner border,
// smaller shadow/padding, a connector rail via ::before — see
// standalone.css) plus text-xs instead of text-sm, so nested items read
// visually as children of their parent group rather than as peers of the
// top-level sidebar buttons.
export default function NavGroup({ item, depth, isActive, expandedKeys, onToggle, onNavigate }) {
const hasChildren = Array.isArray(item.children) && item.children.length > 0;
const paddingClass = depth === 0 ? '' : depth === 1 ? 'pl-4' : 'pl-8';
const textSizeClass = depth === 0 ? 'text-sm' : 'text-xs';
const nestedClass = depth === 0 ? '' : 'nav-link-nested';
if (!hasChildren) {
const active = isActive(item.path);
return (
<button
onClick={() => onNavigate(item)}
disabled={item.disabled}
className={`nav-link ${textSizeClass} ${nestedClass} w-full ${paddingClass} ${item.disabled ? 'nav-link-disabled' : active ? 'nav-link-active' : 'nav-link-default'} ${item.comingSoon ? 'justify-between' : ''}`}
>
<span>{item.label}</span>
{item.comingSoon && <Badge variant="info">Coming Soon</Badge>}
</button>
);
}
const isExpanded = expandedKeys.has(item.key);
return (
<div>
<button
onClick={() => onToggle(item.key)}
className={`nav-link ${textSizeClass} ${nestedClass} w-full ${paddingClass} justify-between nav-link-default`}
aria-expanded={isExpanded}
>
<span>{item.label}</span>
{isExpanded ? <ChevronDownIcon className="w-3.5 h-3.5" /> : <ChevronRightIcon className="w-3.5 h-3.5" />}
</button>
{isExpanded && (
<div className="space-y-1 mt-1">
{item.children.map((child) => (
<NavGroup
key={child.key}
item={child}
depth={depth + 1}
isActive={isActive}
expandedKeys={expandedKeys}
onToggle={onToggle}
onNavigate={onNavigate}
/>
))}
</div>
)}
</div>
);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- [ ] Step 4: Run to verify the new test passes
Run: npm run test:client -- client/src/components/NavGroup.test.jsx Expected: PASS (5 tests: the 4 existing ones plus the new one).
- [ ] Step 5: Add the CSS for
.nav-link-nested
In client/src/standalone.css, find this existing block (inside the @layer components { ... } that defines .nav-link, .nav-link-default, .nav-link-active, .nav-link-disabled — the disabled rule is the last one in that group, immediately before the /* Card styles - Neo-Brutalist */ comment):
css
.nav-link-disabled {
color: var(--muted-foreground);
cursor: not-allowed;
opacity: 0.6;
}1
2
3
4
5
2
3
4
5
Add this immediately after it (still inside the same @layer components block, still before /* Card styles - Neo-Brutalist */):
css
.nav-link-nested {
border-width: 2px;
box-shadow: 2px 2px 0px #000000;
padding: 0.4375rem 0.625rem;
margin-bottom: 6px;
position: relative;
}
.nav-link-nested::before {
content: '';
position: absolute;
left: -11px;
top: 0;
bottom: 0;
width: 2px;
background: #000000;
opacity: 0.25;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(No new :hover rule needed — the existing base .nav-link:hover rule already applies box-shadow: 1px 1px 0px #000000;, which matches what a shrunk hover state should look like.)
Then find the dark-mode block near the end of the file (search for .dark .nav-link:hover):
css
.dark .nav-link:hover {
box-shadow: 1px 1px 0px rgb(255 255 255 / 0.08) !important;
}1
2
3
2
3
Add this immediately after it, so it wins the cascade over the earlier .dark .nav-link, .dark .nav-link-active { box-shadow: 3px 3px 0px rgb(255 255 255 / 0.08) !important; } rule for nested items specifically:
css
.dark .nav-link-nested,
.dark .nav-link-nested.nav-link-active {
box-shadow: 2px 2px 0px rgb(255 255 255 / 0.08) !important;
}
.dark .nav-link-nested::before {
background: #ffffff;
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- [ ] Step 6: Manually verify in the browser (CSS changes aren't covered by jsdom tests)
Run npm run dev, open the app, expand a sidebar group with nested items (e.g. Store Settings, or a Catalog game group), and confirm:
Nested items are visibly smaller/thinner than top-level items, with a faint vertical line connecting them to their parent.
Toggle dark mode and confirm the connector line and shadow are still visible (not invisible-black-on-dark).
Confirm clicking still navigates correctly (no behavior change, only appearance).
[ ] Step 7: Run the full client suite and lint
Run: npm run test:client Expected: PASS.
Run: npx eslint client/src/components/NavGroup.jsx client/src/components/NavGroup.test.jsx Expected: no errors.
- [ ] Step 8: Commit
bash
git add client/src/components/NavGroup.jsx client/src/components/NavGroup.test.jsx client/src/standalone.css
git commit -m "feat(client): add connector-rail styling to distinguish nested sidebar items from top-level ones"1
2
2
