wcz-layout 7.6.1 → 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.

@@ -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.