create-kuckit-app 0.2.0 → 0.3.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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/templates/base/AGENTS.md +203 -0
  3. package/templates/base/apps/server/AGENTS.md +64 -8
  4. package/templates/base/apps/web/AGENTS.md +82 -8
  5. package/templates/base/apps/web/src/components/KuckitModuleRoute.tsx +119 -0
  6. package/templates/base/apps/web/src/components/dashboard/app-sidebar.tsx +120 -0
  7. package/templates/base/apps/web/src/components/dashboard/dashboard-layout.tsx +46 -0
  8. package/templates/base/apps/web/src/components/dashboard/dashboard-overview.tsx +24 -0
  9. package/templates/base/apps/web/src/components/dashboard/index.ts +2 -0
  10. package/templates/base/apps/web/src/components/dashboard/nav-user.tsx +77 -0
  11. package/templates/base/apps/web/src/components/ui/avatar.tsx +39 -0
  12. package/templates/base/apps/web/src/components/ui/breadcrumb.tsx +102 -0
  13. package/templates/base/apps/web/src/components/ui/collapsible.tsx +21 -0
  14. package/templates/base/apps/web/src/components/ui/separator.tsx +26 -0
  15. package/templates/base/apps/web/src/components/ui/sheet.tsx +130 -0
  16. package/templates/base/apps/web/src/components/ui/sidebar.tsx +694 -0
  17. package/templates/base/apps/web/src/components/ui/skeleton.tsx +13 -0
  18. package/templates/base/apps/web/src/components/ui/tooltip.tsx +55 -0
  19. package/templates/base/apps/web/src/hooks/use-mobile.ts +19 -0
  20. package/templates/base/apps/web/src/lib/utils.ts +6 -0
  21. package/templates/base/apps/web/src/modules.client.ts +4 -3
  22. package/templates/base/apps/web/src/providers/KuckitProvider.tsx +1 -25
  23. package/templates/base/apps/web/src/routes/$.tsx +14 -0
  24. package/templates/base/apps/web/src/routes/dashboard/$.tsx +9 -0
  25. package/templates/base/apps/web/src/routes/dashboard/index.tsx +6 -0
  26. package/templates/base/apps/web/src/routes/dashboard.tsx +25 -0
  27. package/templates/base/apps/web/tsconfig.json +5 -1
  28. package/templates/base/apps/web/vite.config.ts +6 -0
  29. package/templates/base/packages/api/AGENTS.md +44 -5
  30. package/templates/base/packages/auth/AGENTS.md +17 -1
  31. package/templates/base/packages/db/AGENTS.md +16 -1
  32. package/templates/base/packages/items-module/AGENTS.md +99 -1
  33. package/templates/base/packages/items-module/src/ui/ItemsPage.tsx +50 -68
  34. package/templates/base/apps/web/src/lib/kuckit-router.ts +0 -42
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kuckit-app",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Create a new Kuckit application",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -71,6 +71,209 @@ adapters → Implements ports, depends on domain
71
71
  api → Wires everything together
