@zerotal/arch 1.7.4 → 1.8.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.
Files changed (126) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/README.md +4 -2
  3. package/api-surface.md +2 -0
  4. package/docs/about.md +16 -16
  5. package/docs/admin/actions.md +9 -9
  6. package/docs/admin/auth.md +2 -2
  7. package/docs/admin/dashboard.md +5 -5
  8. package/docs/admin/extending-ui.md +12 -12
  9. package/docs/admin/extending.md +16 -7
  10. package/docs/admin/forms.md +4 -4
  11. package/docs/admin/index.md +2 -2
  12. package/docs/admin/operations.md +9 -9
  13. package/docs/admin/resources.md +3 -3
  14. package/docs/admin/structure.md +6 -6
  15. package/docs/admin/tables.md +8 -8
  16. package/docs/admin/testing.md +4 -4
  17. package/docs/ai.md +118 -11
  18. package/docs/application.md +12 -12
  19. package/docs/arch.md +25 -3
  20. package/docs/assets.md +11 -11
  21. package/docs/audit.md +13 -13
  22. package/docs/authentication.md +39 -39
  23. package/docs/authorization.md +13 -13
  24. package/docs/broadcasting/channels.md +2 -2
  25. package/docs/broadcasting/client.md +2 -2
  26. package/docs/broadcasting/events.md +9 -10
  27. package/docs/broadcasting/index.md +1 -1
  28. package/docs/broadcasting/testing.md +5 -5
  29. package/docs/cache.md +15 -13
  30. package/docs/carbon.md +35 -35
  31. package/docs/changelog.md +268 -1
  32. package/docs/client/index.md +13 -13
  33. package/docs/commands.md +36 -4
  34. package/docs/components.md +116 -115
  35. package/docs/config-system.md +4 -4
  36. package/docs/container.md +27 -27
  37. package/docs/context.md +27 -27
  38. package/docs/contributing.md +28 -0
  39. package/docs/controllers.md +10 -10
  40. package/docs/conventions.md +23 -15
  41. package/docs/cookies.md +3 -3
  42. package/docs/csrf.md +6 -6
  43. package/docs/database.md +22 -17
  44. package/docs/deployment.md +99 -2
  45. package/docs/devtools.md +16 -16
  46. package/docs/email-verification.md +8 -8
  47. package/docs/encryption.md +7 -7
  48. package/docs/errors.md +6 -6
  49. package/docs/events.md +12 -11
  50. package/docs/flow/components.md +32 -32
  51. package/docs/flow/decorators.md +18 -18
  52. package/docs/flow/events.md +16 -16
  53. package/docs/flow/forms.md +17 -17
  54. package/docs/flow/icons.md +5 -5
  55. package/docs/flow/index.md +21 -21
  56. package/docs/flow/layouts.md +99 -35
  57. package/docs/flow/lifecycle.md +16 -16
  58. package/docs/flow/models.md +9 -9
  59. package/docs/flow/pagination.md +8 -8
  60. package/docs/flow/performance.md +7 -7
  61. package/docs/flow/references.md +3 -3
  62. package/docs/flow/routing.md +26 -26
  63. package/docs/flow/testing.md +20 -20
  64. package/docs/getting-started.md +18 -13
  65. package/docs/health.md +17 -8
  66. package/docs/helpers.md +17 -17
  67. package/docs/i18n.md +10 -10
  68. package/docs/inertia/devtools.md +4 -4
  69. package/docs/inertia/index.md +6 -6
  70. package/docs/inertia/props.md +21 -21
  71. package/docs/inertia/rendering.md +9 -9
  72. package/docs/inertia/ssr.md +2 -2
  73. package/docs/lifecycle.md +2 -2
  74. package/docs/lock.md +11 -11
  75. package/docs/logger.md +12 -12
  76. package/docs/media.md +22 -22
  77. package/docs/middleware.md +17 -17
  78. package/docs/migrations.md +13 -13
  79. package/docs/monitor.md +6 -6
  80. package/docs/notifications.md +24 -24
  81. package/docs/orm/casts.md +14 -14
  82. package/docs/orm/factories.md +8 -8
  83. package/docs/orm/index.md +22 -20
  84. package/docs/orm/lifecycle.md +13 -13
  85. package/docs/orm/queries.md +28 -28
  86. package/docs/orm/relationships.md +16 -16
  87. package/docs/orm/serialization.md +11 -11
  88. package/docs/package-development.md +10 -10
  89. package/docs/pagination.md +14 -14
  90. package/docs/password-reset.md +6 -6
  91. package/docs/providers.md +11 -11
  92. package/docs/query-builder.md +18 -18
  93. package/docs/queue.md +14 -12
  94. package/docs/rate-limiting.md +6 -6
  95. package/docs/responses.md +14 -14
  96. package/docs/roles-and-2fa.md +9 -9
  97. package/docs/routing.md +44 -44
  98. package/docs/scaffolding.md +1 -1
  99. package/docs/scheduler.md +10 -10
  100. package/docs/seeding.md +6 -6
  101. package/docs/session.md +16 -14
  102. package/docs/social.md +14 -14
  103. package/docs/storage.md +25 -13
  104. package/docs/structure.md +2 -2
  105. package/docs/support-policy.md +23 -10
  106. package/docs/telemetry.md +12 -12
  107. package/docs/tenancy.md +34 -40
  108. package/docs/testing/browser.md +6 -6
  109. package/docs/testing/console.md +3 -3
  110. package/docs/testing/database.md +10 -10
  111. package/docs/testing/flow-browser.md +6 -6
  112. package/docs/testing/http.md +12 -12
  113. package/docs/testing/index.md +29 -3
  114. package/docs/testing/mocking.md +8 -8
  115. package/docs/upgrade.md +1 -1
  116. package/docs/validator.md +19 -19
  117. package/docs/view.md +14 -14
  118. package/package.json +4 -4
  119. package/src/config.ts +8 -0
  120. package/src/index.ts +25 -1
  121. package/src/install/ArchInstallCommand.ts +46 -3
  122. package/src/install/guidelines.ts +84 -0
  123. package/src/install/shape.ts +177 -0
  124. package/src/install/skills.ts +271 -0
  125. package/src/provider/ArchProvider.ts +148 -0
  126. package/src/tools/searchDocs.ts +7 -0
