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,212 @@
|
|
|
1
|
+
# Field Components Reference
|
|
2
|
+
|
|
3
|
+
All field components are accessed via `field.*` inside a `form.AppField` children render prop. Props listed below are the MUI props you can pass — `FormOmittedProps` (`name`, `value`, `onChange`, `onBlur`, `error`, `helperText`, `renderInput`, `type`, `aria-label`) are automatically stripped and managed by TanStack Form.
|
|
4
|
+
|
|
5
|
+
## TextField
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
<form.AppField
|
|
9
|
+
name="title"
|
|
10
|
+
children={(field) => (
|
|
11
|
+
<field.TextField
|
|
12
|
+
label="Title"
|
|
13
|
+
required
|
|
14
|
+
multiline // optional: enables textarea
|
|
15
|
+
rows={3} // optional: fixed row count for multiline
|
|
16
|
+
placeholder="..." // optional
|
|
17
|
+
disabled // optional
|
|
18
|
+
slotProps={{ // optional: MUI slot props
|
|
19
|
+
input: { startAdornment: <InputAdornment position="start">$</InputAdornment> }
|
|
20
|
+
}}
|
|
21
|
+
/>
|
|
22
|
+
)}
|
|
23
|
+
/>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## NumberField
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
<form.AppField
|
|
30
|
+
name="quantity"
|
|
31
|
+
children={(field) => (
|
|
32
|
+
<field.NumberField
|
|
33
|
+
label="Quantity"
|
|
34
|
+
required
|
|
35
|
+
slotProps={{
|
|
36
|
+
input: { inputProps: { min: 0, max: 100, step: 1 } }
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
/>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Autocomplete
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
<form.AppField
|
|
47
|
+
name="assignee"
|
|
48
|
+
children={(field) => (
|
|
49
|
+
<field.Autocomplete
|
|
50
|
+
label="Assignee"
|
|
51
|
+
options={users}
|
|
52
|
+
getOptionLabel={(user) => user.displayName}
|
|
53
|
+
isOptionEqualToValue={(option, value) => option.id === value.id}
|
|
54
|
+
multiple // optional: multi-select
|
|
55
|
+
freeSolo // optional: allow custom input
|
|
56
|
+
loading={isLoading} // optional: show loading indicator
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Checkbox
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
<form.AppField
|
|
66
|
+
name="isActive"
|
|
67
|
+
children={(field) => (
|
|
68
|
+
<field.Checkbox
|
|
69
|
+
label="Active"
|
|
70
|
+
disabled // optional
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
/>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Renders `FormControlLabel` wrapping MUI `Checkbox`.
|
|
77
|
+
|
|
78
|
+
## Switch
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
<form.AppField
|
|
82
|
+
name="notifications"
|
|
83
|
+
children={(field) => (
|
|
84
|
+
<field.Switch
|
|
85
|
+
label="Enable Notifications"
|
|
86
|
+
disabled // optional
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
/>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Renders `FormControlLabel` wrapping MUI `Switch`.
|
|
93
|
+
|
|
94
|
+
## RadioGroup
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
<form.AppField
|
|
98
|
+
name="priority"
|
|
99
|
+
children={(field) => (
|
|
100
|
+
<field.RadioGroup
|
|
101
|
+
label="Priority"
|
|
102
|
+
options={[
|
|
103
|
+
{ label: "Low", value: "low" },
|
|
104
|
+
{ label: "Medium", value: "medium" },
|
|
105
|
+
{ label: "High", value: "high" },
|
|
106
|
+
]}
|
|
107
|
+
row // optional: horizontal layout
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
/>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Warning:** `onChange` returns `event.target.value` as a string. Use string values.
|
|
114
|
+
|
|
115
|
+
## Slider
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
<form.AppField
|
|
119
|
+
name="rating"
|
|
120
|
+
children={(field) => (
|
|
121
|
+
<field.Slider
|
|
122
|
+
min={0}
|
|
123
|
+
max={10}
|
|
124
|
+
step={1}
|
|
125
|
+
marks
|
|
126
|
+
valueLabelDisplay="auto"
|
|
127
|
+
/>
|
|
128
|
+
)}
|
|
129
|
+
/>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## DatePicker
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
<form.AppField
|
|
136
|
+
name="dueDate"
|
|
137
|
+
children={(field) => (
|
|
138
|
+
<field.DatePicker
|
|
139
|
+
label="Due Date"
|
|
140
|
+
minDate={dayjs()} // optional
|
|
141
|
+
maxDate={dayjs().add(1, "year")} // optional
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
144
|
+
/>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## DateRangePicker
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
<form.AppField
|
|
151
|
+
name="dateRange"
|
|
152
|
+
children={(field) => (
|
|
153
|
+
<field.DateRangePicker
|
|
154
|
+
localeText={{ start: "Start", end: "End" }}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
/>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## TimePicker
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
<form.AppField
|
|
164
|
+
name="startTime"
|
|
165
|
+
children={(field) => (
|
|
166
|
+
<field.TimePicker
|
|
167
|
+
label="Start Time"
|
|
168
|
+
ampm={false} // optional: 24-hour format
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
/>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## TimeRangePicker
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
<form.AppField
|
|
178
|
+
name="timeRange"
|
|
179
|
+
children={(field) => (
|
|
180
|
+
<field.TimeRangePicker
|
|
181
|
+
localeText={{ start: "From", end: "To" }}
|
|
182
|
+
/>
|
|
183
|
+
)}
|
|
184
|
+
/>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## DateTimePicker
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
<form.AppField
|
|
191
|
+
name="scheduledAt"
|
|
192
|
+
children={(field) => (
|
|
193
|
+
<field.DateTimePicker
|
|
194
|
+
label="Scheduled At"
|
|
195
|
+
ampm={false}
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
198
|
+
/>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## DateTimeRangePicker
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
<form.AppField
|
|
205
|
+
name="availability"
|
|
206
|
+
children={(field) => (
|
|
207
|
+
<field.DateTimeRangePicker
|
|
208
|
+
localeText={{ start: "From", end: "Until" }}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
/>
|
|
212
|
+
```
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: layout-navigation
|
|
3
|
+
description: >
|
|
4
|
+
Configure LayoutProvider with theme, options, and Navigation structure.
|
|
5
|
+
Shell: AppBar with env chip + NavigationRail (permanent sm+, modal xs).
|
|
6
|
+
Navigation items: page (title, icon, to/href), header, divider.
|
|
7
|
+
LayoutOptions: showShell, snackbarOrigin, publicRoutes. Dark mode via
|
|
8
|
+
theme.applyStyles("dark", ...) with colorSchemeSelector
|
|
9
|
+
data-mui-color-scheme. i18n with i18next, locale files auto-detected.
|
|
10
|
+
Activate when configuring the layout shell, navigation, or theming.
|
|
11
|
+
type: core
|
|
12
|
+
library: wcz-layout
|
|
13
|
+
library_version: "7.6.1"
|
|
14
|
+
sources:
|
|
15
|
+
- "wcz-layout:src/providers/LayoutProvider.tsx"
|
|
16
|
+
- "wcz-layout:src/components/core/Layout.tsx"
|
|
17
|
+
- "wcz-layout:src/models/Navigation.ts"
|
|
18
|
+
- "wcz-layout:src/models/LayoutOptions.ts"
|
|
19
|
+
- "wcz-layout:src/lib/theme.ts"
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Layout & Navigation
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
The layout shell is configured in the root route via `LayoutProvider`:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// src/routes/__root.tsx
|
|
30
|
+
import { LayoutProvider, rootRouteHead, RouterNotFound, RouterError } from "wcz-layout";
|
|
31
|
+
import { theme } from "~/lib/theme";
|
|
32
|
+
import { navigation } from "~/navigation";
|
|
33
|
+
import type { LayoutOptions } from "wcz-layout/models";
|
|
34
|
+
|
|
35
|
+
const options: LayoutOptions = {
|
|
36
|
+
showShell: true,
|
|
37
|
+
publicRoutes: ["/login"],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Route = createRootRouteWithContext()({
|
|
41
|
+
head: rootRouteHead,
|
|
42
|
+
component: RootComponent,
|
|
43
|
+
notFoundComponent: RouterNotFound,
|
|
44
|
+
errorComponent: RouterError,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function RootComponent() {
|
|
48
|
+
return (
|
|
49
|
+
<LayoutProvider theme={theme} navigation={navigation} options={options}>
|
|
50
|
+
<Outlet />
|
|
51
|
+
</LayoutProvider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Core Patterns
|
|
57
|
+
|
|
58
|
+
### Navigation structure
|
|
59
|
+
|
|
60
|
+
Navigation is an array of items with three types — discriminated by the presence of `kind`:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import type { Navigation } from "wcz-layout/models";
|
|
64
|
+
import HomeIcon from "@mui/icons-material/Home";
|
|
65
|
+
import ListIcon from "@mui/icons-material/List";
|
|
66
|
+
import SettingsIcon from "@mui/icons-material/Settings";
|
|
67
|
+
|
|
68
|
+
export const navigation: Navigation = [
|
|
69
|
+
{ title: "Home", icon: <HomeIcon />, to: "/" },
|
|
70
|
+
{ kind: "divider" },
|
|
71
|
+
{ kind: "header", title: "Management" },
|
|
72
|
+
{ title: "Todos", icon: <ListIcon />, to: "/todos" },
|
|
73
|
+
{ kind: "divider" },
|
|
74
|
+
{ title: "Settings", icon: <SettingsIcon />, to: "/settings" },
|
|
75
|
+
];
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Navigation item types
|
|
79
|
+
|
|
80
|
+
| Type | Shape | Required fields |
|
|
81
|
+
| --------- | --------------------------- | --------------- |
|
|
82
|
+
| Page item | `{ title, icon, to }` | `title`, `icon` |
|
|
83
|
+
| Divider | `{ kind: "divider" }` | `kind` |
|
|
84
|
+
| Header | `{ kind: "header", title }` | `kind`, `title` |
|
|
85
|
+
|
|
86
|
+
Page items also accept:
|
|
87
|
+
|
|
88
|
+
- `to` — internal route (TanStack Router link, type-safe)
|
|
89
|
+
- `href` — external URL (opens in new tab)
|
|
90
|
+
- `params`, `search` — TanStack Router link params
|
|
91
|
+
- `children` — nested navigation items (sub-menu)
|
|
92
|
+
- `hidden` — conditionally hide the item
|
|
93
|
+
|
|
94
|
+
### Nested navigation
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const navigation: Navigation = [
|
|
98
|
+
{
|
|
99
|
+
title: "Admin",
|
|
100
|
+
icon: <AdminIcon />,
|
|
101
|
+
to: "/admin",
|
|
102
|
+
children: [
|
|
103
|
+
{ title: "Users", icon: <PeopleIcon />, to: "/admin/users" },
|
|
104
|
+
{ title: "Roles", icon: <SecurityIcon />, to: "/admin/roles" },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### LayoutOptions
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
interface LayoutOptions {
|
|
114
|
+
showShell?: boolean; // Show AppBar + NavigationRail (default: true)
|
|
115
|
+
snackbarOrigin?: SnackbarOrigin; // Notification position
|
|
116
|
+
publicRoutes?: string[]; // Routes that skip authentication
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Theme configuration
|
|
121
|
+
|
|
122
|
+
The theme is pre-configured in the template at `src/lib/theme.ts`. Use MUI's `extendTheme` to customize:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// src/lib/theme.ts
|
|
126
|
+
import { extendTheme } from "@mui/material/styles";
|
|
127
|
+
|
|
128
|
+
export const theme = extendTheme({
|
|
129
|
+
colorSchemes: {
|
|
130
|
+
light: { palette: { primary: { main: "#1976d2" } } },
|
|
131
|
+
dark: { palette: { primary: { main: "#90caf9" } } },
|
|
132
|
+
},
|
|
133
|
+
colorSchemeSelector: "data-mui-color-scheme",
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Dark mode styling
|
|
138
|
+
|
|
139
|
+
Always use `theme.applyStyles("dark", ...)` for mode-specific styling:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
<Box
|
|
143
|
+
sx={(theme) => ({
|
|
144
|
+
backgroundColor: "#fff",
|
|
145
|
+
...theme.applyStyles("dark", {
|
|
146
|
+
backgroundColor: "#121212",
|
|
147
|
+
}),
|
|
148
|
+
})}
|
|
149
|
+
/>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### i18n setup
|
|
153
|
+
|
|
154
|
+
Locale files in `src/locales/` are auto-detected by `viteWczLayout()`:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
src/locales/en.json # English (default)
|
|
158
|
+
src/locales/cs.json # Czech
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The Vite plugin generates the `virtual:wcz-layout` module with all locale resources. `LayoutProvider` initializes `i18next` with language detection (cookie-based, 1-year expiry) and syncs Zod and DayJS locales on language change.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { useTranslation } from "react-i18next";
|
|
165
|
+
|
|
166
|
+
function MyComponent() {
|
|
167
|
+
const { t } = useTranslation();
|
|
168
|
+
return <Typography>{t("welcome")}</Typography>;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Root route head
|
|
173
|
+
|
|
174
|
+
`rootRouteHead` provides meta tags and manifest link:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
export const Route = createRootRouteWithContext()({
|
|
178
|
+
head: rootRouteHead,
|
|
179
|
+
// optionally: head: rootRouteHead({ manifest: "/manifest.json" }),
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Service worker
|
|
184
|
+
|
|
185
|
+
`LayoutProvider` registers `/sw.js` on mount for PWA support.
|
|
186
|
+
|
|
187
|
+
## Common Mistakes
|
|
188
|
+
|
|
189
|
+
### HIGH Using theme.palette for dark mode instead of theme.applyStyles
|
|
190
|
+
|
|
191
|
+
Wrong:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
sx={{ color: mode === "dark" ? "white" : "black" }}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Correct:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
sx={(theme) => ({
|
|
201
|
+
color: "black",
|
|
202
|
+
...theme.applyStyles("dark", { color: "white" }),
|
|
203
|
+
})}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The app uses `colorSchemeSelector: "data-mui-color-scheme"`. Mode-specific styles must use `theme.applyStyles("dark", ...)`, not conditional palette checks or mode variables.
|
|
207
|
+
|
|
208
|
+
Source: copilot-instructions.md / wcz-layout:src/lib/theme.ts
|
|
209
|
+
|
|
210
|
+
Cross-skill: See also skills/ui-pages/SKILL.md § Common Mistakes
|
|
211
|
+
|
|
212
|
+
### HIGH Using href for internal routes in Navigation
|
|
213
|
+
|
|
214
|
+
Wrong:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
{ title: "Todos", icon: <ListIcon />, href: "/todos" }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Correct:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
{ title: "Todos", icon: <ListIcon />, to: "/todos" }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`NavigationPageItem` supports dual destination: `to` for internal TanStack Router links, `href` for external URLs. Using `href` for internal routes causes full page reloads.
|
|
227
|
+
|
|
228
|
+
Source: wcz-layout:src/models/Navigation.ts
|
|
229
|
+
|
|
230
|
+
### MEDIUM Creating NavigationPageItem without icon
|
|
231
|
+
|
|
232
|
+
Wrong:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
{ title: "Todos", to: "/todos" }
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Correct:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
{ title: "Todos", icon: <ListIcon />, to: "/todos" }
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`NavigationPageItem` requires both `title` and `icon`. Missing `icon` renders an empty slot in the NavigationRail.
|
|
245
|
+
|
|
246
|
+
Source: wcz-layout:src/models/Navigation.ts
|
|
247
|
+
|
|
248
|
+
### MEDIUM Adding locale without updating supportedLngs
|
|
249
|
+
|
|
250
|
+
This is actually **not** an issue — the Vite plugin auto-detects locale files in `src/locales/`. Adding a new JSON file (e.g. `de.json`) is sufficient; `supportedLngs` is derived from `Object.keys(resources)` automatically.
|
|
251
|
+
|
|
252
|
+
Source: wcz-layout:src/providers/LayoutProvider.tsx
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
See also:
|
|
257
|
+
|
|
258
|
+
- skills/ui-pages/SKILL.md — Pages render inside the layout shell.
|
|
259
|
+
- skills/auth/SKILL.md — publicRoutes bypass auth; permission guards need auth context.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: project-initialization
|
|
3
|
+
description: >
|
|
4
|
+
Scaffold a new wcz-layout project from the template. Covers placeholder
|
|
5
|
+
replacement across three naming conventions (kebab-case, Title Case,
|
|
6
|
+
snake_case), .env and .env.local configuration, VITE_ENTRA_CLIENT_ID,
|
|
7
|
+
VITE_APP_TITLE, VITE_MUI_LICENSE_KEY, favicon generation via favicon.io,
|
|
8
|
+
and viteWczLayout() Vite plugin registration. Activate when starting a
|
|
9
|
+
new internal project or configuring environment variables.
|
|
10
|
+
type: lifecycle
|
|
11
|
+
library: wcz-layout
|
|
12
|
+
library_version: "7.6.1"
|
|
13
|
+
sources:
|
|
14
|
+
- "wcz-layout:src/env.ts"
|
|
15
|
+
- "wcz-layout:src/lib/vite-plugin.ts"
|
|
16
|
+
- "wcz-layout:vite.config.ts"
|
|
17
|
+
- "wcz-layout:.env"
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Project Initialization
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
After cloning the template repository, initialize the project in this order:
|
|
25
|
+
|
|
26
|
+
### 1. Replace all template placeholders
|
|
27
|
+
|
|
28
|
+
Use your editor's replace-all (across all files) for each naming convention:
|
|
29
|
+
|
|
30
|
+
| Find | Replace with | Used in |
|
|
31
|
+
| -------------- | ------------ | ------------------------------------------------ |
|
|
32
|
+
| `project-name` | `app-store` | package.json name, URLs, CSS classes |
|
|
33
|
+
| `Project Name` | `App Store` | VITE_APP_TITLE, display strings, docs |
|
|
34
|
+
| `my_app` | `app_store` | Database names, env vars, snake_case identifiers |
|
|
35
|
+
|
|
36
|
+
### 2. Configure environment variables
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
# .env — committed to repo, shared across team
|
|
40
|
+
VITE_ENTRA_CLIENT_ID=your-entra-client-id
|
|
41
|
+
VITE_ENTRA_TENANT_ID=your-tenant-id
|
|
42
|
+
VITE_APP_TITLE=App Store
|
|
43
|
+
VITE_MUI_LICENSE_KEY=your-mui-license-key
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Create `.env.local` (gitignored) for local secret overrides:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
# .env.local — never committed
|
|
50
|
+
VITE_ENTRA_CLIENT_ID=your-dev-client-id
|
|
51
|
+
ENTRA_CLIENT_ID=your-server-client-id
|
|
52
|
+
ENTRA_TENANT_ID=your-server-tenant-id
|
|
53
|
+
ENTRA_CLIENT_SECRET=your-client-secret
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. Generate favicon
|
|
57
|
+
|
|
58
|
+
Go to https://favicon.io/favicon-converter/, upload your logo, and place the generated files in `public/`.
|
|
59
|
+
|
|
60
|
+
### 4. Verify Vite plugin registration
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// vite.config.ts
|
|
64
|
+
import { viteWczLayout } from "wcz-layout/vite";
|
|
65
|
+
|
|
66
|
+
export default defineConfig({
|
|
67
|
+
plugins: [
|
|
68
|
+
tanstackStart(),
|
|
69
|
+
nitro(),
|
|
70
|
+
viteReact(),
|
|
71
|
+
babel(reactCompilerPreset()),
|
|
72
|
+
checker({ typescript: true }),
|
|
73
|
+
viteWczLayout(),
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Core Patterns
|
|
79
|
+
|
|
80
|
+
### Environment validation with createEnv
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// src/env.ts
|
|
84
|
+
import { createEnv } from "wcz-layout/utils";
|
|
85
|
+
import { z } from "zod";
|
|
86
|
+
|
|
87
|
+
export const clientEnv = createEnv({
|
|
88
|
+
clientPrefix: "VITE_",
|
|
89
|
+
client: {
|
|
90
|
+
VITE_ENTRA_CLIENT_ID: z.string(),
|
|
91
|
+
VITE_ENTRA_TENANT_ID: z.string(),
|
|
92
|
+
VITE_APP_TITLE: z.string(),
|
|
93
|
+
VITE_MUI_LICENSE_KEY: z.string(),
|
|
94
|
+
},
|
|
95
|
+
runtimeEnv: import.meta.env,
|
|
96
|
+
emptyStringAsUndefined: true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export const serverEnv = createEnv({
|
|
100
|
+
server: {
|
|
101
|
+
ENTRA_CLIENT_ID: z.string(),
|
|
102
|
+
ENTRA_TENANT_ID: z.string(),
|
|
103
|
+
ENTRA_CLIENT_SECRET: z.string(),
|
|
104
|
+
},
|
|
105
|
+
runtimeEnv: process.env,
|
|
106
|
+
emptyStringAsUndefined: true,
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Vault secrets for local development
|
|
111
|
+
|
|
112
|
+
The `viteWczLayout()` plugin automatically loads secrets from HashiCorp Vault during `vite dev` if `VAULT_ADDRESS`, `VAULT_USERNAME`, `VAULT_PASSWORD`, and `VAULT_SECRET_PATH` are set in `.env`. Vault-loaded secrets only populate `process.env` keys that are not already set.
|
|
113
|
+
|
|
114
|
+
## Common Mistakes
|
|
115
|
+
|
|
116
|
+
### CRITICAL Missing viteWczLayout() in Vite config
|
|
117
|
+
|
|
118
|
+
Wrong:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// vite.config.ts
|
|
122
|
+
export default defineConfig({
|
|
123
|
+
plugins: [tanstackStart(), viteReact()],
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Correct:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// vite.config.ts
|
|
131
|
+
import { viteWczLayout } from "wcz-layout/vite";
|
|
132
|
+
export default defineConfig({
|
|
133
|
+
plugins: [tanstackStart(), viteReact(), viteWczLayout()],
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Without `viteWczLayout()`, the `virtual:wcz-layout` module won't resolve, breaking i18n resources, permissions, and scopes at build time.
|
|
138
|
+
|
|
139
|
+
Source: wcz-layout:src/lib/vite-plugin.ts
|
|
140
|
+
|
|
141
|
+
### HIGH Forgetting .env.local for local secrets
|
|
142
|
+
|
|
143
|
+
`.env` is committed to the repo and shared across the team. Local overrides (Entra client IDs, secrets) must go in `.env.local` which is gitignored. Committing secrets to `.env` exposes them in the repository.
|
|
144
|
+
|
|
145
|
+
Source: maintainer interview
|
|
146
|
+
|
|
147
|
+
### HIGH Not replacing all naming conventions
|
|
148
|
+
|
|
149
|
+
The template uses three naming conventions. Use replace-all across the entire project for each one. Missing any convention leaves stale references in package.json, env files, or source imports.
|
|
150
|
+
|
|
151
|
+
Source: maintainer interview
|
|
152
|
+
|
|
153
|
+
### MEDIUM Empty string env vars pass silently
|
|
154
|
+
|
|
155
|
+
Wrong:
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
# .env
|
|
159
|
+
VITE_APP_TITLE=
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Correct:
|
|
163
|
+
|
|
164
|
+
```sh
|
|
165
|
+
# .env
|
|
166
|
+
VITE_APP_TITLE=My Application
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`createEnv` uses `emptyStringAsUndefined: true`, so empty strings become `undefined` and fail Zod validation at runtime, not build time.
|
|
170
|
+
|
|
171
|
+
Source: wcz-layout:src/env.ts
|
|
172
|
+
|
|
173
|
+
### HIGH Tension: Simplicity vs. full-stack rigor
|
|
174
|
+
|
|
175
|
+
This skill's simple setup conflicts with production readiness from api-routes. Agents scaffolding a quick prototype may skip authorizationMiddleware and validationMiddleware, producing code that works locally but is insecure in production. Always include middleware from the start.
|
|
176
|
+
|
|
177
|
+
See also: skills/api-routes/SKILL.md § Common Mistakes
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
See also: skills/project-structure/SKILL.md — After init, understand where to place new code.
|