wcz-layout 7.6.0 → 7.6.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.
Potentially problematic release.
This version of wcz-layout might be problematic. Click here for more details.
- package/dist/{RouterListItemButton-CvfZk2zD.js → RouterListItemButton-DeaQB4ym.js} +1 -1
- package/dist/{RouterListItemButton-CvfZk2zD.js.map → RouterListItemButton-DeaQB4ym.js.map} +1 -1
- package/dist/components/core/Layout.d.ts +1 -1
- package/dist/components/core/navigation/NavigationList.d.ts +4 -4
- package/dist/components/core/navigation/NavigationListItem.d.ts +3 -3
- package/dist/components.js +2 -2
- package/dist/hooks.js +1 -1
- package/dist/index.js +617 -621
- package/dist/index.js.map +1 -1
- package/dist/lib/auth/msalClient.d.ts +8 -2
- package/dist/middleware.js +11 -11
- package/dist/models/Navigation.d.ts +23 -11
- package/dist/{queries-DzKY6YXz.js → queries-D-DV5lCw.js} +3 -3
- package/dist/{queries-DzKY6YXz.js.map → queries-D-DV5lCw.js.map} +1 -1
- package/dist/query.js +2 -2
- package/dist/{queryClient-uWNhcABg.js → queryClient-B__OEZ70.js} +1 -1
- package/dist/{queryClient-uWNhcABg.js.map → queryClient-B__OEZ70.js.map} +1 -1
- package/dist/{msalClient-BLrbVP5z.js → utils-C4oJ0tr5.js} +58 -47
- package/dist/utils-C4oJ0tr5.js.map +1 -0
- package/dist/utils.js +5 -5
- package/package.json +38 -6
- package/skills/api-routes/SKILL.md +251 -0
- package/skills/auth/SKILL.md +268 -0
- package/skills/data-grid/SKILL.md +229 -0
- package/skills/database-schema/SKILL.md +182 -0
- package/skills/dialogs-notifications/SKILL.md +241 -0
- package/skills/forms-validation/SKILL.md +331 -0
- package/skills/forms-validation/references/field-components.md +212 -0
- package/skills/layout-navigation/SKILL.md +259 -0
- package/skills/project-initialization/SKILL.md +181 -0
- package/skills/project-structure/SKILL.md +157 -0
- package/skills/tanstack-db-collections/SKILL.md +270 -0
- package/skills/ui-pages/SKILL.md +278 -0
- package/dist/msalClient-BLrbVP5z.js.map +0 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: project-structure
|
|
3
|
+
description: >
|
|
4
|
+
Domain-scoped co-location rules for TanStack Router file-based routing.
|
|
5
|
+
Dash-prefix convention (-components/, -hooks/) excludes folders from
|
|
6
|
+
route generation. Domain-specific code lives in route folders; shared
|
|
7
|
+
code in src/components/, src/hooks/. Drizzle schemas in src/lib/db/schemas/,
|
|
8
|
+
Zod schemas in src/schemas/, collections in src/lib/db/collections/.
|
|
9
|
+
Activate when deciding where to place a new file or organizing a feature.
|
|
10
|
+
type: lifecycle
|
|
11
|
+
library: wcz-layout
|
|
12
|
+
library_version: "7.6.1"
|
|
13
|
+
requires:
|
|
14
|
+
- project-initialization
|
|
15
|
+
sources:
|
|
16
|
+
- "wcz-layout:src/routeTree.gen.ts"
|
|
17
|
+
- "TanStack/router:docs/router/api/file-based-routing.md"
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
This skill builds on project-initialization. Read it first for env and plugin setup.
|
|
21
|
+
|
|
22
|
+
# Project Structure Conventions
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
A typical project follows this structure:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
src/
|
|
30
|
+
├── components/ # SHARED components (used across multiple domains)
|
|
31
|
+
│ ├── StyledCard.tsx
|
|
32
|
+
│ └── DataGridToolbar.tsx
|
|
33
|
+
├── hooks/ # SHARED hooks
|
|
34
|
+
├── lib/
|
|
35
|
+
│ ├── auth/
|
|
36
|
+
│ │ ├── permissions.ts # AD group → permission key mapping
|
|
37
|
+
│ │ └── scopes.ts # OAuth scope definitions
|
|
38
|
+
│ ├── db/
|
|
39
|
+
│ │ ├── schemas/ # Drizzle pgTable definitions
|
|
40
|
+
│ │ │ └── todo.ts
|
|
41
|
+
│ │ ├── collections/ # TanStack DB collections
|
|
42
|
+
│ │ │ └── todoCollection.ts
|
|
43
|
+
│ │ └── migrations/ # Drizzle migrations
|
|
44
|
+
│ └── theme.ts
|
|
45
|
+
├── middleware/ # Custom middleware (e.g. databaseMiddleware)
|
|
46
|
+
├── schemas/ # Zod validation schemas (derived from Drizzle)
|
|
47
|
+
│ └── todo.ts
|
|
48
|
+
├── locales/ # i18n translation files
|
|
49
|
+
│ ├── en.json
|
|
50
|
+
│ └── cs.json
|
|
51
|
+
├── routes/
|
|
52
|
+
│ ├── __root.tsx # Root layout with LayoutProvider
|
|
53
|
+
│ ├── index.tsx # Home page
|
|
54
|
+
│ ├── api/
|
|
55
|
+
│ │ ├── health.ts # Health check endpoint
|
|
56
|
+
│ │ └── todos/
|
|
57
|
+
│ │ ├── index.ts # GET / POST handlers
|
|
58
|
+
│ │ └── $id.ts # GET / PUT / DELETE by id
|
|
59
|
+
│ └── todos/
|
|
60
|
+
│ ├── -components/ # Domain-specific components (dash prefix!)
|
|
61
|
+
│ │ └── Form.tsx
|
|
62
|
+
│ ├── index.tsx # List page
|
|
63
|
+
│ ├── create.tsx # Create page
|
|
64
|
+
│ ├── $id.tsx # Detail page
|
|
65
|
+
│ └── edit.$id.tsx # Edit page
|
|
66
|
+
├── env.ts # Environment variable validation
|
|
67
|
+
├── router.tsx # Router configuration
|
|
68
|
+
├── start.ts # TanStack Start configuration
|
|
69
|
+
└── routeTree.gen.ts # AUTO-GENERATED — never edit
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Core Patterns
|
|
73
|
+
|
|
74
|
+
### Domain-scoped co-location
|
|
75
|
+
|
|
76
|
+
When a component, hook, or utility is used only within one route domain, place it inside that route folder with a dash-prefix directory:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
src/routes/todos/-components/Form.tsx # Only used in /todos routes
|
|
80
|
+
src/routes/todos/-hooks/useTodoFilters.ts # Only used in /todos routes
|
|
81
|
+
src/routes/orders/-components/OrderCard.tsx
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
When the same component is reused across multiple domains, move it to the top-level shared directory:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
src/components/StyledCard.tsx # Used by both /todos and /orders
|
|
88
|
+
src/hooks/useDebounce.ts # Used everywhere
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Data layer file placement
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
src/lib/db/schemas/todo.ts # Drizzle table: pgTable("todos", { ... })
|
|
95
|
+
src/schemas/todo.ts # Zod schema: createSelectSchema(todoTable, { ... })
|
|
96
|
+
src/lib/db/collections/todoCollection.ts # TanStack DB collection
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Common Mistakes
|
|
100
|
+
|
|
101
|
+
### CRITICAL Missing dash prefix on route-scoped folders
|
|
102
|
+
|
|
103
|
+
Wrong:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
src/routes/todos/components/Form.tsx
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Correct:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
src/routes/todos/-components/Form.tsx
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
TanStack Router treats folders without a dash prefix as route segments. `components/Form.tsx` becomes a `/todos/components/Form` route. The dash prefix (`-components/`) tells the router to ignore this directory.
|
|
116
|
+
|
|
117
|
+
Source: TanStack/router:docs/router/api/file-based-routing.md
|
|
118
|
+
|
|
119
|
+
### CRITICAL Editing routeTree.gen.ts
|
|
120
|
+
|
|
121
|
+
Wrong:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// src/routeTree.gen.ts — manually adding a route
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Correct:
|
|
128
|
+
|
|
129
|
+
Create a new file in `src/routes/` and let TanStack Router auto-generate the route tree.
|
|
130
|
+
|
|
131
|
+
This file is auto-generated and overwritten on every route change. Manual edits are silently lost.
|
|
132
|
+
|
|
133
|
+
Source: wcz-layout:src/routeTree.gen.ts header comment
|
|
134
|
+
|
|
135
|
+
### HIGH Placing domain-specific component in src/components/
|
|
136
|
+
|
|
137
|
+
Wrong:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// src/components/TodoForm.tsx — only used in /todos routes
|
|
141
|
+
export function TodoForm() {
|
|
142
|
+
return <form>...</form>;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Correct:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// src/routes/todos/-components/TodoForm.tsx
|
|
150
|
+
export function TodoForm() {
|
|
151
|
+
return <form>...</form>;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Components used only within one route domain should live in that domain's `-components/` folder. `src/components/` is reserved for components shared across multiple domains.
|
|
156
|
+
|
|
157
|
+
Source: maintainer interview
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-db-collections
|
|
3
|
+
description: >
|
|
4
|
+
Wire TanStack DB createCollection with queryCollectionOptions for optimistic
|
|
5
|
+
client-side data. Set up axios instance with MSAL token interceptor using
|
|
6
|
+
getAccessToken(scopeKey). CRUD mutation handlers (onInsert, onUpdate, onDelete).
|
|
7
|
+
Use shared queryClient from wcz-layout/query. Consume with useLiveQuery /
|
|
8
|
+
useLiveSuspenseQuery. Activate when creating or modifying data collections.
|
|
9
|
+
type: core
|
|
10
|
+
library: wcz-layout
|
|
11
|
+
library_version: "7.6.1"
|
|
12
|
+
requires:
|
|
13
|
+
- database-schema
|
|
14
|
+
- api-routes
|
|
15
|
+
sources:
|
|
16
|
+
- "wcz-layout:src/lib/db/collections/"
|
|
17
|
+
- "wcz-layout:src/exports/query.ts"
|
|
18
|
+
- "wcz-layout:src/lib/queryClient.ts"
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# TanStack DB Collections
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
Collection files live in `src/lib/db/collections/`:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
src/lib/db/collections/todoCollection.ts
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Core Patterns
|
|
32
|
+
|
|
33
|
+
### Creating a collection
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// src/lib/db/collections/todoCollection.ts
|
|
37
|
+
import { createCollection } from "@tanstack/react-db";
|
|
38
|
+
import { queryCollectionOptions } from "@tanstack/query-db-collection";
|
|
39
|
+
import axios from "axios";
|
|
40
|
+
import { getAccessToken, queryClient } from "wcz-layout";
|
|
41
|
+
import { TodoSchema } from "~/schemas/todo";
|
|
42
|
+
import type { Todo } from "~/schemas/todo";
|
|
43
|
+
|
|
44
|
+
const api = axios.create({ baseURL: "/api/todos" });
|
|
45
|
+
|
|
46
|
+
api.interceptors.request.use(async (config) => {
|
|
47
|
+
const accessToken = await getAccessToken("api");
|
|
48
|
+
config.headers.set("Authorization", `Bearer ${accessToken}`);
|
|
49
|
+
return config;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const todoCollection = createCollection<Todo>({
|
|
53
|
+
id: "todos",
|
|
54
|
+
getKey: (item) => item.id,
|
|
55
|
+
...queryCollectionOptions({
|
|
56
|
+
queryClient,
|
|
57
|
+
queryKey: ["todos"],
|
|
58
|
+
queryFn: async () => {
|
|
59
|
+
const response = await api.get<Todo[]>("");
|
|
60
|
+
return response.data;
|
|
61
|
+
},
|
|
62
|
+
schema: TodoSchema,
|
|
63
|
+
onInsert: async (item) => {
|
|
64
|
+
await api.post("", item);
|
|
65
|
+
},
|
|
66
|
+
onUpdate: async (item) => {
|
|
67
|
+
await api.put(`/${item.id}`, item);
|
|
68
|
+
},
|
|
69
|
+
onDelete: async (item) => {
|
|
70
|
+
await api.delete(`/${item.id}`);
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Choosing the scope key
|
|
77
|
+
|
|
78
|
+
The `getAccessToken(scopeKey)` argument depends on the target service:
|
|
79
|
+
|
|
80
|
+
| Target | Scope key | When |
|
|
81
|
+
| ------------------- | --------- | --------------------------------- |
|
|
82
|
+
| Your own API routes | `"api"` | Default — calls to `/api/*` |
|
|
83
|
+
| Microsoft Graph | `"graph"` | Calls to `graph.microsoft.com` |
|
|
84
|
+
| File microservice | `"file"` | Calls to the file storage service |
|
|
85
|
+
|
|
86
|
+
Scope keys are defined in your app's `src/lib/auth/scopes.ts`.
|
|
87
|
+
|
|
88
|
+
### Consuming collection data in components
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { useLiveQuery } from "@tanstack/react-db";
|
|
92
|
+
import { todoCollection } from "~/lib/db/collections/todoCollection";
|
|
93
|
+
|
|
94
|
+
function TodoList() {
|
|
95
|
+
const { data: todos } = useLiveQuery((query) =>
|
|
96
|
+
query.from({ todos: todoCollection })
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<ul>
|
|
101
|
+
{todos.map((todo) => (
|
|
102
|
+
<li key={todo.id}>{todo.name}</li>
|
|
103
|
+
))}
|
|
104
|
+
</ul>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
For Suspense boundaries, use `useLiveSuspenseQuery` instead.
|
|
110
|
+
|
|
111
|
+
### Optimistic mutations
|
|
112
|
+
|
|
113
|
+
Collections handle mutations optimistically. Insert, update, and delete operations update the local collection immediately and sync to the server via `onInsert`, `onUpdate`, `onDelete`:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { todoCollection } from "~/lib/db/collections/todoCollection";
|
|
117
|
+
import { uuidv7 } from "uuidv7";
|
|
118
|
+
|
|
119
|
+
// Insert — collection updates instantly, API call runs in background
|
|
120
|
+
todoCollection.insert({
|
|
121
|
+
id: uuidv7(),
|
|
122
|
+
name: "New task",
|
|
123
|
+
isCompleted: false,
|
|
124
|
+
createdBy: user.email,
|
|
125
|
+
createdAt: new Date(),
|
|
126
|
+
updatedAt: new Date(),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Update
|
|
130
|
+
todoCollection.update({ ...existingTodo, name: "Updated name" });
|
|
131
|
+
|
|
132
|
+
// Delete
|
|
133
|
+
todoCollection.delete(existingTodo);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Common Mistakes
|
|
137
|
+
|
|
138
|
+
### CRITICAL Using useQuery / useSuspenseQuery instead of TanStack DB
|
|
139
|
+
|
|
140
|
+
Wrong:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { useQuery } from "@tanstack/react-query";
|
|
144
|
+
|
|
145
|
+
const { data } = useQuery({
|
|
146
|
+
queryKey: ["todos"],
|
|
147
|
+
queryFn: fetchTodos,
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Correct:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { useLiveQuery } from "@tanstack/react-db";
|
|
155
|
+
import { todoCollection } from "~/lib/db/collections/todoCollection";
|
|
156
|
+
|
|
157
|
+
const { data } = useLiveQuery((query) => query.from({ todos: todoCollection }));
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The enforced pattern is TanStack DB collections with `useLiveQuery` / `useLiveSuspenseQuery` for all API data. Plain TanStack Query bypasses the optimistic update layer.
|
|
161
|
+
|
|
162
|
+
Source: maintainer interview
|
|
163
|
+
|
|
164
|
+
### CRITICAL Creating a new QueryClient instead of using shared one
|
|
165
|
+
|
|
166
|
+
Wrong:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
170
|
+
const queryClient = new QueryClient();
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Correct:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { queryClient } from "wcz-layout/query";
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
wcz-layout exports a singleton `queryClient`. Creating a new one breaks cache sharing and SSR query integration.
|
|
180
|
+
|
|
181
|
+
Source: wcz-layout:src/lib/queryClient.ts
|
|
182
|
+
|
|
183
|
+
### HIGH Forgetting token interceptor on axios instance
|
|
184
|
+
|
|
185
|
+
Wrong:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const api = axios.create({ baseURL: "/api/todos" });
|
|
189
|
+
// No interceptor — requests will return 401
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Correct:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { getAccessToken } from "wcz-layout/utils";
|
|
196
|
+
|
|
197
|
+
const api = axios.create({ baseURL: "/api/todos" });
|
|
198
|
+
api.interceptors.request.use(async (config) => {
|
|
199
|
+
const accessToken = await getAccessToken("api");
|
|
200
|
+
config.headers.set("Authorization", `Bearer ${accessToken}`);
|
|
201
|
+
return config;
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
API endpoints require Bearer token authentication. Without the interceptor, all requests return 401.
|
|
206
|
+
|
|
207
|
+
Source: consumer project example
|
|
208
|
+
|
|
209
|
+
### HIGH Using getKey that returns non-unique values
|
|
210
|
+
|
|
211
|
+
Wrong:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
export const todoCollection = createCollection<Todo>({
|
|
215
|
+
id: "todos",
|
|
216
|
+
getKey: (item) => item.name, // names can be duplicated!
|
|
217
|
+
...queryCollectionOptions({ ... }),
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Correct:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
export const todoCollection = createCollection<Todo>({
|
|
225
|
+
id: "todos",
|
|
226
|
+
getKey: (item) => item.id, // uuid — always unique
|
|
227
|
+
...queryCollectionOptions({ ... }),
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`getKey` must return a unique identifier for each item. Using a non-unique field causes silent collection corruption — items overwrite each other.
|
|
232
|
+
|
|
233
|
+
Source: TanStack DB documentation
|
|
234
|
+
|
|
235
|
+
### MEDIUM Forgetting schema option in queryCollectionOptions
|
|
236
|
+
|
|
237
|
+
Wrong:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
...queryCollectionOptions({
|
|
241
|
+
queryClient,
|
|
242
|
+
queryKey: ["todos"],
|
|
243
|
+
queryFn: async () => (await api.get("")).data,
|
|
244
|
+
// no schema — API responses are not validated
|
|
245
|
+
}),
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Correct:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
...queryCollectionOptions({
|
|
252
|
+
queryClient,
|
|
253
|
+
queryKey: ["todos"],
|
|
254
|
+
queryFn: async () => (await api.get("")).data,
|
|
255
|
+
schema: TodoSchema,
|
|
256
|
+
}),
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The `schema` option enables runtime validation of API responses. Omitting it removes type safety on incoming data — malformed payloads pass silently.
|
|
260
|
+
|
|
261
|
+
Source: consumer project example
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
See also:
|
|
266
|
+
|
|
267
|
+
- skills/ui-pages/SKILL.md — Pages consume collections via useLiveQuery.
|
|
268
|
+
- skills/database-schema/SKILL.md — Same Zod schema used as collection schema.
|
|
269
|
+
- skills/api-routes/SKILL.md — Collections consume API routes via axios baseURL.
|
|
270
|
+
- skills/data-grid/SKILL.md — Grid rows come from useLiveQuery results.
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ui-pages
|
|
3
|
+
description: >
|
|
4
|
+
Build route pages (index, detail, create, edit) with TanStack Router
|
|
5
|
+
createFileRoute. Use Router-bridged MUI components: RouterButton,
|
|
6
|
+
RouterLink, RouterIconButton, RouterFab, RouterTab, RouterListItemButton,
|
|
7
|
+
RouterGridActionsCellItem. Type-safe navigation with to prop. Route params,
|
|
8
|
+
search params, beforeLoad with requirePermission. Client-first rendering
|
|
9
|
+
(defaultSsr: false). Activate when creating or modifying page routes.
|
|
10
|
+
type: core
|
|
11
|
+
library: wcz-layout
|
|
12
|
+
library_version: "7.6.1"
|
|
13
|
+
requires:
|
|
14
|
+
- tanstack-db-collections
|
|
15
|
+
sources:
|
|
16
|
+
- "wcz-layout:src/components/router/"
|
|
17
|
+
- "wcz-layout:src/routes/"
|
|
18
|
+
- "wcz-layout:src/start.ts"
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# UI Pages & Routing
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
Page routes live under `src/routes/` following TanStack Router file-based conventions:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
src/routes/
|
|
29
|
+
├── __root.tsx # Root layout (LayoutProvider)
|
|
30
|
+
├── index.tsx # Home page (/)
|
|
31
|
+
└── todos/
|
|
32
|
+
├── -components/ # Domain-scoped components (dash prefix!)
|
|
33
|
+
│ └── TodoForm.tsx
|
|
34
|
+
├── index.tsx # /todos — list page
|
|
35
|
+
├── create.tsx # /todos/create — create page
|
|
36
|
+
├── $id.tsx # /todos/$id — detail page
|
|
37
|
+
└── edit.$id.tsx # /todos/edit/$id — edit page
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Core Patterns
|
|
41
|
+
|
|
42
|
+
### List page
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// src/routes/todos/index.tsx
|
|
46
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
47
|
+
import { useLiveQuery } from "@tanstack/react-db";
|
|
48
|
+
import { RouterButton } from "wcz-layout/components";
|
|
49
|
+
import { todoCollection } from "~/lib/db/collections/todoCollection";
|
|
50
|
+
|
|
51
|
+
export const Route = createFileRoute("/todos/")({
|
|
52
|
+
component: TodosPage,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function TodosPage() {
|
|
56
|
+
const { data: todos } = useLiveQuery((query) =>
|
|
57
|
+
query.from({ todos: todoCollection })
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div>
|
|
62
|
+
<RouterButton to="/todos/create" variant="contained">
|
|
63
|
+
Create Todo
|
|
64
|
+
</RouterButton>
|
|
65
|
+
{todos.map((todo) => (
|
|
66
|
+
<RouterLink key={todo.id} to="/todos/$id" params={{ id: todo.id }}>
|
|
67
|
+
{todo.name}
|
|
68
|
+
</RouterLink>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Detail page with route params
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// src/routes/todos/$id.tsx
|
|
79
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
80
|
+
|
|
81
|
+
export const Route = createFileRoute("/todos/$id")({
|
|
82
|
+
component: TodoDetailPage,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function TodoDetailPage() {
|
|
86
|
+
const { id } = Route.useParams();
|
|
87
|
+
// use id to filter collection data or fetch detail
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Permission-guarded page
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
95
|
+
import { requirePermission } from "wcz-layout/utils";
|
|
96
|
+
|
|
97
|
+
export const Route = createFileRoute("/admin/")({
|
|
98
|
+
beforeLoad: requirePermission("admin"),
|
|
99
|
+
component: AdminPage,
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`requirePermission` is a route `beforeLoad` guard that checks if the current user has the specified permission. Redirects to unauthorized if not.
|
|
104
|
+
|
|
105
|
+
### Router-bridged MUI components
|
|
106
|
+
|
|
107
|
+
These components merge MUI props with TanStack Router `LinkProps`. Use `to` for internal navigation (type-safe), `href` only for external URLs:
|
|
108
|
+
|
|
109
|
+
| Component | MUI base | Use case |
|
|
110
|
+
| --------------------------- | ------------------- | ----------------------- |
|
|
111
|
+
| `RouterButton` | Button | Navigation buttons |
|
|
112
|
+
| `RouterLink` | Link (Typography) | Inline text links |
|
|
113
|
+
| `RouterIconButton` | IconButton | Icon-only navigation |
|
|
114
|
+
| `RouterFab` | Fab | Floating action buttons |
|
|
115
|
+
| `RouterTab` | Tab | Tab-based navigation |
|
|
116
|
+
| `RouterListItemButton` | ListItemButton | Sidebar/list navigation |
|
|
117
|
+
| `RouterGridActionsCellItem` | GridActionsCellItem | DataGrid row actions |
|
|
118
|
+
|
|
119
|
+
All accept the same TanStack Router link props: `to`, `params`, `search`, `hash`.
|
|
120
|
+
|
|
121
|
+
### Root route structure
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// src/routes/__root.tsx
|
|
125
|
+
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
|
|
126
|
+
import { LayoutProvider, rootRouteHead } from "wcz-layout";
|
|
127
|
+
import type { ProvidersProps } from "wcz-layout";
|
|
128
|
+
|
|
129
|
+
export const Route = createRootRouteWithContext()({
|
|
130
|
+
head: rootRouteHead,
|
|
131
|
+
component: RootComponent,
|
|
132
|
+
notFoundComponent: RouterNotFound,
|
|
133
|
+
errorComponent: RouterError,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
function RootComponent() {
|
|
137
|
+
return (
|
|
138
|
+
<LayoutProvider theme={theme} navigation={navigation} options={options}>
|
|
139
|
+
<Outlet />
|
|
140
|
+
</LayoutProvider>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Common Mistakes
|
|
146
|
+
|
|
147
|
+
### CRITICAL Using React Router or window.location for navigation
|
|
148
|
+
|
|
149
|
+
Wrong:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { useNavigate } from "react-router-dom";
|
|
153
|
+
// or
|
|
154
|
+
window.location.href = "/todos";
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Correct:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
161
|
+
const navigate = useNavigate();
|
|
162
|
+
navigate({ to: "/todos" });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This stack uses TanStack Router exclusively. React Router does not exist in the dependency tree. `window.location` causes a full page reload, bypassing the SPA router.
|
|
166
|
+
|
|
167
|
+
Source: copilot-instructions.md
|
|
168
|
+
|
|
169
|
+
### HIGH Using MUI Button with href instead of RouterButton
|
|
170
|
+
|
|
171
|
+
Wrong:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
<Button href="/todos/create">Create</Button>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Correct:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { RouterButton } from "wcz-layout/components";
|
|
181
|
+
|
|
182
|
+
<RouterButton to="/todos/create" variant="contained">
|
|
183
|
+
Create
|
|
184
|
+
</RouterButton>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Plain MUI `Button` with `href` causes a full page reload. `RouterButton` uses TanStack Router's `createLink` for SPA navigation with type-safe routes.
|
|
188
|
+
|
|
189
|
+
Source: wcz-layout:src/components/router/RouterButton.tsx
|
|
190
|
+
|
|
191
|
+
### HIGH Assuming SSR is enabled
|
|
192
|
+
|
|
193
|
+
Wrong:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// Expecting server-side data in a route loader
|
|
197
|
+
export const Route = createFileRoute("/todos/")({
|
|
198
|
+
loader: async () => {
|
|
199
|
+
return { todos: await fetchTodos() }; // runs on server — but SSR is off
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Correct:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// Use TanStack DB collections for client-side data
|
|
208
|
+
function TodosPage() {
|
|
209
|
+
const { data } = useLiveQuery((q) => q.from({ todos: todoCollection }));
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`start.ts` sets `defaultSsr: false`. Pages render client-side first. Do not rely on SSR data fetching patterns — use TanStack DB collections instead.
|
|
214
|
+
|
|
215
|
+
Source: wcz-layout:src/start.ts
|
|
216
|
+
|
|
217
|
+
### MEDIUM Omitting suppressHydrationWarning on html element
|
|
218
|
+
|
|
219
|
+
The root route's `<html>` element must include `suppressHydrationWarning` because i18n language detection and color scheme can differ between server and client initial renders.
|
|
220
|
+
|
|
221
|
+
Source: wcz-layout:src/routes/\_\_root.tsx
|
|
222
|
+
|
|
223
|
+
### HIGH Using useMemo or useCallback
|
|
224
|
+
|
|
225
|
+
Wrong:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const filteredTodos = useMemo(() => todos.filter((t) => t.isCompleted), [todos]);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Correct:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const filteredTodos = todos.filter((t) => t.isCompleted);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
React Compiler handles memoization automatically. Manual `useMemo` / `useCallback` is forbidden per project conventions.
|
|
238
|
+
|
|
239
|
+
Source: copilot-instructions.md
|
|
240
|
+
|
|
241
|
+
Cross-skill: See also skills/forms-validation/SKILL.md § Common Mistakes
|
|
242
|
+
|
|
243
|
+
### HIGH Tension: MUI component API vs. TanStack Router types
|
|
244
|
+
|
|
245
|
+
Router-bridged components merge MUI props with TanStack Router `LinkProps`. Use `to` for internal routes and `href` only for external URLs. Passing `href` for an internal route causes a full page reload.
|
|
246
|
+
|
|
247
|
+
See also: skills/data-grid/SKILL.md § Common Mistakes
|
|
248
|
+
|
|
249
|
+
### HIGH Using theme.palette for dark mode instead of theme.applyStyles
|
|
250
|
+
|
|
251
|
+
Wrong:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
sx={{ color: mode === "dark" ? "white" : "black" }}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Correct:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
sx={(theme) => ({
|
|
261
|
+
color: "black",
|
|
262
|
+
...theme.applyStyles("dark", { color: "white" }),
|
|
263
|
+
})}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The app uses `colorSchemeSelector: "data-mui-color-scheme"`. Always use `theme.applyStyles("dark", ...)` for mode-specific styling.
|
|
267
|
+
|
|
268
|
+
Source: copilot-instructions.md
|
|
269
|
+
|
|
270
|
+
Cross-skill: See also skills/layout-navigation/SKILL.md § Common Mistakes
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
See also:
|
|
275
|
+
|
|
276
|
+
- skills/forms-validation/SKILL.md — Create/edit pages embed forms.
|
|
277
|
+
- skills/layout-navigation/SKILL.md — Pages render inside the layout shell.
|
|
278
|
+
- skills/tanstack-db-collections/SKILL.md — Pages consume collections via useLiveQuery.
|