@@ -16,7 +16,7 @@ Marks a property or method as part of the public contract with the browser.
16
16
  - **On a property** — the value is included in the signed snapshot and synced to the client on every patch. The client can update it via a bound `value`/`checked` attribute or a client expression like `onClick={() => this.count++}`.
17
17
  - **On a method** — makes it callable from the browser over the WebSocket via event bindings like `onClick={this.save}`.
18
18
 
19
- ```typescript
19
+ ```typescript fragment
20
20
  import { expose } from "@zerotal/flow";
21
21
 
22
22
  export class CounterPage extends Component {
@@ -48,7 +48,7 @@ A `value={this.x}` binding on a `@locked` property renders as a read-only displa
48
48
 
49
49
  Use `@locked` for anything the server owns: a record the route resolved, results loaded in `onMount()`, computed totals, child props from the parent:
50
50
 
51
- ```tsx
51
+ ```tsx fragment
52
52
  export class PostsPage extends Component {
53
53
  @locked user!: User; // /users/:user — the record, already found
54
54
  @locked posts: Post[] = [];
@@ -87,7 +87,7 @@ with no arguments, and used for [real-time validation](#real-time-validation) on
87
87
 
88
88
  Combine with `@expose` for a property that is both two-way bound and validated:
89
89
 
90
- ```typescript
90
+ ```typescript fragment
91
91
  @expose @validate((rule) => rule.required().email()) email: string = "";
92
92
  @expose @validate((rule) => rule.required().min(8)) password: string = "";
93
93
  @expose @validate((rule) => rule.required().min(2).max(50)) name: string = "";
@@ -102,7 +102,7 @@ Combine with `@expose` for a property that is both two-way bound and validated:
102
102
 
103
103
  The validation runs when you call `this.validate()` inside an action:
104
104
 
105
- ```typescript
105
+ ```typescript fragment
106
106
  @expose async register(): Promise<void> {
107
107
  await this.validate(); // reads all @validate rules on the class
108
108
 
@@ -120,7 +120,7 @@ The validation runs when you call `this.validate()` inside an action:
120
120
 
121
121
  You can also pass rules directly to `this.validate()` — they override the decorator-based rules:
122
122
 
123
- ```typescript
123
+ ```typescript fragment
124
124
  @expose async update(): Promise<void> {
125
125
  await this.validate({
126
126
  email: (rule) => rule.required().email(),
@@ -142,7 +142,7 @@ as the user edits, with no action call and without affecting any other field.
142
142
  > form is not writable in TSX: the `.` in an attribute name is a parse error (`TS1003`), so
143
143
  > copying it out of the emitted HTML into a component will not build.
144
144
 
145
- ```tsx
145
+ ```tsx fragment
146
146
  @expose @validate((rule) => rule.required().email()) email = "";
147
147
 
148
148
  async render() {
@@ -165,7 +165,7 @@ See [Forms & Validation](/docs/flow/forms) for the full validation rules referen
165
165
 
166
166
  Syncs a property to the browser URL query string. Initialised from the query string on the initial page load; updated in the URL on every patch. The URL becomes shareable and bookmark-friendly automatically:
167
167
 
168
- ```typescript
168
+ ```typescript fragment
169
169
  // Page and search stay in the URL: /posts?page=2&search=TypeScript
170
170
  @url page: number = 1;
171
171
  @url search: string = "";
@@ -189,14 +189,14 @@ Binds a property to the HTTP session, so the value survives a browser refresh. R
189
189
 
190
190
  The field reads and writes the session key of the same name — the same value a controller or another component sees:
191
191
 
192
- ```typescript
192
+ ```typescript fragment
193
193
  @session userId: string = ""; // the session's `userId`
194
194
  @session preferredTheme: string = "light";
195
195
  ```
196
196
 
197
197
  Pass options to change the key, or to keep it to this component:
198
198
 
199
- ```typescript
199
+ ```typescript fragment
200
200
  // Read a differently-named key
201
201
  @session({ key: "s" }) whatever: number = 0;
202
202
 
@@ -216,7 +216,7 @@ A `@session` field is **not** in the WebSocket snapshot: it is read from and wri
216
216
 
217
217
  A getter derived from other state. Not stored in the snapshot — recomputed on every render pass. The result is **memoized for the duration of a single render**, so an expensive getter read multiple times in the same template runs only once:
218
218
 
219
- ```typescript
219
+ ```typescript fragment
220
220
  @computed get fullName(): string {
221
221
  return `${this.firstName} ${this.lastName}`;
222
222
  }
@@ -241,7 +241,7 @@ Use them freely in a template as a text child — `{this.fullName}` — where th
241
241
 
242
242
  Excludes a property from the snapshot entirely. Reset to its class-level default on every WebSocket round-trip. Use for ephemeral UI state that shouldn't persist between server calls:
243
243
 
244
- ```typescript
244
+ ```typescript fragment
245
245
  @transient isUploading: boolean = false;
246
246
  @transient dropzoneActive: boolean = false;
247
247
  @transient tempMessage: string = "";
@@ -254,7 +254,7 @@ The pattern: start an upload, set `this.isUploading = true`, send a response. On
254
254
 
255
255
  An `@expose`d method that runs on the server but **skips the re-render cycle**. Use for side-effects that don't change the UI: file downloads, external API calls, jobs that just need to fire:
256
256
 
257
- ```typescript
257
+ ```typescript fragment
258
258
  @expose @renderless async exportCsv(): Promise<void> {
259
259
  const rows = await Report.all();
260
260
  const csv = rows.map((r) => `${r.id},${r.name},${r.email}`).join("\n");
@@ -282,7 +282,7 @@ Because the render cycle is skipped, `@renderless` actions are faster and cheape
282
282
 
283
283
  Registers a method as a listener for cross-component events dispatched via `this.dispatch()`. The method is implicitly exposed — no separate `@expose` needed:
284
284
 
285
- ```typescript
285
+ ```typescript fragment
286
286
  @on("post-created")
287
287
  async handlePostCreated(data: { id: number; title: string }): Promise<void> {
288
288
  this.posts = await Post.query().orderBy("created_at", "desc").limit(10).get();
@@ -319,7 +319,7 @@ For child components — marks a prop the parent can re-push whenever its value
319
319
 
320
320
  Declare the reactive prop on the child:
321
321
 
322
- ```tsx
322
+ ```tsx fragment
323
323
  export class PriceTag extends Component {
324
324
  @reactive currency = "USD";
325
325
  @reactive amount = 0;
@@ -339,7 +339,7 @@ export class PriceTag extends Component {
339
339
 
340
340
  Pass the prop from the parent:
341
341
 
342
- ```tsx
342
+ ```tsx fragment
343
343
  // When this.currency changes, PriceTag re-renders automatically:
344
344
  <PriceTag currency={this.currency} amount={this.subtotal} />
345
345
  ```
@@ -350,7 +350,7 @@ Unlike `@locked`, `@reactive` props are **live** — the parent keeps them curre
350
350
 
351
351
  A reactive prop that **also syncs back to the parent** (two-way). The parent property and the child prop stay in lock-step. Use for reusable input/control components that need to write a value back to their parent:
352
352
 
353
- ```tsx
353
+ ```tsx fragment
354
354
  export class StarRating extends Component {
355
355
  @modelable rating: number = 0; // two-way bound to parent
356
356
 
@@ -409,7 +409,7 @@ See [Layouts & Composition](/docs/flow/layouts#two-way-props) for the full `@mod
409
409
 
410
410
  These helpers are available inside any `@expose`d method:
411
411
 
412
- ```typescript
412
+ ```typescript fragment
413
413
  // Flash a toast notification to the browser
414
414
  this.flash("Saved successfully!"); // level defaults to "success"
415
415
  this.flash("Could not connect.", "error");
@@ -442,7 +442,7 @@ this.download("export.json", JSON.stringify(data, null, 2), "application/json");
442
442
 
443
443
  `this.errors` is a typed proxy over the validation error bag. It is populated by `this.validate()` and `this.addError()`.
444
444
 
445
- ```tsx
445
+ ```tsx fragment
446
446
  // Check if any validation errors exist
447
447
  if (this.errors.any()) {
448
448
  return; // stop processing
@@ -11,7 +11,7 @@ Flow components communicate by dispatching named events. Listeners registered wi
11
11
 
12
12
  Call `this.dispatch()` inside any `@expose`d action. The event is delivered to all matching `@on` listeners on the page after the action completes:
13
13
 
14
- ```typescript
14
+ ```typescript fragment
15
15
  @expose async save(): Promise<void> {
16
16
  const post = await Post.create({
17
17
  title: this.title,
@@ -27,7 +27,7 @@ Call `this.dispatch()` inside any `@expose`d action. The event is delivered to a
27
27
 
28
28
  The second argument is the payload — any JSON-serialisable value. Omit it for events that carry no data:
29
29
 
30
- ```typescript
30
+ ```typescript fragment
31
31
  this.dispatch("cart-cleared");
32
32
  ```
33
33
 
@@ -35,7 +35,7 @@ this.dispatch("cart-cleared");
35
35
 
36
36
  By default, `dispatch()` notifies every `@on` listener on the page. Use `dispatchTo` or `dispatchSelf` to narrow the target:
37
37
 
38
- ```typescript
38
+ ```typescript fragment
39
39
  // Default: all @on("post-created") listeners on the page
40
40
  this.dispatch("post-created", { id });
41
41
 
@@ -52,7 +52,7 @@ this.dispatchSelf("refresh");
52
52
 
53
53
  Register a method as an event listener with `@on`. The method is **implicitly exposed** — no `@expose` needed:
54
54
 
55
- ```typescript
55
+ ```typescript fragment
56
56
  import { on } from "@zerotal/flow";
57
57
 
58
58
  export class PostList extends Component {
@@ -95,7 +95,7 @@ A component can have as many `@on` listeners as it needs. Each fires independent
95
95
 
96
96
  Several components on the same page can all listen for the same event. Each component is updated independently — Flow sends a separate patch frame to each listener:
97
97
 
98
- ```typescript
98
+ ```typescript fragment
99
99
  // PostList.tsx
100
100
  @on("post-created")
101
101
  async onPostCreated(data: { id: number }) {
@@ -121,7 +121,7 @@ All three components update after a single `this.dispatch("post-created", ...)`
121
121
 
122
122
  The same `dispatch`, `dispatchTo`, and `dispatchSelf` methods work inside **client expressions** — no server round-trip needed to start the dispatch. The `@on` listeners still run server-side when they're notified:
123
123
 
124
- ```tsx
124
+ ```tsx fragment
125
125
  {
126
126
  /* Notify all listeners without a preceding server action */
127
127
  }
@@ -158,7 +158,7 @@ declare module "@zerotal/flow" {
158
158
 
159
159
  Every `dispatch` / `dispatchTo` / `dispatchSelf` site is now checked against it — in server actions **and** client expressions, since they call the same typed method:
160
160
 
161
- ```ts
161
+ ```ts fragment
162
162
  this.dispatch("post-created", { id: post.id, title: post.title }); // ✓
163
163
  this.dispatch("post-created", { id: post.id }); // ✗ missing `title`
164
164
  this.dispatch("post-created"); // ✗ payload required
@@ -168,7 +168,7 @@ this.dispatch("cart-cleared", { anything: 1 }); // ✗ void takes no payload
168
168
 
169
169
  On the listener side, `@on` autocompletes to the known event names, and you annotate the handler's parameter with `EventPayload<K>` to type the payload against the same contract:
170
170
 
171
- ```ts
171
+ ```ts fragment
172
172
  import { on } from "@zerotal/flow";
173
173
  import type { EventPayload } from "@zerotal/flow";
174
174
 
@@ -235,7 +235,7 @@ Listen for server-broadcast events over WebSockets with `@on("socket:…")`. Whe
235
235
 
236
236
  The socket client is bundled into the Flow runtime and created the first time a page declares one of these listeners, so there is no script to add and nothing to publish on `window`. An app that needs a configured client — a different host, its own auth endpoint — assigns `window.Socket` before the runtime loads and that one is used instead. Pages with no such listener open no broadcast connection at all.
237
237
 
238
- ```typescript
238
+ ```typescript fragment
239
239
  export class OrderDashboard extends Component {
240
240
  @locked orderCount: number = 0;
241
241
  @locked recentOrders: Order[] = [];
@@ -299,7 +299,7 @@ Pass a resolver instead. It is called with the component when the snapshot is bu
299
299
  [`@presence`](#presence--whos-here-multiplayer) and [`@shared`](#shared-state--everyone-converges-multiplayer)
300
300
  resolve theirs:
301
301
 
302
- ```typescript
302
+ ```typescript fragment
303
303
  export class IssuePage extends Component {
304
304
  @locked issue!: Issue;
305
305
  @locked comments: Comment[] = [];
@@ -326,7 +326,7 @@ Broadcasting requires a global `window.Socket` client configured by your applica
326
326
 
327
327
  If `window.Socket` is not present, all `socket:` listeners are silently inert — no errors, no subscriptions attempted.
328
328
 
329
- ```typescript
329
+ ```typescript fragment
330
330
  // In your frontend bootstrap (app.ts or similar):
331
331
  import { Socket } from "@zerotal/client";
332
332
 
@@ -339,7 +339,7 @@ window.Socket = new Socket();
339
339
 
340
340
  `@presence` binds a property to a broadcast **presence channel** and keeps it filled with the live member list — the framework joins the channel, seeds the list, and refreshes it as people join and leave. No event classes, no manual `@on` wiring:
341
341
 
342
- ```tsx
342
+ ```tsx fragment
343
343
  import { Component, presence } from "@zerotal/flow";
344
344
  import type { PresenceMember } from "@zerotal/flow";
345
345
 
@@ -362,7 +362,7 @@ export class Board extends Component {
362
362
 
363
363
  The channel is resolved on the server from the component (so it can't be forged from the client) and carried, signed, in the snapshot. `who` is server-controlled (like `@locked`): it lives in the snapshot and the client can't write it. Authorize the channel — and shape the member data — in `routes/channels.ts`:
364
364
 
365
- ```ts
365
+ ```ts fragment
366
366
  Broadcast.channel("board.[boardId]", (user, boardId) =>
367
367
  user.canView(boardId) ? { id: user.id, name: user.name } : null,
368
368
  );
@@ -385,7 +385,7 @@ Like all `socket:` features, presence needs a `window.Socket` client configured
385
385
 
386
386
  Where `@presence` answers _who's here_, `@shared` answers _what do we all see_. It binds a property to convergent, **server-authoritative** state on a channel: mutate it in an action and the framework writes it to a per-channel **room store** and broadcasts to the channel, so every other subscriber re-reads and converges. No store to wire, no events, no dispatch:
387
387
 
388
- ```tsx
388
+ ```tsx fragment
389
389
  import { Component, presence, shared, expose } from "@zerotal/flow";
390
390
 
391
391
  export class Board extends Component {
@@ -416,7 +416,7 @@ Broadcasting is an **optional peer**. With `window.Socket` and `BroadcastProvide
416
416
 
417
417
  Calling `this.refresh()` inside an action inserts `onMount()` back into the WebSocket round-trip cycle:
418
418
 
419
- ```typescript
419
+ ```typescript fragment
420
420
  @expose async syncOrders(): Promise<void> {
421
421
  await OrderSync.run();
422
422
  this.refresh(); // onMount() re-runs → this.orders is freshly loaded
@@ -434,7 +434,7 @@ Calling `this.refresh()` inside an action inserts `onMount()` back into the WebS
434
434
 
435
435
  In the browser, `this.refresh()` sends a lightweight re-render request with no data change — useful for polling or a manual reload button:
436
436
 
437
- ```tsx
437
+ ```tsx fragment
438
438
  {
439
439
  /* Manual reload button */
440
440
  }
@@ -11,7 +11,7 @@ Validate input, bind form fields to component state with two-way binding, encaps
11
11
 
12
12
  Attach rules with `@validate` on individual fields, or pass them explicitly to `this.validate()`:
13
13
 
14
- ```typescript
14
+ ```typescript fragment
15
15
  import { expose, validate } from "@zerotal/flow";
16
16
 
17
17
  @expose @validate((rule) => rule.required().email()) email: string = "";
@@ -29,7 +29,7 @@ builder (`rule.number()`, …). The same `@validate` rule also powers
29
29
 
30
30
  Call `this.validate()` at the start of your action. It reads the `@validate` rules and throws a `ValidationError` if any fail — Flow catches it, populates `this.errors`, and re-renders:
31
31
 
32
- ```typescript
32
+ ```typescript fragment
33
33
  @expose async register(): Promise<void> {
34
34
  await this.validate(); // uses @validate rules declared on the class
35
35
 
@@ -46,7 +46,7 @@ Call `this.validate()` at the start of your action. It reads the `@validate` rul
46
46
 
47
47
  Pass explicit rules to override or extend `@validate`:
48
48
 
49
- ```typescript
49
+ ```typescript fragment
50
50
  @expose async update(): Promise<void> {
51
51
  await this.validate({
52
52
  email: (rule) => rule.required().email(),
@@ -83,7 +83,7 @@ Pass explicit rules to override or extend `@validate`:
83
83
 
84
84
  Pass a field off `this.errors` to the `error` prop. It renders the first message for that field and hides itself when the field is valid — no manual show/hide logic:
85
85
 
86
- ```tsx
86
+ ```tsx fragment
87
87
  <input value={this.email} />
88
88
  <span error={this.errors.email} class="text-sm text-red-500" />
89
89
 
@@ -93,7 +93,7 @@ Pass a field off `this.errors` to the `error` prop. It renders the first message
93
93
 
94
94
  Use the `<Field>` component for accessible label + error wiring:
95
95
 
96
- ```tsx
96
+ ```tsx fragment
97
97
  import { Field } from "@zerotal/flow";
98
98
 
99
99
  <Field label="Email" error={this.errors.email}>
@@ -107,7 +107,7 @@ import { Field } from "@zerotal/flow";
107
107
 
108
108
  Show all errors at once with `<Errors>`:
109
109
 
110
- ```tsx
110
+ ```tsx fragment
111
111
  import { Errors } from "@zerotal/flow";
112
112
 
113
113
  <Errors /> {/* all current errors */}
@@ -116,7 +116,7 @@ import { Errors } from "@zerotal/flow";
116
116
 
117
117
  ### Checking errors in server code
118
118
 
119
- ```typescript
119
+ ```typescript fragment
120
120
  // Check if any errors exist
121
121
  if (this.errors.any()) {
122
122
  return;
@@ -131,7 +131,7 @@ if (this.errors.has("email")) {
131
131
 
132
132
  ### Manual errors
133
133
 
134
- ```typescript
134
+ ```typescript fragment
135
135
  this.addError("email", "That email address is already taken.");
136
136
  this.addError("username", "Username must be unique.");
137
137
 
@@ -143,7 +143,7 @@ this.resetValidation("email"); // clear one field
143
143
 
144
144
  Pass state to `value` (or `checked`). Flow makes it two-way for `@expose` properties and read-only for `@locked` ones — no helper needed:
145
145
 
146
- ```tsx
146
+ ```tsx fragment
147
147
  {/* Text inputs */}
148
148
  <input value={this.name} />
149
149
  <input value={this.email} />
@@ -194,7 +194,7 @@ export class LoginForm extends Form {
194
194
 
195
195
  Mount the form on a component:
196
196
 
197
- ```typescript
197
+ ```typescript fragment
198
198
  export class LoginPage extends Component {
199
199
  @expose form = new LoginForm();
200
200
 
@@ -219,7 +219,7 @@ export class LoginPage extends Component {
219
219
 
220
220
  Bind fields with nested `value={this.form.email}`:
221
221
 
222
- ```tsx
222
+ ```tsx fragment
223
223
  <form onSubmit={this.login} class="space-y-4">
224
224
  <Field label="Email" error={this.errors.email}>
225
225
  <input value={this.form.email} type="email" live class="input" />
@@ -242,7 +242,7 @@ Bind fields with nested `value={this.form.email}`:
242
242
 
243
243
  ### Form helpers
244
244
 
245
- ```typescript
245
+ ```typescript fragment
246
246
  // Get all field values as a plain object
247
247
  const data = this.form.data();
248
248
  // { email: "alice@example.com", password: "…", remember: true }
@@ -273,7 +273,7 @@ See [Pagination](/docs/flow/pagination) for the full guide — `paginate()`, `Pa
273
273
 
274
274
  Quick reference for in-memory pagination:
275
275
 
276
- ```typescript
276
+ ```typescript fragment
277
277
  import { paginate } from "@zerotal/flow";
278
278
 
279
279
  export class PostsPage extends Component {
@@ -342,7 +342,7 @@ export class PostsPage extends Component {
342
342
 
343
343
  Compose `Pagination` to get the page state and navigation methods automatically — no boilerplate:
344
344
 
345
- ```tsx
345
+ ```tsx fragment
346
346
  import { Component, Pagination } from "@zerotal/flow";
347
347
 
348
348
  export class PostsPage extends Component.using(Pagination) {
@@ -404,7 +404,7 @@ export class PostsPage extends Component.using(Pagination) {
404
404
 
405
405
  Reset the page when a filter changes to avoid showing an empty page:
406
406
 
407
- ```typescript
407
+ ```typescript fragment
408
408
  @expose async applyFilter(status: string): Promise<void> {
409
409
  this.filter = status;
410
410
  this.resetPage(); // go back to page 1
@@ -413,7 +413,7 @@ Reset the page when a filter changes to avoid showing an empty page:
413
413
 
414
414
  Compose with other mixins:
415
415
 
416
- ```typescript
416
+ ```typescript fragment
417
417
  // `Sorting` here is a mixin you author yourself (see Layouts & Composition);
418
418
  // `Pagination` is the one shipped by Flow.
419
419
  export class PostsPage extends Component.using(Sorting, Pagination) {
@@ -425,7 +425,7 @@ export class PostsPage extends Component.using(Sorting, Pagination) {
425
425
 
426
426
  For database queries, skip the in-memory `paginate()` and use the ORM query builder directly. Combine with `@url page`:
427
427
 
428
- ```typescript
428
+ ```typescript fragment
429
429
  export class PostsPage extends Component {
430
430
  @url page = 1;
431
431
  @locked posts: Post[] = [];
@@ -8,7 +8,7 @@ description: 2,060 icons bundled with Flow's component library — typed by name
8
8
  `<Icon>` draws an icon by name. The set ships inside `@zerotal/flow-ui`, so this
9
9
  works in a new app with nothing installed and nothing configured:
10
10
 
11
- ```tsx
11
+ ```tsx fragment
12
12
  import { Icon } from "@zerotal/flow-ui";
13
13
 
14
14
  <Icon name="inbox" />
@@ -42,7 +42,7 @@ An icon is `1em` square and painted in `currentColor`, so by default it matches
42
42
  the text it sits beside — size, weight of colour, and all. Override with classes
43
43
  rather than attributes:
44
44
 
45
- ```tsx
45
+ ```tsx fragment
46
46
  <p class="text-sm text-slate-600">
47
47
  <Icon name="info" /> Saved a moment ago
48
48
  </p>
@@ -63,7 +63,7 @@ would read the meaning out twice.
63
63
  An icon that is the **only** content of a control is not decoration. Without a
64
64
  label, that button has no accessible name at all:
65
65
 
66
- ```tsx
66
+ ```tsx fragment
67
67
  <button onClick={this.remove}>
68
68
  <Icon name="trash-2" label="Delete order" />
69
69
  </button>
@@ -74,7 +74,7 @@ label, that button has no accessible name at all:
74
74
  A name from a database column or a URL segment is not a literal, so it does not
75
75
  satisfy the union. `isIconName()` narrows it:
76
76
 
77
- ```tsx
77
+ ```tsx fragment
78
78
  import { Icon, isIconName } from "@zerotal/flow-ui";
79
79
 
80
80
  override async render() {
@@ -113,7 +113,7 @@ differently: `git-branch` not `branch`, `file-json` not `json`, `paperclip` not
113
113
  Three sign-in providers ship as brand marks, because `@zerotal/auth` has a code
114
114
  path for each and a sign-in button wants the provider's actual logo:
115
115
 
116
- ```tsx
116
+ ```tsx fragment
117
117
  <button><Icon name="brand-google" /> Continue with Google</button>
118
118
  <button><Icon name="brand-github" /> Continue with GitHub</button>
119
119
  <button><Icon name="brand-apple" /> Continue with Apple</button>
@@ -93,7 +93,7 @@ middleware is a `_middleware.ts` in the page tree. It stacks from the root down,
93
93
  covers every page beneath it, and — being route middleware — re-runs on every
94
94
  action with nothing else to configure:
95
95
 
96
- ```typescript
96
+ ```typescript fragment
97
97
  // app/flow/pages/_middleware.ts
98
98
  import { TenantMiddleware } from "../../middleware/TenantMiddleware.ts";
99
99
 
@@ -107,7 +107,7 @@ guards the pages inside it without changing their URLs. See
107
107
  Reach for `persistentMiddleware` only when the middleware is genuinely **global** —
108
108
  registered app-wide with `Application.use()` because non-Flow routes need it too:
109
109
 
110
- ```typescript
110
+ ```typescript fragment
111
111
  // app/providers/AppServiceProvider.ts — inside onRegister():
112
112
  FlowProvider.persistMiddleware(TenantMiddleware);
113
113
  ```
@@ -172,7 +172,7 @@ The distinction is the syntax: a named method reference is always a server actio
172
172
 
173
173
  Register the route:
174
174
 
175
- ```typescript
175
+ ```typescript fragment
176
176
  // routes/web.ts
177
177
  import { Router } from "zerotal";
178
178
  import { CounterPage } from "./components/CounterPage.tsx";
@@ -195,7 +195,7 @@ The names in use:
195
195
  | Group | Names |
196
196
  | ----------------- | --------------------------------------------------------------------------------------------------------------------- |
197
197
  | Lifecycle | `onBoot` `onMount` `onHydrate` `onDehydrate` `onRendering` `onRendered` `onUpdate` `onUpdating` `onUpdated` `onError` |
198
- | Rendering | `render` `layout` `placeholder` `slot` `hasSlot` `child` |
198
+ | Rendering | `render` `layout` `placeholder` `slot` `hasSlot` `child` `isInteractive` |
199
199
  | Actions & state | `bind` `validate` `resetValidation` `errors` `addError` `refresh` `$refresh` `$set` `cancelled` `signal` |
200
200
  | Navigation | `redirect` `redirectRoute` `redirectIntended` `currentUrl` `navigateCurrent` |
201
201
  | Events & realtime | `dispatch` `dispatchSelf` `dispatchTo` `stream` `client` `$` |
@@ -239,7 +239,7 @@ State that the server should never see — a hover flag, which accordion section
239
239
 
240
240
  A list with per-row actions needs to tell the server _which_ row. Write the call as you would expect, and Flow compiles the arguments into the markup:
241
241
 
242
- ```tsx
242
+ ```tsx fragment
243
243
  {
244
244
  this.enquiries.map((row) => (
245
245
  <tr>
@@ -254,7 +254,7 @@ A list with per-row actions needs to tell the server _which_ row. Write the call
254
254
 
255
255
  The arguments are evaluated **on the server, during the render** — where `row` exists — and travel with the action as `data-args`. Your action receives them as ordinary parameters:
256
256
 
257
- ```ts
257
+ ```ts fragment
258
258
  @expose async archive(id: number) {
259
259
  await Enquiry.findOrFail(id).archive();
260
260
  }
@@ -262,7 +262,7 @@ The arguments are evaluated **on the server, during the render** — where `row`
262
262
 
263
263
  You can also write `data-args` yourself, which is useful when the handler is built dynamically:
264
264
 
265
- ```tsx
265
+ ```tsx fragment
266
266
  <button onClick={this.archive} data-args={JSON.stringify([row.id])}>
267
267
  Archive
268
268
  </button>
@@ -280,7 +280,7 @@ By contrast, a handler pointing at a method you forgot to `@expose` **is** a har
280
280
 
281
281
  Bind an input by passing state straight to `value` (or `checked`). Flow wires up two-way binding when the property is `@expose`, and read-only reflection when it's `@locked`:
282
282
 
283
- ```tsx
283
+ ```tsx fragment
284
284
  <input value={this.name} /> {/* @expose → two-way */}
285
285
  <input type="checkbox" checked={this.agree} /> {/* @expose → two-way */}
286
286
  <input value={this.ownerName} /> {/* @locked → read-only */}
@@ -288,7 +288,7 @@ Bind an input by passing state straight to `value` (or `checked`). Flow wires up
288
288
 
289
289
  A **radio group** is bound as a unit rather than per input, because every option writes the same property. Pass the option's own value as a second argument to `bind()`:
290
290
 
291
- ```tsx
291
+ ```tsx fragment
292
292
  {
293
293
  ["CUSTOM", "ROUTE", "TEAMS"].map((t) => (
294
294
  <label>
@@ -302,7 +302,7 @@ Each option renders with the shared `flow:model="type"`, its own `value`, and `c
302
302
 
303
303
  By default the value stays **local** — it updates the DOM instantly and is flushed to the server with your next action. Add `live` to sync to the server as you type, or `blur` to sync when the input loses focus:
304
304
 
305
- ```tsx
305
+ ```tsx fragment
306
306
  <input value={this.draft} /> {/* local; flushed with the next action */}
307
307
  <input value={this.search} live /> {/* syncs to the server as you type */}
308
308
  <input value={this.title} blur /> {/* syncs to the server on blur */}
@@ -312,20 +312,20 @@ By default the value stays **local** — it updates the DOM instantly and is flu
312
312
 
313
313
  Two more modifiers clean the value at the edge, so the server never sees a numeric string or stray whitespace and you write no coercion in `onUpdated`:
314
314
 
315
- ```tsx
315
+ ```tsx fragment
316
316
  <input type="number" value={this.age} number /> {/* the bound value is a real number, not "42" */}
317
317
  <input value={this.name} trim /> {/* whitespace stripped before it syncs */}
318
318
  ```
319
319
 
320
320
  Add `draft="key"` to keep an unsubmitted value across a reload or crash — it mirrors to `localStorage` and restores on mount (only when the field is empty, so server content always wins), then clears itself once the server empties the field after a successful submit. It's a client-side safety net; the server snapshot stays the authority:
321
321
 
322
- ```tsx
322
+ ```tsx fragment
323
323
  <textarea value={this.body} draft="post-body" /> {/* survives a refresh until you submit */}
324
324
  ```
325
325
 
326
326
  And two focus helpers close the loop after a re-render, where focus is easily lost:
327
327
 
328
- ```tsx
328
+ ```tsx fragment
329
329
  <input value={this.email} autoFocus /> {/* focus on mount (won't steal focus you've placed) */}
330
330
  <input value={this.email} focusOnError /> {/* after a failed submit, focus jumps to the first invalid field */}
331
331
  ```
@@ -336,7 +336,7 @@ And two focus helpers close the loop after a re-render, where focus is easily lo
336
336
 
337
337
  Pass a field off `this.errors` to the `error` prop and Flow renders that field's first validation message reactively — it appears when the field is invalid and clears when it's fixed:
338
338
 
339
- ```tsx
339
+ ```tsx fragment
340
340
  <input value={this.email} />
341
341
  <span error={this.errors.email} class="text-red-500" />
342
342
  ```
@@ -347,7 +347,7 @@ No `errors.has(...)` checks, no manual show/hide.
347
347
 
348
348
  A `className`/`class` (or `style`, `href`, …) that depends on `@expose` or `@locked` state compiles to a reactive client binding, so it updates without a round-trip:
349
349
 
350
- ```tsx
350
+ ```tsx fragment
351
351
  <span className={this.count > 10 ? "text-emerald-400" : "text-white"}>
352
352
  {this.count}
353
353
  </span>
@@ -361,7 +361,7 @@ A `className`/`class` (or `style`, `href`, …) that depends on `@expose` or `@l
361
361
 
362
362
  Use normal TypeScript control flow in `render()` — `.map()`, ternaries, and `&&` are all supported:
363
363
 
364
- ```tsx
364
+ ```tsx fragment
365
365
  override async render() {
366
366
  return (
367
367
  <div>
@@ -389,7 +389,7 @@ Always provide a `key` when mapping over items — the morph algorithm uses it t
389
389
 
390
390
  Common interaction states are first-class props:
391
391
 
392
- ```tsx
392
+ ```tsx fragment
393
393
  {/* Disable the button while the action is in flight */}
394
394
  <button onClick={this.save} loadingAttr="disabled">Save</button>
395
395
 
@@ -420,7 +420,7 @@ Common interaction states are first-class props:
420
420
 
421
421
  Add `transition` to animate the show/hide instead of an instant flip — a single prop covers **both** enter and leave (the leave half plain `show=` can't do, since the element would otherwise vanish before any animation runs):
422
422
 
423
- ```tsx
423
+ ```tsx fragment
424
424
  <div show={this.modal} transition class="modal">…</div> {/* default: fade */}
425
425
  <div show={this.menu} transition="scale">…</div> {/* preset */}
426
426
  <aside show={this.drawer} transition="slide-right">…</aside> {/* directional */}
@@ -446,7 +446,7 @@ The `flow:*` directives (`flow:click`, `flow:model`, `flow:show`, …) that the
446
446
 
447
447
  A handful of client-only helpers — writing a prop and syncing it, toggling a boolean, calling a parent action, optimistic list mutations — don't belong to any one component and never run on the server. Rather than crowd the component class (and reserve common names like `set`, `on`, `watch`, `parent` that you might want for your own methods), they all live on a single global object, `$flow`. Framework helpers wear a `$`; the bare names stay yours.
448
448
 
449
- ```tsx
449
+ ```tsx fragment
450
450
  <button onClick={() => $flow.set("open", true)}>Open</button> {/* write + sync an @expose prop */}
451
451
  <button onClick={() => $flow.toggle("open")}>Toggle</button>
452
452
  <button onClick={() => $flow.parent.save()}>Save</button> {/* call a parent action */}
@@ -508,7 +508,7 @@ A page that _reads_ `$flow.store` in a binding must be AOT-compilable — the co
508
508
 
509
509
  ### Full page example
510
510
 
511
- ```tsx
511
+ ```tsx fragment
512
512
  import { Component, expose, locked, validate, url } from "@zerotal/flow";
513
513
  import type { Post } from "#app/models/Post.ts";
514
514
 
@@ -576,4 +576,4 @@ Flow is a large surface. Each section below is its own page.
576
576
  - [Session](/docs/session) — the store behind `@session` and `SessionMiddleware`.
577
577
  - [Broadcasting](/docs/broadcasting) — drive `@on("socket:…")` real-time updates from the server.
578
578
  - [Storage](/docs/storage) — configure the disks that file uploads write to.
579
- - [Testing](/docs/testing/index) — patterns for the `FlowTest` harness and the rest of the suite.
579
+ - [Testing](/docs/testing) — patterns for the `FlowTest` harness and the rest of the suite.