create-warlock 5.34.2 → 5.35.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/CHANGELOG.md +38 -0
- package/README.md +8 -0
- package/esm/commands/create-new-app/index.mjs +4 -3
- package/esm/commands/create-new-app/index.mjs.map +1 -1
- package/esm/commands/create-warlock-app/index.mjs +7 -1
- package/esm/commands/create-warlock-app/index.mjs.map +1 -1
- package/esm/features/features-map.mjs +8 -2
- package/esm/features/features-map.mjs.map +1 -1
- package/esm/helpers/app.mjs +1 -0
- package/esm/helpers/app.mjs.map +1 -1
- package/esm/prompts/ask-stack.mjs +1 -1
- package/esm/prompts/ask-stack.mjs.map +1 -1
- package/llms-full.txt +64 -28
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/{create-a-warlock-project/SKILL.md → SKILL.md} +63 -27
- package/templates/design-system/SOURCES.md +23 -0
- package/templates/design-system/package.json +14 -0
- package/templates/design-system/src/web/components/ui/LICENSE.md +21 -0
- package/templates/design-system/src/web/components/ui/button.tsx +61 -0
- package/templates/design-system/src/web/components/ui/calendar.tsx +178 -0
- package/templates/design-system/src/web/components/ui/checkbox.tsx +28 -0
- package/templates/design-system/src/web/components/ui/command.tsx +161 -0
- package/templates/design-system/src/web/components/ui/dialog.tsx +144 -0
- package/templates/design-system/src/web/components/ui/input.tsx +20 -0
- package/templates/design-system/src/web/components/ui/label.tsx +20 -0
- package/templates/design-system/src/web/components/ui/popover.tsx +73 -0
- package/templates/design-system/src/web/components/ui/radio-group.tsx +44 -0
- package/templates/design-system/src/web/components/ui/switch.tsx +34 -0
- package/templates/design-system/src/web/components/ui/textarea.tsx +17 -0
- package/templates/design-system/src/web/design-system/components/checkbox-input.tsx +19 -0
- package/templates/design-system/src/web/design-system/components/date-picker-input.tsx +59 -0
- package/templates/design-system/src/web/design-system/components/date-time-picker-input.tsx +67 -0
- package/templates/design-system/src/web/design-system/components/email-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/field-frame.tsx +36 -0
- package/templates/design-system/src/web/design-system/components/file-input.tsx +24 -0
- package/templates/design-system/src/web/design-system/components/multi-select-input.tsx +94 -0
- package/templates/design-system/src/web/design-system/components/password-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/radio-input.tsx +41 -0
- package/templates/design-system/src/web/design-system/components/select-input.tsx +94 -0
- package/templates/design-system/src/web/design-system/components/submit-button.tsx +20 -0
- package/templates/design-system/src/web/design-system/components/switch-input.tsx +19 -0
- package/templates/design-system/src/web/design-system/components/text-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/textarea-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/time-picker-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/hooks/use-calendar-day-button.ts +9 -0
- package/templates/design-system/src/web/design-system/hooks/use-control.ts +29 -0
- package/templates/design-system/src/web/design-system/hooks/use-date-control.ts +42 -0
- package/templates/design-system/src/web/design-system/hooks/use-file-control.ts +24 -0
- package/templates/design-system/src/web/design-system/hooks/use-select-control.ts +78 -0
- package/templates/design-system/src/web/design-system/hooks/use-submit-control.ts +13 -0
- package/templates/design-system/src/web/design-system/types/control-props.ts +46 -0
- package/templates/toolchains/oxc/.oxlintrc.json +3 -1
- package/templates/warlock/.oxlintrc.json +3 -1
- package/templates/warlock/AGENTS.md +10 -10
- package/templates/warlock/e2e/home.e2e.ts +19 -0
- package/templates/warlock/package.json +1 -1
- package/templates/warlock/skills/api-design/SKILL.md +180 -57
- package/templates/warlock/skills/code-standards/SKILL.md +48 -39
- package/templates/warlock/skills/data-and-persistence/SKILL.md +72 -57
- package/templates/warlock/skills/git-workflow/SKILL.md +9 -9
- package/templates/warlock/skills/module-boundaries/SKILL.md +2 -2
- package/templates/warlock/skills/observability-and-resilience/SKILL.md +8 -6
- package/templates/warlock/skills/security-baseline/SKILL.md +23 -22
- package/templates/warlock/skills/testing-strategy/SKILL.md +53 -12
- package/templates/warlock/skills/web-code-standards/SKILL.md +195 -60
- package/templates/warlock/src/app/auth/controllers/forgot-password.controller.ts +6 -0
- package/templates/warlock/src/app/auth/controllers/login.controller.ts +17 -0
- package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +6 -0
- package/templates/warlock/src/app/auth/controllers/logout.controller.ts +6 -0
- package/templates/warlock/src/app/auth/controllers/me.controller.ts +7 -0
- package/templates/warlock/src/app/auth/controllers/refresh-token.controller.ts +12 -0
- package/templates/warlock/src/app/auth/controllers/reset-password.controller.ts +6 -0
- package/templates/warlock/src/app/auth/models/otp/otp.model.ts +9 -3
- package/templates/warlock/src/app/auth/services/otp.service.ts +18 -6
- package/templates/warlock/src/app/contact/controllers/contact.controller.ts +6 -0
- package/templates/warlock/src/app/home/controllers/home-page.controller.ts +4 -0
- package/templates/warlock/src/app/locale/controllers/locale.controller.ts +6 -0
- package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +7 -0
- package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +15 -9
- package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +1 -0
- package/templates/warlock/src/app/posts/models/post/post.model.ts +6 -0
- package/templates/warlock/src/app/posts/resources/post.resource.ts +1 -1
- package/templates/warlock/src/app/posts/schema/update-post.schema.ts +1 -0
- package/templates/warlock/src/app/shared/utils/format-http-error-response.ts +53 -0
- package/templates/warlock/src/app/users/commands/hello-world.command.ts +2 -1
- package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +13 -1
- package/templates/warlock/src/app/users/controllers/list-users.controller.ts +24 -1
- package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +1 -0
- package/templates/warlock/src/app/users/models/user/user.model.ts +5 -0
- package/templates/warlock/src/app/users/repositories/users.repository.ts +2 -2
- package/templates/warlock/src/app/users/resources/user.resource.ts +1 -1
- package/templates/warlock/src/app/users/schema/list-users.schema.ts +1 -1
- package/templates/warlock/src/config/http.ts +6 -0
- package/templates/warlock/src/web/home/components/contact-field.tsx +18 -0
- package/templates/warlock/src/web/home/components/contact-section.tsx +5 -29
- package/templates/warlock/src/web/home/components/contact-submit-button.tsx +16 -0
- package/templates/warlock/src/web/home/hooks/use-contact-field.ts +9 -0
- package/templates/warlock/src/web/home/hooks/use-contact-form.ts +5 -8
- package/templates/warlock/src/web/home/hooks/use-contact-locale.ts +34 -0
- package/templates/warlock/src/web/home/hooks/use-contact-submit-button.ts +6 -0
- package/templates/warlock/src/web/home/index.page.tsx +1 -1
- package/templates/warlock/src/web/home/types/contact-controls.type.ts +13 -0
- package/templates/warlock/src/web/shared/utils/set-form-errors.ts +3 -1
- package/templates/warlock/warlock.config.ts +1 -0
- package/templates/warlock/src/web/home/components/contact-form-controls.tsx +0 -42
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: web-code-standards
|
|
3
|
-
description:
|
|
4
|
-
Strict separation-of-concerns standards for Warlock.js src/web pages and
|
|
5
|
-
components: loaders delegate data and business rules to app services;
|
|
6
|
-
components delegate all behavior to separate custom-hook files.
|
|
7
|
-
Use alongside code-standards for web work; skip for backend-only changes.
|
|
3
|
+
description: "Warlock web pages and setup files, component and hook ownership, Mongez forms, accessible controls, locale behavior and UI kit choices. Use when building or reviewing src/web features. Use code-standards instead for shared TypeScript rules."
|
|
8
4
|
---
|
|
9
5
|
|
|
10
6
|
# Web code standards
|
|
11
7
|
|
|
12
8
|
**Applies to:** `src/web/**` in this project. Read
|
|
13
|
-
|
|
9
|
+
the `code-standards` skill for the shared TypeScript rules,
|
|
14
10
|
and the relevant `@warlock.js/web` skill before using a framework feature.
|
|
15
11
|
This skill adds the web-specific rules; it does not override security or
|
|
16
12
|
module-boundary requirements. When reviewing existing code, identify debt
|
|
@@ -45,7 +41,7 @@ ancestor and child dictionaries cannot redefine the same key. Keep legacy
|
|
|
45
41
|
Within a web feature, use `components/`, `hooks/`, and `utils/` for their
|
|
46
42
|
named purpose; do not create `lib/` as a catch-all. These folders have no
|
|
47
43
|
barrel `index.ts`: import the named component, hook, or helper from its own
|
|
48
|
-
file. Keep types in `types/` when a feature has several contracts, or in one
|
|
44
|
+
file. These exact imports keep client bundles lean and prevent server-only code from being pulled through a barrel. The `code-standards` skill allows sub-folder barrels only for `src/app/**` server modules. Keep types in `types/` when a feature has several contracts, or in one
|
|
49
45
|
`<feature>.types.ts` file when they are small. Do not duplicate a contract in
|
|
50
46
|
both places. Put component props in that dedicated type module by default;
|
|
51
47
|
keep a tiny private prop type beside a component only when extracting it would
|
|
@@ -54,13 +50,17 @@ make the feature harder to read.
|
|
|
54
50
|
## 1. Keep the page contract, make the implementation thin
|
|
55
51
|
|
|
56
52
|
Page logic lives in the matching `.setup.ts` file: `config`, `loader`,
|
|
57
|
-
`register`, and `action`/`actions`. UI
|
|
58
|
-
component
|
|
53
|
+
`register`, and `action`/`actions`. The UI file owns its named default
|
|
54
|
+
component; re-export a framework-required named component such as `ErrorBoundary`
|
|
55
|
+
from that component's own file. Pair `layout.tsx` with
|
|
59
56
|
`layout.setup.ts`, `root.tsx` with `root.setup.ts`, and `error.page.tsx` with
|
|
60
57
|
`error.setup.ts`. Use type-only imports from setup for loader/action inference.
|
|
61
58
|
The framework projects browser-safe `register()` code to the client. Put route,
|
|
62
59
|
cache, validation, middleware, metadata, and sitemap policy inside `config`;
|
|
63
|
-
metadata always belongs in `config.metadata`.
|
|
60
|
+
metadata always belongs in `config.metadata`. Do not add an empty setup file
|
|
61
|
+
when a component has no configuration, loader, action, or registration work.
|
|
62
|
+
The starter follows this boundary for root configuration, home route policy
|
|
63
|
+
and translations, and not-found metadata.
|
|
64
64
|
|
|
65
65
|
Use the config type that matches the module. A page can preserve loader data
|
|
66
66
|
inference for metadata by satisfying `PageConfig<typeof loader>`:
|
|
@@ -82,15 +82,18 @@ export const config = {
|
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
```tsx title="src/web/posts.page.tsx"
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
import type { PageProps } from "@warlock.js/web";
|
|
86
|
+
import type { loader } from "./posts.setup";
|
|
87
|
+
|
|
88
|
+
export default function PostsPage({ data }: PageProps<typeof loader>) {
|
|
89
|
+
return <main>{data.title}</main>;
|
|
87
90
|
}
|
|
88
91
|
```
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
Use `href(name, params, query)` for named page navigation. Generated declarations
|
|
94
|
+
in `.warlock/typings/web-routes.d.ts` check route names and required path
|
|
95
|
+
parameters when included in the app's TypeScript project; runtime route-table
|
|
96
|
+
validation still applies. Do not widen those contracts to strings or add casts.
|
|
94
97
|
|
|
95
98
|
Layouts use one `config satisfies LayoutConfig` object for `prefix`,
|
|
96
99
|
`middleware`, `metadata`, and a sitemap default. Roots use one
|
|
@@ -133,11 +136,22 @@ queries, aggregations, and domain transformations in the service. A service
|
|
|
133
136
|
must have a meaningful domain name and own the operation, not exist as a
|
|
134
137
|
generic pass-through for a repository method.
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
Give every component one responsibility and one reason to change. Define one
|
|
140
|
+
component per file, with a named default export. Extract substantive view
|
|
141
|
+
sections into cohesive components. Put hooks in `hooks/` and pure UI helpers
|
|
142
|
+
in `utils/`, one hook or helper per file, exported by name. A hook owns state,
|
|
143
|
+
effects, handlers, navigation, and client mutations; a utility has no React or
|
|
144
|
+
browser lifecycle dependency. A presentational component needs no hook.
|
|
145
|
+
|
|
146
|
+
Keep a small piece of logic or a private helper inline only when it is tightly
|
|
147
|
+
coupled to that one component and keeping it there leaves the file small and
|
|
148
|
+
focused. A component file past roughly 150 lines, a second component, or multiple
|
|
149
|
+
helpers is a split signal, not a mechanical line limit: split at ownership
|
|
150
|
+
boundaries, never into arbitrary chunks to meet a count. The one-component rule
|
|
151
|
+
still applies. Framework logic exports remain in setup even when delegated.
|
|
152
|
+
Keep feature CSS beside the feature and import it from the owning presentation
|
|
153
|
+
boundary; keep loading, error, empty, focus, reduced-motion, and locale behavior
|
|
154
|
+
with that boundary.
|
|
141
155
|
|
|
142
156
|
Preserve the framework's rendering boundary: `root.tsx` owns the document,
|
|
143
157
|
`#vessel` and `<Scripts />`; the loader runs on the server, while the rendered
|
|
@@ -153,35 +167,6 @@ output; the page composes `PostArticle`, `PostComments`, and `RelatedPosts`;
|
|
|
153
167
|
`use-post-comments.ts` owns subscription, state, and handlers for
|
|
154
168
|
`PostComments`. Each piece has one reason to change.
|
|
155
169
|
|
|
156
|
-
## Setup and component files
|
|
157
|
-
|
|
158
|
-
Use a setup file when a feature registers framework behavior. The setup file
|
|
159
|
-
owns its `register`, `config`, and `loader` exports; its matching component
|
|
160
|
-
only renders the supplied data. Keep loader inference type-only so it does
|
|
161
|
-
not pull server work into a browser component:
|
|
162
|
-
|
|
163
|
-
The default starter follows this boundary: `root.setup.ts` owns strict-mode
|
|
164
|
-
configuration, `home/index.setup.ts` owns route policy, its loader, and the
|
|
165
|
-
universal translations `register()`, and `404.setup.ts` owns not-found
|
|
166
|
-
metadata. Do not add an empty setup file when a component has no server
|
|
167
|
-
configuration, loader, or registration work.
|
|
168
|
-
|
|
169
|
-
```tsx
|
|
170
|
-
import type { PageProps } from "@warlock.js/web";
|
|
171
|
-
import type { loader } from "./posts.setup";
|
|
172
|
-
|
|
173
|
-
export default function PostsPage({ data }: PageProps<typeof loader>) {
|
|
174
|
-
return <PostsList posts={data.posts} />;
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Each React component file has one named default component. Export hooks,
|
|
179
|
-
utilities, and types by name. A component renders; its feature hook owns
|
|
180
|
-
state, effects, handlers, navigation, and client mutations. A utility stays
|
|
181
|
-
pure and has no React or browser lifecycle dependency. Keep feature CSS beside
|
|
182
|
-
the feature and import it from the owning presentation boundary; keep loading,
|
|
183
|
-
error, empty, focus, reduced-motion, and locale behavior with that boundary.
|
|
184
|
-
|
|
185
170
|
## Declaration spacing and local types
|
|
186
171
|
|
|
187
172
|
Put one blank line after the final import. Keep a short related declaration run
|
|
@@ -218,17 +203,17 @@ state starts unset, write `useState<ErrorText>()`; do not add a redundant
|
|
|
218
203
|
same validated, public data as the page. Keep private routes out of public
|
|
219
204
|
discovery, and do not generate database-backed assets on every GET.
|
|
220
205
|
|
|
221
|
-
## 3. Components render; custom hooks own
|
|
206
|
+
## 3. Components render; custom hooks own behavior
|
|
222
207
|
|
|
223
|
-
**
|
|
224
|
-
component
|
|
225
|
-
|
|
208
|
+
**Delegate behavior to the component's dedicated custom hook by default.** A
|
|
209
|
+
component declares props, calls that hook if needed, and renders JSX from its
|
|
210
|
+
props and the hook's returned values. Put `useState`,
|
|
226
211
|
`useEffect`, subscriptions, timers, browser API calls, derived-value
|
|
227
212
|
calculations, event handlers, mutations, and navigation decisions in a
|
|
228
213
|
separate custom-hook file. Keep pure domain calculations in `src/app`
|
|
229
214
|
services; a hook may call a small pure UI helper outside the component.
|
|
230
|
-
|
|
231
|
-
|
|
215
|
+
Use section 1's inline exception only for small, tightly coupled logic;
|
|
216
|
+
reuse or file growth means it belongs in its own hook or utility file.
|
|
232
217
|
JSX conditionals that only select which markup to show are presentation,
|
|
233
218
|
not a license to implement business rules in render.
|
|
234
219
|
|
|
@@ -244,6 +229,11 @@ Avoid copying props into hook state without a plan for refresh and client
|
|
|
244
229
|
navigation. Avoid setting another state variable inside a state-updater
|
|
245
230
|
callback. Prefer deriving display values from one authoritative state.
|
|
246
231
|
|
|
232
|
+
**State ownership.** Loader data is server truth. UI interaction state (open,
|
|
233
|
+
selection, draft input) lives in the component's own hook. Shared client state
|
|
234
|
+
goes only through a named store module, never a global variable or a copy
|
|
235
|
+
spread across components. Never copy props into state without a refresh plan.
|
|
236
|
+
|
|
247
237
|
Effects own their subscriptions and timers: unsubscribe, cancel, or ignore
|
|
248
238
|
stale async work on unmount or entity change. A failed script/socket load
|
|
249
239
|
must not leave a permanently rejected shared promise or silently disable a
|
|
@@ -272,6 +262,150 @@ enforce the same policy independently. Use a full navigation when an auth or
|
|
|
272
262
|
document boundary requires it, otherwise prefer the framework's navigation
|
|
273
263
|
and refresh primitives so state and loader data stay in sync.
|
|
274
264
|
|
|
265
|
+
### Use Mongez React Form for every form
|
|
266
|
+
|
|
267
|
+
Always use `Form`, `useFormControl`, and `useSubmitButton` from
|
|
268
|
+
`@mongez/react-form`; the web stack already uses this package, including the
|
|
269
|
+
starter's contact form. Do not add a second form library or manage field
|
|
270
|
+
validation and submission state in parallel React state. Keep those hooks in
|
|
271
|
+
the control's and submit button's dedicated hook files, following section 1.
|
|
272
|
+
|
|
273
|
+
Declare validation in a browser-safe `<Form schema={schema}>` (Seal implements
|
|
274
|
+
Standard Schema), a control's `schema`, or `useFormControl({ rules: [...] })`.
|
|
275
|
+
The `rules` array contains imported rule objects, never rule-name strings.
|
|
276
|
+
For example, `{ name: "email", type: "email", required: true,
|
|
277
|
+
rules: [requiredRule, emailRule] }` uses Mongez's rules: `requiredRule` needs
|
|
278
|
+
`required: true`, and `emailRule` needs the control's `type: "email"`.
|
|
279
|
+
Put `requiredRule` first. Schema issues map back to registered control names,
|
|
280
|
+
so keep schema paths and control names aligned.
|
|
281
|
+
|
|
282
|
+
Never use `required`, `pattern`, `min`, `max`, `minLength`, or `type="email"`
|
|
283
|
+
as browser validation, or rely on the browser's constraint UI. These may be
|
|
284
|
+
Mongez rule configuration props; do not spread raw control props onto DOM
|
|
285
|
+
inputs. Mongez v4's `Form` renders `noValidate` by default; keep it enabled
|
|
286
|
+
and preserve it if using a custom form host. A schema-backed email field can
|
|
287
|
+
render `type="text"` with `inputMode="email"` and `autoComplete="email"`,
|
|
288
|
+
leaving email validation entirely to the form.
|
|
289
|
+
|
|
290
|
+
Render the control's `error` through `getErrorProps()` and connect the field
|
|
291
|
+
with `getInputProps()` (or equivalent explicit wiring for a custom widget).
|
|
292
|
+
This keeps field identity, focus, blur validation, and error announcement in
|
|
293
|
+
the same form contract. For example, each block below is its own file:
|
|
294
|
+
|
|
295
|
+
```ts title="src/web/contact/contact.schema.ts"
|
|
296
|
+
import { v } from "@warlock.js/seal";
|
|
297
|
+
|
|
298
|
+
export const contactSchema = v.object({ email: v.email() });
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
```ts title="src/web/contact/hooks/use-contact-email.ts"
|
|
302
|
+
import { useFormControl } from "@mongez/react-form";
|
|
303
|
+
|
|
304
|
+
export function useContactEmail() {
|
|
305
|
+
return useFormControl({ name: "email" });
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```tsx title="src/web/contact/components/contact-email.tsx"
|
|
310
|
+
import { useContactEmail } from "../hooks/use-contact-email";
|
|
311
|
+
|
|
312
|
+
export default function ContactEmail() {
|
|
313
|
+
const { id, error, getInputProps, getErrorProps } = useContactEmail();
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<div>
|
|
317
|
+
<label htmlFor={id}>Email</label>
|
|
318
|
+
<input {...getInputProps({ type: "text", inputMode: "email", autoComplete: "email" })} />
|
|
319
|
+
{error ? <span {...getErrorProps()}>{error}</span> : null}
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Render `ContactEmail` inside `<Form schema={contactSchema} onSubmit={submit}>`;
|
|
326
|
+
the feature hook owns `submit`. The submit button's hook calls
|
|
327
|
+
`useSubmitButton()` and exposes `disabled` and `isSubmitting` to its component.
|
|
328
|
+
For API mutations, use `useSubmitForm` from `@warlock.js/web/form` in the
|
|
329
|
+
feature hook. With generated route declarations and a handler's `responseSchema`,
|
|
330
|
+
`useSubmitForm({ route })` infers the declared success body; `ApiResponse` from
|
|
331
|
+
`@warlock.js/web` reads a declared status body. Do not replace this inference
|
|
332
|
+
with casts. Server endpoints must still validate input independently.
|
|
333
|
+
|
|
334
|
+
### Use the shared design-system controls
|
|
335
|
+
|
|
336
|
+
For Tailwind + shadcn projects, use the controls in `src/web/design-system/`:
|
|
337
|
+
TextInput, EmailInput, PasswordInput, TextareaInput, SelectInput (search),
|
|
338
|
+
MultiSelectInput, DatePickerInput, TimePickerInput, DateTimePickerInput,
|
|
339
|
+
CheckboxInput, RadioInput, SwitchInput, FileInput, and SubmitButton.
|
|
340
|
+
Import each component directly, for example
|
|
341
|
+
`import TextInput from "web/design-system/components/text-input";` — no barrels.
|
|
342
|
+
Use `Form` from `@mongez/react-form` directly. Every control requires a stable
|
|
343
|
+
explicit `id`, `name`, and visible `label`, and accepts `hint`, `error`,
|
|
344
|
+
Mongez `schema`/`rules`, and `validateOn`. Put behavior in design-system hooks
|
|
345
|
+
and props in design-system types. See the Web `build-accessible-controls`
|
|
346
|
+
topic for keyboard/focus, RTL, reset and calendar-date semantics.
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
import { Form } from "@mongez/react-form";
|
|
350
|
+
import TextInput from "web/design-system/components/text-input";
|
|
351
|
+
import SubmitButton from "web/design-system/components/submit-button";
|
|
352
|
+
|
|
353
|
+
export default function ProfileForm() {
|
|
354
|
+
return (
|
|
355
|
+
<Form>
|
|
356
|
+
<TextInput id="profile-name" name="name" label="Name" hint="Display name" />
|
|
357
|
+
<SubmitButton id="profile-save" name="save" label="Save" />
|
|
358
|
+
</Form>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
The full-stack preset and shadcn selection scaffold these controls. Existing
|
|
364
|
+
Tailwind apps can run `npx warlock add design-system`; install Tailwind first
|
|
365
|
+
with `npx warlock add tailwind` when missing. Existing files are preserved.
|
|
366
|
+
|
|
367
|
+
### Build custom controls with the project's UI kit
|
|
368
|
+
|
|
369
|
+
Do not render native `<select>`, `<input type="checkbox">`,
|
|
370
|
+
`<input type="radio">`, or `<input type="date">` UI. Build a searchable
|
|
371
|
+
select/dropdown with a search input, a custom checkbox, custom radio
|
|
372
|
+
controls, and a custom date picker. Each field uses `useFormControl` through
|
|
373
|
+
its dedicated hook; a radio group registers one selected value for the group.
|
|
374
|
+
Connect value/checked changes, blur, disabled state, focus refs, validation,
|
|
375
|
+
and errors to that control. Configure a checkbox control with
|
|
376
|
+
`type: "checkbox"` and wire `checked` / `setChecked`. Adapt other widget
|
|
377
|
+
callbacks to `changeValue`; do not blindly spread DOM input props onto a kit
|
|
378
|
+
component with a different callback contract. Keep search text and popup state separate from
|
|
379
|
+
the selected form value. A kit's hidden input for submission is acceptable;
|
|
380
|
+
it must not display native UI or become a second validation source.
|
|
381
|
+
|
|
382
|
+
Check the existing styling stack and UI kit before building controls:
|
|
383
|
+
|
|
384
|
+
- With Tailwind and shadcn/ui already installed, compose its components:
|
|
385
|
+
Command + Popover for a searchable select, Checkbox, RadioGroup, and
|
|
386
|
+
Calendar + Popover for dates. Keep Mongez as the form owner rather than
|
|
387
|
+
copying a kit example that introduces another form library.
|
|
388
|
+
- With Tailwind but no shadcn/ui, or with no UI kit, ask the user which kit
|
|
389
|
+
to use **before installing anything**. Recommend shadcn/ui for Tailwind
|
|
390
|
+
projects because its composable accessible primitives fit the styling
|
|
391
|
+
stack. Explain the recommendation for other stacks and wait for the
|
|
392
|
+
user's choice; never silently add dependencies.
|
|
393
|
+
- With another established kit and no Tailwind, use its corresponding
|
|
394
|
+
accessible primitives and the same Mongez adapters.
|
|
395
|
+
|
|
396
|
+
Custom controls must preserve the accessibility native controls supplied.
|
|
397
|
+
Follow the WAI-ARIA APG [combobox/listbox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/),
|
|
398
|
+
[checkbox](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/),
|
|
399
|
+
[radiogroup](https://www.w3.org/WAI/ARIA/apg/patterns/radio/), and
|
|
400
|
+
[date picker dialog/grid](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/)
|
|
401
|
+
patterns. Preserve labels, visible focus, disabled/selected/checked states,
|
|
402
|
+
`aria-invalid`, and error descriptions linked to `getErrorProps()` output.
|
|
403
|
+
Support keyboard search and arrow navigation, Enter selection, Space toggling,
|
|
404
|
+
and Escape dismissal as appropriate. Manage popup entry and focus return;
|
|
405
|
+
a modal date dialog traps focus and its calendar grid supports day/month
|
|
406
|
+
keyboard navigation. Kit composition still needs correct labels, focus refs,
|
|
407
|
+
and announced errors; styling alone does not make a control accessible.
|
|
408
|
+
|
|
275
409
|
## 5. Ship usable, localizable presentation
|
|
276
410
|
|
|
277
411
|
- Use semantic landmarks, headings, buttons, links, and explicit form labels.
|
|
@@ -311,12 +445,13 @@ Before approving a web change, ask:
|
|
|
311
445
|
effects without untangling unrelated responsibilities?
|
|
312
446
|
2. Does every loader obtain data and business decisions from a named app
|
|
313
447
|
service, with models/repositories kept out of the web layer?
|
|
314
|
-
3. Does each
|
|
315
|
-
|
|
448
|
+
3. Does each component have one responsibility and its own file, with hooks
|
|
449
|
+
and helpers extracted except for the small, tightly coupled inline case?
|
|
316
450
|
4. Is the service query bounded, the resource shape intentional, and the
|
|
317
451
|
cache scope safe?
|
|
318
452
|
5. Do refresh, navigation, errors, retries, and cleanup leave the UI correct?
|
|
319
|
-
6.
|
|
320
|
-
|
|
453
|
+
6. Do forms use Mongez schema/rules and accessible custom controls, with
|
|
454
|
+
native validation disabled and the UI kit choice agreed before installation?
|
|
455
|
+
Are interactions translated to the degree the interface promises?
|
|
321
456
|
7. Are the relevant quality rules enforced or covered by focused tests,
|
|
322
457
|
rather than described only in comments?
|
|
@@ -23,3 +23,9 @@ forgotPasswordController.validation = {
|
|
|
23
23
|
email: v.email(),
|
|
24
24
|
}),
|
|
25
25
|
};
|
|
26
|
+
|
|
27
|
+
forgotPasswordController.responseSchema = {
|
|
28
|
+
200: { body: { message: "string" } },
|
|
29
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
30
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
31
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Request, type RequestHandler } from "@warlock.js/core";
|
|
2
|
+
import { UserResource } from "app/users/resources/user.resource";
|
|
2
3
|
import { loginSchema, type LoginSchema } from "../schema/login.schema";
|
|
3
4
|
import { loginService } from "../services/auth.service";
|
|
4
5
|
|
|
@@ -23,3 +24,19 @@ loginController.description = "User Login";
|
|
|
23
24
|
loginController.validation = {
|
|
24
25
|
schema: loginSchema,
|
|
25
26
|
};
|
|
27
|
+
|
|
28
|
+
// The starter disables refresh tokens; add tokens.refreshToken when enabling them.
|
|
29
|
+
loginController.responseSchema = {
|
|
30
|
+
200: {
|
|
31
|
+
body: {
|
|
32
|
+
user: UserResource,
|
|
33
|
+
tokens: {
|
|
34
|
+
accessToken: { token: "string", expiresAt: "string" },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
401: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
39
|
+
403: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
40
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
41
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
42
|
+
};
|
|
@@ -17,3 +17,9 @@ export const logoutAllController: GuardedRequestHandler = async ({ request, resp
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
logoutAllController.description = "Logout from all devices";
|
|
20
|
+
|
|
21
|
+
logoutAllController.responseSchema = {
|
|
22
|
+
200: { body: { message: "string" } },
|
|
23
|
+
401: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
24
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
25
|
+
};
|
|
@@ -19,3 +19,9 @@ export const logoutController: GuardedRequestHandler = async ({ request, respons
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
logoutController.description = "User Logout";
|
|
22
|
+
|
|
23
|
+
logoutController.responseSchema = {
|
|
24
|
+
200: { body: { message: "string" } },
|
|
25
|
+
401: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
26
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
27
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
2
|
+
import { UserResource } from "app/users/resources/user.resource";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Get current user controller
|
|
@@ -16,3 +17,9 @@ export const meController: GuardedRequestHandler = async ({ request, response })
|
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
meController.description = "Get Current User";
|
|
20
|
+
|
|
21
|
+
meController.responseSchema = {
|
|
22
|
+
200: { body: { user: UserResource } },
|
|
23
|
+
401: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
24
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
25
|
+
};
|
|
@@ -24,3 +24,15 @@ refreshTokenController.validation = {
|
|
|
24
24
|
refreshToken: v.string(),
|
|
25
25
|
}),
|
|
26
26
|
};
|
|
27
|
+
|
|
28
|
+
refreshTokenController.responseSchema = {
|
|
29
|
+
200: {
|
|
30
|
+
body: {
|
|
31
|
+
accessToken: { token: "string", expiresAt: "string" },
|
|
32
|
+
refreshToken: { token: "string", expiresAt: "string" },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
401: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
36
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
37
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
38
|
+
};
|
|
@@ -21,3 +21,9 @@ resetPasswordController.description = "Reset password with OTP";
|
|
|
21
21
|
resetPasswordController.validation = {
|
|
22
22
|
schema: resetPasswordSchema,
|
|
23
23
|
};
|
|
24
|
+
|
|
25
|
+
resetPasswordController.responseSchema = {
|
|
26
|
+
200: { body: { message: "string" } },
|
|
27
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
28
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
29
|
+
};
|
|
@@ -33,9 +33,15 @@ export class OTP extends Model<OTPSchema> {
|
|
|
33
33
|
* Check if OTP is valid (not expired, not used, not max attempts)
|
|
34
34
|
*/
|
|
35
35
|
public get isValid(): boolean {
|
|
36
|
-
if (this.get("usedAt"))
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
if (this.get("usedAt")) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if ((this.get("attempts") ?? 0) >= (this.get("maxAttempts") ?? 0)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (new Date() > new Date(this.get("expiresAt"))) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
39
45
|
return true;
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -19,12 +19,24 @@ const DEFAULT_OTP_EXPIRATION: Duration = { minutes: 15 };
|
|
|
19
19
|
// Parse duration to milliseconds
|
|
20
20
|
function parseDurationToMs(duration: Duration): number {
|
|
21
21
|
let ms = 0;
|
|
22
|
-
if (duration.milliseconds)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (duration.
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
if (duration.milliseconds) {
|
|
23
|
+
ms += duration.milliseconds;
|
|
24
|
+
}
|
|
25
|
+
if (duration.seconds) {
|
|
26
|
+
ms += duration.seconds * 1000;
|
|
27
|
+
}
|
|
28
|
+
if (duration.minutes) {
|
|
29
|
+
ms += duration.minutes * 60 * 1000;
|
|
30
|
+
}
|
|
31
|
+
if (duration.hours) {
|
|
32
|
+
ms += duration.hours * 60 * 60 * 1000;
|
|
33
|
+
}
|
|
34
|
+
if (duration.days) {
|
|
35
|
+
ms += duration.days * 24 * 60 * 60 * 1000;
|
|
36
|
+
}
|
|
37
|
+
if (duration.weeks) {
|
|
38
|
+
ms += duration.weeks * 7 * 24 * 60 * 60 * 1000;
|
|
39
|
+
}
|
|
28
40
|
return ms;
|
|
29
41
|
}
|
|
30
42
|
|
|
@@ -19,3 +19,9 @@ export const contactController: RequestHandler<Request<ContactSchema>> = async (
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
contactController.validation = { schema: contactSchema };
|
|
22
|
+
|
|
23
|
+
contactController.responseSchema = {
|
|
24
|
+
200: { body: { message: "string" } },
|
|
25
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
26
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
27
|
+
};
|
|
@@ -10,3 +10,7 @@ export const homePageController: RequestHandler = async ({ response }) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
homePageController.description = "Welcome Home Page";
|
|
13
|
+
|
|
14
|
+
homePageController.responseSchema = {
|
|
15
|
+
200: { body: { message: "string", version: "string", docs: "string" } },
|
|
16
|
+
};
|
|
@@ -14,3 +14,9 @@ export const localeController: RequestHandler<Request<LocaleSchema>> = async ({
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
localeController.validation = { schema: localeSchema };
|
|
17
|
+
|
|
18
|
+
localeController.responseSchema = {
|
|
19
|
+
200: { body: { locale: "string" } },
|
|
20
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
21
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
22
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
2
|
+
import { PostResource } from "app/posts/resources/post.resource";
|
|
2
3
|
import { Post } from "../models/post/post.model";
|
|
3
4
|
import { createPostSchema, type CreatePostSchema } from "../schema/create-post.schema";
|
|
4
5
|
|
|
@@ -19,3 +20,9 @@ export const createNewPostController: GuardedRequestHandler<CreatePostSchema> =
|
|
|
19
20
|
createNewPostController.validation = {
|
|
20
21
|
schema: createPostSchema,
|
|
21
22
|
};
|
|
23
|
+
|
|
24
|
+
createNewPostController.responseSchema = {
|
|
25
|
+
201: { body: { post: PostResource } },
|
|
26
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
27
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
28
|
+
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { ResourceNotFoundError } from "@warlock.js/core";
|
|
2
|
+
import { v } from "@warlock.js/seal";
|
|
1
3
|
import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
4
|
+
import { PostResource } from "app/posts/resources/post.resource";
|
|
2
5
|
import { Post } from "../models/post/post.model";
|
|
3
6
|
import { updatePostSchema, type UpdatePostSchema } from "../schema/update-post.schema";
|
|
4
7
|
|
|
@@ -6,19 +9,14 @@ export const updatePostController: GuardedRequestHandler<UpdatePostSchema> = asy
|
|
|
6
9
|
request,
|
|
7
10
|
response,
|
|
8
11
|
}) => {
|
|
9
|
-
const id = request.
|
|
10
|
-
|
|
11
|
-
if (!id) {
|
|
12
|
-
return response.notFound();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const post = await Post.find(id);
|
|
12
|
+
const { id, ...input } = request.validated();
|
|
13
|
+
const post = await Post.first({ publicId: id });
|
|
16
14
|
|
|
17
15
|
if (!post) {
|
|
18
|
-
|
|
16
|
+
throw new ResourceNotFoundError("Post not found.");
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
await post.save({ merge:
|
|
19
|
+
await post.save({ merge: input });
|
|
22
20
|
|
|
23
21
|
return response.success({
|
|
24
22
|
post,
|
|
@@ -27,4 +25,12 @@ export const updatePostController: GuardedRequestHandler<UpdatePostSchema> = asy
|
|
|
27
25
|
|
|
28
26
|
updatePostController.validation = {
|
|
29
27
|
schema: updatePostSchema,
|
|
28
|
+
params: v.object({ id: v.string().uuid() }),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
updatePostController.responseSchema = {
|
|
32
|
+
200: { body: { post: PostResource } },
|
|
33
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
34
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
35
|
+
404: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
30
36
|
};
|
package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { integer, Migration, string, text, timestamp } from "@warlock.js/cascade
|
|
|
2
2
|
import { Post } from "../post.model";
|
|
3
3
|
|
|
4
4
|
export default Migration.create(Post, {
|
|
5
|
+
publicId: string(36).unique(),
|
|
5
6
|
title: string(255),
|
|
6
7
|
description: text(),
|
|
7
8
|
slug: string(255).unique(),
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { Model, RegisterModel } from "@warlock.js/cascade";
|
|
2
3
|
import { useComputedSlug } from "@warlock.js/core";
|
|
3
4
|
import { v, type Infer } from "@warlock.js/seal";
|
|
4
5
|
import { PostResource } from "app/posts/resources/post.resource";
|
|
5
6
|
|
|
6
7
|
export const postSchema = v.object({
|
|
8
|
+
publicId: v
|
|
9
|
+
.string()
|
|
10
|
+
.uuid()
|
|
11
|
+
.default(() => randomUUID()),
|
|
12
|
+
authorId: v.number(),
|
|
7
13
|
title: v.string(),
|
|
8
14
|
description: v.string(),
|
|
9
15
|
slug: v.computed(useComputedSlug()),
|