72
72
  ```
73
73
 
74
+ ## SDK Module System
75
+
76
+ > **SDK Documentation**: For comprehensive module patterns, see [@kuckit/sdk](https://github.com/draphonix/kuckit)
77
+
78
+ ### Module Lifecycle
79
+
80
+ Modules are loaded in a specific sequence:
81
+
82
+ ```
83
+ 1. createKuckitContainer()
84
+ └─► Core services registered (logger, db, cache, eventBus)
85
+
86
+ 2. loadKuckitModules({ modules: [...] })
87
+ ├─► Phase 1: register() - DI bindings for all modules
88
+ ├─► Phase 2: registerApi() - API registrations collected
89
+ ├─► Phase 3: onApiRegistrations callback (wire routers HERE)
90
+ ├─► Phase 4: onBootstrap() - Startup logic
91
+ └─► Phase 5: onComplete callback
92
+
93
+ 3. Application runs...
94
+
95
+ 4. disposeContainer()
96
+ └─► For each module: onShutdown()
97
+ ```
98
+
99
+ ### Core Services (CoreCradle)
100
+
101
+ After container creation, these services are available via DI:
102
+
103
+ | Token | Type | Lifetime | Description |
104
+ | ------------------ | ------------------ | --------- | --------------------------- |
105
+ | `config` | `CoreConfig` | Singleton | Application configuration |
106
+ | `db` | Drizzle | Singleton | Database query builder |
107
+ | `dbPool` | `Pool` | Singleton | PostgreSQL connection pool |
108
+ | `logger` | `Logger` | Singleton | Structured logging |
109
+ | `eventBus` | `EventBus` | Singleton | Pub/sub event system |
110
+ | `clock` | `Clock` | Singleton | Time abstraction (testable) |
111
+ | `cacheStore` | `CacheStore` | Singleton | Key-value cache |
112
+ | `rateLimiterStore` | `RateLimiterStore` | Singleton | Rate limiting |
113
+ | `auth` | Better-Auth | Singleton | Authentication utilities |
114
+ | `requestId` | `string` | Scoped | Per-request unique ID |
115
+ | `requestLogger` | `Logger` | Scoped | Logger with request context |
116
+
117
+ ### Server Module Definition
118
+
119
+ ```typescript
120
+ import { defineKuckitModule, asClass, asFunction } from '@kuckit/sdk'
121
+
122
+ export const kuckitModule = defineKuckitModule({
123
+ id: 'myapp.billing',
124
+ displayName: 'Billing',
125
+ version: '1.0.0',
126
+ capabilities: ['nav.item', 'api.public'],
127
+
128
+ register(ctx) {
129
+ // Phase 1: Register DI bindings
130
+ ctx.container.register({
131
+ invoiceRepository: asClass(InvoiceRepository).scoped(),
132
+ createInvoice: asFunction(makeCreateInvoiceUseCase).scoped(),
133
+ })
134
+ },
135
+
136
+ registerApi(ctx) {
137
+ // Phase 2: Register API routes
138
+ ctx.addApiRegistration({
139
+ type: 'rpc-router',
140
+ name: 'invoices',
141
+ router: invoicesRouter,
142
+ })
143
+ },
144
+
145
+ onBootstrap(ctx) {
146
+ // Phase 4: Startup logic (cache warming, logging, etc.)
147
+ ctx.container.resolve('logger').info('Billing module started')
148
+ },
149
+
150
+ onShutdown(ctx) {
151
+ // Cleanup on shutdown
152
+ ctx.container.resolve('logger').info('Billing module stopped')
153
+ },
154
+ })
155
+ ```
156
+
157
+ ### Client Module Definition
158
+
159
+ ```typescript
160
+ import { defineKuckitClientModule } from '@kuckit/sdk-react'
161
+
162
+ export const kuckitClientModule = defineKuckitClientModule({
163
+ id: 'myapp.billing',
164
+ displayName: 'Billing',
165
+ capabilities: ['nav.item', 'dashboard.widget'],
166
+
167
+ routes: [
168
+ {
169
+ id: 'billing-invoices',
170
+ path: '/billing/invoices',
171
+ component: InvoicesPage,
172
+ },
173
+ ],
174
+
175
+ navItems: [
176
+ {
177
+ id: 'billing-nav',
178
+ label: 'Billing',
179
+ href: '/billing/invoices',
180
+ icon: CreditCard,
181
+ order: 50,
182
+ },
183
+ ],
184
+
185
+ slots: {
186
+ 'dashboard.widgets': {
187
+ component: BillingWidget,
188
+ order: 10,
189
+ },
190
+ },
191
+ })
192
+ ```
193
+
194
+ ### Module Capabilities
195
+
196
+ Modules can declare capabilities for discovery:
197
+
198
+ - `nav.item` - Provides navigation items
199
+ - `settings.page` - Has settings page
200
+ - `dashboard.widget` - Provides dashboard widgets
201
+ - `api.webhook` - Exposes webhooks
202
+ - `api.public` - Has public API endpoints
203
+ - `slot.provider` - Provides slot components
204
+
205
+ ### useRpc with TanStack Query
206
+
207
+ Module components use the `useRpc` hook to access the API:
208
+
209
+ ```typescript
210
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
211
+ import { useRpc } from '@kuckit/sdk-react'
212
+
213
+ interface ItemsRpc {
214
+ items: {
215
+ list: (input: Record<string, never>) => Promise<Item[]>
216
+ create: (input: { name: string }) => Promise<Item>
217
+ }
218
+ }
219
+
220
+ function ItemsPage() {
221
+ const rpc = useRpc<ItemsRpc>()
222
+ const queryClient = useQueryClient()
223
+
224
+ const { data: items = [], isLoading } = useQuery({
225
+ queryKey: ['items'],
226
+ queryFn: () => rpc.items.list({}),
227
+ })
228
+
229
+ const createMutation = useMutation({
230
+ mutationFn: (data: { name: string }) => rpc.items.create(data),
231
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
232
+ })
233
+
234
+ // ... component render
235
+ }
236
+ ```
237
+
238
+ ### oRPC Router Wiring (Important)
239
+
240
+ oRPC's `RPCHandler` captures the router object at construction time. Module routers must be wired **before** the handler is created:
241
+
242
+ 1. Modules register routers via `registerApi()` hook
243
+ 2. Server wires routers in `onApiRegistrations` into a **mutable router object**
244
+ 3. `RPCHandler` is created **after** modules are loaded
245
+
246
+ ```typescript
247
+ // apps/server/src/rpc-router-registry.ts
248
+ export const rootRpcRouter = { ...appRouter }
249
+
250
+ export const wireModuleRpcRouters = (registrations: ApiRegistration[]) => {
251
+ for (const reg of registrations) {
252
+ if (reg.type === 'rpc-router') {
253
+ rootRpcRouter[reg.name] = reg.router
254
+ }
255
+ }
256
+ }
257
+ ```
258
+
259
+ ### Typing DI in Routers
260
+
261
+ Use a module-local interface to type `context.di.cradle`:
262
+
263
+ ```typescript
264
+ // In your module's router file
265
+ interface BillingCradle {
266
+ createInvoice: (input: CreateInvoiceInput) => Promise<Invoice>
267
+ }
268
+
269
+ export const invoicesRouter = {
270
+ create: protectedProcedure.input(createInvoiceSchema).handler(async ({ input, context }) => {
271
+ const { createInvoice } = context.di.cradle as BillingCradle
272
+ return createInvoice(input)
273
+ }),
274
+ }
275
+ ```
276
+
74
277
  ## Common Tasks
75
278
 
76
279
  ### Add a new API endpoint
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md - Server App
2
2
 
3
- > See root [AGENTS.md](../../AGENTS.md) for project overview
3
+ > See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
4
4
 
5
5
  ## Purpose
6
6
 
@@ -8,13 +8,69 @@ Express backend hosting oRPC API, Better-Auth authentication, and the Kuckit mod
8
8
 
9
9
  ## Key Files
10
10
 
11
- | File | Purpose |
12
- | ------------------- | ------------------------- |
13
- | `server.ts` | Entry point, bootstrap |
14
- | `container.ts` | DI container setup |
15
- | `config/modules.ts` | Module registration |
16
- | `rpc.ts` | oRPC handler setup |
17
- | `auth.ts` | Better-Auth configuration |
11
+ | File | Purpose |
12
+ | ------------------------ | -------------------------- |
13
+ | `server.ts` | Entry point, bootstrap |
14
+ | `container.ts` | DI container setup |
15
+ | `config/modules.ts` | Module registration |
16
+ | `rpc.ts` | oRPC handler setup |
17
+ | `rpc-router-registry.ts` | Mutable router for modules |
18
+ | `auth.ts` | Better-Auth configuration |
19
+
20
+ ## Module Loading Sequence
21
+
22
+ The server bootstraps in this order:
23
+
24
+ ```
25
+ 1. createKuckitContainer() - Core services (db, logger, cache, etc.)
26
+ 2. loadKuckitModules() with onApiRegistrations callback:
27
+ ├─► register() hooks run (DI bindings)
28
+ ├─► registerApi() hooks run (routers collected)
29
+ ├─► onApiRegistrations() - Wire routers to rootRpcRouter
30
+ └─► onBootstrap() hooks run (startup logic)
31
+ 3. setupRPC() - Create RPCHandler AFTER modules loaded
32
+ 4. Express listens
33
+ ```
34
+
35
+ ## Router Registry Pattern
36
+
37
+ oRPC's `RPCHandler` captures the router at construction time. Modules wire their routers into a **mutable object** before the handler is created:
38
+
39
+ ```typescript
40
+ // rpc-router-registry.ts
41
+ export const rootRpcRouter = { ...appRouter }
42
+
43
+ export const wireModuleRpcRouters = (registrations: ApiRegistration[]) => {
44
+ for (const reg of registrations) {
45
+ if (reg.type === 'rpc-router') {
46
+ rootRpcRouter[reg.name] = reg.router
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ```typescript
53
+ // server.ts
54
+ await loadKuckitModules({
55
+ container,
56
+ modules: getModuleSpecs(),
57
+ onApiRegistrations: (registrations) => {
58
+ wireModuleRpcRouters(registrations) // Wire BEFORE setupRPC()
59
+ },
60
+ })
61
+
62
+ setupRPC(app) // Now create handler with fully-wired router
63
+ ```
64
+
65
+ ## Per-Request Scoping
66
+
67
+ Each HTTP request gets a scoped container with:
68
+
69
+ - `requestId` - Unique request identifier
70
+ - `requestLogger` - Logger with request context
71
+ - `session` - Current user session (if authenticated)
72
+
73
+ Access scoped services in routers via `context.di.cradle`.
18
74
 
19
75
  ## Adding New Modules
20
76
 
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md - Web App
2
2
 
3
- > See root [AGENTS.md](../../AGENTS.md) for project overview
3
+ > See root [AGENTS.md](../../AGENTS.md) for SDK Module System patterns
4
4
 
5
5
  ## Purpose
6
6
 
@@ -8,13 +8,15 @@ React frontend using TanStack Router, TanStack Query, and Kuckit client modules.
8
8
 
9
9
  ## Key Files
10
10
 
11
- | File | Purpose |
12
- | -------------------------------- | --------------------------------- |
13
- | `main.tsx` | App entry point |
14
- | `modules.client.ts` | Client module registration |
15
- | `providers/KuckitProvider.tsx` | Kuckit context setup |
16
- | `providers/ServicesProvider.tsx` | Services context (RPC, auth) |
17
- | `routes/` | TanStack Router file-based routes |
11
+ | File | Purpose |
12
+ | ---------------------------------- | --------------------------------- |
13
+ | `main.tsx` | App entry point |
14
+ | `modules.client.ts` | Client module registration |
15
+ | `providers/KuckitProvider.tsx` | Kuckit context setup |
16
+ | `providers/ServicesProvider.tsx` | Services context (RPC, auth) |
17
+ | `routes/` | TanStack Router file-based routes |
18
+ | `routes/$.tsx` | Catch-all for module routes |
19
+ | `components/KuckitModuleRoute.tsx` | Renders module-registered routes |
18
20
 
19
21
  ## Adding Routes
20
22
 
@@ -24,6 +26,78 @@ Create files in `src/routes/`:
24
26
  - `src/routes/users/$id.tsx` → `/users/:id`
25
27
  - `src/routes/settings/index.tsx` → `/settings`
26
28
 
29
+ **File-based routes take precedence** over module-registered routes.
30
+
31
+ ## Module Route Integration
32
+
33
+ Modules register routes via `defineKuckitClientModule`. These are rendered via a catch-all pattern:
34
+
35
+ 1. Module registers route in `routes` array
36
+ 2. `RouteRegistry` collects routes during `loadKuckitClientModules()`
37
+ 3. Catch-all route (`$.tsx`) matches any unhandled path
38
+ 4. `KuckitModuleRoute` looks up path in registry and renders the component
39
+
40
+ ```tsx
41
+ // routes/$.tsx - Catches module routes
42
+ import { KuckitModuleRoute } from '@/components/KuckitModuleRoute'
43
+
44
+ export function Route() {
45
+ return <KuckitModuleRoute />
46
+ }
47
+ ```
48
+
49
+ ## Context Providers
50
+
51
+ The app wraps content in Kuckit providers:
52
+
53
+ ```tsx
54
+ <ServicesProvider>
55
+ {' '}
56
+ {/* RPC client, auth */}
57
+ <KuckitProvider>
58
+ {' '}
59
+ {/* Nav, slots, routes registries */}
60
+ <KuckitRpcProvider>
61
+ {' '}
62
+ {/* RPC context for modules */}
63
+ {children}
64
+ </KuckitRpcProvider>
65
+ </KuckitProvider>
66
+ </ServicesProvider>
67
+ ```
68
+
69
+ **Available hooks:**
70
+
71
+ - `useRpc<T>()` - Get typed RPC client for API calls
72
+ - `useNavItems()` - Flat navigation items list
73
+ - `useNavTree()` - Hierarchical navigation tree
74
+ - `useSlot(name)` - Get components for a slot
75
+ - `useHasSlot(name)` - Check if slot has content
76
+
77
+ ## useRpc Hook
78
+
79
+ Module components must use `useRpc()` to access the API:
80
+
81
+ ```tsx
82
+ import { useRpc } from '@kuckit/sdk-react'
83
+ import { useQuery } from '@tanstack/react-query'
84
+
85
+ interface MyRpc {
86
+ items: { list: (input: {}) => Promise<Item[]> }
87
+ }
88
+
89
+ function MyComponent() {
90
+ const rpc = useRpc<MyRpc>()
91
+
92
+ const { data } = useQuery({
93
+ queryKey: ['items'],
94
+ queryFn: () => rpc.items.list({}),
95
+ })
96
+ }
97
+ ```
98
+
99
+ **Important**: Never use `import.meta.env.VITE_SERVER_URL` directly in module components - it's unavailable in external packages. The `useRpc` hook provides the correctly-configured client.
100
+
27
101
  ## Using Module UI Components
28
102
 
29
103
  ```tsx
@@ -0,0 +1,119 @@
1
+ import { useRouterState, Link, useNavigate } from '@tanstack/react-router'
2
+ import { useKuckit } from '@/providers/KuckitProvider'
3
+ import { useServices } from '@/providers/ServicesProvider'
4
+ import { useEffect, useState } from 'react'
5
+
6
+ /**
7
+ * Helper to determine if a route path is a root-level route (outside dashboard).
8
+ * Root routes start with '/' but NOT '/dashboard'.
9
+ */
10
+ function isRootLevelRoute(path: string): boolean {
11
+ return path.startsWith('/') && !path.startsWith('/dashboard')
12
+ }
13
+
14
+ /**
15
+ * Dynamic route renderer for Kuckit module routes.
16
+ *
17
+ * This component looks up the current path in the RouteRegistry
18
+ * and renders the corresponding module component if found.
19
+ *
20
+ * Route resolution is context-aware:
21
+ * - When rendered under /dashboard/*: matches dashboard routes
22
+ * - When rendered at root level: matches root-level routes (paths starting with '/' but not '/dashboard/')
23
+ *
24
+ * For root routes with meta.requiresAuth: true, redirects to /login if not authenticated.
25
+ */
26
+ export function KuckitModuleRoute() {
27
+ const { routeRegistry } = useKuckit()
28
+ const { authClient } = useServices()
29
+ const { location } = useRouterState()
30
+ const navigate = useNavigate()
31
+ const pathname = location.pathname
32
+
33
+ const [authChecked, setAuthChecked] = useState(false)
34
+ const [isAuthenticated, setIsAuthenticated] = useState(false)
35
+
36
+ // Determine if we're in dashboard context
37
+ const isDashboardContext = pathname.startsWith('/dashboard')
38
+
39
+ // Filter routes based on context
40
+ const contextRoutes = routeRegistry.getAll().filter((r) => {
41
+ const isRoot = isRootLevelRoute(r.path)
42
+ // In dashboard context: show non-root routes
43
+ // In root context: show root routes
44
+ return isDashboardContext ? !isRoot : isRoot
45
+ })
46
+
47
+ // For dashboard context, also try matching with /dashboard prefix stripped
48
+ const modulePathname = isDashboardContext ? pathname.replace('/dashboard', '') || '/' : pathname
49
+
50
+ // Find matching route
51
+ let routeDef = contextRoutes.find((r) => r.path === pathname)
52
+ if (!routeDef && isDashboardContext) {
53
+ // For dashboard routes, also try matching stripped path
54
+ routeDef = contextRoutes.find((r) => r.path === modulePathname)
55
+ }
56
+
57
+ // Check auth for root routes with requiresAuth
58
+ useEffect(() => {
59
+ async function checkAuth() {
60
+ if (!routeDef) {
61
+ setAuthChecked(true)
62
+ return
63
+ }
64
+
65
+ // Dashboard routes are already protected by the dashboard layout
66
+ if (isDashboardContext) {
67
+ setAuthChecked(true)
68
+ setIsAuthenticated(true)
69
+ return
70
+ }
71
+
72
+ // For root routes, check if auth is required
73
+ if (routeDef.meta?.requiresAuth) {
74
+ const session = await authClient.getSession()
75
+ if (!session.data) {
76
+ // Redirect to login with return URL
77
+ navigate({
78
+ to: '/login',
79
+ search: { redirect: pathname },
80
+ })
81
+ return
82
+ }
83
+ setIsAuthenticated(true)
84
+ }
85
+ setAuthChecked(true)
86
+ }
87
+
88
+ checkAuth()
89
+ }, [routeDef, isDashboardContext, pathname, navigate, authClient])
90
+
91
+ // Show loading while checking auth for protected root routes
92
+ if (!isDashboardContext && routeDef?.meta?.requiresAuth && !authChecked) {
93
+ return (
94
+ <div className="flex items-center justify-center min-h-screen">
95
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
96
+ </div>
97
+ )
98
+ }
99
+
100
+ // Protected root route - waiting for redirect
101
+ if (!isDashboardContext && routeDef?.meta?.requiresAuth && !isAuthenticated) {
102
+ return null
103
+ }
104
+
105
+ if (!routeDef) {
106
+ return (
107
+ <div className="flex flex-col items-center justify-center min-h-[50vh] gap-4">
108
+ <h1 className="text-2xl font-bold">Page Not Found</h1>
109
+ <p className="text-muted-foreground">The page "{pathname}" could not be found.</p>
110
+ <Link to="/" className="text-primary hover:underline">
111
+ Go Home
112
+ </Link>
113
+ </div>
114
+ )
115
+ }
116
+
117
+ const Component = routeDef.component as React.ComponentType
118
+ return <Component />
119
+ }
@@ -0,0 +1,120 @@
1
+ import { Link, useRouterState } from '@tanstack/react-router'
2
+ import { LayoutDashboard, Settings, Blocks, type LucideIcon } from 'lucide-react'
3
+ import { useKuckitNav } from '@kuckit/sdk-react'
4
+ import {
5
+ Sidebar,
6
+ SidebarContent,
7
+ SidebarFooter,
8
+ SidebarGroup,
9
+ SidebarGroupContent,
10
+ SidebarGroupLabel,
11
+ SidebarHeader,
12
+ SidebarMenu,
13
+ SidebarMenuButton,
14
+ SidebarMenuItem,
15
+ SidebarRail,
16
+ } from '@/components/ui/sidebar'
17
+ import { NavUser } from './nav-user'
18
+
19
+ const iconMap: Record<string, LucideIcon> = {
20
+ 'layout-dashboard': LayoutDashboard,
21
+ settings: Settings,
22
+ blocks: Blocks,
23
+ }
24
+
25
+ const coreNavItems = [
26
+ { label: 'Dashboard', path: '/dashboard', icon: LayoutDashboard },
27
+ { label: 'Settings', path: '/dashboard/settings', icon: Settings },
28
+ ]
29
+
30
+ export function AppSidebar() {
31
+ const navRegistry = useKuckitNav()
32
+ const pathname = useRouterState({ select: (s) => s.location.pathname })
33
+
34
+ const mainNavModuleItems = navRegistry.getMainNavItems()
35
+ const moduleItems = navRegistry.getModuleNavItems()
36
+
37
+ return (
38
+ <Sidebar collapsible="icon">
39
+ <SidebarHeader>
40
+ <div className="flex items-center gap-2 px-2 py-2">
41
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-primary/80">
42
+ <span className="text-sm font-bold text-primary-foreground">K</span>
43
+ </div>
44
+ <span className="font-semibold group-data-[collapsible=icon]:hidden">__APP_TITLE__</span>
45
+ </div>
46
+ </SidebarHeader>
47
+
48
+ <SidebarContent>
49
+ <SidebarGroup>
50
+ <SidebarGroupLabel>Navigation</SidebarGroupLabel>
51
+ <SidebarGroupContent>
52
+ <SidebarMenu>
53
+ {coreNavItems.map((item) => (
54
+ <SidebarMenuItem key={item.path}>
55
+ <SidebarMenuButton asChild isActive={pathname === item.path} tooltip={item.label}>
56
+ <Link to={item.path}>
57
+ <item.icon className="h-4 w-4" />
58
+ <span>{item.label}</span>
59
+ </Link>
60
+ </SidebarMenuButton>
61
+ </SidebarMenuItem>
62
+ ))}
63
+
64
+ {mainNavModuleItems.map((item) => {
65
+ const Icon = item.icon ? iconMap[item.icon] : Blocks
66
+ return (
67
+ <SidebarMenuItem key={item.id}>
68
+ <SidebarMenuButton
69
+ asChild
70
+ isActive={pathname === item.path || pathname.startsWith(item.path + '/')}
71
+ tooltip={item.label}
72
+ >
73
+ <Link to={item.path}>
74
+ {Icon && <Icon className="h-4 w-4" />}
75
+ <span>{item.label}</span>
76
+ </Link>
77
+ </SidebarMenuButton>
78
+ </SidebarMenuItem>
79
+ )
80
+ })}
81
+ </SidebarMenu>
82
+ </SidebarGroupContent>
83
+ </SidebarGroup>
84
+
85
+ {moduleItems.length > 0 && (
86
+ <SidebarGroup>
87
+ <SidebarGroupLabel>Modules</SidebarGroupLabel>
88
+ <SidebarGroupContent>
89
+ <SidebarMenu>
90
+ {moduleItems.map((item) => {
91
+ const Icon = item.icon ? iconMap[item.icon] : Blocks
92
+ return (
93
+ <SidebarMenuItem key={item.id}>
94
+ <SidebarMenuButton
95
+ asChild
96
+ isActive={pathname === item.path || pathname.startsWith(item.path + '/')}
97
+ tooltip={item.label}
98
+ >
99
+ <Link to={item.path}>
100
+ {Icon && <Icon className="h-4 w-4" />}
101
+ <span>{item.label}</span>
102
+ </Link>
103
+ </SidebarMenuButton>
104
+ </SidebarMenuItem>
105
+ )
106
+ })}
107
+ </SidebarMenu>
108
+ </SidebarGroupContent>
109
+ </SidebarGroup>
110
+ )}
111
+ </SidebarContent>
112
+
113
+ <SidebarFooter>
114
+ <NavUser />
115
+ </SidebarFooter>
116
+
117
+ <SidebarRail />
118
+ </Sidebar>
119
+ )
120
+ }
@@ -0,0 +1,46 @@
1
+ import type { ReactNode } from 'react'
2
+ import { SidebarProvider, SidebarInset, SidebarTrigger } from '@/components/ui/sidebar'
3
+ import { AppSidebar } from './app-sidebar'
4
+ import { Separator } from '@/components/ui/separator'
5
+ import {
6
+ Breadcrumb,
7
+ BreadcrumbItem,
8
+ BreadcrumbList,
9
+ BreadcrumbPage,
10
+ } from '@/components/ui/breadcrumb'
11
+ import { useRouterState } from '@tanstack/react-router'
12
+
13
+ interface DashboardLayoutProps {
14
+ children: ReactNode
15
+ }
16
+
17
+ export function DashboardLayout({ children }: DashboardLayoutProps) {
18
+ const pathname = useRouterState({ select: (s) => s.location.pathname })
19
+
20
+ const pathSegments = pathname.split('/').filter(Boolean)
21
+ const pageTitle =
22
+ pathSegments.length > 1
23
+ ? pathSegments[pathSegments.length - 1].charAt(0).toUpperCase() +
24
+ pathSegments[pathSegments.length - 1].slice(1)
25
+ : 'Dashboard'
26
+
27
+ return (
28
+ <SidebarProvider>
29
+ <AppSidebar />
30
+ <SidebarInset>
31
+ <header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
32
+ <SidebarTrigger className="-ml-1" />
33
+ <Separator orientation="vertical" className="mr-2 h-4" />
34
+ <Breadcrumb>
35
+ <BreadcrumbList>
36
+ <BreadcrumbItem>
37
+ <BreadcrumbPage>{pageTitle}</BreadcrumbPage>
38
+ </BreadcrumbItem>
39
+ </BreadcrumbList>
40
+ </Breadcrumb>
41
+ </header>
42
+ <main className="flex-1 overflow-auto p-4">{children}</main>
43
+ </SidebarInset>
44
+ </SidebarProvider>
45
+ )
46
+ }