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.
- package/package.json +1 -1
- package/templates/base/AGENTS.md +203 -0
- package/templates/base/apps/server/AGENTS.md +64 -8
- package/templates/base/apps/web/AGENTS.md +82 -8
- package/templates/base/apps/web/src/components/KuckitModuleRoute.tsx +119 -0
- package/templates/base/apps/web/src/components/dashboard/app-sidebar.tsx +120 -0
- package/templates/base/apps/web/src/components/dashboard/dashboard-layout.tsx +46 -0
- package/templates/base/apps/web/src/components/dashboard/dashboard-overview.tsx +24 -0
- package/templates/base/apps/web/src/components/dashboard/index.ts +2 -0
- package/templates/base/apps/web/src/components/dashboard/nav-user.tsx +77 -0
- package/templates/base/apps/web/src/components/ui/avatar.tsx +39 -0
- package/templates/base/apps/web/src/components/ui/breadcrumb.tsx +102 -0
- package/templates/base/apps/web/src/components/ui/collapsible.tsx +21 -0
- package/templates/base/apps/web/src/components/ui/separator.tsx +26 -0
- package/templates/base/apps/web/src/components/ui/sheet.tsx +130 -0
- package/templates/base/apps/web/src/components/ui/sidebar.tsx +694 -0
- package/templates/base/apps/web/src/components/ui/skeleton.tsx +13 -0
- package/templates/base/apps/web/src/components/ui/tooltip.tsx +55 -0
- package/templates/base/apps/web/src/hooks/use-mobile.ts +19 -0
- package/templates/base/apps/web/src/lib/utils.ts +6 -0
- package/templates/base/apps/web/src/modules.client.ts +4 -3
- package/templates/base/apps/web/src/providers/KuckitProvider.tsx +1 -25
- package/templates/base/apps/web/src/routes/$.tsx +14 -0
- package/templates/base/apps/web/src/routes/dashboard/$.tsx +9 -0
- package/templates/base/apps/web/src/routes/dashboard/index.tsx +6 -0
- package/templates/base/apps/web/src/routes/dashboard.tsx +25 -0
- package/templates/base/apps/web/tsconfig.json +5 -1
- package/templates/base/apps/web/vite.config.ts +6 -0
- package/templates/base/packages/api/AGENTS.md +44 -5
- package/templates/base/packages/auth/AGENTS.md +17 -1
- package/templates/base/packages/db/AGENTS.md +16 -1
- package/templates/base/packages/items-module/AGENTS.md +99 -1
- package/templates/base/packages/items-module/src/ui/ItemsPage.tsx +50 -68
- package/templates/base/apps/web/src/lib/kuckit-router.ts +0 -42
package/package.json
CHANGED
package/templates/base/AGENTS.md
CHANGED
|
@@ -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
|
|
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
|
|
12
|
-
|
|
|
13
|
-
| `server.ts`
|
|
14
|
-
| `container.ts`
|
|
15
|
-
| `config/modules.ts`
|
|
16
|
-
| `rpc.ts`
|
|
17
|
-
| `
|
|
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
|
|
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
|
|
12
|
-
|
|
|
13
|
-
| `main.tsx`
|
|
14
|
-
| `modules.client.ts`
|
|
15
|
-
| `providers/KuckitProvider.tsx`
|
|
16
|
-
| `providers/ServicesProvider.tsx`
|
|
17
|
-
| `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
|
+
}
|