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