ux-toolkit 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -7
- package/agents/card-reviewer.md +173 -0
- package/agents/comparison-reviewer.md +143 -0
- package/agents/density-reviewer.md +207 -0
- package/agents/detail-page-reviewer.md +143 -0
- package/agents/editor-reviewer.md +165 -0
- package/agents/form-reviewer.md +156 -0
- package/agents/game-ui-reviewer.md +181 -0
- package/agents/list-page-reviewer.md +132 -0
- package/agents/navigation-reviewer.md +145 -0
- package/agents/panel-reviewer.md +182 -0
- package/agents/replay-reviewer.md +174 -0
- package/agents/settings-reviewer.md +166 -0
- package/agents/ux-auditor.md +145 -45
- package/agents/ux-engineer.md +211 -38
- package/dist/cli.js +172 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +172 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -4
- package/dist/index.d.ts +128 -4
- package/dist/index.js +172 -5
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/skills/canvas-grid-patterns/SKILL.md +367 -0
- package/skills/comparison-patterns/SKILL.md +354 -0
- package/skills/data-density-patterns/SKILL.md +493 -0
- package/skills/detail-page-patterns/SKILL.md +522 -0
- package/skills/drag-drop-patterns/SKILL.md +406 -0
- package/skills/editor-workspace-patterns/SKILL.md +552 -0
- package/skills/event-timeline-patterns/SKILL.md +542 -0
- package/skills/form-patterns/SKILL.md +608 -0
- package/skills/info-card-patterns/SKILL.md +531 -0
- package/skills/keyboard-shortcuts-patterns/SKILL.md +365 -0
- package/skills/list-page-patterns/SKILL.md +351 -0
- package/skills/modal-patterns/SKILL.md +750 -0
- package/skills/navigation-patterns/SKILL.md +476 -0
- package/skills/page-structure-patterns/SKILL.md +271 -0
- package/skills/playback-replay-patterns/SKILL.md +695 -0
- package/skills/react-ux-patterns/SKILL.md +434 -0
- package/skills/split-panel-patterns/SKILL.md +609 -0
- package/skills/status-visualization-patterns/SKILL.md +635 -0
- package/skills/toast-notification-patterns/SKILL.md +207 -0
- package/skills/turn-based-ui-patterns/SKILL.md +506 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: navigation-patterns
|
|
3
|
+
description: Sidebar navigation, mobile drawers, breadcrumbs, and app shell patterns for React/Next.js applications
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Navigation Design Patterns
|
|
8
|
+
|
|
9
|
+
## Collapsible Sidebar
|
|
10
|
+
|
|
11
|
+
### Desktop Sidebar with Collapse Toggle
|
|
12
|
+
```tsx
|
|
13
|
+
interface SidebarProps {
|
|
14
|
+
isCollapsed: boolean;
|
|
15
|
+
setIsCollapsed: (collapsed: boolean) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Sidebar({ isCollapsed, setIsCollapsed }: SidebarProps) {
|
|
19
|
+
const router = useRouter();
|
|
20
|
+
|
|
21
|
+
const isPathActive = (href: string) => {
|
|
22
|
+
if (href === '/') return router.pathname === '/';
|
|
23
|
+
return router.pathname.startsWith(href);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<aside
|
|
28
|
+
className={`
|
|
29
|
+
bg-surface-deep border-r border-border
|
|
30
|
+
fixed inset-y-0 left-0 z-30
|
|
31
|
+
flex flex-col
|
|
32
|
+
${isCollapsed ? 'w-16' : 'w-56'}
|
|
33
|
+
transition-[width] duration-300 ease-in-out
|
|
34
|
+
`}
|
|
35
|
+
>
|
|
36
|
+
{/* Header / Brand */}
|
|
37
|
+
<div className={`
|
|
38
|
+
flex items-center h-14 border-b border-border
|
|
39
|
+
${isCollapsed ? 'justify-center px-2' : 'px-4 gap-3'}
|
|
40
|
+
`}>
|
|
41
|
+
<Logo className="w-8 h-8 flex-shrink-0" />
|
|
42
|
+
{!isCollapsed && (
|
|
43
|
+
<div className="flex-1 min-w-0">
|
|
44
|
+
<span className="text-base font-bold">AppName</span>
|
|
45
|
+
<span className="block text-xs text-text-muted">Subtitle</span>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
{/* Collapse toggle */}
|
|
50
|
+
<button
|
|
51
|
+
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
52
|
+
className="p-1.5 rounded-lg text-text-secondary hover:text-white hover:bg-surface-raised"
|
|
53
|
+
aria-label={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
54
|
+
>
|
|
55
|
+
{isCollapsed ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* Navigation */}
|
|
60
|
+
<nav className="flex-1 overflow-y-auto py-4">
|
|
61
|
+
<NavSection title="Browse" isCollapsed={isCollapsed}>
|
|
62
|
+
<NavItem href="/items" icon={<ItemsIcon />} label="Items" isCollapsed={isCollapsed} isActive={isPathActive('/items')} />
|
|
63
|
+
<NavItem href="/catalog" icon={<CatalogIcon />} label="Catalog" isCollapsed={isCollapsed} isActive={isPathActive('/catalog')} />
|
|
64
|
+
</NavSection>
|
|
65
|
+
|
|
66
|
+
<NavSection title="Tools" isCollapsed={isCollapsed}>
|
|
67
|
+
<NavItem href="/editor" icon={<EditorIcon />} label="Editor" isCollapsed={isCollapsed} isActive={isPathActive('/editor')} />
|
|
68
|
+
</NavSection>
|
|
69
|
+
</nav>
|
|
70
|
+
|
|
71
|
+
{/* Footer */}
|
|
72
|
+
<div className={`border-t border-border py-3 ${isCollapsed ? 'px-2 text-center' : 'px-4'}`}>
|
|
73
|
+
{isCollapsed ? (
|
|
74
|
+
<span className="text-xs text-text-muted">v1.0</span>
|
|
75
|
+
) : (
|
|
76
|
+
<div className="flex items-center justify-between text-xs text-text-muted">
|
|
77
|
+
<span>v1.0.0</span>
|
|
78
|
+
<a href="https://github.com" className="hover:text-accent">
|
|
79
|
+
<GithubIcon />
|
|
80
|
+
</a>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</aside>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Navigation Item Component
|
|
90
|
+
```tsx
|
|
91
|
+
interface NavItemProps {
|
|
92
|
+
href: string;
|
|
93
|
+
icon: ReactElement;
|
|
94
|
+
label: string;
|
|
95
|
+
isCollapsed: boolean;
|
|
96
|
+
isActive: boolean;
|
|
97
|
+
onClick?: () => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function NavItem({ href, icon, label, isCollapsed, isActive, onClick }: NavItemProps) {
|
|
101
|
+
return (
|
|
102
|
+
<Link href={href}>
|
|
103
|
+
<a
|
|
104
|
+
onClick={onClick}
|
|
105
|
+
className={`
|
|
106
|
+
relative flex items-center px-3 py-2.5 mx-2 rounded-lg
|
|
107
|
+
transition-all duration-150 group
|
|
108
|
+
${isCollapsed ? 'justify-center' : 'gap-3'}
|
|
109
|
+
${isActive
|
|
110
|
+
? 'bg-accent/15 text-accent'
|
|
111
|
+
: 'text-text-secondary hover:text-white hover:bg-surface-raised/50'
|
|
112
|
+
}
|
|
113
|
+
`}
|
|
114
|
+
>
|
|
115
|
+
{/* Active indicator - left border */}
|
|
116
|
+
{isActive && (
|
|
117
|
+
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-accent rounded-r-full" />
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{/* Icon */}
|
|
121
|
+
<span className={`flex-shrink-0 ${isActive ? 'text-accent' : ''}`}>
|
|
122
|
+
{icon}
|
|
123
|
+
</span>
|
|
124
|
+
|
|
125
|
+
{/* Label */}
|
|
126
|
+
{!isCollapsed && (
|
|
127
|
+
<span className="text-sm font-medium truncate">{label}</span>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{/* Tooltip for collapsed state */}
|
|
131
|
+
{isCollapsed && (
|
|
132
|
+
<div className="absolute left-full ml-2 px-2 py-1 bg-surface-base text-white text-sm rounded shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-50 whitespace-nowrap border border-border">
|
|
133
|
+
{label}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</a>
|
|
137
|
+
</Link>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Navigation Section Component
|
|
143
|
+
```tsx
|
|
144
|
+
function NavSection({ title, children, isCollapsed }: { title?: string; children: React.ReactNode; isCollapsed: boolean }) {
|
|
145
|
+
return (
|
|
146
|
+
<div className="mb-2">
|
|
147
|
+
{title && !isCollapsed && (
|
|
148
|
+
<div className="px-3 py-2">
|
|
149
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-text-muted">
|
|
150
|
+
{title}
|
|
151
|
+
</span>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
{!isCollapsed && title && <div className="mx-4 border-t border-border mb-4" />}
|
|
155
|
+
<div className="space-y-0.5">
|
|
156
|
+
{children}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Mobile Sidebar Drawer
|
|
164
|
+
|
|
165
|
+
### Mobile Sidebar with Overlay
|
|
166
|
+
```tsx
|
|
167
|
+
// Store for mobile sidebar state (Zustand example)
|
|
168
|
+
const useMobileSidebarStore = create((set) => ({
|
|
169
|
+
isOpen: false,
|
|
170
|
+
open: () => set({ isOpen: true }),
|
|
171
|
+
close: () => set({ isOpen: false }),
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
function MobileSidebar({ children }) {
|
|
175
|
+
const { isOpen, close } = useMobileSidebarStore();
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<>
|
|
179
|
+
{/* Backdrop overlay */}
|
|
180
|
+
{isOpen && (
|
|
181
|
+
<div
|
|
182
|
+
className="fixed inset-0 bg-black/50 z-40 lg:hidden transition-opacity duration-300"
|
|
183
|
+
onClick={close}
|
|
184
|
+
aria-hidden="true"
|
|
185
|
+
/>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
{/* Sidebar drawer */}
|
|
189
|
+
<aside
|
|
190
|
+
className={`
|
|
191
|
+
fixed inset-y-0 left-0 z-50 w-64
|
|
192
|
+
bg-surface-deep border-r border-border
|
|
193
|
+
transform transition-transform duration-300 ease-in-out
|
|
194
|
+
${isOpen ? 'translate-x-0' : '-translate-x-full'}
|
|
195
|
+
lg:hidden
|
|
196
|
+
`}
|
|
197
|
+
>
|
|
198
|
+
{children}
|
|
199
|
+
</aside>
|
|
200
|
+
</>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Mobile Header with Menu Button
|
|
206
|
+
```tsx
|
|
207
|
+
function MobileHeader({ title }) {
|
|
208
|
+
const openMobileSidebar = useMobileSidebarStore((s) => s.open);
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<header className="lg:hidden flex items-center justify-between h-12 px-3 bg-surface-base border-b border-border">
|
|
212
|
+
<span className="text-sm font-semibold text-white truncate">
|
|
213
|
+
{title}
|
|
214
|
+
</span>
|
|
215
|
+
<button
|
|
216
|
+
onClick={openMobileSidebar}
|
|
217
|
+
className="p-2 -mr-2 rounded-lg text-text-secondary hover:text-white hover:bg-surface-raised/50"
|
|
218
|
+
aria-label="Open navigation menu"
|
|
219
|
+
>
|
|
220
|
+
<HamburgerIcon />
|
|
221
|
+
</button>
|
|
222
|
+
</header>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Auto-Close on Navigation
|
|
228
|
+
```tsx
|
|
229
|
+
// In _app.tsx or layout
|
|
230
|
+
function App({ Component, pageProps }) {
|
|
231
|
+
const router = useRouter();
|
|
232
|
+
const closeMobileSidebar = useMobileSidebarStore((s) => s.close);
|
|
233
|
+
|
|
234
|
+
// Close mobile drawer on route change
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
const handleRouteChange = () => {
|
|
237
|
+
if (window.innerWidth < 1024) {
|
|
238
|
+
closeMobileSidebar();
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
router.events.on('routeChangeComplete', handleRouteChange);
|
|
243
|
+
return () => router.events.off('routeChangeComplete', handleRouteChange);
|
|
244
|
+
}, [router.events, closeMobileSidebar]);
|
|
245
|
+
|
|
246
|
+
return <Component {...pageProps} />;
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## App Shell Layout
|
|
251
|
+
|
|
252
|
+
### Responsive Layout with Sidebar
|
|
253
|
+
```tsx
|
|
254
|
+
interface LayoutProps {
|
|
255
|
+
children: ReactNode;
|
|
256
|
+
title?: string;
|
|
257
|
+
sidebarComponent?: ReactNode;
|
|
258
|
+
isSidebarCollapsed?: boolean;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function Layout({ children, title, sidebarComponent, isSidebarCollapsed }: LayoutProps) {
|
|
262
|
+
// Calculate main content margin based on sidebar state
|
|
263
|
+
const hasSidebar = !!sidebarComponent;
|
|
264
|
+
const contentMargin = hasSidebar
|
|
265
|
+
? (isSidebarCollapsed ? 'lg:ml-16' : 'lg:ml-56')
|
|
266
|
+
: '';
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<>
|
|
270
|
+
<Head>
|
|
271
|
+
<title>{title}</title>
|
|
272
|
+
</Head>
|
|
273
|
+
|
|
274
|
+
<div className="flex flex-col h-screen bg-surface-deep overflow-hidden">
|
|
275
|
+
{/* Mobile header - only on mobile */}
|
|
276
|
+
{hasSidebar && <MobileHeader title={title} />}
|
|
277
|
+
|
|
278
|
+
<div className="flex flex-1 overflow-hidden">
|
|
279
|
+
{/* Sidebar - fixed position */}
|
|
280
|
+
{sidebarComponent}
|
|
281
|
+
|
|
282
|
+
{/* Main content */}
|
|
283
|
+
<main className={`flex-1 overflow-auto ${contentMargin} transition-all duration-300`}>
|
|
284
|
+
{children}
|
|
285
|
+
</main>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
</>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Breadcrumbs
|
|
294
|
+
|
|
295
|
+
### Breadcrumb Component
|
|
296
|
+
```tsx
|
|
297
|
+
interface BreadcrumbItem {
|
|
298
|
+
label: string;
|
|
299
|
+
href?: string;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
interface BreadcrumbsProps {
|
|
303
|
+
items: BreadcrumbItem[];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function Breadcrumbs({ items }: BreadcrumbsProps) {
|
|
307
|
+
return (
|
|
308
|
+
<nav aria-label="Breadcrumb" className="mb-4">
|
|
309
|
+
<ol className="flex items-center gap-2 text-sm">
|
|
310
|
+
{items.map((item, index) => {
|
|
311
|
+
const isLast = index === items.length - 1;
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<li key={index} className="flex items-center gap-2">
|
|
315
|
+
{index > 0 && (
|
|
316
|
+
<ChevronRightIcon className="w-4 h-4 text-text-muted" />
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{isLast || !item.href ? (
|
|
320
|
+
<span className={isLast ? 'text-white font-medium' : 'text-text-secondary'}>
|
|
321
|
+
{item.label}
|
|
322
|
+
</span>
|
|
323
|
+
) : (
|
|
324
|
+
<Link href={item.href}>
|
|
325
|
+
<a className="text-text-secondary hover:text-accent transition-colors">
|
|
326
|
+
{item.label}
|
|
327
|
+
</a>
|
|
328
|
+
</Link>
|
|
329
|
+
)}
|
|
330
|
+
</li>
|
|
331
|
+
);
|
|
332
|
+
})}
|
|
333
|
+
</ol>
|
|
334
|
+
</nav>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Usage
|
|
339
|
+
<Breadcrumbs items={[
|
|
340
|
+
{ label: 'Home', href: '/' },
|
|
341
|
+
{ label: 'Campaigns', href: '/campaigns' },
|
|
342
|
+
{ label: 'Operation Serpent' } // Current page, no href
|
|
343
|
+
]} />
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Quick Navigation Tags
|
|
347
|
+
|
|
348
|
+
### Settings-Style Quick Nav
|
|
349
|
+
```tsx
|
|
350
|
+
interface QuickNavProps {
|
|
351
|
+
sections: { id: string; title: string; icon: ReactNode }[];
|
|
352
|
+
activeSection: string;
|
|
353
|
+
onNavigate: (sectionId: string) => void;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function QuickNavigation({ sections, activeSection, onNavigate }: QuickNavProps) {
|
|
357
|
+
return (
|
|
358
|
+
<div className="sticky top-0 z-10 bg-surface-deep/95 backdrop-blur-sm border-b border-border -mx-4 px-4 py-3 mb-4">
|
|
359
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
360
|
+
<span className="text-xs text-text-muted mr-1">Jump to:</span>
|
|
361
|
+
{sections.map((section) => {
|
|
362
|
+
const isActive = activeSection === section.id;
|
|
363
|
+
return (
|
|
364
|
+
<button
|
|
365
|
+
key={section.id}
|
|
366
|
+
onClick={() => onNavigate(section.id)}
|
|
367
|
+
className={`
|
|
368
|
+
inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-md
|
|
369
|
+
transition-all duration-150
|
|
370
|
+
${isActive
|
|
371
|
+
? 'bg-accent/20 text-accent border border-accent/30'
|
|
372
|
+
: 'bg-surface-raised/50 text-text-secondary border border-border hover:bg-surface-raised hover:text-white'
|
|
373
|
+
}
|
|
374
|
+
`}
|
|
375
|
+
>
|
|
376
|
+
{section.icon}
|
|
377
|
+
{section.title}
|
|
378
|
+
</button>
|
|
379
|
+
);
|
|
380
|
+
})}
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Scrolling to Section with URL Hash
|
|
388
|
+
```tsx
|
|
389
|
+
function useHashNavigation(sections: string[]) {
|
|
390
|
+
const [activeSection, setActiveSection] = useState(sections[0]);
|
|
391
|
+
const sectionRefs = useRef<Record<string, HTMLElement | null>>({});
|
|
392
|
+
|
|
393
|
+
// Parse hash on mount
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
const hash = window.location.hash.replace('#', '');
|
|
396
|
+
if (hash && sections.includes(hash)) {
|
|
397
|
+
setActiveSection(hash);
|
|
398
|
+
setTimeout(() => {
|
|
399
|
+
sectionRefs.current[hash]?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
400
|
+
}, 100);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const onHashChange = () => {
|
|
404
|
+
const newHash = window.location.hash.replace('#', '');
|
|
405
|
+
if (newHash && sections.includes(newHash)) {
|
|
406
|
+
setActiveSection(newHash);
|
|
407
|
+
sectionRefs.current[newHash]?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
window.addEventListener('hashchange', onHashChange);
|
|
412
|
+
return () => window.removeEventListener('hashchange', onHashChange);
|
|
413
|
+
}, [sections]);
|
|
414
|
+
|
|
415
|
+
const navigateToSection = useCallback((sectionId: string) => {
|
|
416
|
+
window.history.pushState(null, '', `#${sectionId}`);
|
|
417
|
+
setActiveSection(sectionId);
|
|
418
|
+
setTimeout(() => {
|
|
419
|
+
sectionRefs.current[sectionId]?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
420
|
+
}, 50);
|
|
421
|
+
}, []);
|
|
422
|
+
|
|
423
|
+
const createSectionRef = (id: string) => (el: HTMLElement | null) => {
|
|
424
|
+
sectionRefs.current[id] = el;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
return { activeSection, navigateToSection, createSectionRef };
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Sidebar Navigation Best Practices
|
|
432
|
+
|
|
433
|
+
### Do's
|
|
434
|
+
- Use clear, recognizable icons
|
|
435
|
+
- Show tooltips when collapsed
|
|
436
|
+
- Indicate active state clearly (color + left border)
|
|
437
|
+
- Auto-close mobile drawer on navigation
|
|
438
|
+
- Persist collapsed state to localStorage
|
|
439
|
+
- Use section dividers for grouping
|
|
440
|
+
|
|
441
|
+
### Don'ts
|
|
442
|
+
- Don't nest more than 2 levels deep
|
|
443
|
+
- Don't hide important navigation in submenus
|
|
444
|
+
- Don't forget mobile experience
|
|
445
|
+
- Don't animate width changes too slowly (>300ms feels sluggish)
|
|
446
|
+
- Don't use horizontal scrolling in sidebar
|
|
447
|
+
|
|
448
|
+
### Accessibility
|
|
449
|
+
- Use `aria-label` on collapse/expand buttons
|
|
450
|
+
- Ensure focus is visible in all states
|
|
451
|
+
- Support keyboard navigation (Tab, Enter, Escape)
|
|
452
|
+
- Announce state changes to screen readers
|
|
453
|
+
- Use semantic HTML (nav, ul, li)
|
|
454
|
+
|
|
455
|
+
## Audit Checklist for Navigation
|
|
456
|
+
|
|
457
|
+
### Critical (Must Fix)
|
|
458
|
+
- [ ] All main sections have nav items - users can't find features
|
|
459
|
+
- [ ] Active state is clear - users don't know where they are
|
|
460
|
+
- [ ] Mobile navigation accessible - mobile users blocked
|
|
461
|
+
- [ ] Keyboard navigable (Tab, Enter) - accessibility violation
|
|
462
|
+
- [ ] Uses semantic HTML (nav element) - accessibility violation
|
|
463
|
+
|
|
464
|
+
### Major (Should Fix)
|
|
465
|
+
- [ ] Icons are recognizable - confusion about meaning
|
|
466
|
+
- [ ] Tooltips when collapsed - can't identify items
|
|
467
|
+
- [ ] Mobile drawer closes on navigation - users get stuck
|
|
468
|
+
- [ ] Breadcrumbs on nested pages - users lose context
|
|
469
|
+
- [ ] URL updates reflect navigation state - can't share deep links
|
|
470
|
+
- [ ] Active detection works for nested routes - wrong item highlighted
|
|
471
|
+
|
|
472
|
+
### Minor (Nice to Have)
|
|
473
|
+
- [ ] Collapse state persisted to localStorage - convenience
|
|
474
|
+
- [ ] Section dividers group related items - visual organization
|
|
475
|
+
- [ ] Animations are smooth (<300ms) - polish
|
|
476
|
+
- [ ] Footer shows version/links - helpful info
|