@vadimcomanescu/nadicode-design-system 4.0.1 → 4.0.2
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/.agents/skills/seed/SKILL.md +34 -163
- package/.agents/skills/seed/references/animation.md +2 -2
- package/.agents/skills/seed/references/responsive.md +1 -1
- package/README.md +2 -2
- package/eslint-rules/nadicode/rules/no-has-svg-selector.js +1 -1
- package/package.json +1 -2
- package/scripts/ds-check.mjs +0 -10
- package/scripts/sync-seed-skill.mjs +0 -3
- package/.agents/skills/seed/contract.md +0 -110
- package/.agents/skills/seed/intent-map.md +0 -320
- package/.agents/skills/seed/recipes/agency-home.md +0 -311
- package/.agents/skills/seed/recipes/agents-chat.md +0 -305
- package/.agents/skills/seed/recipes/analytics.md +0 -253
- package/.agents/skills/seed/recipes/auth.md +0 -254
- package/.agents/skills/seed/recipes/blog-content.md +0 -307
- package/.agents/skills/seed/recipes/checkout.md +0 -311
- package/.agents/skills/seed/recipes/company-about.md +0 -276
- package/.agents/skills/seed/recipes/company-contact.md +0 -234
- package/.agents/skills/seed/recipes/crud-form.md +0 -233
- package/.agents/skills/seed/recipes/crud-list-detail.md +0 -230
- package/.agents/skills/seed/recipes/dashboard.md +0 -354
- package/.agents/skills/seed/recipes/digital-workers.md +0 -314
- package/.agents/skills/seed/recipes/error-pages.md +0 -199
- package/.agents/skills/seed/recipes/marketing-landing.md +0 -293
- package/.agents/skills/seed/recipes/marketing-shell.md +0 -156
- package/.agents/skills/seed/recipes/navigation-shell.md +0 -787
- package/.agents/skills/seed/recipes/onboarding.md +0 -258
- package/.agents/skills/seed/recipes/pricing.md +0 -271
- package/.agents/skills/seed/recipes/service-detail.md +0 -302
- package/.agents/skills/seed/recipes/settings.md +0 -252
- package/.agents/skills/seed/references/blocks.md +0 -128
- package/.agents/skills/seed/references/components.md +0 -287
- package/.agents/skills/seed/references/icons.md +0 -169
- package/.agents/skills/seed/references/nextjs.md +0 -49
- package/.agents/skills/seed/references/tokens.md +0 -88
|
@@ -1,787 +0,0 @@
|
|
|
1
|
-
# Recipe: Navigation Shell
|
|
2
|
-
|
|
3
|
-
Shared app navigation frame with sidebar, top bar, Cmd+K search, breadcrumbs, and user menu.
|
|
4
|
-
|
|
5
|
-
## Purpose
|
|
6
|
-
|
|
7
|
-
The navigation shell wraps all app-shell pages. It is not a page itself but the persistent frame around page content. This recipe defines the complete app shell layout with working wiring for every navigation subsystem.
|
|
8
|
-
|
|
9
|
-
## Shell
|
|
10
|
-
|
|
11
|
-
`app-shell` (this IS the shell definition)
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Layout Blueprint (Desktop)
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
+--+----------------------------------------------+
|
|
19
|
-
| | [=] Home > Settings [Cmd+K] [🔔] [User] |
|
|
20
|
-
|S | |
|
|
21
|
-
|I | +------------------------------------------+ |
|
|
22
|
-
|D | | | |
|
|
23
|
-
|E | | Page Content | |
|
|
24
|
-
|B | | (filled by child route) | |
|
|
25
|
-
|A | | | |
|
|
26
|
-
|R | | | |
|
|
27
|
-
| | +------------------------------------------+ |
|
|
28
|
-
+--+----------------------------------------------+
|
|
29
|
-
|
|
30
|
-
Sidebar (expanded):
|
|
31
|
-
+------------------+
|
|
32
|
-
| [v] Acme Inc | <-- Workspace switcher (DropdownMenu)
|
|
33
|
-
+------------------+
|
|
34
|
-
| Group: Main |
|
|
35
|
-
| Dashboard |
|
|
36
|
-
| Analytics |
|
|
37
|
-
| Agents |
|
|
38
|
-
+------------------+
|
|
39
|
-
| Group: Manage |
|
|
40
|
-
| Team |
|
|
41
|
-
| Settings |
|
|
42
|
-
+------------------+
|
|
43
|
-
| [Avatar] Jane | <-- NavUser (DropdownMenu)
|
|
44
|
-
+------------------+
|
|
45
|
-
|
|
46
|
-
Mobile: Sidebar hidden, hamburger in top bar opens Sheet overlay
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## Section Sequence
|
|
52
|
-
|
|
53
|
-
### 1. App Shell Layout (the file)
|
|
54
|
-
|
|
55
|
-
This is your app route group layout (e.g. src/app/(app)/layout.tsx). It composes every subsystem.
|
|
56
|
-
|
|
57
|
-
```tsx
|
|
58
|
-
'use client'
|
|
59
|
-
|
|
60
|
-
import { usePathname } from 'next/navigation'
|
|
61
|
-
import {
|
|
62
|
-
Sidebar,
|
|
63
|
-
SidebarProvider,
|
|
64
|
-
SidebarTrigger,
|
|
65
|
-
SidebarInset,
|
|
66
|
-
SidebarHeader,
|
|
67
|
-
SidebarContent,
|
|
68
|
-
SidebarFooter,
|
|
69
|
-
SidebarGroup,
|
|
70
|
-
SidebarGroupLabel,
|
|
71
|
-
SidebarMenu,
|
|
72
|
-
SidebarMenuItem,
|
|
73
|
-
SidebarMenuButton,
|
|
74
|
-
SidebarMenuSub,
|
|
75
|
-
SidebarMenuSubItem,
|
|
76
|
-
SidebarMenuSubButton,
|
|
77
|
-
} from '@vadimcomanescu/nadicode-design-system/sidebar'
|
|
78
|
-
import { seedComponents } from '@vadimcomanescu/nadicode-design-system/catalog/components'
|
|
79
|
-
const { NavUser } = seedComponents
|
|
80
|
-
import { AppBreadcrumb } from '@/components/blocks/AppBreadcrumb'
|
|
81
|
-
import { AppSearch } from '@/components/blocks/AppSearch'
|
|
82
|
-
import { WorkspaceSwitcher } from '@/components/blocks/WorkspaceSwitcher'
|
|
83
|
-
import { ThemeToggle } from '@vadimcomanescu/nadicode-design-system/theme-toggle'
|
|
84
|
-
import { Button } from '@vadimcomanescu/nadicode-design-system/button'
|
|
85
|
-
import { BellIcon } from '@vadimcomanescu/nadicode-design-system/icons/bell'
|
|
86
|
-
import { LayoutDashboardIcon } from '@vadimcomanescu/nadicode-design-system/icons/layout-dashboard'
|
|
87
|
-
import { ChartBarIcon } from '@vadimcomanescu/nadicode-design-system/icons/chart-bar'
|
|
88
|
-
import { BotIcon } from '@vadimcomanescu/nadicode-design-system/icons/bot'
|
|
89
|
-
import { UsersIcon } from '@vadimcomanescu/nadicode-design-system/icons/users'
|
|
90
|
-
import { SettingsIcon } from '@vadimcomanescu/nadicode-design-system/icons/settings'
|
|
91
|
-
|
|
92
|
-
const NAV_ITEMS = [
|
|
93
|
-
{
|
|
94
|
-
group: 'Main',
|
|
95
|
-
items: [
|
|
96
|
-
{ label: 'Dashboard', href: '/dashboard', icon: LayoutDashboardIcon },
|
|
97
|
-
{ label: 'Analytics', href: '/analytics', icon: ChartBarIcon },
|
|
98
|
-
{
|
|
99
|
-
label: 'Agents',
|
|
100
|
-
href: '/agents',
|
|
101
|
-
icon: BotIcon,
|
|
102
|
-
children: [
|
|
103
|
-
{ label: 'Active', href: '/agents/active' },
|
|
104
|
-
{ label: 'History', href: '/agents/history' },
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
],
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
group: 'Manage',
|
|
111
|
-
items: [
|
|
112
|
-
{ label: 'Team', href: '/team', icon: UsersIcon },
|
|
113
|
-
{ label: 'Settings', href: '/settings', icon: SettingsIcon },
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
]
|
|
117
|
-
|
|
118
|
-
export default function AppShellLayout({
|
|
119
|
-
children,
|
|
120
|
-
}: {
|
|
121
|
-
children: React.ReactNode
|
|
122
|
-
}) {
|
|
123
|
-
const pathname = usePathname()
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
<SidebarProvider>
|
|
127
|
-
<Sidebar>
|
|
128
|
-
<SidebarHeader>
|
|
129
|
-
<WorkspaceSwitcher />
|
|
130
|
-
</SidebarHeader>
|
|
131
|
-
|
|
132
|
-
<SidebarContent>
|
|
133
|
-
{NAV_ITEMS.map((group) => (
|
|
134
|
-
<SidebarGroup key={group.group}>
|
|
135
|
-
<SidebarGroupLabel>{group.group}</SidebarGroupLabel>
|
|
136
|
-
<SidebarMenu>
|
|
137
|
-
{group.items.map((item) => (
|
|
138
|
-
<SidebarMenuItem key={item.href}>
|
|
139
|
-
<SidebarMenuButton
|
|
140
|
-
asChild
|
|
141
|
-
isActive={pathname === item.href || pathname.startsWith(item.href + '/')}
|
|
142
|
-
tooltip={item.label}
|
|
143
|
-
>
|
|
144
|
-
<a href={item.href}>
|
|
145
|
-
<item.icon size={16} />
|
|
146
|
-
{item.label}
|
|
147
|
-
</a>
|
|
148
|
-
</SidebarMenuButton>
|
|
149
|
-
|
|
150
|
-
{item.children && (
|
|
151
|
-
<SidebarMenuSub>
|
|
152
|
-
{item.children.map((child) => (
|
|
153
|
-
<SidebarMenuSubItem key={child.href}>
|
|
154
|
-
<SidebarMenuSubButton
|
|
155
|
-
asChild
|
|
156
|
-
isActive={pathname === child.href}
|
|
157
|
-
>
|
|
158
|
-
<a href={child.href}>{child.label}</a>
|
|
159
|
-
</SidebarMenuSubButton>
|
|
160
|
-
</SidebarMenuSubItem>
|
|
161
|
-
))}
|
|
162
|
-
</SidebarMenuSub>
|
|
163
|
-
)}
|
|
164
|
-
</SidebarMenuItem>
|
|
165
|
-
))}
|
|
166
|
-
</SidebarMenu>
|
|
167
|
-
</SidebarGroup>
|
|
168
|
-
))}
|
|
169
|
-
</SidebarContent>
|
|
170
|
-
|
|
171
|
-
<SidebarFooter>
|
|
172
|
-
<NavUser
|
|
173
|
-
user={{ name: 'Jane Smith', email: 'jane@acme.com', avatar: '/avatars/jane.jpg' }}
|
|
174
|
-
items={[
|
|
175
|
-
{ label: 'Profile', href: '/settings/profile' },
|
|
176
|
-
{ label: 'Billing', href: '/settings/billing' },
|
|
177
|
-
{ label: 'Sign out', onClick: handleSignOut },
|
|
178
|
-
]}
|
|
179
|
-
/>
|
|
180
|
-
</SidebarFooter>
|
|
181
|
-
</Sidebar>
|
|
182
|
-
|
|
183
|
-
<SidebarInset>
|
|
184
|
-
<header className="flex items-center gap-3 px-4 py-3 border-b border-border">
|
|
185
|
-
<SidebarTrigger />
|
|
186
|
-
<AppBreadcrumb />
|
|
187
|
-
<div className="ml-auto flex items-center gap-2">
|
|
188
|
-
<AppSearch />
|
|
189
|
-
<Button variant="ghost" size="icon" className="relative">
|
|
190
|
-
<BellIcon size={16} />
|
|
191
|
-
<span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-destructive" />
|
|
192
|
-
</Button>
|
|
193
|
-
<ThemeToggle />
|
|
194
|
-
</div>
|
|
195
|
-
</header>
|
|
196
|
-
|
|
197
|
-
<div className="flex-1 p-4 lg:p-6">
|
|
198
|
-
{children}
|
|
199
|
-
</div>
|
|
200
|
-
</SidebarInset>
|
|
201
|
-
</SidebarProvider>
|
|
202
|
-
)
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
---
|
|
207
|
-
|
|
208
|
-
### 2. Breadcrumb Generation
|
|
209
|
-
|
|
210
|
-
The breadcrumb builds itself from the current pathname and a route config map. This is a reusable block, not inline logic.
|
|
211
|
-
|
|
212
|
-
**Consumer file: src/components/blocks/AppBreadcrumb.tsx**
|
|
213
|
-
|
|
214
|
-
```tsx
|
|
215
|
-
'use client'
|
|
216
|
-
|
|
217
|
-
import { usePathname } from 'next/navigation'
|
|
218
|
-
import {
|
|
219
|
-
Breadcrumb,
|
|
220
|
-
BreadcrumbList,
|
|
221
|
-
BreadcrumbItem,
|
|
222
|
-
BreadcrumbLink,
|
|
223
|
-
BreadcrumbPage,
|
|
224
|
-
BreadcrumbSeparator,
|
|
225
|
-
BreadcrumbEllipsis,
|
|
226
|
-
} from '@vadimcomanescu/nadicode-design-system/breadcrumb'
|
|
227
|
-
import { Fragment } from 'react'
|
|
228
|
-
|
|
229
|
-
// Map route segments to display labels.
|
|
230
|
-
// Dynamic segments use a placeholder; the caller can resolve them.
|
|
231
|
-
const ROUTE_LABELS: Record<string, string> = {
|
|
232
|
-
dashboard: 'Dashboard',
|
|
233
|
-
analytics: 'Analytics',
|
|
234
|
-
agents: 'Agents',
|
|
235
|
-
active: 'Active',
|
|
236
|
-
history: 'History',
|
|
237
|
-
team: 'Team',
|
|
238
|
-
settings: 'Settings',
|
|
239
|
-
profile: 'Profile',
|
|
240
|
-
billing: 'Billing',
|
|
241
|
-
security: 'Security',
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Max visible crumbs before collapsing middle segments.
|
|
245
|
-
const MAX_VISIBLE = 4
|
|
246
|
-
|
|
247
|
-
export function AppBreadcrumb() {
|
|
248
|
-
const pathname = usePathname()
|
|
249
|
-
const segments = pathname.split('/').filter(Boolean)
|
|
250
|
-
|
|
251
|
-
if (segments.length === 0) return null
|
|
252
|
-
|
|
253
|
-
// Build crumb list: [{label, href}]
|
|
254
|
-
const crumbs = segments.map((segment, i) => ({
|
|
255
|
-
label: ROUTE_LABELS[segment] ?? decodeURIComponent(segment),
|
|
256
|
-
href: '/' + segments.slice(0, i + 1).join('/'),
|
|
257
|
-
}))
|
|
258
|
-
|
|
259
|
-
// Collapse if too many crumbs
|
|
260
|
-
const shouldCollapse = crumbs.length > MAX_VISIBLE
|
|
261
|
-
const visible = shouldCollapse
|
|
262
|
-
? [crumbs[0], ...crumbs.slice(-2)]
|
|
263
|
-
: crumbs
|
|
264
|
-
|
|
265
|
-
return (
|
|
266
|
-
<Breadcrumb>
|
|
267
|
-
<BreadcrumbList>
|
|
268
|
-
{visible.map((crumb, i) => (
|
|
269
|
-
<Fragment key={crumb.href}>
|
|
270
|
-
{i > 0 && <BreadcrumbSeparator />}
|
|
271
|
-
{shouldCollapse && i === 1 && (
|
|
272
|
-
<>
|
|
273
|
-
<BreadcrumbItem>
|
|
274
|
-
<BreadcrumbEllipsis />
|
|
275
|
-
</BreadcrumbItem>
|
|
276
|
-
<BreadcrumbSeparator />
|
|
277
|
-
</>
|
|
278
|
-
)}
|
|
279
|
-
<BreadcrumbItem>
|
|
280
|
-
{i === visible.length - 1 ? (
|
|
281
|
-
<BreadcrumbPage>{crumb.label}</BreadcrumbPage>
|
|
282
|
-
) : (
|
|
283
|
-
<BreadcrumbLink href={crumb.href}>{crumb.label}</BreadcrumbLink>
|
|
284
|
-
)}
|
|
285
|
-
</BreadcrumbItem>
|
|
286
|
-
</Fragment>
|
|
287
|
-
))}
|
|
288
|
-
</BreadcrumbList>
|
|
289
|
-
</Breadcrumb>
|
|
290
|
-
)
|
|
291
|
-
}
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
**Dynamic segments** (e.g., `/agents/[id]`): extend `ROUTE_LABELS` with a resolver function, or pass a `resolveLabel` prop that fetches the entity name from a cache.
|
|
295
|
-
|
|
296
|
-
---
|
|
297
|
-
|
|
298
|
-
### 3. SearchCommand (Cmd+K) Wiring
|
|
299
|
-
|
|
300
|
-
The search palette needs three things: route items, action items, and a router integration. Wrap it in a Dialog triggered by Cmd+K.
|
|
301
|
-
|
|
302
|
-
**Consumer file: src/components/blocks/AppSearch.tsx**
|
|
303
|
-
|
|
304
|
-
```tsx
|
|
305
|
-
'use client'
|
|
306
|
-
|
|
307
|
-
import { useCallback, useMemo, useState } from 'react'
|
|
308
|
-
import { useRouter } from 'next/navigation'
|
|
309
|
-
import { Dialog, DialogContent } from '@vadimcomanescu/nadicode-design-system/dialog'
|
|
310
|
-
import { SearchCommand, SearchResult } from '@vadimcomanescu/nadicode-design-system/search-command'
|
|
311
|
-
import { Button } from '@vadimcomanescu/nadicode-design-system/button'
|
|
312
|
-
import { SearchIcon } from '@vadimcomanescu/nadicode-design-system/icons/search'
|
|
313
|
-
import { useHotkey } from '@/hooks/useHotkey'
|
|
314
|
-
|
|
315
|
-
// Route items: all navigable pages in the app.
|
|
316
|
-
const PAGE_ITEMS: SearchResult[] = [
|
|
317
|
-
{ id: 'dashboard', title: 'Dashboard', category: 'Pages', description: 'Overview and KPIs' },
|
|
318
|
-
{ id: 'analytics', title: 'Analytics', category: 'Pages', description: 'Charts and trends' },
|
|
319
|
-
{ id: 'agents', title: 'Agents', category: 'Pages', description: 'AI agent management' },
|
|
320
|
-
{ id: 'team', title: 'Team', category: 'Pages', description: 'Team members' },
|
|
321
|
-
{ id: 'settings', title: 'Settings', category: 'Pages', description: 'App configuration' },
|
|
322
|
-
]
|
|
323
|
-
|
|
324
|
-
// Action items: commands that do something (not navigation).
|
|
325
|
-
const ACTION_ITEMS: SearchResult[] = [
|
|
326
|
-
{ id: 'new-agent', title: 'Create Agent', category: 'Actions' },
|
|
327
|
-
{ id: 'invite-user', title: 'Invite Team Member', category: 'Actions' },
|
|
328
|
-
{ id: 'toggle-theme', title: 'Toggle Theme', category: 'Actions' },
|
|
329
|
-
]
|
|
330
|
-
|
|
331
|
-
// Combine all items into a single searchable list.
|
|
332
|
-
const ALL_ITEMS = [...PAGE_ITEMS, ...ACTION_ITEMS]
|
|
333
|
-
|
|
334
|
-
// Map item IDs to routes (pages) or callbacks (actions).
|
|
335
|
-
const ROUTE_MAP: Record<string, string> = {
|
|
336
|
-
dashboard: '/dashboard',
|
|
337
|
-
analytics: '/analytics',
|
|
338
|
-
agents: '/agents',
|
|
339
|
-
team: '/team',
|
|
340
|
-
settings: '/settings',
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
export function AppSearch() {
|
|
344
|
-
const [open, setOpen] = useState(false)
|
|
345
|
-
const [query, setQuery] = useState('')
|
|
346
|
-
const router = useRouter()
|
|
347
|
-
|
|
348
|
-
// Cmd+K to open
|
|
349
|
-
useHotkey('mod+k', (e) => {
|
|
350
|
-
e.preventDefault()
|
|
351
|
-
setOpen(true)
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
// Filter results by query
|
|
355
|
-
const results = useMemo(() => {
|
|
356
|
-
if (!query) return ALL_ITEMS
|
|
357
|
-
const q = query.toLowerCase()
|
|
358
|
-
return ALL_ITEMS.filter(
|
|
359
|
-
(item) =>
|
|
360
|
-
item.title.toLowerCase().includes(q) ||
|
|
361
|
-
item.description?.toLowerCase().includes(q),
|
|
362
|
-
)
|
|
363
|
-
}, [query])
|
|
364
|
-
|
|
365
|
-
const handleSelect = useCallback(
|
|
366
|
-
(result: SearchResult) => {
|
|
367
|
-
setOpen(false)
|
|
368
|
-
setQuery('')
|
|
369
|
-
|
|
370
|
-
const route = ROUTE_MAP[result.id]
|
|
371
|
-
if (route) {
|
|
372
|
-
router.push(route)
|
|
373
|
-
return
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Handle action items
|
|
377
|
-
switch (result.id) {
|
|
378
|
-
case 'toggle-theme':
|
|
379
|
-
document.documentElement.classList.toggle('dark')
|
|
380
|
-
break
|
|
381
|
-
case 'new-agent':
|
|
382
|
-
router.push('/agents/new')
|
|
383
|
-
break
|
|
384
|
-
case 'invite-user':
|
|
385
|
-
router.push('/team?invite=true')
|
|
386
|
-
break
|
|
387
|
-
}
|
|
388
|
-
},
|
|
389
|
-
[router],
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
return (
|
|
393
|
-
<>
|
|
394
|
-
<Button
|
|
395
|
-
variant="ghost"
|
|
396
|
-
size="sm"
|
|
397
|
-
className="gap-2 text-text-secondary"
|
|
398
|
-
onClick={() => setOpen(true)}
|
|
399
|
-
>
|
|
400
|
-
<SearchIcon size={16} />
|
|
401
|
-
<span className="hidden sm:inline">Search...</span>
|
|
402
|
-
<kbd className="hidden sm:inline text-xs bg-surface-raised px-1.5 py-0.5 rounded border border-border">
|
|
403
|
-
⌘K
|
|
404
|
-
</kbd>
|
|
405
|
-
</Button>
|
|
406
|
-
|
|
407
|
-
<Dialog open={open} onOpenChange={setOpen}>
|
|
408
|
-
<DialogContent className="p-0 max-w-lg">
|
|
409
|
-
<SearchCommand
|
|
410
|
-
value={query}
|
|
411
|
-
onChange={setQuery}
|
|
412
|
-
results={results}
|
|
413
|
-
onSelect={handleSelect}
|
|
414
|
-
placeholder="Search pages and actions..."
|
|
415
|
-
/>
|
|
416
|
-
</DialogContent>
|
|
417
|
-
</Dialog>
|
|
418
|
-
</>
|
|
419
|
-
)
|
|
420
|
-
}
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
**Extending with recent items**: store last 5 selected IDs in `localStorage`, prepend them as a "Recent" category on mount.
|
|
424
|
-
|
|
425
|
-
---
|
|
426
|
-
|
|
427
|
-
### 4. NavUser Wiring
|
|
428
|
-
|
|
429
|
-
NavUser sits in the sidebar footer. It needs auth data and menu items.
|
|
430
|
-
|
|
431
|
-
```tsx
|
|
432
|
-
// In the layout, wire NavUser to your auth context:
|
|
433
|
-
|
|
434
|
-
import { useAuth } from '@/hooks/useAuth' // your auth hook
|
|
435
|
-
|
|
436
|
-
function AppShellSidebarFooter() {
|
|
437
|
-
const { user, signOut } = useAuth()
|
|
438
|
-
|
|
439
|
-
if (!user) return null
|
|
440
|
-
|
|
441
|
-
return (
|
|
442
|
-
<SidebarFooter>
|
|
443
|
-
<NavUser
|
|
444
|
-
user={{
|
|
445
|
-
name: user.name,
|
|
446
|
-
email: user.email,
|
|
447
|
-
avatar: user.avatarUrl,
|
|
448
|
-
}}
|
|
449
|
-
items={[
|
|
450
|
-
{ label: 'Profile', href: '/settings/profile' },
|
|
451
|
-
{ label: 'Billing', href: '/settings/billing' },
|
|
452
|
-
]}
|
|
453
|
-
footer={
|
|
454
|
-
<button
|
|
455
|
-
onClick={signOut}
|
|
456
|
-
className="w-full text-left px-2 py-1.5 text-sm text-text-secondary hover:text-text-primary"
|
|
457
|
-
>
|
|
458
|
-
Sign out
|
|
459
|
-
</button>
|
|
460
|
-
}
|
|
461
|
-
/>
|
|
462
|
-
</SidebarFooter>
|
|
463
|
-
)
|
|
464
|
-
}
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
NavUser renders a `DropdownMenu` with the user's avatar and name as trigger. On mobile, the dropdown opens upward (`side="top"`). On desktop, it opens right (`side="right"`).
|
|
468
|
-
|
|
469
|
-
---
|
|
470
|
-
|
|
471
|
-
### 5. Mobile Sidebar (Sheet Overlay)
|
|
472
|
-
|
|
473
|
-
The Sidebar component handles mobile automatically. No extra code needed, but you need to understand the behavior:
|
|
474
|
-
|
|
475
|
-
```
|
|
476
|
-
MOBILE BEHAVIOR
|
|
477
|
-
===============
|
|
478
|
-
|
|
479
|
-
Trigger: SidebarTrigger (hamburger icon in top bar)
|
|
480
|
-
Container: Sheet overlay (slides from left, 80% viewport width)
|
|
481
|
-
Backdrop: Semi-transparent overlay, tap to close
|
|
482
|
-
Close: Tap backdrop, tap X button, or navigate to a new route
|
|
483
|
-
Focus trap: Yes (keyboard focus stays inside sheet)
|
|
484
|
-
Scroll: SidebarContent scrolls independently
|
|
485
|
-
Width: var(--sidebar-width), max 320px
|
|
486
|
-
|
|
487
|
-
DESKTOP BEHAVIOR
|
|
488
|
-
================
|
|
489
|
-
|
|
490
|
-
< lg: Sidebar hidden (mobile mode, Sheet overlay)
|
|
491
|
-
lg: Sidebar visible, collapsible to icon-only (var(--sidebar-width-icon) = 3rem)
|
|
492
|
-
xl: Sidebar expanded by default (var(--sidebar-width) = 16rem)
|
|
493
|
-
Collapse: SidebarTrigger or Cmd+B toggles between expanded and icon-only
|
|
494
|
-
Rail: SidebarRail provides invisible drag handle on sidebar edge
|
|
495
|
-
Cookie: Expanded/collapsed state persists via cookie
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
To close the mobile sidebar on navigation:
|
|
499
|
-
|
|
500
|
-
```tsx
|
|
501
|
-
// In nav item click handlers or via useEffect:
|
|
502
|
-
const { setOpenMobile } = useSidebar()
|
|
503
|
-
|
|
504
|
-
// Close mobile sidebar after navigating
|
|
505
|
-
function handleNavClick(href: string) {
|
|
506
|
-
setOpenMobile(false)
|
|
507
|
-
router.push(href)
|
|
508
|
-
}
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
---
|
|
512
|
-
|
|
513
|
-
### 6. Top Bar Composition
|
|
514
|
-
|
|
515
|
-
The top bar is a `<header>` inside `SidebarInset`. It contains four zones:
|
|
516
|
-
|
|
517
|
-
```
|
|
518
|
-
+---+------------------+-----------------------------------+
|
|
519
|
-
| = | Breadcrumb | [Search] [Notifications] [Theme] |
|
|
520
|
-
+---+------------------+-----------------------------------+
|
|
521
|
-
^ ^ ^ ^ ^
|
|
522
|
-
| | | | |
|
|
523
|
-
Trigger AppBreadcrumb AppSearch NotifButton ThemeToggle
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
**Notification button pattern** (badge + dropdown):
|
|
527
|
-
|
|
528
|
-
```tsx
|
|
529
|
-
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@vadimcomanescu/nadicode-design-system/dropdown-menu'
|
|
530
|
-
import { BellIcon } from '@vadimcomanescu/nadicode-design-system/icons/bell'
|
|
531
|
-
import { Button } from '@vadimcomanescu/nadicode-design-system/button'
|
|
532
|
-
import { Badge } from '@vadimcomanescu/nadicode-design-system/badge'
|
|
533
|
-
import { ScrollArea } from '@vadimcomanescu/nadicode-design-system/scroll-area'
|
|
534
|
-
|
|
535
|
-
function NotificationButton({ notifications }: { notifications: Notification[] }) {
|
|
536
|
-
const unreadCount = notifications.filter((n) => !n.read).length
|
|
537
|
-
|
|
538
|
-
return (
|
|
539
|
-
<DropdownMenu>
|
|
540
|
-
<DropdownMenuTrigger asChild>
|
|
541
|
-
<Button variant="ghost" size="icon" className="relative">
|
|
542
|
-
<BellIcon size={16} />
|
|
543
|
-
{unreadCount > 0 && (
|
|
544
|
-
<span className="absolute -top-0.5 -right-0.5 size-4 rounded-full bg-destructive text-destructive-foreground text-[10px] flex items-center justify-center">
|
|
545
|
-
{unreadCount}
|
|
546
|
-
</span>
|
|
547
|
-
)}
|
|
548
|
-
</Button>
|
|
549
|
-
</DropdownMenuTrigger>
|
|
550
|
-
<DropdownMenuContent align="end" className="w-80">
|
|
551
|
-
<div className="px-3 py-2 border-b border-border">
|
|
552
|
-
<span className="text-sm font-medium">Notifications</span>
|
|
553
|
-
</div>
|
|
554
|
-
<ScrollArea className="max-h-64">
|
|
555
|
-
{notifications.length === 0 ? (
|
|
556
|
-
<div className="px-3 py-6 text-center text-sm text-text-secondary">
|
|
557
|
-
No notifications
|
|
558
|
-
</div>
|
|
559
|
-
) : (
|
|
560
|
-
notifications.map((n) => (
|
|
561
|
-
<DropdownMenuItem key={n.id} className="flex flex-col items-start gap-1 py-2">
|
|
562
|
-
<span className="text-sm">{n.title}</span>
|
|
563
|
-
<span className="text-xs text-text-secondary">{n.timeAgo}</span>
|
|
564
|
-
</DropdownMenuItem>
|
|
565
|
-
))
|
|
566
|
-
)}
|
|
567
|
-
</ScrollArea>
|
|
568
|
-
</DropdownMenuContent>
|
|
569
|
-
</DropdownMenu>
|
|
570
|
-
)
|
|
571
|
-
}
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
---
|
|
575
|
-
|
|
576
|
-
### 7. Workspace Switcher
|
|
577
|
-
|
|
578
|
-
Logo area in sidebar header doubles as a workspace/org switcher.
|
|
579
|
-
|
|
580
|
-
**Consumer file: src/components/blocks/WorkspaceSwitcher.tsx**
|
|
581
|
-
|
|
582
|
-
```tsx
|
|
583
|
-
'use client'
|
|
584
|
-
|
|
585
|
-
import {
|
|
586
|
-
DropdownMenu,
|
|
587
|
-
DropdownMenuTrigger,
|
|
588
|
-
DropdownMenuContent,
|
|
589
|
-
DropdownMenuItem,
|
|
590
|
-
DropdownMenuSeparator,
|
|
591
|
-
} from '@vadimcomanescu/nadicode-design-system/dropdown-menu'
|
|
592
|
-
import { SidebarMenuButton } from '@vadimcomanescu/nadicode-design-system/sidebar'
|
|
593
|
-
import { ChevronsUpDownIcon } from '@vadimcomanescu/nadicode-design-system/icons/chevrons-up-down'
|
|
594
|
-
import { PlusIcon } from '@vadimcomanescu/nadicode-design-system/icons/plus'
|
|
595
|
-
import { Avatar, AvatarFallback, AvatarImage } from '@vadimcomanescu/nadicode-design-system/avatar'
|
|
596
|
-
|
|
597
|
-
interface Workspace {
|
|
598
|
-
id: string
|
|
599
|
-
name: string
|
|
600
|
-
logo?: string
|
|
601
|
-
plan: string
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
export function WorkspaceSwitcher({
|
|
605
|
-
workspaces,
|
|
606
|
-
activeId,
|
|
607
|
-
onSwitch,
|
|
608
|
-
}: {
|
|
609
|
-
workspaces: Workspace[]
|
|
610
|
-
activeId: string
|
|
611
|
-
onSwitch: (id: string) => void
|
|
612
|
-
}) {
|
|
613
|
-
const active = workspaces.find((w) => w.id === activeId)
|
|
614
|
-
|
|
615
|
-
return (
|
|
616
|
-
<DropdownMenu>
|
|
617
|
-
<DropdownMenuTrigger asChild>
|
|
618
|
-
<SidebarMenuButton size="lg" className="gap-2">
|
|
619
|
-
<Avatar className="size-6 rounded-md">
|
|
620
|
-
<AvatarImage src={active?.logo} />
|
|
621
|
-
<AvatarFallback className="rounded-md text-xs">
|
|
622
|
-
{active?.name.slice(0, 2).toUpperCase()}
|
|
623
|
-
</AvatarFallback>
|
|
624
|
-
</Avatar>
|
|
625
|
-
<div className="flex-1 text-left">
|
|
626
|
-
<span className="text-sm font-medium truncate">{active?.name}</span>
|
|
627
|
-
<span className="text-xs text-text-secondary block">{active?.plan}</span>
|
|
628
|
-
</div>
|
|
629
|
-
<ChevronsUpDownIcon size={14} className="text-text-secondary" />
|
|
630
|
-
</SidebarMenuButton>
|
|
631
|
-
</DropdownMenuTrigger>
|
|
632
|
-
<DropdownMenuContent align="start" className="w-56">
|
|
633
|
-
{workspaces.map((ws) => (
|
|
634
|
-
<DropdownMenuItem
|
|
635
|
-
key={ws.id}
|
|
636
|
-
onClick={() => onSwitch(ws.id)}
|
|
637
|
-
className={ws.id === activeId ? 'bg-sidebar-accent' : ''}
|
|
638
|
-
>
|
|
639
|
-
<Avatar className="size-5 rounded-md mr-2">
|
|
640
|
-
<AvatarImage src={ws.logo} />
|
|
641
|
-
<AvatarFallback className="rounded-md text-[10px]">
|
|
642
|
-
{ws.name.slice(0, 2).toUpperCase()}
|
|
643
|
-
</AvatarFallback>
|
|
644
|
-
</Avatar>
|
|
645
|
-
{ws.name}
|
|
646
|
-
</DropdownMenuItem>
|
|
647
|
-
))}
|
|
648
|
-
<DropdownMenuSeparator />
|
|
649
|
-
<DropdownMenuItem>
|
|
650
|
-
<PlusIcon size={14} className="mr-2" />
|
|
651
|
-
Create workspace
|
|
652
|
-
</DropdownMenuItem>
|
|
653
|
-
</DropdownMenuContent>
|
|
654
|
-
</DropdownMenu>
|
|
655
|
-
)
|
|
656
|
-
}
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
---
|
|
660
|
-
|
|
661
|
-
## Navigation Rules
|
|
662
|
-
|
|
663
|
-
1. **Max 3 levels**: Group > Item > Sub-item. No deeper nesting.
|
|
664
|
-
2. **Group labels required**: Every `SidebarGroup` must have `SidebarGroupLabel` (ESLint enforced).
|
|
665
|
-
3. **Active state**: Use `isActive` prop on `SidebarMenuButton`. Match with `pathname === href || pathname.startsWith(href + '/')`.
|
|
666
|
-
4. **Icons required**: Every top-level nav item has an icon from `@vadimcomanescu/nadicode-design-system/icons/`.
|
|
667
|
-
5. **Max 7 top-level items**: Keep primary nav focused. Use groups to organize.
|
|
668
|
-
6. **Labels from config**: Nav labels come from the `NAV_ITEMS` config array, not hardcoded in JSX.
|
|
669
|
-
7. **Close on navigate (mobile)**: Call `setOpenMobile(false)` before `router.push()`.
|
|
670
|
-
8. **Breadcrumb on every page**: Every app-shell page must show breadcrumb context in the top bar.
|
|
671
|
-
|
|
672
|
-
---
|
|
673
|
-
|
|
674
|
-
## Animation Storyboard
|
|
675
|
-
|
|
676
|
-
```
|
|
677
|
-
ANIMATION STORYBOARD
|
|
678
|
-
====================
|
|
679
|
-
BUDGET: 200ms | SPRING: snappy | REDUCED: none (shell is instant)
|
|
680
|
-
|
|
681
|
-
T+0ms [sidebar] Sidebar visible (instant)
|
|
682
|
-
T+0ms [topbar] Top bar visible (instant)
|
|
683
|
-
T+0ms [content] Page content area visible (instant)
|
|
684
|
-
|
|
685
|
-
SIDEBAR COLLAPSE/EXPAND:
|
|
686
|
-
T+0ms [sidebar] Width transitions {CSS ease-in-out-cubic, 200ms}
|
|
687
|
-
T+0ms [labels] Text fades out (collapse) {opacity, 100ms}
|
|
688
|
-
T+100ms [icons] Icons center-align {CSS ease-out, 100ms}
|
|
689
|
-
|
|
690
|
-
MOBILE SIDEBAR OPEN:
|
|
691
|
-
T+0ms [backdrop] Backdrop fades in {opacity 0->1, 150ms}
|
|
692
|
-
T+0ms [sheet] Sidebar slides from left {translateX(-100% -> 0), snappy spring}
|
|
693
|
-
|
|
694
|
-
CMD+K OPEN:
|
|
695
|
-
T+0ms [overlay] Backdrop fades in {fadeIn, 150ms}
|
|
696
|
-
T+0ms [dialog] Command palette scales in {scaleIn 0.95->1, snappy}
|
|
697
|
-
|
|
698
|
-
REDUCED MOTION: All transitions instant
|
|
699
|
-
```
|
|
700
|
-
|
|
701
|
-
---
|
|
702
|
-
|
|
703
|
-
## Required Components
|
|
704
|
-
|
|
705
|
-
| Component | Import Path | Purpose |
|
|
706
|
-
|-----------|-------------|---------|
|
|
707
|
-
| `Sidebar` (full system) | `@vadimcomanescu/nadicode-design-system/sidebar` | App sidebar with all subcomponents |
|
|
708
|
-
| `NavUser` | `@vadimcomanescu/nadicode-design-system/catalog/components` via `seedComponents` | User menu in sidebar footer |
|
|
709
|
-
| `AppBreadcrumb` | Consumer block (see Section 2) | Route-aware breadcrumb |
|
|
710
|
-
| `AppSearch` | Consumer block (see Section 3) | Cmd+K search palette |
|
|
711
|
-
| `WorkspaceSwitcher` | Consumer block (see Section 7) | Org/workspace picker |
|
|
712
|
-
| `SearchCommand` | `@vadimcomanescu/nadicode-design-system/search-command` | Search UI primitive |
|
|
713
|
-
| `Breadcrumb` (full system) | `@vadimcomanescu/nadicode-design-system/breadcrumb` | Breadcrumb primitives |
|
|
714
|
-
| `Dialog` | `@vadimcomanescu/nadicode-design-system/dialog` | Cmd+K dialog wrapper |
|
|
715
|
-
| `DropdownMenu` | `@vadimcomanescu/nadicode-design-system/dropdown-menu` | Notifications, workspace switcher |
|
|
716
|
-
| `ThemeToggle` | `@vadimcomanescu/nadicode-design-system/theme-toggle` | Light/dark switch |
|
|
717
|
-
| `Button` | `@vadimcomanescu/nadicode-design-system/button` | Top bar actions |
|
|
718
|
-
| `ScrollArea` | `@vadimcomanescu/nadicode-design-system/scroll-area` | Notification dropdown scroll |
|
|
719
|
-
| `Avatar` | `@vadimcomanescu/nadicode-design-system/avatar` | User and workspace avatars |
|
|
720
|
-
|
|
721
|
-
### Allowed (optional)
|
|
722
|
-
|
|
723
|
-
`NotificationCenter`, `Badge` (notification count), `StyleToggle`, `Tooltip`, `ContextMenu`, `Menubar`
|
|
724
|
-
|
|
725
|
-
### Forbidden
|
|
726
|
-
|
|
727
|
-
Marketing blocks (`HeroBlock`, `FooterBlock`), chart components, form components, agentic components in the shell itself
|
|
728
|
-
|
|
729
|
-
---
|
|
730
|
-
|
|
731
|
-
## Responsive Contract
|
|
732
|
-
|
|
733
|
-
| Breakpoint | Sidebar | Top Bar | Breadcrumb |
|
|
734
|
-
|------------|---------|---------|------------|
|
|
735
|
-
| Mobile | Hidden, Sheet overlay via SidebarTrigger | Always visible, hamburger left | Hidden (not enough space) |
|
|
736
|
-
| `sm:` | Same as mobile | Same | Visible, max 2 crumbs |
|
|
737
|
-
| `lg:` | Collapsible (icon-only or full, Cmd+B) | Visible with all actions | Full breadcrumb trail |
|
|
738
|
-
| `xl:` | Full sidebar (expanded by default) | Same | Same |
|
|
739
|
-
|
|
740
|
-
---
|
|
741
|
-
|
|
742
|
-
## Styling Rules
|
|
743
|
-
|
|
744
|
-
- Sidebar: `bg-sidebar` token (not `bg-background`)
|
|
745
|
-
- Sidebar border: `border-sidebar-border`
|
|
746
|
-
- Active item: `bg-sidebar-accent text-sidebar-accent-foreground`
|
|
747
|
-
- Top bar: `border-b border-border`, same `bg-background` as content
|
|
748
|
-
- Content area: `bg-background`
|
|
749
|
-
- No raw palette colors
|
|
750
|
-
- Sidebar width: `var(--sidebar-width)` (16rem) and `var(--sidebar-width-icon)` (3rem)
|
|
751
|
-
- Workspace switcher: `SidebarMenuButton size="lg"` for consistent sizing
|
|
752
|
-
|
|
753
|
-
---
|
|
754
|
-
|
|
755
|
-
## Accessibility
|
|
756
|
-
|
|
757
|
-
- Sidebar: `<nav aria-label="Main navigation">`
|
|
758
|
-
- Groups: `role="group"` with `aria-labelledby` pointing to group label
|
|
759
|
-
- Active item: `aria-current="page"` (handled by `isActive` prop)
|
|
760
|
-
- SidebarTrigger: `aria-label="Toggle sidebar"`, `aria-expanded`
|
|
761
|
-
- Cmd+K: keyboard-triggered (Cmd/Ctrl+K), focus trapped in dialog
|
|
762
|
-
- Skip-nav link: `<SkipNav />` before sidebar targets `#main-content`
|
|
763
|
-
- Mobile sheet: focus trap, Escape to close, backdrop click to close
|
|
764
|
-
- Notification button: `aria-label="Notifications"`, badge announces count
|
|
765
|
-
- Breadcrumb: `<nav aria-label="breadcrumb">` with `aria-current="page"` on last item
|
|
766
|
-
|
|
767
|
-
---
|
|
768
|
-
|
|
769
|
-
## Reference Implementations
|
|
770
|
-
|
|
771
|
-
- `src/app/(showcase)/layout.tsx` (showcase shell, uses AnimatedTabs instead of Sidebar)
|
|
772
|
-
- `src/components/ui/Sidebar.tsx` (sidebar system)
|
|
773
|
-
- `src/components/blocks/NavUser.tsx` (user menu)
|
|
774
|
-
- `src/components/ui/SearchCommand.tsx` (search primitive)
|
|
775
|
-
- `src/components/ui/Breadcrumb.tsx` (breadcrumb primitives)
|
|
776
|
-
- `src/components/ui/Command.tsx` (cmdk wrapper)
|
|
777
|
-
|
|
778
|
-
---
|
|
779
|
-
|
|
780
|
-
## Verification
|
|
781
|
-
|
|
782
|
-
```bash
|
|
783
|
-
npx tsc --noEmit
|
|
784
|
-
npm run lint
|
|
785
|
-
npm run test
|
|
786
|
-
npx vitest run src/test/opinion-navigation.test.ts
|
|
787
|
-
```
|